变更预识别修改
This commit is contained in:
@@ -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>
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user