From da61394d3612b5497cfb8b2b7e1fc1466843df31 Mon Sep 17 00:00:00 2001 From: bestlee Date: Fri, 18 Sep 2026 14:04:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=BD=AE=E7=94=B3=E8=AF=B7=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/api/myTasks.ts | 17 + src/views/details/closeDoc/index.vue | 1 - src/views/details/disposal/index.vue | 1501 ++++++++++------- .../my/initiateChange/modules/footer.vue | 2 +- src/views/my/myTask/index.vue | 136 +- 5 files changed, 948 insertions(+), 709 deletions(-) diff --git a/src/service/api/myTasks.ts b/src/service/api/myTasks.ts index 6dd8d74..e79551a 100644 --- a/src/service/api/myTasks.ts +++ b/src/service/api/myTasks.ts @@ -30,3 +30,20 @@ export function myTasksCloseMaterialApi(change_id:number,data: any) { data, }); } +// 处置申请保存提交 +export function myTasksDisposalSubmitApi(change_id:number,data: any) { + return request({ + url: `/api/changes/${change_id}/disposal`, + method: 'post', + data, + }); +} + +// 新增检查项 +export function myTasksAddItemApi(change_id:number,data: any) { + return request({ + url: `/api/changes/${change_id}/pssr/item`, + method: 'post', + data, + }); +} diff --git a/src/views/details/closeDoc/index.vue b/src/views/details/closeDoc/index.vue index 2b84f02..18a6207 100644 --- a/src/views/details/closeDoc/index.vue +++ b/src/views/details/closeDoc/index.vue @@ -178,7 +178,6 @@ const handleFileType = (type:string) => { } // 打开预览 const preview = (file:any) => { - console.log(file) previewFile.value = file; previewModal.value = true; } diff --git a/src/views/details/disposal/index.vue b/src/views/details/disposal/index.vue index 2cef108..9afb15a 100644 --- a/src/views/details/disposal/index.vue +++ b/src/views/details/disposal/index.vue @@ -4,10 +4,15 @@ import { Icon } from '@iconify/vue' import { useThemeStore } from '@/store/modules/theme'; import { useRouterPush } from '@/hooks/common/router'; import { useRoute } from 'vue-router'; -import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common'; +import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor, formatTimestamp } from '@/utils/common'; import { AiError, aiTypeJudge } from "@/service/api/ai"; import { systemConfigApi } from '@/service/api/system'; import { getChangeDetailApi } from '@/service/api/change'; +import { myTasksDisposalSubmitApi, myTasksAddItemApi } from '@/service/api/myTasks'; + +import PDF from 'pdf-vue3' +import { VueOfficeDocx, VueOfficeExcel, VueOfficePptx } from 'vue3-office-preview' +import 'vue3-office-preview/lib/style.css' const themeStore = useThemeStore(); const { routerPushByKey } = useRouterPush(); @@ -21,8 +26,6 @@ const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_r const currentInfo = ref(null); // 当前变更id const currentId = ref(0); -// 当前变更状态 -const currentStatus = ref(''); // 是否开启 AI 辅助功能 const isAiOn = ref(false); // 变更等级选项 @@ -33,6 +36,27 @@ const typeList = ref([]); const level = computed(() => { return currentInfo.value?.apply_form?.change_level; }); +// 处置申请类型 unExecute:未实施;unUse:未投用;recover:申请恢复;delay:申请延期;transform:申请转为永久 +const disposalType = ref(''); +// 情况说明 +const description = ref(''); +// 影响判定 +const judge = ref(''); +// 延期时间 +const delayDate = ref(null); +// 审批流程抽屉 +const approvalDrawer = ref(false); +// 存储每个步骤选中的审核人 user_id +const approvers = ref>({}) +const flowList = ref([]); +const steps = ref([]); +// 审批链 +const approvalFlowText = computed(() => { + if (!flowList.value || flowList.value.length === 0) return '暂无步骤' + // 按 step_order 排序 + const sorted = [...flowList.value].sort((a, b) => a.step_order - b.step_order) + return sorted.map(item => item.name).join(' → ') +}) // 总得分 const totalScore = computed(() => { return currentInfo.value?.apply_form?.level_score?.reduce((sum: number, item: any) => { @@ -45,14 +69,25 @@ const allMembers = computed(() => { return currentInfo.value?.approval_flow?.steps?.map((item: any) => item.members).flat(); }); // pssr 检查内容 -const pssrlist = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([ - { g: "一、风险分析行动项落实", type:1, na_flag:0, items: [] }, - { g: "二、现场设备安装检查确认", type:2, na_flag:0, items: [] }, - { g: "三、保护措施确认", type:3, na_flag:0, items: [] }, - { g: "四、资料归档确认", type:4, na_flag:0, items: [] }, -]); +const pssrList = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([]); +// pssr总条数 +const totalPssr = computed(() => { + return pssrList.value.flatMap((group:any) => group.items || []).length; +}); +// pssr已确认条数 +const confirmedPssr = computed(() => { + return pssrList.value.flatMap((group:any) => group.items || []).filter((item:any) => item.confirm_flag).length; +}); +// 资料关闭确认列表 +const closeConfirmList = ref([]); // 验收评价 const acceptList = ref([]); + +// 当前预览文件 +const previewFile = ref(null); +// 预览文件弹框 +const previewModal = ref(false); + // 等级判断问题 const questions = ref([ { @@ -130,75 +165,10 @@ const questions = ref([ ]); -const role = ref("applicant"); // 判定说明抽屉 const describeDrawer = ref(false); const levelDrawer = ref(false); -// ai 标注抽屉 -const aiDrawer = ref(false); -const mandClosed = ref(false); -const confirmClose = () => { - mandClosed.value = true; - window.$message?.success('强制合规项已确认闭环,操作已留痕'); -} -const conflictHide = ref(false); -// 关键参数对比(静态示例作 fallback,AI 审批辅助完成后回填) -const paramDiff = ref([ - { k: "反应温度", before: "80℃", after: "90℃", over: false, note: "仍在溶剂沸点与物料分解温度的安全裕度内" }, - { k: "保温时间", before: "8 h", after: "4 h", over: false, note: "终点判断窗口变窄,须修订操作规程" }, - { k: "DCS 温度高报警", before: "85℃", after: "95℃", over: true, note: "超出原报警裕度,须重新整定验证" }, - { k: "联锁高高报", before: "90℃", after: "100℃", over: true, note: "接近设备设计温度上限,投用前须完成联锁测试" }, - { k: "未知杂质含量", before: "0.05%", after: "0.12%", over: false, note: "结构未定,须纳入日常分析计划" }, -]); -const aiRead = ref(false); -const roleName = ref('张工艺'); -const suggs = ref([ - { id: 1, text: "将未知杂质含量纳入日常分析计划(每周 1 次,连续 8 周)", st: "待定" }, - { id: 2, text: "修订操作规程并补充超温应急处置卡后再投用", st: "待定" }, -]); -const opinion = ref(""); - -// 已阅 AI 建议 -const readSuggest = () => { - aiRead.value = true; - window.$message?.success(`${roleName} 已阅 AI 建议,时间与操作已写入审批留痕`); -} -// 采纳为行动项 -const acceptSuggest = (s: any) => { - suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '已采纳' } : x)); - opinion.value = (opinion.value ? `${opinion.value}\n` : '') + `采纳 AI 建议:${s.text}`; - window.$message?.success('已采纳,自动生成 PSSR 行动项(来源:AI 建议),并纳入审批意见'); -} -// 不采纳 AI 建议 -const rejectSuggest = (s: any) => { - suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '不采纳' } : x)); - window.$message?.info('不采纳理由「现有分析频次已覆盖风险」已记录留痕'); -} -// 查看归档包 -const viewArchive = (name: string) => { - window.$message?.info(`新窗口打开 ${name} 归档包,不影响当前审批`); -} - -// AI 审批辅助(抽屉打开时触发,异步任务轮询;失败保留静态示例,不阻塞页面) -const aiAssistLoading = ref(false); -const aiAssistStep = ref(''); -const aiAssistProgress = ref(0); -// 强制合规描述(静态示例作 fallback) -const mandText = ref('本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。'); -const mandItem = ref('联锁测试记录须在投用前完成并上传归档'); -// 冲突提示 -const conflictText = ref('涉及设备 R-101 关联变更 MOC-2026-0038「机封冲洗方案调整」正在审批中,请确认施工时序与隔离方案无冲突。'); -// 属地影响摘要 -const deptRows = ref([ - { label: '涉及单元:', text: '一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。' }, - { label: '现行规程影响:', text: '操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。' }, - { label: '交接班提示:', text: '投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。' }, -]); -// 参考信息(相似变更 / 历史经验) -const similarText = ref('相似变更 MOC-2025-0112「酯化反应温度上调」投用后运行正常,无异常记录'); -const experienceText = ref('历史经验:温度类变更投用后首周超温报警占比约 38%,建议关注投用首周报警趋势'); - const aiFail = (e: any) => { if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`); else window.$message?.warning("AI 分析失败,可手动填写"); @@ -252,8 +222,34 @@ const downloadFile = (type: string) => { } }; // 变更附带资料预览 -const previewFile = () => { - window.$message?.info('还没导入插件'); +const openPreview = (file:any) => { + previewFile.value = file; + previewModal.value = true; +} +// 判断文件类型 +const handleFileType = (type:string) => { + if(type){ + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'] + const wordExtensions = ['docx'] + const excelExtensions = ['xlsx'] + const pptExtensions = ['pptx'] + if(imageExtensions.includes(type)){ + return 'image' + } + if(wordExtensions.includes(type)){ + return 'word' + } + if(excelExtensions.includes(type)){ + return 'excel' + } + if(pptExtensions.includes(type)){ + return 'ppt' + } + if(type==='pdf'){ + return 'pdf' + } + } + return '' } // 催办 @@ -262,6 +258,62 @@ const handleReminder = (m: any) => { window.$message?.success(`已催办 ${m.user_name}(系统消息 + 企业微信 + 短信),连续 2 次未响应将上报部门负责人`) } +// 更新选中审批人 user_id +const updateApprover = (stepId: number, usersId: number[]) => { + approvers.value[stepId] = usersId +} +// 匹配审批人 +function matchApprovers(steps: any[], mapping: Record) { + const result = []; + for (const [stepId, usersId] of Object.entries(mapping)) { + const step = steps.find(s => s.id === Number(stepId)); + if (step) { + const memberList = step.members.filter((m: any) => usersId.includes(m.user_id)); + result.push({ + members: memberList + }) + } + } + return result; +} +// 完成设置 +const finishApprover = async () => { + const matched = matchApprovers(flowList.value, approvers.value); + steps.value = matched; + window.$message?.success("审批流程设置完成"); + approvalDrawer.value = false; +} + +// 添加检查项 +const addItem = (index: number) => { + pssrList.value[index].items.push({ + id: 0, + content: "", + group_type: pssrList.value[index].type, + na_flag: 0, + phase: 1, + confirm_flag: 0, + }) +} +// 保存检查项 +const saveItem = async (index: number, subIndex: number) => { + const item = pssrList.value[index].items[subIndex]; + if(item?.content?.trim() === ''){ + return window.$message?.warning('请填写检查项内容'); + } + const {data,error} = await myTasksAddItemApi(currentId.value,{ + content: item?.content, + group_type: item?.group_type, + na_flag: item?.na_flag, + phase: item?.phase, + confirm_flag: item?.confirm_flag, + }) + if(!error){ + pssrList.value[index].items[subIndex].id = data.id + window.$message?.success('保存成功'); + } +} + // 匹配评分依据 const matchBasis = (items:any,score: number) => { return items.find((item:any) => item.score === score)?.label || ''; @@ -272,36 +324,113 @@ const matchLabel = (list:any,v: number) => { return list.find((item:any) => item.value === v) || null; } +// 保存/提交处置申请 +const submitDisposal = async (type: string) => { + let txt = '' + // 类型 + let actType = 0; + // 未实施 + if(disposalType.value==='unExecute'){ + actType = 4; + txt = '请填写情况说明'; + } + // 未投用 + if(disposalType.value==='unUse'){ + actType = 5; + txt = '请填写情况说明'; + } + // 申请恢复 + if(disposalType.value==='recover'){ + actType = 1; + txt = '请填写申请恢复原因'; + } + // 申请延期 + if(disposalType.value==='delay'){ + actType = 2; + txt = '请填写变更延期原因'; + } + // 申请转永久 + if(disposalType.value==='transform'){ + actType = 3; + txt = '请填写申请转永久原因'; + } + if(description.value?.trim() === ''){ + return window.$message?.warning(txt); + } + if(disposalType.value==='delay' && !delayDate.value){ + return window.$message?.warning('请选择变更延期时间'); + } + // 去掉name + const closeItems = closeConfirmList.value.map(({ name, ...rest }: any) => rest); + // 转换格式 + const pssrItems = pssrList.value.flatMap((group: any) => group.items || []).map((item: any) => ({ + item_id: item.id, + confirm_flag: item.confirm_flag + })); + // 去掉id, step_order, user_name, status + const result = steps.value.map(({ members }:any) => ({ + members: members.map(({ id, step_order, user_name, status, ...rest }:any) => rest) + })); + const {error} = await myTasksDisposalSubmitApi(currentId.value,{ + action: type, + act: actType, + reason: description.value, + risk_text: judge.value, + new_deadline: formatTimestamp(delayDate.value,'yyyy-MM-dd'), + close_docs: closeItems, + pssr: pssrItems, + approval_flow: { + steps: result + } + }) + if(!error){ + if(type==='save'){ + window.$message?.success('保存成功'); + }else{ + window.$message?.success('提交成功'); + back(); + } + } +} + // 获取变更详情 const getDetail = async () => { loading.value = true; const {data,error} = await getChangeDetailApi(currentId.value) if(!error){ currentInfo.value = data; - console.log(data) - + // ******审批流程******** + flowList.value = data?.approval_flow?.steps || []; + // ******默认审批人******** + if(Object.keys(approvers.value).length === 0){ + flowList.value.forEach((item: any) => { + if (item.members && item.members.length > 0) { + // 优先选 is_sign=1 的成员 ,否则选第一个 + const defaultMembers = item.members + .filter((m: any) => m.is_sign === 1) + .map((m: any) => m.user_id); + approvers.value[item.id] = defaultMembers.length > 0 ? defaultMembers : [item.members[0].user_id]; + } + }) + } + const matched = matchApprovers(flowList.value, approvers.value); + steps.value = matched; // ******解析 PSSR 检查内容******** - // 1. 按 group_type 分组 const map = new Map(); - currentInfo.value?.pssr?.items?.forEach((item:any) => { - const t = item.group_type; - if (!map.has(t)) map.set(t, []); - map.get(t).push(item); - }); - - // 2. 生成目标数组 - const result = pssrlist.value.map(({ type, g }:any) => { - const items = map.get(type) || []; - // na_flag 取该组第一个元素的 na_flag,无数据时默认为 0 - const na_flag = items.length > 0 ? items[0].na_flag : 0; - return { - g, - type, - na_flag, - items - }; - }); - // 3. 过滤掉不需要的字段 + for (const item of currentInfo.value?.pssr?.items) { + const type = item.group_type; + if (!map.has(type)) { + map.set(type, { + g: item.group_name, + type: item.group_type, + na_flag: item.na_flag, + items: [] + }); + } + map.get(type).items.push(item); + } + const result = Array.from(map.values()); + // 过滤掉不需要的字段 const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name']; const newResult = result.map((group:any) => ({ ...group, @@ -309,7 +438,7 @@ const getDetail = async () => { Object.fromEntries(Object.entries(item).filter(([k]) => !omitFields.includes(k))) ) })); - pssrlist.value = newResult; + pssrList.value = newResult; // ******解析 验收评价******** const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k))); @@ -372,8 +501,51 @@ onMounted(async () => { currentId.value = Number(route.query.id); getConfig(); } - if(route.query.status){ - currentStatus.value = route.query.status.toString(); + if(route.query.type){ + disposalType.value = route.query.type.toString(); + // 未实施 + if(disposalType.value==='unExecute'){ + closeConfirmList.value = [ + {doc_id:1,name:'未实施情况说明(本单)',status:null}, + {doc_id:2,name:'原风险分析(归档备查,不重做)',status:null}, + {doc_id:3,name:'PSSR 检查项(标注「不涉及」)',status:null}, + {doc_id:4,name:'资料归档清单(转「不涉及」确认)',status:null}, + ]; + } + // 未投用 + if(disposalType.value==='unUse'){ + closeConfirmList.value = [ + {doc_id:1,name:'未投用情况说明(本单)',status:null}, + {doc_id:2,name:'原风险分析(归档备查,不重做)',status:null}, + {doc_id:3,name:'PSSR 检查项(标注「不涉及」)',status:null}, + {doc_id:4,name:'资料归档清单(转「不涉及」确认)',status:null}, + ]; + } + // 申请恢复 + if(disposalType.value==='recover'){ + closeConfirmList.value = [ + {doc_id:1,name:'恢复操作票(执行记录与照片)',status:null}, + {doc_id:2,name:'恢复后联锁 / 报警设定值回装核对记录',status:null}, + {doc_id:3,name:'更新的设备 / 安全阀台账',status:null}, + ]; + } + // 申请延期 + if(disposalType.value==='delay'){ + closeConfirmList.value = [ + {doc_id:1,name:'延期审批子单(随原单归档)',status:null}, + {doc_id:2,name:'原风险分析记录(延期期间持续有效确认)',status:null}, + {doc_id:3,name:'到期提醒与再评估记录',status:null}, + ]; + } + // 申请转永久 + if(disposalType.value==='transform'){ + closeConfirmList.value = [ + {doc_id:1,name:'转永久审批子单)',status:null}, + {doc_id:2,name:'补充风险分析报告(HAZOP / FMEA)',status:null}, + {doc_id:3,name:'规程 / 台账正式版',status:null}, + {doc_id:4,name:'原临时变更关闭记录',status:null}, + ]; + } } } }) @@ -381,590 +553,613 @@ onMounted(async () => { diff --git a/src/views/my/initiateChange/modules/footer.vue b/src/views/my/initiateChange/modules/footer.vue index 8435ab2..6104387 100644 --- a/src/views/my/initiateChange/modules/footer.vue +++ b/src/views/my/initiateChange/modules/footer.vue @@ -246,7 +246,7 @@ defineExpose({clearApprovers, setRegenerateLoading}) - 完成设置 + 完成设置 diff --git a/src/views/my/myTask/index.vue b/src/views/my/myTask/index.vue index 3a7c977..5de8222 100644 --- a/src/views/my/myTask/index.vue +++ b/src/views/my/myTask/index.vue @@ -1,5 +1,5 @@