变更预识别修改

This commit is contained in:
2026-09-11 12:02:46 +08:00
parent 7030414146
commit 1c4c8e7b62
12 changed files with 376 additions and 85 deletions
+7 -2
View File
@@ -10,7 +10,7 @@ import { useChangeStore } from '@/store/modules/change';
import { queryUnsavedApi, saveChangeApi, submitChangeApi, getChangeDetailApi } from '@/service/api/change';
import { systemConfigApi, templateListApi, } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { formatTimestamp } from "@/utils/common";
import { formatTimestamp, anyToTimestamp } from "@/utils/common";
import ChangePre from "./modules/changePre.vue";
import ChangeApply from "./modules/changeApply.vue";
import ChangeTraining from "./modules/changeTraining.vue";
@@ -230,7 +230,7 @@ const submitConfirm = async (type: string) => {
// 变更申请表
if(type==='form'){
if(changeApplyRules(form)){
if(changeApplyRules(form?.apply_form)){
return;
}
}
@@ -254,6 +254,11 @@ const getChangeDetail = async () => {
changeNo.value = data.change_no;
changePreForm.value = data.precheck;
changeApplyForm.value = data.apply_form;
// 解析时间戳
changeApplyForm.value.plan_use_date = anyToTimestamp(changeApplyForm.value.plan_use_date);
changeApplyForm.value.restore_deadline = anyToTimestamp(changeApplyForm.value.restore_deadline);
riskAnalysisForm.value = data.risk_records;
changeTrainingForm.value = data.train;
pssrForm.value = data.pssr;
@@ -1,16 +1,18 @@
<script setup lang="ts">
import { computed, h, ref, watch } from 'vue'
import { useThemeStore } from '@/store/modules/theme';
import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { Icon } from '@iconify/vue'
import RichTextInput from './RichTextInput.vue'
const themeStore = useThemeStore();
interface ChangeItem {
item: string
before_text: string
after_text: string
}
const props = defineProps<{
data: ChangeItem[]
editable?: boolean
@@ -135,7 +137,14 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
</script>
<template>
<div class="change-compare-table">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{data.length ? `变更识别结果(${data.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
</div>
</div>
<div class="change-compare-table p-4">
<NDataTable
:columns="columns"
:data="localData"
@@ -149,6 +158,7 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
</NButton>
</div>
</div>
</div>
</template>
<style scoped>
@@ -7,12 +7,14 @@ const props = withDefaults(defineProps<{
minRows?: number
maxRows?: number
disabled?: boolean
bordered?: boolean
}>(), {
modelValue: '',
placeholder: '',
minRows: 1,
maxRows: 100,
disabled: false,
bordered: true,
})
const emit = defineEmits<{
@@ -235,7 +237,7 @@ onBeforeUnmount(() => {
<template>
<div
class="rich-text-input"
:class="{ 'is-focused': isFocused, 'is-disabled': disabled }"
:class="{ 'is-focused': isFocused, 'is-disabled': disabled, 'bordered': bordered }"
>
<!-- 工具栏 -->
<div v-show="isFocused && !disabled" class="rti-toolbar">
@@ -296,12 +298,14 @@ onBeforeUnmount(() => {
<style scoped>
.rich-text-input {
position: relative;
border: 1px solid #e5e8ef;
border-radius: 6px;
background: #fafbfc;
transition: all 0.2s ease;
overflow: visible;
}
.bordered {
border: 1px solid #e5e8ef;
border-radius: 6px;
}
.rich-text-input:hover {
border-color: #2080f0;
@@ -1,10 +1,11 @@
<!-- 变更预识别 -->
<script setup lang="ts">
import { inject, watch, ref } from 'vue';
import { inject, watch, ref, computed } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiApplication, aiRecognize, aiRiskPreAnalysis } from "@/service/api/ai";
import ChangeCompareTable from "./ChangeCompareTable.vue";
import PreRisk from "./preRisk.vue";
const themeStore = useThemeStore();
@@ -17,18 +18,16 @@ const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
// 是否是ai生成
const isAiGenerated = ref<boolean>(false);
// 变更识别loading
const preLoading = ref<boolean>(false);
// 生成变更申请单loading
const generateLoading = ref<boolean>(false);
// 风险预分析loading
const analyzing = ref<boolean>(false);
// 编辑风险预分析
const preRiskEdit = ref<string>('');
// 是否显示险预分析
const preRiskShow = ref<boolean>(false);
const preRiskShow = computed(() => form.value.severity !== '' || form.value.probability !== '' || form.value.protection !== '');
// 是否显示变更识别结果
const identifyShow = computed(() => form.value.compare_rows.length > 0);
// 表单
const form = ref<{
description: string,
@@ -54,7 +53,6 @@ const runIdentify = async () => {
try {
const res = await aiRecognize(form.value.description);
form.value.compare_rows = (res?.rows ?? []).map((x, i) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text }));
isAiGenerated.value = true;
window.$message?.success(`AI 变更识别完成,共 ${form.value.compare_rows.length} 项,请逐项人工确认(可直接修改)`);
} catch (e: any) {
form.value.compare_rows = [];
@@ -79,7 +77,6 @@ const runRiskAnalysis = async () => {
probability: res?.probability ?? form.value.probability,
protection: res?.protection ?? form.value.protection,
};
preRiskShow.value = true;
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
} catch (e: any) {
aiFail(e);
@@ -115,9 +112,6 @@ const saveDraft = () => {
defineExpose({form})
watch(() => props.currentForm, (newVal:any) => {
if(newVal.severity){
preRiskShow.value = true;
}
const {
attachments,
change_id,
@@ -140,7 +134,7 @@ watch(() => props.currentForm, (newVal:any) => {
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="font-semibold text-base ml-2">变更内容描述</span>
</div>
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled">
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled || !form.description.trim()">
<Icon v-if="preLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
变更识别
@@ -151,52 +145,17 @@ watch(() => props.currentForm, (newVal:any) => {
</div>
</div>
<!-- 变更识别结果 -->
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">{{form.compare_rows.length ? `变更识别结果(${form.compare_rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
</div>
<span v-if="isAiGenerated" class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
</div>
<div class="p-4">
<ChangeCompareTable
:data="form.compare_rows"
:editable="true"
@update:data="(d) => form.compare_rows = d"
/>
</div>
</div>
<template v-if="identifyShow">
<ChangeCompareTable
:data="form.compare_rows"
:editable="true"
@update:data="(d) => form.compare_rows = d"
/>
</template>
<!-- 风险预分析报告 -->
<div v-if="preRiskShow" class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
</div>
<div class="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = preRiskEdit === s.key ? '' : s.key">
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
</NButton>
</div>
<NInput v-model:value="form[s.key]" :readonly="preRiskEdit !== s.key" type="textarea" :autosize="{ minRows: 2 }" :bordered="false" autofocus />
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
</div>
</div>
</div>
</div>
<template v-if="preRiskShow">
<PreRisk :formInfo="form" @update:data="(key:'severity' | 'probability' | 'protection',val:string) => form[key] = val" />
</template>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { Icon } from '@iconify/vue'
import RichTextInput from './RichTextInput.vue'
const themeStore = useThemeStore();
const props = defineProps(['formInfo'])
const emit = defineEmits(['update:data']);
const form = ref<any>(null);
watch(() => props.formInfo,
(val: any) => {
// 深度拷贝以避免直接修改 props,并确保响应式
form.value = val
},
{ immediate: true, deep: true },
)
// 编辑风险预分析
const preRiskEdit = ref<boolean>(false);
// 更新数据
const onCellChange = (key:string,val:string) => {
form.value[key] = val;
emit('update:data', key, val)
}
</script>
<template>
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-2 border-b">
<div class="flex items-center">
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
<span class="font-semibold text-base ml-2">风险预分析报告</span>
</div>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = !preRiskEdit">
{{ preRiskEdit ? "完成" : "编辑" }}
</NButton>
</div>
<div class="p-4">
<div class="space-y-4">
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }])" :key="s.key" class="rounded-lg border">
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
</div>
<RichTextInput :disabled="!preRiskEdit" :modelValue="form[s.key]" :minRows="2" :bordered="false" @update:modelValue="(v) => onCellChange(s.key,v)" />
</div>
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
AI 风险预分析仅供参考用于申请阶段的快速研判正式风险识别请前往风险分析记录表标签页使用 HAZOP / JSA / 检查表法开展并留存记录
</div>
</div>
</div>
</div>
</template>
+1 -1
View File
@@ -33,7 +33,7 @@ watch(() => props.currentForm, (newVal:any) => {
};
});
// 3. 过滤掉不需要的字段
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_type', 'group_name'];
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name'];
const newResult = result.map((group:any) => ({
...group,
items: group.items.map((item:any) =>