发起变更,底部封装通用组件
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { AiError, aiRecognize } from "@/service/api/ai";
|
||||
import { AiError, aiRecognize, aiRiskPreAnalysis } from "@/service/api/ai";
|
||||
import { precheckSaveApi } from "@/service/api/change";
|
||||
import ChangeCompareTable from "./ChangeCompareTable.vue";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const emit = defineEmits(['updateBtn']);
|
||||
const props = defineProps(['title','currentId'])
|
||||
|
||||
|
||||
const aiFail = (e: any) => {
|
||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||
@@ -18,6 +20,12 @@ const aiFail = (e: any) => {
|
||||
const isAiGenerated = ref<boolean>(false);
|
||||
// 变更识别loading
|
||||
const preLoading = ref<boolean>(false);
|
||||
// 生成变更申请单loading
|
||||
const generateLoading = ref<boolean>(false);
|
||||
// 保存loading
|
||||
const saveLoading = ref<boolean>(false);
|
||||
// 风险预分析loading
|
||||
const analyzing = ref<boolean>(false);
|
||||
// 编辑风险预分析
|
||||
const preRiskEdit = ref<string>('');
|
||||
// 是否显示险预分析
|
||||
@@ -44,105 +52,204 @@ const runIdentify = async () => {
|
||||
return window.$message?.warning("请先输入变更内容描述");
|
||||
}
|
||||
form.value.compare_rows = [];
|
||||
emit('updateBtn', true);
|
||||
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;
|
||||
emit('updateBtn', false);
|
||||
btnDisabled.value = false;
|
||||
}
|
||||
};
|
||||
// 更新表单
|
||||
const updateForm = (res: any, isShow: boolean) => {
|
||||
form.value = {
|
||||
...form.value,
|
||||
severity: res?.severity ?? form.value.severity,
|
||||
probability: res?.probability ?? form.value.probability,
|
||||
protection: res?.protection ?? form.value.protection,
|
||||
};
|
||||
preRiskShow.value = isShow;
|
||||
}
|
||||
// 更新按钮禁用状态
|
||||
const updateBtnDisabled = (disabled: boolean) => {
|
||||
btnDisabled.value = disabled;
|
||||
// 运行风险预分析
|
||||
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 () => {
|
||||
// window.$message?.info("AI 正在生成变更申请单");
|
||||
// generateLoading.value = true;
|
||||
// try {
|
||||
// const res = await aiApplication(desc.value, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
||||
// const pick = (v: string | null | undefined, opts: string[], cur: string) => (v && opts.includes(v) ? v : cur);
|
||||
// const main = (res?.main_changes ?? []).filter((x) => x && x.trim());
|
||||
// const purposes = (res?.purposes ?? []).filter((x) => PURPOSE_OPTIONS.includes(x));
|
||||
// const docs = (res?.update_docs ?? []).filter((x) => UPDATE_DOC_OPTIONS.includes(x));
|
||||
// const discs = (res?.disciplines ?? []).filter((x) => DISCIPLINE_OPTIONS.includes(x));
|
||||
// const aiTools = (res?.risk_tools ?? [])
|
||||
// .map((x) => RISK_TOOLS_FORM.find((t) => t.key === x || t.label === x)?.key)
|
||||
// .filter((x): x is string => !!x);
|
||||
// form.value = {
|
||||
// ...form.value,
|
||||
// name: res?.name || form.value.name,
|
||||
// mainChanges: main.length ? main : form.value.mainChanges,
|
||||
// related: (res?.related_changes ?? []).filter(Boolean).join("、") || form.value.related,
|
||||
// purposes: purposes.length ? purposes : form.value.purposes,
|
||||
// effect: res?.effect || form.value.effect,
|
||||
// type: pick(res?.change_type, ['工艺', '设备', '管理'], form.value.type),
|
||||
// duration: pick(res?.duration_suggestion, ['永久', '临时'], form.value.duration),
|
||||
// };
|
||||
// if (res?.materials_text) materialsText.value = res.materials_text;
|
||||
// if (docs.length) updateDocs.value = docs;
|
||||
// if (discs.length) {
|
||||
// disciplines.value = discs;
|
||||
// toggleDiscipline(discs);
|
||||
// }
|
||||
// if (res?.signers && Object.keys(res.signers).length) signers.value = { ...res.signers };
|
||||
// if (aiTools.length) tools.value = aiTools;
|
||||
// currentPlugin.value = "";
|
||||
// tab.value = "form";
|
||||
// window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
||||
// } catch (e: any) {
|
||||
// aiFail(e);
|
||||
// } finally {
|
||||
// generateLoading.value = false;
|
||||
// }
|
||||
};
|
||||
|
||||
// 保存草稿
|
||||
const saveDraft = async () => {
|
||||
// 变更预识别
|
||||
if(props.title?.trim()===''){
|
||||
return window.$message?.warning('请填写变更名称');
|
||||
}
|
||||
if(form.value.description?.trim() === ''){
|
||||
return window.$message?.warning('请填写变更内容描述');
|
||||
}
|
||||
saveLoading.value = true;
|
||||
btnDisabled.value = true;
|
||||
const {error} = await precheckSaveApi({
|
||||
change_id: props.currentId,
|
||||
title: props.title,
|
||||
description: form.value.description,
|
||||
severity: form.value.severity,
|
||||
probability: form.value.probability,
|
||||
protection: form.value.protection,
|
||||
compare_rows: form.value.compare_rows,
|
||||
})
|
||||
if(!error){
|
||||
window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||
}
|
||||
saveLoading.value = false;
|
||||
btnDisabled.value = false;
|
||||
}
|
||||
|
||||
defineExpose({form,updateForm,updateBtnDisabled})
|
||||
defineExpose({form})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 变更内容描述 -->
|
||||
<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 || preLoading">
|
||||
<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="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: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>
|
||||
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
|
||||
<span class="font-semibold text-base ml-2">变更内容描述</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>
|
||||
<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">
|
||||
<ChangeCompareTable
|
||||
:data="form.compare_rows"
|
||||
:editable="true"
|
||||
@update:data="(d) => form.compare_rows = d"
|
||||
/>
|
||||
<NInput type="textarea" v-model:value="form.description" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
|
||||
</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-if="preRiskEdit === s.key" type="textarea" :bordered="false" v-model:value="form[s.key]" autofocus rows="6" />
|
||||
<p v-else class="whitespace-pre-wrap px-3 py-1.5 text-sm leading-relaxed text-slate-700">{{ form[s.key] }}</p>
|
||||
<!-- 变更识别结果 -->
|
||||
<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>
|
||||
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
||||
AI 风险预分析仅供参考,用于申请阶段的快速研判;正式风险识别请前往「风险分析记录表」标签页,使用 HAZOP / JSA / 检查表法开展并留存记录。
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
Reference in New Issue
Block a user