处置申请完成
This commit is contained in:
@@ -30,3 +30,20 @@ export function myTasksCloseMaterialApi(change_id:number,data: any) {
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 处置申请保存提交
|
||||
export function myTasksDisposalSubmitApi(change_id:number,data: any) {
|
||||
return request({
|
||||
url: `/api/changes/${change_id}/disposal`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 新增检查项
|
||||
export function myTasksAddItemApi(change_id:number,data: any) {
|
||||
return request({
|
||||
url: `/api/changes/${change_id}/pssr/item`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ const handleFileType = (type:string) => {
|
||||
}
|
||||
// 打开预览
|
||||
const preview = (file:any) => {
|
||||
console.log(file)
|
||||
previewFile.value = file;
|
||||
previewModal.value = true;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
|
||||
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor, formatTimestamp } from '@/utils/common';
|
||||
import { AiError, aiTypeJudge } from "@/service/api/ai";
|
||||
import { systemConfigApi } from '@/service/api/system';
|
||||
import { getChangeDetailApi } from '@/service/api/change';
|
||||
import { myTasksDisposalSubmitApi, myTasksAddItemApi } from '@/service/api/myTasks';
|
||||
|
||||
import PDF from 'pdf-vue3'
|
||||
import { VueOfficeDocx, VueOfficeExcel, VueOfficePptx } from 'vue3-office-preview'
|
||||
import 'vue3-office-preview/lib/style.css'
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
@@ -21,8 +26,6 @@ const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_r
|
||||
const currentInfo = ref<any>(null);
|
||||
// 当前变更id
|
||||
const currentId = ref<number>(0);
|
||||
// 当前变更状态
|
||||
const currentStatus = ref<string>('');
|
||||
// 是否开启 AI 辅助功能
|
||||
const isAiOn = ref<boolean>(false);
|
||||
// 变更等级选项
|
||||
@@ -33,6 +36,27 @@ const typeList = ref<any>([]);
|
||||
const level = computed(() => {
|
||||
return currentInfo.value?.apply_form?.change_level;
|
||||
});
|
||||
// 处置申请类型 unExecute:未实施;unUse:未投用;recover:申请恢复;delay:申请延期;transform:申请转为永久
|
||||
const disposalType = ref<string>('');
|
||||
// 情况说明
|
||||
const description = ref<string>('');
|
||||
// 影响判定
|
||||
const judge = ref<string>('');
|
||||
// 延期时间
|
||||
const delayDate = ref<number | null>(null);
|
||||
// 审批流程抽屉
|
||||
const approvalDrawer = ref<boolean>(false);
|
||||
// 存储每个步骤选中的审核人 user_id
|
||||
const approvers = ref<Record<number, number[]>>({})
|
||||
const flowList = ref<any[]>([]);
|
||||
const steps = 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 totalScore = computed(() => {
|
||||
return currentInfo.value?.apply_form?.level_score?.reduce((sum: number, item: any) => {
|
||||
@@ -45,14 +69,25 @@ const allMembers = computed(() => {
|
||||
return currentInfo.value?.approval_flow?.steps?.map((item: any) => item.members).flat();
|
||||
});
|
||||
// pssr 检查内容
|
||||
const pssrlist = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([
|
||||
{ g: "一、风险分析行动项落实", type:1, na_flag:0, items: [] },
|
||||
{ g: "二、现场设备安装检查确认", type:2, na_flag:0, items: [] },
|
||||
{ g: "三、保护措施确认", type:3, na_flag:0, items: [] },
|
||||
{ g: "四、资料归档确认", type:4, na_flag:0, items: [] },
|
||||
]);
|
||||
const pssrList = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([]);
|
||||
// pssr总条数
|
||||
const totalPssr = computed(() => {
|
||||
return pssrList.value.flatMap((group:any) => group.items || []).length;
|
||||
});
|
||||
// pssr已确认条数
|
||||
const confirmedPssr = computed(() => {
|
||||
return pssrList.value.flatMap((group:any) => group.items || []).filter((item:any) => item.confirm_flag).length;
|
||||
});
|
||||
// 资料关闭确认列表
|
||||
const closeConfirmList = ref<any>([]);
|
||||
// 验收评价
|
||||
const acceptList = ref<any>([]);
|
||||
|
||||
// 当前预览文件
|
||||
const previewFile = ref<any>(null);
|
||||
// 预览文件弹框
|
||||
const previewModal = ref<boolean>(false);
|
||||
|
||||
// 等级判断问题
|
||||
const questions = ref<any>([
|
||||
{
|
||||
@@ -130,75 +165,10 @@ const questions = ref<any>([
|
||||
]);
|
||||
|
||||
|
||||
const role = ref<string>("applicant");
|
||||
// 判定说明抽屉
|
||||
const describeDrawer = ref<boolean>(false);
|
||||
const levelDrawer = ref<boolean>(false);
|
||||
|
||||
// ai 标注抽屉
|
||||
const aiDrawer = ref<boolean>(false);
|
||||
const mandClosed = ref<boolean>(false);
|
||||
const confirmClose = () => {
|
||||
mandClosed.value = true;
|
||||
window.$message?.success('强制合规项已确认闭环,操作已留痕');
|
||||
}
|
||||
const conflictHide = ref<boolean>(false);
|
||||
// 关键参数对比(静态示例作 fallback,AI 审批辅助完成后回填)
|
||||
const paramDiff = ref([
|
||||
{ k: "反应温度", before: "80℃", after: "90℃", over: false, note: "仍在溶剂沸点与物料分解温度的安全裕度内" },
|
||||
{ k: "保温时间", before: "8 h", after: "4 h", over: false, note: "终点判断窗口变窄,须修订操作规程" },
|
||||
{ k: "DCS 温度高报警", before: "85℃", after: "95℃", over: true, note: "超出原报警裕度,须重新整定验证" },
|
||||
{ k: "联锁高高报", before: "90℃", after: "100℃", over: true, note: "接近设备设计温度上限,投用前须完成联锁测试" },
|
||||
{ k: "未知杂质含量", before: "0.05%", after: "0.12%", over: false, note: "结构未定,须纳入日常分析计划" },
|
||||
]);
|
||||
const aiRead = ref<boolean>(false);
|
||||
const roleName = ref<string>('张工艺');
|
||||
const suggs = ref([
|
||||
{ id: 1, text: "将未知杂质含量纳入日常分析计划(每周 1 次,连续 8 周)", st: "待定" },
|
||||
{ id: 2, text: "修订操作规程并补充超温应急处置卡后再投用", st: "待定" },
|
||||
]);
|
||||
const opinion = ref<string>("");
|
||||
|
||||
// 已阅 AI 建议
|
||||
const readSuggest = () => {
|
||||
aiRead.value = true;
|
||||
window.$message?.success(`${roleName} 已阅 AI 建议,时间与操作已写入审批留痕`);
|
||||
}
|
||||
// 采纳为行动项
|
||||
const acceptSuggest = (s: any) => {
|
||||
suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '已采纳' } : x));
|
||||
opinion.value = (opinion.value ? `${opinion.value}\n` : '') + `采纳 AI 建议:${s.text}`;
|
||||
window.$message?.success('已采纳,自动生成 PSSR 行动项(来源:AI 建议),并纳入审批意见');
|
||||
}
|
||||
// 不采纳 AI 建议
|
||||
const rejectSuggest = (s: any) => {
|
||||
suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '不采纳' } : x));
|
||||
window.$message?.info('不采纳理由「现有分析频次已覆盖风险」已记录留痕');
|
||||
}
|
||||
// 查看归档包
|
||||
const viewArchive = (name: string) => {
|
||||
window.$message?.info(`新窗口打开 ${name} 归档包,不影响当前审批`);
|
||||
}
|
||||
|
||||
// AI 审批辅助(抽屉打开时触发,异步任务轮询;失败保留静态示例,不阻塞页面)
|
||||
const aiAssistLoading = ref<boolean>(false);
|
||||
const aiAssistStep = ref<string>('');
|
||||
const aiAssistProgress = ref<number>(0);
|
||||
// 强制合规描述(静态示例作 fallback)
|
||||
const mandText = ref<string>('本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。');
|
||||
const mandItem = ref<string>('联锁测试记录须在投用前完成并上传归档');
|
||||
// 冲突提示
|
||||
const conflictText = ref<string>('涉及设备 R-101 关联变更 MOC-2026-0038「机封冲洗方案调整」正在审批中,请确认施工时序与隔离方案无冲突。');
|
||||
// 属地影响摘要
|
||||
const deptRows = ref([
|
||||
{ label: '涉及单元:', text: '一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。' },
|
||||
{ label: '现行规程影响:', text: '操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。' },
|
||||
{ label: '交接班提示:', text: '投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。' },
|
||||
]);
|
||||
// 参考信息(相似变更 / 历史经验)
|
||||
const similarText = ref<string>('相似变更 MOC-2025-0112「酯化反应温度上调」投用后运行正常,无异常记录');
|
||||
const experienceText = ref<string>('历史经验:温度类变更投用后首周超温报警占比约 38%,建议关注投用首周报警趋势');
|
||||
|
||||
const aiFail = (e: any) => {
|
||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||
else window.$message?.warning("AI 分析失败,可手动填写");
|
||||
@@ -252,8 +222,34 @@ const downloadFile = (type: string) => {
|
||||
}
|
||||
};
|
||||
// 变更附带资料预览
|
||||
const previewFile = () => {
|
||||
window.$message?.info('还没导入插件');
|
||||
const openPreview = (file:any) => {
|
||||
previewFile.value = file;
|
||||
previewModal.value = true;
|
||||
}
|
||||
// 判断文件类型
|
||||
const handleFileType = (type:string) => {
|
||||
if(type){
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||
const wordExtensions = ['docx']
|
||||
const excelExtensions = ['xlsx']
|
||||
const pptExtensions = ['pptx']
|
||||
if(imageExtensions.includes(type)){
|
||||
return 'image'
|
||||
}
|
||||
if(wordExtensions.includes(type)){
|
||||
return 'word'
|
||||
}
|
||||
if(excelExtensions.includes(type)){
|
||||
return 'excel'
|
||||
}
|
||||
if(pptExtensions.includes(type)){
|
||||
return 'ppt'
|
||||
}
|
||||
if(type==='pdf'){
|
||||
return 'pdf'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 催办
|
||||
@@ -262,6 +258,62 @@ const handleReminder = (m: any) => {
|
||||
window.$message?.success(`已催办 ${m.user_name}(系统消息 + 企业微信 + 短信),连续 2 次未响应将上报部门负责人`)
|
||||
}
|
||||
|
||||
// 更新选中审批人 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 memberList = step.members.filter((m: any) => usersId.includes(m.user_id));
|
||||
result.push({
|
||||
members: memberList
|
||||
})
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// 完成设置
|
||||
const finishApprover = async () => {
|
||||
const matched = matchApprovers(flowList.value, approvers.value);
|
||||
steps.value = matched;
|
||||
window.$message?.success("审批流程设置完成");
|
||||
approvalDrawer.value = false;
|
||||
}
|
||||
|
||||
// 添加检查项
|
||||
const addItem = (index: number) => {
|
||||
pssrList.value[index].items.push({
|
||||
id: 0,
|
||||
content: "",
|
||||
group_type: pssrList.value[index].type,
|
||||
na_flag: 0,
|
||||
phase: 1,
|
||||
confirm_flag: 0,
|
||||
})
|
||||
}
|
||||
// 保存检查项
|
||||
const saveItem = async (index: number, subIndex: number) => {
|
||||
const item = pssrList.value[index].items[subIndex];
|
||||
if(item?.content?.trim() === ''){
|
||||
return window.$message?.warning('请填写检查项内容');
|
||||
}
|
||||
const {data,error} = await myTasksAddItemApi(currentId.value,{
|
||||
content: item?.content,
|
||||
group_type: item?.group_type,
|
||||
na_flag: item?.na_flag,
|
||||
phase: item?.phase,
|
||||
confirm_flag: item?.confirm_flag,
|
||||
})
|
||||
if(!error){
|
||||
pssrList.value[index].items[subIndex].id = data.id
|
||||
window.$message?.success('保存成功');
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配评分依据
|
||||
const matchBasis = (items:any,score: number) => {
|
||||
return items.find((item:any) => item.score === score)?.label || '';
|
||||
@@ -272,36 +324,113 @@ const matchLabel = (list:any,v: number) => {
|
||||
return list.find((item:any) => item.value === v) || null;
|
||||
}
|
||||
|
||||
// 保存/提交处置申请
|
||||
const submitDisposal = async (type: string) => {
|
||||
let txt = ''
|
||||
// 类型
|
||||
let actType = 0;
|
||||
// 未实施
|
||||
if(disposalType.value==='unExecute'){
|
||||
actType = 4;
|
||||
txt = '请填写情况说明';
|
||||
}
|
||||
// 未投用
|
||||
if(disposalType.value==='unUse'){
|
||||
actType = 5;
|
||||
txt = '请填写情况说明';
|
||||
}
|
||||
// 申请恢复
|
||||
if(disposalType.value==='recover'){
|
||||
actType = 1;
|
||||
txt = '请填写申请恢复原因';
|
||||
}
|
||||
// 申请延期
|
||||
if(disposalType.value==='delay'){
|
||||
actType = 2;
|
||||
txt = '请填写变更延期原因';
|
||||
}
|
||||
// 申请转永久
|
||||
if(disposalType.value==='transform'){
|
||||
actType = 3;
|
||||
txt = '请填写申请转永久原因';
|
||||
}
|
||||
if(description.value?.trim() === ''){
|
||||
return window.$message?.warning(txt);
|
||||
}
|
||||
if(disposalType.value==='delay' && !delayDate.value){
|
||||
return window.$message?.warning('请选择变更延期时间');
|
||||
}
|
||||
// 去掉name
|
||||
const closeItems = closeConfirmList.value.map(({ name, ...rest }: any) => rest);
|
||||
// 转换格式
|
||||
const pssrItems = pssrList.value.flatMap((group: any) => group.items || []).map((item: any) => ({
|
||||
item_id: item.id,
|
||||
confirm_flag: item.confirm_flag
|
||||
}));
|
||||
// 去掉id, step_order, user_name, status
|
||||
const result = steps.value.map(({ members }:any) => ({
|
||||
members: members.map(({ id, step_order, user_name, status, ...rest }:any) => rest)
|
||||
}));
|
||||
const {error} = await myTasksDisposalSubmitApi(currentId.value,{
|
||||
action: type,
|
||||
act: actType,
|
||||
reason: description.value,
|
||||
risk_text: judge.value,
|
||||
new_deadline: formatTimestamp(delayDate.value,'yyyy-MM-dd'),
|
||||
close_docs: closeItems,
|
||||
pssr: pssrItems,
|
||||
approval_flow: {
|
||||
steps: result
|
||||
}
|
||||
})
|
||||
if(!error){
|
||||
if(type==='save'){
|
||||
window.$message?.success('保存成功');
|
||||
}else{
|
||||
window.$message?.success('提交成功');
|
||||
back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取变更详情
|
||||
const getDetail = async () => {
|
||||
loading.value = true;
|
||||
const {data,error} = await getChangeDetailApi(currentId.value)
|
||||
if(!error){
|
||||
currentInfo.value = data;
|
||||
console.log(data)
|
||||
|
||||
// ******审批流程********
|
||||
flowList.value = data?.approval_flow?.steps || [];
|
||||
// ******默认审批人********
|
||||
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];
|
||||
}
|
||||
})
|
||||
}
|
||||
const matched = matchApprovers(flowList.value, approvers.value);
|
||||
steps.value = matched;
|
||||
// ******解析 PSSR 检查内容********
|
||||
// 1. 按 group_type 分组
|
||||
const map = new Map();
|
||||
currentInfo.value?.pssr?.items?.forEach((item:any) => {
|
||||
const t = item.group_type;
|
||||
if (!map.has(t)) map.set(t, []);
|
||||
map.get(t).push(item);
|
||||
for (const item of currentInfo.value?.pssr?.items) {
|
||||
const type = item.group_type;
|
||||
if (!map.has(type)) {
|
||||
map.set(type, {
|
||||
g: item.group_name,
|
||||
type: item.group_type,
|
||||
na_flag: item.na_flag,
|
||||
items: []
|
||||
});
|
||||
|
||||
// 2. 生成目标数组
|
||||
const result = pssrlist.value.map(({ type, g }:any) => {
|
||||
const items = map.get(type) || [];
|
||||
// na_flag 取该组第一个元素的 na_flag,无数据时默认为 0
|
||||
const na_flag = items.length > 0 ? items[0].na_flag : 0;
|
||||
return {
|
||||
g,
|
||||
type,
|
||||
na_flag,
|
||||
items
|
||||
};
|
||||
});
|
||||
// 3. 过滤掉不需要的字段
|
||||
}
|
||||
map.get(type).items.push(item);
|
||||
}
|
||||
const result = Array.from(map.values());
|
||||
// 过滤掉不需要的字段
|
||||
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name'];
|
||||
const newResult = result.map((group:any) => ({
|
||||
...group,
|
||||
@@ -309,7 +438,7 @@ const getDetail = async () => {
|
||||
Object.fromEntries(Object.entries(item).filter(([k]) => !omitFields.includes(k)))
|
||||
)
|
||||
}));
|
||||
pssrlist.value = newResult;
|
||||
pssrList.value = newResult;
|
||||
|
||||
// ******解析 验收评价********
|
||||
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
|
||||
@@ -372,8 +501,51 @@ onMounted(async () => {
|
||||
currentId.value = Number(route.query.id);
|
||||
getConfig();
|
||||
}
|
||||
if(route.query.status){
|
||||
currentStatus.value = route.query.status.toString();
|
||||
if(route.query.type){
|
||||
disposalType.value = route.query.type.toString();
|
||||
// 未实施
|
||||
if(disposalType.value==='unExecute'){
|
||||
closeConfirmList.value = [
|
||||
{doc_id:1,name:'未实施情况说明(本单)',status:null},
|
||||
{doc_id:2,name:'原风险分析(归档备查,不重做)',status:null},
|
||||
{doc_id:3,name:'PSSR 检查项(标注「不涉及」)',status:null},
|
||||
{doc_id:4,name:'资料归档清单(转「不涉及」确认)',status:null},
|
||||
];
|
||||
}
|
||||
// 未投用
|
||||
if(disposalType.value==='unUse'){
|
||||
closeConfirmList.value = [
|
||||
{doc_id:1,name:'未投用情况说明(本单)',status:null},
|
||||
{doc_id:2,name:'原风险分析(归档备查,不重做)',status:null},
|
||||
{doc_id:3,name:'PSSR 检查项(标注「不涉及」)',status:null},
|
||||
{doc_id:4,name:'资料归档清单(转「不涉及」确认)',status:null},
|
||||
];
|
||||
}
|
||||
// 申请恢复
|
||||
if(disposalType.value==='recover'){
|
||||
closeConfirmList.value = [
|
||||
{doc_id:1,name:'恢复操作票(执行记录与照片)',status:null},
|
||||
{doc_id:2,name:'恢复后联锁 / 报警设定值回装核对记录',status:null},
|
||||
{doc_id:3,name:'更新的设备 / 安全阀台账',status:null},
|
||||
];
|
||||
}
|
||||
// 申请延期
|
||||
if(disposalType.value==='delay'){
|
||||
closeConfirmList.value = [
|
||||
{doc_id:1,name:'延期审批子单(随原单归档)',status:null},
|
||||
{doc_id:2,name:'原风险分析记录(延期期间持续有效确认)',status:null},
|
||||
{doc_id:3,name:'到期提醒与再评估记录',status:null},
|
||||
];
|
||||
}
|
||||
// 申请转永久
|
||||
if(disposalType.value==='transform'){
|
||||
closeConfirmList.value = [
|
||||
{doc_id:1,name:'转永久审批子单)',status:null},
|
||||
{doc_id:2,name:'补充风险分析报告(HAZOP / FMEA)',status:null},
|
||||
{doc_id:3,name:'规程 / 台账正式版',status:null},
|
||||
{doc_id:4,name:'原临时变更关闭记录',status:null},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -381,11 +553,11 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 本专业高风险 -->
|
||||
<!-- 处置申请详情 -->
|
||||
<NSpin :show="loading" size="small">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<!-- 顶部 -->
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
|
||||
class="flex flex-wrap items-center gap-3 px-3 py-2 mb-3 bg-[#fff]"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<NButton ghost type="primary" @click="back">
|
||||
@@ -393,19 +565,17 @@ onMounted(async () => {
|
||||
</NButton>
|
||||
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
|
||||
<NTag size="small" type="info" class="font-mono text-xs">编号:{{ currentInfo?.change_no }}</NTag>
|
||||
<!-- 审批中 -->
|
||||
<NButton
|
||||
v-if="currentStatus === 'APPROVING' && isAiOn"
|
||||
ghost
|
||||
size="small"
|
||||
type="primary"
|
||||
class="text-xs"
|
||||
@click="aiDrawer = !aiDrawer"
|
||||
>
|
||||
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 标注
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="space-y-3 scroll">
|
||||
<!-- 主体 -->
|
||||
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
|
||||
<!-- 左侧 -->
|
||||
<NGi span="24 s:24 m:24 l:16">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center flex-wrap pb-2.5 mb-3 border-b">
|
||||
<span class="text-slate-800 font-600 text-sm">变更全部信息</span>
|
||||
<p class="text-slate-400 text-xs ml-1 mt-1px">审批人视角 · 只读参考,本次操作以子单 / 单据形式关联,不回改原单</p>
|
||||
</div>
|
||||
<div class="space-y-3 left-scroll">
|
||||
<!-- 变更申请表 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p
|
||||
@@ -536,7 +706,7 @@ onMounted(async () => {
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<NButton text type="primary" class="text-xs" @click="previewFile">预览</NButton>
|
||||
<NButton text type="primary" class="text-xs" @click="openPreview(doc)">预览</NButton>
|
||||
<NButton text type="primary" class="text-xs" @click="downloadFile('single')">下载</NButton>
|
||||
</div>
|
||||
</td>
|
||||
@@ -598,7 +768,7 @@ onMounted(async () => {
|
||||
<span class="text-sm font-semibold text-slate-700">PSSR 检查内容</span>
|
||||
</div>
|
||||
<div class="space-y-2.5 border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||
<div v-for="(pssr,index) in pssrlist" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||
<div v-for="(pssr,index) in pssrList" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||
<div class="bg-slate-100 px-3 py-1.6 font-medium text-slate-600">{{pssr.g}}</div>
|
||||
<div v-for="item in pssr.items" :key="item.id" class="flex items-center border-t px-3 py-1.6">
|
||||
<p class="flex-1">{{item.content}}</p>
|
||||
@@ -710,6 +880,145 @@ onMounted(async () => {
|
||||
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
<!-- 右侧 -->
|
||||
<NGi span="24 s:24 m:24 l:8">
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<div class="flex items-center justify-between flex-wrap pb-2.5 mb-3 border-b">
|
||||
<span v-if="disposalType === 'unExecute'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">未实施情况说明表</span>
|
||||
<span v-if="disposalType === 'unUse'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">未投用情况说明表</span>
|
||||
<span v-if="disposalType === 'recover'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">变更恢复申请表</span>
|
||||
<span v-if="disposalType === 'delay'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">变更延期申请表</span>
|
||||
<span v-if="disposalType === 'transform'" class="text-slate-800 font-600 text-sm" :style="{ color: themeStore.themeColor }">临时转永久申请表</span>
|
||||
<NButton v-if="isAiOn" size="small" type="primary" round class="text-xs">AI辅助填报</NButton>
|
||||
</div>
|
||||
<div class="right-scroll">
|
||||
<!-- 未实施情况说明 -->
|
||||
<template v-if="disposalType === 'unExecute'">
|
||||
<!-- 情况说明 -->
|
||||
<div class="space-y-1 mt-1">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">情况说明<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写情况说明(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
<!-- 影响判定 -->
|
||||
<div class="space-y-1 mt-3">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">{{isAiOn?'影响判定(AI 辅助)':'影响判定'}}</p>
|
||||
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 未投用情况说明 -->
|
||||
<template v-if="disposalType === 'unUse'">
|
||||
<!-- 情况说明 -->
|
||||
<div class="space-y-1 mt-1">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">情况说明<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写情况说明(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
<!-- 影响判定 -->
|
||||
<div class="space-y-1 mt-3">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">{{isAiOn?'影响判定(AI 辅助)':'影响判定'}}</p>
|
||||
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 申请恢复 -->
|
||||
<template v-if="disposalType === 'recover'">
|
||||
<!-- 原因 -->
|
||||
<div class="space-y-1 mt-1">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">申请恢复原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写申请恢复原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
<!-- 风险分析 -->
|
||||
<div class="space-y-1 mt-3">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">恢复作业风险分析</p>
|
||||
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 申请延期 -->
|
||||
<template v-if="disposalType === 'delay'">
|
||||
<!-- 原因 -->
|
||||
<div class="space-y-1 mt-1">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">变更延期原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写变更延期原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
<div class="flex items-center flex-wrap gap-2 mt-3">
|
||||
<p class="flex items-center">延期至<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NDatePicker v-model:value="delayDate" type="date" size="small" class="w-120px" />
|
||||
<p class="text-xs text-slate-400">原到期 2026-08-20 · 至多延期 1 次</p>
|
||||
</div>
|
||||
<!-- 风险分析 -->
|
||||
<div class="space-y-1 mt-3">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">延期风险分析</p>
|
||||
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 申请转永久 -->
|
||||
<template v-if="disposalType === 'transform'">
|
||||
<!-- 原因 -->
|
||||
<div class="space-y-1 mt-1">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">申请转永久原因<span class="pl-1" :style="{color: themeStore.otherColor.error}">*</span></p>
|
||||
<NInput v-model:value="description" type="textarea" :autosize="{minRows: 3}" placeholder="填写申请转永久原因(可点击「AI 辅助填报」生成草稿后修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
<!-- 风险分析 -->
|
||||
<div class="space-y-1 mt-3">
|
||||
<p class="text-xs font-600 text-slate-600 flex items-center">风险分析缺口补充(按永久变更要求)</p>
|
||||
<NInput v-model:value="judge" type="textarea" :autosize="{minRows: 3}" placeholder="风险分析内容(AI 预分析草稿,可手动修改)…" class="w-full"></NInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 风险分析行动项落实 -->
|
||||
<div class="rounded-md mt-4 border">
|
||||
<div class="flex items-center gap-2 py-2 px-2 border-b">
|
||||
<span class="text-xs font-600 text-slate-600 flex items-center">风险分析行动项落实</span>
|
||||
<p class="text-xs text-slate-400">{{confirmedPssr}}/{{totalPssr}} 已确认</p>
|
||||
</div>
|
||||
<div class="p-2 space-y-2">
|
||||
<div v-for="(pssr,index) in pssrList" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||
<div class="bg-slate-100 px-2 py-1.5 font-medium text-xs text-slate-600 flex items-center justify-between">
|
||||
{{pssr.g}}
|
||||
<NButton text size="small" type="primary" class="text-xs" @click="addItem(index)">
|
||||
<Icon icon="ic:round-plus" class="size-12px" />添加检查项
|
||||
</NButton>
|
||||
</div>
|
||||
<div v-for="(item,subIndex) in pssr.items" :key="subIndex" class="flex items-center border-t px-2 py-1 gap-2">
|
||||
<NInput v-if="!item.id" v-model:value="item.content" size="small" type="textarea" :autosize="{minRows: 1}" placeholder="填写检查项内容" class="w-full text-xs"></NInput>
|
||||
<NButton v-if="!item.id" text type="success" @click="saveItem(index, subIndex)" title="保存检查项">
|
||||
<Icon icon="boxicons:save" class="size-16px" />
|
||||
</NButton>
|
||||
<NButton v-if="!item.id" text type="error" @click="pssrList[index].items.splice(subIndex, 1)" title="删除">
|
||||
<Icon icon="ep:delete" class="size-14px" />
|
||||
</NButton>
|
||||
<p v-if="item.id" class="flex-1 text-xs">{{item.content}}</p>
|
||||
<NButton v-if="item.confirm_flag" size="tiny" ghost type="success" class="text-xs" @click="item.confirm_flag = 0">已确认</NButton>
|
||||
<NButton v-else size="tiny" ghost type="warning" class="text-xs" @click="item.confirm_flag = 1">待确认</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资料关闭确认 -->
|
||||
<div class="rounded-md mt-4 border">
|
||||
<div class="flex items-center gap-2 py-2 px-2">
|
||||
<span class="text-xs font-600 text-slate-600 flex items-center">资料关闭确认</span>
|
||||
<p class="text-xs text-slate-400">提交后纳入资料关闭流程统一归档</p>
|
||||
</div>
|
||||
<div v-for="item in closeConfirmList" :key="item.doc_id" class="flex items-center gap-2 py-2 px-2 border-t">
|
||||
<p class="flex-1 text-xs flex items-center">{{item.name}}</p>
|
||||
<NButton :type="item.status===1?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 1">已归档</NButton>
|
||||
<NButton :type="item.status===2?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 2">待归档</NButton>
|
||||
<NButton :type="item.status===0?'primary':'default'" size="tiny" class="text-xs" @click="item.status = 0">不涉及</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<!-- 底部 -->
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-end gap-3 px-3 py-2 mt-3 bg-[#fff]"
|
||||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<NButton size="small" @click="approvalDrawer = true">审批流程设置</NButton>
|
||||
<NButton size="small" @click="submitDisposal('save')">保存</NButton>
|
||||
<NButton size="small" type="primary" @click="submitDisposal('submit')">提交</NButton>
|
||||
</div>
|
||||
<!-- 变更类型判定说明(右侧抽屉) -->
|
||||
<NDrawer v-model:show="describeDrawer" :width="576" placement="right">
|
||||
<NDrawerContent title="变更类型判定说明" closable>
|
||||
@@ -772,199 +1081,85 @@ onMounted(async () => {
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
<!-- AI 审批辅助(右侧抽屉查看) -->
|
||||
<NDrawer v-model:show="aiDrawer" :width="576" placement="right">
|
||||
<NDrawerContent closable>
|
||||
<!-- 审批流程设置 -->
|
||||
<NDrawer v-model:show="approvalDrawer" placement="right" width="28rem">
|
||||
<NDrawerContent>
|
||||
<template #header>
|
||||
<span class="flex items-center gap-1.5 text-sm font-bold text-slate-700">
|
||||
<Icon icon="lucide:sparkles" class="size-16px text-violet-500 mr-1" />
|
||||
AI 审批辅助(抽屉式,正文全窗口阅读)
|
||||
</span>
|
||||
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<!-- AI 分析进度(异步任务轮询) -->
|
||||
<div v-if="aiAssistLoading" class="rounded-lg border border-violet-200 bg-violet-50 px-3 py-2.5 text-xs text-violet-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="eos-icons:loading" class="size-14px" />
|
||||
<span>{{ aiAssistStep || 'AI 分析中…' }}({{ aiAssistProgress }}%)</span>
|
||||
<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>
|
||||
<NProgress type="line" :percentage="aiAssistProgress" :show-indicator="false" class="mt-1.5" processing />
|
||||
<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>
|
||||
<!-- 合规警告条(强制合规项,不可关闭;闭环后转绿) -->
|
||||
<div
|
||||
class="px-3 py-2.5 text-sm"
|
||||
:class="mandClosed ? 'text-[var(--success-color)] bg-[var(--success-bg-color)]' : 'text-[var(--warning-color)] bg-[var(--warning-bg-color)]'"
|
||||
:style="{
|
||||
borderRadius:themeStore.themeRadius+'px',
|
||||
'--success-color':themeStore.otherColor.success,
|
||||
'--success-bg-color':getColorWithOpacity(themeStore.otherColor.success,0.1),
|
||||
'--warning-color':themeStore.otherColor.warning,
|
||||
'--warning-bg-color':getColorWithOpacity(themeStore.otherColor.warning,0.1)
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="proicons:alert-triangle" class="size-16px" />
|
||||
<span class="font-semibold">强制合规</span>
|
||||
<span class="ml-auto shrink-0">
|
||||
<NTag v-if="mandClosed" size="small" type="success">
|
||||
<template #icon>
|
||||
<Icon icon="lucide:shield-check" class="size-12px" />
|
||||
</template>
|
||||
已闭环
|
||||
</NTag>
|
||||
<NTag v-else size="small" type="warning">未闭环</NTag>
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-relaxed">
|
||||
{{ mandText }}
|
||||
</p>
|
||||
<div v-if="!mandClosed" class="mt-1 text-right">
|
||||
<NButton text type="primary" class="text-xs" @click="confirmClose">确认闭环(留痕)</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更冲突提示(同设备在途变更,可收起) -->
|
||||
<div
|
||||
v-if="!conflictHide"
|
||||
class="bg-[var(--bg-color)] px-3 py-2.5 text-sm text-[var(--theme-color)]"
|
||||
:style="{
|
||||
borderRadius:themeStore.themeRadius+'px',
|
||||
'--theme-color':themeStore.themeColor,
|
||||
'--bg-color':getColorWithOpacity(themeStore.themeColor,0.1),
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:bell" class="size-14px" />
|
||||
<span class="font-semibold">冲突提示</span>
|
||||
<NButton text type="primary" class="ml-auto shrink-0 text-xs" @click="conflictHide = true">知道了</NButton>
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-relaxed">
|
||||
{{ conflictText }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 属地影响摘要(仅属地负责人视角) -->
|
||||
<div v-if="role === 'dept'" class="border" :style="{borderRadius:themeStore.themeRadius+'px'}">
|
||||
<div class="flex items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
|
||||
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
|
||||
<span class="text-sm font-semibold text-slate-700">属地影响摘要</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>
|
||||
</div>
|
||||
<div class="space-y-1.5 px-3 py-2.5 text-xs leading-relaxed text-slate-600">
|
||||
<p v-for="(r, i) in deptRows" :key="i"><b class="text-slate-700">{{ r.label }}</b>{{ r.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 变更前后关键参数对比(变化标蓝 / 超限标红) -->
|
||||
<div
|
||||
class="border"
|
||||
:style="{
|
||||
borderRadius:themeStore.themeRadius+'px',
|
||||
'--error-color':themeStore.otherColor.error,
|
||||
'--theme-color':themeStore.themeColor
|
||||
}"
|
||||
>
|
||||
<div class="border-b bg-slate-50/70 px-3 py-2 text-xs font-semibold text-slate-600">
|
||||
关键参数对比(AI 提取) · <span class="font-bold text-[var(--theme-color)]">蓝=变化</span> · <span class="font-bold text-[var(--error-color)]">红=超限</span>
|
||||
</div>
|
||||
<div v-for="d in paramDiff" :key="d.k" class="border-b px-3 py-2 last:border-b-0">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 text-sm">
|
||||
<span class="w-24 shrink-0 text-xs text-slate-500">{{ d.k }}</span>
|
||||
<span class="text-xs text-slate-400">{{ d.before }}</span>
|
||||
<span class="text-slate-300">→</span>
|
||||
<span
|
||||
class="text-sm font-semibold"
|
||||
:class="d.over ? 'text-[var(--error-color)]' : 'text-[var(--theme-color)]'"
|
||||
>{{ d.after }}</span>
|
||||
<NTag v-if="d.over" size="small" type="error">超限</NTag>
|
||||
</div>
|
||||
<div
|
||||
class="mt-0.5 text-[11px] leading-snug"
|
||||
:class="d.over ? 'text-[var(--error-color)]' : 'text-slate-400'"
|
||||
>{{ d.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AI 审批建议(版本冻结 · 建议分级 · 已阅留痕 · 无感审计) -->
|
||||
<div
|
||||
class="border"
|
||||
:style="{
|
||||
borderRadius:themeStore.themeRadius+'px',
|
||||
'--error-color':themeStore.otherColor.error,
|
||||
'--theme-color':themeStore.themeColor,
|
||||
'--warning-color':themeStore.otherColor.warning,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
|
||||
<Icon icon="lucide:sparkles" class="size-14px text-violet-500" />
|
||||
<span class="text-sm font-semibold text-slate-700">AI 审批建议</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">基于提交版本 V1 一次性分析 · 版本冻结</NTag>
|
||||
<div class="ml-auto">
|
||||
<NTag v-if="aiRead" size="small" type="success">✓ {{ roleName }} 已阅 AI 建议(已留痕)</NTag>
|
||||
<NButton v-else size="small" class="text-xs" @click="readSuggest">已阅 AI 建议</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 p-3">
|
||||
<!-- 强制合规项 -->
|
||||
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--error-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--error-color)]" /> 强制合规项(驳回前必须处理,否则无法通过)</div>
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-red-100 bg-red-50/50 px-3 py-2 text-sm">
|
||||
<span class="min-w-0 flex-1 text-slate-700">{{ mandItem }}</span>
|
||||
<NTag v-if="mandClosed" size="small" type="success">已闭环</NTag>
|
||||
<NTag v-else size="small" type="warning">未闭环</NTag>
|
||||
</div>
|
||||
<!-- 推荐改进项 -->
|
||||
<div>
|
||||
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--warning-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--warning-color)]" /> 推荐改进项(可采纳为行动项,可忽略但须填理由)</div>
|
||||
<div class="space-y-1.5">
|
||||
<div v-for="s in suggs" :key="s.id" class="rounded-lg border px-3 py-2 text-sm">
|
||||
<div class="leading-relaxed text-slate-700">{{ s.text }}</div>
|
||||
<div class="mt-1.5">
|
||||
<div v-if="s.st === '待定'" class="flex gap-3">
|
||||
<NButton text type="success" class="text-xs hover:underline"
|
||||
@click="acceptSuggest(s)">
|
||||
采纳为行动项
|
||||
</NButton>
|
||||
<NButton text class="text-xs hover:underline"
|
||||
@click="rejectSuggest(s)">
|
||||
已知悉,不采纳
|
||||
</NButton>
|
||||
</div>
|
||||
<NTag v-else-if="s.st === '已采纳'" size="small" type="success">已采纳 → 已生成 PSSR 行动项</NTag>
|
||||
<NTag v-else size="small" type="default">已知悉不采纳(理由已留痕)</NTag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 参考信息 -->
|
||||
<div>
|
||||
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--theme-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--theme-color)]" /> 参考信息(纯参考,无强制力)</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm">
|
||||
<span class="min-w-0 flex-1 text-slate-700">{{ similarText }}</span>
|
||||
<NButton text type="primary" class="hover:underline text-xs" @click="viewArchive('MOC-2025-0112')">查看归档包</NButton>
|
||||
</div>
|
||||
<div class="rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm text-slate-700">
|
||||
{{ experienceText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-400">
|
||||
查看、采纳、已阅等操作均由后台自动记录(无感审计),供内审追溯;AI 建议不强制采纳,决策主导权在审批人。驳回重新提交后,AI 将对新版本重新分析一次。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
<!-- 附件在线预览弹窗 -->
|
||||
<NModal
|
||||
v-model:show="previewModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '80%', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||||
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
|
||||
<span>上传人:{{ previewFile?.uploader_name }}</span>
|
||||
<span>上传时间:{{ previewFile?.created_at }}</span>
|
||||
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
|
||||
</div>
|
||||
<div v-if="previewFile" class="max-h-[70vh] overflow-x-auto rounded-lg border">
|
||||
<!-- 图片预览 -->
|
||||
<template v-if="handleFileType(previewFile?.file_type) === 'image'">
|
||||
<div class="flex items-center justify-center py-2">
|
||||
<NImage :src="previewFile?.file_url" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- Word 预览 -->
|
||||
<template v-if="handleFileType(previewFile?.file_type) === 'word'">
|
||||
<VueOfficeDocx :src="previewFile?.file_url" class="w-full items-center" />
|
||||
</template>
|
||||
<!-- Excel 预览 -->
|
||||
<template v-if="handleFileType(previewFile?.file_type) === 'excel'">
|
||||
<VueOfficeExcel :src="previewFile?.file_url" class="w-full items-center" />
|
||||
</template>
|
||||
<!-- Ppt 预览 -->
|
||||
<template v-if="handleFileType(previewFile?.file_type) === 'ppt'">
|
||||
<VueOfficePptx :src="previewFile?.file_url" class="w-full items-center" />
|
||||
</template>
|
||||
<!-- PDF 预览 -->
|
||||
<template v-if="handleFileType(previewFile?.file_type) === 'pdf'">
|
||||
<div class="h-[68vh]">
|
||||
<PDF :src="previewFile?.file_url" class="w-full h-full items-center" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</NSpin>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 210px);
|
||||
.left-scroll {
|
||||
height: calc(100vh - 307px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.right-scroll {
|
||||
height: calc(100vh - 315px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -246,7 +246,7 @@ defineExpose({clearApprovers, setRegenerateLoading})
|
||||
<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>
|
||||
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
|
||||
</div>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
@@ -64,29 +64,10 @@ const tabChange = (k: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const SUB_ORDERS: any[] = [];
|
||||
const subsOf = (id: string) => SUB_ORDERS.filter((s:any) => s.parentId === id);
|
||||
const pendingSubs = computed(() => subsOf(currentRow.value?.id).filter((s:any) => s.status === "审批中"));
|
||||
interface Opt { act: any; name: string; desc: string; disabled?: boolean; note?: string }
|
||||
const convPassed = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status === "已通过"));
|
||||
const convActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "permanent" && s.status !== "已驳回"));
|
||||
const extActive = computed(() => subsOf(currentRow.value?.id).some((s) => s.kind === "extend" && s.status === "审批中"));
|
||||
const opts = computed<Opt[]>(() => {
|
||||
const list: Opt[] = [
|
||||
{ act: "restore", name: "申请恢复", desc: "投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档" },
|
||||
];
|
||||
if (currentRow.value?.dur === "临时" && !convPassed.value) {
|
||||
list.push(
|
||||
{ act: "extend", name: currentRow.value?.extended ? "申请延期(已延期 1 次)" : "申请延期", desc: "临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次", disabled: !!currentRow.value?.extended || extActive.value, note: extActive.value ? "延期子单审批中" : currentRow.value?.extended ? "本变更已延期 1 次,不可再次延期" : "" },
|
||||
{ act: "permanent", name: "申请转为永久", desc: "临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭", disabled: convActive.value, note: convActive.value ? "转永久子单审批中 / 已通过" : "" },
|
||||
);
|
||||
// 处置申请跳转
|
||||
const jumpTo = (act: string) => {
|
||||
routerPushByKey("details_disposal",{ query: { id: currentRow.value?.change_id.toString(),type:act } });
|
||||
}
|
||||
list.push(
|
||||
{ act: "unimpl", name: "未实施情况说明", desc: "审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程" },
|
||||
{ act: "unuse", name: "未投用情况说明", desc: "变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程" },
|
||||
);
|
||||
return list;
|
||||
});
|
||||
|
||||
// 打开详情、修改重报
|
||||
const openDetail = (c: any,type:string = '') => {
|
||||
@@ -101,7 +82,7 @@ const openDetail = (c: any,type:string = '') => {
|
||||
// 打开资料关闭
|
||||
const openDocClose = (c: any) => {
|
||||
currentRow.value = c;
|
||||
// routerPushByKey("my_docclose",{ query: { id: c.change_id.toString() } });
|
||||
routerPushByKey("details_closedoc",{ query: { id: c.change_id.toString() } });
|
||||
};
|
||||
// 打开处置申请
|
||||
const openDisposal = (c: any,) => {
|
||||
@@ -384,7 +365,6 @@ onMounted(() => {
|
||||
<div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
|
||||
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</NButton>
|
||||
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
|
||||
<!-- <NButton class="text-xs" text type="primary" @click="openDisposal(c)">处置申请</NButton> -->
|
||||
<NButton class="text-xs" text type="error" @click="openWithdraw(c)">撤回</NButton>
|
||||
</div>
|
||||
<!-- 审批通过 -->
|
||||
@@ -493,39 +473,40 @@ onMounted(() => {
|
||||
v-model:show="disposalModal"
|
||||
preset="card"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '670px', height: 'auto' }"
|
||||
:style="{ width: '700px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: false }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="text-base font-semibold text-slate-800">处置申请 · {{ currentRow?.title }}</h3>
|
||||
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号:{{ currentRow?.id }}</span>
|
||||
<span class="shrink-0 border-slate-200 bg-slate-100 font-mono text-xs text-slate-600 px-1.5 py-0.5 border rounded-md">编号:{{ currentRow?.change_no }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="rounded-lg border bg-white px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<NTag v-if="currentRow?.level=== '重要'" type="error" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.level }}</NTag>
|
||||
<NTag v-if="currentRow?.dur=== '临时'" type="warning" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-else type="primary" size="small">{{ currentRow?.dur }}</NTag>
|
||||
<NTag v-if="currentRow?.status==='审批中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已批准'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='实施中'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待PSSR'" size="small" type="warning">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待验收'" size="small" type="success">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='待关闭'" size="small" type="error">{{currentRow?.status}}</NTag>
|
||||
<NTag v-if="currentRow?.status==='已关闭'" size="small" type="primary">{{currentRow?.status}}</NTag>
|
||||
<span v-if="currentRow?.applyTime" class="text-[11px] text-slate-400">申请 {{ currentRow?.applyTime }}</span>
|
||||
<span v-if="currentRow?.dur === '临时' && currentRow?.deadline" class="text-[11px] text-amber-600">
|
||||
到期 {{ currentRow?.deadline }}{{ (currentRow?.day ?? 0) < 0 ? ` · 已超期 ${-(currentRow?.day ?? 0)} 天` : currentRow?.day !== undefined ? ` · 剩 ${currentRow?.day} 天` : '' }}
|
||||
</span>
|
||||
<!-- 变更等级 -->
|
||||
<NTag
|
||||
size="small"
|
||||
:color="{
|
||||
color: tagBgColor(getChangeLevel(currentRow?.change_level)?.color),
|
||||
textColor: tagTextColor(getChangeLevel(currentRow?.change_level)?.color),
|
||||
borderColor: tagBorderColor(getChangeLevel(currentRow?.change_level)?.color),
|
||||
}"
|
||||
>{{getChangeLevel(currentRow?.change_level)?.label}}</NTag>
|
||||
<!-- 变更时限 -->
|
||||
<NTag v-if="currentRow?.duration_type===2" size="small" type="warning">临时</NTag>
|
||||
<NTag v-if="currentRow?.duration_type===1" size="small" type="primary">永久</NTag>
|
||||
<NTag v-if="currentRow?.urgent===1" size="small" type="error">紧急</NTag>
|
||||
<!-- 变更状态 -->
|
||||
<NTag v-if="currentRow?.status==='APPROVING'" size="small" type="warning">审批中</NTag>
|
||||
<NTag v-if="currentRow?.status==='APPROVED'" size="small" type="success">已通过</NTag>
|
||||
<span v-if="currentRow?.apply_time" class="text-[11px] text-slate-400">申请 {{ currentRow?.apply_time.slice(0, 16) }}</span>
|
||||
<!-- 超期/临期/延期 -->
|
||||
<NTag v-if="currentRow?.overdue_status===2" size="small" type="error">已超期 {{ currentRow?.overdue_days }} 天</NTag>
|
||||
<NTag v-if="currentRow?.overdue_status===1" size="small" type="warning">临期 · 剩 {{ currentRow?.overdue_days }} 天</NTag>
|
||||
<NTag v-if="currentRow?.extend_count" size="small">已延期 {{ currentRow?.extend_count }} 次</NTag>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-slate-400">{{ currentRow?.detail }}</div>
|
||||
</div>
|
||||
<!-- 进行中的子单提示 -->
|
||||
<div v-if="pendingSubs.length" class="rounded-lg border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-700">
|
||||
已有子单在审批中:{{ pendingSubs.map((s:any) => s.id).join('、') }} · 审批进度见「待我处理」列表行内展示
|
||||
</div>
|
||||
<!-- 处置选项 -->
|
||||
<div class="rounded-lg border bg-white mt-3">
|
||||
@@ -534,21 +515,68 @@ onMounted(() => {
|
||||
<span class="ml-2 text-[11px] font-normal text-slate-400">申请恢复所有变更可发起;申请延期 / 申请转为永久仅临时变更可用</span>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div v-for="o in opts" :key="o.act" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<!-- 未实施情况说明 -->
|
||||
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">{{ o.name }}</span>
|
||||
<NTag v-if="o.note" type="warning" size="small">{{ o.note }}</NTag>
|
||||
<span class="text-sm font-semibold text-slate-800">未实施情况说明</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ o.desc }}</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">审批通过后变更未实施:填写情况说明(未实施原因、现场维持原状确认),知会原审批人后直接转入关闭流程</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" :disabled="o.disabled">
|
||||
<NButton size="small" type="primary" @click="jumpTo('unExecute')">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
<!-- 未投用情况说明 -->
|
||||
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">未投用情况说明</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">变更已实施完成但未正式投用:填写情况说明(含现场状态与后续安排),知会原审批人后直接转入关闭流程</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" @click="jumpTo('unUse')">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
<!-- 申请恢复 -->
|
||||
<div class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">申请恢复</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">投用后申请恢复原状:生成恢复操作票(执行类单据,随单归档,无需另行审批),现场恢复并拍照确认后纳入归档</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" @click="jumpTo('recover')">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
<!-- 申请延期 -->
|
||||
<div v-if="currentRow?.duration_type===2" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">申请延期</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">临时变更到期前申请延期:子单走属地 + 主管部门简化审批,通过后回写原单到期时间 · 至多延期 1 次</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" @click="jumpTo('delay')">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
<!-- 申请转为永久 -->
|
||||
<div v-if="currentRow?.duration_type===2" class="flex flex-wrap items-center gap-3 px-4 py-3.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-slate-800">申请转为永久</span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-slate-400">临时运行一个验证周期效果稳定后转永久:按永久变更补齐风险评估与完整审批链,通过后原临时变更自动关闭</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" @click="jumpTo('transform')">
|
||||
发起<Icon icon="formkit:right" class="size-12px text-white ml-1" />
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</NModal>
|
||||
</NCard>
|
||||
|
||||
Reference in New Issue
Block a user