feat: 添加商务知识库功能,包括文件夹树和文件管理

This commit is contained in:
Hcc00 2025-07-29 09:59:53 +08:00
parent 9741192bee
commit a4876971a4
5 changed files with 582 additions and 32 deletions

View File

@ -0,0 +1,62 @@
import http from '@/utils/http'
const { request } = http
// 文件信息
export interface KnowledgeFile {
id: string
name: string
size: string
type: string
uploadTime: string
}
// 文件夹信息
export interface KnowledgeFolder {
id: string
name: string
children?: KnowledgeFolder[]
}
// 获取文件夹树
export function getFolderTreeApi() {
return request<KnowledgeFolder[]>({
url: '/knowledge/folders',
method: 'get',
})
}
// 获取文件列表(按文件夹)
export function getFilesApi(folderId: string) {
return request<KnowledgeFile[]>({
url: '/knowledge/files',
method: 'get',
params: { folderId },
})
}
// 创建文件夹
export function createFolderApi(data: { name: string; parentId?: string }) {
return request({
url: '/knowledge/create-folder',
method: 'post',
data,
})
}
// 删除文件
export function deleteFileApi(fileId: string) {
return request({
url: `/knowledge/delete-file/${fileId}`,
method: 'delete',
})
}
// 下载文件
export function downloadFileApi(fileId: string) {
return request<Blob>({
url: `/knowledge/download/${fileId}`,
method: 'get',
responseType: 'blob',
})
}

View File

@ -1,43 +1,46 @@
import { createApp } from 'vue'
import ArcoVue, { Card, Drawer, Modal } from '@arco-design/web-vue'
import '@/styles/arco-ui/index.less'
// import '@arco-themes/vue-gi-demo/index.less'
// import '@arco-design/web-vue/dist/arco.css'
// 额外引入 Arco Design Icon图标库
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import App from './App.vue'
import router from './router'
import pinia from '@/stores'
// 使用动画库
import 'animate.css/animate.min.css'
// UI框架 - Arco Design
import ArcoVue, { Card, Drawer, Modal } from '@arco-design/web-vue'
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import '@/styles/arco-ui/index.less' // 自定义Arco样式
// 自定义过渡动画
import '@/styles/css/transition.css'
// UI框架 - Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 导入全局scss主文件
// 动画相关
import 'animate.css/animate.min.css' // 第三方动画库
import '@/styles/css/transition.css' // 自定义过渡动画
// 样式主文件
import '@/styles/index.scss'
// 支持SVG
// SVG支持
import 'virtual:svg-icons-register'
// 自定义指令
import directives from './directives'
// 状态管理
import pinia from '@/stores'
// 对特定组件进行默认配置
Card.props.bordered = false
// 初始化应用
const app = createApp(App)
Modal._context = app._context
Drawer._context = app._context
// 配置Arco组件全局属性
Card.props.bordered = false // 设置Card组件默认不显示边框
Modal._context = app._context // 修复Modal组件上下文问题
Drawer._context = app._context // 修复Drawer组件上下文问题
// 安装插件
app.use(router)
app.use(pinia)
app.use(ArcoVue)
app.use(ArcoVueIcon)
app.use(directives)
.use(pinia)
.use(ArcoVue)
.use(ArcoVueIcon)
.use(ElementPlus)
.use(directives)
app.mount('#app')
// 挂载应用
app.mount('#app')

View File

