资料档案库完成、去审批页面添加
This commit is contained in:
@@ -228,6 +228,7 @@ const local: App.I18n.Schema = {
|
|||||||
details_closedoc: '资料关闭',
|
details_closedoc: '资料关闭',
|
||||||
details_index: '变更详情',
|
details_index: '变更详情',
|
||||||
details_disposal: '处置申请',
|
details_disposal: '处置申请',
|
||||||
|
details_approval: '审批详情',
|
||||||
changepassword: '修改密码',
|
changepassword: '修改密码',
|
||||||
home: '首页',
|
home: '首页',
|
||||||
my: '我的操作台',
|
my: '我的操作台',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
changeledger: () => import("@/views/changeLedger/index.vue"),
|
changeledger: () => import("@/views/changeLedger/index.vue"),
|
||||||
changepassword: () => import("@/views/changePassword/index.vue"),
|
changepassword: () => import("@/views/changePassword/index.vue"),
|
||||||
changestatistics: () => import("@/views/changeStatistics/index.vue"),
|
changestatistics: () => import("@/views/changeStatistics/index.vue"),
|
||||||
|
details_approval: () => import("@/views/details/approval/index.vue"),
|
||||||
details_closedoc: () => import("@/views/details/closeDoc/index.vue"),
|
details_closedoc: () => import("@/views/details/closeDoc/index.vue"),
|
||||||
details_disposal: () => import("@/views/details/disposal/index.vue"),
|
details_disposal: () => import("@/views/details/disposal/index.vue"),
|
||||||
details_index: () => import("@/views/details/index/index.vue"),
|
details_index: () => import("@/views/details/index/index.vue"),
|
||||||
|
|||||||
@@ -83,6 +83,17 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
hideInMenu: true
|
hideInMenu: true
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'details_approval',
|
||||||
|
path: '/details/approval',
|
||||||
|
component: 'view.details_approval',
|
||||||
|
meta: {
|
||||||
|
title: '审批详情',
|
||||||
|
i18nKey: 'route.details_approval',
|
||||||
|
constant: true,
|
||||||
|
hideInMenu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'details_closedoc',
|
name: 'details_closedoc',
|
||||||
path: '/details/closedoc',
|
path: '/details/closedoc',
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ const routeMap: RouteMap = {
|
|||||||
"changepassword": "/changepassword",
|
"changepassword": "/changepassword",
|
||||||
"changestatistics": "/changestatistics",
|
"changestatistics": "/changestatistics",
|
||||||
"details": "/details",
|
"details": "/details",
|
||||||
|
"details_approval": "/details/approval",
|
||||||
"details_closedoc": "/details/closedoc",
|
"details_closedoc": "/details/closedoc",
|
||||||
"details_disposal": "/details/disposal",
|
"details_disposal": "/details/disposal",
|
||||||
"details_index": "/details/index",
|
"details_index": "/details/index",
|
||||||
|
|||||||
+55
-4
@@ -1,16 +1,67 @@
|
|||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
|
|
||||||
// 资料库树状列表
|
// 资料库树状列表
|
||||||
export function docListApi() {
|
export function docTreeListApi() {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/knowledge/tree`,
|
url: `/api/knowledge/tree`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 历史版本列表
|
|
||||||
export function historyVersionListApi(docId:number) {
|
// 新增文件夹
|
||||||
|
export function addFolderApi(data: {name: string, parent_id: number, org_id: number}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/knowledge/${docId}/history`,
|
url: `/api/knowledge/folder`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 编辑文件夹
|
||||||
|
export function editFolderApi(id: number, data: {name: string}) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/folder/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 删除文件夹
|
||||||
|
export function deleteFolderApi(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/folder/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 资料列表
|
||||||
|
export function docListApi(params:{folder_id:number,name?:string}) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/files`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传资料
|
||||||
|
export function uploadDocApi(params: any) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/upload`,
|
||||||
|
method: 'post',
|
||||||
|
data: params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 更新资料
|
||||||
|
export function updateDocApi(id: number, params: any) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/${id}/update`,
|
||||||
|
method: 'post',
|
||||||
|
data: params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史版本列表
|
||||||
|
export function docHistoryListApi(doc_id:number) {
|
||||||
|
return request({
|
||||||
|
url: `/api/knowledge/${doc_id}/history`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -24,6 +24,7 @@ declare module "@elegant-router/types" {
|
|||||||
"changepassword": "/changepassword";
|
"changepassword": "/changepassword";
|
||||||
"changestatistics": "/changestatistics";
|
"changestatistics": "/changestatistics";
|
||||||
"details": "/details";
|
"details": "/details";
|
||||||
|
"details_approval": "/details/approval";
|
||||||
"details_closedoc": "/details/closedoc";
|
"details_closedoc": "/details/closedoc";
|
||||||
"details_disposal": "/details/disposal";
|
"details_disposal": "/details/disposal";
|
||||||
"details_index": "/details/index";
|
"details_index": "/details/index";
|
||||||
@@ -120,6 +121,7 @@ declare module "@elegant-router/types" {
|
|||||||
| "changeledger"
|
| "changeledger"
|
||||||
| "changepassword"
|
| "changepassword"
|
||||||
| "changestatistics"
|
| "changestatistics"
|
||||||
|
| "details_approval"
|
||||||
| "details_closedoc"
|
| "details_closedoc"
|
||||||
| "details_disposal"
|
| "details_disposal"
|
||||||
| "details_index"
|
| "details_index"
|
||||||
|
|||||||
@@ -0,0 +1,989 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
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 { AiError, aiTypeJudge } from "@/service/api/ai";
|
||||||
|
import { systemConfigApi } from '@/service/api/system';
|
||||||
|
import { getChangeDetailApi } from '@/service/api/change';
|
||||||
|
import previewComponent from '@/components/previewFile/index.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const { routerPushByKey } = useRouterPush();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
// 审批意见
|
||||||
|
const comment = ref<string>('');
|
||||||
|
// 返回键
|
||||||
|
const backKey = ref<string>('');
|
||||||
|
// 判定说明
|
||||||
|
const typeAiLoading = ref<boolean>(false);
|
||||||
|
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||||||
|
// 当前变更详情
|
||||||
|
const currentInfo = ref<any>(null);
|
||||||
|
// 当前变更id
|
||||||
|
const currentId = ref<number>(0);
|
||||||
|
// 当前变更状态
|
||||||
|
const currentStatus = ref<string>('');
|
||||||
|
// 是否开启 AI 辅助功能
|
||||||
|
const isAiOn = ref<boolean>(false);
|
||||||
|
// 变更等级选项
|
||||||
|
const levelList = ref<any>([]);
|
||||||
|
// 变更类型选项
|
||||||
|
const typeList = ref<any>([]);
|
||||||
|
// 变更等级
|
||||||
|
const level = computed(() => {
|
||||||
|
return currentInfo.value?.apply_form?.change_level;
|
||||||
|
});
|
||||||
|
// 总得分
|
||||||
|
const totalScore = computed(() => {
|
||||||
|
return currentInfo.value?.apply_form?.level_score?.reduce((sum: number, item: any) => {
|
||||||
|
const n = Number(item.score);
|
||||||
|
return sum + (Number.isNaN(n) ? 0 : n);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
// 当前审批所有人员
|
||||||
|
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 acceptList = ref<any>([]);
|
||||||
|
// 当前预览文件
|
||||||
|
const previewFile = ref<any>(null);
|
||||||
|
// 预览文件弹框
|
||||||
|
const previewModal = ref<boolean>(false);
|
||||||
|
// 等级判断问题
|
||||||
|
const questions = ref<any>([
|
||||||
|
{
|
||||||
|
question_id: 1,
|
||||||
|
name: "1. 设备影响",
|
||||||
|
note: "不合适的材质将会导致",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "对设备带来一定的损害及腐蚀", score: 1 },
|
||||||
|
{ label: "对设备带来中等的损害及腐蚀", score: 2 },
|
||||||
|
{ label: "对设备带来明显的损害及腐蚀", score: 3 },
|
||||||
|
{ label: "对设备带来重大的损害及腐蚀", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: 2,
|
||||||
|
name: "2. 财务影响",
|
||||||
|
note: "工艺设计错误将会导致",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "一定损失,a<1万人民币", score: 1 },
|
||||||
|
{ label: "中等损失,直接损失1万≤a<10万人民币", score: 2 },
|
||||||
|
{ label: "大量损失,直接损失10万≤a<100万人民币", score: 3 },
|
||||||
|
{ label: "巨额损失,直接损失100万人民币≤a", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: 3,
|
||||||
|
name: "3. 质量影响",
|
||||||
|
note: "",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "对质量带来一定的风险(非关键工席、非关键设备设施的变更)", score: 1 },
|
||||||
|
{ label: "对质量带来中等风险(B类原料替代、生产辅助设备变更)", score: 2 },
|
||||||
|
{ label: "对质量带来明显的风险(主要工艺参数、中间体生产工艺变更)", score: 3 },
|
||||||
|
{ label: "对质量有很高风险(主要工艺路线、关键工艺参数变更)", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: 4,
|
||||||
|
name: "4. 设计的复杂程度",
|
||||||
|
note: "工艺设计包含了",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "少量的简单系统(单个设备或单个部件)", score: 1 },
|
||||||
|
{ label: "大量的简单系统(多个单类设备或部件)", score: 2 },
|
||||||
|
{ label: "至少一个复杂系统(涉及多设备/工艺相应调整)", score: 3 },
|
||||||
|
{ label: "大量的复杂系统(需要装置配套改造调整)", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: 5,
|
||||||
|
name: "5. 工艺设计的成熟度",
|
||||||
|
note: "",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "被频繁使用,且被证明为专利方设计", score: 1 },
|
||||||
|
{ label: "被少量使用,或被证明为专利方设计", score: 2 },
|
||||||
|
{ label: "被相关工业设计方法或参考文献认可(非专利方)", score: 3 },
|
||||||
|
{ label: "新的,未发表的", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: 6,
|
||||||
|
name: "6. 工艺危害",
|
||||||
|
note: "物料性质和工艺条件",
|
||||||
|
score: 0,
|
||||||
|
options: [
|
||||||
|
{ label: "无危害(常压、-10℃≤温度<40℃、无毒、无腐蚀不燃)", score: 1 },
|
||||||
|
{ label: "可燃/负压/低正压(0~0.3MPa)、40℃≤温度<80℃", score: 2 },
|
||||||
|
{ label: "易燃/中压(0.3~1.0MPa)、80℃≤温度<150℃", score: 3 },
|
||||||
|
{ label: "极度危害/高压(>1.0MPa)、温度≥150℃", score: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const 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 分析失败,可手动填写");
|
||||||
|
};
|
||||||
|
// 打开变更类型抽屉
|
||||||
|
const openTypeSheet = async () => {
|
||||||
|
typeAi.value = null;
|
||||||
|
describeDrawer.value = true;
|
||||||
|
if (typeAiLoading.value) return;
|
||||||
|
typeAiLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await aiTypeJudge(currentInfo.value?.precheck?.description, (currentInfo.value?.precheck?.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
||||||
|
typeAi.value = {
|
||||||
|
type: res?.type ?? "",
|
||||||
|
confidence: res?.confidence ?? 0,
|
||||||
|
conclusion: res?.conclusion ?? "",
|
||||||
|
hit_rules: res?.hit_rules ?? [],
|
||||||
|
exclude_rules: res?.exclude_rules ?? [],
|
||||||
|
knowledge_refs: res?.knowledge_refs ?? [],
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
|
typeAiLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const back = () => {
|
||||||
|
routerPushByKey(backKey.value as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const exportFile = (type: string) => {
|
||||||
|
window.$message?.info('还差接口')
|
||||||
|
if(type === 'change'){
|
||||||
|
window.$message?.success('导出变更申请表成功');
|
||||||
|
}
|
||||||
|
if(type === 'risk'){
|
||||||
|
window.$message?.success('导出风险检查行动项表成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 变更附带资料下载/一键下载
|
||||||
|
const downloadFile = (type: string) => {
|
||||||
|
window.$message?.info('还差接口')
|
||||||
|
if(type === 'all'){
|
||||||
|
window.$message?.success('打包下载全部附件');
|
||||||
|
}else{
|
||||||
|
window.$message?.success('下载当前附件成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 变更附带资料预览
|
||||||
|
const openPreview = (file:any) => {
|
||||||
|
previewFile.value = file;
|
||||||
|
previewModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 催办
|
||||||
|
const handleReminder = (m: any) => {
|
||||||
|
window.$message?.info('还差接口')
|
||||||
|
window.$message?.success(`已催办 ${m.user_name}(系统消息 + 企业微信 + 短信),连续 2 次未响应将上报部门负责人`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配评分依据
|
||||||
|
const matchBasis = (items:any,score: number) => {
|
||||||
|
return items.find((item:any) => item.score === score)?.label || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配label
|
||||||
|
const matchLabel = (list:any,v: number) => {
|
||||||
|
return list.find((item:any) => item.value === v) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取变更详情
|
||||||
|
const getDetail = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const {data,error} = await getChangeDetailApi(currentId.value)
|
||||||
|
if(!error){
|
||||||
|
currentInfo.value = data;
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
// ******解析 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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. 过滤掉不需要的字段
|
||||||
|
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name'];
|
||||||
|
const newResult = result.map((group:any) => ({
|
||||||
|
...group,
|
||||||
|
items: group.items.map((item:any) =>
|
||||||
|
Object.fromEntries(Object.entries(item).filter(([k]) => !omitFields.includes(k)))
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
pssrlist.value = newResult;
|
||||||
|
|
||||||
|
// ******解析 验收评价********
|
||||||
|
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
|
||||||
|
const result2 = currentInfo.value?.accept_form?.items.map((item:any) => omit(item, [
|
||||||
|
'created_at', 'updated_at', 'change_id'
|
||||||
|
]));
|
||||||
|
acceptList.value = result2 || [];
|
||||||
|
|
||||||
|
// ******解析 等级判断********
|
||||||
|
// 建立映射:question_id -> score
|
||||||
|
const scoreMap = new Map(
|
||||||
|
currentInfo.value?.apply_form?.level_score?.map((item:any) => [item.question_id, item.score])
|
||||||
|
);
|
||||||
|
// 遍历第一个数组,匹配到就替换 score
|
||||||
|
questions.value.forEach((item:any) => {
|
||||||
|
if (scoreMap.has(item.question_id)) {
|
||||||
|
item.score = scoreMap.get(item.question_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
// 获取系统配置
|
||||||
|
const getConfig = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const {data,error} = await systemConfigApi();
|
||||||
|
if(!error){
|
||||||
|
// 解析 AI 辅助功能
|
||||||
|
const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist');
|
||||||
|
if (aiAssistGroup) {
|
||||||
|
const target = aiAssistGroup?.items?.find((item:any) => item.item_key === 'enabled');
|
||||||
|
if (target) {
|
||||||
|
isAiOn.value = target.value==1?true:false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 解析 变更类型设置
|
||||||
|
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
|
||||||
|
if (changeTypeGroup) {
|
||||||
|
typeList.value = changeTypeGroup.items.map((item:any) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 解析 变更等级设置
|
||||||
|
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
|
||||||
|
if (changeLevelGroup) {
|
||||||
|
levelList.value = changeLevelGroup.items.map((item:any) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
getDetail();
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(route.query){
|
||||||
|
if(route.query.key){
|
||||||
|
backKey.value = route.query.key.toString();
|
||||||
|
}
|
||||||
|
if(route.query.id){
|
||||||
|
currentId.value = Number(route.query.id);
|
||||||
|
getConfig();
|
||||||
|
}
|
||||||
|
if(route.query.status){
|
||||||
|
currentStatus.value = route.query.status.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||||
|
>
|
||||||
|
<NButton ghost type="primary" @click="back">
|
||||||
|
<Icon icon="ep:back" class="size-16px mr-1" />返回
|
||||||
|
</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">
|
||||||
|
<!-- 变更申请表 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p
|
||||||
|
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>变更申请表</p>
|
||||||
|
<NButton ghost size="small" class="text-xs" @click="exportFile('change')"><Icon icon="material-symbols:download" class="size-16px" />导出变更申请表</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border border-slate-200"
|
||||||
|
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||||||
|
>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更发起人</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.applicant_name }}</div>
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">申请单位</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.org_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">发起日期</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.created_at?.slice(0,10) }}</div>
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">计划投用日期</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.plan_use_date?.slice(0,10) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更时限</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.duration_type === 1 ? '永久变更' : '临时变更' }}</div>
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">紧急</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.urgent === 1 ? '是' : '否' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更等级</p>
|
||||||
|
<div class="flex items-center gap-2 px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
<NRadioGroup v-model:value="level">
|
||||||
|
<NRadio v-for="item in levelList" :key="item.value" :value="item.value" :disabled="true">{{item.label}}</NRadio>
|
||||||
|
</NRadioGroup>
|
||||||
|
<NTag type="error" size="small">合计 {{totalScore}} 分</NTag>
|
||||||
|
<NButton ghost type="warning" size="small" round class="text-xs" @click="levelDrawer = true">
|
||||||
|
<Icon icon="lucide:eye" class="size-12px mr-1" />查看等级判定
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更类别</p>
|
||||||
|
<div class="flex items-center gap-2 px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{matchLabel(typeList,currentInfo?.apply_form?.change_type)?.label}}
|
||||||
|
<NButton v-if="isAiOn" ghost type="primary" size="small" round class="text-xs" @click="openTypeSheet">
|
||||||
|
<Icon icon="lucide:sparkles" class="size-12px mr-1" />判定说明
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更目的</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.purposes?.join('、')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">预期效果</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.effect}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">主要变更</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
<p v-for="(main,mIndex) in currentInfo?.apply_form?.main_changes" :key="mIndex">{{Number(mIndex)+1}}.{{main}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">连带变更</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
<p v-for="(related,rIndex) in currentInfo?.apply_form?.related_changes" :key="rIndex">{{Number(rIndex)+1}}.{{related}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">风险评估方法</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.risk_tools?.join('、')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">所需材料</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.materials}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid border-b grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">需更新资料</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.update_docs?.join('、')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-[100px_1fr]">
|
||||||
|
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">涉及专业</p>
|
||||||
|
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||||||
|
{{currentInfo?.apply_form?.disciplines?.join('、')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 变更附带资料 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p
|
||||||
|
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>变更附带资料</p>
|
||||||
|
<NButton ghost size="small" class="text-xs" @click="downloadFile('all')"><Icon icon="material-symbols:download" class="size-16px" />一键下载</NButton>
|
||||||
|
</div>
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-60px !text-center">序号</th>
|
||||||
|
<th>文件名</th>
|
||||||
|
<th class="w-100px !text-center">类型</th>
|
||||||
|
<th class="w-100px !text-center">文件大小</th>
|
||||||
|
<th class="w-120px !text-center">上传时间</th>
|
||||||
|
<th class="w-120px !text-center">上传阶段</th>
|
||||||
|
<th class="w-90px !text-center">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(doc,dIndex) in currentInfo?.apply_form?.attachments" :key="dIndex">
|
||||||
|
<td class="text-center">1</td>
|
||||||
|
<td>{{doc.file_name}}</td>
|
||||||
|
<td class="text-center text-xs">{{doc?.file_type}}</td>
|
||||||
|
<td class="text-center text-xs">{{doc?.file_size}}</td>
|
||||||
|
<td class="text-center text-xs">{{doc?.created_at?.slice(0,16)}}</td>
|
||||||
|
<td class="text-center text-xs">
|
||||||
|
<span v-if="doc?.step_no===1">变更申请表</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<NButton text type="primary" class="text-xs" @click="openPreview(doc)">预览</NButton>
|
||||||
|
<NButton text type="primary" class="text-xs" @click="downloadFile('single')">下载</NButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
|
||||||
|
<!-- 风险分析记录表 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p
|
||||||
|
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>风险分析记录表</p>
|
||||||
|
<NButton ghost size="small" class="text-xs" @click="exportFile('risk')"><Icon icon="material-symbols:download" class="size-16px" />导出风险分析记录表</NButton>
|
||||||
|
</div>
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-60px !text-center">序号</th>
|
||||||
|
<th>风险项</th>
|
||||||
|
<th>场景描述</th>
|
||||||
|
<th class="w-80px !text-center">固有风险</th>
|
||||||
|
<th>现有措施</th>
|
||||||
|
<th>建议措施</th>
|
||||||
|
<th class="w-80px !text-center">剩余风险</th>
|
||||||
|
<th class="w-100px !text-center">来源</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(risk,index) in currentInfo?.risk_records?.records" :key="index">
|
||||||
|
<td class="text-center">{{Number(index)+1}}</td>
|
||||||
|
<td>{{risk.item}}</td>
|
||||||
|
<td>{{risk.scene}}</td>
|
||||||
|
<td class="text-center">{{risk.inherent_level}}</td>
|
||||||
|
<td>{{risk.safeguards}}</td>
|
||||||
|
<td>{{risk.suggestions}}</td>
|
||||||
|
<td class="text-center">{{risk.residual_level}}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<NTag size="small" type="info" class="text-11px">{{ risk.source==='RISK'?'风险预分析':risk.source==='RISK_CHECK'?'风险检查': risk.source }}</NTag>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
|
||||||
|
<!-- 变更培训内容 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1.5">
|
||||||
|
<Icon icon="lucide:graduation-cap" class="size-16px text-violet-500" />
|
||||||
|
<span class="text-sm font-semibold text-slate-700">变更培训内容</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2.5 border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||||
|
<p>{{currentInfo?.train?.content}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- PSSR 检查内容 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1.5">
|
||||||
|
<Icon icon="lucide:list-checks" class="size-16px text-violet-500" />
|
||||||
|
<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 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>
|
||||||
|
<NTag v-if="item.phase===1" size="small" type="info">投用前完成</NTag>
|
||||||
|
<NTag v-else size="small" type="info">关闭前完成</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 验收评价 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1.5">
|
||||||
|
<Icon icon="ix:success" class="size-16px text-emerald-500" />
|
||||||
|
<span class="text-sm font-semibold text-slate-700">验收评价</span>
|
||||||
|
</div>
|
||||||
|
<div class="border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-56">验收项目</th>
|
||||||
|
<th>量化验收标准</th>
|
||||||
|
<th>验收方式</th>
|
||||||
|
<th class="w-46 !text-center">组织层级</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(accept,index) in acceptList" :key="index">
|
||||||
|
<td class="text-xs">{{accept.item}}</td>
|
||||||
|
<td class="text-xs">{{accept.standard}}</td>
|
||||||
|
<td class="text-xs">{{accept.method}}</td>
|
||||||
|
<td class="text-xs text-center">{{accept.org_name}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 审批记录 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p
|
||||||
|
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||||||
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
|
>审批记录</p>
|
||||||
|
</div>
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-120px">流程阶段</th>
|
||||||
|
<th class="w-120px">操作人</th>
|
||||||
|
<th class="w-140px !text-center">操作时间</th>
|
||||||
|
<th>操作内容和意见</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(record,index) in currentInfo?.approval_records" :key="index">
|
||||||
|
<td class="text-xs">{{record.action}}</td>
|
||||||
|
<td class="text-xs">{{record.user_name}}</td>
|
||||||
|
<td class="text-center text-xs">{{record.updated_at?.slice(0,16)}}</td>
|
||||||
|
<td class="text-xs">{{record.remark}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
|
||||||
|
<!-- 审批流程进度 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-sm font-semibold">审批流程{{ matchLabel(levelList,level)?.label? '('+matchLabel(levelList,level)?.label+'变更)':'' }}</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
<template v-for="(n, i) in currentInfo?.approval_flow?.steps" :key="n.name">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Icon v-if="i !== 0" icon="formkit:right" class="size-14px text-slate-300" />
|
||||||
|
<div class="rounded-lg border px-3 py-2 text-center"
|
||||||
|
:class="n.status === 'PASSED' ? 'border-emerald-300 bg-emerald-50' : n.status === 'PENDING' ? 'border-amber-400 bg-amber-50' : n.status === 'REJECTED' ? 'border-red-300 bg-red-50' :'border-amber-400 bg-amber-50'">
|
||||||
|
<p class="text-xs font-medium">
|
||||||
|
{{ n.name }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<span v-for="member in n.members" :key="member.id" class="text-xs text-slate-500">{{ member.user_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center py-3 px-3 border rounded-md mt-3 gap-3">
|
||||||
|
<NButton class="text-xs hover:underline" text type="primary">AI辅助</NButton>
|
||||||
|
<NInput v-model:value="comment" type="textarea" size="small" :autosize="{ minRows: 1}" placeholder="审批意见(采纳的 AI 建议自动纳入,可修改,随审批提交留痕)" />
|
||||||
|
<div class="ml-auto flex gap-2">
|
||||||
|
<NButton class="text-xs" size="small" type="error">拒绝</NButton>
|
||||||
|
<NButton class="text-xs" size="small" type="success">同意</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
<!-- 变更类型判定说明(右侧抽屉) -->
|
||||||
|
<NDrawer v-model:show="describeDrawer" :width="576" placement="right">
|
||||||
|
<NDrawerContent title="变更类型判定说明" closable>
|
||||||
|
<NTag v-if="typeAiLoading" type="info" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
|
||||||
|
</template>
|
||||||
|
AI 判定中…
|
||||||
|
</NTag>
|
||||||
|
<div v-else class="flex items-center gap-2 max-h-[22px] mb-3">
|
||||||
|
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||||||
|
✨ AI 生成 · 需人工确认
|
||||||
|
</span>
|
||||||
|
<NTag v-if="typeAi?.type" type="info" size="small">判定:{{ typeAi?.type }}</NTag>
|
||||||
|
<NTag v-if="typeAi" size="small">置信度:{{ `${Math.round(typeAi.confidence * 100)}%` }}</NTag>
|
||||||
|
</div>
|
||||||
|
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||||
|
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||||||
|
<p><b>判定依据:</b></p>
|
||||||
|
<ul class="list-disc space-y-1 pl-5">
|
||||||
|
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||||||
|
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||||||
|
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||||
|
</div>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
<!-- 变更等级判定表(右侧抽屉查看) -->
|
||||||
|
<NDrawer v-model:show="levelDrawer" :width="576" placement="right">
|
||||||
|
<NDrawerContent title="变更等级判定表" closable>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NTag
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: tagBgColor(matchLabel(levelList,level)?.color),
|
||||||
|
textColor: tagTextColor(matchLabel(levelList,level)?.color),
|
||||||
|
borderColor: tagBorderColor(matchLabel(levelList,level)?.color),
|
||||||
|
}"
|
||||||
|
>{{ matchLabel(levelList,level)?.label? matchLabel(levelList,level)?.label+'变更':'' }}</NTag>
|
||||||
|
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||||||
|
</div>
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b bg-slate-50">
|
||||||
|
<th class="w-40">评估维度</th>
|
||||||
|
<th class="w-20 !text-center">得分</th>
|
||||||
|
<th>评分依据</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="s in questions" :key="s.dim" class="border-b last:border-b-0">
|
||||||
|
<td class="font-medium">{{ s.name }}</td>
|
||||||
|
<td class="font-bold text-[#17407F] text-center">{{ s.score }}</td>
|
||||||
|
<td class="text-xs leading-relaxed text-slate-600">{{ matchBasis(s.options,s.score) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
</div>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
<!-- AI 审批辅助(右侧抽屉查看) -->
|
||||||
|
<NDrawer v-model:show="aiDrawer" :width="576" placement="right">
|
||||||
|
<NDrawerContent closable>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<NProgress type="line" :percentage="aiAssistProgress" :show-indicator="false" class="mt-1.5" processing />
|
||||||
|
</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>
|
||||||
|
|
||||||
|
</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>
|
||||||
|
<previewComponent v-if="previewFile" :previewFile="previewFile" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</NSpin>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scroll {
|
||||||
|
height: calc(100vh - 277px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,65 +1,463 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, 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 { aiKnowledgeIngest, pollTask, AiError } from '@/service/api/ai';
|
import { NButton, NTag, NDropdown, useDialog } from 'naive-ui';
|
||||||
import { docListApi, historyVersionListApi } from '@/service/api/doc';
|
import previewComponent from '@/components/previewFile/index.vue'
|
||||||
|
import {
|
||||||
|
docTreeListApi,
|
||||||
|
addFolderApi,
|
||||||
|
editFolderApi,
|
||||||
|
deleteFolderApi,
|
||||||
|
docListApi,
|
||||||
|
uploadDocApi,
|
||||||
|
updateDocApi,
|
||||||
|
docHistoryListApi
|
||||||
|
} from '@/service/api/doc';
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
// 展开节点
|
const fileLoading = ref<boolean>(false);
|
||||||
const openOrgs = ref<Record<string, boolean>>({});
|
const uploadDocRef = ref<any>(null);
|
||||||
// 选中的资料库节点
|
const updateDocRef = ref<any>(null);
|
||||||
const libSel = ref({ org: "", cat: "", label: "" });
|
// 附件在线预览弹窗
|
||||||
// 当前选中的资料库列表
|
const previewModal = ref<boolean>(false);
|
||||||
const libCur = computed(() => docList.value.find((o:any) => o.org_name === libSel.value.org)?.categories.find((g:any) => g.category === libSel.value.cat)?.folders.find((f:any) => f.folder === libSel.value.label));
|
// 附件在线预览文件
|
||||||
|
const previewFile = ref<any>(null);
|
||||||
|
// 历史版本查看抽屉
|
||||||
|
const historyDrawer = ref<boolean>(false);
|
||||||
|
// 历史版本查看加载中
|
||||||
|
const historyLoading = ref<boolean>(false);
|
||||||
|
// 历史版本查看列表
|
||||||
|
const historyList = ref<any[]>([]);
|
||||||
|
// 当前org_id
|
||||||
|
const currentOrgId = ref<number>(0);
|
||||||
|
// 当前资料id
|
||||||
|
const currentDocId = ref<number>(0);
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const keyword = ref<string>('');
|
const keyword = ref<string>('');
|
||||||
// 资料库树状列表
|
// 资料库树状列表
|
||||||
const docList = ref<any[]>([]);
|
const docList = ref<any[]>([]);
|
||||||
// 历史版本列表
|
// 文件版本列表
|
||||||
const historyList = ref<any[]>([]);
|
const fileList = ref<any[]>([]);
|
||||||
|
// 搜索结果列表
|
||||||
|
const queryList = ref<any[]>([]);
|
||||||
// 展示数还是列表
|
// 展示数还是列表
|
||||||
const showTree = ref<boolean>(true);
|
const showTree = ref<boolean>(true);
|
||||||
|
// 当前选中的节点(点击)
|
||||||
|
const currentNode = ref<any>(null)
|
||||||
|
// 当前选中的节点(右键)
|
||||||
|
const currentNodeRightClick = ref<any>(null)
|
||||||
|
// 当前位置
|
||||||
|
const currentPath = ref<string[]>([]);
|
||||||
|
// 展开节点
|
||||||
|
const expandedKeys = ref<string[]>([])
|
||||||
|
// 选中节点
|
||||||
|
const selectedKeys = ref<string[]>([])
|
||||||
|
// 新增/编辑文件夹确认加载中
|
||||||
|
const saveLoading = ref<boolean>(false)
|
||||||
|
// 弹框类型
|
||||||
|
const modalType = ref<string>('add')
|
||||||
|
// 新增/编辑文件夹弹窗
|
||||||
|
const showModal = ref<boolean>(false)
|
||||||
|
// 新增/编辑文件夹名称
|
||||||
|
const name = ref<string>('')
|
||||||
|
// 右键菜单相关
|
||||||
|
const menuVisible = ref(false)
|
||||||
|
const menuX = ref(0)
|
||||||
|
const menuY = ref(0)
|
||||||
|
// 菜单选项
|
||||||
|
const menuOptions = ref<{label:string,key:string}[]>([])
|
||||||
|
|
||||||
// 在线预览资料
|
// 自定义渲染展开图标
|
||||||
const preview = (d:any) => {
|
const renderSwitcherIcon = (node:any) => {
|
||||||
window.$message?.info(`演示:在线预览 ${d.name}`)
|
// 如果节点没有 children 或 children 为空数组,则不显示图标
|
||||||
|
if (!node.option.children || node.option.children.length === 0) {
|
||||||
|
return null; // 返回 null 表示不渲染任何图标
|
||||||
|
}
|
||||||
|
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
|
||||||
|
return h(Icon,{
|
||||||
|
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
|
||||||
|
class: 'size-12px',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 下载资料
|
/**
|
||||||
const download = (d:any) => {
|
* 根据 key 找到节点,返回从自己到最顶层祖先的 label 数组(含自己)
|
||||||
window.$message?.success(`演示:下载 ${d.name}`)
|
* @param {Array} tree 树形数据
|
||||||
|
* @param {string} key 目标节点的 key
|
||||||
|
* @returns {string[] | null} 例如 ["工艺", "测试", "彩色"](从顶层到自身)
|
||||||
|
*/
|
||||||
|
const findPathByKey = (tree:any, key:string) => {
|
||||||
|
function dfs(nodes:any[], path:any[]) {
|
||||||
|
for (const node of nodes) {
|
||||||
|
const currentPath = [...path, node.label];
|
||||||
|
|
||||||
|
if (node.key === key) {
|
||||||
|
return currentPath; // 找到,直接返回整条路径
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children?.length) {
|
||||||
|
const res:any = dfs(node.children, currentPath);
|
||||||
|
if (res) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dfs(tree, []);
|
||||||
}
|
}
|
||||||
// 上传资料
|
/**
|
||||||
const uploadFiles = () => {
|
* 根据 key 删除节点(原地修改原数组)
|
||||||
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
|
* @param {Array} tree 树形数组
|
||||||
|
* @param {string} key 要删除的节点 key
|
||||||
|
* @returns {boolean} 是否删除成功
|
||||||
|
*/
|
||||||
|
const removeNodeByKey = (tree:any, key:string,parent:any = null) => {
|
||||||
|
for (let i = 0; i < tree.length; i++) {
|
||||||
|
const node = tree[i];
|
||||||
|
|
||||||
|
// 命中目标:从当前数组中删除
|
||||||
|
if (node.key === key) {
|
||||||
|
tree.splice(i, 1);
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有子节点:递归删除
|
||||||
|
if (node.children?.length) {
|
||||||
|
const result:any = removeNodeByKey(node.children, key, node);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据 key 找到其父级节点
|
||||||
|
* @param {Array} tree 树形数组
|
||||||
|
* @param {string} key 目标节点 key
|
||||||
|
* @returns {Object | null} 父级节点,顶层节点返回 null,未找到返回 null
|
||||||
|
*/
|
||||||
|
const findParentByKey = (tree:any, key:string) => {
|
||||||
|
for (const node of tree) {
|
||||||
|
if (node.children?.length) {
|
||||||
|
// 子层里有目标 key,那当前 node 就是父级
|
||||||
|
if (node.children.some((child:any) => child.key === key)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
// 继续向下找
|
||||||
|
const parent:any = findParentByKey(node.children, key);
|
||||||
|
if (parent) return parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开节点
|
||||||
|
const handleExpandKeys = (keys: string[],option:any,meta:any) => {
|
||||||
|
expandedKeys.value = keys
|
||||||
|
if (!meta.node)
|
||||||
|
return
|
||||||
|
if(meta.action==='expand'){
|
||||||
|
meta.node.prefix = () =>h(Icon, {
|
||||||
|
icon: 'material-symbols:folder-open-outline',
|
||||||
|
class: 'size-14px',
|
||||||
|
style: {
|
||||||
|
color: themeStore.themeColor,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(meta.action==='collapse'){
|
||||||
|
meta.node.prefix = () =>h(Icon, {
|
||||||
|
icon: 'material-symbols:folder-outline',
|
||||||
|
class: 'size-14px',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 选中节点
|
||||||
|
const handleSelectKeys = (keys: string[],option:any) => {
|
||||||
|
// org层不能被选中
|
||||||
|
if(option[0] && option[0].type === 'org'){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 当前选中的key
|
||||||
|
selectedKeys.value = keys;
|
||||||
|
// 当前选中的节点
|
||||||
|
currentNode.value = option[0];
|
||||||
|
currentPath.value = findPathByKey(docList.value, currentNode.value?.key) || [];
|
||||||
|
getFileList(currentNode.value?.id);
|
||||||
|
}
|
||||||
|
// 处理右键点击树节点
|
||||||
|
const treeProps = ({ option }: { option: any }) => {
|
||||||
|
return {
|
||||||
|
onContextmenu(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if(option.type === 'org'){
|
||||||
|
currentOrgId.value = option.org_id;
|
||||||
|
menuOptions.value = [
|
||||||
|
{ label: '添加文件夹', key: 'add' },
|
||||||
|
]
|
||||||
|
}else{
|
||||||
|
menuOptions.value = [
|
||||||
|
{ label: '编辑文件夹', key: 'edit' },
|
||||||
|
{ label: '添加文件夹', key: 'add' },
|
||||||
|
{ label: '删除文件夹', key: 'delete' },
|
||||||
|
{ label: '上传文件', key: 'upload' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
currentNodeRightClick.value = option
|
||||||
|
menuX.value = e.clientX
|
||||||
|
menuY.value = e.clientY
|
||||||
|
menuVisible.value = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 关闭菜单
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuVisible.value = false
|
||||||
|
}
|
||||||
|
// 点击菜单项
|
||||||
|
const handleMenuSelect = (key: string) => {
|
||||||
|
modalType.value = key
|
||||||
|
switch (key) {
|
||||||
|
case 'edit':
|
||||||
|
name.value = currentNodeRightClick.value.name;
|
||||||
|
showModal.value = true;
|
||||||
|
break
|
||||||
|
case 'add':
|
||||||
|
name.value = '';
|
||||||
|
showModal.value = true;
|
||||||
|
break
|
||||||
|
case 'upload':
|
||||||
|
uploadDocRef.value?.click();
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
const d = dialog.info({
|
||||||
|
title: '删除',
|
||||||
|
content: `确定删除文件夹「${currentNodeRightClick.value.name}」吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
d.loading = true
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const {error} = await deleteFolderApi(currentNodeRightClick.value.id);
|
||||||
|
if(!error){
|
||||||
|
// 删除成功后,更新当前选中的节点
|
||||||
|
const parent = removeNodeByKey(docList.value, currentNodeRightClick.value.key);
|
||||||
|
if(parent){
|
||||||
|
// 移除展开状态
|
||||||
|
if(parent.children?.length===0){
|
||||||
|
expandedKeys.value = expandedKeys.value.filter((k:number | string) => k !== parent.key);
|
||||||
|
handleExpandKeys(expandedKeys.value, null, {action:'collapse',node:parent});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 移除选中状态
|
||||||
|
selectedKeys.value = selectedKeys.value.filter((k:number | string) => k !== currentNodeRightClick.value.key);
|
||||||
|
// 判断当前点击选中的和右键操作的节点是否相同,相同则清空
|
||||||
|
if(currentNode.value?.key === currentNodeRightClick.value.key){
|
||||||
|
currentNode.value = null;
|
||||||
|
}
|
||||||
|
currentNodeRightClick.value = null;
|
||||||
|
// 判断当前路径是否为空,为空则清空文件列表(删除的节点是当前选中的)
|
||||||
|
if(currentPath.value.length===0){
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
window.$message?.success('删除文件夹成功')
|
||||||
|
}
|
||||||
|
d.loading = false
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
d.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增/编辑文件夹确认
|
||||||
|
const modalSave = async () => {
|
||||||
|
if(!name.value){
|
||||||
|
window.$message?.warning('请输入文件夹名称')
|
||||||
|
}
|
||||||
|
saveLoading.value = true;
|
||||||
|
// 新增文件夹
|
||||||
|
if(modalType.value === 'add'){
|
||||||
|
const {error} = await addFolderApi({
|
||||||
|
name: name.value,
|
||||||
|
parent_id: currentNodeRightClick.value.id,
|
||||||
|
org_id: currentOrgId.value
|
||||||
|
});
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success('新增文件夹成功')
|
||||||
|
showModal.value = false;
|
||||||
|
getDocList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 编辑文件夹
|
||||||
|
if(modalType.value === 'edit'){
|
||||||
|
const {error} = await editFolderApi(currentNodeRightClick.value.id,{
|
||||||
|
name: name.value,
|
||||||
|
});
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success('编辑文件夹成功')
|
||||||
|
showModal.value = false;
|
||||||
|
getDocList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const searchHandle = () => {
|
const searchHandle = () => {
|
||||||
if(!keyword.value) {
|
if(!keyword.value) {
|
||||||
|
// 搜索框为空时,显示树状列表
|
||||||
showTree.value = true;
|
showTree.value = true;
|
||||||
return;
|
}else{
|
||||||
};
|
// 搜索框不为空时,显示搜索结果
|
||||||
showTree.value = false;
|
showTree.value = false;
|
||||||
|
getFileList(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将组织-文件夹-文档数据转换为树组件需要的结构
|
||||||
|
* 每层添加 key / label / children
|
||||||
|
* @param {Array} data - 原始数组
|
||||||
|
* @returns {Array} 转换后的树形数组
|
||||||
|
*/
|
||||||
|
function buildTreeData(data = []) {
|
||||||
|
// 递归处理 folder 层(folders / children / docs)
|
||||||
|
function transformFolder(folder: any) {
|
||||||
|
const children = [
|
||||||
|
...(folder.children || []).map(transformFolder),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
key: folder.id+'_folder',
|
||||||
|
label: folder.name,
|
||||||
|
type: 'folder',
|
||||||
|
children,
|
||||||
|
prefix: () =>h(Icon, {
|
||||||
|
icon: expandedKeys.value.includes(folder.id+'_folder')?'material-symbols:folder-open-outline':'material-symbols:folder-outline',
|
||||||
|
class: 'size-14px',
|
||||||
|
style: {
|
||||||
|
color: expandedKeys.value.includes(folder.id+'_folder')?themeStore.themeColor:'',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理顶层 org
|
||||||
|
return data.map((org: any) => ({
|
||||||
|
...org,
|
||||||
|
key: org.org_id+'_org',
|
||||||
|
label: org.org_name,
|
||||||
|
type: 'org',
|
||||||
|
children: (org.folders || []).map(transformFolder),
|
||||||
|
prefix: () =>h(Icon, {
|
||||||
|
icon: expandedKeys.value.includes(org.org_id+'_org')?'material-symbols:folder-open-outline':'material-symbols:folder-outline',
|
||||||
|
class: 'size-14px',
|
||||||
|
style: {
|
||||||
|
color: expandedKeys.value.includes(org.org_id+'_org')?themeStore.themeColor:'',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取资料库树状列表
|
// 获取资料库树状列表
|
||||||
const getDocList = async () => {
|
const getDocList = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const {data,error} = await docListApi();
|
const {data,error} = await docTreeListApi();
|
||||||
if(!error){
|
if(!error){
|
||||||
docList.value = data ?? [];
|
docList.value = buildTreeData(data);
|
||||||
console.log(docList.value);
|
console.log(docList.value);
|
||||||
if(docList.value.length){
|
}
|
||||||
openOrgs.value = {[docList.value[0]?.org_name]:true};
|
loading.value = false;
|
||||||
// libSel.value = {org: docList.value[0]?.org_name, cat: docList.value[0]?.categories[0]?.category, label: docList.value[0]?.categories[0]?.folders[0]?.folder};
|
}
|
||||||
|
// 根据文件夹ID获取资料列表
|
||||||
|
const getFileList = async (folder_id:number) => {
|
||||||
|
fileLoading.value = true;
|
||||||
|
const {data,error} = await docListApi({folder_id,name:keyword.value});
|
||||||
|
if(!error){
|
||||||
|
if(folder_id){
|
||||||
|
// 点击左侧文件夹查询
|
||||||
|
fileList.value = data;
|
||||||
|
}else{
|
||||||
|
data.forEach((item:any) => {
|
||||||
|
item.path = findPathByKey(docList.value, item.folder_id+'_folder') || [];
|
||||||
|
})
|
||||||
|
// 搜索查询
|
||||||
|
queryList.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在线预览资料
|
||||||
|
const preview = (d:any) => {
|
||||||
|
previewFile.value = d;
|
||||||
|
previewModal.value = true;
|
||||||
|
}
|
||||||
|
// 下载资料
|
||||||
|
const download = (d:any) => {
|
||||||
|
window.$message?.info('还差接口')
|
||||||
|
}
|
||||||
|
// 上传资料
|
||||||
|
const uploadFiles = async (e:any) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('folder_id', currentNodeRightClick.value.id);
|
||||||
|
loading.value = true;
|
||||||
|
const {error} = await uploadDocApi(formData);
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success('上传成功');
|
||||||
|
if(currentNodeRightClick.value?.key === currentNode.value?.key){
|
||||||
|
getFileList(currentNode.value.id);
|
||||||
|
}else{
|
||||||
|
getDocList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
// 更新资料
|
||||||
|
const updateDoc = async (e:any) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
loading.value = true;
|
||||||
|
const {error} = await updateDocApi(currentDocId.value,formData);
|
||||||
|
if(!error){
|
||||||
|
window.$message?.success('更新成功');
|
||||||
|
if(showTree.value){
|
||||||
|
getFileList(currentNode.value.id);
|
||||||
|
}else{
|
||||||
|
getFileList(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
// 更新资料
|
||||||
|
const updateFile = (d:any) => {
|
||||||
|
currentDocId.value = d.id;
|
||||||
|
updateDocRef.value?.click()
|
||||||
|
}
|
||||||
|
// 查看历史版本
|
||||||
|
const viewHistory = async (d:any) => {
|
||||||
|
currentDocId.value = d.id;
|
||||||
|
historyDrawer.value = true;
|
||||||
|
historyLoading.value = true;
|
||||||
|
const {data,error} = await docHistoryListApi(d.id);
|
||||||
|
if(!error){
|
||||||
|
historyList.value = data;
|
||||||
|
}
|
||||||
|
historyLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getDocList();
|
getDocList();
|
||||||
@@ -68,14 +466,17 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpace vertical :size="16">
|
<NSpace vertical :size="16">
|
||||||
<!-- 我的草稿箱 -->
|
|
||||||
<NSpin :show="loading" size="small">
|
<NSpin :show="loading" size="small">
|
||||||
|
<!-- 上传资料 -->
|
||||||
|
<input ref="uploadDocRef" type="file" accept="image/*,.pdf,.docx,.xlsx" class="hidden" @change="uploadFiles" />
|
||||||
|
<!-- 更新资料 -->
|
||||||
|
<input ref="updateDocRef" type="file" accept="image/*,.pdf,.docx,.xlsx" class="hidden" @change="updateDoc" />
|
||||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
||||||
<NGi span="24 s:24 m:16">
|
<NGi span="24 s:24 m:16">
|
||||||
<div class="h-100% flex items-center gap-2">
|
<div class="h-100% flex items-center gap-2">
|
||||||
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
||||||
<p class="text-base font-semibold">资料档案库(按组织 / 专业类别建立文件夹 · AI 判定的知识来源)</p>
|
<p class="text-base font-semibold">资料档案库(按组织 / 专业类别建立文件夹)</p>
|
||||||
</div>
|
</div>
|
||||||
</NGi>
|
</NGi>
|
||||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||||
@@ -85,9 +486,6 @@ onMounted(() => {
|
|||||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
|
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
|
||||||
</template>
|
</template>
|
||||||
</NInput>
|
</NInput>
|
||||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
|
||||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传资料
|
|
||||||
</NButton>
|
|
||||||
</div>
|
</div>
|
||||||
</NGi>
|
</NGi>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
@@ -96,65 +494,82 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 全文检索:跨文件夹平铺结果 -->
|
<!-- 全文检索:跨文件夹平铺结果 -->
|
||||||
<div v-if="!showTree">
|
<div v-if="!showTree">
|
||||||
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ historyList.length }} 份</div>
|
<NSpin :show="fileLoading" size="small">
|
||||||
<div class="divide-y rounded-lg border bg-white">
|
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ queryList.length }} 份</div>
|
||||||
<div v-for="d in historyList" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
<div class="top-scroll">
|
||||||
<FileText class="h-4 w-4 shrink-0 text-slate-400" />
|
<div class="divide-y rounded-md border bg-white">
|
||||||
|
<div v-for="d in queryList" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
||||||
|
<Icon icon="akar-icons:file" class="size-16px" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="truncate text-sm">{{ d.name }}</div>
|
<div class="truncate text-sm">{{ d.doc_name }}</div>
|
||||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
||||||
<span>{{ d.cat }} · {{ d.device }}</span>
|
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.version }}</NTag>
|
||||||
<span>更新 {{ d.date }}</span>
|
<span>{{ d.path.join(' · ') }}</span>
|
||||||
|
<span>更新 {{ d?.updated_at?.slice(0, 10) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-3 text-xs">
|
<div class="flex shrink-0 gap-3 text-xs">
|
||||||
<button class="text-[#1D4E9C] hover:underline" @click="preview(d)">查看</button>
|
<NButton type="primary" text class="hover:underline text-xs" @click="viewHistory(d)">查看历史</NButton>
|
||||||
<button class="text-[#1D4E9C] hover:underline" @click="download(d)">下载</button>
|
<NButton type="primary" text class="hover:underline text-xs" @click="updateFile(d)">更新</NButton>
|
||||||
|
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">预览</NButton>
|
||||||
|
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="historyList.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配「{{ keyword }}」的资料</div>
|
<div v-if="queryList.length === 0" class="py-6 text-center text-xs text-slate-400">未匹配到相关资料</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</NSpin>
|
||||||
|
</div>
|
||||||
<!-- 文件夹树 + 当前文件夹资料 -->
|
<!-- 文件夹树 + 当前文件夹资料 -->
|
||||||
<div v-else class="flex gap-3">
|
<div v-else class="flex gap-3">
|
||||||
<div class="shrink-0 space-y-2 w-56 scroll-left">
|
<!-- 左侧 -->
|
||||||
<div v-for="o in docList" :key="o.org" class="overflow-hidden rounded-lg border bg-white">
|
<div class="w-230px border rounded-md p-2">
|
||||||
<button class="flex w-full items-center gap-1.5 px-2.5 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-50 bg-transparent"
|
<div class="scroll-left">
|
||||||
@click="openOrgs[o.org_name] = !openOrgs[o.org_name]">
|
<NTree
|
||||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openOrgs[o.org_name] ? 'rotate-90' : ''" />
|
block-line
|
||||||
<Icon v-if="openOrgs[o.org_name]" icon="quill:folder-open" class="size-14px" :style="{ color: themeStore.themeColor }" />
|
:data="docList"
|
||||||
<Icon v-else icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
label-field="label"
|
||||||
{{ o.org_name }}
|
key-field="key"
|
||||||
</button>
|
:expanded-keys="expandedKeys"
|
||||||
<template v-if="openOrgs[o.org_name]">
|
:selected-keys="selectedKeys"
|
||||||
<div v-for="g in o.categories" :key="g.cat" class="border-t px-2 py-1.5">
|
:render-switcher-icon="renderSwitcherIcon"
|
||||||
<div class="px-1 pb-0.5 text-[10px] font-medium tracking-wide text-slate-400">{{ g.category }}</div>
|
selectable
|
||||||
<button v-for="f in g.folders" :key="f.folder"
|
:cancelable="false"
|
||||||
@click="libSel = { org: o.org_name, cat: g.category, label: f.folder }"
|
:on-update:expanded-keys="handleExpandKeys"
|
||||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-xs transition"
|
:on-update:selected-keys="handleSelectKeys"
|
||||||
:class="libSel.org === o.org_name && libSel.cat === g.category && libSel.label === f.folder ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100 bg-transparent'"
|
:node-props="treeProps"
|
||||||
:style="{ '--theme-color': themeStore.themeColor }"
|
/>
|
||||||
>
|
<NDropdown
|
||||||
<Icon icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
:show="menuVisible"
|
||||||
<span class="min-w-0 flex-1 truncate">{{ f.folder }}</span>
|
:options="menuOptions"
|
||||||
<span class="text-[10px]" :class="libSel.label === f.folder && libSel.cat === g.category && libSel.org === o.org_name ? 'text-white/70' : 'text-slate-300'">{{ f.count }}</span>
|
size="small"
|
||||||
</button>
|
:x="menuX"
|
||||||
</div>
|
:y="menuY"
|
||||||
</template>
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
@clickoutside="closeMenu"
|
||||||
|
@select="handleMenuSelect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 右侧 -->
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="mb-1.5 flex flex-wrap items-center gap-1 text-xs text-slate-400">
|
<div class="mb-1.5 flex flex-wrap items-center gap-1 text-xs text-slate-400">
|
||||||
当前位置:{{ libSel.org }}
|
当前位置:
|
||||||
<Icon icon="formkit:right" class="size-12px" />
|
<div class="flex items-center gap-1">
|
||||||
{{ libSel.cat }}
|
<template v-for="(item,index) in currentPath" :key="index">
|
||||||
<Icon icon="formkit:right" class="size-12px" />
|
<span v-if="index !== (currentPath.length - 1)">{{ item }} </span>
|
||||||
<span class="font-medium text-slate-600">{{ libSel.label }}</span>
|
<span v-else class="font-medium text-slate-600">{{ item }}</span>
|
||||||
<span class="ml-auto">共 {{ libCur?.count ?? 0 }} 份 · 版本受控</span>
|
<Icon v-if="index !== (currentPath.length - 1)" icon="formkit:right" class="size-12px" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span class="ml-auto">共 {{ fileList.length }} 份 · 版本受控</span>
|
||||||
|
</div>
|
||||||
|
<NSpin :show="fileLoading" size="small">
|
||||||
<div class="scroll-right">
|
<div class="scroll-right">
|
||||||
<div class="divide-y rounded-lg border bg-white">
|
<div class="divide-y rounded-md border bg-white">
|
||||||
<div v-for="d in libCur?.docs ?? []" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
<div v-for="d in fileList" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
||||||
<Icon icon="akar-icons:file" class="size-16px" />
|
<Icon icon="akar-icons:file" class="size-16px" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="truncate text-sm">{{ d.doc_name }}</div>
|
<div class="truncate text-sm">{{ d.doc_name }}</div>
|
||||||
@@ -164,17 +579,95 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-3 text-xs">
|
<div class="flex shrink-0 gap-3 text-xs">
|
||||||
<NButton type="primary" text class="hover:underline text-xs">查看历史</NButton>
|
<NButton type="primary" text class="hover:underline text-xs" @click="viewHistory(d)">查看历史</NButton>
|
||||||
|
<NButton type="primary" text class="hover:underline text-xs" @click="updateFile(d)">更新</NButton>
|
||||||
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">预览</NButton>
|
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">预览</NButton>
|
||||||
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
|
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!libCur || libCur?.docs?.length===0" class="text-xs text-slate-400 text-center py-5">暂无数据</p>
|
<p v-if="fileList.length===0" class="text-xs text-slate-400 text-center py-5">暂无资料数据</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</NSpin>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<NModal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
:title="modalType==='add'?'新增文件夹':'编辑文件夹'"
|
||||||
|
:auto-focus="true"
|
||||||
|
:style="{ width: '450px', height: 'auto' }"
|
||||||
|
:segmented="{ content: true, footer: true }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<NInput v-model:value="name" placeholder="请输入文件夹名称" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-[10px]">
|
||||||
|
<NButton size="small" @click="showModal = false">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :loading="saveLoading" @click="modalSave">确定</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
<!-- 历史版本查看 -->
|
||||||
|
<NDrawer v-model:show="historyDrawer" :width="600" placement="right">
|
||||||
|
<NDrawerContent title="历史版本" closable>
|
||||||
|
<NSpin :show="historyLoading" size="small">
|
||||||
|
<NTable size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>名称</th>
|
||||||
|
<th class="!text-center w-80px">版本</th>
|
||||||
|
<th class="!text-center w-120px">更新时间</th>
|
||||||
|
<th class="!text-center w-80px">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="historyList.length===0">
|
||||||
|
<td colspan="4" class="text-center !text-slate-400 !py-20px">暂无历史版本数据</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="item in historyList" :key="item.id">
|
||||||
|
<td>{{ item.doc_name }}</td>
|
||||||
|
<td class="text-center">{{ item.version }}</td>
|
||||||
|
<td class="text-center">{{ item.updated_at?.slice(0, 10) }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<NButton type="primary" text class="hover:underline text-xs" @click="preview(item)">预览</NButton>
|
||||||
|
<NButton type="primary" text class="hover:underline text-xs" @click="download(item)">下载</NButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</NTable>
|
||||||
|
</NSpin>
|
||||||
|
</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?.doc_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?.user_name }}</span>
|
||||||
|
<span>上传时间:{{ previewFile?.created_at?.slice(0, 16) }}</span>
|
||||||
|
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
|
||||||
|
</div>
|
||||||
|
<previewComponent v-if="previewFile" :previewFile="previewFile" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
@@ -184,11 +677,15 @@ onMounted(() => {
|
|||||||
</style>
|
</style>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.scroll-left {
|
.scroll-left {
|
||||||
height: calc(100vh - 260px);
|
height: calc(100vh - 276px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.scroll-right {
|
.scroll-right {
|
||||||
height: calc(100vh - 280px);
|
height: calc(100vh - 280px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.top-scroll {
|
||||||
|
height: calc(100vh - 280px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ const tabChange = (k: string) => {
|
|||||||
}
|
}
|
||||||
// 待我审批
|
// 待我审批
|
||||||
if(k === "approval"){
|
if(k === "approval"){
|
||||||
list.value = resetGrouped([...dataSource.value.todo]);
|
const result = resetGrouped([...dataSource.value.todo]);
|
||||||
|
result.forEach((item: any) => {
|
||||||
|
item.todo = true;
|
||||||
|
})
|
||||||
|
list.value = result;
|
||||||
}
|
}
|
||||||
// 我的申请
|
// 我的申请
|
||||||
if(k === "apply"){
|
if(k === "apply"){
|
||||||
@@ -65,6 +69,11 @@ const tabChange = (k: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 去审批
|
||||||
|
const goApproval = (c: any) => {
|
||||||
|
routerPushByKey("details_approval",{ query: { id: c?.change_id?.toString(),status:c?.status?.toString() || '',key:'my_mytask' } });
|
||||||
|
}
|
||||||
|
|
||||||
// 处置申请跳转
|
// 处置申请跳转
|
||||||
const jumpTo = (act: string) => {
|
const jumpTo = (act: string) => {
|
||||||
routerPushByKey("details_disposal",{ query: { id: currentRow.value?.change_id?.toString(),type:act } });
|
routerPushByKey("details_disposal",{ query: { id: currentRow.value?.change_id?.toString(),type:act } });
|
||||||
@@ -72,7 +81,6 @@ const jumpTo = (act: string) => {
|
|||||||
|
|
||||||
// 打开详情、修改重报
|
// 打开详情、修改重报
|
||||||
const openDetail = (c: any,type:string = '') => {
|
const openDetail = (c: any,type:string = '') => {
|
||||||
console.log(c)
|
|
||||||
if(type==='detail'){
|
if(type==='detail'){
|
||||||
routerPushByKey("details_index",{ query: { id: c?.change_id?.toString(),status:c?.status?.toString() || '',key:'my_mytask' } });
|
routerPushByKey("details_index",{ query: { id: c?.change_id?.toString(),status:c?.status?.toString() || '',key:'my_mytask' } });
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,9 @@ const getList = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const {data,error} = await myTasksListApi();
|
const {data,error} = await myTasksListApi();
|
||||||
if(!error){
|
if(!error){
|
||||||
|
data?.todo?.forEach((item:any) => {
|
||||||
|
item.todo = true;
|
||||||
|
})
|
||||||
dataSource.value = data;
|
dataSource.value = data;
|
||||||
tabChange(currentTab.value);
|
tabChange(currentTab.value);
|
||||||
// 统计数量
|
// 统计数量
|
||||||
@@ -381,6 +392,10 @@ onMounted(() => {
|
|||||||
<span v-if="c.rejected_at" class="text-11px text-slate-400">驳回于 {{ c.rejected_at.slice(0, 16) }}</span>
|
<span v-if="c.rejected_at" class="text-11px text-slate-400">驳回于 {{ c.rejected_at.slice(0, 16) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右区 -->
|
<!-- 右区 -->
|
||||||
|
<!-- 去审批 -->
|
||||||
|
<div v-if="c.todo" class="flex gap-1.5">
|
||||||
|
<NButton class="text-xs" text type="primary" @click="goApproval(c)">去审批</NButton>
|
||||||
|
</div>
|
||||||
<!-- 审批中 -->
|
<!-- 审批中 -->
|
||||||
<div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
|
<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="primary" @click="openDetail(c,'detail')">详情</NButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user