本专业高风险静态页面完成

This commit is contained in:
2026-08-20 17:21:37 +08:00
parent b8571da035
commit 2ac8cf91e9
12 changed files with 1274 additions and 212 deletions
+3
View File
@@ -227,6 +227,9 @@ const local: App.I18n.Schema = {
home: '我的操作台',
home_index: '工作台首页',
home_initiatechange: '发起变更',
home_highrisk: '本专业高风险',
home_highriskdetail: '高风险场景详情',
home_mytask: '我的任务',
home_mydraft: '我的草稿',
home_pending: '待我处理',
home_process: '流程演示',
+3
View File
@@ -23,9 +23,12 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
changestatistics: () => import("@/views/changeStatistics/index.vue"),
docs_archives: () => import("@/views/docs/archives/index.vue"),
docs_query: () => import("@/views/docs/query/index.vue"),
home_highrisk: () => import("@/views/home/highRisk/index.vue"),
home_highriskdetail: () => import("@/views/home/highRiskDetail/index.vue"),
home_index: () => import("@/views/home/index/index.vue"),
home_initiatechange: () => import("@/views/home/initiateChange/index.vue"),
home_mydraft: () => import("@/views/home/myDraft/index.vue"),
home_mytask: () => import("@/views/home/myTask/index.vue"),
home_pending: () => import("@/views/home/pending/index.vue"),
home_process: () => import("@/views/home/process/index.vue"),
home_processdetail: () => import("@/views/home/processDetail/index.vue"),
+44 -5
View File
@@ -127,6 +127,29 @@ export const generatedRoutes: GeneratedRoute[] = [
order: 1
},
children: [
{
name: 'home_highrisk',
path: '/home/highrisk',
component: 'view.home_highrisk',
meta: {
title: 'home_highrisk',
i18nKey: 'route.home_highrisk',
icon: '',
order: 6
}
},
{
name: 'home_highriskdetail',
path: '/home/highriskdetail',
component: 'view.home_highriskdetail',
meta: {
title: 'home_highriskdetail',
i18nKey: 'route.home_highriskdetail',
activeMenu: 'home_highrisk',
hideInMenu: true,
icon: ''
}
},
{
name: 'home_index',
path: '/home/index',
@@ -134,7 +157,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: 'home_index',
i18nKey: 'route.home_index',
icon: ''
icon: '',
order: 1
}
},
{
@@ -144,7 +168,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: 'home_initiatechange',
i18nKey: 'route.home_initiatechange',
icon: ''
icon: '',
order: 2
}
},
{
@@ -154,7 +179,19 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: 'home_mydraft',
i18nKey: 'route.home_mydraft',
icon: ''
icon: '',
order: 3
}
},
{
name: 'home_mytask',
path: '/home/mytask',
component: 'view.home_mytask',
meta: {
title: 'home_mytask',
i18nKey: 'route.home_mytask',
icon: '',
order: 5
}
},
{
@@ -164,7 +201,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: 'home_pending',
i18nKey: 'route.home_pending',
icon: ''
icon: '',
order: 4
}
},
{
@@ -174,7 +212,8 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: 'home_process',
i18nKey: 'route.home_process',
icon: ''
icon: '',
order: 7
}
},
{
+3
View File
@@ -174,9 +174,12 @@ const routeMap: RouteMap = {
"docs_archives": "/docs/archives",
"docs_query": "/docs/query",
"home": "/home",
"home_highrisk": "/home/highrisk",
"home_highriskdetail": "/home/highriskdetail",
"home_index": "/home/index",
"home_initiatechange": "/home/initiatechange",
"home_mydraft": "/home/mydraft",
"home_mytask": "/home/mytask",
"home_pending": "/home/pending",
"home_process": "/home/process",
"home_processdetail": "/home/processdetail",
+2 -2
View File
@@ -22,7 +22,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const userInfo: Api.Auth.UserInfo = reactive({
userId: '',
userName: '',
username: '',
roles: [],
buttons: []
});
@@ -116,7 +116,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
window.$notification?.success({
title: '登录成功',
content: `欢迎回来,${userInfo.userName} !`,
content: `欢迎回来,${userInfo.username} !`,
duration: 4500
});
}
+2
View File
@@ -69,6 +69,7 @@ declare module 'vue' {
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs']
@@ -151,6 +152,7 @@ declare global {
const NScrollbar: typeof import('naive-ui')['NScrollbar']
const NSelect: typeof import('naive-ui')['NSelect']
const NSpace: typeof import('naive-ui')['NSpace']
const NSpin: typeof import('naive-ui')['NSpin']
const NSwitch: typeof import('naive-ui')['NSwitch']
const NTab: typeof import('naive-ui')['NTab']
const NTabs: typeof import('naive-ui')['NTabs']
+6
View File
@@ -28,9 +28,12 @@ declare module "@elegant-router/types" {
"docs_archives": "/docs/archives";
"docs_query": "/docs/query";
"home": "/home";
"home_highrisk": "/home/highrisk";
"home_highriskdetail": "/home/highriskdetail";
"home_index": "/home/index";
"home_initiatechange": "/home/initiatechange";
"home_mydraft": "/home/mydraft";
"home_mytask": "/home/mytask";
"home_pending": "/home/pending";
"home_process": "/home/process";
"home_processdetail": "/home/processdetail";
@@ -103,9 +106,12 @@ declare module "@elegant-router/types" {
| "changestatistics"
| "docs_archives"
| "docs_query"
| "home_highrisk"
| "home_highriskdetail"
| "home_index"
| "home_initiatechange"
| "home_mydraft"
| "home_mytask"
| "home_pending"
| "home_process"
| "home_processdetail"
+472
View File
@@ -0,0 +1,472 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
const keyword = ref<string>("");
const progressDrawer = ref<boolean>(false);
const currentTab = ref<string>("highRisk");
const range = ref<string>("近一个月");
const dept = ref<string>("全部部门");
const method = ref<string>("全部");
// 当前记录
const currentRecord = ref<any>(null);
const TABS = [
{ k: "highRisk", name: "高风险场景" },
{ k: "history", name: "历史分析检索" },
{ k: "tpl", name: "方法模板库" },
];
const RISK_RANGES = ["近一个月", "近三个月", "近半年", "全部"];
const departments = ref<any[]>([
{ id: 1, name: "全部部门" },
{ id: 2, name: "一车间" },
{ id: 3, name: "公用工程车间" },
{ id: 4, name: "供应部" },
{ id: 5, name: "三车间" },
]);
const list = ref<any[]>([]);
const highRiskList = [
{
cid: "MOC-2026-R01-0036", item: "反应飞温", scene: "温度上限提高至 82℃ 后飞温反应加速,联锁动作前安全裕量收窄",
method: "HAZOP", residual: "中", dept: "一车间", adate: "2026-08-02", owner: "王海燕",
title: '聚合反应温度控制上限调整(78℃→82℃)', status: '审批中',
info: 'MOC-2026-R01-0036 聚合反应温度控制上限调整(78℃→82℃)', count: 3,
hazopRows: [
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "温控回路失效或冷却水中断;82℃ 新上限更接近溶剂回流温度,超温溜温概率上升", cons: "反应溜温冲料,釜压骤升,物料喷溅造成灼烫与环境污染", L: 3, S: 4, safety: "DCS 温度高报 82℃、高高报及联锁 85℃(维持安全边界不变);紧急冷却有效", suggest: "投用前完成联锁测试并留存记录;投用首周每班复核温度趋势" },
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "未知杂质含量升高(0.05%→0.12%),热稳定性尚未定性,长期累积加速釜壁结垢", cons: "副反应加剧,可能引发二次分解放热,搅拌负荷异常", L: 2, S: 4, safety: "小试热稳定性筛查;安全阀泄放能力不受本次调整影响,仍然有效", suggest: "将杂质含量纳入日常分析计划(每周 1 次,连续 8 周)" },
{ node: "节点2 · DCS 温控与联锁", dev: "报警/联锁值设置不当", cause: "上限调整后报警值与联锁值未同步复核整定", cons: "报警提前量不足,操作响应时间被压缩", L: 2, S: 4, safety: "修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
],
},
{
cid: "MOC-2026-R01-0035", item: "动火与临时用电燃爆", scene: "电机更换动火与临时用电作业引发可燃气体燃爆",
method: "JSA", residual: "中", dept: "一车间", adate: "2026-07-30", owner: "王仪表",
title: '反应釜搅拌器电机更换(55kW→75kW 防爆电机)', status: '审批中',
info: 'MOC-2026-R01-0035 R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)', count: 3,
jsaRows: [
{ item: "动火作业燃爆", scene: "步骤2:旧电机拆除与新电机安装动火,可燃蒸气聚集遇火源燃爆", inherent: "高", existing: "作业票证办理;动火点可燃气体检测合格;消防器材到位", suggest: "动火点下方设接火盘与防火毯,专人监护并每 30 分钟复测", residual: "中" },
{ item: "临时用电触电", scene: "步骤1:临时电缆敷设与配电箱接线,绝缘破损导致触电", inherent: "中", existing: "临时用电票证;漏保校验合格", suggest: "电缆架空敷设并挂牌,每日开工前绝缘检查", residual: "低" },
{ item: "起重吊装伤害", scene: "步骤3:75kW 电机吊装就位,吊具失效或指挥失误导致起重伤害", inherent: "中", existing: "吊具检验合格;起重指挥持证", suggest: "吊装区设警戒线,禁止交叉作业", residual: "低" },
],
},
{
cid: null, changeTitle: "MOC-2026-R01-0048 R-101 温度联锁修改", item: "VOCs 直排超标", scene: "活性炭吸附饱和穿透,VOCs 直排大气超标",
method: "JSA", residual: "高", dept: "一车间", adate: "2026-07-31", owner: "王仪表",
title: '温度联锁修改', status: '审批中',
info: 'MOC-2026-R01-0048 R-101 温度联锁修改', count: 2,
jsaRows: [
{ item: "吸附饱和穿透", scene: "步骤3:活性炭吸附床运行后期,吸附饱和穿透导致 VOCs 直排大气", inherent: "高", existing: "出口 VOCs 在线监测报警;按周期更换活性炭", suggest: "增设穿透点压差监测与双床切换程序;更换周期由 6 个月缩短至 4 个月", residual: "中" },
{ item: "联锁参数误改", scene: "步骤3:DCS 参数修改与联锁测试,参数误改导致报警/联锁失效", inherent: "高", existing: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控", residual: "中" },
],
},
{
cid: "MOC-2026-R02-0041", item: "超压与窒息", scene: "氮气减压阀组失效导致下游设备超压、作业人员窒息",
method: "检查表", residual: "低", dept: "公用工程车间", adate: "2026-07-29", owner: "周设备",
title: '公用工程氮气减压阀组改造', status: '审批中',
info: 'MOC-2026-R02-0041 公用工程氮气减压阀组改造', count: 2,
jsaRows: [
{ item: "下游设备超压", scene: "减压阀失效全开,高压氮气直入低压管网导致下游设备超压", inherent: "高", existing: "下游设安全阀;管网压力高报", suggest: "减压阀双阀串联并定期校验;安全阀起跳压力复核", residual: "低" },
{ item: "人员窒息", scene: "阀组间通风不良,氮气泄漏积聚导致氧含量不足", inherent: "中", existing: "阀组间强制通风;进入前测氧", suggest: "增设固定式氧含量报警仪并联动风机", residual: "低" },
],
},
{
cid: "MOC-2026-R01-0039", item: "超压保护裕量降低", scene: "安全阀起跳压力临时上调期间,V-102 超压保护裕量降低",
method: "AI 预分析", residual: "中", dept: "一车间", adate: "2026-07-15", owner: "王海燕",
title: '紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', status: '审批中',
info: 'MOC-2026-R01-0039 紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', count: 2,
jsaRows: [
{ item: "超压保护裕量降低", scene: "起跳压力由 1.0MPa 临时上调至 1.15MPaV-102 超压工况下保护动作延迟", inherent: "高", existing: "临时期限 30 天;操作压力监控加强至每 2 小时记录", suggest: "恢复前完成新安全阀校验更换;临时期间压力联锁值同步下调验证", residual: "中" },
{ item: "紧急变更手续补办遗漏", scene: "紧急变更先行实施,后补手续存在资料缺项风险", inherent: "中", existing: "紧急变更 48 小时内补办审批", suggest: "资料归档清单逐项核对,安全部门复核", residual: "低" },
],
},
{
cid: "MOC-2026-R03-0040", item: "原料杂质副反应", scene: "新供应商环己烷杂质谱变化引发副反应,影响产品色相与收率",
method: "HAZOP", residual: "中", dept: "供应部", adate: "2026-07-18", owner: "陈质量",
title: '原料环己烷供应商变更(新增 B 供应商)', status: '审批中',
info: 'MOC-2026-R03-0040 原料环己烷供应商变更(新增 B 供应商)', count: 2,
hazopRows: [
{ node: "节点1 · 原料接收与储存", dev: "杂质含量高", cause: "B 供应商环己烷杂质谱(苯/硫)高于原供应商,进厂检验项目未覆盖", cons: "副反应加剧,产品色相超标、收率下降,结垢加速", L: 3, S: 3, safety: "进厂检验按原指标执行;供应商 COA 随货同行", suggest: "检验项目增加杂质谱分析;首批三批加严检验" },
{ node: "节点2 · 聚合反应", dev: "副反应加剧", cause: "微量苯参与链转移,分子量分布变宽", cons: "产品性能波动,下游加工投诉", L: 2, S: 3, safety: "聚合配方留有调节余量", suggest: "建立 B 供应商原料的配方微调作业指导" },
],
},
{
cid: "MOC-2026-R03-0042", item: "回路切换失误", scene: "自动化改造调试期间控制回路切换失误,造成装置非计划停车",
method: "JSA", residual: "中", dept: "三车间", adate: "2026-07-31", owner: "王仪表",
title: '三车间自动化和安全隐患整改项目变更', status: '审批中',
info: 'MOC-2026-R03-0042 三车间自动化和安全隐患整改项目变更', count: 2,
jsaRows: [
{ item: "回路切换失误", scene: "步骤4:新旧控制系统切换,回路误切导致装置非计划停车", inherent: "高", existing: "切换方案审批;切换前模拟演练", suggest: "切换双人复核逐项签字;保留一键回切手段 72 小时", residual: "中" },
{ item: "静电损伤卡件", scene: "步骤2:卡件插拔作业静电防护不到位,DCS 卡件损坏", inherent: "中", existing: "佩戴防静电手环", suggest: "卡件备件现场备妥,作业区铺设防静电垫", residual: "低" },
],
},
];
const historyList = [
{ id: "HAZOP-2025-018", change: "MOC-2025-R01-0023 常压炉燃料气切断阀改造", method: "HAZOP", unit: "01-常减压装置", scope: "燃料气系统节点", rows: 12, date: "2025-10-20" },
{ id: "HAZOP-2024-006", change: "MOC-2024-R01-0003 减一线换热器 E-108 芯子更换", method: "HAZOP", unit: "01-常减压装置", scope: "换热系统节点", rows: 8, date: "2024-03-15" },
{ id: "JSA-2025-041", change: "MOC-2025-R02-0014 再生器旋风分离器更换", method: "JSA", unit: "02-催化裂化装置", scope: "受限空间作业", rows: 6, date: "2025-05-08" },
{ id: "JSA-2026-012", change: "MOC-2026-R01-0035 搅拌电机更换", method: "JSA", unit: "01-常减压装置", scope: "检维修作业", rows: 5, date: "2026-07-29" },
{ id: "SCL-2025-033", change: "MOC-2025-R02-0021 气压机出口压力联锁逻辑修改", method: "检查表", unit: "02-催化裂化装置", scope: "联锁系统", rows: 14, date: "2025-09-18" },
{ id: "AI-2026-008", change: "MOC-2026-R03-0008 聚合釜搅拌器密封形式变更", method: "AI 预分析", unit: "03-聚合装置", scope: "—", rows: 4, date: "2026-05-06" },
];
const templateList = [
{ name: "HAZOP 节点划分模板", desc: "含偏差引导词库(过高/过低/无/反向等 18 个)与节点划分示例", tag: "HAZOP" },
{ name: "JSA 作业步骤分析模板", desc: "作业步骤分解 → 危害识别 → 控制措施三段式,含检维修示例", tag: "JSA" },
{ name: "安全检查表(SCL)模板库", desc: "按设备类型 28 套(机泵 / 换热器 / 塔器 / 储罐 / 电气 / 仪表)", tag: "检查表" },
{ name: "风险矩阵 5×5 分级定义", desc: "可能性 × 严重性分级标准(公司 Q/SH 0600),AI 评级引用此定义", tag: "通用" },
];
const isHazop = computed(() => !!currentRecord.value?.hazopRows);
const th = "border-b bg-slate-50 px-3 py-2 text-left text-[11px] font-semibold text-slate-500";
const td = "border-b px-3 py-2 align-top text-xs leading-relaxed text-slate-700";
const hazopNodes = computed(() => {
const scene = currentRecord.value;
if (!scene?.hazopRows) return [] as { node: string; dev: string; n: number }[];
const nodes = Array.from(new Map(scene.hazopRows.map((r:any) => [`${r.node}|${r.dev}`, { node: r.node, dev: r.dev, n: 0 }])).values());
scene.hazopRows.forEach((r:any) => { const k:any = nodes.find((n:any) => n.node === r.node && n.dev === r.dev); if (k) k.n += 1; });
return nodes;
});
const sel = ref(0);
const selRows = computed(() => {
const scene = currentRecord.value;
if (!scene?.hazopRows) return [] as any;
const n:any = hazopNodes.value[sel.value];
return scene.hazopRows.filter((r:any) => r.node === n?.node && r.dev === n?.dev);
});
// HAZOP 风险矩阵:RR = L × S
const rrTag = (L: number, S: number) => {
const v = L * S;
return v >= 12 ? { label: `较大 (${v})`, cls: "error" }
: v >= 8 ? { label: `一般 (${v})`, cls: "warning" }
: { label: `低 (${v})`, cls: "success" };
};
// 切换tab
const tabChange = (k: string) => {
list.value = []
currentTab.value = k;
loading.value = true;
if(k === 'highRisk'){
setTimeout(() => {
list.value = highRiskList;
loading.value = false;
}, 1000);
}
if(k === 'history'){
setTimeout(() => {
list.value = historyList;
loading.value = false;
}, 1000);
}
if(k === 'tpl'){
setTimeout(() => {
list.value = templateList;
loading.value = false;
}, 1000);
}
}
// 高风险场景查看变更
const openSceneChange = (record: any) => {
routerPushByKey('home_highriskdetail', { query: { params: JSON.stringify(record) } })
};
// 查看分析记录
const viewAnalysis = (record: any) => {
currentRecord.value = record;
progressDrawer.value = true;
};
// 查看历史分析检索
const viewHistory = (id: string) => {
window.$message?.info(`演示:在线查看 ${id} 完整分析记录`)
};
// 复用到新变更
const copyHistory = (id: string) => {
window.$message?.success(`演示:${id} 已复用到新变更草稿,分析行已带入待确认`)
}
// 预览模板
const previewTemplate = (name: string) => {
window.$message?.info(`演示:在线预览「${name}`)
}
// 下载模板
const downloadTemplate = (name: string) => {
window.$message?.success(`演示:下载「${name}」(Word`)
}
onMounted(() => {
tabChange('highRisk');
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 本专业高风险 -->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<div v-for="t in TABS" :key="t.k" @click="tabChange(t.k)"
class="flex items-center gap-1.5 px-3 py-1 text-xs cursor-pointer select-none rounded-[20px] border"
:style="{ '--theme-color': themeStore.themeColor}"
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white border-[var(--theme-color)]' : 'text-slate-600 hover:bg-slate-100'"
>
{{ t.name }}
</div>
</div>
<!-- 高风险场景 -->
<div v-if="currentTab === 'highRisk'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">变更新增高风险场景</p>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<span class="mr-1 text-xs text-slate-400">时间</span>
<NButton
size="tiny"
round
:type="range === r ? 'primary' : 'default'"
v-for="r in RISK_RANGES"
:key="r"
@click="range = r"
>
{{ r }}
</NButton>
<span class="ml-3 mr-1 text-xs text-slate-400">部门</span>
<NButton
size="tiny"
round
v-for="d in departments"
:key="d.id"
:type="dept === d.name ? 'primary' : 'default'"
@click="dept = d.name"
>
{{ d.name }}
</NButton>
<span class="ml-auto self-center text-[11px] text-slate-400">{{ list.length }} 个高风险场景</span>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div v-for="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{ s.method }}</NTag>
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
<NTag type="error" size="small">固有风险 · </NTag>
<NTag v-if="s.residual==='高'" type="error" size="small">剩余风险 · {{ s.residual }}</NTag>
<NTag v-if="s.residual==='中'" type="warning" size="small">剩余风险 · {{ s.residual }}</NTag>
<NTag v-if="s.residual==='低'" type="info" size="small">剩余风险 · {{ s.residual }}</NTag>
</div>
<div class="mt-0.5 text-xs text-slate-600">{{ s.scene }}</div>
<div class="mt-0.5 text-xs text-slate-400">
变更{{ s.info }} · {{ s.dept }} · 分析日期 {{ s.adate }} · 责任{{ s.owner }} · 分析记录 {{ s.count }}
</div>
</div>
<NButton ghost size="small" class="text-xs" @click="openSceneChange(s)">查看变更</NButton>
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">当前筛选条件下无新增高风险场景</div>
</div>
</div>
</div>
<!-- 历史分析检索 -->
<div v-if="currentTab === 'history'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="mage:book" class="size-18px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">历史分析记录检索企业风险知识资产</p>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NInput v-model:value="keyword" size="small" class="text-xs !w-200px" placeholder="按单号 / 装置 / 关键词检索…">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
</template>
</NInput>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NButton
size="tiny"
round
v-for="m in ['全部', 'HAZOP', 'JSA', '检查表', 'AI 预分析']"
:key="m"
:type="method === m ? 'primary' : 'default'"
@click="method = m"
>
{{ m }}
</NButton>
<span class="ml-auto self-center text-[11px] text-slate-400">{{ list.length }} 条记录</span>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div v-for="h in list" :key="h.id" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-2.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-[11px] text-slate-400">{{ h.id }}</span>
<NTag type="info" size="small">{{ h.method }}</NTag>
<span class="text-sm text-slate-800">{{ h.change }}</span>
</div>
<div class="mt-0.5 text-xs text-slate-500">{{ h.unit }} · 范围{{ h.scope }} · {{ h.rows }} 行分析记录 · {{ h.date }}</div>
</div>
<NButton text type="primary" @click="viewHistory(h.id)">
<Icon icon="lucide:eye" class="size-16px" />
</NButton>
<NButton ghost size="small" class="text-xs" @click="copyHistory(h.id)">复用到新变更</NButton>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的分析记录</div>
</div>
</div>
</div>
<!-- 方法模板库 -->
<div v-if="currentTab === 'tpl'" class="border border-slate-200" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="mage:book" class="size-18px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">方法模板库AI 风险预分析的知识来源之一</p>
</div>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div class="grid gap-3 grid-cols-2">
<div v-for="t in list" :key="t.name" class="rounded-lg border p-4">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-slate-800">{{ t.name }}</span>
<NTag size="small" type="info">{{ t.tag }}</NTag>
</div>
<p class="mt-1.5 text-xs leading-relaxed text-slate-500">{{ t.desc }}</p>
<div class="mt-3 flex gap-2">
<NButton ghost size="small" class="text-xs" @click="previewTemplate(t.name)">查看</NButton>
<NButton ghost size="small" class="text-xs" @click="downloadTemplate(t.name)">
<Icon icon="material-symbols:download" class="size-14px" />下载
</NButton>
</div>
</div>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的方法模板库</div>
</div>
</div>
</div>
<!-- 进度弹框 -->
<NDrawer v-model:show="progressDrawer" width="85%" placement="right">
<NDrawerContent closable>
<template #header>
<span class="flex flex-wrap items-center gap-2 text-base">
风险分析记录表
<NTag size="small" type="info">{{ currentRecord.method }}</NTag>
<NTag size="small" type="error">含固有风险 · 场景</NTag>
</span>
</template>
<div>
<div class="mb-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-slate-500">
<span>变更<span class="font-medium text-slate-700">{{ currentRecord.info }}</span></span>
<span>分析日期{{ currentRecord.adate }}</span>
<span>责任{{ currentRecord.owner }}</span>
<span class="ml-auto text-slate-400">分析编制与修改在变更上下文内完成此处只读</span>
</div>
<div v-if="!isHazop" class="overflow-x-auto rounded-lg border border-b-0">
<table class="w-full min-w-[860px] border-collapse">
<thead>
<tr>
<th :class="th" class="w-[130px]">风险项</th>
<th :class="th">场景描述</th>
<th :class="th" class="w-[76px] text-center">固有风险</th>
<th :class="th">现有措施</th>
<th :class="th">建议措施</th>
<th :class="th" class="w-[76px] text-center">剩余风险</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in currentRecord.jsaRows" :key="i" :class="r.item === currentRecord.item && r.inherent === '高' ? 'bg-red-50/40' : ''">
<td :class="td" class="font-medium text-slate-800">{{ r.item }}</td>
<td :class="td">{{ r.scene }}</td>
<td :class="td" class="text-center">
<NTag size="small" type="error" v-if="r.inherent==='高'">{{ r.inherent }}</NTag>
<NTag size="small" type="warning" v-if="r.inherent==='中'">{{ r.inherent }}</NTag>
<NTag size="small" type="info" v-if="r.inherent==='低'">{{ r.inherent }}</NTag>
</td>
<td :class="td">{{ r.existing }}</td>
<td :class="td">{{ r.suggest }}</td>
<td :class="td" class="text-center">
<NTag size="small" type="error" v-if="r.residual==='高'">{{ r.residual }}</NTag>
<NTag size="small" type="warning" v-if="r.residual==='中'">{{ r.residual }}</NTag>
<NTag size="small" type="info" v-if="r.residual==='低'">{{ r.residual }}</NTag>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="flex gap-3">
<!-- 节点 · 偏离导航 -->
<div class="w-48 shrink-0 space-y-1.5">
<div v-for="(n, i) in hazopNodes" :key="i" @click="sel = i"
class="w-full rounded-lg border px-3 py-2 text-left text-xs cursor-pointer"
:class="sel === i ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'hover:border-[var(--theme-color)]'"
:style="{'--theme-color': themeStore.themeColor}"
>
<span class="block truncate font-medium">{{ n.dev }}</span>
<span class="mt-0.5 flex items-center justify-between text-[11px]" :class="sel === i ? 'text-white/80' : 'text-slate-400'">
<span class="truncate">{{ n.node.replace(/^节点\d+ · /, '') }}</span>
<NTag size="tiny" round class="ml-1" :type="sel === i ? 'default' : 'success'">{{ n.n }} </NTag>
</span>
</div>
<p class="px-1 pt-1 text-[11px] leading-relaxed text-slate-400">L / S 取值 15RR 5×5 风险矩阵实时重算</p>
</div>
<!-- 偏差分析表 -->
<div class="min-w-0 flex-1 overflow-x-auto rounded-lg border border-b-0">
<table class="w-full min-w-[760px] border-collapse">
<thead>
<tr>
<th :class="th">原因</th>
<th :class="th">后果</th>
<th :class="th" class="w-10 !text-center">L</th>
<th :class="th" class="w-10 !text-center">S</th>
<th :class="th" class="w-[92px] !text-center">RR</th>
<th :class="th">安全措施</th>
<th :class="th">建议措施</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in selRows" :key="i">
<td :class="td">{{ r.cause }}</td>
<td :class="td">{{ r.cons }}</td>
<td :class="td" class="text-center">{{ r.L }}</td>
<td :class="td" class="text-center">{{ r.S }}</td>
<td :class="td" class="text-center">
<NTag size="small" :type="rrTag(r.L, r.S).cls">{{ rrTag(r.L, r.S).label }}</NTag>
</td>
<td :class="td">{{ r.safety }}</td>
<td :class="td">{{ r.suggest }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<NButton ghost @click="progressDrawer = false"><Icon icon="material-symbols:download" class="size-16px" />导出</NButton>
<NButton type="primary" @click="progressDrawer = false">关闭</NButton>
</div>
</template>
</NDrawerContent>
</NDrawer>
</NCard>
</NSpin>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 303px);
overflow-y: auto;
}
</style>
+86
View File
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { useRoute } from 'vue-router';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const route = useRoute();
const loading = ref<boolean>(false);
// 当前高风险详情
const currentInfo = ref<any>(null);
// 是否开启 AI 标注
const aiOn = ref<boolean>(false);
const editMode = ref<boolean>(false);
// 返回
const back = () => {
routerPushByKey('home_highrisk')
};
onMounted(() => {
if(route.query && route.query.params){
currentInfo.value = JSON.parse(route.query.params as string);
}
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 本专业高风险 -->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton ghost type="primary" @click="back">
<Icon icon="ep:back" class="size-16px mr-1" />返回
</NButton>
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
<NTag size="small" type="info" class="font-mono text-xs">编号{{ currentInfo?.cid }}</NTag>
<NButton
v-if="currentInfo?.status === '审批中'"
ghost
size="small"
class="text-xs"
:type="aiOn ? 'success' : 'default'"
@click="aiOn = !aiOn"
>
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 标注{{ aiOn ? ' · 已开启' : ' · 已关闭' }}
</NButton>
</div>
<div class="space-y-3 scroll">
<!-- 直接修改红线留痕示例 -->
<div v-if="editMode" class="rounded-lg border border-dashed border-red-300 bg-white p-3 text-sm">
<div class="mb-1 text-xs font-semibold text-red-500">红线留痕示例</div>
<p>
变更等级<del class="text-red-500 line-through">一般</del>
<ins class="bg-emerald-50 px-1 text-emerald-600 no-underline">重要</ins>
<span class="ml-2 text-xs text-slate-400">王海燕 · 2026-07-30 14:22 修改已通知申请人确认</span>
</p>
</div>
<div class="flex items-center justify-between">
<p
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
:style="{'--theme-color': themeStore.themeColor}"
>变更申请表</p>
<NButton ghost size="small" class="text-xs"><Icon icon="material-symbols:download" class="size-16px" />导出变更申请表</NButton>
</div>
</div>
</NCard>
</NSpin>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 303px);
overflow-y: auto;
}
</style>
+631
View File
@@ -0,0 +1,631 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
interface MyChange {
id: string;title: string;status: string;detail: string;applyTime: string;updateTime: string;
level: "重要" | "一般"; dur: "永久" | "临时"; urgent?: boolean;deadline?: string;
stage: string; meta: string; left?: number; extended?: boolean;
node?: string; members?: { name: string; done: boolean }[];
}
const currentRow = ref<any>(null);
const progressDrawer = ref<boolean>(false);
const revokeModal = ref<boolean>(false);
const revokeSaving = ref<boolean>(false);
const revokeReason = ref<string>("");
const disposalModal = ref<boolean>(false);
const stages = ["提交申请", "审批", "实施与投用前确认", "投用", "验收", "关闭"];
const curIdx = computed(() => ({ 审批中: 1, 已批准: 2, 实施中: 2, 待PSSR: 2, 待验收: 4, 待关闭: 5, 已关闭: 6 } as Record<string, number>)[currentRow.value?.status ?? ""] ?? 1);
const closed = computed(() => currentRow.value?.status === "已关闭");
const timeOf = (i: number) => (i === 0 ? currentRow.value?.applyTime : i === 3 ? currentRow.value?.actualUse : i === 5 ? currentRow.value?.closeTime : undefined);
const currentTab = ref<string>("all");
const visibleTabs = ref<{ k: string; name: string; count: number }[]>([
{ k: "all", name: "全部", count: 10 },
{ k: "approval", name: "待我审批", count: 1 },
]);
interface ListType {
id: string;
title: string;
status: string;
list: any[];
}
const list = ref<ListType[]>([
{
id: "MOC-2026-R01-001",
title: '待我审批',
status: 'pending',
list: [
{
id: "MOC-2026-R01-0001",
title:'R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)',
level: "重要",
dur: "永久",
stage: "pending",
status: "审批中",
node: "专业会签",
overdue: false,//是否超期
near: false,//是否临期
extended: false,//是否延期
extendedCount: 0,//延期次数
day: 0,
urgent: false,
members: [{ name: "王仪表", done: true },{ name: "周设备", done: false },{ name: "王海燕", done: false }],
applyTime: "2026-07-28 09:12",
updateTime: "2026-07-30 14:20",
meta: "计划投用 2026-08-10",
detail: "专业会签中(周设备、王海燕待签)"
},
{
id: "MOC-2026-R01-0002",
title:'操作规程修订(夏季工况)',
level: "一般",
dur: "临时",
stage: "pending",
status: "审批中",
node: "部门审核",
overdue: true,
near: false,
extended: false,
extendedCount: 0,
day: 2,
urgent: false,
members: [{ name: "李主任", done: false }],
applyTime: "2026-06-20 11:26",
updateTime: "2026-06-21 08:40",
meta: "计划投用 2026-07-01",
deadline: "2026-08-01",
detail: "部门审核(李主任)"
},
]
},
{
id: "MOC-2026-R01-002",
title: '我的申请',
status: 'approved',
list: [
{
id: "MOC-2026-R02-0001",
title:'循环水加药泵冲程临时调整',
level: "一般",
dur: "临时",
stage: "approved",
status: "已批准",
node: "投用准备",
overdue: false,//是否超期
near: false,//是否临期
extended: false,//是否延期
extendedCount: 0,//延期次数
day: 0,
urgent: false,
members: [{ name: "张工艺", done: false }],
applyTime: "2026-07-26 10:15",
updateTime: "2026-07-30 16:20",
meta: "批准 2026-07-30 · 待实施",
deadline: "2026-08-20",
detail: "培训 / PSSR 资料待上传(线下签字后拍照上传)",
},
{
id: "MOC-2026-R02-0002",
title:'氮气吹扫流程临时调整',
level: "一般",
dur: "临时",
stage: "approved",
status: "已批准",
node: "实施执行",
overdue: true,
near: false,
extended: false,
extendedCount: 0,
day: 3,
urgent: false,
members: [],
applyTime: "2026-07-10 10:05",
updateTime: "2026-07-31 08:10",
meta: "批准 2026-07-12 · 待实施",
deadline: "2026-07-31",
detail: "已到期未办理恢复 / 延期 / 转永久",
},
{
id: "MOC-2026-R02-0003",
title:'P-204 出口管线伴热改造',
level: "一般",
dur: "永久",
stage: "approved",
status: "已批准",
node: "实施执行",
overdue: false,
near: false,
extended: false,
extendedCount: 0,
day: 0,
urgent: false,
members: [],
applyTime: "2026-07-15 09:40",
updateTime: "2026-07-20 11:00",
meta: "审批通过 2026-07-20 · 计划投用 2026-07-30",
detail: "因装置检修窗口调整未实施",
},
{
id: "MOC-2026-R02-0004",
title:'循环水旁滤器临时绕流运行',
level: "一般",
dur: "临时",
stage: "inuse",
status: "待验收",
node: "变更验收",
overdue: false,
near: true,
extended: false,
extendedCount: 0,
day: 6,
urgent: false,
members: [],
applyTime: "2026-07-28 14:10",
updateTime: "2026-08-02 08:30",
deadline: "2026-08-09",
meta: "投用 2026-08-02 · 到期 2026-08-09",
detail: "验收材料准备中",
},
{
id: "MOC-2026-R02-0005",
title:'空压站干燥器再生周期调整',
level: "一般",
dur: "永久",
stage: "inuse",
status: "待验收",
node: "运行验证",
overdue: false,
near: false,
extended: false,
extendedCount: 0,
day: 0,
urgent: false,
members: [],
applyTime: "2026-07-18 10:25",
updateTime: "2026-07-25 09:12",
meta: "投用 2026-07-25 · 计划验收 2026-08-24",
detail: "运行数据收集中(验收周期 30 天)",
},
{
id: "MOC-2026-R02-0006",
title:'V-102 安全阀起跳压力临时调整(紧急补办)',
level: "一般",
dur: "临时",
stage: "closing",
status: "待关闭",
node: "关闭确认",
overdue: true,
near: false,
extended: false,
extendedCount: 0,
day: 4,
urgent: true,
members: [{ name: "钱峰", done: false }],
applyTime: "2026-07-15 19:40",
updateTime: "2026-07-27 09:02",
deadline: "2026-07-27",
meta: "已投用 · 到期 2026-07-27",
detail: "待关闭确认(资料更新核查中)",
},
]
},
{
id: "MOC-2026-R01-003",
title: '驳回处理',
status: 'reject',
list: [
{
id: "MOC-2026-R03-0001",
title: 'P-301 机封冲洗方案调整',
stage: "reject",
overdue: false,
rejectReason: '变更前后对比描述不完整,缺少冲洗流量核算数据(李主任 · 车间主任)',
updateTime: "2026-07-29",
},
{
id: "MOC-2026-R03-0002",
title: '活性炭更换周期延长(3→6 个月)',
stage: "reject",
overdue: true,
rejectReason: '风险分析未覆盖吸附饱和穿透情景,请补充 JSA 分析(王海燕 · 安全工程师)',
updateTime: "2026-07-25",
},
]
},
]);
interface SubOrder {
id: string; // 子单号:原单号-Y1(延期)/ -Z1(转永久)
parentId: string; // 原变更单号
parentTitle: string;
kind: "extend" | "permanent";
applicant: string;
reason: string;
newDeadline?: string; // 延期子单:申请延期至
status: "审批中" | "已通过" | "已驳回";
submitTime: string;
steps: { role: string; name: string; done: boolean }[]; // 会签:全员同意即通过
rejectNote?: string;
}
const SUB_ORDERS: SubOrder[] = [];
const subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
const convPassed = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status === "已通过"));
const convActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status !== "已驳回"));
const extActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "extend" && s.status === "审批中"));
const opts = computed<Opt[]>(() => {
const list: Opt[] = [
{ act: "restore", name: "申请恢复", desc: "投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档" },
];
if (currentRow.value?.dur === "临时" && !convPassed.value) {
list.push(
{ act: "extend", name: currentRow.value?.extended ? "申请延期(已延期 1 次)" : "申请延期", desc: "临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次", disabled: !!currentRow.value?.extended || extActive.value, note: extActive.value ? "延期子单审批中" : currentRow.value?.extended ? "本变更已延期 1 次,不可再次延期" : "" },
{ act: "permanent", name: "申请转为永久", desc: "临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭", disabled: convActive.value, note: convActive.value ? "转永久子单审批中 / 已通过" : "" },
);
}
list.push(
{ act: "unimpl", name: "未实施情况说明", desc: "审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程" },
{ act: "unuse", name: "未投用情况说明", desc: "变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程" },
);
return list;
});
// 打开详情
const openDetail = (c: MyChange,type:string = '') => {
if(type==='reject'){
window.$message?.info("演示:打开详情");
return;
}
};
// 打开进度
const openProgress = (c: MyChange,type:string = '') => {
currentRow.value = c;
if(type==='reject'){
window.$message?.info("该任务暂无关联变更进度");
return;
}
progressDrawer.value = true;
};
// 打开撤回
const openWithdraw = (c: MyChange) => {
revokeReason.value = "";
currentRow.value = c;
revokeModal.value = true;
};
// 打开处置申请、修改重报
const openDisposal = (c: MyChange,type:string = '') => {
currentRow.value = c;
if(type==='reject'){
window.$message?.info("该任务暂无处置申请");
return;
}
disposalModal.value = true;
};
// 打开资料关闭、关闭变更
const openClosure = (c: MyChange,type:string = '') => {
if(type==='reject'){
window.$message?.info("关闭该变更(驳回后不再推进,流程终止并归档备查)");
return;
}
};
// 撤回取消
const revokeCancel = () => {
revokeModal.value = false;
};
// 撤回确认
const revokeSave = () => {
if (!revokeReason.value) {
window.$message?.warning("请填写撤回理由");
return;
}
revokeSaving.value = true;
setTimeout(() => {
revokeSaving.value = false;
revokeModal.value = false;
const pendingGroup = list.value.find((item: any) => item.status === 'pending');
if (pendingGroup) {
// 过滤掉匹配 targetId 的项
pendingGroup.list = pendingGroup.list.filter((item:any) => item.id !== currentRow.value?.id);
}
window.$message?.success(`已撤回 ${currentRow.value?.id},申请退回草稿箱,审批记录保留可查,修改后可重新提交`);
}, 1000);
};
</script>
<template>
<NSpace vertical :size="16">
<!-- 待我处理 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="h-100% flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<div v-for="t in visibleTabs" :key="t.k" @click="currentTab = t.k"
class="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer select-none"
:style="{
borderRadius: themeStore.themeRadius + 'px',
'--theme-color': themeStore.themeColor,
}"
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
{{ t.name }}
<!-- 驳回处理/资料上传/临时变更超期未处理/变更超期未关闭 -->
<NTag v-if="t.k==='reject' || t.k==='upload' || t.k==='tempOverdue' || t.k==='closeOverdue'" size="tiny" round :type="currentTab === t.k ?'default':'error'">{{ t.count }}</NTag>
<NTag v-else size="tiny" round :type="currentTab === t.k ?'default':'primary'">{{ t.count }}</NTag>
</div>
</div>
<div class="space-y-2 scroll">
<div class="flex flex-col gap-3">
<div
class="border border-slate-200 p-4"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
v-for="item in list"
:key="item.id"
>
<div class="flex items-center gap-2">
<!-- 待我审批 -->
<Icon v-if="item.status==='pending'" icon="ix:success" class="size-16px" :style="{ color: themeStore.otherColor.success }" />
<!-- 我的申请 -->
<Icon v-if="item.status==='approved'" icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
<!-- 驳回处理 -->
<Icon v-if="item.status==='reject'" icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.otherColor.error }" />
<h3 class="text-sm font-bold text-slate-800">{{item.title}}</h3>
<!-- 待我审批 -->
<p v-if="item.status==='pending'" class="text-11px text-slate-400"> {{ item.list.length }} · 流程当前节点轮到您处理 · 点击进入审批</p>
<!-- 我的申请 -->
<p v-if="item.status==='approved'" class="text-11px text-slate-400"> {{item.list.length}} </p>
<!-- 驳回处理 -->
<p v-if="item.status==='reject'" class="text-11px text-slate-400">{{item.list.length}} 项待处理</p>
</div>
<div v-if="item.list && item.list.length" class="flex flex-col gap-3 mt-3">
<div v-for="c in item.list" :key="c.id"
class="flex flex-wrap items-center gap-3 border px-4 py-3"
:class="c.overdue ? 'border-red-200 bg-[#FFF2F0]' : c.near ? 'border-amber-200 bg-amber-50/40' : ''"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<!-- 左区 -->
<!-- 待我审批 -->
<div v-if="item.status==='pending' || item.status==='approved'" class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
<NTag v-if="c.level==='重要'" size="small" type="error">{{c.level}}</NTag>
<NTag v-if="c.level==='一般'" size="small" type="primary">{{c.level}}</NTag>
<NTag v-if="c.dur==='临时'" size="small" type="warning">{{c.dur}}</NTag>
<NTag v-if="c.dur==='永久'" size="small" type="primary">{{c.dur}}</NTag>
<NTag v-if="c.dur==='临时转永久'" size="small">{{c.dur}}</NTag>
<NTag v-if="c.urgent" size="small" type="error">紧急</NTag>
<NTag v-if="c.overdue" size="small" type="error">已超期 {{ c.day }} </NTag>
<NTag v-if="c.near" size="small" type="warning">临期 · {{ c.day }} </NTag>
<NTag v-if="c.extended" size="small">已延期 {{ c.extendedCount }} </NTag>
<NTag v-if="c.status==='审批中'" size="small" type="warning">{{c.status}}</NTag>
<NTag v-if="c.status==='已批准'" size="small" type="primary">{{c.status}}</NTag>
<NTag v-if="c.status==='待验收'" size="small" type="success">{{c.status}}</NTag>
<NTag v-if="c.status==='待关闭'" size="small" type="error">{{c.status}}</NTag>
</div>
<div class="mt-1 flex flex-wrap items-baseline gap-x-2">
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
<span v-if="c.applyTime" class="text-[11px] text-slate-400">申请 {{ c.applyTime }}</span>
</div>
<div v-if="c.stage === 'closing'" class="mt-0.5 text-[11px] text-slate-400">{{ c.meta }}</div>
<!-- 当前节点与会签成员进度状态更新时间跟在审核人员名字后面 -->
<div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
<span class="text-slate-400">当前节点</span>
<span class="font-medium text-slate-600">{{ c.node ?? c.status }}</span>
<div v-if="c.members && c.members.length" class="flex items-center gap-1">
<div v-for="(m, mi) in c.members" :key="mi">
<NTag v-if="m.done" size="small" type="success">
<div class="flex items-center">
<Icon icon="ix:success" />{{ m.name }}
</div>
</NTag>
<NTag v-else size="small" type="warning">
<div class="flex items-center">
<Icon icon="tabler:clock" />{{ m.name }}
</div>
</NTag>
</div>
</div>
<span v-else class="text-slate-400">{{ c.detail }}</span>
<span v-if="c.updateTime" class="text-slate-400">状态更新 {{ c.updateTime }}</span>
</div>
</div>
<!-- 驳回处理 -->
<NTag v-if="item.status==='reject'" size="small" type="error">
<div class="flex items-center">
<Icon icon="system-uicons:reset" />驳回处理
</div>
</NTag>
<div v-if="item.status==='reject'" class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span>
</div>
<p class="px-2 py-1 text-xs border border-red-100 bg-red-50/60 px-2 py-1 text-red-600 rounded-md mt-1">
驳回原因{{ c.rejectReason }}
</p>
<span v-if="c.updateTime" class="text-11px text-slate-400">驳回于 {{ c.updateTime }}</span>
</div>
<!-- 右区 -->
<!-- 待我审批 -->
<div v-if="c.stage === 'pending'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c)">去审批</NButton>
</div>
<!-- 我的申请 -->
<div v-if="c.stage === 'approved'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c)">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
<NButton class="text-xs" text type="error" @click="openClosure(c)">资料关闭</NButton>
</div>
<!-- 驳回处理 -->
<div v-if="c.stage === 'reject'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'reject')">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c,'reject')">进度</NButton>
<NButton class="text-xs" text type="primary" @click="openDisposal(c,'reject')">修改重报</NButton>
<NButton class="text-xs" text type="error" @click="openClosure(c,'reject')">关闭变更</NButton>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 进度弹框 -->
<NDrawer v-model:show="progressDrawer" :width="502" placement="right">
<NDrawerContent title="变更进度">
<div class="rounded-lg border bg-slate-50/60 px-3 py-2.5">
<div class="text-sm font-semibold text-slate-800">{{ currentRow?.title }}</div>
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-400">
<span class="font-mono">{{ currentRow?.id }}</span>
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow.status}}</NTag>
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow.status}}</NTag>
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow.status}}</NTag>
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow.status}}</NTag>
<span v-if="currentRow?.overdue" class="font-medium text-red-500">超期 {{ currentRow?.day }} </span>
</div>
<div v-if="currentRow?.dept || currentRow?.applicant" class="mt-1 text-[11px] text-slate-400">{{ [currentRow?.dept, currentRow?.applicant].filter(Boolean).join(' · ') }}</div>
<div v-if="currentRow?.meta" class="mt-1 text-[11px] text-slate-400">{{ currentRow?.meta }}</div>
</div>
<div class="mt-4">
<div v-for="(s, i) in stages" :key="s" class="flex gap-3">
<div class="flex flex-col items-center">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2"
:class="i < curIdx || closed ? 'border-[#7CC242] bg-[#7CC242] text-white' : i === curIdx && !closed ? 'border-[#1D4E9C] bg-[#1D4E9C] text-white' : 'border-slate-200 bg-white text-slate-300'">
<Icon v-if="i < curIdx || closed" icon="ix:success" />
<Icon v-else-if="i === curIdx" icon="tabler:clock" />
<span v-else class="text-[10px]">{{ i + 1 }}</span>
</span>
<span v-if="i < stages.length - 1" class="w-0.5 flex-1" :class="i < curIdx ? 'bg-[#7CC242]' : 'bg-slate-200'" />
</div>
<div class="pb-5">
<div class="text-sm" :class="i === curIdx && !closed ? 'font-semibold text-[#1D4E9C]' : i < curIdx || closed ? 'font-medium text-slate-700' : 'text-slate-400'">
{{ s }}{{ i === curIdx && !closed ? '(当前)' : '' }}
</div>
<div v-if="timeOf(i)" class="mt-0.5 text-[11px] text-slate-400">{{ timeOf(i) }}</div>
<div v-if="i === curIdx && !closed && (currentRow?.detail || currentRow?.updateTime)" class="mt-0.5 text-[11px] text-slate-400">
{{ [currentRow?.detail, currentRow?.updateTime ? `更新 ${currentRow?.updateTime}` : ''].filter(Boolean).join(' · ') }}
</div>
</div>
</div>
</div>
<div v-if="currentRow?.dur === '临时'" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
临时变更计划恢复 {{ currentRow?.deadline ?? '' }}{{ currentRow?.extended ? ` · 延期后恢复 ${currentRow?.deadline}` : '' }} · 恢复 / 延期 / 转永久请在详情页中操作
</div>
</NDrawerContent>
</NDrawer>
<!-- 撤回弹框 -->
<NModal
v-model:show="revokeModal"
preset="card"
:auto-focus="false"
:style="{ width: '450px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #header>
<h3 class="text-sm font-bold text-slate-800">撤回变更申请</h3>
</template>
<template #default>
<div class="rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
变更编号 {{ currentRow?.id }}撤回后申请退回草稿状态已产生的审批记录保留可查修改内容后可重新提交重新提交按新申请计
</div>
<NInput
class="mt-3"
v-model:value="revokeReason"
type="textarea"
:rows="3"
placeholder="撤回理由(必填):如补充材料、变更范围调整、需重新组织风险分析…"
/>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="revokeCancel">取消</NButton>
<NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">保存</NButton>
</div>
</template>
</NModal>
<!-- 处置申请弹框 -->
<NModal
v-model:show="disposalModal"
preset="card"
:auto-focus="false"
:style="{ width: '670px', height: 'auto' }"
:segmented="{ content: true, footer: false }"
>
<template #header>
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-base font-semibold text-slate-800">处置申请 · {{ currentRow?.title }}</h3>
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号{{ currentRow?.id }}</span>
</div>
</template>
<template #default>
<div class="rounded-lg border bg-white px-4 py-3">
<div class="flex flex-wrap items-center gap-2">
<NTag v-if="currentRow?.level=== '重要'" type="error" size="small">{{ currentRow?.level }}</NTag>
<NTag v-else type="primary" size="small">{{ currentRow?.level }}</NTag>
<NTag v-if="currentRow?.dur=== '临时'" type="warning" size="small">{{ currentRow?.dur }}</NTag>
<NTag v-else type="primary" size="small">{{ currentRow?.dur }}</NTag>
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='实施中'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待PSSR'" size="small" type="warning">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow?.status}}</NTag>
<NTag v-if="currentRow?.status==='已关闭'" size="small" type="primary">{{currentRow?.status}}</NTag>
<span v-if="currentRow?.applyTime" class="text-[11px] text-slate-400">申请 {{ currentRow?.applyTime }}</span>
<span v-if="currentRow?.dur === '临时' && currentRow?.deadline" class="text-[11px] text-amber-600">
到期 {{ currentRow?.deadline }}{{ (currentRow?.day ?? 0) < 0 ? ` · 已超期 ${-(currentRow?.day ?? 0)} ` : currentRow?.day !== undefined ? ` · ${currentRow?.day} ` : '' }}
</span>
</div>
<div class="mt-1 text-[11px] text-slate-400">{{ currentRow?.detail }}</div>
</div>
<!-- 进行中的子单提示 -->
<div v-if="pendingSubs.length" class="rounded-lg border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-700">
已有子单在审批中{{ pendingSubs.map((s:any) => s.id).join('、') }} · 审批进度见待我处理列表行内展示
</div>
<!-- 处置选项 -->
<div class="rounded-lg border bg-white mt-3">
<div class="border-b px-4 py-2.5 text-sm font-bold text-slate-800">
选择处置方式
<span class="ml-2 text-[11px] font-normal text-slate-400">申请恢复所有变更可发起申请延期 / 申请转为永久仅临时变更可用</span>
</div>
<div class="divide-y">
<div v-for="o in opts" :key="o.act" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-slate-800">{{ o.name }}</span>
<NTag v-if="o.note" type="warning" size="small">{{ o.note }}</NTag>
</div>
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ o.desc }}</div>
</div>
<NButton size="small" type="primary" :disabled="o.disabled">
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
</NButton>
</div>
</div>
</div>
</template>
</NModal>
</NCard>
</NSpace>
</template>
<style scoped>
</style>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 260px);
overflow-y: auto;
}
</style>
-184
View File
@@ -1,184 +0,0 @@
<script setup lang="ts">
import { onMounted, ref, computed, watch } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { useRoute } from 'vue-router';
const route = useRoute();
const { routerPushByKey } = useRouterPush();
const themeStore = useThemeStore();
const sc = ref<any>(null);
const cur = ref<number>(0);
const playing = ref(false);
const curStageIdx = computed(() => (sc.value ? sc.value.stages.indexOf(sc.value.steps[cur.value].stage) : 0));
const finished = computed(() => (sc.value ? cur.value >= sc.value?.steps?.length - 1 : false));
const ROLES_NAME: Record<string, string> = {
applicant: "申请人(张工艺)", process: "专业会签(王仪表)", process2: "专业会签(周设备)",
safety: "安全工程师(王海燕)", dept: "车间主任(李主任)", vp: "分管领导(刘副总)", admin: "系统管理员",
quality: "质量工程师(陈质量)", viewer: "非审批人员(吴新)",
};
const VIEW_NAME: Record<string, string> = {
apply: "变更申请", tasks: "我的任务", approvals: "审批中心", execution: "PSSR / 验收", closure: "关闭管理",
};
let timer: ReturnType<typeof setTimeout> | undefined;
watch([playing, cur, sc], ([p, c, s]) => {
if (timer) { clearTimeout(timer); timer = undefined; }
if (!p || !s) return;
if (c >= s.steps.length - 1) { playing.value = false; return; }
timer = setTimeout(() => { cur.value = Math.min(cur.value + 1, (sc.value?.steps.length ?? 1) - 1); }, 2000);
});
function jumpFor(s: any): { role: string; view: string } | null {
const a = s.actor;
if (a === "系统") return null;
let role: string;
if (a.includes("张工艺")) role = "applicant";
else if (a.includes("李主任")) role = "dept";
else if (a.includes("王仪表")) role = "process";
else if (a.includes("周设备")) role = "process2";
else if (a.includes("刘副总")) role = "vp";
else if (a.includes("专人") || a.includes("安全")) role = "safety";
else if (a.includes("岗位") || a.includes("维修班")) role = "applicant";
else if (a.includes("车间")) role = "dept";
else return null;
let view: string;
if (role === "applicant") view = s.stage === "变更申请" ? "apply" : "tasks";
else if (s.stage.includes("审批")) view = "approvals";
else if (s.stage === "关闭") view = "closure";
else view = "execution";
return { role, view };
}
function prev() { playing.value = false; cur.value = Math.max(0, cur.value - 1); }
function next() { playing.value = false; if (sc.value) cur.value = Math.min(sc.value.steps.length - 1, cur.value + 1); }
function reset() { playing.value = false; cur.value = 0; }
function back() {
routerPushByKey("home_process");
}
function jumpTo() {
routerPushByKey("home_initiatechange");
}
onMounted(() => {
if(route.query && route.query.params){
sc.value = JSON.parse(route.query.params as string);
}
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 流程演示详情 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-2">
<NButton ghost type="primary" class="text-sm font-medium" @click="back">
<Icon icon="ep:back" class="size-16px mr-1" /> 返回示例列表
</NButton>
<span class="text-base font-bold text-slate-800">{{ sc?.no }} {{ sc?.title }}</span>
<NTag size="small" v-for="t in sc?.tags" :key="t.text" :class="t.cls">{{ t.text }}</NTag>
<span class="text-xs text-slate-400">示例变更{{ sc?.changeName }}</span>
</div>
<!-- 阶段进度条 -->
<div class="flex items-center gap-1 overflow-x-auto rounded-lg border bg-white p-3">
<div v-for="(st, i) in sc?.stages" :key="st" class="flex shrink-0 items-center gap-1">
<span
class="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium"
:class="i < curStageIdx ? 'bg-emerald-50 text-emerald-600' : i === curStageIdx ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
:style="{ '--theme-color': themeStore.themeColor }"
>
<Icon v-if="i < curStageIdx" icon="ix:success" class="size-14px" />
<span v-else class="flex h-3.5 w-3.5 items-center justify-center rounded-full border text-[10px]" :class="i === curStageIdx ? 'border-white' : 'border-slate-300'">{{ Number(i) + 1 }}</span>
{{ st }}
</span>
<Icon v-if="Number(i) < sc?.stages?.length - 1" icon="formkit:right" class="size-12px text-slate-300" />
</div>
</div>
<!-- 控制条 -->
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-white px-3 py-2">
<NButton ghost size="small" class="text-xs" :disabled="cur === 0" @click="prev">
<Icon icon="formkit:left" class="size-12px" /> 上一步
</NButton>
<NButton type="primary" size="small" class="text-xs text-white" :disabled="finished" @click="next">
下一步 <Icon icon="formkit:right" class="size-12px" />
</NButton>
<NButton ghost size="small" class="text-xs" :disabled="finished && !playing" @click="playing = !playing">
<template v-if="playing"><Icon icon="material-symbols:pause-outline-rounded" class="size-14px" /> 暂停</template>
<template v-else><Icon icon="griddy-icons:play" class="size-14px" /> 自动播放</template>
</NButton>
<NButton ghost size="small" class="text-xs" @click="reset">
<Icon icon="system-uicons:reset" class="size-12px" /> 重置
</NButton>
<span class="ml-auto text-xs text-slate-400"> {{ cur + 1 }} / {{ sc?.steps?.length }} {{ finished ? " · 流程已走通 ✓" : "" }}</span>
</div>
<!-- 完成横幅 -->
<div v-if="finished" class="flex flex-wrap items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-sm text-emerald-700">
<Icon icon="ix:success" class="size-16px" />
<b>流程已走通</b> {{ sc.steps.length }} {{ sc.stages.length }} 个阶段全部闭环全程操作留痕资料归档公告发布完整可追溯
</div>
<!-- 步骤时间线 -->
<div class="space-y-2">
<div
v-for="(s, i) in sc?.steps" :key="i" @click="playing = false; cur = Number(i)"
class="flex w-full cursor-pointer items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition"
:class="i === cur ? 'border-[var(--theme-color)] bg-[#EAF0F9]/50 shadow-sm' : Number(i) < cur ? 'border-emerald-100 bg-emerald-50/30' : 'bg-white hover:border-slate-300'"
:style="{ '--theme-color': themeStore.themeColor }"
>
<span
class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold"
:class="Number(i) < cur ? 'bg-emerald-500 text-white' : i === cur ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
:style="{ '--theme-color': themeStore.themeColor }"
>
<Icon v-if="Number(i) < cur" icon="ix:success" class="size-16px" />
<template v-else>{{ Number(i) + 1 }}</template>
</span>
<span class="min-w-0 flex-1">
<span class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="flex items-center gap-1 text-xs text-slate-500">
<Icon icon="lucide:user-round" class="size-14px" />
{{ s.actor }}{{ s.role ? `${s.role}` : "" }}
</span>
<span class="rounded bg-slate-100 px-1.5 text-[10px] text-slate-400">{{ s.stage }}</span>
<span class="text-sm font-medium" :class="i === cur ? 'text-[#17407F]' : 'text-slate-700'">{{ s.title }}</span>
<NTag v-if="s.branch" class="border-violet-200 bg-violet-50 text-violet-600">{{ s.branch }}</NTag>
<span class="ml-auto shrink-0 font-mono text-[11px] text-slate-400">{{ s.time }}</span>
<NButton ghost type="primary" size="tiny" v-if="jumpFor(s) && s.actor !== '系统'"
class="text-[10px] font-medium"
:title="`切换到${ROLES_NAME[jumpFor(s)!.role]}的「${VIEW_NAME[jumpFor(s)!.view] ?? jumpFor(s)!.view}」界面`"
@click.stop="jumpTo"
>
进入界面
</NButton>
</span>
<span v-if="i === cur && (s.detail || s.system)" class="mt-2 block space-y-1.5 border-t border-[#ACC6E5]/40 pt-2">
<span v-if="s.detail" class="block text-xs leading-relaxed text-slate-600"><b class="text-slate-500">内容</b>{{ s.detail }}</span>
<span
v-if="s.system"
class="flex items-start gap-1.5 text-xs leading-relaxed"
:style="{ color: themeStore.themeColor }"
>
<Icon icon="lucide:sparkles" class="mt-0.8 size-12px" />{{ s.system }}
</span>
</span>
</span>
</div>
</div>
</div>
</NCard>
</NSpace>
</template>
<style scoped>
</style>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 185px);
overflow-y: auto;
}
</style>
+22 -21
View File
@@ -1,46 +1,47 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { ref, reactive } from 'vue';
import { useAuthStore } from '@/store/modules/auth';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'PwdLogin'
});
const authStore = useAuthStore();
const { validate } = useNaiveForm();
interface FormModel {
userName: string;
username: string;
password: string;
}
const model: FormModel = reactive({
userName: 'admin1',
password: '123456'
});
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
const { formRules } = useFormRules();
return {
userName: formRules.userName,
password: formRules.pwd
};
username: 'admin',
password: 'Admin@123'
});
const btnLoading = ref(false);
async function handleSubmit() {
await validate();
await authStore.login(model.userName, model.password);
if(!model.username){
return window.$message?.warning('用户名不能为空')
}
if(!model.password){
return window.$message?.warning('密码不能为空')
}
btnLoading.value = true;
try {
await authStore.login(model.username, model.password);
} finally {
btnLoading.value = false;
}
}
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="userName">
<NInput v-model:value="model.userName" placeholder="请输入用户名" />
<NForm ref="formRef" :model="model" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem>
<NInput v-model:value="model.username" placeholder="请输入用户名" />
</NFormItem>
<NFormItem path="password">
<NFormItem>
<NInput
v-model:value="model.password"
type="password"
@@ -49,7 +50,7 @@ async function handleSubmit() {
/>
</NFormItem>
<NSpace vertical :size="24">
<NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
<NButton type="primary" size="large" round block :loading="btnLoading" @click="handleSubmit">
登录
</NButton>
</NSpace>