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

@@ -8,8 +8,8 @@ import { MODULES_API_URL } from '@/config/setting';
*/
export async function pageAiCloudFile(params: AiCloudFileParam) {
const res = await request.get<ApiResult<PageResult<AiCloudFile>>>(
MODULES_API_URL + '/ai/file/page',
{ params }
MODULES_API_URL + '/ai/file/page',
{ params }
);
if (res.data.code === 0) {
return res.data.data;
@@ -22,8 +22,8 @@ export async function pageAiCloudFile(params: AiCloudFileParam) {
*/
export async function listAiCloudFile(params?: AiCloudFileParam) {
const res = await request.get<ApiResult<AiCloudFile[]>>(
MODULES_API_URL + '/ai/file',
{ params }
MODULES_API_URL + '/ai/file',
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -34,22 +34,28 @@ 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();
formData.append('docId', docId);
formData.append('categoryId', categoryId);
files.forEach(file => {
files.forEach((file) => {
formData.append('files', file);
});
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/ai/file/uploadFiles',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
MODULES_API_URL + '/ai/file/uploadFiles',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress
}
);
if (res.data.code === 0) {
return res.data.message;
@@ -62,7 +68,7 @@ export async function uploadFiles(docId: number, categoryId: string, files: File
*/
export async function removeAiCloudFile(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/ai/file/' + id
MODULES_API_URL + '/ai/file/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -75,7 +81,7 @@ export async function removeAiCloudFile(id?: number) {
*/
export async function getAiCloudFile(id: number) {
const res = await request.get<ApiResult<AiCloudFile>>(
MODULES_API_URL + '/ai/file/' + id
MODULES_API_URL + '/ai/file/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -88,8 +94,8 @@ export async function getAiCloudFile(id: number) {
*/
export async function updateAiCloudFile(data: AiCloudFile) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/ai/file',
data
MODULES_API_URL + '/ai/file',
data
);
if (res.data.code === 0) {
return res.data.message;

View File

@@ -1,114 +1,197 @@
<!-- 文档导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
:title="`批量导入文档`"
:visible="visible"
@update:visible="updateVisible"
:width="520"
:footer="null"
:title="`批量导入文档`"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<div v-if="queuedFileNames.length" class="upload-queue-info">
已加入 {{ queuedFileNames.length }} 个文件
</div>
<a-upload-dragger
accept=".pdf,.doc,.docx,.txt,.md,.xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
:multiple="true"
style="padding: 24px 0; margin-bottom: 16px"
accept=".pdf,.doc,.docx,.txt,.md,.xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
:multiple="true"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</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 等文档格式
</p>
</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>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { debounce } from 'lodash-es';
import { uploadFiles } from '@/api/ai/aiCloudFile';
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { debounce } from 'lodash-es';
import { uploadFiles } from '@/api/ai/aiCloudFile';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 在顶层定义 props - 修改为接收 doc 对象
const props = defineProps<{
visible: boolean;
doc: { // 改为接收完整的 doc 对象
id: number;
categoryId: string;
// 在顶层定义 props - 修改为接收 doc 对象
const props = defineProps<{
visible: boolean;
doc: {
// 改为接收完整的 doc 对象
id: number;
categoryId: string;
};
}>();
const loading = ref(false);
const uploadProgress = ref(0);
const queuedFileNames = ref<string[]>([]);
const progressText = computed(() => {
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;
const files = [...uploadQueue];
queuedFileNames.value = files.map((file) => file.name);
uploadQueue.length = 0; // 清空队列
loading.value = true;
uploadProgress.value = 0;
try {
// 修改:传入 doc.id 和 doc.categoryId
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 || '上传成功');
updateVisible(false);
emit('done');
} catch (e: any) {
message.error(e.message || '上传失败');
} finally {
loading.value = false;
window.setTimeout(() => {
uploadProgress.value = 0;
queuedFileNames.value = [];
}, 600);
}
}, 500);
// 修改后的上传方法
const doUpload = ({ file }: { file: File }) => {
// 检查文件类型
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/markdown',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const allowedExtensions = [
'.pdf',
'.doc',
'.docx',
'.txt',
'.md',
'.xls',
'.xlsx'
];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
if (
!allowedTypes.includes(file.type) &&
!allowedExtensions.includes(fileExtension)
) {
message.error('只能上传文档文件PDF、Word、Excel、TXT、MD');
return false;
}
if (file.size / 1024 / 1024 > 100) {
message.error('文件大小不能超过 100MB');
return false;
}
// 将文件加入队列并触发防抖上传
uploadQueue.push(file);
handleBatchUpload();
return false;
};
}>();
const loading = ref(false);
// 新增文件队列
const uploadQueue: File[] = [];
// 新增批量上传方法 - 修改调用参数
const handleBatchUpload = debounce(async () => {
if (uploadQueue.length === 0) return;
/* 更新 visible */
const updateVisible = (value: boolean) => {
if (!value && !loading.value) {
uploadProgress.value = 0;
queuedFileNames.value = [];
uploadQueue.length = 0;
}
emit('update:visible', value);
};
</script>
const files = [...uploadQueue];
uploadQueue.length = 0; // 清空队列
loading.value = true;
try {
// 修改:传入 doc.id 和 doc.categoryId
const msg = await uploadFiles(props.doc.id, props.doc.categoryId, files);
message.success(msg || '上传成功');
updateVisible(false);
emit('done');
} catch (e: any) {
message.error(e.message || '上传失败');
} finally {
loading.value = false;
}
}, 500);
// 修改后的上传方法
const doUpload = ({ file }: { file: File }) => {
// 检查文件类型
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/markdown',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.md', '.xls', '.xlsx'];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
message.error('只能上传文档文件PDF、Word、Excel、TXT、MD');
return false;
<style lang="less" scoped>
.upload-queue-info {
margin-bottom: 12px;
color: #666;
font-size: 12px;
}
if (file.size / 1024 / 1024 > 100) {
message.error('文件大小不能超过 100MB');
return false;
.upload-progress-wrapper {
margin-top: 12px;
}
// 将文件加入队列并触发防抖上传
uploadQueue.push(file);
handleBatchUpload();
return false;
};
// 新增文件队列
const uploadQueue: File[] = [];
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
.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-divider type="vertical" />
<!-- 文档管理按钮 -->
<a
:class="{ 'disabled-text': !record.kbId }"
:style="!record.kbId ? 'cursor: not-allowed; color: #999' : ''"
@click="record.kbId && openDocManage(record)"
>文档管理</a
>
<a-divider type="vertical" />
<!-- <a-->
<!-- :class="{ 'disabled-text': !record.kbId }"-->
<!-- :style="!record.kbId ? 'cursor: not-allowed; color: #999' : ''"-->
<!-- @click="record.kbId && openDocManage(record)"-->
<!-- >文档管理</a-->
<!-- >-->
<!-- <a-divider type="vertical" />-->
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"

View File

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

View File

@@ -268,11 +268,7 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a
:href="`http://view.officeapps.live.com/op/view.aspx?src=${record.fileUrl}`"
target="_blank"
>{{ record.fileName }}</a
>
<a @click="openDocPreview(record)">{{ record.fileName }}</a>
</template>
<template v-if="column.key === 'action'">
<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 = () => {
selectedDocRowKeys.value = [];
selectedDocRows.value = [];