Files
moc/src/views/changeStatistics/index.vue
T
2026-10-19 10:11:16 +08:00

339 lines
16 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 { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { formatTimestamp, getTimeRangeStamp } from "@/utils/common";
import { localStg } from '@/utils/storage';
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 themeStore = useThemeStore();
const loading = ref<boolean>(false);
// 组织
const org = ref<string>(localStg.get('userInfo')?.department?.label || '');
// 变更临期天数
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 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){
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">
<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">{{org?org+' · ':''}}本部门口径</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>
</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] shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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">优于全厂均值 {{deptStatistics?.kpis?.plant_avg_approval_days || 0}} 天</p>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-white px-16px py-12px text-[#94a3b8] shadow-[0_0_15px_rgba(0,0,0,0.1)]"
: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>
</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" class="shadow-[0_0_15px_rgba(0,0,0,0.1)]" :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="text-[11px] text-slate-400">阶段口径:审批中(含会签)→ 已批准待投用 → 待验收 → 待关闭 · 培训 / PSSR 线下签字后拍照上传,并入资料归档 · 明细见变更台账</div>
</div>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:18">
<NCard :bordered="false" size="small" class="shadow-[0_0_15px_rgba(0,0,0,0.1)]" :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>
</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" class="shadow-[0_0_15px_rgba(0,0,0,0.1)]" :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 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 v-if="deptStatistics?.approver_time?.length === 0" class="h-100% flex items-center justify-center">
<NEmpty description="暂无数据" class="!text-xs"></NEmpty>
</div>
</div>
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="false" size="small" class="shadow-[0_0_15px_rgba(0,0,0,0.1)]" :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="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>
</NCard>
</NGi>
</NGrid>
</div>
</NSpin>
</NSpace>
</template>