变更申请表保存接口完成

This commit is contained in:
2026-09-02 16:28:26 +08:00
parent 4f47ac0318
commit cb918ea562
5 changed files with 503 additions and 405 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ export function applicationSaveApi(id: number, params: {
}) {
return request({
url: `/api/changes/${id}/apply-form`,
method: 'post',
method: 'put',
data: params
});
}
+68 -55
View File
@@ -12,7 +12,7 @@ import {
aiRiskOrganize,
aiRiskPreAnalysis,
} from "@/service/api/ai";
import { precheckSaveApi } from "@/service/api/change";
import { precheckSaveApi, applicationSaveApi } from "@/service/api/change";
import Ed from "./modules/Ed.vue";
import Hazop from "./modules/hazop.vue";
import Jsa from "./modules/jsa.vue";
@@ -228,7 +228,6 @@ const confirmTab = (t: string) => {
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成${TAB_NAME[tab.value]}(本页内容已刷新)`);
const download = () => window.$message?.success(`演示:${TAB_NAME[tab.value]}已下载(Word/PDF`);
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
const finishApprover = () => {
sheet.value = '';
window.$message?.success('审批流程已更新(底部固定行摘要同步刷新)');
@@ -589,6 +588,44 @@ const TAB_NAME: Record<string, string> = {
train: "变更培训内容", pssr: "PSSR检查内容", accept: "验收评价", close: "变更关闭确认表",
};
// 变更申请表表单必填判断
const changeApplyRules = () => {
if(title.value.trim()===''){
window.$message?.warning('请填写变更名称');
return true
}
let form = changeApplyRef.value?.form;
if(form?.manage_dept?.trim() === ''){
window.$message?.warning('请填写申请部门');
return true
}
if(form?.main_changes?.length===0){
window.$message?.warning('请添加主要变更内容');
return true
}
if(form?.main_changes.some((str:string)=>str.trim()==='')){
window.$message?.warning('请填写主要变更内容');
return true
}
if(form?.change_type===null){
window.$message?.warning('请选择变更类型');
return true
}
if(form?.change_level===null){
window.$message?.warning('请选择变更等级');
return true
}
if(form?.duration_type===null){
window.$message?.warning('请选择变更时限');
return true
}
if(form?.duration_type === 1 && form?.restore_deadline===null){
window.$message?.warning('请选择计划恢复时间');
return true
}
return false
}
// 保存草稿
const saveDraft = async () => {
// 变更预识别
@@ -617,6 +654,18 @@ const saveDraft = async () => {
updateLoading('save', false);
changePreRef.value?.updateBtnDisabled(false);
}
// 变更申请表
if(tab.value === 'form'){
if(changeApplyRules()){
return
}
updateLoading('save', true);
const {error} = await applicationSaveApi(currentId.value, changeApplyRef.value?.form);
if(!error){
window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
}
updateLoading('save', false);
}
}
// 更新按钮禁用状态
@@ -681,19 +730,18 @@ onMounted(async () => {
</div>
<!-- 中间 -->
<div class="p-4">
<div class="space-y-4" :class="tab === 'pre'?'scroll':'scroll2'">
<div :class="tab === 'pre'?'scroll':'scroll2'">
<!-- ===== 变更预识别 ===== -->
<template v-if="tab === 'pre'">
<div v-show="tab === 'pre'">
<ChangePre ref="changePreRef" @updateBtn="updateBtn" />
</template>
</div>
<!-- ===== 变更申请表 ===== -->
<template v-if="tab === 'form'">
<ChangeApply ref="changeApplyRef" :formInfo="changePreForm" @updateBtn="updateBtn" />
</template>
<div v-show="tab === 'form'">
<ChangeApply ref="changeApplyRef" :isAiOpen="aiEnabled" :formInfo="changePreForm" @updateBtn="updateBtn" />
</div>
<!-- ===== 风险分析记录表 ===== -->
<template v-if="tab === 'risk'">
<div v-show="tab === 'risk'" class="space-y-4">
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex flex-wrap items-center gap-2">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
@@ -805,9 +853,9 @@ onMounted(async () => {
</table>
</div>
</div>
</template>
</div>
<!-- 变更培训内容 -->
<template v-if="tab === 'train'">
<div v-show="tab === 'train'" class="space-y-4">
<div class="flex flex-wrap items-center gap-3 mb-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="lucide:graduation-cap" class="size-16px text-violet-500" />
@@ -827,9 +875,9 @@ onMounted(async () => {
</div>
<NInput v-model:value="trainText" type="textarea" :bordered="false" :autosize="{minRows: 4}" class="font-mono text-sm" placeholder="" />
</div>
</template>
</div>
<!-- PSSR 检查内容 -->
<template v-if="tab === 'pssr'">
<div v-show="tab === 'pssr'" class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="tabler:clipboard-check" class="size-16px text-violet-500" />
@@ -914,9 +962,9 @@ onMounted(async () => {
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
清单由 AI 依据变更类型与风险分析自动生成专业人员审核后随审批发布确认方式不作硬性要求可在 PC 端直接点击确认也可在移动端拍照上传作为佐证系统自动记录确认人与日期资料归档组确认后自动同步变更关闭确认表显示已归档
</div>
</template>
</div>
<!-- 验收评价 -->
<template v-if="tab === 'accept'">
<div v-show="tab === 'accept'" class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="ix:success" class="size-18px text-emerald-500" />
@@ -1050,9 +1098,9 @@ onMounted(async () => {
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
流程说明投用运行满验收周期后系统向车间属地与涉及专业推送验收任务各责任单位按上表逐项给出评估结论并上传佐证数据报表 / 化验报告 / 现场照片全部完成后汇总至综合验收结论结论与佐证资料一并纳入变更关闭校验验收不通过时退回整改或评估恢复原状
</div>
</template>
</div>
<!-- 变更关闭确认表 -->
<template v-if="tab === 'close'">
<div v-show="tab === 'close'" class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="quill:folder-open" class="size-16px" />
@@ -1125,7 +1173,7 @@ onMounted(async () => {
<div class="rounded-lg bg-[#EAF0F9] px-4 py-2.5 text-xs leading-5 text-[#17407F]">
关闭校验说明判定为的资料项全部归档后系统将全部流程完毕信号推送变更主管部门专责专责确认后点击关闭系统记录变更关闭时间 / 不涉及 AI 辅助判定一次人工改判须填写理由并留痕临时变更恢复完成后恢复操作票与参数核对记录自动追加至本清单
</div>
</template>
</div>
</div>
</div>
@@ -1179,42 +1227,7 @@ onMounted(async () => {
</template>
</div>
</div>
<!-- 附件在线预览弹窗 -->
<NModal
:show="previewFile !== null"
preset="card"
:auto-focus="false"
:style="{ width: '700px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@update:show="(v: boolean) => !v && (previewFile = null)"
>
<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 }}
</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>上传人{{ ROLES[0].person }}</span><span>上传时间{{ today() }}</span><span>版本V1受控</span>
</div>
<div class="flex h-72 flex-col items-center justify-center gap-2 rounded-lg border bg-slate-50 text-slate-400">
<Icon icon="akar-icons:file" class="size-42px" />
<span class="text-sm">文件在线预览区演示</span>
<span class="text-xs">正式环境由文档预览服务渲染 PDF / Word / 图片内容支持缩放翻页与移动端查看</span>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton ghost size="small" @click="downloadPdf">
<Icon icon="material-symbols:download" class="size-16px" /> 下载
</NButton>
<NButton size="small" type="primary" @click="previewFile = null">关闭</NButton>
</div>
</template>
</NModal>
<!-- 侧边抽屉AI 类型判定 / 等级判定表 / 审批流程设置 -->
<!-- 侧边抽屉审批流程设置 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
<NDrawerContent>
<template #header>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, h, ref, watch } from 'vue'
import { NButton, NDataTable, NInput } from 'naive-ui'
import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { Icon } from '@iconify/vue'
import RichTextInput from './RichTextInput.vue'
@@ -106,12 +106,28 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
width: 50,
align: 'center',
render: (_row: ChangeItem, rowIndex: number) =>
h(NButton, {
text: true,
size: 'tiny',
type: 'error',
onClick: () => removeRow(rowIndex),
}, () => h(Icon, { icon: 'line-md:close-circle', class: 'size-16px' })),
h(NPopconfirm, {
// 组件 Props
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
removeRow(rowIndex)
},
// 可选:自定义确认/取消按钮的样式
positiveButtonProps: { size: 'tiny', type: 'primary' },
negativeButtonProps: { size: 'tiny' }
}, {
// 插槽 (Slots)
trigger: () => h(NButton,
{
text: true,
size: 'tiny',
type: 'error',
},
() => h(Icon, { icon: 'ci:close-circle', class: 'size-16px' })
),
default: () => '确定要删除吗?' // 弹出框的提示内容
}),
})
}
return cols
@@ -9,6 +9,7 @@ import { localStg } from '@/utils/storage';
const themeStore = useThemeStore();
const props = defineProps({
isAiOpen: Boolean,
formInfo: Object as () => any
})
@@ -18,8 +19,7 @@ const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
// 是否开启ai功能
const aiEnabled = ref<boolean>(localStg.get('userInfo')?.ai_assist_enabled=='1' ? true : false);
const today = () => new Date().toISOString().slice(0, 10);
// 按钮禁用
const btnDisabled = ref<boolean>(false);
// 总分数
@@ -66,8 +66,6 @@ const form = ref<{
plan_use_date: null,
risk_tools: [],
});
// 上传的资料
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
// 变更目的选项
const PURPOSE_OPTIONS = [
"满足市场需求", "提高生产效能", "改善操作条件", "增加安全性能",
@@ -226,6 +224,17 @@ const openLevelSheet = async () => {
levelAiLoading.value = false;
}
};
// 处理等级判定表评分变化
const handleScoreChange = (val:number,index:number) => {
LEVEL_DIMS.value[index].score = val;
let score = 0;
LEVEL_DIMS.value.forEach((d:any) => {
score += d.score;
});
totalScore.value = score;
}
// 下载PDF
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
// 更新表单
const updateForm = (res: any, isShow: boolean) => {
@@ -241,298 +250,356 @@ defineExpose({form,updateForm,updateBtnDisabled})
</script>
<template>
<!-- 申请部门 / 申请人 -->
<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 border border-[#ACC6E5] 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">
<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="" />
<NButton v-if="form.main_changes.length > 1" text type="error" @click="form.main_changes = form.main_changes.splice(i, 1)">
<Icon icon="line-md:close-circle" class="size-16px" />
</NButton>
</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">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
<NButton v-if="form.related_changes.length > 1" text type="error" @click="form.related_changes = form.related_changes.splice(i, 1)">
<Icon icon="line-md:close-circle" class="size-16px" />
</NButton>
</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 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 :key="0" :value="0">一般</NRadio>
<NRadio :key="1" :value="1">重要</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 [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'"
<NPopconfirm
v-if="form.main_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="form.main_changes.splice(i, 1)"
>
{{ 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">
<NButton size="small" class="text-xs" v-for="f in files" :key="f" @click="previewFile = f" title="点击在线预览">
<Icon icon="akar-icons:file" class="size-14px" />
{{ f }}
</NButton>
<NButton @click="files = [...files, `资料_${files.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" />
<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">
<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 ['工艺', '设备', '管理']" :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>
</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>
</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>
<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" :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>
</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>
<!-- 侧边抽屉AI 类型判定 / 等级判定表 / 审批流程设置 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
<NDrawerContent>
<template #header>
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<!-- AI 变更类型判定说明 -->
<template v-if="sheet === 'type'">
<div 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 v-if="typeAiLoading" type="info" size="small">
<template #icon>
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
</template>
AI 判定中
</NTag>
<template v-else>
<NTag type="info" size="small">判定{{ typeAi?.type || '工艺' }}</NTag>
<NTag size="small">置信度{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '高' }}</NTag>
</template>
</div>
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
<p><b>判定结论</b>{{ typeAi.conclusion }}</p>
<p><b>判定依据</b></p>
<ul class="list-disc space-y-1 pl-5">
<li v-for="r in typeAi.hit_rules" :key="r">命中规则{{ r }}</li>
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则{{ r }}</li>
</ul>
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用</b>{{ typeAi.knowledge_refs.join('、') }}</p>
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考最终类型以人工确认为准如调整为其他类型系统将记录修改留痕</p>
</div>
<div v-else class="space-y-3 text-sm leading-relaxed text-slate-700">
<p><b>判定结论</b>本次变更涉及反应温度8090保温时间8h4h等工艺参数调整及 DCS 报警/联锁值修改未涉及设备本体更换或材质变更依据变更类型规则判定为工艺类变更</p>
<p><b>判定依据</b></p>
<ul class="list-disc space-y-1 pl-5">
<li>命中规则 2.1操作参数超出原设计控制范围 工艺类</li>
<li>命中规则 2.4DCS 报警值/联锁值调整 工艺类仪表联动</li>
<li>排除规则 3.2无设备规格型号变化 不构成设备类</li>
</ul>
<p><b>知识库引用</b>工艺卡片 R-101Rev BDCS 联锁台账变更管理制度 4.2 </p>
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考最终类型以人工确认为准如调整为其他类型系统将记录修改留痕</p>
</div>
</template>
<!-- 变更等级判定表 -->
<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>
<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">
<div class="mb-2 flex items-baseline justify-between gap-2">
<span class="text-sm font-semibold text-slate-700">
{{ dim.name }}
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">{{ dim.note }}</span>
</span>
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ LEVEL_DIMS[di].score }}</b></span>
</div>
<div class="space-y-1.5">
<NRadioGroup v-model:value="LEVEL_DIMS[di].score" size="small" :disabled="levelAiLoading" name="radiogroup" :on-update:value="(v)=>handleScoreChange(v,di)">
<div class="flex flex-col justify-center gap-2">
<NRadio v-for="op in dim.options" :key="op.score" :value="op.score">
<p class="text-xs">{{ op.label }}{{ op.score }}</p>
</NRadio>
</div>
</NRadioGroup>
</div>
</div>
</div>
<div 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="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中
</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>
</div>
</template>
<NDrawerContent>
<template #header>
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<!-- AI 变更类型判定说明 -->
<template v-if="sheet === 'type'">
<NTag v-if="typeAiLoading" type="info" size="small">
<template #icon>
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
</template>
AI 判定中
</NTag>
<div v-else class="flex items-center gap-2 max-h-[22px]">
<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 type="info" size="small">判定{{ typeAi?.type || '工艺' }}</NTag>
<NTag size="small">置信度{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '' }}</NTag>
</div>
</NDrawerContent>
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
<p><b>判定结论</b>{{ typeAi.conclusion }}</p>
<p><b>判定依据</b></p>
<ul class="list-disc space-y-1 pl-5">
<li v-for="r in typeAi.hit_rules" :key="r">命中规则{{ r }}</li>
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则{{ r }}</li>
</ul>
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用</b>{{ typeAi.knowledge_refs.join('、') }}</p>
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考最终类型以人工确认为准如调整为其他类型系统将记录修改留痕</p>
</div>
</template>
<!-- 变更等级判定表 -->
<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>
<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">
<div class="mb-2 flex items-baseline justify-between gap-2">
<span class="text-sm font-semibold text-slate-700">
{{ dim.name }}
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">{{ dim.note }}</span>
</span>
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ LEVEL_DIMS[di].score }}</b></span>
</div>
<div class="space-y-1.5">
<NRadioGroup v-model:value="LEVEL_DIMS[di].score" size="small" :disabled="levelAiLoading" name="radiogroup" :on-update:value="(v)=>handleScoreChange(v,di)">
<div class="flex flex-col justify-center gap-2">
<NRadio v-for="op in dim.options" :key="op.score" :value="op.score">
<p class="text-xs">{{ op.label }}{{ op.score }}</p>
</NRadio>
</div>
</NRadioGroup>
</div>
</div>
</div>
<div 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="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中
</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>
</div>
</template>
</div>
</NDrawerContent>
</NDrawer>
<!-- 附件在线预览弹窗 -->
<NModal
:show="previewFile !== null"
preset="card"
:auto-focus="false"
:style="{ width: '700px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@update:show="(v) => !v && (previewFile = null)"
>
<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 }}
</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>上传人{{ userInfo?.real_name }}</span><span>上传时间{{ today() }}</span><span>版本V1受控</span>
</div>
<div class="flex h-72 flex-col items-center justify-center gap-2 rounded-lg border bg-slate-50 text-slate-400">
<Icon icon="akar-icons:file" class="size-42px" />
<span class="text-sm">文件在线预览区演示</span>
<span class="text-xs">正式环境由文档预览服务渲染 PDF / Word / 图片内容支持缩放翻页与移动端查看</span>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton ghost size="small" @click="downloadPdf">
<Icon icon="material-symbols:download" class="size-16px" /> 下载
</NButton>
<NButton size="small" type="primary" @click="previewFile = null">关闭</NButton>
</div>
</template>
</NModal>
</template>
@@ -78,67 +78,69 @@ defineExpose({form,updateForm,updateBtnDisabled})
</script>
<template>
<!-- 变更内容描述 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="font-semibold text-base ml-2">变更内容描述</span>
</div>
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled || preLoading">
<Icon v-if="preLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
变更识别
</NButton>
</div>
<div class="p-4">
<NInput type="textarea" v-model:value="form.description" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
</div>
</div>
<!-- 变更识别结果 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="space-y-4">
<!-- 变更内容描述 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{form.compare_rows.length ? `变更识别结果(${form.compare_rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="font-semibold text-base ml-2">变更内容描述</span>
</div>
<span v-if="isAiGenerated" 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>
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled || preLoading">
<Icon v-if="preLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
变更识别
</NButton>
</div>
<div class="p-4">
<ChangeCompareTable
:data="form.compare_rows"
:editable="true"
@update:data="(d) => form.compare_rows = d"
/>
<NInput type="textarea" v-model:value="form.description" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
</div>
</div>
<!-- 风险预分析报告 -->
<div v-if="preRiskShow" class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<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="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = preRiskEdit === s.key ? '' : s.key">
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
</NButton>
</div>
<NInput v-if="preRiskEdit === s.key" type="textarea" :bordered="false" v-model:value="form[s.key]" autofocus rows="6" />
<p v-else class="whitespace-pre-wrap px-3 py-1.5 text-sm leading-relaxed text-slate-700">{{ form[s.key] }}</p>
<!-- 变更识别结果 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{form.compare_rows.length ? `变更识别结果(${form.compare_rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
<span v-if="isAiGenerated" 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="p-4">
<ChangeCompareTable
:data="form.compare_rows"
:editable="true"
@update:data="(d) => form.compare_rows = d"
/>
</div>
</div>
<!-- 风险预分析报告 -->
<div v-if="preRiskShow" class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<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="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = preRiskEdit === s.key ? '' : s.key">
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
</NButton>
</div>
<NInput v-if="preRiskEdit === s.key" type="textarea" :bordered="false" v-model:value="form[s.key]" autofocus rows="6" />
<p v-else class="whitespace-pre-wrap px-3 py-1.5 text-sm leading-relaxed text-slate-700">{{ form[s.key] }}</p>
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
</div>
</div>
</div>
</div>