@ -911,14 +911,32 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
// ],
// },
// 添加商务知识库
{
path: '/bussiness-knowledge',
name: 'bussinesskonwledge',
component: Layout,
redirect: '/bussiness-knowledge/data',
meta: { title: '商务资料知识库', icon: 'message', hidden: false, sort: 6 },
children: [
{
path: '/bussiness-konwledge/data',
name: 'bussiness-knowledge',
component: () => import('@/views/bussiness-data/bussiness.vue'),
meta: {
title: '商务数据库信息',
icon: 'info-circle',
hidden: false,
},
},
],
},
{
path: '/chat-platform',
name: 'ChatPlatform',
component: Layout,
redirect: '/chat-platform/options',
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 7 },
children: [
// {
// path: '/chat-platform/options',
@ -937,7 +955,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'EnterpriseSettings',
component: Layout,
redirect: '/enterprise-settings/company-info',
meta: { title: '企业设置', icon: 'setting', hidden: false, sort: 7 },
meta: { title: '企业设置', icon: 'setting', hidden: false, sort: 8 },
children: [
{
path: '/enterprise-settings/company-info',
@ -986,7 +1004,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'EnterpriseDashboard',
component: Layout,
redirect: '/enterprise-dashboard/overview',
meta: { title: '企业看板', icon: 'dashboard', hidden: false, sort: 8 },
meta: { title: '企业看板', icon: 'dashboard', hidden: false, sort: 9},
children: [
{
path: '/enterprise-dashboard/overview',
@ -1035,7 +1053,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'SystemResource',
component: Layout,
redirect: '/system-resource/device-management/warehouse',
meta: { title: '关于平台', icon: 'server', hidden: false, sort: 9 },
meta: { title: '关于平台', icon: 'server', hidden: false, sort: 10 },
children: [
{
path: '/system-resource/device-management/warehouse',

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -0,0 +1,467 @@
<template>
<div class="business-knowledge-container">
<el-container>
<!-- 左侧文件夹树 -->
<el-aside width="300px" class="folder-tree-container">
<div class="header">
<h3>文件夹结构</h3>
<el-button type="primary" size="small" @click="handleCreateFolder">
新建文件夹
</el-button>
</div>
<el-tree
:data="folderTree"
node-key="id"
:props="defaultProps"
@node-click="handleFolderClick"
empty-text="暂无文件夹"
>
<template #default="{ node, data }">
<span class="folder-item">
<el-icon><Folder /></el-icon>
<span class="folder-name">{{ node.label }}</span>
</span>
</template>
</el-tree>
<!-- 当没有文件夹时显示 -->
<div v-if="!folderTree || folderTree.length === 0" class="empty-folder">
<el-empty description="暂无文件夹" :image-size="80" />
</div>
</el-aside>
<!-- 右侧文件列表 -->
<el-main class="file-list-container">
<div class="header">
<h3>{{ currentFolderName }}</h3>
<el-button type="primary" @click="handleUploadFile">
<el-icon><Upload /></el-icon>
上传文件
</el-button>
</div>
<!-- 文件列表 -->
<el-table
:data="fileList"
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="name" label="文件名">
<template #default="{ row }">
<el-icon><Document /></el-icon>
<span style="margin-left: 10px">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="type" label="文件类型" width="120" />
<el-table-column prop="size" label="文件大小" width="120">
<template #default="{ row }">
{{ formatFileSize(row.size) }}
</template>
</el-table-column>
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handlePreview(row)">
预览
</el-button>
<el-button size="small" type="primary" @click="handleDownload(row)">
下载
</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<el-empty v-if="!loading && fileList.length === 0" description="暂时没有文件" />
</el-main>
</el-container>
<!-- 新建文件夹对话框 -->
<el-dialog
v-model="folderDialogVisible"
title="新建文件夹"
width="400px"
>
<el-form :model="folderForm" ref="folderFormRef" label-width="80px" :rules="folderRules">
<el-form-item label="文件夹名" prop="name">
<el-input v-model="folderForm.name" />
</el-form-item>
<el-form-item label="父级目录" prop="parentId">
<el-select v-model="folderForm.parentId" placeholder="请选择父级目录">
<el-option label="根目录" :value="''" />
<el-option
v-for="folder in allFolders"
:key="folder.id"
:label="folder.name"
:value="folder.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="folderDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitFolderForm">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 上传文件对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传文件"
width="500px"
>
<el-form :model="uploadForm" ref="uploadFormRef" label-width="80px">
<el-form-item label="选择文件" prop="file">
<el-upload
class="upload-demo"
drag
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileListTemp"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="resetUpload">取消</el-button>
<el-button type="primary" @click="submitUploadForm">上传</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, computed } from 'vue'
import { ElMessage, ElMessageBox, ElForm } from 'element-plus'
import { Document, Folder, Upload, UploadFilled } from '@element-plus/icons-vue'
import http from '@/utils/http'
// API
import {
createFolderApi,
deleteFileApi,
downloadFileApi,
getFilesApi,
getFolderTreeApi
} from '@/apis/bussiness/bussiness'
//
const folderTree = ref([])
//
const fileList = ref([])
//
const fileListTemp = ref([])
//
const defaultProps = {
children: 'children',
label: 'name',
}
//
const loading = ref(false)
// ID
const currentFolderId = ref('')
//
const currentFolderName = ref('根目录')
//
const folderDialogVisible = ref(false)
const uploadDialogVisible = ref(false)
//
const folderForm = reactive({
name: '',
parentId: ''
})
//
const folderRules = {
name: [
{ required: true, message: '请输入文件夹名称', trigger: 'blur' },
{ max: 50, message: '文件夹名称不能超过50个字符', trigger: 'blur' }
]
}
//
const uploadForm = reactive({
file: null
})
//
const allFolders = ref([])
//
const folderFormRef = ref(null)
const uploadFormRef = ref(null)
//
const getAllFolders = async () => {
try {
const response = await getFolderTreeApi()
// @ts-ignore
folderTree.value = response.data
//
flattenFolders(response.data)
} catch (error) {
// @ts-ignore
ElMessage.error('获取文件夹失败:' + (error.response?.data?.message || error.message))
}
}
//
const flattenFolders = (folders, parent = null) => {
folders.forEach(folder => {
// @ts-ignore
allFolders.value.push(folder)
if (folder.children && folder.children.length > 0) {
flattenFolders(folder.children, folder)
}
})
}
//
const formatFileSize = (size) => {
if (!size) return '0 B'
// API
const fileSize = Number(size)
if (isNaN(fileSize)) return '未知'
if (fileSize < 1024) {
return fileSize + ' B'
} else if (fileSize < 1024 * 1024) {
return (fileSize / 1024).toFixed(2) + ' KB'
} else if (fileSize < 1024 * 1024 * 1024) {
return (fileSize / (1024 * 1024)).toFixed(2) + ' MB'
} else {
return (fileSize / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
}
}
//
const handleFolderClick = (data) => {
currentFolderId.value = data.id
currentFolderName.value = data.name
loadFiles(data.id)
}
//
const loadFiles = async (folderId) => {
loading.value = true
try {
const response = await getFilesApi(folderId)
// @ts-ignore
fileList.value = response.data || []
} catch (error) {
ElMessage.error('获取文件列表失败:' + (error.response?.data?.message || error.message))
fileList.value = []
} finally {
loading.value = false
}
}
//
const handleCreateFolder = () => {
folderForm.name = ''
folderForm.parentId = currentFolderId.value
folderDialogVisible.value = true
}
//
const submitFolderForm = async () => {
//
const formRef = folderFormRef.value
if (!formRef) return
try {
// @ts-ignore
await formRef.validate()
// API
await createFolderApi(folderForm)
ElMessage.success('文件夹创建成功')
folderDialogVisible.value = false
//
getAllFolders()
} catch (error) {
if (error.name === 'ValidationError') return
ElMessage.error('创建文件夹失败:' + (error.response?.data?.message || error.message))
}
}
//
const handleUploadFile = () => {
resetUpload()
uploadDialogVisible.value = true
}
//
const handleFileChange = (file, fileList) => {
uploadForm.file = file.raw
fileListTemp.value = fileList
}
//
const resetUpload = () => {
uploadForm.file = null
fileListTemp.value = []
if (uploadFormRef.value) {
uploadFormRef.value.resetFields()
}
}
//
const submitUploadForm = async () => {
if (!uploadForm.file) {
ElMessage.warning('请选择文件')
return
}
try {
// FormData
const formData = new FormData()
formData.append('file', uploadForm.file)
formData.append('folderId', currentFolderId.value)
// 使httppost
await http.post('/knowledge/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
ElMessage.success('文件上传成功')
uploadDialogVisible.value = false
//
loadFiles(currentFolderId.value)
} catch (error) {
// http
console.error('文件上传失败', error)
}
}
//
const handlePreview = (row) => {
//
ElMessage.info(`预览文件: ${row.name}`)
//
// window.open(`/knowledge/preview/${row.id}`)
}
//
const handleDownload = async (row) => {
try {
// 使httpdownload
const response = await http.download(`/knowledge/download/${row.id}`)
//
const blob = new Blob([response.data])
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = row.name
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
ElMessage.success('文件下载成功')
} catch (error) {
console.error('文件下载失败', error)
}
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定要删除文件 "${row.name}" 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
},
)
// API
await deleteFileApi(row.id)
ElMessage.success('文件删除成功')
//
loadFiles(currentFolderId.value)
} catch (error) {
if (error === 'cancel') return
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
ElMessage.error(`文件删除失败:${error.response?.data?.message || error.message}`)
}
}
//
onMounted(() => {
getAllFolders()
loadFiles(currentFolderId.value)
})
</script>
<style scoped>
.business-knowledge-container {
padding: 20px;
background-color: #f5f5f5;
height: 100%;
}
.folder-tree-container {
background-color: white;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
position: relative;
}
.folder-tree-container .header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.folder-item {
display: flex;
align-items: center;
}
.folder-name {
margin-left: 8px;
}
.empty-folder {
text-align: center;
padding: 20px 0;
}
.file-list-container {
background-color: white;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.file-list-container .header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.upload-demo {
width: 100%;
}
</style>