本部门变更统计完成

This commit is contained in:
2026-09-21 13:28:53 +08:00
parent 680c43b3d5
commit c679642c40
5 changed files with 344 additions and 225 deletions
+11
View File
@@ -0,0 +1,11 @@
// 数据统计
import { request } from '../request';
// 本部门变更统计
export function departmentStatisticsApi(params: {from: string | null, to: string | null}) {
return request({
url: `/api/reports/dept-stats`,
method: 'get',
params
});
}
+301 -214
View File
@@ -1,246 +1,333 @@
<!-- 本部门变更统计 -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { CHANGES, TEMP_WATCH, countOpenStatus } from "@/typings/model";
import { formatTimestamp } from "@/utils/common";
import { formatTimestamp, getTimeRangeStamp } from "@/utils/common";
import RingChart from './modules/ringChart.vue';
import PieChart from './modules/pieChart.vue';
import PieChart2 from './modules/pieChart2.vue';
import { systemConfigApi } from "@/service/api/system";
import { departmentStatisticsApi } from "@/service/api/statistics";
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const P_PERIODS = ["近一年", "近半年", "近三个月", "近一个月", "自定义"] as const;
const period = ref<(typeof P_PERIODS)[number]>("近一年");
const from = ref<any>(null);
const to = ref<any>(null);
const rangeText = computed(() => period.value === "自定义"
? (from.value || to.value ? `${formatTimestamp(from.value,'yyyy-MM-dd') || "开始"} ~ ${formatTimestamp(to.value,'yyyy-MM-dd') || "至今"}` : "自定义(未选日期)")
: period.value);
const loading = ref<boolean>(false);
// 变更临期天数
const remindDays = ref<number>(0);
// 本部门变更统计
const deptStatistics = ref<any>(null);
// 变更类型列表
const typeList = ref<any>([]);
// 时间范围
const period = ref<string>("year");
// 开始时间
const from = ref<number | null>(getTimeRangeStamp(period.value)?.from ?? null);
// 结束时间
const to = ref<number | null>(getTimeRangeStamp(period.value)?.to ?? null);
// 时间范围选项
const TIME_RANGE = [
{label: "近一年", value: "year"},
{label: "近半年", value: "half-year"},
{label: "近三个月", value: "quarter"},
{label: "近一个月", value: "month"},
{label: "自定义", value: "custom"}
]
// 尚未关闭总数
const openTotal = computed(() => {
return deptStatistics.value?.open_status_counts?.reduce((sum:number, item:any) => sum + item.count, 0) || 0;
});
// 验收达标总数
const acceptTotal = computed(() => {
return deptStatistics.value?.accept_dist?.reduce((sum:number, item:any) => sum + item.count, 0) || 0;
});
// 临时变更到期提醒情况
const tempTotal = computed(() => {
return (deptStatistics.value?.kpis?.unclosed_total || 0) + (deptStatistics.value?.kpis?.near_overdue_total || 0) + (deptStatistics.value?.kpis?.overdue_total || 0);
});
const deptOpenStatus = countOpenStatus(CHANGES);
const DEPT_ACCEPT = [
{ name: "完全达到", value: 13 },
{ name: "基本达到", value: 4 },
{ name: "未达到", value: 2 },
{ name: "处置关闭(未实施 / 未投用)", value: 3 },
];
const acceptTotal = DEPT_ACCEPT.reduce((a, b) => a + b.value, 0);
const ACCEPT_COLORS = ["#7CC242", "#F5A623", "#E15555", "#94A3B8", "#64748B"];
const DEPT_TYPE = [
{ name: "工艺", value: 11 }, { name: "设备", value: 12 }, { name: "管理", value: 5 },
];
const DEPT_DUR = [
{ name: "永久", value: 21 }, { name: "临时", value: 7 },
];
const DUR_COLORS = ["#1D4E9C", "#FAAD14"];
const PIE_COLORS = ["#1D4E9C", "#7CC242", "#F5A623"];
const APPROVER_TIME = [
{ name: "李主任(部门审核)", days: 1.2 },
{ name: "王仪表(仪表会签)", days: 0.8 },
{ name: "周设备(设备会签)", days: 1.5 },
{ name: "王海燕(安全审查)", days: 2.1 },
{ name: "刘副总(公司终批)", days: 1.9 },
];
const openTotal = deptOpenStatus.reduce((a, b) => a + b.n, 0);
const tempTotal = TEMP_WATCH.reduce((a, b) => a + b.n, 0);
// 时间范围切换
const timeChange = (val: string) => {
period.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 getData = async () => {
loading.value = true;
const {data,error} = await departmentStatisticsApi({
from: formatTimestamp(from.value,'yyyy-MM-dd') || null,
to: formatTimestamp(to.value,'yyyy-MM-dd') || null
});
if(!error){
console.log(data);
deptStatistics.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;
}
}
}
onMounted(() => {
getConfig();
getData();
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 本部门变更统计 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex flex-wrap items-center justify-between">
<div class="flex-y-center">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="akar-icons:calendar" class="size-14px" :style="{color: themeStore.themeColor}" />
<span>本部门变更统计</span>
<span class="text-xs font-normal text-slate-400">一车间 · 本部门口径</span>
</span>
<div class="flex flex-wrap items-center gap-1.5 min-h-28px">
<span class="text-xs text-slate-400">时间段</span>
<NButton size="tiny" round v-for="p in P_PERIODS" :key="p" @click="period = p"
:ghost="period === p ? false : true"
:type="period === p ? 'primary' : 'default'"
>{{ p }}</NButton>
<span v-if="period === '自定义'" class="flex items-center gap-1">
<NDatePicker v-model:value="from" type="date" size="small" class="w-32" />
<span class="text-xs text-slate-400"></span>
<NDatePicker v-model:value="to" type="date" size="small" class="w-32" />
<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' }">
<div class="flex flex-wrap items-center justify-between">
<div class="flex-y-center gap-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="akar-icons:calendar" class="size-14px" :style="{color: themeStore.themeColor}" />
<span>本部门变更统计</span>
<span class="text-xs font-normal text-slate-400">一车间 · 本部门口径</span>
</span>
<div class="flex flex-wrap items-center gap-1.5 min-h-28px">
<span class="text-xs text-slate-400">时间段</span>
<NButton size="tiny" round v-for="p in TIME_RANGE" :key="p.value" @click="timeChange(p.value)"
:ghost="period === p.value ? false : true"
:type="period === p.value ? 'primary' : 'default'"
>{{ p.label }}</NButton>
<span v-if="period === 'custom'" class="flex items-center gap-1">
<NDatePicker :value="from" type="date" size="small" class="w-32" :on-update:value="(v) => customTimeChange('start',v)" />
<span class="text-xs text-slate-400"></span>
<NDatePicker :value="to" type="date" size="small" class="w-32" :on-update:value="(v) => customTimeChange('end',v)" />
</span>
</div>
</div>
</div>
<span class="text-[11px] text-slate-400">统计口径{{ rangeText }}</span>
</div>
</NCard>
<!-- 卡片数据 -->
<NGrid cols="s:1 m:5 l:5" responsive="screen" :x-gap="gap" :y-gap="16">
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">申请变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="28"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px"></p>
</NCard>
<!-- 卡片数据 -->
<NGrid cols="s:1 m:5 l:5" responsive="screen" :x-gap="16" :y-gap="16">
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">申请变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="deptStatistics?.kpis?.apply_total || 0"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px"></p>
</div>
<p class="text-12px">永久 {{deptStatistics?.kpis?.permanent_total || 0}} · 临时 {{deptStatistics?.kpis?.temp_total || 0}} </p>
</div>
<p class="text-12px">永久 21 · 临时 7</p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">重要变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="7"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px"></p>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">重要变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="deptStatistics?.kpis?.important_total || 0"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px"></p>
</div>
<p class="text-12px">占比 {{deptStatistics?.kpis?.important_pct || 0}}% · 均含公司终批</p>
</div>
<p class="text-12px">占比 25% · 均含公司终批</p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">未关闭变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="6"
class="text-26px font-600 text-[#E15555]"
/><p class="mt-10px ml-5px"></p>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">未关闭变更数量</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="deptStatistics?.kpis?.unclosed_total || 0"
class="text-26px font-600 text-[#E15555]"
/><p class="mt-10px ml-5px"></p>
</div>
<p class="text-12px">其中超期 {{deptStatistics?.kpis?.overdue_total || 0}} · 临期 {{deptStatistics?.kpis?.near_overdue_total || 0}} </p>
</div>
<p class="text-12px">其中超期 1 · 临期 1 </p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">平均审批通过时长</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="7.4"
:decimals="1"
class="text-26px font-600 text-[#333]"
/>
<p class="mt-10px ml-5px"></p>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">平均审批通过时长</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="deptStatistics?.kpis?.avg_approval_days || 0"
:decimals="1"
class="text-26px font-600 text-[#333]"
/>
<p class="mt-10px ml-5px"></p>
</div>
<p class="text-12px">优于全厂均值 8.2 </p>
</div>
<p class="text-12px">优于全厂均值 8.2 </p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">申请驳回率</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="6.1"
:decimals="1"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px">%</p>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8]"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<p class="text-12px font-600">申请驳回率</p>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="deptStatistics?.kpis?.reject_rate || 0"
:decimals="1"
class="text-26px font-600 text-[#333]"
/><p class="mt-10px ml-5px">%</p>
</div>
<p class="text-12px">{{deptStatistics?.kpis?.reject_count || 0}} 项被驳回 · 均已重新提交</p>
</div>
<p class="text-12px">2 项被驳回 · 均已重新提交</p>
</div>
</NGi>
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:24 l:6">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">尚未关闭的变更所处的状态数量 {{openTotal}} </p>
<div class="space-y-3">
<div v-for="s in deptOpenStatus" :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>
</NGi>
</NGrid>
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:24 l:6">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">尚未关闭的变更所处的状态数量 {{openTotal}} </p>
<div class="space-y-3">
<div v-for="s in deptStatistics?.open_status_counts" :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="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 class="text-[11px] text-slate-400">阶段口径审批中含会签 已批准待投用 待验收 待关闭 · 培训 / PSSR 线下签字后拍照上传并入资料归档 · 明细见变更台账</div>
</div>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:18">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">验收达标 · 变更类型 · 变更时限分布</p>
<div class="grid gap-4 grid-cols-3">
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">验收达标分布 {{ acceptTotal }} · 处置关闭单列</div>
<RingChart v-if="deptStatistics?.accept_dist?.length" :data="deptStatistics?.accept_dist || []" :total="acceptTotal" />
</div>
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">变更类型分布</div>
<PieChart v-if="deptStatistics?.type_dist?.length && typeList?.length" :data="deptStatistics?.type_dist || []" :typeList="typeList" />
</div>
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">变更时限分布</div>
<PieChart2 v-if="deptStatistics?.duration_dist?.length" :data="deptStatistics?.duration_dist || []" />
</div>
</div>
<div class="text-[11px] text-slate-400">阶段口径审批中含会签 已批准待投用 待验收 待关闭 · 培训 / PSSR 线下签字后拍照上传并入资料归档 · 明细见变更台账</div>
</div>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:18">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">验收达标 · 变更类型 · 变更时限分布</p>
<div class="grid gap-4 grid-cols-3">
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">验收达标分布 {{ acceptTotal }} · 处置关闭单列</div>
<RingChart :data="DEPT_ACCEPT" :colors="ACCEPT_COLORS" />
</NCard>
</NGi>
</NGrid>
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<div class="border-b border-slate-100 pb-3 mb-4 flex justify-between items-center">
<p class=" text-base font-semibold">各人员审批时间对比平均处理时长</p>
<p class="text-[11px] text-slate-400 mt-5px">超过 2 天红色标识 · 用于会签催办与流程优化参考</p>
</div>
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">变更类型分布</div>
<PieChart :data="DEPT_TYPE" :colors="PIE_COLORS" />
</div>
<div>
<div class="mb-1 text-center text-xs font-semibold text-slate-600">变更时限分布</div>
<PieChart2 :data="DEPT_DUR" :colors="DUR_COLORS" />
</div>
</div>
<div class="mt-3 rounded-lg bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-500">
未达到 2 P-204 伴热改造温控未达预期已退回整改加药泵冲程调整流量偏差重新约定 30 天验证期处置关闭 3 未实施 2 检修窗口调整方案合并实施未投用 1 投用条件不具备均经情况说明后关闭验收结论不再含未实施 / 未投用
</div>
</NCard>
</NGi>
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">各人员审批时间对比平均处理时长</p>
<div class="space-y-3">
<div v-for="a in APPROVER_TIME" :key="a.name">
<div class="flex items-center gap-2 text-xs">
<span class="min-w-0 flex-1 text-slate-700">{{ a.name }}</span>
<span class="shrink-0 font-semibold text-slate-600">{{ a.days }} </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: `${(a.days / 2.5) * 100}%`, background: a.days > 2 ? '#E15555' : '#1D4E9C' }" />
<div class="space-y-3">
<div v-for="a in deptStatistics?.approver_time" :key="a.name">
<div class="flex items-center gap-2 text-xs">
<span class="min-w-0 flex-1 text-slate-700">{{ a.user_name }}{{a.step_name?''+a.step_name+'':''}}</span>
<span class="shrink-0 font-semibold text-slate-600">{{ a.avg_days }} </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: `${(a.avg_days / a.total) * 100}%`, background: a.avg_days > 2 ? themeStore.otherColor.error : themeStore.themeColor }"></div>
</div>
</div>
</div>
<div class="text-[11px] text-slate-400">超过 2 天红色标识 · 用于会签催办与流程优化参考</div>
</div>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">临时变更到期提醒情况 {{tempTotal}} </p>
<div class="space-y-3">
<div v-for="t in TEMP_WATCH" :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 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>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 pb-3 mb-4">临时变更到期提醒情况 {{tempTotal}} </p>
<div class="space-y-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">{{deptStatistics?.kpis?.unclosed_total || 0}} </span>
</div>
</div>
<div class="mt-1 pl-4 text-[11px] text-slate-400">{{ t.note }}</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">{{deptStatistics?.kpis?.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">{{deptStatistics?.kpis?.overdue_total || 0}} </span>
</div>
</div>
<div class="text-[11px] text-slate-400">提醒规则到期前 {{remindDays}} 天触发可在我的任务 · 我的申请中调整· 临期 / 超期项请按期办理延期恢复或转永久</div>
</div>
<div class="text-[11px] text-slate-400">提醒规则到期前 7 天触发可在待我处理 · 我的申请中调整· 临期 / 超期项请按期办理延期恢复或转永久</div>
</div>
</NCard>
</NGi>
</NGrid>
</NSpace>
</NCard>
</NGi>
</NGrid>
</div>
</NSpin>
</NSpace>
</template>
<style scoped></style>
@@ -6,7 +6,7 @@ defineOptions({
name: 'PieChart'
});
const props = defineProps<{ data?: { name: string; value: number }[]; colors?: string[] }>();
const props = defineProps(['data','typeList'])
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
@@ -47,8 +47,19 @@ const { domRef, updateOptions } = useEcharts(() => ({
function init() {
updateOptions(opts => {
opts.series[0].data = props.data || [];
opts.series[0].color = props.colors || [];
// 处理数据
const result = props.data?.map((item: any) => ({
...item,
name: item.label,
value: item.count
}));
opts.series[0].data = result || [];
// 处理颜色
// 用 Map 建立 value -> color 映射
const colorMap = new Map(props.typeList?.map((item: any) => [item.value, item.color]));
// 按 list2 的顺序返回 color 数组
const colors = result.map((item: any) => colorMap.get(item.key) || null);
opts.series[0].color = colors;
return opts;
});
}
@@ -6,7 +6,7 @@ defineOptions({
name: 'PieChart'
});
const props = defineProps<{ data?: { name: string; value: number }[]; colors?: string[] }>();
const props = defineProps(['data'])
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
@@ -47,8 +47,13 @@ const { domRef, updateOptions } = useEcharts(() => ({
function init() {
updateOptions(opts => {
opts.series[0].data = props.data || [];
opts.series[0].color = props.colors || [];
const result = props.data?.map((item: any) => ({
...item,
name: item.label,
value: item.count
}));
opts.series[0].data = result || [];
opts.series[0].color = ["#2080f0", "#faad14"];
return opts;
});
}
@@ -6,7 +6,7 @@ defineOptions({
name: 'RingChart'
});
const props = defineProps<{ data?: { name: string; value: number }[]; colors?: string[] }>();
const props = defineProps(['data','total'])
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
@@ -24,7 +24,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
},
title: {
text: `{a|22}\n{b|已评价}`,
text: `{a|${props.total}}\n{b|已评价}`,
textStyle: {
rich: {
a: {
@@ -72,8 +72,13 @@ const { domRef, updateOptions } = useEcharts(() => ({
function init() {
updateOptions(opts => {
opts.series[0].data = props.data || [];
opts.series[0].color = props.colors || [];
const result = props.data?.map((item: any) => ({
...item,
name: item.label,
value: item.count
}));
opts.series[0].data = result || [];
opts.series[0].color = ["#52c41a", "#faad14", "#f5222d", "#94A3B8"];
return opts;
});
}
@@ -82,7 +87,7 @@ init();
</script>
<template>
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
<div ref="domRef" class="w-full h-220px"></div>
</template>
<style scoped></style>