删除多余路由

This commit is contained in:
2026-09-16 09:22:14 +08:00
parent 8272d0653f
commit 30c2a79f51
2 changed files with 485 additions and 1115 deletions
+318 -383
View File
@@ -1,262 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { systemConfigApi } from '@/service/api/system';
import { myTasksListApi, myTasksWithdrawApi, myTasksCloseApi } from '@/service/api/myTasks';
const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const emit = defineEmits<{ go: [v: string, preset?: string];}>(); const loading = ref<boolean>(false);
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 currentRow = ref<any>(null);
// 进度抽屉
const progressDrawer = ref<boolean>(false); const progressDrawer = ref<boolean>(false);
// 撤回弹框
const revokeModal = ref<boolean>(false); const revokeModal = ref<boolean>(false);
const revokeSaving = ref<boolean>(false); const revokeSaving = ref<boolean>(false);
// 撤回原因
const revokeReason = ref<string>(""); const revokeReason = ref<string>("");
// 处置申请弹框
const disposalModal = ref<boolean>(false); const disposalModal = ref<boolean>(false);
// 变更类型
const stages = ["提交申请", "审批", "实施与投用前确认", "投用", "验收", "关闭"]; const typeList = ref<any[]>([]);
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 levelList = ref<any[]>([]);
const timeOf = (i: number) => (i === 0 ? currentRow.value?.applyTime : i === 3 ? currentRow.value?.actualUse : i === 5 ? currentRow.value?.closeTime : undefined); // 提醒天数
const remindDays = ref<number>(7);
// 源数据
const dataSource = ref<any>(null);
// 列表
const list = ref<any[]>([]);
// 数量统计
const tabCount = ref<Record<string, number>>({
"all": 0,
"approval": 0,
"apply": 0,
"reject": 0,
});
// 选项卡
const currentTab = ref<string>("all"); const currentTab = ref<string>("all");
const visibleTabs = ref<{ k: string; name: string; count: number }[]>([ // 选项卡切换
{ k: "all", name: "全部", count: 10 }, const tabChange = (k: string) => {
{ k: "approval", name: "待我审批", count: 1 }, currentTab.value = k;
]); // 全部
if(k === "all"){
list.value = resetGrouped([...dataSource.value.my_applications,...dataSource.value.todo]);
}
// 待我审批
if(k === "approval"){
list.value = resetGrouped([...dataSource.value.todo]);
}
// 我的申请
if(k === "apply"){
list.value = resetGrouped([...dataSource.value.my_applications]);
}
// 驳回处理
if(k === "reject"){
const arr = [...dataSource.value.my_applications,...dataSource.value.todo];
const rejectedList = arr.filter((item: any) => item.status === "REJECTED");
list.value = resetGrouped(rejectedList);
}
};
interface ListType { const SUB_ORDERS: any[] = [];
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 subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中")); const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string } interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
@@ -280,202 +88,329 @@ const opts = computed<Opt[]>(() => {
return list; return list;
}); });
// 打开详情 // 打开详情、修改重报
const openDetail = (c: MyChange,type:string = '') => { const openDetail = (c: any,type:string = '') => {
if(type==='reject'){ if(type==='detail'){
window.$message?.info("演示:打开详情"); routerPushByKey("my_highriskdetail",{ query: { id: c.change_id.toString() } });
return; }
if(type==='edit'){
routerPushByKey("my_initiatechange",{ query: { id: c.change_id.toString() } });
}
};
// 打开资料关闭
const openDocClose = (c: any) => {
currentRow.value = c;
// routerPushByKey("my_docclose",{ query: { id: c.change_id.toString() } });
};
// 打开处置申请
const openDisposal = (c: any,) => {
currentRow.value = c;
disposalModal.value = true;
};
// 关闭变更
const closeChange = async (c: any) => {
const {error} = await myTasksCloseApi(c?.change_id);
if(!error){
window.$message?.success(`关闭变更成功`);
getList();
} }
}; };
// 打开进度 // 打开进度
const openProgress = (c: MyChange,type:string = '') => { const openProgress = (c: any) => {
currentRow.value = c; currentRow.value = c;
if(type==='reject'){
window.$message?.info("该任务暂无关联变更进度");
return;
}
progressDrawer.value = true; progressDrawer.value = true;
}; };
// 打开撤回 // 打开撤回
const openWithdraw = (c: MyChange) => { const openWithdraw = (c: any) => {
revokeReason.value = ""; revokeReason.value = "";
currentRow.value = c; currentRow.value = c;
revokeModal.value = true; 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 = () => { const revokeSave = async () => {
if (!revokeReason.value) { if (!revokeReason.value) {
window.$message?.warning("请填写撤回理由"); return window.$message?.warning("请填写撤回理由");
return;
} }
revokeSaving.value = true; revokeSaving.value = true;
setTimeout(() => { const {error} = await myTasksWithdrawApi(currentRow.value?.change_id,{reason:revokeReason.value});
revokeSaving.value = false; if(!error){
revokeModal.value = false; revokeModal.value = false;
const pendingGroup = list.value.find((item: any) => item.status === 'pending'); window.$message?.success(`撤回成功,申请退回草稿箱,审批记录保留可查,修改后可重新提交`);
if (pendingGroup) { getList();
// 过滤掉匹配 targetId 的项
pendingGroup.list = pendingGroup.list.filter((item:any) => item.id !== currentRow.value?.id);
} }
window.$message?.success(`已撤回 ${currentRow.value?.id},申请退回草稿箱,审批记录保留可查,修改后可重新提交`); revokeSaving.value = false;
}, 1000);
}; };
// 匹配变更等级
const getChangeLevel = (level:number) => {
const changeLevel = levelList.value.find((item:any) => item.value == level);
return changeLevel || null;
}
// 按 status 归纳
const resetGrouped = (data:any) => {
const grouped = Object.values(
data.reduce((acc:any, item:any) => {
const key = item.status;
if (!acc[key]) {
acc[key] = { status: key, items: [] };
}
acc[key].items.push(item);
return acc;
}, {})
);
return grouped;
}
// 获取列表数据
const getList = async () => {
loading.value = true;
const {data,error} = await myTasksListApi();
if(!error){
dataSource.value = data;
tabChange(currentTab.value);
// 统计数量
// 全部
tabCount.value["all"] = [...dataSource.value.my_applications,...dataSource.value.todo].length;
// 待我审批
tabCount.value["approval"] = [...dataSource.value.todo].length;
// 我的申请
tabCount.value["apply"] = [...dataSource.value.my_applications].length;
// 驳回处理
const arr = [...dataSource.value.my_applications,...dataSource.value.todo];
const rejectedList = arr.filter((item: any) => item.status === "REJECTED");
tabCount.value["reject"] = rejectedList.length;
}
loading.value = false;
};
// 获取配置
const getConfig = async () => {
loading.value = true;
const {data,error} = await systemConfigApi();
if(!error){
// 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
if (changeTypeGroup) {
typeList.value = changeTypeGroup.items;
}
// 解析 变更等级设置
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelList.value = changeLevelGroup.items;
}
getList();
}
loading.value = false;
};
onMounted(() => {
getConfig();
});
</script> </script>
<template> <template>
<NSpace vertical :size="16"> <NSpace vertical :size="16">
<!-- 待我处理 --> <NSpin :show="loading" size="small">
<!-- 我的任务 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }"> <NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div <div
class="h-100% flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3" 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' }" :style="{ borderRadius: themeStore.themeRadius + 'px' }"
> >
<div v-for="t in visibleTabs" :key="t.k" @click="currentTab = t.k" <div @click="tabChange('all')"
class="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer select-none" class="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer select-none"
:style="{ :style="{
borderRadius: themeStore.themeRadius + 'px', borderRadius: themeStore.themeRadius + 'px',
'--theme-color': themeStore.themeColor, '--theme-color': themeStore.themeColor,
}" }"
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'"> :class="currentTab === 'all' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
{{ t.name }} 全部
<!-- 驳回处理/资料上传/临时变更超期未处理/变更超期未关闭 --> <NTag size="tiny" round :type="currentTab === 'all' ?'default':'primary'">{{tabCount["all"]}}</NTag>
<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 @click="tabChange('approval')"
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 === 'approval' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
待我审批
<NTag size="tiny" round :type="currentTab === 'approval' ?'default':'primary'">{{tabCount["approval"]}}</NTag>
</div>
<div @click="tabChange('apply')"
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 === 'apply' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
我的申请
<NTag size="tiny" round :type="currentTab === 'apply' ?'default':'primary'">{{tabCount["apply"]}}</NTag>
</div>
<div @click="tabChange('reject')"
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 === 'reject' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
驳回处理
<NTag size="tiny" round :type="currentTab === 'reject' ?'default':'error'">{{tabCount["reject"]}}</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">
{{tabCount["all"]}} · 点击行内按钮办理各阶段业务
</span>
</div> </div>
<div class="space-y-2 scroll"> <div class="space-y-2 scroll">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div v-if="list.length===0" class="text-center text-slate-400">
暂无数据
</div>
<div <div
class="border border-slate-200 p-4" class="border border-slate-200 p-4"
:style="{ borderRadius: themeStore.themeRadius + 'px' }" :style="{ borderRadius: themeStore.themeRadius + 'px' }"
v-for="item in list" v-for="(item,index) in list"
:key="item.id" :key="index"
> >
<div class="flex items-center gap-2"> <!-- 审批中 -->
<!-- 待我审批 --> <div v-if="item.status==='APPROVING'" class="flex items-center gap-2">
<Icon v-if="item.status==='pending'" icon="ix:success" class="size-16px" :style="{ color: themeStore.otherColor.success }" /> <Icon icon="ant-design:clock-circle-outlined" class="size-16px" :style="{ color: themeStore.otherColor.warning }" />
<!-- 我的申请 --> <p class="text-13px font-700" :style="{ color: themeStore.otherColor.warning }">审批中</p>
<Icon v-if="item.status==='approved'" icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" /> <p class="text-11px text-#94a3b8"> {{item.items.length}} · 审批流程推进中 · 可查看进度或撤回</p>
<!-- 驳回处理 -->
<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>
<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" <div v-if="item.status==='APPROVED'" class="flex items-center gap-2">
<Icon icon="ix:success" class="size-16px" :style="{ color: themeStore.otherColor.success }" />
<p class="text-13px font-700" :style="{ color: themeStore.otherColor.success }">审批通过</p>
<p class="text-11px text-#94a3b8"> {{item.items.length}} · 待实施 / 待验收 / 待关闭 · 处置申请与资料关闭在行内办理</p>
</div>
<!-- 已驳回 -->
<div v-if="item.status==='REJECTED'" class="flex items-center gap-2">
<Icon icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.otherColor.error }" />
<p class="text-13px font-700" :style="{ color: themeStore.otherColor.error }">驳回处理</p>
<p class="text-11px text-#94a3b8">{{item.items.length}} 项待处理</p>
</div>
<div v-if="item.items.length===0" class="text-center text-slate-400 mt-3 border-t pt-4 text-xs">
暂无数据
</div>
<div v-if="item.items && item.items.length" class="flex flex-col gap-3 mt-3">
<div v-for="c in item.items" :key="c.change_no"
class="flex flex-wrap items-center gap-3 border px-4 py-3" 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' : ''" :class="c.overdue_status===2 ? 'border-red-200 bg-[#FFF2F0]' : c.overdue_status===1 ? 'border-amber-200 bg-amber-50/40' : ''"
:style="{ borderRadius: themeStore.themeRadius + 'px' }" :style="{ borderRadius: themeStore.themeRadius + 'px' }"
> >
<!-- 左区 --> <!-- 左区 -->
<!-- 待我审批 --> <!-- 审批/已通过 -->
<div v-if="item.status==='pending' || item.status==='approved'" class="min-w-0 flex-1"> <div v-if="item.status==='APPROVING' || item.status==='APPROVED'" class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2"> <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> <span class="font-mono text-[11px] text-slate-400">{{ c.change_no }}</span>
<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
<NTag v-if="c.dur==='永久'" size="small" type="primary">{{c.dur}}</NTag> size="small"
<NTag v-if="c.dur==='临时转永久'" size="small">{{c.dur}}</NTag> :color="{
<NTag v-if="c.urgent" size="small" type="error">紧急</NTag> color: tagBgColor(getChangeLevel(c.change_level)?.color),
<NTag v-if="c.overdue" size="small" type="error">已超期 {{ c.day }} </NTag> textColor: tagTextColor(getChangeLevel(c.change_level)?.color),
<NTag v-if="c.near" size="small" type="warning">临期 · {{ c.day }} </NTag> borderColor: tagBorderColor(getChangeLevel(c.change_level)?.color),
<NTag v-if="c.extended" size="small">已延期 {{ c.extendedCount }} </NTag> }"
<NTag v-if="c.status==='审批中'" size="small" type="warning">{{c.status}}</NTag> >{{getChangeLevel(c.change_level)?.label}}</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.duration_type===2" size="small" type="warning">临时</NTag>
<NTag v-if="c.status==='待关闭'" size="small" type="error">{{c.status}}</NTag> <NTag v-if="c.duration_type===1" size="small" type="primary">永久</NTag>
<NTag v-if="c.urgent===1" size="small" type="error">紧急</NTag>
<!-- 超期/临期/延期 -->
<NTag v-if="c.overdue_status===2" size="small" type="error">已超期 {{ c.overdue_days }} </NTag>
<NTag v-if="c.overdue_status===1" size="small" type="warning">临期 · {{ c.overdue_days }} </NTag>
<NTag v-if="c.extend_count" size="small">已延期 {{ c.extend_count }} </NTag>
<!-- 变更状态 -->
<NTag v-if="c.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="c.status==='APPROVED'" size="small" type="success">已通过</NTag>
</div> </div>
<div class="mt-1 flex flex-wrap items-baseline gap-x-2"> <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 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> <span v-if="c.apply_time" class="text-[11px] text-slate-400">申请 {{ c.apply_time.slice(0, 16) }}</span>
</div> </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]"> <div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
<span class="text-slate-400">当前节点</span> <span class="text-slate-400">当前节点</span>
<span class="font-medium text-slate-600">{{ c.node ?? c.status }}</span> <span class="font-medium text-slate-600">{{ c.step_name }}</span>
<div v-if="c.members && c.members.length" class="flex items-center gap-1"> <div v-if="c.current_step_members && c.current_step_members.length" class="flex items-center gap-1">
<div v-for="(m, mi) in c.members" :key="mi"> <div v-for="(m, mi) in c.current_step_members" :key="mi">
<NTag v-if="m.done" size="small" type="success"> <NTag v-if="m.status==='PENDING'" size="small" type="warning">
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="ix:success" />{{ m.name }} <Icon icon="tabler:clock" />{{ m.user_name }}
</div> </div>
</NTag> </NTag>
<NTag v-else size="small" type="warning"> <NTag v-else size="small" type="success">
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="tabler:clock" />{{ m.name }} <Icon icon="ix:success" />{{ m.user_name }}
</div> </div>
</NTag> </NTag>
</div> </div>
</div> </div>
<span v-else class="text-slate-400">{{ c.detail }}</span> <span v-if="c.status_changed_at" class="text-slate-400">状态更新 {{ c.status_changed_at.slice(0, 16) }}</span>
<span v-if="c.updateTime" class="text-slate-400">状态更新 {{ c.updateTime }}</span>
</div> </div>
</div> </div>
<!-- 驳回处理 --> <!-- 驳回处理 -->
<NTag v-if="item.status==='reject'" size="small" type="error"> <NTag v-if="item.status==='REJECTED'" size="small" type="error">
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="system-uicons:reset" />驳回处理 <Icon icon="system-uicons:reset" />驳回处理
</div> </div>
</NTag> </NTag>
<div v-if="item.status==='reject'" class="min-w-0 flex-1"> <div v-if="item.status==='REJECTED'" class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-medium text-slate-800">{{ c.title }}</span> <span class="text-sm font-medium text-slate-800">{{ c.title }}</span>
<span class="font-mono text-[11px] text-slate-400">{{ c.id }}</span> <span class="font-mono text-[11px] text-slate-400">{{ c.change_no }}</span>
</div> </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"> <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 }} 驳回原因{{ c.reject_comment }}
</p> </p>
<span v-if="c.updateTime" class="text-11px text-slate-400">驳回于 {{ c.updateTime }}</span> <span v-if="c.rejected_at" class="text-11px text-slate-400">驳回于 {{ c.rejected_at.slice(0, 16) }}</span>
</div> </div>
<!-- 右区 --> <!-- 右区 -->
<!-- 待我审批 --> <!-- 审批 -->
<div v-if="c.stage === 'pending'" class="flex gap-1.5"> <div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c)">去审批</NButton> <NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</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="warning" @click="openProgress(c)">进度</NButton>
<NButton class="text-xs" text type="error" @click="openClosure(c)">资料关闭</NButton> <NButton class="text-xs" text type="error" @click="openWithdraw(c)">撤回</NButton>
</div>
<!-- 审批通过 -->
<div v-if="c.status === 'APPROVED'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</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="openDocClose(c)">资料关闭</NButton>
</div> </div>
<!-- 驳回处理 --> <!-- 驳回处理 -->
<div v-if="c.stage === 'reject'" class="flex gap-1.5"> <div v-if="c.status === 'REJECTED'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'reject')">详情</NButton> <NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c,'reject')">进度</NButton> <NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
<NButton class="text-xs" text type="primary" @click="openDisposal(c,'reject')">修改重报</NButton> <NButton class="text-xs" text type="primary" @click="openDetail(c,'edit')">修改重报</NButton>
<NButton class="text-xs" text type="error" @click="openClosure(c,'reject')">关闭变更</NButton> <NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="closeChange(c)"
>
<template #trigger>
<NButton class="text-xs" text type="error">关闭变更</NButton>
</template>
确定关闭该变更吗
</NPopconfirm>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -485,40 +420,39 @@ const revokeSave = () => {
<div class="rounded-lg border bg-slate-50/60 px-3 py-2.5"> <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="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"> <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> <span class="font-mono">{{ currentRow?.change_no }}</span>
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow.status}}</NTag> <NTag v-if="currentRow?.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow.status}}</NTag> <NTag v-if="currentRow?.status==='APPROVED'" size="small" type="success">已通过</NTag>
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow.status}}</NTag> <NTag v-if="currentRow?.status==='IMPLEMENTED'" size="small" type="warning">待验收</NTag>
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow.status}}</NTag> <NTag v-if="currentRow?.status==='ACCEPTED'" size="small" type="error">待关闭</NTag>
<span v-if="currentRow?.overdue" class="font-medium text-red-500">超期 {{ currentRow?.day }} </span> <NTag v-if="currentRow?.status==='REJECTED'" size="small" type="error">已驳回</NTag>
<NTag v-if="currentRow?.status==='CLOSED'" size="small" type="error">已关闭</NTag>
<NTag v-if="currentRow?.status==='WITHDRAWN'" size="small" type="error">已撤回</NTag>
<span v-if="currentRow?.overdue_status===2" class="font-medium text-red-500">超期 {{ currentRow?.overdue_days }} </span>
</div> </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?.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 v-if="currentRow?.meta" class="mt-1 text-[11px] text-slate-400">{{ currentRow?.meta }}</div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div v-for="(s, i) in stages" :key="s" class="flex gap-3"> <div v-for="(s, i) in currentRow?.approval_records" :key="s.id" class="flex gap-3">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2" <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-[#7CC242] bg-[#7CC242] text-white">
: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 icon="ix:success" />
<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>
<span v-if="i < stages.length - 1" class="w-0.5 flex-1" :class="i < curIdx ? 'bg-[#7CC242]' : 'bg-slate-200'" /> <span
v-if="Number(i) < Number(currentRow?.approval_records?.length) - 1"
class="w-0.5 flex-1"
:class="Number(i) < Number(currentRow?.approval_records?.length) ? 'bg-[#7CC242]' : 'bg-slate-200'"
></span>
</div> </div>
<div class="pb-5"> <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'"> <p class="text-sm">{{ s.action }}</p>
{{ s }}{{ i === curIdx && !closed ? '(当前)' : '' }} <span class="mt-0.5 text-[11px] text-slate-400">{{s.finished_at?.slice(0, 16)}}</span>
</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> </div>
</div> <div v-if="currentRow?.duration_type === 2" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
<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?.restore_deadline ?? '' }} · 恢复 / 延期 / 转永久请在详情页中操作
临时变更计划恢复 {{ currentRow?.deadline ?? '' }}{{ currentRow?.extended ? ` · 延期后恢复 ${currentRow?.deadline}` : '' }} · 恢复 / 延期 / 转永久请在详情页中操作
</div> </div>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>
@@ -547,7 +481,7 @@ const revokeSave = () => {
</template> </template>
<template #footer> <template #footer>
<div class="flex justify-end gap-[10px]"> <div class="flex justify-end gap-[10px]">
<NButton size="small" @click="revokeCancel">取消</NButton> <NButton size="small" @click="revokeModal = false">取消</NButton>
<NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">保存</NButton> <NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">保存</NButton>
</div> </div>
</template> </template>
@@ -616,6 +550,7 @@ const revokeSave = () => {
</template> </template>
</NModal> </NModal>
</NCard> </NCard>
</NSpin>
</NSpace> </NSpace>
</template> </template>
-565
View File
@@ -1,565 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { systemConfigApi } from '@/service/api/system';
import { myTasksListApi, myTasksWithdrawApi, myTasksCloseApi } from '@/service/api/myTasks';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
// 当前行
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 typeList = ref<any[]>([]);
// 变更等级
const levelList = ref<any[]>([]);
// 提醒天数
const remindDays = ref<number>(7);
// 源数据
const dataSource = ref<any>(null);
// 列表
const list = ref<any[]>([]);
// 数量统计
const tabCount = ref<Record<string, number>>({
"all": 0,
"approval": 0,
"apply": 0,
"reject": 0,
});
// 选项卡
const currentTab = ref<string>("all");
// 选项卡切换
const tabChange = (k: string) => {
currentTab.value = k;
// 全部
if(k === "all"){
list.value = resetGrouped([...dataSource.value.my_applications,...dataSource.value.todo]);
}
// 待我审批
if(k === "approval"){
list.value = resetGrouped([...dataSource.value.todo]);
}
// 我的申请
if(k === "apply"){
list.value = resetGrouped([...dataSource.value.my_applications]);
}
// 驳回处理
if(k === "reject"){
const arr = [...dataSource.value.my_applications,...dataSource.value.todo];
const rejectedList = arr.filter((item: any) => item.status === "REJECTED");
list.value = resetGrouped(rejectedList);
}
};
const SUB_ORDERS: any[] = [];
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: any,type:string = '') => {
if(type==='detail'){
routerPushByKey("my_highriskdetail",{ query: { id: c.change_id.toString() } });
}
if(type==='edit'){
routerPushByKey("my_initiatechange",{ query: { id: c.change_id.toString() } });
}
};
// 打开资料关闭
const openDocClose = (c: any) => {
currentRow.value = c;
// routerPushByKey("my_docclose",{ query: { id: c.change_id.toString() } });
};
// 打开处置申请
const openDisposal = (c: any,) => {
currentRow.value = c;
disposalModal.value = true;
};
// 关闭变更
const closeChange = async (c: any) => {
const {error} = await myTasksCloseApi(c?.change_id);
if(!error){
window.$message?.success(`关闭变更成功`);
getList();
}
};
// 打开进度
const openProgress = (c: any) => {
currentRow.value = c;
progressDrawer.value = true;
};
// 打开撤回
const openWithdraw = (c: any) => {
revokeReason.value = "";
currentRow.value = c;
revokeModal.value = true;
};
// 撤回确认
const revokeSave = async () => {
if (!revokeReason.value) {
return window.$message?.warning("请填写撤回理由");
}
revokeSaving.value = true;
const {error} = await myTasksWithdrawApi(currentRow.value?.change_id,{reason:revokeReason.value});
if(!error){
revokeModal.value = false;
window.$message?.success(`撤回成功,申请退回草稿箱,审批记录保留可查,修改后可重新提交`);
getList();
}
revokeSaving.value = false;
};
// 匹配变更等级
const getChangeLevel = (level:number) => {
const changeLevel = levelList.value.find((item:any) => item.value == level);
return changeLevel || null;
}
// 按 status 归纳
const resetGrouped = (data:any) => {
const grouped = Object.values(
data.reduce((acc:any, item:any) => {
const key = item.status;
if (!acc[key]) {
acc[key] = { status: key, items: [] };
}
acc[key].items.push(item);
return acc;
}, {})
);
return grouped;
}
// 获取列表数据
const getList = async () => {
loading.value = true;
const {data,error} = await myTasksListApi();
if(!error){
dataSource.value = data;
tabChange(currentTab.value);
// 统计数量
// 全部
tabCount.value["all"] = [...dataSource.value.my_applications,...dataSource.value.todo].length;
// 待我审批
tabCount.value["approval"] = [...dataSource.value.todo].length;
// 我的申请
tabCount.value["apply"] = [...dataSource.value.my_applications].length;
// 驳回处理
const arr = [...dataSource.value.my_applications,...dataSource.value.todo];
const rejectedList = arr.filter((item: any) => item.status === "REJECTED");
tabCount.value["reject"] = rejectedList.length;
}
loading.value = false;
};
// 获取配置
const getConfig = async () => {
loading.value = true;
const {data,error} = await systemConfigApi();
if(!error){
// 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
if (changeTypeGroup) {
typeList.value = changeTypeGroup.items;
}
// 解析 变更等级设置
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelList.value = changeLevelGroup.items;
}
getList();
}
loading.value = false;
};
onMounted(() => {
getConfig();
});
</script>
<template>
<NSpace vertical :size="16">
<NSpin :show="loading" size="small">
<!-- 我的任务 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="h-100% flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<div @click="tabChange('all')"
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 === 'all' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
全部
<NTag size="tiny" round :type="currentTab === 'all' ?'default':'primary'">{{tabCount["all"]}}</NTag>
</div>
<div @click="tabChange('approval')"
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 === 'approval' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
待我审批
<NTag size="tiny" round :type="currentTab === 'approval' ?'default':'primary'">{{tabCount["approval"]}}</NTag>
</div>
<div @click="tabChange('apply')"
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 === 'apply' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
我的申请
<NTag size="tiny" round :type="currentTab === 'apply' ?'default':'primary'">{{tabCount["apply"]}}</NTag>
</div>
<div @click="tabChange('reject')"
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 === 'reject' ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100'">
驳回处理
<NTag size="tiny" round :type="currentTab === 'reject' ?'default':'error'">{{tabCount["reject"]}}</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">
{{tabCount["all"]}} · 点击行内按钮办理各阶段业务
</span>
</div>
<div class="space-y-2 scroll">
<div class="flex flex-col gap-3">
<div v-if="list.length===0" class="text-center text-slate-400">
暂无数据
</div>
<div
class="border border-slate-200 p-4"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
v-for="(item,index) in list"
:key="index"
>
<!-- 审批中 -->
<div v-if="item.status==='APPROVING'" class="flex items-center gap-2">
<Icon icon="ant-design:clock-circle-outlined" class="size-16px" :style="{ color: themeStore.otherColor.warning }" />
<p class="text-13px font-700" :style="{ color: themeStore.otherColor.warning }">审批中</p>
<p class="text-11px text-#94a3b8"> {{item.items.length}} · 审批流程推进中 · 可查看进度或撤回</p>
</div>
<!-- 已通过 -->
<div v-if="item.status==='APPROVED'" class="flex items-center gap-2">
<Icon icon="ix:success" class="size-16px" :style="{ color: themeStore.otherColor.success }" />
<p class="text-13px font-700" :style="{ color: themeStore.otherColor.success }">审批通过</p>
<p class="text-11px text-#94a3b8"> {{item.items.length}} · 待实施 / 待验收 / 待关闭 · 处置申请与资料关闭在行内办理</p>
</div>
<!-- 已驳回 -->
<div v-if="item.status==='REJECTED'" class="flex items-center gap-2">
<Icon icon="system-uicons:reset" class="size-16px" :style="{ color: themeStore.otherColor.error }" />
<p class="text-13px font-700" :style="{ color: themeStore.otherColor.error }">驳回处理</p>
<p class="text-11px text-#94a3b8">{{item.items.length}} 项待处理</p>
</div>
<div v-if="item.items.length===0" class="text-center text-slate-400 mt-3 border-t pt-4 text-xs">
暂无数据
</div>
<div v-if="item.items && item.items.length" class="flex flex-col gap-3 mt-3">
<div v-for="c in item.items" :key="c.change_no"
class="flex flex-wrap items-center gap-3 border px-4 py-3"
:class="c.overdue_status===2 ? 'border-red-200 bg-[#FFF2F0]' : c.overdue_status===1 ? 'border-amber-200 bg-amber-50/40' : ''"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<!-- 左区 -->
<!-- 审批中/已通过 -->
<div v-if="item.status==='APPROVING' || 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.change_no }}</span>
<!-- 变更等级 -->
<NTag
size="small"
:color="{
color: tagBgColor(getChangeLevel(c.change_level)?.color),
textColor: tagTextColor(getChangeLevel(c.change_level)?.color),
borderColor: tagBorderColor(getChangeLevel(c.change_level)?.color),
}"
>{{getChangeLevel(c.change_level)?.label}}</NTag>
<!-- 变更时限 -->
<NTag v-if="c.duration_type===2" size="small" type="warning">临时</NTag>
<NTag v-if="c.duration_type===1" size="small" type="primary">永久</NTag>
<NTag v-if="c.urgent===1" size="small" type="error">紧急</NTag>
<!-- 超期/临期/延期 -->
<NTag v-if="c.overdue_status===2" size="small" type="error">已超期 {{ c.overdue_days }} </NTag>
<NTag v-if="c.overdue_status===1" size="small" type="warning">临期 · {{ c.overdue_days }} </NTag>
<NTag v-if="c.extend_count" size="small">已延期 {{ c.extend_count }} </NTag>
<!-- 变更状态 -->
<NTag v-if="c.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="c.status==='APPROVED'" size="small" type="success">已通过</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.apply_time" class="text-[11px] text-slate-400">申请 {{ c.apply_time.slice(0, 16) }}</span>
</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.step_name }}</span>
<div v-if="c.current_step_members && c.current_step_members.length" class="flex items-center gap-1">
<div v-for="(m, mi) in c.current_step_members" :key="mi">
<NTag v-if="m.status==='PENDING'" size="small" type="warning">
<div class="flex items-center">
<Icon icon="tabler:clock" />{{ m.user_name }}
</div>
</NTag>
<NTag v-else size="small" type="success">
<div class="flex items-center">
<Icon icon="ix:success" />{{ m.user_name }}
</div>
</NTag>
</div>
</div>
<span v-if="c.status_changed_at" class="text-slate-400">状态更新 {{ c.status_changed_at.slice(0, 16) }}</span>
</div>
</div>
<!-- 驳回处理 -->
<NTag v-if="item.status==='REJECTED'" size="small" type="error">
<div class="flex items-center">
<Icon icon="system-uicons:reset" />驳回处理
</div>
</NTag>
<div v-if="item.status==='REJECTED'" 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.change_no }}</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.reject_comment }}
</p>
<span v-if="c.rejected_at" class="text-11px text-slate-400">驳回于 {{ c.rejected_at.slice(0, 16) }}</span>
</div>
<!-- 右区 -->
<!-- 审批中 -->
<div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</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.status === 'APPROVED'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</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="openDocClose(c)">资料关闭</NButton>
</div>
<!-- 驳回处理 -->
<div v-if="c.status === 'REJECTED'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
<NButton class="text-xs" text type="primary" @click="openDetail(c,'edit')">修改重报</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="closeChange(c)"
>
<template #trigger>
<NButton class="text-xs" text type="error">关闭变更</NButton>
</template>
确定关闭该变更吗
</NPopconfirm>
</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?.change_no }}</span>
<NTag v-if="currentRow?.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="currentRow?.status==='APPROVED'" size="small" type="success">已通过</NTag>
<NTag v-if="currentRow?.status==='IMPLEMENTED'" size="small" type="warning">待验收</NTag>
<NTag v-if="currentRow?.status==='ACCEPTED'" size="small" type="error">待关闭</NTag>
<NTag v-if="currentRow?.status==='REJECTED'" size="small" type="error">已驳回</NTag>
<NTag v-if="currentRow?.status==='CLOSED'" size="small" type="error">已关闭</NTag>
<NTag v-if="currentRow?.status==='WITHDRAWN'" size="small" type="error">已撤回</NTag>
<span v-if="currentRow?.overdue_status===2" class="font-medium text-red-500">超期 {{ currentRow?.overdue_days }} </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 currentRow?.approval_records" :key="s.id" 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 border-[#7CC242] bg-[#7CC242] text-white">
<Icon icon="ix:success" />
</span>
<span
v-if="Number(i) < Number(currentRow?.approval_records?.length) - 1"
class="w-0.5 flex-1"
:class="Number(i) < Number(currentRow?.approval_records?.length) ? 'bg-[#7CC242]' : 'bg-slate-200'"
></span>
</div>
<div class="pb-5">
<p class="text-sm">{{ s.action }}</p>
<span class="mt-0.5 text-[11px] text-slate-400">{{s.finished_at?.slice(0, 16)}}</span>
</div>
</div>
</div>
<div v-if="currentRow?.duration_type === 2" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
临时变更计划恢复 {{ currentRow?.restore_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="revokeModal = false">取消</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>
</NSpin>
</NSpace>
</template>
<style scoped>
</style>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 260px);
overflow-y: auto;
}
</style>