发起变更,底部封装通用组件
This commit is contained in:
+2
-1
@@ -3,5 +3,6 @@ export enum SetupStoreId {
|
|||||||
Theme = 'theme-store',
|
Theme = 'theme-store',
|
||||||
Auth = 'auth-store',
|
Auth = 'auth-store',
|
||||||
Route = 'route-store',
|
Route = 'route-store',
|
||||||
Tab = 'tab-store'
|
Tab = 'tab-store',
|
||||||
|
Change = 'change-store',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ export function applicationApi(id: number) {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function applicationSaveApi(id: number, params: {
|
export function applicationSaveApi(id: number, params: {
|
||||||
change_type: number,
|
change_type: number | null,
|
||||||
change_level: number,
|
change_level: any,
|
||||||
duration_type: number,
|
duration_type: number | null,
|
||||||
restore_deadline: string,
|
restore_deadline: number | null,
|
||||||
org_id: number,
|
org_id: number | null,
|
||||||
urgent: number,
|
urgent: number,
|
||||||
purposes: string[],
|
purposes: string[],
|
||||||
effect: string,
|
effect: string,
|
||||||
@@ -67,7 +67,7 @@ export function applicationSaveApi(id: number, params: {
|
|||||||
update_docs: string[],
|
update_docs: string[],
|
||||||
disciplines: string[],
|
disciplines: string[],
|
||||||
manage_dept: string,
|
manage_dept: string,
|
||||||
plan_use_date: string,
|
plan_use_date: number | null,
|
||||||
risk_tools: string[],
|
risk_tools: string[],
|
||||||
}) {
|
}) {
|
||||||
return request({
|
return request({
|
||||||
@@ -76,3 +76,14 @@ export function applicationSaveApi(id: number, params: {
|
|||||||
data: params
|
data: params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 变更培训内容保存
|
||||||
|
export function trainingSaveApi(id: number, params: {
|
||||||
|
content: string,
|
||||||
|
}) {
|
||||||
|
return request({
|
||||||
|
url: `/api/changes/${id}/train/content`,
|
||||||
|
method: 'put',
|
||||||
|
data: params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,6 +77,23 @@ export function deleteTemplateApi(id:number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取审批流程(按三键匹配模板)
|
||||||
|
export function getFlowApi(data:{category:number | null,level:number | null,duration:number | null}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/approval-templates/match',
|
||||||
|
method: 'get',
|
||||||
|
params: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 本单审批流程·审批人设置
|
||||||
|
export function setApproverApi(id:number,data:any) {
|
||||||
|
return request({
|
||||||
|
url: `/api/changes/${id}/approval-flow`,
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 获取风险矩阵配置
|
// 获取风险矩阵配置
|
||||||
export function matrixConfigApi(bizType:string) {
|
export function matrixConfigApi(bizType:string) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { SetupStoreId } from '@/enum';
|
||||||
|
|
||||||
|
export const useChangeStore = defineStore(SetupStoreId.Change, () => {
|
||||||
|
const stepsInfo = ref<Api.Change.StepInfo[]>([]);
|
||||||
|
const flowInfo = ref<Api.Change.FlowInfo[]>([]);
|
||||||
|
// 获取选中模板信息
|
||||||
|
const getStepsInfo = computed(() => stepsInfo.value);
|
||||||
|
// 获取审批链信息
|
||||||
|
const getFlowInfo = computed(() => flowInfo.value);
|
||||||
|
|
||||||
|
// 更新选中模板信息
|
||||||
|
function setStepsInfo(data: Api.Change.StepInfo[]) {
|
||||||
|
stepsInfo.value = data;
|
||||||
|
setFlowInfo(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedMembers(steps: Api.Change.StepInfo[]) {
|
||||||
|
return steps.map(step => {
|
||||||
|
// 先找 is_sign === 1 的成员
|
||||||
|
const signed = step.members.find((m: any) => m.is_sign === 1);
|
||||||
|
// 若没有则取第一个,若成员列表为空则返回 null
|
||||||
|
const selected = signed || (step.members.length > 0 ? step.members[0] : null);
|
||||||
|
return {
|
||||||
|
stepId: step.id,
|
||||||
|
name: step.name,
|
||||||
|
countersign: step.countersign, // 添加会签标识
|
||||||
|
selectedMember: selected? [selected] : [], // 选中的成员对象
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 更新审批链信息
|
||||||
|
function setFlowInfo(data: Api.Change.StepInfo[]) {
|
||||||
|
const sortedSteps = [...data || []].sort((a, b) => a.step_order - b.step_order)
|
||||||
|
flowInfo.value = getSelectedMembers(sortedSteps);
|
||||||
|
}
|
||||||
|
function updateFlowInfo(data: Api.Change.FlowInfo[]) {
|
||||||
|
flowInfo.value = data;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getStepsInfo,
|
||||||
|
setStepsInfo,
|
||||||
|
getFlowInfo,
|
||||||
|
updateFlowInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
Vendored
+40
@@ -0,0 +1,40 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Change
|
||||||
|
*
|
||||||
|
* backend api module: "change"
|
||||||
|
*/
|
||||||
|
namespace Change {
|
||||||
|
interface MemberInfo {
|
||||||
|
id: number;
|
||||||
|
is_sign: number;
|
||||||
|
org_id: number;
|
||||||
|
org_name: string;
|
||||||
|
specialty: string;
|
||||||
|
user_id: number;
|
||||||
|
user_name: string;
|
||||||
|
}
|
||||||
|
interface StepInfo {
|
||||||
|
id: number;
|
||||||
|
countersign: boolean;
|
||||||
|
name: string;
|
||||||
|
step_order: number;
|
||||||
|
members: MemberInfo[];
|
||||||
|
}
|
||||||
|
interface TemplateInfo {
|
||||||
|
id: number;
|
||||||
|
category: number;
|
||||||
|
created_at?: string;
|
||||||
|
duration: number;
|
||||||
|
level: number;
|
||||||
|
steps: StepInfo[];
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
interface FlowInfo {
|
||||||
|
stepId: number;
|
||||||
|
countersign: boolean;
|
||||||
|
name: string;
|
||||||
|
selectedMember: MemberInfo[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
|||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { setApproverApi } from '@/service/api/system';
|
||||||
|
|
||||||
|
const props = defineProps(['currentId','currentTemplate'])
|
||||||
|
const emit = defineEmits(['finish'])
|
||||||
|
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
// 存储每个步骤选中的审核人 user_id
|
||||||
|
const approvers = reactive<Record<number, number>>({})
|
||||||
|
// 审批链
|
||||||
|
const approvalFlowText = computed(() => {
|
||||||
|
if (!list.value || list.value.length === 0) return '暂无步骤'
|
||||||
|
// 按 step_order 排序
|
||||||
|
const sorted = [...list.value].sort((a, b) => a.step_order - b.step_order)
|
||||||
|
return sorted.map(item => item.name).join(' → ')
|
||||||
|
})
|
||||||
|
const list = ref<any[]>(props.currentTemplate?.steps || []);
|
||||||
|
|
||||||
|
// 更新选中值
|
||||||
|
const updateApprover = (stepId: number, userId: number) => {
|
||||||
|
approvers[stepId] = userId
|
||||||
|
}
|
||||||
|
// 匹配审批人
|
||||||
|
function matchApprovers(steps: any[], mapping: Record<number, number>) {
|
||||||
|
const result = [];
|
||||||
|
for (const [stepId, userId] of Object.entries(mapping)) {
|
||||||
|
const step = steps.find(s => s.id === Number(stepId));
|
||||||
|
if (step) {
|
||||||
|
const member = step.members.filter((m: any) => m.user_id === userId);
|
||||||
|
if (member.length > 0) {
|
||||||
|
result.push({
|
||||||
|
step_id: step.id,
|
||||||
|
member: member
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// 完成设置
|
||||||
|
const finishApprover = async () => {
|
||||||
|
const matched = matchApprovers(list.value, approvers);
|
||||||
|
loading.value = true;
|
||||||
|
const {error} = await setApproverApi(props.currentId,{steps: matched})
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success("审批流程设置已保存");
|
||||||
|
emit('finish', matched, false)
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
list.value.forEach((item: any) => {
|
||||||
|
if (item.members && item.members.length > 0) {
|
||||||
|
// 优先选 is_sign=1 的成员 ,否则选第一个
|
||||||
|
const defaultMember = item.members.find((m: any) => m.is_sign === 1) || item.members[0]
|
||||||
|
approvers[item.id] = defaultMember.user_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full space-y-4 overflow-y-auto">
|
||||||
|
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
|
||||||
|
<div class="rounded-lg border p-3" v-for="(item,index) in list" :key="item.id">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<NTag type="info" size="small">{{Number(index)+1}}</NTag> {{item.name}} {{item.countersign ? '(专业会签)' : ''}}
|
||||||
|
</div>
|
||||||
|
<NSelect :value="approvers[item.id]" :options="item.members" label-field="user_name" value-field="user_id"
|
||||||
|
@update:value="(v) => updateApprover(item.id, v)" />
|
||||||
|
</div>
|
||||||
|
<NButton type="primary" class="w-full" :loading="loading" @click="finishApprover">完成设置</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,34 +1,36 @@
|
|||||||
<!-- 变更申请表 -->
|
<!-- 变更申请表 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, h } from 'vue';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
|
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
|
import { useChangeStore } from '@/store/modules/change';
|
||||||
|
import Footer from './footer.vue'
|
||||||
|
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
const changeStore = useChangeStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps(['orgTree','templateList','typeList','levelList','isAiOpen','title','currentId','formInfo'])
|
||||||
isAiOpen: Boolean,
|
const emit = defineEmits(['confirmTab']);
|
||||||
formInfo: Object as () => any
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['updateBtn']);
|
|
||||||
|
|
||||||
const aiFail = (e: any) => {
|
const aiFail = (e: any) => {
|
||||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||||
else window.$message?.warning("AI 分析失败,可手动填写");
|
else window.$message?.warning("AI 分析失败,可手动填写");
|
||||||
};
|
};
|
||||||
const today = () => new Date().toISOString().slice(0, 10);
|
const today = () => new Date().toISOString().slice(0, 10);
|
||||||
// 按钮禁用
|
|
||||||
const btnDisabled = ref<boolean>(false);
|
const footerRef = ref<any>(null);
|
||||||
|
const specialties = ['生产','技术', '仪表', '电气', '设备', '安全', '质量'];
|
||||||
// 总分数
|
// 总分数
|
||||||
const totalScore = ref<number>(0);
|
const totalScore = ref<number>(0);
|
||||||
|
// 当前等级
|
||||||
|
const currentLevel = computed(() => props.levelList.find((x:{value:number | null, label:string, color:string}) => x.value === form.value.change_level));
|
||||||
// 当前用户
|
// 当前用户
|
||||||
const userInfo = ref<any>(localStg.get('userInfo'));
|
const userInfo = ref<any>(localStg.get('userInfo'));
|
||||||
// 预览文件
|
// 预览文件
|
||||||
const previewFile = ref<string | null>(null);
|
const previewFile = ref<string | null>(null);
|
||||||
// 抽屉类型
|
|
||||||
const sheet = ref<string>('');
|
const sheet = ref<string>('');
|
||||||
// 表单
|
// 表单
|
||||||
const form = ref<{
|
const form = ref<{
|
||||||
@@ -50,7 +52,7 @@ const form = ref<{
|
|||||||
risk_tools: string[],
|
risk_tools: string[],
|
||||||
}>({
|
}>({
|
||||||
change_type: null,
|
change_type: null,
|
||||||
change_level: computed(() => totalScore.value >= 20 ? 1 : 0),
|
change_level: computed(() => totalScore.value >= 20 ? 2 : 1),
|
||||||
duration_type: null,
|
duration_type: null,
|
||||||
restore_deadline: null,
|
restore_deadline: null,
|
||||||
org_id: null,
|
org_id: null,
|
||||||
@@ -141,12 +143,44 @@ const LEVEL_DIMS = ref([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const disciplines = ref<string[]>([]);
|
// 筛选涉及专业的人员和部门
|
||||||
const signers = ref<Record<string, string>>({});
|
const filterMembersBySpecialties = (data: any, specialties: string[]) => {
|
||||||
|
const result: any[] = [];
|
||||||
|
data.steps.forEach((step: any) => {
|
||||||
|
step.members.forEach((member: any) => {
|
||||||
|
if (member.specialty && specialties.includes(member.specialty)) {
|
||||||
|
result.push({
|
||||||
|
stepName: step.name, // 上级部门名称
|
||||||
|
member: member // 成员对象
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// 当前模板
|
||||||
|
const currentTemplate = computed(() => {
|
||||||
|
let template = props.templateList.find((item: any) =>
|
||||||
|
item.level === form.value?.change_level &&
|
||||||
|
item.category === form.value?.change_type &&
|
||||||
|
item.duration === form.value?.duration_type
|
||||||
|
) || null;
|
||||||
|
footerRef.value?.clearApprovers();
|
||||||
|
changeStore.setStepsInfo(template?template.steps:[]);
|
||||||
|
return template?template:null;
|
||||||
|
});
|
||||||
|
// 联动专业会签人员
|
||||||
|
const signers = computed(() => {
|
||||||
|
if(currentTemplate.value){
|
||||||
|
const matchedMembers = filterMembersBySpecialties(currentTemplate.value, form.value.disciplines);
|
||||||
|
return matchedMembers
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
// 按等级推荐
|
||||||
const recommendTools = () => {
|
const recommendTools = () => {
|
||||||
const lv = form.value?.change_level === 1 ? "重要" : "一般";
|
const lv = totalScore.value >= 20 ? "重要" : "一般";
|
||||||
form.value.risk_tools = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
form.value.risk_tools = totalScore.value >= 20 ? ["HAZOP"] : ["RISK_CHECK"];
|
||||||
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,17 +189,6 @@ const toggleTool = (t: {key:string,label:string}) => {
|
|||||||
const { risk_tools } = form.value;
|
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];
|
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 typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||||||
@@ -203,11 +226,17 @@ const levelAiLoading = ref<boolean>(false);
|
|||||||
// 打开等级判定表抽屉
|
// 打开等级判定表抽屉
|
||||||
const openLevelSheet = async () => {
|
const openLevelSheet = async () => {
|
||||||
let form = props.formInfo;
|
let form = props.formInfo;
|
||||||
if (!form?.description) {
|
if (!form?.description && props.isAiOpen) {
|
||||||
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
|
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
|
||||||
}
|
}
|
||||||
sheet.value = "level";
|
sheet.value = "level";
|
||||||
if (levelAiLoading.value) return;
|
if (levelAiLoading.value) return;
|
||||||
|
};
|
||||||
|
// 使用AI 评分
|
||||||
|
const useAiLevel = async () => {
|
||||||
|
let form = props.formInfo;
|
||||||
|
if (levelAiLoading.value) return;
|
||||||
|
levelAiLoading.value = true;
|
||||||
levelAiLoading.value = true;
|
levelAiLoading.value = true;
|
||||||
try {
|
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 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)) }));
|
||||||
@@ -223,7 +252,7 @@ const openLevelSheet = async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
levelAiLoading.value = false;
|
levelAiLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// 处理等级判定表评分变化
|
// 处理等级判定表评分变化
|
||||||
const handleScoreChange = (val:number,index:number) => {
|
const handleScoreChange = (val:number,index:number) => {
|
||||||
LEVEL_DIMS.value[index].score = val;
|
LEVEL_DIMS.value[index].score = val;
|
||||||
@@ -233,29 +262,42 @@ const handleScoreChange = (val:number,index:number) => {
|
|||||||
});
|
});
|
||||||
totalScore.value = score;
|
totalScore.value = score;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载PDF
|
// 下载PDF
|
||||||
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
|
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
|
||||||
|
// 自定义渲染展开图标
|
||||||
// 更新表单
|
const renderSwitcherIcon = (node:any) => {
|
||||||
const updateForm = (res: any, isShow: boolean) => {
|
// 如果节点没有 children 或 children 为空数组,则不显示图标
|
||||||
|
if (!node.option.children || node.option.children.length === 0) {
|
||||||
}
|
return null; // 返回 null 表示不渲染任何图标
|
||||||
// 更新按钮禁用状态
|
}
|
||||||
const updateBtnDisabled = (disabled: boolean) => {
|
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
|
||||||
btnDisabled.value = disabled;
|
return h(Icon,{
|
||||||
|
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
|
||||||
|
class: 'size-12px',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({form,updateForm,updateBtnDisabled})
|
defineExpose({form})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
<!-- 申请部门 / 申请人 -->
|
<!-- 申请部门 / 申请人 -->
|
||||||
<div class="grid gap-4 grid-cols-2">
|
<div class="grid gap-4 grid-cols-2">
|
||||||
<div class="flex gap-3">
|
<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="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="请输入申请部门" />
|
<NTreeSelect
|
||||||
|
v-model:value="form.org_id"
|
||||||
|
default-expand-all
|
||||||
|
label-field="label"
|
||||||
|
key-field="id"
|
||||||
|
:options="props.orgTree"
|
||||||
|
:render-switcher-icon="renderSwitcherIcon"
|
||||||
|
placeholder="请选择申请部门"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<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="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
|
||||||
@@ -340,10 +382,10 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
<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">
|
<NRadioGroup v-model:value="form.change_type">
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
<NRadio v-for="t in props.typeList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
|
||||||
</div>
|
</div>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton v-if="isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
<NButton v-if="props.isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
||||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||||
AI 判定说明
|
AI 判定说明
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -353,13 +395,12 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<div class="flex gap-3">
|
<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="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">
|
<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="isAiOpen">
|
<NRadioGroup v-model:value="form.change_level" disabled>
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
<NRadio :key="0" :value="0">一般</NRadio>
|
<NRadio v-for="t in props.levelList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
|
||||||
<NRadio :key="1" :value="1">重要</NRadio>
|
|
||||||
</div>
|
</div>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton v-if="isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
||||||
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,8 +413,8 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
<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">
|
<NRadioGroup v-model:value="form.duration_type">
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
<NRadio v-for="t in [0, 1]" :key="t" :value="t">
|
<NRadio v-for="t in [1, 2]" :key="t" :value="t">
|
||||||
{{ t === 0 ? '永久' : '临时' }}
|
{{ t === 1 ? '永久' : '临时' }}
|
||||||
</NRadio>
|
</NRadio>
|
||||||
</div>
|
</div>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
@@ -396,13 +437,14 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="form.urgent" class="text-xs text-red-500 ml-27">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
|
||||||
<!-- 计划投用时间 -->
|
<!-- 计划投用时间 -->
|
||||||
<div class="flex gap-3">
|
<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="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">
|
<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]" />
|
<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">
|
<template v-if="form.duration_type === 2">
|
||||||
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
|
<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]" />
|
<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>
|
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
|
||||||
@@ -411,7 +453,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
|
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="form.urgent" class="text-xs text-red-500">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
|
|
||||||
<!-- 所需材料 -->
|
<!-- 所需材料 -->
|
||||||
<div class="flex gap-3">
|
<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="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
|
||||||
@@ -463,22 +505,30 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
|
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
|
||||||
<div class="flex-1">
|
<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">
|
<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">
|
<NCheckboxGroup v-model:value="form.disciplines">
|
||||||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||||||
<NCheckbox v-for="p in ['技术', '仪表', '电气', '环保', '质量']" :key="p" :value="p" :label="p" />
|
<NCheckbox v-for="p in specialties" :key="p" :value="p" :label="p" />
|
||||||
</div>
|
</div>
|
||||||
</NCheckboxGroup>
|
</NCheckboxGroup>
|
||||||
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
|
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
|
||||||
</div>
|
</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">
|
<div v-if="signers && signers.length" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
||||||
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
|
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
|
||||||
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ d }} · {{ p }}</NTag>
|
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ p?.member?.specialty }} · {{ p?.member?.user_name }}{{p?.stepName ? ` (${p.stepName})` : ''}}</NTag>
|
||||||
<span class="text-slate-400">(在底部「审批流程设置」中可调整人选)</span>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 / 审批流程设置 -->
|
<Footer
|
||||||
|
ref="footerRef"
|
||||||
|
type="form"
|
||||||
|
:currentId="props.currentId"
|
||||||
|
:title="props.title"
|
||||||
|
:isAiOpen="props.isAiOpen"
|
||||||
|
:currentForm="form"
|
||||||
|
/>
|
||||||
|
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 -->
|
||||||
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
||||||
<NDrawerContent>
|
<NDrawerContent>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -515,18 +565,30 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
|
|
||||||
<!-- 变更等级判定表 -->
|
<!-- 变更等级判定表 -->
|
||||||
<template v-if="sheet === 'level'">
|
<template v-if="sheet === 'level'">
|
||||||
<span v-if="levelAiLoading" class="flex items-center gap-1 text-md font-normal text-violet-500">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" /> AI 评分中…
|
<NButton v-if="props.isAiOpen" size="small" type="primary">
|
||||||
</span>
|
<div v-if="levelAiLoading" class="flex items-center gap-1 text-xs">
|
||||||
<div v-else class="flex items-center gap-2">
|
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />AI 评分中
|
||||||
<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">
|
</div>
|
||||||
✨ AI 生成 · 需人工确认
|
<div v-else class="flex items-center gap-1 text-xs" @click="useAiLevel">
|
||||||
</span>
|
使用AI 评分
|
||||||
<NTag size="small" :type="totalScore >= 20 ? 'error' : 'warning'">
|
</div>
|
||||||
{{ form.change_level === 1 ? '重要变更' : '一般变更' }}
|
</NButton>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NTag
|
||||||
|
v-if="currentLevel"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: tagBgColor(currentLevel?.color),
|
||||||
|
textColor: tagTextColor(currentLevel?.color),
|
||||||
|
borderColor: tagBorderColor(currentLevel?.color),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ currentLevel?.label ? currentLevel.label+'变更' : '' }}
|
||||||
</NTag>
|
</NTag>
|
||||||
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="grid gap-3 grid-cols-2">
|
<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 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">
|
<div class="mb-2 flex items-baseline justify-between gap-2">
|
||||||
@@ -547,7 +609,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
|
<div v-if="props.isAiOpen && (levelAiText || levelAiBasis.some((x) => x))" 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">
|
<div class="flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||||||
<Icon icon="lucide:sparkles" class="size-14px" />
|
<Icon icon="lucide:sparkles" class="size-14px" />
|
||||||
AI 评分分析过程
|
AI 评分分析过程
|
||||||
@@ -555,7 +617,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
|
<div class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
|
||||||
<p v-if="levelAiText">{{ levelAiText }}</p>
|
<p v-if="levelAiText">{{ levelAiText }}</p>
|
||||||
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
|
<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>
|
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
|
||||||
@@ -567,6 +629,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
</div>
|
</div>
|
||||||
</NDrawerContent>
|
</NDrawerContent>
|
||||||
</NDrawer>
|
</NDrawer>
|
||||||
|
|
||||||
<!-- 附件在线预览弹窗 -->
|
<!-- 附件在线预览弹窗 -->
|
||||||
<NModal
|
<NModal
|
||||||
:show="previewFile !== null"
|
:show="previewFile !== null"
|
||||||
@@ -603,3 +666,13 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.n-radio) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,12 +3,14 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
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";
|
import ChangeCompareTable from "./ChangeCompareTable.vue";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const emit = defineEmits(['updateBtn']);
|
const props = defineProps(['title','currentId'])
|
||||||
|
|
||||||
|
|
||||||
const aiFail = (e: any) => {
|
const aiFail = (e: any) => {
|
||||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||||
@@ -18,6 +20,12 @@ const aiFail = (e: any) => {
|
|||||||
const isAiGenerated = ref<boolean>(false);
|
const isAiGenerated = ref<boolean>(false);
|
||||||
// 变更识别loading
|
// 变更识别loading
|
||||||
const preLoading = ref<boolean>(false);
|
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>('');
|
const preRiskEdit = ref<string>('');
|
||||||
// 是否显示险预分析
|
// 是否显示险预分析
|
||||||
@@ -44,41 +52,121 @@ const runIdentify = async () => {
|
|||||||
return window.$message?.warning("请先输入变更内容描述");
|
return window.$message?.warning("请先输入变更内容描述");
|
||||||
}
|
}
|
||||||
form.value.compare_rows = [];
|
form.value.compare_rows = [];
|
||||||
emit('updateBtn', true);
|
btnDisabled.value = true;
|
||||||
preLoading.value = true;
|
preLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await aiRecognize(form.value.description);
|
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 }));
|
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} 项,请逐项人工确认(可直接修改)`);
|
window.$message?.success(`AI 变更识别完成,共 ${form.value.compare_rows.length} 项,请逐项人工确认(可直接修改)`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
form.value.compare_rows = [];
|
form.value.compare_rows = [];
|
||||||
aiFail(e);
|
aiFail(e);
|
||||||
} finally {
|
} finally {
|
||||||
preLoading.value = false;
|
preLoading.value = false;
|
||||||
emit('updateBtn', false);
|
btnDisabled.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 更新表单
|
// 运行风险预分析
|
||||||
const updateForm = (res: any, isShow: boolean) => {
|
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 = {
|
||||||
...form.value,
|
...form.value,
|
||||||
severity: res?.severity ?? form.value.severity,
|
severity: res?.severity ?? form.value.severity,
|
||||||
probability: res?.probability ?? form.value.probability,
|
probability: res?.probability ?? form.value.probability,
|
||||||
protection: res?.protection ?? form.value.protection,
|
protection: res?.protection ?? form.value.protection,
|
||||||
};
|
};
|
||||||
preRiskShow.value = isShow;
|
preRiskShow.value = true;
|
||||||
}
|
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||||||
// 更新按钮禁用状态
|
} catch (e: any) {
|
||||||
const updateBtnDisabled = (disabled: boolean) => {
|
aiFail(e);
|
||||||
btnDisabled.value = disabled;
|
} 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
<!-- 变更内容描述 -->
|
<!-- 变更内容描述 -->
|
||||||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
<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 justify-between px-4 py-2 border-b">
|
||||||
@@ -86,7 +174,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
|
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
|
||||||
<span class="font-semibold text-base ml-2">变更内容描述</span>
|
<span class="font-semibold text-base ml-2">变更内容描述</span>
|
||||||
</div>
|
</div>
|
||||||
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled || preLoading">
|
<NButton ghost type="primary" @click="runIdentify" :disabled="btnDisabled">
|
||||||
<Icon v-if="preLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
|
<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" />
|
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
|
||||||
变更识别
|
变更识别
|
||||||
@@ -135,8 +223,7 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
|
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
<NInput v-if="preRiskEdit === s.key" type="textarea" :bordered="false" v-model:value="form[s.key]" autofocus rows="6" />
|
<NInput v-model:value="form[s.key]" :readonly="preRiskEdit !== s.key" type="textarea" :bordered="false" autofocus />
|
||||||
<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>
|
||||||
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
||||||
AI 风险预分析仅供参考,用于申请阶段的快速研判;正式风险识别请前往「风险分析记录表」标签页,使用 HAZOP / JSA / 检查表法开展并留存记录。
|
AI 风险预分析仅供参考,用于申请阶段的快速研判;正式风险识别请前往「风险分析记录表」标签页,使用 HAZOP / JSA / 检查表法开展并留存记录。
|
||||||
@@ -144,5 +231,25 @@ defineExpose({form,updateForm,updateBtnDisabled})
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 326px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<!-- 变更培训内容 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import Footer from './footer.vue';
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const props = defineProps(['isAiOpen','title','currentId'])
|
||||||
|
const emit = defineEmits(['confirmTab']);
|
||||||
|
// 是否是ai生成
|
||||||
|
const isAiGenerated = ref<boolean>(false);
|
||||||
|
const form = ref<{ content: string }>({
|
||||||
|
content: ''
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-3 min-h-[22px]">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||||||
|
<Icon icon="lucide:graduation-cap" class="size-16px text-violet-500" />
|
||||||
|
变更培训内容
|
||||||
|
</span>
|
||||||
|
<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>
|
||||||
|
<span v-if="isAiGenerated" class="ml-auto text-xs text-slate-400">AI 生成内容均可点击修改自定义</span>
|
||||||
|
</div>
|
||||||
|
<!-- 培训内容 -->
|
||||||
|
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2">
|
||||||
|
<span class="text-sm font-medium text-slate-700">培训内容</span>
|
||||||
|
<span class="text-[11px] text-slate-400">含培训对象、培训方式、变更内容、操作方式、潜在风险与应对措施,一个文本框自动分段换行</span>
|
||||||
|
<span class="ml-auto text-[11px] text-slate-400">内容支持直接修改</span>
|
||||||
|
</div>
|
||||||
|
<NInput v-model:value="form.content" type="textarea" :bordered="false" :autosize="{minRows: 4}" class="font-mono text-sm" placeholder="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer
|
||||||
|
type="train"
|
||||||
|
:currentId="props.currentId"
|
||||||
|
:title="props.title"
|
||||||
|
:isAiOpen="props.isAiOpen"
|
||||||
|
:currentForm="form"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<!-- 变更培训内容 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import ApprovalDrawer from './approvalDrawer.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// 抽屉类型
|
||||||
|
const approvalDrawerShow = ref<boolean>(false);
|
||||||
|
const trainText = ref<string>('');
|
||||||
|
const btnDisabled = ref<boolean>(false);
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
|
const downloadLoading = ref<boolean>(false);
|
||||||
|
const regenerateLoading = ref<boolean>(false);
|
||||||
|
const submitLoading = ref<boolean>(false);
|
||||||
|
const confirmed = ref<boolean>(false);
|
||||||
|
const approvers = ref<Record<string, string>>({
|
||||||
|
部门审核: "李主任(一车间主任)",
|
||||||
|
最终批准: "李主任(部门负责人 · 固定)",
|
||||||
|
});
|
||||||
|
const signers = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
const closeDocs = ref([
|
||||||
|
{ name: "PID 图纸(PID-R201-03,受控最新版)", cat: "图纸与规程类", scope: "是", archived: true },
|
||||||
|
{ name: "操作规程(含超温应急处置卡)", cat: "图纸与规程类", scope: "是", archived: false },
|
||||||
|
{ name: "应急预案修订", cat: "图纸与规程类", scope: "不涉及", archived: false },
|
||||||
|
{ name: "总图更新", cat: "图纸与规程类", scope: "否", archived: false },
|
||||||
|
{ name: "DCS 联锁台账(新报警 / 联锁值)", cat: "台账与制度类", scope: "是", archived: false },
|
||||||
|
{ name: "设备台账与备件清单", cat: "台账与制度类", scope: "是", archived: true },
|
||||||
|
{ name: "风险分析报告(HAZOP / 检查表法)", cat: "过程与结果文件类", scope: "是", archived: true },
|
||||||
|
{ name: "培训签到与考核记录", cat: "过程与结果文件类", scope: "是", archived: true },
|
||||||
|
{ name: "PSSR 检查记录(含现场照片)", cat: "过程与结果文件类", scope: "是", archived: false },
|
||||||
|
{ name: "验收报告与化验数据", cat: "过程与结果文件类", scope: "是", archived: false },
|
||||||
|
]);
|
||||||
|
const closeFold = ref<Record<string, boolean>>({});
|
||||||
|
const uploadDoc = ref<string | null>(null);
|
||||||
|
const uploadSel = ref<string | null>(null);
|
||||||
|
|
||||||
|
const updateDocs = ref<string[]>([]);
|
||||||
|
const UPDATE_DOC_KW: Record<string, string> = { PID图纸: "PID", 操作规程: "操作规程", 总图: "总图", 设备台账: "设备台账", 工艺卡片: "工艺卡片", 联锁台账: "联锁台账", 应急预案: "应急预案" };
|
||||||
|
const needUpdate = (name: string) => updateDocs.value.some((d:any) => name.includes(UPDATE_DOC_KW[d] ?? d));
|
||||||
|
|
||||||
|
const changeScope = (d: any, v: string) => {
|
||||||
|
closeDocs.value = closeDocs.value.map((x:any) => (x.name === d.name ? { ...x, scope: v } : x));
|
||||||
|
if (v !== d.scope) window.$message?.info(`演示:「${d.name}」改判为「${v}」,改判理由与操作人已留痕`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadDocInfo = computed(() => {
|
||||||
|
if (!uploadDoc.value) return null;
|
||||||
|
const doc = closeDocs.value.find((x) => x.name === uploadDoc.value);
|
||||||
|
const base = uploadDoc.value.replace(/[((].*$/, "");
|
||||||
|
return {
|
||||||
|
storePath: `\\\\PLANT-DMS\\变更管理\\2026\\MOC-2026-R01-0041\\${doc?.cat ?? ""}\\`,
|
||||||
|
candidates: [`${base}_RevC_受控版.pdf`, `${base}_2026-08 更新版.docx`],
|
||||||
|
base,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const openDir = () => {
|
||||||
|
window.$message?.info('演示:已在文档管理系统中打开该目录')
|
||||||
|
}
|
||||||
|
const uploadFiles = () => {
|
||||||
|
uploadSel.value = `本地文件:${uploadDocInfo.value?.base}_最新版.pdf`;
|
||||||
|
window.$message?.info('演示:已接收拖拽 / 选择的本地文件');
|
||||||
|
}
|
||||||
|
const uploadConfirm = () => {
|
||||||
|
closeDocs.value = closeDocs.value.map((x) => (x.name === uploadDoc.value ? { ...x, archived: true } : x));
|
||||||
|
window.$message?.success(`「${uploadDoc.value}」已上传并归档(${uploadSel.value},受控版本,哈希留痕)`);
|
||||||
|
uploadDoc.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
|
||||||
|
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
|
||||||
|
|
||||||
|
// 保存草稿
|
||||||
|
const saveDraft = async () => {
|
||||||
|
// if(changeApplyRules()){
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// saveLoading.value = true;
|
||||||
|
// const {error} = await applicationSaveApi(props.currentId, form.value);
|
||||||
|
// if(!error){
|
||||||
|
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
// }
|
||||||
|
// saveLoading.value = false;
|
||||||
|
}
|
||||||
|
// ---------- 提交 ----------
|
||||||
|
const submitApply = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const confirmTab = () => {
|
||||||
|
confirmed.value = !confirmed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({trainText})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||||||
|
<Icon icon="quill:folder-open" class="size-16px" />
|
||||||
|
变更关闭确认表(资料归档清单)
|
||||||
|
</span>
|
||||||
|
<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" class="text-xs">AI 辅助判定一次 · 支持人工改判留痕</NTag>
|
||||||
|
<span class="ml-auto text-xs text-slate-400">判定为「是」的资料全部归档后,变更主管部门专责方可确认关闭</span>
|
||||||
|
</div>
|
||||||
|
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<div class="flex items-center gap-3 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-500">
|
||||||
|
<span class="w-5"></span>
|
||||||
|
<span class="flex-1">资料名称(与申请表「需更新的资料」勾选联动,勾选资料标记「需更新版本」)</span>
|
||||||
|
<span class="w-48 text-center">是否涉及(AI 判定,可改判)</span>
|
||||||
|
<span class="w-32 text-center">归档状态</span>
|
||||||
|
</div>
|
||||||
|
<template v-for="cat in (['图纸与规程类', '台账与制度类', '过程与结果文件类'])" :key="cat">
|
||||||
|
<div v-if="closeDocs.filter((d) => d.cat === cat).length">
|
||||||
|
<button @click="closeFold = { ...closeFold, [cat]: !closeFold[cat] }"
|
||||||
|
class="flex w-full items-center gap-2 border-t bg-slate-50 px-3 py-2 text-left text-xs font-semibold text-slate-600 transition hover:bg-[#EAF0F9]">
|
||||||
|
<Icon icon="formkit:right" class="size-14px text-slate-400 transition" :class="closeFold[cat] ? '' : 'rotate-90'" />
|
||||||
|
{{ cat }}
|
||||||
|
<span class="font-normal text-slate-400">
|
||||||
|
{{ closeDocs.filter((d) => d.cat === cat).length }} 项 · 已归档
|
||||||
|
{{ closeDocs.filter((d) => d.cat === cat && d.scope === '是' && d.archived).length }}/{{ closeDocs.filter((d) => d.cat === cat && d.scope === '是').length }}
|
||||||
|
</span>
|
||||||
|
<span class="ml-auto text-[11px] font-normal text-slate-400">{{ closeFold[cat] ? '点击展开' : '点击折叠' }}</span>
|
||||||
|
</button>
|
||||||
|
<template v-if="!closeFold[cat]">
|
||||||
|
<div v-for="d in closeDocs.filter((x) => x.cat === cat)" :key="d.name" class="flex flex-wrap items-center gap-3 border-t px-3 py-2.5 text-sm">
|
||||||
|
<span class="w-5 text-slate-400">{{ closeDocs.indexOf(d) + 1 }}</span>
|
||||||
|
<span class="min-w-0 flex-1 text-slate-800">
|
||||||
|
{{ d.name }}
|
||||||
|
<NTag v-if="needUpdate(d.name)" size="small" type="info" class="ml-2">需更新版本</NTag>
|
||||||
|
</span>
|
||||||
|
<span class="flex w-48 shrink-0 justify-center gap-1.5">
|
||||||
|
<NButton size="tiny" round v-for="v in (['是', '否', '不涉及'])" :key="v"
|
||||||
|
@click="changeScope(d, v)"
|
||||||
|
class="text-[11px]"
|
||||||
|
:type="d.scope === v ? 'primary' : 'default'">
|
||||||
|
{{ v }}
|
||||||
|
</NButton>
|
||||||
|
</span>
|
||||||
|
<span class="flex w-32 shrink-0 items-center justify-center gap-2">
|
||||||
|
<template v-if="d.scope === '是'">
|
||||||
|
<NTag v-if="d.archived" size="small" type="success">已归档</NTag>
|
||||||
|
<template v-else>
|
||||||
|
<NTag size="small" type="warning">待上传</NTag>
|
||||||
|
<NButton text type="primary" class="text-xs hover:underline" @click="uploadDoc = d.name; uploadSel = null;">上传</NButton>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<NTag v-else size="small">{{ d.scope }}</NTag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
||||||
|
<span>
|
||||||
|
统计:涉及 <b class="text-[#17407F]">{{ closeDocs.filter((d) => d.scope === '是').length }}</b> 项 ·
|
||||||
|
已归档 <b class="text-emerald-600">{{ closeDocs.filter((d) => d.scope === '是' && d.archived).length }}</b> 项 ·
|
||||||
|
待上传 <b class="text-amber-600">{{ closeDocs.filter((d) => d.scope === '是' && !d.archived).length }}</b> 项 ·
|
||||||
|
需更新版本 <b class="text-[#1D4E9C]">{{ closeDocs.filter((d) => needUpdate(d.name)).length }}</b> 项(联动申请表勾选)
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-400">资料上传支持 PC / 移动端(拍照上传),版本受控</span>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-[#EAF0F9] px-4 py-2.5 text-xs leading-5 text-[#17407F]">
|
||||||
|
关闭校验说明:判定为「是」的资料项全部归档后,系统将全部流程完毕信号推送变更主管部门专责,专责确认后点击关闭,系统记录变更关闭时间;「否 / 不涉及」由 AI 辅助判定一次,人工改判须填写理由并留痕。临时变更恢复完成后,恢复操作票与参数核对记录自动追加至本清单。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||||||
|
<Icon icon="material-symbols:download" class="size-16px" /> 下载变更关闭确认表
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||||||
|
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成变关闭确认表
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||||||
|
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||||||
|
</NButton>
|
||||||
|
</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)]">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||||
|
<Icon icon="lucide:users" class="size-14px" />
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||||||
|
</span>
|
||||||
|
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
|
||||||
|
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||||||
|
审批流程设置
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||||||
|
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span> -->
|
||||||
|
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||||||
|
提交 <Icon icon="formkit:right" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||||
|
</template>
|
||||||
|
<ApprovalDrawer />
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
<!-- 资料上传弹窗 -->
|
||||||
|
<NModal
|
||||||
|
:show="uploadDoc !== null"
|
||||||
|
preset="card"
|
||||||
|
:auto-focus="false"
|
||||||
|
:style="{ width: '550px', height: 'auto' }"
|
||||||
|
:segmented="{ content: true, footer: true }"
|
||||||
|
@update:show="(v) => !v && (uploadDoc = null)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||||||
|
<Icon icon="material-symbols:upload" class="size-20px" /> 资料上传归档 — {{ uploadDoc }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs font-medium text-slate-500">系统文档存储地址(受控目录)</div>
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border bg-slate-50 px-3 py-2">
|
||||||
|
<Icon icon="quill:folder-open" class="size-16px" />
|
||||||
|
<code class="min-w-0 flex-1 truncate text-xs text-slate-600">{{ uploadDocInfo?.storePath }}</code>
|
||||||
|
<NButton text type="primary" class="text-xs hover:underline" @click="openDir">打开目录</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs font-medium text-slate-500">方式一:从存储地址选择资料</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<p v-for="f in uploadDocInfo?.candidates" :key="f" @click="uploadSel = f"
|
||||||
|
class="flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-left text-xs transition cursor-pointer"
|
||||||
|
:class="uploadSel === f ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-[var(--theme-color)] text-white' : 'bg-white text-slate-600 hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]'"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>
|
||||||
|
<Icon icon="akar-icons:file" class="size-14px mr-1" /> {{ f }}
|
||||||
|
<Icon v-if="uploadSel === f" icon="ix:success" class="ml-auto size-16px" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs font-medium text-slate-500">方式二:拖拽本地文件到区域</div>
|
||||||
|
<button @click="uploadFiles"
|
||||||
|
class="flex h-20 w-full flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed px-3 text-xs transition"
|
||||||
|
:class="uploadSel?.startsWith('本地文件') ? 'border-[var(--theme-color)] bg-[#EAF0F9] text-[var(--theme-color)]' : 'border-slate-300 text-slate-400 hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]'"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>
|
||||||
|
<Icon icon="material-symbols:upload" class="size-24px" />
|
||||||
|
{{ uploadSel?.startsWith('本地文件') ? uploadSel : '将资料文件拖拽到此处,或点击选择本地文件' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<NButton @click="uploadDoc = null">取消</NButton>
|
||||||
|
<NButton type="primary" :disabled="!uploadSel" @click="uploadConfirm">
|
||||||
|
确认上传归档
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<!-- 变更培训内容 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { getColorWithOpacity } from "@/utils/common";
|
||||||
|
import ApprovalDrawer from './approvalDrawer.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// 抽屉类型
|
||||||
|
const approvalDrawerShow = ref<boolean>(false);
|
||||||
|
const trainText = ref<string>('');
|
||||||
|
const btnDisabled = ref<boolean>(false);
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
|
const downloadLoading = ref<boolean>(false);
|
||||||
|
const regenerateLoading = ref<boolean>(false);
|
||||||
|
const submitLoading = ref<boolean>(false);
|
||||||
|
const confirmed = ref<boolean>(false);
|
||||||
|
const approvers = ref<Record<string, string>>({
|
||||||
|
部门审核: "李主任(一车间主任)",
|
||||||
|
最终批准: "李主任(部门负责人 · 固定)",
|
||||||
|
});
|
||||||
|
const signers = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
const editing = ref<any>(null);
|
||||||
|
const acceptRows = ref([
|
||||||
|
{ id: 1, item: "反应转化率", basis: "转化率不低于 98%", standard: "转化率 ≥ 98%(连续 3 批化验合格)", method: "化验室逐批取样分析,出具检验报告", owner: "车间 · 张工艺", result: "待验收" },
|
||||||
|
{ id: 2, item: "单批反应时间 / 产能", basis: "反应时间缩短 50%、产能提升一倍", standard: "保温时间 4h±0.5h,产能提升 ≥ 80%", method: "DCS 批次记录统计,对比变更前 30 天均值", owner: "车间 · 李主任", result: "待验收" },
|
||||||
|
{ id: 3, item: "未知杂质含量", basis: "杂质水平受控、产品质量合格", standard: "杂质 ≤ 0.15%,产品质量指标全部合格", method: "每批化验跟踪,连续 5 批数据趋势评估", owner: "质量 · 王海燕", result: "待验收" },
|
||||||
|
{ id: 4, item: "温控与联锁有效性", basis: "温度窗口上调后保护层有效", standard: "温度波动 ≤ ±2℃,高报/联锁动作正确率 100%", method: "DCS 趋势核查 + 联锁测试记录复核", owner: "仪表 · 王仪表", result: "待验收" },
|
||||||
|
{ id: 5, item: "设备运行状态", basis: "负荷变化后设备长周期稳定", standard: "搅拌电流、釜壁温度无异常趋势,无泄漏", method: "设备巡检 + 振动/温度记录评估", owner: "设备 · 周设备", result: "待验收" },
|
||||||
|
]);
|
||||||
|
const acceptNote = ref("");
|
||||||
|
|
||||||
|
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
|
||||||
|
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
|
||||||
|
|
||||||
|
// 保存草稿
|
||||||
|
const saveDraft = async () => {
|
||||||
|
// if(changeApplyRules()){
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// saveLoading.value = true;
|
||||||
|
// const {error} = await applicationSaveApi(props.currentId, form.value);
|
||||||
|
// if(!error){
|
||||||
|
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
// }
|
||||||
|
// saveLoading.value = false;
|
||||||
|
}
|
||||||
|
// ---------- 提交 ----------
|
||||||
|
const submitApply = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const confirmTab = () => {
|
||||||
|
confirmed.value = !confirmed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({trainText})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||||||
|
<Icon icon="ix:success" class="size-18px text-emerald-500" />
|
||||||
|
验收评价(依据预期效果设定可量化验收标准)
|
||||||
|
</span>
|
||||||
|
<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>
|
||||||
|
<span class="ml-auto text-xs text-slate-400">申请阶段由 AI 依据「预期效果」生成量化标准与验收方式,可手动修改;投用运行后由车间和各专业按标准验收并给出评估结论</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
|
||||||
|
:style="{
|
||||||
|
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
|
||||||
|
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- <div><b :style="{color: themeStore.themeColor}">变更目的:</b>{{ form?.purposes?.join('、') || '提高生产效能、改善操作条件' }}</div>
|
||||||
|
<div><b :style="{color: themeStore.themeColor}">预期效果:</b>{{ form?.effect || '在保持转化率不低于98%的前提下,反应时间缩短50%,产能提升一倍' }}</div> -->
|
||||||
|
<div
|
||||||
|
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
|
||||||
|
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
|
||||||
|
>
|
||||||
|
<b :style="{color: themeStore.themeColor}">对应说明:</b>验收项目由预期效果逐条拆解设定,各项目对应的预期效果来源见表格「验收项目」列下方灰色小字(点击可修改);验收时须逐项对照预期效果判定达成程度。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<table class="w-full min-w-[900px] border-collapse text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-100 text-left text-slate-600">
|
||||||
|
<th class="w-8 px-3 py-2 font-medium">#</th>
|
||||||
|
<th class="w-56 px-3 py-2 font-medium">验收项目(含对应预期效果)</th>
|
||||||
|
<th class="px-3 py-2 font-medium">量化验收标准</th>
|
||||||
|
<th class="px-3 py-2 font-medium">验收方式(可执行)</th>
|
||||||
|
<th class="w-32 px-3 py-2 font-medium">责任单位/人</th>
|
||||||
|
<th class="px-3 py-2 font-medium">评估结论</th>
|
||||||
|
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, i) in acceptRows" :key="row.id" class="align-top">
|
||||||
|
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
|
||||||
|
<td class="border-t px-3 py-2.5">
|
||||||
|
<NInput
|
||||||
|
v-if="editing === `acc-${row.id}-item`"
|
||||||
|
class="text-xs"
|
||||||
|
v-model:value="row.item"
|
||||||
|
type="textarea"
|
||||||
|
autofocus
|
||||||
|
rows="2"
|
||||||
|
@blur="editing = null"
|
||||||
|
/>
|
||||||
|
<button v-else @click="editing = `acc-${row.id}-item`" title="点击修改"
|
||||||
|
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
|
||||||
|
>
|
||||||
|
{{ row.item }}
|
||||||
|
</button>
|
||||||
|
<NInput
|
||||||
|
v-if="editing === `acc-${row.id}-basis`"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
v-model:value="row.basis"
|
||||||
|
autofocus
|
||||||
|
@blur="editing = null"
|
||||||
|
/>
|
||||||
|
<button v-else @click="editing = `acc-${row.id}-basis`" title="本项目对应的预期效果来源,点击修改"
|
||||||
|
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
|
||||||
|
>
|
||||||
|
对应预期:{{ row.basis || '(点击填写对应预期效果)' }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td v-for="field in (['standard', 'method', 'owner'])" :key="field" class="border-t px-3 py-2.5">
|
||||||
|
<NInput
|
||||||
|
v-if="editing === `acc-${row.id}-${field}`"
|
||||||
|
class="text-xs"
|
||||||
|
v-model:value="row[field]"
|
||||||
|
type="textarea"
|
||||||
|
autofocus
|
||||||
|
rows="2"
|
||||||
|
@blur="editing = null"
|
||||||
|
/>
|
||||||
|
<button v-else @click="editing = `acc-${row.id}-${field}`" title="点击修改"
|
||||||
|
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
|
||||||
|
>
|
||||||
|
{{ row[field] }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="border-t px-3 py-2.5">
|
||||||
|
<div class="flex flex-nowrap items-center gap-1">
|
||||||
|
<NButton v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
@click="acceptRows = acceptRows.map((x) => (x.id === row.id ? { ...x, result: x.result === v ? '待验收' : v } : x))"
|
||||||
|
class="text-[11px]"
|
||||||
|
:type="row.result === v
|
||||||
|
? v === '未达成' ? 'error' : v === '基本达到' ? 'warning' : 'success'
|
||||||
|
: 'default'">
|
||||||
|
{{ v }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border-t px-2 py-2.5 text-center">
|
||||||
|
<NButton text type="error" title="删除本验收项" @click="acceptRows = acceptRows.filter((x) => x.id !== row.id)">
|
||||||
|
<Icon icon="iconamoon:trash" class="size-16px" />
|
||||||
|
</NButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<NButton ghost class="text-xs"
|
||||||
|
@click="acceptRows = [...acceptRows, { id: Date.now(), item: '新验收项目', basis: '', standard: '', method: '', owner: '', result: '待验收' }]">
|
||||||
|
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
|
||||||
|
</NButton>
|
||||||
|
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写:点击选择「完全达到 / 基本达到 / 未达成」,再次点击取消;行尾可删除验收项</span>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border">
|
||||||
|
<div class="border-b bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">综合验收结论(验收阶段由车间会同各专业评估填写)</div>
|
||||||
|
<div class="space-y-3 p-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v">
|
||||||
|
<NRadio :checked="acceptNote !== '' && acceptNote.startsWith(v)" :value="'v'" :label="v" @change="acceptNote = v + ':'"></NRadio>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-slate-400">「基本达到 / 未达成」须说明偏差原因与处置措施,并纳入变更关闭审核</span>
|
||||||
|
</div>
|
||||||
|
<NInput v-model:value="acceptNote" type="textarea" rows="2" placeholder="验收意见:总体评价、偏差说明、后续跟踪措施…(验收阶段填写)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
|
||||||
|
流程说明:投用运行满验收周期后,系统向车间(属地)与涉及专业推送验收任务;各责任单位按上表逐项给出评估结论并上传佐证(数据报表 / 化验报告 / 现场照片),全部完成后汇总至综合验收结论,结论与佐证资料一并纳入变更关闭校验,验收不通过时退回整改或评估恢复原状。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||||||
|
<Icon icon="material-symbols:download" class="size-16px" /> 下载验收评价
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||||||
|
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成验收评价
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||||||
|
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||||||
|
</NButton>
|
||||||
|
</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)]">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||||
|
<Icon icon="lucide:users" class="size-14px" />
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||||||
|
</span>
|
||||||
|
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
|
||||||
|
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||||||
|
审批流程设置
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||||||
|
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span> -->
|
||||||
|
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||||||
|
提交 <Icon icon="formkit:right" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||||
|
</template>
|
||||||
|
<ApprovalDrawer />
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<!-- 通用底部操作 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useChangeStore } from '@/store/modules/change';
|
||||||
|
import { applicationSaveApi, trainingSaveApi } from '@/service/api/change';
|
||||||
|
|
||||||
|
const changeStore = useChangeStore();
|
||||||
|
const { getStepsInfo, getFlowInfo } = storeToRefs(changeStore);
|
||||||
|
|
||||||
|
const props = defineProps([
|
||||||
|
'type',
|
||||||
|
'currentId',
|
||||||
|
'title',
|
||||||
|
'isAiOpen',
|
||||||
|
'currentForm'
|
||||||
|
])
|
||||||
|
const emit = defineEmits(['confirmTab']);
|
||||||
|
|
||||||
|
const approvalDrawerShow = ref<boolean>(false);
|
||||||
|
const btnDisabled = ref<boolean>(false);
|
||||||
|
const downloadLoading = ref<boolean>(false);
|
||||||
|
const regenerateLoading = ref<boolean>(false);
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
|
const submitLoading = ref<boolean>(false);
|
||||||
|
const confirmed = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 存储每个步骤选中的审核人 user_id
|
||||||
|
const approvers = ref<Record<number, number[]>>({})
|
||||||
|
const flowList = ref<any[]>([]);
|
||||||
|
// 审批链
|
||||||
|
const approvalFlowText = computed(() => {
|
||||||
|
if (!flowList.value || flowList.value.length === 0) return '暂无步骤'
|
||||||
|
// 按 step_order 排序
|
||||||
|
const sorted = [...flowList.value].sort((a, b) => a.step_order - b.step_order)
|
||||||
|
return sorted.map(item => item.name).join(' → ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空审批人
|
||||||
|
const clearApprovers = () => {
|
||||||
|
approvers.value = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开审批流程抽屉
|
||||||
|
const openApprovalDrawer = () => {
|
||||||
|
if(props.type==='form'){
|
||||||
|
if(!props.currentForm?.change_type || !props.currentForm?.change_level || !props.currentForm?.duration_type){
|
||||||
|
return window.$message?.warning('请先选择变更类型、变更等级、变更时限');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flowList.value = getStepsInfo.value;
|
||||||
|
if(Object.keys(approvers.value).length === 0){
|
||||||
|
flowList.value.forEach((item: any) => {
|
||||||
|
if (item.members && item.members.length > 0) {
|
||||||
|
// 优先选 is_sign=1 的成员 ,否则选第一个
|
||||||
|
const defaultMembers = item.members
|
||||||
|
.filter((m: any) => m.is_sign === 1)
|
||||||
|
.map((m: any) => m.user_id);
|
||||||
|
approvers.value[item.id] = defaultMembers.length > 0 ? defaultMembers : [item.members[0].user_id];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
approvalDrawerShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批链
|
||||||
|
const approvalChain = computed(() => {
|
||||||
|
let template = getFlowInfo.value;
|
||||||
|
if (!template || template.length === 0) return '暂无审批步骤'
|
||||||
|
const parts = template.map((step: any) => {
|
||||||
|
// 根据 countersign 决定显示名称
|
||||||
|
const displayName = step.countersign ? '专业会签' : step.name
|
||||||
|
const members = step.selectedMember
|
||||||
|
let memberText;
|
||||||
|
if (members && members.length > 0) {
|
||||||
|
memberText = members.map((m: any) => `${m.user_name}(${m.org_name})`).join('、');
|
||||||
|
} else {
|
||||||
|
memberText = '未指定';
|
||||||
|
}
|
||||||
|
return `${displayName}:${memberText}`;
|
||||||
|
})
|
||||||
|
return parts.join(' → ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下载
|
||||||
|
const download = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
// 重新生成
|
||||||
|
const regenerate = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
// 确认标签内容完毕
|
||||||
|
const confirmTab = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变更申请表表单必填判断
|
||||||
|
const changeApplyRules = () => {
|
||||||
|
let form = props.currentForm;
|
||||||
|
if(form.org_id === null){
|
||||||
|
window.$message?.warning('请选择申请部门');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.main_changes?.length===0){
|
||||||
|
window.$message?.warning('请添加主要变更内容');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.main_changes.some((str:string)=>str.trim()==='')){
|
||||||
|
window.$message?.warning('请填写主要变更内容');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.change_type===null){
|
||||||
|
window.$message?.warning('请选择变更类型');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.change_level===null){
|
||||||
|
window.$message?.warning('请选择变更等级');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.duration_type===null){
|
||||||
|
window.$message?.warning('请选择变更时限');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(form.duration_type === 1 && form.restore_deadline===null){
|
||||||
|
window.$message?.warning('请选择计划恢复时间');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 保存
|
||||||
|
const saveDraft = async () => {
|
||||||
|
if(props.type==='form'){
|
||||||
|
if(changeApplyRules()){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(props.type==='train'){
|
||||||
|
if(props.currentForm.content.trim() === ''){
|
||||||
|
return window.$message?.warning(`请输入变更培训内容`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveLoading.value = true;
|
||||||
|
btnDisabled.value = true;
|
||||||
|
// 保存变更申请表
|
||||||
|
if(props.type==='form'){
|
||||||
|
const {error} = await applicationSaveApi(props.currentId, props.currentForm);
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 保存变更培训内容
|
||||||
|
if(props.type==='train'){
|
||||||
|
const {error} = await trainingSaveApi(props.currentId, {content: props.currentForm.content});
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success(`变更培训内容已保存`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveLoading.value = false;
|
||||||
|
btnDisabled.value = false;
|
||||||
|
}
|
||||||
|
// 提交
|
||||||
|
const submitApply = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新选中审批人 user_id
|
||||||
|
const updateApprover = (stepId: number, usersId: number[]) => {
|
||||||
|
approvers.value[stepId] = usersId
|
||||||
|
}
|
||||||
|
// 匹配审批人
|
||||||
|
function matchApprovers(steps: any[], mapping: Record<number, number[]>) {
|
||||||
|
const result = [];
|
||||||
|
for (const [stepId, usersId] of Object.entries(mapping)) {
|
||||||
|
const step = steps.find(s => s.id === Number(stepId));
|
||||||
|
if (step) {
|
||||||
|
const members = step.members.filter((m: any) => usersId.includes(m.user_id));
|
||||||
|
if (members.length > 0) {
|
||||||
|
result.push({
|
||||||
|
countersign: step.countersign,
|
||||||
|
stepId: step.id,
|
||||||
|
name: step.name,
|
||||||
|
selectedMember: members
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// 完成设置
|
||||||
|
const finishApprover = async () => {
|
||||||
|
const matched = matchApprovers(flowList.value, approvers.value);
|
||||||
|
changeStore.updateFlowInfo(matched);
|
||||||
|
window.$message?.success("审批流程设置完成");
|
||||||
|
approvalDrawerShow.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({clearApprovers})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||||||
|
<Icon icon="material-symbols:download" class="size-16px" />
|
||||||
|
<span v-if="props.type==='form'">下载变更申请表</span>
|
||||||
|
<span v-if="props.type==='train'">下载变更培训内容</span>
|
||||||
|
</NButton>
|
||||||
|
<NButton v-if="props.isAiOpen" ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||||||
|
<Icon icon="tdesign:refresh" class="size-14px mr-1" />
|
||||||
|
<span v-if="props.type==='form'">重新生成变更申请表</span>
|
||||||
|
<span v-if="props.type==='train'">重新生成变更培训内容</span>
|
||||||
|
</NButton>
|
||||||
|
<NButton v-if="props.isAiOpen" ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||||||
|
<Icon icon="ix:success" class="size-16px mr-1" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||||||
|
</NButton>
|
||||||
|
</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)]">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||||
|
<Icon icon="lucide:users" class="size-14px" />
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
部门审核:{{ approvalChain }}
|
||||||
|
</span>
|
||||||
|
<NButton v-if="getStepsInfo && getStepsInfo.length" ghost size="small" type="primary" :disabled="btnDisabled" @click="openApprovalDrawer">
|
||||||
|
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||||||
|
审批流程设置
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||||||
|
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||||||
|
提交 <Icon icon="formkit:right" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||||
|
</template>
|
||||||
|
<div class="h-full space-y-4 overflow-y-auto">
|
||||||
|
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
|
||||||
|
<div class="rounded-lg border p-3" v-for="(item,index) in flowList" :key="item.id">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
|
<NTag type="info" size="small">{{Number(index)+1}}</NTag> {{item.name}} {{item.countersign ? '(专业会签)' : ''}}
|
||||||
|
</div>
|
||||||
|
<NSelect :value="approvers[item.id]" multiple :options="item.members" label-field="user_name" value-field="user_id"
|
||||||
|
@update:value="(v) => updateApprover(item.id, v)" />
|
||||||
|
</div>
|
||||||
|
<NButton type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
|
||||||
|
</div>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<!-- PSSR检查内容 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import ApprovalDrawer from './approvalDrawer.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
// 抽屉类型
|
||||||
|
const approvalDrawerShow = ref<boolean>(false);
|
||||||
|
const trainText = ref<string>('');
|
||||||
|
const btnDisabled = ref<boolean>(false);
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
|
const downloadLoading = ref<boolean>(false);
|
||||||
|
const regenerateLoading = ref<boolean>(false);
|
||||||
|
const submitLoading = ref<boolean>(false);
|
||||||
|
const confirmed = ref<boolean>(false);
|
||||||
|
const approvers = ref<Record<string, string>>({
|
||||||
|
部门审核: "李主任(一车间主任)",
|
||||||
|
最终批准: "李主任(部门负责人 · 固定)",
|
||||||
|
});
|
||||||
|
const signers = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
const editing = ref<any>(null);
|
||||||
|
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
||||||
|
{ g: "一、风险分析行动项落实", items: [
|
||||||
|
{ c: "联锁跳车电流设定值校核并留存记录", p: "投用前" },
|
||||||
|
{ c: "电缆与开关容量校核升级完成", p: "投用前" },
|
||||||
|
{ c: "操作人员培训完成并考核合格", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "二、现场设备安装检查确认", items: [
|
||||||
|
{ c: "电机安装就位,联轴器对中合格", p: "投用前" },
|
||||||
|
{ c: "地脚螺栓紧固,减振垫安装到位", p: "投用前" },
|
||||||
|
{ c: "电缆接线、防爆格兰头密封良好", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "三、保护措施确认", items: [
|
||||||
|
{ c: "联锁跳车设定值与新电机额定电流匹配", p: "投用前" },
|
||||||
|
{ c: "电机外壳接地、防护罩完好", p: "投用前" },
|
||||||
|
{ c: "紧急停机按钮功能试验正常", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "四、资料归档确认", items: [
|
||||||
|
{ c: "P&ID(图号 PID-R201-03)已更新并受控", p: "关闭前" },
|
||||||
|
{ c: "设备台账、备件清单已更新", p: "关闭前" },
|
||||||
|
{ c: "培训签到表、考核记录已归档", p: "关闭前" },
|
||||||
|
] },
|
||||||
|
]);
|
||||||
|
const pssrDone = ref<Record<string, { by: string; at: string }>>({});
|
||||||
|
const pssrNa = ref<Record<string, boolean>>({});
|
||||||
|
const pssrActive = computed(() => pssrGroups.value.filter((g:any) => !pssrNa.value[g.g]));
|
||||||
|
const pssrTotal = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.length, 0));
|
||||||
|
const pssrDoneCount = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.filter((it:any) => pssrDone.value[it.c]).length, 0));
|
||||||
|
const pssrAllDone = computed(() => pssrDoneCount.value === pssrTotal.value && (pssrTotal.value > 0 || (pssrGroups.value.length > 0 && pssrGroups.value.every((g) => !!pssrNa.value[g.g]))));
|
||||||
|
const pssrFileRef = ref<any>(null);
|
||||||
|
const pssrSheet = ref<any>(null);
|
||||||
|
const closeDocs = ref([
|
||||||
|
{ name: "PID 图纸(PID-R201-03,受控最新版)", cat: "图纸与规程类", scope: "是", archived: true },
|
||||||
|
{ name: "操作规程(含超温应急处置卡)", cat: "图纸与规程类", scope: "是", archived: false },
|
||||||
|
{ name: "应急预案修订", cat: "图纸与规程类", scope: "不涉及", archived: false },
|
||||||
|
{ name: "总图更新", cat: "图纸与规程类", scope: "否", archived: false },
|
||||||
|
{ name: "DCS 联锁台账(新报警 / 联锁值)", cat: "台账与制度类", scope: "是", archived: false },
|
||||||
|
{ name: "设备台账与备件清单", cat: "台账与制度类", scope: "是", archived: true },
|
||||||
|
{ name: "风险分析报告(HAZOP / 检查表法)", cat: "过程与结果文件类", scope: "是", archived: true },
|
||||||
|
{ name: "培训签到与考核记录", cat: "过程与结果文件类", scope: "是", archived: true },
|
||||||
|
{ name: "PSSR 检查记录(含现场照片)", cat: "过程与结果文件类", scope: "是", archived: false },
|
||||||
|
{ name: "验收报告与化验数据", cat: "过程与结果文件类", scope: "是", archived: false },
|
||||||
|
]);
|
||||||
|
const today = () => new Date().toISOString().slice(0, 10);
|
||||||
|
const onPssrFile = (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
const f = input.files?.[0]; input.value = "";
|
||||||
|
if (!f) return;
|
||||||
|
pssrSheet.value = f.name;
|
||||||
|
const at = today();
|
||||||
|
const n = { ...pssrDone.value };
|
||||||
|
pssrGroups.value.forEach((g:any) => { if (!pssrNa.value[g.g]) g.items.forEach((it:any) => { n[it.c] = { by: "纸质确认表上传", at }; }); });
|
||||||
|
pssrDone.value = n;
|
||||||
|
const archGroup = pssrGroups.value[pssrGroups.value.length - 1];
|
||||||
|
if (archGroup && !pssrNa.value[archGroup.g]) {
|
||||||
|
closeDocs.value = closeDocs.value.map((d:any) => (d.name.includes("PID") || d.name.includes("设备台账") || d.name.includes("培训签到") ? { ...d, archived: true } : d));
|
||||||
|
}
|
||||||
|
window.$message?.success(`已上传 PSSR 纸质确认表「${f.name}」:不做内容识别,上传即视为全部检查项完成确认(「不涉及」组除外),原件归档至变更档案`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
|
||||||
|
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
|
||||||
|
|
||||||
|
// 保存草稿
|
||||||
|
const saveDraft = async () => {
|
||||||
|
// if(changeApplyRules()){
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// saveLoading.value = true;
|
||||||
|
// const {error} = await applicationSaveApi(props.currentId, form.value);
|
||||||
|
// if(!error){
|
||||||
|
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
// }
|
||||||
|
// saveLoading.value = false;
|
||||||
|
}
|
||||||
|
// ---------- 提交 ----------
|
||||||
|
const submitApply = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const confirmTab = () => {
|
||||||
|
confirmed.value = !confirmed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({trainText})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||||||
|
<Icon icon="tabler:clipboard-check" class="size-16px text-violet-500" />
|
||||||
|
PSSR 投用前安全检查内容
|
||||||
|
</span>
|
||||||
|
<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="pssrAllDone ? 'success' : 'info'">
|
||||||
|
已完成 {{ pssrDoneCount }}/{{ pssrTotal }} 项
|
||||||
|
</NTag>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
|
||||||
|
<Icon icon="material-symbols:upload" class="size-14px" />
|
||||||
|
上传纸质确认表
|
||||||
|
</NButton>
|
||||||
|
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
|
||||||
|
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段:点击条目内容直接编辑,可增删项,完成时点互斥选择;审批通过后逐项点击确认(PC / 移动端均可),全部完成自动推送属地负责人</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="pssrSheet" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-xs text-emerald-700">
|
||||||
|
<Icon icon="ix:success" class="size-16px" />
|
||||||
|
已上传 PSSR 纸质确认表「{{ pssrSheet }}」:上传即视为全部检查项完成确认(未做内容识别),原件已归档至变更档案
|
||||||
|
</div>
|
||||||
|
<div v-if="pssrAllDone" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-xs text-emerald-700">
|
||||||
|
<Icon icon="ix:success" class="size-16px" />
|
||||||
|
全部 PSSR 检查项已确认完成,「具备投用条件」信息已自动推送属地负责人(车间 / 装置主任),可进行投用确认
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(g, gi) in pssrGroups" :key="g.g" class="border" :class="pssrNa[g.g] ? 'opacity-70' : ''" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<div class="flex items-center bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">
|
||||||
|
{{ g.g }}
|
||||||
|
<NTag v-if="pssrNa[g.g]" size="small" class="ml-2">不涉及</NTag>
|
||||||
|
<span v-else class="ml-2 text-xs font-normal text-slate-400">{{ g.items.filter((it) => pssrDone[it.c]).length }}/{{ g.items.length }} 已确认</span>
|
||||||
|
<NButton text type="primary" class="ml-auto text-xs font-normal hover:underline"
|
||||||
|
:title="pssrNa[g.g] ? '恢复本组检查清单' : '本次变更不涉及本组内容时点击标记,标记后不计入完成统计'"
|
||||||
|
@click="pssrNa = { ...pssrNa, [g.g]: !pssrNa[g.g] }">
|
||||||
|
{{ pssrNa[g.g] ? '恢复清单' : '标记不涉及' }}
|
||||||
|
</NButton>
|
||||||
|
<NButton text type="primary" v-if="!pssrNa[g.g]" class="ml-3 text-xs font-normal hover:underline"
|
||||||
|
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: [...x.items, { c: '新检查项(点击直接编辑内容)', p: '投用前' }] } : x)">
|
||||||
|
<Icon icon="ic:round-plus" class="size-14px" /> 添加检查项
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="pssrNa[g.g]" class="border-t bg-slate-50 px-3 py-3 text-xs text-slate-400">
|
||||||
|
本组内容经评估本次变更不涉及(如:无现场安装作业 / 不改变保护措施 / 无需归档资料),已不计入 PSSR 完成统计;如需恢复检查清单,点击右上角「恢复清单」。
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div v-for="(it, i) in g.items" :key="`${gi}-${i}`" class="flex flex-wrap items-center gap-3 border-t px-3 py-2.5 text-sm">
|
||||||
|
<span class="w-5 text-slate-400">{{ i + 1 }}</span>
|
||||||
|
<NInput
|
||||||
|
v-if="editing === `pssr-c-${gi}-${i}`"
|
||||||
|
class="flex-1"
|
||||||
|
v-model:value="it.c"
|
||||||
|
type="textarea"
|
||||||
|
autofocus
|
||||||
|
rows="2"
|
||||||
|
@blur="editing = null"
|
||||||
|
/>
|
||||||
|
<button v-else @click="editing = `pssr-c-${gi}-${i}`" title="点击编辑内容"
|
||||||
|
class="min-w-0 flex-1 rounded-md px-1 text-left text-slate-800 transition hover:bg-[#EAF0F9] bg-transparent"
|
||||||
|
>
|
||||||
|
{{ it.c }}
|
||||||
|
</button>
|
||||||
|
<span class="flex items-center gap-1 py-1">
|
||||||
|
<NRadio :checked="it.p === '投用前'" :value="'投用前'" label="投用前完成"
|
||||||
|
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '投用前' } : y) } : x)" />
|
||||||
|
<NRadio :checked="it.p === '关闭前'" :value="'关闭前'" label="关闭前完成"
|
||||||
|
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '关闭前' } : y) } : x)" />
|
||||||
|
</span>
|
||||||
|
<NButton text type="error" title="删除该项"
|
||||||
|
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.filter((_, yi) => yi !== i) } : x)"
|
||||||
|
>
|
||||||
|
<Icon icon="iconamoon:trash" class="size-16px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
|
||||||
|
完成时点说明:「投用前完成 / 关闭前完成」为互斥选择,点击即可切换。投用前完成的项未确认时属地负责人无法确认投用;关闭前完成的项不阻塞投用,但纳入关闭校验清单,未完成时变更主管部门专责无法确认关闭。
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
|
||||||
|
清单由 AI 依据变更类型与风险分析自动生成,专业人员审核后随审批发布;确认方式不作硬性要求——可在 PC 端直接点击确认,也可在移动端拍照上传作为佐证(系统自动记录确认人与日期);资料归档组确认后自动同步「变更关闭确认表」显示已归档。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||||||
|
<Icon icon="material-symbols:download" class="size-16px" /> 下载PSSR检查内容
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||||||
|
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成PSSR检查内容
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||||||
|
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||||||
|
</NButton>
|
||||||
|
</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)]">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||||
|
<Icon icon="lucide:users" class="size-14px" />
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||||||
|
</span>
|
||||||
|
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
|
||||||
|
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||||||
|
审批流程设置
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||||||
|
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span> -->
|
||||||
|
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||||||
|
提交 <Icon icon="formkit:right" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||||
|
</template>
|
||||||
|
<ApprovalDrawer />
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
<!-- 风险分析记录表 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import Ed from "./Ed.vue";
|
||||||
|
import Hazop from "./hazop.vue";
|
||||||
|
import Jsa from "./jsa.vue";
|
||||||
|
import Scl from "./scl.vue";
|
||||||
|
import RiskCheck from "./riskCheck.vue";
|
||||||
|
import ApprovalDrawer from './approvalDrawer.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
// 抽屉类型
|
||||||
|
const approvalDrawerShow = ref<boolean>(false);
|
||||||
|
const trainText = ref<string>('');
|
||||||
|
const btnDisabled = ref<boolean>(false);
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
|
const downloadLoading = ref<boolean>(false);
|
||||||
|
const regenerateLoading = ref<boolean>(false);
|
||||||
|
const submitLoading = ref<boolean>(false);
|
||||||
|
const confirmed = ref<boolean>(false);
|
||||||
|
const approvers = ref<Record<string, string>>({
|
||||||
|
部门审核: "李主任(一车间主任)",
|
||||||
|
最终批准: "李主任(部门负责人 · 固定)",
|
||||||
|
});
|
||||||
|
const signers = ref<Record<string, string>>({});
|
||||||
|
const pluginDone = ref<Record<string, number>>({});
|
||||||
|
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||||||
|
const riskRecords = ref<any>([]);
|
||||||
|
const pluginDrawer = ref<boolean>(false);
|
||||||
|
const currentPlugin = ref<string>('');
|
||||||
|
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
||||||
|
|
||||||
|
// 风险识别分析记录表
|
||||||
|
const TOOL_CARDS = [
|
||||||
|
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
||||||
|
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
||||||
|
{ key: "SCL" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
||||||
|
{ key: "RISK_CHECK" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
||||||
|
];
|
||||||
|
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
||||||
|
{ g: "一、风险分析行动项落实", items: [
|
||||||
|
{ c: "联锁跳车电流设定值校核并留存记录", p: "投用前" },
|
||||||
|
{ c: "电缆与开关容量校核升级完成", p: "投用前" },
|
||||||
|
{ c: "操作人员培训完成并考核合格", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "二、现场设备安装检查确认", items: [
|
||||||
|
{ c: "电机安装就位,联轴器对中合格", p: "投用前" },
|
||||||
|
{ c: "地脚螺栓紧固,减振垫安装到位", p: "投用前" },
|
||||||
|
{ c: "电缆接线、防爆格兰头密封良好", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "三、保护措施确认", items: [
|
||||||
|
{ c: "联锁跳车设定值与新电机额定电流匹配", p: "投用前" },
|
||||||
|
{ c: "电机外壳接地、防护罩完好", p: "投用前" },
|
||||||
|
{ c: "紧急停机按钮功能试验正常", p: "投用前" },
|
||||||
|
] },
|
||||||
|
{ g: "四、资料归档确认", items: [
|
||||||
|
{ c: "P&ID(图号 PID-R201-03)已更新并受控", p: "关闭前" },
|
||||||
|
{ c: "设备台账、备件清单已更新", p: "关闭前" },
|
||||||
|
{ c: "培训签到表、考核记录已归档", p: "关闭前" },
|
||||||
|
] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toPssrAll = () => {
|
||||||
|
const rest = riskRecords.value.map((x:any, i:any) => (x.pssr ? -1 : i)).filter((i:any) => i >= 0);
|
||||||
|
if (!rest.length) { window.$message?.info("所有建议措施均已转为 PSSR 行动项"); return; }
|
||||||
|
const items = rest.map((i:any) => ({ c: `${riskRecords.value[i].suggest}(来源:风险分析记录表 · ${riskRecords.value[i].item})`, p: "投用前" }));
|
||||||
|
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0 ? { ...g, items: [...g.items, ...items] } : g);
|
||||||
|
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
||||||
|
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lsLevel = (l: number | null, s: number | null) => {
|
||||||
|
const rr = (l ?? 0) * (s ?? 0);
|
||||||
|
return rr >= 10 ? "高" : rr >= 5 ? "中" : "低";
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiOrganize = async () => {
|
||||||
|
// let form = changePreRef.value?.form;
|
||||||
|
// analyzing.value = true;
|
||||||
|
// try {
|
||||||
|
// 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,
|
||||||
|
// inherent: lsLevel(r.l, r.s),
|
||||||
|
// existing: r.existing,
|
||||||
|
// suggest: r.suggest,
|
||||||
|
// residual: lsLevel(r.residual_l, r.residual_s),
|
||||||
|
// source: "AI 整理",
|
||||||
|
// }));
|
||||||
|
// riskRecords.value = [...riskRecords.value, ...gen];
|
||||||
|
// window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
|
||||||
|
// } catch (e: any) {
|
||||||
|
// aiFail(e);
|
||||||
|
// } finally {
|
||||||
|
// analyzing.value = false;
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPssr = (i: number) => {
|
||||||
|
const rec = riskRecords.value[i];
|
||||||
|
if (!rec || rec.pssr) return;
|
||||||
|
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0
|
||||||
|
? { ...g, items: [...g.items, { c: `${rec.suggest}(来源:风险分析记录表 · ${rec.item})`, p: "投用前" }] }
|
||||||
|
: g);
|
||||||
|
updRec(i, { pssr: true });
|
||||||
|
window.$message?.success("建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)");
|
||||||
|
};
|
||||||
|
|
||||||
|
const updRec = (i: number, patch: Partial<any>) => {
|
||||||
|
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
||||||
|
};
|
||||||
|
const onBack = () => {
|
||||||
|
pluginDrawer.value = false;
|
||||||
|
}
|
||||||
|
const exportAll = (items: any[], mode: "manual" | "ai") => {
|
||||||
|
riskRecords.value = [...riskRecords.value, ...items];
|
||||||
|
if (mode === "ai") {
|
||||||
|
pluginDrawer.value = false;
|
||||||
|
currentPlugin.value = '';
|
||||||
|
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
||||||
|
} else {
|
||||||
|
window.$message?.success(`已选入 ${items.length} 条风险记录,可继续挑选或返回查看记录表`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
|
||||||
|
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
|
||||||
|
|
||||||
|
// 保存草稿
|
||||||
|
const saveDraft = async () => {
|
||||||
|
// if(changeApplyRules()){
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// saveLoading.value = true;
|
||||||
|
// const {error} = await applicationSaveApi(props.currentId, form.value);
|
||||||
|
// if(!error){
|
||||||
|
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
// }
|
||||||
|
// saveLoading.value = false;
|
||||||
|
}
|
||||||
|
// ---------- 提交 ----------
|
||||||
|
const submitApply = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const confirmTab = () => {
|
||||||
|
confirmed.value = !confirmed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({trainText})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-4 scroll">
|
||||||
|
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||||||
|
<Icon icon="lucide:shield-alert" class="size-16px" />
|
||||||
|
风险识别工具(按变更等级匹配)
|
||||||
|
</span>
|
||||||
|
<!-- <NTag size="small" :type="form?.level === '重要' ? 'error' : 'default'">
|
||||||
|
{{ form?.level === '重要' ? '重要变更' : '一般变更' }}
|
||||||
|
</NTag>
|
||||||
|
<span class="text-xs text-slate-400">
|
||||||
|
{{ form?.level === '重要' ? '规则:HAZOP 必做,同时开展 JSA 与检查表法(SCL)' : '规则:JSA + 检查表法(SCL),涉及工艺安全边界的应升级 HAZOP' }};可在申请表「风险分析」栏手动重选
|
||||||
|
</span> -->
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 grid-cols-4 mt-3">
|
||||||
|
<div v-for="t in TOOL_CARDS" :key="t.key" :title="t.desc"
|
||||||
|
class="flex items-center gap-2 rounded-lg border px-3 py-2 transition"
|
||||||
|
:class="pluginDone[t.key] !== undefined ? 'border-[var(--theme-color)] bg-[var(--theme-color)]' : tools.includes(t.key) ? 'border-[var(--theme-color)] bg-white' : 'border-slate-200 bg-white'"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1.5 text-xs font-semibold"
|
||||||
|
:class="pluginDone[t.key] !== undefined ? 'text-white' : tools.includes(t.key) ? 'text-[var(--theme-color)]' : 'text-slate-500'"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>
|
||||||
|
<Icon icon="fluent:wand-24-regular" class="size-16px" />
|
||||||
|
{{ t.name }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5 text-[10px]" :class="pluginDone[t.key] !== undefined ? 'text-white/75' : 'text-slate-400'">
|
||||||
|
{{ pluginDone[t.key] !== undefined ? `已使用并分析了 ${pluginDone[t.key]} 条内容` : '未使用此工具分析' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NButton size="small" class="text-xs" :type="pluginDone[t.key] !== undefined ? 'default' : tools.includes(t.key) ? 'primary' : 'default'"
|
||||||
|
>
|
||||||
|
使用
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-medium text-slate-700">
|
||||||
|
<Icon icon="proicons:alert-triangle" class="size-16px text-amber-500" />
|
||||||
|
风险分析记录表
|
||||||
|
</span>
|
||||||
|
<span class="text-[11px] text-slate-400">共 {{ riskRecords.length }} 项</span>
|
||||||
|
<span class="ml-auto hidden text-[11px] text-slate-400 lg:block">插件结果「入表」或 AI 自动整理形成;单元格点击修改,风险等级点击切换</span>
|
||||||
|
<NButton ghost size="small" color="#7c3aed" class="text-xs" @click="toPssrAll">
|
||||||
|
<Icon icon="tabler:clipboard-check" class="size-14px" />
|
||||||
|
全部转 PSSR 行动项
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost size="small" type="primary" class="text-xs" @click="aiOrganize">
|
||||||
|
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||||
|
AI 自动整理
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400">
|
||||||
|
尚无风险记录:调用上方插件完成分析后逐条「入表」,或点击「AI 自动整理」形成记录表
|
||||||
|
</div>
|
||||||
|
<div v-else class="overflow-x-auto p-3">
|
||||||
|
<table class="w-full min-w-[960px] border-collapse text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
|
||||||
|
class="border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">{{ h }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(rec, i) in riskRecords" :key="i">
|
||||||
|
<td class="border px-1.5 py-1 align-top font-medium text-slate-700">
|
||||||
|
<Ed :v="rec.item" @update="(x) => updRec(i, { item: x })" />
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.scene" @update="(x) => updRec(i, { scene: x })" /></td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag v-if="rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.inherent }}
|
||||||
|
</NTag>
|
||||||
|
<NTag v-if="rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.inherent }}
|
||||||
|
</NTag>
|
||||||
|
<NTag v-if="rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.inherent }}
|
||||||
|
</NTag>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.existing" @update="(x) => updRec(i, { existing: x })" /></td>
|
||||||
|
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.suggest" @update="(x) => updRec(i, { suggest: x })" /></td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag v-if="rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.residual }}
|
||||||
|
</NTag>
|
||||||
|
<NTag v-if="rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.residual }}
|
||||||
|
</NTag>
|
||||||
|
<NTag v-if="rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||||||
|
{{ rec.residual }}
|
||||||
|
</NTag>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag size="small" type="info">{{ rec.source }}</NTag>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag v-if="rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }">已转 PSSR</NTag>
|
||||||
|
<NButton text size="tiny" color="#7c3aed" v-else class="hover:underline" @click="toPssr(i)">转 PSSR</NButton>
|
||||||
|
<NButton text type="error" size="tiny" class="mt-2" @click="riskRecords = riskRecords.filter((_, j) => j !== i)">删除</NButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||||||
|
<Icon icon="material-symbols:download" class="size-16px" /> 下载风险分析记录表
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||||||
|
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成风险分析记录表
|
||||||
|
</NButton>
|
||||||
|
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||||||
|
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||||||
|
</NButton>
|
||||||
|
</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)]">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||||
|
<Icon icon="lucide:users" class="size-14px" />
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||||||
|
</span>
|
||||||
|
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
|
||||||
|
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||||||
|
审批流程设置
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||||||
|
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span> -->
|
||||||
|
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||||||
|
提交 <Icon icon="formkit:right" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 插件抽屉 -->
|
||||||
|
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template v-if="currentPlugin === 'HAZOP'">
|
||||||
|
<Hazop @back="onBack" @export="exportAll" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentPlugin === 'JSA'">
|
||||||
|
<Jsa @back="onBack" @export="exportAll" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentPlugin === 'SCL'">
|
||||||
|
<Scl @back="onBack" @export="exportAll" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
||||||
|
<RiskCheck @back="onBack" />
|
||||||
|
</template>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
<!-- 审批流程设置 -->
|
||||||
|
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||||||
|
<NDrawerContent>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||||
|
</template>
|
||||||
|
<ApprovalDrawer />
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 393px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -609,9 +609,9 @@ onMounted(() => {
|
|||||||
<Icon icon="ep:setting" class="size-16px mr-1" />
|
<Icon icon="ep:setting" class="size-16px mr-1" />
|
||||||
变更等级设定(支持自定义)
|
变更等级设定(支持自定义)
|
||||||
</div>
|
</div>
|
||||||
<NButton size="small" type="primary" class="text-xs" @click="addLevel">
|
<!-- <NButton size="small" type="primary" class="text-xs" @click="addLevel">
|
||||||
<Icon icon="ic:round-plus" class="size-16px" />新增等级
|
<Icon icon="ic:round-plus" class="size-16px" />新增等级
|
||||||
</NButton>
|
</NButton> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="py-3 px-4">
|
<div class="py-3 px-4">
|
||||||
<div v-if="levelList.length" class="space-y-2">
|
<div v-if="levelList.length" class="space-y-2">
|
||||||
@@ -635,7 +635,7 @@ onMounted(() => {
|
|||||||
<NButton text type="primary" class="text-xs" @click="editLevel(item)">
|
<NButton text type="primary" class="text-xs" @click="editLevel(item)">
|
||||||
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
||||||
</NButton>
|
</NButton>
|
||||||
<NPopconfirm
|
<!-- <NPopconfirm
|
||||||
positive-text="确定"
|
positive-text="确定"
|
||||||
:positiveButtonProps="{ size: 'tiny' }"
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
:negativeButtonProps="{ size: 'tiny' }"
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
@@ -647,7 +647,7 @@ onMounted(() => {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
确定删除吗?
|
确定删除吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex flex-wrap mt-2">
|
<!-- <div class="flex flex-wrap mt-2">
|
||||||
@@ -1117,9 +1117,9 @@ onMounted(() => {
|
|||||||
{{levelInfo.label}}
|
{{levelInfo.label}}
|
||||||
</NTag>
|
</NTag>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="等级判定方式" path="remark">
|
<!-- <NFormItem label="等级判定方式" path="remark">
|
||||||
<NInput v-model:value="levelInfo.remark" placeholder="如:等级评分 ≥ 20 分" />
|
<NInput v-model:value="levelInfo.remark" placeholder="如:等级评分 ≥ 20 分" />
|
||||||
</NFormItem>
|
</NFormItem> -->
|
||||||
<!-- <NFormItem label="对应分析工具(多选)" path="tools">
|
<!-- <NFormItem label="对应分析工具(多选)" path="tools">
|
||||||
<NSelect v-model:value="levelInfo.tools" multiple :options="toolsOptions" placeholder="请选择分析工具" />
|
<NSelect v-model:value="levelInfo.tools" multiple :options="toolsOptions" placeholder="请选择分析工具" />
|
||||||
</NFormItem> -->
|
</NFormItem> -->
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ const saveTemplate = async (e: MouseEvent) => {
|
|||||||
const setStepCountersign = (target: any, v: boolean) => {
|
const setStepCountersign = (target: any, v: boolean) => {
|
||||||
currentTemplate.value.steps.forEach((s:any) => {
|
currentTemplate.value.steps.forEach((s:any) => {
|
||||||
s.countersign = s === target ? v : false;
|
s.countersign = s === target ? v : false;
|
||||||
if (!s.countersign) s.members.forEach((m:any) => { m.specialty = ""; m.is_sign = false; });
|
if (!s.countersign) s.members.forEach((m:any) => { m.specialty = ""; m.is_sign = 0; });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 当前打开人员选择器的步骤(步骤行「+」按钮切换)
|
// 当前打开人员选择器的步骤(步骤行「+」按钮切换)
|
||||||
|
|||||||
@@ -733,6 +733,7 @@ onMounted(() => {
|
|||||||
<NTreeSelect
|
<NTreeSelect
|
||||||
v-model:value="currentUser.dept_ids"
|
v-model:value="currentUser.dept_ids"
|
||||||
multiple
|
multiple
|
||||||
|
default-expand-all
|
||||||
label-field="label"
|
label-field="label"
|
||||||
key-field="id"
|
key-field="id"
|
||||||
:options="orgTree"
|
:options="orgTree"
|
||||||
|
|||||||
Reference in New Issue
Block a user