style: 统一行尾符为LF(gitattributes renormalize,无代码变更)

This commit is contained in:
2026-10-12 10:14:11 +08:00
parent 7c5f0dea0b
commit 7b71038949
37 changed files with 12551 additions and 12551 deletions
+224 -224
View File
@@ -1,224 +1,224 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { analysisRecordListApi } from "@/service/api/highRisk";
const themeStore = useThemeStore();
const props = defineProps(['currentId', 'info', 'fmeaRiskMatrixConfig'])
const emit = defineEmits(['close'])
const loading = ref<boolean>(false);
// 当前偏离
const currentDev = ref<any>(null);
// 展开收起
const collapsed = ref<Record<string, boolean>>({})
// 节点列表
const nodes = ref<any>([]);
// 点击偏离
const clickDev = (dev:any) => {
currentDev.value = dev
}
// 根据l和s计算RR风险等级
const riskLevel = (l: number, s: number) => {
const num = l * s;
for (let i = 0; i < props.fmeaRiskMatrixConfig?.risk_grades.length; i++) {
const item = props.fmeaRiskMatrixConfig?.risk_grades[i];
if (num >= item.min && num <= item.max) {
return item; // 返回整条数据
}
}
return null;
}
// 计算RR1风险等级
const countRiskLevel = (row: any) => {
const l = row.l_value;
const s = row.s_value;
const newArr = [...row?.safeguards || [],...row?.suggestions || []];
const result:any = { L: 0, S: 0 };
newArr.forEach(item => {
if (!item.value) return; // 跳过空值
const parts = item.value.split('_');
if (parts.length === 2) {
const type = parts[0].toUpperCase(); // 统一大写
const num = parseInt(parts[1], 10);
if (!isNaN(num) && (type === 'L' || type === 'S')) {
result[type] += num;
}
}
});
const newL = (l - result.L) > 0 ? l - result.L : 1;
const newS = (s - result.S) > 0 ? s - result.S : 1;
return riskLevel(newL, newS);
}
// 展开收起
const leftToggle = (id: string) => {
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
}
// 导出数据
const exportData = () => {
window.$message?.info('还差接口')
}
// 获取记录
const getList = async () => {
loading.value = true;
const {data,error} = await analysisRecordListApi(props.currentId);
if(!error){
nodes.value = data?.fmea?.nodes || [];
clickDev(nodes.value[0]?.deviations[0] || null)
}
loading.value = false;
}
onMounted(() => {
getList();
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="flex items-center gap-4 mb-3">
<p v-if="props.info?.change_title" class="text-xs text-slate-500">变更:<span class="text-slate-800">{{ props.info?.change_title }}</span></p>
<p v-if="props.info?.created_at" class="text-xs text-slate-500">分析日期:{{ props.info?.created_at?.slice(0,10) }}</p>
<p v-if="props.info?.applicant_name" class="text-xs text-slate-500">责任人:{{ props.info?.applicant_name }}</p>
<p class="ml-auto text-xs text-slate-500">分析编制与修改在变更上下文内完成,此处只读</p>
</div>
<div
class="flex gap-3"
:style="{
'--theme-color': themeStore.themeColor,
'--success-color': themeStore.otherColor.success
}"
>
<!-- 左:节点 / 偏离树 -->
<div class="w-240px">
<div class="shrink-0 rounded-md border p-2 scroll">
<p v-if="nodes.length === 0" class="text-center text-xs text-slate-400 py-10">暂无数据</p>
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
<div
class="cursor-pointer flex items-center select-none"
@click="leftToggle(node.id)"
>
<Icon v-if="collapsed[node.id]" icon="akar-icons:chevron-right" class="size-12px" />
<Icon v-else icon="akar-icons:chevron-down" class="size-12px" />
<p class="flex-1 px-2 py-1.5 text-xs font-semibold text-slate-700">步骤{{ Number(nodeIndex) + 1 }}:{{ node.node_name }}</p>
</div>
<template v-if="!collapsed[node.id]">
<div v-for="(dev, devIndex) in node.deviations" :key="devIndex"
@click="clickDev(dev)"
class="cursor-pointer flex items-center ml-4 gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition"
:class="currentDev?.id === dev.id ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'"
>
<span class="flex-1">{{ dev.deviation }}</span>
<span class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
{{ dev.rows.length }}条
</span>
</div>
</template>
</div>
</div>
</div>
<!-- 右:FMEA 记录表 -->
<div class="min-w-0 flex-1 bg-white">
<div class="scroll">
<NTable size="small" class="w-full text-xs">
<thead>
<tr>
<th class="w-60">原因</th>
<th class="w-60">后果</th>
<th class="w-6">L</th>
<th class="w-6">S</th>
<th class="w-20">RR</th>
<th class="w-60">安全措施</th>
<th class="w-60">建议措施</th>
<th class="w-20">RR1</th>
</tr>
</thead>
<tbody>
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
<td colspan="9" class="px-3 py-8 text-center text-xs text-slate-400">
当前暂无分析记录
</td>
</tr>
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
<td class="align-top">
<p class="text-xs">{{row.cause}}</p>
</td>
<td class="align-top">
<p class="text-xs">{{row.consequence}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.l_value}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.s_value}}</p>
</td>
<td class="text-center">
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: riskLevel(row.l_value, row.s_value)?.bg_color,
textColor: riskLevel(row.l_value, row.s_value)?.font_color,
borderColor: riskLevel(row.l_value, row.s_value)?.bg_color,
}"
>
{{riskLevel(row.l_value, row.s_value)?.name}}({{row.l_value * row.s_value}})
</NTag>
</td>
<td class="align-top">
<div v-if="row.safeguards && row.safeguards.length" class="flex flex-col gap-3">
<div v-for="(sItem, si) in row.safeguards" :key="si">
<p class="text-xs">{{Number(si)+1}}、{{sItem.name}}</p>
</div>
</div>
</td>
<td class="align-top">
<div v-if="row.suggestions && row.suggestions.length" class="flex flex-col gap-2">
<div v-for="(sItem2, si2) in row.suggestions" :key="si2">
<p class="text-xs">{{Number(si2)+1}}、{{sItem2.name}}</p>
</div>
</div>
</td>
<td class="text-center">
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: countRiskLevel(row)?.bg_color,
textColor: countRiskLevel(row)?.font_color,
borderColor: countRiskLevel(row)?.bg_color,
}"
>
{{countRiskLevel(row)?.name}}
</NTag>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-3">
<NButton size="small" @click="exportData">
<Icon icon="material-symbols:download" class="mr-1 size-14px" />
导出
</NButton>
<NButton type="primary" size="small" @click="emit('close')">关闭</NButton>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 155px);
overflow-y: auto;
}
</style>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { analysisRecordListApi } from "@/service/api/highRisk";
const themeStore = useThemeStore();
const props = defineProps(['currentId', 'info', 'fmeaRiskMatrixConfig'])
const emit = defineEmits(['close'])
const loading = ref<boolean>(false);
// 当前偏离
const currentDev = ref<any>(null);
// 展开收起
const collapsed = ref<Record<string, boolean>>({})
// 节点列表
const nodes = ref<any>([]);
// 点击偏离
const clickDev = (dev:any) => {
currentDev.value = dev
}
// 根据l和s计算RR风险等级
const riskLevel = (l: number, s: number) => {
const num = l * s;
for (let i = 0; i < props.fmeaRiskMatrixConfig?.risk_grades.length; i++) {
const item = props.fmeaRiskMatrixConfig?.risk_grades[i];
if (num >= item.min && num <= item.max) {
return item; // 返回整条数据
}
}
return null;
}
// 计算RR1风险等级
const countRiskLevel = (row: any) => {
const l = row.l_value;
const s = row.s_value;
const newArr = [...row?.safeguards || [],...row?.suggestions || []];
const result:any = { L: 0, S: 0 };
newArr.forEach(item => {
if (!item.value) return; // 跳过空值
const parts = item.value.split('_');
if (parts.length === 2) {
const type = parts[0].toUpperCase(); // 统一大写
const num = parseInt(parts[1], 10);
if (!isNaN(num) && (type === 'L' || type === 'S')) {
result[type] += num;
}
}
});
const newL = (l - result.L) > 0 ? l - result.L : 1;
const newS = (s - result.S) > 0 ? s - result.S : 1;
return riskLevel(newL, newS);
}
// 展开收起
const leftToggle = (id: string) => {
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
}
// 导出数据
const exportData = () => {
window.$message?.info('还差接口')
}
// 获取记录
const getList = async () => {
loading.value = true;
const {data,error} = await analysisRecordListApi(props.currentId);
if(!error){
nodes.value = data?.fmea?.nodes || [];
clickDev(nodes.value[0]?.deviations[0] || null)
}
loading.value = false;
}
onMounted(() => {
getList();
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="flex items-center gap-4 mb-3">
<p v-if="props.info?.change_title" class="text-xs text-slate-500">变更:<span class="text-slate-800">{{ props.info?.change_title }}</span></p>
<p v-if="props.info?.created_at" class="text-xs text-slate-500">分析日期:{{ props.info?.created_at?.slice(0,10) }}</p>
<p v-if="props.info?.applicant_name" class="text-xs text-slate-500">责任人:{{ props.info?.applicant_name }}</p>
<p class="ml-auto text-xs text-slate-500">分析编制与修改在变更上下文内完成,此处只读</p>
</div>
<div
class="flex gap-3"
:style="{
'--theme-color': themeStore.themeColor,
'--success-color': themeStore.otherColor.success
}"
>
<!-- 左:节点 / 偏离树 -->
<div class="w-240px">
<div class="shrink-0 rounded-md border p-2 scroll">
<p v-if="nodes.length === 0" class="text-center text-xs text-slate-400 py-10">暂无数据</p>
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
<div
class="cursor-pointer flex items-center select-none"
@click="leftToggle(node.id)"
>
<Icon v-if="collapsed[node.id]" icon="akar-icons:chevron-right" class="size-12px" />
<Icon v-else icon="akar-icons:chevron-down" class="size-12px" />
<p class="flex-1 px-2 py-1.5 text-xs font-semibold text-slate-700">步骤{{ Number(nodeIndex) + 1 }}:{{ node.node_name }}</p>
</div>
<template v-if="!collapsed[node.id]">
<div v-for="(dev, devIndex) in node.deviations" :key="devIndex"
@click="clickDev(dev)"
class="cursor-pointer flex items-center ml-4 gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition"
:class="currentDev?.id === dev.id ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'"
>
<span class="flex-1">{{ dev.deviation }}</span>
<span class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
{{ dev.rows.length }}条
</span>
</div>
</template>
</div>
</div>
</div>
<!-- 右:FMEA 记录表 -->
<div class="min-w-0 flex-1 bg-white">
<div class="scroll">
<NTable size="small" class="w-full text-xs">
<thead>
<tr>
<th class="w-60">原因</th>
<th class="w-60">后果</th>
<th class="w-6">L</th>
<th class="w-6">S</th>
<th class="w-20">RR</th>
<th class="w-60">安全措施</th>
<th class="w-60">建议措施</th>
<th class="w-20">RR1</th>
</tr>
</thead>
<tbody>
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
<td colspan="9" class="px-3 py-8 text-center text-xs text-slate-400">
当前暂无分析记录
</td>
</tr>
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
<td class="align-top">
<p class="text-xs">{{row.cause}}</p>
</td>
<td class="align-top">
<p class="text-xs">{{row.consequence}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.l_value}}</p>
</td>
<td>
<p class="text-xs text-center w-10">{{row.s_value}}</p>
</td>
<td class="text-center">
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: riskLevel(row.l_value, row.s_value)?.bg_color,
textColor: riskLevel(row.l_value, row.s_value)?.font_color,
borderColor: riskLevel(row.l_value, row.s_value)?.bg_color,
}"
>
{{riskLevel(row.l_value, row.s_value)?.name}}({{row.l_value * row.s_value}})
</NTag>
</td>
<td class="align-top">
<div v-if="row.safeguards && row.safeguards.length" class="flex flex-col gap-3">
<div v-for="(sItem, si) in row.safeguards" :key="si">
<p class="text-xs">{{Number(si)+1}}、{{sItem.name}}</p>
</div>
</div>
</td>
<td class="align-top">
<div v-if="row.suggestions && row.suggestions.length" class="flex flex-col gap-2">
<div v-for="(sItem2, si2) in row.suggestions" :key="si2">
<p class="text-xs">{{Number(si2)+1}}、{{sItem2.name}}</p>
</div>
</div>
</td>
<td class="text-center">
<NTag
v-if="row.l_value>0 && row.s_value>0"
class="text-[11px]"
size="small"
:color="{
color: countRiskLevel(row)?.bg_color,
textColor: countRiskLevel(row)?.font_color,
borderColor: countRiskLevel(row)?.bg_color,
}"
>
{{countRiskLevel(row)?.name}}
</NTag>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-3">
<NButton size="small" @click="exportData">
<Icon icon="material-symbols:download" class="mr-1 size-14px" />
导出
</NButton>
<NButton type="primary" size="small" @click="emit('close')">关闭</NButton>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 155px);
overflow-y: auto;
}
</style>
+403 -403
View File
@@ -1,403 +1,403 @@
<script setup lang="ts">
import { ref, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { getTimeRangeStamp, formatTimestamp } from '@/utils/common';
import { matrixConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { highRiskListApi } from '@/service/api/highRisk';
import RiskPre from "./riskPre.vue";
import Hazop from "./hazop.vue";
import Fmea from "./fmea.vue";
import RiskCheck from "./riskCheck.vue";
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
// 当前记录
const currentRecord = ref<any>(null);
const RISK_RANGES = [
{ label: "近一个月", value: "month" },
{ label: "近三个月", value: "quarter" },
{ label: "近半年", value: "half_year" },
{ label: "全部", value: 'all' }
];
// 风险矩阵配置
const hazopMatrix = ref<any>(null);
const fmeaMatrix = ref<any>(null);
// 插件抽屉
const pluginDrawer = ref<boolean>(false);
// 高风险场景列表
const list = ref<any[]>([]);
// 查看变更
const jumpTo = (r: any) => {
routerPushByKey("details_index",{ query: { id: r?.change_id?.toString(),key:'my_highrisk' } });
};
// 查看分析记录
const viewAnalysis = (record: any) => {
currentRecord.value = record;
pluginDrawer.value = true;
};
// 时间范围
const timeRange = ref<string>('month');
// 组织树
const orgTree = ref<any[]>([]);
// 筛选条件
const formInfo = ref<any>({
kw: '',
org_id: 1,
from: getTimeRangeStamp(timeRange.value)?.from ?? null,
to: getTimeRangeStamp(timeRange.value)?.to ?? null,
});
const pagination = ref<{page: number, pageSize: number, itemCount: number}>({
page: 1,
pageSize: 20,
itemCount: 0,
})
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 搜索
const searchHandle = () => {
pagination.value.page = 1;
getList();
}
// 组织选择改变
const orgChange = (val:any) => {
formInfo.value.org_id = val;
searchHandle();
}
// 时间范围改变
const timeRangeChange = (val: string) => {
timeRange.value = val;
if(val === 'all'){
formInfo.value.from = null;
formInfo.value.to = null;
}else{
const timeRange = getTimeRangeStamp(val);
formInfo.value.from = timeRange?.from ?? null;
formInfo.value.to = timeRange?.to ?? null;
}
searchHandle();
}
// 重置
const reset = () => {
timeRange.value = 'month';
formInfo.value.kw = '';
formInfo.value.org_id = 1;
formInfo.value.from = getTimeRangeStamp('month')?.from ?? null;
formInfo.value.to = getTimeRangeStamp('month')?.to ?? null;
searchHandle();
};
// 分页改变
const pageChange = (page: number) => {
pagination.value.page = page;
getList();
}
// 获取列表
const getList = async () => {
loading.value = true;
const {data,error} = await highRiskListApi({
kw: formInfo.value.kw,
org_id: formInfo.value.org_id,
from: formatTimestamp(formInfo.value.from),
to: formatTimestamp(formInfo.value.to),
page_num: pagination.value.page,
page_size: pagination.value.pageSize,
});
if(!error){
list.value = data?.list ?? [];
pagination.value.itemCount = data?.total ?? 0;
}
loading.value = false;
}
// 获取组织列表
const getOrgList = async () => {
loading.value = true;
const {data,error} = await orgListApi();
if(!error){
orgTree.value = data;
if(orgTree.value.length > 0){
getList();
}
}
loading.value = false;
}
// 获取风险矩阵配置
const matrixConfig = async () => {
const {data,error} = await matrixConfigApi();
if(!error){
hazopMatrix.value = data?.hazop || null;
fmeaMatrix.value = data?.fmea || null;
}
}
// 风险矩阵等级匹配
const matchRiskLevel = (configList:any,name: string) => {
if(!configList){
return null;
}
const target = configList.find((item:any) => item.name === name);
return target ?? null;
}
onMounted(async () => {
matrixConfig();
getOrgList();
})
</script>
<template>
<!-- 高风险场景 -->
<NSpin :show="loading" size="small">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">变更新增高风险场景</p>
<span class="text-xs text-slate-400 mt-2px">{{ list.length }} 个高风险场景</span>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NTreeSelect
class="!w-200px"
:value="formInfo.org_id"
:options="orgTree"
size="small"
default-expand-all
label-field="label"
key-field="id"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@update:value="orgChange"
/>
<span class="mr-1 text-xs text-slate-400">时间</span>
<NButton
size="tiny"
round
v-for="r in RISK_RANGES"
:key="r.value"
:type="timeRange === r.value ? 'primary' : 'default'"
@click="timeRangeChange(r.value)"
>
{{ r.label }}
</NButton>
<NInput v-model:value="formInfo.kw" size="small" class="text-xs !w-200px ml-auto" clearable placeholder="关键词检索…" @clear="formInfo.kw = ''; searchHandle()">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
</template>
</NInput>
<NButton size="small" class="text-sm" @click="reset">
<Icon icon="system-uicons:reset" class="size-12px" />重置
</NButton>
</div>
<div class="scroll">
<div v-if="list.length" class="space-y-3 px-4 pb-3">
<div v-for="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag size="small" type="info" class="text-11px">{{ s.source==='RISK'?'风险预分析':s.source==='RISK_CHECK'?'风险检查': s.source }}</NTag>
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
<!-- 固有风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.inherent_level && s.inherent_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.inherent_level ?? '-'}}
</span>
</template>
<!-- 剩余风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.residual_level && s.residual_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.residual_level ?? '-'}}
</span>
</template>
</div>
<div class="mt-1 text-xs text-slate-600">{{ s.scene }}</div>
<div class="mt-0.5 text-xs text-slate-400">
变更:{{ s?.change_title }} · {{ s?.org_name }} · 分析日期 {{ s?.created_at?.slice(0, 10) }} · 分析记录 {{ s?.record_count }} 行
</div>
</div>
<NButton ghost size="small" class="text-xs" @click="jumpTo(s)">查看变更</NButton>
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
</div>
</div>
<div v-else class="h-100% flex items-center justify-center">
<NEmpty description="暂无数据"></NEmpty>
</div>
</div>
<div class="flex justify-end mt-3 px-4 py-2 border-t">
<NPagination
v-model:page="pagination.page"
:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:on-update:page="pageChange"
>
<template #prefix="{ itemCount }">
共 {{ itemCount }} 条
</template>
</NPagination>
</div>
</div>
<!-- 插件抽屉 -->
<NDrawer v-model:show="pluginDrawer" width="80%" placement="right" :auto-focus="false">
<NDrawerContent closable>
<template #header>
<div class="flex items-center gap-2">
<span>风险分析记录表</span>
<NTag v-if="currentRecord?.source" size="small" type="info">
<span v-if="currentRecord?.source === 'RISK'">风险预分析</span>
<span v-else-if="currentRecord?.source === 'HAZOP'">HAZOP</span>
<span v-else-if="currentRecord?.source === 'FMEA'">FMEA</span>
<span v-else-if="currentRecord?.source === 'RISK_CHECK'">风险检查</span>
<span v-else>未知分析记录</span>
</NTag>
</div>
</template>
<template v-if="currentRecord?.source === 'RISK'">
<RiskPre :currentId="currentRecord?.change_id" :info="currentRecord" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'HAZOP'">
<Hazop :currentId="currentRecord?.change_id" :info="currentRecord" :hazopRiskMatrixConfig="hazopMatrix" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'FMEA'">
<Fmea :currentId="currentRecord?.change_id" :info="currentRecord" :fmeaRiskMatrixConfig="fmeaMatrix" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'RISK_CHECK'">
<RiskCheck :currentId="currentRecord?.change_id" :info="currentRecord" @close="pluginDrawer = false" />
</template>
</NDrawerContent>
</NDrawer>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 310px);
overflow-y: auto;
}
</style>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { getTimeRangeStamp, formatTimestamp } from '@/utils/common';
import { matrixConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { highRiskListApi } from '@/service/api/highRisk';
import RiskPre from "./riskPre.vue";
import Hazop from "./hazop.vue";
import Fmea from "./fmea.vue";
import RiskCheck from "./riskCheck.vue";
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
// 当前记录
const currentRecord = ref<any>(null);
const RISK_RANGES = [
{ label: "近一个月", value: "month" },
{ label: "近三个月", value: "quarter" },
{ label: "近半年", value: "half_year" },
{ label: "全部", value: 'all' }
];
// 风险矩阵配置
const hazopMatrix = ref<any>(null);
const fmeaMatrix = ref<any>(null);
// 插件抽屉
const pluginDrawer = ref<boolean>(false);
// 高风险场景列表
const list = ref<any[]>([]);
// 查看变更
const jumpTo = (r: any) => {
routerPushByKey("details_index",{ query: { id: r?.change_id?.toString(),key:'my_highrisk' } });
};
// 查看分析记录
const viewAnalysis = (record: any) => {
currentRecord.value = record;
pluginDrawer.value = true;
};
// 时间范围
const timeRange = ref<string>('month');
// 组织树
const orgTree = ref<any[]>([]);
// 筛选条件
const formInfo = ref<any>({
kw: '',
org_id: 1,
from: getTimeRangeStamp(timeRange.value)?.from ?? null,
to: getTimeRangeStamp(timeRange.value)?.to ?? null,
});
const pagination = ref<{page: number, pageSize: number, itemCount: number}>({
page: 1,
pageSize: 20,
itemCount: 0,
})
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 搜索
const searchHandle = () => {
pagination.value.page = 1;
getList();
}
// 组织选择改变
const orgChange = (val:any) => {
formInfo.value.org_id = val;
searchHandle();
}
// 时间范围改变
const timeRangeChange = (val: string) => {
timeRange.value = val;
if(val === 'all'){
formInfo.value.from = null;
formInfo.value.to = null;
}else{
const timeRange = getTimeRangeStamp(val);
formInfo.value.from = timeRange?.from ?? null;
formInfo.value.to = timeRange?.to ?? null;
}
searchHandle();
}
// 重置
const reset = () => {
timeRange.value = 'month';
formInfo.value.kw = '';
formInfo.value.org_id = 1;
formInfo.value.from = getTimeRangeStamp('month')?.from ?? null;
formInfo.value.to = getTimeRangeStamp('month')?.to ?? null;
searchHandle();
};
// 分页改变
const pageChange = (page: number) => {
pagination.value.page = page;
getList();
}
// 获取列表
const getList = async () => {
loading.value = true;
const {data,error} = await highRiskListApi({
kw: formInfo.value.kw,
org_id: formInfo.value.org_id,
from: formatTimestamp(formInfo.value.from),
to: formatTimestamp(formInfo.value.to),
page_num: pagination.value.page,
page_size: pagination.value.pageSize,
});
if(!error){
list.value = data?.list ?? [];
pagination.value.itemCount = data?.total ?? 0;
}
loading.value = false;
}
// 获取组织列表
const getOrgList = async () => {
loading.value = true;
const {data,error} = await orgListApi();
if(!error){
orgTree.value = data;
if(orgTree.value.length > 0){
getList();
}
}
loading.value = false;
}
// 获取风险矩阵配置
const matrixConfig = async () => {
const {data,error} = await matrixConfigApi();
if(!error){
hazopMatrix.value = data?.hazop || null;
fmeaMatrix.value = data?.fmea || null;
}
}
// 风险矩阵等级匹配
const matchRiskLevel = (configList:any,name: string) => {
if(!configList){
return null;
}
const target = configList.find((item:any) => item.name === name);
return target ?? null;
}
onMounted(async () => {
matrixConfig();
getOrgList();
})
</script>
<template>
<!-- 高风险场景 -->
<NSpin :show="loading" size="small">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">变更新增高风险场景</p>
<span class="text-xs text-slate-400 mt-2px">{{ list.length }} 个高风险场景</span>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NTreeSelect
class="!w-200px"
:value="formInfo.org_id"
:options="orgTree"
size="small"
default-expand-all
label-field="label"
key-field="id"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@update:value="orgChange"
/>
<span class="mr-1 text-xs text-slate-400">时间</span>
<NButton
size="tiny"
round
v-for="r in RISK_RANGES"
:key="r.value"
:type="timeRange === r.value ? 'primary' : 'default'"
@click="timeRangeChange(r.value)"
>
{{ r.label }}
</NButton>
<NInput v-model:value="formInfo.kw" size="small" class="text-xs !w-200px ml-auto" clearable placeholder="关键词检索…" @clear="formInfo.kw = ''; searchHandle()">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
</template>
</NInput>
<NButton size="small" class="text-sm" @click="reset">
<Icon icon="system-uicons:reset" class="size-12px" />重置
</NButton>
</div>
<div class="scroll">
<div v-if="list.length" class="space-y-3 px-4 pb-3">
<div v-for="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag size="small" type="info" class="text-11px">{{ s.source==='RISK'?'风险预分析':s.source==='RISK_CHECK'?'风险检查': s.source }}</NTag>
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
<!-- 固有风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.inherent_level && s.inherent_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.inherent_level ?? '-'}}
</span>
</template>
<!-- 剩余风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.residual_level && s.residual_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.residual_level ?? '-'}}
</span>
</template>
</div>
<div class="mt-1 text-xs text-slate-600">{{ s.scene }}</div>
<div class="mt-0.5 text-xs text-slate-400">
变更:{{ s?.change_title }} · {{ s?.org_name }} · 分析日期 {{ s?.created_at?.slice(0, 10) }} · 分析记录 {{ s?.record_count }} 行
</div>
</div>
<NButton ghost size="small" class="text-xs" @click="jumpTo(s)">查看变更</NButton>
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
</div>
</div>
<div v-else class="h-100% flex items-center justify-center">
<NEmpty description="暂无数据"></NEmpty>
</div>
</div>
<div class="flex justify-end mt-3 px-4 py-2 border-t">
<NPagination
v-model:page="pagination.page"
:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:on-update:page="pageChange"
>
<template #prefix="{ itemCount }">
共 {{ itemCount }} 条
</template>
</NPagination>
</div>
</div>
<!-- 插件抽屉 -->
<NDrawer v-model:show="pluginDrawer" width="80%" placement="right" :auto-focus="false">
<NDrawerContent closable>
<template #header>
<div class="flex items-center gap-2">
<span>风险分析记录表</span>
<NTag v-if="currentRecord?.source" size="small" type="info">
<span v-if="currentRecord?.source === 'RISK'">风险预分析</span>
<span v-else-if="currentRecord?.source === 'HAZOP'">HAZOP</span>
<span v-else-if="currentRecord?.source === 'FMEA'">FMEA</span>
<span v-else-if="currentRecord?.source === 'RISK_CHECK'">风险检查</span>
<span v-else>未知分析记录</span>
</NTag>
</div>
</template>
<template v-if="currentRecord?.source === 'RISK'">
<RiskPre :currentId="currentRecord?.change_id" :info="currentRecord" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'HAZOP'">
<Hazop :currentId="currentRecord?.change_id" :info="currentRecord" :hazopRiskMatrixConfig="hazopMatrix" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'FMEA'">
<Fmea :currentId="currentRecord?.change_id" :info="currentRecord" :fmeaRiskMatrixConfig="fmeaMatrix" @close="pluginDrawer = false" />
</template>
<template v-if="currentRecord?.source === 'RISK_CHECK'">
<RiskCheck :currentId="currentRecord?.change_id" :info="currentRecord" @close="pluginDrawer = false" />
</template>
</NDrawerContent>
</NDrawer>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 310px);
overflow-y: auto;
}
</style>
+88 -88
View File
@@ -1,88 +1,88 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { analysisRecordListApi } from '@/service/api/highRisk';
const themeStore = useThemeStore();
const props = defineProps(['isAiOpen','currentId'])
const emit = defineEmits(['close','toTable']);
// 表格样式
const TH = "border bg-slate-100 px-2 py-1.5 text-[11px] font-medium text-slate-500";
const TD = "border px-1.5 py-1 align-top";
// 加载状态
const loading = ref<boolean>(false);
// 风险分析记录
const riskRecords = ref<any[]>([]);
// 获取详情
const getDetail = async () => {
loading.value = true;
const {data,error} = await analysisRecordListApi(props.currentId);
if(!error){
riskRecords.value = data?.risk_summary?.rows || [];
}
loading.value = false;
}
onMounted(() => {
getDetail();
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="space-y-3" :style="{'--theme-color': themeStore.themeColor}">
<div class="bg-white">
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
风险分析记录表
</div>
<p v-if="riskRecords.length===0" class="border py-5 text-center text-xs text-slate-400">暂无风险分析记录</p>
<div v-else class="scroll">
<table class="w-full">
<thead>
<tr>
<th :class="TH" class="w-30">风险项</th>
<th :class="TH" class="w-60">场景描述</th>
<th :class="TH" class="w-20">固有风险</th>
<th :class="TH" class="w-50">现有措施</th>
<th :class="TH" class="w-60">建议措施</th>
<th :class="TH" class="w-20">剩余风险</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in riskRecords" :key="rowIndex">
<td :class="TD">
<span>{{row.risk_item}}</span>
</td>
<td :class="TD">
<span>{{row.scene_desc}}</span>
</td>
<td :class="TD" class="text-center">
<span>{{row.risk_value}}</span>
</td>
<td :class="TD">
<span>{{row.safeguards}}</span>
</td>
<td :class="TD">
<span>{{row.suggestions}}</span>
</td>
<td :class="TD" class="text-center">
<span>{{row.residual_risk_value}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll{
height: calc(100vh - 135px);
overflow-y: auto;
}
</style>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { analysisRecordListApi } from '@/service/api/highRisk';
const themeStore = useThemeStore();
const props = defineProps(['isAiOpen','currentId'])
const emit = defineEmits(['close','toTable']);
// 表格样式
const TH = "border bg-slate-100 px-2 py-1.5 text-[11px] font-medium text-slate-500";
const TD = "border px-1.5 py-1 align-top";
// 加载状态
const loading = ref<boolean>(false);
// 风险分析记录
const riskRecords = ref<any[]>([]);
// 获取详情
const getDetail = async () => {
loading.value = true;
const {data,error} = await analysisRecordListApi(props.currentId);
if(!error){
riskRecords.value = data?.risk_summary?.rows || [];
}
loading.value = false;
}
onMounted(() => {
getDetail();
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="space-y-3" :style="{'--theme-color': themeStore.themeColor}">
<div class="bg-white">
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
风险分析记录表
</div>
<p v-if="riskRecords.length===0" class="border py-5 text-center text-xs text-slate-400">暂无风险分析记录</p>
<div v-else class="scroll">
<table class="w-full">
<thead>
<tr>
<th :class="TH" class="w-30">风险项</th>
<th :class="TH" class="w-60">场景描述</th>
<th :class="TH" class="w-20">固有风险</th>
<th :class="TH" class="w-50">现有措施</th>
<th :class="TH" class="w-60">建议措施</th>
<th :class="TH" class="w-20">剩余风险</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in riskRecords" :key="rowIndex">
<td :class="TD">
<span>{{row.risk_item}}</span>
</td>
<td :class="TD">
<span>{{row.scene_desc}}</span>
</td>
<td :class="TD" class="text-center">
<span>{{row.risk_value}}</span>
</td>
<td :class="TD">
<span>{{row.safeguards}}</span>
</td>
<td :class="TD">
<span>{{row.suggestions}}</span>
</td>
<td :class="TD" class="text-center">
<span>{{row.residual_risk_value}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll{
height: calc(100vh - 135px);
overflow-y: auto;
}
</style>