Files
moc/src/views/home/modules/template4.vue
T
2026-10-08 14:33:48 +08:00

407 lines
19 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 { homeProfessionalApi } 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 jumpTo = (key: string,qid:number = 0) => {
if(key==='details_index' && qid){
routerPushByKey(key,{ query: { id: qid.toString(),key:'home' } });
}else if(key==='details_approval' && qid){
routerPushByKey(key,{ query: { id: qid.toString(),key:'home' } });
}else{
routerPushByKey(key as any);
}
}
// 获取当前时间字符串
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 homeProfessionalApi();
if(!error){
homeData.value = 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:12">
<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:12" class="flex items-center justify-end gap-12px">
<p class="text-#999 text-xs">{{clock}}</p>
<NButton ghost size="small" @click="jumpTo('changeledger')">
专业变更查询
</NButton>
<NButton type="primary" size="small" @click="jumpTo('my_mytask')">
<Icon icon="lucide:clipboard-check" class="size-14px" />
审批待办
</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-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_mytask')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.todo_sign?.total || 0"
class="text-26px font-600"
:style="{ color: homeData?.todo_sign?.overdue_count > 0?themeStore.otherColor.error:themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">待我会签</p>
<p v-if="homeData?.todo_sign?.overdue_count > 0" class="text-12px" :style="{ color: themeStore.otherColor.error }">其中超期 {{homeData?.todo_sign?.overdue_count}} 项 · 请优先处理</p>
<p v-else class="text-12px text-slate-400">无超期</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('professionalstatistics')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.sign_stats?.month_signed || 0"
class="text-26px font-600"
:style="{ color: themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">本月已会签</p>
<p class="text-12px text-slate-400">含驳回 {{homeData?.sign_stats?.month_rejected || 0}} 起 · 系统自动采集</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_highrisk')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.high_risk?.total || 0"
class="text-26px font-600"
:style="{ color: themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">在办高风险</p>
<p class="text-12px text-slate-400">本专业相关 · 建议关注</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('professionalstatistics')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.sign_stats?.risk_analysis_count || 0"
class="text-26px font-600"
:style="{ color: themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">参与风险分析</p>
<p class="text-12px text-slate-400">作为分析组成员(选择式)· 近一年</p>
</div>
</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="lucide:clipboard-check" 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?.todo_sign?.total > 0" ghost size="small" class="text-xs" @click="jumpTo('my_mytask')">
我的任务
<Icon icon="formkit:right" class="size-12px" />
</NButton>
</div>
<div class="flex flex-col gap-3">
<template v-if="homeData?.todo_sign?.total > 0">
<div v-for="row in homeData?.todo_sign?.items?.slice(0, 3) || []" :key="row.change_id" @click="jumpTo('details_approval',row.change_id)"
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.overdue_status===2" class="text-xs" :style="{ color: themeStore.otherColor.error }">已超期 {{row.overdue_days || 0}} 天</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>
<NTag v-if="row.step_name" size="small" type="warning">{{row.step_name}}</NTag>
<NButton size="small" type="primary" class="text-xs">
去审批
<Icon icon="akar-icons:arrow-right" class="size-13px ml-1" />
</NButton>
</div>
<div class="mt-0.5 truncate text-[11px] text-slate-400 flex items-center gap-2">
<p>申请人:{{row.applicant_name}}</p>
<span v-if="row.apply_time" class="text-[11px] text-slate-400">{{ row.apply_time?.slice(0, 10) }}</span>
</div>
</div>
</div>
<NButton dashed type="primary" size="small" class="text-xs" @click="jumpTo('my_mytask')">
还有{{(homeData?.todo_sign?.total - 3) > 0 ? (homeData?.todo_sign?.total - 3) : 0}}条,前往我的任务查看全部 →
</NButton>
</template>
<template v-else>
<div class="h-100% flex items-center justify-center py-3">
<NEmpty description="暂无数据" class="!text-xs"></NEmpty>
</div>
</template>
</div>
</NCard>
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive class="mt-[16px]">
<!-- 最近已会签 -->
<NGi span="24 s:24 m:24 l:24">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center mb-3 gap-2">
<Icon icon="ix:success" 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 v-if="homeData?.signed_recent?.items.length" class="flex flex-col gap-3">
<div v-for="item in homeData?.signed_recent?.items" :key="item.change_id" class="rounded-md border px-3 py-2 flex items-center justify-between gap-2 cursor-pointer" @click="jumpTo('details_index', item.change_id)">
<div>
<p class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white">{{ item.title }}</p>
<p class="font-mono text-[11px] text-slate-400 flex items-center gap-3">
<span>{{ item.change_no }}</span>
<span>{{ item.finished_at?.slice(0, 16) }}</span>
</p>
</div>
<NTag v-if="item.status==='PENDING'" size="small" type="warning" class="text-xs">待会签</NTag>
<NTag v-if="item.status==='PASSED'" size="small" type="success" class="text-xs">通过</NTag>
<NTag v-if="item.status==='REJECTED'" size="small" type="error" class="text-xs">驳回</NTag>
</div>
</div>
<div v-else class="h-100% flex items-center justify-center py-3">
<NEmpty description="暂无数据" class="!text-xs"></NEmpty>
</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="lucide:shield-alert" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">高风险变更</p>
<p class="text-11px text-#94a3b8">在办 · 建议重点关注</p>
</div>
<template v-if="homeData?.high_risk?.total > 0">
<div class="space-y-2.5">
<div v-for="h in homeData?.high_risk?.items || []" :key="h.record_id" class="rounded-md border border-amber-200 bg-amber-50/60 px-3.5 py-2.5">
<div class="flex items-center gap-2">
<span class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800">{{ h.change_title }}</span>
<NButton size="tiny" ghost type="warning" class="text-xs" @click="jumpTo('my_highrisk')">查看分析</NButton>
</div>
<div class="mt-1 text-[11px]" :style="{ color: themeStore.otherColor.warning }">{{ h.scene }}</div>
</div>
</div>
</template>
<template v-else>
<div class="h-100% flex items-center justify-center py-3">
<NEmpty description="暂无数据" class="!text-xs"></NEmpty>
</div>
</template>
</NCard>
<!-- 我的会签效率 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
<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">系统自动采集 · 近 30 天</p>
</div>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<span class="text-xs text-slate-500 w-16">平均耗时</span>
<NProgress
type="line"
status="info"
:percentage="((homeData?.sign_stats?.avg_audit_days || 0) / 2) * 100"
:height="10"
border-radius="6px"
>
<span class="text-xs text-slate-700 font-semibold">{{homeData?.sign_stats?.avg_audit_days || 0}}天</span>
</NProgress>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-500 w-16">最长耗时</span>
<NProgress
type="line"
status="error"
:percentage="((homeData?.sign_stats?.max_audit_days || 0) / 2) * 100"
:height="10"
border-radius="6px"
>
<span class="text-xs text-slate-700 font-semibold" :style="{ color: themeStore.otherColor.error }">{{homeData?.sign_stats?.max_audit_days || 0}}天</span>
</NProgress>
</div>
<div class="flex items-center justify-between text-[11px] text-slate-400">
<span>会签及时率 <b :style="{ color: themeStore.otherColor.success }">{{homeData?.sign_stats?.timely_rate || 0}}%</b></span>
<span>目标 2 天内签署</span>
</div>
</div>
</NCard>
</NGi>
</NGrid>
</div>
</NSpin>
</template>
<style scoped></style>