验收评价完成

This commit is contained in:
2026-08-14 14:25:09 +08:00
parent d5fb5e4d7a
commit 3a56cea50f
+273 -3
View File
@@ -31,7 +31,7 @@ const form = ref<ApplyForm>({
name: "", dept: "", mainChanges: [""], related: "", purposes: [], effect: "",
type: "", level: "", duration: "", urgent: false, date: null, restoreDate: null,
});
const tab = ref<string>("train");
const tab = ref<string>("accept");
const TABS = [
{ id: "pre", name: "变更预识别", icon: 'lucide:sparkles' },
{ id: "form", name: "变更申请表", icon: 'akar-icons:file' },
@@ -480,6 +480,52 @@ M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT
2. 风险:联锁设定错误导致拒动 / 误动 → 应对:投用前完成跳车试验并留存记录
3. 风险:防爆接合面或格兰头密封失效 → 应对:检查防爆接合面与格兰头,严禁带电开盖`
);
// ---------- PSSR 检查内容 ----------
const pssrDone = ref<Record<string, { by: string; at: string }>>({});
const pssrNa = ref<Record<string, boolean>>({});
const pssrActive = computed(() => pssrGroups.value.filter((g:any) => !pssrNa.value[g.g]));
const pssrTotal = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.length, 0));
const pssrDoneCount = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.filter((it:any) => pssrDone.value[it.c]).length, 0));
const pssrAllDone = computed(() => pssrDoneCount.value === pssrTotal.value && (pssrTotal.value > 0 || (pssrGroups.value.length > 0 && pssrGroups.value.every((g) => !!pssrNa.value[g.g]))));
const pssrFileRef = ref<any>(null);
const pssrSheet = ref<any>(null);
const closeDocs = ref([
{ name: "PID 图纸(PID-R201-03,受控最新版)", cat: "图纸与规程类", scope: "是", archived: true },
{ name: "操作规程(含超温应急处置卡)", cat: "图纸与规程类", scope: "是", archived: false },
{ name: "应急预案修订", cat: "图纸与规程类", scope: "不涉及", archived: false },
{ name: "总图更新", cat: "图纸与规程类", scope: "否", archived: false },
{ name: "DCS 联锁台账(新报警 / 联锁值)", cat: "台账与制度类", scope: "是", archived: false },
{ name: "设备台账与备件清单", cat: "台账与制度类", scope: "是", archived: true },
{ name: "风险分析报告(HAZOP / 检查表法)", cat: "过程与结果文件类", scope: "是", archived: true },
{ name: "培训签到与考核记录", cat: "过程与结果文件类", scope: "是", archived: true },
{ name: "PSSR 检查记录(含现场照片)", cat: "过程与结果文件类", scope: "是", archived: false },
{ name: "验收报告与化验数据", cat: "过程与结果文件类", scope: "是", archived: false },
]);
const onPssrFile = (e: Event) => {
const input = e.target as HTMLInputElement;
const f = input.files?.[0]; input.value = "";
if (!f) return;
pssrSheet.value = f.name;
const at = today();
const n = { ...pssrDone.value };
pssrGroups.value.forEach((g:any) => { if (!pssrNa.value[g.g]) g.items.forEach((it:any) => { n[it.c] = { by: "纸质确认表上传", at }; }); });
pssrDone.value = n;
const archGroup = pssrGroups.value[pssrGroups.value.length - 1];
if (archGroup && !pssrNa.value[archGroup.g]) {
closeDocs.value = closeDocs.value.map((d:any) => (d.name.includes("PID") || d.name.includes("设备台账") || d.name.includes("培训签到") ? { ...d, archived: true } : d));
}
window.$message?.success(`已上传 PSSR 纸质确认表「${f.name}」:不做内容识别,上传即视为全部检查项完成确认(「不涉及」组除外),原件归档至变更档案`);
};
// ---------- 验收评价 ----------
const acceptRows = ref([
{ id: 1, item: "反应转化率", basis: "转化率不低于 98%", standard: "转化率 ≥ 98%(连续 3 批化验合格)", method: "化验室逐批取样分析,出具检验报告", owner: "车间 · 张工艺", result: "待验收" },
{ id: 2, item: "单批反应时间 / 产能", basis: "反应时间缩短 50%、产能提升一倍", standard: "保温时间 4h±0.5h,产能提升 ≥ 80%", method: "DCS 批次记录统计,对比变更前 30 天均值", owner: "车间 · 李主任", result: "待验收" },
{ id: 3, item: "未知杂质含量", basis: "杂质水平受控、产品质量合格", standard: "杂质 ≤ 0.15%,产品质量指标全部合格", method: "每批化验跟踪,连续 5 批数据趋势评估", owner: "质量 · 王海燕", result: "待验收" },
{ id: 4, item: "温控与联锁有效性", basis: "温度窗口上调后保护层有效", standard: "温度波动 ≤ ±2℃,高报/联锁动作正确率 100%", method: "DCS 趋势核查 + 联锁测试记录复核", owner: "仪表 · 王仪表", result: "待验收" },
{ id: 5, item: "设备运行状态", basis: "负荷变化后设备长周期稳定", standard: "搅拌电流、釜壁温度无异常趋势,无泄漏", method: "设备巡检 + 振动/温度记录评估", owner: "设备 · 周设备", result: "待验收" },
]);
const acceptNote = ref("");
</script>
@@ -981,11 +1027,235 @@ M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT
<NInput v-model:value="trainText" type="textarea" :bordered="false" :autosize="{minRows: 4}" class="font-mono text-sm" placeholder="" />
</div>
</template>
<!-- PSSR 检查内容 -->
<template v-if="tab === 'pssr'">
<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" />
PSSR 投用前安全检查内容
</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" :type="pssrAllDone ? 'success' : 'info'">
已完成 {{ pssrDoneCount }}/{{ pssrTotal }}
</NTag>
<span class="flex items-center gap-2">
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
<Icon icon="material-symbols:upload" class="size-14px" />
上传纸质确认表
</NButton>
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段点击条目内容直接编辑可增删项完成时点互斥选择审批通过后逐项点击确认PC / 移动端均可全部完成自动推送属地负责人</span>
</span>
</div>
<div v-if="pssrSheet" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-xs text-emerald-700">
<Icon icon="ix:success" class="size-16px" />
已上传 PSSR 纸质确认表{{ pssrSheet }}上传即视为全部检查项完成确认未做内容识别原件已归档至变更档案
</div>
<div v-if="pssrAllDone" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-xs text-emerald-700">
<Icon icon="ix:success" class="size-16px" />
全部 PSSR 检查项已确认完成具备投用条件信息已自动推送属地负责人车间 / 装置主任可进行投用确认
</div>
<div v-for="(g, gi) in pssrGroups" :key="g.g" class="border" :class="pssrNa[g.g] ? 'opacity-70' : ''" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">
{{ g.g }}
<NTag v-if="pssrNa[g.g]" size="small" class="ml-2">不涉及</NTag>
<span v-else class="ml-2 text-xs font-normal text-slate-400">{{ g.items.filter((it) => pssrDone[it.c]).length }}/{{ g.items.length }} 已确认</span>
<NButton text type="primary" class="ml-auto text-xs font-normal hover:underline"
:title="pssrNa[g.g] ? '恢复本组检查清单' : '本次变更不涉及本组内容时点击标记,标记后不计入完成统计'"
@click="pssrNa = { ...pssrNa, [g.g]: !pssrNa[g.g] }">
{{ pssrNa[g.g] ? '恢复清单' : '标记不涉及' }}
</NButton>
<NButton text type="primary" v-if="!pssrNa[g.g]" class="ml-3 text-xs font-normal hover:underline"
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: [...x.items, { c: '新检查项(点击直接编辑内容)', p: '投用前' }] } : x)">
<Icon icon="ic:round-plus" class="size-14px" /> 添加检查项
</NButton>
</div>
<div v-if="pssrNa[g.g]" class="border-t bg-slate-50 px-3 py-3 text-xs text-slate-400">
本组内容经评估本次变更不涉及无现场安装作业 / 不改变保护措施 / 无需归档资料已不计入 PSSR 完成统计如需恢复检查清单点击右上角恢复清单
</div>
<template v-else>
<div v-for="(it, i) in g.items" :key="`${gi}-${i}`" class="flex flex-wrap items-center gap-3 border-t px-3 py-2.5 text-sm">
<span class="w-5 text-slate-400">{{ i + 1 }}</span>
<NInput
v-if="editing === `pssr-c-${gi}-${i}`"
class="flex-1"
v-model:value="it.c"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `pssr-c-${gi}-${i}`" title="点击编辑内容"
class="min-w-0 flex-1 rounded-md px-1 text-left text-slate-800 transition hover:bg-[#EAF0F9] bg-transparent"
>
{{ it.c }}
</button>
<span class="flex items-center gap-1 py-1">
<NRadio :checked="it.p === '投用前'" :value="'投用前'" label="投用前完成"
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '投用前' } : y) } : x)" />
<NRadio :checked="it.p === '关闭前'" :value="'关闭前'" label="关闭前完成"
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '关闭前' } : y) } : x)" />
</span>
<NButton text type="error" title="删除该项"
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.filter((_, yi) => yi !== i) } : x)"
>
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</div>
</template>
</div>
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
完成时点说明投用前完成 / 关闭前完成为互斥选择点击即可切换投用前完成的项未确认时属地负责人无法确认投用关闭前完成的项不阻塞投用但纳入关闭校验清单未完成时变更主管部门专责无法确认关闭
</div>
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
清单由 AI 依据变更类型与风险分析自动生成专业人员审核后随审批发布确认方式不作硬性要求可在 PC 端直接点击确认也可在移动端拍照上传作为佐证系统自动记录确认人与日期资料归档组确认后自动同步变更关闭确认表显示已归档
</div>
</template>
<!-- 验收评价 -->
<template v-if="tab === 'accept'">
<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" />
验收评价依据预期效果设定可量化验收标准
</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>
<span class="ml-auto text-xs text-slate-400">申请阶段由 AI 依据预期效果生成量化标准与验收方式可手动修改投用运行后由车间和各专业按标准验收并给出评估结论</span>
</div>
<div
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
:style="{
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
}"
>
<div><b :style="{color: themeStore.themeColor}">变更目的</b>{{ form?.purposes?.join('、') || '提高生产效能、改善操作条件' }}</div>
<div><b :style="{color: themeStore.themeColor}">预期效果</b>{{ form?.effect || '在保持转化率不低于98%的前提下,反应时间缩短50%,产能提升一倍' }}</div>
<div
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
>
<b :style="{color: themeStore.themeColor}">对应说明</b>验收项目由预期效果逐条拆解设定各项目对应的预期效果来源见表格验收项目列下方灰色小字点击可修改验收时须逐项对照预期效果判定达成程度
</div>
</div>
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<table class="w-full min-w-[900px] border-collapse text-sm">
<thead>
<tr class="bg-slate-100 text-left text-slate-600">
<th class="w-8 px-3 py-2 font-medium">#</th>
<th class="w-56 px-3 py-2 font-medium">验收项目含对应预期效果</th>
<th class="px-3 py-2 font-medium">量化验收标准</th>
<th class="px-3 py-2 font-medium">验收方式可执行</th>
<th class="w-32 px-3 py-2 font-medium">责任单位/</th>
<th class="px-3 py-2 font-medium">评估结论</th>
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in acceptRows" :key="row.id" class="align-top">
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `acc-${row.id}-item`"
class="text-xs"
v-model:value="row.item"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `acc-${row.id}-item`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.item }}
</button>
<NInput
v-if="editing === `acc-${row.id}-basis`"
class="text-[11px]"
size="small"
v-model:value="row.basis"
autofocus
@blur="editing = null"
/>
<button v-else @click="editing = `acc-${row.id}-basis`" title="本项目对应的预期效果来源,点击修改"
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
>
对应预期{{ row.basis || '(点击填写对应预期效果)' }}
</button>
</td>
<td v-for="field in (['standard', 'method', 'owner'])" :key="field" class="border-t px-3 py-2.5">
<NInput
v-if="editing === `acc-${row.id}-${field}`"
class="text-xs"
v-model:value="row[field]"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `acc-${row.id}-${field}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row[field] }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<div class="flex flex-nowrap items-center gap-1">
<NButton v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v"
size="small"
round
@click="acceptRows = acceptRows.map((x) => (x.id === row.id ? { ...x, result: x.result === v ? '待验收' : v } : x))"
class="text-[11px]"
:type="row.result === v
? v === '未达成' ? 'error' : v === '基本达到' ? 'warning' : 'success'
: 'default'">
{{ v }}
</NButton>
</div>
</td>
<td class="border-t px-2 py-2.5 text-center">
<NButton text type="error" title="删除本验收项" @click="acceptRows = acceptRows.filter((x) => x.id !== row.id)">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center gap-3">
<NButton ghost class="text-xs"
@click="acceptRows = [...acceptRows, { id: Date.now(), item: '新验收项目', basis: '', standard: '', method: '', owner: '', result: '待验收' }]">
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
</NButton>
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写点击选择完全达到 / 基本达到 / 未达成再次点击取消行尾可删除验收项</span>
</div>
<div class="rounded-lg border">
<div class="border-b bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">综合验收结论验收阶段由车间会同各专业评估填写</div>
<div class="space-y-3 p-3">
<div class="flex flex-wrap items-center gap-2">
<span v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v">
<NRadio :checked="acceptNote !== '' && acceptNote.startsWith(v)" :value="'v'" :label="v" @change="acceptNote = v + ''"></NRadio>
</span>
<span class="text-xs text-slate-400">基本达到 / 未达成须说明偏差原因与处置措施并纳入变更关闭审核</span>
</div>
<NInput v-model:value="acceptNote" type="textarea" rows="2" placeholder="验收意见:总体评价、偏差说明、后续跟踪措施…(验收阶段填写)" />
</div>
</div>
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
流程说明投用运行满验收周期后系统向车间属地与涉及专业推送验收任务各责任单位按上表逐项给出评估结论并上传佐证数据报表 / 化验报告 / 现场照片全部完成后汇总至综合验收结论结论与佐证资料一并纳入变更关闭校验验收不通过时退回整改或评估恢复原状
</div>
</template>
</div>
</div>
<!-- 底部 -->
<!-- 全局按钮下载 / 重新生成 / 确认完毕 -->
<div v-if="tab !== 'pre'" class="mt-4 flex flex-wrap items-center justify-between gap-2 border-t p-4">
<div v-if="tab !== 'pre'" class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
<div class="flex flex-wrap gap-2">
<NButton ghost type="success" @click="download">
<Icon icon="material-symbols:download" class="size-16px" /> 下载{{ TAB_NAME[tab] }}
@@ -1204,7 +1474,7 @@ M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT
overflow-y: auto;
}
.scroll2 {
height: calc(100vh - 409px);
height: calc(100vh - 393px);
overflow-y: auto;
}
</style>