Files
moc/src/views/systemManage/setting/index.vue
T
2026-08-28 15:04:44 +08:00

1045 lines
53 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 { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { systemConfigApi, addSystemConfigApi, editSystemConfigApi, deleteSystemConfigApi, updateConfigApi } from '@/service/api/system';
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
// *********************AI 辅助功能配置*********************//
const aiConfig = ref<any[]>([])
const switchLoading = ref<boolean>(false);
const modeOptions = ref<{label:string, value:number}[]>([
{label:'kimi-k2(企业私有化部署)',value:1}
])
const testing = ref<boolean>(false);
const showLog = ref<boolean>(false);
const AI_LOGS = [
{ scene: "AI-01 变更识别", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:12", cost: "1.8s", adopted: "修改后采纳" },
{ scene: "AI-03 风险预分析", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:15", cost: "2.4s", adopted: "采纳" },
{ scene: "AI-07 审批辅助", user: "李主任", change: "MOC-2026-R01-0039", time: "08-24 08:47", cost: "1.2s", adopted: "采纳" },
{ scene: "AI-06 PSSR 生成", user: "张工艺", change: "MOC-2026-R02-0052", time: "08-23 16:20", cost: "2.1s", adopted: "未采纳" },
{ scene: "AI-09 关闭辅助判定", user: "系统管理员", change: "MOC-2026-R01-0035", time: "08-23 15:02", cost: "1.5s", adopted: "采纳" },
];
// AI 辅助功能切换
const toggle = async (v: boolean, item:any) => {
switchLoading.value = true;
const {error} = await updateConfigApi({items:[{
id:item.id,
label:item.label,
remark:item.remark,
value:v ? '1' : '0'
}]});
if(!error){
item.value = v ? '1' : '0';
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
}
switchLoading.value = false;
}
// 测试连接
const testConn = () => {
testing.value = true;
setTimeout(() => {
testing.value = false;
window.$message?.success(`连接测试通过:${aiConfig.value.mode} 响应正常(耗时 0.8s`);
}, 900);
}
// 保存ai配置
const saveAiCfg = () => {
window.$message?.success("AI 接口配置已保存(配置变更留痕,立即生效)");
}
// *********************变更类型*********************//
const typeRef = ref<any>(null);
const typeModal = ref<boolean>(false);
const typeLoading = ref<boolean>(false);
const currentType = ref<string>('add');
const typeInfo = ref<{id:number, label:string, remark?:string, color:string,value:string,group_key:string}>({
id:0,
label:'',
remark:'',
color:'',
value: '',
group_key:'change_type'
})
const typeRules = {
label:{required:true, message:'请输入类型名称', trigger:'blur'},
color:{required:true, message:'请选择类型颜色', trigger:['blur','change']},
}
const typeList = ref<{id:number, label:string, remark?:string, color:string,value:string}[]>([])
// 新增类型
const addType = () => {
typeInfo.value = {
id:0,
label:'',
remark:'',
color:'',
value: '',
group_key:'change_type'
}
currentType.value = 'add';
typeModal.value = true;
}
// 编辑类型
const editType = (row:any) => {
typeInfo.value = {
id:row.id,
label:row.label,
remark:row.remark,
color:row.color,
value: row.value,
group_key:'change_type'
}
currentType.value = 'edit';
typeModal.value = true;
}
// 删除类型
const deleteType = async (id:number) => {
const {error} = await deleteSystemConfigApi(id);
if(!error){
typeList.value = typeList.value.filter((i:any) => i.id !== id);
window.$message?.success('删除成功')
}
}
// 保存类型
const typeSave = async (e:MouseEvent) => {
e.preventDefault()
typeRef.value?.validate(async (errors:any) => {
if (!errors) {
typeLoading.value = true;
// 新增类型
if(currentType.value === 'add'){
// 去掉id字段
const { id, ...rest } = typeInfo.value;
rest.value = (typeList.value.length + 1).toString();
const {error} = await addSystemConfigApi(rest);
if(!error){
window.$message?.success('添加成功')
typeModal.value = false;
}
}
// 编辑类型
if(currentType.value === 'edit'){
// 去掉id字段
const { id, ...rest } = typeInfo.value;
const {error} = await editSystemConfigApi(typeInfo.value.id, rest);
if(!error){
window.$message?.success('编辑成功')
typeModal.value = false;
}
}
typeLoading.value = false;
getConfig();
}
})
}
// *********************变更等级*********************//
const levelRef = ref<any>(null);
const levelModal = ref<boolean>(false);
const levelLoading = ref<boolean>(false);
const currentLevel = ref<string>('add');
// const toolsOptions = ref<{label:string, value:number}[]>([
// {label:'AI预分析',value:1},
// {label:'检查表法',value:2},
// {label:'HAZOP',value:3},
// {label:'JSA',value:4},
// {label:'SCL',value:5},
// ])
const levelInfo = ref<{id:number, label:string, remark:string,color:string,value:string,group_key:string}>({
id:0,
label:'',
remark:'',
color:'',
value: '',
group_key:'change_level'
})
const levelRules = {
label:{required:true, message:'请输入等级名称', trigger:'blur'},
color:{required:true, message:'请选择等级颜色', trigger:['blur','change']},
remark:{required:true, message:'请输入等级判定方式', trigger:'blur'},
}
const levelList = ref<{id:number, label:string, remark:string,color:string,value:string}[]>([])
// 新增等级
const addLevel = () => {
levelInfo.value = {
id:0,
label:'',
remark:'',
color:'',
value:'',
group_key:'change_level'
}
currentLevel.value = 'add';
levelModal.value = true;
}
// 编辑等级
const editLevel = (row:any) => {
levelInfo.value = {
id:row.id,
label:row.label,
remark:row.remark,
color:row.color,
value:row.value,
group_key:'change_level'
}
currentLevel.value = 'edit';
levelModal.value = true;
}
// 删除等级
const deleteLevel = async (id:number) => {
const {error} = await deleteSystemConfigApi(id);
if(!error){
levelList.value = levelList.value.filter((i:any) => i.id !== id);
window.$message?.success('删除成功')
}
}
// 保存等级
const levelSave = async (e:MouseEvent) => {
e.preventDefault()
levelRef.value?.validate(async (errors:any) => {
if (!errors) {
levelLoading.value = true;
if(currentLevel.value === 'add'){
// 去掉id字段
const { id, ...rest } = levelInfo.value;
rest.value = (levelList.value.length + 1).toString();
const {error} = await addSystemConfigApi(rest);
if(!error){
window.$message?.success('添加成功')
levelModal.value = false;
}
}
if(currentLevel.value === 'edit'){
// 去掉id字段
const { id, ...rest } = levelInfo.value;
const {error} = await editSystemConfigApi(levelInfo.value.id, rest);
if(!error){
window.$message?.success('编辑成功')
levelModal.value = false;
}
}
levelLoading.value = false;
getConfig();
}
})
}
// *********************变更编号规则*********************//
const ruleLoading = ref<boolean>(false);
const previewNumber = computed(() => {
let prefix = ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.value || '';
let year = '';
let code = ruleConfig.value.find((item:any) => item.item_key === 'unit_code')?.value || '';
let digit = '';
const currentYear = new Date().getFullYear();
if (ruleConfig.value.find((item:any) => item.item_key === 'year_format')?.value === 'yyyy') {
year = currentYear.toString();
} else if (ruleConfig.value.find((item:any) => item.item_key === 'year_format')?.value === 'yy') {
year = currentYear.toString().substring(2);
}
if (ruleConfig.value.find((item:any) => item.item_key === 'seq_length')?.value) {
digit = '0'.repeat(ruleConfig.value.find((item:any) => item.item_key === 'seq_length')?.value - 1) + '1';
}
return prefix+'-' + year + '-' + code + '-' + digit;
});
const ruleConfig = ref<any[]>([])
const yearOptions = ref<{label:string, value:string}[]>([
{label:'四位年份(yyyy',value:'yyyy'},
{label:'两位年份(yy',value:'yy'},
])
const codeOptions = ref<{label:string, value:string}[]>([
{label:'申请部门装置代码',value:'R01'},
{label:'公司统一编号(不带装置代码)',value:'R02'},
])
const ruleOptions = ref<{label:string, value:string}[]>([
{label:'按年度+装置代码重置',value:'YEARLY+CODE'},
{label:'按年度重置',value:'YEARLY'},
{label:'连续不重置',value:'CONTINUE'},
])
// 保存编号规则
const saveRule = async () => {
if(ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.value===''){
return window.$message?.warning(`请输入${ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.label}`);
}
ruleLoading.value = true;
const {error} = await updateConfigApi({items:ruleConfig.value});
if(!error){
window.$message?.success("保存成功");
}
ruleLoading.value = false;
}
// *************** 风险矩阵设置(按分析方法) ***************//
const dimensionModal = ref<boolean>(false);
const dimensionLoading = ref<boolean>(false);
const dimForm = ref({ s: [], l: [] });
const LV_CLS = [
"border-emerald-300 bg-emerald-100 text-emerald-700",
"border-yellow-300 bg-yellow-100 text-yellow-700",
"border-orange-300 bg-orange-100 text-orange-700",
"border-red-300 bg-red-100 text-red-700",
];
const matrices = ref([
{
tool: "HAZOP",
sLabels: ["轻微", "一般", "较重", "严重", "灾难"],
lLabels: ["极少发生", "较少发生", "偶尔发生", "可能发生", "频繁发生"],
levels: [
{ name: "低", min: 1, max: 4, cls: LV_CLS[0] },
{ name: "一般", min: 5, max: 9, cls: LV_CLS[1] },
{ name: "较大", min: 10, max: 16, cls: LV_CLS[2] },
{ name: "重大", min: 17, max: 25, cls: LV_CLS[3] },
],
},
{
tool: "JSA",
sLabels: ["轻微伤害", "轻伤", "重伤", "单人死亡", "多人死亡"],
lLabels: ["极不可能", "不太可能", "有可能", "较可能", "很可能"],
levels: [
{ name: "低", min: 1, max: 6, cls: LV_CLS[0] },
{ name: "一般", min: 7, max: 12, cls: LV_CLS[1] },
{ name: "较大", min: 13, max: 19, cls: LV_CLS[2] },
{ name: "重大", min: 20, max: 25, cls: LV_CLS[3] },
],
},
{
tool: "SCL",
sLabels: ["轻微影响", "一般影响", "较大影响", "严重影响", "灾难性影响"],
lLabels: ["极少", "较少", "偶尔", "可能", "经常"],
levels: [
{ name: "低", min: 1, max: 2, cls: LV_CLS[0] },
{ name: "一般", min: 3, max: 6, cls: LV_CLS[1] },
{ name: "较大", min: 7, max: 12, cls: LV_CLS[2] },
{ name: "重大", min: 13, max: 25, cls: LV_CLS[3] },
],
},
]);
const activeMatrix = ref(0);
const curMatrix = computed(() => matrices.value[activeMatrix.value]);
const cellScore = (s:number, l:number) => (s + 1) * (l + 1);
const cellLevel = (s:number, l:number) => {
const sc = cellScore(s, l);
return curMatrix.value.levels.find((lv:{min:number,max:number}) => sc >= lv.min && sc <= lv.max);
}
const dimensionSave = () => {
dimensionLoading.value = true;
setTimeout(() => {
window.$message?.success("操作成功");
dimensionLoading.value = false;
dimensionModal.value = false;
}, 1000);
}
const openDimEdit = () => {
dimForm.value = { s: [...curMatrix.value.sLabels], l: [...curMatrix.value.lLabels] };
dimensionModal.value = true;
}
// tabs 切换
const handleTabChange = (v: number) => {
activeMatrix.value = v;
}
// 获取配置
const getConfig = async () => {
const {data,error} = await systemConfigApi();
if(!error){
// 解析 AI 辅助功能配置
const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist');
if (aiAssistGroup) {
aiConfig.value = aiAssistGroup.items;
const target = aiConfig.value.find((item:any) => item.item_key === 'timeout_seconds');
if (target) {
target.value = Number(target.value);
}
}
// 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
if (changeTypeGroup) {
typeList.value = changeTypeGroup.items;
}
// 解析 变更等级设置
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelList.value = changeLevelGroup.items;
}
// 解析 变更编号规则
const changeCodeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_no_rule');
if (changeCodeGroup) {
ruleConfig.value = changeCodeGroup.items;
const target = ruleConfig.value.find((item:any) => item.item_key === 'seq_length');
if (target) {
target.value = Number(target.value);
}
}
}
}
onMounted(() => {
getConfig();
})
</script>
<template>
<NSpace vertical :size="16">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<!-- AI 辅助功能 -->
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="true" size="small" :content-style="{padding: 0}"
:style="{
borderRadius: themeStore.themeRadius + 'px',
height: '100%',
'--success-color': themeStore.otherColor.success,
'--warning-color': themeStore.otherColor.warning,
'--success-bg': getColorWithOpacity(themeStore.otherColor.success, 0.1),
'--warning-bg': getColorWithOpacity(themeStore.otherColor.warning, 0.1),
}"
>
<p class=" text-base font-semibold border-b border-slate-100 p-3 flex items-center">
<Icon icon="lucide:sparkles" class="size-14px text-violet-500 mr-1" />AI 辅助功能
</p>
<div class="py-3 px-4">
<template v-for="item in aiConfig" :key="item.item_key">
<div v-if="item.item_key==='enabled'" class="rounded-lg border p-4">
<div class="flex items-center justify-between">
<div class="text-sm font-medium">{{item.label}}</div>
<NSwitch size="small" :value="item.value==='1'" :loading="switchLoading" @update:value="(v)=>toggle(v,item)" />
</div>
<div class="mt-1 text-xs text-slate-500">
{{item.remark}}
</div>
</div>
<div v-if="item.item_key==='enabled'" class="mt-3 flex items-center gap-2 rounded-lg px-3 py-2 text-xs"
:class="item.value==='1' ? 'bg-[var(--success-bg)] text-[var(--success-color)]' : 'bg-[var(--warning-bg)] text-[var(--warning-color)]'">
<Icon v-if="item.value==='1'" icon="material-symbols:check-circle-outline" class="size-16px" />
<Icon v-else icon="proicons:alert-triangle" class="size-16px" />
当前状态{{ item.value==='1' ? "已启用 —— 变更申请界面提供 AI 变更识别与申请表生成" : "已关闭 —— 变更申请界面为纯手动模式" }}
</div>
</template>
<div class="space-y-2 border rounded-md p-3 mt-3">
<div class="flex items-center">
<Icon icon="bx:plug" class="size-14px text-slate-400 mr-1" />
AI 接口配置
</div>
<template v-for="item in aiConfig" :key="item.item_key">
<div v-if="item.item_key==='api_url'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NInput v-model:value="item.value" placeholder="请输入接口地址" />
</div>
</div>
<div v-if="item.item_key==='model'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NSelect v-model:value="item.value" :options="modeOptions" placeholder="请选择模型" />
</div>
</div>
<div v-if="item.item_key==='api_key'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NInput v-model:value="item.value" placeholder="请输入API Key" />
</div>
</div>
<div v-if="item.item_key==='timeout_seconds'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NInputNumber v-model:value="item.value" :min="1" :show-button="false" :precision="0" placeholder="请输入超时时间" />
</div>
</div>
</template>
<!-- <div class="flex items-center gap-3">
<NButton size="small" type="primary" class="text-xs" :loading="testing" @click="testConn">测试连接</NButton>
<NButton size="small" class="text-xs" @click="saveAiCfg">保存配置</NButton>
<NButton size="small" class="text-xs" @click="showLog = true">
<Icon icon="akar-icons:file" class="size-14px mr-1" />
调用记录查询
</NButton>
</div> -->
</div>
</div>
</NCard>
</NGi>
<!-- 变更类型设置支持自定义 -->
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="true" size="small" :content-style="{padding: 0}" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
<div class="flex items-center justify-between text-base font-semibold border-b border-slate-100 p-3">
<div class="flex items-center">
<Icon icon="ep:setting" class="size-16px mr-1" />
变更类型设置支持自定义
</div>
<NButton size="small" type="primary" class="text-xs" @click="addType">
<Icon icon="ic:round-plus" class="size-16px" />新增类型
</NButton>
</div>
<div class="py-3 px-4">
<div v-if="typeList.length" class="border rounded-md">
<template v-for="(item,index) in typeList" :key="item.id">
<div class="p-3 flex items-center gap-2" :class="{'border-t':index !== 0}">
<div class="flex-1 flex gap-2">
<NTag
size="small"
:color="{
color: tagBgColor(item.color),
textColor: tagTextColor(item.color),
borderColor: tagBorderColor(item.color),
}"
>{{item.label}}</NTag>
<p class="flex-1">{{item.remark}}</p>
</div>
<div class="flex items-center gap-2">
<NButton text type="primary" class="text-xs" @click="editType(item)">
<Icon icon="akar-icons:edit" class="size-14px" />编辑
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="deleteType(item.id)"
>
<template #trigger>
<NButton text type="error" class="text-xs">
<Icon icon="ep:delete" class="size-12px" />删除
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</template>
</div>
<div v-else>
<NEmpty description="暂无变更类型"></NEmpty>
</div>
<p class="mt-3 text-xs text-slate-400">每个类型可关联默认审批流程模板材料清单模板和 PSSR 检查模板;历史变更单保留原类型快照不受修改/删除影响</p>
</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="true" size="small" :content-style="{padding: 0}"
:style="{
borderRadius: themeStore.themeRadius + 'px',
height: '100%',
'--theme-color': themeStore.themeColor,
'--warning-color': themeStore.otherColor.warning,
'--error-color': themeStore.otherColor.error,
}"
>
<div class="flex items-center justify-between text-base font-semibold border-b border-slate-100 p-3">
<div class="flex items-center">
<Icon icon="ep:setting" class="size-16px mr-1" />
变更等级设定支持自定义
</div>
<NButton size="small" type="primary" class="text-xs" @click="addLevel">
<Icon icon="ic:round-plus" class="size-16px" />新增等级
</NButton>
</div>
<div class="py-3 px-4">
<div v-if="levelList.length" class="space-y-2">
<div v-for="item in levelList" :key="item.id" class="border rounded-md p-3">
<div class="flex items-center">
<div class="flex-1 flex items-center gap-2">
<NTag
size="small"
:color="{
color: tagBgColor(item.color),
textColor: tagTextColor(item.color),
borderColor: tagBorderColor(item.color),
}"
>
{{item.label}}
</NTag>
<p class="flex-1">判定方式<b>{{item.remark}}</b></p>
</div>
<div class="flex items-center gap-2">
<NButton text type="primary" class="text-xs" @click="editLevel(item)">
<Icon icon="akar-icons:edit" class="size-14px" />编辑
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="deleteLevel(item.id)"
>
<template #trigger>
<NButton text type="error" class="text-xs">
<Icon icon="ep:delete" class="size-12px" />删除
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
<!-- <div class="flex flex-wrap mt-2">
<p>对应分析工具</p>
<div class="flex items-center flex-wrap gap-2">
<NTag v-for="tool in item.tools" :key="tool.id" size="small" type="info">{{tool.name}}</NTag>
</div>
</div> -->
</div>
</div>
<div v-else>
<NEmpty description="暂无变更等级"></NEmpty>
</div>
<p class="mt-3 text-xs text-slate-400">等级决定审批流程风险分析深度与会签范围;申请时系统按判定方式自动定级并推荐对应分析工具支持人工改判留痕勾选 HAZOP/JSA/SCL的风险定级规则见下方风险矩阵设置」。</p>
</div>
</NCard>
</NGi>
<!-- 变更编号规则 -->
<NGi span="24 s:24 m:24 l:12">
<NCard :bordered="true" size="small" :content-style="{padding: 0}"
:style="{
borderRadius: themeStore.themeRadius + 'px',
height: '100%',
'--theme-color': themeStore.themeColor,
'--warning-color': themeStore.otherColor.warning,
'--error-color': themeStore.otherColor.error,
}"
>
<div class="flex items-center justify-between text-base font-semibold border-b border-slate-100 p-3">
<div class="flex items-center">
<Icon icon="ep:setting" class="size-16px mr-1" />
变更编号规则
</div>
</div>
<div class="py-3 px-4">
<div class="space-y-2">
<template v-for="item in ruleConfig" :key="item.item_key">
<div v-if="item.item_key==='prefix'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NInput v-model:value="item.value" :placeholder="`请输入${item.label}`" />
</div>
</div>
<div v-if="item.item_key==='year_format'" class="flex items-center">
<p class="w-100px text-left">年份格式</p>
<div class="flex-1">
<NSelect v-model:value="item.value" :options="yearOptions" :placeholder="`请选择${item.label}`" />
</div>
</div>
<div v-if="item.item_key==='unit_code'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NSelect v-model:value="item.value" :options="codeOptions" :placeholder="`请选择${item.label}`" />
</div>
</div>
<div v-if="item.item_key==='seq_length'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NInputNumber v-model:value="item.value" :min="1" :show-button="false" :precision="0" :placeholder="`请输入${item.label}`" />
</div>
</div>
<div v-if="item.item_key==='seq_reset'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1">
<NSelect v-model:value="item.value" :options="ruleOptions" :placeholder="`请选择${item.label}`" />
</div>
</div>
</template>
<div class="flex items-center p-3 bg-[#f8fafc] rounded-md">
<span>编号示例预览</span>
<span class="text-[var(--theme-color)]">{{previewNumber}}</span>
<NButton size="small" type="primary" class="text-xs ml-auto" :loading="ruleLoading" @click="saveRule">保存规则</NButton>
</div>
</div>
<p class="mt-3 text-xs text-slate-400">保存草稿即预占编号提交后正式生效;规则修改不影响历史单据编号</p>
</div>
</NCard>
</NGi>
</NGrid>
<!-- 风险矩阵设置按分析方法 -->
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:24 l:24">
<NCard :bordered="true" size="small" :content-style="{padding: 0}"
:style="{
borderRadius: themeStore.themeRadius + 'px',
height: '100%',
'--theme-color': themeStore.themeColor,
'--warning-color': themeStore.otherColor.warning,
'--error-color': themeStore.otherColor.error,
}"
>
<div class="flex items-center justify-between text-base font-semibold border-b border-slate-100 p-3">
<div class="flex items-center">
<Icon icon="icon-park-outline:all-application" class="size-16px mr-1" />
风险矩阵设置按分析方法
</div>
<NButton size="small" class="text-xs" @click="openDimEdit">
<Icon icon="akar-icons:edit" class="size-14px mr-1" />
维度分级定义
</NButton>
<NButton size="small" type="primary" class="text-xs">
保存矩阵
</NButton>
</div>
<div class="py-3 px-4">
<NTabs type="segment" animated :on-update:value="handleTabChange">
<NTabPane :name="0" tab="HAZOP分析矩阵">
<div class="flex flex-wrap items-start gap-6">
<div class="shrink-0">
<table class="border-collapse text-center text-[11px]">
<thead>
<tr>
<th class="px-1"></th>
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
L{{ li + 1 }}<br />{{ lb }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in 5" :key="row">
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
<td v-for="col in 5" :key="col" class="p-0.5">
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
{{ cellScore(5 - row, col - 1) }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
<span>纵轴后果严重性 S</span>
<span>横轴发生可能性 L</span>
</div>
</div>
<!-- 等级分值区间 -->
<div class="min-w-72 flex-1">
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L5×5),按分值区间定级</div>
<div class="space-y-2">
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
<span class="shrink-0 text-xs text-slate-400">分值</span>
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<span class="text-slate-400"></span>
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
</div>
</div>
<p class="mt-2.5 text-[11px] text-slate-400">
各分析方法独立维护矩阵与维度分级风险分析记录表按所选工具对应的矩阵自动定级分值区间不得重叠须落在 125 在途分析记录按原矩阵保留定级快照
</p>
</div>
</div>
</NTabPane>
<NTabPane :name="1" tab="JSA分析矩阵">
<div class="flex flex-wrap items-start gap-6">
<div class="shrink-0">
<table class="border-collapse text-center text-[11px]">
<thead>
<tr>
<th class="px-1"></th>
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
L{{ li + 1 }}<br />{{ lb }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in 5" :key="row">
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
<td v-for="col in 5" :key="col" class="p-0.5">
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
{{ cellScore(5 - row, col - 1) }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
<span>纵轴后果严重性 S</span>
<span>横轴发生可能性 L</span>
</div>
</div>
<!-- 等级分值区间 -->
<div class="min-w-72 flex-1">
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L5×5),按分值区间定级</div>
<div class="space-y-2">
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
<span class="shrink-0 text-xs text-slate-400">分值</span>
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<span class="text-slate-400"></span>
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
</div>
</div>
<p class="mt-2.5 text-[11px] text-slate-400">
各分析方法独立维护矩阵与维度分级风险分析记录表按所选工具对应的矩阵自动定级分值区间不得重叠须落在 125 在途分析记录按原矩阵保留定级快照
</p>
</div>
</div>
</NTabPane>
<NTabPane :name="2" tab="SCL分析矩阵">
<div class="flex flex-wrap items-start gap-6">
<div class="shrink-0">
<table class="border-collapse text-center text-[11px]">
<thead>
<tr>
<th class="px-1"></th>
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
L{{ li + 1 }}<br />{{ lb }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in 5" :key="row">
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
<td v-for="col in 5" :key="col" class="p-0.5">
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
{{ cellScore(5 - row, col - 1) }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
<span>纵轴后果严重性 S</span>
<span>横轴发生可能性 L</span>
</div>
</div>
<!-- 等级分值区间 -->
<div class="min-w-72 flex-1">
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L5×5),按分值区间定级</div>
<div class="space-y-2">
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
<span class="shrink-0 text-xs text-slate-400">分值</span>
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<span class="text-slate-400"></span>
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
</div>
</div>
<p class="mt-2.5 text-[11px] text-slate-400">
各分析方法独立维护矩阵与维度分级风险分析记录表按所选工具对应的矩阵自动定级分值区间不得重叠须落在 125 在途分析记录按原矩阵保留定级快照
</p>
</div>
</div>
</NTabPane>
</NTabs>
</div>
</NCard>
</NGi>
</NGrid>
<!-- AI调用记录查询 -->
<NDrawer v-model:show="showLog" :width="700" placement="right">
<NDrawerContent title="调用记录查询" closable>
<table class="w-full text-xs">
<thead>
<tr class="border-b text-left text-slate-500">
<th class="py-2 font-semibold">AI 场景</th>
<th class="py-2 font-semibold">调用人</th>
<th class="py-2 font-semibold">关联变更</th>
<th class="py-2 font-semibold">时间</th>
<th class="py-2 text-right font-semibold">耗时</th>
<th class="py-2 text-right font-semibold">采纳结果</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in AI_LOGS" :key="i" class="border-b last:border-0">
<td class="py-2 text-slate-700">{{ r.scene }}</td>
<td class="py-2 text-slate-600">{{ r.user }}</td>
<td class="py-2 font-mono text-slate-500">{{ r.change }}</td>
<td class="py-2 text-slate-500">{{ r.time }}</td>
<td class="py-2 text-right text-slate-600">{{ r.cost }}</td>
<td class="py-2 text-right">
<NTag v-if="r.adopted === '采纳'" size="small" type="success">{{ r.adopted }}</NTag>
<NTag v-if="r.adopted === '修改后采纳'" size="small" type="warning">{{ r.adopted }}</NTag>
<NTag v-if="r.adopted === '未采纳'" size="small">{{ r.adopted }}</NTag>
</td>
</tr>
</tbody>
</table>
<p class="mt-3 text-[11px] text-slate-400">调用记录含输入摘要脱敏)、模型与提示词版本耗时与采纳结果支持采纳率统计完整输出存对象存储</p>
</NDrawerContent>
</NDrawer>
<!-- 新增/编辑类型 -->
<NModal
v-model:show="typeModal"
preset="card"
:title="currentType === 'add' ? '新增变更类型' : '编辑变更类型'"
:auto-focus="false"
:style="{ width: '450px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div>
<NForm
ref="typeRef"
label-width="auto"
:model="typeInfo"
:rules="typeRules"
>
<NFormItem label="类型名称" path="label">
<NInput v-model:value="typeInfo.label" placeholder="如:仪表控制" />
</NFormItem>
<NFormItem label="颜色" path="color">
<NColorPicker v-model:value="typeInfo.color" :modes="['hex']">
<template #trigger="{ value, onClick, ref: triggerRef }">
<div
:ref="triggerRef"
:style="{
width: '40px',
height: '26px',
borderRadius: '8px',
backgroundColor: value || '#333',
cursor: 'pointer',
border: '2px solid #fff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
}"
@click="onClick"
></div>
</template>
</NColorPicker>
<NTag
v-if="typeInfo.label"
size="small"
class="ml-5"
:color="{
color: tagBgColor(typeInfo.color),
textColor: tagTextColor(typeInfo.color),
borderColor: tagBorderColor(typeInfo.color),
}"
>
{{typeInfo.label}}
</NTag>
</NFormItem>
<NFormItem label="类型定义描述" path="remark">
<NInput v-model:value="typeInfo.remark" type="textarea" placeholder="该类型覆盖的变更范围说明" />
</NFormItem>
</NForm>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="typeModal = false">取消</NButton>
<NButton size="small" type="primary" :loading="typeLoading" @click="typeSave">确定</NButton>
</div>
</template>
</NModal>
<!-- 新增/编辑等级 -->
<NModal
v-model:show="levelModal"
preset="card"
:title="currentLevel === 'add' ? '新增变更等级' : '编辑变更等级'"
:auto-focus="false"
:style="{ width: '500px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div>
<NForm
ref="levelRef"
label-width="auto"
:model="levelInfo"
:rules="levelRules"
>
<NFormItem label="等级名称" path="label">
<NInput v-model:value="levelInfo.label" placeholder="如:重大" />
</NFormItem>
<NFormItem label="颜色" path="color">
<NColorPicker v-model:value="levelInfo.color" :modes="['hex']">
<template #trigger="{ value, onClick, ref: triggerRef }">
<div
:ref="triggerRef"
:style="{
width: '40px',
height: '26px',
borderRadius: '8px',
backgroundColor: value || '#333',
cursor: 'pointer',
border: '2px solid #fff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
}"
@click="onClick"
></div>
</template>
</NColorPicker>
<NTag
v-if="levelInfo.label"
size="small"
class="ml-5"
:color="{
color: tagBgColor(levelInfo.color),
textColor: tagTextColor(levelInfo.color),
borderColor: tagBorderColor(levelInfo.color),
}"
>
{{levelInfo.label}}
</NTag>
</NFormItem>
<NFormItem label="等级判定方式" path="remark">
<NInput v-model:value="levelInfo.remark" placeholder="如:等级评分 ≥ 20 分" />
</NFormItem>
<!-- <NFormItem label="对应分析工具(多选)" path="tools">
<NSelect v-model:value="levelInfo.tools" multiple :options="toolsOptions" placeholder="请选择分析工具" />
</NFormItem> -->
</NForm>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="levelModal = false">取消</NButton>
<NButton size="small" type="primary" :loading="levelLoading" @click="levelSave">确定</NButton>
</div>
</template>
</NModal>
<!-- 维度分级定义 -->
<NModal
v-model:show="dimensionModal"
preset="card"
:title="curMatrix.tool+'矩阵维度分级定义'"
:auto-focus="false"
:style="{ width: '500px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div class="space-y-4">
<div>
<div class="mb-1.5 text-xs font-semibold text-slate-600">后果严重性 SS1 S5 递增</div>
<div class="grid grid-cols-5 gap-1.5">
<label v-for="(_, i) in dimForm.s" :key="'s' + i" class="text-center text-[10px] text-slate-400">
S{{ i + 1 }}
<NInput v-model:value="dimForm.s[i]" size="small" class="mt-0.5 text-center text-xs" />
</label>
</div>
</div>
<div>
<div class="mb-1.5 text-xs font-semibold text-slate-600">发生可能性 LL1 L5 递增</div>
<div class="grid grid-cols-5 gap-1.5">
<label v-for="(_, i) in dimForm.l" :key="'l' + i" class="text-center text-[10px] text-slate-400">
L{{ i + 1 }}
<NInput v-model:value="dimForm.l[i]" size="small" class="mt-0.5 text-center text-xs" />
</label>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="dimensionModal = false">取消</NButton>
<NButton size="small" type="primary" :loading="dimensionLoading" @click="dimensionSave">确定</NButton>
</div>
</template>
</NModal>
</NSpace>
</template>
<style scoped></style>