变更预识别接口完成
This commit is contained in:
@@ -255,7 +255,7 @@ export interface LevelScoreResult {
|
||||
analysis_text: string;
|
||||
}
|
||||
|
||||
export const aiLevelScore = (content: string, rows: CompareRow[], levelDims: LevelDim[] = DEFAULT_LEVEL_DIMS) =>
|
||||
export const aiLevelScore = (content: string, rows: CompareRow[], levelDims: LevelDim[]) =>
|
||||
aiFetch<LevelScoreResult>('/open/v1/moc/change/level-score', {
|
||||
request_id: nextRequestId('level'),
|
||||
content,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { request } from '../request';
|
||||
|
||||
/** 查询变更未保存数据
|
||||
*
|
||||
*/
|
||||
export function queryUnsavedApi() {
|
||||
return request({
|
||||
url: `/api/changes/unsaved`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/** 变更预识别查询
|
||||
*
|
||||
*/
|
||||
@@ -24,7 +34,7 @@ export function precheckSaveApi(params: {
|
||||
}) {
|
||||
return request({
|
||||
url: '/api/changes/precheck',
|
||||
method: 'post',
|
||||
method: 'put',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import {
|
||||
@@ -9,20 +9,20 @@ import { getColorWithOpacity } from "@/utils/common";
|
||||
import {
|
||||
AiError,
|
||||
aiApplication,
|
||||
aiLevelScore,
|
||||
aiRecognize,
|
||||
aiRiskOrganize,
|
||||
aiRiskPreAnalysis,
|
||||
aiTypeJudge,
|
||||
type CompareRow,
|
||||
} from "@/service/api/ai";
|
||||
import { precheckSaveApi } from "@/service/api/change";
|
||||
import Ed from "./modules/Ed.vue";
|
||||
import Hazop from "./modules/hazop.vue";
|
||||
import Jsa from "./modules/jsa.vue";
|
||||
import Scl from "./modules/scl.vue";
|
||||
import RiskCheck from "./modules/riskCheck.vue";
|
||||
import ChangeCompareTable from "./modules/ChangeCompareTable.vue";
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { queryUnsavedApi } from '@/service/api/change';
|
||||
import ChangePre from "./modules/changePre.vue";
|
||||
import ChangeApply from "./modules/changeApply.vue";
|
||||
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
@@ -57,14 +57,9 @@ const aiFail = (e: any) => {
|
||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||
else window.$message?.warning("AI 分析失败,可手动填写");
|
||||
};
|
||||
// 页面当前对比行 → AI 契约行
|
||||
const compareRows = (): CompareRow[] => (rows.value ?? []).map((x: any) => ({ item: x.item, before: x.before, after: x.after, hl: x.hl }));
|
||||
|
||||
const editing = ref<any>(null);
|
||||
|
||||
// ---------- 风险预分析 ----------
|
||||
const saveDraft = () => window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||
|
||||
// ---------- 生成变更申请单 ----------
|
||||
const materialsText = ref("");
|
||||
const updateDocs = ref<string[]>([]);
|
||||
@@ -79,7 +74,6 @@ const approvers = ref<Record<string, string>>({
|
||||
最终批准: "李主任(部门负责人 · 固定)",
|
||||
});
|
||||
const signers = ref<Record<string, string>>({});
|
||||
const sheet = ref<"type" | "level" | "approver" | null>(null);
|
||||
let demoSeq = 43;
|
||||
function nextChangeId() {
|
||||
return `MOC-2026-R01-${String(demoSeq++).padStart(4, "0")}`;
|
||||
@@ -212,11 +206,6 @@ const submitApply = () => {
|
||||
const DEPT_APPROVER_OPTS = ["李主任(一车间主任)", "赵主任(二车间主任)", "陈主任(三车间主任)"].map((x) => ({ label: x, value: x }));
|
||||
const SIGNER_POOL = ["王仪表(技术部)", "王仪表(技术部 · 仪表)", "周设备(设备科 · 电气)", "王海燕(安全环保部)", "王海燕(质量部)"];
|
||||
const signerOpts = (cur: string) => [...new Set([cur, ...SIGNER_POOL])].map((x) => ({ label: x, value: x }));
|
||||
const recommendTools = () => {
|
||||
const lv = form.value?.level === "重要" ? "重要" : "一般";
|
||||
tools.value = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
||||
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
||||
};
|
||||
|
||||
const previewFile = ref<string | null>(null);
|
||||
const DISCIPLINE_SIGNER: Record<string, string> = {
|
||||
@@ -240,64 +229,8 @@ const regenerate = () => window.$message?.success(`演示:已根据变更描
|
||||
const download = () => window.$message?.success(`演示:${TAB_NAME[tab.value]}已下载(Word/PDF)`);
|
||||
|
||||
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
|
||||
const LEVEL_DIMS = [
|
||||
{
|
||||
name: "1. 设备影响", note: "不合适的材质将会导致",
|
||||
options: [
|
||||
{ label: "对设备带来一定的损害及腐蚀", score: 1 },
|
||||
{ label: "对设备带来中等的损害及腐蚀", score: 2 },
|
||||
{ label: "对设备带来明显的损害及腐蚀", score: 3 },
|
||||
{ label: "对设备带来重大的损害及腐蚀", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "2. 财务影响", note: "工艺设计错误将会导致",
|
||||
options: [
|
||||
{ label: "一定损失,a<1万人民币", score: 1 },
|
||||
{ label: "中等损失,直接损失1万≤a<10万人民币", score: 2 },
|
||||
{ label: "大量损失,直接损失10万≤a<100万人民币", score: 3 },
|
||||
{ label: "巨额损失,直接损失100万人民币≤a", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "3. 质量影响", note: "",
|
||||
options: [
|
||||
{ label: "对质量带来一定的风险(非关键工席、非关键设备设施的变更)", score: 1 },
|
||||
{ label: "对质量带来中等风险(B类原料替代、生产辅助设备变更)", score: 2 },
|
||||
{ label: "对质量带来明显的风险(主要工艺参数、中间体生产工艺变更)", score: 3 },
|
||||
{ label: "对质量有很高风险(主要工艺路线、关键工艺参数变更)", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "4. 设计的复杂程度", note: "工艺设计包含了",
|
||||
options: [
|
||||
{ label: "少量的简单系统(单个设备或单个部件)", score: 1 },
|
||||
{ label: "大量的简单系统(多个单类设备或部件)", score: 2 },
|
||||
{ label: "至少一个复杂系统(涉及多设备/工艺相应调整)", score: 3 },
|
||||
{ label: "大量的复杂系统(需要装置配套改造调整)", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "5. 工艺设计的成熟度", note: "",
|
||||
options: [
|
||||
{ label: "被频繁使用,且被证明为专利方设计", score: 1 },
|
||||
{ label: "被少量使用,或被证明为专利方设计", score: 2 },
|
||||
{ label: "被相关工业设计方法或参考文献认可(非专利方)", score: 3 },
|
||||
{ label: "新的,未发表的", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "6. 工艺危害", note: "物料性质和工艺条件",
|
||||
options: [
|
||||
{ label: "无危害(常压、-10℃≤温度<40℃、无毒、无腐蚀不燃)", score: 1 },
|
||||
{ label: "可燃/负压/低正压(0~0.3MPa)、40℃≤温度<80℃", score: 2 },
|
||||
{ label: "易燃/中压(0.3~1.0MPa)、80℃≤温度<150℃", score: 3 },
|
||||
{ label: "极度危害/高压(>1.0MPa)、温度≥150℃", score: 4 },
|
||||
],
|
||||
},
|
||||
];
|
||||
const finishApprover = () => {
|
||||
sheet.value = null;
|
||||
sheet.value = '';
|
||||
window.$message?.success('审批流程已更新(底部固定行摘要同步刷新)');
|
||||
}
|
||||
// 风险识别分析记录表
|
||||
@@ -358,9 +291,10 @@ const lsLevel = (l: number | null, s: number | null) => {
|
||||
return rr >= 10 ? "高" : rr >= 5 ? "中" : "低";
|
||||
};
|
||||
const aiOrganize = async () => {
|
||||
let form = changePreRef.value?.form;
|
||||
analyzing.value = true;
|
||||
try {
|
||||
const res = await aiRiskOrganize(desc.value, compareRows());
|
||||
const res = await aiRiskOrganize(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
||||
const gen = (res?.records ?? []).map((r) => ({
|
||||
item: r.item,
|
||||
scene: r.scene,
|
||||
@@ -378,59 +312,8 @@ const aiOrganize = async () => {
|
||||
analyzing.value = false;
|
||||
}
|
||||
};
|
||||
// ---------- AI 类型判定 / 等级评分(抽屉打开时调用,静态示例作 fallback) ----------
|
||||
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||||
const typeAiLoading = ref<boolean>(false);
|
||||
const openTypeSheet = async () => {
|
||||
sheet.value = "type";
|
||||
if (typeAiLoading.value) return;
|
||||
typeAiLoading.value = true;
|
||||
try {
|
||||
const res = await aiTypeJudge(desc.value, compareRows());
|
||||
typeAi.value = {
|
||||
type: res?.type ?? "",
|
||||
confidence: res?.confidence ?? 0,
|
||||
conclusion: res?.conclusion ?? "",
|
||||
hit_rules: res?.hit_rules ?? [],
|
||||
exclude_rules: res?.exclude_rules ?? [],
|
||||
knowledge_refs: res?.knowledge_refs ?? [],
|
||||
};
|
||||
} catch (e: any) {
|
||||
aiFail(e);
|
||||
} finally {
|
||||
typeAiLoading.value = false;
|
||||
}
|
||||
};
|
||||
const levelAiText = ref<string>("");
|
||||
const levelAiBasis = ref<string[]>([]);
|
||||
const levelAiLoading = ref<boolean>(false);
|
||||
const openLevelSheet = async () => {
|
||||
sheet.value = "level";
|
||||
if (levelAiLoading.value) return;
|
||||
levelAiLoading.value = true;
|
||||
try {
|
||||
const dims = LEVEL_DIMS.map((d, i) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o) => o.score)) }));
|
||||
const res = await aiLevelScore(desc.value, compareRows(), dims);
|
||||
// 返回维度按 name 匹配、匹配不到按顺序对齐回填各维度得分;总分/等级阈值换算沿用页面本地逻辑
|
||||
const scores = [...levelScores.value];
|
||||
(res?.dims ?? []).forEach((d, i) => {
|
||||
const di = LEVEL_DIMS.findIndex((x) => x.name === d.name || x.name.endsWith(d.name));
|
||||
const idx = di >= 0 ? di : i;
|
||||
if (idx >= 0 && idx < scores.length && typeof d.score === "number") scores[idx] = d.score;
|
||||
if (d.basis) levelAiBasis.value[idx] = d.basis;
|
||||
});
|
||||
levelScores.value = scores;
|
||||
levelAiText.value = res?.analysis_text ?? "";
|
||||
} catch (e: any) {
|
||||
aiFail(e);
|
||||
} finally {
|
||||
levelAiLoading.value = false;
|
||||
}
|
||||
};
|
||||
// ---------- 变更培训 ----------
|
||||
// 注:培训区块暂无独立「AI 生成」触发按钮,保留静态示例内容;
|
||||
// 接入时可在此调 aiTraining(desc.value, compareRows(), riskRecords.value),
|
||||
// 将 training_text 填入 trainText、suggest_posts 展示为建议培训岗位。
|
||||
const trainText = ref(
|
||||
`培训对象:R-201 岗位操作人员(4 人)、维修班(2 人)
|
||||
培训方式:现场讲解 + 操作演示
|
||||
@@ -554,10 +437,10 @@ const onBack = () => {
|
||||
|
||||
|
||||
// **********************通用字段***********************
|
||||
// 当前用户
|
||||
const userInfo = ref<any>(localStg.get('userInfo'));
|
||||
// 是否开启ai
|
||||
const aiEnabled = ref<boolean>(true);
|
||||
const changePreRef = ref<any>(null);
|
||||
const changeApplyRef = ref<any>(null);
|
||||
// 是否开启ai功能
|
||||
const aiEnabled = ref<boolean>(localStg.get('userInfo')?.ai_assist_enabled=='1' ? true : false);
|
||||
// 保存loading
|
||||
const saveLoading = ref<boolean>(false);
|
||||
// 提交loading
|
||||
@@ -566,23 +449,25 @@ const submitLoading = ref<boolean>(false);
|
||||
const downloadLoading = ref<boolean>(false);
|
||||
// 重新生成loading
|
||||
const regenerateLoading = ref<boolean>(false);
|
||||
// 变更识别loading
|
||||
const preLoading = ref<boolean>(false);
|
||||
// 风险预分析loading
|
||||
const analyzing = ref<boolean>(false);
|
||||
// 生成变更申请单loading
|
||||
const generateLoading = ref<boolean>(false);
|
||||
// btn disabled
|
||||
const btnDisabled = computed(() => saveLoading.value || preLoading.value || analyzing.value || generateLoading.value || submitLoading.value || downloadLoading.value || regenerateLoading.value);
|
||||
const btnDisabled = ref<boolean>(false);
|
||||
// 变更名称
|
||||
const title = ref<string>('');
|
||||
// 当前id
|
||||
const currentId = ref<number>(0);
|
||||
// 是否确认
|
||||
const confirmed = ref<Record<string, boolean>>({});
|
||||
// 抽屉类型
|
||||
const sheet = ref<string>('');
|
||||
|
||||
// 变更预分析表单数据
|
||||
const changePreForm = ref<any>(null)
|
||||
|
||||
// **********************tab切换***********************
|
||||
const tab = ref<string>("pre");
|
||||
const TABS = [
|
||||
{ id: "pre", name: "变更预识别", icon: 'lucide:sparkles' },
|
||||
{ id: "form", name: "变更申请表", icon: 'akar-icons:file' },
|
||||
@@ -592,98 +477,62 @@ const TABS = [
|
||||
{ id: "accept", name: "验收评价", icon: 'material-symbols:check-circle-outline' },
|
||||
{ id: "close", name: "变更关闭确认表", icon: 'quill:folder-open' },
|
||||
];
|
||||
const tab = ref<string>(aiEnabled.value ? "pre" : "form");
|
||||
const tabList = computed(() => {
|
||||
return aiEnabled.value ? TABS : TABS.filter((x) => x.id !== "pre");
|
||||
});
|
||||
// tab 切换
|
||||
const tabChange = (id: string) => {
|
||||
if(tab.value==='pre'){
|
||||
changePreForm.value = changePreRef.value?.form;
|
||||
if(!changePreForm.value?.description){
|
||||
return window.$message?.warning('请先填写变更内容描述');
|
||||
}
|
||||
}
|
||||
if(tab.value === id) {
|
||||
return;
|
||||
}
|
||||
// if(title.value.trim() === '') {
|
||||
// return window.$message?.warning('请先填写变更名称');
|
||||
// }
|
||||
// if(formOne.value.description.trim() === '') {
|
||||
// return window.$message?.warning('请先填写变更内容描述');
|
||||
// }
|
||||
tab.value = id;
|
||||
}
|
||||
// 更新loading
|
||||
const updateLoading = (type:string,loading: boolean) => {
|
||||
btnDisabled.value = loading;
|
||||
if(type === 'analyzing'){
|
||||
analyzing.value = loading;
|
||||
}
|
||||
if(type==='save'){
|
||||
saveLoading.value = loading;
|
||||
}
|
||||
}
|
||||
|
||||
// **********************变更预识别***********************
|
||||
// 是否是ai生成
|
||||
const isAiGenerated = ref<boolean>(false);
|
||||
// 表单
|
||||
const formOne = 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 (!formOne.value.description.trim()) {
|
||||
return window.$message?.warning("请先输入变更内容描述");
|
||||
}
|
||||
preLoading.value = true;
|
||||
formOne.value.compare_rows = [];
|
||||
try {
|
||||
const res = await aiRecognize(formOne.value.description);
|
||||
formOne.value.compare_rows = (res?.rows ?? []).map((x, i) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text }));
|
||||
window.$message?.success(`AI 变更识别完成,共 ${formOne.value.compare_rows.length} 项,请逐项人工确认(可直接修改)`);
|
||||
} catch (e: any) {
|
||||
formOne.value.compare_rows = [];
|
||||
aiFail(e);
|
||||
} finally {
|
||||
preLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑风险预分析
|
||||
const preRiskEdit = ref<string>('');
|
||||
// 是否显示险预分析
|
||||
const preRiskShow = ref<boolean>(false);
|
||||
// 运行风险与分析
|
||||
// 变更编号
|
||||
const changeNo = ref<string>('');
|
||||
// 运行风险预分析
|
||||
const runRiskAnalysis = async () => {
|
||||
if(formOne.value.description.trim() === '') {
|
||||
let form = changePreRef.value?.form;
|
||||
if(form?.description.trim() === '') {
|
||||
return window.$message?.warning('请先填写变更内容描述');
|
||||
}
|
||||
analyzing.value = true;
|
||||
updateLoading('analyzing',true);
|
||||
changePreRef.value?.updateBtnDisabled(true);
|
||||
try {
|
||||
const res = await aiRiskPreAnalysis(formOne.value.description, formOne.value.compare_rows);
|
||||
formOne.value.severity = res?.severity ?? formOne.value.severity;
|
||||
formOne.value.probability = res?.probability ?? formOne.value.probability
|
||||
formOne.value.protection = res?.protection ?? formOne.value.protection
|
||||
preRiskShow.value = true;
|
||||
const res = await aiRiskPreAnalysis(form?.description, form?.compare_rows);
|
||||
changePreRef.value?.updateForm(res,true);
|
||||
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||||
} catch (e: any) {
|
||||
aiFail(e);
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
updateLoading('analyzing',false);
|
||||
changePreRef.value?.updateBtnDisabled(false);
|
||||
}
|
||||
|
||||
// const {data,error} = await aiRiskPreAnalysis({
|
||||
// content: formOne.value.description,
|
||||
// rows: formOne.value.compare_rows
|
||||
// });
|
||||
// if(!error){
|
||||
// formOne.value.severity = data?.severity ?? formOne.value.severity;
|
||||
// formOne.value.probability = data?.probability ?? formOne.value.probability
|
||||
// formOne.value.protection = data?.protection ?? formOne.value.protection
|
||||
// preRiskShow.value = true;
|
||||
// window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||||
// }
|
||||
// analyzing.value = false;
|
||||
};
|
||||
// 生成变更申请单
|
||||
const genForm = async () => {
|
||||
window.$message?.info("AI 正在生成变更申请单");
|
||||
generateLoading.value = true;
|
||||
try {
|
||||
const res = await aiApplication(desc.value, compareRows());
|
||||
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));
|
||||
@@ -721,53 +570,13 @@ const genForm = async () => {
|
||||
};
|
||||
|
||||
// --------------------- 变更申请表 ---------------------
|
||||
// 表单
|
||||
const changForm = ref<{
|
||||
change_type: number | null,
|
||||
change_level: number | null,
|
||||
duration_type: number | null,
|
||||
restore_deadline: number | null,
|
||||
org_id: number | null,
|
||||
urgent: number,
|
||||
purposes: string[],
|
||||
effect: string,
|
||||
main_changes: string[],
|
||||
related_changes: string[],
|
||||
materials: string,
|
||||
update_docs: string[],
|
||||
disciplines: string[],
|
||||
manage_dept: string,
|
||||
plan_use_date: number | null,
|
||||
risk_tools: string[],
|
||||
}>({
|
||||
change_type: null,
|
||||
change_level: null,
|
||||
duration_type: null,
|
||||
restore_deadline: null,
|
||||
org_id: null,
|
||||
urgent: 0,
|
||||
purposes: [],
|
||||
effect: '',
|
||||
main_changes: [''],
|
||||
related_changes: [''],
|
||||
materials: '',
|
||||
update_docs: [],
|
||||
disciplines: [],
|
||||
manage_dept: '',
|
||||
plan_use_date: null,
|
||||
risk_tools: [],
|
||||
});
|
||||
// 上传的资料
|
||||
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
|
||||
// 变更目的选项
|
||||
const PURPOSE_OPTIONS = [
|
||||
"满足市场需求", "提高生产效能", "改善操作条件", "增加安全性能",
|
||||
"达到环保要求", "法律合规整改", "减少能耗物损", "稳定系统运行",
|
||||
"完善管理制度", "达到质量改进", "设备可靠性升级",
|
||||
];
|
||||
const levelScores = ref<number[]>([2, 2, 3, 1, 2, 3]);
|
||||
const totalScore = computed(() => levelScores.value.reduce((a: number, b: number) => a + b, 0));
|
||||
const levelResult = computed(() => (totalScore.value >= 20 ? "重要变更" : "一般变更"));
|
||||
|
||||
// 风险分析选项
|
||||
const RISK_TOOLS_FORM = [
|
||||
{ key: "HAZOP", label: "HAZOP" },
|
||||
@@ -775,16 +584,59 @@ const RISK_TOOLS_FORM = [
|
||||
{ key: "SCL", label: "设备SCL" },
|
||||
{ key: "RISK_CHECK", label: "风险检查表" },
|
||||
];
|
||||
// 风险分析插件选择
|
||||
const toggleTool = (t: {key:string,label:string}) => {
|
||||
const { risk_tools } = changForm.value;
|
||||
changForm.value.risk_tools = risk_tools.includes(t.key) ? risk_tools.filter((key:string) => key !== t.key) : [...risk_tools, t.key];
|
||||
};
|
||||
const TAB_NAME: Record<string, string> = {
|
||||
pre: "变更预识别", form: "变更申请表", risk: "风险分析记录表",
|
||||
train: "变更培训内容", pssr: "PSSR检查内容", accept: "验收评价", close: "变更关闭确认表",
|
||||
};
|
||||
|
||||
// 保存草稿
|
||||
const saveDraft = async () => {
|
||||
// 变更预识别
|
||||
if(tab.value === 'pre'){
|
||||
if(title.value.trim()===''){
|
||||
return window.$message?.warning('请填写变更名称');
|
||||
}
|
||||
changePreForm.value = changePreRef.value?.form;
|
||||
if(changePreForm.value?.description?.trim() === ''){
|
||||
return window.$message?.warning('请填写变更内容描述');
|
||||
}
|
||||
updateLoading('save', true);
|
||||
changePreRef.value?.updateBtnDisabled(true);
|
||||
const {error} = await precheckSaveApi({
|
||||
change_id: currentId.value,
|
||||
title: title.value,
|
||||
description: changePreForm.value?.description || '',
|
||||
severity: changePreForm.value?.severity || '',
|
||||
probability: changePreForm.value?.probability || '',
|
||||
protection: changePreForm.value?.protection || '',
|
||||
compare_rows: changePreForm.value?.compare_rows || [],
|
||||
})
|
||||
if(!error){
|
||||
window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||
}
|
||||
updateLoading('save', false);
|
||||
changePreRef.value?.updateBtnDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮禁用状态
|
||||
const updateBtn = (val: boolean) => {
|
||||
btnDisabled.value = val;
|
||||
};
|
||||
|
||||
// 查询变更未保存数据
|
||||
const getId = async () => {
|
||||
const {data,error} = await queryUnsavedApi();
|
||||
if(!error){
|
||||
currentId.value = data.id;
|
||||
changeNo.value = data.change_no;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getId();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -806,10 +658,10 @@ const TAB_NAME: Record<string, string> = {
|
||||
<span class="whitespace-nowrap text-sm font-medium text-slate-600">变更名称 <span class="text-red-500">*</span></span>
|
||||
<NInput v-model:value="title" maxlength="28" show-count placeholder="AI 自动总结(28 字以内),可手动修改" class="!w-420px" />
|
||||
<span class="ml-auto whitespace-nowrap text-sm text-slate-500">变更编号</span>
|
||||
<NInput value="MOC-2026-R01-0041" disabled class="!w-44" />
|
||||
<NInput :value="changeNo" disabled class="!w-44" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1 overflow-x-auto px-3 pt-2">
|
||||
<div v-for="t in TABS" :key="t.id" @click="tabChange(t.id)"
|
||||
<div v-for="t in tabList" :key="t.id" @click="tabChange(t.id)"
|
||||
class="flex items-center gap-1.5 whitespace-nowrap px-3 py-2 text-sm transition cursor-pointer border hover:bg-white/60 rounded-t-lg"
|
||||
:class="tab === t.id ? 'border-b-transparent border-[var(--border-color)] bg-white text-[var(--active-color)]':'border-transparent text-slate-500'"
|
||||
:style="{
|
||||
@@ -819,7 +671,7 @@ const TAB_NAME: Record<string, string> = {
|
||||
>
|
||||
<Icon :icon="t.icon" class="size-14px" />
|
||||
{{ t.name }}
|
||||
<span v-if="(btnDisabled && t.id === 'pre')">
|
||||
<span v-if="(btnDisabled && t.id === tab)">
|
||||
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||
</span>
|
||||
<Icon v-if="confirmed[t.id]" icon="material-symbols:check-circle-outline" class="size-14px text-emerald-500" />
|
||||
@@ -832,273 +684,14 @@ const TAB_NAME: Record<string, string> = {
|
||||
<div class="space-y-4" :class="tab === 'pre'?'scroll':'scroll2'">
|
||||
<!-- ===== 变更预识别 ===== -->
|
||||
<template v-if="tab === 'pre'">
|
||||
<!-- 变更内容描述 -->
|
||||
<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 v-if="aiEnabled" 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>
|
||||
<NTag type="info" v-else>AI 未启用</NTag>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<NInput type="textarea" v-model:value="formOne.description" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
|
||||
<div v-if="!aiEnabled" class="mt-2 flex items-center gap-2 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||
<Icon icon="proicons:alert-triangle" class="size-14px" />
|
||||
AI 辅助功能未启用(可在「系统设置 → AI 辅助功能」开启),识别结果与申请表需手动填写。
|
||||
</div>
|
||||
</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">{{formOne.compare_rows.length ? `变更识别结果(${formOne.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="formOne.compare_rows"
|
||||
:editable="true"
|
||||
@update:data="(d) => formOne.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: '保护措施影响分析' }] as const)" :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="formOne[s.key]" autofocus rows="6" />
|
||||
<p v-else class="whitespace-pre-wrap px-3 py-1.5 text-sm leading-relaxed text-slate-700">{{ formOne[s.key] }}</p>
|
||||
</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>
|
||||
<ChangePre ref="changePreRef" @updateBtn="updateBtn" />
|
||||
</template>
|
||||
|
||||
<!-- ===== 变更申请表 ===== -->
|
||||
<template v-if="tab === 'form'">
|
||||
<!-- 申请部门 / 申请人 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
|
||||
<NInput v-model:value="changForm.manage_dept" placeholder="请输入申请部门" />
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
|
||||
<NInput :value="userInfo?.real_name" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更目的 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
|
||||
<div class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 p-3">
|
||||
<NCheckboxGroup v-model:value="changForm.purposes">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 预期效果 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
|
||||
<NInput type="textarea" v-model:value="changForm.effect" rows="2" placeholder="" />
|
||||
</div>
|
||||
<!-- 主要变更内容 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div v-for="(_, i) in changForm.main_changes" :key="i" class="flex items-center gap-2">
|
||||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||||
<NInput v-model:value="changForm.main_changes[i]" type="text" placeholder="" />
|
||||
<NButton v-if="changForm.main_changes.length > 1" text type="error" @click="changForm.main_changes = changForm.main_changes.splice(i, 1)">
|
||||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
<NButton text type="primary" class="text-xs" @click="changForm.main_changes = [...changForm.main_changes, '']">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 连带变更内容 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div v-for="(_, i) in changForm.related_changes" :key="i" class="flex items-center gap-2">
|
||||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||||
<NInput v-model:value="changForm.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
|
||||
<NButton v-if="changForm.related_changes.length > 1" text type="error" @click="changForm.related_changes = changForm.related_changes.splice(i, 1)">
|
||||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
<NButton text type="primary" class="text-xs" @click="changForm.related_changes = [...changForm.related_changes, '']">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更类型 / 变更等级 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<!-- 变更类型 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="changForm.change_type">
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||
AI 判定说明
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更等级 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="changForm.change_level">
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio v-for="t in ['一般', '重要']" :key="t" :value="t">{{t}}</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
||||
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更时限 / 风险分析 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="changForm.duration_type">
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio v-for="t in [0, 1]" :key="t" :value="t">
|
||||
{{ t === 0 ? '永久' : '临时' }}
|
||||
</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NCheckbox v-model:checked="changForm.urgent" :checked-value="1" :unchecked-value="0">紧急</NCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
|
||||
class="text-sm"
|
||||
:ghost="changForm.risk_tools.includes(t.key) ? false : true"
|
||||
:type="changForm.risk_tools.includes(t.key) ? 'primary' : 'default'"
|
||||
>
|
||||
{{ t.label }}
|
||||
</NButton>
|
||||
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
|
||||
按等级重新推荐
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 计划投用时间 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||||
<NDatePicker v-model:value="changForm.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
|
||||
<!-- 临时 -->
|
||||
<template v-if="changForm.duration_type === 1">
|
||||
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
|
||||
<NDatePicker v-model:value="changForm.restore_deadline" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
|
||||
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
|
||||
</template>
|
||||
<!-- 永久 -->
|
||||
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.urgent" class="text-xs text-red-500">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
|
||||
<!-- 所需材料 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="text-[11px] text-slate-400">施工所需材料(生成变更申请表时系统自动生成,支持手动修改)</div>
|
||||
<NInput type="textarea" v-model:value="changForm.materials" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资料上传 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
|
||||
<NButton size="small" class="text-xs" v-for="f in files" :key="f" @click="previewFile = f" title="点击在线预览">
|
||||
<Icon icon="akar-icons:file" class="size-14px" />
|
||||
{{ f }}
|
||||
</NButton>
|
||||
<NButton @click="files = [...files, `资料_${files.length + 1}.pdf`]">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
|
||||
</NButton>
|
||||
<span class="text-xs text-slate-400">(点击附件名弹窗预览,支持 PDF / Word / 图片在线查看)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 需更新的资料 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NCheckboxGroup v-model:value="changForm.update_docs">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
<span class="self-center text-xs text-slate-400">(多选,变更关闭前逐项确认上传最新版本)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 涉及专业 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NCheckboxGroup v-model:value="changForm.disciplines" @update:value="toggleDiscipline">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in ['技术', '仪表', '电气', '环保', '质量']" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
|
||||
</div>
|
||||
<!-- <div v-if="Object.keys(signers).length > 0" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
||||
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
|
||||
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ d }} · {{ p }}</NTag>
|
||||
<span class="text-slate-400">(在底部「审批流程设置」中可调整人选)</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChangeApply ref="changeApplyRef" :formInfo="changePreForm" @updateBtn="updateBtn" />
|
||||
</template>
|
||||
|
||||
<!-- ===== 风险分析记录表 ===== -->
|
||||
<template v-if="tab === 'risk'">
|
||||
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
@@ -1622,7 +1215,7 @@ const TAB_NAME: Record<string, string> = {
|
||||
</template>
|
||||
</NModal>
|
||||
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 / 审批流程设置 -->
|
||||
<NDrawer :show="sheet !== null" @update:show="(v: boolean) => !v && (sheet = null)" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
||||
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
||||
<NDrawerContent>
|
||||
<template #header>
|
||||
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
|
||||
@@ -1630,110 +1223,6 @@ const TAB_NAME: Record<string, string> = {
|
||||
<div v-if="sheet === 'approver'" class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||
</template>
|
||||
<div class="h-full space-y-4 overflow-y-auto">
|
||||
<!-- AI 变更类型判定说明 -->
|
||||
<template v-if="sheet === 'type'">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<NTag v-if="typeAiLoading" type="info" size="small">
|
||||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 判定中…
|
||||
</NTag>
|
||||
<template v-else>
|
||||
<NTag type="info" size="small">判定:{{ typeAi?.type || '工艺' }}</NTag>
|
||||
<NTag size="small">置信度:{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '高' }}</NTag>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||||
<p><b>判定依据:</b></p>
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||||
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||||
</ul>
|
||||
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||
</div>
|
||||
<div v-else class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||
<p><b>判定结论:</b>本次变更涉及反应温度(80℃→90℃)、保温时间(8h→4h)等工艺参数调整及 DCS 报警/联锁值修改,未涉及设备本体更换或材质变更,依据变更类型规则判定为「工艺」类变更。</p>
|
||||
<p><b>判定依据:</b></p>
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
<li>命中规则 2.1:操作参数超出原设计控制范围 → 工艺类</li>
|
||||
<li>命中规则 2.4:DCS 报警值/联锁值调整 → 工艺类(仪表联动)</li>
|
||||
<li>排除规则 3.2:无设备规格型号变化 → 不构成设备类</li>
|
||||
</ul>
|
||||
<p><b>知识库引用:</b>工艺卡片 R-101(Rev B)、DCS 联锁台账、《变更管理制度》第 4.2 条。</p>
|
||||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 变更等级判定表 -->
|
||||
<template v-if="sheet === 'level'">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<NTag size="small" :type="totalScore >= 20 ? 'error' : 'warning'">
|
||||
{{ levelResult }}
|
||||
</NTag>
|
||||
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||||
</div>
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
<div v-for="(dim, di) in LEVEL_DIMS" :key="dim.name" class="rounded-lg border p-3">
|
||||
<div class="mb-2 flex items-baseline justify-between gap-2">
|
||||
<span class="text-sm font-semibold text-slate-700">
|
||||
{{ dim.name }}
|
||||
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">({{ dim.note }})</span>
|
||||
</span>
|
||||
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ levelScores[di] }}</b></span>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<button v-for="op in dim.options" :key="op.score" @click="levelScores = levelScores.map((s, i) => (i === di ? op.score : s))"
|
||||
class="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition"
|
||||
:class="levelScores[di] === op.score ? 'bg-[#EAF0F9] text-[var(--theme-color)]' : 'text-slate-600 hover:bg-slate-50 bg-white'"
|
||||
:style="{'--theme-color': themeStore.themeColor}"
|
||||
>
|
||||
<span
|
||||
class="mt-0.5 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border"
|
||||
:class="levelScores[di] === op.score ? 'border-[var(--theme-color)]' : 'border-slate-300'"
|
||||
:style="{'--theme-color': themeStore.themeColor}"
|
||||
>
|
||||
<span
|
||||
v-if="levelScores[di] === op.score"
|
||||
class="h-1.5 w-1.5 rounded-full bg-[var(--theme-color)]"
|
||||
:style="{'--theme-color': themeStore.themeColor}"
|
||||
/>
|
||||
</span>
|
||||
<span class="flex-1 leading-snug">{{ op.label }}({{ op.score }}分)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
|
||||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||||
<Icon icon="lucide:sparkles" class="size-14px" />
|
||||
AI 评分分析过程
|
||||
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
|
||||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||||
<p v-if="levelAiText">{{ levelAiText }}</p>
|
||||
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
|
||||
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
|
||||
</p>
|
||||
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||||
</div>
|
||||
<div v-else class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||||
<p>我们分析变更内容:缩合反应温度从80℃提高到90℃,反应时间从8小时缩短到4小时。设备材质SS304,设计温度100℃,设计压力0.2MPa,压力不变(常压)。物料为原料A与原料B,未具体说明毒性、腐蚀性。逐维度评分如下。</p>
|
||||
<p><b class="text-[#17407F]">1. 设备影响:</b>温度从80℃升至90℃,仍在设计温度100℃范围内,但接近上限。SS304 在 80→90℃ 下腐蚀速率可能略有增加,但变化不大;保守起见给 2 分(中等损害)。</p>
|
||||
<p><b class="text-[#17407F]">2. 财务影响:</b>变更目的是提升产能,但若设计错误可能导致反应失败、批次报废。温度提高10℃、时间缩短一半,控制不好可能影响产品质量,损失可能中等(1万~10万),取 2 分。</p>
|
||||
<p><b class="text-[#17407F]">3. 质量影响:</b>涉及主要工艺参数变更且有未知杂质(0.05%→0.12%)结构未定,对质量带来明显风险,取 3 分。</p>
|
||||
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 审批流程设置 -->
|
||||
<template v-if="sheet === 'approver'">
|
||||
<p class="text-xs leading-relaxed text-slate-400">审批链:部门审核 → 专业会签 → 最终批准。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
|
||||
@@ -1850,6 +1339,10 @@ const TAB_NAME: Record<string, string> = {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.n-radio) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.scroll {
|
||||
height: calc(100vh - 326px);
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
<!-- 变更申请表 -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
|
||||
import { localStg } from '@/utils/storage';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const props = defineProps({
|
||||
formInfo: Object as () => any
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updateBtn']);
|
||||
|
||||
const aiFail = (e: any) => {
|
||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||
else window.$message?.warning("AI 分析失败,可手动填写");
|
||||
};
|
||||
// 是否开启ai功能
|
||||
const aiEnabled = ref<boolean>(localStg.get('userInfo')?.ai_assist_enabled=='1' ? true : false);
|
||||
// 按钮禁用
|
||||
const btnDisabled = ref<boolean>(false);
|
||||
// 总分数
|
||||
const totalScore = ref<number>(0);
|
||||
// 当前用户
|
||||
const userInfo = ref<any>(localStg.get('userInfo'));
|
||||
// 预览文件
|
||||
const previewFile = ref<string | null>(null);
|
||||
// 抽屉类型
|
||||
const sheet = ref<string>('');
|
||||
// 表单
|
||||
const form = ref<{
|
||||
change_type: number | null,
|
||||
change_level: any,
|
||||
duration_type: number | null,
|
||||
restore_deadline: number | null,
|
||||
org_id: number | null,
|
||||
urgent: number,
|
||||
purposes: string[],
|
||||
effect: string,
|
||||
main_changes: string[],
|
||||
related_changes: string[],
|
||||
materials: string,
|
||||
update_docs: string[],
|
||||
disciplines: string[],
|
||||
manage_dept: string,
|
||||
plan_use_date: number | null,
|
||||
risk_tools: string[],
|
||||
}>({
|
||||
change_type: null,
|
||||
change_level: computed(() => totalScore.value >= 20 ? 1 : 0),
|
||||
duration_type: null,
|
||||
restore_deadline: null,
|
||||
org_id: null,
|
||||
urgent: 0,
|
||||
purposes: [],
|
||||
effect: '',
|
||||
main_changes: [''],
|
||||
related_changes: [''],
|
||||
materials: '',
|
||||
update_docs: [],
|
||||
disciplines: [],
|
||||
manage_dept: '',
|
||||
plan_use_date: null,
|
||||
risk_tools: [],
|
||||
});
|
||||
// 上传的资料
|
||||
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
|
||||
// 变更目的选项
|
||||
const PURPOSE_OPTIONS = [
|
||||
"满足市场需求", "提高生产效能", "改善操作条件", "增加安全性能",
|
||||
"达到环保要求", "法律合规整改", "减少能耗物损", "稳定系统运行",
|
||||
"完善管理制度", "达到质量改进", "设备可靠性升级",
|
||||
];
|
||||
// 风险分析选项
|
||||
const RISK_TOOLS_FORM = [
|
||||
{ key: "HAZOP", label: "HAZOP" },
|
||||
{ key: "JSA", label: "JSA" },
|
||||
{ key: "SCL", label: "设备SCL" },
|
||||
{ key: "RISK_CHECK", label: "风险检查表" },
|
||||
];
|
||||
const LEVEL_DIMS = ref([
|
||||
{
|
||||
name: "1. 设备影响", note: "不合适的材质将会导致",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "对设备带来一定的损害及腐蚀", score: 1 },
|
||||
{ label: "对设备带来中等的损害及腐蚀", score: 2 },
|
||||
{ label: "对设备带来明显的损害及腐蚀", score: 3 },
|
||||
{ label: "对设备带来重大的损害及腐蚀", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "2. 财务影响", note: "工艺设计错误将会导致",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "一定损失,a<1万人民币", score: 1 },
|
||||
{ label: "中等损失,直接损失1万≤a<10万人民币", score: 2 },
|
||||
{ label: "大量损失,直接损失10万≤a<100万人民币", score: 3 },
|
||||
{ label: "巨额损失,直接损失100万人民币≤a", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "3. 质量影响", note: "",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "对质量带来一定的风险(非关键工席、非关键设备设施的变更)", score: 1 },
|
||||
{ label: "对质量带来中等风险(B类原料替代、生产辅助设备变更)", score: 2 },
|
||||
{ label: "对质量带来明显的风险(主要工艺参数、中间体生产工艺变更)", score: 3 },
|
||||
{ label: "对质量有很高风险(主要工艺路线、关键工艺参数变更)", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "4. 设计的复杂程度", note: "工艺设计包含了",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "少量的简单系统(单个设备或单个部件)", score: 1 },
|
||||
{ label: "大量的简单系统(多个单类设备或部件)", score: 2 },
|
||||
{ label: "至少一个复杂系统(涉及多设备/工艺相应调整)", score: 3 },
|
||||
{ label: "大量的复杂系统(需要装置配套改造调整)", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "5. 工艺设计的成熟度", note: "",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "被频繁使用,且被证明为专利方设计", score: 1 },
|
||||
{ label: "被少量使用,或被证明为专利方设计", score: 2 },
|
||||
{ label: "被相关工业设计方法或参考文献认可(非专利方)", score: 3 },
|
||||
{ label: "新的,未发表的", score: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "6. 工艺危害", note: "物料性质和工艺条件",
|
||||
score: 0,
|
||||
options: [
|
||||
{ label: "无危害(常压、-10℃≤温度<40℃、无毒、无腐蚀不燃)", score: 1 },
|
||||
{ label: "可燃/负压/低正压(0~0.3MPa)、40℃≤温度<80℃", score: 2 },
|
||||
{ label: "易燃/中压(0.3~1.0MPa)、80℃≤温度<150℃", score: 3 },
|
||||
{ label: "极度危害/高压(>1.0MPa)、温度≥150℃", score: 4 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const disciplines = ref<string[]>([]);
|
||||
const signers = ref<Record<string, string>>({});
|
||||
|
||||
const recommendTools = () => {
|
||||
const lv = form.value?.change_level === 1 ? "重要" : "一般";
|
||||
form.value.risk_tools = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
||||
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
||||
};
|
||||
|
||||
// 风险分析插件选择
|
||||
const toggleTool = (t: {key:string,label:string}) => {
|
||||
const { risk_tools } = form.value;
|
||||
form.value.risk_tools = risk_tools.includes(t.key) ? risk_tools.filter((key:string) => key !== t.key) : [...risk_tools, t.key];
|
||||
};
|
||||
// 涉及专业
|
||||
const DISCIPLINE_SIGNER: Record<string, string> = {
|
||||
技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)", 电气: "周设备(设备科 · 电气)", 环保: "王海燕(安全环保部)", 质量: "王海燕(质量部)",
|
||||
};
|
||||
const toggleDiscipline = (d:any) => {
|
||||
const result = disciplines.value.reduce((acc: Record<string, string>, key: string) => {
|
||||
acc[key] = DISCIPLINE_SIGNER[key]; // 如果键不存在,值为 undefined
|
||||
return acc;
|
||||
}, {});
|
||||
signers.value = {...result};
|
||||
};
|
||||
|
||||
// 变更类型判断
|
||||
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||||
const typeAiLoading = ref<boolean>(false);
|
||||
// 打开变更类型抽屉
|
||||
const openTypeSheet = async () => {
|
||||
let form = props.formInfo;
|
||||
if (!form?.description) {
|
||||
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
|
||||
}
|
||||
sheet.value = "type";
|
||||
if (typeAiLoading.value) return;
|
||||
typeAiLoading.value = true;
|
||||
try {
|
||||
const res = await aiTypeJudge(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
||||
typeAi.value = {
|
||||
type: res?.type ?? "",
|
||||
confidence: res?.confidence ?? 0,
|
||||
conclusion: res?.conclusion ?? "",
|
||||
hit_rules: res?.hit_rules ?? [],
|
||||
exclude_rules: res?.exclude_rules ?? [],
|
||||
knowledge_refs: res?.knowledge_refs ?? [],
|
||||
};
|
||||
} catch (e: any) {
|
||||
aiFail(e);
|
||||
} finally {
|
||||
typeAiLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 变更等级判断
|
||||
const levelAiText = ref<string>("");
|
||||
const levelAiBasis = ref<string[]>([]);
|
||||
const levelAiLoading = ref<boolean>(false);
|
||||
// 打开等级判定表抽屉
|
||||
const openLevelSheet = async () => {
|
||||
let form = props.formInfo;
|
||||
if (!form?.description) {
|
||||
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
|
||||
}
|
||||
sheet.value = "level";
|
||||
if (levelAiLoading.value) return;
|
||||
levelAiLoading.value = true;
|
||||
try {
|
||||
const dims = LEVEL_DIMS.value.map((d:any, i:any) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o:any) => o.score)) }));
|
||||
const res = await aiLevelScore(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })), dims);
|
||||
res?.dims?.forEach((d, i) => {
|
||||
totalScore.value += d.score ?? 0;
|
||||
LEVEL_DIMS.value[i].score = d.score ?? 0;
|
||||
levelAiBasis.value[i] = d.basis ?? "";
|
||||
});
|
||||
levelAiText.value = res?.analysis_text ?? "";
|
||||
} catch (e: any) {
|
||||
aiFail(e);
|
||||
} finally {
|
||||
levelAiLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新表单
|
||||
const updateForm = (res: any, isShow: boolean) => {
|
||||
|
||||
}
|
||||
// 更新按钮禁用状态
|
||||
const updateBtnDisabled = (disabled: boolean) => {
|
||||
btnDisabled.value = disabled;
|
||||
}
|
||||
|
||||
defineExpose({form,updateForm,updateBtnDisabled})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 申请部门 / 申请人 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
|
||||
<NInput v-model:value="form.manage_dept" placeholder="请输入申请部门" />
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
|
||||
<NInput :value="userInfo?.real_name" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更目的 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
|
||||
<div class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 p-3">
|
||||
<NCheckboxGroup v-model:value="form.purposes">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 预期效果 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
|
||||
<NInput type="textarea" v-model:value="form.effect" rows="2" placeholder="" />
|
||||
</div>
|
||||
<!-- 主要变更内容 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div v-for="(_, i) in form.main_changes" :key="i" class="flex items-center gap-2">
|
||||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||||
<NInput v-model:value="form.main_changes[i]" type="text" placeholder="" />
|
||||
<NButton v-if="form.main_changes.length > 1" text type="error" @click="form.main_changes = form.main_changes.splice(i, 1)">
|
||||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
<NButton text type="primary" class="text-xs" @click="form.main_changes = [...form.main_changes, '']">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 连带变更内容 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div v-for="(_, i) in form.related_changes" :key="i" class="flex items-center gap-2">
|
||||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||||
<NInput v-model:value="form.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
|
||||
<NButton v-if="form.related_changes.length > 1" text type="error" @click="form.related_changes = form.related_changes.splice(i, 1)">
|
||||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
<NButton text type="primary" class="text-xs" @click="form.related_changes = [...form.related_changes, '']">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更类型 / 变更等级 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<!-- 变更类型 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="form.change_type">
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||
AI 判定说明
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更等级 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="form.change_level" disabled>
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio :key="0" :value="0">一般</NRadio>
|
||||
<NRadio :key="1" :value="1">重要</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
||||
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更时限 / 风险分析 -->
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
|
||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NRadioGroup v-model:value="form.duration_type">
|
||||
<div class="flex gap-x-2">
|
||||
<NRadio v-for="t in [0, 1]" :key="t" :value="t">
|
||||
{{ t === 0 ? '永久' : '临时' }}
|
||||
</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
<NCheckbox v-model:checked="form.urgent" :checked-value="1" :unchecked-value="0">紧急</NCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
|
||||
class="text-sm"
|
||||
:ghost="form.risk_tools.includes(t.key) ? false : true"
|
||||
:type="form.risk_tools.includes(t.key) ? 'primary' : 'default'"
|
||||
>
|
||||
{{ t.label }}
|
||||
</NButton>
|
||||
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
|
||||
按等级重新推荐
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 计划投用时间 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||||
<NDatePicker v-model:value="form.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
|
||||
<!-- 临时 -->
|
||||
<template v-if="form.duration_type === 1">
|
||||
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
|
||||
<NDatePicker v-model:value="form.restore_deadline" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
|
||||
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
|
||||
</template>
|
||||
<!-- 永久 -->
|
||||
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.urgent" class="text-xs text-red-500">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
|
||||
<!-- 所需材料 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="text-[11px] text-slate-400">施工所需材料(生成变更申请表时系统自动生成,支持手动修改)</div>
|
||||
<NInput type="textarea" v-model:value="form.materials" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资料上传 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
|
||||
<NButton size="small" class="text-xs" v-for="f in files" :key="f" @click="previewFile = f" title="点击在线预览">
|
||||
<Icon icon="akar-icons:file" class="size-14px" />
|
||||
{{ f }}
|
||||
</NButton>
|
||||
<NButton @click="files = [...files, `资料_${files.length + 1}.pdf`]">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
|
||||
</NButton>
|
||||
<span class="text-xs text-slate-400">(点击附件名弹窗预览,支持 PDF / Word / 图片在线查看)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 需更新的资料 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NCheckboxGroup v-model:value="form.update_docs">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
<span class="self-center text-xs text-slate-400">(多选,变更关闭前逐项确认上传最新版本)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 涉及专业 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||||
<NCheckboxGroup v-model:value="form.disciplines" :on-update:value="toggleDiscipline">
|
||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||
<NCheckbox v-for="p in ['技术', '仪表', '电气', '环保', '质量']" :key="p" :value="p" :label="p" />
|
||||
</div>
|
||||
</NCheckboxGroup>
|
||||
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
|
||||
</div>
|
||||
<!-- <div v-if="Object.keys(signers).length > 0" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
||||
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
|
||||
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ d }} · {{ p }}</NTag>
|
||||
<span class="text-slate-400">(在底部「审批流程设置」中可调整人选)</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 / 审批流程设置 -->
|
||||
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
||||
<NDrawerContent>
|
||||
<template #header>
|
||||
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
|
||||
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
|
||||
</template>
|
||||
<div class="h-full space-y-4 overflow-y-auto">
|
||||
<!-- AI 变更类型判定说明 -->
|
||||
<template v-if="sheet === 'type'">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<NTag v-if="typeAiLoading" type="info" size="small">
|
||||
<template #icon>
|
||||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
|
||||
</template>
|
||||
AI 判定中…
|
||||
</NTag>
|
||||
<template v-else>
|
||||
<NTag type="info" size="small">判定:{{ typeAi?.type || '工艺' }}</NTag>
|
||||
<NTag size="small">置信度:{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '高' }}</NTag>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||||
<p><b>判定依据:</b></p>
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||||
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||||
</ul>
|
||||
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||
</div>
|
||||
<div v-else class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||
<p><b>判定结论:</b>本次变更涉及反应温度(80℃→90℃)、保温时间(8h→4h)等工艺参数调整及 DCS 报警/联锁值修改,未涉及设备本体更换或材质变更,依据变更类型规则判定为「工艺」类变更。</p>
|
||||
<p><b>判定依据:</b></p>
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
<li>命中规则 2.1:操作参数超出原设计控制范围 → 工艺类</li>
|
||||
<li>命中规则 2.4:DCS 报警值/联锁值调整 → 工艺类(仪表联动)</li>
|
||||
<li>排除规则 3.2:无设备规格型号变化 → 不构成设备类</li>
|
||||
</ul>
|
||||
<p><b>知识库引用:</b>工艺卡片 R-101(Rev B)、DCS 联锁台账、《变更管理制度》第 4.2 条。</p>
|
||||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 变更等级判定表 -->
|
||||
<template v-if="sheet === 'level'">
|
||||
<span v-if="levelAiLoading" class="flex items-center gap-1 text-md font-normal text-violet-500">
|
||||
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" /> AI 评分中…
|
||||
</span>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<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>
|
||||
<NTag size="small" :type="totalScore >= 20 ? 'error' : 'warning'">
|
||||
{{ form.change_level === 1 ? '重要变更' : '一般变更' }}
|
||||
</NTag>
|
||||
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||||
</div>
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
<div v-for="(dim, di) in LEVEL_DIMS" :key="dim.name" class="rounded-lg border p-3">
|
||||
<div class="mb-2 flex items-baseline justify-between gap-2">
|
||||
<span class="text-sm font-semibold text-slate-700">
|
||||
{{ dim.name }}
|
||||
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">({{ dim.note }})</span>
|
||||
</span>
|
||||
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ LEVEL_DIMS[di].score }}</b></span>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<NRadioGroup v-model:value="LEVEL_DIMS[di].score" size="small" :disabled="levelAiLoading" name="radiogroup" :on-update:value="(v)=>handleScoreChange(v,di)">
|
||||
<div class="flex flex-col justify-center gap-2">
|
||||
<NRadio v-for="op in dim.options" :key="op.score" :value="op.score">
|
||||
<p class="text-xs">{{ op.label }}({{ op.score }}分)</p>
|
||||
</NRadio>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||||
<Icon icon="lucide:sparkles" class="size-14px" />
|
||||
AI 评分分析过程
|
||||
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
|
||||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
|
||||
<p v-if="levelAiText">{{ levelAiText }}</p>
|
||||
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
|
||||
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
|
||||
</p>
|
||||
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
@@ -0,0 +1,146 @@
|
||||
<!-- 变更预识别 -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { AiError, aiRecognize } from "@/service/api/ai";
|
||||
import ChangeCompareTable from "./ChangeCompareTable.vue";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const emit = defineEmits(['updateBtn']);
|
||||
|
||||
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);
|
||||
// 编辑风险预分析
|
||||
const preRiskEdit = ref<string>('');
|
||||
// 是否显示险预分析
|
||||
const preRiskShow = ref<boolean>(false);
|
||||
// 按钮禁用
|
||||
const btnDisabled = 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 = [];
|
||||
emit('updateBtn', 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 }));
|
||||
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);
|
||||
}
|
||||
};
|
||||
// 更新表单
|
||||
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;
|
||||
}
|
||||
|
||||
defineExpose({form,updateForm,updateBtnDisabled})
|
||||
|
||||
</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: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="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-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>
|
||||
<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>
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
matrixConfigApi,
|
||||
saveMatrixConfigApi
|
||||
} from '@/service/api/system';
|
||||
import { localStg } from '@/utils/storage';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
@@ -47,6 +48,11 @@ const toggle = async (v: boolean, item:any) => {
|
||||
if(!error){
|
||||
item.value = v ? '1' : '0';
|
||||
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
|
||||
const userInfo = localStg.get('userInfo');
|
||||
if(userInfo){
|
||||
userInfo.ai_assist_enabled = v ? '1' : '0';
|
||||
localStg.set('userInfo', userInfo);
|
||||
}
|
||||
}
|
||||
switchLoading.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user