| | |
| | | }); |
| | | } |
| | | |
| | | // 用印申请详情(按审批流程id) |
| | | export function getSealDetail(approveProcessId) { |
| | | return request({ |
| | | url: "/sealApplicationManagement/detail", |
| | | method: "get", |
| | | params: { approveProcessId }, |
| | | }); |
| | | } |
| | | |
| | | // 新增印章申请 |
| | | export function addSealApplication(data) { |
| | | return request({ |
| | |
| | | }); |
| | | } |
| | | |
| | | // 报工弹窗数据查询(返回默认班组成员 reportPersons) |
| | | export function getProductWorkOrderById(id) { |
| | | return request({ |
| | | url: "/productWorkOrder/getProductWorkOrderById", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // 下载工单流转卡(返回文件流) |
| | | export function downProductWorkOrder(id) { |
| | | return request({ |
| | |
| | | <div class="header-info"> |
| | | <span class="type-name">{{ currentApproveTypeName }}</span> |
| | | <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light"> |
| | | {{ approverList.length > 0 ? `已配置 ${approverList.length} 个审批人` : '未配置审批人' }} |
| | | {{ approverCountText }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | |
| | | class="flow-item" |
| | | > |
| | | <!-- 审批节点卡片 --> |
| | | <div class="node-card" :class="{ 'empty': !item.approverId }"> |
| | | <div class="node-card" :class="{ 'empty': !(item.approverIds && item.approverIds.length) }"> |
| | | <!-- 顶部序号和级别 --> |
| | | <div class="node-badge">{{ index + 1 }}</div> |
| | | |
| | | |
| | | <!-- 头像区域 --> |
| | | <div class="node-avatar-section"> |
| | | <div |
| | | class="node-avatar" |
| | | :class="{ 'has-user': item.approverId }" |
| | | :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}" |
| | | <div |
| | | class="node-avatar" |
| | | :class="{ 'has-user': item.approverIds && item.approverIds.length }" |
| | | :style="item.approverIds && item.approverIds.length ? { backgroundColor: getAvatarColor(item.approverNames?.[0] || '') } : {}" |
| | | > |
| | | <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span> |
| | | <span v-if="item.approverIds && item.approverIds.length">{{ (item.approverNames?.[0] || '?').charAt(0) }}</span> |
| | | <el-icon v-else :size="24"><User /></el-icon> |
| | | </div> |
| | | <div class="node-level">{{ getLevelText(index) }}</div> |
| | |
| | | <!-- 选择区域 --> |
| | | <div class="node-select-section"> |
| | | <el-select |
| | | v-model="item.approverId" |
| | | placeholder="选择审批人" |
| | | v-model="item.approverIds" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | placeholder="选择审批人(可多选)" |
| | | filterable |
| | | size="default" |
| | | @change="(val) => handleApproverChange(val, item)" |
| | |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div class="node-mode"> |
| | | <span class="mode-label">审批方式</span> |
| | | <el-radio-group v-model="item.approveMode" size="small"> |
| | | <el-radio-button value="or">或签</el-radio-button> |
| | | <el-radio-button value="and">会签</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 操作按钮 --> |
| | |
| | | <div class="add-icon-wrapper"> |
| | | <el-icon :size="28"><Plus /></el-icon> |
| | | </div> |
| | | <span class="add-text">新增审批人</span> |
| | | <span class="add-text">新增审批节点</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="empty-subtext">点击下方按钮添加第一个审批人</div> |
| | | <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | 新增审批人 |
| | | 新增审批节点 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | |
| | | <!-- 底部提示 --> |
| | | <div class="bottom-tips"> |
| | | <el-icon><InfoFilled /></el-icon> |
| | | <span>提示:每个流程至少配置一个审批人,审批按顺序流转,可通过箭头调整顺序</span> |
| | | <span>提示:每个节点可配置多名审批人并选择或签/会签;或签=任意一人通过即流转,会签=需全部人通过;审批按节点顺序流转</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | import { |
| | | Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting, |
| | | Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked, |
| | | Van, ArrowRight, User, InfoFilled |
| | | Van, ArrowRight, User, InfoFilled, Stamp, WarningFilled |
| | | } from '@element-plus/icons-vue'; |
| | | import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement'; |
| | | import { userListNoPage } from '@/api/system/user'; |
| | |
| | | { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' }, |
| | | { value: '8', label: '危险作业审批', icon: 'WarningFilled', color: '#E74C3C' }, |
| | | { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' }, |
| | | ]; |
| | | |
| | | // 审批类型名称映射 |
| | |
| | | 5: '采购审批', |
| | | 6: '报价审批', |
| | | 7: '发货审批', |
| | | 8: '危险作业审批', |
| | | 9: '用印审批', |
| | | }; |
| | | |
| | | // 审批类型图标映射 |
| | |
| | | 5: 'ShoppingCart', |
| | | 6: 'DocumentChecked', |
| | | 7: 'Van', |
| | | 8: 'WarningFilled', |
| | | 9: 'Stamp', |
| | | }; |
| | | |
| | | // 审批类型颜色映射 |
| | |
| | | 5: '#909399', |
| | | 6: '#9B59B6', |
| | | 7: '#1ABC9C', |
| | | 8: '#E74C3C', |
| | | 9: '#7D3C98', |
| | | }; |
| | | |
| | | // 头像颜色池 |
| | |
| | | // 当前审批类型 |
| | | const currentApproveType = computed(() => { |
| | | return Number(activeTab.value); |
| | | }); |
| | | |
| | | // 头部统计文案:节点数 / 审批人数 |
| | | const approverCountText = computed(() => { |
| | | const nodes = approverList.value.length; |
| | | const users = approverList.value.reduce((sum, item) => sum + (item.approverIds ? item.approverIds.length : 0), 0); |
| | | if (nodes === 0) return '未配置审批人'; |
| | | return `已配置 ${nodes} 节点 / ${users} 审批人`; |
| | | }); |
| | | |
| | | // 获取类型图标 |
| | |
| | | loadData(); |
| | | }; |
| | | |
| | | // 加载审批配置数据(模拟) |
| | | // 加载审批配置数据:接口扁平行 → 按 nodeOrder 分组合并为节点卡片(同节点多审批人) |
| | | const loadData = async () => { |
| | | loading.value = true; |
| | | try { |
| | |
| | | : Array.isArray(res?.data?.records) |
| | | ? res.data.records |
| | | : []; |
| | | const data = source.map((item, index) => ({ |
| | | ...item, |
| | | sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1, |
| | | })); |
| | | approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); |
| | | originalList.value = JSON.parse(JSON.stringify(approverList.value)); |
| | | const map = new Map(); |
| | | source.forEach(item => { |
| | | const order = item.nodeOrder ?? 1; |
| | | if (!map.has(order)) { |
| | | map.set(order, { nodeOrder: order, approverIds: [], approverNames: [], approveMode: item.approveMode || 'or' }); |
| | | } |
| | | const card = map.get(order); |
| | | card.approverIds.push(item.approverId); |
| | | if (item.approverName) card.approverNames.push(item.approverName); |
| | | }); |
| | | const cards = [...map.values()].sort((a, b) => a.nodeOrder - b.nodeOrder); |
| | | approverList.value = cards; |
| | | originalList.value = JSON.parse(JSON.stringify(cards)); |
| | | } catch (error) { |
| | | approverList.value = []; |
| | | originalList.value = []; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 审批人选择变化 |
| | | const handleApproverChange = (userId, row) => { |
| | | const user = userList.value.find((u) => u.userId === userId); |
| | | if (user) { |
| | | row.approverName = user.nickName; |
| | | } |
| | | // 审批人选择变化(多选) |
| | | const handleApproverChange = (ids, row) => { |
| | | row.approverNames = (ids || []).map(id => { |
| | | const user = userList.value.find((u) => u.userId === id); |
| | | return user ? user.nickName : ''; |
| | | }).filter(Boolean); |
| | | }; |
| | | |
| | | // 新增审批人 |
| | | // 新增审批节点 |
| | | const handleAdd = () => { |
| | | const newOrder = approverList.value.length + 1; |
| | | approverList.value.push({ |
| | | id: null, |
| | | approveType: currentApproveType.value, |
| | | approverId: null, |
| | | approverName: '', |
| | | sortOrder: newOrder, |
| | | nodeOrder: newOrder, |
| | | approverIds: [], |
| | | approverNames: [], |
| | | approveMode: 'or', |
| | | }); |
| | | }; |
| | | |
| | | // 删除审批人 |
| | | // 删除审批节点 |
| | | const handleDelete = (index) => { |
| | | ElMessageBox.confirm('确定删除该审批人吗?', '提示', { |
| | | ElMessageBox.confirm('确定删除该审批节点吗?', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning', |
| | |
| | | .then(() => { |
| | | approverList.value.splice(index, 1); |
| | | approverList.value.forEach((item, idx) => { |
| | | item.sortOrder = idx + 1; |
| | | item.nodeOrder = idx + 1; |
| | | }); |
| | | ElMessage.success('删除成功'); |
| | | }) |
| | |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index - 1]; |
| | | approverList.value[index - 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index - 1].sortOrder = index; |
| | | approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; }); |
| | | }; |
| | | |
| | | // 后移 |
| | |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index + 1]; |
| | | approverList.value[index + 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index + 1].sortOrder = index + 2; |
| | | approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; }); |
| | | }; |
| | | |
| | | // 保存配置 |
| | | // 保存配置:节点卡片按审批人展开为多行 payload |
| | | const handleSave = async () => { |
| | | if (approverList.value.length === 0) { |
| | | ElMessage.warning('请至少配置一个审批人'); |
| | | ElMessage.warning('请至少配置一个审批节点'); |
| | | return; |
| | | } |
| | | |
| | | const hasEmptyApprover = approverList.value.some((item) => !item.approverId); |
| | | const hasEmptyApprover = approverList.value.some((item) => !item.approverIds || item.approverIds.length === 0); |
| | | if (hasEmptyApprover) { |
| | | ElMessage.warning('请选择所有审批人'); |
| | | return; |
| | | } |
| | | |
| | | const approverIds = approverList.value.map((item) => item.approverId); |
| | | const uniqueIds = [...new Set(approverIds)]; |
| | | if (uniqueIds.length !== approverIds.length) { |
| | | ElMessage.warning('审批人不能重复'); |
| | | ElMessage.warning('请为每个节点选择审批人'); |
| | | return; |
| | | } |
| | | |
| | | saveLoading.value = true; |
| | | try { |
| | | const payload = approverList.value.map((item, index) => ({ |
| | | approveType: currentApproveType.value, |
| | | nodeOrder: index + 1, |
| | | approverId: item.approverId, |
| | | approverName: item.approverName, |
| | | })); |
| | | const payload = []; |
| | | approverList.value.forEach((card) => { |
| | | const uniqueIds = [...new Set(card.approverIds || [])]; |
| | | uniqueIds.forEach(id => { |
| | | const user = userList.value.find(u => u.userId === id); |
| | | payload.push({ |
| | | approveType: currentApproveType.value, |
| | | nodeOrder: card.nodeOrder, |
| | | approverId: id, |
| | | approverName: user ? user.nickName : '', |
| | | approveMode: card.approveMode || 'or', |
| | | }); |
| | | }); |
| | | }); |
| | | await addApproveProcessConfigNode(payload); |
| | | ElMessage.success('保存成功'); |
| | | await loadData(); |
| | |
| | | width: 100%; |
| | | } |
| | | |
| | | .node-mode { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-top: 10px; |
| | | padding-top: 10px; |
| | | border-top: 1px dashed var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .mode-label { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | justify-content: center; |
| | |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="申请部门:"> |
| | | <el-select |
| | | disabled |
| | | v-model="form.approveDeptId" |
| | | placeholder="选择部门" |
| | | > |
| | | <el-option |
| | | v-for="user in productOptions" |
| | | :key="user.deptId" |
| | | :label="user.deptName" |
| | | :value="user.deptId" |
| | | /> |
| | | </el-select> |
| | | <el-input v-model="form.approveDeptName" placeholder="申请部门" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <!-- 用印审批:展示用印申请详情 --> |
| | | <div v-if="isSealApproval" style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">用印详情</el-divider> |
| | | <el-skeleton :loading="sealLoading" animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" style="width: 30%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentSeal || !currentSeal.applicationNum" description="未查询到对应用印申请详情" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="申请编号">{{ currentSeal.applicationNum }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请标题">{{ currentSeal.title }}</el-descriptions-item> |
| | | <el-descriptions-item label="用印类型">{{ getSealTypeText(currentSeal.sealType) }}</el-descriptions-item> |
| | | <el-descriptions-item label="紧急程度">{{ getUrgencyText(currentSeal.urgency) }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请原因" :span="2">{{ currentSeal.reason }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="currentSeal.commonFileList?.length" style="margin-top: 20px;"> |
| | | <h4>附件</h4> |
| | | <el-table :data="currentSeal.commonFileList" border style="width: 100%"> |
| | | <el-table-column label="附件名称" show-overflow-tooltip> |
| | | <template #default="scope">{{ scope.row.name }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewSealFile(scope.row)">预览</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadSealFile(scope.row)">下载</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <el-form :model="{ activities }" ref="formRef" label-position="top"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-step |
| | | v-for="(activity, index) in activities" |
| | | :key="index" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)" |
| | | v-for="(group, gIndex) in stepGroups" |
| | | :key="gIndex" |
| | | finish-status="success" |
| | | :title="getNodeTitle(gIndex, stepGroups.length)" |
| | | :icon="getNodeIcon(group)" |
| | | > |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon> |
| | | <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon> |
| | | <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon> |
| | | </template> |
| | | <template #icon> |
| | | <el-icon v-if="group.some(a => a.approveNodeStatus === 2)" color="red" :size="22"><WarningFilled /></el-icon> |
| | | <el-icon v-else-if="group.some(a => a.isShen)" color="#1890ff" :size="22"><Edit /></el-icon> |
| | | <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)" color="#67C23A" :size="26"><Check /></el-icon> |
| | | <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon> |
| | | </template> |
| | | <template #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span> |
| | | <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}</el-tag> |
| | | </template> |
| | | <template #description> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" alt=""/> |
| | | <div v-for="(activity, aIndex) in group" :key="activity.id || aIndex" class="node-user-item"> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" alt=""/> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}({{ activity.isApproval }})</span> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span> |
| | | </div> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-step> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <filePreview ref="filePreviewRef" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { getSealDetail } from "@/api/collaborativeApproval/sealManagement.js"; |
| | | import FilePreview from '@/components/filePreview/index.vue' |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | |
| | | const currentQuotation = ref({}) |
| | | const purchaseLoading = ref(false) |
| | | const currentPurchase = ref({}) |
| | | const sealLoading = ref(false) |
| | | const currentSeal = ref({}) |
| | | const filePreviewRef = ref() |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6) |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5) |
| | | const isSealApproval = computed(() => Number(props.approveType) === 9) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | |
| | | return '审批'; |
| | | }; |
| | | |
| | | // 获取当前激活步骤 |
| | | // 获取当前激活步骤(分组后:含待审人的组下标,无则返回组总数=全部完成) |
| | | const getActiveStep = () => { |
| | | // 如果所有 isShen 都为 false,返回最后一个步骤(全部完成) |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // 当前节点索引 |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true)); |
| | | if (idx === -1) return stepGroups.value.length; |
| | | return idx; |
| | | }; |
| | | // 步骤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // 不通过 |
| | | if (activity.isShen) return 'Edit'; |
| | | // 步骤icon(组级:任一人驳回/任一人待审) |
| | | const getNodeIcon = (group) => { |
| | | if (group.some(a => a.approveNodeStatus === 2)) return 'el-icon-warning'; // 不通过 |
| | | if (group.some(a => a.isShen)) return 'Edit'; |
| | | return ''; |
| | | }; |
| | | // 审批节点分组(同一 approveNodeOrder = 同一节点的多名审批人,或签/会签) |
| | | const stepGroups = computed(() => { |
| | | const map = new Map(); |
| | | (activities.value || []).forEach(a => { |
| | | const order = a.approveNodeOrder; |
| | | if (!map.has(order)) map.set(order, []); |
| | | map.get(order).push(a); |
| | | }); |
| | | return Array.from(map.values()); |
| | | }); |
| | | // 获取节点成员在 activities 中的索引(用于表单校验 prop) |
| | | const getActivityIndex = (activity) => { |
| | | return activities.value.findIndex(a => a.id === activity.id); |
| | | }; |
| | | |
| | | // 打开弹框 |
| | |
| | | } |
| | | } |
| | | |
| | | // 用印审批:按审批流程 DB 主键(row.id)查用印申请详情 |
| | | if (isSealApproval.value) { |
| | | sealLoading.value = true |
| | | getSealDetail(row.id).then((res) => { |
| | | currentSeal.value = res.data || {} |
| | | }).catch((err) => { |
| | | console.error('查询用印详情失败:', err) |
| | | proxy.$modal.msgError('查询用印详情失败') |
| | | }).finally(() => { |
| | | sealLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then((res) => { |
| | | activities.value = res.data |
| | | // 增加isApproval字段 |
| | |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // 用印类型文本 |
| | | const getSealTypeText = (sealType) => { |
| | | const map = { official: '公章', contract: '合同专用章', finance: '财务专用章', legal: '法人章' } |
| | | return map[sealType] || sealType || '未知' |
| | | } |
| | | // 紧急程度文本 |
| | | const getUrgencyText = (urgency) => { |
| | | const map = { normal: '普通', urgent: '紧急', 'very-urgent': '特急' } |
| | | return map[urgency] || urgency || '未知' |
| | | } |
| | | // 预览附件(URL 为完整地址,原样打开,不拼 host) |
| | | const previewSealFile = (row) => { |
| | | const url = row.previewURL || row.previewUrl || row.url |
| | | if (url && filePreviewRef.value) { |
| | | filePreviewRef.value.openUrl(url) |
| | | } else { |
| | | proxy.$modal.msgWarning('文件地址无效,无法预览') |
| | | } |
| | | } |
| | | // 下载附件(锚点方式,URL 为完整地址) |
| | | const downloadSealFile = (row) => { |
| | | const url = row.downloadURL || row.downloadUrl || row.url |
| | | if (url) { |
| | | const filename = row.originalFilename || row.name || row.fileName || 'download' |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = filename |
| | | a.target = '_blank' |
| | | a.rel = 'noopener noreferrer' |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | document.body.removeChild(a) |
| | | } else { |
| | | proxy.$modal.msgWarning('文件地址无效,无法下载') |
| | | } |
| | | } |
| | | // 提交审批 |
| | | const submitForm = (status) => { |
| | | const filteredActivities = activities.value.filter(activity => activity.isShen); |
| | |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 判断是否为最后一步 |
| | | const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1; |
| | | updateApproveNode({ ...currentActivity, isLast }).then(() => { |
| | | // 流转判断由后端完成,前端不再传 isLast |
| | | updateApproveNode({ ...currentActivity }).then(() => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | closeDia(); |
| | | }); |
| | |
| | | currentQuotation.value = {} |
| | | purchaseLoading.value = false |
| | | currentPurchase.value = {} |
| | | sealLoading.value = false |
| | | currentSeal.value = {} |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-user-item { |
| | | margin: 8px 0; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | |
| | | <el-button |
| | | type="primary" |
| | | @click="openForm('add')" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9" |
| | | class="action-btn primary" |
| | | > |
| | | <el-icon><Plus /></el-icon> |
| | |
| | | type="danger" |
| | | plain |
| | | @click="handleDelete" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9" |
| | | class="action-btn danger" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | | const userStore = useUserStore(); |
| | | // 当前用户是否为管理员(角色 普通2/admin/最高1),可不受审批人限制审批 |
| | | const isAdminUser = computed(() => |
| | | ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r)) |
| | | ); |
| | | const route = useRoute(); |
| | | |
| | | // 管理员账号(角色为 普通2 / admin / 最高1)可审批任意流程,不受审批人限制 |
| | | const isAdminUser = computed(() => |
| | | ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r)) |
| | | ); |
| | | |
| | | // 当前选中的标签页,默认为公出管理 |
| | | const activeTab = ref('1'); |
| | |
| | | { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' }, |
| | | { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' }, |
| | | ]; |
| | | |
| | | // 当前审批类型信息 |
| | |
| | | } |
| | | ); |
| | | |
| | | // 当前审批人列 |
| | | // 当前审批人列(支持同节点多审批人) |
| | | baseColumns.push({ |
| | | label: "当前审批人", |
| | | prop: "approveUserCurrentName", |
| | | width: 120 |
| | | prop: "currentApproverNames", |
| | | width: 180 |
| | | }); |
| | | |
| | | // 操作列 |
| | |
| | | currentApproveType.value === 5 || |
| | | currentApproveType.value === 6 || |
| | | currentApproveType.value === 7 || |
| | | currentApproveType.value === 9 || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 1 || |
| | | row.approveStatus == 4 |
| | |
| | | openApprovalDia("approval", row); |
| | | }, |
| | | disabled: (row) => |
| | | row.approveUserCurrentId == null || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 3 || |
| | | row.approveStatus == 4 || |
| | | row.approveUserCurrentId !== userStore.id |
| | | !row._canApprove |
| | | }, |
| | | { |
| | | name: "详情", |
| | |
| | | }, |
| | | ]; |
| | | |
| | | // 报价审批(类型 6)不展示"附件"操作 |
| | | if (!isQuotationType) { |
| | | // 报价审批(类型 6)、用印审批(类型 9)不展示"附件"操作(附件走 common_file,不走 storage_attachment) |
| | | if (!isQuotationType && currentApproveType.value !== 9) { |
| | | actionOperations.push({ |
| | | name: "附件", |
| | | type: "text", |
| | |
| | | tableLoading.value = true; |
| | | approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | // 富化行数据:当前审批人显示(多值兜底旧单值)+ 当前用户是否可审核 |
| | | tableData.value = (res.data.records || []).map(r => ({ |
| | | ...r, |
| | | currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-', |
| | | _canApprove: isAdminUser.value || String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id |
| | | })); |
| | | page.total = res.data.total; |
| | | // 更新当前类型数量 |
| | | typeCounts.value[activeTab.value] = res.data.total; |
| | |
| | | 5: "/approveProcess/exportFive", |
| | | 6: "/approveProcess/exportSix", |
| | | 7: "/approveProcess/exportSeven", |
| | | 9: "/approveProcess/exportNine", |
| | | } |
| | | const url = urlMap[type] || urlMap[0] |
| | | const nameMap = { |
| | |
| | | 5: "采购申请审批表", |
| | | 6: "报价审批表", |
| | | 7: "发货审批表", |
| | | 9: "用印审批表", |
| | | } |
| | | const fileName = nameMap[type] || nameMap[0] |
| | | proxy.download(url, {}, `${fileName}.xlsx`) |
| | |
| | | </div> |
| | | </template> |
| | | |
| | | |
| | | <!-- 用印申请管理 --> |
| | | <div class="tab-content"> |
| | | <el-row :gutter="20" class="mb-20 "> |
| | |
| | | <el-form-item label="申请原因" prop="reason"> |
| | | <el-input v-model="sealForm.reason" type="textarea" :rows="4" placeholder="请详细说明用印原因" /> |
| | | </el-form-item> |
| | | <el-form-item label="审批人" prop="approveUserId"> |
| | | <el-select v-model="sealForm.approveUserId" placeholder="请选择审批人" style="width: 100%" filterable> |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="紧急程度" prop="urgency"> |
| | | <el-radio-group v-model="sealForm.urgency"> |
| | | <el-radio label="normal">普通</el-radio> |
| | | <el-radio label="urgent">紧急</el-radio> |
| | | <el-radio label="very-urgent">特急</el-radio> |
| | | <el-radio value="normal">普通</el-radio> |
| | | <el-radio value="urgent">紧急</el-radio> |
| | | <el-radio value="very-urgent">特急</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="附件上传"> |
| | | <el-upload |
| | | multiple |
| | | :show-file-list="false" |
| | | :action="sealUploadUrl" |
| | | name="files" |
| | | :headers="uploadHeaders" |
| | | :limit="10" |
| | | :before-upload="handleBeforeUpload" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :on-exceed="handleUploadExceed" |
| | | > |
| | | <el-button type="primary">点击上传附件</el-button> |
| | | </el-upload> |
| | | <el-table v-if="sealForm.attachmentList.length" :data="sealForm.attachmentList" border class="attachment-table"> |
| | | <el-table-column label="附件名称" show-overflow-tooltip> |
| | | <template #default="scope">{{ scope.row.name }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="140" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button> |
| | | <el-button link type="danger" size="small" @click="removeAttachment(scope.$index)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-form-item> |
| | | </el-form> |
| | | </FormDialog> |
| | |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <!-- 附件列表 --> |
| | | <div v-if="currentSealDetail.commonFileList?.length" class="attachment-section"> |
| | | <div class="attachment-title">附件列表:</div> |
| | | <el-table :data="currentSealDetail.commonFileList" border class="attachment-table"> |
| | | <el-table-column label="附件名称" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="操作" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | <!-- 文件预览组件 --> |
| | | <FilePreview ref="filePreviewRef" /> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue' |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { listSealApplication, addSealApplication, updateSealApplication } from '@/api/collaborativeApproval/sealManagement.js' |
| | | import { userListNoPageByTenantId } from '@/api/system/user.js' |
| | | import { ElMessage } from 'element-plus' |
| | | import { listSealApplication, addSealApplication } from '@/api/collaborativeApproval/sealManagement.js' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { getToken } from '@/utils/auth' |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import FilePreview from '@/components/filePreview/index.vue' |
| | | |
| | | // 响应式数据 |
| | | // 用印申请相关 |
| | |
| | | const tableLoading = ref(false) |
| | | const showSealDetailDialog = ref(false) |
| | | const currentSealDetail = ref(null) |
| | | const filePreviewRef = ref(null) |
| | | const sealFormRef = ref() |
| | | const userList = ref([]) |
| | | // 附件上传(本地上传,非 minio,返回 [{ name, url }]) |
| | | const sealUploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/uploadsList' |
| | | const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() }) |
| | | const sealForm = reactive({ |
| | | id: null, |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | | reason: '', |
| | | approveUserId: '', |
| | | urgency: 'normal', |
| | | status: 'pending' |
| | | status: 'pending', |
| | | attachmentList: [] |
| | | }) |
| | | |
| | | const sealRules = { |
| | | applicationNum: [{ required: true, message: '请输入申请编号', trigger: 'blur' }], |
| | | title: [{ required: true, message: '请输入申请标题', trigger: 'blur' }], |
| | | sealType: [{ required: true, message: '请选择用印类型', trigger: 'change' }], |
| | | reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }], |
| | | approveUserId: [{ required: true, message: '请选择审批人', trigger: 'change' }] |
| | | reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }] |
| | | } |
| | | |
| | | const sealSearchForm = reactive({ |
| | |
| | | |
| | | // 用印申请表格列配置(需在 getStatusText/getSealTypeText 等之后定义) |
| | | const sealTableColumn = ref([ |
| | | { label: '申请编号', prop: 'applicationNum',}, |
| | | { label: '申请编号', prop: 'applicationNum' }, |
| | | { label: '申请标题', prop: 'title', showOverflowTooltip: true }, |
| | | { label: '申请人', prop: 'createUserName', }, |
| | | { label: '申请人', prop: 'createUserName' }, |
| | | { label: '所属部门', prop: 'department', width: 150 }, |
| | | { |
| | | label: '用印类型', |
| | |
| | | formatData: (v) => getStatusText(v), |
| | | formatType: (v) => getStatusType(v) |
| | | }, |
| | | { label: '审批人', prop: 'approveUserName', width: 120 }, |
| | | { |
| | | dataType: 'action', |
| | | label: '操作', |
| | | width: 250, |
| | | width: 150, |
| | | fixed: 'right', |
| | | align: 'center', |
| | | operation: [ |
| | | { name: '查看', clickFun: (row) => viewSealDetail(row) }, |
| | | { |
| | | name: '审批', |
| | | clickFun: (row) => approveSeal(row), |
| | | showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id |
| | | }, |
| | | { |
| | | name: '拒绝', |
| | | clickFun: (row) => rejectSeal(row), |
| | | showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id |
| | | }, |
| | | { |
| | | name: '重新申请', |
| | | clickFun: (row) => reapplySeal(row), |
| | | showHide: (row) => row.status === 'rejected' && row.createUser === userStore.id |
| | | } |
| | | showHide: (row) => row.status === 'rejected' && Number(userStore.id) === row.createUser |
| | | }, |
| | | { name: '详情', clickFun: (row) => viewSealDetail(row) } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | // 搜索印章申请 |
| | | const searchSealApplications = () => { |
| | | page.current=1 |
| | | page.current = 1 |
| | | getSealApplicationList() |
| | | |
| | | // ElMessage.success('搜索完成') |
| | | } |
| | | // 重置印章申请搜索 |
| | | const resetSealSearch = () => { |
| | |
| | | sealSearchForm.applicationNum = '' |
| | | searchSealApplications() |
| | | } |
| | | // 提交用印申请 |
| | | |
| | | // 重新申请用印 |
| | | const reapplySeal = (row) => { |
| | | // 预填表单数据 |
| | | Object.assign(sealForm, { |
| | | id: row.id, |
| | | applicationNum: row.applicationNum, |
| | | title: row.title, |
| | | sealType: row.sealType, |
| | | reason: row.reason, |
| | | urgency: row.urgency || 'normal', |
| | | status: 'pending', |
| | | attachmentList: row.commonFileList || [] |
| | | }) |
| | | showSealApplyDialog.value = true |
| | | } |
| | | |
| | | // 提交用印申请(新增与重新申请均走 add 接口:重新申请会重新发起协同审批流程) |
| | | const submitSealApplication = async () => { |
| | | try { |
| | | await sealFormRef.value.validate() |
| | | addSealApplication(sealForm).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success('申请提交成功') |
| | | if (res.code == 200) { |
| | | ElMessage.success(sealForm.id ? '重新申请成功' : '申请提交成功') |
| | | closeSealApplyDialog() |
| | | getSealApplicationList() |
| | | Object.assign(sealForm, { |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | | reason: '', |
| | | approveUserId: '', |
| | | urgency: 'normal', |
| | | status: 'pending' |
| | | }) |
| | | } |
| | | }).catch(err => { |
| | | console.log(err.msg) |
| | | }) |
| | | |
| | | } catch (error) { |
| | | } |
| | | } |
| | | |
| | | // 关闭用印申请对话框 |
| | | const closeSealApplyDialog = () => { |
| | | // 清空表单数据 |
| | | Object.assign(sealForm, { |
| | | id: null, |
| | | applicationNum: '', |
| | | title: '', |
| | | sealType: '', |
| | | reason: '', |
| | | approveUserId: '', |
| | | urgency: 'normal', |
| | | status: 'pending' |
| | | status: 'pending', |
| | | attachmentList: [] |
| | | }) |
| | | // 清除表单验证状态 |
| | | if (sealFormRef.value) { |
| | |
| | | currentSealDetail.value = row |
| | | showSealDetailDialog.value = true |
| | | } |
| | | // 审批用印申请 |
| | | const approveSeal = (row) => { |
| | | ElMessageBox.confirm('确认通过该用印申请?', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | row.status = 'approved' |
| | | updateSealApplication(row).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success('审批通过') |
| | | getSealApplicationList() |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | // 附件上传前校验(单个文件不超过 50MB) |
| | | const handleBeforeUpload = (file) => { |
| | | if (file.size / 1024 / 1024 > 50) { |
| | | ElMessage.error('文件大小不能超过 50MB') |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | // 拒绝用印申请 |
| | | const rejectSeal = (row) => { |
| | | ElMessageBox.prompt('请输入拒绝原因', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | inputPattern: /\S+/, |
| | | inputErrorMessage: '拒绝原因不能为空' |
| | | }).then(({ value }) => { |
| | | row.status = 'rejected' |
| | | updateSealApplication(row).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success('已拒绝申请') |
| | | getSealApplicationList() |
| | | } |
| | | }) |
| | | }) |
| | | // 附件上传成功回调 |
| | | const handleUploadSuccess = (res) => { |
| | | if (res.code === 200) { |
| | | const list = res.data || [] |
| | | list.forEach(item => sealForm.attachmentList.push({ name: item.name, url: item.url })) |
| | | ElMessage.success('上传成功') |
| | | } else { |
| | | ElMessage.error(res.msg || '上传失败') |
| | | } |
| | | } |
| | | // 附件上传失败回调 |
| | | const handleUploadError = () => { |
| | | ElMessage.error('附件上传失败') |
| | | } |
| | | // 附件数量超出 |
| | | const handleUploadExceed = () => { |
| | | ElMessage.warning('最多上传 10 个附件') |
| | | } |
| | | // 删除待提交附件 |
| | | const removeAttachment = (index) => { |
| | | sealForm.attachmentList.splice(index, 1) |
| | | } |
| | | // 预览文件(完整可访问 URL,直接打开,不拼 host) |
| | | const previewFile = (row) => { |
| | | const url = row.previewURL || row.previewUrl || row.url |
| | | if (url && filePreviewRef.value) { |
| | | filePreviewRef.value.openUrl(url) |
| | | } else { |
| | | ElMessage.warning('文件地址无效,无法预览') |
| | | } |
| | | } |
| | | |
| | | // 重新申请用印 |
| | | const reapplySeal = (row) => { |
| | | // 预填表单数据 |
| | | Object.assign(sealForm, { |
| | | applicationNum: row.applicationNum, |
| | | title: row.title, |
| | | sealType: row.sealType, |
| | | reason: row.reason, |
| | | approveUserId: row.approveUserId, |
| | | urgency: row.urgency || 'normal', |
| | | status: 'pending' |
| | | }) |
| | | showSealApplyDialog.value = true |
| | | // 下载文件 |
| | | const downloadFile = (row) => { |
| | | const url = row.downloadURL || row.downloadUrl || row.url |
| | | if (url) { |
| | | const filename = row.originalFilename || row.name || row.fileName || 'download' |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = filename |
| | | a.target = '_blank' |
| | | a.rel = 'noopener noreferrer' |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | document.body.removeChild(a) |
| | | } else { |
| | | ElMessage.warning('文件地址无效,无法下载') |
| | | } |
| | | } |
| | | |
| | | // 导出用印申请 |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | |
| | | const getSealApplicationList = async () => { |
| | | tableLoading.value = true |
| | | listSealApplication(page, sealSearchForm) |
| | | .then(res => { |
| | | sealApplications.value = res.data.records |
| | | page.total = res.data.total |
| | | tableLoading.value = false |
| | | }).catch(err => { |
| | | tableLoading.value = false |
| | | }) |
| | | .then(res => { |
| | | sealApplications.value = res.data.records |
| | | page.total = res.data.total |
| | | tableLoading.value = false |
| | | }).catch(err => { |
| | | tableLoading.value = false |
| | | }) |
| | | } |
| | | // 分页变化处理 |
| | | const paginationChange = (obj) => { |
| | |
| | | page.size = obj.limit; |
| | | getSealApplicationList(); |
| | | }; |
| | | |
| | | // 监听对话框打开,获取用户列表 |
| | | watch(showSealApplyDialog, (newVal) => { |
| | | if (newVal) { |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | // 路由携带 applicationNum 时,预填并查询 |
| | |
| | | .ml-10 { |
| | | margin-left: 10px; |
| | | } |
| | | </style> |
| | | |
| | | .attachment-section { |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .attachment-title { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 10px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .attachment-table { |
| | | border-radius: 4px; |
| | | } |
| | | </style> |
| | |
| | | :tableLoading="tableLoading1" |
| | | style="margin-right: 20px;" |
| | | @pagination="pagination1" |
| | | ></PIMTable> |
| | | > |
| | | <template #personWorkMinutes="{ row }"> |
| | | {{ row.personWorkMinutes ?? row.workMinutes ?? '-' }} |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | prop: "workMinutes", |
| | | width: 110, |
| | | }, |
| | | { |
| | | label: "人员工时(分钟)", |
| | | prop: "personWorkMinutes", |
| | | width: 120, |
| | | dataType: "slot", |
| | | slot: "personWorkMinutes", |
| | | }, |
| | | ]); |
| | | |
| | | // 左侧汇总台账列(生产人、产量、工资、合格率) |
| | |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination"> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | </PIMTable> |
| | | @pagination="pagination" /> |
| | | </div> |
| | | <el-dialog v-model="editDialogVisible" |
| | | title="编辑时间" |
| | |
| | | <!-- </el-col>--> |
| | | </el-row> |
| | | </el-form> |
| | | <el-table :data="personList" border style="margin-top: 12px;"> |
| | | <el-table-column label="生产人" prop="userName" min-width="100" align="center" /> |
| | | <el-table-column label="产量" min-width="120" align="center"> |
| | | <template #default="scope"> |
| | | <el-input-number v-model="scope.row.quantity" :min="0" :controls="false" placeholder="产量" style="width: 100%" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="生产时长(分钟)" min-width="140" align="center"> |
| | | <template #default="scope"> |
| | | <el-input-number v-model="scope.row.workHour" :min="0" :controls="false" placeholder="时长(分钟)" style="width: 100%" /> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" |
| | |
| | | productWorkOrderPage, |
| | | updateProductWorkOrder, |
| | | addProductMain, |
| | | getProductWorkOrderById, |
| | | downProductWorkOrder, |
| | | addProductionMachineRecord, |
| | | productionMachineRecordListPage, |
| | |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "加放数", |
| | | prop: "processRouteAddNum", |
| | | width: "100", |
| | | }, |
| | | { |
| | | label: "完成数量", |
| | | prop: "completeQuantity", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "完成进度", |
| | | prop: "completionStatus", |
| | | dataType: "slot", |
| | | slot: "completionStatus", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划开始时间", |
| | | prop: "planStartTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划结束时间", |
| | | prop: "planEndTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | |
| | | clickFun: row => { |
| | | showReportDialog(row); |
| | | }, |
| | | // 用户当前id在工单报工人或工序报工人中 |
| | | // 用户当前id在工单报工人或工序报工人中,且完成数量未达计划数量时可继续分批报工 |
| | | disabled: row => |
| | | row.completeQuantity >= row.planQuantity || |
| | | !canCurrentUserReport(row) || |
| | | row.hasUnreportedMachine, |
| | | !canCurrentUserReport(row), |
| | | }, |
| | | { |
| | | name: "生产排产", |
| | |
| | | teamList: [], |
| | | deviceId: null, |
| | | }); |
| | | // 班组人员产量时长明细 [{ userId, userName, quantity, workHour }] |
| | | const personList = ref([]); |
| | | const teamSelectOptions = computed(() => { |
| | | const base = |
| | | Array.isArray(reportForm.userIdsList) && reportForm.userIdsList.length > 0 |
| | |
| | | }); |
| | | }; |
| | | |
| | | const showReportDialog = row => { |
| | | const showReportDialog = async row => { |
| | | currentReportRowData.value = row; |
| | | reportForm.planQuantity = row.planQuantity - row.completeQuantity; |
| | | reportForm.quantity = row.planQuantity - row.completeQuantity; |
| | |
| | | reportForm.replenishQty = 0; |
| | | reportForm.teamList = []; |
| | | reportForm.scrapQty = 0; |
| | | reportForm.addQty = 0; |
| | | reportForm.addQty = row.processRouteAddNum ? Number(row.processRouteAddNum) : 0; |
| | | reportForm.userIds = row.userIds || []; |
| | | |
| | | // 合并工单报工人和工序报工人,去重 |
| | |
| | | userId: item.userId, |
| | | nickName: item.nickName, |
| | | })); |
| | | |
| | | // 获取报工弹窗数据,带出默认班组成员(合并排产人员与机台操作员,去重) |
| | | personList.value = []; |
| | | try { |
| | | const res = await getProductWorkOrderById(row.id); |
| | | const reportPersons = (Array.isArray(res?.data?.reportPersons) |
| | | ? res.data.reportPersons |
| | | : [] |
| | | ) |
| | | .filter(p => p && typeof p === "object") |
| | | .map(p => ({ |
| | | userId: p.userId ?? p.id ?? "", |
| | | userName: String(p.userName || p.nickName || "").trim(), |
| | | })) |
| | | .filter(p => p.userName !== ""); |
| | | personList.value = reportPersons.map(p => ({ |
| | | userId: p.userId, |
| | | userName: p.userName, |
| | | quantity: null, |
| | | workHour: null, |
| | | })); |
| | | reportForm.teamList = reportPersons.map(p => ({ |
| | | userId: p.userId, |
| | | userName: p.userName, |
| | | })); |
| | | } catch (err) { |
| | | console.error("获取报工弹窗数据失败", err); |
| | | } |
| | | |
| | | nextTick(() => { |
| | | reportFormRef.value?.clearValidate(); |
| | |
| | | // return; |
| | | // } |
| | | |
| | | // 校验各班组产量之和等于报工总产量 |
| | | const personQuantityTotal = personList.value.reduce( |
| | | (sum, p) => sum + (Number(p.quantity) || 0), |
| | | 0 |
| | | ); |
| | | if (personQuantityTotal !== quantity) { |
| | | ElMessageBox.alert("各班组产量之和必须等于报工总产量", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const submitData = { |
| | | ...reportForm, |
| | | quantity: quantity, |
| | | scrapQty: scrapQty, |
| | | personList: personList.value, |
| | | }; |
| | | addProductMain(submitData).then(res => { |
| | | if (res.code === 200) { |
| | |
| | | <el-form :model="{ activities }" |
| | | ref="formRef" |
| | | label-position="top"> |
| | | <el-steps :active="getActiveStep()" |
| | | <el-steps :active="getActiveStep()" |
| | | finish-status="success" |
| | | process-status="process" |
| | | align-center |
| | | direction="vertical"> |
| | | <el-step v-for="(activity, index) in activities" |
| | | :key="index" |
| | | <el-step v-for="(group, gIndex) in stepGroups" |
| | | :key="gIndex" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)"> |
| | | :title="getNodeTitle(gIndex, stepGroups.length)" |
| | | :icon="getNodeIcon(group)"> |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" |
| | | <el-icon v-if="group.some(a => a.approveNodeStatus === 2)" |
| | | color="red" |
| | | :size="22"> |
| | | <WarningFilled /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.isShen" |
| | | <el-icon v-else-if="group.some(a => a.isShen)" |
| | | color="#1890ff" |
| | | :size="22"> |
| | | <Edit /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" |
| | | <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)" |
| | | color="#67C23A" |
| | | :size="26"> |
| | | <Check /> |
| | |
| | | </el-icon> |
| | | </template> |
| | | <template #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span> |
| | | <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}</el-tag> |
| | | </template> |
| | | <template #description> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" |
| | | class="user-avatar" |
| | | alt="" /> |
| | | <div v-for="(activity, aIndex) in group" |
| | | :key="activity.id || aIndex" |
| | | class="node-user-item"> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" |
| | | class="user-avatar" |
| | | alt="" /> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}({{ activity.isApproval }})</span> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span> |
| | | </div> |
| | | <div v-if="!activity.isShen" |
| | | class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-if="!activity.isShen" |
| | | class="node-reason"> |
| | | <span>签名:</span> |
| | | <img :src="activity.urlTem" |
| | | class="signImg" |
| | | alt="" |
| | | v-if="activity.urlTem" /> |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"> |
| | | <el-input v-model="activity.approveNodeReason" |
| | | clearable |
| | | type="textarea" |
| | | :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | <div v-if="!activity.isShen" |
| | | class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-if="!activity.isShen" |
| | | class="node-reason"> |
| | | <span>签名:</span> |
| | | <img :src="activity.urlTem" |
| | | class="signImg" |
| | | alt="" |
| | | v-if="activity.urlTem" /> |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"> |
| | | <el-input v-model="activity.approveNodeReason" |
| | | clearable |
| | | type="textarea" |
| | | :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-step> |
| | |
| | | return "审批"; |
| | | }; |
| | | |
| | | // 获取当前激活步骤 |
| | | // 获取当前激活步骤(分组后:含待审人的组下标,无则返回组总数=全部完成) |
| | | const getActiveStep = () => { |
| | | // 如果所有 isShen 都为 false,返回最后一个步骤(全部完成) |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // 当前节点索引 |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true)); |
| | | if (idx === -1) return stepGroups.value.length; |
| | | return idx; |
| | | }; |
| | | // 步骤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return "el-icon-warning"; // 不通过 |
| | | if (activity.isShen) return "Edit"; |
| | | // 步骤icon(组级:任一人驳回/任一人待审) |
| | | const getNodeIcon = group => { |
| | | if (group.some(a => a.approveNodeStatus === 2)) return "el-icon-warning"; // 不通过 |
| | | if (group.some(a => a.isShen)) return "Edit"; |
| | | return ""; |
| | | }; |
| | | // 审批节点分组(同一 approveNodeOrder = 同一节点的多名审批人,或签/会签) |
| | | const stepGroups = computed(() => { |
| | | const map = new Map(); |
| | | (activities.value || []).forEach(a => { |
| | | const order = a.approveNodeOrder; |
| | | if (!map.has(order)) map.set(order, []); |
| | | map.get(order).push(a); |
| | | }); |
| | | return Array.from(map.values()); |
| | | }); |
| | | // 获取节点成员在 activities 中的索引(用于表单校验 prop) |
| | | const getActivityIndex = activity => { |
| | | return activities.value.findIndex(a => a.id === activity.id); |
| | | }; |
| | | |
| | | // 打开弹框 |
| | |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 判断是否为最后一步 |
| | | const isLast = |
| | | activities.value.findIndex(a => a.isShen) === activities.value.length - 1; |
| | | updateApproveNode({ ...currentActivity, isLast }).then(() => { |
| | | // 流转判断由后端完成,前端不再传 isLast |
| | | updateApproveNode({ ...currentActivity }).then(() => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | closeDia(); |
| | | }); |
| | |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-user-item { |
| | | margin: 8px 0; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | | const userStore = useUserStore(); |
| | | // 当前用户是否为管理员(角色 普通2/admin/最高1),可不受审批人限制审批 |
| | | const isAdminUser = computed(() => |
| | | ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r)) |
| | | ); |
| | | const route = useRoute(); |
| | | |
| | | // 管理员账号(角色为 普通2 / admin / 最高1)可审批任意流程,不受审批人限制 |
| | | const isAdminUser = computed(() => |
| | | ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r)) |
| | | ); |
| | | |
| | | // 当前审批类型,根据选中的标签页计算 |
| | | const currentApproveType = computed(() => { |
| | |
| | | } |
| | | ); |
| | | |
| | | // 当前审批人列 |
| | | // 当前审批人列(支持同节点多审批人) |
| | | baseColumns.push({ |
| | | label: "当前审批人", |
| | | prop: "approveUserCurrentName", |
| | | width: 120, |
| | | prop: "currentApproverNames", |
| | | width: 180, |
| | | }); |
| | | |
| | | // 操作列 |
| | |
| | | openApprovalDia("approval", row); |
| | | }, |
| | | disabled: row => |
| | | row.approveUserCurrentId == null || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 3 || |
| | | row.approveStatus == 4 || |
| | | row.approveUserCurrentId !== userStore.id, |
| | | !row._canApprove, |
| | | }, |
| | | { |
| | | name: "详情", |
| | |
| | | }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | // 富化行数据:当前审批人显示(多值兜底旧单值)+ 当前用户是否可审核 |
| | | tableData.value = (res.data.records || []).map(r => ({ |
| | | ...r, |
| | | currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-', |
| | | _canApprove: isAdminUser.value || String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id |
| | | })); |
| | | page.total = res.data.total; |
| | | }) |
| | | .catch(err => { |
| | |
| | | :min="0" |
| | | v-model="productForm.taxInclusiveUnitPrice" |
| | | style="width: 100%" |
| | | :precision="4" |
| | | :precision="5" |
| | | placeholder="请输入" |
| | | clearable |
| | | @change="calculateFromQuantity" /> |
| | |
| | | if (Number.isNaN(num)) return "0.0000"; |
| | | return num.toFixed(4); |
| | | }; |
| | | const formatAmount5 = value => { |
| | | if (value === undefined || value === null || value === "") return "0.00000"; |
| | | const num = Number(value); |
| | | if (Number.isNaN(num)) return "0.00000"; |
| | | return num.toFixed(5); |
| | | }; |
| | | const findLedgerRecordByRow = row => { |
| | | if (!row) return null; |
| | | if ( |
| | |
| | | productForm.value.taxInclusiveUnitPrice !== null && |
| | | productForm.value.taxInclusiveUnitPrice !== "" |
| | | ) { |
| | | productForm.value.taxInclusiveUnitPrice = formatAmount4( |
| | | productForm.value.taxInclusiveUnitPrice = formatAmount5( |
| | | productForm.value.taxInclusiveUnitPrice |
| | | ); |
| | | } |
| | |
| | | productForm.value.taxInclusiveTotalPrice = totalPrice.toFixed(4); |
| | | |
| | | // 计算含税单价 = 含税总价 / 数量 |
| | | productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(4); |
| | | productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(5); |
| | | |
| | | // 如果有税率,计算不含税总价 |
| | | if (productForm.value.taxRate) { |
| | |
| | | // 计算含税单价 = 含税总价 / 数量 |
| | | productForm.value.taxInclusiveUnitPrice = ( |
| | | inclusiveTotalPrice / quantity |
| | | ).toFixed(4); |
| | | ).toFixed(5); |
| | | |
| | | isCalculating.value = false; |
| | | }; |
| | |
| | | const { VITE_APP_ENV } = env; |
| | | const baseUrl = |
| | | env.VITE_APP_ENV === "development" |
| | | ? "http://1.15.17.182:9038" |
| | | ? "http://192.168.0.10:7003" |
| | | : env.VITE_BASE_API; |
| | | const javaUrl = |
| | | env.VITE_APP_ENV === "development" |