变更预识别修改

This commit is contained in:
2026-09-11 12:02:46 +08:00
parent 7030414146
commit 1c4c8e7b62
12 changed files with 376 additions and 85 deletions
+2
View File
@@ -199,6 +199,7 @@ export const generatedRoutes: GeneratedRoute[] = [
component: 'view.my_highriskdetail',
meta: {
title: '高风险场景详情',
constant: true,
hideInMenu: true,
i18nKey: 'route.my_highriskdetail'
}
@@ -310,6 +311,7 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: {
title: '修改密码',
i18nKey: 'route.systemmanage_changepassword',
constant: true,
order: 5
}
},
+11
View File
@@ -0,0 +1,11 @@
import { request } from '../request';
/** 获取列表
*
*/
export function pendingListApi() {
return request({
url: `/api/my-tasks`,
method: 'get',
});
}
+60
View File
@@ -97,6 +97,66 @@ export function formatTimestamp(timestamp: number | string | Date | null,format:
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
/**
* 任意时间格式 → 时间戳(支持 只有小时)
* @param {string|number|Date} input
* @param {'ms'|'s'} unit 默认毫秒
*/
export function anyToTimestamp(input: string | number | Date | null | undefined, unit = 'ms') {
if (input == null || input === '') return null;
const toResult = (ms: number) =>
Number.isNaN(ms) ? null : unit === 's' ? Math.floor(ms / 1000) : ms;
// 1. 数字
if (typeof input === 'number') return toResult(input);
// 2. Date
if (input instanceof Date) return toResult(input.getTime());
let str = String(input).trim();
// 3. 纯数字字符串
if (/^\d+$/.test(str)) return toResult(Number(str));
// 4. 统一分隔符
str = str
.replace(/[年月]/g, '-')
.replace(/[日号]/g, '')
.replace(/[时分]/g, ':')
.replace(/秒/g, '')
.replace(/[./]/g, '-')
.replace(/\s+/g, ' ')
.trim();
// 5. 手动解析:小时、分钟、秒、毫秒全部可选
const m = str.match(
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}))?(?::(\d{1,2}))?(?::(\d{1,2}))?(?:\.(\d{1,3}))?/
);
if (m) {
const [, y, mo, d, h = '0', mi = '0', s = '0', ms = '0'] = m;
const t = new Date(
+y,
+mo - 1,
+d,
+h,
+mi,
+s,
+ms.padEnd(3, '0')
).getTime();
if (!Number.isNaN(t)) return toResult(t);
}
// 6. 兜底
let t = new Date(str.replace(' ', 'T')).getTime();
if (!Number.isNaN(t)) return toResult(t);
t = Date.parse(str);
if (!Number.isNaN(t)) return toResult(t);
return null;
};
// 辅助函数:Fisher-Yates 洗牌算法
function shuffle(arr:any) {
for (let i = arr.length - 1; i > 0; i--) {
+7 -2
View File
@@ -10,7 +10,7 @@ import { useChangeStore } from '@/store/modules/change';
import { queryUnsavedApi, saveChangeApi, submitChangeApi, getChangeDetailApi } from '@/service/api/change';
import { systemConfigApi, templateListApi, } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { formatTimestamp } from "@/utils/common";
import { formatTimestamp, anyToTimestamp } from "@/utils/common";
import ChangePre from "./modules/changePre.vue";
import ChangeApply from "./modules/changeApply.vue";
import ChangeTraining from "./modules/changeTraining.vue";
@@ -230,7 +230,7 @@ const submitConfirm = async (type: string) => {
// 变更申请表
if(type==='form'){
if(changeApplyRules(form)){
if(changeApplyRules(form?.apply_form)){
return;
}
}
@@ -254,6 +254,11 @@ const getChangeDetail = async () => {
changeNo.value = data.change_no;
changePreForm.value = data.precheck;
changeApplyForm.value = data.apply_form;
// 解析时间戳
changeApplyForm.value.plan_use_date = anyToTimestamp(changeApplyForm.value.plan_use_date);
changeApplyForm.value.restore_deadline = anyToTimestamp(changeApplyForm.value.restore_deadline);
riskAnalysisForm.value = data.risk_records;
changeTrainingForm.value = data.train;
pssrForm.value = data.pssr;
@@ -1,16 +1,18 @@
<script setup lang="ts">
import { computed, h, ref, watch } from 'vue'
import { useThemeStore } from '@/store/modules/theme';
import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { Icon } from '@iconify/vue'
import RichTextInput from './RichTextInput.vue'
const themeStore = useThemeStore();
interface ChangeItem {
item: string
before_text: string
after_text: string
}
const props = defineProps<{
data: ChangeItem[]
editable?: boolean
@@ -135,7 +137,14 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
</script>
<template>
<div class="change-compare-table">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{data.length ? `变更识别结果(${data.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
</div>
</div>
<div class="change-compare-table p-4">
<NDataTable
:columns="columns"
:data="localData"
@@ -149,6 +158,7 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
</NButton>
</div>
</div>
</div>
</template>
<style scoped>
@@ -7,12 +7,14 @@ const props = withDefaults(defineProps<{
minRows?: number
maxRows?: number
disabled?: boolean
bordered?: boolean
}>(), {
modelValue: '',
placeholder: '',
minRows: 1,
maxRows: 100,
disabled: false,
bordered: true,
})
const emit = defineEmits<{
@@ -235,7 +237,7 @@ onBeforeUnmount(() => {
<template>
<div
class="rich-text-input"
:class="{ 'is-focused': isFocused, 'is-disabled': disabled }"
:class="{ 'is-focused': isFocused, 'is-disabled': disabled, 'bordered': bordered }"
>
<!-- 工具栏 -->
<div v-show="isFocused && !disabled" class="rti-toolbar">
@@ -296,12 +298,14 @@ onBeforeUnmount(() => {
<style scoped>
.rich-text-input {
position: relative;
border: 1px solid #e5e8ef;
border-radius: 6px;
background: #fafbfc;
transition: all 0.2s ease;
overflow: visible;
}
.bordered {
border: 1px solid #e5e8ef;
border-radius: 6px;
}
.rich-text-input:hover {
border-color: #2080f0;
@@ -1,10 +1,11 @@
<!-- 变更预识别 -->
<script setup lang="ts">
import { inject, watch, ref } from 'vue';
import { inject, watch, ref, computed } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiApplication, aiRecognize, aiRiskPreAnalysis } from "@/service/api/ai";
import ChangeCompareTable from "./ChangeCompareTable.vue";
import PreRisk from "./preRisk.vue";
const themeStore = useThemeStore();
@@ -17,18 +18,16 @@ const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
// 是否是ai生成
const isAiGenerated = ref<boolean>(false);
// 变更识别loading
const preLoading = ref<boolean>(false);
// 生成变更申请单loading
const generateLoading = ref<boolean>(false);
// 风险预分析loading
const analyzing = ref<boolean>(false);
// 编辑风险预分析
const preRiskEdit = ref<string>('');
// 是否显示险预分析
const preRiskShow = ref<boolean>(false);
const preRiskShow = computed(() => form.value.severity !== '' || form.value.probability !== '' || form.value.protection !== '');
// 是否显示变更识别结果
const identifyShow = computed(() => form.value.compare_rows.length > 0);
// 表单
const form = ref<{
description: string,
@@ -54,7 +53,6 @@ const runIdentify = async () => {
try {
const res = await aiRecognize(form.value.description);
form.value.compare_rows = (res?.rows ?? []).map((x, i) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text }));
isAiGenerated.value = true;
window.$message?.success(`AI 变更识别完成,共 ${form.value.compare_rows.length} 项,请逐项人工确认(可直接修改)`);
} catch (e: any) {
form.value.compare_rows = [];
@@ -79,7 +77,6 @@ const runRiskAnalysis = async () => {
probability: res?.probability ?? form.value.probability,
protection: res?.protection ?? form.value.protection,
};
preRiskShow.value = true;
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
} catch (e: any) {
aiFail(e);
@@ -115,9 +112,6 @@ const saveDraft = () => {
defineExpose({form})
watch(() => props.currentForm, (newVal:any) => {
if(newVal.severity){
preRiskShow.value = true;
}
const {
attachments,
change_id,
@@ -140,7 +134,7 @@ watch(() => props.currentForm, (newVal:any) => {
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="font-semibold text-base ml-2">变更内容描述</span>
</div>
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled">
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled || !form.description.trim()">
<Icon v-if="preLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
变更识别
@@ -151,52 +145,17 @@ watch(() => props.currentForm, (newVal:any) => {
</div>
</div>
<!-- 变更识别结果 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{form.compare_rows.length ? `变更识别结果(${form.compare_rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
</div>
<span v-if="isAiGenerated" class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
</div>
<div class="p-4">
<template v-if="identifyShow">
<ChangeCompareTable
:data="form.compare_rows"
:editable="true"
@update:data="(d) => form.compare_rows = d"
/>
</div>
</div>
</template>
<!-- 风险预分析报告 -->
<div v-if="preRiskShow" class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
</div>
<div class="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = preRiskEdit === s.key ? '' : s.key">
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
</NButton>
</div>
<NInput v-model:value="form[s.key]" :readonly="preRiskEdit !== s.key" type="textarea" :autosize="{ minRows: 2 }" :bordered="false" autofocus />
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
</div>
</div>
</div>
</div>
<template v-if="preRiskShow">
<PreRisk :formInfo="form" @update:data="(key:'severity' | 'probability' | 'protection',val:string) => form[key] = val" />
</template>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { Icon } from '@iconify/vue'
import RichTextInput from './RichTextInput.vue'
const themeStore = useThemeStore();
const props = defineProps(['formInfo'])
const emit = defineEmits(['update:data']);
const form = ref<any>(null);
watch(() => props.formInfo,
(val: any) => {
// 深度拷贝以避免直接修改 props,并确保响应式
form.value = val
},
{ immediate: true, deep: true },
)
// 编辑风险预分析
const preRiskEdit = ref<boolean>(false);
// 更新数据
const onCellChange = (key:string,val:string) => {
form.value[key] = val;
emit('update:data', key, val)
}
</script>
<template>
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = !preRiskEdit">
{{ preRiskEdit ? "完成" : "编辑" }}
</NButton>
</div>
<div class="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
</div>
<RichTextInput :disabled="!preRiskEdit" :modelValue="form[s.key]" :minRows="2" :bordered="false" @update:modelValue="(v) => onCellChange(s.key,v)" />
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
</div>
</div>
</div>
</div>
</template>
+1 -1
View File
@@ -33,7 +33,7 @@ watch(() => props.currentForm, (newVal:any) => {
};
});
// 3. 过滤掉不需要的字段
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_type', 'group_name'];
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name'];
const newResult = result.map((group:any) => ({
...group,
items: group.items.map((item:any) =>
-1
View File
@@ -6,7 +6,6 @@ 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 {
+204 -19
View File
@@ -3,10 +3,11 @@ import { ref, computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const { routerPushByKey } = useRouterPush();
const emit = defineEmits<{ go: [v: string, preset?: string];}>();
interface MyChange {
@@ -45,14 +46,14 @@ const specialTab = computed(() => currentTab.value === "tempOverdue" || currentT
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-0046", title: "循环水加药泵冲程临时调整", level: "一般", dur: "临时", stage: "pass", 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: "pass", 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: "pass", 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 tempOverdueList = computed(() => (role.value === "applicant" ? myChanges.value : []).filter((c: MyChange) => c.stage === "pass" && 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 {
@@ -64,7 +65,7 @@ interface ListType {
const list = ref<ListType[]>([
{
id: "MOC-2026-R01-001",
title: '审批流转中',
title: '待我审批',
status: 'pending',
list: [
{
@@ -110,10 +111,189 @@ const list = ref<ListType[]>([
},
]
},
{
id: "MOC-2026-R01-002",
title: '我的申请',
status: 'my',
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-001",
title: '审批流转中',
status: 'approval',
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',
status: 'pass',
list: [
{
id: "MOC-2026-R02-0001",
@@ -305,10 +485,7 @@ const opts = computed<Opt[]>(() => {
// 打开详情
const openDetail = (c: MyChange,type:string = '') => {
if(type==='reject'){
window.$message?.info("演示:打开详情");
return;
}
routerPushByKey("my_highriskdetail",{ query: { id: c.id.toString() } });
};
// 打开进度
const openProgress = (c: MyChange,type:string = '') => {
@@ -355,7 +532,7 @@ const revokeSave = () => {
setTimeout(() => {
revokeSaving.value = false;
revokeModal.value = false;
const pendingGroup = list.value.find((item: any) => item.status === 'pending');
const pendingGroup = list.value.find((item: any) => item.status === 'approval');
if (pendingGroup) {
// 过滤掉匹配 targetId 的项
pendingGroup.list = pendingGroup.list.filter((item:any) => item.id !== currentRow.value?.id);
@@ -367,7 +544,7 @@ const revokeSave = () => {
<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"
@@ -408,15 +585,23 @@ const revokeSave = () => {
: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==='my'" icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" />
<!-- 审批流转中/已通过 -->
<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==='approval' || item.status==='pass'" 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-slate-400"> {{ item.list.length }} · 流程当前节点轮到您处理 · 点击进入审批</p>
<!-- 我的申请 -->
<p v-if="item.status==='my'" class="text-11px text-slate-400"> {{item.list.length}} </p>
<!-- 审批流转中 -->
<p v-if="item.status==='pending'" class="text-11px text-#94a3b8"> {{item.list.length}} · 审批流程推进中 · 可查看进度或撤回</p>
<p v-if="item.status==='approval'" 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==='pass'" 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>
@@ -428,7 +613,7 @@ const revokeSave = () => {
>
<!-- 左区 -->
<!-- 审批流转中/已通过 -->
<div v-if="item.status==='pending' || item.status==='approved'" class="min-w-0 flex-1">
<div v-if="item.status==='approval' || item.status==='pass'" 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>
@@ -490,13 +675,13 @@ const revokeSave = () => {
</div>
<!-- 右区 -->
<!-- 审批流转中 -->
<div v-if="c.stage === 'pending'" class="flex gap-1.5">
<div v-if="c.stage === 'approval'" 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">
<div v-if="c.stage === 'pass' || 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>
+3
View File
@@ -171,6 +171,7 @@ const getAllPaths = (tree:any) => {
}
// 获取所有路由
const getAllRoutes = () => {
console.log(router.getRoutes())
const filtered = router.getRoutes().filter((item:any) => {
// 条件1:有name且非常量
if (item.name === undefined || item.meta?.constant === true) return false;
@@ -182,6 +183,8 @@ const getAllRoutes = () => {
})
const tree = filtered.map((item:any) => copyTitleToTop(item));
routeTree.value = tree;
console.log(tree)
allPaths.value = getAllPaths(tree);
}