This commit is contained in:
amb
2026-09-01 21:43:27 +08:00
parent e47235dfff
commit 2d42b0e73d
7 changed files with 2035 additions and 1171 deletions
+320 -151
View File
@@ -7,6 +7,14 @@ import apiClient from '@/api/client'
const route = useRoute()
const kbId = route.params.id as string
const isMobile = ref(window.innerWidth <= 768)
const showMobileSidebar = ref(false)
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768
if (!isMobile.value) showMobileSidebar.value = false
})
const kb = ref<any>(null)
const docs = ref<any[]>([])
const categories = ref<any[]>([])
@@ -247,159 +255,160 @@ function getCategoryName(catId: string) {
</script>
<template>
<div v-if="kb" style="display: flex; gap: 24px; height: calc(100vh - 120px)">
<!-- 左侧目录树 -->
<div style="width: 300px; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #e4e7ed; padding-right: 20px">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px">
<h3 style="margin: 0; font-size: 16px">📁 目录结构</h3>
<el-button size="small" type="primary" @click="openAddCategory(null)">+ 新建</el-button>
<div v-if="kb">
<!-- 手机端顶部操作栏 -->
<div class="mobile-header">
<h1 style="margin: 0; font-size: 20px">{{ kb.name }}</h1>
<div style="display: flex; gap: 8px; margin-top: 10px">
<el-button @click="handleCopyLink" size="small" style="flex: 1">📋 复制链接</el-button>
<el-button type="warning" @click="handleRegenerateLink" size="small" style="flex: 1">🔄 重置链接</el-button>
</div>
<!-- 全部文档 -->
<div
style="padding: 10px 12px; cursor: pointer; border-radius: 6px; margin-bottom: 8px; font-size: 14px; transition: all 0.2s"
:style="{ background: !selectedCategoryId ? '#ecf5ff' : '#f5f7fa', color: !selectedCategoryId ? '#409eff' : '#333', fontWeight: !selectedCategoryId ? 'bold' : 'normal' }"
@click="clearCategoryFilter"
>
📋 全部文档
</div>
<!-- 目录树 -->
<el-tree
:data="categories"
node-key="id"
default-expand-all
:expand-on-click-node="false"
:props="{ children: 'children', label: 'name' }"
>
<template #default="{ node, data }">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 6px 0">
<span
style="cursor: pointer; font-size: 14px; flex: 1"
:style="{ color: selectedCategoryId === data.id ? '#409eff' : '#333', fontWeight: selectedCategoryId === data.id ? 'bold' : 'normal' }"
@click="selectCategory(data)"
>
{{ data.is_folder ? '📁' : '📄' }} {{ data.name }}
<span v-if="data.doc_count > 0" style="color: #999; font-size: 12px; margin-left: 4px">({{ data.doc_count }})</span>
</span>
<span style="display: flex; gap: 2px">
<el-button size="small" text @click.stop="openAddCategory(data.id)" style="font-size: 12px">+</el-button>
<el-button size="small" text @click.stop="openEditCategory(data)" style="font-size: 12px"></el-button>
<el-button size="small" text type="danger" @click.stop="handleDeleteCategory(data)" style="font-size: 12px">×</el-button>
</span>
</div>
</template>
</el-tree>
</div>
<!-- 右侧文档列表 -->
<div style="flex: 1; overflow-y: auto">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px">
<h1 style="margin: 0; font-size: 24px">{{ kb.name }}</h1>
<div style="display: flex; gap: 12px">
<el-button @click="handleCopyLink" size="large">📋 复制 AI 链接</el-button>
<el-button type="warning" @click="handleRegenerateLink" size="large">🔄 重新生成链接</el-button>
<div class="main-layout">
<!-- 左侧目录树桌面端常驻手机端抽屉 -->
<aside class="sidebar" :class="{ 'mobile-show': showMobileSidebar }">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px">
<h3 style="margin: 0; font-size: 16px">📁 目录</h3>
<el-button size="small" type="primary" @click="openAddCategory(null)">+ 新建</el-button>
</div>
</div>
<!-- AI 链接显示 -->
<el-card style="margin-bottom: 20px" shadow="hover">
<div style="display: flex; align-items: center; gap: 12px">
<span style="font-weight: bold; font-size: 14px; white-space: nowrap">AI 访问链接</span>
<el-input :model-value="aiUrl" readonly style="flex: 1" size="large">
<template #append>
<el-button @click="handleCopyLink">复制</el-button>
</template>
</el-input>
<div
class="cat-item"
:class="{ active: !selectedCategoryId }"
@click="clearCategoryFilter"
>
📋 全部文档
</div>
</el-card>
<el-card shadow="hover">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-size: 16px; font-weight: bold">
📄 文档列表
<el-tag v-if="selectedCategoryPath" closable @close="clearCategoryFilter" style="margin-left: 8px" size="large">
{{ selectedCategoryPath }}
</el-tag>
</span>
<div style="display: flex; gap: 10px">
<el-button size="large" @click="showTextDialog = true"> 添加文本</el-button>
<el-upload :show-file-list="false" :http-request="handleUpload" accept=".docx,.pdf">
<el-button type="primary" :loading="uploading" size="large">📤 上传文档</el-button>
</el-upload>
<el-tree :data="categories" node-key="id" default-expand-all :expand-on-click-node="false">
<template #default="{ data }">
<div class="tree-node">
<span class="tree-label" :class="{ selected: selectedCategoryId === data.id }" @click="selectCategory(data)">
{{ data.is_folder ? '📁' : '📄' }} {{ data.name }}
<span v-if="data.doc_count > 0" style="color: #999; font-size: 12px">({{ data.doc_count }})</span>
</span>
<span class="tree-actions">
<el-button size="small" text @click.stop="openAddCategory(data.id)">+</el-button>
<el-button size="small" text @click.stop="openEditCategory(data)"></el-button>
<el-button size="small" text type="danger" @click.stop="handleDeleteCategory(data)">×</el-button>
</span>
</div>
</template>
</el-tree>
</aside>
<!-- 手机端遮罩 -->
<div v-if="showMobileSidebar" class="mobile-overlay" @click="showMobileSidebar = false"></div>
<!-- 右侧文档列表 -->
<main class="content">
<!-- 桌面端标题 -->
<div class="desktop-header">
<h1 style="margin: 0; font-size: 24px">{{ kb.name }}</h1>
<div style="display: flex; gap: 12px">
<el-button @click="handleCopyLink" size="large">📋 复制 AI 链接</el-button>
<el-button type="warning" @click="handleRegenerateLink" size="large">🔄 重新生成链接</el-button>
</div>
</div>
<!-- AI 链接 -->
<el-card style="margin-bottom: 16px" shadow="hover">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap">
<span style="font-weight: bold; font-size: 14px">AI 链接</span>
<el-input :model-value="aiUrl" readonly style="flex: 1; min-width: 200px" size="large">
<template #append>
<el-button @click="handleCopyLink">复制</el-button>
</template>
</el-input>
</div>
</el-card>
<!-- 手机端目录按钮 + 操作按钮 -->
<div class="mobile-actions">
<el-button @click="showMobileSidebar = true" style="flex: 1">📁 目录</el-button>
<el-button @click="showTextDialog = true" style="flex: 1"> 添加文本</el-button>
<el-upload :show-file-list="false" :http-request="handleUpload" accept=".docx,.pdf" style="flex: 1">
<el-button type="primary" :loading="uploading" style="width: 100%">📤 上传</el-button>
</el-upload>
</div>
<el-card shadow="hover">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px">
<span style="font-size: 16px; font-weight: bold">
📄 文档列表
<el-tag v-if="selectedCategoryPath" closable @close="clearCategoryFilter" style="margin-left: 8px">
{{ selectedCategoryPath }}
</el-tag>
</span>
<div class="desktop-actions">
<el-button @click="showTextDialog = true"> 添加文本</el-button>
<el-upload :show-file-list="false" :http-request="handleUpload" accept=".docx,.pdf">
<el-button type="primary" :loading="uploading">📤 上传文档</el-button>
</el-upload>
</div>
</div>
</template>
<!-- 桌面端表格 -->
<div class="desktop-table">
<el-table :data="docs" v-loading="loading" style="width: 100%">
<el-table-column prop="original_filename" label="标题" min-width="180" show-overflow-tooltip />
<el-table-column label="目录" width="180">
<template #default="{ row }">
<el-select :model-value="row.category_id" @change="(val: string) => handleChangeDocCategory(row, val)" placeholder="选择目录" size="small" style="width: 100%">
<el-option v-for="cat in allCategoriesFlat" :key="cat.id" :label="cat.name" :value="cat.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="大小" width="80" align="center">
<template #default="{ row }">{{ formatSize(row.file_size) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<template #default="{ row }">
<el-button size="small" @click="handleReprocess(row)">重新解析</el-button>
<el-button size="small" type="danger" @click="handleDeleteDoc(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 手机端卡片列表 -->
<div class="mobile-cards">
<div v-for="doc in docs" :key="doc.id" class="doc-card">
<div style="font-weight: bold; font-size: 15px; margin-bottom: 8px">{{ doc.original_filename }}</div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap">
<el-tag :type="statusType(doc.status)" size="small">{{ doc.status }}</el-tag>
<span style="color: #999; font-size: 12px">{{ formatSize(doc.file_size) }}</span>
</div>
<el-select :model-value="doc.category_id" @change="(val: string) => handleChangeDocCategory(doc, val)" placeholder="选择目录" size="small" style="width: 100%; margin-bottom: 8px">
<el-option v-for="cat in allCategoriesFlat" :key="cat.id" :label="cat.name" :value="cat.id" />
</el-select>
<div style="display: flex; gap: 8px">
<el-button size="small" style="flex: 1" @click="handleReprocess(doc)">重新解析</el-button>
<el-button size="small" type="danger" style="flex: 1" @click="handleDeleteDoc(doc)">删除</el-button>
</div>
</div>
</div>
</template>
<el-table :data="docs" v-loading="loading" style="width: 100%" size="large">
<el-table-column prop="original_filename" label="标题/文件名" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span style="font-size: 14px; font-weight: 500">{{ row.original_filename }}</span>
</template>
</el-table-column>
<el-table-column label="所属目录" width="200">
<template #default="{ row }">
<el-select
:model-value="row.category_id"
@change="(val: string) => handleChangeDocCategory(row, val)"
placeholder="选择目录"
size="default"
style="width: 100%"
>
<el-option
v-for="cat in allCategoriesFlat"
:key="cat.id"
:label="cat.name"
:value="cat.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" size="default">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="类型" width="80" align="center">
<template #default="{ row }">
<span style="font-size: 13px">{{ row.file_ext }}</span>
</template>
</el-table-column>
<el-table-column label="大小" width="100" align="center">
<template #default="{ row }">
<span style="font-size: 13px">{{ formatSize(row.file_size) }}</span>
</template>
</el-table-column>
<el-table-column prop="title" label="解析标题" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span style="font-size: 13px; color: #666">{{ row.title || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<el-button size="default" @click="handleReprocess(row)">🔄 重新解析</el-button>
<el-button size="default" type="danger" @click="handleDeleteDoc(row)">🗑 删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-card>
</main>
</div>
<!-- 文本内容对话框 -->
<el-dialog v-model="showTextDialog" title="✏️ 添加文本内容" width="700px">
<!-- 对话框 -->
<el-dialog v-model="showTextDialog" title="✏️ 添加文本内容" :width="isMobile ? '95%' : '700px'">
<el-form :model="textForm" label-position="top">
<el-form-item label="标题" required>
<el-input v-model="textForm.title" placeholder="文档标题" size="large" />
</el-form-item>
<el-form-item label="所属目录">
<el-select v-model="textForm.category_id" placeholder="选择目录(可选)" clearable style="width: 100%" size="large">
<el-option
v-for="cat in allCategoriesFlat"
:key="cat.id"
:label="cat.name"
:value="cat.id"
/>
<el-select v-model="textForm.category_id" placeholder="选择目录" clearable style="width: 100%" size="large">
<el-option v-for="cat in allCategoriesFlat" :key="cat.id" :label="cat.name" :value="cat.id" />
</el-select>
</el-form-item>
<el-form-item label="格式">
@@ -409,13 +418,7 @@ function getCategoryName(catId: string) {
</el-radio-group>
</el-form-item>
<el-form-item label="内容" required>
<el-input
v-model="textForm.content"
type="textarea"
:rows="15"
placeholder="输入文本内容(支持 Markdown 格式)"
size="large"
/>
<el-input v-model="textForm.content" type="textarea" :rows="12" placeholder="输入文本内容(支持 Markdown" size="large" />
</el-form-item>
</el-form>
<template #footer>
@@ -424,25 +427,191 @@ function getCategoryName(catId: string) {
</template>
</el-dialog>
<!-- 目录管理对话框 -->
<el-dialog v-model="showCatDialog" :title="editingCatId ? '✏️ 编辑目录' : '📁 新建目录'" width="450px">
<el-dialog v-model="showCatDialog" :title="editingCatId ? '✏️ 编辑目录' : '📁 新建目录'" :width="isMobile ? '95%' : '450px'">
<el-form :model="catForm" label-position="top">
<el-form-item label="目录名称" required>
<el-input v-model="catForm.name" placeholder="如:公司基本信息" size="large" />
</el-form-item>
<el-form-item label="类型">
<el-radio-group v-model="catForm.is_folder" size="large">
<el-radio :value="true">📁 文件夹可包含子目录</el-radio>
<el-radio :value="false">📄 叶子分类存放文档</el-radio>
<el-radio :value="true">📁 文件夹</el-radio>
<el-radio :value="false">📄 叶子分类</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCatDialog = false" size="large">取消</el-button>
<el-button type="primary" :loading="catLoading" @click="handleSaveCategory" size="large">
{{ editingCatId ? '保存' : '创建' }}
</el-button>
<el-button type="primary" :loading="catLoading" @click="handleSaveCategory" size="large">{{ editingCatId ? '保存' : '创建' }}</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.main-layout {
display: flex;
gap: 20px;
}
.sidebar {
width: 280px;
flex-shrink: 0;
overflow-y: auto;
max-height: calc(100vh - 180px);
position: sticky;
top: 20px;
}
.content {
flex: 1;
min-width: 0;
}
.cat-item {
padding: 10px 12px;
cursor: pointer;
border-radius: 6px;
margin-bottom: 6px;
font-size: 14px;
background: #f5f7fa;
transition: all 0.2s;
}
.cat-item:hover {
background: #e8e8e8;
}
.cat-item.active {
background: #ecf5ff;
color: #409eff;
font-weight: bold;
}
.tree-node {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 4px 0;
}
.tree-label {
cursor: pointer;
font-size: 14px;
flex: 1;
}
.tree-label.selected {
color: #409eff;
font-weight: bold;
}
.tree-actions {
display: flex;
gap: 2px;
}
.mobile-header {
display: none;
}
.mobile-actions {
display: none;
}
.mobile-cards {
display: none;
}
.desktop-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.desktop-actions {
display: flex;
gap: 10px;
}
.mobile-overlay {
display: none;
}
/* 手机端适配 */
@media (max-width: 768px) {
.main-layout {
flex-direction: column;
}
.sidebar {
display: none;
position: fixed;
top: 0;
left: 0;
width: 80%;
max-width: 300px;
height: 100vh;
background: #fff;
z-index: 1000;
padding: 20px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
overflow-y: auto;
max-height: 100vh;
}
.sidebar.mobile-show {
display: block;
}
.mobile-overlay {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.content {
width: 100%;
}
.mobile-header {
display: block;
margin-bottom: 16px;
}
.desktop-header {
display: none;
}
.mobile-actions {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.desktop-actions {
display: none;
}
.desktop-table {
display: none;
}
.mobile-cards {
display: block;
}
.doc-card {
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
}
</style>