Files
moc/src/views/home/modules/template10.vue
T

447 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
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 themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
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 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>
<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>
</NGi>
<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>
</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>
<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="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>
</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">
<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>
<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="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>
<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>
</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>
</template>
<style scoped></style>