Files
moc/src/views/home/modules/template7.vue
T
2026-10-19 10:20:53 +08:00

678 lines
34 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 { 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, formatTimestamp, getTimeRangeStamp } from '@/utils/common';
import RingChart from "./ringChart.vue";
import { systemConfigApi } from "@/service/api/system";
import { urgeApi } from "@/service/api/change";
import { homeManagerApi } from "@/service/api/statistics";
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const DATE_RANGE_OPTIONS = [
{ label: "近一年", value: 'year' },
{ label: "近半年", value: 'half_year' },
{ label: "近三个月", value: 'quarter' },
{ label: "近一个月", value: 'month' },
{ label: "自定义", value: 'custom' },
]
const loading = ref<boolean>(false);
// 催办按钮loading
const btnLoading = ref<boolean>(false);
// 日期范围
const dateRange = ref<string>('year');
// 自定义日期范围
const from = ref<number | null>(getTimeRangeStamp(dateRange.value)?.from ?? null);
const to = ref<number | null>(getTimeRangeStamp(dateRange.value)?.to ?? null);
// 变更临期天数
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 timeChange = (val: string) => {
dateRange.value = val;
if(val === 'custom'){
from.value = null;
to.value = null;
return
}else{
from.value = getTimeRangeStamp(val)?.from ?? null;
to.value = getTimeRangeStamp(val)?.to ?? null;
}
getData();
}
// 自定义时间选择
const customTimeChange = (type:string,val:any) => {
if(type === 'start'){
from.value = val;
}else{
to.value = val;
}
if(from.value && to.value){
if(from.value > to.value){
return window.$message?.warning('开始时间不能大于结束时间');
}
getData();
}
}
// 跳转页面
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 urge = async (change_id:number) => {
btnLoading.value = true;
const {error} = await urgeApi(change_id);
if(!error){
window.$message?.success('催办成功');
}
btnLoading.value = false;
}
// 匹配对象
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 homeManagerApi({
from: formatTimestamp(from.value,'yyyy-MM-dd'),
to: formatTimestamp(to.value,'yyyy-MM-dd'),
});
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"
class="shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }">
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:8">
<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:16" class="flex items-center justify-end gap-12px">
<p class="text-#999 text-xs">{{ clock }}</p>
<NButton ghost size="small" @click="jumpTo('workshopstatistics')">
车间变更看板
</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-white px-16px py-12px text-[#94a3b8] cursor-pointer transition hover:-translate-y-1 shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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?.todo_approvals?.total || 0"
class="text-26px font-600 text-[#333]"
/>
</div>
<p v-if="homeData?.todo_approvals?.overdue_count" class="text-12px text-[#e15555]">含超期未审 · 请优先处理</p>
<p v-else class="text-12px">属地变更审批</p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer transition hover:-translate-y-1 shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('changeledger')"
>
<p class="text-12px font-600">重要变更未关闭</p>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.important_open?.total || 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 transition hover:-translate-y-1 shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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?.rejected_open?.total || 0"
class="text-26px font-600 text-[#333]"
/>
</div>
<p class="text-12px">本车间 · 驳回待修改重报</p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff2f0] px-16px py-12px text-[#e15555] cursor-pointer transition hover:-translate-y-1 shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('changeledger')"
>
<p class="text-12px font-600">超期未关闭</p>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.overdue_open?.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"
class="shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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_approvals?.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_approvals?.total > 0">
<div
v-for="row in homeData?.todo_approvals?.items || []" :key="row.change_id" class="cursor-pointer flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition"
:style="{
borderRadius: themeStore.themeRadius + 'px',
borderColor: row.overdue_status===2?getColorWithOpacity(themeStore.otherColor.error, 0.25):getColorWithOpacity(themeStore.themeColor, 0.25),
backgroundColor: row.overdue_status===2?getColorWithOpacity(themeStore.otherColor.error, 0.05):getColorWithOpacity(themeStore.themeColor, 0.05),
}"
@click="jumpTo('details_approval',row.change_id)"
>
<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 v-if="row.urgent===1" class="text-[11px]" 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 v-if="row.duration_type===2" class="text-[11px]" type="warning" size="small">临时</NTag>
<NTag v-else class="text-[11px]" type="info" size="small">永久</NTag>
<span v-if="row.overdue_status===2" class="text-xs" :style="{ color: themeStore.otherColor.error }">已超期 {{ row.overdue_days }} 天</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 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>
</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>
<!-- 未关闭变更进度一览 -->
<NCard
:bordered="false"
size="small"
class="mt-16px shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center 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 mt-2px">本车间 · 全部未关闭变更</p>
</div>
<NButton v-if="homeData?.unclosed_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?.unclosed_progress?.total > 0">
<div
v-for="row in homeData?.unclosed_progress?.items || []" :key="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)]"
:style="{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border': getColorWithOpacity(themeStore.themeColor, 0.25),
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.05)
}"
@click="jumpTo('details_index',row.change_id)"
>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<!-- 状态标签 -->
<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>
<span class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white">{{ row.title }}</span>
<!-- 变更等级 -->
<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>
</div>
<div class="mt-1 flex items-center gap-2 font-mono text-[11px] text-slate-400">
<span>{{ row.change_no }}</span>
<span>{{ row.org_name }}</span>
<span>{{ row.applicant_name }}</span>
</div>
<!-- 审批流程 -->
<div v-if="row.flow" class="mt-0.5 truncate text-[11px] flex items-center gap-2">
<div v-for="(item,index) in row.flow" :key="index" class="flex items-center gap-1">
<Icon v-if="index!==0" icon="formkit:right" class="size-12px text-slate-400" />
<!-- 通过 -->
<div v-if="item.status==='PASSED'" class="w-6px h-6px rounded-6px" :style="{ backgroundColor: themeStore.otherColor.success }"></div>
<p v-if="item.status==='PASSED'" :style="{ color: themeStore.otherColor.success }">{{ item.name }}</p>
<!-- 进行中 -->
<div v-if="item.status==='ACTIVE'" class="w-6px h-6px rounded-6px" :style="{ backgroundColor: themeStore.otherColor.warning }"></div>
<p v-if="item.status==='ACTIVE'" :style="{ color: themeStore.otherColor.warning }">{{ item.name }}</p>
<!-- 待 -->
<div v-if="item.status==='PENDING'" class="w-6px h-6px rounded-6px bg-slate-400"></div>
<p v-if="item.status==='PENDING'" class="text-slate-400">{{ item.name }}</p>
<!-- 驳回 -->
<div v-if="item.status==='REJECTED'" class="w-6px h-6px rounded-6px" :style="{ backgroundColor: themeStore.otherColor.error }"></div>
<p v-if="item.status==='REJECTED'" :style="{ color: themeStore.otherColor.error }">{{ item.name }}</p>
</div>
</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>
</div>
</NCard>
<!-- 临时变更投用后情况 -->
<NCard
:bordered="false"
size="small"
class="mt-16px shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center 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 mt-2px">临期提醒 · 超期催办 · 延期 / 转永久跟踪</p>
</div>
</div>
<div class="flex flex-col gap-3">
<template v-if="homeData?.temp_after_use?.total > 0">
<div
v-for="row in homeData?.temp_after_use?.items || []" :key="row.change_id"
class="flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition"
:style="{
borderRadius: themeStore.themeRadius + 'px',
borderColor: row.overdue_status===2?getColorWithOpacity(themeStore.otherColor.error, 0.25):getColorWithOpacity(themeStore.themeColor, 0.25),
backgroundColor: row.overdue_status===2?getColorWithOpacity(themeStore.otherColor.error, 0.05):getColorWithOpacity(themeStore.themeColor, 0.05),
}"
>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap 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.overdue_status===2" size="small" type="error">已超期{{ row.overdue_days }}天</NTag>
<NButton v-if="row.overdue_status===2" size="tiny" :disabled="btnLoading" @click="urge(row.change_id)">去催办</NButton>
<NButton v-else size="tiny" @click="jumpTo('changeledger')">查看</NButton>
</div>
<div class="mt-1 flex items-center gap-2 text-[11px] text-slate-400">
<span>{{ row.change_no }}</span>
<!-- 状态 -->
<span v-if="row.status==='APPROVING'">审批中</span>
<span v-if="row.status==='APPROVED'">已通过</span>
<span v-if="row.status==='IMPLEMENTED'">待验收</span>
<span v-if="row.status==='ACCEPTED'">待关闭</span>
<span v-if="row.status==='CLOSED'">已关闭</span>
<span v-if="row.status==='WITHDRAWN'">已撤回</span>
<span v-if="row.status==='REJECTED'">已驳回</span>
<span v-if="row.use_time">{{ row.use_time?.slice(0, 16) }} 投用</span>
<span v-else>尚未投用</span>
<p v-if="row.use_time">计划恢复 {{ row.restore_deadline?.slice(0, 10) }}<span>(已延期 1 次)</span></p>
<span v-else>原计划恢复 {{ row.restore_deadline?.slice(0, 10) }}</span>
</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>
</div>
</NCard>
<!-- 重点关注 -->
<NCard
:bordered="false"
size="small"
class="mt-16px shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center 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 mt-2px">安全管控重点 · 点击打开变更单(含风险分析)</p>
</div>
</div>
<div class="flex flex-col gap-3">
<template v-if="homeData?.key_attention?.total > 0">
<div
v-for="row in homeData?.key_attention?.items || []" :key="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)]"
:style="{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border': getColorWithOpacity(themeStore.otherColor.error, 0.3),
backgroundColor: getColorWithOpacity(themeStore.otherColor.error, 0.05),
borderColor: getColorWithOpacity(themeStore.otherColor.error, 0.05),
}"
@click="jumpTo('details_index',row.change_id)"
>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag size="small" type="error">重要</NTag>
<span class="text-xs text-slate-400">{{ row.change_no }}</span>
</div>
<p class="mt-1 truncate text-sm font-medium text-slate-800">{{ row.title }}</p>
<div class="mt-1 flex items-center gap-2 text-[11px] text-slate-400">
<!-- 状态 -->
<span v-if="row.status==='APPROVING'">审批中</span>
<span v-if="row.status==='APPROVED'">已通过</span>
<span v-if="row.status==='IMPLEMENTED'">待验收</span>
<span v-if="row.status==='ACCEPTED'">待关闭</span>
<span v-if="row.status==='CLOSED'">已关闭</span>
<span v-if="row.status==='WITHDRAWN'">已撤回</span>
<span v-if="row.status==='REJECTED'">已驳回</span>
<span>{{ row.applicant_name }}</span>
</div>
</div>
<NButton text><Icon icon="formkit:right" class="size-14px text-slate-400" /></NButton>
</div>
</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>
</NGi>
<!-- 右侧 -->
<NGi span="24 s:24 m:24 l:8">
<!-- 本车间统计 -->
<NCard
:bordered="false"
size="small"
class="shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center flex-wrap 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>
<NSelect :value="dateRange" size="small" :options="DATE_RANGE_OPTIONS" class="ml-auto w-120px" :on-update:value="timeChange"></NSelect>
</div>
<div v-if="dateRange==='custom'" class="flex items-center gap-2 mt-2">
<NDatePicker :value="from" type="date" size="small" class="w-full" :on-update:value="(v) => customTimeChange('start',v)" />
<span class="text-xs text-slate-400 flex-1">至</span>
<NDatePicker :value="to" type="date" size="small" class="w-full" :on-update:value="(v) => customTimeChange('end',v)" />
</div>
<div class="flex flex-col gap-3 mt-3">
<div class="flex items-end justify-between">
<div>
<span class="text-3xl font-extrabold text-slate-800">{{ homeData?.dept_summary?.total || 0 }}</span>
<span class="ml-1.5 text-[11px] text-slate-500">变更总数</span>
</div>
<span class="text-[11px] text-slate-400">状态构成占比条</span>
</div>
<div class="flex h-3 w-full overflow-hidden rounded-full bg-slate-100">
<div :title="`已关闭 ${homeData?.dept_summary?.closed || 0} 起`" :style="{width: `${(homeData?.dept_summary?.closed || 0) / (homeData?.dept_summary?.total || 0) * 100}%`, background: themeStore.themeColor}"></div>
<div :title="`审批中 ${homeData?.dept_summary?.approving || 0} 起`" :style="{width: `${(homeData?.dept_summary?.approving || 0) / (homeData?.dept_summary?.total || 0) * 100}%`, background: themeStore.otherColor.warning}"></div>
<div :title="`实施中 ${homeData?.dept_summary?.executing || 0} 起`" :style="{width: `${(homeData?.dept_summary?.executing || 0) / (homeData?.dept_summary?.total || 0) * 100}%`, background: themeStore.otherColor.success}"></div>
<div :title="`超期未关闭 ${homeData?.dept_summary?.overdue || 0} 起`" :style="{width: `${(homeData?.dept_summary?.overdue || 0) / (homeData?.dept_summary?.total || 0) * 100}%`, background: themeStore.otherColor.error}"></div>
</div>
<div class="grid grid-cols-2 gap-1.5">
<div class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 bg-slate-50">
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: themeStore.themeColor }"></span>
<span class="text-[11px] text-slate-500">已关闭</span>
<span class="ml-auto text-sm font-bold text-slate-800">{{ homeData?.dept_summary?.closed || 0 }}</span>
</div>
<div class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 bg-slate-50">
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: themeStore.otherColor.warning }"></span>
<span class="text-[11px] text-slate-500">审批中</span>
<span class="ml-auto text-sm font-bold text-slate-800">{{ homeData?.dept_summary?.approving || 0 }}</span>
</div>
<div class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 bg-slate-50">
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: themeStore.otherColor.success }"></span>
<span class="text-[11px] text-slate-500">实施中</span>
<span class="ml-auto text-sm font-bold text-slate-800">{{ homeData?.dept_summary?.executing || 0 }}</span>
</div>
<div class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 bg-red-50">
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: themeStore.otherColor.error }"></span>
<span class="text-[11px] text-slate-500">超期未关闭</span>
<span class="ml-auto text-sm font-bold" :style="{ color: themeStore.otherColor.error }">{{ homeData?.dept_summary?.overdue || 0 }}</span>
</div>
</div>
</div>
</NCard>
<!-- 资料管理 -->
<NCard
:bordered="false"
size="small"
class="shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
<div class="flex items-center mb-3 gap-2">
<Icon icon="material-symbols:folder-open-outline" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">资料管理</p>
</div>
<div
class="space-y-2"
:style="{
'--hover-border': getColorWithOpacity(themeStore.themeColor, 0.25),
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.05)
}"
>
<div
class="cursor-pointer flex w-full items-center gap-3 rounded-lg border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)]"
@click="jumpTo('docs_archives')"
>
<Icon icon="material-symbols:folder-open-outline" class="size-16px" :style="{ color: themeStore.themeColor }" />
<div class="min-w-0 flex-1">
<p class="truncate text-xs font-semibold text-slate-700">本车间操作规程</p>
<p class="truncate text-[11px] text-slate-400">现行受控版本查阅</p>
</div>
<Icon icon="formkit:right" class="size-14px text-slate-400" />
</div>
<div
class="cursor-pointer flex w-full items-center gap-3 rounded-lg border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)]"
@click="jumpTo('docs_archives')"
>
<Icon icon="material-symbols:folder-open-outline" class="size-16px" :style="{ color: themeStore.themeColor }" />
<div class="min-w-0 flex-1">
<p class="truncate text-xs font-semibold text-slate-700">本车间变更档案</p>
<p class="truncate text-[11px] text-slate-400">历史变更记录与验收材料</p>
</div>
<Icon icon="formkit:right" class="size-14px text-slate-400" />
</div>
</div>
</NCard>
<!-- 近一年验收结论占比 -->
<NCard
:bordered="false"
size="small"
class="shadow-[0_0_15px_rgba(0,0,0,0.1)]"
:style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
<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">本车间 · 已验收变更</p>
</div>
<RingChart v-if="homeData" :data="homeData?.accept_conclusion?.items || []" :total="homeData?.accept_conclusion?.total_count || 0" />
<div class="flex items-center justify-between border-slate-200 border-t pt-2 border-dashed">
<span class="text-[11px] text-slate-400">投入合计(变更材料和实施费)</span>
<span class="text-xs font-bold" :style="{ color: themeStore.themeColor }">{{ homeData?.accept_conclusion?.total_fee || 0 }} 万元</span>
</div>
</NCard>
</NGi>
</NGrid>
</div>
</NSpin>
</template>
<style scoped></style>