Files
moc/src/views/my/initiateChange/modules/changePre.vue
T

206 lines
8.6 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 { inject, ref } 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";
const themeStore = useThemeStore();
const emit = defineEmits(['save', 'apply-generated'])
const generalLoading = inject('generalLoading', {saveLoading: ref<boolean>(false),btnDisabled: ref<boolean>(false)})
const {saveLoading,btnDisabled} = generalLoading
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 form = ref<{
description: string,
compare_rows: {item: string, before_text: string, after_text: string}[],
severity: string,
probability: string,
protection: string
}>({
description: '',
compare_rows: [],
severity: "",
probability: "",
protection: "",
});
// AI 变更识别
const runIdentify = async () => {
if (!form.value.description.trim()) {
return window.$message?.warning("请先输入变更内容描述");
}
form.value.compare_rows = [];
btnDisabled.value = true;
preLoading.value = true;
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 = [];
aiFail(e);
} finally {
preLoading.value = false;
btnDisabled.value = false;
}
};
// 运行风险预分析
const runRiskAnalysis = async () => {
if(form.value.description.trim() === '') {
return window.$message?.warning('请先填写变更内容描述');
}
analyzing.value = true;
btnDisabled.value = true;
try {
const res = await aiRiskPreAnalysis(form.value.description, form.value.compare_rows);
form.value = {
...form.value,
severity: res?.severity ?? form.value.severity,
probability: res?.probability ?? form.value.probability,
protection: res?.protection ?? form.value.protection,
};
preRiskShow.value = true;
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
} catch (e: any) {
aiFail(e);
} finally {
analyzing.value = false;
btnDisabled.value = false;
}
};
// 生成变更申请单
const genForm = async () => {
if (!form.value.description.trim()) {
return window.$message?.warning("请先输入变更内容描述");
}
generateLoading.value = true;
btnDisabled.value = true;
try {
const res = await aiApplication(form.value.description, form.value.compare_rows);
// 由父组件把 AI 结果回填到「变更申请表」并切换标签
emit('apply-generated', res);
} catch (e: any) {
aiFail(e);
} finally {
generateLoading.value = false;
btnDisabled.value = false;
}
};
// 保存草稿
const saveDraft = () => {
emit('save');
}
defineExpose({form})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<!-- 变更内容描述 -->
<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: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">
<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" />
变更识别
</NButton>
</div>
<div class="p-4">
<NInput type="textarea" v-model:value="form.description" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
</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>
<!-- 风险预分析报告 -->
<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" :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>
</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)]">
<span class="text-xs text-slate-400">预识别阶段不设审批人与提交完成识别与预分析后请切换至变更申请表选择审批人并提交</span>
<div class="ml-auto flex shrink-0 gap-2">
<NButton ghost type="success" size="small" :loading="generateLoading" :disabled="btnDisabled" @click="genForm">
<Icon icon="akar-icons:file" class="size-14px" /> 生成变更申请单
</NButton>
<NButton ghost type="warning" size="small" @click="runRiskAnalysis" :disabled="btnDisabled">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="mdi:shield-alert-outline" class="size-14px" />风险预分析
</NButton>
<NButton type="primary" size="small" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
</div>
</div>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 326px);
overflow-y: auto;
}
</style>