整个路由布局修改
This commit is contained in:
@@ -1,213 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { getColorWithOpacity } from '@/utils/common';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
|
||||
|
||||
const restoresDone = ref<boolean>(false);
|
||||
const restores = ref<{ item: string, done: boolean }[]>([
|
||||
{ item: "安全阀起跳压力已恢复至原设定 0.8MPa(恢复操作票 CAO-2026-0812)", done: true },
|
||||
{ item: "恢复后运行参数核对:连续 24h 无起跳、阀体无泄漏", done: false },
|
||||
{ item: "恢复性检查记录与现场照片已上传", done: false },
|
||||
]);
|
||||
const canRestore = ref<boolean>(false);
|
||||
const readyToClose = ref<boolean>(false);
|
||||
|
||||
const checks = ref<{ item: string, done: boolean, note?: string }[]>([
|
||||
{ item: "变更验收完成(结论:基本达到 · 遗留项 2 项已登记跟踪)", done: true },
|
||||
{ item: "资料上传齐全(验收报告、运行记录、恢复确认记录)", done: false, note: "缺:更新后的安全阀台账" },
|
||||
{ item: "行动项全部关闭(4/4,含「关闭前完成」标记项)", done: true },
|
||||
{ item: "受控文件已更新至生效版(P&ID Rev.4、操作规程)", done: false, note: "安全阀台账待更新发布" },
|
||||
{ item: "培训考核完成(12/12 人),签到与成绩已归档", done: true },
|
||||
]);
|
||||
|
||||
const archiving = ref<boolean>(false);
|
||||
const step = ref<number>(0);
|
||||
const archivedHash = ref<string | null>(null);
|
||||
const allDone = computed(() => checks.value.every((c: { done: boolean }) => c.done));
|
||||
const isCloser = ref<boolean>(true);
|
||||
const startArchive = () => { step.value = 0; archiving.value = true; };
|
||||
|
||||
// 关闭即归档:自动归档八步流程
|
||||
const ARCHIVE_STEPS = [
|
||||
"关闭触发:接收「同意关闭」归档指令(不可逆)",
|
||||
"文件收集:申请表 / 审批记录 / 风险分析 / 培训 / PSSR / 验收单生成 PDF",
|
||||
"附件归集:原始附件、培训签到照片、PSSR 现场照片、验收签字文件",
|
||||
"受控文件复制:生效版与被替代旧版物理复制,与工作区脱离",
|
||||
"清单生成:自动生成归档索引与文件更新清单(新旧版本对照)",
|
||||
"打包锁定:写入归档区(装置 / 年份 / 变更单号),设为只读",
|
||||
"哈希存证:计算归档包 SHA-256 哈希值入库,防篡改校验",
|
||||
"状态更新:变更为「已关闭-已归档」,首页数据同步",
|
||||
];
|
||||
const closedAt = ref<string | null>(null);
|
||||
|
||||
const OVERDUE_LIST = [
|
||||
{ id: "MOC-2026-R01-0039", title: "V-102 安全阀起跳压力临时调整", stage: "待关闭", days: 4 },
|
||||
{ id: "MOC-2026-R02-0031", title: "冷冻盐水泵 P-103 叶轮材质变更", stage: "待验收", days: 7 },
|
||||
{ id: "MOC-2026-R01-0029", title: "操作规程修订(夏季工况)", stage: "审批中", days: 3 },
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 关闭确认 -->
|
||||
<NCard
|
||||
:bordered="false"
|
||||
size="small"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--error-color': themeStore.otherColor.error,
|
||||
'--success-color': themeStore.otherColor.success,
|
||||
'--success-bg': getColorWithOpacity(themeStore.otherColor.success, 0.1),
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
'--warning-bg': getColorWithOpacity(themeStore.otherColor.warning, 0.1),
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--theme-bg': getColorWithOpacity(themeStore.themeColor, 0.1),
|
||||
}"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<NCard :bordered="true" :content-style="{ padding: '0' }" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="py-3 px-4 border-b flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-base font-semibold">
|
||||
<Icon icon="system-uicons:reset" class="size-16px text-[var(--error-color)]" />临时变更恢复确认 · 期限 30 天
|
||||
</div>
|
||||
<NTag v-if="restoresDone" size="small" type="success">恢复确认完成</NTag>
|
||||
<NTag v-else size="small" type="error">已超期 4 天 · 已升级上报刘副总</NTag>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-slate-500">
|
||||
<span>计划恢复时间:2026-08-14(临时调整起跳压力 0.88MPa → 恢复原设定 0.8MPa)</span>
|
||||
<span class="text-[var(--error-color)]">超期未恢复:每日 08:00 提醒申请人与属地负责人,已自动升级上报分管领导</span>
|
||||
</div>
|
||||
<div class="space-y-2 pt-3">
|
||||
<div v-for="(r, i) in restores" :key="r.item" class="flex items-center gap-3 rounded-lg border p-3"
|
||||
:class="r.done ? 'border-emerald-200 bg-emerald-50/40' : 'border-amber-200 bg-amber-50/40'">
|
||||
<Icon v-if="r.done" icon="material-symbols:check-circle-outline" class="size-20px text-[var(--success-color)]" />
|
||||
<Icon v-else icon="system-uicons:reset" class="size-18px text-[var(--warning-color)]" />
|
||||
<div class="flex-1 text-sm">{{ r.item }}</div>
|
||||
<NButton v-if="!r.done && canRestore" size="small" ghost class="text-xs"
|
||||
@click="restores = restores.map((x, xi) => (xi === i ? { ...x, done: true } : x)); toast.success('演示:恢复确认完成(移动端可拍照上传凭证)');">
|
||||
<Icon icon="ant-design:camera-outlined" class="size-16px mr-1" />
|
||||
确认恢复
|
||||
</NButton>
|
||||
<span v-if="!r.done && !canRestore" class="text-xs text-slate-400">待申请人 / 属地负责人确认</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="restoresDone" class="mt-3 rounded-lg bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
|
||||
临时变更恢复确认完成,恢复操作票、参数核对记录与照片将自动纳入归档包「5-验收记录」
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
<NCard :bordered="true" :content-style="{ padding: '0' }" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="py-3 px-4 border-b flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-base font-semibold">
|
||||
<Icon icon="material-symbols:check-circle-outline" class="size-18px text-slate-500" />关闭校验 · MOC-2026-R01-0039(紧急变更补办)
|
||||
</div>
|
||||
<NTag v-if="readyToClose" size="small" type="success">校验全绿 · 已推送专责</NTag>
|
||||
<NTag v-else size="small" type="warning">超期 4 天</NTag>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-2 rounded-lg bg-[var(--theme-bg)] px-3 py-2 text-xs text-[var(--theme-color)]">
|
||||
<Icon icon="lucide:bell" class="size-14px" />
|
||||
前置条件(验收完成 / 资料齐全 / 行动项关闭 / 恢复确认 / 受控文件更新)全部满足后,系统自动推送变更主管部门专责(王海燕 · 安全环保部),并抄送申请人与属地负责人
|
||||
</div>
|
||||
<div class="space-y-2 pt-3">
|
||||
<div v-for="(c, i) in checks" :key="c.item" class="flex items-center gap-3 rounded-lg border p-3"
|
||||
:class="c.done ? 'border-emerald-200 bg-emerald-50/40' : 'border-red-200 bg-[#FFF2F0]'">
|
||||
<Icon v-if="c.done" icon="material-symbols:check-circle-outline" class="size-20px text-[var(--success-color)]" />
|
||||
<Icon v-else icon="ant-design:close-circle-outlined" class="size-18px text-[var(--error-color)]" />
|
||||
<div class="flex-1 text-sm">{{ c.item }}<span v-if="c.note" class="ml-2 text-xs text-[var(--error-color)]">{{ c.note }}</span></div>
|
||||
<NButton v-if="!c.done" text type="primary"
|
||||
@click="checks = checks.map((x, xi) => (xi === i ? { ...x, done: true } : x)); toast.success('演示:资料已补充上传,关闭校验清单已同步更新并重新推送专责');">去补充</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 关闭按钮区 -->
|
||||
<div v-if="!archivedHash && !archiving" class="mt-4 space-y-2">
|
||||
<div v-if="readyToClose" class="flex items-center gap-2 rounded-lg bg-[var(--theme-bg)] px-3 py-2 text-xs text-[var(--theme-color)]">
|
||||
<Icon icon="lucide:bell" class="size-14px" />
|
||||
系统已于今日 09:12 自动推送关闭待办至变更主管部门专责(王海燕)· 系统消息 + 企业微信
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-slate-50 p-3">
|
||||
<span class="text-sm text-slate-500">
|
||||
{{ !restoresDone
|
||||
? '临时变更尚未完成恢复确认,禁止关闭(恢复确认是临时变更关闭的前置条件)'
|
||||
: allDone
|
||||
? isCloser
|
||||
? '全部条件满足。确认关闭后系统记录关闭时间与关闭人,并自动生成归档包(副本归档 · 只读锁定 · 哈希存证)'
|
||||
: '全部条件满足,待变更主管部门专责(王海燕 · 安全环保部)确认关闭'
|
||||
: '存在未满足条件,禁止关闭;确需例外须填写说明并经分管领导确认' }}
|
||||
</span>
|
||||
<NButton v-if="isCloser" size="small" type="primary" :disabled="!readyToClose" class="text-xs" @click="startArchive">
|
||||
<Icon icon="uil:lock" class="size-14px mr-1" /> 确认关闭并归档
|
||||
</NButton>
|
||||
<span v-else class="text-xs text-[var(--warning-color)]">仅变更主管部门专责可确认关闭 · 切换至「王海燕 · 安全工程师」体验</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 自动归档流程 -->
|
||||
<div v-if="archiving" class="mt-4 space-y-1.5 rounded-lg border border-[var(--theme-color)] bg-[var(--theme-bg)] p-4">
|
||||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-[var(--theme-color)]">
|
||||
<Icon icon="ri:loader-4-fill" class="size-18px animate-spin" />
|
||||
系统自动归档中,无需人工整理…
|
||||
</div>
|
||||
<div v-for="(s, i) in ARCHIVE_STEPS" :key="s" class="flex items-center gap-2 text-xs"
|
||||
:class="i < step ? 'text-[var(--success-color)]' : i === step ? 'font-medium text-[var(--theme-color)]' : 'text-slate-400'">
|
||||
<Icon v-if="i < step" icon="material-symbols:check-circle-outline" class="size-16px" />
|
||||
<Icon v-else-if="i === step" icon="ri:loader-4-fill" class="size-16px animate-spin" />
|
||||
<span v-else class="h-3.5 w-3.5 shrink-0 rounded-full border border-slate-300"></span>
|
||||
{{ i + 1 }}. {{ s }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 归档完成 -->
|
||||
<div v-if="archivedHash" class="mt-4 space-y-3 rounded-lg border border-emerald-300 bg-emerald-50/60 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm font-semibold text-emerald-800">
|
||||
<Icon icon="uil:lock" class="size-18px" /> 已关闭-已归档 · 归档包只读锁定,任何角色不可修改或删除
|
||||
</div>
|
||||
<div class="grid gap-1.5 text-xs text-emerald-800/90 sm:grid-cols-2">
|
||||
<span>关闭时间:{{ closedAt }} · 关闭人:王海燕(变更管理专责 · 安全环保部)</span>
|
||||
<span>归档路径:归档区 / 01-常减压装置 / 2026 / MOC-2026-R01-0039/</span>
|
||||
<span class="flex items-center gap-1"><Hash class="h-3 w-3" /> SHA-256:{{ archivedHash }}</span>
|
||||
<span>归档内容:6 类文件夹 + 归档索引.txt(共 24 个文件,18.4MB)</span>
|
||||
<span>防篡改:系统定期校验哈希值,异常自动告警并从备份恢复</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<NButton size="small" type="success" class="text-xs" @click="emit('go', 'knowledge')">
|
||||
<Icon icon="lucide:folder-archive" class="size-14px mr-1" /> 查看归档包
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" class="text-xs" @click="toast.success('演示:下载完整归档包(ZIP,用于外部审计)')">
|
||||
<Icon icon="material-symbols:download" class="size-18px" /> 下载 ZIP
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
<NCard :bordered="true" :content-style="{ padding: '0' }" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="py-3 px-4 border-b flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-base font-semibold">
|
||||
<Icon icon="proicons:alert-triangle" class="size-18px text-slate-500" />关闭提醒
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 p-4">
|
||||
<div v-for="o in OVERDUE_LIST" :key="o.id" class="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-3 py-2">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm">{{ o.title }}</div>
|
||||
<div class="text-xs text-slate-500">{{ o.id }} · {{ o.stage }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NTag size="small" type="error">超期 {{ o.days }} 天</NTag>
|
||||
<NButton size="small" class="text-xs" @click="toast.info('演示:已催办(系统消息 + 企业微信)')">催办</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
@@ -1,280 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
import ColumnChart from './modules/columnChart.vue';
|
||||
import RingChart from "./modules/ringChart.vue";
|
||||
import RingChart2 from "./modules/ringChart2.vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string]; openChange: [c: any] }>();
|
||||
|
||||
const range = ref("近一年");
|
||||
const rangeOptions = [
|
||||
{label: "近一年", value: "近一年"},
|
||||
{label: "近半年", value: "近半年"},
|
||||
{label: "近三个月", value: "近三个月"},
|
||||
{label: "近一个月", value: "近一个月"},
|
||||
];
|
||||
|
||||
const changeData = ref([
|
||||
{
|
||||
name: '一车间',
|
||||
value: 42,
|
||||
},
|
||||
{
|
||||
name: '二车间',
|
||||
value: 35,
|
||||
},
|
||||
{
|
||||
name: '三车间',
|
||||
value: 28,
|
||||
},
|
||||
{
|
||||
name: '供应部',
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: '质量部',
|
||||
value: 9,
|
||||
},
|
||||
]);
|
||||
|
||||
const DEPT_ACCEPT = [
|
||||
{ name: "工艺", value: 58 },
|
||||
{ name: "设备", value: 41 },
|
||||
{ name: "管理", value: 27 },
|
||||
];
|
||||
const ACCEPT_COLORS = ["#2080f0", "#faad14", "#52c41a"];
|
||||
|
||||
const DEPT_ACCEPT2 = [
|
||||
{ name: "完全达到", value: 13, money: 61.2, rate: 68 },
|
||||
{ name: "基本达到", value: 4, money: 19.8, rate: 21 },
|
||||
{ name: "未达到", value: 2, money: 5.4, rate: 11 },
|
||||
];
|
||||
const ACCEPT_COLORS2 = ["#52c41a", "#faad14", "#ff4d4f"];
|
||||
|
||||
const residualHigh = ref<{id: string, title: string}>({id:'MOC-2026-R01-0035',title:'R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)'});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的工作台 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-sm font-bold text-slate-800">全厂变更数据中心</p>
|
||||
<p class="text-[11px] text-slate-400 mt-[1px]">时间相关数据统一在此查询 · 统计时段</p>
|
||||
<NSelect v-model:value="range" :options="rangeOptions" size="tiny" class="w-28" />
|
||||
<NButton ghost size="small" class="ml-auto text-xs">导出</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
<!-- 卡片数据 -->
|
||||
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="gap" :y-gap="16"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--theme-bg': getColorWithOpacity(themeStore.themeColor, 0.2),
|
||||
'--warning-bg': getColorWithOpacity(themeStore.otherColor.warning, 0.2),
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
'--success-color': themeStore.otherColor.success,
|
||||
'--success-bg': getColorWithOpacity(themeStore.otherColor.success, 0.2),
|
||||
'--error-color': themeStore.otherColor.error,
|
||||
'--error-bg': getColorWithOpacity(themeStore.otherColor.error, 0.2),
|
||||
}"
|
||||
>
|
||||
<NGi>
|
||||
<div
|
||||
class="h-100% flex-1 flex items-center gap-3 bg-white px-16px py-12px cursor-pointer transition hover:-translate-y-1"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<span class="shrink-0 rounded-lg p-2.5 bg-[var(--theme-bg)]">
|
||||
<Icon icon="akar-icons:file" class="size-24px text-[--theme-color]" />
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="126"
|
||||
class="text-26px font-600"
|
||||
/>
|
||||
<p class="text-[11px] font-semibold text-slate-400">变更总数 · {{ range }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="h-100% flex-1 flex items-center gap-3 bg-white px-16px py-12px cursor-pointer transition hover:-translate-y-1"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<span class="shrink-0 rounded-lg p-2.5 bg-[var(--warning-bg)]">
|
||||
<Icon icon="lucide:shield-check" class="size-24px text-[--warning-color]" />
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="18"
|
||||
class="text-26px font-600"
|
||||
/>
|
||||
<p class="text-[11px] font-semibold text-slate-400">重要变更数量 · {{ range }} · 占比 14%</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="h-100% flex-1 flex items-center gap-3 bg-white px-16px py-12px cursor-pointer transition hover:-translate-y-1"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<span class="shrink-0 rounded-lg p-2.5 bg-[var(--warning-bg)]">
|
||||
<Icon icon="lucide:shield-check" class="size-24px text-[--warning-color]" />
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="4"
|
||||
class="text-26px font-600 text-[var(--error-color)]"
|
||||
/>
|
||||
<p class="text-26px font-600 text-[var(--error-color)]"> 高 /</p>
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="11"
|
||||
class="text-26px font-600 text-[var(--error-color)]"
|
||||
/><p class="text-26px font-600 text-[var(--error-color)]"> 中</p>
|
||||
</div>
|
||||
<p class="text-[11px] font-semibold text-slate-400">变更新增风险 · 剩余高风险 1 · 剩余中风险 6</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="h-100% flex-1 flex items-center gap-3 bg-white px-16px py-12px cursor-pointer transition hover:-translate-y-1"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<span class="shrink-0 rounded-lg p-2.5 bg-[var(--success-bg)]">
|
||||
<Icon icon="material-symbols:check-circle-outline" class="size-24px text-[--success-color]" />
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="86.4"
|
||||
class="text-26px font-600"
|
||||
/>
|
||||
<p class="text-[11px] font-semibold text-slate-400">验收投入合计 · 已验收 19 起 · 完全达标率 68%</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<NGrid
|
||||
:x-gap="gap" :y-gap="16" responsive="screen" item-responsive
|
||||
:style="{
|
||||
'--theme-color':themeStore.themeColor
|
||||
}"
|
||||
>
|
||||
<NGi span="24 s:24 m:24 l:14">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="akar-icons:file" class="size-16px text-[--theme-color]" />
|
||||
<p class="text-13px font-700 text-#64749a">各车间变更数量</p>
|
||||
<p class="text-11px text-#94a3b8 mt-2px">{{range}} · 按申请部门</p>
|
||||
</div>
|
||||
<ColumnChart :data="changeData" style="height: 100%;" />
|
||||
</NCard>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:24 l:10">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="akar-icons:file" class="size-16px text-[--theme-color]" />
|
||||
<p class="text-13px font-700 text-#64749a">变更类型分布</p>
|
||||
<p class="text-11px text-#94a3b8 mt-2px">{{range}} · 工艺 / 设备 / 管理</p>
|
||||
</div>
|
||||
<RingChart :data="DEPT_ACCEPT" :colors="ACCEPT_COLORS" :range="range" />
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<NGrid
|
||||
:x-gap="gap" :y-gap="16" responsive="screen" item-responsive
|
||||
:style="{
|
||||
'--error-color':themeStore.otherColor.error,
|
||||
'--warning-color':themeStore.otherColor.warning,
|
||||
'--success-color':themeStore.otherColor.success,
|
||||
'--theme-color':themeStore.themeColor
|
||||
}"
|
||||
>
|
||||
<NGi span="24 s:24 m:24 l:10">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="lucide:shield-alert" class="size-16px text-[--theme-color]" />
|
||||
<p class="text-13px font-700 text-#64749a">风险增量与管控</p>
|
||||
<p class="text-11px text-#94a3b8 mt-2px">{{range}} · 变更新增风险与管控落实</p>
|
||||
</div>
|
||||
<div class="space-y-2.5">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-lg bg-red-50 px-3 py-2">
|
||||
<div class="text-[11px] text-[var(--error-color)]">新增固有高风险项</div>
|
||||
<div class="text-xl font-bold text-[var(--error-color)]">4</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-amber-50 px-3 py-2">
|
||||
<div class="text-[11px] text-[var(--warning-color)]">新增中风险项</div>
|
||||
<div class="text-xl font-bold text-[var(--warning-color)]">11</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2">
|
||||
<div class="text-[11px] text-slate-500">剩余高风险项</div>
|
||||
<div class="text-xl font-bold text-slate-800">1</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2">
|
||||
<div class="text-[11px] text-slate-500">剩余中风险项</div>
|
||||
<div class="text-xl font-bold text-slate-800">6</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] text-slate-500">
|
||||
<span>管控措施落实率</span>
|
||||
<span class="font-semibold text-[var(--success-color)]">96%</span>
|
||||
</div>
|
||||
<div class="mt-1 h-2 w-full rounded-full bg-slate-100">
|
||||
<div class="h-2 rounded-full bg-[var(--success-color)]" style="width: 96%" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="residualHigh.id" @click="emit('openChange', residualHigh)"
|
||||
class="cursor-pointer w-full rounded-lg border border-red-100 bg-[#FFF2F0] px-3 py-2 text-left transition hover:border-red-300">
|
||||
<span class="block text-[11px] font-medium text-[var(--error-color)]">剩余高风险 · 措施落实中</span>
|
||||
<span class="mt-0.5 block truncate text-xs font-medium text-slate-800">{{ residualHigh.title }}</span>
|
||||
<span class="mt-0.5 block text-[11px] text-slate-400">{{ residualHigh.id }} · 电机吊装作业管控措施执行跟踪</span>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:24 l:14">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="material-symbols:check-circle-outline" class="size-16px text-[--theme-color]" />
|
||||
<p class="text-13px font-700 text-#64749a">变更投入和效果</p>
|
||||
<p class="text-11px text-#94a3b8 mt-2px">{{range}} · 支持查询验收明细</p>
|
||||
</div>
|
||||
<RingChart2 :data="DEPT_ACCEPT2" :colors="ACCEPT_COLORS2" style="height: 210px;" />
|
||||
<div class="flex items-center justify-between border-slate-200 pt-1.5">
|
||||
<span class="text-[11px] text-slate-400">投入合计(变更材料和实施费) · 完全达到率 68%</span>
|
||||
<span class="text-xs font-bold text-[var(--theme-color)]">86.4 万元</span>
|
||||
</div>
|
||||
<div @click="emit('go', 'invest')"
|
||||
class="cursor-pointer mt-3 flex w-full items-center justify-center gap-1 rounded-lg border border-dashed py-2 text-[11px] text-slate-400 transition hover:border-[#6E93C6] hover:text-[#1D4E9C]">
|
||||
验收明细(名称 / 日期链 / 费用 / 结论)见左边栏「变更效果跟踪」<ChevronRight class="h-3 w-3" />
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
|
||||
defineOptions({
|
||||
name: 'RingChart'
|
||||
});
|
||||
|
||||
const props = defineProps<{ data: { name: string; value: number; }[]; }>();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show:false,
|
||||
bottom: '20%',
|
||||
left: 'center',
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: "5%",
|
||||
left: "5%",
|
||||
right: "5%",
|
||||
bottom: "5%",
|
||||
},
|
||||
calculable: true,
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: true, // 新增,固定柱子位置
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitArea: { show: false },
|
||||
data: [] as string[],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
barWidth: 45,
|
||||
barGap: "10%",
|
||||
color: "#2080f0",
|
||||
label: { show: true, position: 'top',color: '#333',fontWeight: 'bold' },
|
||||
data: [] as number[],
|
||||
},
|
||||
]
|
||||
}));
|
||||
|
||||
function init() {
|
||||
updateOptions(opts => {
|
||||
let allMonth = props.data.map((item:any) => item.name);
|
||||
let values = props.data.map((item:any) => item.value);
|
||||
opts.xAxis[0].data = allMonth;
|
||||
opts.series[0].data = values;
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
|
||||
defineOptions({
|
||||
name: 'RingChart'
|
||||
});
|
||||
|
||||
const props = defineProps<{ data?: { name: string; value: number }[]; colors?: string[]; range?: string }>();
|
||||
const range = ref("近一年");
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
},
|
||||
},
|
||||
title: {
|
||||
text: `{a|126}\n{b|${range.value}}`,
|
||||
textStyle: {
|
||||
rich: {
|
||||
a: {
|
||||
fontSize: 24,
|
||||
color: '#333',
|
||||
fontWeight: 800,
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
b: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
fontWeight: 500,
|
||||
padding: [5, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
},
|
||||
left: 'center',
|
||||
top: '35%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||
radius: ['45%', '65%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{c} ({d}%)',
|
||||
fontSize: 12,
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 10,
|
||||
length2: 10
|
||||
},
|
||||
data: [] as { name: string; value: number }[]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
function init() {
|
||||
updateOptions(opts => {
|
||||
range.value = props.range || "近一年";
|
||||
opts.series[0].data = props.data || [];
|
||||
opts.series[0].color = props.colors || [];
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,117 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
|
||||
defineOptions({
|
||||
name: 'RingChart'
|
||||
});
|
||||
|
||||
const props = defineProps<{ data?: { name: string; value: number; money: number; rate: number }[]; colors?: string[]; range?: string }>();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '0%',
|
||||
top: 'center',
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
},
|
||||
textStyle: {
|
||||
rich: {
|
||||
name: {
|
||||
width: 50,
|
||||
},
|
||||
count: {
|
||||
width: 80,
|
||||
align: 'right',
|
||||
fontWeight: 600,
|
||||
},
|
||||
rate: {
|
||||
align: 'left',
|
||||
},
|
||||
money: {
|
||||
width: 80,
|
||||
align: 'right',
|
||||
fontWeight: 600,
|
||||
},
|
||||
}
|
||||
},
|
||||
formatter: function (name: string) {
|
||||
var info = props.data?.find((item: { name: string }) => item.name === name);
|
||||
if(info){
|
||||
return '{name|'+name+'} {count|'+(info.value)+' 起}{rate|'+(info.rate)+' %} {money|'+(info.money)+' 万元}';
|
||||
}else{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: `{a|19}\n{b|已验收}`,
|
||||
textStyle: {
|
||||
rich: {
|
||||
a: {
|
||||
fontSize: 24,
|
||||
color: '#333',
|
||||
fontWeight: 800,
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
b: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
fontWeight: 500,
|
||||
padding: [5, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
},
|
||||
left: '32%',
|
||||
top: '35%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||
radius: ['50%', '70%'],
|
||||
center: ['35%', '45%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{c} ({d}%)',
|
||||
fontSize: 12,
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 10,
|
||||
length2: 10
|
||||
},
|
||||
data: [] as { name: string; value: number }[]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
function init() {
|
||||
updateOptions(opts => {
|
||||
opts.series[0].data = props.data || [];
|
||||
opts.series[0].color = props.colors || [];
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const list = ref<any[]>([]);
|
||||
const lists = [
|
||||
{ title: "氮气管网压力分级调整", id: "MOC-2026-R01-0024", applicant: "张工艺", type: "工艺", level: "重要", duration: "永久", applyDate: "2026-05-28", useTime: "2026-06-20", acceptDate: "2026-07-08", cost: 3.2, result: "完全达到" },
|
||||
{ title: "R-201 搅拌密封改造", id: "MOC-2026-R01-0019", applicant: "张工艺", type: "设备", level: "一般", duration: "永久", applyDate: "2026-03-18", useTime: "2026-04-12", acceptDate: "2026-05-20", cost: 6.8, result: "完全达到" },
|
||||
{ title: "循环水加药泵冲程临时调整", id: "MOC-2026-R01-0022", applicant: "张工艺", type: "设备", level: "一般", duration: "临时", applyDate: "2026-04-25", useTime: "2026-05-18", acceptDate: "2026-06-30", cost: 1.5, result: "基本达到" },
|
||||
];
|
||||
// 获取列表数据
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
setTimeout(() => {
|
||||
list.value = lists;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 导出文件
|
||||
const exportFile = () => {
|
||||
window.$message?.success(`导出本车间已验收变更投入和效果表(Excel)`)
|
||||
}
|
||||
// 查看
|
||||
const openRow = (r: any) => {
|
||||
window.$message?.info(`打开变更详情页 ${r.id}(含验收报告与费用明细)`);
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- PSSR监督 -->
|
||||
<NSpin :show="loading" size="small">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div
|
||||
class="h-100% flex flex-wrap items-center gap-x-5 gap-y-2 border border-slate-200 px-3 py-2 mb-3"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--success-bg': themeStore.otherColor.success,
|
||||
'--warning-bg': themeStore.otherColor.warning,
|
||||
'--error-bg': themeStore.otherColor.error,
|
||||
}"
|
||||
>
|
||||
<span class="text-sm font-bold text-slate-800">本车间已验收变更</span>
|
||||
<span class="text-xs text-slate-500">共 <b class="text-[#1D4E9C]">{{ list.length }}</b> 起</span>
|
||||
<span class="text-xs text-slate-500">投入合计 <b class="text-[#1D4E9C]">11.5</b> 万元</span>
|
||||
<span class="text-xs text-slate-500">完全达到率 <b class="text-[var(--success-bg)]">67%</b></span>
|
||||
<span class="hidden h-4 w-px bg-slate-200 sm:block"></span>
|
||||
<span class="text-xs text-slate-500">
|
||||
<span class="mr-1 inline-block h-2 w-2 rounded-full align-middle bg-[var(--success-bg)]"></span>
|
||||
完全达到<b class="text-slate-800">2</b> 起 · <b class="text-slate-800">10.0</b> 万元
|
||||
</span>
|
||||
<span class="text-xs text-slate-500">
|
||||
<span class="mr-1 inline-block h-2 w-2 rounded-full align-middle bg-[var(--warning-bg)]"></span>
|
||||
基本达到<b class="text-slate-800">1</b> 起 · <b class="text-slate-800">1.5</b> 万元
|
||||
</span>
|
||||
<span class="text-xs text-slate-500">
|
||||
<span class="mr-1 inline-block h-2 w-2 rounded-full align-middle bg-[var(--error-bg)]"></span>
|
||||
未达到<b class="text-slate-800">0</b> 起 · <b class="text-slate-800">0.0</b> 万元
|
||||
</span>
|
||||
<NButton size="small" ghost class="ml-auto text-xs" @click="exportFile">
|
||||
<Icon icon="material-symbols:download" class="size-16px" />导出
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="rounded-lg bg-white shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
|
||||
<div class="flex items-center gap-3 border-b bg-slate-50 px-4 py-2 text-[11px] font-semibold text-slate-500">
|
||||
<span class="min-w-0 flex-1">变更名称 / 编号</span>
|
||||
<span class="w-[92px] shrink-0">申请日期</span>
|
||||
<span class="w-[92px] shrink-0">投用日期</span>
|
||||
<span class="w-[92px] shrink-0">验收日期</span>
|
||||
<span class="w-[80px] shrink-0 text-right">费用(万元)</span>
|
||||
<span class="w-[100px] shrink-0 text-center">是否达到预期效果</span>
|
||||
<span class="w-[64px] shrink-0 text-right">操作</span>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="divide-y">
|
||||
<div v-for="r in list" :key="r.id" class="flex flex-wrap items-center gap-x-3 gap-y-1.5 px-4 py-3 transition hover:bg-[#EAF0F9]/40">
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="truncate text-sm font-medium text-slate-800">{{ r.title }}</span>
|
||||
<NTag v-if="r.level==='一般'" size="small">{{ r.level }}</NTag>
|
||||
<NTag v-if="r.level==='重要'" size="small" type="error">{{ r.level }}</NTag>
|
||||
<NTag size="small" type="info">{{ r.type }}</NTag>
|
||||
<NTag v-if="r.duration==='永久'" size="small">{{ r.duration }}</NTag>
|
||||
<NTag v-if="r.duration==='临时'" size="small" type="warning">{{ r.duration }}</NTag>
|
||||
</span>
|
||||
<span class="mt-0.5 block font-mono text-[11px] text-slate-400">{{ r.id }} · {{ r.applicant }} · 费用取验收时填报的「变更材料和实施费」</span>
|
||||
</span>
|
||||
<span class="w-[92px] shrink-0 text-xs text-slate-600">{{ r.applyDate }}</span>
|
||||
<span class="w-[92px] shrink-0 text-xs text-slate-600">{{ r.useTime }}</span>
|
||||
<span class="w-[92px] shrink-0 text-xs text-slate-600">{{ r.acceptDate }}</span>
|
||||
<span class="w-[80px] shrink-0 text-right text-sm font-bold text-slate-800">{{ r.cost.toFixed(1) }}</span>
|
||||
<span class="w-[100px] shrink-0 text-center">
|
||||
<NTag v-if="r.result==='完全达到'" size="small" type="success">{{ r.result }}</NTag>
|
||||
<NTag v-if="r.result==='基本达到'" size="small" type="warning">{{ r.result }}</NTag>
|
||||
<NTag v-if="r.result==='未达到'" size="small" type="error">{{ r.result }}</NTag>
|
||||
</span>
|
||||
<span class="w-[64px] shrink-0 text-right">
|
||||
<NButton size="small" ghost class="text-xs" @click="openRow(r)">查看</NButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpin>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 240px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,476 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const keyword = ref<string>("");
|
||||
const progressDrawer = ref<boolean>(false);
|
||||
const currentTab = ref<string>("highRisk");
|
||||
const range = ref<string>("近一个月");
|
||||
const dept = ref<string>("全部部门");
|
||||
const method = ref<string>("全部");
|
||||
// 当前记录
|
||||
const currentRecord = ref<any>(null);
|
||||
const TABS = [
|
||||
{ k: "highRisk", name: "高风险场景" },
|
||||
{ k: "history", name: "历史分析检索" },
|
||||
{ k: "tpl", name: "方法模板库" },
|
||||
];
|
||||
const RISK_RANGES = ["近一个月", "近三个月", "近半年", "全部"];
|
||||
const departments = ref<any[]>([
|
||||
{ id: 1, name: "全部部门" },
|
||||
{ id: 2, name: "一车间" },
|
||||
{ id: 3, name: "公用工程车间" },
|
||||
{ id: 4, name: "供应部" },
|
||||
{ id: 5, name: "三车间" },
|
||||
]);
|
||||
const list = ref<any[]>([]);
|
||||
const highRiskList = [
|
||||
{
|
||||
cid: "MOC-2026-R01-0036", item: "反应飞温", scene: "温度上限提高至 82℃ 后飞温反应加速,联锁动作前安全裕量收窄",
|
||||
method: "HAZOP", residual: "中", dept: "一车间", adate: "2026-08-02", owner: "王海燕",
|
||||
title: '聚合反应温度控制上限调整(78℃→82℃)', status: '审批中',
|
||||
info: 'MOC-2026-R01-0036 聚合反应温度控制上限调整(78℃→82℃)', count: 3,
|
||||
hazopRows: [
|
||||
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "温控回路失效或冷却水中断;82℃ 新上限更接近溶剂回流温度,超温溜温概率上升", cons: "反应溜温冲料,釜压骤升,物料喷溅造成灼烫与环境污染", L: 3, S: 4, safety: "DCS 温度高报 82℃、高高报及联锁 85℃(维持安全边界不变);紧急冷却有效", suggest: "投用前完成联锁测试并留存记录;投用首周每班复核温度趋势" },
|
||||
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "未知杂质含量升高(0.05%→0.12%),热稳定性尚未定性,长期累积加速釜壁结垢", cons: "副反应加剧,可能引发二次分解放热,搅拌负荷异常", L: 2, S: 4, safety: "小试热稳定性筛查;安全阀泄放能力不受本次调整影响,仍然有效", suggest: "将杂质含量纳入日常分析计划(每周 1 次,连续 8 周)" },
|
||||
{ node: "节点2 · DCS 温控与联锁", dev: "报警/联锁值设置不当", cause: "上限调整后报警值与联锁值未同步复核整定", cons: "报警提前量不足,操作响应时间被压缩", L: 2, S: 4, safety: "修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: "MOC-2026-R01-0035", item: "动火与临时用电燃爆", scene: "电机更换动火与临时用电作业引发可燃气体燃爆",
|
||||
method: "JSA", residual: "中", dept: "一车间", adate: "2026-07-30", owner: "王仪表",
|
||||
title: '反应釜搅拌器电机更换(55kW→75kW 防爆电机)', status: '审批中',
|
||||
info: 'MOC-2026-R01-0035 R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)', count: 3,
|
||||
jsaRows: [
|
||||
{ item: "动火作业燃爆", scene: "步骤2:旧电机拆除与新电机安装动火,可燃蒸气聚集遇火源燃爆", inherent: "高", existing: "作业票证办理;动火点可燃气体检测合格;消防器材到位", suggest: "动火点下方设接火盘与防火毯,专人监护并每 30 分钟复测", residual: "中" },
|
||||
{ item: "临时用电触电", scene: "步骤1:临时电缆敷设与配电箱接线,绝缘破损导致触电", inherent: "中", existing: "临时用电票证;漏保校验合格", suggest: "电缆架空敷设并挂牌,每日开工前绝缘检查", residual: "低" },
|
||||
{ item: "起重吊装伤害", scene: "步骤3:75kW 电机吊装就位,吊具失效或指挥失误导致起重伤害", inherent: "中", existing: "吊具检验合格;起重指挥持证", suggest: "吊装区设警戒线,禁止交叉作业", residual: "低" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: null, changeTitle: "MOC-2026-R01-0048 R-101 温度联锁修改", item: "VOCs 直排超标", scene: "活性炭吸附饱和穿透,VOCs 直排大气超标",
|
||||
method: "JSA", residual: "高", dept: "一车间", adate: "2026-07-31", owner: "王仪表",
|
||||
title: '温度联锁修改', status: '审批中',
|
||||
info: 'MOC-2026-R01-0048 R-101 温度联锁修改', count: 2,
|
||||
jsaRows: [
|
||||
{ item: "吸附饱和穿透", scene: "步骤3:活性炭吸附床运行后期,吸附饱和穿透导致 VOCs 直排大气", inherent: "高", existing: "出口 VOCs 在线监测报警;按周期更换活性炭", suggest: "增设穿透点压差监测与双床切换程序;更换周期由 6 个月缩短至 4 个月", residual: "中" },
|
||||
{ item: "联锁参数误改", scene: "步骤3:DCS 参数修改与联锁测试,参数误改导致报警/联锁失效", inherent: "高", existing: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控", residual: "中" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: "MOC-2026-R02-0041", item: "超压与窒息", scene: "氮气减压阀组失效导致下游设备超压、作业人员窒息",
|
||||
method: "检查表", residual: "低", dept: "公用工程车间", adate: "2026-07-29", owner: "周设备",
|
||||
title: '公用工程氮气减压阀组改造', status: '审批中',
|
||||
info: 'MOC-2026-R02-0041 公用工程氮气减压阀组改造', count: 2,
|
||||
jsaRows: [
|
||||
{ item: "下游设备超压", scene: "减压阀失效全开,高压氮气直入低压管网导致下游设备超压", inherent: "高", existing: "下游设安全阀;管网压力高报", suggest: "减压阀双阀串联并定期校验;安全阀起跳压力复核", residual: "低" },
|
||||
{ item: "人员窒息", scene: "阀组间通风不良,氮气泄漏积聚导致氧含量不足", inherent: "中", existing: "阀组间强制通风;进入前测氧", suggest: "增设固定式氧含量报警仪并联动风机", residual: "低" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: "MOC-2026-R01-0039", item: "超压保护裕量降低", scene: "安全阀起跳压力临时上调期间,V-102 超压保护裕量降低",
|
||||
method: "AI 预分析", residual: "中", dept: "一车间", adate: "2026-07-15", owner: "王海燕",
|
||||
title: '紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', status: '审批中',
|
||||
info: 'MOC-2026-R01-0039 紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', count: 2,
|
||||
jsaRows: [
|
||||
{ item: "超压保护裕量降低", scene: "起跳压力由 1.0MPa 临时上调至 1.15MPa,V-102 超压工况下保护动作延迟", inherent: "高", existing: "临时期限 30 天;操作压力监控加强至每 2 小时记录", suggest: "恢复前完成新安全阀校验更换;临时期间压力联锁值同步下调验证", residual: "中" },
|
||||
{ item: "紧急变更手续补办遗漏", scene: "紧急变更先行实施,后补手续存在资料缺项风险", inherent: "中", existing: "紧急变更 48 小时内补办审批", suggest: "资料归档清单逐项核对,安全部门复核", residual: "低" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: "MOC-2026-R03-0040", item: "原料杂质副反应", scene: "新供应商环己烷杂质谱变化引发副反应,影响产品色相与收率",
|
||||
method: "HAZOP", residual: "中", dept: "供应部", adate: "2026-07-18", owner: "陈质量",
|
||||
title: '原料环己烷供应商变更(新增 B 供应商)', status: '审批中',
|
||||
info: 'MOC-2026-R03-0040 原料环己烷供应商变更(新增 B 供应商)', count: 2,
|
||||
hazopRows: [
|
||||
{ node: "节点1 · 原料接收与储存", dev: "杂质含量高", cause: "B 供应商环己烷杂质谱(苯/硫)高于原供应商,进厂检验项目未覆盖", cons: "副反应加剧,产品色相超标、收率下降,结垢加速", L: 3, S: 3, safety: "进厂检验按原指标执行;供应商 COA 随货同行", suggest: "检验项目增加杂质谱分析;首批三批加严检验" },
|
||||
{ node: "节点2 · 聚合反应", dev: "副反应加剧", cause: "微量苯参与链转移,分子量分布变宽", cons: "产品性能波动,下游加工投诉", L: 2, S: 3, safety: "聚合配方留有调节余量", suggest: "建立 B 供应商原料的配方微调作业指导" },
|
||||
],
|
||||
},
|
||||
{
|
||||
cid: "MOC-2026-R03-0042", item: "回路切换失误", scene: "自动化改造调试期间控制回路切换失误,造成装置非计划停车",
|
||||
method: "JSA", residual: "中", dept: "三车间", adate: "2026-07-31", owner: "王仪表",
|
||||
title: '三车间自动化和安全隐患整改项目变更', status: '审批中',
|
||||
info: 'MOC-2026-R03-0042 三车间自动化和安全隐患整改项目变更', count: 2,
|
||||
jsaRows: [
|
||||
{ item: "回路切换失误", scene: "步骤4:新旧控制系统切换,回路误切导致装置非计划停车", inherent: "高", existing: "切换方案审批;切换前模拟演练", suggest: "切换双人复核逐项签字;保留一键回切手段 72 小时", residual: "中" },
|
||||
{ item: "静电损伤卡件", scene: "步骤2:卡件插拔作业静电防护不到位,DCS 卡件损坏", inherent: "中", existing: "佩戴防静电手环", suggest: "卡件备件现场备妥,作业区铺设防静电垫", residual: "低" },
|
||||
],
|
||||
},
|
||||
];
|
||||
const historyList = [
|
||||
{ id: "HAZOP-2025-018", change: "MOC-2025-R01-0023 常压炉燃料气切断阀改造", method: "HAZOP", unit: "01-常减压装置", scope: "燃料气系统节点", rows: 12, date: "2025-10-20" },
|
||||
{ id: "HAZOP-2024-006", change: "MOC-2024-R01-0003 减一线换热器 E-108 芯子更换", method: "HAZOP", unit: "01-常减压装置", scope: "换热系统节点", rows: 8, date: "2024-03-15" },
|
||||
{ id: "JSA-2025-041", change: "MOC-2025-R02-0014 再生器旋风分离器更换", method: "JSA", unit: "02-催化裂化装置", scope: "受限空间作业", rows: 6, date: "2025-05-08" },
|
||||
{ id: "JSA-2026-012", change: "MOC-2026-R01-0035 搅拌电机更换", method: "JSA", unit: "01-常减压装置", scope: "检维修作业", rows: 5, date: "2026-07-29" },
|
||||
{ id: "SCL-2025-033", change: "MOC-2025-R02-0021 气压机出口压力联锁逻辑修改", method: "检查表", unit: "02-催化裂化装置", scope: "联锁系统", rows: 14, date: "2025-09-18" },
|
||||
{ id: "AI-2026-008", change: "MOC-2026-R03-0008 聚合釜搅拌器密封形式变更", method: "AI 预分析", unit: "03-聚合装置", scope: "—", rows: 4, date: "2026-05-06" },
|
||||
];
|
||||
const templateList = [
|
||||
{ name: "HAZOP 节点划分模板", desc: "含偏差引导词库(过高/过低/无/反向等 18 个)与节点划分示例", tag: "HAZOP" },
|
||||
{ name: "JSA 作业步骤分析模板", desc: "作业步骤分解 → 危害识别 → 控制措施三段式,含检维修示例", tag: "JSA" },
|
||||
{ name: "安全检查表(SCL)模板库", desc: "按设备类型 28 套(机泵 / 换热器 / 塔器 / 储罐 / 电气 / 仪表)", tag: "检查表" },
|
||||
{ name: "风险矩阵 5×5 分级定义", desc: "可能性 × 严重性分级标准(公司 Q/SH 0600),AI 评级引用此定义", tag: "通用" },
|
||||
];
|
||||
const isHazop = computed(() => !!currentRecord.value?.hazopRows);
|
||||
const th = "border-b bg-slate-50 px-3 py-2 text-left text-[11px] font-semibold text-slate-500";
|
||||
const td = "border-b px-3 py-2 align-top text-xs leading-relaxed text-slate-700";
|
||||
|
||||
const hazopNodes = computed(() => {
|
||||
const scene = currentRecord.value;
|
||||
if (!scene?.hazopRows) return [] as { node: string; dev: string; n: number }[];
|
||||
const nodes = Array.from(new Map(scene.hazopRows.map((r:any) => [`${r.node}|${r.dev}`, { node: r.node, dev: r.dev, n: 0 }])).values());
|
||||
scene.hazopRows.forEach((r:any) => { const k:any = nodes.find((n:any) => n.node === r.node && n.dev === r.dev); if (k) k.n += 1; });
|
||||
return nodes;
|
||||
});
|
||||
const sel = ref(0);
|
||||
const selRows = computed(() => {
|
||||
const scene = currentRecord.value;
|
||||
if (!scene?.hazopRows) return [] as any;
|
||||
const n:any = hazopNodes.value[sel.value];
|
||||
return scene.hazopRows.filter((r:any) => r.node === n?.node && r.dev === n?.dev);
|
||||
});
|
||||
// HAZOP 风险矩阵:RR = L × S
|
||||
const rrTag = (L: number, S: number) => {
|
||||
const v = L * S;
|
||||
return v >= 12 ? { label: `较大 (${v})`, cls: "error" }
|
||||
: v >= 8 ? { label: `一般 (${v})`, cls: "warning" }
|
||||
: { label: `低 (${v})`, cls: "success" };
|
||||
};
|
||||
|
||||
// 切换tab
|
||||
const tabChange = (k: string) => {
|
||||
list.value = []
|
||||
currentTab.value = k;
|
||||
loading.value = true;
|
||||
if(k === 'highRisk'){
|
||||
setTimeout(() => {
|
||||
list.value = highRiskList;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
if(k === 'history'){
|
||||
setTimeout(() => {
|
||||
list.value = historyList;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
if(k === 'tpl'){
|
||||
setTimeout(() => {
|
||||
list.value = templateList;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 高风险场景查看变更
|
||||
const openSceneChange = (record: any) => {
|
||||
routerPushByKey('home_highriskdetail', { query: { params: JSON.stringify(record) } })
|
||||
};
|
||||
// 查看分析记录
|
||||
const viewAnalysis = (record: any) => {
|
||||
currentRecord.value = record;
|
||||
progressDrawer.value = true;
|
||||
};
|
||||
// 查看历史分析检索
|
||||
const viewHistory = (id: string) => {
|
||||
window.$message?.info(`演示:在线查看 ${id} 完整分析记录`)
|
||||
};
|
||||
// 复用到新变更
|
||||
const copyHistory = (id: string) => {
|
||||
window.$message?.success(`演示:${id} 已复用到新变更草稿,分析行已带入待确认`)
|
||||
}
|
||||
// 预览模板
|
||||
const previewTemplate = (name: string) => {
|
||||
window.$message?.info(`演示:在线预览「${name}」`)
|
||||
}
|
||||
// 下载模板
|
||||
const downloadTemplate = (name: string) => {
|
||||
window.$message?.success(`演示:下载「${name}」(Word)`)
|
||||
}
|
||||
// 导出
|
||||
const exportFile = () => {
|
||||
window.$message?.success('分析记录表已导出(Excel)');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tabChange('highRisk');
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 本专业高风险 -->
|
||||
<NSpin :show="loading" size="small">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<div v-for="t in TABS" :key="t.k" @click="tabChange(t.k)"
|
||||
class="flex items-center gap-1.5 px-3 py-1 text-xs cursor-pointer select-none rounded-[20px] border"
|
||||
:style="{ '--theme-color': themeStore.themeColor}"
|
||||
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white border-[var(--theme-color)]' : 'text-slate-600 hover:bg-slate-100'"
|
||||
>
|
||||
{{ t.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 高风险场景 -->
|
||||
<div v-if="currentTab === 'highRisk'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
|
||||
<p class="text-base font-semibold">变更新增高风险场景</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-h-28px">
|
||||
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
|
||||
<span class="mr-1 text-xs text-slate-400">时间</span>
|
||||
<NButton
|
||||
size="tiny"
|
||||
round
|
||||
:type="range === r ? 'primary' : 'default'"
|
||||
v-for="r in RISK_RANGES"
|
||||
:key="r"
|
||||
@click="range = r"
|
||||
>
|
||||
{{ r }}
|
||||
</NButton>
|
||||
<span class="ml-3 mr-1 text-xs text-slate-400">部门</span>
|
||||
<NButton
|
||||
size="tiny"
|
||||
round
|
||||
v-for="d in departments"
|
||||
:key="d.id"
|
||||
:type="dept === d.name ? 'primary' : 'default'"
|
||||
@click="dept = d.name"
|
||||
>
|
||||
{{ d.name }}
|
||||
</NButton>
|
||||
<span class="ml-auto self-center text-[11px] text-slate-400">{{ list.length }} 个高风险场景</span>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="space-y-3 px-4 pb-3">
|
||||
<div v-for="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<NTag type="info" size="small">{{ s.method }}</NTag>
|
||||
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
|
||||
<NTag type="error" size="small">固有风险 · 高</NTag>
|
||||
<NTag v-if="s.residual==='高'" type="error" size="small">剩余风险 · {{ s.residual }}</NTag>
|
||||
<NTag v-if="s.residual==='中'" type="warning" size="small">剩余风险 · {{ s.residual }}</NTag>
|
||||
<NTag v-if="s.residual==='低'" type="info" size="small">剩余风险 · {{ s.residual }}</NTag>
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-slate-600">{{ s.scene }}</div>
|
||||
<div class="mt-0.5 text-xs text-slate-400">
|
||||
变更:{{ s.info }} · {{ s.dept }} · 分析日期 {{ s.adate }} · 责任:{{ s.owner }} · 分析记录 {{ s.count }} 行
|
||||
</div>
|
||||
</div>
|
||||
<NButton ghost size="small" class="text-xs" @click="openSceneChange(s)">查看变更</NButton>
|
||||
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">当前筛选条件下无新增高风险场景</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 历史分析检索 -->
|
||||
<div v-if="currentTab === 'history'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="mage:book" class="size-18px" :style="{color:themeStore.themeColor}" />
|
||||
<p class="text-base font-semibold">历史分析记录检索(企业风险知识资产)</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-h-28px">
|
||||
<NInput v-model:value="keyword" size="small" class="text-xs !w-200px" placeholder="按单号 / 装置 / 关键词检索…">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
|
||||
<NButton
|
||||
size="tiny"
|
||||
round
|
||||
v-for="m in ['全部', 'HAZOP', 'JSA', '检查表', 'AI 预分析']"
|
||||
:key="m"
|
||||
:type="method === m ? 'primary' : 'default'"
|
||||
@click="method = m"
|
||||
>
|
||||
{{ m }}
|
||||
</NButton>
|
||||
<span class="ml-auto self-center text-[11px] text-slate-400">{{ list.length }} 条记录</span>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="space-y-3 px-4 pb-3">
|
||||
<div v-for="h in list" :key="h.id" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-2.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ h.id }}</span>
|
||||
<NTag type="info" size="small">{{ h.method }}</NTag>
|
||||
<span class="text-sm text-slate-800">{{ h.change }}</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-slate-500">{{ h.unit }} · 范围:{{ h.scope }} · {{ h.rows }} 行分析记录 · {{ h.date }}</div>
|
||||
</div>
|
||||
<NButton text type="primary" @click="viewHistory(h.id)">
|
||||
<Icon icon="lucide:eye" class="size-16px" />
|
||||
</NButton>
|
||||
<NButton ghost size="small" class="text-xs" @click="copyHistory(h.id)">复用到新变更</NButton>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的分析记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 方法模板库 -->
|
||||
<div v-if="currentTab === 'tpl'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="akar-icons:file" class="size-16px" :style="{color:themeStore.themeColor}" />
|
||||
<p class="text-base font-semibold">方法模板库(AI 风险预分析的知识来源之一)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="space-y-3 px-4 pb-3">
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
<div v-for="t in list" :key="t.name" class="rounded-lg border p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-slate-800">{{ t.name }}</span>
|
||||
<NTag size="small" type="info">{{ t.tag }}</NTag>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs leading-relaxed text-slate-500">{{ t.desc }}</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<NButton ghost size="small" class="text-xs" @click="previewTemplate(t.name)">查看</NButton>
|
||||
<NButton ghost size="small" class="text-xs" @click="downloadTemplate(t.name)">
|
||||
<Icon icon="material-symbols:download" class="size-14px" />下载
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的方法模板库</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度弹框 -->
|
||||
<NDrawer v-model:show="progressDrawer" width="85%" placement="right">
|
||||
<NDrawerContent closable>
|
||||
<template #header>
|
||||
<span class="flex flex-wrap items-center gap-2 text-base">
|
||||
风险分析记录表
|
||||
<NTag size="small" type="info">{{ currentRecord.method }}</NTag>
|
||||
<NTag size="small" type="error">含固有风险 · 高 场景</NTag>
|
||||
</span>
|
||||
</template>
|
||||
<div>
|
||||
<div class="mb-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-slate-500">
|
||||
<span>变更:<span class="font-medium text-slate-700">{{ currentRecord.info }}</span></span>
|
||||
<span>分析日期:{{ currentRecord.adate }}</span>
|
||||
<span>责任:{{ currentRecord.owner }}</span>
|
||||
<span class="ml-auto text-slate-400">分析编制与修改在变更上下文内完成,此处只读</span>
|
||||
</div>
|
||||
<div v-if="!isHazop" class="overflow-x-auto rounded-lg border border-b-0">
|
||||
<table class="w-full min-w-[860px] border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="th" class="w-[130px]">风险项</th>
|
||||
<th :class="th">场景描述</th>
|
||||
<th :class="th" class="w-[76px] text-center">固有风险</th>
|
||||
<th :class="th">现有措施</th>
|
||||
<th :class="th">建议措施</th>
|
||||
<th :class="th" class="w-[76px] text-center">剩余风险</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(r, i) in currentRecord.jsaRows" :key="i" :class="r.item === currentRecord.item && r.inherent === '高' ? 'bg-red-50/40' : ''">
|
||||
<td :class="td" class="font-medium text-slate-800">{{ r.item }}</td>
|
||||
<td :class="td">{{ r.scene }}</td>
|
||||
<td :class="td" class="text-center">
|
||||
<NTag size="small" type="error" v-if="r.inherent==='高'">{{ r.inherent }}</NTag>
|
||||
<NTag size="small" type="warning" v-if="r.inherent==='中'">{{ r.inherent }}</NTag>
|
||||
<NTag size="small" type="info" v-if="r.inherent==='低'">{{ r.inherent }}</NTag>
|
||||
</td>
|
||||
<td :class="td">{{ r.existing }}</td>
|
||||
<td :class="td">{{ r.suggest }}</td>
|
||||
<td :class="td" class="text-center">
|
||||
<NTag size="small" type="error" v-if="r.residual==='高'">{{ r.residual }}</NTag>
|
||||
<NTag size="small" type="warning" v-if="r.residual==='中'">{{ r.residual }}</NTag>
|
||||
<NTag size="small" type="info" v-if="r.residual==='低'">{{ r.residual }}</NTag>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="flex gap-3">
|
||||
<!-- 节点 · 偏离导航 -->
|
||||
<div class="w-48 shrink-0 space-y-1.5">
|
||||
<div v-for="(n, i) in hazopNodes" :key="i" @click="sel = i"
|
||||
class="w-full rounded-lg border px-3 py-2 text-left text-xs cursor-pointer"
|
||||
:class="sel === i ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'hover:border-[var(--theme-color)]'"
|
||||
:style="{'--theme-color': themeStore.themeColor}"
|
||||
>
|
||||
<span class="block truncate font-medium">{{ n.dev }}</span>
|
||||
<span class="mt-0.5 flex items-center justify-between text-[11px]" :class="sel === i ? 'text-white/80' : 'text-slate-400'">
|
||||
<span class="truncate">{{ n.node.replace(/^节点\d+ · /, '') }}</span>
|
||||
<NTag size="tiny" round class="ml-1" :type="sel === i ? 'default' : 'success'">{{ n.n }} 条</NTag>
|
||||
</span>
|
||||
</div>
|
||||
<p class="px-1 pt-1 text-[11px] leading-relaxed text-slate-400">L / S 取值 1–5,RR 按 5×5 风险矩阵实时重算</p>
|
||||
</div>
|
||||
<!-- 偏差分析表 -->
|
||||
<div class="min-w-0 flex-1 overflow-x-auto rounded-lg border border-b-0">
|
||||
<table class="w-full min-w-[760px] border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="th">原因</th>
|
||||
<th :class="th">后果</th>
|
||||
<th :class="th" class="w-10 !text-center">L</th>
|
||||
<th :class="th" class="w-10 !text-center">S</th>
|
||||
<th :class="th" class="w-[92px] !text-center">RR</th>
|
||||
<th :class="th">安全措施</th>
|
||||
<th :class="th">建议措施</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(r, i) in selRows" :key="i">
|
||||
<td :class="td">{{ r.cause }}</td>
|
||||
<td :class="td">{{ r.cons }}</td>
|
||||
<td :class="td" class="text-center">{{ r.L }}</td>
|
||||
<td :class="td" class="text-center">{{ r.S }}</td>
|
||||
<td :class="td" class="text-center">
|
||||
<NTag size="small" :type="rrTag(r.L, r.S).cls">{{ rrTag(r.L, r.S).label }}</NTag>
|
||||
</td>
|
||||
<td :class="td">{{ r.safety }}</td>
|
||||
<td :class="td">{{ r.suggest }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton ghost @click="exportFile"><Icon icon="material-symbols:download" class="size-16px" />导出</NButton>
|
||||
<NButton type="primary" @click="progressDrawer = false">关闭</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</NCard>
|
||||
</NSpin>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 303px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import {
|
||||
countOpenStatus, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||
} from "@/typings/model";
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
import FlowRing from "../modules/FlowRing.vue";
|
||||
import FlowRing from "./modules/FlowRing.vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// 单元格点击编辑器(风险记录表/插件表格通用)
|
||||
import { ref } from "vue";
|
||||
const props = defineProps<{ v: string }>();
|
||||
const emit = defineEmits<{ update: [x: string] }>();
|
||||
const ed = ref(false);
|
||||
const val = ref(props.v);
|
||||
const start = () => { val.value = props.v; ed.value = true; };
|
||||
const done = () => { ed.value = false; if (val.value !== props.v) emit("update", val.value); };
|
||||
</script>
|
||||
<template>
|
||||
<NInput v-if="ed" class="text-xs" v-model:value="val" type="textarea" autofocus rows="3" @blur="done" />
|
||||
<p v-else @click="start" title="点击修改"
|
||||
class="cursor-pointer w-full rounded px-1 py-0.5 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]">
|
||||
<template v-if="v">{{ v }}</template>
|
||||
<span v-else class="text-slate-300">请输入</span>
|
||||
</p>
|
||||
</template>
|
||||
@@ -1,19 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ status: any }>();
|
||||
const BIG_STAGES = ["申请审批", "实施与关闭", "变更关闭"];
|
||||
const bigStageOf = (s: any) => (s === "审批中" ? 0 : s === "已关闭" ? 2 : 1);
|
||||
const cur = bigStageOf(props.status);
|
||||
const pct = ((cur + 1) / BIG_STAGES.length) * 100;
|
||||
const size = 34, stroke = 4, r = (size - stroke) / 2, c = 2 * Math.PI * r;
|
||||
const title = `阶段进度 ${cur + 1}/${BIG_STAGES.length}:${BIG_STAGES.slice(0, cur + 1).join(" → ")}${cur < BIG_STAGES.length - 1 ? " → …" : ""}`;
|
||||
</script>
|
||||
<template>
|
||||
<span class="relative inline-flex shrink-0 items-center justify-center" :style="{ width: size + 'px', height: size + 'px' }" :title="title">
|
||||
<svg :width="size" :height="size" class="-rotate-90">
|
||||
<circle :cx="size / 2" :cy="size / 2" :r="r" stroke="#E2E8F0" :stroke-width="stroke" fill="none" />
|
||||
<circle :cx="size / 2" :cy="size / 2" :r="r" stroke="#2080f0" :stroke-width="stroke" fill="none"
|
||||
:stroke-dasharray="c" :stroke-dashoffset="c * (1 - pct / 100)" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span class="absolute inset-0 flex items-center justify-center text-[9px] font-bold text-[#2080f0] dark:text-white">{{ cur + 1 }}/3</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// 变更后单元格蓝色高亮
|
||||
import { computed } from "vue";
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const props = defineProps<{ text: string; hl?: string[] }>();
|
||||
interface Seg { t: string; hit: boolean }
|
||||
const segs = computed<Seg[]>(() => {
|
||||
const text = props.text;
|
||||
if (!props.hl || props.hl.length === 0) return [{ t: text, hit: false }];
|
||||
const out: Seg[] = [];
|
||||
let rest = text;
|
||||
const hls = [...props.hl].sort((a, b) => b.length - a.length);
|
||||
while (rest.length) {
|
||||
let idx = -1, kw = "";
|
||||
for (const h of hls) {
|
||||
const i = rest.indexOf(h);
|
||||
if (i >= 0 && (idx < 0 || i < idx)) { idx = i; kw = h; }
|
||||
}
|
||||
if (idx < 0) { out.push({ t: rest, hit: false }); break; }
|
||||
if (idx > 0) out.push({ t: rest.slice(0, idx), hit: false });
|
||||
out.push({ t: kw, hit: true });
|
||||
rest = rest.slice(idx + kw.length);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<span class="whitespace-pre-line"><template v-for="(s, i) in segs" :key="i">
|
||||
<mark
|
||||
v-if="s.hit" class="rounded bg-blue-100 px-0.5"
|
||||
:style="{color: themeStore.themeColor}"
|
||||
>{{ s.t }}</mark>
|
||||
<template v-else>{{ s.t }}</template></template></span>
|
||||
</template>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
|
||||
|
||||
interface Draft {
|
||||
id: string;
|
||||
name: string;
|
||||
time: string;
|
||||
step: string;
|
||||
progress: string;
|
||||
}
|
||||
const drafts = ref<Draft[]>([
|
||||
{ id: "D-2026-011", name: "R-101 搅拌提速至 180rpm(80℃→95℃)", time: "2026-08-02 09:41", step: "已生成申请表 · 待提交", progress: "4/6 页" },
|
||||
{ id: "D-2026-009", name: "P-301 机封冲洗方案调整(驳回重报)", time: "2026-07-30 16:22", step: "变更预识别完成", progress: "2/6 页" },
|
||||
{ id: "D-2026-007", name: "化验室新增 COD 在线分析仪", time: "2026-07-28 11:05", step: "仅填写变更描述", progress: "1/6 页" },
|
||||
]);
|
||||
|
||||
const continueEdit = (d: Draft) => {
|
||||
window.$message?.success(`已载入草稿 ${d.id},从「${d.step}」断点继续编辑`);
|
||||
emit("go", "apply");
|
||||
};
|
||||
const remove = (d: Draft) => {
|
||||
drafts.value = drafts.value.filter((x: Draft) => x.id !== d.id);
|
||||
window.$message?.success(`草稿 ${d.id} 已删除`);
|
||||
};
|
||||
const progressPct = (p: string) => {
|
||||
const m = p.match(/(\d+)\s*\/\s*(\d+)/);
|
||||
return m ? Math.round((Number(m[1]) / Number(m[2])) * 100) : 0;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="border mb-3 py-3 px-4" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
<NGi span="24 s:24 m:16">
|
||||
<div class="h-100% flex items-center gap-2">
|
||||
<Icon icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-sm font-600">我的草稿箱</p>
|
||||
<span class="rounded bg-slate-100 px-1.5 text-xs font-semibold text-slate-700">{{ drafts.length }}</span>
|
||||
<p class="text-xs text-#94a3b8">草稿仅本人可见,提交后自动从草稿箱移除</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<NButton type="primary" size="small" @click="emit('go', 'apply')">
|
||||
<Icon icon="ic:round-plus" class="size-16px" />
|
||||
新建空白变更
|
||||
</NButton>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</div>
|
||||
<div class="space-y-2 scroll">
|
||||
<div v-for="d in drafts" :key="d.id" class="rounded-lg border px-4 py-3">
|
||||
<div class="flex flex-wrap items-start gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-semibold text-slate-800 dark:text-white">{{ d.name }}</div>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-slate-400">
|
||||
<span class="font-mono">{{ d.id }}</span>
|
||||
<span>保存于 {{ d.time }}</span>
|
||||
<NTag type="primary" size="small" class="text-11px">{{ d.step }}</NTag>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<div class="h-1.5 w-40 overflow-hidden rounded-full bg-slate-100">
|
||||
<div class="h-full rounded-full bg-[#6E93C6]" :style="{ width: progressPct(d.progress) + '%' }" />
|
||||
</div>
|
||||
<span class="text-[11px] text-slate-400">编辑进度 {{ d.progress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2 pt-2px">
|
||||
<NButton class="text-xs" text type="error" @click="remove(d)">删除</NButton>
|
||||
<NButton class="text-xs" text type="primary" @click="continueEdit(d)">继续编辑</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="drafts.length === 0" class="rounded-lg border border-dashed bg-white py-12 text-center">
|
||||
<div class="text-sm text-slate-400">暂无草稿</div>
|
||||
<div class="mt-1 text-xs text-slate-300">在变更申请页点击右上角「保存」,即可将当前内容存为草稿</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 212px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,631 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
|
||||
interface MyChange {
|
||||
id: string;title: string;status: string;detail: string;applyTime: string;updateTime: string;
|
||||
level: "重要" | "一般"; dur: "永久" | "临时"; urgent?: boolean;deadline?: string;
|
||||
stage: string; meta: string; left?: number; extended?: boolean;
|
||||
node?: string; members?: { name: string; done: boolean }[];
|
||||
}
|
||||
|
||||
const currentRow = ref<any>(null);
|
||||
const progressDrawer = ref<boolean>(false);
|
||||
const revokeModal = ref<boolean>(false);
|
||||
const revokeSaving = ref<boolean>(false);
|
||||
const revokeReason = ref<string>("");
|
||||
const disposalModal = ref<boolean>(false);
|
||||
|
||||
const stages = ["提交申请", "审批", "实施与投用前确认", "投用", "验收", "关闭"];
|
||||
const curIdx = computed(() => ({ 审批中: 1, 已批准: 2, 实施中: 2, 待PSSR: 2, 待验收: 4, 待关闭: 5, 已关闭: 6 } as Record<string, number>)[currentRow.value?.status ?? ""] ?? 1);
|
||||
const closed = computed(() => currentRow.value?.status === "已关闭");
|
||||
const timeOf = (i: number) => (i === 0 ? currentRow.value?.applyTime : i === 3 ? currentRow.value?.actualUse : i === 5 ? currentRow.value?.closeTime : undefined);
|
||||
const currentTab = ref<string>("all");
|
||||
const visibleTabs = ref<{ k: string; name: string; count: number }[]>([
|
||||
{ k: "all", name: "全部", count: 10 },
|
||||
{ k: "approval", name: "待我审批", count: 1 },
|
||||
]);
|
||||
|
||||
interface ListType {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
list: any[];
|
||||
}
|
||||
const list = ref<ListType[]>([
|
||||
{
|
||||
id: "MOC-2026-R01-001",
|
||||
title: '待我审批',
|
||||
status: 'pending',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R01-0001",
|
||||
title:'R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)',
|
||||
level: "重要",
|
||||
dur: "永久",
|
||||
stage: "pending",
|
||||
status: "审批中",
|
||||
node: "专业会签",
|
||||
overdue: false,//是否超期
|
||||
near: false,//是否临期
|
||||
extended: false,//是否延期
|
||||
extendedCount: 0,//延期次数
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [{ name: "王仪表", done: true },{ name: "周设备", done: false },{ name: "王海燕", done: false }],
|
||||
applyTime: "2026-07-28 09:12",
|
||||
updateTime: "2026-07-30 14:20",
|
||||
meta: "计划投用 2026-08-10",
|
||||
detail: "专业会签中(周设备、王海燕待签)"
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0002",
|
||||
title:'操作规程修订(夏季工况)',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "pending",
|
||||
status: "审批中",
|
||||
node: "部门审核",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 2,
|
||||
urgent: false,
|
||||
members: [{ name: "李主任", done: false }],
|
||||
applyTime: "2026-06-20 11:26",
|
||||
updateTime: "2026-06-21 08:40",
|
||||
meta: "计划投用 2026-07-01",
|
||||
deadline: "2026-08-01",
|
||||
detail: "部门审核(李主任)"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-002",
|
||||
title: '我的申请',
|
||||
status: 'approved',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R02-0001",
|
||||
title:'循环水加药泵冲程临时调整',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "投用准备",
|
||||
overdue: false,//是否超期
|
||||
near: false,//是否临期
|
||||
extended: false,//是否延期
|
||||
extendedCount: 0,//延期次数
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [{ name: "张工艺", done: false }],
|
||||
applyTime: "2026-07-26 10:15",
|
||||
updateTime: "2026-07-30 16:20",
|
||||
meta: "批准 2026-07-30 · 待实施",
|
||||
deadline: "2026-08-20",
|
||||
detail: "培训 / PSSR 资料待上传(线下签字后拍照上传)",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0002",
|
||||
title:'氮气吹扫流程临时调整',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "实施执行",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 3,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-10 10:05",
|
||||
updateTime: "2026-07-31 08:10",
|
||||
meta: "批准 2026-07-12 · 待实施",
|
||||
deadline: "2026-07-31",
|
||||
detail: "已到期未办理恢复 / 延期 / 转永久",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0003",
|
||||
title:'P-204 出口管线伴热改造',
|
||||
level: "一般",
|
||||
dur: "永久",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "实施执行",
|
||||
overdue: false,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-15 09:40",
|
||||
updateTime: "2026-07-20 11:00",
|
||||
meta: "审批通过 2026-07-20 · 计划投用 2026-07-30",
|
||||
detail: "因装置检修窗口调整未实施",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0004",
|
||||
title:'循环水旁滤器临时绕流运行',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "inuse",
|
||||
status: "待验收",
|
||||
node: "变更验收",
|
||||
overdue: false,
|
||||
near: true,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 6,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-28 14:10",
|
||||
updateTime: "2026-08-02 08:30",
|
||||
deadline: "2026-08-09",
|
||||
meta: "投用 2026-08-02 · 到期 2026-08-09",
|
||||
detail: "验收材料准备中",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0005",
|
||||
title:'空压站干燥器再生周期调整',
|
||||
level: "一般",
|
||||
dur: "永久",
|
||||
stage: "inuse",
|
||||
status: "待验收",
|
||||
node: "运行验证",
|
||||
overdue: false,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-18 10:25",
|
||||
updateTime: "2026-07-25 09:12",
|
||||
meta: "投用 2026-07-25 · 计划验收 2026-08-24",
|
||||
detail: "运行数据收集中(验收周期 30 天)",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0006",
|
||||
title:'V-102 安全阀起跳压力临时调整(紧急补办)',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "closing",
|
||||
status: "待关闭",
|
||||
node: "关闭确认",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 4,
|
||||
urgent: true,
|
||||
members: [{ name: "钱峰", done: false }],
|
||||
applyTime: "2026-07-15 19:40",
|
||||
updateTime: "2026-07-27 09:02",
|
||||
deadline: "2026-07-27",
|
||||
meta: "已投用 · 到期 2026-07-27",
|
||||
detail: "待关闭确认(资料更新核查中)",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-003",
|
||||
title: '驳回处理',
|
||||
status: 'reject',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R03-0001",
|
||||
title: 'P-301 机封冲洗方案调整',
|
||||
stage: "reject",
|
||||
overdue: false,
|
||||
rejectReason: '变更前后对比描述不完整,缺少冲洗流量核算数据(李主任 · 车间主任)',
|
||||
updateTime: "2026-07-29",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R03-0002",
|
||||
title: '活性炭更换周期延长(3→6 个月)',
|
||||
stage: "reject",
|
||||
overdue: true,
|
||||
rejectReason: '风险分析未覆盖吸附饱和穿透情景,请补充 JSA 分析(王海燕 · 安全工程师)',
|
||||
updateTime: "2026-07-25",
|
||||
},
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
interface SubOrder {
|
||||
id: string; // 子单号:原单号-Y1(延期)/ -Z1(转永久)
|
||||
parentId: string; // 原变更单号
|
||||
parentTitle: string;
|
||||
kind: "extend" | "permanent";
|
||||
applicant: string;
|
||||
reason: string;
|
||||
newDeadline?: string; // 延期子单:申请延期至
|
||||
status: "审批中" | "已通过" | "已驳回";
|
||||
submitTime: string;
|
||||
steps: { role: string; name: string; done: boolean }[]; // 会签:全员同意即通过
|
||||
rejectNote?: string;
|
||||
}
|
||||
const SUB_ORDERS: SubOrder[] = [];
|
||||
const subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
|
||||
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
|
||||
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
|
||||
const convPassed = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status === "已通过"));
|
||||
const convActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status !== "已驳回"));
|
||||
const extActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "extend" && s.status === "审批中"));
|
||||
const opts = computed<Opt[]>(() => {
|
||||
const list: Opt[] = [
|
||||
{ act: "restore", name: "申请恢复", desc: "投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档" },
|
||||
];
|
||||
if (currentRow.value?.dur === "临时" && !convPassed.value) {
|
||||
list.push(
|
||||
{ act: "extend", name: currentRow.value?.extended ? "申请延期(已延期 1 次)" : "申请延期", desc: "临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次", disabled: !!currentRow.value?.extended || extActive.value, note: extActive.value ? "延期子单审批中" : currentRow.value?.extended ? "本变更已延期 1 次,不可再次延期" : "" },
|
||||
{ act: "permanent", name: "申请转为永久", desc: "临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭", disabled: convActive.value, note: convActive.value ? "转永久子单审批中 / 已通过" : "" },
|
||||
);
|
||||
}
|
||||
list.push(
|
||||
{ act: "unimpl", name: "未实施情况说明", desc: "审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程" },
|
||||
{ act: "unuse", name: "未投用情况说明", desc: "变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程" },
|
||||
);
|
||||
return list;
|
||||
});
|
||||
|
||||
// 打开详情
|
||||
const openDetail = (c: MyChange,type:string = '') => {
|
||||
if(type==='reject'){
|
||||
window.$message?.info("演示:打开详情");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 打开进度
|
||||
const openProgress = (c: MyChange,type:string = '') => {
|
||||
currentRow.value = c;
|
||||
if(type==='reject'){
|
||||
window.$message?.info("该任务暂无关联变更进度");
|
||||
return;
|
||||
}
|
||||
progressDrawer.value = true;
|
||||
};
|
||||
// 打开撤回
|
||||
const openWithdraw = (c: MyChange) => {
|
||||
revokeReason.value = "";
|
||||
currentRow.value = c;
|
||||
revokeModal.value = true;
|
||||
};
|
||||
// 打开处置申请、修改重报
|
||||
const openDisposal = (c: MyChange,type:string = '') => {
|
||||
currentRow.value = c;
|
||||
if(type==='reject'){
|
||||
window.$message?.info("该任务暂无处置申请");
|
||||
return;
|
||||
}
|
||||
disposalModal.value = true;
|
||||
};
|
||||
// 打开资料关闭、关闭变更
|
||||
const openClosure = (c: MyChange,type:string = '') => {
|
||||
if(type==='reject'){
|
||||
window.$message?.info("关闭该变更(驳回后不再推进,流程终止并归档备查)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 撤回取消
|
||||
const revokeCancel = () => {
|
||||
revokeModal.value = false;
|
||||
};
|
||||
// 撤回确认
|
||||
const revokeSave = () => {
|
||||
if (!revokeReason.value) {
|
||||
window.$message?.warning("请填写撤回理由");
|
||||
return;
|
||||
}
|
||||
revokeSaving.value = true;
|
||||
setTimeout(() => {
|
||||
revokeSaving.value = false;
|
||||
revokeModal.value = false;
|
||||
const pendingGroup = list.value.find((item: any) => item.status === 'pending');
|
||||
if (pendingGroup) {
|
||||
// 过滤掉匹配 targetId 的项
|
||||
pendingGroup.list = pendingGroup.list.filter((item:any) => item.id !== currentRow.value?.id);
|
||||
}
|
||||
window.$message?.success(`已撤回 ${currentRow.value?.id},申请退回草稿箱,审批记录保留可查,修改后可重新提交`);
|
||||
}, 1000);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 待我处理 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div
|
||||
class="h-100% flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<div v-for="t in visibleTabs" :key="t.k" @click="currentTab = t.k"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer select-none"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--theme-color': themeStore.themeColor,
|
||||
}"
|
||||
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
|
||||
{{ t.name }}
|
||||
<!-- 驳回处理/资料上传/临时变更超期未处理/变更超期未关闭 -->
|
||||
<NTag v-if="t.k==='reject' || t.k==='upload' || t.k==='tempOverdue' || t.k==='closeOverdue'" size="tiny" round :type="currentTab === t.k ?'default':'error'">{{ t.count }}</NTag>
|
||||
<NTag v-else size="tiny" round :type="currentTab === t.k ?'default':'primary'">{{ t.count }}</NTag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 scroll">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
class="border border-slate-200 p-4"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 待我审批 -->
|
||||
<Icon v-if="item.status==='pending'" icon="ix:success" class="size-16px" :style="{ color: themeStore.otherColor.success }" />
|
||||
<!-- 我的申请 -->
|
||||
<Icon v-if="item.status==='approved'" icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<!-- 驳回处理 -->
|
||||
<Icon v-if="item.status==='reject'" icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.otherColor.error }" />
|
||||
<h3 class="text-sm font-bold text-slate-800">{{item.title}}</h3>
|
||||
|
||||
<!-- 待我审批 -->
|
||||
<p v-if="item.status==='pending'" class="text-11px text-slate-400">共 {{ item.list.length }} 项 · 流程当前节点轮到您处理 · 点击进入审批</p>
|
||||
<!-- 我的申请 -->
|
||||
<p v-if="item.status==='approved'" class="text-11px text-slate-400">共 {{item.list.length}} 项</p>
|
||||
<!-- 驳回处理 -->
|
||||
<p v-if="item.status==='reject'" class="text-11px text-slate-400">{{item.list.length}} 项待处理</p>
|
||||
</div>
|
||||
<div v-if="item.list && item.list.length" class="flex flex-col gap-3 mt-3">
|
||||
<div v-for="c in item.list" :key="c.id"
|
||||
class="flex flex-wrap items-center gap-3 border px-4 py-3"
|
||||
:class="c.overdue ? 'border-red-200 bg-[#FFF2F0]' : c.near ? 'border-amber-200 bg-amber-50/40' : ''"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<!-- 左区 -->
|
||||
<!-- 待我审批 -->
|
||||
<div v-if="item.status==='pending' || item.status==='approved'" class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
|
||||
<NTag v-if="c.level==='重要'" size="small" type="error">{{c.level}}</NTag>
|
||||
<NTag v-if="c.level==='一般'" size="small" type="primary">{{c.level}}</NTag>
|
||||
<NTag v-if="c.dur==='临时'" size="small" type="warning">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.dur==='永久'" size="small" type="primary">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.dur==='临时转永久'" size="small">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.urgent" size="small" type="error">紧急</NTag>
|
||||
<NTag v-if="c.overdue" size="small" type="error">已超期 {{ c.day }} 天</NTag>
|
||||
<NTag v-if="c.near" size="small" type="warning">临期 · 剩 {{ c.day }} 天</NTag>
|
||||
<NTag v-if="c.extended" size="small">已延期 {{ c.extendedCount }} 次</NTag>
|
||||
<NTag v-if="c.status==='审批中'" size="small" type="warning">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='已批准'" size="small" type="primary">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='待验收'" size="small" type="success">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='待关闭'" size="small" type="error">{{c.status}}</NTag>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap items-baseline gap-x-2">
|
||||
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
|
||||
<span v-if="c.applyTime" class="text-[11px] text-slate-400">申请 {{ c.applyTime }}</span>
|
||||
</div>
|
||||
<div v-if="c.stage === 'closing'" class="mt-0.5 text-[11px] text-slate-400">{{ c.meta }}</div>
|
||||
<!-- 当前节点与会签成员进度;状态更新时间跟在审核人员名字后面 -->
|
||||
<div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
|
||||
<span class="text-slate-400">当前节点</span>
|
||||
<span class="font-medium text-slate-600">{{ c.node ?? c.status }}</span>
|
||||
<div v-if="c.members && c.members.length" class="flex items-center gap-1">
|
||||
<div v-for="(m, mi) in c.members" :key="mi">
|
||||
<NTag v-if="m.done" size="small" type="success">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="ix:success" />{{ m.name }}
|
||||
</div>
|
||||
</NTag>
|
||||
<NTag v-else size="small" type="warning">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="tabler:clock" />{{ m.name }}
|
||||
</div>
|
||||
</NTag>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-slate-400">{{ c.detail }}</span>
|
||||
<span v-if="c.updateTime" class="text-slate-400">状态更新 {{ c.updateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 驳回处理 -->
|
||||
<NTag v-if="item.status==='reject'" size="small" type="error">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="system-uicons:reset" />驳回处理
|
||||
</div>
|
||||
</NTag>
|
||||
<div v-if="item.status==='reject'" class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
|
||||
</div>
|
||||
<p class="px-2 py-1 text-xs border border-red-100 bg-red-50/60 px-2 py-1 text-red-600 rounded-md mt-1">
|
||||
驳回原因:{{ c.rejectReason }}
|
||||
</p>
|
||||
<span v-if="c.updateTime" class="text-11px text-slate-400">驳回于 {{ c.updateTime }}</span>
|
||||
</div>
|
||||
<!-- 右区 -->
|
||||
<!-- 待我审批 -->
|
||||
<div v-if="c.stage === 'pending'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c)">去审批</NButton>
|
||||
</div>
|
||||
<!-- 我的申请 -->
|
||||
<div v-if="c.stage === 'approved'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c)">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
|
||||
<NButton class="text-xs" text type="error" @click="openClosure(c)">资料关闭</NButton>
|
||||
</div>
|
||||
<!-- 驳回处理 -->
|
||||
<div v-if="c.stage === 'reject'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c,'reject')">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c,'reject')">进度</NButton>
|
||||
<NButton class="text-xs" text type="primary" @click="openDisposal(c,'reject')">修改重报</NButton>
|
||||
<NButton class="text-xs" text type="error" @click="openClosure(c,'reject')">关闭变更</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度弹框 -->
|
||||
<NDrawer v-model:show="progressDrawer" :width="502" placement="right">
|
||||
<NDrawerContent title="变更进度">
|
||||
<div class="rounded-lg border bg-slate-50/60 px-3 py-2.5">
|
||||
<div class="text-sm font-semibold text-slate-800">{{ currentRow?.title }}</div>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-400">
|
||||
<span class="font-mono">{{ currentRow?.id }}</span>
|
||||
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow.status}}</NTag>
|
||||
<span v-if="currentRow?.overdue" class="font-medium text-red-500">超期 {{ currentRow?.day }} 天</span>
|
||||
</div>
|
||||
<div v-if="currentRow?.dept || currentRow?.applicant" class="mt-1 text-[11px] text-slate-400">{{ [currentRow?.dept, currentRow?.applicant].filter(Boolean).join(' · ') }}</div>
|
||||
<div v-if="currentRow?.meta" class="mt-1 text-[11px] text-slate-400">{{ currentRow?.meta }}</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div v-for="(s, i) in stages" :key="s" class="flex gap-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2"
|
||||
:class="i < curIdx || closed ? 'border-[#7CC242] bg-[#7CC242] text-white' : i === curIdx && !closed ? 'border-[#1D4E9C] bg-[#1D4E9C] text-white' : 'border-slate-200 bg-white text-slate-300'">
|
||||
<Icon v-if="i < curIdx || closed" icon="ix:success" />
|
||||
<Icon v-else-if="i === curIdx" icon="tabler:clock" />
|
||||
<span v-else class="text-[10px]">{{ i + 1 }}</span>
|
||||
</span>
|
||||
<span v-if="i < stages.length - 1" class="w-0.5 flex-1" :class="i < curIdx ? 'bg-[#7CC242]' : 'bg-slate-200'" />
|
||||
</div>
|
||||
<div class="pb-5">
|
||||
<div class="text-sm" :class="i === curIdx && !closed ? 'font-semibold text-[#1D4E9C]' : i < curIdx || closed ? 'font-medium text-slate-700' : 'text-slate-400'">
|
||||
{{ s }}{{ i === curIdx && !closed ? '(当前)' : '' }}
|
||||
</div>
|
||||
<div v-if="timeOf(i)" class="mt-0.5 text-[11px] text-slate-400">{{ timeOf(i) }}</div>
|
||||
<div v-if="i === curIdx && !closed && (currentRow?.detail || currentRow?.updateTime)" class="mt-0.5 text-[11px] text-slate-400">
|
||||
{{ [currentRow?.detail, currentRow?.updateTime ? `更新 ${currentRow?.updateTime}` : ''].filter(Boolean).join(' · ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentRow?.dur === '临时'" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
|
||||
临时变更:计划恢复 {{ currentRow?.deadline ?? '—' }}{{ currentRow?.extended ? ` · 延期后恢复 ${currentRow?.deadline}` : '' }} · 恢复 / 延期 / 转永久请在详情页中操作
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
<!-- 撤回弹框 -->
|
||||
<NModal
|
||||
v-model:show="revokeModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '450px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-sm font-bold text-slate-800">撤回变更申请</h3>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||||
变更编号 {{ currentRow?.id }}:撤回后申请退回草稿状态,已产生的审批记录保留可查;修改内容后可重新提交,重新提交按新申请计。
|
||||
</div>
|
||||
<NInput
|
||||
class="mt-3"
|
||||
v-model:value="revokeReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="撤回理由(必填):如补充材料、变更范围调整、需重新组织风险分析…"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-[10px]">
|
||||
<NButton size="small" @click="revokeCancel">取消</NButton>
|
||||
<NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">保存</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
<!-- 处置申请弹框 -->
|
||||
<NModal
|
||||
v-model:show="disposalModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '670px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: false }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="text-base font-semibold text-slate-800">处置申请 · {{ currentRow?.title }}</h3>
|
||||
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号:{{ currentRow?.id }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="rounded-lg border bg-white px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<NTag v-if="currentRow?.level=== '重要'" type="error" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-if="currentRow?.dur=== '临时'" type="warning" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='实施中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待PSSR'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已关闭'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<span v-if="currentRow?.applyTime" class="text-[11px] text-slate-400">申请 {{ currentRow?.applyTime }}</span>
|
||||
<span v-if="currentRow?.dur === '临时' && currentRow?.deadline" class="text-[11px] text-amber-600">
|
||||
到期 {{ currentRow?.deadline }}{{ (currentRow?.day ?? 0) < 0 ? ` · 已超期 ${-(currentRow?.day ?? 0)} 天` : currentRow?.day !== undefined ? ` · 剩 ${currentRow?.day} 天` : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-slate-400">{{ currentRow?.detail }}</div>
|
||||
</div>
|
||||
<!-- 进行中的子单提示 -->
|
||||
<div v-if="pendingSubs.length" class="rounded-lg border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-700">
|
||||
已有子单在审批中:{{ pendingSubs.map((s:any) => s.id).join('、') }} · 审批进度见「待我处理」列表行内展示
|
||||
</div>
|
||||
<!-- 处置选项 -->
|
||||
<div class="rounded-lg border bg-white mt-3">
|
||||
<div class="border-b px-4 py-2.5 text-sm font-bold text-slate-800">
|
||||
选择处置方式
|
||||
<span class="ml-2 text-[11px] font-normal text-slate-400">申请恢复所有变更可发起;申请延期 / 申请转为永久仅临时变更可用</span>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div v-for="o in opts" :key="o.act" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">{{ o.name }}</span>
|
||||
<NTag v-if="o.note" type="warning" size="small">{{ o.note }}</NTag>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ o.desc }}</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" :disabled="o.disabled">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</NModal>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,666 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
|
||||
interface MyChange {
|
||||
id: string;title: string;status: string;detail: string;applyTime: string;updateTime: string;
|
||||
level: "重要" | "一般"; dur: "永久" | "临时"; urgent?: boolean;deadline?: string;
|
||||
stage: string; meta: string; left?: number; extended?: boolean;
|
||||
node?: string; members?: { name: string; done: boolean }[];
|
||||
}
|
||||
|
||||
const currentRow = ref<any>(null);
|
||||
const progressDrawer = ref<boolean>(false);
|
||||
const revokeModal = ref<boolean>(false);
|
||||
const revokeSaving = ref<boolean>(false);
|
||||
const revokeReason = ref<string>("");
|
||||
const disposalModal = ref<boolean>(false);
|
||||
|
||||
const stages = ["提交申请", "审批", "实施与投用前确认", "投用", "验收", "关闭"];
|
||||
const curIdx = computed(() => ({ 审批中: 1, 已批准: 2, 实施中: 2, 待PSSR: 2, 待验收: 4, 待关闭: 5, 已关闭: 6 } as Record<string, number>)[currentRow.value?.status ?? ""] ?? 1);
|
||||
const closed = computed(() => currentRow.value?.status === "已关闭");
|
||||
const timeOf = (i: number) => (i === 0 ? currentRow.value?.applyTime : i === 3 ? currentRow.value?.actualUse : i === 5 ? currentRow.value?.closeTime : undefined);
|
||||
|
||||
const role = ref<string>("applicant");
|
||||
const currentTab = ref<string>("all");
|
||||
const visibleTabs = ref<{ k: string; name: string; count: number }[]>([
|
||||
{ k: "all", name: "全部", count: 10 },
|
||||
{ k: "approval", name: "待我审批", count: 1 },
|
||||
{ k: "apply", name: "我的申请", count: 8 },
|
||||
{ k: "tempOverdue", name: "临时变更超期未处理", count: 1 },
|
||||
{ k: "closeOverdue", name: "变更超期未关闭", count: 1 },
|
||||
{ k: "reject", name: "驳回处理", count: 2 },
|
||||
{ k: "accept", name: "参与验收", count: 3 },
|
||||
{ k: "upload", name: "资料上传", count: 2 },
|
||||
]);
|
||||
const remindDays = ref<number>(7);
|
||||
const specialTab = computed(() => currentTab.value === "tempOverdue" || currentTab.value === "closeOverdue");
|
||||
const myChanges = ref<MyChange[]>([
|
||||
{ id: "MOC-2026-R01-0035", title: "R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)", level: "重要", dur: "永久", stage: "pending", status: "审批中", detail: "专业会签中(周设备、王海燕待签)", applyTime: "2026-07-28 09:12", meta: "计划投用 2026-08-10", updateTime: "2026-07-30 14:20", node: "专业会签", members: [{ name: "王仪表", done: true }, { name: "周设备", done: false }, { name: "王海燕", done: false }] },
|
||||
{ id: "MOC-2026-R01-0029", title: "操作规程修订(夏季工况)", level: "一般", dur: "临时", stage: "pending", status: "审批中", detail: "部门审核(李主任)", applyTime: "2026-06-20 11:26", meta: "计划投用 2026-07-01", updateTime: "2026-06-21 08:40", deadline: "2026-08-01", left: -2, extended: false, node: "部门审核", members: [{ name: "李主任", done: false }] },
|
||||
{ id: "MOC-2026-R02-0046", title: "循环水加药泵冲程临时调整", level: "一般", dur: "临时", stage: "approved", status: "已批准", detail: "培训 / PSSR 资料待上传(线下签字后拍照上传)", applyTime: "2026-07-26 10:15", meta: "批准 2026-07-30 · 待实施", updateTime: "2026-07-30 16:20", deadline: "2026-08-20", left: 17, extended: false, node: "投用准备", members: [{ name: "张工艺", done: false }] },
|
||||
{ id: "MOC-2026-R02-0047", title: "氮气吹扫流程临时调整", level: "一般", dur: "临时", stage: "approved", status: "已批准", detail: "已到期未办理恢复 / 延期 / 转永久", applyTime: "2026-07-10 10:05", meta: "批准 2026-07-12 · 待实施", updateTime: "2026-07-31 08:10", deadline: "2026-07-31", left: -3, extended: false, node: "实施执行" },
|
||||
{ id: "MOC-2026-R02-0045", title: "P-204 出口管线伴热改造", level: "一般", dur: "永久", stage: "approved", status: "已批准", detail: "因装置检修窗口调整未实施", applyTime: "2026-07-15 09:40", meta: "审批通过 2026-07-20 · 计划投用 2026-07-30", updateTime: "2026-07-20 11:00", node: "实施执行" },
|
||||
{ id: "MOC-2026-R02-0042", title: "循环水旁滤器临时绕流运行", level: "一般", dur: "临时", stage: "inuse", status: "待验收", detail: "验收材料准备中", applyTime: "2026-07-28 14:10", meta: "投用 2026-08-02 · 到期 2026-08-09", updateTime: "2026-08-02 08:30", deadline: "2026-08-09", left: 6, extended: false, node: "变更验收" },
|
||||
{ id: "MOC-2026-R02-0043", title: "空压站干燥器再生周期调整", level: "一般", dur: "永久", stage: "inuse", status: "待验收", detail: "运行数据收集中(验收周期 30 天)", applyTime: "2026-07-18 10:25", meta: "投用 2026-07-25 · 计划验收 2026-08-24", updateTime: "2026-07-25 09:12", node: "运行验证" },
|
||||
{ id: "MOC-2026-R01-0039", title: "V-102 安全阀起跳压力临时调整(紧急补办)", level: "一般", dur: "临时", urgent: true, stage: "closing", status: "待关闭", detail: "待关闭确认(资料更新核查中)", applyTime: "2026-07-15 19:40", meta: "已投用 · 到期 2026-07-27", updateTime: "2026-07-27 09:02", deadline: "2026-07-27", left: -4, extended: false, node: "关闭确认", members: [{ name: "钱峰", done: false }] },
|
||||
]);
|
||||
const tempOverdueList = computed(() => (role.value === "applicant" ? myChanges.value : []).filter((c: MyChange) => c.stage === "approved" && c.dur === "临时" && (c.left ?? 0) < 0));
|
||||
const closeOverdueList = computed(() => (role.value === "applicant" ? myChanges.value : []).filter((c: MyChange) => c.stage === "closing" && (c.left ?? 0) < 0));
|
||||
|
||||
interface ListType {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
list: any[];
|
||||
}
|
||||
const list = ref<ListType[]>([
|
||||
{
|
||||
id: "MOC-2026-R01-001",
|
||||
title: '审批流转中',
|
||||
status: 'pending',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R01-0001",
|
||||
title:'R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)',
|
||||
level: "重要",
|
||||
dur: "永久",
|
||||
stage: "pending",
|
||||
status: "审批中",
|
||||
node: "专业会签",
|
||||
overdue: false,//是否超期
|
||||
near: false,//是否临期
|
||||
extended: false,//是否延期
|
||||
extendedCount: 0,//延期次数
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [{ name: "王仪表", done: true },{ name: "周设备", done: false },{ name: "王海燕", done: false }],
|
||||
applyTime: "2026-07-28 09:12",
|
||||
updateTime: "2026-07-30 14:20",
|
||||
meta: "计划投用 2026-08-10",
|
||||
detail: "专业会签中(周设备、王海燕待签)"
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0002",
|
||||
title:'操作规程修订(夏季工况)',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "pending",
|
||||
status: "审批中",
|
||||
node: "部门审核",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 2,
|
||||
urgent: false,
|
||||
members: [{ name: "李主任", done: false }],
|
||||
applyTime: "2026-06-20 11:26",
|
||||
updateTime: "2026-06-21 08:40",
|
||||
meta: "计划投用 2026-07-01",
|
||||
deadline: "2026-08-01",
|
||||
detail: "部门审核(李主任)"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-002",
|
||||
title: '审批通过',
|
||||
status: 'approved',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R02-0001",
|
||||
title:'循环水加药泵冲程临时调整',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "投用准备",
|
||||
overdue: false,//是否超期
|
||||
near: false,//是否临期
|
||||
extended: false,//是否延期
|
||||
extendedCount: 0,//延期次数
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [{ name: "张工艺", done: false }],
|
||||
applyTime: "2026-07-26 10:15",
|
||||
updateTime: "2026-07-30 16:20",
|
||||
meta: "批准 2026-07-30 · 待实施",
|
||||
deadline: "2026-08-20",
|
||||
detail: "培训 / PSSR 资料待上传(线下签字后拍照上传)",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0002",
|
||||
title:'氮气吹扫流程临时调整',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "实施执行",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 3,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-10 10:05",
|
||||
updateTime: "2026-07-31 08:10",
|
||||
meta: "批准 2026-07-12 · 待实施",
|
||||
deadline: "2026-07-31",
|
||||
detail: "已到期未办理恢复 / 延期 / 转永久",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0003",
|
||||
title:'P-204 出口管线伴热改造',
|
||||
level: "一般",
|
||||
dur: "永久",
|
||||
stage: "approved",
|
||||
status: "已批准",
|
||||
node: "实施执行",
|
||||
overdue: false,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-15 09:40",
|
||||
updateTime: "2026-07-20 11:00",
|
||||
meta: "审批通过 2026-07-20 · 计划投用 2026-07-30",
|
||||
detail: "因装置检修窗口调整未实施",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0004",
|
||||
title:'循环水旁滤器临时绕流运行',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "inuse",
|
||||
status: "待验收",
|
||||
node: "变更验收",
|
||||
overdue: false,
|
||||
near: true,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 6,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-28 14:10",
|
||||
updateTime: "2026-08-02 08:30",
|
||||
deadline: "2026-08-09",
|
||||
meta: "投用 2026-08-02 · 到期 2026-08-09",
|
||||
detail: "验收材料准备中",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0005",
|
||||
title:'空压站干燥器再生周期调整',
|
||||
level: "一般",
|
||||
dur: "永久",
|
||||
stage: "inuse",
|
||||
status: "待验收",
|
||||
node: "运行验证",
|
||||
overdue: false,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 0,
|
||||
urgent: false,
|
||||
members: [],
|
||||
applyTime: "2026-07-18 10:25",
|
||||
updateTime: "2026-07-25 09:12",
|
||||
meta: "投用 2026-07-25 · 计划验收 2026-08-24",
|
||||
detail: "运行数据收集中(验收周期 30 天)",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0006",
|
||||
title:'V-102 安全阀起跳压力临时调整(紧急补办)',
|
||||
level: "一般",
|
||||
dur: "临时",
|
||||
stage: "closing",
|
||||
status: "待关闭",
|
||||
node: "关闭确认",
|
||||
overdue: true,
|
||||
near: false,
|
||||
extended: false,
|
||||
extendedCount: 0,
|
||||
day: 4,
|
||||
urgent: true,
|
||||
members: [{ name: "钱峰", done: false }],
|
||||
applyTime: "2026-07-15 19:40",
|
||||
updateTime: "2026-07-27 09:02",
|
||||
deadline: "2026-07-27",
|
||||
meta: "已投用 · 到期 2026-07-27",
|
||||
detail: "待关闭确认(资料更新核查中)",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-003",
|
||||
title: '驳回处理',
|
||||
status: 'reject',
|
||||
list: [
|
||||
{
|
||||
id: "MOC-2026-R03-0001",
|
||||
title: 'P-301 机封冲洗方案调整',
|
||||
stage: "reject",
|
||||
overdue: false,
|
||||
rejectReason: '变更前后对比描述不完整,缺少冲洗流量核算数据(李主任 · 车间主任)',
|
||||
updateTime: "2026-07-29",
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R03-0002",
|
||||
title: '活性炭更换周期延长(3→6 个月)',
|
||||
stage: "reject",
|
||||
overdue: true,
|
||||
rejectReason: '风险分析未覆盖吸附饱和穿透情景,请补充 JSA 分析(王海燕 · 安全工程师)',
|
||||
updateTime: "2026-07-25",
|
||||
},
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
interface SubOrder {
|
||||
id: string; // 子单号:原单号-Y1(延期)/ -Z1(转永久)
|
||||
parentId: string; // 原变更单号
|
||||
parentTitle: string;
|
||||
kind: "extend" | "permanent";
|
||||
applicant: string;
|
||||
reason: string;
|
||||
newDeadline?: string; // 延期子单:申请延期至
|
||||
status: "审批中" | "已通过" | "已驳回";
|
||||
submitTime: string;
|
||||
steps: { role: string; name: string; done: boolean }[]; // 会签:全员同意即通过
|
||||
rejectNote?: string;
|
||||
}
|
||||
const SUB_ORDERS: SubOrder[] = [];
|
||||
const subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
|
||||
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
|
||||
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
|
||||
const convPassed = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status === "已通过"));
|
||||
const convActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status !== "已驳回"));
|
||||
const extActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "extend" && s.status === "审批中"));
|
||||
const opts = computed<Opt[]>(() => {
|
||||
const list: Opt[] = [
|
||||
{ act: "restore", name: "申请恢复", desc: "投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档" },
|
||||
];
|
||||
if (currentRow.value?.dur === "临时" && !convPassed.value) {
|
||||
list.push(
|
||||
{ act: "extend", name: currentRow.value?.extended ? "申请延期(已延期 1 次)" : "申请延期", desc: "临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次", disabled: !!currentRow.value?.extended || extActive.value, note: extActive.value ? "延期子单审批中" : currentRow.value?.extended ? "本变更已延期 1 次,不可再次延期" : "" },
|
||||
{ act: "permanent", name: "申请转为永久", desc: "临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭", disabled: convActive.value, note: convActive.value ? "转永久子单审批中 / 已通过" : "" },
|
||||
);
|
||||
}
|
||||
list.push(
|
||||
{ act: "unimpl", name: "未实施情况说明", desc: "审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程" },
|
||||
{ act: "unuse", name: "未投用情况说明", desc: "变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程" },
|
||||
);
|
||||
return list;
|
||||
});
|
||||
|
||||
// 打开详情
|
||||
const openDetail = (c: MyChange,type:string = '') => {
|
||||
if(type==='reject'){
|
||||
window.$message?.info("演示:打开详情");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 打开进度
|
||||
const openProgress = (c: MyChange,type:string = '') => {
|
||||
currentRow.value = c;
|
||||
if(type==='reject'){
|
||||
window.$message?.info("该任务暂无关联变更进度");
|
||||
return;
|
||||
}
|
||||
progressDrawer.value = true;
|
||||
};
|
||||
// 打开撤回
|
||||
const openWithdraw = (c: MyChange) => {
|
||||
revokeReason.value = "";
|
||||
currentRow.value = c;
|
||||
revokeModal.value = true;
|
||||
};
|
||||
// 打开处置申请、修改重报
|
||||
const openDisposal = (c: MyChange,type:string = '') => {
|
||||
currentRow.value = c;
|
||||
if(type==='reject'){
|
||||
window.$message?.info("该任务暂无处置申请");
|
||||
return;
|
||||
}
|
||||
disposalModal.value = true;
|
||||
};
|
||||
// 打开资料关闭、关闭变更
|
||||
const openClosure = (c: MyChange,type:string = '') => {
|
||||
if(type==='reject'){
|
||||
window.$message?.info("关闭该变更(驳回后不再推进,流程终止并归档备查)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 撤回取消
|
||||
const revokeCancel = () => {
|
||||
revokeModal.value = false;
|
||||
};
|
||||
// 撤回确认
|
||||
const revokeSave = () => {
|
||||
if (!revokeReason.value) {
|
||||
window.$message?.warning("请填写撤回理由");
|
||||
return;
|
||||
}
|
||||
revokeSaving.value = true;
|
||||
setTimeout(() => {
|
||||
revokeSaving.value = false;
|
||||
revokeModal.value = false;
|
||||
const pendingGroup = list.value.find((item: any) => item.status === 'pending');
|
||||
if (pendingGroup) {
|
||||
// 过滤掉匹配 targetId 的项
|
||||
pendingGroup.list = pendingGroup.list.filter((item:any) => item.id !== currentRow.value?.id);
|
||||
}
|
||||
window.$message?.success(`已撤回 ${currentRow.value?.id},申请退回草稿箱,审批记录保留可查,修改后可重新提交`);
|
||||
}, 1000);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 待我处理 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div
|
||||
class="h-100% flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<div v-for="t in visibleTabs" :key="t.k" @click="currentTab = t.k"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer select-none"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--theme-color': themeStore.themeColor,
|
||||
}"
|
||||
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
|
||||
{{ t.name }}
|
||||
<!-- 驳回处理/资料上传/临时变更超期未处理/变更超期未关闭 -->
|
||||
<NTag v-if="t.k==='reject' || t.k==='upload' || t.k==='tempOverdue' || t.k==='closeOverdue'" size="tiny" round :type="currentTab === t.k ?'default':'error'">{{ t.count }}</NTag>
|
||||
<NTag v-else size="tiny" round :type="currentTab === t.k ?'default':'primary'">{{ t.count }}</NTag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 临时变更临期提醒 -->
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 text-xs text-slate-400 mb-3"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<Icon icon="lets-icons:clock" class="size-16px text-amber-600" />
|
||||
临时变更临期提醒:到期前
|
||||
<NInputNumber v-model:value="remindDays" :min="1" :max="30" size="tiny" placeholder="" :show-button="false" class="text-11px w-14 text-center" />
|
||||
天提醒(可设置)· 超期红色、临期琥珀色标识
|
||||
<span class="ml-auto">
|
||||
{{ specialTab ? (currentTab === 'tempOverdue' ? `超期 ${tempOverdueList.length} 项 · 点击行内按钮办理恢复 / 延期 / 转永久` : `超期 ${closeOverdueList.length} 项 · 点击行内按钮进入关闭确认`) : `共 ${myChanges.length} 项 · 点击行内按钮办理各阶段业务` }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2 scroll">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
class="border border-slate-200 p-4"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 审批流转中/已通过 -->
|
||||
<Icon v-if="item.status==='pending' || item.status==='approved'" icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<!-- 被驳回处理 -->
|
||||
<Icon v-if="item.status==='reject'" icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">{{item.title}}</p>
|
||||
<!-- 审批流转中 -->
|
||||
<p v-if="item.status==='pending'" class="text-11px text-#94a3b8">共 {{item.list.length}} 项 · 审批流程推进中 · 可查看进度或撤回</p>
|
||||
<!-- 已通过 -->
|
||||
<p v-if="item.status==='approved'" class="text-11px text-#94a3b8">共 {{item.list.length}} 项 · 待实施 / 待验收 / 待关闭 · 处置申请与资料关闭在行内办理</p>
|
||||
<!-- 驳回处理 -->
|
||||
<p v-if="item.status==='reject'" class="text-11px text-#94a3b8">{{item.list.length}} 项待处理</p>
|
||||
</div>
|
||||
<div v-if="item.list && item.list.length" class="flex flex-col gap-3 mt-3">
|
||||
<div v-for="c in item.list" :key="c.id"
|
||||
class="flex flex-wrap items-center gap-3 border px-4 py-3"
|
||||
:class="c.overdue ? 'border-red-200 bg-[#FFF2F0]' : c.near ? 'border-amber-200 bg-amber-50/40' : ''"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<!-- 左区 -->
|
||||
<!-- 审批流转中/已通过 -->
|
||||
<div v-if="item.status==='pending' || item.status==='approved'" class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
|
||||
<NTag v-if="c.level==='重要'" size="small" type="error">{{c.level}}</NTag>
|
||||
<NTag v-if="c.level==='一般'" size="small" type="primary">{{c.level}}</NTag>
|
||||
<NTag v-if="c.dur==='临时'" size="small" type="warning">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.dur==='永久'" size="small" type="primary">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.dur==='临时转永久'" size="small">{{c.dur}}</NTag>
|
||||
<NTag v-if="c.urgent" size="small" type="error">紧急</NTag>
|
||||
<NTag v-if="c.overdue" size="small" type="error">已超期 {{ c.day }} 天</NTag>
|
||||
<NTag v-if="c.near" size="small" type="warning">临期 · 剩 {{ c.day }} 天</NTag>
|
||||
<NTag v-if="c.extended" size="small">已延期 {{ c.extendedCount }} 次</NTag>
|
||||
<NTag v-if="c.status==='审批中'" size="small" type="warning">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='已批准'" size="small" type="primary">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='待验收'" size="small" type="success">{{c.status}}</NTag>
|
||||
<NTag v-if="c.status==='待关闭'" size="small" type="error">{{c.status}}</NTag>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap items-baseline gap-x-2">
|
||||
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
|
||||
<span v-if="c.applyTime" class="text-[11px] text-slate-400">申请 {{ c.applyTime }}</span>
|
||||
</div>
|
||||
<div v-if="c.stage === 'closing'" class="mt-0.5 text-[11px] text-slate-400">{{ c.meta }}</div>
|
||||
<!-- 当前节点与会签成员进度;状态更新时间跟在审核人员名字后面 -->
|
||||
<div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
|
||||
<span class="text-slate-400">当前节点</span>
|
||||
<span class="font-medium text-slate-600">{{ c.node ?? c.status }}</span>
|
||||
<div v-if="c.members && c.members.length" class="flex items-center gap-1">
|
||||
<div v-for="(m, mi) in c.members" :key="mi">
|
||||
<NTag v-if="m.done" size="small" type="success">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="ix:success" />{{ m.name }}
|
||||
</div>
|
||||
</NTag>
|
||||
<NTag v-else size="small" type="warning">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="tabler:clock" />{{ m.name }}
|
||||
</div>
|
||||
</NTag>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-slate-400">{{ c.detail }}</span>
|
||||
<span v-if="c.updateTime" class="text-slate-400">状态更新 {{ c.updateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 驳回处理 -->
|
||||
<NTag v-if="item.status==='reject'" size="small" type="error">
|
||||
<div class="flex items-center">
|
||||
<Icon icon="system-uicons:reset" />驳回处理
|
||||
</div>
|
||||
</NTag>
|
||||
<div v-if="item.status==='reject'" class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
|
||||
</div>
|
||||
<p class="px-2 py-1 text-xs border border-red-100 bg-red-50/60 px-2 py-1 text-red-600 rounded-md mt-1">
|
||||
驳回原因:{{ c.rejectReason }}
|
||||
</p>
|
||||
<span v-if="c.updateTime" class="text-11px text-slate-400">驳回于 {{ c.updateTime }}</span>
|
||||
</div>
|
||||
<!-- 右区 -->
|
||||
<!-- 审批流转中 -->
|
||||
<div v-if="c.stage === 'pending'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c)">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
|
||||
<NButton class="text-xs" text type="error" @click="openWithdraw(c)">撤回</NButton>
|
||||
</div>
|
||||
<!-- 审批通过、待验收、待关闭 -->
|
||||
<div v-if="c.stage === 'approved' || c.stage === 'inuse' || c.stage === 'closing'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c)">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
|
||||
<NButton class="text-xs" text type="primary" @click="openDisposal(c)">处置申请</NButton>
|
||||
<NButton class="text-xs" text type="error" @click="openClosure(c)">资料关闭</NButton>
|
||||
</div>
|
||||
<!-- 驳回处理 -->
|
||||
<div v-if="c.stage === 'reject'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c,'reject')">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c,'reject')">进度</NButton>
|
||||
<NButton class="text-xs" text type="primary" @click="openDisposal(c,'reject')">修改重报</NButton>
|
||||
<NButton class="text-xs" text type="error" @click="openClosure(c,'reject')">关闭变更</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度弹框 -->
|
||||
<NDrawer v-model:show="progressDrawer" :width="502" placement="right">
|
||||
<NDrawerContent title="变更进度">
|
||||
<div class="rounded-lg border bg-slate-50/60 px-3 py-2.5">
|
||||
<div class="text-sm font-semibold text-slate-800">{{ currentRow?.title }}</div>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-400">
|
||||
<span class="font-mono">{{ currentRow?.id }}</span>
|
||||
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow.status}}</NTag>
|
||||
<span v-if="currentRow?.overdue" class="font-medium text-red-500">超期 {{ currentRow?.day }} 天</span>
|
||||
</div>
|
||||
<div v-if="currentRow?.dept || currentRow?.applicant" class="mt-1 text-[11px] text-slate-400">{{ [currentRow?.dept, currentRow?.applicant].filter(Boolean).join(' · ') }}</div>
|
||||
<div v-if="currentRow?.meta" class="mt-1 text-[11px] text-slate-400">{{ currentRow?.meta }}</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div v-for="(s, i) in stages" :key="s" class="flex gap-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2"
|
||||
:class="i < curIdx || closed ? 'border-[#7CC242] bg-[#7CC242] text-white' : i === curIdx && !closed ? 'border-[#1D4E9C] bg-[#1D4E9C] text-white' : 'border-slate-200 bg-white text-slate-300'">
|
||||
<Icon v-if="i < curIdx || closed" icon="ix:success" />
|
||||
<Icon v-else-if="i === curIdx" icon="tabler:clock" />
|
||||
<span v-else class="text-[10px]">{{ i + 1 }}</span>
|
||||
</span>
|
||||
<span v-if="i < stages.length - 1" class="w-0.5 flex-1" :class="i < curIdx ? 'bg-[#7CC242]' : 'bg-slate-200'" />
|
||||
</div>
|
||||
<div class="pb-5">
|
||||
<div class="text-sm" :class="i === curIdx && !closed ? 'font-semibold text-[#1D4E9C]' : i < curIdx || closed ? 'font-medium text-slate-700' : 'text-slate-400'">
|
||||
{{ s }}{{ i === curIdx && !closed ? '(当前)' : '' }}
|
||||
</div>
|
||||
<div v-if="timeOf(i)" class="mt-0.5 text-[11px] text-slate-400">{{ timeOf(i) }}</div>
|
||||
<div v-if="i === curIdx && !closed && (currentRow?.detail || currentRow?.updateTime)" class="mt-0.5 text-[11px] text-slate-400">
|
||||
{{ [currentRow?.detail, currentRow?.updateTime ? `更新 ${currentRow?.updateTime}` : ''].filter(Boolean).join(' · ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentRow?.dur === '临时'" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
|
||||
临时变更:计划恢复 {{ currentRow?.deadline ?? '—' }}{{ currentRow?.extended ? ` · 延期后恢复 ${currentRow?.deadline}` : '' }} · 恢复 / 延期 / 转永久请在详情页中操作
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
<!-- 撤回弹框 -->
|
||||
<NModal
|
||||
v-model:show="revokeModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '450px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-sm font-bold text-slate-800">撤回变更申请</h3>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||||
变更编号 {{ currentRow?.id }}:撤回后申请退回草稿状态,已产生的审批记录保留可查;修改内容后可重新提交,重新提交按新申请计。
|
||||
</div>
|
||||
<NInput
|
||||
class="mt-3"
|
||||
v-model:value="revokeReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="撤回理由(必填):如补充材料、变更范围调整、需重新组织风险分析…"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-[10px]">
|
||||
<NButton size="small" @click="revokeCancel">取消</NButton>
|
||||
<NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">保存</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
<!-- 处置申请弹框 -->
|
||||
<NModal
|
||||
v-model:show="disposalModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '670px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: false }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="text-base font-semibold text-slate-800">处置申请 · {{ currentRow?.title }}</h3>
|
||||
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号:{{ currentRow?.id }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="rounded-lg border bg-white px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<NTag v-if="currentRow?.level=== '重要'" type="error" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-if="currentRow?.dur=== '临时'" type="warning" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='实施中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待PSSR'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已关闭'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<span v-if="currentRow?.applyTime" class="text-[11px] text-slate-400">申请 {{ currentRow?.applyTime }}</span>
|
||||
<span v-if="currentRow?.dur === '临时' && currentRow?.deadline" class="text-[11px] text-amber-600">
|
||||
到期 {{ currentRow?.deadline }}{{ (currentRow?.day ?? 0) < 0 ? ` · 已超期 ${-(currentRow?.day ?? 0)} 天` : currentRow?.day !== undefined ? ` · 剩 ${currentRow?.day} 天` : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-slate-400">{{ currentRow?.detail }}</div>
|
||||
</div>
|
||||
<!-- 进行中的子单提示 -->
|
||||
<div v-if="pendingSubs.length" class="rounded-lg border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-700">
|
||||
已有子单在审批中:{{ pendingSubs.map((s:any) => s.id).join('、') }} · 审批进度见「待我处理」列表行内展示
|
||||
</div>
|
||||
<!-- 处置选项 -->
|
||||
<div class="rounded-lg border bg-white mt-3">
|
||||
<div class="border-b px-4 py-2.5 text-sm font-bold text-slate-800">
|
||||
选择处置方式
|
||||
<span class="ml-2 text-[11px] font-normal text-slate-400">申请恢复所有变更可发起;申请延期 / 申请转为永久仅临时变更可用</span>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div v-for="o in opts" :key="o.act" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">{{ o.name }}</span>
|
||||
<NTag v-if="o.note" type="warning" size="small">{{ o.note }}</NTag>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ o.desc }}</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" :disabled="o.disabled">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</NModal>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,195 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { getColorWithOpacity } from '@/utils/common';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const useTime = ref<any>(null);
|
||||
const useTimeModal = ref<boolean>(false);
|
||||
const currentRow = ref<any>(null);
|
||||
const list = ref<any[]>([]);
|
||||
const lists = [
|
||||
{
|
||||
id: "MOC-2026-R02-0038",statusDetail: "培训 / PSSR 资料已传 · 待确认投用",
|
||||
approveTime: "2026-07-31", applicant: "吴强", level: "一般", type: "工艺",
|
||||
title: 'DCS 罐区液位报警值 LIA-301 调整', status: '待PSSR', duration: "永久",
|
||||
useTime: "", planUse: "2026-08-01", pssr: "PSSR 检查确认表-签字版.pdf",
|
||||
hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0038",statusDetail: "培训 / PSSR 资料已传 · 待确认投用",
|
||||
approveTime: "2026-07-31", applicant: "吴强", level: "重要", type: "工艺",
|
||||
title: 'DCS 罐区液位报警值 LIA-301 调整', status: '待PSSR', duration: "临时",
|
||||
useTime: "2026-07-16 06:30", planUse: "2026-08-01", pssr: "",
|
||||
hasChange: true,
|
||||
},
|
||||
];
|
||||
|
||||
const timePickerProps = {
|
||||
format: 'HH:mm', // 时间选择器面板上显示的格式
|
||||
seconds: false // 隐藏秒选择器[reference:3][reference:4]
|
||||
}
|
||||
|
||||
// 禁用今天之前的日期(今天可选)
|
||||
const disabledDate = (ts: number) => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0) // 今天的 00:00:00
|
||||
return ts < today.getTime() // 时间戳小于今天 0 点则禁用
|
||||
}
|
||||
// 获取列表数据
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
setTimeout(() => {
|
||||
list.value = lists;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 查看
|
||||
const viewFile = (record: any) => {
|
||||
window.$message?.info(`预览「${record.pssr}」(上传件不做内容识别,原件已随单归档)`)
|
||||
};
|
||||
// 下载
|
||||
const downloadFile = (record: any) => {
|
||||
window.$message?.success(`下载「${record.pssr}」`)
|
||||
};
|
||||
// 打开变更单
|
||||
const openChange = (record: any) => {
|
||||
routerPushByKey('home_highriskdetail', { query: { params: JSON.stringify(record) } })
|
||||
};
|
||||
// 填写投用日期
|
||||
const fillUseTime = (record: any) => {
|
||||
useTime.value = null;
|
||||
currentRow.value = record;
|
||||
useTimeModal.value = true;
|
||||
};
|
||||
// 保存投用日期
|
||||
const saveUseTime = () => {
|
||||
if (!useTime.value) {
|
||||
return window.$message?.warning("请选择投用时间");
|
||||
}
|
||||
btnLoading.value = true;
|
||||
setTimeout(() => {
|
||||
btnLoading.value = false;
|
||||
useTimeModal.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- PSSR监督 -->
|
||||
<NSpin :show="loading" size="small">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span class="text-sm font-semibold text-slate-700">待投用变更(本车间 · 一车间)</span>
|
||||
<NTag size="small" type="primary">{{ list.length }} 起</NTag>
|
||||
<span class="ml-auto text-[11px] text-slate-400">按审批通过时间排序 · 投用时间由发起人在资料关闭页填报 · 支持属地负责人在此代录投用日期</span>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div
|
||||
class="space-y-3 h-100%"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
}"
|
||||
>
|
||||
<div v-for="(s, i) in list" :key="`${s.id ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-x-3 gap-y-2 border p-4" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NTag v-if="s.status==='待PSSR'" type="warning" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='待关闭'" type="error" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='实施中'" type="primary" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='待验收'" type="success" size="small">{{ s.status }}</NTag>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<p class="truncate text-sm font-medium text-slate-800">{{ s.title }}</p>
|
||||
<NTag v-if="s.level==='一般'" size="small">{{s.level}}</NTag>
|
||||
<NTag v-if="s.level==='重要'" size="small" type="error">{{s.level}}</NTag>
|
||||
<NTag size="small" type="primary">{{s.type}}</NTag>
|
||||
<NTag v-if="s.duration==='永久'" size="small">{{s.duration}}</NTag>
|
||||
<NTag v-if="s.duration==='临时'" size="small" type="warning">{{s.duration}}</NTag>
|
||||
</div>
|
||||
<p class="mt-0.5 text-[11px] text-slate-400">{{ s.id }} · {{ s.applicant }}</p>
|
||||
<p class="text-slate-400 text-[11px]">审批通过时间:<span class="font-medium text-slate-600">{{ s.approveTime ?? '—' }}</span></p>
|
||||
<p class="text-slate-400 text-[11px]">变更投用时间:<span class="font-medium text-[var(--warning-color)]">{{ s.useTime ? s.useTime : `待投用(计划 ${s.planUse})` }}</span></p>
|
||||
<p class="mt-0.5 text-[11px] text-slate-400">{{ s.statusDetail }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 rounded-md border bg-slate-50/60 px-2 py-1">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-[var(--theme-color)]" />
|
||||
<span class="text-[11px] font-medium text-slate-600">PSSR 确认表</span>
|
||||
<template v-if="s.pssr">
|
||||
<NButton ghost size="tiny" class="text-11px" @click="viewFile(s)">查看</NButton>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="downloadFile(s)">下载</NButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-[11px] text-[var(--warning-color)]">待传</span>
|
||||
</template>
|
||||
</div>
|
||||
<NButton v-if="!s.useTime" size="small" type="primary" class="text-xs" @click="fillUseTime(s)">填写投用日期</NButton>
|
||||
<NButton v-if="s.hasChange" size="small" ghost class="text-xs" @click="openChange(s)">打开变更单</NButton>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="h-100% flex items-center justify-center">
|
||||
<NEmpty description="暂无数据"></NEmpty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
<!-- 填写投用日期弹窗 -->
|
||||
<NModal
|
||||
v-model:show="useTimeModal"
|
||||
preset="card"
|
||||
title="填写投用日期"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '450px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<template #default>
|
||||
<div
|
||||
class="space-y-3 text-sm"
|
||||
:style="{
|
||||
'--warning-bg': getColorWithOpacity(themeStore.otherColor.warning, 0.1),
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
}"
|
||||
>
|
||||
<p class="text-xs text-slate-500">{{ currentRow?.id }} · {{ currentRow?.title }}</p>
|
||||
<div class="rounded-lg border border-[var(--warning-color)] bg-[var(--warning-bg)] px-3 py-2 text-xs leading-relaxed text-[var(--warning-color)]">
|
||||
提示:投用前应完成变更培训与 PSSR 确认(资料已上传)。投用后变更进入待验收阶段,临时变更自投用日起计算时限。
|
||||
</div>
|
||||
<NDatePicker
|
||||
v-model:value="useTime"
|
||||
type="datetime"
|
||||
:is-date-disabled="disabledDate"
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
placeholder="请选择投用时间"
|
||||
:time-picker-props="timePickerProps"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-[10px]">
|
||||
<NButton size="small" @click="useTimeModal = false">取消</NButton>
|
||||
<NButton size="small" type="primary" :loading="btnLoading" @click="saveUseTime">确认投用</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</NSpin>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,218 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface FlowStep {
|
||||
stage: string;
|
||||
actor: string;
|
||||
role?: string;
|
||||
title: string;
|
||||
detail?: string;
|
||||
system?: string;
|
||||
time: string;
|
||||
branch?: string;
|
||||
}
|
||||
interface Scenario {
|
||||
id: string; no: string; title: string; changeName: string;
|
||||
tags: { text: string; cls: string }[];
|
||||
desc: string; stages: string[]; steps: FlowStep[];
|
||||
}
|
||||
const T = {
|
||||
perm: "border-[#ACC6E5] bg-[#DFE9F6] text-[#1D4E9C]",
|
||||
temp: "border-amber-200 bg-amber-50 text-amber-700",
|
||||
general: "bg-slate-100 text-slate-600",
|
||||
major: "border-red-200 bg-red-50 text-red-600",
|
||||
urgent: "border-red-600 bg-red-600 text-white",
|
||||
branch: "border-violet-200 bg-violet-50 text-violet-600",
|
||||
};
|
||||
const STAGE_MAIN = ["变更申请", "会签与审批", "投用前确认", "投用", "验收", "关闭"];
|
||||
const STAGE_TEMP = ["变更申请", "会签与审批", "投用前确认", "投用", "临时运行与恢复", "验收", "关闭"];
|
||||
const SCENARIOS: Scenario[] = [
|
||||
{
|
||||
id: "s1", no: "示例 1", title: "永久 · 一般变更 全流程",
|
||||
changeName: "MOC-2026-R01-0041「缩合反应温度及时间优化」",
|
||||
tags: [{ text: "永久", cls: T.perm }, { text: "一般", cls: T.general }],
|
||||
desc: "覆盖:AI 识别 → 申请表 → 风险分析 → 培训/PSSR/验收/关闭表生成 → 部门审核 → 专业会签 → 最终批准 → 培训签到 → PSSR → 确认投用 → 验收 → 关闭归档。",
|
||||
stages: STAGE_MAIN,
|
||||
steps: [
|
||||
{ stage: "变更申请", actor: "张工艺", role: "车间工程师", title: "录入变更描述,AI 完成变更识别", detail: "判定为「变更」(非同类替换):反应温度 80℃→90℃、保温 8h→4h、DCS 报警/联锁值上调等 4 项前后对比,输出所需材料清单;识别结果人工确认,可直接修改", system: "AI 识别完成,自动预填变更前后对比表", time: "08-01 09:12" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "生成变更申请表", detail: "AI 自动总结名称「缩合反应温度及时间优化」(28 字内);类型判定:工艺;等级评分 12 分 → 一般变更;时限:永久;上传小试报告、杂质分析等附件", system: "附件版本受控,变更编号 MOC-2026-R01-0041 生成", time: "08-01 09:40" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "AI 风险预分析,形成风险分析记录表", detail: "输出危害严重性 / 事件概率 / 保护层影响预分析;系统按规则推荐 HAZOP + 检查表法(支持手动重选),形成风险分析记录与管控措施", system: "预分析报告经人工确认后生效", time: "08-01 10:05" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "生成培训 / PSSR / 验收评价 / 关闭确认表,提交审批", detail: "验收评价按预期效果量化:转化率 ≥98%、保温 4h±0.5h、杂质 ≤0.15% 等 5 项;选择审批人后提交", system: "进入一般变更审批流,企业微信 + 短信通知李主任", time: "08-01 10:20" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "车间主任", title: "部门审核:同意", detail: "AI 审批辅助:强制合规提示(联锁测试投用前完成)、冲突提示(无冲突)、关键参数对比;意见:变更方案可行,同意", system: "审批留痕,流转专业会签", time: "08-01 14:32" },
|
||||
{ stage: "会签与审批", actor: "王仪表", role: "专业会签 · 仪表", title: "专业会签:同意", detail: "核对 DCS 高报 85→95℃、联锁 90→100℃ 整定方案;采纳 AI 建议「联锁测试记录投用前完成」为行动项", system: "行动项自动写入 PSSR 清单,会签进度 1/2", time: "08-02 09:15" },
|
||||
{ stage: "会签与审批", actor: "周设备", role: "专业会签 · 设备", title: "专业会签:同意(会签完成 2/2)", detail: "确认保温更换与紧固件方案满足要求", system: "全体会签通过,流转最终批准", time: "08-02 10:40" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "部门负责人", title: "最终批准,变更下达实施", system: "自动生成培训任务与 PSSR 行动项并推送相关人员;状态 → 投用前确认;发布「审批通过」公告", time: "08-02 11:05" },
|
||||
{ stage: "投用前确认", actor: "岗位人员(4 人)+ 维修班(2 人)", title: "培训签到与考核", detail: "扫码签到 + 现场提问考核合格;维修班拍照上传纸质签到表", system: "签到与考核记录自动归档至变更档案", time: "08-03 15:00" },
|
||||
{ stage: "投用前确认", actor: "周设备 / 王仪表", title: "PSSR 逐项确认", detail: "联锁校核记录上传、送电前容量校核、保温验收;移动端现场拍照上传", system: "全部完成,自动推送属地负责人「具备投用条件」", time: "08-04 10:30" },
|
||||
{ stage: "投用", actor: "李主任", role: "属地负责人", title: "确认投用", detail: "投用时间默认当前日期时间,手动确认为 08-05 08:30", system: "状态 → 实施中;发布「确定投用」公告", time: "08-05 08:30" },
|
||||
{ stage: "验收", actor: "车间 + 各专业", title: "投用后验收评估", detail: "72h 运行验证 + 连续 3 批化验:转化率 98.6%(完全达到)、保温 4h(完全达到)、杂质 0.11%(完全达到)、温控联锁动作正确(完全达到)", system: "验收结论留痕:完全达到", time: "08-08 16:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "变更关闭确认", detail: "8 项资料全部归档、行动项全部关闭,系统推送专人;关闭确认表 AI 辅助判定逐项核对", system: "记录关闭时间,台账 → 已关闭;发布「变更关闭」公告", time: "08-09 09:20" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "s2", no: "示例 2", title: "永久 · 重要变更 全流程",
|
||||
changeName: "MOC-2026-R01-0035「R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)」",
|
||||
tags: [{ text: "永久", cls: T.perm }, { text: "重要", cls: T.major }],
|
||||
desc: "重要变更差异:等级评分 ≥20 分,审批流增加安全审查与公司领导终批,PSSR 需安全部门审核,投用由公司领导确认。",
|
||||
stages: STAGE_MAIN,
|
||||
steps: [
|
||||
{ stage: "变更申请", actor: "张工艺", role: "车间工程师", title: "AI 变更识别:判定为变更(排除同类替换)", detail: "功率 55→75kW、防爆等级 ExdⅡBT4→ExdⅡCT4、额定电流 102A→138A,超出同类替换范围;输出前后对比与所需材料清单", system: "AI 类型预判:设备类", time: "07-28 09:12" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "生成变更申请表", detail: "类型:设备;等级评分 21 分 → 重要变更;时限:永久;上传变更方案、设备数据表、防爆合格证、电气负荷校核计算书", system: "编号 MOC-2026-R01-0035;按「重要变更」自动匹配审批流:部门审核 → 专业会签 → 安全审查 → 公司领导终批", time: "07-28 09:45" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "风险分析:HAZOP + JSA + 检查表法", detail: "风险辨识会议(张工艺、王仪表、周设备、王海燕、李主任);4 项安全措施:联锁校核、送电前容量校核、培训考核、P&ID 与台账更新", system: "风险分析记录表随申请单一并提交", time: "07-29 13:32" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "车间主任", title: "部门审核:同意", detail: "经综合评审,变更方案与风险管控措施可行", system: "审批留痕,流转专业会签", time: "07-29 08:31" },
|
||||
{ stage: "会签与审批", actor: "王仪表 / 周设备 / 工艺专业", title: "专业会签:3/3 全部同意", detail: "王仪表核对联锁设定;周设备确认安装与校核方案;工艺专业确认运行窗口无变化", system: "会签全体通过,流转安全审查", time: "07-30 09:00" },
|
||||
{ stage: "会签与审批", actor: "安全部", role: "安全审查", title: "安全审查:同意", detail: "强制合规项确认:联锁测试记录投用前完成并归档;防爆接合面检查纳入 PSSR", system: "审查留痕,流转公司领导终批", time: "07-30 15:20" },
|
||||
{ stage: "会签与审批", actor: "刘副总", role: "公司领导", title: "最终批准", system: "变更下达实施;发布「审批通过」公告;培训与 PSSR 任务自动推送", time: "07-31 10:00" },
|
||||
{ stage: "投用前确认", actor: "岗位人员", title: "培训签到与考核合格", system: "培训记录自动归档", time: "08-01 14:00" },
|
||||
{ stage: "投用前确认", actor: "安全部", title: "PSSR 清单审核通过(重要变更)", detail: "审核现场检查项目与保护措施确认项", time: "08-02 09:00" },
|
||||
{ stage: "投用前确认", actor: "周设备 / 王仪表", title: "现场 PSSR 逐项确认", detail: "联锁跳车试验记录、防爆接合面与格兰头检查、送电前校核;移动端拍照上传", system: "全部完成,推送属地负责人与公司领导", time: "08-03 11:00" },
|
||||
{ stage: "投用", actor: "刘副总", role: "公司领导", title: "确认投用(重要变更由公司领导确认)", detail: "属地负责人确认条件具备后,公司领导确认投用 08-05 08:00", system: "状态 → 实施中;发布「确定投用」公告", time: "08-05 08:00" },
|
||||
{ stage: "验收", actor: "车间 + 各专业", title: "验收评估", detail: "投用首周每班测温与振动记录:无过载跳闸(完全达到)、电流裕量 27%(完全达到)、批次合格率 +1.6%(完全达到)", system: "验收结论留痕", time: "08-12 17:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "关闭确认", detail: "资料归档齐全(含联锁测试记录、PSSR 现场照片、验收报告)", system: "记录关闭时间;发布「变更关闭」公告", time: "08-13 10:00" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "s3", no: "示例 3", title: "临时 · 一般变更 全流程(3.1 到期恢复)",
|
||||
changeName: "MOC-2026-R02-0052「夏季工况循环水旁路临时调整」",
|
||||
tags: [{ text: "临时", cls: T.temp }, { text: "一般", cls: T.general }, { text: "3.1 到期恢复", cls: T.branch }],
|
||||
desc: "临时变更差异:强制填写计划恢复时间,到期前 7 天系统提醒;本示例按「3.1 到期恢复原状」走通全流程。",
|
||||
stages: STAGE_TEMP,
|
||||
steps: [
|
||||
{ stage: "变更申请", actor: "张工艺", role: "车间工程师", title: "AI 变更识别", detail: "循环水旁路阀开度由 30% 调整为 60%,应对夏季高温工况;判定为变更", time: "06-10 09:00" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "生成变更申请表", detail: "类型:工艺;等级评分 10 分 → 一般;时限:临时(计划恢复时间 2026-09-15),非紧急", system: "临时变更强制填写计划恢复时间", time: "06-10 09:30" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "风险分析并提交", detail: "AI 预分析 + 检查表法;验收评价:换热器出口温度 ≤42℃、无超温报警", system: "进入一般变更审批流", time: "06-10 10:00" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "车间主任", title: "部门审核:同意", time: "06-10 14:00" },
|
||||
{ stage: "会签与审批", actor: "王仪表 / 周设备", title: "专业会签:2/2 同意", time: "06-11 09:30" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "部门负责人", title: "最终批准", system: "变更下达;发布「审批通过」公告", time: "06-11 10:20" },
|
||||
{ stage: "投用前确认", actor: "岗位人员", title: "培训签到完成", system: "记录自动归档", time: "06-12 09:00" },
|
||||
{ stage: "投用前确认", actor: "周设备", title: "PSSR 确认完成", detail: "旁路阀行程确认、温度仪表校验", system: "全部完成,推送属地负责人", time: "06-12 15:00" },
|
||||
{ stage: "投用", actor: "李主任", role: "属地负责人", title: "确认投用 06-13 08:00", system: "台账标记「临时变更 · 计划恢复 09-15」;发布「确定投用」公告", time: "06-13 08:00" },
|
||||
{ stage: "临时运行与恢复", actor: "系统", title: "到期前 7 天自动提醒", detail: "向申请人、属地负责人推送「临时变更将于 09-15 到期,请安排恢复原状」(企业微信 + 短信),每日复提", branch: "分支:到期恢复", time: "09-08 09:00" },
|
||||
{ stage: "临时运行与恢复", actor: "张工艺", title: "现场恢复原状", detail: "旁路阀开度恢复至 30%,移动端拍照上传恢复确认", system: "恢复记录归档,推送属地确认", time: "09-10 10:00" },
|
||||
{ stage: "临时运行与恢复", actor: "李主任", role: "属地负责人", title: "属地确认恢复完成", time: "09-10 15:00" },
|
||||
{ stage: "验收", actor: "车间 + 各专业", title: "验收评估", detail: "临时运行期间换热器出口温度均值 39.8℃(完全达到)、无超温报警(完全达到)", time: "09-12 16:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "确认关闭(含恢复记录归档)", system: "记录关闭时间;发布「变更关闭」公告", time: "09-13 09:00" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "s32", no: "示例 3.2", title: "临时 · 一般变更(延期一次,到期再恢复)",
|
||||
changeName: "MOC-2026-R02-0052「夏季工况循环水旁路临时调整」(延续示例 3)",
|
||||
tags: [{ text: "临时", cls: T.temp }, { text: "一般", cls: T.general }, { text: "3.2 延期后恢复", cls: T.branch }],
|
||||
desc: "从到期提醒开始:延期申请 → 部门审批 → 到期时间更新(已延期 1 次)→ 新到期恢复 → 验收 → 关闭。",
|
||||
stages: ["到期提醒", "延期申请与审批", "恢复", "验收", "关闭"],
|
||||
steps: [
|
||||
{ stage: "到期提醒", actor: "系统", title: "前置流程与示例 3 相同", detail: "申请 → 审批 → 培训 → PSSR → 投用(共 9 步)已于 06-13 完成,变更处于临时运行中", time: "06-13" },
|
||||
{ stage: "到期提醒", actor: "系统", title: "到期前 7 天提醒", detail: "推送「将于 09-15 到期」;因高温天气持续,申请人决定办理延期", time: "09-08 09:00" },
|
||||
{ stage: "延期申请与审批", actor: "张工艺", title: "发起延期申请", detail: "延期事项:计划恢复时间 09-15 → 10-15;理由:高温天气持续,装置仍需旁路运行;系统自动校验剩余延期次数(≤1 次)", branch: "分支:延期一次", time: "09-08 10:00" },
|
||||
{ stage: "延期申请与审批", actor: "李主任", role: "部门负责人", title: "延期审批:同意", detail: "一般临时变更延期由部门负责人批准", system: "留痕并更新到期时间;台账标记「已延期 1 次」", time: "09-09 09:00" },
|
||||
{ stage: "恢复", actor: "系统", title: "新到期日前 7 天再次提醒", time: "10-08 09:00" },
|
||||
{ stage: "恢复", actor: "张工艺", title: "恢复原状 + 拍照上传", detail: "旁路阀开度恢复至 30%,移动端拍照上传", system: "恢复记录归档,推送属地确认", time: "10-10 10:00" },
|
||||
{ stage: "恢复", actor: "李主任", role: "属地负责人", title: "属地确认恢复完成", time: "10-10 15:00" },
|
||||
{ stage: "验收", actor: "车间 + 各专业", title: "验收评估:延期期间运行数据达标", detail: "出口温度均值 40.2℃(完全达到)、无超温报警(完全达到)", time: "10-12 16:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "确认关闭", system: "延期与恢复全程留痕可追溯;发布「变更关闭」公告", time: "10-13 09:00" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "s33", no: "示例 3.3", title: "临时 · 一般变更(到期转化为永久)",
|
||||
changeName: "MOC-2026-R02-0052「夏季工况循环水旁路临时调整」(延续示例 3)",
|
||||
tags: [{ text: "临时", cls: T.temp }, { text: "一般", cls: T.general }, { text: "3.3 转为永久", cls: T.branch }],
|
||||
desc: "从到期提醒开始:转永久申请 → AI 补充长期风险分析 → 按原等级审批 → 时限改判留痕、取消恢复期限 → 验收 → 关闭。",
|
||||
stages: ["到期提醒", "转永久申请", "补充风险分析", "转永久审批", "验收", "关闭"],
|
||||
steps: [
|
||||
{ stage: "到期提醒", actor: "系统", title: "前置流程与示例 3 相同", detail: "已投用,变更处于临时运行中(计划恢复 09-15)", time: "06-13" },
|
||||
{ stage: "到期提醒", actor: "系统", title: "到期前 7 天提醒", time: "09-08 09:00" },
|
||||
{ stage: "转永久申请", actor: "张工艺", title: "发起「临时转永久」申请", detail: "理由:临时运行 3 个月效果稳定,夏季工况每年重现,建议转为永久;转永久后不再恢复原状", branch: "分支:转为永久", time: "09-08 10:30" },
|
||||
{ stage: "补充风险分析", actor: "张工艺", title: "AI 重新分析长期运行风险", detail: "补充长期运行场景:旁路长期 60% 开度对换热器结垢的影响、冬季低负荷工况影响;更新管控措施", system: "风险分析记录版本 V2 留痕", time: "09-09 09:00" },
|
||||
{ stage: "转永久审批", actor: "李主任", role: "车间主任", title: "部门审核:同意", time: "09-09 14:00" },
|
||||
{ stage: "转永久审批", actor: "王仪表 / 周设备", title: "专业会签:同意", time: "09-10 09:00" },
|
||||
{ stage: "转永久审批", actor: "李主任", role: "部门负责人", title: "批准转为永久变更", system: "变更时限「临时 → 永久」改判留痕;取消恢复期限与到期提醒", time: "09-10 10:00" },
|
||||
{ stage: "验收", actor: "车间 + 各专业", title: "按永久变更标准验收", detail: "连续 30 天运行数据评估:温度、能耗指标全部达标(完全达到)", time: "10-10 16:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "确认关闭", system: "转永久审批记录并入变更档案;发布「变更关闭」公告", time: "10-12 09:00" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "s4", no: "示例 4", title: "临时 · 重要变更 全流程",
|
||||
changeName: "MOC-2026-R01-0039「V-102 安全阀起跳压力临时调整」",
|
||||
tags: [{ text: "临时", cls: T.temp }, { text: "重要", cls: T.major }, { text: "紧急", cls: T.urgent }],
|
||||
desc: "重要 + 临时 + 紧急:紧急标记各节点加急提醒当日办结,安全审查强制合规项,到期恢复起跳压力并重新校验。",
|
||||
stages: STAGE_TEMP,
|
||||
steps: [
|
||||
{ stage: "变更申请", actor: "张工艺", role: "车间工程师", title: "AI 变更识别", detail: "V-102 安全阀起跳压力由 1.6MPa 临时调整为 1.45MPa(上游管网改造期间)", system: "紧急标记:安全生产需要,各节点加急提醒", time: "07-16 08:00" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "生成变更申请表", detail: "类型:设备;等级评分 22 分 → 重要;时限:临时(计划恢复 08-30);紧急标记", system: "按「重要 + 紧急」匹配审批流并加急推送", time: "07-16 08:20" },
|
||||
{ stage: "变更申请", actor: "张工艺", title: "风险分析:JSA + 检查表法 + AI 预分析", detail: "起跳压力下调后泄放能力复核、工艺波动场景评估", time: "07-16 09:00" },
|
||||
{ stage: "会签与审批", actor: "李主任", role: "车间主任", title: "部门审核:同意(加急)", time: "07-16 10:00" },
|
||||
{ stage: "会签与审批", actor: "王仪表 / 周设备 / 安全员", title: "专业会签:3/3 同意", detail: "安全阀校验报告复核、泄放量核算", time: "07-16 11:30" },
|
||||
{ stage: "会签与审批", actor: "安全部", role: "安全审查", title: "安全审查:同意", detail: "强制合规:调整期间每班巡查记录、恢复后重新校验", time: "07-16 14:00" },
|
||||
{ stage: "会签与审批", actor: "刘副总", role: "公司领导", title: "最终批准(加急)", system: "当日完成全部审批;发布「审批通过」公告", time: "07-16 15:30" },
|
||||
{ stage: "投用前确认", actor: "岗位人员", title: "培训签到(新起跳压力与应急处置)", system: "记录自动归档", time: "07-17 09:00" },
|
||||
{ stage: "投用前确认", actor: "安全部 / 周设备", title: "PSSR 确认", detail: "安全阀重新铅封、校验标签更新、巡查记录表就位", system: "全部完成,推送属地负责人与公司领导", time: "07-17 14:00" },
|
||||
{ stage: "投用", actor: "刘副总", role: "公司领导", title: "确认投用(重要变更)", system: "状态 → 实施中;发布「确定投用」公告", time: "07-18 08:00" },
|
||||
{ stage: "临时运行与恢复", actor: "系统", title: "到期前 7 天提醒(临时变更 08-30 到期)", branch: "分支:到期恢复", time: "08-23 09:00" },
|
||||
{ stage: "临时运行与恢复", actor: "张工艺 / 周设备", title: "恢复起跳压力至 1.6MPa", detail: "安全阀重新校验、铅封,校验报告与现场照片上传", system: "恢复记录归档,推送属地确认", time: "08-25 10:00" },
|
||||
{ stage: "验收", actor: "车间 + 各专业 + 安全部", title: "验收评估", detail: "调整期间无超压、无泄放事件;恢复后校验合格(完全达到)", time: "08-27 16:00" },
|
||||
{ stage: "关闭", actor: "变更主管部门专人", title: "确认关闭", system: "记录关闭时间;发布「变更关闭」公告", time: "08-28 09:00" },
|
||||
],
|
||||
},
|
||||
];
|
||||
function open(item:any) {
|
||||
routerPushByKey('home_processdetail', { query: { params: JSON.stringify(item) } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 流程演示 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-3.5">
|
||||
<span class="flex items-center gap-1.5 text-base font-bold text-slate-800">
|
||||
<Icon icon="fluent:flow-dot-24-filled" class="size-22px" :style="{ color: themeStore.themeColor }" />全流程走通演示
|
||||
</span>
|
||||
<span class="text-xs text-slate-400">
|
||||
6 个完整示例:永久 / 临时 × 一般 / 重要,含临时到期恢复、延期、转永久分支;逐步推进查看每个环节的操作人与系统行为
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2 scroll">
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
<div
|
||||
v-for="s in SCENARIOS" :key="s.id"
|
||||
class="flex flex-col border bg-white p-4 transition hover:border-[var(--hover-border-color)] hover:shadow-sm"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--hover-border-color': themeStore.themeColor,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-bold" :style="{ color: themeStore.themeColor }">{{ s.no }}</span>
|
||||
<span class="text-sm font-semibold text-slate-800">{{ s.title }}</span>
|
||||
<NTag v-for="t in s.tags" :key="t.text" :class="t.cls" size="small">{{ t.text }}</NTag>
|
||||
</div>
|
||||
<div class="mt-1.5 text-xs text-slate-500">示例变更:{{ s.changeName }}</div>
|
||||
<p class="mt-2 flex-1 text-xs leading-relaxed text-slate-400">{{ s.desc }}</p>
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<span class="text-[11px] text-slate-400">{{ s.stages.length }} 个阶段 · {{ s.steps.length }} 个步骤</span>
|
||||
<NButton type="primary" size="small" class="text-xs" @click="open(s)">
|
||||
<Icon icon="griddy-icons:play" class="size-16px" /> 开始演示
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 185px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,184 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const sc = ref<any>(null);
|
||||
const cur = ref<number>(0);
|
||||
const playing = ref(false);
|
||||
const curStageIdx = computed(() => (sc.value ? sc.value.stages.indexOf(sc.value.steps[cur.value].stage) : 0));
|
||||
const finished = computed(() => (sc.value ? cur.value >= sc.value?.steps?.length - 1 : false));
|
||||
const ROLES_NAME: Record<string, string> = {
|
||||
applicant: "申请人(张工艺)", process: "专业会签(王仪表)", process2: "专业会签(周设备)",
|
||||
safety: "安全工程师(王海燕)", dept: "车间主任(李主任)", vp: "分管领导(刘副总)", admin: "系统管理员",
|
||||
quality: "质量工程师(陈质量)", viewer: "非审批人员(吴新)",
|
||||
};
|
||||
const VIEW_NAME: Record<string, string> = {
|
||||
apply: "变更申请", tasks: "我的任务", approvals: "审批中心", execution: "PSSR / 验收", closure: "关闭管理",
|
||||
};
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
watch([playing, cur, sc], ([p, c, s]) => {
|
||||
if (timer) { clearTimeout(timer); timer = undefined; }
|
||||
if (!p || !s) return;
|
||||
if (c >= s.steps.length - 1) { playing.value = false; return; }
|
||||
timer = setTimeout(() => { cur.value = Math.min(cur.value + 1, (sc.value?.steps.length ?? 1) - 1); }, 2000);
|
||||
});
|
||||
|
||||
function jumpFor(s: any): { role: string; view: string } | null {
|
||||
const a = s.actor;
|
||||
if (a === "系统") return null;
|
||||
let role: string;
|
||||
if (a.includes("张工艺")) role = "applicant";
|
||||
else if (a.includes("李主任")) role = "dept";
|
||||
else if (a.includes("王仪表")) role = "process";
|
||||
else if (a.includes("周设备")) role = "process2";
|
||||
else if (a.includes("刘副总")) role = "vp";
|
||||
else if (a.includes("专人") || a.includes("安全")) role = "safety";
|
||||
else if (a.includes("岗位") || a.includes("维修班")) role = "applicant";
|
||||
else if (a.includes("车间")) role = "dept";
|
||||
else return null;
|
||||
let view: string;
|
||||
if (role === "applicant") view = s.stage === "变更申请" ? "apply" : "tasks";
|
||||
else if (s.stage.includes("审批")) view = "approvals";
|
||||
else if (s.stage === "关闭") view = "closure";
|
||||
else view = "execution";
|
||||
return { role, view };
|
||||
}
|
||||
function prev() { playing.value = false; cur.value = Math.max(0, cur.value - 1); }
|
||||
function next() { playing.value = false; if (sc.value) cur.value = Math.min(sc.value.steps.length - 1, cur.value + 1); }
|
||||
function reset() { playing.value = false; cur.value = 0; }
|
||||
function back() {
|
||||
routerPushByKey("home_process");
|
||||
}
|
||||
function jumpTo() {
|
||||
routerPushByKey("home_initiatechange");
|
||||
}
|
||||
onMounted(() => {
|
||||
if(route.query && route.query.params){
|
||||
sc.value = JSON.parse(route.query.params as string);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 流程演示详情 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<NButton ghost type="primary" class="text-sm font-medium" @click="back">
|
||||
<Icon icon="ep:back" class="size-16px mr-1" /> 返回示例列表
|
||||
</NButton>
|
||||
<span class="text-base font-bold text-slate-800">{{ sc?.no }} {{ sc?.title }}</span>
|
||||
<NTag size="small" v-for="t in sc?.tags" :key="t.text" :class="t.cls">{{ t.text }}</NTag>
|
||||
<span class="text-xs text-slate-400">示例变更:{{ sc?.changeName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 阶段进度条 -->
|
||||
<div class="flex items-center gap-1 overflow-x-auto rounded-lg border bg-white p-3">
|
||||
<div v-for="(st, i) in sc?.stages" :key="st" class="flex shrink-0 items-center gap-1">
|
||||
<span
|
||||
class="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium"
|
||||
:class="i < curStageIdx ? 'bg-emerald-50 text-emerald-600' : i === curStageIdx ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
|
||||
:style="{ '--theme-color': themeStore.themeColor }"
|
||||
>
|
||||
<Icon v-if="i < curStageIdx" icon="ix:success" class="size-14px" />
|
||||
<span v-else class="flex h-3.5 w-3.5 items-center justify-center rounded-full border text-[10px]" :class="i === curStageIdx ? 'border-white' : 'border-slate-300'">{{ Number(i) + 1 }}</span>
|
||||
{{ st }}
|
||||
</span>
|
||||
<Icon v-if="Number(i) < sc?.stages?.length - 1" icon="formkit:right" class="size-12px text-slate-300" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制条 -->
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-white px-3 py-2">
|
||||
<NButton ghost size="small" class="text-xs" :disabled="cur === 0" @click="prev">
|
||||
<Icon icon="formkit:left" class="size-12px" /> 上一步
|
||||
</NButton>
|
||||
<NButton type="primary" size="small" class="text-xs text-white" :disabled="finished" @click="next">
|
||||
下一步 <Icon icon="formkit:right" class="size-12px" />
|
||||
</NButton>
|
||||
<NButton ghost size="small" class="text-xs" :disabled="finished && !playing" @click="playing = !playing">
|
||||
<template v-if="playing"><Icon icon="material-symbols:pause-outline-rounded" class="size-14px" /> 暂停</template>
|
||||
<template v-else><Icon icon="griddy-icons:play" class="size-14px" /> 自动播放</template>
|
||||
</NButton>
|
||||
<NButton ghost size="small" class="text-xs" @click="reset">
|
||||
<Icon icon="system-uicons:reset" class="size-12px" /> 重置
|
||||
</NButton>
|
||||
<span class="ml-auto text-xs text-slate-400">第 {{ cur + 1 }} / {{ sc?.steps?.length }} 步{{ finished ? " · 流程已走通 ✓" : "" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 完成横幅 -->
|
||||
<div v-if="finished" class="flex flex-wrap items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-sm text-emerald-700">
|
||||
<Icon icon="ix:success" class="size-16px" />
|
||||
<b>流程已走通:</b>共 {{ sc.steps.length }} 步,{{ sc.stages.length }} 个阶段全部闭环;全程操作留痕、资料归档、公告发布完整可追溯。
|
||||
</div>
|
||||
|
||||
<!-- 步骤时间线 -->
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(s, i) in sc?.steps" :key="i" @click="playing = false; cur = Number(i)"
|
||||
class="flex w-full cursor-pointer items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition"
|
||||
:class="i === cur ? 'border-[var(--theme-color)] bg-[#EAF0F9]/50 shadow-sm' : Number(i) < cur ? 'border-emerald-100 bg-emerald-50/30' : 'bg-white hover:border-slate-300'"
|
||||
:style="{ '--theme-color': themeStore.themeColor }"
|
||||
>
|
||||
<span
|
||||
class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold"
|
||||
:class="Number(i) < cur ? 'bg-emerald-500 text-white' : i === cur ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
|
||||
:style="{ '--theme-color': themeStore.themeColor }"
|
||||
>
|
||||
<Icon v-if="Number(i) < cur" icon="ix:success" class="size-16px" />
|
||||
<template v-else>{{ Number(i) + 1 }}</template>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span class="flex items-center gap-1 text-xs text-slate-500">
|
||||
<Icon icon="lucide:user-round" class="size-14px" />
|
||||
{{ s.actor }}{{ s.role ? `(${s.role})` : "" }}
|
||||
</span>
|
||||
<span class="rounded bg-slate-100 px-1.5 text-[10px] text-slate-400">{{ s.stage }}</span>
|
||||
<span class="text-sm font-medium" :class="i === cur ? 'text-[#17407F]' : 'text-slate-700'">{{ s.title }}</span>
|
||||
<NTag v-if="s.branch" class="border-violet-200 bg-violet-50 text-violet-600">{{ s.branch }}</NTag>
|
||||
<span class="ml-auto shrink-0 font-mono text-[11px] text-slate-400">{{ s.time }}</span>
|
||||
<NButton ghost type="primary" size="tiny" v-if="jumpFor(s) && s.actor !== '系统'"
|
||||
class="text-[10px] font-medium"
|
||||
:title="`切换到${ROLES_NAME[jumpFor(s)!.role]}的「${VIEW_NAME[jumpFor(s)!.view] ?? jumpFor(s)!.view}」界面`"
|
||||
@click.stop="jumpTo"
|
||||
>
|
||||
进入界面 →
|
||||
</NButton>
|
||||
</span>
|
||||
<span v-if="i === cur && (s.detail || s.system)" class="mt-2 block space-y-1.5 border-t border-[#ACC6E5]/40 pt-2">
|
||||
<span v-if="s.detail" class="block text-xs leading-relaxed text-slate-600"><b class="text-slate-500">内容:</b>{{ s.detail }}</span>
|
||||
<span
|
||||
v-if="s.system"
|
||||
class="flex items-start gap-1.5 text-xs leading-relaxed"
|
||||
:style="{ color: themeStore.themeColor }"
|
||||
>
|
||||
<Icon icon="lucide:sparkles" class="mt-0.8 size-12px" />{{ s.system }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 185px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,265 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const keyword = ref<string>("");
|
||||
const openCount = ref<number>(4);
|
||||
const closedCount = ref<number>(2);
|
||||
const currentTab = ref<string>("open");
|
||||
const range = ref<string>("全部时间");
|
||||
const list = ref<any[]>([]);
|
||||
const openList = [
|
||||
{
|
||||
id: "MOC-2026-R02-0038",statusDetail: "培训 / PSSR 资料已传 · 待确认投用",
|
||||
dept: "一车间", planUse: "2026-07-31", applicant: "吴强",
|
||||
title: 'DCS 罐区液位报警值 LIA-301 调整', status: '待PSSR',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0039",statusDetail: "待关闭确认(资料更新核查中)",
|
||||
dept: "一车间", planUse: "2026-07-16(已投用)", applicant: "张工艺",
|
||||
title: 'V-102 安全阀起跳压力临时调整(紧急补办)', status: '待关闭',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R03-0040",statusDetail: "实施准备中 · 培训 / PSSR 资料待上传",
|
||||
dept: "供应部", planUse: "2026-08-01", applicant: "刘芳",
|
||||
title: '原料环己烷供应商变更(新增 B 供应商)', status: '实施中',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0031",statusDetail: "变更验收(72h 运行验证)",
|
||||
dept: "二车间", planUse: "2026-07-15", applicant: "周涛",
|
||||
title: '冷冻盐水泵 P-103 叶轮材质变更', status: '待验收',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: false,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0038",statusDetail: "培训 / PSSR 资料已传 · 待确认投用",
|
||||
dept: "一车间", planUse: "2026-07-31", applicant: "吴强",
|
||||
title: 'DCS 罐区液位报警值 LIA-301 调整', status: '待PSSR',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0039",statusDetail: "待关闭确认(资料更新核查中)",
|
||||
dept: "一车间", planUse: "2026-07-16(已投用)", applicant: "张工艺",
|
||||
title: 'V-102 安全阀起跳压力临时调整(紧急补办)', status: '待关闭',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R03-0040",statusDetail: "实施准备中 · 培训 / PSSR 资料待上传",
|
||||
dept: "供应部", planUse: "2026-08-01", applicant: "刘芳",
|
||||
title: '原料环己烷供应商变更(新增 B 供应商)', status: '实施中',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0031",statusDetail: "变更验收(72h 运行验证)",
|
||||
dept: "二车间", planUse: "2026-07-15", applicant: "周涛",
|
||||
title: '冷冻盐水泵 P-103 叶轮材质变更', status: '待验收',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: false,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0038",statusDetail: "培训 / PSSR 资料已传 · 待确认投用",
|
||||
dept: "一车间", planUse: "2026-07-31", applicant: "吴强",
|
||||
title: 'DCS 罐区液位报警值 LIA-301 调整', status: '待PSSR',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0039",statusDetail: "待关闭确认(资料更新核查中)",
|
||||
dept: "一车间", planUse: "2026-07-16(已投用)", applicant: "张工艺",
|
||||
title: 'V-102 安全阀起跳压力临时调整(紧急补办)', status: '待关闭',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R03-0040",statusDetail: "实施准备中 · 培训 / PSSR 资料待上传",
|
||||
dept: "供应部", planUse: "2026-08-01", applicant: "刘芳",
|
||||
title: '原料环己烷供应商变更(新增 B 供应商)', status: '实施中',
|
||||
pssr: "",hasChange: true,
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R02-0031",statusDetail: "变更验收(72h 运行验证)",
|
||||
dept: "二车间", planUse: "2026-07-15", applicant: "周涛",
|
||||
title: '冷冻盐水泵 P-103 叶轮材质变更', status: '待验收',
|
||||
pssr: "PSSR 检查确认表-签字版.pdf",hasChange: false,
|
||||
},
|
||||
];
|
||||
const closedList = [
|
||||
{
|
||||
id: "MOC-2026-R03-0027",statusDetail: "关闭确认完成,已归档",
|
||||
dept: "质量部", planUse: "2026-06-25", applicant: "陈静",
|
||||
title: '化验室通风橱整体更换', status: '已关闭',
|
||||
pssr: "PSSR 检查确认表-归档版.pdf"
|
||||
},
|
||||
{
|
||||
id: "MOC-2026-R01-0024",statusDetail: "关闭确认完成,已归档",
|
||||
dept: "一车间", planUse: "2026-06-20", applicant: "孙丽",
|
||||
title: '氮气管网压力分级调整', status: '已关闭',
|
||||
pssr: "PSSR 检查确认表-归档版.pdf"
|
||||
},
|
||||
];
|
||||
|
||||
// 切换tab
|
||||
const tabChange = (k: string) => {
|
||||
list.value = []
|
||||
currentTab.value = k;
|
||||
loading.value = true;
|
||||
if(k === 'open'){
|
||||
setTimeout(() => {
|
||||
list.value = openList;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
if(k === 'closed'){
|
||||
setTimeout(() => {
|
||||
list.value = closedList;
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 查看
|
||||
const viewFile = (record: any) => {
|
||||
window.$message?.info(`预览「${record.pssr}」(上传件不做内容识别,原件已随单归档)`)
|
||||
};
|
||||
// 下载
|
||||
const downloadFile = (record: any) => {
|
||||
window.$message?.success(`下载「${record.pssr}」`)
|
||||
};
|
||||
// 下载模板
|
||||
const downloadTemplate = (record: any) => {
|
||||
window.$message?.success(`下载 PSSR 确认表空白模板(打印后线下签署)`)
|
||||
};
|
||||
// 打开变更单
|
||||
const openChange = (record: any) => {
|
||||
routerPushByKey('home_highriskdetail', { query: { params: JSON.stringify(record) } })
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
tabChange('open');
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- PSSR监督 -->
|
||||
<NSpin :show="loading" size="small">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 mb-3"
|
||||
:style="{ '--theme-color': themeStore.themeColor}"
|
||||
>
|
||||
<div class="flex overflow-hidden rounded-full border">
|
||||
<div @click="tabChange('open')" class="px-3.5 py-1.5 text-xs cursor-pointer select-none"
|
||||
:class="currentTab === 'open' ? 'bg-[var(--theme-color)] font-medium text-white' : 'bg-white text-slate-600 hover:bg-slate-50'">
|
||||
未关闭的变更 <b>{{ openCount }}</b>
|
||||
</div>
|
||||
<div @click="tabChange('closed')" class="border-l px-3.5 py-1.5 text-xs cursor-pointer select-none"
|
||||
:class="currentTab === 'closed' ? 'bg-[var(--theme-color)] font-medium text-white' : 'bg-white text-slate-600 hover:bg-slate-50'">
|
||||
已关闭的变更 <b>{{ closedCount }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<NInput v-model:value="keyword" size="small" class="text-xs !w-160px" placeholder="搜索变更名称 / 编号">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<NButton
|
||||
size="tiny"
|
||||
round
|
||||
v-for="t in ['全部时间', '近 7 天', '近 30 天', '近 90 天']"
|
||||
:key="t"
|
||||
class="text-[11px]"
|
||||
:type="range === t ? 'primary' : 'default'"
|
||||
@click="range = t"
|
||||
>
|
||||
{{ t }}
|
||||
</NButton>
|
||||
</div>
|
||||
<span class="ml-auto text-[11px] text-slate-400">培训 / PSSR 为线下确认 + 资料上传制 · PSSR 检查表同行查看 / 下载</span>
|
||||
</div>
|
||||
<div class="text-[11px] leading-4 text-slate-400 pb-2">
|
||||
培训 / PSSR 为线下组织并签字,拍照或扫描上传即视为完成确认并随单归档;系统不做内容识别,本页为 PSSR 检查表的查看 / 下载入口。
|
||||
</div>
|
||||
<!-- 未关闭的变更 -->
|
||||
<div v-if="currentTab === 'open'" class="scroll">
|
||||
<div
|
||||
class="space-y-3 h-100%"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
}"
|
||||
>
|
||||
<div v-for="(s, i) in list" :key="`${s.id ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-x-3 gap-y-2 border p-4" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NTag v-if="s.status==='待PSSR'" type="warning" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='待关闭'" type="error" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='实施中'" type="primary" size="small">{{ s.status }}</NTag>
|
||||
<NTag v-if="s.status==='待验收'" type="success" size="small">{{ s.status }}</NTag>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="block truncate text-sm font-medium text-slate-800">{{ s.title }}</p>
|
||||
<span class="mt-0.5 block text-[11px] text-slate-400">{{ s.id }} · {{ s.dept }} · {{ s.applicant }} · {{ s.statusDetail }} · 计划投用 {{ s.planUse }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 rounded-md border bg-slate-50/60 px-2 py-1">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-[var(--theme-color)]" />
|
||||
<span class="text-[11px] font-medium text-slate-600">PSSR 检查确认表</span>
|
||||
<template v-if="s.pssr">
|
||||
<span class="max-w-[150px] truncate text-[11px] text-slate-400">{{ s.pssr }}</span>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="viewFile(s)">查看</NButton>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="downloadFile(s)">下载</NButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-[11px] text-[var(--warning-color)]">待传</span>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="downloadTemplate(s)">下载模板</NButton>
|
||||
</template>
|
||||
</div>
|
||||
<NButton v-if="s.hasChange" size="small" type="primary" class="text-xs" @click="openChange(s)">打开变更单</NButton>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="h-100% flex items-center justify-center">
|
||||
<NEmpty description="暂无数据"></NEmpty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 已关闭的变更 -->
|
||||
<div v-if="currentTab === 'closed'" class="scroll">
|
||||
<div
|
||||
class="space-y-3 h-100%"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--warning-color': themeStore.otherColor.warning,
|
||||
}"
|
||||
>
|
||||
<div v-for="(s, i) in list" :key="`${s.id ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-x-3 gap-y-2 border p-4" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NTag v-if="s.status==='已关闭'" type="success" size="small">{{ s.status }}</NTag>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="block truncate text-sm font-medium text-slate-800">{{ s.title }}</p>
|
||||
<span class="mt-0.5 block text-[11px] text-slate-400">{{ s.id }} · {{ s.dept }} · {{ s.applicant }} · {{ s.statusDetail }} · 计划投用 {{ s.planUse }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 rounded-md border bg-slate-50/60 px-2 py-1">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-[var(--theme-color)]" />
|
||||
<span class="text-[11px] font-medium text-slate-600">PSSR 检查确认表</span>
|
||||
<span class="max-w-[150px] truncate text-[11px] text-slate-400">{{ s.pssr }}</span>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="viewFile(s)">查看</NButton>
|
||||
<NButton ghost size="tiny" class="text-11px" @click="downloadFile(s)">下载</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="list.length === 0" class="h-100% flex items-center justify-center">
|
||||
<NEmpty description="暂无数据"></NEmpty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpin>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 212px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user