|
|
|
@ -9,11 +9,13 @@ |
|
|
|
v-model="uploadFolderId" |
|
|
|
:data="folderTreeData" |
|
|
|
:keys="{ value: 'value', label: 'label', children: 'children' }" |
|
|
|
:disabled="!uploadCategory" |
|
|
|
clearable |
|
|
|
filterable |
|
|
|
placeholder="选择目录(可选,默认分类根)" |
|
|
|
placeholder="请先选择分类" |
|
|
|
size="small" |
|
|
|
style="width:240px;" |
|
|
|
@change="onFolderChange" |
|
|
|
/> |
|
|
|
<div class="tag-input-wrapper"> |
|
|
|
<t-tag v-for="(tag, idx) in tagList" :key="idx" closable size="small" @close="removeTag(idx)">{{ tag }}</t-tag> |
|
|
|
@ -50,13 +52,13 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, computed, onMounted } from 'vue' |
|
|
|
import { ref, computed, watch, h } from 'vue' |
|
|
|
import { DialogPlugin } from 'tdesign-vue-next' |
|
|
|
import type { UploadFile } from 'tdesign-vue-next' |
|
|
|
import { useCategoryStore } from '@/stores/category' |
|
|
|
import { useDocumentStore } from '@/stores/document' |
|
|
|
import { uploadFile, uploadMarkdown, uploadJsonBasic, uploadFolder } from '@/api/upload' |
|
|
|
import { getFolderList } from '@/api/folder' |
|
|
|
import { getCategoryTree } from '@/api/category' |
|
|
|
import { getFolderTree } from '@/api/folder' |
|
|
|
import { toast } from '@/utils/toast' |
|
|
|
|
|
|
|
const categoryStore = useCategoryStore() |
|
|
|
@ -99,9 +101,9 @@ function handleTagBackspace() { |
|
|
|
if (!tagInputValue.value && tagList.value.length > 0) removeTag(tagList.value.length - 1) |
|
|
|
} |
|
|
|
|
|
|
|
/** 去掉 'folder:' 前缀,取真实目录 id */ |
|
|
|
/** 去掉 'folder:' 前缀取真实目录 id;非目录节点(如分类)返回空串,避免误传 */ |
|
|
|
function stripFolderPrefix(value: string): string { |
|
|
|
return value.replace(/^folder:/, '') |
|
|
|
return value.startsWith('folder:') ? value.slice('folder:'.length) : '' |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -112,7 +114,10 @@ function buildFormData(file: File, includeFolder = false): FormData { |
|
|
|
const fd = new FormData() |
|
|
|
fd.append('file', file) |
|
|
|
if (uploadCategory.value) fd.append('categoryId', uploadCategory.value) |
|
|
|
if (includeFolder && uploadFolderId.value) fd.append('folderId', stripFolderPrefix(uploadFolderId.value)) |
|
|
|
if (includeFolder) { |
|
|
|
const fid = stripFolderPrefix(uploadFolderId.value) |
|
|
|
if (fid) fd.append('folderId', fid) |
|
|
|
} |
|
|
|
if (tagList.value.length) fd.append('tags', tagList.value.join(',')) |
|
|
|
return fd |
|
|
|
} |
|
|
|
@ -183,62 +188,82 @@ async function uploadText() { |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 目录树(t-tree-select 用) ==================== |
|
|
|
/** 组装「分类 → 目录」可选树:分类节点禁用仅作分组,目录节点可选中 */ |
|
|
|
function buildFolderSelectTree(cats: any[], folders: any[]): any[] { |
|
|
|
const byCat = new Map<string, any[]>() |
|
|
|
for (const f of folders || []) { |
|
|
|
const cid = String(f.categoryId ?? '') |
|
|
|
if (!byCat.has(cid)) byCat.set(cid, []) |
|
|
|
byCat.get(cid)!.push(f) |
|
|
|
} |
|
|
|
|
|
|
|
function buildFolderTree(list: any[]): any[] { |
|
|
|
const map = new Map<string, any>() |
|
|
|
list.forEach(f => map.set(String(f.id), { ...f, children: [] })) |
|
|
|
const roots: any[] = [] |
|
|
|
map.forEach(node => { |
|
|
|
const pid = String(node.parentId ?? '') |
|
|
|
if (pid && pid !== '0' && map.has(pid)) map.get(pid)!.children.push(node) |
|
|
|
else roots.push(node) |
|
|
|
}) |
|
|
|
return roots |
|
|
|
} |
|
|
|
|
|
|
|
function folderNode(f: any): any { |
|
|
|
return { value: 'folder:' + f.id, label: f.name, children: (f.children || []).map(folderNode) } |
|
|
|
// 分类 = 盘符(根),目录 = 盘符下的文件夹;有分类才可能有目录,故目录树只含目录、不含分类节点 |
|
|
|
/** 目录节点 → tree-select 选项 */ |
|
|
|
function folderNodeToOption(n: any): any { |
|
|
|
return { |
|
|
|
value: 'folder:' + n.id, |
|
|
|
label: n.name, |
|
|
|
categoryId: n.categoryId, |
|
|
|
children: (n.children || []).map(folderNodeToOption), |
|
|
|
} |
|
|
|
|
|
|
|
function categoryNode(c: any): any { |
|
|
|
return { |
|
|
|
value: 'cat:' + c.id, |
|
|
|
label: c.name, |
|
|
|
disabled: true, |
|
|
|
children: [ |
|
|
|
...buildFolderTree(byCat.get(String(c.id)) || []).map(folderNode), |
|
|
|
...(c.children || []).map(categoryNode), |
|
|
|
], |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return (cats || []).map(categoryNode) |
|
|
|
} |
|
|
|
|
|
|
|
/** 加载当前分类下的目录树;未选分类时目录选择器为空/禁用 */ |
|
|
|
async function loadFolderTree() { |
|
|
|
const catId = uploadCategory.value |
|
|
|
if (!catId) { folderTreeData.value = []; return } |
|
|
|
try { |
|
|
|
const [catRes, folderRes] = await Promise.all([getCategoryTree(), getFolderList()]) |
|
|
|
const cats = catRes.success ? (catRes.data || []) : [] |
|
|
|
const folders = folderRes.success ? (folderRes.data || []) : [] |
|
|
|
folderTreeData.value = buildFolderSelectTree(cats, folders) |
|
|
|
const res = await getFolderTree(catId) |
|
|
|
folderTreeData.value = res.success ? (res.data || []).map(folderNodeToOption) : [] |
|
|
|
} catch (e: any) { |
|
|
|
toast('加载目录失败:' + e.message, 'error') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** 分类变化 → 清空目录并刷新目录树(分类是目录的前提) */ |
|
|
|
watch(uploadCategory, () => { |
|
|
|
uploadFolderId.value = '' |
|
|
|
loadFolderTree() |
|
|
|
}) |
|
|
|
|
|
|
|
/** 目录选中 → 回填分类,保持两者一致 */ |
|
|
|
function onFolderChange(_value: any, ctx: any) { |
|
|
|
if (ctx?.trigger === 'check' && ctx?.data?.categoryId != null) { |
|
|
|
uploadCategory.value = String(ctx.data.categoryId) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 文件夹上传 ==================== |
|
|
|
function pickFolder() { |
|
|
|
folderInput.value?.click() |
|
|
|
} |
|
|
|
|
|
|
|
/** 将 details 按失败原因分组,返回 [{ error, files: string[] }] */ |
|
|
|
function groupFailures(details: Array<{ file?: string; error?: string }> = []) { |
|
|
|
const map = new Map<string, string[]>() |
|
|
|
for (const d of details) { |
|
|
|
const err = d?.error || '未知错误' |
|
|
|
const file = d?.file || '未知文件' |
|
|
|
if (!map.has(err)) map.set(err, []) |
|
|
|
map.get(err)!.push(file) |
|
|
|
} |
|
|
|
return Array.from(map.entries()).map(([error, files]) => ({ error, files })) |
|
|
|
} |
|
|
|
|
|
|
|
/** 弹窗展示文件夹上传结果(成功/失败数 + 按原因分组的失败文件清单) */ |
|
|
|
function showFolderUploadResult(successCount: number, failCount: number, details: any[]) { |
|
|
|
const groups = groupFailures(details) |
|
|
|
const body = h('div', { style: 'max-height:320px;overflow-y:auto;' }, [ |
|
|
|
h('p', { style: 'margin:0 0 12px;' }, `成功 ${successCount} 个,失败 ${failCount} 个`), |
|
|
|
...groups.map((g) => h('div', { style: 'margin-bottom:12px;' }, [ |
|
|
|
h('div', { style: 'color:var(--td-error-color);font-weight:600;margin-bottom:4px;' }, g.error), |
|
|
|
h('ul', { style: 'margin:0;padding-left:18px;color:var(--td-text-color-secondary);font-size:12px;' }, |
|
|
|
g.files.slice(0, 10).map((f) => h('li', {}, f))), |
|
|
|
g.files.length > 10 |
|
|
|
? h('div', { style: 'color:var(--td-text-color-placeholder);font-size:12px;' }, `…等 ${g.files.length} 个文件`) |
|
|
|
: null, |
|
|
|
])), |
|
|
|
]) |
|
|
|
const dlg = DialogPlugin.alert({ |
|
|
|
header: '文件夹上传结果', |
|
|
|
body, |
|
|
|
confirmBtn: '知道了', |
|
|
|
// 确认按钮默认只触发 onConfirm、不自动关闭,需手动 hide |
|
|
|
onConfirm: () => { dlg.hide() }, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
async function onFolderSelected(e: Event) { |
|
|
|
const input = e.target as HTMLInputElement |
|
|
|
const list = Array.from(input.files || []) |
|
|
|
@ -265,17 +290,14 @@ async function onFolderSelected(e: Event) { |
|
|
|
if (skipped.length) toast(`已跳过 ${skipped.length} 个不支持的文件:${skipped.join('、')}`, 'warning') |
|
|
|
if (valid.length === 0) { toast('所选文件夹内没有可上传的文件', 'error'); input.value = ''; return } |
|
|
|
|
|
|
|
// 构造 FormData:files + relativePaths(去掉所选文件夹根名) |
|
|
|
// 构造 FormData:files + relativePaths(保留完整相对路径,含所选文件夹根名,后端据此建目录树) |
|
|
|
const fd = new FormData() |
|
|
|
valid.forEach(f => fd.append('files', f)) |
|
|
|
const relativePaths: string[] = valid.map(f => { |
|
|
|
const p = (f as any).webkitRelativePath || '' |
|
|
|
const idx = p.indexOf('/') |
|
|
|
return idx >= 0 ? p.slice(idx + 1) : f.name |
|
|
|
}) |
|
|
|
const relativePaths: string[] = valid.map(f => (f as any).webkitRelativePath || f.name) |
|
|
|
relativePaths.forEach(p => fd.append('relativePaths', p)) |
|
|
|
if (uploadCategory.value) fd.append('categoryId', uploadCategory.value) |
|
|
|
if (uploadFolderId.value) fd.append('folderId', stripFolderPrefix(uploadFolderId.value)) |
|
|
|
const fid = stripFolderPrefix(uploadFolderId.value) |
|
|
|
if (fid) fd.append('folderId', fid) |
|
|
|
if (tagList.value.length) fd.append('tags', tagList.value.join(',')) |
|
|
|
|
|
|
|
folderUploading.value = true |
|
|
|
@ -285,8 +307,10 @@ async function onFolderSelected(e: Event) { |
|
|
|
if (r.success) { |
|
|
|
const successCount = r.successCount ?? r.data?.successCount |
|
|
|
const failCount = r.failCount ?? r.data?.failCount ?? 0 |
|
|
|
const details = r.data?.details ?? [] |
|
|
|
if (failCount > 0) { |
|
|
|
toast(`文件夹上传完成:成功 ${successCount ?? '?'} 个,失败 ${failCount} 个`, 'warning') |
|
|
|
showFolderUploadResult(successCount ?? 0, failCount, details) |
|
|
|
} else { |
|
|
|
toast(`文件夹上传完成:成功 ${successCount ?? valid.length} 个`, 'success') |
|
|
|
} |
|
|
|
@ -303,7 +327,6 @@ async function onFolderSelected(e: Event) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
onMounted(async () => { await loadFolderTree() }) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
|