<c:forEach>标签报isValid()==false错问题

博主遇到了关于JSTL标签的错误,并提及给出的图是正确写法,围绕JSTL标签的使用展开。

今天遇到了这样一个错,关于jstl标签的错误,上图是正确写法。


<%@ page contentType="text/html;charset=UTF-8"%> <%@ include file="/WEB-INF/include/adminCommon.jsp"%> <tags:wjs items="calendar.js"></tags:wjs> <body> <!-- 导航条区 --> <c:import url="/WEB-INF/views/admin/adminNavbar.jsp"></c:import> <div id="content"> <div class="container-fluid"> <div class="row-fluid"> <div class="span3"> <!-- 个人信息区 --> <c:import url="/WEB-INF/views/admin/adminInfo.jsp"></c:import> <hr /> <!-- 左侧菜单区 --> <c:import url="/WEB-INF/views/admin/adminLeft.jsp"></c:import> </div> <!-- /span3 --> <div class="span9"> <!--这里是操作完成的消息提示区 --> <c:import url="/WEB-INF/include/message.jsp"></c:import> <!-- 标题区 --> <ul class="breadcrumb"> <li>项目管理<span class="divider">/</span></li> <li><a href="${ctx}/processArea">过程域管理</a> <span class="divider">/</span></li> <li class="active">添加过程</li> </ul> <!-- 表单区 --> <form id="searchForm1" action="${ctx}/processArea/submitadd" method="post"> <div class="widget-header"> <i class="icon-plus"></i> <h3>添加过程</h3> </div> <!-- /widget-header --> <div class="widget-content" style="margin-bottom: 15px;"> <table style="width: 100%"> <tr> <td>过城域标识</td> <td><input type="text" id="txt_processAreaID" name="processAreaID" class="input-medium"></td> </tr> <tr> <tr> <td>过程名称</td> <td><input type="text" id="txt_processAreaName" name="processAreaName" class="input-medium"></td> </tr> <td>过程类型</td> <td> <select id="txt_processAreaType" name="processAreaType" style="width:160px;"> <option value="">--请选择--</option> <c:forEach items="${processAreaList }" var="processAreaType"> <option value="${processArea.processAreaKey }">${processArea.processAreaName }</option> </c:forEach> </select> </td> </tr> <tr> <td>简介</td> <td><textarea rows="10" cols="10" name="remark"></textarea> </td> </tr> <tr> <td align="right" colspan="4"><input class="btn" type="button" onclick="window.history.back(-1)" value="返回"> <input class="btn btn-primary" type="submit" value="确定"> </td> </tr> </table> </div> </form> </div> </div> <!-- /row --> </div> <!-- /container --> </div> <!-- /content --> <!-- 页脚区 --> <c:import url="/WEB-INF/views/admin/adminFooter.jsp"></c:import> </body> <!-- 表单校验区 --> <script type="text/javascript"> $(document).ready(function() { $("#searchForm1").validate({ rules : { processAreaName : { required : true, remote : { url : "${ctx}/processArea/processNameUnique", type : "post", dataType : "json", data : { systemName : function() { return $("#txt_processAreaName").val(); } } } }, processAreaID : { required: true, remote : { url : "${ctx}/processArea/processIDUnique", type : "post", dataType : "json", data : { systemID : function() { return $("#txt_processAreaID").val(); } } } } }, messages : { processAreaName : { remote : "该过程域名称已存在" }, processAreaID : { remote : "该过程标识已存在" } } }); }); </script>
最新发布
09-25
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流程审批系统</title> <!-- 引入Element Plus样式 --> <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"> <!-- 引入LogicFlow样式 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core/lib/style/index.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/style/index.css"> <!-- 引入Vue 3和Element Plus --> <script src="https://unpkg.com/vue@3"></script> <script src="https://unpkg.com/element-plus"></script> <!-- 引入LogicFlow --> <script src="https://cdn.jsdelivr.net/npm/@logicflow/core/dist/logic-flow.js"></script> <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/index.js"></script> <style> :root { --primary-color: #409EFF; --success-color: #67C23A; --warning-color: #E6A23C; --danger-color: #F56C6C; --info-color: #909399; } body { margin: 0; padding: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f5f7fa; color: #303133; } .app-container { display: flex; flex-direction: column; min-height: 100vh; } .header { background-color: var(--primary-color); color: white; padding: 0 20px; height: 60px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .main-content { display: flex; flex: 1; } .sidebar { width: 220px; background-color: #304156; color: white; height: calc(100vh - 60px); overflow-y: auto; } .content { flex: 1; padding: 20px; overflow-y: auto; height: calc(100vh - 60px); } .page-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #ebeef5; } .card-container { background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } .toolbar { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .logicflow-container { height: 500px; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden; } .property-panel { position: absolute; right: 20px; top: 100px; width: 300px; background: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 15px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); z-index: 100; } .task-filter { margin-bottom: 20px; display: flex; gap: 10px; } .task-item { border-left: 4px solid var(--primary-color); padding: 15px; margin-bottom: 15px; background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .task-item.urgent { border-left-color: var(--danger-color); } .task-item.completed { border-left-color: var(--success-color); } .task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .task-title { font-weight: bold; font-size: 16px; } .task-meta { color: var(--info-color); font-size: 14px; margin-bottom: 10px; } .task-actions { display: flex; gap: 10px; } .process-status { display: inline-block; padding: 5px 10px; border-radius: 3px; font-size: 12px; color: white; } .status-pending { background-color: var(--warning-color); } .status-approved { background-color: var(--success-color); } .status-rejected { background-color: var(--danger-color); } .status-processing { background-color: var(--primary-color); } .menu-item { padding: 15px 20px; cursor: pointer; transition: background-color 0.3s; } .menu-item:hover, .menu-item.active { background-color: #263445; } .menu-item i { margin-right: 10px; } </style> </head> <body> <div id="app"> <div class="app-container"> <!-- 顶部导航栏 --> <div class="header"> <h2>流程审批系统</h2> <div> <el-dropdown> <span class="el-dropdown-link"> 管理员 <i class="el-icon-arrow-down el-icon--right"></i> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>个人信息</el-dropdown-item> <el-dropdown-item>退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </div> <div class="main-content"> <!-- 侧边栏菜单 --> <div class="sidebar"> <div v-for="item in menuItems" :key="item.key" :class="['menu-item', { active: activeMenu === item.key }]" @click="activeMenu = item.key" > <i :class="item.icon"></i> <span>{{ item.label }}</span> </div> </div> <!-- 主内容区域 --> <div class="content"> <!-- 流程设计器 --> <div v-if="activeMenu === 'design'"> <h2 class="page-title">流程设计器</h2> <div class="card-container"> <div class="toolbar"> <div> <el-select v-model="currentTemplate" placeholder="选择流程模板" style="width: 200px;"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" /> </el-select> <el-button type="primary" style="margin-left: 10px;" @click="saveFlow">保存流程</el-button> <el-button @click="createNewTemplate">新建模板</el-button> </div> <div> <el-button @click="importFlow">导入</el-button> <el-button @click="exportFlow">导出</el-button> </div> </div> <div class="logicflow-container" ref="container"></div> <!-- 节点属性面板 --> <div v-if="selectedNode" class="property-panel"> <h3>节点属性</h3> <el-form label-width="80px"> <el-form-item label="节点名称"> <el-input v-model="selectedNode.properties.name"></el-input> </el-form-item> <el-form-item v-if="selectedNode.type === 'approval'" label="审批人"> <el-select v-model="selectedNode.properties.approver" multiple placeholder="选择审批人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> <el-form-item v-if="selectedNode.type === 'condition'" label="条件表达式"> <el-input v-model="selectedNode.properties.condition" type="textarea" :rows="2"></el-input> </el-form-item> <el-form-item label="描述信息"> <el-input v-model="selectedNode.properties.desc" type="textarea" :rows="2"></el-input> </el-form-item> </el-form> </div> </div> </div> <!-- 我的任务 --> <div v-if="activeMenu === 'tasks'"> <h2 class="page-title">我的任务</h2> <div class="card-container"> <div class="task-filter"> <el-radio-group v-model="taskFilter"> <el-radio-button label="pending">待我审批</el-radio-button> <el-radio-button label="processed">我已审批</el-radio-button> <el-radio-button label="started">我发起的</el-radio-button> </el-radio-group> <el-input placeholder="搜索流程名称" style="width: 200px;" v-model="searchKeyword"> <template #append> <el-button icon="el-icon-search"></el-button> </template> </el-input> </div> <div v-if="filteredTasks.length > 0"> <div v-for="task in filteredTasks" :key="task.id" :class="['task-item', { urgent: task.priority === 'high', completed: task.status === 'completed' }]" > <div class="task-header"> <div class="task-title">{{ task.processName }}</div> <div> <span :class="['process-status', `status-${task.status}`]"> {{ taskStatusMap[task.status] }} </span> </div> </div> <div class="task-meta"> <div>申请人: {{ task.applicant }} | 申请时间: {{ task.applyTime }}</div> <div>当前节点: {{ task.currentNode }}</div> <div v-if="task.comment">审批意见: {{ task.comment }}</div> </div> <div class="task-actions"> <el-button v-if="task.status === 'pending'" type="primary" size="small" @click="handleApprove(task)" >审批</el-button> <el-button v-if="task.status === 'pending'" type="danger" size="small" @click="handleReject(task)" >驳回</el-button> <el-button v-if="task.status === 'pending'" type="warning" size="small" @click="handleTransfer(task)" >转办</el-button> <el-button size="small" @click="viewProcess(task)" >查看进度</el-button> </div> </div> </div> <el-empty v-else description="暂无任务"></el-empty> </div> </div> <!-- 新建流程 --> <div v-if="activeMenu === 'create'"> <h2 class="page-title">新建流程申请</h2> <div class="card-container"> <el-form :model="formData" :rules="formRules" ref="applyForm" label-width="120px"> <!-- 流程模板选择 --> <el-form-item label="流程模板" prop="templateId"> <el-select v-model="formData.templateId" placeholder="请选择流程模板" @change="handleTemplateChange"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" > <span style="float: left">{{ template.name }}</span> <span style="float: right; color: #8492a6; font-size: 13px"> {{ template.category }} </span> </el-option> </el-select> </el-form-item> <!-- 动态表单区域 --> <div v-if="selectedTemplate"> <el-divider content-position="left">申请信息</el-divider> <div v-for="field in selectedTemplate.fields" :key="field.name"> <el-form-item :label="field.label" :prop="'fields.' + field.name" :rules="field.rules" > <el-input v-if="field.type === 'text' || field.type === 'textarea'" v-model="formData.fields[field.name]" :type="field.type" :rows="field.type === 'textarea' ? 4 : 2" :placeholder="'请输入' + field.label" /> <el-input-number v-else-if="field.type === 'number'" v-model="formData.fields[field.name]" :placeholder="'请输入' + field.label" /> <el-date-picker v-else-if="field.type === 'date' || field.type === 'datetime'" v-model="formData.fields[field.name]" :type="field.type" :placeholder="'请选择' + field.label" style="width: 100%" /> <el-select v-else-if="field.type === 'select'" v-model="formData.fields[field.name]" :placeholder="'请选择' + field.label" style="width: 100%" > <el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </el-select> </el-form-item> </div> <el-divider content-position="left">附件材料</el-divider> <el-upload action="#" :auto-upload="false" :on-change="handleFileChange" :on-remove="handleFileRemove" :file-list="fileList" multiple > <el-button type="primary">点击上传</el-button> <template #tip> <div class="el-upload__tip"> 支持扩展名:.doc .docx .pdf .jpg .png,单个文件不超过10MB </div> </template> </el-upload> <div v-if="fileList.length > 0" style="margin-top: 15px;"> <div v-for="(file, index) in fileList" :key="index" style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background-color: #f5f7fa; margin-bottom: 8px; border-radius: 4px;"> <div style="display: flex; align-items: center;"> <el-icon><Document /></el-icon> <span style="margin-left: 8px">{{ file.name }}</span> </div> <el-button size="small" type="danger" text @click="handleFileRemove(file)">删除</el-button> </div> </div> <el-divider content-position="left">审批流程</el-divider> <el-timeline> <el-timeline-item v-for="(approver, index) in selectedTemplate.approvers" :key="index" :timestamp="'第' + (index + 1) + '步'" placement="top" > <el-card> <h4>{{ approver.nodeName }}</h4> <p>审批人: {{ approver.userName }}</p> <p v-if="approver.description">{{ approver.description }}</p> </el-card> </el-timeline-item> </el-timeline> </div> <!-- 操作按钮 --> <el-form-item style="margin-top: 30px;"> <el-button type="primary" @click="submitForm" :loading="submitting">提交申请</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </div> </div> <!-- 流程监控 --> <div v-if="activeMenu === 'monitor'"> <h2 class="page-title">流程监控</h2> <div class="card-container"> <el-table :data="processInstances" style="width: 100%"> <el-table-column prop="name" label="流程名称" width="180"></el-table-column> <el-table-column prop="applicant" label="申请人" width="120"></el-table-column> <el-table-column prop="startTime" label="开始时间" width="150"></el-table-column> <el-table-column prop="currentNode" label="当前节点" width="120"></el-table-column> <el-table-column label="状态" width="100"> <template #default="scope"> <el-tag :type="statusType(scope.row.status)"> {{ scope.row.status }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="150"> <template #default="scope"> <el-button size="small" @click="viewProcessDetail(scope.row)">查看详情</el-button> </template> </el-table-column> </el-table> </div> </div> </div> </div> </div> <!-- 审批对话框 --> <el-dialog :title="dialogTitle" v-model="approvalDialogVisible" width="500px"> <el-form :model="approvalForm" label-width="80px"> <el-form-item label="审批意见"> <el-input v-model="approvalForm.comment" type="textarea" :rows="3"></el-input> </el-form-item> <el-form-item v-if="dialogType === 'transfer'" label="转办给"> <el-select v-model="approvalForm.transferTo" placeholder="选择转办人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="approvalDialogVisible = false">取消</el-button> <el-button v-if="dialogType === 'approve'" type="primary" @click="confirmApprove" >通过</el-button> <el-button v-if="dialogType === 'reject'" type="danger" @click="confirmReject" >驳回</el-button> <el-button v-if="dialogType === 'transfer'" type="warning" @click="confirmTransfer" >确认转办</el-button> </template> </el-dialog> </div> <script> const { createApp, ref, reactive, computed, onMounted } = Vue; createApp({ setup() { // 菜单项 const menuItems = reactive([ { key: 'design', label: '流程设计', icon: 'el-icon-s-promotion' }, { key: 'tasks', label: '我的任务', icon: 'el-icon-tickets' }, { key: 'create', label: '新建流程', icon: 'el-icon-circle-plus-outline' }, { key: 'monitor', label: '流程监控', icon: 'el-icon-data-line' } ]); const activeMenu = ref('tasks'); // LogicFlow相关 const container = ref(null); let lf = null; const selectedNode = ref(null); const currentTemplate = ref(''); // 模板列表 const templateList = ref([ { id: 1, name: '请假申请', category: '人力资源', description: '用于员工请假申请审批流程', fields: [ { name: 'leaveType', label: '请假类型', type: 'select', rules: [{ required: true, message: '请选择请假类型', trigger: 'change' }], options: [ { value: 'annual', label: '年假' }, { value: 'sick', label: '病假' }, { value: 'personal', label: '事假' }, { value: 'marriage', label: '婚假' }, { value: 'maternity', label: '产假' } ] }, { name: 'startDate', label: '开始时间', type: 'datetime', rules: [{ required: true, message: '请选择开始时间', trigger: 'change' }] }, { name: 'endDate', label: '结束时间', type: 'datetime', rules: [{ required: true, message: '请选择结束时间', trigger: 'change' }] }, { name: 'duration', label: '请假时长(天)', type: 'number', rules: [ { required: true, message: '请输入请假时长', trigger: 'blur' }, { type: 'number', min: 0.5, max: 365, message: '时长应在0.5到365天之间', trigger: 'blur' } ] }, { name: 'reason', label: '请假事由', type: 'textarea', rules: [{ required: true, message: '请输入请假事由', trigger: 'blur' }] } ], approvers: [ { nodeName: '直属上级审批', userName: '张经理', description: '审批请假合理性' }, { nodeName: '人事部门审核', userName: '李人事', description: '核对假期余额及合规性' } ] }, { id: 2, name: '采购申请', category: '财务', description: '用于物品采购申请审批流程', fields: [ { name: 'itemName', label: '物品名称', type: 'text', rules: [{ required: true, message: '请输入物品名称', trigger: 'blur' }] }, { name: 'specification', label: '规格型号', type: 'text', rules: [{ required: true, message: '请输入规格型号', trigger: 'blur' }] }, { name: 'quantity', label: '数量', type: 'number', rules: [ { required: true, message: '请输入数量', trigger: 'blur' }, { type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' } ] }, { name: 'estimatedPrice', label: '预估单价', type: 'number', rules: [ { required: true, message: '请输入预估单价', trigger: 'blur' }, { type: 'number', min: 0, message: '单价不能为负数', trigger: 'blur' } ] }, { name: 'purpose', label: '用途说明', type: 'textarea', rules: [{ required: true, message: '请输入用途说明', trigger: 'blur' }] } ], approvers: [ { nodeName: '部门经理审批', userName: '王经理', description: '审核采购必要性' }, { nodeName: '财务审核', userName: '赵会计', description: '审核预算及价格合理性' }, { nodeName: '总经理审批', userName: '孙总', description: '最终审批' } ] } ]); // 用户列表 const userList = ref([ { id: 1, name: '张三', department: '技术部' }, { id: 2, name: '李四', department: '人事部' }, { id: 3, name: '王五', department: '财务部' }, { id: 4, name: '赵六', department: '市场部' }, { id: 5, name: '钱七', department: '技术部' } ]); // 初始化LogicFlow const initLogicFlow = () => { if (!container.value) return; // 使用LogicFlow的BPMN扩展 LogicFlow.use(LogicFlow.BpmnElement); LogicFlow.use(LogicFlow.Snapshot); LogicFlow.use(LogicFlow.Menu); LogicFlow.use(LogicFlow.Control); lf = new LogicFlow({ container: container.value, grid: true, width: container.value.clientWidth, height: 500, keyboard: { enabled: true }, style: { rect: { width: 100, height: 60, radius: 6 } } }); // 渲染初始流程图 lf.render({ nodes: [ { id: '1', type: 'bpmn:startEvent', x: 100, y: 100, properties: { name: '开始' } } ] }); // 监听节点选择事件 lf.on('node:click', ({ data }) => { selectedNode.value = data; }); // 监听画布点击事件(点击空白处取消选择) lf.on('blank:click', () => { selectedNode.value = null; }); }; // 保存流程 const saveFlow = () => { if (!currentTemplate.value) { ElMessage.warning('请先选择或创建流程模板'); return; } const graphData = lf.getGraphData(); console.log('保存流程数据:', graphData); // 这里应该调用API保存流程数据 ElMessage.success('流程保存成功'); }; // 新建模板 const createNewTemplate = () => { ElMessageBox.prompt('请输入新模板名称', '新建模板', { confirmButtonText: '确定', cancelButtonText: '取消', }).then(({ value }) => { if (!value) return; const newTemplate = { id: templateList.value.length + 1, name: value, category: '自定义', description: '新建的流程模板', fields: [], approvers: [] }; templateList.value.push(newTemplate); currentTemplate.value = newTemplate.id; ElMessage.success('模板创建成功'); }); }; // 导入导出 const importFlow = () => { ElMessage.info('导入功能'); }; const exportFlow = () => { const graphData = lf.getGraphData(); const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(graphData, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "flow.json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); ElMessage.success('流程已导出'); }; // 任务相关 const taskFilter = ref('pending'); const searchKeyword = ref(''); // 模拟任务数据 const tasks = ref([ { id: 1, processName: '请假申请', applicant: '张三', applyTime: '2023-10-01 09:30', currentNode: '部门审批', status: 'pending', priority: 'normal', comment: '' }, { id: 2, processName: '采购申请', applicant: '李四', applyTime: '2023-10-01 10:15', currentNode: '财务审核', status: 'pending', priority: 'high', comment: '' }, { id: 3, processName: '费用销', applicant: '王五', applyTime: '2023-09-30 14:20', currentNode: '已完成', status: 'completed', priority: 'normal', comment: '符合公司销政策' } ]); // 任务状态映射 const taskStatusMap = { 'pending': '待处理', 'processing': '处理中', 'completed': '已完成', 'rejected': '已驳回' }; // 过滤后的任务列表 const filteredTasks = computed(() => { let result = tasks.value; // 根据筛选条件过滤 if (taskFilter.value === 'pending') { result = result.filter(task => task.status === 'pending'); } else if (taskFilter.value === 'processed') { result = result.filter(task => task.status === 'completed' || task.status === 'rejected'); } else if (taskFilter.value === 'started') { // 这里应该是用户自己发起的流程 result = result.filter(task => task.applicant === '当前用户'); } // 根据关键词搜索 if (searchKeyword.value) { const keyword = searchKeyword.value.toLowerCase(); result = result.filter(task => task.processName.toLowerCase().includes(keyword) || task.applicant.toLowerCase().includes(keyword) ); } return result; }); // 审批对话框相关 const approvalDialogVisible = ref(false); const dialogType = ref(''); // approve, reject, transfer const dialogTitle = ref(''); const currentTask = ref(null); const approvalForm = reactive({ comment: '', transferTo: '' }); // 打开审批对话框 const handleApprove = (task) => { currentTask.value = task; dialogType.value = 'approve'; dialogTitle.value = `审批通过 - ${task.processName}`; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开驳回对话框 const handleReject = (task) => { currentTask.value = task; dialogType.value = 'reject'; dialogTitle.value = `驳回申请 - ${task.processName}`; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开转办对话框 const handleTransfer = (task) => { currentTask.value = task; dialogType.value = 'transfer'; dialogTitle.value = `转办任务 - ${task.processName}`; approvalForm.comment = ''; approvalForm.transferTo = ''; approvalDialogVisible.value = true; }; // 确认审批通过 const confirmApprove = () => { if (!approvalForm.comment) { ElMessage.warning('请填写审批意见'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'completed'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.success('审批通过'); }; // 确认驳回 const confirmReject = () => { if (!approvalForm.comment) { ElMessage.warning('请填写驳回原因'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'rejected'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.warning('申请已驳回'); }; // 确认转办 const confirmTransfer = () => { if (!approvalForm.transferTo) { ElMessage.warning('请选择转办人'); return; } // 查找转办人 const transferToUser = userList.value.find(user => user.id === approvalForm.transferTo); if (!transferToUser) { ElMessage.error('选择的转办人不存在'); return; } // 更新任务 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.comment = `转办给: ${transferToUser.name}` + (approvalForm.comment ? `, 备注: ${approvalForm.comment}` : ''); // 在实际应用中,这里应该将任务转交给其他用户 } approvalDialogVisible.value = false; ElMessage.success(`任务已转办给 ${transferToUser.name}`); }; // 查看流程进度 const viewProcess = (task) => { ElMessage.info(`查看流程 ${task.processName} 的进度`); // 这里应该打开流程进度详情页面 }; // 新建流程相关 const formData = reactive({ templateId: '', fields: {} }); const fileList = ref([]); const submitting = ref(false); const applyForm = ref(null); // 当前选中的模板 const selectedTemplate = computed(() => { return templateList.value.find(t => t.id === formData.templateId) || null; }); // 表单验证规则 const formRules = reactive({ templateId: [ { required: true, message: '请选择流程模板', trigger: 'change' } ] }); // 处理模板选择变化 const handleTemplateChange = (templateId) => { // 重置表单字段 formData.fields = {}; // 为选中模板的每个字段初始化值 if (selectedTemplate.value) { selectedTemplate.value.fields.forEach(field => { formData.fields[field.name] = field.type === 'number' ? 0 : ''; }); } }; // 处理文件变化 const handleFileChange = (file, files) => { // 限制文件大小 if (file.size > 10 * 1024 * 1024) { ElMessage.error('文件大小不能超过10MB'); return false; } // 更新文件列表 fileList.value = files; }; // 处理文件移除 const handleFileRemove = (file) => { const index = fileList.value.indexOf(file); if (index !== -1) { fileList.value.splice(index, 1); } }; // 提交表单 const submitForm = () => { if (!applyForm.value) return; applyForm.value.validate((valid) => { if (valid) { submitting.value = true; // 模拟API调用 setTimeout(() => { ElMessage.success('申请提交成功!'); submitting.value = false; // 重置表单 resetForm(); // 切换到任务页面 activeMenu.value = 'tasks'; }, 1500); } else { ElMessage.error('请完善表单信息'); return false; } }); }; // 重置表单 const resetForm = () => { if (applyForm.value) { applyForm.value.resetFields(); } formData.fields = {}; fileList.value = []; }; // 流程监控相关 const processInstances = ref([ { id: 1, name: '请假申请', applicant: '张三', startTime: '2023-10-01 09:30', currentNode: '部门审批', status: '审批中' }, { id: 2, name: '采购申请', applicant: '李四', startTime: '2023-10-01 10:15', currentNode: '财务审核', status: '审批中' }, { id: 3, name: '费用销', applicant: '王五', startTime: '2023-09-30 14:20', currentNode: '已完成', status: '已通过' } ]); // 状态标签类型 const statusType = (status) => { const map = { '审批中': 'primary', '已通过': 'success', '已拒绝': 'danger', '已撤销': 'info' }; return map[status] || 'info'; }; // 查看流程详情 const viewProcessDetail = (process) => { ElMessage.info(`查看流程 ${process.name} 的详情`); // 这里应该打开流程详情页面 }; // 初始化 onMounted(() => { initLogicFlow(); }); return { menuItems, activeMenu, container, selectedNode, currentTemplate, templateList, userList, saveFlow, createNewTemplate, importFlow, exportFlow, taskFilter, searchKeyword, filteredTasks, taskStatusMap, handleApprove, handleReject, handleTransfer, viewProcess, approvalDialogVisible, dialogType, dialogTitle, approvalForm, confirmApprove, confirmReject, confirmTransfer, formData, formRules, fileList, submitting, applyForm, selectedTemplate, handleTemplateChange, handleFileChange, handleFileRemove, submitForm, resetForm, processInstances, statusType, viewProcessDetail }; } }).use(ElementPlus).mount('#app'); </script> </body> </html>优化页面
09-03
<template> <div> <!-- 编辑模态框 --> <ElDialog v-model="showEditModal" title="编辑测试用例" width="70%" :close-on-click-modal="false"> <ElForm ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="120px"> <!-- 使用过滤后的字段 --> <ElFormItem v-for="(field, key) in filteredTestCaseFields" :key="key" :label="field.label" :prop="key"> <template v-if="field.type === 'select'"> <ElSelect v-model="editForm[key]" :placeholder="`请选择${field.label}`" class="w-full" @focus="loadOptionsIfNeeded(key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </ElSelect> </template> <template v-else-if="key === 'test_steps'"> <TestStepAdd :test-steps="editForm.test_steps" :expected-results="editForm.expected_result" fieldname="测试步骤" @update:test-steps="editForm.test_steps = $event" @update:expected-results="editForm.expected_result = $event" /> </template> <template v-else-if="field.type === 'select-multi'"> <ElSelect v-model="editForm[key]" :placeholder="`请选择${field.label}`" class="w-full" multiple @focus="loadOptionsIfNeeded(key)" @blur="handleSort(editForm, key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> <template #footer> <el-button v-if="!isAdding[key]" text bg size="small" @click="() => onAddOption(key)"> 增加选项 </el-button> <template v-else> <el-input v-model="optionName[key]" class="option-input" placeholder="输入选项" size="small" /> <el-button type="primary" size="small" @click="() => onConfirm(key)"> 确认 </el-button> <el-button size="small" @click="() => clear(key)"> 取消 </el-button> </template> </template> </ElSelect> </template> <template v-else-if="field.type === 'image'"> <el-upload class="upload-demo" :auto-upload="false" :on-change="(file) => handleImageChange(file, key, 'edit')" :on-remove="(file) => handleImageRemove(key, 'edit', file)" :on-preview="(file) => handlePreview(file)" :limit="3" :file-list="getFileList(key, 'edit')" list-type="picture-card" accept="image/*" multiple > <el-icon><Plus /></el-icon> <!-- <el-button type="primary">选择图片</el-button> --> <template #tip> <div class="el-upload__tip">最多3张,支持jpg、png格式</div> </template> </el-upload> </template> <template v-else> <ElInput v-model="editForm[key]" :placeholder="`请输入${field.label}`" :type="field.type" :rows="field.type === 'textarea' ? 3 : undefined" :disabled=" key === 'test_case_id' || key === 'test_task' || key === 'modification_date' || key === 'modification_user' || key === 'scenario' " /> </template> </ElFormItem> </ElForm> <template #footer> <span class="dialog-footer"> <ElButton @click="showEditModal = false">取消</ElButton> <ElButton type="primary" @click="handleEditSubmit">确定</ElButton> </span> </template> </ElDialog> <!-- 新建用例模态框 --> <ElDialog v-model="showNewCaseModal" title="新建测试用例" width="70%" :close-on-click-modal="false"> <ElForm ref="newFormRef" :model="newForm" :rules="editFormRules" label-width="120px"> <!-- 使用过滤后的字段 --> <ElFormItem v-for="(field, key) in filteredTestCaseFields" :key="key" :label="field.label" :prop="key"> <template v-if="field.type === 'select'"> <ElSelect v-model="newForm[key]" :placeholder="`请选择${field.label}`" class="w-full" @focus="loadOptionsIfNeeded(key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </ElSelect> </template> <template v-else-if="key === 'test_steps'"> <TestStepAdd :test-steps="newForm.test_steps" :expected-results="newForm.expected_result" fieldname="测试步骤" @update:test-steps="newForm.test_steps = $event" @update:expected-results="newForm.expected_result = $event" /> </template> <template v-else-if="field.type === 'select-multi'"> <ElSelect v-model="newForm[key]" :placeholder="`请选择${field.label}`" class="w-full" multiple @focus="loadOptionsIfNeeded(key)" @blur="handleSort(newForm, key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> <template #footer> <el-button v-if="!isAdding[key]" text bg size="small" @click="() => onAddOption(key)"> 增加选项 </el-button> <template v-else> <el-input v-model="optionName[key]" class="option-input" placeholder="输入选项" size="small" /> <el-button type="primary" size="small" @click="() => onConfirm(key)"> 确认 </el-button> <el-button size="small" @click="() => clear(key)"> 取消 </el-button> </template> </template> </ElSelect> </template> <template v-else-if="field.type === 'image'"> <el-upload class="upload-demo" :auto-upload="false" :on-change="(file) => handleImageChange(file, key, 'new')" :on-remove="(file) => handleImageRemove(key, 'new', file)" :on-preview="(file) => handlePreview(file)" :limit="3" :file-list="getFileList(key, 'new')" list-type="picture-card" accept="image/*" multiple > <el-icon><Plus /></el-icon> <template #tip> <div class="el-upload__tip">最多3张,支持jpg、png格式</div> </template> </el-upload> </template> <template v-else> <ElInput v-model="newForm[key]" :placeholder="key === 'test_case_id' ? '由功能名称决定' : `请输入${field.label}`" :type="field.type" :rows="field.type === 'textarea' ? 3 : undefined" @input="key === 'test_case_id' ? checkIdDuplicate() : null" :class="{ 'is-error': key === 'test_case_id' && isIdDuplicate }" :disabled=" key === 'test_task' || key === 'modification_date' || key === 'test_case_id' || key === 'modification_user' " /> </template> <div v-if="key === 'test_case_id' && isIdDuplicate" class="el-form-item__error"> 用例ID已存在,请使用其他ID </div> </ElFormItem> </ElForm> <template #footer> <span class="dialog-footer"> <ElButton @click="showNewCaseModal = false">取消</ElButton> <ElButton type="primary" @click="handleNewSubmit">确定</ElButton> </span> </template> </ElDialog> <!-- 预览对话框 --> <el-dialog v-model="previewDialogVisible" title="图片预览" width="50%" :close-on-click-modal="true"> <div class="flex justify-center"> <img :src="previewImageUrl" class="max-w-full max-h-[70vh] object-contain" alt="预览图片" /> </div> </el-dialog> </div> </template> <script setup> import { ref, watch, computed } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { testcaseApi } from '@/api/modules/api.testcases' import { testCaseFields, getRequiredFields, getDefaultFormData } from '@/views/testcases/fields' import TestStepAdd from './TestStepAdd.vue' const props = defineProps({ module: { type: String, required: true }, tableData: { type: Array, default: () => [] } }) const emit = defineEmits(['refresh']) // 状态变量 const showEditModal = ref(false) const showNewCaseModal = ref(false) const editFormRef = ref(null) const newFormRef = ref(null) const isIdDuplicate = ref(false) const uploadedFiles = ref({}) const previewDialogVisible = ref(false) const previewImageUrl = ref('') const isAdding = reactive({}) const optionName = reactive({}) // 选项加载状态 const optionsloading = reactive({}) // 选项是否已加载 const optionsLoaded = reactive({}) // 表单数据 const editForm = ref(getDefaultFormData()) const newForm = ref(getDefaultFormData()) // 历史用例类型,功能,新用例id const editscenario = ref('') const edittestcaseid = ref('') // 使用配置的必填字段生成验证规则 const editFormRules = {} getRequiredFields().forEach((field) => { editFormRules[field] = [ { required: true, message: `请输入${testCaseFields[field].label}`, trigger: 'blur' } ] }) // 获取字段选项 const loadOptionsIfNeeded = async (key) => { // 如果选项已加载或正在加载,不重复请求 if (optionsLoaded[key] || optionsloading[key]) { return } optionsloading[key] = true try { // 调用API获取选项 const response = await testcaseApi.getFieldOptions(key) // 更新字段配置中的选项 testCaseFields[key].options = response.data.option // 标记选项已加载 optionsLoaded[key] = true } catch (error) { console.error(`加载${testCaseFields[key].label}选项失败`, error) // 显示误提示 ElMessage.error(`加载${testCaseFields[key].label}选项失败: ${error.message}`) } finally { optionsloading[key] = false } } // 获取北京时间的函数 const getBeijingTime = () => { const now = new Date() const utc = now.getTime() + now.getTimezoneOffset() * 60000 const beijingTime = new Date(utc + 3600000 * 8) return beijingTime } // 格式化时间为 YYYY-MM-DD HH:mm:ss const formatDateTime = (date) => { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') const seconds = String(date.getSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } // 生成唯一ID const generateUniqueId = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } // 获取文件列表 const getFileList = (field, formType = 'edit') => { if (!uploadedFiles.value[formType]) { uploadedFiles.value[formType] = {} } if (!uploadedFiles.value[formType][field]) { uploadedFiles.value[formType][field] = [] } return uploadedFiles.value[formType][field] } // 处理图片变更 - 支持多文件 const handleImageChange = (file, field, formType = 'edit') => { if (file.raw) { // 生成唯一ID作为图片名称 const uniqueId = generateUniqueId() // 获取文件扩展名 const fileExtension = file.raw.name.split('.').pop() // 创建新的文件名 const newFileName = `${uniqueId}.${fileExtension}` // 创建新的 File 对象,使用新的文件名 const newFile = new File([file.raw], newFileName, { type: file.raw.type, lastModified: file.raw.lastModified }) // 初始化表单数据存储结构 if (!uploadedFiles.value[formType]) { uploadedFiles.value[formType] = {} } if (!uploadedFiles.value[formType][field]) { uploadedFiles.value[formType][field] = [] } // 限制数量(保留最新上传的3个文件) const fileList = [ ...uploadedFiles.value[formType][field], { name: file.name, raw: newFile, originalName: file.name, uniqueId: uniqueId, // 新增唯一标识 url: URL.createObjectURL(newFile) } ] if (fileList.length > 3) { fileList.shift() // 移除最早的文件 } // 更新文件列表 uploadedFiles.value[formType][field] = fileList // 更新表单数据,如果是文件存文件,不是文件存URL if (formType === 'edit') { editForm.value[field] = fileList.map((f) => (f.raw ? f.raw : f.url)) } else { newForm.value[field] = fileList.map((f) => (f.raw ? f.raw : f.url)) } } } // 处理图片移除 - 支持多文件 const handleImageRemove = (field, formType = 'edit', fileToRemove) => { if (!uploadedFiles.value[formType] || !uploadedFiles.value[formType][field]) return // 过滤掉要移除的文件(兼容编辑模式的不同结构) const updatedFiles = uploadedFiles.value[formType][field].filter((file) => { if (formType === 'edit' && file.url && !file.uniqueId) { return file.url !== fileToRemove.url // 通过 URL 匹配移除 } return file.uniqueId !== fileToRemove.uniqueId }) // 更新文件列表 uploadedFiles.value[formType][field] = updatedFiles // 更新表单数据 if (formType === 'edit') { // 新增:编辑模式下直接使用 URL 数组 editForm.value[field] = updatedFiles.map((f) => f.url || f.raw) } else { newForm.value[field] = updatedFiles.map((f) => f.url || f.raw) } // 如果预览的是被移除的图片,则关闭预览 if ( fileToRemove.url === previewImageUrl.value || (fileToRemove.raw && URL.createObjectURL(fileToRemove.raw) === previewImageUrl.value) ) { previewDialogVisible.value = false } } // 处理图片预览 const handlePreview = (file) => { if (file.url) { previewImageUrl.value = file.url } else if (file.raw) { previewImageUrl.value = URL.createObjectURL(file.raw) } previewDialogVisible.value = true } // 拆分 URL 字符串 const splitImageUrls = (url) => { if (!url) return const imageUrls = url .split(',') .map((url) => url.trim()) .map((path) => (path.startsWith('http') ? path : `${import.meta.env.VITE_APP_BASE_URL}${path}`)) .map((path) => path.replace(':81', ':80')) // 去除可能的空格 return imageUrls } // 检查ID是否重复 const checkIdDuplicate = () => { if (!newForm.value.test_case_id) { isIdDuplicate.value = false return } isIdDuplicate.value = props.tableData.some((item) => item.test_case_id === newForm.value.test_case_id) } //增加选项 const onAddOption = (key) => { isAdding[key] = true } const onConfirm = async (key) => { const newLabel = optionName[key].trim() const newValue = optionName[key].trim() if (!newLabel) { ElMessage.warning('选项名称不能为空') return } // 检查前端选项列表中是否已存在 const exists = testCaseFields[key].options.some( (option) => option.value === newValue || option.label === newLabel ) if (exists) { ElMessage.warning('选项已存在') clear(key) return } if (optionName[key]) { const response = await testcaseApi.createFieldOptions(key, optionName[key], optionName[key]) if (response.status) { // 添加新选项到 field.options testCaseFields[key].options.push({ label: optionName[key], value: optionName[key] }) } // 清空输入并隐藏添加表单 clear(key) } } const clear = (key) => { optionName[key] = '' isAdding[key] = false } const fieldToLabel = (fieldName, fieldsConfig = testCaseFields) => { // 从配置中查找字段的标签 const fieldConfig = fieldsConfig[fieldName] return fieldConfig.label } // 提交编辑 const handleEditSubmit = async () => { if (!editFormRef.value) return try { await editFormRef.value.validate() // 冲突验证 // 获取后端最新的版本,检查最后修改时间 const lastestTestCase = await testcaseApi.getTestcase(props.module, editForm.value.test_case_id) // 如果最后修改时间与当前编辑的用例一致,则说明没有冲突,反之。 if ( editForm.value.modification_date !== lastestTestCase.data.modification_date && lastestTestCase.data.test_case_history[0].changes && Object.keys(lastestTestCase.data.test_case_history[0].changes).length > 0 ) { // 构建修改内容摘要列表 let changesHtml = '' if (lastestTestCase.data.test_case_history[0].changes) { const changes = lastestTestCase.data.test_case_history[0].changes changesHtml = Object.entries(changes) .filter( ([field]) => field !== 'modification_date' && field !== 'modification_status' && field !== 'modification_user' ) .map( ([field, change]) => ` <li class="mb-2"> <div> <span class="text-sm font-medium text-gray-700"> ${fieldToLabel(field)} </span> <p class="text-sm text-gray-500 mt-1"> 旧值:${change.old || '无'}<br /> 新值:${change.new || '无'} </p> </div> </li> ` ) .join('') } const message = ` <div style="line-height: 1.6; color: #666;"> <p>检测到冲突:该用例已被<strong>${lastestTestCase.data.modification_user}</strong>修改</p> <p>最后修改时间:<strong>${lastestTestCase.data.modification_date}</strong></p> <p>修改内容摘要:</p> <ul class="space-y-3" style="margin-left: 20px; list-style-type: disc;"> ${changesHtml} </ul> </div> ` ElMessageBox.confirm(message, '编辑冲突', { confirmButtonText: '加载最新', cancelButtonText: '继续编辑', type: 'warning', dangerouslyUseHTMLString: true }) .then(() => { // 用户选择加载最新版本 openEditModal(lastestTestCase.data) }) .catch(() => { // 用户选择继续编辑 editForm.value.modification_date = lastestTestCase.data.modification_date }) return } const formData = new FormData() Object.entries(editForm.value).forEach(([key, value]) => { if ( value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0) ) { return } else if (key === 'ui' || key === 'case_source_screenshot') { // 确保value是数组(兼容单文件场景) const files = Array.isArray(value) ? value : [value] files.forEach((file) => { if (file instanceof File) { formData.append(key, file) // 逐个添加File对象 } else { formData.append(key, String(file)) } }) } else if (key === 'test_steps' || key === 'expected_result') { // 在每个步骤前加上序号 const numberedSteps = value.map((step, index) => `${index + 1}. ${step}`) formData.append(key, numberedSteps.join('\n')) } else { formData.append(key, String(value)) } }) formData.append('modification_date', formatDateTime(getBeijingTime())) formData.append( 'modification_status', editForm.value.modification_status === '新建' ? '修改' : editForm.value.modification_status ) await testcaseApi.postTestcases(props.module, formData) ElMessage.success('更新成功') showEditModal.value = false emit('refresh') } catch (error) { if (error.response) { ElMessage.error(error.response.data.message || '更新失败') } else { ElMessage.error('更新失败') } } } // 提交新建 const handleNewSubmit = async () => { if (!newFormRef.value) return try { if (isIdDuplicate.value) { ElMessage.error('用例ID已存在,请使用其他ID') return } await newFormRef.value.validate() const formData = new FormData() Object.entries(newForm.value).forEach(([key, value]) => { if (key === 'ui' || key === 'case_source_screenshot') { // 确保value是数组(兼容单文件场景) const files = Array.isArray(value) ? value : [value] files.forEach((file) => { if (file instanceof File) { formData.append(key, file) // 逐个添加File对象 } else { formData.append(key, String(file)) } }) } else if (key === 'test_steps' || key === 'expected_result') { // 在每个步骤前加上序号 const numberedSteps = value.map((step, index) => `${index + 1}. ${step}`) formData.append(key, numberedSteps.join('\n')) } else { formData.append(key, String(value)) } }) formData.append('modification_date', formatDateTime(getBeijingTime())) formData.append('modification_status', '新建') await testcaseApi.postTestcases(props.module, formData) ElMessage.success('创建成功') showNewCaseModal.value = false newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false emit('refresh') } catch (error) { if (error.response) { ElMessage.error(error.response.data.message || '创建失败') } else { ElMessage.error('创建失败') } } } // 打开编辑模态框 const openEditModal = (row) => { editForm.value = { ...row } editscenario.value = row.scenario edittestcaseid.value = row.test_case_id // 将字符串转换为数组,并移除序号 if (typeof row.test_steps === 'string') { // 处理测试步骤 if (row.test_steps) { const steps = row.test_steps.split('\n') editForm.value.test_steps = steps .map((step) => { // 移除开头的序号(例如:"1. 步骤内容" -> "步骤内容") return step.replace(/^\d+\.\s*/, '').trim() }) .filter((step) => step !== '') // 过滤掉空步骤 } else { editForm.value.test_steps = [''] } } else if (!Array.isArray(editForm.value.test_steps)) { editForm.value.test_steps = [''] } // 处理预期结果 if (typeof row.expected_result === 'string') { if (row.expected_result) { const results = row.expected_result.split('\n') editForm.value.expected_result = results .map((result) => { // 移除开头的序号 return result.replace(/^\d+\.\s*/, '').trim() }) .filter((result) => result !== '') // 过滤掉空结果 } else { editForm.value.expected_result = [''] } } else if (!Array.isArray(editForm.value.expected_result)) { editForm.value.expected_result = [''] } // 确保测试步骤和预期结果数量一致 const maxLength = Math.max(editForm.value.test_steps.length, editForm.value.expected_result.length) while (editForm.value.test_steps.length < maxLength) { editForm.value.test_steps.push('') } while (editForm.value.expected_result.length < maxLength) { editForm.value.expected_result.push('') } // 初始化图片字段的文件列表 Object.keys(testCaseFields).forEach((key) => { if (testCaseFields[key].type === 'image' && row[key]) { const imageUrls = splitImageUrls(row[key]) if (!uploadedFiles.value.edit) { uploadedFiles.value.edit = {} } // 将每个URL单独添加到uploadedFiles中 uploadedFiles.value.edit[key] = imageUrls.map((url, index) => ({ url: url })) editForm.value[key] = imageUrls } else if (testCaseFields[key].type === 'select-multi') { if (typeof row[key] === 'string') { const allValues = row[key].split(/[、,]/) const validValues = allValues.filter((value) => value.trim() !== '') editForm.value[key] = validValues } } }) showEditModal.value = true } // 打开新建模态框 const openNewModal = (row) => { if (row instanceof PointerEvent) { newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false } else { newForm.value = { ...row } newForm.value.test_case_id = '' delete newForm.value.test_case_history editscenario.value = row.scenario edittestcaseid.value = row.test_case_id // 将字符串转换为数组,并移除序号 if (typeof row.test_steps === 'string') { // 处理测试步骤 if (row.test_steps) { const steps = row.test_steps.split('\n') newForm.value.test_steps = steps .map((step) => { // 移除开头的序号(例如:"1. 步骤内容" -> "步骤内容") return step.replace(/^\d+\.\s*/, '').trim() }) .filter((step) => step !== '') // 过滤掉空步骤 } else { newForm.value.test_steps = [''] } } else if (!Array.isArray(newForm.value.test_steps)) { newForm.value.test_steps = [''] } // 处理预期结果 if (typeof row.expected_result === 'string') { if (row.expected_result) { const results = row.expected_result.split('\n') newForm.value.expected_result = results .map((result) => { // 移除开头的序号 return result.replace(/^\d+\.\s*/, '').trim() }) .filter((result) => result !== '') // 过滤掉空结果 } else { newForm.value.expected_result = [''] } } else if (!Array.isArray(newForm.value.expected_result)) { newForm.value.expected_result = [''] } // 确保测试步骤和预期结果数量一致 const maxLength = Math.max(newForm.value.test_steps.length, newForm.value.expected_result.length) while (newForm.value.test_steps.length < maxLength) { newForm.value.test_steps.push('') } while (newForm.value.expected_result.length < maxLength) { newForm.value.expected_result.push('') } // 初始化图片字段的文件列表 Object.keys(testCaseFields).forEach((key) => { if (testCaseFields[key].type === 'image' && row[key]) { const imageUrls = splitImageUrls(row[key]) if (!uploadedFiles.value.new) { uploadedFiles.value.new = {} } // 将每个URL单独添加到uploadedFiles中 uploadedFiles.value.new[key] = imageUrls.map((url, index) => ({ url: url })) newForm.value[key] = imageUrls } else if (testCaseFields[key].type === 'select-multi') { if (typeof row[key] === 'string') { const allValues = row[key].split(/[、,]/) const validValues = allValues.filter((value) => value.trim() !== '') newForm.value[key] = validValues } } }) } showNewCaseModal.value = true } // 处理排序逻辑 const handleSort = (formRef, key) => { // const formValue = formRef.value; // 获取当前选中的值 let selectedValues = formRef[key] // 对值进行排序 selectedValues.sort((a, b) => { // 如果值是对象,假设对象有一个label属性作为显示文本 if (typeof a === 'object' && typeof b === 'object') { return a.label.localeCompare(b.label) } // 如果值是基本类型(字符串或数字) return a.toString().localeCompare(b.toString()) }) // 更新绑定的数据 formRef[key] = selectedValues } // 监听预览对话框关闭 watch(previewDialogVisible, (newVal) => { if (!newVal) { if (previewImageUrl.value.startsWith('blob:')) { URL.revokeObjectURL(previewImageUrl.value) } previewImageUrl.value = '' } }) // 监听编辑模态框关闭 watch(showEditModal, (newVal) => { if (!newVal) { uploadedFiles.value = {} // 重置所有字段的添加选项状态 Object.keys(isAdding).forEach((key) => { isAdding[key] = false optionName[key] = '' }) } }) // 监听新建模态框关闭 watch(showNewCaseModal, (newVal) => { if (!newVal) { newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false // 重置所有字段的添加选项状态 Object.keys(isAdding).forEach((key) => { isAdding[key] = false optionName[key] = '' }) } }) // 添加计算属性来过滤字段 const filteredTestCaseFields = computed(() => { const fields = { ...testCaseFields } if (showNewCaseModal.value || showEditModal.value) { delete fields.expected_result } if (showNewCaseModal.value) { delete fields.test_case_id } return fields }) // 暴露方法给父组件 defineExpose({ openEditModal, openNewModal }) </script> <style scoped> .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } .option-input { width: 100%; margin-bottom: 8px; } </style> 将新建和编辑逻辑统一,不改变任何功能
07-30
<template> <div class="vote-container"> <!-- 投票人信息 --> <div class="voter-info" v-if="voterName && voterIdCard"> <p>投票人:{{ voterName }}</p> <p>身份证:{{ formattedIdCard }}</p> </div> <!-- 投票统计信息 --> <div class="stats"> <div class="stat"> <h3>经理投票</h3> <div class="progress"> <div class="progress-bar" :style="{ width: (votes.A / 5) * 100 + '%' }"></div> </div> <p>{{ votes.A }} / 5</p> </div> <div class="stat"> <h3>厂长投票</h3> <div class="progress"> <div class="progress-bar" :style="{ width: (votes.B / 5) * 100 + '%' }"></div> </div> <p>{{ votes.B }} / 5</p> </div> <div class="stat"> <h3>副厂长投票</h3> <div class="progress"> <div class="progress-bar" :style="{ width: (votes.C / 15) * 100 + '%' }"></div> </div> <p>{{ votes.C }} / 15</p> </div> <div class="stat"> <h3>总票数</h3> <div class="progress"> <div class="progress-bar" :style="{ width: (totalVotes / 25) * 100 + '%' }"></div> </div> <p>{{ totalVotes }} / 25</p> </div> </div> <!-- 被投票人列表 --> <div class="voters-grid"> <div v-for="voter in voters" :key="voter.id" class="voter-card"> <h4>{{ voter.name }}</h4> <p class="voter-id">ID: {{ voter.id }}</p> <div class="vote-options"> <button @click="castVote(voter, 'A')" :disabled="!canVote(voter, 'A')" :class="{ 'selected': voter.vote === 'A', 'disabled': !canVote(voter, 'A') }" > 经理 </button> <button @click="castVote(voter, 'B')" :disabled="!canVote(voter, 'B')" :class="{ 'selected': voter.vote === 'B', 'disabled': !canVote(voter, 'B') }" > 厂长 </button> <button @click="castVote(voter, 'C')" :disabled="!canVote(voter, 'C')" :class="{ 'selected': voter.vote === 'C', 'disabled': !canVote(voter, 'C') }" > 副厂长 </button> </div> </div> </div> <!-- 操作按钮 --> <div class="action-buttons"> <button @click="submitVotes" :disabled="totalVotes === 0 || isSubmitting">提交投票</button> <button @click="resetVotes" :disabled="isSubmitting">重置投票</button> </div> </div> </template> <script setup> import { ref, reactive, computed } from 'vue'; import { useRoute } from 'vue-router'; import { onMounted } from 'vue' const voters = ref([]); //候选人 // onMounted生命周期钩子 onMounted(async () => { // 从sessionStorage获取投票人信息并立即清除 const voterInfo = sessionStorage.getItem('voterInfo'); if (voterInfo) { const { name, idCard } = JSON.parse(voterInfo); voterName.value = name; voterIdCard.value = idCard; sessionStorage.removeItem('voterInfo'); } // 加载候选人数据 voters.value = await fetchCandidates(); }); // 获取路由信息 const route = useRoute(); // 添加用于存储投票人信息的变量 const voterName = ref(''); const voterIdCard = ref(''); // 格式化身份证显示(安全脱敏) const formattedIdCard = computed(() => { if (!voterIdCard.value) return ''; // 显示前6位和后4位,中间用*代替 return voterIdCard.value.substring(0, 6) + '******' + voterIdCard.value.substring(voterIdCard.value.length - 4); }); onMounted(() => { // 从sessionStorage获取数据并立即清除 const voterInfo = sessionStorage.getItem('voterInfo'); if (voterInfo) { const { name, idCard } = JSON.parse(voterInfo); voterName.value = name; voterIdCard.value = idCard; // 关键:立即清除存储防止数据残留 sessionStorage.removeItem('voterInfo'); } }); //获取候选人明细 const fetchCandidates = async () => { try { const response = await fetch('/api/wechat/getInvestigate', { method: 'POST', body: JSON.stringify({ id: '9', dcl: '123' }) }); const result = await response.json(); if (!result || !result.root) throw new Error('无效API响应'); // 提取候选人数据 const candidateArray = []; let idCounter = 1; // 自增计数器,名称序号 result.root.forEach(rootItem => { if (!rootItem.childEntList) return; rootItem.childEntList.forEach(candidate => { if (!candidate.dcbt || !candidate.dcxbt) return; candidateArray.push({ originalid: candidate.dcbt, name: candidate.dcxbt, vote: null, id:idCounter++, }); }); }); return candidateArray; } catch (error) { console.error('获取候选人失败:', error); return []; // 返回空数组保持安全 } }; // 投票统计 const votes = reactive({ A: 0, B: 0, C: 0 }); // 计算总票数 const totalVotes = computed(() => { return votes.A + votes.B + votes.C; }); // 投票方法(优化) const canVote = (voter, type) => { // 情况1:用户取消当前选择的类型(总是允许) if (voter.vote === type) return true; // 情况2:用户从其他类型转换到当前类型 if (voter.vote && voter.vote !== type) { if (type === 'A' && votes.A >= 5) return false; if (type === 'B' && votes.B >= 5) return false; if (type === 'C' && votes.C >= 15) return false; } // 情况3:用户首次投票 if (!voter.vote) { if (type === 'A' && votes.A >= 5) return false; if (type === 'B' && votes.B >= 5) return false; if (type === 'C' && votes.C >= 15) return false; if (totalVotes.value >= 25) return false; } return true; }; // 投票方法(要优化) const castVote = (voter, type) => { // 如果已投票且点击相同类型,取消投票 if (voter.vote === type) { voter.vote = null; votes[type]--; return; } // 如果之前有投票,先取消 if (voter.vote !== null) { votes[voter.vote]--; } // 投新票 voter.vote = type; votes[type]++; }; //投票人信息 // 添加投票人信息数据模型 const voterInfo = reactive({ name: '', idNumber: '' }); // // 添加基本信息验证 // const isValid = computed(() => { // return voterInfo.name.trim() !== '' && // voterInfo.idNumber.trim() !== '' && // /^\d{17}[\dXx]$/.test(voterInfo.idNumber); // }); // 提交投票 // 防止重复提交 const isSubmitting = ref(false); // 提交投票到API const submitVotes = async () => { // 防止重复提交 if (isSubmitting.value) return; isSubmitting.value = true; try { // 构建请求数据 const requestData = { voterInfo: { // 添加投票人信息字段 name: voterName.value, idCard: voterIdCard.value }, voteStatistics: { 经理: votes.A, 厂长: votes.B, 副厂长: votes.C }, voteDetails: voters.value .filter(voter => voter.vote !== null) .map(voter => ({ id: voter.id, name: voter.name, voteType: voter.vote === 'A' ? '经理' : voter.vote === 'B' ? '厂长' : '副厂长' })) }; // 发送POST请求 const response = await fetch('/api/wechat/getInvestigate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); alert('投票提交成功!'); // console.log('API响应:', result); // 提交成功后重置表单 // resetVotes(); // 存储已投票标识 localStorage.setItem('voted_' + voterIdCard.value, 'true'); } catch (error) { console.error('提交失败:', error); // 检查是否已投票误(假设后端返回409状态码) if (error.response?.status === 409) { alert('您已投过票,无法重复提交'); } else { alert('投票提交失败,请重试'); } } finally { isSubmitting.value = false; } }; // 重置投票 const resetVotes = () => { if (confirm('确定要重置所有投票吗?')) { voters.value.forEach(voter => { voter.vote = null; }); votes.A = 0; votes.B = 0; votes.C = 0; voterInfo.name = ''; voterInfo.idNumber = ''; } }; </script> <style scoped> /* 移动端垂直布局 */ @media (max-width: 480px) { .input-group { flex-direction: column; } } /* 平板/桌面端水平布局 */ @media (min-width: 768px) { .input-group { flex-direction: row; } } .vote-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .stats { display: flex; justify-content: space-between; margin-bottom: 30px; background: #f5f7fa; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .stat { flex: 1; text-align: center; padding: 0 15px; } .progress { height: 20px; background: #e0e0e0; border-radius: 10px; margin: 10px 0; overflow: hidden; } .progress-bar { height: 100%; background: #3498db; transition: width 0.3s; } .voters-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 20px; } .voter-card { background: white; border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s; } .voter-card:hover { transform: translateY(-5px); } .voter-id { color: #777; font-size: 0.9rem; margin-bottom: 15px; } .vote-options { display: flex; justify-content: space-between; } .vote-options button { flex: 1; margin: 0 5px; padding: 8px 0; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s; } .vote-options button:not(.selected):hover { opacity: 0.9; transform: scale(1.05); } .vote-options button:first-child { background: #ff6b6b; color: white; } .vote-options button:nth-child(2) { background: #4ecdc4; color: white; } .vote-options button:last-child { background: #ffd166; color: white; } .selected { border: 2px solid #2c3e50 !important; font-weight: bold; box-shadow: 0 0 2 rgba(61, 60, 60, 0.5); } .disabled { opacity: 0.5 !important; cursor: not-allowed !important; } .action-buttons { margin-top: 30px; display: flex; justify-content: center; gap: 20px; } .action-buttons button { padding: 12px 30px; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; font-weight: 600; transition: all 0.2s; } .action-buttons button:first-child { background: #3498db; color: white; } .action-buttons button:first-child:disabled { background: #bdc3c7; cursor: not-allowed; } .action-buttons button:last-child { background: #e74c3c; color: white; } .action-buttons button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } </style>这里有一个问题,就是我通过接口验证进入到投票界面后,我把该界面的浏览器地址发给其他浏览器或者其他人也会进入该页面进行投票
06-10
<!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 --> <template> <!-- 查询表单 --> <SearchForm v-show="isShowSearch" :search="_search" :reset="_reset" :columns="searchColumns" :search-param="searchParam" :search-col="searchCol" /> <!-- 表格主体 --> <div class="card table-main"> <!-- 表格头部 操作按钮 --> <div class="table-header"> <div class="header-button-lf"> <slot name="tableHeader" :selected-list="selectedList" :selected-list-ids="selectedListIds" :is-selected="isSelected" /> </div> <div v-if="toolButton" class="header-button-ri"> <slot name="toolButton"> <el-button v-if="showToolButton('refresh')" :icon="Refresh" circle @click="refreshData" /> <el-button v-if="showToolButton('setting') && columns.length" :icon="Operation" circle @click="openColSetting" /> <el-button v-if="showToolButton('search') && searchColumns?.length" :icon="Search" circle @click="isShowSearch = !isShowSearch" /> </slot> </div> </div> <!-- 表格上方的提示信息 --> <div v-if="tableTipsFlag"> <slot name="tableTips"></slot> </div> <!-- 表格主体 --> <el-table ref="tableRef" v-bind="$attrs" :data="processTableData" :border="border" :row-key="rowKey" @selection-change="selectionChange" > <!-- 默认插槽 --> <slot /> <template v-for="item in tableColumns" :key="item"> <!-- selection || radio || index || expand || sort --> <el-table-column v-if="item.type && columnTypes.includes(item.type)" v-bind="item" :align="item.align ?? 'center'" :reserve-selection="item.type == 'selection'" :selectable="item.isSelectable"> <template #default="scope"> <!-- expand --> <template v-if="item.type == 'expand'"> <component :is="item.render" v-bind="scope" v-if="item.render" /> <slot v-else :name="item.type" v-bind="scope" /> </template> <!-- radio --> <el-radio v-if="item.type == 'radio'" v-model="radio" :label="scope.row[rowKey]"> <!-- <i></i> --> </el-radio> <!-- sort --> <el-tag v-if="item.type == 'sort'" class="move"> <el-icon> <DCaret /></el-icon> </el-tag> </template> </el-table-column> <!-- other --> <TableColumn v-if="!item.type && item.prop && item.isShow" :column="item"> <template v-for="slot in Object.keys($slots)" #[slot]="scope"> <slot :name="slot" v-bind="scope" /> </template> </TableColumn> </template> <!-- 插入表格最后一行之后的插槽 --> <template #append> <slot name="append" /> </template> <!-- 无数据 --> <template #empty> <div class="table-empty"> <slot name="empty"> <img src="@/assets/images/notData.png" alt="notData" /> <div>暂无数据</div> </slot> </div> </template> </el-table> <!-- 分页组件 --> <slot name="pagination"> <Pagination v-if="pagination" :pageable="pageable" :handle-size-change="handleSizeChange" :handle-current-change="handleCurrentChange" /> </slot> </div> <!-- 列设置 --> <ColSetting v-if="toolButton" ref="colRef" v-model:col-setting="colSetting" /> </template> <script setup lang="ts" name="ProTable"> import { ref, watch, provide, onMounted, unref, computed, reactive } from "vue"; import { ElTable } from "element-plus"; import { useTable } from "@/hooks/useTable"; import { useSelection } from "@/hooks/useSelection"; import { BreakPoint } from "@/components/Grid/interface"; import { ColumnProps, TypeProps } from "@/components/ProTable/interface"; import { Refresh, Operation, Search } from "@element-plus/icons-vue"; import { handleProp } from "@/utils"; import SearchForm from "@/components/SearchForm/index.vue"; import Pagination from "./components/Pagination.vue"; import ColSetting from "./components/ColSetting.vue"; import TableColumn from "./components/TableColumn.vue"; import Sortable from "sortablejs"; export interface ProTableProps { columns: ColumnProps[]; // 列配置项 ==> 必传 data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传 requestApi?: (params: any) => Promise<any>; // 请求表格数据的 api ==> 非必传 requestAuto?: boolean; // 是否自动执行请求 api ==> 非必传(默认为true) requestError?: (params: any) => void; // 表格 api 请求误监听 ==> 非必传 dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理 ==> 非必传 title?: string; // 表格标题 ==> 非必传 pagination?: boolean; // 是否需要分页组件 ==> 非必传(默认为true) initParam?: any; // 初始化请求参数 ==> 非必传(默认为{}) border?: boolean; // 是否带有纵向边框 ==> 非必传(默认为true) toolButton?: ("refresh" | "setting" | "search")[] | boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true) rowKey?: string; // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ==> 非必传(默认为 id) searchCol?: number | Record<BreakPoint, number>; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } tableTipsFlag?: boolean; // 表格上方的提示信息 ==> 非必传 (默认为false) } // 接受父组件参数,配置默认值 const props = withDefaults(defineProps<ProTableProps>(), { columns: () => [], requestAuto: true, pagination: true, initParam: {}, border: true, toolButton: true, rowKey: "id", searchCol: () => ({ xs: 1, sm: 3, md: 3, lg: 4, xl: 5 }), tableTipsFlag: false }); // table 实例 const tableRef = ref<InstanceType<typeof ElTable>>(); // column 列类型 const columnTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"]; // 是否显示搜索模块 const isShowSearch = ref(true); // 控制 ToolButton 显示 const showToolButton = (key: "refresh" | "setting" | "search") => { return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton; }; // 单选值 const radio = ref(""); // 表格多选 Hooks const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey); // 表格操作 Hooks const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange } = useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback, props.requestError, tableRef); // 清空选中数据列表 const clearSelection = () => tableRef.value!.clearSelection(); // 初始化表格数据 && 拖拽排序 onMounted(() => { dragSort(); props.requestAuto && getTableList(); props.data && (pageable.value.total = props.data.length); }); // 处理表格数据 const processTableData = computed(() => { if (!props.data) return tableData.value; if (!props.pagination) return props.data; return props.data.slice( (pageable.value.pageNum - 1) * pageable.value.pageSize, pageable.value.pageSize * pageable.value.pageNum ); }); // 监听页面 initParam 改化,重新获取表格数据 watch(() => props.initParam, () => { // 将初始化initParam参数赋值给表单 searchParam.value = { ...searchParam.value, ...props.initParam, }; getTableList() }, { deep: true }); // 接收 columns 并设置为响应式 const tableColumns = reactive<ColumnProps[]>(props.columns); // 扁平化 columns const flatColumns = computed(() => flatColumnsFunc(tableColumns)); // 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择) const enumMap = ref(new Map<string, { [key: string]: any }[]>()); const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => { if (!enumValue) return; // 如果当前 enumMap 存在相同的值 return if (enumMap.value.has(prop!) && (typeof enumValue === "function" || enumMap.value.get(prop!) === enumValue)) return; // 当前 enum 为静态数据,则直接存储到 enumMap if (typeof enumValue !== "function") return enumMap.value.set(prop!, unref(enumValue!)); // 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储 enumMap.value.set(prop!, []); // 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap const { data } = await enumValue(); enumMap.value.set(prop!, data); }; const refreshData = () => { pageable.value.pageNum = 1; getTableList(); }; // 注入 enumMap provide("enumMap", enumMap); // 扁平化 columns 的方法 const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => { columns.forEach(async col => { if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children)); flatArr.push(col); // column 添加默认 isShow && isFilterEnum 属性值 col.isShow = col.isShow ?? true; col.isFilterEnum = col.isFilterEnum ?? true; // 设置 enumMap await setEnumMap(col); }); return flatArr.filter(item => !item._children?.length); }; // 过滤需要搜索的配置项 && 排序 const searchColumns = computed(() => { return flatColumns.value ?.filter(item => item.search?.el || item.search?.render) .sort((a, b) => a.search!.order! - b.search!.order!); }); // 设置 搜索表单默认排序 && 搜索表单项的默认值 searchColumns.value?.forEach((column, index) => { column.search!.order = column.search?.order ?? index + 2; const key = column.search?.key ?? handleProp(column.prop!); const defaultValue = column.search?.defaultValue; if (defaultValue !== undefined && defaultValue !== null) { searchInitParam.value[key] = defaultValue; searchParam.value[key] = defaultValue; } }); // 列设置 ==> 需要过滤掉不需要设置的列 const colRef = ref(); const colSetting = tableColumns!.filter(item => { const { type, prop, isShow } = item; return !columnTypes.includes(type!) && prop !== "operation" && isShow; }); const openColSetting = () => colRef.value.openColSetting(); // 定义 emit 事件 const emit = defineEmits<{ search: []; reset: []; dargSort: [{ newIndex?: number; oldIndex?: number }]; }>(); const _search = () => { search(); emit("search"); }; const _reset = () => { reset(); emit("reset"); }; // 拖拽排序 const dragSort = () => { const tbody = document.querySelector(".el-table__body-wrapper tbody") as HTMLElement; Sortable.create(tbody, { handle: ".move", animation: 300, onEnd({ newIndex, oldIndex }) { const [removedItem] = processTableData.value.splice(oldIndex!, 1); processTableData.value.splice(newIndex!, 0, removedItem); emit("dargSort", { newIndex, oldIndex }); } }); }; // 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去) defineExpose({ element: tableRef, tableData: processTableData, radio, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange, clearSelection, enumMap, isSelected, selectedList, selectedListIds, refreshData: getTableList // 暴露刷新方法 }); </script> import { ref, computed } from "vue"; /** * @description 表格多选数据操作 * @param {String} rowKey 当表格可以多选时,所指定的 id * */ export const useSelection = (rowKey: string = "id") => { const isSelected = ref<boolean>(false); const selectedList = ref<{ [key: string]: any }[]>([]); // 当前选中的所有 ids 数组 const selectedListIds = computed((): string[] => { let ids: string[] = []; selectedList.value.forEach(item => ids.push(item[rowKey])); return ids; }); /** * @description 多选操作 * @param {Array} rowArr 当前选择的所有数据 * @return void */ const selectionChange = (rowArr: { [key: string]: any }[]) => { rowArr.length ? (isSelected.value = true) : (isSelected.value = false); selectedList.value = rowArr; }; return { isSelected, selectedList, selectedListIds, selectionChange }; }; <template> <div class="table-box"> <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback"> <!-- 表格 header 按钮 --> <template #tableHeader="scope"> <el-button type="primary" v-if="hasBtnPermission('asset:inventory:save')" @click="addNewData('新增资产','add',{})">新增资产</el-button> <el-button type="primary" v-if="hasBtnPermission('asset:inventory:update')" @click="batcEdit(scope.selectedListIds)">批量编辑</el-button> <el-button type="primary" v-if="hasBtnPermission('asset:inventory:delete')" @click="batchDelete(scope.selectedListIds)">批量删除</el-button> <el-dropdown style="margin-left: 10px" v-if="hasBtnPermission('asset:inventory:downloadData')" @command="batchExport"> <el-button type="primary"> 批量导出<i class="el-icon-arrow-down el-icon--right"></i> </el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="1">导出所选数据</el-dropdown-item> <el-dropdown-item command="2">导出全部</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-button type="primary" @click="openTagPrint(scope.selectedListIds)" v-if="hasBtnPermission('asset:inventory:printMark')" style="margin-left: 10px">打印资产标签</el-button> </template> <el-table-column type="selection" width="55"></el-table-column> <!-- 图片 --> <el-table-column prop="imageUrl" label="图片" minWidth="100px" class-name="is-center"> <template #default="{ row }"> <div class="more_imgs" v-if="row.imageUrlList && row.imageUrlList.length > 0"> <viewer :images="row.imageUrlList"> <span class="viewImage" v-for="(itemImg, index) in row.imageUrlList" :key="index"> <img v-if="itemImg" :src="itemImg" style="width: 100%; height: 100%" /> </span> </viewer> </div> </template> </el-table-column> <template #expand="scope"> {{ scope.row }} </template> <template #operation="scope" v-if="hasBtnPermission('asset:inventory:update')" > <el-button type="primary" link @click="editData('编辑资产','edit', scope.row)">编辑</el-button> </template> </ProTable> <!-- 选择新增资产类型 --> <div class="new-Dialog-type" v-if="dialogFormVisible"> <el-dialog v-model="dialogFormVisible" title="选择资产类型" width="450" draggable > <el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="93px"> <el-form-item label="类型" prop="type"> <el-select v-model="form.type" placeholder="请选择"> <el-option v-for="item in assetType" :key="item.value" :label="item.label" :value="item.value"></el-option> </el-select> </el-form-item> <el-form-item label="非标准资产" v-if="form.type === 2" prop="nonStandardAssetsId"> <el-select v-model="form.nonStandardAssetsId" placeholder="请选择"> <el-option v-for="item in nonstandardData" :key="item.id" :label="item.name" :value="item.id" :disabled="item.status == 0" ></el-option> </el-select> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="closeDialog">取消</el-button> <el-button type="primary" @click="nextTips('新增资产','add',{})">下一步</el-button> </div> </template> </el-dialog> </div> <!-- 编辑,批量编辑,新增组件 --> <addAsset ref="addAssetRef" @previous-step="handlePreviousStep"/> <!-- 详情公共组件 --> <assetInfo ref="assetInfoRef"></assetInfo> </div> </template> <script setup lang="tsx" name="assetInventory"> import { ref, reactive, inject, onMounted,nextTick } from "vue"; import ProTable from "@/components/ProTable/index.vue"; import { assetListData ,queryGroupList,addAssetList,deleteAssetList,assetListDown,editBatchAssetList,assetListInfo,editAssetList} from "@/api/modules/assetAllocation"; import { ProTableInstance } from "@/components/ProTable/interface"; import { setTreeData,formatToTree } from "@/utils/tools"; import { assetClassificationList,getPositionList,setValueClildList,nonstandardList,getOrgSubjectList,getInstitution } from '@/api/modules/public'; import { getUserDepartment } from "@/api/modules/user"; import { assetListType,assetType,markStatusType } from "@/utils/dict"; import { da, fa, pa } from "element-plus/es/locale"; import addAsset from "./mode/addAsset.vue"; import assetInfo from "./mode/assetInfo.vue"; import { useHandleData } from "@/hooks/useHandleData"; import { ElMessage, FormInstance } from "element-plus"; import { printAssetMark } from "@/api/modules/assetAllocation"; import moment from "moment"; const hasBtnPermission: any = inject('hasBtnPermission'); // ProTable 实例 const proTable = ref<ProTableInstance>(); // 子界面需要用到的设置值 const configuration = reactive({ assetCategory: [], // 资产分类 positionList: [], // 存放地点 departmentList: [], // 使用部门 sourceList: [], // 资产来源 nonstandardList: [], // 非标准资产 unitList:[], // 计量单位 institutionalEntity:[], //机构主体 assentityList:[], // 主体 assinstitutional:[],//机构 }) const selectedIds = ref<string[]>([]); // 在组件顶部定义 // 非标准资产 const nonstandardData = ref([]); const assentityList = ref([]) const assinstitutional = ref([]) // 资产类型弹窗 const dialogFormVisible = ref(false) const form = reactive({ type:'', nonStandardAssetsId:'' }) const rules = reactive({ type: [{ required: true, message: "请选择资产类型", trigger: ["blur", "change"] }], nonStandardAssetsId: [{ required: true, message: "请选择非标准资产", trigger: ["blur", "change"] }], }) const getTableList = (params: any) => { if (params.purchaseDate) { params.purchaseDateStart = params.purchaseDate[0] + ' 00:00:00'; params.purchaseDateEnd= params.purchaseDate[1] + ' 23:59:59'; delete params.purchaseDate; } if (params.maintenanceExpirationDate) { params.maintenanceExpirationDateStart = params.maintenanceExpirationDate[0] + ' 00:00:00'; params.maintenanceExpirationDateEnd = params.maintenanceExpirationDate[1] + ' 23:59:59'; delete params.maintenanceExpirationDate; } if(params.useUserNameData) { params.useUserNameList = params.useUserNameData.split('\n'); delete params.useUserNameData } if(params.assetCodeData) { params.assetCodeList = params.assetCodeData.split('\n'); delete params.assetCodeData } return assetListData(params) } const refreshTable = () => { proTable.value!.pageable.pageNum = 1; proTable.value?.refreshData(); // 清除表格勾选项 if (proTable.value) { proTable.value.clearSelection(); } }; const dataCallback = (data: any) => { const dataList = data?.dataList || []; const processedList = dataList.map(item => { try { return { ...item, imageUrlList: typeof item?.imageUrl === 'string' ? item.imageUrl.split(',') : [], purchaseDate: item.purchaseDate ? item.purchaseDate.split(" ")[0] : '' }; } catch (error) { return { ...item, imageUrlList: [] }; } }); return { list: processedList, total: data?.totalCount, pageNum: data?.page, pageSize: data?.pageSize }; }; // 查询计量单位 const getUnitList = async () => { const response = await setValueClildList({dictCode: 'UNIT_MEASUREMENT'}); const data = response.data || []; configuration.unitList = data } // 查询机构主体 const getOrgSubjectListData = async() => { // 机构 const responseAss = await getInstitution({ id: 'ASS_INSTITUTIONAL' }); const data = responseAss.data || []; if(data && data.length > 0) { configuration.assinstitutional = data assinstitutional.value = data } // 主体 const response = await getInstitution({ id: 'OFFICIAL_SEAL_ORG' }); const data1 = response.data || []; if(data1 && data1.length > 0) { configuration.assentityList = data1 assentityList.value = data1 } // 机构主体(二和一接口),用来把主体,机构以及部门三者关联起来,单独调用上面接口,主要是为了排序好看,无语子...... const res = await getOrgSubjectList({}); const data2 = res.data || []; if(data && data.length > 0) { configuration.institutionalEntity = data2 } } const formatToDepTree = (arr, pid = 0) =>{ let result = []; for (let i = 0; i < arr.length; i++) { if (arr[i].pid === pid) { arr[i].label = arr[i].name let children = formatToDepTree(arr, arr[i].workOADepartmentId); if (children.length > 0) { arr[i].children = children; } result.push(arr[i]); } } return result; } // 表格配置项 const columns = reactive([ { prop: "assetStatus", label: "资产状态", fixed: "left", minWidth:100 , enum: assetListType, search: { el: "select", props: { filterable: true } }, render: scope => { if (scope.row.assetStatus == '1') { return ( <span style="color: #49c625">空闲</span> ); } else if (scope.row.assetStatus == '2') { return ( <span style="color: #ff7f00">在用</span> ); } else if (scope.row.assetStatus == '3') { return ( <span style="color: #1890ff">已处置</span> ); } } }, { prop: "markStatus", label: "资产标记", isShow:false, enum:markStatusType, search: { el: "select", props: { filterable: true } }, fieldNames: { label: "label", value: "value" } }, { prop: "markTagsName", label: "资产标记", fixed: "left", minWidth:100 , render: scope => { if (scope.row.markTags == '0') { return ( <span style="color: #49c625">派发待领用</span> ); } else if (scope.row.markTags == '1') { return ( <span style="color: #ff7f00">领用审批中</span> ); } else if (scope.row.markTags == '2') { return ( <span style="color: #ff7f00">退还审批中</span> ); } else if (scope.row.markTags == '3') { return ( <span style="color: #ff7f00">借用审批中</span> ); } else if (scope.row.markTags == '4') { return ( <span style="color: #1890ff">借用</span> ); } else if (scope.row.markTags == '5') { return ( <span style="color: #ff7f00">调拨审批中</span> ); } else if (scope.row.markTags == '6') { return ( <span style="color: #ff7f00">维修审批中</span> ); } else if (scope.row.markTags == '7') { return ( <span style="color: #ff7f00">处置审批中</span> ); } else if (scope.row.markTags == '8') { return ( <span style="color: #ff0000">待处理</span> ); } else if (scope.row.markTags == '9') { return ( <span style="color: #ff7f00">归还审批中</span> ); } } }, { prop: "assetCodeData", label: "资产编码", isShow:false, search: { el: "input", type: 'textarea', placeholder: '多个编码请换行' } , minWidth:100, }, { prop: "assetCode", label: "资产编码", fixed: "left", minWidth:100, render: (scope) => { return ( <span style="color: #49c625" onClick={() => handleAssetCodeClick(scope.row)}> {scope.row.assetCode} </span> ); } }, { prop: "assetName", label: "资产名称", fixed: "left", search: { el: "input" },minWidth:100 }, { prop: "assetCategoryName", label: "资产分类", minWidth:100 , }, { prop: "assetCategoryIdList", label: "资产分类", isShow:false, enum: async () => { // 获取资产分类数据,扁平数据 const { data } = await assetClassificationList({}); const treeData = formatToTree(data); configuration.assetCategory = treeData; return { data: treeData } }, fieldNames: { label: "categoryName", value: "id" }, search: { el: "tree-select", props: { filterable: true, multiple: true, checkStrictly: true, // 允许选择父节点 nodeKey: "id", // 每个节点的唯一标识字段 props: { label: "label", value: "id", children: "children" } } }, }, { prop: "nonStandardAssetsId", label: "资产类型", minWidth:100 , isShow:false, }, { prop: "type", label: "资产类型", minWidth:100 ,enum: assetType, search: { el: "select", props: { filterable: true } },}, { prop: "useUserName", label: "使用人" }, { prop: "useUserNameData", label: "使用人", isShow:false, search: { el: "input", type: 'textarea', placeholder: '多个使用人请换行' } }, { prop: "useOrgIdList", label: "使用机构", search: { el: "select", props: { filterable: true, multiple: true}}, minWidth:100 , enum: assinstitutional, isShow:false, fieldNames: { label: "name", value: "detailCode" }, }, { prop: "useOrgName", label: "使用机构", minWidth:100 , }, { prop: "useSubjectId", label: "使用主体", search: { el: "select" } , minWidth:100 , enum: assentityList, isShow:false, fieldNames: { label: "remarks", value: "detailCode" }, }, { prop: "useSubjectName", label: "使用主体", minWidth:100 , }, { prop: "useDepartmentId", label: "使用部门", minWidth:100 , enum: async () => { // 获取部门数据 const { data } = await getUserDepartment(); data.forEach(item => { item.pid = item.extMap.parentWorkOADepartmentId item.workOADepartmentId = item.value item.id = item.value item.name = item.label }) const treeData = formatToDepTree(data); configuration.departmentList = treeData return { data: treeData } }, fieldNames: { label: "label", value: "value" }, search: { el: "tree-select", props: { filterable: true, multiple: true, checkStrictly: true, // 允许选择父节点 nodeKey: "value", // 每个节点的唯一标识字段 props: { label: "label", value: "value", children: "children" } } } }, { prop: "useDepartmentName", label: "使用部门", isShow:false, minWidth:100 , }, { prop: "storageLocationIdList", label: "存放地点", minWidth:100 , isShow:false, enum: async () => { // 获取存放地点 const { data } = await getPositionList({ pageNum: 1, pageSize: 9999 }); const deepCopy = JSON.parse(JSON.stringify(data['dataList'])); configuration.positionList = deepCopy; return { data: data['dataList'] }; }, fieldNames: { label: "position", value: "id" }, search: { el: "select", props: { filterable: true , multiple: true} }, }, { prop: "storageLocationName", label: "存放地点", minWidth:100 , }, { prop: "adminName", label: "管理员", search: { el: "input" } }, { prop: "affiliatedInstitutionName", label: "所属机构",minWidth:100 }, { prop: "affiliatedInstitutionIdList", label: "所属机构", isShow:false, search: { el: "select" , props: { filterable: true , multiple: true}},minWidth:100 ,enum: assinstitutional, fieldNames: { label: "name", value: "detailCode" }, }, { prop: "affiliatedSubjectName", label: "所属主体",minWidth:100 }, { prop: "affiliatedSubjectId", label: "所属主体", isShow:false, search: { el: "select" },minWidth:100,enum: assentityList, fieldNames: { label: "remarks", value: "detailCode" } }, { prop: "assetSourceTypeList", label: "资产来源", isShow:false, enum: async () => { // 获取资产来源 const data = await setValueClildList({dictCode:'SOURCE_ASSETS'}); configuration.sourceList = data['data'] return { data: data['data'] } }, fieldNames: { label: "itemLabel", value: "itemValue" }, search: { el: "select", props: { filterable: true , multiple: true}}, }, { prop: "sourceCode", label: "资产来源", minWidth:100 , }, { prop: "brand", label: "品牌", search: { el: "input" }, }, { prop: "specificationModel", label: "规格型号", search: { el: "input" },minWidth:100 }, { prop: "serialNumber", label: "序列号", search: { el: "input" } }, { prop: "measurementUnit", label: "计量单位",minWidth:100 }, { prop: "remarks", label: "备注",search: { el: "input" } }, { prop: "supplierName", label: "供应商", search: { el: "input" }, }, { prop: "inBoundNo", label: "入库单号",minWidth:100 }, { prop: "nonStandardAssetsId", label: "非标准资产", enum: async () => { // 获取非标准资产 const data = await nonstandardList({}); nonstandardData.value = data configuration.nonstandardList = data return { data: data } }, isShow: false, fieldNames: { label: "name", value: "id" }, search: { el: "select", props: { filterable: true } }, }, { prop: "purchaseDate", label: "购入日期", minWidth:148 , search: { el: "date-picker", span: 2, props: { type: "daterange", valueFormat: "YYYY-MM-DD" }, }, }, { prop: "maintenanceExpirationDate", label: "维保到期日期", isShow: false, search: { el: "date-picker", span: 2, props: { type: "daterange", valueFormat: "YYYY-MM-DD" }, }, }, { prop: "operation", label: "操作",fixed: "right", isShow: true, sortable: false } ]) // 批量导出 const batchExport = async (command: any) => { try { const selectedIds = proTable.value?.selectedListIds || []; // 验证选择(如果command不是2,则需要选择数据) if (command != 2 && selectedIds.length === 0) { ElMessage.error({ message: `请选择要操作的数据` }); return; } const params = { idList: command === 2 ? [] : selectedIds // command=2表示导出全部 }; // 1. 获取文件数据(确保response是Blob或ArrayBuffer) const response = await assetListDown(params); // 2. 检查响应数据是否有效 if (!response) { ElMessage.error("导出失败:未获取到文件数据"); return; } // 3. 创建Blob对象(明确指定MIME类型) const blob = new Blob([response], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8" }); // 4. 创建下载链接 const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `资产清单_${new Date().toLocaleDateString()}.xlsx`; // 添加日期避免重复 // 5. 触发下载 document.body.appendChild(link); link.click(); // 6. 清理 setTimeout(() => { document.body.removeChild(link); window.URL.revokeObjectURL(url); }, 100); } catch (error) { console.error("导出失败:", error); } }; // 批量删除 const batchDelete = async (ids: string[]) => { if(ids && ids.length === 0) { ElMessage.error({ message: `请选择要操作的数据` }); return } await useHandleData(deleteAssetList, { idList: ids }, `确认删除`); refreshTable() } // 批量编辑 const batcEdit = async (ids: string[]) => { if (ids && ids.length === 0) { ElMessage.error({ message: `请选择要操作的数据` }); return; } // 从表格中获取当前所有选中的行数据 const selectedRows = proTable.value?.selectedList || []; const types = selectedRows.map(row => row.type); const uniqueTypes = [...new Set(types)]; if (uniqueTypes.length > 1) { ElMessage.warning("只能选择相同类型的资产进行批量编辑"); return; } selectedIds.value = ids; editBatchData('批量编辑', 'batchEdit', {}); } // 打印标签 const openTagPrint = async (ids: string[]) => { if(ids && ids.length === 0) { ElMessage.error({ message: `请选择要操作的数据` }); return } const data = await printAssetMark({ idList: ids }); if (data.code == 0) { ElMessage.success({ message: data.msg }); refreshTable() } else { ElMessage.error({ message: data.msg }); } } const closeDialog = () => { dialogFormVisible.value = false // 清除表格勾选项 if (proTable.value) { proTable.value.clearSelection(); } } // 子组件的上一步操作 const handlePreviousStep = () => { dialogFormVisible.value = true; // 重新打开对话框 // if( Type.value == 'batchEdit') {} // 回显之前选择的数据(form 已在 openDrawer 时保存) nextTick(() => { ruleFormRef.value?.clearValidate(); // 清除校验状态 }); proTable.value!.setCheckedRows(proTable.value?.selectedList); // console.log('proTable.value',proTable.value?.selectedList) // proTable.value!.toggleRowSelection(proTable.value?.selectedList[0]) // rows.forEach((row) => { // multipleTableRef.value!.toggleRowSelection( // row, // undefined, // ignoreSelectable // ) // }) }; const Title = ref(""); const Type = ref('add') const Row = ref({}) // 新增 const addNewData = (title: string,type:string, row: any = {}) => { Title.value = title Type.value = type Row.value = row // 清空表单值 form.type = ''; form.nonStandardAssetsId = ''; // 重置表单校验状态 nextTick(() => { ruleFormRef.value?.resetFields(); }); dialogFormVisible.value = true } // 编辑 const editData = async(title: string,type:string, row: any = {}) => { const {code , data ,msg} = await assetListInfo({ id: row.id }); if(code == 0) { form.type = row.type form.nonStandardAssetsId = '' let listData = [data] Title.value = title Type.value = type Row.value = listData openDrawer() } else { ElMessage.error(msg); } } // 批量编辑 const editBatchData = (title: string,type:string, row: any = {}) => { Title.value = title Type.value = type Row.value = row // 清空表单值 form.type = ''; form.nonStandardAssetsId = ''; // 重置表单校验状态 nextTick(() => { ruleFormRef.value?.resetFields(); }); dialogFormVisible.value = true } // 查看详情 const assetInfoRef = ref<InstanceType<typeof addAsset> | null>(null); const handleAssetCodeClick = async(row: any) => { const params = { row:{...row}, api:deleteAssetList, configuration:configuration, refreshTable: () => { proTable.value!.pageable.pageNum = 1; proTable.value?.refreshData(); } } assetInfoRef.value?.acceptParams(params) } // 下一步 const nextTips = () => { ruleFormRef.value!.validate(async valid => { if (!valid) return; try { openDrawer() } catch (error) { console.log(error); } }) } // 新增/编辑 const ruleFormRef = ref<FormInstance>(); const addAssetRef = ref<InstanceType<typeof addAsset> | null>(null); const openDialog = () => { // 清空表单值 form.type = ''; form.nonStandardAssetsId = ''; // 重置表单校验状态 nextTick(() => { ruleFormRef.value?.resetFields(); }); dialogFormVisible.value = true } const openDrawer = () => { if(Type.value === 'add') { dialogFormVisible.value = false const params = { title:Title.value, type:Type.value, row:{...Row.value}, form:{...form}, configuration:configuration, isView:false, api: addAssetList, refreshTable: () => { proTable.value!.pageable.pageNum = 1; proTable.value?.refreshData(); } } addAssetRef.value?.acceptParams(params) } else if(Type.value === 'edit'){ const params = { title:Title.value, type:Type.value, configuration:configuration, isView:false, row:{}, form:{...form}, infoRow:{...Row.value}, api: editAssetList, refreshTable: () => { proTable.value!.pageable.pageNum = 1; proTable.value?.refreshData(); } } addAssetRef.value?.acceptParams(params) } else { dialogFormVisible.value = false const params = { title:Title.value, type:Type.value, configuration:configuration, isView:false, form:{...form}, row:{selectedIds:selectedIds.value}, api: editBatchAssetList, refreshTable: () => { proTable.value!.pageable.pageNum = 1; proTable.value?.refreshData(); } } addAssetRef.value?.acceptParams(params) } } onMounted(() => { getUnitList(); getOrgSubjectListData(); }) </script> <style lang="scss" scoped> .more_imgs{ div { display: inline-flex; } .viewImage{ width: 25px; height: 25px; cursor: pointer; } } /* 或仅针对特定列 */ .el-table .el-table__header th.is-center .cell { text-align: center; } /* 确保选择列可见 */ ::v-deep .el-table__fixed-left { .el-table__cell.is-hidden > * { visibility: visible !important; } .el-checkbox { display: inline-block; } } </style>点击handlePreviousStep后,回显勾选的数据
08-26
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值