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

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
@@ -0,0 +1,246 @@
<!-- PSSR检查内容 -->
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import ApprovalDrawer from './approvalDrawer.vue'
const themeStore = useThemeStore();
// 抽屉类型
const approvalDrawerShow = ref<boolean>(false);
const trainText = ref<string>('');
const btnDisabled = ref<boolean>(false);
const saveLoading = ref<boolean>(false);
const downloadLoading = ref<boolean>(false);
const regenerateLoading = ref<boolean>(false);
const submitLoading = ref<boolean>(false);
const confirmed = ref<boolean>(false);
const approvers = ref<Record<string, string>>({
部门审核: "李主任(一车间主任)",
最终批准: "李主任(部门负责人 · 固定)",
});
const signers = ref<Record<string, string>>({});
const editing = ref<any>(null);
const pssrGroups = ref<{ g: string; items: any[] }[]>([
{ g: "一、风险分析行动项落实", items: [
{ c: "联锁跳车电流设定值校核并留存记录", p: "投用前" },
{ c: "电缆与开关容量校核升级完成", p: "投用前" },
{ c: "操作人员培训完成并考核合格", p: "投用前" },
] },
{ g: "二、现场设备安装检查确认", items: [
{ c: "电机安装就位,联轴器对中合格", p: "投用前" },
{ c: "地脚螺栓紧固,减振垫安装到位", p: "投用前" },
{ c: "电缆接线、防爆格兰头密封良好", p: "投用前" },
] },
{ g: "三、保护措施确认", items: [
{ c: "联锁跳车设定值与新电机额定电流匹配", p: "投用前" },
{ c: "电机外壳接地、防护罩完好", p: "投用前" },
{ c: "紧急停机按钮功能试验正常", p: "投用前" },
] },
{ g: "四、资料归档确认", items: [
{ c: "P&ID(图号 PID-R201-03)已更新并受控", p: "关闭前" },
{ c: "设备台账、备件清单已更新", p: "关闭前" },
{ c: "培训签到表、考核记录已归档", p: "关闭前" },
] },
]);
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 today = () => new Date().toISOString().slice(0, 10);
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 regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
// 保存草稿
const saveDraft = async () => {
// if(changeApplyRules()){
// return
// }
// saveLoading.value = true;
// const {error} = await applicationSaveApi(props.currentId, form.value);
// if(!error){
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
// }
// saveLoading.value = false;
}
// ---------- 提交 ----------
const submitApply = () => {
}
const confirmTab = () => {
confirmed.value = !confirmed.value;
}
defineExpose({trainText})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<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>
</div>
</div>
<div 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" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
<Icon icon="material-symbols:download" class="size-16px" /> 下载PSSR检查内容
</NButton>
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成PSSR检查内容
</NButton>
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
</NButton>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-14px" />
<span class="min-w-0 truncate">
部门审核{{ approvers.部门审核 }} 专业会签{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} 最终批准{{ approvers.最终批准 }}
</span>
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
<Icon icon="octicon:sliders-24" class="size-14px" />
审批流程设置
</NButton>
</div>
<div class="ml-auto flex shrink-0 items-center gap-2">
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在变更预识别页生成变更申请单</span> -->
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
提交 <Icon icon="formkit:right" class="size-14px" />
</NButton>
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
<NDrawerContent>
<template #header>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<ApprovalDrawer />
</NDrawerContent>
</NDrawer>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 393px);
overflow-y: auto;
}
</style>