chore: 统一换行符为LF,与.gitattributes规范一致(内容无变化)
This commit is contained in:
+236
-236
@@ -1,236 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const keyword = ref<string>('');
|
||||
const uploadFiles = () => {
|
||||
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
|
||||
}
|
||||
const KNOWLEDGE_DOCS = [
|
||||
{ name: "PID-R201-03 反应釜工艺流程图(Rev C)", cat: "工艺技术资料", device: "一车间", ver: "Rev C", date: "2026-07-29" },
|
||||
{ name: "聚合岗位操作规程(2026 版)", cat: "工艺技术资料", device: "一车间", ver: "V2026", date: "2026-07-26" },
|
||||
{ name: "M-201A 搅拌电机设备台账", cat: "设备资料", device: "一车间", ver: "V3.2", date: "2026-07-28" },
|
||||
{ name: "环己烷 MSDS(供应商 B)", cat: "化学品资料", device: "原料罐区", ver: "V1.0", date: "2026-07-18" },
|
||||
{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", cat: "法规标准库", device: "全厂", ver: "2013 版", date: "2025-12-01" },
|
||||
{ name: "PSSR 检查表模板(设备类)", cat: "检查表模板库", device: "全厂", ver: "V2.1", date: "2026-05-10" },
|
||||
{ name: "2025 年度典型变更案例汇编", cat: "历史变更案例库", device: "全厂", ver: "2025 版", date: "2026-01-15" },
|
||||
{ name: "HAZOP 分析报告(R-101 温度变更)", cat: "历史变更案例库", device: "一车间", ver: "V1.0", date: "2026-07-26" },
|
||||
];
|
||||
const libHits = computed(() => (keyword.value ? KNOWLEDGE_DOCS.filter((d:any) => d.name.includes(keyword.value) || d.cat.includes(keyword.value)) : []));
|
||||
const preview = (d:any) => {
|
||||
window.$message?.info(`演示:在线预览 ${d.name}`)
|
||||
}
|
||||
const download = (d:any) => {
|
||||
window.$message?.success(`演示:下载 ${d.name}`)
|
||||
}
|
||||
const call = (d:any) => {
|
||||
window.$message?.success(`演示:已将「${d.name}」调用为当前变更的 AI 知识来源(调用记录留痕)`)
|
||||
}
|
||||
interface LibDoc { name: string; ver: string; date: string }
|
||||
interface LibFolder { label: string; docs: LibDoc[] }
|
||||
interface LibGroup { cat: string; folders: LibFolder[] }
|
||||
const LIB_ORGS: { org: string; groups: LibGroup[] }[] = [
|
||||
{ org: "一车间", groups: [
|
||||
{ cat: "工艺", folders: [
|
||||
{ label: "操作规程", docs: [
|
||||
{ name: "聚合岗位操作规程(2026 版)", ver: "V2026", date: "2026-07-26" },
|
||||
{ name: "干燥岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-30" } ]},
|
||||
{ label: "PID 图纸", docs: [
|
||||
{ name: "PID-R201-03 反应釜工艺流程图(Rev C)", ver: "Rev C", date: "2026-07-29" },
|
||||
{ name: "PID-V102-01 罐区工艺流程图(Rev B)", ver: "Rev B", date: "2026-05-12" } ]},
|
||||
{ label: "工艺卡片", docs: [
|
||||
{ name: "R-201 反应釜工艺卡片(夏季工况)", ver: "V2.0", date: "2026-07-01" } ]},
|
||||
]},
|
||||
{ cat: "设备", folders: [
|
||||
{ label: "设备台账", docs: [
|
||||
{ name: "M-201A 搅拌电机设备台账", ver: "V3.2", date: "2026-07-28" },
|
||||
{ name: "P-204 输送泵设备台账", ver: "V2.8", date: "2026-06-15" } ]},
|
||||
{ label: "安全阀台账", docs: [
|
||||
{ name: "一车间安全阀台账(PSV-101 ~ PSV-118)", ver: "V5.1", date: "2026-07-10" } ]},
|
||||
]},
|
||||
{ cat: "仪表", folders: [
|
||||
{ label: "报警台账", docs: [
|
||||
{ name: "一车间 DCS 报警台账(2026H1)", ver: "V4.0", date: "2026-07-20" } ]},
|
||||
{ label: "联锁台账", docs: [
|
||||
{ name: "一车间联锁逻辑台账(SIS-01 ~ SIS-09)", ver: "V3.6", date: "2026-07-22" } ]},
|
||||
]},
|
||||
{ cat: "安全", folders: [
|
||||
{ label: "总图", docs: [{ name: "一车间装置总平面布置图", ver: "Rev D", date: "2026-04-18" } ]},
|
||||
{ label: "现场气体报警仪布置图", docs: [{ name: "一车间可燃 / 有毒气体报警仪布置图", ver: "Rev B", date: "2026-05-28" } ]},
|
||||
{ label: "防爆分区图", docs: [{ name: "一车间爆炸危险区域划分图", ver: "Rev C", date: "2026-03-15" } ]},
|
||||
]},
|
||||
]},
|
||||
{ org: "二车间", groups: [
|
||||
{ cat: "工艺", folders: [
|
||||
{ label: "操作规程", docs: [{ name: "二车间精馏岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-20" } ]},
|
||||
{ label: "PID 图纸", docs: [{ name: "PID-T301-02 精馏塔工艺流程图(Rev A)", ver: "Rev A", date: "2026-03-08" } ]},
|
||||
]},
|
||||
{ cat: "设备", folders: [
|
||||
{ label: "设备台账", docs: [{ name: "P-103 冷冻盐水泵设备台账", ver: "V2.2", date: "2026-05-30" } ]},
|
||||
{ label: "安全阀台账", docs: [{ name: "二车间安全阀台账(PSV-201 ~ PSV-212)", ver: "V4.3", date: "2026-06-11" } ]},
|
||||
]},
|
||||
{ cat: "仪表", folders: [
|
||||
{ label: "报警台账", docs: [{ name: "二车间 DCS 报警台账(2026H1)", ver: "V3.5", date: "2026-07-02" } ]},
|
||||
{ label: "联锁台账", docs: [{ name: "二车间联锁逻辑台账(SIS-11 ~ SIS-16)", ver: "V2.9", date: "2026-06-25" } ]},
|
||||
]},
|
||||
{ cat: "安全", folders: [
|
||||
{ label: "总图", docs: [{ name: "二车间装置总平面布置图", ver: "Rev B", date: "2026-02-14" } ]},
|
||||
{ label: "现场气体报警仪布置图", docs: [{ name: "二车间可燃 / 有毒气体报警仪布置图", ver: "Rev A", date: "2026-04-02" } ]},
|
||||
{ label: "防爆分区图", docs: [{ name: "二车间爆炸危险区域划分图", ver: "Rev B", date: "2026-01-20" } ]},
|
||||
]},
|
||||
]},
|
||||
{ org: "全厂共用", groups: [
|
||||
{ cat: "法规", folders: [
|
||||
{ label: "法规标准库", docs: [{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", ver: "2013 版", date: "2025-12-01" } ]},
|
||||
]},
|
||||
{ cat: "化学品", folders: [
|
||||
{ label: "MSDS", docs: [{ name: "环己烷 MSDS(供应商 B)", ver: "V1.0", date: "2026-07-18" } ]},
|
||||
]},
|
||||
{ cat: "模板", folders: [
|
||||
{ label: "检查表模板", docs: [{ name: "PSSR 检查表模板(设备类)", ver: "V2.1", date: "2026-05-10" } ]},
|
||||
]},
|
||||
{ cat: "案例", folders: [
|
||||
{ label: "历史变更案例", docs: [
|
||||
{ name: "2025 年度典型变更案例汇编", ver: "2025 版", date: "2026-01-15" },
|
||||
{ name: "HAZOP 分析报告(R-101 温度变更)", ver: "V1.0", date: "2026-07-26" } ]},
|
||||
]},
|
||||
]},
|
||||
];
|
||||
const openOrgs = ref<Record<string, boolean>>({ 一车间: true });
|
||||
const libSel = ref({ org: "一车间", cat: "工艺", label: "操作规程" });
|
||||
const libCur = computed(() => LIB_ORGS.find((o) => o.org === libSel.value.org)?.groups.find((g) => g.cat === libSel.value.cat)?.folders.find((f) => f.label === libSel.value.label));
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
||||
<NGi span="24 s:24 m:16">
|
||||
<div class="h-100% flex items-center gap-2">
|
||||
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-base font-semibold">资料档案库(按组织 / 专业类别建立文件夹 · AI 判定的知识来源)</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<div class="flex items-center gap-2">
|
||||
<NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" placeholder="全文检索…">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传资料
|
||||
</NButton>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<div class="mt-4 mb-2 rounded-lg bg-slate-50 px-3 py-1.5 text-[11px] text-slate-400">
|
||||
按「组织 → 专业类别 → 资料文件夹」归档具体资料(操作规程 / PID 图纸 / 工艺卡片 / 设备台账 / 安全阀台账 / 报警台账 / 联锁台账 / 总图 / 现场气体报警仪布置图 / 防爆分区图等);资料由指定维护人(资料管理员 / 各专业专责)上传维护,版本受控;「调用」可将资料引用为当前变更的 AI 知识来源,调用记录留痕。
|
||||
</div>
|
||||
<!-- 全文检索:跨文件夹平铺结果 -->
|
||||
<div v-if="keyword">
|
||||
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ libHits.length }} 份</div>
|
||||
<div class="divide-y rounded-lg border bg-white">
|
||||
<div v-for="d in libHits" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
||||
<FileText class="h-4 w-4 shrink-0 text-slate-400" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm">{{ d.name }}</div>
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
||||
<span>{{ d.cat }} · {{ d.device }}</span>
|
||||
<span>更新 {{ d.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3 text-xs">
|
||||
<button class="text-[#1D4E9C] hover:underline" @click="preview(d)">查看</button>
|
||||
<button class="text-[#1D4E9C] hover:underline" @click="download(d)">下载</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="libHits.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配「{{ keyword }}」的资料</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件夹树 + 当前文件夹资料 -->
|
||||
<div v-else class="flex gap-3">
|
||||
<div class="shrink-0 space-y-2 w-56 scroll-left">
|
||||
<div v-for="o in LIB_ORGS" :key="o.org" class="overflow-hidden rounded-lg border bg-white">
|
||||
<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"
|
||||
@click="openOrgs[o.org] = !openOrgs[o.org]">
|
||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openOrgs[o.org] ? 'rotate-90' : ''" />
|
||||
<Icon v-if="openOrgs[o.org]" icon="quill:folder-open" class="size-14px" :style="{ color: themeStore.themeColor }" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
||||
{{ o.org }}
|
||||
</button>
|
||||
<template v-if="openOrgs[o.org]">
|
||||
<div v-for="g in o.groups" :key="g.cat" class="border-t px-2 py-1.5">
|
||||
<div class="px-1 pb-0.5 text-[10px] font-medium tracking-wide text-slate-400">{{ g.cat }}专业</div>
|
||||
<button v-for="f in g.folders" :key="f.label"
|
||||
@click="libSel = { org: o.org, cat: g.cat, label: f.label }"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-xs transition"
|
||||
:class="libSel.org === o.org && libSel.cat === g.cat && libSel.label === f.label ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100 bg-transparent'"
|
||||
:style="{ '--theme-color': themeStore.themeColor }"
|
||||
>
|
||||
<Icon icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ f.label }}</span>
|
||||
<span class="text-[10px]" :class="libSel.label === f.label && libSel.cat === g.cat && libSel.org === o.org ? 'text-white/70' : 'text-slate-300'">{{ f.docs.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<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" />
|
||||
{{ libSel.cat }}专业
|
||||
<Icon icon="formkit:right" class="size-12px" />
|
||||
<span class="font-medium text-slate-600">{{ libSel.label }}</span>
|
||||
<span class="ml-auto">共 {{ libCur?.docs.length ?? 0 }} 份 · 版本受控</span>
|
||||
</div>
|
||||
<div class="scroll-right">
|
||||
<div class="divide-y rounded-lg 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">
|
||||
<Icon icon="akar-icons:file" class="size-16px" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm">{{ d.name }}</div>
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
||||
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.ver }}</NTag>
|
||||
<span>更新 {{ d.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3 text-xs">
|
||||
<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="call(d)">调用</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll-left {
|
||||
height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.scroll-right {
|
||||
height: calc(100vh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const keyword = ref<string>('');
|
||||
const uploadFiles = () => {
|
||||
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
|
||||
}
|
||||
const KNOWLEDGE_DOCS = [
|
||||
{ name: "PID-R201-03 反应釜工艺流程图(Rev C)", cat: "工艺技术资料", device: "一车间", ver: "Rev C", date: "2026-07-29" },
|
||||
{ name: "聚合岗位操作规程(2026 版)", cat: "工艺技术资料", device: "一车间", ver: "V2026", date: "2026-07-26" },
|
||||
{ name: "M-201A 搅拌电机设备台账", cat: "设备资料", device: "一车间", ver: "V3.2", date: "2026-07-28" },
|
||||
{ name: "环己烷 MSDS(供应商 B)", cat: "化学品资料", device: "原料罐区", ver: "V1.0", date: "2026-07-18" },
|
||||
{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", cat: "法规标准库", device: "全厂", ver: "2013 版", date: "2025-12-01" },
|
||||
{ name: "PSSR 检查表模板(设备类)", cat: "检查表模板库", device: "全厂", ver: "V2.1", date: "2026-05-10" },
|
||||
{ name: "2025 年度典型变更案例汇编", cat: "历史变更案例库", device: "全厂", ver: "2025 版", date: "2026-01-15" },
|
||||
{ name: "HAZOP 分析报告(R-101 温度变更)", cat: "历史变更案例库", device: "一车间", ver: "V1.0", date: "2026-07-26" },
|
||||
];
|
||||
const libHits = computed(() => (keyword.value ? KNOWLEDGE_DOCS.filter((d:any) => d.name.includes(keyword.value) || d.cat.includes(keyword.value)) : []));
|
||||
const preview = (d:any) => {
|
||||
window.$message?.info(`演示:在线预览 ${d.name}`)
|
||||
}
|
||||
const download = (d:any) => {
|
||||
window.$message?.success(`演示:下载 ${d.name}`)
|
||||
}
|
||||
const call = (d:any) => {
|
||||
window.$message?.success(`演示:已将「${d.name}」调用为当前变更的 AI 知识来源(调用记录留痕)`)
|
||||
}
|
||||
interface LibDoc { name: string; ver: string; date: string }
|
||||
interface LibFolder { label: string; docs: LibDoc[] }
|
||||
interface LibGroup { cat: string; folders: LibFolder[] }
|
||||
const LIB_ORGS: { org: string; groups: LibGroup[] }[] = [
|
||||
{ org: "一车间", groups: [
|
||||
{ cat: "工艺", folders: [
|
||||
{ label: "操作规程", docs: [
|
||||
{ name: "聚合岗位操作规程(2026 版)", ver: "V2026", date: "2026-07-26" },
|
||||
{ name: "干燥岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-30" } ]},
|
||||
{ label: "PID 图纸", docs: [
|
||||
{ name: "PID-R201-03 反应釜工艺流程图(Rev C)", ver: "Rev C", date: "2026-07-29" },
|
||||
{ name: "PID-V102-01 罐区工艺流程图(Rev B)", ver: "Rev B", date: "2026-05-12" } ]},
|
||||
{ label: "工艺卡片", docs: [
|
||||
{ name: "R-201 反应釜工艺卡片(夏季工况)", ver: "V2.0", date: "2026-07-01" } ]},
|
||||
]},
|
||||
{ cat: "设备", folders: [
|
||||
{ label: "设备台账", docs: [
|
||||
{ name: "M-201A 搅拌电机设备台账", ver: "V3.2", date: "2026-07-28" },
|
||||
{ name: "P-204 输送泵设备台账", ver: "V2.8", date: "2026-06-15" } ]},
|
||||
{ label: "安全阀台账", docs: [
|
||||
{ name: "一车间安全阀台账(PSV-101 ~ PSV-118)", ver: "V5.1", date: "2026-07-10" } ]},
|
||||
]},
|
||||
{ cat: "仪表", folders: [
|
||||
{ label: "报警台账", docs: [
|
||||
{ name: "一车间 DCS 报警台账(2026H1)", ver: "V4.0", date: "2026-07-20" } ]},
|
||||
{ label: "联锁台账", docs: [
|
||||
{ name: "一车间联锁逻辑台账(SIS-01 ~ SIS-09)", ver: "V3.6", date: "2026-07-22" } ]},
|
||||
]},
|
||||
{ cat: "安全", folders: [
|
||||
{ label: "总图", docs: [{ name: "一车间装置总平面布置图", ver: "Rev D", date: "2026-04-18" } ]},
|
||||
{ label: "现场气体报警仪布置图", docs: [{ name: "一车间可燃 / 有毒气体报警仪布置图", ver: "Rev B", date: "2026-05-28" } ]},
|
||||
{ label: "防爆分区图", docs: [{ name: "一车间爆炸危险区域划分图", ver: "Rev C", date: "2026-03-15" } ]},
|
||||
]},
|
||||
]},
|
||||
{ org: "二车间", groups: [
|
||||
{ cat: "工艺", folders: [
|
||||
{ label: "操作规程", docs: [{ name: "二车间精馏岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-20" } ]},
|
||||
{ label: "PID 图纸", docs: [{ name: "PID-T301-02 精馏塔工艺流程图(Rev A)", ver: "Rev A", date: "2026-03-08" } ]},
|
||||
]},
|
||||
{ cat: "设备", folders: [
|
||||
{ label: "设备台账", docs: [{ name: "P-103 冷冻盐水泵设备台账", ver: "V2.2", date: "2026-05-30" } ]},
|
||||
{ label: "安全阀台账", docs: [{ name: "二车间安全阀台账(PSV-201 ~ PSV-212)", ver: "V4.3", date: "2026-06-11" } ]},
|
||||
]},
|
||||
{ cat: "仪表", folders: [
|
||||
{ label: "报警台账", docs: [{ name: "二车间 DCS 报警台账(2026H1)", ver: "V3.5", date: "2026-07-02" } ]},
|
||||
{ label: "联锁台账", docs: [{ name: "二车间联锁逻辑台账(SIS-11 ~ SIS-16)", ver: "V2.9", date: "2026-06-25" } ]},
|
||||
]},
|
||||
{ cat: "安全", folders: [
|
||||
{ label: "总图", docs: [{ name: "二车间装置总平面布置图", ver: "Rev B", date: "2026-02-14" } ]},
|
||||
{ label: "现场气体报警仪布置图", docs: [{ name: "二车间可燃 / 有毒气体报警仪布置图", ver: "Rev A", date: "2026-04-02" } ]},
|
||||
{ label: "防爆分区图", docs: [{ name: "二车间爆炸危险区域划分图", ver: "Rev B", date: "2026-01-20" } ]},
|
||||
]},
|
||||
]},
|
||||
{ org: "全厂共用", groups: [
|
||||
{ cat: "法规", folders: [
|
||||
{ label: "法规标准库", docs: [{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", ver: "2013 版", date: "2025-12-01" } ]},
|
||||
]},
|
||||
{ cat: "化学品", folders: [
|
||||
{ label: "MSDS", docs: [{ name: "环己烷 MSDS(供应商 B)", ver: "V1.0", date: "2026-07-18" } ]},
|
||||
]},
|
||||
{ cat: "模板", folders: [
|
||||
{ label: "检查表模板", docs: [{ name: "PSSR 检查表模板(设备类)", ver: "V2.1", date: "2026-05-10" } ]},
|
||||
]},
|
||||
{ cat: "案例", folders: [
|
||||
{ label: "历史变更案例", docs: [
|
||||
{ name: "2025 年度典型变更案例汇编", ver: "2025 版", date: "2026-01-15" },
|
||||
{ name: "HAZOP 分析报告(R-101 温度变更)", ver: "V1.0", date: "2026-07-26" } ]},
|
||||
]},
|
||||
]},
|
||||
];
|
||||
const openOrgs = ref<Record<string, boolean>>({ 一车间: true });
|
||||
const libSel = ref({ org: "一车间", cat: "工艺", label: "操作规程" });
|
||||
const libCur = computed(() => LIB_ORGS.find((o) => o.org === libSel.value.org)?.groups.find((g) => g.cat === libSel.value.cat)?.folders.find((f) => f.label === libSel.value.label));
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
||||
<NGi span="24 s:24 m:16">
|
||||
<div class="h-100% flex items-center gap-2">
|
||||
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-base font-semibold">资料档案库(按组织 / 专业类别建立文件夹 · AI 判定的知识来源)</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<div class="flex items-center gap-2">
|
||||
<NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" placeholder="全文检索…">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传资料
|
||||
</NButton>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<div class="mt-4 mb-2 rounded-lg bg-slate-50 px-3 py-1.5 text-[11px] text-slate-400">
|
||||
按「组织 → 专业类别 → 资料文件夹」归档具体资料(操作规程 / PID 图纸 / 工艺卡片 / 设备台账 / 安全阀台账 / 报警台账 / 联锁台账 / 总图 / 现场气体报警仪布置图 / 防爆分区图等);资料由指定维护人(资料管理员 / 各专业专责)上传维护,版本受控;「调用」可将资料引用为当前变更的 AI 知识来源,调用记录留痕。
|
||||
</div>
|
||||
<!-- 全文检索:跨文件夹平铺结果 -->
|
||||
<div v-if="keyword">
|
||||
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ libHits.length }} 份</div>
|
||||
<div class="divide-y rounded-lg border bg-white">
|
||||
<div v-for="d in libHits" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
|
||||
<FileText class="h-4 w-4 shrink-0 text-slate-400" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm">{{ d.name }}</div>
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
||||
<span>{{ d.cat }} · {{ d.device }}</span>
|
||||
<span>更新 {{ d.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3 text-xs">
|
||||
<button class="text-[#1D4E9C] hover:underline" @click="preview(d)">查看</button>
|
||||
<button class="text-[#1D4E9C] hover:underline" @click="download(d)">下载</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="libHits.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配「{{ keyword }}」的资料</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件夹树 + 当前文件夹资料 -->
|
||||
<div v-else class="flex gap-3">
|
||||
<div class="shrink-0 space-y-2 w-56 scroll-left">
|
||||
<div v-for="o in LIB_ORGS" :key="o.org" class="overflow-hidden rounded-lg border bg-white">
|
||||
<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"
|
||||
@click="openOrgs[o.org] = !openOrgs[o.org]">
|
||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openOrgs[o.org] ? 'rotate-90' : ''" />
|
||||
<Icon v-if="openOrgs[o.org]" icon="quill:folder-open" class="size-14px" :style="{ color: themeStore.themeColor }" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
||||
{{ o.org }}
|
||||
</button>
|
||||
<template v-if="openOrgs[o.org]">
|
||||
<div v-for="g in o.groups" :key="g.cat" class="border-t px-2 py-1.5">
|
||||
<div class="px-1 pb-0.5 text-[10px] font-medium tracking-wide text-slate-400">{{ g.cat }}专业</div>
|
||||
<button v-for="f in g.folders" :key="f.label"
|
||||
@click="libSel = { org: o.org, cat: g.cat, label: f.label }"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-xs transition"
|
||||
:class="libSel.org === o.org && libSel.cat === g.cat && libSel.label === f.label ? 'bg-[var(--theme-color)] font-medium text-white' : 'text-slate-600 hover:bg-slate-100 bg-transparent'"
|
||||
:style="{ '--theme-color': themeStore.themeColor }"
|
||||
>
|
||||
<Icon icon="material-symbols:folder-outline" class="size-14px text-slate-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ f.label }}</span>
|
||||
<span class="text-[10px]" :class="libSel.label === f.label && libSel.cat === g.cat && libSel.org === o.org ? 'text-white/70' : 'text-slate-300'">{{ f.docs.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<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" />
|
||||
{{ libSel.cat }}专业
|
||||
<Icon icon="formkit:right" class="size-12px" />
|
||||
<span class="font-medium text-slate-600">{{ libSel.label }}</span>
|
||||
<span class="ml-auto">共 {{ libCur?.docs.length ?? 0 }} 份 · 版本受控</span>
|
||||
</div>
|
||||
<div class="scroll-right">
|
||||
<div class="divide-y rounded-lg 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">
|
||||
<Icon icon="akar-icons:file" class="size-16px" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm">{{ d.name }}</div>
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-slate-500">
|
||||
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.ver }}</NTag>
|
||||
<span>更新 {{ d.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3 text-xs">
|
||||
<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="call(d)">调用</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll-left {
|
||||
height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.scroll-right {
|
||||
height: calc(100vh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+332
-332
@@ -1,332 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const aq = ref<string>('');
|
||||
const uploadFiles = () => {
|
||||
window.$message?.success('演示:上传归档补充材料(限变更主管部门专责;材料单独存入「7-补充材料」,不改动原归档包,哈希留痕)')
|
||||
}
|
||||
interface ArchivePkg {
|
||||
id: string; name: string; closedAt: string; size: string; files: number;
|
||||
hash: string; revoked?: boolean; supplement?: boolean;
|
||||
}
|
||||
interface ArchiveUnit { unit: string; years: { year: string; pkgs: ArchivePkg[] }[] }
|
||||
|
||||
const ARCHIVE_TREE: ArchiveUnit[] = [
|
||||
{
|
||||
unit: "01-常减压装置",
|
||||
years: [
|
||||
{ year: "2024", pkgs: [
|
||||
{ id: "MOC-2024-R01-0003", name: "减一线换热器 E-108 芯子更换", closedAt: "2024-04-18", size: "12.6MB", files: 19, hash: "3a91d7c4f0e2…5b17" },
|
||||
{ id: "MOC-2024-R01-0007", name: "常压塔顶回流泵 P-105B 叶轮更换", closedAt: "2024-09-02", size: "9.8MB", files: 16, hash: "8e02b1a6d3c9…42fa" },
|
||||
] },
|
||||
{ year: "2025", pkgs: [
|
||||
{ id: "MOC-2025-R01-0011", name: "初馏塔进料温度联锁值调整(已撤销)", closedAt: "2025-03-11", size: "7.4MB", files: 14, hash: "c5d8f2a1b7e4…9c03", revoked: true },
|
||||
{ id: "MOC-2025-R01-0019", name: "电脱盐系统注水泵型号替换", closedAt: "2025-07-25", size: "15.2MB", files: 22, hash: "1f4a9c7e2b6d…d831", supplement: true },
|
||||
{ id: "MOC-2025-R01-0023", name: "常压炉燃料气切断阀改造", closedAt: "2025-11-14", size: "21.0MB", files: 27, hash: "6b3e0d8a5f1c…77e2" },
|
||||
] },
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R01-0028", name: "减顶真空泵 P-107A 电机更换", closedAt: "2026-06-30", size: "18.4MB", files: 24, hash: "b8e3a1c5d2f7…4c60" },
|
||||
{ id: "MOC-2026-R01-0031", name: "常顶油气冷却器管束更新", closedAt: "2026-07-12", size: "16.9MB", files: 21, hash: "d2a7c4f88b1e…5a96" },
|
||||
{ id: "MOC-2026-R01-0039", name: "进料泵 P-101A 更换为高效节能泵", closedAt: "2026-08-01", size: "18.4MB", files: 24, hash: "7c4f9be2a1d3…e804" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
{
|
||||
unit: "02-催化裂化装置",
|
||||
years: [
|
||||
{ year: "2025", pkgs: [
|
||||
{ id: "MOC-2025-R02-0014", name: "再生器旋风分离器更换", closedAt: "2025-05-20", size: "26.3MB", files: 31, hash: "9c1e5b3d7a2f…b450" },
|
||||
{ id: "MOC-2025-R02-0021", name: "气压机出口压力联锁逻辑修改", closedAt: "2025-10-08", size: "11.7MB", files: 18, hash: "4d8f0a2c6e9b…31cd" },
|
||||
] },
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R02-0025", name: "分馏塔顶循回流管线扩径", closedAt: "2026-06-30", size: "14.5MB", files: 20, hash: "a7b2d9e4c1f8…6e05" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
{
|
||||
unit: "03-聚合装置",
|
||||
years: [
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R03-0008", name: "聚合釜搅拌器密封形式变更", closedAt: "2026-05-19", size: "13.2MB", files: 17, hash: "e6f1a8c3b5d0…92d7" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const ARCHIVE_FOLDERS: { dir: string; files: { name: string; size: string }[] }[] = [
|
||||
{ dir: "1-申请与审批", files: [
|
||||
{ name: "变更申请表(终版).pdf", size: "486KB" },
|
||||
{ name: "审批记录(含会签意见与留痕).pdf", size: "312KB" },
|
||||
{ name: "等级判定表.pdf", size: "154KB" },
|
||||
{ name: "原始附件(方案/会议纪要/数据表).zip", size: "2.8MB" },
|
||||
] },
|
||||
{ dir: "2-风险分析", files: [
|
||||
{ name: "AI 风险预分析报告.pdf", size: "268KB" },
|
||||
{ name: "JSA 作业安全分析表.pdf", size: "196KB" },
|
||||
{ name: "HAZOP 分析记录.pdf", size: "742KB" },
|
||||
{ name: "安全检查表(SCL).pdf", size: "188KB" },
|
||||
] },
|
||||
{ dir: "3-培训与考核", files: [
|
||||
{ name: "变更培训材料(终版).pdf", size: "1.1MB" },
|
||||
{ name: "培训签到表(照片).jpg", size: "2.4MB" },
|
||||
{ name: "考核成绩单.xlsx", size: "32KB" },
|
||||
] },
|
||||
{ dir: "4-PSSR检查", files: [
|
||||
{ name: "PSSR 检查清单(终版).pdf", size: "356KB" },
|
||||
{ name: "现场确认照片.zip", size: "6.7MB" },
|
||||
{ name: "电子签名确认记录.pdf", size: "142KB" },
|
||||
] },
|
||||
{ dir: "5-验收记录", files: [
|
||||
{ name: "变更验收单.pdf", size: "204KB" },
|
||||
{ name: "整改记录与复验结论.pdf", size: "168KB" },
|
||||
] },
|
||||
{ dir: "6-受控文件副本", files: [
|
||||
{ name: "受控文件更新清单(新旧对照).pdf", size: "96KB" },
|
||||
{ name: "PID-101_Rev.4_20260805.pdf", size: "3.2MB" },
|
||||
{ name: "PID-101_Rev.3_20250101.pdf", size: "3.1MB" },
|
||||
] },
|
||||
];
|
||||
const SUPPLEMENT_FOLDER = { dir: "7-补充材料(归档后补充)", files: [
|
||||
{ name: "补充说明(泵轴振动复测数据).pdf", size: "214KB" },
|
||||
{ name: "补充审批记录.pdf", size: "88KB" },
|
||||
] };
|
||||
const ROLE_SCOPE: Record<any, { text: string; filter: (u: any, p: any) => boolean }> = {
|
||||
applicant: { text: "本人发起或参与的变更", filter: (u, p) => u.unit.startsWith("01") && ["MOC-2026-R01-0039", "MOC-2026-R01-0028", "MOC-2025-R01-0019"].includes(p.id) },
|
||||
dept: { text: "本车间(装置)的全部变更", filter: (u) => u.unit.startsWith("01") },
|
||||
process: { text: "本人参与审批或检查的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
process2: { text: "本人参与审批或检查的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
safety: { text: "全厂变更(安全监管需要)", filter: () => true },
|
||||
vp: { text: "全厂变更", filter: () => true },
|
||||
admin: { text: "全厂变更(系统管理:归档内容只读锁定,不可修改删除)", filter: () => true },
|
||||
quality: { text: "本人参与会签或检验确认的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
viewer: { text: "全厂变更(信息查看角色:只读查阅)", filter: () => true },
|
||||
};
|
||||
const tF = ref("全部时间");
|
||||
const oF = ref("全部组织");
|
||||
const pF = ref("全部专业");
|
||||
const FILTER_GROUPS = computed(() => [
|
||||
{ label: "时间", opts: ["全部时间", "近一年", "近半年", "近三个月"], val: tF.value, set: (v: string) => { tF.value = v; } },
|
||||
{ label: "组织", opts: ["全部组织", "一车间", "二车间", "三车间"], val: oF.value, set: (v: string) => { oF.value = v; } },
|
||||
{ label: "专业", opts: ["全部专业", "工艺", "设备", "仪表", "管理"], val: pF.value, set: (v: string) => { pF.value = v; } },
|
||||
]);
|
||||
const scope = computed(() => ROLE_SCOPE['applicant'] ?? ROLE_SCOPE.vp);
|
||||
const searching = computed(() => aq.value.trim().length > 0);
|
||||
const filtering = computed(() => tF.value !== "全部时间" || oF.value !== "全部组织" || pF.value !== "全部专业");
|
||||
const UNIT_ORG: Record<string, string> = { "01-常减压装置": "一车间", "02-催化裂化装置": "二车间", "03-聚合装置": "三车间" };
|
||||
const profOf = (name: string) => /联锁|报警/.test(name) ? "仪表" : /规程|制度|管理/.test(name) ? "管理" : /泵|阀|电机|换热器|搅拌|密封|分离器|管束|冷却器|气压机|叶轮|芯子/.test(name) ? "设备" : "工艺";
|
||||
const REF = new Date("2026-08-03");
|
||||
const selId = ref("MOC-2026-R01-0039");
|
||||
const passF = (p: (typeof allPkgs.value)[number]) => {
|
||||
const daysOf = tF.value === "近一年" ? 365 : tF.value === "近半年" ? 183 : tF.value === "近三个月" ? 92 : 0;
|
||||
return (daysOf === 0 || (REF.getTime() - new Date(p.closedAt).getTime()) / 86400000 <= daysOf) &&
|
||||
(oF.value === "全部组织" || UNIT_ORG[p.unit] === oF.value) &&
|
||||
(pF.value === "全部专业" || profOf(p.name) === pF.value);
|
||||
};
|
||||
const tree = computed(() => ARCHIVE_TREE
|
||||
.map((u:any) => ({ unit: u.unit, years: u.years.map((y) => ({ year: y.year, pkgs: y.pkgs.filter((p:any) => scope.value.filter(u, p)) })).filter((y:any) => y.pkgs.length > 0) }))
|
||||
.filter((u:any) => u.years.length > 0));
|
||||
const allPkgs = computed(() => tree.value.flatMap((u:any) => u.years.flatMap((y:any) => y.pkgs.map((p:any) => ({ ...p, unit: u.unit, year: y.year })))));
|
||||
const results = computed(() => (searching.value || filtering.value)
|
||||
? allPkgs.value.filter((p:any) => passF(p) && (!searching.value || p.id.toLowerCase().includes(aq.value.trim().toLowerCase()) || p.name.includes(aq.value.trim())))
|
||||
: []);
|
||||
const sel = computed(() => allPkgs.value.find((p:any) => p.id === selId.value) ?? allPkgs.value[0]);
|
||||
const selFolders = computed(() => (sel.value?.supplement ? [...ARCHIVE_FOLDERS, SUPPLEMENT_FOLDER] : ARCHIVE_FOLDERS));
|
||||
const openUnits = ref<Record<string, boolean>>({ "01-常减压装置": true });
|
||||
const openFolders = ref<Record<string, boolean>>({ "1-申请与审批": true });
|
||||
const downloadZip = (sel:any) => {
|
||||
window.$message?.success(`演示:下载 ${sel.id} 归档包 ZIP(含归档索引.txt)`)
|
||||
}
|
||||
const exportEvidenceChain = () => {
|
||||
window.$message?.success('演示:导出证据链(哈希清单 + 操作日志)')
|
||||
}
|
||||
const applySupplement = () => {
|
||||
window.$message?.info('演示:发起归档补充申请(补充材料单独存入 7-补充材料,不改动原归档)')
|
||||
}
|
||||
const viewIndex = () => {
|
||||
window.$message?.info('演示:查看归档索引(文件清单 + 逐项 SHA-256)')
|
||||
}
|
||||
const previewFile = (file:any) => {
|
||||
window.$message?.info(`演示:在线预览 ${file.name}`)
|
||||
}
|
||||
const downloadFile = (file:any) => {
|
||||
window.$message?.success(`演示:下载 ${file.name}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
||||
<NGi span="24 s:24 m:16">
|
||||
<div class="h-100% flex items-center gap-2">
|
||||
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-base font-semibold">变更归档(一次变更一个归档包 · 关闭即自动归档 · 只读锁定)</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传归档补充
|
||||
</NButton>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<div
|
||||
class="mt-4 flex items-center gap-2 rounded-lg bg-[var(--bg-color)] px-3 py-2 text-xs text-[var(--theme-color)]"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<Icon icon="uil:lock" class="size-16px" />
|
||||
当前角色调阅范围:{{ scope.text }} · 归档包关闭时自动生成,内容只读锁定并记录 SHA-256 哈希,任何修改留痕可追责
|
||||
</div>
|
||||
<NInput v-model:value="aq" type="text" class="w-40 text-xs mt-3" placeholder="按变更单号精确查找,或按名称关键词检索(如 MOC-2026-R01-0039)">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
<!-- 筛选:时间 / 组织 / 专业 -->
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 mt-3">
|
||||
<div v-for="g in FILTER_GROUPS" :key="g.label" class="flex flex-wrap items-center gap-1">
|
||||
<span class="text-[11px] text-slate-400">{{ g.label }}</span>
|
||||
<NButton size="tiny" round v-for="o in g.opts" :key="o" @click="g.set(o)" class="text-[11px]" :type="g.val === o ? 'primary' : 'default'">{{ o }}</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 grid-cols-3 mt-3">
|
||||
<!-- 左:三层目录树 / 筛选检索结果 -->
|
||||
<div class="rounded-lg border p-2">
|
||||
<div class="px-2 pb-1 text-xs font-medium text-slate-500">{{ (searching || filtering) ? `筛选 / 检索结果(${results.length})` : '归档区目录(装置 / 年份 / 变更单号)' }}</div>
|
||||
<template v-if="searching || filtering">
|
||||
<div v-if="results.length === 0" class="px-2 py-4 text-center text-xs text-slate-400">未找到匹配的归档包</div>
|
||||
<button v-for="p in results" :key="p.id" @click="selId = p.id"
|
||||
class="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs hover:bg-[#DFE9F6]/60"
|
||||
:class="sel?.id === p.id ? 'bg-[#DFE9F6] font-medium text-[#1D4E9C]' : ''">
|
||||
<FolderArchive class="h-3.5 w-3.5 shrink-0 text-[#6E93C6]" />
|
||||
<span class="truncate">{{ p.id }}</span>
|
||||
<Tag v-if="p.revoked" class="border-red-200 bg-red-100 px-1 py-0 text-[10px] text-red-600">已撤销</Tag>
|
||||
<Tag v-if="p.supplement" class="border-amber-200 bg-amber-100 px-1 py-0 text-[10px] text-amber-700">补充</Tag>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="u in tree" :key="u.unit">
|
||||
<button @click="openUnits[u.unit] = !openUnits[u.unit]"
|
||||
class="flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs font-medium hover:bg-slate-50 bg-transparent">
|
||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openUnits[u.unit] ? 'rotate-90' : ''" />
|
||||
<Icon v-if="openUnits[u.unit]" icon="quill:folder-open" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-16px text-slate-400" />
|
||||
{{ u.unit }}
|
||||
</button>
|
||||
<template v-if="openUnits[u.unit]">
|
||||
<div v-for="y in u.years" :key="y.year">
|
||||
<div class="pl-7 pt-1 text-[11px] font-medium text-slate-400">{{ y.year }} 年({{ y.pkgs.length }})</div>
|
||||
<button v-for="p in y.pkgs" :key="p.id" @click="selId = p.id"
|
||||
class="flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-2 text-left text-xs hover:bg-[#DFE9F6]/60"
|
||||
:class="sel?.id === p.id ? 'bg-[var(--bg-color)] font-medium text-[var(--theme-color)]' : 'bg-transparent'"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<Icon icon="lucide:folder-archive" class="size-14px" :style="{ color: themeStore.themeColor }" />
|
||||
|
||||
<span class="truncate">{{ p.id }}</span>
|
||||
<NTag v-if="p.revoked" size="small" type="error" class="text-[10px]">已撤销</NTag>
|
||||
<NTag v-if="p.supplement" size="small" type="warning" class="text-[10px]">补充</NTag>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
<!-- 右:归档包详情 -->
|
||||
<div v-if="sel" class="rounded-lg border col-span-2">
|
||||
<div class="border-b p-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold">{{ sel.id }}</span>
|
||||
<NTag v-if="sel.revoked" size="small" type="error">已撤销(保留归档备查)</NTag>
|
||||
<NTag v-else size="small" type="info">
|
||||
<template #icon>
|
||||
<Icon icon="uil:lock" class="size-14px" />
|
||||
</template>
|
||||
只读锁定
|
||||
</NTag>
|
||||
<NTag v-if="sel.supplement" size="small" type="warning">含归档补充</NTag>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-600">{{ sel.name }}</div>
|
||||
<div class="mt-2 flex flex-wrap justify-between gap-x-4 gap-y-1 text-[11px] text-slate-500">
|
||||
<span>归档路径:归档区/{{ sel.unit }}/{{ sel.year }}/{{ sel.id }}/</span>
|
||||
<span>关闭时间:{{ sel.closedAt }}</span>
|
||||
<span>共 {{ sel.files }} 个文件 · {{ sel.size }}</span>
|
||||
<span class="flex items-center gap-1 font-mono">
|
||||
<Icon icon="lucide:hash" class="size-12px" />
|
||||
{{ sel.hash }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<NButton size="small" class="text-xs" @click="downloadZip(sel)">
|
||||
<Icon icon="material-symbols:download" class="size-14px" />
|
||||
下载归档包 ZIP
|
||||
</NButton>
|
||||
<NButton size="small" class="text-xs" @click="exportEvidenceChain">导出证据链</NButton>
|
||||
<NButton v-if="!sel.revoked" size="small" class="text-xs" @click="applySupplement">发起归档补充申请</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y scroll-right">
|
||||
<div v-for="f in selFolders" :key="f.dir">
|
||||
<button @click="openFolders[f.dir] = !openFolders[f.dir]"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium hover:bg-slate-50 bg-transparent">
|
||||
<Icon icon="formkit:right" class="size-14px text-slate-400 transition-transform" :class="openFolders[f.dir] ? 'rotate-90' : ''" />
|
||||
<Icon icon="quill:folder-open" v-if="openFolders[f.dir]" class="size-16px text-[#6E93C6]" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-16px text-slate-400" />
|
||||
{{ f.dir }}
|
||||
<span class="ml-auto text-[11px] font-normal text-slate-400">{{ f.files.length }} 个文件</span>
|
||||
</button>
|
||||
<template v-if="openFolders[f.dir]">
|
||||
<div v-for="file in f.files" :key="file.name" class="flex items-center gap-2 py-1.5 pl-10 pr-3 text-xs hover:bg-slate-50">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-slate-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ file.name }}</span>
|
||||
<span class="text-[11px] text-slate-400">{{ file.size }}</span>
|
||||
<NButton text type="primary" @click="previewFile(file)">
|
||||
<Icon icon="lucide:eye" class="size-14px" />
|
||||
</NButton>
|
||||
<NButton text type="primary" @click="downloadFile(file)">
|
||||
<Icon icon="material-symbols:download" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs hover:bg-slate-50">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-slate-400" />
|
||||
<span class="flex-1 font-mono">归档索引.txt</span>
|
||||
<span class="text-[11px] text-slate-400">清单与哈希</span>
|
||||
<NButton text type="primary" @click="viewIndex">
|
||||
<Icon icon="lucide:eye" class="size-14px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll-right {
|
||||
height: calc(100vh - 460px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
const aq = ref<string>('');
|
||||
const uploadFiles = () => {
|
||||
window.$message?.success('演示:上传归档补充材料(限变更主管部门专责;材料单独存入「7-补充材料」,不改动原归档包,哈希留痕)')
|
||||
}
|
||||
interface ArchivePkg {
|
||||
id: string; name: string; closedAt: string; size: string; files: number;
|
||||
hash: string; revoked?: boolean; supplement?: boolean;
|
||||
}
|
||||
interface ArchiveUnit { unit: string; years: { year: string; pkgs: ArchivePkg[] }[] }
|
||||
|
||||
const ARCHIVE_TREE: ArchiveUnit[] = [
|
||||
{
|
||||
unit: "01-常减压装置",
|
||||
years: [
|
||||
{ year: "2024", pkgs: [
|
||||
{ id: "MOC-2024-R01-0003", name: "减一线换热器 E-108 芯子更换", closedAt: "2024-04-18", size: "12.6MB", files: 19, hash: "3a91d7c4f0e2…5b17" },
|
||||
{ id: "MOC-2024-R01-0007", name: "常压塔顶回流泵 P-105B 叶轮更换", closedAt: "2024-09-02", size: "9.8MB", files: 16, hash: "8e02b1a6d3c9…42fa" },
|
||||
] },
|
||||
{ year: "2025", pkgs: [
|
||||
{ id: "MOC-2025-R01-0011", name: "初馏塔进料温度联锁值调整(已撤销)", closedAt: "2025-03-11", size: "7.4MB", files: 14, hash: "c5d8f2a1b7e4…9c03", revoked: true },
|
||||
{ id: "MOC-2025-R01-0019", name: "电脱盐系统注水泵型号替换", closedAt: "2025-07-25", size: "15.2MB", files: 22, hash: "1f4a9c7e2b6d…d831", supplement: true },
|
||||
{ id: "MOC-2025-R01-0023", name: "常压炉燃料气切断阀改造", closedAt: "2025-11-14", size: "21.0MB", files: 27, hash: "6b3e0d8a5f1c…77e2" },
|
||||
] },
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R01-0028", name: "减顶真空泵 P-107A 电机更换", closedAt: "2026-06-30", size: "18.4MB", files: 24, hash: "b8e3a1c5d2f7…4c60" },
|
||||
{ id: "MOC-2026-R01-0031", name: "常顶油气冷却器管束更新", closedAt: "2026-07-12", size: "16.9MB", files: 21, hash: "d2a7c4f88b1e…5a96" },
|
||||
{ id: "MOC-2026-R01-0039", name: "进料泵 P-101A 更换为高效节能泵", closedAt: "2026-08-01", size: "18.4MB", files: 24, hash: "7c4f9be2a1d3…e804" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
{
|
||||
unit: "02-催化裂化装置",
|
||||
years: [
|
||||
{ year: "2025", pkgs: [
|
||||
{ id: "MOC-2025-R02-0014", name: "再生器旋风分离器更换", closedAt: "2025-05-20", size: "26.3MB", files: 31, hash: "9c1e5b3d7a2f…b450" },
|
||||
{ id: "MOC-2025-R02-0021", name: "气压机出口压力联锁逻辑修改", closedAt: "2025-10-08", size: "11.7MB", files: 18, hash: "4d8f0a2c6e9b…31cd" },
|
||||
] },
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R02-0025", name: "分馏塔顶循回流管线扩径", closedAt: "2026-06-30", size: "14.5MB", files: 20, hash: "a7b2d9e4c1f8…6e05" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
{
|
||||
unit: "03-聚合装置",
|
||||
years: [
|
||||
{ year: "2026", pkgs: [
|
||||
{ id: "MOC-2026-R03-0008", name: "聚合釜搅拌器密封形式变更", closedAt: "2026-05-19", size: "13.2MB", files: 17, hash: "e6f1a8c3b5d0…92d7" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const ARCHIVE_FOLDERS: { dir: string; files: { name: string; size: string }[] }[] = [
|
||||
{ dir: "1-申请与审批", files: [
|
||||
{ name: "变更申请表(终版).pdf", size: "486KB" },
|
||||
{ name: "审批记录(含会签意见与留痕).pdf", size: "312KB" },
|
||||
{ name: "等级判定表.pdf", size: "154KB" },
|
||||
{ name: "原始附件(方案/会议纪要/数据表).zip", size: "2.8MB" },
|
||||
] },
|
||||
{ dir: "2-风险分析", files: [
|
||||
{ name: "AI 风险预分析报告.pdf", size: "268KB" },
|
||||
{ name: "JSA 作业安全分析表.pdf", size: "196KB" },
|
||||
{ name: "HAZOP 分析记录.pdf", size: "742KB" },
|
||||
{ name: "安全检查表(SCL).pdf", size: "188KB" },
|
||||
] },
|
||||
{ dir: "3-培训与考核", files: [
|
||||
{ name: "变更培训材料(终版).pdf", size: "1.1MB" },
|
||||
{ name: "培训签到表(照片).jpg", size: "2.4MB" },
|
||||
{ name: "考核成绩单.xlsx", size: "32KB" },
|
||||
] },
|
||||
{ dir: "4-PSSR检查", files: [
|
||||
{ name: "PSSR 检查清单(终版).pdf", size: "356KB" },
|
||||
{ name: "现场确认照片.zip", size: "6.7MB" },
|
||||
{ name: "电子签名确认记录.pdf", size: "142KB" },
|
||||
] },
|
||||
{ dir: "5-验收记录", files: [
|
||||
{ name: "变更验收单.pdf", size: "204KB" },
|
||||
{ name: "整改记录与复验结论.pdf", size: "168KB" },
|
||||
] },
|
||||
{ dir: "6-受控文件副本", files: [
|
||||
{ name: "受控文件更新清单(新旧对照).pdf", size: "96KB" },
|
||||
{ name: "PID-101_Rev.4_20260805.pdf", size: "3.2MB" },
|
||||
{ name: "PID-101_Rev.3_20250101.pdf", size: "3.1MB" },
|
||||
] },
|
||||
];
|
||||
const SUPPLEMENT_FOLDER = { dir: "7-补充材料(归档后补充)", files: [
|
||||
{ name: "补充说明(泵轴振动复测数据).pdf", size: "214KB" },
|
||||
{ name: "补充审批记录.pdf", size: "88KB" },
|
||||
] };
|
||||
const ROLE_SCOPE: Record<any, { text: string; filter: (u: any, p: any) => boolean }> = {
|
||||
applicant: { text: "本人发起或参与的变更", filter: (u, p) => u.unit.startsWith("01") && ["MOC-2026-R01-0039", "MOC-2026-R01-0028", "MOC-2025-R01-0019"].includes(p.id) },
|
||||
dept: { text: "本车间(装置)的全部变更", filter: (u) => u.unit.startsWith("01") },
|
||||
process: { text: "本人参与审批或检查的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
process2: { text: "本人参与审批或检查的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
safety: { text: "全厂变更(安全监管需要)", filter: () => true },
|
||||
vp: { text: "全厂变更", filter: () => true },
|
||||
admin: { text: "全厂变更(系统管理:归档内容只读锁定,不可修改删除)", filter: () => true },
|
||||
quality: { text: "本人参与会签或检验确认的变更", filter: (u, p) => u.unit.startsWith("01") && p.id >= "MOC-2025" },
|
||||
viewer: { text: "全厂变更(信息查看角色:只读查阅)", filter: () => true },
|
||||
};
|
||||
const tF = ref("全部时间");
|
||||
const oF = ref("全部组织");
|
||||
const pF = ref("全部专业");
|
||||
const FILTER_GROUPS = computed(() => [
|
||||
{ label: "时间", opts: ["全部时间", "近一年", "近半年", "近三个月"], val: tF.value, set: (v: string) => { tF.value = v; } },
|
||||
{ label: "组织", opts: ["全部组织", "一车间", "二车间", "三车间"], val: oF.value, set: (v: string) => { oF.value = v; } },
|
||||
{ label: "专业", opts: ["全部专业", "工艺", "设备", "仪表", "管理"], val: pF.value, set: (v: string) => { pF.value = v; } },
|
||||
]);
|
||||
const scope = computed(() => ROLE_SCOPE['applicant'] ?? ROLE_SCOPE.vp);
|
||||
const searching = computed(() => aq.value.trim().length > 0);
|
||||
const filtering = computed(() => tF.value !== "全部时间" || oF.value !== "全部组织" || pF.value !== "全部专业");
|
||||
const UNIT_ORG: Record<string, string> = { "01-常减压装置": "一车间", "02-催化裂化装置": "二车间", "03-聚合装置": "三车间" };
|
||||
const profOf = (name: string) => /联锁|报警/.test(name) ? "仪表" : /规程|制度|管理/.test(name) ? "管理" : /泵|阀|电机|换热器|搅拌|密封|分离器|管束|冷却器|气压机|叶轮|芯子/.test(name) ? "设备" : "工艺";
|
||||
const REF = new Date("2026-08-03");
|
||||
const selId = ref("MOC-2026-R01-0039");
|
||||
const passF = (p: (typeof allPkgs.value)[number]) => {
|
||||
const daysOf = tF.value === "近一年" ? 365 : tF.value === "近半年" ? 183 : tF.value === "近三个月" ? 92 : 0;
|
||||
return (daysOf === 0 || (REF.getTime() - new Date(p.closedAt).getTime()) / 86400000 <= daysOf) &&
|
||||
(oF.value === "全部组织" || UNIT_ORG[p.unit] === oF.value) &&
|
||||
(pF.value === "全部专业" || profOf(p.name) === pF.value);
|
||||
};
|
||||
const tree = computed(() => ARCHIVE_TREE
|
||||
.map((u:any) => ({ unit: u.unit, years: u.years.map((y) => ({ year: y.year, pkgs: y.pkgs.filter((p:any) => scope.value.filter(u, p)) })).filter((y:any) => y.pkgs.length > 0) }))
|
||||
.filter((u:any) => u.years.length > 0));
|
||||
const allPkgs = computed(() => tree.value.flatMap((u:any) => u.years.flatMap((y:any) => y.pkgs.map((p:any) => ({ ...p, unit: u.unit, year: y.year })))));
|
||||
const results = computed(() => (searching.value || filtering.value)
|
||||
? allPkgs.value.filter((p:any) => passF(p) && (!searching.value || p.id.toLowerCase().includes(aq.value.trim().toLowerCase()) || p.name.includes(aq.value.trim())))
|
||||
: []);
|
||||
const sel = computed(() => allPkgs.value.find((p:any) => p.id === selId.value) ?? allPkgs.value[0]);
|
||||
const selFolders = computed(() => (sel.value?.supplement ? [...ARCHIVE_FOLDERS, SUPPLEMENT_FOLDER] : ARCHIVE_FOLDERS));
|
||||
const openUnits = ref<Record<string, boolean>>({ "01-常减压装置": true });
|
||||
const openFolders = ref<Record<string, boolean>>({ "1-申请与审批": true });
|
||||
const downloadZip = (sel:any) => {
|
||||
window.$message?.success(`演示:下载 ${sel.id} 归档包 ZIP(含归档索引.txt)`)
|
||||
}
|
||||
const exportEvidenceChain = () => {
|
||||
window.$message?.success('演示:导出证据链(哈希清单 + 操作日志)')
|
||||
}
|
||||
const applySupplement = () => {
|
||||
window.$message?.info('演示:发起归档补充申请(补充材料单独存入 7-补充材料,不改动原归档)')
|
||||
}
|
||||
const viewIndex = () => {
|
||||
window.$message?.info('演示:查看归档索引(文件清单 + 逐项 SHA-256)')
|
||||
}
|
||||
const previewFile = (file:any) => {
|
||||
window.$message?.info(`演示:在线预览 ${file.name}`)
|
||||
}
|
||||
const downloadFile = (file:any) => {
|
||||
window.$message?.success(`演示:下载 ${file.name}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
|
||||
<NGi span="24 s:24 m:16">
|
||||
<div class="h-100% flex items-center gap-2">
|
||||
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
|
||||
<p class="text-base font-semibold">变更归档(一次变更一个归档包 · 关闭即自动归档 · 只读锁定)</p>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传归档补充
|
||||
</NButton>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<div
|
||||
class="mt-4 flex items-center gap-2 rounded-lg bg-[var(--bg-color)] px-3 py-2 text-xs text-[var(--theme-color)]"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<Icon icon="uil:lock" class="size-16px" />
|
||||
当前角色调阅范围:{{ scope.text }} · 归档包关闭时自动生成,内容只读锁定并记录 SHA-256 哈希,任何修改留痕可追责
|
||||
</div>
|
||||
<NInput v-model:value="aq" type="text" class="w-40 text-xs mt-3" placeholder="按变更单号精确查找,或按名称关键词检索(如 MOC-2026-R01-0039)">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
|
||||
</template>
|
||||
</NInput>
|
||||
<!-- 筛选:时间 / 组织 / 专业 -->
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 mt-3">
|
||||
<div v-for="g in FILTER_GROUPS" :key="g.label" class="flex flex-wrap items-center gap-1">
|
||||
<span class="text-[11px] text-slate-400">{{ g.label }}</span>
|
||||
<NButton size="tiny" round v-for="o in g.opts" :key="o" @click="g.set(o)" class="text-[11px]" :type="g.val === o ? 'primary' : 'default'">{{ o }}</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 grid-cols-3 mt-3">
|
||||
<!-- 左:三层目录树 / 筛选检索结果 -->
|
||||
<div class="rounded-lg border p-2">
|
||||
<div class="px-2 pb-1 text-xs font-medium text-slate-500">{{ (searching || filtering) ? `筛选 / 检索结果(${results.length})` : '归档区目录(装置 / 年份 / 变更单号)' }}</div>
|
||||
<template v-if="searching || filtering">
|
||||
<div v-if="results.length === 0" class="px-2 py-4 text-center text-xs text-slate-400">未找到匹配的归档包</div>
|
||||
<button v-for="p in results" :key="p.id" @click="selId = p.id"
|
||||
class="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs hover:bg-[#DFE9F6]/60"
|
||||
:class="sel?.id === p.id ? 'bg-[#DFE9F6] font-medium text-[#1D4E9C]' : ''">
|
||||
<FolderArchive class="h-3.5 w-3.5 shrink-0 text-[#6E93C6]" />
|
||||
<span class="truncate">{{ p.id }}</span>
|
||||
<Tag v-if="p.revoked" class="border-red-200 bg-red-100 px-1 py-0 text-[10px] text-red-600">已撤销</Tag>
|
||||
<Tag v-if="p.supplement" class="border-amber-200 bg-amber-100 px-1 py-0 text-[10px] text-amber-700">补充</Tag>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="u in tree" :key="u.unit">
|
||||
<button @click="openUnits[u.unit] = !openUnits[u.unit]"
|
||||
class="flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs font-medium hover:bg-slate-50 bg-transparent">
|
||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openUnits[u.unit] ? 'rotate-90' : ''" />
|
||||
<Icon v-if="openUnits[u.unit]" icon="quill:folder-open" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-16px text-slate-400" />
|
||||
{{ u.unit }}
|
||||
</button>
|
||||
<template v-if="openUnits[u.unit]">
|
||||
<div v-for="y in u.years" :key="y.year">
|
||||
<div class="pl-7 pt-1 text-[11px] font-medium text-slate-400">{{ y.year }} 年({{ y.pkgs.length }})</div>
|
||||
<button v-for="p in y.pkgs" :key="p.id" @click="selId = p.id"
|
||||
class="flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-2 text-left text-xs hover:bg-[#DFE9F6]/60"
|
||||
:class="sel?.id === p.id ? 'bg-[var(--bg-color)] font-medium text-[var(--theme-color)]' : 'bg-transparent'"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<Icon icon="lucide:folder-archive" class="size-14px" :style="{ color: themeStore.themeColor }" />
|
||||
|
||||
<span class="truncate">{{ p.id }}</span>
|
||||
<NTag v-if="p.revoked" size="small" type="error" class="text-[10px]">已撤销</NTag>
|
||||
<NTag v-if="p.supplement" size="small" type="warning" class="text-[10px]">补充</NTag>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
<!-- 右:归档包详情 -->
|
||||
<div v-if="sel" class="rounded-lg border col-span-2">
|
||||
<div class="border-b p-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold">{{ sel.id }}</span>
|
||||
<NTag v-if="sel.revoked" size="small" type="error">已撤销(保留归档备查)</NTag>
|
||||
<NTag v-else size="small" type="info">
|
||||
<template #icon>
|
||||
<Icon icon="uil:lock" class="size-14px" />
|
||||
</template>
|
||||
只读锁定
|
||||
</NTag>
|
||||
<NTag v-if="sel.supplement" size="small" type="warning">含归档补充</NTag>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-600">{{ sel.name }}</div>
|
||||
<div class="mt-2 flex flex-wrap justify-between gap-x-4 gap-y-1 text-[11px] text-slate-500">
|
||||
<span>归档路径:归档区/{{ sel.unit }}/{{ sel.year }}/{{ sel.id }}/</span>
|
||||
<span>关闭时间:{{ sel.closedAt }}</span>
|
||||
<span>共 {{ sel.files }} 个文件 · {{ sel.size }}</span>
|
||||
<span class="flex items-center gap-1 font-mono">
|
||||
<Icon icon="lucide:hash" class="size-12px" />
|
||||
{{ sel.hash }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<NButton size="small" class="text-xs" @click="downloadZip(sel)">
|
||||
<Icon icon="material-symbols:download" class="size-14px" />
|
||||
下载归档包 ZIP
|
||||
</NButton>
|
||||
<NButton size="small" class="text-xs" @click="exportEvidenceChain">导出证据链</NButton>
|
||||
<NButton v-if="!sel.revoked" size="small" class="text-xs" @click="applySupplement">发起归档补充申请</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y scroll-right">
|
||||
<div v-for="f in selFolders" :key="f.dir">
|
||||
<button @click="openFolders[f.dir] = !openFolders[f.dir]"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium hover:bg-slate-50 bg-transparent">
|
||||
<Icon icon="formkit:right" class="size-14px text-slate-400 transition-transform" :class="openFolders[f.dir] ? 'rotate-90' : ''" />
|
||||
<Icon icon="quill:folder-open" v-if="openFolders[f.dir]" class="size-16px text-[#6E93C6]" />
|
||||
<Icon v-else icon="material-symbols:folder-outline" class="size-16px text-slate-400" />
|
||||
{{ f.dir }}
|
||||
<span class="ml-auto text-[11px] font-normal text-slate-400">{{ f.files.length }} 个文件</span>
|
||||
</button>
|
||||
<template v-if="openFolders[f.dir]">
|
||||
<div v-for="file in f.files" :key="file.name" class="flex items-center gap-2 py-1.5 pl-10 pr-3 text-xs hover:bg-slate-50">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-slate-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ file.name }}</span>
|
||||
<span class="text-[11px] text-slate-400">{{ file.size }}</span>
|
||||
<NButton text type="primary" @click="previewFile(file)">
|
||||
<Icon icon="lucide:eye" class="size-14px" />
|
||||
</NButton>
|
||||
<NButton text type="primary" @click="downloadFile(file)">
|
||||
<Icon icon="material-symbols:download" class="size-16px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs hover:bg-slate-50">
|
||||
<Icon icon="akar-icons:file" class="size-14px text-slate-400" />
|
||||
<span class="flex-1 font-mono">归档索引.txt</span>
|
||||
<span class="text-[11px] text-slate-400">清单与哈希</span>
|
||||
<NButton text type="primary" @click="viewIndex">
|
||||
<Icon icon="lucide:eye" class="size-14px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll-right {
|
||||
height: calc(100vh - 460px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user