Files
moc/src/views/my/initiateChange/modules/scl.vue
T

238 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref } from "vue";
import { useMessage } from "naive-ui";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import Ed from "./Ed.vue";
const themeStore = useThemeStore();
const message = useMessage();
const emit = defineEmits<{
back: [];
done: [records: number];
export: [items: any[], mode: "manual" | "ai"];
save: [];
}>();
const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
return lv === "高" ? "中" : "低";
}
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
const v = l * s;
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
}
const toRec = (row: any): any => ({
item: row.item || "(未命名检查项)",
scene: `标准:${row.std};不符合可能导致:${row.cons}`,
inherent: inherentOf(row.l, row.s),
existing: row.ctrl,
suggest: row.sug,
residual: lowerOf(inherentOf(row.l, row.s)),
source: "SCL",
});
const gi = ref(0);
const cur = computed(() => groups.value[gi.value]);
const recTotal = computed(() => groups.value.reduce((a, g) => a + g.rows.length, 0));
const updRow = (ri: number, patch: Partial<any>) => {
const row = cur.value.rows[ri];
if (row) Object.assign(row, patch);
};
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
const addRow = () => cur.value.rows.push({ item: "", std: "", cons: "", l: 2, s: 3, ctrl: "", sug: "" });
const delRow = (ri: number) => cur.value.rows.splice(ri, 1);
const exportAll = () => emit("export", groups.value.flatMap((g) => g.rows.map(toRec)), "ai");
const save = () => {
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
};
const SCL_INFO = {
dev: "R-101 缩合反应釜",
mat: "缩合反应体系(有机溶剂 / 反应物料)",
param: "设计温度:-10/120 ℃,设计压力:0.6 MPa;变更后操作温度:90±2 ℃(原 80±2 ℃),常压",
};
const TH = "border bg-slate-100 px-2 py-1.5 text-[11px] font-medium text-slate-500";
const TD = "border px-1.5 py-1 align-top";
const ANALYSIS_POOL = [
{ name: "张工艺", org: "一车间", post: "工艺工程师" },
{ name: "王仪表", org: "技术部", post: "仪表工程师" },
{ name: "周设备", org: "设备科", post: "设备工程师" },
{ name: "刘文静", org: "技术部", post: "工艺员" },
{ name: "王海燕", org: "安全环保部", post: "安全工程师" },
{ name: "李主任", org: "一车间", post: "车间主任" },
{ name: "吴海涛", org: "设备科", post: "维修班长" },
{ name: "陈国栋", org: "一车间", post: "班长" },
];
const open = ref(false);
const q = ref("");
const sels = ref<string[]>(["张工艺", "王仪表"]);
const list = computed(() => ANALYSIS_POOL.filter((x) => !q.value || x.name.includes(q.value)));
const toggle = (n: string) => {
sels.value = sels.value.includes(n) ? sels.value.filter((x) => x !== n) : [...sels.value, n];
};
const rrOf = (l: number, s: number) => {
const v = l * s;
if (v >= 15) return { t: `重大 (${v})`, c: "bg-red-500 text-white" };
if (v >= 10) return { t: `较大 (${v})`, c: "bg-[#F5A623] text-white" };
if (v >= 5) return { t: `一般 (${v})`, c: "bg-amber-300 text-amber-900" };
return { t: `低 (${v})`, c: "bg-emerald-500 text-white" };
}
const groups = ref([
{ g: "本体", rows: [
{ item: "釜体及夹套壁厚", std: "测厚值 ≥ 设计最小壁厚,无超标腐蚀减薄", cons: "壁厚不足导致强度失效、泄漏", l: 1, s: 4, ctrl: "工程控制:定期测厚;管理措施:年度检验计划", sug: "变更后温度提升,复核腐蚀速率评估" },
{ item: "搅拌器运行状态", std: "电流、振动在正常范围,无异响", cons: "搅拌失效导致传热不均、局部过热", l: 2, s: 3, ctrl: "工程控制:电流在线监视;培训教育:异常处置规程", sug: "投用首周每班记录搅拌电流趋势" },
] },
{ g: "安全附件", rows: [
{ item: "安全阀", std: "校验在有效期内,起跳压力 0.5 MPa,铅封完好", cons: "超压时无法正常泄放", l: 1, s: 5, ctrl: "工程控制:定期校验;管理措施:校验台账管理", sug: "本次温度变更不影响泄放工况,维持原校验周期" },
{ item: "温度 / 压力仪表", std: "校验合格,现场示值与 DCS 一致", cons: "测量失真导致误判", l: 2, s: 3, ctrl: "管理措施:周期校验;现场与 DCS 比对核查", sug: "变更投用前完成一次示值比对" },
] },
{ g: "DCS 联锁与报警", rows: [
{ item: "温度高报 / 联锁设定值", std: "高报 95℃、高高报及联锁 100℃,与变更单一致", cons: "设定错误导致保护失效或误动作", l: 2, s: 5, ctrl: "管理措施:双人复核修改;工程控制:联锁测试", sug: "测试记录归档至变更资料" },
{ item: "联锁传动测试", std: "试验动作正确率 100%", cons: "联锁拒动 / 误动", l: 1, s: 5, ctrl: "管理措施:投用前强制测试;培训教育", sug: "纳入 PSSR 必查项" },
] },
{ g: "操作规程与应急", rows: [
{ item: "新版操作规程", std: "90℃ / 4h 工艺参数已修订发布并完成培训", cons: "沿用旧规程导致误操作", l: 2, s: 3, ctrl: "管理措施:规程受控发放;培训教育:签到考核", sug: "旧版规程现场收回,防止混用" },
{ item: "超温应急处置卡", std: "处置卡已更新并放置岗位", cons: "应急处置不当扩大事态", l: 1, s: 4, ctrl: "培训教育:应急演练;个体防护:应急物资就位", sug: "投用前组织一次超温处置桌面演练" },
] },
])
</script>
<template>
<div class="space-y-3">
<div
class="flex flex-wrap items-center gap-2 rounded-lg border border-[#ACC6E5] bg-[#EAF0F9] px-3 py-2"
:style="{
'--theme-color': themeStore.themeColor
}"
>
<NButton size="small" ghost type="primary" class="text-xs" @click="emit('back')">
<Icon icon="basil:arrow-left-outline" class="size-18px" />
返回工具列表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
<span class="text-sm font-bold text-[var(--theme-color)]">SCL 安全检查表插件</span>
<NTag size="small" type="info">插件</NTag>
<NTag size="small">MOC 变更缩合反应温度及时间优化演示</NTag>
<span class="text-[11px] text-slate-500">{{ `${groups.length} 个检查分类 · ${recTotal} 项检查记录` }}</span>
<div class="relative">
<NButton size="small" ghost type="primary" class="text-[11px]" @click="open = !open">
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ sels.length }}
</NButton>
<template v-if="open">
<div class="fixed inset-0 z-10" @click="open = false"></div>
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
<div class="mb-1.5 flex items-center justify-between">
<span class="text-xs font-semibold text-slate-700">选择分析组成员岗位人员库</span>
<span class="text-[11px] text-slate-400">已选 {{ sels.length }} </span>
</div>
<NInput v-model:value="q" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
<div class="max-h-48 space-y-0.5 overflow-y-auto">
<div v-for="x in list" :key="x.name" @click="toggle(x.name)"
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
:class="sels.includes(x.name) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
:class="sels.includes(x.name) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ sels.includes(x.name) ? "✓" : "" }}</span>
<span class="font-medium text-slate-800">{{ x.name }}</span>
<span class="text-[11px] text-slate-400">{{ x.org }} · {{ x.post }}</span>
<NTag v-if="sels[0] === x.name" size="small" type="primary" class="ml-auto">组长</NTag>
</div>
</div>
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
首位选择者默认为组长成员为选择式非手写),参与记录自动归属到人用于参与风险分析次数统计采集
</div>
</div>
</template>
</div>
<div class="ml-auto flex items-center gap-1.5">
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll">
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('SCL 检查表已导出(Word')">
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
</NButton>
<NButton size="small" type="primary" class="text-xs" @click="message.success('演示:AI 辅助分析完成,已按变更内容核对检查标准(支持手动修改)')">
<Icon icon="lucide:sparkles" class="size-12px" />
辅助分析
</NButton>
</div>
</div>
<!-- 设备信息头 -->
<div class="grid gap-2 rounded-lg border bg-white p-3 text-xs sm:grid-cols-3">
<div><span class="text-slate-400">设备名称</span><b class="text-slate-700">{{ SCL_INFO.dev }}</b></div>
<div><span class="text-slate-400">物料信息</span><b class="text-slate-700">{{ SCL_INFO.mat }}</b></div>
<div class="sm:col-span-3"><span class="text-slate-400">运行参数</span><span class="text-slate-600">{{ SCL_INFO.param }}</span></div>
</div>
<div
class="flex gap-3"
:style="{'--theme-color':themeStore.themeColor}"
>
<!-- 检查分类 -->
<div class="shrink-0 rounded-lg border bg-white p-2 w-44">
<div v-for="(g, i) in groups" :key="g.g" @click="gi = i"
class="cursor-pointer flex w-full items-center rounded-md px-3 py-1.5 text-left text-xs transition"
:class="gi === i ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'">
<span class="flex-1">{{ g.g }}</span>
<span class="rounded-full px-1.5 text-[10px]" :class="gi === i ? 'bg-white/25' : 'bg-slate-100 text-slate-400'">{{ g.rows.length }}</span>
</div>
</div>
<!-- 检查表 -->
<div class="min-w-0 flex-1 bg-white">
<div class="flex items-center gap-2 pb-2">
<NButton size="small" class="text-xs" @click="addRow">
<Icon icon="ic:round-plus" class="size-14px" />新增检查项
</NButton>
<span class="text-[11px] text-slate-400">单元格点击即可修改L / S 取值 1-5风险等级按矩阵实时重算;「入表将本条选入风险分析记录表</span>
</div>
<table class="w-full min-w-[900px] border-collapse text-xs">
<thead>
<tr>
<th :class="TH">检查项目</th>
<th :class="TH">标准</th>
<th :class="TH">不符合标准情况及后果</th>
<th :class="TH" class="w-8">L</th>
<th :class="TH" class="w-8">S</th>
<th :class="TH" class="w-20">RR</th>
<th :class="TH">现有控制措施</th>
<th :class="TH">建议措施</th>
<th :class="TH" class="w-16">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, ri) in cur.rows" :key="ri">
<td :class="TD"><Ed :v="row.item" @update="(x) => updRow(ri, { item: x })" /></td>
<td :class="TD"><Ed :v="row.std" @update="(x) => updRow(ri, { std: x })" /></td>
<td :class="TD"><Ed :v="row.cons" @update="(x) => updRow(ri, { cons: x })" /></td>
<td :class="TD" class="text-center">
<input :value="row.l" @change="(e) => updRow(ri, { l: clampLS((e.target as HTMLInputElement).value) })"
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
</td>
<td :class="TD" class="text-center">
<input :value="row.s" @change="(e) => updRow(ri, { s: clampLS((e.target as HTMLInputElement).value) })"
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
</td>
<td :class="TD" class="text-center">
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium" :class="rrOf(row.l, row.s).c">{{ rrOf(row.l, row.s).t }}</span>
</td>
<td :class="TD"><Ed :v="row.ctrl" @update="(x) => updRow(ri, { ctrl: x })" /></td>
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(ri, { sug: x })" /></td>
<td :class="TD">
<div class="flex items-center justify-between px-1 mt-1">
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
@click="emit('export', [toRec(row)], 'manual')">入表
</NButton>
<NButton text type="error" title="删除行" @click="delRow(ri)">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>