339 lines
16 KiB
Vue
339 lines
16 KiB
Vue
<!-- 本部门变更统计 -->
|
||
<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>
|
||
|