1、文档新增批量删除

2、文档新增点击文件名预览
This commit is contained in:
2026-05-27 14:31:52 +08:00
parent f1a9f53099
commit c61328a81e
5 changed files with 289 additions and 142 deletions

View File

@@ -34,11 +34,16 @@ export async function listAiCloudFile(params?: AiCloudFileParam) {
/** /**
* 上传文件到云中心 * 上传文件到云中心
*/ */
export async function uploadFiles(docId: number, categoryId: string, files: File[]) { export async function uploadFiles(
docId: number,
categoryId: string,
files: File[],
onUploadProgress?: (event: ProgressEvent) => void
) {
const formData = new FormData(); const formData = new FormData();
formData.append('docId', docId); formData.append('docId', docId);
formData.append('categoryId', categoryId); formData.append('categoryId', categoryId);
files.forEach(file => { files.forEach((file) => {
formData.append('files', file); formData.append('files', file);
}); });
@@ -48,7 +53,8 @@ export async function uploadFiles(docId: number, categoryId: string, files: File
{ {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} },
onUploadProgress
} }
); );
if (res.data.code === 0) { if (res.data.code === 0) {

View File

@@ -8,6 +8,9 @@
@update:visible="updateVisible" @update:visible="updateVisible"
> >
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div v-if="queuedFileNames.length" class="upload-queue-info">
已加入 {{ queuedFileNames.length }} 个文件
</div>
<a-upload-dragger <a-upload-dragger
accept=".pdf,.doc,.docx,.txt,.md,.xls,.xlsx" accept=".pdf,.doc,.docx,.txt,.md,.xls,.xlsx"
:show-upload-list="false" :show-upload-list="false"
@@ -19,48 +22,87 @@
<cloud-upload-outlined /> <cloud-upload-outlined />
</p> </p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p> <p class="ant-upload-hint">将文件拖到此处或点击上传</p>
<p class="ant-upload-hint" style="font-size: 12px; color: #999;"> <p class="ant-upload-hint" style="font-size: 12px; color: #999">
支持格式PDFWordExcelTXTMD 等文档格式 支持格式PDFWordExcelTXTMD 等文档格式
</p> </p>
</a-upload-dragger> </a-upload-dragger>
<div v-if="loading || uploadProgress > 0" class="upload-progress-wrapper">
<div class="upload-progress-header">
<span>{{ progressText }}</span>
<span>{{ uploadProgress }}%</span>
</div>
<a-progress
:percent="uploadProgress"
:status="uploadProgress === 100 ? 'success' : 'active'"
/>
</div>
</a-spin> </a-spin>
</ele-modal> </ele-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue'; import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { uploadFiles } from '@/api/ai/aiCloudFile'; import { uploadFiles } from '@/api/ai/aiCloudFile';
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'done'): void; (e: 'done'): void;
(e: 'update:visible', visible: boolean): void; (e: 'update:visible', visible: boolean): void;
}>(); }>();
// 在顶层定义 props - 修改为接收 doc 对象 // 在顶层定义 props - 修改为接收 doc 对象
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
doc: { // 改为接收完整的 doc 对象 doc: {
// 改为接收完整的 doc 对象
id: number; id: number;
categoryId: string; categoryId: string;
}; };
}>(); }>();
const loading = ref(false); const loading = ref(false);
const uploadProgress = ref(0);
const queuedFileNames = ref<string[]>([]);
// 新增批量上传方法 - 修改调用参数 const progressText = computed(() => {
const handleBatchUpload = debounce(async () => { if (loading.value) {
return queuedFileNames.value.length
? `正在上传 ${queuedFileNames.value.length} 个文件`
: '正在上传文件';
}
if (uploadProgress.value === 100) {
return '上传完成';
}
return '等待上传';
});
// 新增批量上传方法 - 修改调用参数
const handleBatchUpload = debounce(async () => {
if (uploadQueue.length === 0) return; if (uploadQueue.length === 0) return;
const files = [...uploadQueue]; const files = [...uploadQueue];
queuedFileNames.value = files.map((file) => file.name);
uploadQueue.length = 0; // 清空队列 uploadQueue.length = 0; // 清空队列
loading.value = true; loading.value = true;
uploadProgress.value = 0;
try { try {
// 修改:传入 doc.id 和 doc.categoryId // 修改:传入 doc.id 和 doc.categoryId
const msg = await uploadFiles(props.doc.id, props.doc.categoryId, files); const msg = await uploadFiles(
props.doc.id,
props.doc.categoryId,
files,
(event) => {
if (!event.total) return;
uploadProgress.value = Math.min(
100,
Math.max(0, Math.round((event.loaded / event.total) * 100))
);
}
);
uploadProgress.value = 100;
message.success(msg || '上传成功'); message.success(msg || '上传成功');
updateVisible(false); updateVisible(false);
emit('done'); emit('done');
@@ -68,11 +110,15 @@ const handleBatchUpload = debounce(async () => {
message.error(e.message || '上传失败'); message.error(e.message || '上传失败');
} finally { } finally {
loading.value = false; loading.value = false;
window.setTimeout(() => {
uploadProgress.value = 0;
queuedFileNames.value = [];
}, 600);
} }
}, 500); }, 500);
// 修改后的上传方法 // 修改后的上传方法
const doUpload = ({ file }: { file: File }) => { const doUpload = ({ file }: { file: File }) => {
// 检查文件类型 // 检查文件类型
const allowedTypes = [ const allowedTypes = [
'application/pdf', 'application/pdf',
@@ -84,10 +130,21 @@ const doUpload = ({ file }: { file: File }) => {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
]; ];
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.md', '.xls', '.xlsx']; const allowedExtensions = [
'.pdf',
'.doc',
'.docx',
'.txt',
'.md',
'.xls',
'.xlsx'
];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) { if (
!allowedTypes.includes(file.type) &&
!allowedExtensions.includes(fileExtension)
) {
message.error('只能上传文档文件PDF、Word、Excel、TXT、MD'); message.error('只能上传文档文件PDF、Word、Excel、TXT、MD');
return false; return false;
} }
@@ -102,13 +159,39 @@ const doUpload = ({ file }: { file: File }) => {
handleBatchUpload(); handleBatchUpload();
return false; return false;
}; };
// 新增文件队列 // 新增文件队列
const uploadQueue: File[] = []; const uploadQueue: File[] = [];
/* 更新 visible */ /* 更新 visible */
const updateVisible = (value: boolean) => { const updateVisible = (value: boolean) => {
if (!value && !loading.value) {
uploadProgress.value = 0;
queuedFileNames.value = [];
uploadQueue.length = 0;
}
emit('update:visible', value); emit('update:visible', value);
}; };
</script> </script>
<style lang="less" scoped>
.upload-queue-info {
margin-bottom: 12px;
color: #666;
font-size: 12px;
}
.upload-progress-wrapper {
margin-top: 12px;
}
.upload-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
color: #666;
font-size: 12px;
}
</style>

