资料档案库完成、去审批页面添加
This commit is contained in:
+604
-107
@@ -1,65 +1,463 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted, h } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { aiKnowledgeIngest, pollTask, AiError } from '@/service/api/ai';
|
||||
import { docListApi, historyVersionListApi } from '@/service/api/doc';
|
||||
import { NButton, NTag, NDropdown, useDialog } from 'naive-ui';
|
||||
import previewComponent from '@/components/previewFile/index.vue'
|
||||
import {
|
||||
docTreeListApi,
|
||||
addFolderApi,
|
||||
editFolderApi,
|
||||
deleteFolderApi,
|
||||
docListApi,
|
||||
uploadDocApi,
|
||||
updateDocApi,
|
||||
docHistoryListApi
|
||||
} from '@/service/api/doc';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const dialog = useDialog()
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
// 展开节点
|
||||
const openOrgs = ref<Record<string, boolean>>({});
|
||||
// 选中的资料库节点
|
||||
const libSel = ref({ org: "", cat: "", label: "" });
|
||||
// 当前选中的资料库列表
|
||||
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 fileLoading = ref<boolean>(false);
|
||||
const uploadDocRef = ref<any>(null);
|
||||
const updateDocRef = ref<any>(null);
|
||||
// 附件在线预览弹窗
|
||||
const previewModal = ref<boolean>(false);
|
||||
// 附件在线预览文件
|
||||
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 docList = ref<any[]>([]);
|
||||
// 历史版本列表
|
||||
const historyList = ref<any[]>([]);
|
||||
// 文件版本列表
|
||||
const fileList = ref<any[]>([]);
|
||||
// 搜索结果列表
|
||||
const queryList = ref<any[]>([]);
|
||||
// 展示数还是列表
|
||||
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) => {
|
||||
window.$message?.info(`演示:在线预览 ${d.name}`)
|
||||
// 自定义渲染展开图标
|
||||
const renderSwitcherIcon = (node:any) => {
|
||||
// 如果节点没有 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) => {
|
||||
window.$message?.success(`演示:下载 ${d.name}`)
|
||||
/**
|
||||
* 根据 key 找到节点,返回从自己到最顶层祖先的 label 数组(含自己)
|
||||
* @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 = () => {
|
||||
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
|
||||
/**
|
||||
* 根据 key 删除节点(原地修改原数组)
|
||||
* @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 = () => {
|
||||
if(!keyword.value) {
|
||||
// 搜索框为空时,显示树状列表
|
||||
showTree.value = true;
|
||||
return;
|
||||
}else{
|
||||
// 搜索框不为空时,显示搜索结果
|
||||
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:'',
|
||||
}
|
||||
}),
|
||||
};
|
||||
showTree.value = false;
|
||||
|
||||
}
|
||||
|
||||
// 处理顶层 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 () => {
|
||||
loading.value = true;
|
||||
const {data,error} = await docListApi();
|
||||
const {data,error} = await docTreeListApi();
|
||||
if(!error){
|
||||
docList.value = data ?? [];
|
||||
docList.value = buildTreeData(data);
|
||||
console.log(docList.value);
|
||||
if(docList.value.length){
|
||||
openOrgs.value = {[docList.value[0]?.org_name]:true};
|
||||
// libSel.value = {org: docList.value[0]?.org_name, cat: docList.value[0]?.categories[0]?.category, label: docList.value[0]?.categories[0]?.folders[0]?.folder};
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
// 根据文件夹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;
|
||||
}
|
||||
// 更新资料
|
||||
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(() => {
|
||||
getDocList();
|
||||
@@ -68,26 +466,26 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 我的草稿箱 -->
|
||||
<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' }">
|
||||
<NGrid :x-gap="16" :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>
|
||||
<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">
|
||||
<NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" clearable placeholder="全文检索…" @clear="keyword = ''; searchHandle();">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
|
||||
</template>
|
||||
</NInput>
|
||||
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
|
||||
<Icon icon="material-symbols:upload" class="size-16px" /> 上传资料
|
||||
</NButton>
|
||||
<div class="flex items-center gap-2">
|
||||
<NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" clearable placeholder="全文检索…" @clear="keyword = ''; searchHandle();">
|
||||
<template #suffix>
|
||||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
|
||||
</template>
|
||||
</NInput>
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
@@ -96,85 +494,180 @@ onMounted(() => {
|
||||
</div>
|
||||
<!-- 全文检索:跨文件夹平铺结果 -->
|
||||
<div v-if="!showTree">
|
||||
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ historyList.length }} 份</div>
|
||||
<div class="divide-y rounded-lg border bg-white">
|
||||
<div v-for="d in historyList" :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="historyList.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 docList" :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_name] = !openOrgs[o.org_name]">
|
||||
<Icon icon="formkit:right" class="size-14px transition-transform" :class="openOrgs[o.org_name] ? 'rotate-90' : ''" />
|
||||
<Icon v-if="openOrgs[o.org_name]" 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_name }}
|
||||
</button>
|
||||
<template v-if="openOrgs[o.org_name]">
|
||||
<div v-for="g in o.categories" :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.category }}</div>
|
||||
<button v-for="f in g.folders" :key="f.folder"
|
||||
@click="libSel = { org: o.org_name, cat: g.category, label: f.folder }"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-xs transition"
|
||||
: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'"
|
||||
: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.folder }}</span>
|
||||
<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>
|
||||
</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?.count ?? 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">
|
||||
<NSpin :show="fileLoading" size="small">
|
||||
<div class="mb-1.5 text-xs text-slate-400">全文检索「{{ keyword }}」:跨全部文件夹命中 {{ queryList.length }} 份</div>
|
||||
<div class="top-scroll">
|
||||
<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="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">
|
||||
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.version }}</NTag>
|
||||
<span>更新 {{ d?.updated_at?.slice(0, 10) }}</span>
|
||||
</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">
|
||||
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.version }}</NTag>
|
||||
<span>{{ d.path.join(' · ') }}</span>
|
||||
<span>更新 {{ d?.updated_at?.slice(0, 10) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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="download(d)">下载</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!libCur || libCur?.docs?.length===0" class="text-xs text-slate-400 text-center py-5">暂无数据</p>
|
||||
<div v-if="queryList.length === 0" class="py-6 text-center text-xs text-slate-400">未匹配到相关资料</div>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
</div>
|
||||
<!-- 文件夹树 + 当前文件夹资料 -->
|
||||
<div v-else class="flex gap-3">
|
||||
<!-- 左侧 -->
|
||||
<div class="w-230px border rounded-md p-2">
|
||||
<div class="scroll-left">
|
||||
<NTree
|
||||
block-line
|
||||
:data="docList"
|
||||
label-field="label"
|
||||
key-field="key"
|
||||
:expanded-keys="expandedKeys"
|
||||
:selected-keys="selectedKeys"
|
||||
:render-switcher-icon="renderSwitcherIcon"
|
||||
selectable
|
||||
:cancelable="false"
|
||||
:on-update:expanded-keys="handleExpandKeys"
|
||||
:on-update:selected-keys="handleSelectKeys"
|
||||
:node-props="treeProps"
|
||||
/>
|
||||
<NDropdown
|
||||
:show="menuVisible"
|
||||
:options="menuOptions"
|
||||
size="small"
|
||||
:x="menuX"
|
||||
:y="menuY"
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
@clickoutside="closeMenu"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</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">
|
||||
当前位置:
|
||||
<div class="flex items-center gap-1">
|
||||
<template v-for="(item,index) in currentPath" :key="index">
|
||||
<span v-if="index !== (currentPath.length - 1)">{{ item }} </span>
|
||||
<span v-else class="font-medium text-slate-600">{{ item }}</span>
|
||||
<Icon v-if="index !== (currentPath.length - 1)" icon="formkit:right" class="size-12px" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<span class="ml-auto">共 {{ fileList.length }} 份 · 版本受控</span>
|
||||
</div>
|
||||
<NSpin :show="fileLoading" size="small">
|
||||
<div class="scroll-right">
|
||||
<div class="divide-y rounded-md border bg-white">
|
||||
<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" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<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">
|
||||
<NTag size="small" type="info" class="text-[10px]">版本 {{ d.version }}</NTag>
|
||||
<span>更新 {{ d?.updated_at?.slice(0, 10) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3 text-xs">
|
||||
<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="download(d)">下载</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="fileList.length===0" class="text-xs text-slate-400 text-center py-5">暂无资料数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</NSpace>
|
||||
</template>
|
||||
@@ -184,11 +677,15 @@ onMounted(() => {
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.scroll-left {
|
||||
height: calc(100vh - 260px);
|
||||
height: calc(100vh - 276px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.scroll-right {
|
||||
height: calc(100vh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.top-scroll {
|
||||
height: calc(100vh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user