@@ -1,361 +1,453 @@
< script setup lang = "ts" >
import { computed } from 'vue' ;
import { useAppStore } from '@/store/modules/app' ;
import { computed , onMounted , onUnmounted , ref } from 'vue' ;
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme' ;
import {
CHANGES , DOC _TODO , LEVEL _COLOR , STATUS _COLOR , TEMP _WATCH , URGE _RECORDS ,
countOpenStatus , type ChangeLevel , type ChangeOrder , type ChangeStatus , type ChangeType ,
} from "@/typings/model " ;
import { getColorWithOpacity } from "@/utils/common" ;
import FlowRing from "./modules/FlowRing.vue" ;
import { useRouterPush } from '@/hooks/common/router' ;
import { getColorWithOpacity , tagTextColor , tagBgColor , tagBorderColor } from '@/utils/common' ;
import { systemConfigApi } from "@/service/api/system" ;
import { homeApi } from "@/service/api/statistics " ;
const appStore = useAppStore ( ) ;
const themeStore = useThemeStore ( ) ;
const gap = computed ( ( ) => ( appStore . isMobile ? 0 : 16 ) ) ;
const { routerPushByKey } = useRouterPush ( ) ;
const emit = defineEmits < { go : [ v : string , preset ? : string ] ; openChange : [ c : ChangeOrder ] } > ( ) ;
const DOC _TODO = [
{ id : 1 , title : "DCS 罐区液位报警值 LIA-301 调整" , missing : "验收报告与运行数据" } ,
{ id : 2 , title : "紧急变更:V-102 安全阀起跳压力临时调整(补办手续)" , missing : "更新后的 P&ID / 操作规程(受控最新版)、设备 / 联锁台账更新记录" } ,
] ;
// 我的变更进度
const MY _CLOSED = [
{ id : "MOC-2026-R01-0021" , title : "T-101 塔顶冷凝器清洗周期调整" , type : "管理" , level : "一般" , status : "已关闭" , date : "2026-05-18" , duration : "永久" } ,
{ id : "MOC-2026-R01-0017" , title : "夏季循环水旁滤临时停用(已恢复)" , type : "设备" , level : "一般" , status : "已关闭" , date : "2026-04-02" , duration : "临时" } ,
] ;
const ROLES = [
{ id : "applicant" , name : "申请人" , person : "张工艺" , org : "一车间 · 工艺组" , desc : "发起变更申请、AI 辅助判定、跟踪进度" } ,
{ id : "process" , name : "专业会签(仪表)" , person : "王仪表" , org : "技术部 · 仪表科" , desc : "专业会签、HAZOP 分析、确认连带变更" } ,
{ id : "process2" , name : "专业会签(设备)" , person : "周设备" , org : "设备管理部 · 设备科" , desc : "专业会签、设备完整性检查、行动项落实" } ,
{ id : "safety" , name : "安全工程师" , person : "王海燕" , org : "安全环保部" , desc : "安全审查、PSSR 检查表确认、风险分析组织" } ,
{ id : "dept" , name : "车间主任" , person : "李主任" , org : "一车间" , desc : "部门审核、现场验收、变更关闭确认" } ,
{ id : "vp" , name : "分管领导" , person : "刘副总" , org : "公司领导" , desc : "重要变更最终批准、移动端审批" } ,
{ id : "admin" , name : "系统管理员" , person : "刘敏" , org : "信息化部" , desc : "流程配置、字典维护、权限分配" } ,
{ id : "quality" , name : "质量工程师" , person : "陈质量" , org : "质量部" , desc : "质量会签、产品合格判定、检验方案确认" } ,
{ id : "viewer" , name : "非审批人员" , person : "吴新" , org : "相关部门" , desc : "以信息查看为主,不参与变更流程" } ,
] ;
const mine = computed ( ( ) => CHANGES . filter ( ( c ) => c . applicant === ROLES [ 0 ] . person ) ) ;
const myRows = computed ( ( ) => [
... mine . value . map ( ( c : ChangeOrder ) => ( { c , id : c . id , title : c . title , type : c . type , level : c . level , status : c . status , date : c . date , urgent : c . urgent , duration : c . duration } ) ) ,
... MY _CLOSED . map ( ( m ) => ( { c : null , ... m , urgent : false } ) ) ,
] ) ;
const BIG _STAGES = [ "申请审批" , "实施与关闭" , "变更关闭" ] ;
const bigStageOf = ( s : any ) => ( s === "审批中" ? 0 : s === "已关闭" ? 2 : 1 ) ;
const openRow = ( row : any ) => {
if ( row . c ) {
emit ( "openChange" , row . c ) ;
} else {
window . $message ? . info ( ` 演示:查看 ${ row . id } 归档详情 ` ) ;
}
} ;
const loading = ref < boolean > ( false ) ;
// 变更临期天数
const remindDays = ref < number > ( 0 ) ;
// 变更类型列表
const typeList = ref < any > ( [ ] ) ;
// 变更等级列表
const levelList = ref < any > ( [ ] ) ;
// 时间显示
const clock = ref < string > ( '' ) ;
let timer : any = null ;
// 首页数据
const homeData = ref < any > ( null ) ;
// 尚未关闭总数
const openTotal = computed ( ( ) => {
return homeData . value ? . unclosed _status ? . items ? . reduce ( ( sum : number , item : any ) => sum + item . count , 0 ) || 0 ;
} ) ;
// 临时变更到期提醒情况
const tempRemind = TEMP _WATCH ;
const tempTotal = tempRemind . reduce ( ( a , b ) => a + b . n , 0 ) ;
// 尚未关闭变更所处状态
const openStatus = computed ( ( ) => countOpenStatus ( mine . value ) ) ;
const openTotal = computed ( ( ) => openStatus . value . reduce ( ( a , b ) => a + b . n , 0 ) ) ;
// 被驳回信息处理
const REJECTED _ITEMS = [
{ id : "MOC-2026-R01-0033" , title : "P-301 机封冲洗方案调整" , reason : "变更前后对比描述不完整,缺少冲洗流量核算数据" , by : "李主任 · 车间主任" , date : "2026-07-29" } ,
{ id : "MOC-2026-R01-0029" , title : "活性炭更换周期延长(3→6 个月)" , reason : "风险分析未覆盖吸附饱和穿透情景,请补充 JSA 分析" , by : "王海燕 · 安全工程师" , date : "2026-07-25" } ,
] ;
// 资料管理
const DOC _LINKS = [ { name : '受控文件库' , desc : '现行版规程、图纸、数据表' } , { name : '变更档案查询' , desc : '历史变更档案调取' } ]
const tempTotal = computed ( ( ) => {
return ( homeData . value ? . dept _stats ? . unclosed _total || 0 ) + ( homeData . value ? . dept _stats ? . near _overdue _total || 0 ) + ( homeData . value ? . dept _stats ? . overdue _total || 0 ) ;
} ) ;
// 跳转变更申请页面、变更台账、我的任务、发起变更
const jumpTo = ( key : string , qid : number = 0 ) => {
if ( key === 'my_initiatechange' && qid ) {
routerPushByKey ( key , { query : { id : qid . toString ( ) } } ) ;
} else if ( key === 'details_closedoc' && qid ) {
routerPushByKey ( key , { query : { id : qid . toString ( ) , key : 'home' } } ) ;
} else {
routerPushByKey ( key as any ) ;
}
}
// 打开变更详情
const openDetail = ( row : any ) => {
routerPushByKey ( "details_index" , { query : { id : row ? . change _id ? . toString ( ) , status : row ? . status ? . toString ( ) || '' , key : 'home' } } ) ;
} ;
// 获取当前时间字符串
const getCurrentTimeStr = ( ) => {
const now = new Date ( ) ;
const year = now . getFullYear ( ) ;
const month = now . getMonth ( ) + 1 ;
const date = now . getDate ( ) ;
const week = [ '星期日' , '星期一' , '星期二' , '星期三' , '星期四' , '星期五' , '星期六' ] [ now . getDay ( ) ] ;
const hours = String ( now . getHours ( ) ) . padStart ( 2 , '0' ) ;
const minutes = String ( now . getMinutes ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ year } 年 ${ month } 月 ${ date } 日 ${ week } ${ hours } : ${ minutes } ` ;
}
// 更新时间显示
const tick = ( ) => {
clock . value = getCurrentTimeStr ( ) ;
// 计算距离下一分钟还有多少毫秒(+50ms 容错,防止定时器提前触发)
const now = new Date ( ) ;
const delay = ( 60 - now . getSeconds ( ) ) * 1000 - now . getMilliseconds ( ) + 50 ;
timer = setTimeout ( tick , delay ) ;
}
// 匹配对象
const matchObj = ( list : any , val : number ) => {
const obj = list . find ( ( item : any ) => item . value == val ) ;
return obj || null ;
}
// 获取数据
const getData = async ( ) => {
loading . value = true ;
const { data , error } = await homeApi ( ) ;
if ( ! error ) {
homeData . value = data ;
console . log ( data )
}
loading . value = false ;
}
// 获取系统配置
const getConfig = async ( ) => {
const { data , error } = await systemConfigApi ( ) ;
if ( ! error ) {
// 解析 临时变更临期提醒
const remindDaysGroup = data . find ( ( group : { group _key : string } ) => group . group _key === 'change_remind' ) ;
if ( remindDaysGroup ) {
remindDaysGroup ? . items . forEach ( ( item : any ) => {
if ( item . item _key === 'temp_due_remind_days' ) {
remindDays . value = Number ( item . value ) ;
}
} )
}
// 解析 变更类型设置
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 ;
}
}
}
onMounted ( ( ) => {
tick ( ) ;
getConfig ( ) ;
getData ( ) ;
} )
onUnmounted ( ( ) => {
if ( timer ) {
clearTimeout ( timer ) ;
timer = null ;
}
} )
< / script >
< template >
< NSpace vertical :size = "16" >
<!-- 我的工作台 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' } ">
< NGrid :x-gap = "gap" :y-gap = "16" responsive = "screen" item -responsive >
< NGi span = "24 s:24 m:16" >
< div class = "flex-y-center " >
< p
class = "size-32px flex items-center justify-center bg-[rgb(var(--primary-color))] text-white text-xs font-bold"
: style = "{ borderRadius: themeStore.themeRadius + 'px' } "
>
MOC
< / p >
< div class = "pl-12px" >
< p class = "text-14px font-semibold" > 我的工作台 < / p >
< p class = "text-#999 text-11px" > 车间工程师操作台 · 发起变更 、 推进实施 、 完成闭环 < / p >
< NSpace vertical :size = "16" >
< NSpin :show = "loading" size = "small" >
< div class = "space-y-3.5 ">
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< NGrid :x-gap = "16" :y-gap = "16" responsive = "screen" item -responsive >
< NGi span = "24 s:24 m:16 " >
< div class = "flex-y-center" >
< p
class = "py-1.5 px-1.5 flex items-center justify-center bg-[rgb(var(--primary-color))] text-white text-xs font-bold "
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
>
MOC
< / p >
< p class = "pl-12px text-14px font-semibold" > 我的工作台 < / p >
< / div >
< / div >
< / NGi >
< NGi span = "24 s:24 m:8" class = "flex items-center justify-end gap-12px" >
< p class = "text-#999 text-xs" > 2026 年8月10日 星期一 10 : 21 < / p >
< NButton type = "primary" size = "small" >
< Icon icon = "ic:round-plus" class = "size-18px" / >
发起变更
< / NButton >
< / NGi >
< / NGrid >
< / NCard >
<!-- 卡片数据 -- >
< NGrid cols = "s:1 m:2 l:4" responsive = "screen" :x-gap = "gap" :y-gap = "16" >
< NGi >
< div
class = "flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="emit('go', 'tasks')"
>
< p class = "text-12px font-600" > 待修改驳回 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
:end-value = "2"
class = "text-26px font-600"
/ >
< / div >
< p class = "text-12px" > 修改后重新提交 < / p >
< / div >
< / NGi >
< NGi >
< div
class = "flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="emit('go', 'tasks')"
>
< p class = "text-12px font-600" > 待上传资料 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
:end-value = "2"
class = "text-26px font-600 text-[#333]"
/ >
< / div >
< p class = "text-12px" > 培训 / PSSR / 验收材料补交 < / p >
< / div >
< / NGi >
< NGi >
< div
class = "flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="emit('go', 'tasks')"
>
< p class = "text-12px font-600" > 临时变更到期 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
:end-value = "1"
class = "text-26px font-600 text-[#333]"
/ >
< / div >
< p class = "text-12px" > 到期 ≤ 7 天 · 及时处置 < / p >
< / div >
< / NGi >
< NGi >
< div
class = "flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="emit('go', 'tasks')"
>
< p class = "text-12px font-600" > 超期未处置 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
:end-value = "1"
class = "text-26px font-600"
/ >
< / div >
< p class = "text-12px" > 超期临时变更 · 需恢复 / 延期 / 转永久 < / p >
< / div >
< / NGi >
< / NGrid >
< NGrid :x-gap = "gap" :y-gap = "16" responsive = "screen" item -responsive >
<!-- 左侧 -- >
< NGi span = "24 s:24 m:24 l:16" >
<!-- 我的变更进度 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center justify-between mb-3" >
< div class = "flex items-center gap-2" >
< Icon icon = "akar-icons:file" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 我的变更进度 < / p >
< p class = "text-11px text-#94a3b8" > 阶段进度与台账实时联动 < / p >
< / div >
< NButton ghost size = "small" class = "text-xs" @click ="emit('go', 'ledger')" >
全部台账
< Icon icon = "formkit:right" class = "size-12px" / >
< / NButton >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "row in myRows.slice(0, 3)" :key="row.id" @click="openRow(row)"
class = "cursor-pointer flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
: style = "{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border': themeStore.themeColor,
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.02)
}"
>
< FlowRing :status = "row.status" / >
< div class = "min-w-0 flex-1" >
< div class = "flex flex-wrap items-center gap-2" >
< span class = "font-mono text-[11px] text-slate-400" > { { row . id } } < / span >
< NTag class = "text-[11px]" v-if = "row.urgent" type="error" size="small" > 紧急 < / NTag >
< NTag class = "text-[11px]" v-if = "row.level==='重要'" type="error" size="small" > {{ row.level }} < / NTag >
< NTag class = "text-[11px]" v-else size = "small" > { { row . level } } < / NTag >
< NTag class = "text-[11px]" size = "small" > { { row . type } } < / NTag >
< NTag class = "text-[11px]" v-if = "row.duration==='临时'" type="warning" size="small" > {{ row.duration }} < / NTag >
< NTag class = "text-[11px]" v-else type = "info" size = "small" > { { row . duration } } < / NTag >
< span class = "ml-auto text-[11px] text-slate-400" > 申请时间 { { row . date } } < / span >
< / div >
< div class = "mt-1 flex items-center gap-2" >
< span class = "min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white" > { { row . title } } < / span >
< span class = "shrink-0 text-[11px] text-slate-400" > 当前状态 < / span >
< NTag class = "text-[11px]" v-if = "row.status==='审批中' || row.status==='实施中' || row.status==='待PSSR' || row.status==='待验收'" type="warning" size="small" > {{ row.status }} < / NTag >
< NTag class = "text-[11px]" v-if = "row.status==='待关闭'" type="error" size="small" > {{ row.status }} < / NTag >
< NTag class = "text-[11px]" v-if = "row.status==='已批准'" type="success" size="small" > 已批准 < / NTag >
< NTag class = "text-[11px]" v-if = "row.status==='已关闭'" type="info" size="small" > 已关闭 < / NTag >
< / div >
< div class = "mt-0.5 truncate text-[11px] text-slate-400" >
< template v-if = "row.status === '已关闭'" > 3 / 3 阶段全部完成 , 流程闭环已归档 < / template >
< template v-else > 当前阶段 : {{ BIG_STAGES [ bigStageOf ( row.status ) ] }} ( {{ bigStageOf ( row.status ) + 1 }} / 3 ) {{ row.c ? .flow.find ( ( n ) = > n . status === 'current' ) ? ` · 当前节点: ${ row . c . flow . find ( ( n ) => n . status === 'current' ) ! . name } ` : "" } } < / template >
< / div >
< / div >
< / div >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="emit('go', 'ledger')" >
还有2条 , 前往台账查看全部 →
< / NButton >
< / div >
< / NCard >
< NGrid :x-gap = "gap" :y-gap = "16" responsive = "screen" item -responsive class = "mt-[16px]" >
<!-- 临时变更到期提醒情况 -- >
< NGi span = "12 s:12 m:12 l:12" >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "lets-icons:clock" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 临时变更到期提醒情况 < / p >
< p class = "text-11px text-#94a3b8" > 共 { { tempTotal } } 项 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "t in tempRemind" :key="t.label" class="rounded-lg border px-3 py-2" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "h-2 w-2 shrink-0 rounded-full" : style = "{ background: t.c }" / >
< span class = "font-medium text-slate-700" > { { t . label } } < / span >
< span class = "ml-auto font-semibold text-slate-600" > { { t . n } } 项 < / span >
< / div >
< div class = "mt-1 pl-4 text-[11px] text-slate-400" > { { t . note } } < / div >
< / div >
< div class = "text-[11px] text-slate-400" > 提醒规则 : 到期前 7 天触发 ( 可在 「 待我处理 · 我的申请 」 中调整 ) · 临期 / 超期项请按期办理延期 、 恢复或转永久 < / div >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="emit('go', 'tasks')" >
前往 「 待我处理 」 办理延期 / 恢复 / 转永久 →
< / NButton >
< / div >
< / NCard >
< / NGi >
< ! - - 尚未关闭的变更所处状态 - - >
< NGi span = "12 s:12 m:12 l:12" >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px',height:'100%' } ">
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "akar-icons:file" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 尚未关闭的变更所处状态 < / p >
< p class = "text-11px text-#94a3b8" > 共 { { openTotal } } 项 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "s in openStatus" :key="s.name" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "min-w-0 flex-1 text-slate-700" > { { s . name } } < / span >
< span class = "shrink-0 font-semibold text-slate-600" > { { s . n } } 项 < / span >
< / div >
< div class = "mt-1 h-1.5 w-full overflow-hidden rounded-full bg-slate-100" >
< div class = "h-full rounded-full" : style = "{ width: (openTotal ? (s.n / openTotal) * 100 : 0) + '%', background: s.c }" / >
< / div >
< / div >
< div class = "text-[11px] text-slate-400" > 阶段口径 : 审批中 ( 含会签 ) → 已批准待投用 → 待验收 → 待关闭 · 培训 / PSSR 线下签字后拍照上传 , 并入资料归档 < / div >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="emit('go', 'ledger')" >
明细见变更台账 →
< / NButton >
< / div >
< / NCard >
< NGi span = "24 s:24 m:8" class = "flex items-center justify-end gap-12px" >
< p class = "text-#999 text-xs" > { { clock } } < / p >
< NButton type = "primary" size = "small" @click ="jumpTo('my_initiatechange') ">
< Icon icon = "ic:round-plus" class = "size-18px" / >
发起变更
< / NButton >
< / NGi >
< / NGrid >
< / NGi >
< ! - - 右侧 - - >
< NGi span = "24 s:24 m:24 l:8 " >
<!-- 被驳回信息处理 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "system-uicons:reset" class = "size-16px" : style = "{ color : themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 被驳回信息处理 < / p >
< p class = "text-11px text-#94a3b8" > 修改后可重新提交 < / p >
< / div >
< div class = "flex flex-col gap-3 " >
< div
v-for = "r in REJECTED_ITEMS.slice(0, 1)"
:key = "r.id"
class = "border border-red-100 bg-red-50/50 px-3.5 py-2.5 "
: style = "{
borderRadius: themeStore.themeRadius + 'px',
}"
>
< div class = "flex items-center gap-2" >
< NTag type = "error" size = "small" class = "text-11px" > 已驳回 < / NTag >
< span class = "font-mono text-[11px] text-slate-400" > { { r . id } } < / span >
< span class = "ml-auto text-[11px] text-slate-400" > { { r . date } } < / span >
< / div >
< div class = "mt-0.5 text-sm font-medium text-slate-800" > { { r . title } } < / div >
< div class = "mt-0.5 line-clamp-1 text-[11px] leading-4 text-red-600/80" > 驳回原因 : { { r . reason } } < / div >
< div class = "mt-0.5 text-[11px] text-slate-400" > 驳回人 : { { r . by } } < / div >
< NButton ghost type = "error" size = "small" class = "text-xs mt-1.5" @click ="emit('go', 'apply')" > 修改重报 → < / NButton >
< / NCard >
<!-- 卡片数据 -- >
< NGrid cols = "s:1 m:2 l:4" responsive = "screen" :x-gap = "16" :y-gap = "16 ">
< NGi >
< div
class = "flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
: style = "{ borderRadius : themeStore.themeRadius + 'px' }"
@click ="jumpTo('my_mytask')"
>
< p class = "text-12px font-600" > 待修改驳回 < / p >
< div class = "flex justify-between " >
< CountTo
:start-value = "0"
: end -value = " homeData ? .dept_stats ? .reject_count | | 0 "
class = "text-26px font-600 "
/ >
< / div >
< NButton v-if = "REJECTED_ITEMS.length > 1" dashed type="error" size="small" class="text-xs" @click="emit('go', 'tasks')" >
还有 {{ REJECTED_ITEMS.length - 1 }} 条驳回 , 前往我的任务处理 →
< / NButton >
< p class = "text-12px" > 修改后重新提交 < / p >
< / div >
< / NCard >
< ! - - 资料归档待办 - - >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "quill:folder-open" class = "size-16px" : style = "{ color : themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 资料归档待办 < / p >
< p class = "text-11px text-#94a3b8" > { { DOC _TODO . length } } 项待补交 < / p >
< / NGi >
< NGi >
< div
class = "flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
: style = "{ borderRadius : themeStore.themeRadius + 'px' }"
@click ="jumpTo('my_mytask')"
>
< p class = "text-12px font-600" > 待上传资料 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
: end -value = " homeData ? .archive_todo ? .total | | 0 "
class = "text-26px font-600 text-[#333]"
/ >
< / div >
< p class = "text-12px" > 培训 / PSSR / 验收材料补交 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "d in DOC_TODO" :key="d.id" class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 px-3.5 py-2.5" >
< / NGi >
< NGi >
< div
class = "flex-1 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="jumpTo('my_mytask')"
>
< p class = "text-12px font-600" > 临时变更到期 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
: end -value = " homeData ? .dept_stats ? .near_overdue_total | | 0 "
class = "text-26px font-600 text-[#333]"
/ >
< / div >
< p class = "text-12px" > 到期 ≤ { { remindDays } } 天 · 及时处置 < / p >
< / div >
< / NGi >
< NGi >
< div
class = "flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer"
: style = "{ borderRadius: themeStore.themeRadius + 'px' }"
@click ="jumpTo('my_mytask')"
>
< p class = "text-12px font-600" > 超期未处置 < / p >
< div class = "flex justify-between" >
< CountTo
:start-value = "0"
: end -value = " homeData ? .dept_stats ? .overdue_total | | 0 "
class = "text-26px font-600"
/ >
< / div >
< p class = "text-12px" > 超期临时变更 · 需恢复 / 延期 / 转永久 < / p >
< / div >
< / NGi >
< / NGrid >
< NGrid :x-gap = "16" :y-gap = "16" responsive = "screen" item -responsive >
<!-- 左侧 -- >
< NGi span = "24 s:24 m:24 l:16" >
<!-- 我的变更进度 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center justify-between mb-3" >
< div class = "flex items-center gap-2" >
< NTag type = "warning " size = "small" class = "text-11px" > 待归档 < / NTag >
< span class = "font-mono text-[11px] text-slate-400" > { { d . id } } < / span >
< Icon icon = "akar-icons:file " class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 我的变更进度 < / p >
< p class = "text-11px text-#94a3b8 mt-2px" > 阶段进度与台账实时联动 < / p >
< / div >
< div class = "mt-0.5 text-sm font-medium text-slate-800" > { { d . title } } < / div >
< div class = "mt-0.5 line-clamp-1 text-[11px] leading-4 text-slate-400" > 缺项 : { { d . missing } } < / div >
< NButton type = "primary" size = "small" class = "text-xs mt-1.5" @click ="emit('go', 'tasks')" >
去补交 ( 资料关闭页上传 ) →
< NButton v-if = "homeData?.my_changes_progress?.total > 0" ghost size="small" class="text-xs" @click="jumpTo('changeledger')" >
全部台账
< Icon icon = "formkit:right" class = "size-12px" / >
< / NButton >
< / div >
< div class = "text-[11px] leading-4 text-slate-400" > 上传纸质扫描件 / 照片即视为完成确认并联动归档清单 ; 支持传错删除重传 、 阶段性保存 < / div >
< / div >
< / NCard >
<!-- 资料管理 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "quill:folder-open" class = "size-16px" : style = "{ color : themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 资料管理 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "d in DOC_LINKS" :key="d.name" @click="emit('go', 'knowledge')"
class = "cursor-pointer flex w-full items-center justify-between border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
: style = "{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border': themeStore.themeColor,
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.02)
} "
>
< div class = "flex items-center flex-1 gap-3" >
< Icon icon = "quill:folder-open" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< div class = "min-w-0" >
< p class = "truncate text-xs font-semibold text-slate-700" > { { d . name } } < / p >
< p class = "truncate text-[11px] text-slate-400" > { { d . desc } } < / p >
< div class = "flex flex-col gap-3" >
< template v-if = "homeData?.my_changes_progress?.total > 0" >
< div v-for = "row in homeData?.my_changes_progress?.items?.slice(0, 3) || []" :key="row.change_id" @click="openDetail(row)"
class = "cursor-pointer flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
: style = "{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border' : themeStore.themeColor,
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.05)
}"
>
< div class = "min-w-0 flex-1" >
< div class = "flex flex-wrap items-center gap-2" >
< span class = "font-mono text-[11px] text-slate-400" > { { row . change _no } } < / span >
< NTag class = "text-[11px]" v-if = "row.urgent===1" type="error" size="small" > 紧急 < / NTag >
< ! - - 变更等级 - - >
< NTag
size = "small "
: color = "{
color: tagBgColor(matchObj(levelList,row.change_level)?.color),
textColor: tagTextColor(matchObj(levelList,row.change_level)?.color),
borderColor: tagBorderColor(matchObj(levelList,row.change_level)?.color),
}"
> { { matchObj ( levelList , row . change _level ) ? . label } } < / NTag >
<!-- 变更类型 -- >
< NTag
size = "small"
: color = "{
color: tagBgColor(matchObj(typeList,row.change_type)?.color),
textColor: tagTextColor(matchObj(typeList,row.change_type)?.color),
borderColor: tagBorderColor(matchObj(typeList,row.change_type)?.color),
}"
> { { matchObj ( typeList , row . change _type ) ? . label } } < / NTag >
< NTag class = "text-[11px]" v-if = "row.duration_type===2" type="warning" size="small" > 临时 < / NTag >
< NTag class = "text-[11px]" v-else type = "info" size = "small" > 永久 < / NTag >
< span v-if = "row.apply_time" class="ml-auto text-[11px] text-slate-400" > 申请时间 {{ row.apply_time ? .slice ( 0 , 10 ) }} < / span >
< / div >
< div class = "mt-1 flex items-center gap-2" >
< span class = "min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white" > { { row . title } } < / span >
< span class = "shrink-0 text-[11px] text-slate-400" > 当前状态 < / span >
<!-- 状态标签 -- >
< NTag v-if = "row.status==='APPROVING'" size="small" type="warning" > 审批中 < / NTag >
< NTag v-if = "row.status==='APPROVED'" size="small" type="success" > 已通过 < / NTag >
< NTag v-if = "row.status==='IMPLEMENTED'" size="small" type="success" > 待验收 < / NTag >
< NTag v-if = "row.status==='ACCEPTED'" size="small" type="error" > 待关闭 < / NTag >
< NTag v-if = "row.status==='CLOSED'" size="small" type="success" > 已关闭 < / NTag >
< NTag v-if = "row.status==='WITHDRAWN'" size="small" type="error" > 已撤回 < / NTag >
< NTag v-if = "row.status==='REJECTED'" size="small" type="error" > 已驳回 < / NTag >
< / div >
< div class = "mt-0.5 truncate text-[11px] text-slate-400" >
< p v-if = "row.status === 'CLOSED'" > 阶段全部完成 , 流程闭环已归档 < / p >
< p v-else > 当前节点 : {{ row.step_name }} < / p >
< / div >
< / div >
< / div >
< / div >
< Icon icon = "formkit:right" class = "size-14px text-slate-400" / >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="jumpTo('changeledger')" >
还有 {{ ( homeData ? .my_changes_progress ? .total - 3 ) > 0 ? ( homeData ? . my _changes _progress ? . total - 3 ) : 0 } } 条 , 前往台账查看全部 →
< / NButton >
< / template >
< template v-else >
< p class = "text-xs text-slate-400 text-center" > 暂无数据 < / p >
< / template >
< / div >
< / div >
< / NCard >
< / NGi >
< / NGrid >
< / NSpace >
< / NCard >
< NGrid :x-gap = "16" :y-gap = "16" responsive = "screen" item -responsive class = "mt-[16px]" >
<!-- 临时变更到期提醒情况 -- >
< NGi span = "12 s:12 m:12 l:12" >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "lets-icons:clock" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 临时变更到期提醒情况 < / p >
< p class = "text-11px text-#94a3b8" > 共 { { tempTotal } } 项 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div class = "rounded-lg border px-3 py-2" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "h-2 w-2 rounded-full" : style = "{ background: themeStore.otherColor.success }" > < / span >
< span class = "font-medium text-slate-700" > 正常 ( 到期 > { { remindDays } } 天 ) < / span >
< span class = "ml-auto font-semibold text-slate-600" > { { homeData ? . dept _stats ? . unclosed _total || 0 } } 项 < / span >
< / div >
< / div >
< div class = "rounded-lg border px-3 py-2" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "h-2 w-2 rounded-full" : style = "{ background: themeStore.otherColor.warning }" > < / span >
< span class = "font-medium text-slate-700" > 临期 ( 到期 ≤ { { remindDays } } 天 ) < / span >
< span class = "ml-auto font-semibold text-slate-600" > { { homeData ? . dept _stats ? . near _overdue _total || 0 } } 项 < / span >
< / div >
< / div >
< div class = "rounded-lg border px-3 py-2" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "h-2 w-2 rounded-full" : style = "{ background: themeStore.otherColor.error }" > < / span >
< span class = "font-medium text-slate-700" > 已超期 < / span >
< span class = "ml-auto font-semibold text-slate-600" > { { homeData ? . dept _stats ? . overdue _total || 0 } } 项 < / span >
< / div >
< / div >
< div class = "text-[11px] text-slate-400" > 提醒规则 : 到期前 { { remindDays } } 天触发 ( 可在 「 我的任务 · 我的申请 」 中调整 ) · 临期 / 超期项请按期办理延期 、 恢复或转永久 < / div >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="jumpTo('my_mytask')" >
前往 「 我的任务 」 办理延期 / 恢复 / 转永久 →
< / NButton >
< / div >
< / NCard >
< / NGi >
< ! - - 尚未关闭的变更所处状态 - - >
< NGi span = "12 s:12 m:12 l:12" >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px',height:'100%' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "akar-icons:file" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 尚未关闭的变更所处状态 < / p >
< p class = "text-11px text-#94a3b8" > 共 { { openTotal } } 项 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "s in homeData?.unclosed_status?.items" :key="s.status" >
< div class = "flex items-center gap-2 text-xs" >
< span class = "min-w-0 flex-1 text-slate-700" > { { s . label } } < / span >
< span class = "shrink-0 font-semibold text-slate-600" > { { s . count } } 项 < / span >
< / div >
< div class = "mt-1 h-1.5 w-full overflow-hidden rounded-full bg-slate-100" >
<!-- 审批中 -- >
< div v-if = "s.status === 'APPROVING'" class="h-full rounded-full" :style="{ width: `${openTotal ? (s.count / openTotal) * 100 : 0}%`, background: themeStore.otherColor.warning }" > < / div >
< ! - - 已驳回 - - >
< div v-if = "s.status === 'REJECTED'" class="h-full rounded-full" :style="{ width: `${openTotal ? (s.count / openTotal) * 100 : 0}%`, background: themeStore.otherColor.error }" > < / div >
< ! - - 已批准待投用 - - >
< div v-if = "s.status === 'APPROVED'" class="h-full rounded-full" :style="{ width: `${openTotal ? (s.count / openTotal) * 100 : 0}%`, background: themeStore.themeColor }" > < / div >
< ! - - 待验收 - - >
< div v-if = "s.status === 'IMPLEMENTED'" class="h-full rounded-full" :style="{ width: `${openTotal ? (s.count / openTotal) * 100 : 0}%`, background: themeStore.otherColor.success }" > < / div >
< ! - - 待关闭 - - >
< div v-if = "s.status === 'ACCEPTED'" class="h-full rounded-full" :style="{ width: `${openTotal ? (s.count / openTotal) * 100 : 0}%`, background: themeStore.otherColor.error }" > < / div >
< / div >
< / div >
< div class = "text-[11px] text-slate-400" > 阶段口径 : 审批中 ( 含会签 ) → 已批准待投用 → 待验收 → 待关闭 · 培训 / PSSR 线下签字后拍照上传 , 并入资料归档 < / div >
< NButton dashed type = "primary" size = "small" class = "text-xs" @click ="jumpTo('changeledger')" >
明细见变更台账 →
< / NButton >
< / div >
< / NCard >
< / NGi >
< / NGrid >
< / NGi >
< ! - - 右侧 - - >
< NGi span = "24 s:24 m:24 l:8" >
<!-- 被驳回信息处理 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "system-uicons:reset" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 被驳回信息处理 < / p >
< p class = "text-11px text-#94a3b8" > 修改后可重新提交 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< template v-if = "homeData?.rejected_todo?.total > 0" >
< div
v-for = "r in homeData?.rejected_todo?.items?.slice(0, 1)"
:key = "r.id"
class = "border border-red-100 bg-red-50/50 px-3.5 py-2.5"
: style = "{
borderRadius: themeStore.themeRadius + 'px',
}"
>
< div class = "flex items-center gap-2" >
< NTag type = "error" size = "small" class = "text-11px" > 已驳回 < / NTag >
< span class = "font-mono text-[11px] text-slate-400" > { { r . change _no } } < / span >
< span class = "ml-auto text-[11px] text-slate-400" > { { r . rejected _at ? . slice ( 0 , 10 ) } } < / span >
< / div >
< div class = "mt-0.5 text-sm font-medium text-slate-800" > { { r . title } } < / div >
< div class = "mt-0.5 text-[11px] leading-4 text-red-600/80" > 驳回原因 : { { r . reject _comment } } < / div >
< div class = "mt-0.5 text-[11px] text-slate-400" > 驳回人 : { { r . reject _user _name } } < / div >
< NButton ghost type = "error" size = "small" class = "text-xs mt-1.5" @click ="jumpTo('my_initiatechange',r.change_id)" > 修改重报 → < / NButton >
< / div >
< NButton dashed type = "error" size = "small" class = "text-xs" @click ="jumpTo('my_mytask')" >
还有 {{ ( homeData ? .rejected_todo ? .total - 1 ) > 0 ? ( homeData ? . rejected _todo ? . total - 1 ) : 0 } } 条驳回 , 前往我的任务处理 →
< / NButton >
< / template >
< template v-else >
< p class = "text-xs text-slate-400 text-center" > 暂无数据 < / p >
< / template >
< / div >
< / NCard >
<!-- 资料归档待办 -- >
< NCard :bordered = "false" size = "small" : style = "{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }" >
< div class = "flex items-center mb-3 gap-2" >
< Icon icon = "quill:folder-open" class = "size-16px" : style = "{ color: themeStore.themeColor }" / >
< p class = "text-13px font-700 text-#64749a" > 资料归档待办 < / p >
< p class = "text-11px text-#94a3b8" > { { homeData ? . archive _todo ? . total || 0 } } 项待补交 < / p >
< / div >
< div class = "flex flex-col gap-3" >
< div v-for = "d in homeData?.archive_todo?.items" :key="d.change_id" class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 px-3.5 py-2.5" >
< div class = "flex items-center gap-2" >
< NTag type = "warning" size = "small" class = "text-11px" > 待归档 < / NTag >
< span class = "font-mono text-[11px] text-slate-400" > { { d . change _no } } < / span >
< / div >
< div class = "mt-0.5 text-sm font-medium text-slate-800" > { { d . title } } < / div >
< div class = "mt-0.5 line-clamp-1 text-[11px] leading-4 text-slate-400" > 缺项 : { { d . missing _names } } < / div >
< NButton type = "primary" size = "small" class = "text-xs mt-1.5" @click ="jumpTo('details_closedoc',d.change_id)" >
去补交 ( 资料关闭页上传 ) →
< / NButton >
< / div >
< div class = "text-[11px] leading-4 text-slate-400" > 上传纸质扫描件 / 照片即视为完成确认并联动归档清单 ; 支持传错删除重传 、 阶段性保存 < / div >
< / div >
< / NCard >
< / NGi >
< / NGrid >
< / div >
< / NSpin >
< / NSpace >
< / template >
< style scoped > < / style >