发起变更,底部封装通用组件

This commit is contained in:
2026-09-04 11:24:27 +08:00
parent cb918ea562
commit 7d05236399
18 changed files with 2260 additions and 1608 deletions
@@ -1,34 +1,36 @@
<!-- 变更申请表 -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { localStg } from '@/utils/storage';
import { useChangeStore } from '@/store/modules/change';
import Footer from './footer.vue'
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
const themeStore = useThemeStore();
const changeStore = useChangeStore();
const props = defineProps({
isAiOpen: Boolean,
formInfo: Object as () => any
})
const emit = defineEmits(['updateBtn']);
const props = defineProps(['orgTree','templateList','typeList','levelList','isAiOpen','title','currentId','formInfo'])
const emit = defineEmits(['confirmTab']);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
const today = () => new Date().toISOString().slice(0, 10);
// 按钮禁用
const btnDisabled = ref<boolean>(false);
const footerRef = ref<any>(null);
const specialties = ['生产','技术', '仪表', '电气', '设备', '安全', '质量'];
// 总分数
const totalScore = ref<number>(0);
// 当前等级
const currentLevel = computed(() => props.levelList.find((x:{value:number | null, label:string, color:string}) => x.value === form.value.change_level));
// 当前用户
const userInfo = ref<any>(localStg.get('userInfo'));
// 预览文件
const previewFile = ref<string | null>(null);
// 抽屉类型
const sheet = ref<string>('');
// 表单
const form = ref<{
@@ -50,7 +52,7 @@ const form = ref<{
risk_tools: string[],
}>({
change_type: null,
change_level: computed(() => totalScore.value >= 20 ? 1 : 0),
change_level: computed(() => totalScore.value >= 20 ? 2 : 1),
duration_type: null,
restore_deadline: null,
org_id: null,
@@ -141,12 +143,44 @@ const LEVEL_DIMS = ref([
],
},
]);
const disciplines = ref<string[]>([]);
const signers = ref<Record<string, string>>({});
// 筛选涉及专业的人员和部门
const filterMembersBySpecialties = (data: any, specialties: string[]) => {
const result: any[] = [];
data.steps.forEach((step: any) => {
step.members.forEach((member: any) => {
if (member.specialty && specialties.includes(member.specialty)) {
result.push({
stepName: step.name, // 上级部门名称
member: member // 成员对象
});
}
});
});
return result;
}
// 当前模板
const currentTemplate = computed(() => {
let template = props.templateList.find((item: any) =>
item.level === form.value?.change_level &&
item.category === form.value?.change_type &&
item.duration === form.value?.duration_type
) || null;
footerRef.value?.clearApprovers();
changeStore.setStepsInfo(template?template.steps:[]);
return template?template:null;
});
// 联动专业会签人员
const signers = computed(() => {
if(currentTemplate.value){
const matchedMembers = filterMembersBySpecialties(currentTemplate.value, form.value.disciplines);
return matchedMembers
}
return null
})
// 按等级推荐
const recommendTools = () => {
const lv = form.value?.change_level === 1 ? "重要" : "一般";
form.value.risk_tools = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
const lv = totalScore.value >= 20 ? "重要" : "一般";
form.value.risk_tools = totalScore.value >= 20 ? ["HAZOP"] : ["RISK_CHECK"];
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
};
@@ -155,17 +189,6 @@ const toggleTool = (t: {key:string,label:string}) => {
const { risk_tools } = form.value;
form.value.risk_tools = risk_tools.includes(t.key) ? risk_tools.filter((key:string) => key !== t.key) : [...risk_tools, t.key];
};
// 涉及专业
const DISCIPLINE_SIGNER: Record<string, string> = {
技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)", 电气: "周设备(设备科 · 电气)", 环保: "王海燕(安全环保部)", 质量: "王海燕(质量部)",
};
const toggleDiscipline = (d:any) => {
const result = disciplines.value.reduce((acc: Record<string, string>, key: string) => {
acc[key] = DISCIPLINE_SIGNER[key]; // 如果键不存在,值为 undefined
return acc;
}, {});
signers.value = {...result};
};
// 变更类型判断
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
@@ -203,11 +226,17 @@ const levelAiLoading = ref<boolean>(false);
// 打开等级判定表抽屉
const openLevelSheet = async () => {
let form = props.formInfo;
if (!form?.description) {
if (!form?.description && props.isAiOpen) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
sheet.value = "level";
if (levelAiLoading.value) return;
};
// 使用AI 评分
const useAiLevel = async () => {
let form = props.formInfo;
if (levelAiLoading.value) return;
levelAiLoading.value = true;
levelAiLoading.value = true;
try {
const dims = LEVEL_DIMS.value.map((d:any, i:any) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o:any) => o.score)) }));
@@ -223,7 +252,7 @@ const openLevelSheet = async () => {
} finally {
levelAiLoading.value = false;
}
};
}
// 处理等级判定表评分变化
const handleScoreChange = (val:number,index:number) => {
LEVEL_DIMS.value[index].score = val;
@@ -233,91 +262,77 @@ const handleScoreChange = (val:number,index:number) => {
});
totalScore.value = score;
}
// 下载PDF
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
// 更新表单
const updateForm = (res: any, isShow: boolean) => {
}
// 更新按钮禁用状态
const updateBtnDisabled = (disabled: boolean) => {
btnDisabled.value = disabled;
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
defineExpose({form,updateForm,updateBtnDisabled})
defineExpose({form})
</script>
<template>
<div class="space-y-4">
<!-- 申请部门 / 申请人 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
<NInput v-model:value="form.manage_dept" placeholder="请输入申请部门" />
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
<NInput :value="userInfo?.real_name" disabled />
</div>
</div>
<!-- 变更目的 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
<div class="rounded-lg bg-[#EAF0F9]/40 p-3">
<NCheckboxGroup v-model:value="form.purposes">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
</div>
</div>
<!-- 预期效果 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
<NInput type="textarea" v-model:value="form.effect" rows="2" placeholder="" />
</div>
<!-- 主要变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.main_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.main_changes[i]" type="text" placeholder="" />
<NPopconfirm
v-if="form.main_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="form.main_changes.splice(i, 1)"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.main_changes = [...form.main_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
</NButton>
</div>
</div>
<!-- 连带变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.related_changes" :key="i" class="flex items-center gap-2">
<div class="p-4">
<div class="space-y-4 scroll">
<!-- 申请部门 / 申请人 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
<NTreeSelect
v-model:value="form.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择申请部门"
/>
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
<NInput :value="userInfo?.real_name" disabled />
</div>
</div>
<!-- 变更目的 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
<div class="rounded-lg bg-[#EAF0F9]/40 p-3">
<NCheckboxGroup v-model:value="form.purposes">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
</div>
</div>
<!-- 预期效果 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
<NInput type="textarea" v-model:value="form.effect" rows="2" placeholder="" />
</div>
<!-- 主要变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.main_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
<NInput v-model:value="form.main_changes[i]" type="text" placeholder="" />
<NPopconfirm
v-if="form.related_changes.length > 1"
v-if="form.main_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="form.related_changes.splice(i, 1)"
@positive-click="form.main_changes.splice(i, 1)"
>
<template #trigger>
<NButton text type="error">
@@ -326,159 +341,194 @@ defineExpose({form,updateForm,updateBtnDisabled})
</template>
确定删除吗
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.related_changes = [...form.related_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
</NButton>
</div>
</div>
<!-- 变更类型 / 变更等级 -->
<div class="grid gap-4 grid-cols-2">
<!-- 变更类型 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_type">
<div class="flex gap-x-2">
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
</div>
</NRadioGroup>
<NButton v-if="isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
<Icon icon="lucide:sparkles" class="size-12px" />
AI 判定说明
</NButton>
</div>
</div>
<!-- 变更等级 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_level" :disabled="isAiOpen">
<div class="flex gap-x-2">
<NRadio :key="0" :value="0">一般</NRadio>
<NRadio :key="1" :value="1">重要</NRadio>
</div>
</NRadioGroup>
<NButton v-if="isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }})
</NButton>
</div>
</div>
</div>
<!-- 变更时限 / 风险分析 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.duration_type">
<div class="flex gap-x-2">
<NRadio v-for="t in [0, 1]" :key="t" :value="t">
{{ t === 0 ? '永久' : '临时' }}
</NRadio>
</div>
</NRadioGroup>
<NCheckbox v-model:checked="form.urgent" :checked-value="1" :unchecked-value="0">紧急</NCheckbox>
</div>
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
class="text-sm"
:ghost="form.risk_tools.includes(t.key) ? false : true"
:type="form.risk_tools.includes(t.key) ? 'primary' : 'default'"
>
{{ t.label }}
</NButton>
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
按等级重新推荐
</NButton>
</div>
</div>
</div>
<!-- 计划投用时间 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
<div class="flex flex-1 flex-wrap items-center gap-2">
<NDatePicker v-model:value="form.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<!-- 临时 -->
<template v-if="form.duration_type === 1">
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
<NDatePicker v-model:value="form.restore_deadline" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
</template>
<!-- 永久 -->
<span v-else class="whitespace-nowrap text-xs text-slate-300">勾选临时后在此并排填写计划恢复时间</span>
</div>
</div>
<div v-if="form.urgent" class="text-xs text-red-500">紧急变更为单独标记走快速通道事后限期补办风险分析与完整审批</div>
<!-- 所需材料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
<div class="flex-1 space-y-2">
<div class="text-[11px] text-slate-400">施工所需材料生成变更申请表时系统自动生成支持手动修改</div>
<NInput type="textarea" v-model:value="form.materials" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
</div>
</div>
<!-- 资料上传 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
<div
class="relative"
:style="{'--theme-color':themeStore.themeColor}"
v-for="(f,index) in form.update_docs" :key="f"
>
<div
class="text-xs cursor-pointer flex items-center border py-1.5 px-2 rounded-md hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]"
title="点击在线预览"
@click="previewFile = f"
>
<Icon icon="akar-icons:file" class="size-14px mr-1" />{{ f }}
</div>
<NButton text type="error" class="absolute top-[-8px] right-[-8px] bg-[#fff]" @click="form.update_docs.splice(index, 1)">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</div>
<NButton @click="form.update_docs = [...form.update_docs, `资料_${form.update_docs.length + 1}.pdf`]">
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
<NButton text type="primary" class="text-xs" @click="form.main_changes = [...form.main_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
</NButton>
<span class="text-xs text-slate-400">点击附件名弹窗预览支持 PDF / Word / 图片在线查看</span>
</div>
</div>
<!-- 需更新的资料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.update_docs">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
</div>
</div>
<!-- 连带变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.related_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
<NPopconfirm
v-if="form.related_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="form.related_changes.splice(i, 1)"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.related_changes = [...form.related_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
</NButton>
</div>
</div>
<!-- 变更类型 / 变更等级 -->
<div class="grid gap-4 grid-cols-2">
<!-- 变更类型 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_type">
<div class="flex gap-x-2">
<NRadio v-for="t in props.typeList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
</div>
</NRadioGroup>
<NButton v-if="props.isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
<Icon icon="lucide:sparkles" class="size-12px" />
AI 判定说明
</NButton>
</div>
</div>
<!-- 变更等级 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_level" disabled>
<div class="flex gap-x-2">
<NRadio v-for="t in props.levelList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
</div>
</NRadioGroup>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }})
</NButton>
</div>
</div>
</div>
<!-- 变更时限 / 风险分析 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.duration_type">
<div class="flex gap-x-2">
<NRadio v-for="t in [1, 2]" :key="t" :value="t">
{{ t === 1 ? '永久' : '临时' }}
</NRadio>
</div>
</NRadioGroup>
<NCheckbox v-model:checked="form.urgent" :checked-value="1" :unchecked-value="0">紧急</NCheckbox>
</div>
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
class="text-sm"
:ghost="form.risk_tools.includes(t.key) ? false : true"
:type="form.risk_tools.includes(t.key) ? 'primary' : 'default'"
>
{{ t.label }}
</NButton>
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
按等级重新推荐
</NButton>
</div>
</div>
</div>
<div v-if="form.urgent" class="text-xs text-red-500 ml-27">紧急变更为单独标记走快速通道事后限期补办风险分析与完整审批</div>
<!-- 计划投用时间 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
<div class="flex flex-1 flex-wrap items-center gap-2">
<NDatePicker v-model:value="form.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<!-- 临时 -->
<template v-if="form.duration_type === 2">
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
<NDatePicker v-model:value="form.restore_deadline" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
</template>
<!-- 永久 -->
<span v-else class="whitespace-nowrap text-xs text-slate-300">勾选临时后在此并排填写计划恢复时间</span>
</div>
</div>
<!-- 所需材料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
<div class="flex-1 space-y-2">
<div class="text-[11px] text-slate-400">施工所需材料生成变更申请表时系统自动生成支持手动修改</div>
<NInput type="textarea" v-model:value="form.materials" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
</div>
</div>
<!-- 资料上传 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
<div
class="relative"
:style="{'--theme-color':themeStore.themeColor}"
v-for="(f,index) in form.update_docs" :key="f"
>
<div
class="text-xs cursor-pointer flex items-center border py-1.5 px-2 rounded-md hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]"
title="点击在线预览"
@click="previewFile = f"
>
<Icon icon="akar-icons:file" class="size-14px mr-1" />{{ f }}
</div>
<NButton text type="error" class="absolute top-[-8px] right-[-8px] bg-[#fff]" @click="form.update_docs.splice(index, 1)">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">多选变更关闭前逐项确认上传最新版本</span>
</div>
</div>
<!-- 涉及专业 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.disciplines" :on-update:value="toggleDiscipline">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in ['技术', '仪表', '电气', '环保', '质量']" :key="p" :value="p" :label="p" />
</div>
<NButton @click="form.update_docs = [...form.update_docs, `资料_${form.update_docs.length + 1}.pdf`]">
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
</NButton>
<span class="text-xs text-slate-400">点击附件名弹窗预览支持 PDF / Word / 图片在线查看</span>
</div>
</div>
<!-- 需更新的资料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.update_docs">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">多选变更关闭前逐项确认上传最新版本</span>
</div>
</div>
<!-- 涉及专业 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.disciplines">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in specialties" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">多选决定审批流中专业会签/审批的路由范围</span>
</div>
<!-- <div v-if="Object.keys(signers).length > 0" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-12px" />已联动专业会签
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ d }} · {{ p }}</NTag>
<span class="text-slate-400">在底部审批流程设置中可调整人选</span>
</div> -->
</div>
</div>
<div v-if="signers && signers.length" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-12px" />已联动专业会签
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ p?.member?.specialty }} · {{ p?.member?.user_name }}{{p?.stepName ? ` (${p.stepName})` : ''}}</NTag>
</div>
</div>
</div>
</div>
</div>
<!-- 侧边抽屉AI 类型判定 / 等级判定表 / 审批流程设置 -->
<Footer
ref="footerRef"
type="form"
:currentId="props.currentId"
:title="props.title"
:isAiOpen="props.isAiOpen"
:currentForm="form"
/>
<!-- 侧边抽屉AI 类型判定 / 等级判定表 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
<NDrawerContent>
<template #header>
@@ -515,17 +565,29 @@ defineExpose({form,updateForm,updateBtnDisabled})
<!-- 变更等级判定表 -->
<template v-if="sheet === 'level'">
<span v-if="levelAiLoading" class="flex items-center gap-1 text-md font-normal text-violet-500">
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" /> AI 评分中
</span>
<div v-else class="flex items-center gap-2">
<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" :type="totalScore >= 20 ? 'error' : 'warning'">
{{ form.change_level === 1 ? '重要变更' : '一般变更' }}
</NTag>
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> &lt;20 分为一般20 分为重要</span>
<div class="flex items-center gap-2">
<NButton v-if="props.isAiOpen" size="small" type="primary">
<div v-if="levelAiLoading" class="flex items-center gap-1 text-xs">
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />AI 评分中
</div>
<div v-else class="flex items-center gap-1 text-xs" @click="useAiLevel">
使用AI 评分
</div>
</NButton>
<div class="flex items-center gap-2">
<NTag
v-if="currentLevel"
size="small"
:color="{
color: tagBgColor(currentLevel?.color),
textColor: tagTextColor(currentLevel?.color),
borderColor: tagBorderColor(currentLevel?.color),
}"
>
{{ currentLevel?.label ? currentLevel.label+'变更' : '' }}
</NTag>
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> &lt;20 分为一般20 分为重要</span>
</div>
</div>
<div class="grid gap-3 grid-cols-2">
<div v-for="(dim, di) in LEVEL_DIMS" :key="dim.name" class="rounded-lg border p-3">
@@ -547,26 +609,27 @@ defineExpose({form,updateForm,updateBtnDisabled})
</div>
</div>
</div>
<div class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
<div v-if="props.isAiOpen && (levelAiText || levelAiBasis.some((x) => x))" class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-violet-700">
<Icon icon="lucide:sparkles" class="size-14px" />
AI 评分分析过程
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
<Icon icon="lucide:sparkles" class="size-14px" />
AI 评分分析过程
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中
</span>
</span>
</div>
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
<p v-if="levelAiText">{{ levelAiText }}</p>
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}</b>{{ levelAiBasis[di] }}</template>
</p>
<p class="text-slate-400">各维度得分均可人工改判修改留痕改判后等级自动回填至申请表</p>
<div class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
<p v-if="levelAiText">{{ levelAiText }}</p>
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}</b>{{ levelAiBasis[di] }}</template>
</p>
<p class="text-slate-400">各维度得分均可人工改判修改留痕改判后等级自动回填至申请表</p>
</div>
</div>
</template>
</div>
</NDrawerContent>
</NDrawer>
<!-- 附件在线预览弹窗 -->
<NModal
:show="previewFile !== null"
@@ -602,4 +665,14 @@ defineExpose({form,updateForm,updateBtnDisabled})
</div>
</template>
</NModal>
</template>
</template>
<style scoped lang="scss">
:deep(.n-radio) {
display: flex;
align-items: center;
}
.scroll {
height: calc(100vh - 393px);
overflow-y: auto;
}
</style>