安全与风险统计页面完成

This commit is contained in:
2026-09-29 16:34:30 +08:00
parent 6f64a39586
commit f671337bc7
15 changed files with 168 additions and 358 deletions
+86 -97
View File
@@ -1,83 +1,61 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { TEMP_WATCH } from "@/typings/model";
import { useRouterPush } from '@/hooks/common/router';
import RingChart from './modules/ringChart.vue';
import ColumnChart from './modules/columnChart.vue';
import { safeRiskStatisticsApi } from '@/service/api/statistics';
import { urgeApi } from '@/service/api/change';
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const { routerPushByKey } = useRouterPush();
const addRisk = ref([
{
name: '2月',
highRisk: 28,
middleRisk: 27,
},
{
name: '3月',
highRisk: 28,
middleRisk: 27,
},
{
name: '4月',
highRisk: 28,
middleRisk: 27,
},
{
name: '5月',
highRisk: 28,
middleRisk: 27,
},
{
name: '6月',
highRisk: 28,
middleRisk: 27,
},
{
name: '7月',
highRisk: 28,
middleRisk: 27,
},
]);
const RISK_COLORS = ["#f5222d", "#faad14"];
const DEPT_ACCEPT = [
{ name: "高风险项", value: 1 },
{ name: "中风险项", value: 6 },
{ name: "低风险项", value: 12 },
];
const ACCEPT_COLORS = ["#f5222d", "#faad14", "#b2bcca"];
const PSSR_EXEC = [
{ pct: 94, label: "投用前完成率", detail: "29/31 变更 · 投用时培训 / PSSR 资料已齐", status: "finish" },
{ pct: 88, label: "行动项按期关闭率", detail: "15/17 项 · 2 项跟进中(风险分析建议措施转入)", status: "close" },
{ pct: 100, label: "投用后补传率", detail: "0 起 · 投用前未完成而先行投用", status: "fix" },
];
const OVERDUE_LIST = [
{ id: "MOC-2026-R01-0039", title: "V-102 安全阀起跳压力临时调整", stage: "待关闭", days: 4 },
{ id: "MOC-2026-R02-0031", title: "冷冻盐水泵 P-103 叶轮材质变更", stage: "待验收", days: 7 },
{ id: "MOC-2026-R01-0029", title: "操作规程修订(夏季工况)", stage: "审批中", days: 3 },
];
const loading = ref<boolean>(false);
const btnLoading = ref<boolean>(false);
const homeData = ref<any>(null);
const SAFETY_EXPORTS = ["高风险变更清单", "新增固有风险项月度表", "剩余风险与措施落实表", "PSSR 执行统计", "超期未关闭清单", "验收一次通过率分析"];
// 催办
const handleReminder = (id: string) => {
window.$message?.success(`已催办 ${id} 当前节点负责人(系统消息 + 企业微信)`)
const handleReminder = async (id: number) => {
btnLoading.value = true;
const {error} = await urgeApi(id);
if(!error){
window.$message?.success('催办成功')
}
btnLoading.value = false;
}
// 导出报表
const handleExport = (name: string) => {
window.$message?.success(`${name}已导出 Excel`)
window.$message?.info('还差接口')
}
// 跳转页面
const jumpTo = (key:string) => {
routerPushByKey(key as any);
}
// 获取数据
const getData = async () => {
loading.value = true;
const {data,error} = await safeRiskStatisticsApi({
from: null,
to: null
})
if(!error){
homeData.value = data;
}
loading.value = false;
}
onMounted(() => {
getData();
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 卡片数据 -->
<NGrid
cols="s:1 m:4 l:4" responsive="screen" :x-gap="gap" :y-gap="16"
cols="s:1 m:4 l:4" responsive="screen" :x-gap="16" :y-gap="16"
:style="{
'--error-color':themeStore.otherColor.error,
'--warning-color':themeStore.otherColor.warning
@@ -85,13 +63,14 @@ const handleExport = (name: string) => {
>
<NGi>
<div
class="flex-1 flex items-center gap-3 bg-white px-16px py-12px text-[#94a3b8]"
class="flex-1 flex items-center gap-3 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_highrisk')"
>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="4"
:end-value="homeData?.kpis?.high_risk_in_progress || 0"
class="text-28px font-600 text-[var(--error-color)]"
/><p class="mt-10px ml-5px">起</p>
</div>
@@ -109,7 +88,7 @@ const handleExport = (name: string) => {
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="1"
:end-value="homeData?.kpis?.residual_high || 0"
class="text-28px font-600 text-[var(--error-color)]"
/><p class="mt-10px ml-5px">项</p>
</div>
@@ -127,7 +106,7 @@ const handleExport = (name: string) => {
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="6"
:end-value="homeData?.kpis?.residual_mid || 0"
class="text-28px font-600 text-[var(--warning-color)]"
/><p class="mt-10px ml-5px">项</p>
</div>
@@ -139,26 +118,26 @@ const handleExport = (name: string) => {
</NGi>
<NGi>
<div
class="flex-1 flex items-center gap-3 bg-white px-16px py-12px text-[#94a3b8]"
class="flex-1 flex items-center gap-3 bg-white px-16px py-12px text-[#94a3b8] cursor-pointer"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('changeledger')"
>
<div class="flex items-center">
<CountTo
:start-value="0"
:end-value="3"
:end-value="homeData?.kpis?.overdue_unclosed || 0"
class="text-28px font-600 text-[var(--error-color)]"
/><p class="mt-10px ml-5px">起</p>
</div>
<div>
<p class="text-xs font-semibold text-slate-600">超期未关闭</p>
<p class="text-[11px] text-slate-400">含临时变更到期 1 起</p>
</div>
</div>
</NGi>
</NGrid>
<NGrid
:x-gap="gap" :y-gap="16" responsive="screen" item-responsive
:x-gap="16" :y-gap="16" responsive="screen" item-responsive
:style="{
'--error-color':themeStore.otherColor.error,
'--warning-color':themeStore.otherColor.warning,
@@ -169,14 +148,14 @@ const handleExport = (name: string) => {
<NGi span="24 s:24 m:24 l:8">
<NCard :bordered="true" size="small" :content-style="{ padding: '0' }" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 p-3">月度新增固有风险项(管控前)</p>
<ColumnChart :data="addRisk" :colors="RISK_COLORS" style="height: 100%;" />
<ColumnChart v-if="homeData" :data="homeData?.monthly_inherent || []" />
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:8">
<NCard :bordered="true" size="small" :content-style="{ padding: '0' }" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 p-3">验收达标 · 变更类型 · 变更时限分布</p>
<RingChart :data="DEPT_ACCEPT" :colors="ACCEPT_COLORS" />
<div class="px-3 pt-2 pb-3">
<RingChart v-if="homeData" :data="homeData?.residual_dist || null" />
<!-- <div class="px-3 pt-2 pb-3">
<div class="border-t pt-2.5">
<div class="flex items-baseline justify-between">
<span class="text-xs text-slate-500">管控措施落实率</span>
@@ -187,49 +166,49 @@ const handleExport = (name: string) => {
</div>
<div class="mt-1 text-[11px] text-slate-400">剩余高风险项 1 项:措施已落实 · 复核中(聚合反应温度上限调整 · 联锁传动试验)</div>
</div>
</div>
</div> -->
</NCard>
</NGi>
<NGi span="24 s:24 m:24 l:8">
<NCard :bordered="true" size="small" :content-style="{ padding: '0' }" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 p-3">PSSR 执行情况</p>
<div class="space-y-5 px-3 pt-5 pb-3">
<div v-for="r in PSSR_EXEC" :key="r.label">
<!-- 投用前完成率 -->
<div>
<div class="flex items-baseline gap-2">
<span class="text-2xl font-bold text-[#1D2129]">{{ r.pct }}<span class="text-sm font-semibold">%</span></span>
<span class="text-xs font-semibold text-slate-500">{{ r.label }}</span>
<span class="text-2xl font-bold text-[#1D2129]">{{ homeData?.pssr?.pre_use_rate || 0 }}<span class="text-sm font-semibold">%</span></span>
<span class="text-xs font-semibold text-slate-500">投用前完成率</span>
</div>
<div class="mt-1.5 h-1 w-full overflow-hidden rounded-full bg-slate-100">
<div v-if="r.status==='finish'" class="h-full rounded-full bg-[var(--theme-color)]" :style="{ width: `${r.pct}%`}"></div>
<div v-if="r.status==='close'" class="h-full rounded-full bg-[var(--success-color)]" :style="{ width: `${r.pct}%`}"></div>
<div v-if="r.status==='fix'" class="h-full rounded-full bg-[#b2bcca]" :style="{ width: `${r.pct}%`}"></div>
<div class="h-full rounded-full bg-[var(--theme-color)]" :style="{ width: `${homeData?.pssr?.pre_use_rate || 0}%` }"></div>
</div>
<div class="mt-1 text-[11px] text-slate-400">{{ r.detail }}</div>
<div class="mt-1 text-[11px] text-slate-400">{{ homeData?.pssr?.pre_use_done || 0 }}/{{ homeData?.pssr?.pre_use_total || 0 }}变更 · 投用时培训 / PSSR 资料已齐</div>
</div>
<!-- 行动项按期关闭率 -->
<div>
<div class="flex items-baseline gap-2">
<span class="text-2xl font-bold text-[#1D2129]">{{ homeData?.pssr?.action_rate || 0 }}<span class="text-sm font-semibold">%</span></span>
<span class="text-xs font-semibold text-slate-500">行动项按期关闭率</span>
</div>
<div class="mt-1.5 h-1 w-full overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full bg-[var(--success-color)]" :style="{ width: `${homeData?.pssr?.action_rate || 0}%`}"></div>
</div>
<div class="mt-1 text-[11px] text-slate-400">{{ homeData?.pssr?.action_confirmed || 0 }}/{{ homeData?.pssr?.action_total || 0 }}项</div>
</div>
</div>
</NCard>
</NGi>
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive :style="{'--theme-color':themeStore.themeColor}">
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive :style="{'--theme-color':themeStore.themeColor}">
<NGi span="24 s:24 m:24 l:8">
<NCard :bordered="true" size="small" :content-style="{ padding: '0' }" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 p-3">验收与投用质量</p>
<div class="flex pt-5 pb-3">
<div class="flex justify-center pt-5 pb-3">
<div class="w-50% flex flex-col items-center gap-1.5">
<NProgress type="circle" :percentage="88" :color="themeStore.otherColor.warning" :offset-degree="180">
<NProgress type="circle" :percentage="homeData?.accept?.on_time_rate || 0" :offset-degree="180">
<div class="flex flex-col items-center">
<p class="text-2xl font-bold leading-none text-[#1D2129]">88<span class="text-sm font-semibold">%</span></p>
<p class="mt-1 text-[11px] text-slate-400">Fair</p>
</div>
</NProgress>
<span class="text-xs font-semibold text-slate-600">验收一次通过率</span>
<span class="text-[11px] text-slate-400">近一年 · 全厂</span>
</div>
<div class="w-50% flex flex-col items-center gap-1.5">
<NProgress type="circle" :percentage="85" :offset-degree="180">
<div class="flex flex-col items-center">
<p class="text-2xl font-bold leading-none text-[#1D2129]">85<span class="text-sm font-semibold">%</span></p>
<p class="mt-1 text-[11px] text-slate-400">Good</p>
<p class="text-2xl font-bold leading-none text-[#1D2129]">{{homeData?.accept?.on_time_rate || 0}}<span class="text-sm font-semibold">%</span></p>
<!-- <p class="mt-1 text-[11px] text-slate-400">Good</p> -->
</div>
</NProgress>
<span class="text-xs font-semibold text-slate-600">按期投用率</span>
@@ -242,13 +221,23 @@ const handleExport = (name: string) => {
<NCard :bordered="true" size="small" :content-style="{ padding: '0' }" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<p class=" text-base font-semibold border-b border-slate-100 p-3">超期未关闭清单</p>
<div class="space-y-2 p-3">
<div v-for="o in OVERDUE_LIST" :key="o.id" class="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50/40 px-3 py-2">
<div v-for="o in homeData?.overdue_list || []" :key="o.change_id" class="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50/40 px-3 py-2">
<span class="min-w-0 flex-1">
<span class="block truncate text-xs font-medium text-slate-800">{{ o.title }}</span>
<span class="text-[11px] text-slate-400">{{ o.id }} · 卡在 {{ o.stage }}</span>
<div class="text-[11px] text-slate-400">
{{ o.change_no }} · 卡在
<!-- 状态标签 -->
<span v-if="o.status==='APPROVING'">审批中</span>
<span v-if="o.status==='APPROVED'">已通过</span>
<span v-if="o.status==='IMPLEMENTED'">待验收</span>
<span v-if="o.status==='ACCEPTED'">待关闭</span>
<span v-if="o.status==='CLOSED'">已关闭</span>
<span v-if="o.status==='WITHDRAWN'">已撤回</span>
<span v-if="o.status==='REJECTED'">已驳回</span>
</div>
</span>
<NTag size="small" type="error">超期 {{ o.days }} 天</NTag>
<NButton size="small" ghost class="text-xs" @click="handleReminder(o.id)">催办</NButton>
<NTag size="small" type="error">超期 {{ o.overdue_days }} 天</NTag>
<NButton size="small" ghost class="text-xs" :disabled="btnLoading" @click="handleReminder(o.change_id)">催办</NButton>
</div>
</div>
</NCard>
@@ -3,10 +3,11 @@ import { ref } from 'vue';
import { useEcharts } from '@/hooks/common/echarts';
defineOptions({
name: 'RingChart'
name: 'ColumnChart'
});
const props = defineProps<{ data: { name: string; highRisk: number; middleRisk: number }[]; colors: string[] }>();
const props = defineProps<{ data: any[]; }>();
const COLORS = ['#f5222d', '#faad14'];
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
@@ -19,7 +20,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
},
legend: {
bottom: '20%',
bottom: 10,
left: 'center',
icon: 'circle',
itemWidth: 10,
@@ -29,10 +30,10 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
},
grid: {
top: "10%",
top: "3%",
left: "5%",
right: "5%",
bottom: "35%",
bottom: "40%",
},
calculable: true,
xAxis: [
@@ -44,6 +45,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
axisLabel: {
interval: 0,
rotate: 50,
},
splitLine: { show: false },
axisTick: { show: false },
@@ -62,9 +64,9 @@ const { domRef, updateOptions } = useEcharts(() => ({
name: "高风险项",
stack: "总数",
type: "bar",
barWidth: 30,
barWidth: 20,
barGap: "10%",
color: "#00B42A",
color: COLORS[0],
data: [] as number[],
},
// 中风险项 - 橙色
@@ -72,23 +74,33 @@ const { domRef, updateOptions } = useEcharts(() => ({
name: "中风险项",
stack: "总数",
type: "bar",
barWidth: 30,
color: "#FF7D00",
barWidth: 20,
color: COLORS[1],
data: [] as number[],
},
]
],
// dataZoom: [{ // 使用dataZoom组件
// show: months.value.length<=5?false:true,
// type: 'slider', // 内置型数据区域缩放组件
// start: 0, // 数据窗口范围的起始百分比
// end: months.value.length<=4?100:(4/months.value.length)*100, // 数据窗口范围的结束百分比
// zoomLock: true,
// handleSize: 0,// 两边的按钮大小
// moveHandleSize: 10,
// height: 10,
// brushSelect: false,
// top: '93%'
// }]
}));
function init() {
updateOptions(opts => {
let allMonth = props.data.map((item:any) => item.name);
let highRis = props.data.map((item:any) => item.highRisk);
let middleRisk = props.data.map((item:any) => item.middleRisk);
let allMonth = props.data.map((item:any) => item.month);
let highRis = props.data.map((item:any) => item.high);
let middleRisk = props.data.map((item:any) => item.mid);
opts.xAxis[0].data = allMonth;
opts.series[0].data = highRis;
opts.series[1].data = middleRisk;
opts.series[0].color = props.colors[0];
opts.series[1].color = props.colors[1];
return opts;
});
}
@@ -97,7 +109,7 @@ init();
</script>
<template>
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
<div ref="domRef" class="w-full h-200px overflow-hidden"></div>
</template>
<style scoped></style>
+36 -7
View File
@@ -6,7 +6,8 @@ defineOptions({
name: 'RingChart'
});
const props = defineProps<{ data?: { name: string; value: number }[]; colors?: string[] }>();
const props = defineProps<{ data: any; }>();
const chartData = ref<any[]>([]);
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
@@ -14,7 +15,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
formatter: '{b}: {c} ({d}%)'
},
legend: {
bottom: 0,
bottom: 10,
left: 'center',
icon: 'circle',
itemWidth: 10,
@@ -22,9 +23,27 @@ const { domRef, updateOptions } = useEcharts(() => ({
itemStyle: {
borderWidth: 0
},
textStyle: {
rich: {
name: {
padding: [0, 0, 0, 0],
},
count: {
padding: [0, 0, 0, 0],
},
}
},
formatter: function (name: string) {
var info = chartData.value?.find((item: { name: string }) => item.name === name);
if(info){
return '{name|'+name+'} {count|'+(info.value)+'}';
}else{
return name;
}
}
},
title: {
text: `{a|19}\n{b|剩余风险项}`,
text: '',
textStyle: {
rich: {
a: {
@@ -50,7 +69,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
series: [
{
type: 'pie',
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
color: ["#f5222d", "#faad14", "#b2bcca"],
radius: ['45%', '65%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
@@ -71,9 +90,19 @@ const { domRef, updateOptions } = useEcharts(() => ({
}));
function init() {
if (!props.data) {
return;
}
updateOptions(opts => {
opts.series[0].data = props.data || [];
opts.series[0].color = props.colors || [];
const total = props.data.high + props.data.mid + props.data.low;
const data = [
{name:'高风险项',value:props.data.high},
{name:'中风险项',value:props.data.mid},
{name:'低风险项',value:props.data.low},
]
chartData.value = data;
opts.series[0].data = data;
opts.title.text = `{a|${total}}\n{b|剩余风险项}`;
return opts;
});
}
@@ -82,7 +111,7 @@ init();
</script>
<template>
<div ref="domRef" class="w-full h-220px overflow-hidden"></div>
<div ref="domRef" class="w-full h-200px overflow-hidden"></div>
</template>
<style scoped></style>