View File

@@ -44,13 +44,13 @@
<a @click="openEdit(record)">修改</a> <a @click="openEdit(record)">修改</a>
<a-divider type="vertical" /> <a-divider type="vertical" />
<!-- 文档管理按钮 --> <!-- 文档管理按钮 -->
<a <!-- <a-->
:class="{ 'disabled-text': !record.kbId }" <!-- :class="{ 'disabled-text': !record.kbId }"-->
:style="!record.kbId ? 'cursor: not-allowed; color: #999' : ''" <!-- :style="!record.kbId ? 'cursor: not-allowed; color: #999' : ''"-->
@click="record.kbId && openDocManage(record)" <!-- @click="record.kbId && openDocManage(record)"-->
>文档管理</a <!-- >文档管理</a-->
> <!-- >-->
<a-divider type="vertical" /> <!-- <a-divider type="vertical" />-->
<a-popconfirm <a-popconfirm
title="确定要删除此记录吗?" title="确定要删除此记录吗?"
@confirm="remove(record)" @confirm="remove(record)"

View File

@@ -24,12 +24,14 @@
:tabBarStyle="{ margin: 0 }" :tabBarStyle="{ margin: 0 }"
@change="handleSourceChange" @change="handleSourceChange"
> >
<a-tab-pane key="company" tab="公司文档" /> <a-tab-pane key="company" tab="项目文档" />
<a-tab-pane key="public" tab="公共库" /> <a-tab-pane key="public" tab="公共库" />
</a-tabs> </a-tabs>
</div> </div>
<div class="dir-header"> <div class="dir-header">
<span>{{ activeSource === 'company' ? '公司文档目录' : '公共库目录' }}</span> <span>{{
activeSource === 'company' ? '项目文档目录' : '公共库目录'
}}</span>
</div> </div>
<div class="tree-container"> <div class="tree-container">
<a-tree <a-tree
@@ -62,7 +64,10 @@
<span class="doc-tips" <span class="doc-tips"
>当前已选择 {{ checkedDirKeys.length }} 个目录, >当前已选择 {{ checkedDirKeys.length }} 个目录,
{{ selectedFileList.length }} 个文件合计 {{ selectedFileList.length }} 个文件合计
{{ mergedDirKeys.length }} 个目录{{ mergedFileKeys.length }} 个文件</span {{ mergedDirKeys.length }} 个目录{{
mergedFileKeys.length
}}
个文件</span
> >
<a-space> <a-space>
<a-button @click="clearSelection">清空当前</a-button> <a-button @click="clearSelection">清空当前</a-button>
@@ -79,9 +84,7 @@
:loading="docLoading" :loading="docLoading"
rowKey="id" rowKey="id"
:scroll=" :scroll="
activeSource === 'public' activeSource === 'public' ? { x: 2300, y: 400 } : { y: 400 }
? { x: 2300, y: 400 }
: { y: 400 }
" "
:pagination="pagination" :pagination="pagination"
@change="handleTableChange" @change="handleTableChange"
@@ -307,7 +310,8 @@
width: 160, width: 160,
ellipsis: true, ellipsis: true,
customRender: ({ record, text }: { record: AiCloudFile; text: any }) => customRender: ({ record, text }: { record: AiCloudFile; text: any }) =>
text || renderDocText(record, ['version', 'versionNumber', 'versionName']) text ||
renderDocText(record, ['version', 'versionNumber', 'versionName'])
}, },
{ {
title: '成文日期', title: '成文日期',
@@ -352,7 +356,12 @@
width: 220, width: 220,
ellipsis: true, ellipsis: true,
customRender: ({ record, text }: { record: AiCloudFile; text: any }) => customRender: ({ record, text }: { record: AiCloudFile; text: any }) =>
text || renderDocText(record, ['relatedDocuments', 'relatedDoc', 'relatedFiles']) text ||
renderDocText(record, [
'relatedDocuments',
'relatedDoc',
'relatedFiles'
])
}, },
{ {
title: '适用区域', title: '适用区域',
@@ -382,7 +391,9 @@
]; ];
const currentDocColumns = computed(() => { const currentDocColumns = computed(() => {
return activeSource.value === 'public' ? publicDocColumns : companyDocColumns; return activeSource.value === 'public'
? publicDocColumns
: companyDocColumns;
}); });
// 计算树形数据 // 计算树形数据
@@ -406,12 +417,7 @@
return buildTree(0); return buildTree(0);
}); });
const selectionStateMap = ref< const selectionStateMap = ref<Record<string, FileSelectionStatePayload>>({});
Record<
string,
FileSelectionStatePayload
>
>({});
const selectedDocList = ref<string[]>([]); const selectedDocList = ref<string[]>([]);
const selectedFileList = ref<string[]>([]); const selectedFileList = ref<string[]>([]);
const selectedFileKeys = ref<SelectionKey[]>([]); const selectedFileKeys = ref<SelectionKey[]>([]);
@@ -491,7 +497,8 @@
sourceState.fileKeys = [...getAllSelectedFileKeys(sourceState)]; sourceState.fileKeys = [...getAllSelectedFileKeys(sourceState)];
sourceState.currentDirKey = sourceState.currentDirKey =
selectedKeys.value[0] !== undefined ? selectedKeys.value[0] : undefined; selectedKeys.value[0] !== undefined ? selectedKeys.value[0] : undefined;
selectionStateMap.value[getSelectionStorageKey()] = getMergedSelectionPayload(); selectionStateMap.value[getSelectionStorageKey()] =
getMergedSelectionPayload();
}; };
const restoreSelectionState = () => { const restoreSelectionState = () => {
@@ -528,9 +535,13 @@
const loadAllCloudDocs = async () => { const loadAllCloudDocs = async () => {
try { try {
const params = activeSource.value === 'public' const params =
activeSource.value === 'public'
? { companyId: 0 } ? { companyId: 0 }
: { companyId: props.currentCompanyId, projectId: props.currentProjectId }; : {
companyId: props.currentCompanyId,
projectId: props.currentProjectId
};
const result = await listAiCloudDoc(params); const result = await listAiCloudDoc(params);
allDirs.value = result || []; allDirs.value = result || [];
@@ -541,10 +552,14 @@
sourceState.dirFileSelections || {} sourceState.dirFileSelections || {}
).find((key) => { ).find((key) => {
const dirId = Number(key); const dirId = Number(key);
return sourceState.dirFileSelections[key]?.length > 0 && availableIds.has(dirId); return (
sourceState.dirFileSelections[key]?.length > 0 &&
availableIds.has(dirId)
);
}); });
const preferredSelectedKey = const preferredSelectedKey =
(selectedKeys.value[0] && availableIds.has(selectedKeys.value[0] as number) (selectedKeys.value[0] &&
availableIds.has(selectedKeys.value[0] as number)
? selectedKeys.value[0] ? selectedKeys.value[0]
: undefined) || : undefined) ||
(sourceState.currentDirKey !== undefined && (sourceState.currentDirKey !== undefined &&
@@ -640,7 +655,9 @@
addedKeys.map((key) => getDirFileKeys(key)) addedKeys.map((key) => getDirFileKeys(key))
); );
addedKeys.forEach((key, index) => { addedKeys.forEach((key, index) => {
sourceState.dirFileSelections[String(key)] = [...addedFileGroups[index]]; sourceState.dirFileSelections[String(key)] = [
...addedFileGroups[index]
];
}); });
removedKeys.forEach((key) => { removedKeys.forEach((key) => {
delete sourceState.dirFileSelections[String(key)]; delete sourceState.dirFileSelections[String(key)];
@@ -679,9 +696,7 @@
}; };
// 文件选择变化 // 文件选择变化
const onFileSelectionChange = ( const onFileSelectionChange = (selectedRowKeys: SelectionKey[]) => {
selectedRowKeys: SelectionKey[]
) => {
const currentDirKey = getCurrentDirKey(); const currentDirKey = getCurrentDirKey();
if (!currentDirKey) return; if (!currentDirKey) return;
@@ -689,9 +704,7 @@
sourceState.dirFileSelections[currentDirKey] = [...selectedRowKeys]; sourceState.dirFileSelections[currentDirKey] = [...selectedRowKeys];
sourceState.fileKeys = getAllSelectedFileKeys(sourceState); sourceState.fileKeys = getAllSelectedFileKeys(sourceState);
selectedFileKeys.value = selectedRowKeys; selectedFileKeys.value = selectedRowKeys;
selectedFileList.value = sourceState.fileKeys.map((key) => selectedFileList.value = sourceState.fileKeys.map((key) => key.toString());
key.toString()
);
saveSelectionState(); saveSelectionState();
}; };
@@ -766,5 +779,4 @@
}; };
defineExpose({ open }); defineExpose({ open });
</script> </script>

View File

@@ -268,11 +268,7 @@
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'"> <template v-if="column.key === 'fileName'">
<a <a @click="openDocPreview(record)">{{ record.fileName }}</a>
:href="`http://view.officeapps.live.com/op/view.aspx?src=${record.fileUrl}`"
target="_blank"
>{{ record.fileName }}</a
>
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a-space> <a-space>
@@ -629,6 +625,56 @@
); );
}; };
const getDocFileUrl = (record: AiCloudFile) => {
const value = getDocFieldValue(
record,
['fileUrl', 'downloadUrl', 'url'],
''
);
return value === '-' ? '' : String(value).trim();
};
const getDocFileExt = (record: AiCloudFile) => {
const candidates = [
record.fileExt,
record.fileType,
record.fileName,
getDocFileUrl(record)
].filter(Boolean) as string[];
for (const item of candidates) {
const cleanValue = item.split('?')[0];
const ext = cleanValue.includes('.')
? cleanValue.split('.').pop()
: cleanValue;
if (ext) {
return ext.toLowerCase().replace(/^\./, '');
}
}
return '';
};
const openDocPreview = (record: AiCloudFile) => {
const fileUrl = getDocFileUrl(record);
if (!fileUrl) {
message.warning('该文件暂无可预览地址');
return;
}
const ext = getDocFileExt(record);
const officePreviewExts = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
const previewUrl =
ext === 'pdf'
? fileUrl
: officePreviewExts.includes(ext)
? `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(
fileUrl
)}`
: fileUrl;
window.open(previewUrl, '_blank', 'noopener,noreferrer');
};
const resetDocSelection = () => { const resetDocSelection = () => {
selectedDocRowKeys.value = []; selectedDocRowKeys.value = [];
selectedDocRows.value = []; selectedDocRows.value = [];