moc变更管理系统框架
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import {
|
||||
CHANGES, DOC_TODO, LEVEL_COLOR, STATUS_COLOR, TEMP_WATCH, URGE_RECORDS,
|
||||
countOpenStatus, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||
} from "@/typings/model";
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
import FlowRing from "../modules/FlowRing.vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const emit = defineEmits<{ go: [v: string, preset?: string]; openChange: [c: ChangeOrder] }>();
|
||||
|
||||
// 我的变更进度
|
||||
const MY_CLOSED = [
|
||||
{ id: "MOC-2026-R01-0021", title: "T-101 塔顶冷凝器清洗周期调整", type: "管理", level: "一般", status: "已关闭", date: "2026-05-18", duration: "永久" },
|
||||
{ id: "MOC-2026-R01-0017", title: "夏季循环水旁滤临时停用(已恢复)", type: "设备", level: "一般", status: "已关闭", date: "2026-04-02", duration: "临时" },
|
||||
];
|
||||
const ROLES = [
|
||||
{ id: "applicant", name: "申请人", person: "张工艺", org: "一车间 · 工艺组", desc: "发起变更申请、AI 辅助判定、跟踪进度" },
|
||||
{ id: "process", name: "专业会签(仪表)", person: "王仪表", org: "技术部 · 仪表科", desc: "专业会签、HAZOP 分析、确认连带变更" },
|
||||
{ id: "process2", name: "专业会签(设备)", person: "周设备", org: "设备管理部 · 设备科", desc: "专业会签、设备完整性检查、行动项落实" },
|
||||
{ id: "safety", name: "安全工程师", person: "王海燕", org: "安全环保部", desc: "安全审查、PSSR 检查表确认、风险分析组织" },
|
||||
{ id: "dept", name: "车间主任", person: "李主任", org: "一车间", desc: "部门审核、现场验收、变更关闭确认" },
|
||||
{ id: "vp", name: "分管领导", person: "刘副总", org: "公司领导", desc: "重要变更最终批准、移动端审批" },
|
||||
{ id: "admin", name: "系统管理员", person: "刘敏", org: "信息化部", desc: "流程配置、字典维护、权限分配" },
|
||||
{ id: "quality", name: "质量工程师", person: "陈质量", org: "质量部", desc: "质量会签、产品合格判定、检验方案确认" },
|
||||
{ id: "viewer", name: "非审批人员", person: "吴新", org: "相关部门", desc: "以信息查看为主,不参与变更流程" },
|
||||
];
|
||||
const mine = computed(() => CHANGES.filter((c) => c.applicant === ROLES[0].person));
|
||||
interface MyRow { c: ChangeOrder | null; id: string; title: string; type: ChangeType; level: ChangeLevel; status: ChangeStatus; date: string; urgent: boolean; duration: "永久" | "临时" }
|
||||
const myRows = computed(() => [
|
||||
...mine.value.map((c:ChangeOrder) => ({ c, id: c.id, title: c.title, type: c.type, level: c.level, status: c.status, date: c.date, urgent: c.urgent, duration: c.duration })),
|
||||
...MY_CLOSED.map((m) => ({ c: null, ...m, urgent: false })),
|
||||
]);
|
||||
const BIG_STAGES = ["申请审批", "实施与关闭", "变更关闭"];
|
||||
const bigStageOf = (s: ChangeStatus) => (s === "审批中" ? 0 : s === "已关闭" ? 2 : 1);
|
||||
const openRow = (row: MyRow) => {
|
||||
if (row.c){
|
||||
emit("openChange", row.c);
|
||||
} else {
|
||||
window.$message?.info(`演示:查看 ${row.id} 归档详情`);
|
||||
}
|
||||
};
|
||||
// 临时变更到期提醒情况
|
||||
const tempRemind = TEMP_WATCH;
|
||||
const tempTotal = tempRemind.reduce((a, b) => a + b.n, 0);
|
||||
// 尚未关闭变更所处状态
|
||||
const openStatus = computed(() => countOpenStatus(mine.value));
|
||||
const openTotal = computed(() => openStatus.value.reduce((a, b) => a + b.n, 0));
|
||||
// 被驳回信息处理
|
||||
const REJECTED_ITEMS = [
|
||||
{ id: "MOC-2026-R01-0033", title: "P-301 机封冲洗方案调整", reason: "变更前后对比描述不完整,缺少冲洗流量核算数据", by: "李主任 · 车间主任", date: "2026-07-29" },
|
||||
{ id: "MOC-2026-R01-0029", title: "活性炭更换周期延长(3→6 个月)", reason: "风险分析未覆盖吸附饱和穿透情景,请补充 JSA 分析", by: "王海燕 · 安全工程师", date: "2026-07-25" },
|
||||
];
|
||||
// 资料管理
|
||||
const DOC_LINKS = [{ name: '受控文件库', desc: '现行版规程、图纸、数据表' }, { name: '变更档案查询', desc: '历史变更档案调取' }]
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的工作台 -->
|
||||
<NCard :bordered="false" size="small" :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="flex-y-center">
|
||||
<p
|
||||
class="size-32px flex items-center justify-center bg-[rgb(var(--primary-color))] text-white text-xs font-bold"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
MOC
|
||||
</p>
|
||||
<div class="pl-12px">
|
||||
<p class="text-14px font-semibold">我的工作台</p>
|
||||
<p class="text-#999 text-11px">车间工程师操作台 · 发起变更、推进实施、完成闭环</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<p class="text-#999 text-xs">2026年8月10日 星期一 10:21</p>
|
||||
<NButton type="primary" size="small">
|
||||
<Icon icon="ic:round-plus" class="size-18px" />
|
||||
发起变更
|
||||
</NButton>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NCard>
|
||||
<!-- 卡片数据 -->
|
||||
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="gap" :y-gap="16">
|
||||
<NGi>
|
||||
<div
|
||||
class="flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<p class="text-12px font-600">待修改驳回</p>
|
||||
<div class="flex justify-between">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="2"
|
||||
class="text-26px font-600"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-12px">修改后重新提交</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<p class="text-12px font-600">待上传资料</p>
|
||||
<div class="flex justify-between">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="2"
|
||||
class="text-26px font-600 text-[#333]"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-12px">培训 / PSSR / 验收材料补交</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<p class="text-12px font-600">临时变更到期</p>
|
||||
<div class="flex justify-between">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="1"
|
||||
class="text-26px font-600 text-[#333]"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-12px">到期 ≤ 7 天 · 及时处置</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<div
|
||||
class="flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
@click="emit('go', 'tasks')"
|
||||
>
|
||||
<p class="text-12px font-600">超期未处置</p>
|
||||
<div class="flex justify-between">
|
||||
<CountTo
|
||||
:start-value="0"
|
||||
:end-value="1"
|
||||
class="text-26px font-600"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-12px">超期临时变更 · 需恢复 / 延期 / 转永久</p>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
<!-- 左侧 -->
|
||||
<NGi span="24 s:24 m:24 l:16">
|
||||
<!-- 我的变更进度 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">我的变更进度</p>
|
||||
<p class="text-11px text-#94a3b8">阶段进度与台账实时联动</p>
|
||||
</div>
|
||||
<NButton ghost size="small" class="text-xs" @click="emit('go', 'ledger')">
|
||||
全部台账
|
||||
<Icon icon="formkit:right" class="size-12px" />
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="row in myRows.slice(0, 3)" :key="row.id" @click="openRow(row)"
|
||||
class="cursor-pointer flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--hover-border': themeStore.themeColor,
|
||||
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.02)
|
||||
}"
|
||||
>
|
||||
<FlowRing :status="row.status" />
|
||||
<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">{{ row.id }}</span>
|
||||
<NTag class="text-[11px]" v-if="row.urgent" type="error" size="small">紧急</NTag>
|
||||
<NTag class="text-[11px]" v-if="row.level==='重要'" type="error" size="small">{{ row.level }}</NTag>
|
||||
<NTag class="text-[11px]" v-else size="small">{{ row.level }}</NTag>
|
||||
<NTag class="text-[11px]" size="small">{{ row.type }}</NTag>
|
||||
<NTag class="text-[11px]" v-if="row.duration==='临时'" type="warning" size="small">{{ row.duration }}</NTag>
|
||||
<NTag class="text-[11px]" v-else type="info" size="small">{{ row.duration }}</NTag>
|
||||
<span class="ml-auto text-[11px] text-slate-400">申请时间 {{ row.date }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white">{{ row.title }}</span>
|
||||
<span class="shrink-0 text-[11px] text-slate-400">当前状态</span>
|
||||
<NTag class="text-[11px]" v-if="row.status==='审批中' || row.status==='实施中' || row.status==='待PSSR' || row.status==='待验收'" type="warning" size="small">{{ row.status }}</NTag>
|
||||
<NTag class="text-[11px]" v-if="row.status==='待关闭'" type="error" size="small">{{ row.status }}</NTag>
|
||||
<NTag class="text-[11px]" v-if="row.status==='已批准'" type="success" size="small">已批准</NTag>
|
||||
<NTag class="text-[11px]" v-if="row.status==='已关闭'" type="info" size="small">已关闭</NTag>
|
||||
</div>
|
||||
<div class="mt-0.5 truncate text-[11px] text-slate-400">
|
||||
<template v-if="row.status === '已关闭'">3/3 阶段全部完成,流程闭环已归档</template>
|
||||
<template v-else>当前阶段:{{ BIG_STAGES[bigStageOf(row.status)] }}({{ bigStageOf(row.status) + 1 }}/3){{ row.c?.flow.find((n) => n.status === 'current') ? ` · 当前节点:${row.c.flow.find((n) => n.status === 'current')!.name}` : "" }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NButton dashed type="primary" size="small" class="text-xs" @click="emit('go', 'ledger')">
|
||||
还有2条,前往台账查看全部 →
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="mt-[16px]">
|
||||
<!-- 临时变更到期提醒情况 -->
|
||||
<NGi span="12 s:12 m:12 l:12">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="lets-icons:clock" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">临时变更到期提醒情况</p>
|
||||
<p class="text-11px text-#94a3b8">共 {{tempTotal}} 项</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="t in tempRemind" :key="t.label" class="rounded-lg border px-3 py-2">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: t.c }" />
|
||||
<span class="font-medium text-slate-700">{{ t.label }}</span>
|
||||
<span class="ml-auto font-semibold text-slate-600">{{ t.n }} 项</span>
|
||||
</div>
|
||||
<div class="mt-1 pl-4 text-[11px] text-slate-400">{{ t.note }}</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-400">提醒规则:到期前 7 天触发(可在「待我处理 · 我的申请」中调整)· 临期 / 超期项请按期办理延期、恢复或转永久</div>
|
||||
<NButton dashed type="primary" size="small" class="text-xs" @click="emit('go', 'tasks')">
|
||||
前往「待我处理」办理延期 / 恢复 / 转永久 →
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
<!-- 尚未关闭的变更所处状态 -->
|
||||
<NGi span="12 s:12 m:12 l:12">
|
||||
<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" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">尚未关闭的变更所处状态</p>
|
||||
<p class="text-11px text-#94a3b8">共 {{openTotal}} 项</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="s in openStatus" :key="s.name">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="min-w-0 flex-1 text-slate-700">{{ s.name }}</span>
|
||||
<span class="shrink-0 font-semibold text-slate-600">{{ s.n }} 项</span>
|
||||
</div>
|
||||
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-slate-100">
|
||||
<div class="h-full rounded-full" :style="{ width: (openTotal ? (s.n / openTotal) * 100 : 0) + '%', background: s.c }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-400">阶段口径:审批中(含会签)→ 已批准待投用 → 待验收 → 待关闭 · 培训 / PSSR 线下签字后拍照上传,并入资料归档</div>
|
||||
<NButton dashed type="primary" size="small" class="text-xs" @click="emit('go', 'ledger')">
|
||||
明细见变更台账 →
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NGi>
|
||||
<!-- 右侧 -->
|
||||
<NGi span="24 s:24 m:24 l:8">
|
||||
<!-- 被驳回信息处理 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">被驳回信息处理</p>
|
||||
<p class="text-11px text-#94a3b8">修改后可重新提交</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="r in REJECTED_ITEMS.slice(0, 1)"
|
||||
:key="r.id"
|
||||
class="border border-red-100 bg-red-50/50 px-3.5 py-2.5"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<NTag type="error" size="small" class="text-11px">已驳回</NTag>
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ r.id }}</span>
|
||||
<span class="ml-auto text-[11px] text-slate-400">{{ r.date }}</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-sm font-medium text-slate-800">{{ r.title }}</div>
|
||||
<div class="mt-0.5 line-clamp-1 text-[11px] leading-4 text-red-600/80">驳回原因:{{ r.reason }}</div>
|
||||
<div class="mt-0.5 text-[11px] text-slate-400">驳回人:{{ r.by }}</div>
|
||||
<NButton ghost type="error" size="small" class="text-xs mt-1.5" @click="emit('go', 'apply')">修改重报 →</NButton>
|
||||
</div>
|
||||
<NButton v-if="REJECTED_ITEMS.length > 1" dashed type="error" size="small" class="text-xs" @click="emit('go', 'tasks')">
|
||||
还有 {{ REJECTED_ITEMS.length - 1 }} 条驳回,前往我的任务处理 →
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
<!-- 资料归档待办 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="quill:folder-open" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">资料归档待办</p>
|
||||
<p class="text-11px text-#94a3b8">{{DOC_TODO.length}} 项待补交</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="d in DOC_TODO" :key="d.id" class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 px-3.5 py-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<NTag type="warning" size="small" class="text-11px">待归档</NTag>
|
||||
<span class="font-mono text-[11px] text-slate-400">{{ d.id }}</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-sm font-medium text-slate-800">{{ d.title }}</div>
|
||||
<div class="mt-0.5 line-clamp-1 text-[11px] leading-4 text-slate-400">缺项:{{ d.missing }}</div>
|
||||
<NButton type="primary" size="small" class="text-xs mt-1.5" @click="emit('go', 'tasks')">
|
||||
去补交(资料关闭页上传)→
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="text-[11px] leading-4 text-slate-400">上传纸质扫描件 / 照片即视为完成确认并联动归档清单;支持传错删除重传、阶段性保存</div>
|
||||
</div>
|
||||
</NCard>
|
||||
<!-- 资料管理 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
|
||||
<div class="flex items-center mb-3 gap-2">
|
||||
<Icon icon="quill:folder-open" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-13px font-700 text-#64749a">资料管理</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="d in DOC_LINKS" :key="d.name" @click="emit('go', 'knowledge')"
|
||||
class="cursor-pointer flex w-full items-center justify-between border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--hover-border': themeStore.themeColor,
|
||||
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.02)
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center flex-1 gap-3">
|
||||
<Icon icon="quill:folder-open" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-xs font-semibold text-slate-700">{{ d.name }}</p>
|
||||
<p class="truncate text-[11px] text-slate-400">{{ d.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon="formkit:right" class="size-14px text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import {
|
||||
CHANGES, DOC_TODO, LEVEL_COLOR, STATUS_COLOR, TEMP_WATCH, URGE_RECORDS,
|
||||
countOpenStatus, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||
} from "@/typings/model";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface ApplyForm {
|
||||
name: string;
|
||||
dept: string;
|
||||
mainChanges: string[];
|
||||
related: string;
|
||||
purposes: string[];
|
||||
effect: string;
|
||||
type: string;
|
||||
level: string;
|
||||
duration: string;
|
||||
urgent: boolean;
|
||||
date: string;
|
||||
restoreDate: string;
|
||||
}
|
||||
const form = ref<ApplyForm>({
|
||||
name: "", dept: "", mainChanges: [""], related: "", purposes: [], effect: "",
|
||||
type: "", level: "", duration: "", urgent: false, date: "", restoreDate: "",
|
||||
});
|
||||
const TABS = [
|
||||
{ id: "pre", name: "变更预识别", icon: 'lucide:sparkles' },
|
||||
{ id: "form", name: "变更申请表", icon: 'akar-icons:file' },
|
||||
{ id: "risk", name: "风险分析记录表", icon: 'proicons:alert-triangle' },
|
||||
{ id: "train", name: "变更培训内容", icon: 'lucide:graduation-cap' },
|
||||
{ id: "pssr", name: "PSSR 检查内容", icon: 'tabler:clipboard-check' },
|
||||
{ id: "accept", name: "验收评价", icon: 'material-symbols:check-circle-outline' },
|
||||
{ id: "close", name: "变更关闭确认表", icon: 'quill:folder-open' },
|
||||
];
|
||||
const tab = ref<"pre" | "form" | "risk" | "train" | "pssr" | "accept" | "close">("pre");
|
||||
const formGen = ref<"idle" | "generating" | "done">("idle");
|
||||
const confirmed = ref<Record<string, boolean>>({});
|
||||
const blockedTabs = ref<string[]>([]);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 发起变更 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div class="sticky top-0 z-20 rounded-t-xl border-b bg-[#e8f2fd]">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b px-4 py-2"
|
||||
:style="{borderColor: themeStore.themeColor}"
|
||||
>
|
||||
<span class="whitespace-nowrap text-sm font-medium text-slate-600">变更名称 <span class="text-red-500">*</span></span>
|
||||
<NInput v-model:value="form.name" maxlength="28" show-count placeholder="AI 自动总结(28 字以内),可手动修改" class="!w-420px" />
|
||||
<span class="ml-auto whitespace-nowrap text-sm text-slate-500">变更编号</span>
|
||||
<NInput value="MOC-2026-R01-0041" disabled class="!w-44" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1 overflow-x-auto px-3 pt-2">
|
||||
<div v-for="t in TABS" :key="t.id" @click="tab = t.id"
|
||||
class="flex items-center gap-1.5 whitespace-nowrap px-3 py-2 text-sm transition cursor-pointer border hover:bg-white/60 rounded-t-lg"
|
||||
:class="tab === t.id ? 'border-b-transparent border-[#ACC6E5] bg-white text-[var(--active-color)]':'border-transparent text-slate-500'"
|
||||
:style="{'--active-color': themeStore.themeColor}"
|
||||
>
|
||||
<Icon :icon="t.icon" class="size-14px" />
|
||||
{{ t.name }}
|
||||
<span v-if="t.id === 'form' && formGen === 'generating'"
|
||||
class="flex items-center gap-1 rounded-full border border-violet-300 bg-violet-50 px-1.5 py-0.5 text-[10px] text-violet-600">
|
||||
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||
AI 生成中
|
||||
</span>
|
||||
<Icon v-if="confirmed[t.id]" icon="material-symbols:check-circle-outline" class="size-14px text-emerald-500" />
|
||||
<span v-if="!confirmed[t.id] && blockedTabs.includes(t.id)" class="h-1.5 w-1.5 rounded-full bg-red-500" title="本页尚未确认完毕" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChangeStatus } from "../demo/model";
|
||||
const props = defineProps<{ status: ChangeStatus }>();
|
||||
const BIG_STAGES = ["申请审批", "实施与关闭", "变更关闭"];
|
||||
const bigStageOf = (s: ChangeStatus) => (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>
|
||||
@@ -0,0 +1,102 @@
|
||||
<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' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[16px]">
|
||||
<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-18px" />
|
||||
新建空白变更
|
||||
</NButton>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<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 - 190px);
|
||||
overflow-y: auto;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,667 @@
|
||||
<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;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<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;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<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;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<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;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user