处置申请完成

This commit is contained in:
2026-09-18 14:04:16 +08:00
parent ef8bca3c92
commit da61394d36
5 changed files with 948 additions and 709 deletions
+17
View File
@@ -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,
});
}
-1
View File
@@ -178,7 +178,6 @@ const handleFileType = (type:string) => {
}
// 打开预览
const preview = (file:any) => {
console.log(file)
previewFile.value = file;
previewModal.value = true;
}
+493 -298
View File
@@ -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<any>(null);
// 当前变更id
const currentId = ref<number>(0);
// 当前变更状态
const currentStatus = ref<string>('');
// 是否开启 AI 辅助功能
const isAiOn = ref<boolean>(false);
// 变更等级选项
@@ -33,6 +36,27 @@ const typeList = ref<any>([]);
const level = computed(() => {
return currentInfo.value?.apply_form?.change_level;
});
// 处置申请类型 unExecute:未实施;unUse:未投用;recover:申请恢复;delay:申请延期;transform:申请转为永久
const disposalType = ref<string>('');
// 情况说明
const description = ref<string>('');
// 影响判定
const judge = ref<string>('');
// 延期时间
const delayDate = ref<number | null>(null);
// 审批流程抽屉
const approvalDrawer = ref<boolean>(false);
// 存储每个步骤选中的审核人 user_id
const approvers = ref<Record<number, number[]>>({})
const flowList = ref<any[]>([]);
const steps = ref<any[]>([]);
// 审批链
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<any>([]);
// 验收评价
const acceptList = ref<any>([]);
// 当前预览文件
const previewFile = ref<any>(null);
// 预览文件弹框
const previewModal = ref<boolean>(false);
// 等级判断问题
const questions = ref<any>([
{
@@ -130,75 +165,10 @@ const questions = ref<any>([
]);
const role = ref<string>("applicant");
// 判定说明抽屉
const describeDrawer = ref<boolean>(false);
const levelDrawer = ref<boolean>(false);
// ai 标注抽屉
const aiDrawer = ref<boolean>(false);
const mandClosed = ref<boolean>(false);
const confirmClose = () => {
mandClosed.value = true;
window.$message?.success('强制合规项已确认闭环,操作已留痕');
}
const conflictHide = ref<boolean>(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<boolean>(false);
const roleName = ref<string>('张工艺');
const suggs = ref([
{ id: 1, text: "将未知杂质含量纳入日常分析计划(每周 1 次,连续 8 周)", st: "待定" },
{ id: 2, text: "修订操作规程并补充超温应急处置卡后再投用", st: "待定" },
]);
const opinion = ref<string>("");
// 已阅 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<boolean>(false);
const aiAssistStep = ref<string>('');
const aiAssistProgress = ref<number>(0);
// 强制合规描述(静态示例作 fallback)
const mandText = ref<string>('本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。');
const mandItem = ref<string>('联锁测试记录须在投用前完成并上传归档');
// 冲突提示
const conflictText = ref<string>('涉及设备 R-101 关联变更 MOC-2026-0038「机封冲洗方案调整」正在审批中,请确认施工时序与隔离方案无冲突。');
// 属地影响摘要
const deptRows = ref([
{ label: '涉及单元:', text: '一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。' },
{ label: '现行规程影响:', text: '操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。' },
{ label: '交接班提示:', text: '投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。' },
]);
// 参考信息(相似变更 / 历史经验)
const similarText = ref<string>('相似变更 MOC-2025-0112「酯化反应温度上调」投用后运行正常,无异常记录');
const experienceText = ref<string>('历史经验:温度类变更投用后首周超温报警占比约 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<number, number[]>) {
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);
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: []
});
// 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. 过滤掉不需要的字段
}
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,11 +553,11 @@ onMounted(async () => {
<template>
<NSpace vertical :size="16">
<!-- 本专业高风险 -->
<!-- 处置申请详情 -->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<!-- 顶部 -->
<div
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
class="flex flex-wrap items-center gap-3 px-3 py-2 mb-3 bg-[#fff]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton ghost type="primary" @click="back">
@@ -393,19 +565,17 @@ onMounted(async () => {
</NButton>
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
<NTag size="small" type="info" class="font-mono text-xs">编号{{ currentInfo?.change_no }}</NTag>
<!-- 审批中 -->
<NButton
v-if="currentStatus === 'APPROVING' && isAiOn"
ghost
size="small"
type="primary"
class="text-xs"
@click="aiDrawer = !aiDrawer"
>
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 标注
</NButton>
</div>
<div class="space-y-3 scroll">
<!-- 主体 -->
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<!-- 左侧 -->
<NGi span="24 s:24 m:24 l:16">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center flex-wrap pb-2.5 mb-3 border-b">
<span class="text-slate-800 font-600 text-sm">变更全部信息</span>
<p class="text-slate-400 text-xs ml-1 mt-1px">审批人视角 · 只读参考本次操作以子单 / 单据形式关联不回改原单</p>
</div>
<div class="space-y-3 left-scroll">
<!-- 变更申请表 -->
<div class="flex items-center justify-between">
<p
@@ -536,7 +706,7 @@ onMounted(async () => {
</td>
<td>
<div class="flex items-center justify-center gap-2">
<NButton text type="primary" class="text-xs" @click="previewFile">预览</NButton>
<NButton text type="primary" class="text-xs" @click="openPreview(doc)">预览</NButton>
<NButton text type="primary" class="text-xs" @click="downloadFile('single')">下载</NButton>
</div>
</td>
@@ -598,7 +768,7 @@ onMounted(async () => {
<span class="text-sm font-semibold text-slate-700">PSSR 检查内容</span>
</div>
<div class="space-y-2.5 border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
<div v-for="(pssr,index) in pssrlist" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
<div v-for="(pssr,index) in pssrList" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
<div class="bg-slate-100 px-3 py-1.6 font-medium text-slate-600">{{pssr.g}}</div>
<div v-for="item in pssr.items" :key="item.id" class="flex items-center border-t px-3 py-1.6">
<p class="flex-1">{{item.content}}</p>
@@ -710,6 +880,145 @@ onMounted(async () => {
</div>
</NCard>
</NGi>
<!-- 右侧 -->
<NGi span="24 s:24 m:24 l:8">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between flex-wrap pb-2.5 mb-3 border-b">
<span v-if="disposalType === 'unExecute'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">未实施情况说明表</span>
<span v-if="disposalType === 'unUse'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">未投用情况说明表</span>
<span v-if="disposalType === 'recover'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">变更恢复申请表</span>
<span v-if="disposalType === 'delay'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">变更延期申请表</span>
<span v-if="disposalType === 'transform'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">临时转永久申请表</span>
<NButton v-if="isAiOn" size="small" type="primary" round class="text-xs">AI辅助填报</NButton>
</div>
<div class="right-scroll">
<!-- 未实施情况说明 -->
<template v-if="disposalType === 'unExecute'">
<!-- 情况说明 -->
<div class="space-y-1 mt-1">
<p class="text-xs font-600 text-slate-600 flex items-center">情况说明<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写情况说明(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
</div>
<!-- 影响判定 -->
<div class="space-y-1 mt-3">
<p class="text-xs font-600 text-slate-600 flex items-center">{{isAiOn?'影响判定(AI 辅助)':'影响判定'}}</p>
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
</div>
</template>
<!-- 未投用情况说明 -->
<template v-if="disposalType === 'unUse'">
<!-- 情况说明 -->
<div class="space-y-1 mt-1">
<p class="text-xs font-600 text-slate-600 flex items-center">情况说明<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写情况说明(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
</div>
<!-- 影响判定 -->
<div class="space-y-1 mt-3">
<p class="text-xs font-600 text-slate-600 flex items-center">{{isAiOn?'影响判定(AI 辅助)':'影响判定'}}</p>
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
</div>
</template>
<!-- 申请恢复 -->
<template v-if="disposalType === 'recover'">
<!-- 原因 -->
<div class="space-y-1 mt-1">
<p class="text-xs font-600 text-slate-600 flex items-center">申请恢复原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写申请恢复原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
</div>
<!-- 风险分析 -->
<div class="space-y-1 mt-3">
<p class="text-xs font-600 text-slate-600 flex items-center">恢复作业风险分析</p>
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
</div>
</template>
<!-- 申请延期 -->
<template v-if="disposalType === 'delay'">
<!-- 原因 -->
<div class="space-y-1 mt-1">
<p class="text-xs font-600 text-slate-600 flex items-center">变更延期原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写变更延期原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
</div>
<div class="flex items-center flex-wrap gap-2 mt-3">
<p class="flex items-center">延期至<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NDatePicker v-model:value="delayDate" type="date" size="small" class="w-120px" />
<p class="text-xs text-slate-400">原到期 2026-08-20 · 至多延期 1 </p>
</div>
<!-- 风险分析 -->
<div class="space-y-1 mt-3">
<p class="text-xs font-600 text-slate-600 flex items-center">延期风险分析</p>
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
</div>
</template>
<!-- 申请转永久 -->
<template v-if="disposalType === 'transform'">
<!-- 原因 -->
<div class="space-y-1 mt-1">
<p class="text-xs font-600 text-slate-600 flex items-center">申请转永久原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写申请转永久原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
</div>
<!-- 风险分析 -->
<div class="space-y-1 mt-3">
<p class="text-xs font-600 text-slate-600 flex items-center">风险分析缺口补充按永久变更要求</p>
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
</div>
</template>
<!-- 风险分析行动项落实 -->
<div class="rounded-md mt-4 border">
<div class="flex items-center gap-2 py-2 px-2 border-b">
<span class="text-xs font-600 text-slate-600 flex items-center">风险分析行动项落实</span>
<p class="text-xs text-slate-400">{{confirmedPssr}}/{{totalPssr}} 已确认</p>
</div>
<div class="p-2 space-y-2">
<div v-for="(pssr,index) in pssrList" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
<div class="bg-slate-100 px-2 py-1.5 font-medium text-xs text-slate-600 flex items-center justify-between">
{{pssr.g}}
<NButton text size="small" type="primary" class="text-xs" @click="addItem(index)">
<Icon icon="ic:round-plus" class="size-12px" />添加检查项
</NButton>
</div>
<div v-for="(item,subIndex) in pssr.items" :key="subIndex" class="flex items-center border-t px-2 py-1 gap-2">
<NInput v-if="!item.id" v-model:value="item.content" size="small" type="textarea" :autosize="{minRows: 1}" placeholder="填写检查项内容" class="w-full text-xs"></NInput>
<NButton v-if="!item.id" text type="success" @click="saveItem(index, subIndex)" title="保存检查项">
<Icon icon="boxicons:save" class="size-16px" />
</NButton>
<NButton v-if="!item.id" text type="error" @click="pssrList[index].items.splice(subIndex, 1)" title="删除">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
<p v-if="item.id" class="flex-1 text-xs">{{item.content}}</p>
<NButton v-if="item.confirm_flag" size="tiny" ghost type="success" class="text-xs" @click="item.confirm_flag = 0">已确认</NButton>
<NButton v-else size="tiny" ghost type="warning" class="text-xs" @click="item.confirm_flag = 1">待确认</NButton>
</div>
</div>
</div>
</div>
<!-- 资料关闭确认 -->
<div class="rounded-md mt-4 border">
<div class="flex items-center gap-2 py-2 px-2">
<span class="text-xs font-600 text-slate-600 flex items-center">资料关闭确认</span>
<p class="text-xs text-slate-400">提交后纳入资料关闭流程统一归档</p>
</div>
<div v-for="item in closeConfirmList" :key="item.doc_id" class="flex items-center gap-2 py-2 px-2 border-t">
<p class="flex-1 text-xs flex items-center">{{item.name}}</p>
<NButton :type="item.status===1?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 1">已归档</NButton>
<NButton :type="item.status===2?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 2">待归档</NButton>
<NButton :type="item.status===0?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 0">不涉及</NButton>
</div>
</div>
</div>
</NCard>
</NGi>
</NGrid>
<!-- 底部 -->
<div
class="flex flex-wrap items-center justify-end gap-3 px-3 py-2 mt-3 bg-[#fff]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton size="small" @click="approvalDrawer = true">审批流程设置</NButton>
<NButton size="small" @click="submitDisposal('save')">保存</NButton>
<NButton size="small" type="primary" @click="submitDisposal('submit')">提交</NButton>
</div>
<!-- 变更类型判定说明右侧抽屉 -->
<NDrawer v-model:show="describeDrawer" :width="576" placement="right">
<NDrawerContent title="变更类型判定说明" closable>
@@ -772,199 +1081,85 @@ onMounted(async () => {
</div>
</NDrawerContent>
</NDrawer>
<!-- AI 审批辅助右侧抽屉查看 -->
<NDrawer v-model:show="aiDrawer" :width="576" placement="right">
<NDrawerContent closable>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawer" placement="right" width="28rem">
<NDrawerContent>
<template #header>
<span class="flex items-center gap-1.5 text-sm font-bold text-slate-700">
<Icon icon="lucide:sparkles" class="size-16px text-violet-500 mr-1" />
AI 审批辅助抽屉式正文全窗口阅读
</span>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<div class="space-y-4">
<!-- AI 分析进度异步任务轮询 -->
<div v-if="aiAssistLoading" class="rounded-lg border border-violet-200 bg-violet-50 px-3 py-2.5 text-xs text-violet-700">
<div class="flex items-center gap-2">
<Icon icon="eos-icons:loading" class="size-14px" />
<span>{{ aiAssistStep || 'AI 分析中…' }}{{ aiAssistProgress }}%</span>
<div class="h-full space-y-4 overflow-y-auto">
<p class="text-xs leading-relaxed text-slate-400">审批链{{ approvalFlowText }}专业会签人由申请表涉及专业勾选自动增减可在此调整人选重要变更自动追加安全审查与分管领导终批</p>
<div class="rounded-lg border p-3" v-for="(item,index) in flowList" :key="item.id">
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
<NTag type="info" size="small">{{Number(index)+1}}</NTag> {{item.name}} {{item.countersign ? '(专业会签)' : ''}}
</div>
<NProgress type="line" :percentage="aiAssistProgress" :show-indicator="false" class="mt-1.5" processing />
<NSelect :value="approvers[item.id]" multiple :options="item.members" label-field="user_name" value-field="user_id"
@update:value="(v) => updateApprover(item.id, v)" />
</div>
<!-- 合规警告条强制合规项不可关闭闭环后转绿 -->
<div
class="px-3 py-2.5 text-sm"
:class="mandClosed ? 'text-[var(--success-color)] bg-[var(--success-bg-color)]' : 'text-[var(--warning-color)] bg-[var(--warning-bg-color)]'"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--success-color':themeStore.otherColor.success,
'--success-bg-color':getColorWithOpacity(themeStore.otherColor.success,0.1),
'--warning-color':themeStore.otherColor.warning,
'--warning-bg-color':getColorWithOpacity(themeStore.otherColor.warning,0.1)
}"
>
<div class="flex items-center gap-2">
<Icon icon="proicons:alert-triangle" class="size-16px" />
<span class="font-semibold">强制合规</span>
<span class="ml-auto shrink-0">
<NTag v-if="mandClosed" size="small" type="success">
<template #icon>
<Icon icon="lucide:shield-check" class="size-12px" />
</template>
已闭环
</NTag>
<NTag v-else size="small" type="warning">未闭环</NTag>
</span>
</div>
<p class="mt-1 text-xs leading-relaxed">
{{ mandText }}
</p>
<div v-if="!mandClosed" class="mt-1 text-right">
<NButton text type="primary" class="text-xs" @click="confirmClose">确认闭环留痕</NButton>
</div>
</div>
<!-- 变更冲突提示同设备在途变更可收起 -->
<div
v-if="!conflictHide"
class="bg-[var(--bg-color)] px-3 py-2.5 text-sm text-[var(--theme-color)]"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--theme-color':themeStore.themeColor,
'--bg-color':getColorWithOpacity(themeStore.themeColor,0.1),
}"
>
<div class="flex items-center gap-2">
<Icon icon="lucide:bell" class="size-14px" />
<span class="font-semibold">冲突提示</span>
<NButton text type="primary" class="ml-auto shrink-0 text-xs" @click="conflictHide = true">知道了</NButton>
</div>
<p class="mt-1 text-xs leading-relaxed">
{{ conflictText }}
</p>
</div>
<!-- 属地影响摘要仅属地负责人视角 -->
<div v-if="role === 'dept'" class="border" :style="{borderRadius:themeStore.themeRadius+'px'}">
<div class="flex items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="text-sm font-semibold text-slate-700">属地影响摘要</span>
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
</div>
<div class="space-y-1.5 px-3 py-2.5 text-xs leading-relaxed text-slate-600">
<p v-for="(r, i) in deptRows" :key="i"><b class="text-slate-700">{{ r.label }}</b>{{ r.text }}</p>
</div>
</div>
<!-- 变更前后关键参数对比变化标蓝 / 超限标红 -->
<div
class="border"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--error-color':themeStore.otherColor.error,
'--theme-color':themeStore.themeColor
}"
>
<div class="border-b bg-slate-50/70 px-3 py-2 text-xs font-semibold text-slate-600">
关键参数对比AI 提取 · <span class="font-bold text-[var(--theme-color)]">=变化</span> · <span class="font-bold text-[var(--error-color)]">=超限</span>
</div>
<div v-for="d in paramDiff" :key="d.k" class="border-b px-3 py-2 last:border-b-0">
<div class="flex flex-wrap items-baseline gap-x-2 text-sm">
<span class="w-24 shrink-0 text-xs text-slate-500">{{ d.k }}</span>
<span class="text-xs text-slate-400">{{ d.before }}</span>
<span class="text-slate-300"></span>
<span
class="text-sm font-semibold"
:class="d.over ? 'text-[var(--error-color)]' : 'text-[var(--theme-color)]'"
>{{ d.after }}</span>
<NTag v-if="d.over" size="small" type="error">超限</NTag>
</div>
<div
class="mt-0.5 text-[11px] leading-snug"
:class="d.over ? 'text-[var(--error-color)]' : 'text-slate-400'"
>{{ d.note }}</div>
</div>
</div>
<!-- AI 审批建议版本冻结 · 建议分级 · 已阅留痕 · 无感审计 -->
<div
class="border"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--error-color':themeStore.otherColor.error,
'--theme-color':themeStore.themeColor,
'--warning-color':themeStore.otherColor.warning,
}"
>
<div class="flex flex-wrap items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
<Icon icon="lucide:sparkles" class="size-14px text-violet-500" />
<span class="text-sm font-semibold text-slate-700">AI 审批建议</span>
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
<NTag size="small">基于提交版本 V1 一次性分析 · 版本冻结</NTag>
<div class="ml-auto">
<NTag v-if="aiRead" size="small" type="success"> {{ roleName }} 已阅 AI 建议已留痕</NTag>
<NButton v-else size="small" class="text-xs" @click="readSuggest">已阅 AI 建议</NButton>
</div>
</div>
<div class="space-y-3 p-3">
<!-- 强制合规项 -->
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--error-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--error-color)]" /> 强制合规项驳回前必须处理否则无法通过</div>
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-red-100 bg-red-50/50 px-3 py-2 text-sm">
<span class="min-w-0 flex-1 text-slate-700">{{ mandItem }}</span>
<NTag v-if="mandClosed" size="small" type="success">已闭环</NTag>
<NTag v-else size="small" type="warning">未闭环</NTag>
</div>
<!-- 推荐改进项 -->
<div>
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--warning-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--warning-color)]" /> 推荐改进项可采纳为行动项可忽略但须填理由</div>
<div class="space-y-1.5">
<div v-for="s in suggs" :key="s.id" class="rounded-lg border px-3 py-2 text-sm">
<div class="leading-relaxed text-slate-700">{{ s.text }}</div>
<div class="mt-1.5">
<div v-if="s.st === '待定'" class="flex gap-3">
<NButton text type="success" class="text-xs hover:underline"
@click="acceptSuggest(s)">
采纳为行动项
</NButton>
<NButton text class="text-xs hover:underline"
@click="rejectSuggest(s)">
已知悉不采纳
</NButton>
</div>
<NTag v-else-if="s.st === '已采纳'" size="small" type="success">已采纳 已生成 PSSR 行动项</NTag>
<NTag v-else size="small" type="default">已知悉不采纳理由已留痕</NTag>
</div>
</div>
</div>
</div>
<!-- 参考信息 -->
<div>
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--theme-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--theme-color)]" /> 参考信息纯参考无强制力</div>
<div class="space-y-1.5">
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm">
<span class="min-w-0 flex-1 text-slate-700">{{ similarText }}</span>
<NButton text type="primary" class="hover:underline text-xs" @click="viewArchive('MOC-2025-0112')">查看归档包</NButton>
</div>
<div class="rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm text-slate-700">
{{ experienceText }}
</div>
</div>
</div>
<div class="rounded-lg bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-400">
查看采纳已阅等操作均由后台自动记录无感审计供内审追溯AI 建议不强制采纳决策主导权在审批人驳回重新提交后AI 将对新版本重新分析一次
</div>
</div>
</div>
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</div>
</NDrawerContent>
</NDrawer>
<!-- 附件在线预览弹窗 -->
<NModal
v-model:show="previewModal"
preset="card"
:auto-focus="false"
:style="{ width: '80%', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
</div>
</template>
<template #default>
<div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
<span>上传人{{ previewFile?.uploader_name }}</span>
<span>上传时间{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本${previewFile?.version}受控` }}</span>
</div>
<div v-if="previewFile" class="max-h-[70vh] overflow-x-auto rounded-lg border">
<!-- 图片预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'image'">
<div class="flex items-center justify-center py-2">
<NImage :src="previewFile?.file_url" />
</div>
</template>
<!-- Word 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'word'">
<VueOfficeDocx :src="previewFile?.file_url" class="w-full items-center" />
</template>
<!-- Excel 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'excel'">
<VueOfficeExcel :src="previewFile?.file_url" class="w-full items-center" />
</template>
<!-- Ppt 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'ppt'">
<VueOfficePptx :src="previewFile?.file_url" class="w-full items-center" />
</template>
<!-- PDF 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'pdf'">
<div class="h-[68vh]">
<PDF :src="previewFile?.file_url" class="w-full h-full items-center" />
</div>
</template>
</div>
</div>
</template>
</NModal>
</NSpin>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 210px);
.left-scroll {
height: calc(100vh - 307px);
overflow-y: auto;
}
.right-scroll {
height: calc(100vh - 315px);
overflow-y: auto;
}
</style>
@@ -246,7 +246,7 @@ defineExpose({clearApprovers, setRegenerateLoading})
<NSelect :value="approvers[item.id]" multiple :options="item.members" label-field="user_name" value-field="user_id"
@update:value="(v) => updateApprover(item.id, v)" />
</div>
<NButton type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</div>
</NDrawerContent>
</NDrawer>
+81 -53
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
@@ -64,29 +64,10 @@ const tabChange = (k: string) => {
}
};
const SUB_ORDERS: any[] = [];
const subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
const convPassed = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status === "已通过"));
const convActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status !== "已驳回"));
const extActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "extend" && s.status === "审批中"));
const opts = computed<Opt[]>(() => {
const list: Opt[] = [
{ act: "restore", name: "申请恢复", desc: "投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档" },
];
if (currentRow.value?.dur === "临时" && !convPassed.value) {
list.push(
{ act: "extend", name: currentRow.value?.extended ? "申请延期(已延期 1 次)" : "申请延期", desc: "临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次", disabled: !!currentRow.value?.extended || extActive.value, note: extActive.value ? "延期子单审批中" : currentRow.value?.extended ? "本变更已延期 1 次,不可再次延期" : "" },
{ act: "permanent", name: "申请转为永久", desc: "临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭", disabled: convActive.value, note: convActive.value ? "转永久子单审批中 / 已通过" : "" },
);
// 处置申请跳转
const jumpTo = (act: string) => {
routerPushByKey("details_disposal",{ query: { id: currentRow.value?.change_id.toString(),type:act } });
}
list.push(
{ act: "unimpl", name: "未实施情况说明", desc: "审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程" },
{ act: "unuse", name: "未投用情况说明", desc: "变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程" },
);
return list;
});
// 打开详情、修改重报
const openDetail = (c: any,type:string = '') => {
@@ -101,7 +82,7 @@ const openDetail = (c: any,type:string = '') => {
// 打开资料关闭
const openDocClose = (c: any) => {
currentRow.value = c;
// routerPushByKey("my_docclose",{ query: { id: c.change_id.toString() } });
routerPushByKey("details_closedoc",{ query: { id: c.change_id.toString() } });
};
// 打开处置申请
const openDisposal = (c: any,) => {
@@ -384,7 +365,6 @@ onMounted(() => {
<div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
<!-- <NButton class="text-xs" text type="primary" @click="openDisposal(c)">处置申请</NButton> -->
<NButton class="text-xs" text type="error" @click="openWithdraw(c)">撤回</NButton>
</div>
<!-- 审批通过 -->
@@ -493,39 +473,40 @@ onMounted(() => {
v-model:show="disposalModal"
preset="card"
:auto-focus="false"
:style="{ width: '670px', height: 'auto' }"
:style="{ width: '700px', height: 'auto' }"
:segmented="{ content: true, footer: false }"
>
<template #header>
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-base font-semibold text-slate-800">处置申请 · {{ currentRow?.title }}</h3>
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号{{ currentRow?.id }}</span>
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号{{ currentRow?.change_no }}</span>
</div>
</template>
<template #default>
<div class="rounded-lg border bg-white px-4 py-3">
<div class="flex flex-wrap items-center gap-2">
<NTag v-if="currentRow?.level=== '重要'" type="error" size="small">{{ currentRow?.level }}</NTag>
<NTag v-else type="primary" size="small">{{ currentRow?.level }}</NTag>
<NTag v-if="currentRow?.dur=== '临时'" type="warning" size="small">{{ currentRow?.dur }}</NTag>
<NTag v-else type="primary" size="small">{{ currentRow?.dur }}</NTag>
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='实施中'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待PSSR'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='已关闭'" size="small" type="primary">{{currentRow?.status}}</NTag>
<span v-if="currentRow?.applyTime" class="text-[11px] text-slate-400">申请 {{ currentRow?.applyTime }}</span>
<span v-if="currentRow?.dur === '临时' && currentRow?.deadline" class="text-[11px] text-amber-600">
到期 {{ currentRow?.deadline }}{{ (currentRow?.day ?? 0) < 0 ? ` · 已超期 ${-(currentRow?.day ?? 0)} ` : currentRow?.day !== undefined ? ` · ${currentRow?.day} ` : '' }}
</span>
<!-- 变更等级 -->
<NTag
size="small"
:color="{
color: tagBgColor(getChangeLevel(currentRow?.change_level)?.color),
textColor: tagTextColor(getChangeLevel(currentRow?.change_level)?.color),
borderColor: tagBorderColor(getChangeLevel(currentRow?.change_level)?.color),
}"
>{{getChangeLevel(currentRow?.change_level)?.label}}</NTag>
<!-- 变更时限 -->
<NTag v-if="currentRow?.duration_type===2" size="small" type="warning">临时</NTag>
<NTag v-if="currentRow?.duration_type===1" size="small" type="primary">永久</NTag>
<NTag v-if="currentRow?.urgent===1" size="small" type="error">紧急</NTag>
<!-- 变更状态 -->
<NTag v-if="currentRow?.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="currentRow?.status==='APPROVED'" size="small" type="success">已通过</NTag>
<span v-if="currentRow?.apply_time" class="text-[11px] text-slate-400">申请 {{ currentRow?.apply_time.slice(0, 16) }}</span>
<!-- 超期/临期/延期 -->
<NTag v-if="currentRow?.overdue_status===2" size="small" type="error">已超期 {{ currentRow?.overdue_days }} </NTag>
<NTag v-if="currentRow?.overdue_status===1" size="small" type="warning">临期 · {{ currentRow?.overdue_days }} </NTag>
<NTag v-if="currentRow?.extend_count" size="small">已延期 {{ currentRow?.extend_count }} </NTag>
</div>
<div class="mt-1 text-[11px] text-slate-400">{{ currentRow?.detail }}</div>
</div>
<!-- 进行中的子单提示 -->
<div v-if="pendingSubs.length" class="rounded-lg border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-700">
已有子单在审批中{{ pendingSubs.map((s:any) => s.id).join('、') }} · 审批进度见待我处理列表行内展示
</div>
<!-- 处置选项 -->
<div class="rounded-lg border bg-white mt-3">
@@ -534,21 +515,68 @@ onMounted(() => {
<span class="ml-2 text-[11px] font-normal text-slate-400">申请恢复所有变更可发起申请延期 / 申请转为永久仅临时变更可用</span>
</div>
<div class="divide-y">
<div v-for="o in opts" :key="o.act" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<!-- 未实施情况说明 -->
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">{{ o.name }}</span>
<NTag v-if="o.note" type="warning" size="small">{{ o.note }}</NTag>
<span class="text-sm font-semibold text-slate-800">未实施情况说明</span>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ o.desc }}</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">审批通过后变更未实施填写情况说明未实施原因现场维持原状确认知会原审批人后直接转入关闭流程</div>
</div>
<NButton size="small" type="primary" :disabled="o.disabled">
<NButton size="small" type="primary" @click="jumpTo('unExecute')">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
<!-- 未投用情况说明 -->
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">未投用情况说明</span>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">变更已实施完成但未正式投用填写情况说明含现场状态与后续安排知会原审批人后直接转入关闭流程</div>
</div>
<NButton size="small" type="primary" @click="jumpTo('unUse')">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
<!-- 申请恢复 -->
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">申请恢复</span>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">投用后申请恢复原状生成恢复操作票执行类单据随单归档无需另行审批现场恢复并拍照确认后纳入归档</div>
</div>
<NButton size="small" type="primary" @click="jumpTo('recover')">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
<!-- 申请延期 -->
<div v-if="currentRow?.duration_type===2" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">申请延期</span>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">临时变更到期前申请延期子单走属地 + 主管部门简化审批通过后回写原单到期时间 · 至多延期 1 </div>
</div>
<NButton size="small" type="primary" @click="jumpTo('delay')">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
<!-- 申请转为永久 -->
<div v-if="currentRow?.duration_type===2" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">申请转为永久</span>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">临时运行一个验证周期效果稳定后转永久按永久变更补齐风险评估与完整审批链通过后原临时变更自动关闭</div>
</div>
<NButton size="small" type="primary" @click="jumpTo('transform')">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
</div>
</div>
</template>
</NModal>
</NCard>