知识库历史文件

This commit is contained in:
2025-12-20 17:12:42 +08:00
parent dfd9bb8b95
commit 62850717eb
59 changed files with 2351 additions and 276 deletions

View File

@@ -11,12 +11,12 @@
import { FileUpload, DynamicFormItem } from 'shared/components'
// 单独导入(推荐)
import FileUpload from 'shared/components/FileUpload'
import FileUpload from 'shared/components/file/FileUpload'
import DynamicFormItem from 'shared/components/DynamicFormItem'
```
**可用组件:**
- `shared/components/FileUpload` - 文件上传组件(支持 cover/dialog/content 三种模式)
- `shared/components/file/FileUpload` - 文件上传组件(支持 cover/dialog/content 三种模式)
- `shared/components/DynamicFormItem` - 动态表单项组件
---
@@ -100,7 +100,7 @@ import { authAPI } from 'shared/authAPI'
import { fileAPI } from 'shared/fileAPI'
// ✅ 新路径(推荐)
import FileUpload from 'shared/components/FileUpload'
import FileUpload from 'shared/components/file/FileUpload'
import { authAPI } from 'shared/api/auth'
import { fileAPI } from 'shared/api/file'
```
@@ -113,7 +113,7 @@ import { fileAPI } from 'shared/api/file'
```typescript
// ✅ 推荐:路径清晰,便于理解
import FileUpload from 'shared/components/FileUpload'
import FileUpload from 'shared/components/file/FileUpload'
import { authAPI } from 'shared/api/auth'
// ❌ 不推荐:路径模糊

View File

@@ -1,6 +1,6 @@
import { api } from '@/api/index'
import type { ResultDomain, PageRequest } from '@/types'
import type { TbKnowledge, TbKnowledgeFile, SegmentRequestBody, DocumentStatusRequestBody } from '@/types/ai'
import type { TbKnowledge, TbKnowledgeFile, KnowledgeFileVO, SegmentRequestBody, DocumentStatusRequestBody } from '@/types/ai'
/**
* @description AI知识库相关接口
@@ -171,8 +171,8 @@ export const aiKnowledgeAPI = {
* 获取文件历史版本
* @param fileRootId 文件根ID
*/
async getFileHistory(fileRootId: string): Promise<ResultDomain<TbKnowledgeFile>> {
const response = await api.get<TbKnowledgeFile>(`${this.baseUrl}/file/${fileRootId}/history`)
async getFileHistory(fileRootId: string): Promise<ResultDomain<KnowledgeFileVO[]>> {
const response = await api.get<KnowledgeFileVO[]>(`${this.baseUrl}/file/${fileRootId}/history`)
return response.data
},

View File

@@ -1 +1 @@
export {default as DocumentDetail} from './DocumentDetail/DocumentDetail.vue'
export * from './knowledge'

View File

@@ -0,0 +1,202 @@
.segment-dialog {
:deep(.el-dialog) {
border-radius: 14px;
.el-dialog__header {
padding: 24px 24px 16px;
border-bottom: 1px solid #F3F3F5;
.el-dialog__title {
font-size: 18px;
font-weight: 500;
color: #101828;
letter-spacing: -0.02em;
}
}
.el-dialog__body {
padding: 24px;
background: #FAFAFA;
}
.el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid #F3F3F5;
}
}
}
.top-actions {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 10px;
padding: 8px 20px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
.action-buttons {
display: flex;
gap: 12px;
}
}
.segment-list {
max-height: 520px;
overflow-y: auto;
padding-right: 4px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #D1D5DB;
border-radius: 3px;
&:hover {
background: #9CA3AF;
}
}
.segment-item {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.12);
}
&:last-child {
margin-bottom: 0;
}
}
.segment-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 12px;
.segment-index {
font-weight: 500;
color: #101828;
font-size: 14px;
letter-spacing: -0.01em;
white-space: nowrap;
}
.segment-info {
flex: 1;
font-size: 12px;
color: #6A7282;
letter-spacing: -0.01em;
white-space: nowrap;
}
.segment-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
}
.segment-content {
margin-bottom: 12px;
.el-textarea {
textarea {
font-family: inherit;
font-size: 14px;
line-height: 1.6;
}
}
.segment-text {
padding: 12px;
background: #F9FAFB;
border: 1px solid transparent;
border-radius: 8px;
line-height: 1.6;
color: #4A5565;
font-size: 14px;
white-space: pre-wrap;
word-break: break-word;
letter-spacing: -0.01em;
}
}
.segment-keywords {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-tag) {
background: #EFF6FF;
border: 1px solid transparent;
border-radius: 8px;
color: #1447E6;
font-size: 12px;
padding: 2px 8px;
height: 24px;
line-height: 20px;
}
}
.segment-meta {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 16px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #667085;
letter-spacing: -0.01em;
.el-icon {
font-size: 14px;
color: #9CA3AF;
}
}
}
.empty-state {
text-align: center;
padding: 80px 20px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
p {
font-size: 14px;
color: #6A7282;
margin: 0;
letter-spacing: -0.01em;
}
}
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: center;
}

View File

@@ -0,0 +1,501 @@
<template>
<div>
<el-dialog
v-model="visible"
title="文档分段管理"
width="1200px"
:close-on-click-modal="false"
class="segment-dialog"
>
<!-- 顶部操作栏 -->
<div class="top-actions">
<div class="action-buttons">
<FileUpload
v-if="canUpdate"
mode="dialog"
title="版本更新"
button-text="版本更新"
button-type="warning"
accept=".pdf,.doc,.docx,.txt,.md"
:max-size="50 * 1024 * 1024"
:max-count="1"
:custom-upload="handleUpdateFile"
@upload-error="handleUpdateError"
/>
<el-button
type="success"
@click="showAddSegmentDialog = true"
size="default"
>
添加分段
</el-button>
<el-button
type="primary"
@click="loadSegments(1)"
size="default"
>
刷新
</el-button>
</div>
</div>
<!-- 分段列表 -->
<div class="segment-list" v-loading="loading">
<div
v-for="segment in segments"
:key="segment.id"
class="segment-item"
>
<div class="segment-header">
<span class="segment-index">分段 {{ segment.position }}</span>
<span class="segment-info">
{{ segment.word_count }} · {{ segment.tokens }} tokens
</span>
<div class="segment-actions">
<!-- 启用开关 -->
<el-switch
:model-value="segment.enabled"
:active-text="segment.enabled ? '已启用' : '已禁用'"
:loading="segment._switching"
@change="handleToggleEnabled(segment, $event)"
style="--el-switch-on-color: #67C23A; margin-right: 12px;"
/>
<el-tag
:type="getStatusType(segment.status)"
size="small"
style="margin-right: 8px;"
>
{{ getStatusText(segment.status) }}
</el-tag>
<!-- 编辑按钮 -->
<el-button
v-if="!editingSegmentIds.has(segment.id)"
type="primary"
size="small"
@click="startEdit(segment)"
>
编辑
</el-button>
<!-- 删除按钮 -->
<el-button
v-if="!editingSegmentIds.has(segment.id)"
type="danger"
size="small"
@click="handleDeleteSegment(segment)"
:loading="segment._deleting"
>
删除
</el-button>
<!-- 保存和取消按钮 -->
<template v-else>
<el-button
type="success"
size="small"
@click="saveEdit(segment)"
:loading="segment._saving"
>
保存
</el-button>
<el-button
size="small"
@click="cancelEdit(segment)"
>
取消
</el-button>
</template>
</div>
</div>
<!-- 分段内容显示或编辑 -->
<div class="segment-content">
<!-- 编辑模式 -->
<template v-if="editingSegmentIds.has(segment.id)">
<el-input
v-model="editingContents[segment.id]"
type="textarea"
:rows="8"
placeholder="请输入分段内容"
/>
</template>
<!-- 只读模式 -->
<template v-else>
<div class="segment-text">
{{ segment.content }}
</div>
</template>
</div>
<!-- 关键词标签 -->
<div class="segment-keywords" v-if="segment.keywords?.length">
<el-tag
v-for="keyword in segment.keywords"
:key="keyword"
size="small"
style="margin-right: 8px;"
>
{{ keyword }}
</el-tag>
</div>
<!-- 分段元数据 -->
<div class="segment-meta">
<span class="meta-item">
<el-icon><Clock /></el-icon>
创建时间: {{ formatTimestamp(segment.created_at) }}
</span>
<span class="meta-item" v-if="segment.completed_at">
<el-icon><Check /></el-icon>
完成时间: {{ formatTimestamp(segment.completed_at) }}
</span>
<span class="meta-item">
<el-icon><View /></el-icon>
命中次数: {{ segment.hit_count }}
</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && segments.length === 0" class="empty-state">
<p>暂无分段数据</p>
</div>
</div>
<!-- 分页组件 -->
<div class="pagination-container" v-if="totalCount > 0">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[10, 15, 20, 50]"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-dialog>
<!-- 添加分段对话框 -->
<el-dialog
v-model="showAddSegmentDialog"
title="添加分段"
width="700px"
:close-on-click-modal="false"
>
<el-form :model="newSegmentForm" label-width="80px">
<el-form-item label="分段内容" required>
<el-input
v-model="newSegmentForm.content"
type="textarea"
:rows="10"
placeholder="请输入分段内容"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddSegmentDialog = false">取消</el-button>
<el-button
type="primary"
@click="handleCreateSegment"
:loading="creatingSegment"
>
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Clock, Check, Eye as View } from 'lucide-vue-next'
import { aiKnowledgeAPI } from '@/api/ai'
import FileUpload from '@/components/file/fileupload/FileUpload.vue'
interface Props {
modelValue: boolean
datasetId: string
documentId: string
/** 知识库ID用于版本更新 */
knowledgeId?: string
/** 文件根ID用于版本更新 */
fileRootId?: string
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'file-updated'])
// 是否可以更新文件
const canUpdate = computed(() => !!props.knowledgeId && !!props.fileRootId)
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const loading = ref(false)
const segments = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const editingSegmentIds = ref<Set<string>>(new Set())
const editingContents = ref<Record<string, string>>({})
const originalContents = ref<Record<string, string>>({})
const showAddSegmentDialog = ref(false)
const creatingSegment = ref(false)
const newSegmentForm = ref({
content: '',
keywords: [] as string[]
})
// 版本更新相关
const handleUpdateFile = async (files: File[]) => {
if (!files.length) {
throw new Error('请选择要上传的文件')
}
if (!props.knowledgeId || !props.fileRootId) {
throw new Error('文件信息不完整,无法更新')
}
const result = await aiKnowledgeAPI.updateFile(
files[0],
props.knowledgeId,
props.fileRootId
)
if (result.success) {
ElMessage.success('文件更新成功')
emit('file-updated')
} else {
throw new Error(result.message || '更新失败')
}
}
const handleUpdateError = (error: string) => {
ElMessage.error(error)
}
watch(visible, (val) => {
if (val) {
loadSegments(1)
}
})
async function loadSegments(page = currentPage.value) {
if (!props.datasetId || !props.documentId) return
try {
loading.value = true
const result = await aiKnowledgeAPI.getDocumentSegments(
props.datasetId,
props.documentId
)
if (result.success && result.data) {
segments.value = result.data.data || []
totalCount.value = result.data.total || 0
currentPage.value = page
} else {
segments.value = []
ElMessage.error(result.message || '加载分段失败')
}
} catch (error: any) {
console.error('加载分段失败:', error)
ElMessage.error(error.message || '加载分段失败')
} finally {
loading.value = false
}
}
function handleSizeChange(size: number) {
pageSize.value = size
loadSegments(1)
}
function handleCurrentChange(page: number) {
loadSegments(page)
}
async function handleToggleEnabled(segment: any, enabled: boolean) {
if (!props.datasetId || !props.documentId || !segment.id) {
ElMessage.error('分段信息不完整')
return
}
try {
segment._switching = true
const result = await aiKnowledgeAPI.updateSegment(
props.datasetId,
props.documentId,
segment.id,
{ segment: { enabled } }
)
if (result.success) {
segment.enabled = enabled
ElMessage.success(enabled ? '已启用分段' : '已禁用分段')
} else {
ElMessage.error(result.message || '更新失败')
}
} catch (error: any) {
console.error('更新分段状态失败:', error)
ElMessage.error('更新失败')
} finally {
segment._switching = false
}
}
function startEdit(segment: any) {
editingSegmentIds.value.add(segment.id)
editingContents.value[segment.id] = segment.content
originalContents.value[segment.id] = segment.content
}
async function saveEdit(segment: any) {
const newContent = editingContents.value[segment.id]
if (!newContent || !newContent.trim()) {
ElMessage.warning('分段内容不能为空')
return
}
if (newContent === originalContents.value[segment.id]) {
ElMessage.info('内容未修改')
cancelEdit(segment)
return
}
try {
segment._saving = true
const result = await aiKnowledgeAPI.updateSegment(
props.datasetId,
props.documentId,
segment.id,
{ segment: { content: newContent } }
)
if (result.success) {
segment.content = newContent
editingSegmentIds.value.delete(segment.id)
delete editingContents.value[segment.id]
delete originalContents.value[segment.id]
ElMessage.success('保存成功')
await loadSegments()
} else {
ElMessage.error(result.message || '保存失败')
}
} catch (error: any) {
console.error('保存分段失败:', error)
ElMessage.error('保存失败')
} finally {
segment._saving = false
}
}
function cancelEdit(segment: any) {
editingSegmentIds.value.delete(segment.id)
delete editingContents.value[segment.id]
delete originalContents.value[segment.id]
}
async function handleCreateSegment() {
const content = newSegmentForm.value.content.trim()
if (!content) {
ElMessage.warning('分段内容不能为空')
return
}
try {
creatingSegment.value = true
const result = await aiKnowledgeAPI.createSegment(
props.datasetId,
props.documentId,
{ segments: [{ content }] }
)
if (result.success) {
ElMessage.success('添加成功')
showAddSegmentDialog.value = false
newSegmentForm.value = { content: '', keywords: [] }
await loadSegments()
} else {
ElMessage.error(result.message || '添加失败')
}
} catch (error: any) {
console.error('创建分段失败:', error)
ElMessage.error('添加失败')
} finally {
creatingSegment.value = false
}
}
async function handleDeleteSegment(segment: any) {
try {
await ElMessageBox.confirm(
`确定要删除分段 ${segment.position} 吗?此操作不可恢复。`,
'确认删除',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
)
segment._deleting = true
const result = await aiKnowledgeAPI.deleteSegment(
props.datasetId,
props.documentId,
segment.id
)
if (result.success) {
ElMessage.success('删除成功')
await loadSegments()
} else {
ElMessage.error(result.message || '删除失败')
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除分段失败:', error)
ElMessage.error('删除失败')
}
} finally {
segment._deleting = false
}
}
function getStatusType(status: string): 'success' | 'info' | 'warning' | 'danger' {
const typeMap: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
'completed': 'success',
'indexing': 'warning',
'error': 'danger',
'paused': 'info'
}
return typeMap[status] || 'info'
}
function getStatusText(status: string): string {
const textMap: Record<string, string> = {
'completed': '已完成',
'indexing': '索引中',
'error': '错误',
'paused': '已暂停'
}
return textMap[status] || status
}
function formatTimestamp(timestamp: number): string {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
</script>
<style lang="scss" scoped>
@import url("./DocumentSegment.scss");
</style>

View File

@@ -0,0 +1,2 @@
export { default as DocumentSegment } from './documentSegment/DocumentSegment.vue'
export { default as DocumentDetail } from './documentDetail/DocumentDetail.vue'

View File

@@ -0,0 +1,33 @@
.file-history-dialog {
.el-dialog__body {
padding: 16px 20px;
}
.file-history-table {
width: 100%;
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
.file-icon {
color: #409eff;
font-size: 16px;
}
}
}
.action-buttons {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
// gap: 4px;
// width: 100%;
.el-button {
margin: 0 !important;
}
}
}

View File

@@ -0,0 +1,155 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width"
class="file-history-dialog"
@close="handleClose"
>
<el-table
:data="dataList"
v-loading="loading"
class="file-history-table"
>
<el-table-column prop="fileName" label="文件名" min-width="200" fixed="left">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.fileName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="80" align="center">
<template #default="{ row }">
<el-tag size="small">v{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="文件大小" width="100" align="center">
<template #default="{ row }">
{{ formatFileSize(row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="uploaderName" label="上传人员" width="120" />
<el-table-column prop="createTime" label="上传时间" width="180" />
<el-table-column label="操作" :width="actionColumnWidth" align="center" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button
v-if="showPreview"
type="primary"
link
size="small"
@click="handlePreview(row)"
>
<el-icon><View /></el-icon>预览
</el-button>
<el-button
v-if="showDownload"
type="success"
link
size="small"
@click="handleDownload(row)"
>
<el-icon><Download /></el-icon>下载
</el-button>
<template v-for="action in customActions" :key="action.key">
<el-button
:type="action.type || 'primary'"
link
size="small"
@click="handleCustomAction(action.key, row)"
>
<el-icon v-if="action.icon"><component :is="action.icon" /></el-icon>
{{ action.label }}
</el-button>
</template>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { FileText as Document, Eye as View, Download } from 'lucide-vue-next'
import type { KnowledgeFileVO } from '@/types/ai'
export interface FileAction {
key: string
label: string
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
icon?: any
}
interface Props {
modelValue: boolean
title?: string
width?: string
data?: KnowledgeFileVO[]
loading?: boolean
showPreview?: boolean
showDownload?: boolean
customActions?: FileAction[]
}
const props = withDefaults(defineProps<Props>(), {
title: '历史版本',
width: '800px',
data: () => [],
loading: false,
showPreview: true,
showDownload: true,
customActions: () => []
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'close'): void
(e: 'preview', file: KnowledgeFileVO): void
(e: 'download', file: KnowledgeFileVO): void
(e: 'action', key: string, file: KnowledgeFileVO): void
}>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const dataList = computed(() => props.data)
const actionColumnWidth = computed(() => {
let width = 0
if (props.showPreview) width += 60
if (props.showDownload) width += 60
width += props.customActions.length * 70
return Math.max(width, 120)
})
const formatFileSize = (size?: number) => {
if (!size) return '-'
if (size < 1024) return size + ' B'
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
return (size / 1024 / 1024).toFixed(1) + ' MB'
}
const handleClose = () => {
emit('close')
}
const handlePreview = (row: KnowledgeFileVO) => {
emit('preview', row)
}
const handleDownload = (row: KnowledgeFileVO) => {
emit('download', row)
}
const handleCustomAction = (key: string, row: KnowledgeFileVO) => {
emit('action', key, row)
}
</script>
<style lang="scss" scoped>
@import url('./FileHistory.scss');
</style>

View File

@@ -0,0 +1,2 @@
export { default as FileUpload } from './fileupload/FileUpload.vue'
export { default as FileHistory } from './fileHistory/FileHistory.vue'

View File

@@ -1 +0,0 @@
export { default as FileUpload } from './FileUpload.vue'

View File

@@ -1,7 +1,6 @@
export * from './fileupload'
export * from './base'
export * from './dynamicFormItem'
export * from './ai'
export * from './file'
// 通用视图组件
export { default as IframeView } from './iframe/IframeView.vue'

View File

@@ -1,4 +1,4 @@
import type { BaseDTO } from '../base'
import type { BaseDTO, BaseVO } from '../base'
/**
* 知识库配置
@@ -58,6 +58,41 @@ export interface TbKnowledgeFile extends BaseDTO {
version?: number
}
/**
* 知识库文件视图对象(关联文件信息)
*/
export interface KnowledgeFileVO extends BaseVO {
// TbKnowledgeFile 的字段
/** 知识库ID */
knowledgeId?: string
/** 文件ID */
fileId?: string
/** 文件根ID */
fileRootId?: string
/** Dify文件ID */
difyFileId?: string
/** 文件版本 */
version?: number
// TbSysFile 的字段
/** 文件名 */
fileName?: string
/** 文件路径 */
filePath?: string
/** 文件大小(字节) */
fileSize?: number
/** 文件MIME类型 */
fileMimeType?: string
/** 文件访问URL */
fileUrl?: string
/** 文件扩展名 */
fileExtension?: string
/** 文件MD5值 */
fileMd5Hash?: string
/** 上传人员名称 */
uploaderName?: string
}
/**
* 文档分段请求体
*/

View File

@@ -1,3 +1,13 @@
/**
* 排序字段
*/
export interface OrderField {
/** 排序字段 */
field: string
/** 排序方式 */
order: 'ASC' | 'DESC'
}
/**
* 基础DTO - 包含所有数据传输对象的公共字段
*/
@@ -20,6 +30,14 @@ export interface BaseDTO {
deleteTime?: string
/** 是否已删除 */
deleted?: boolean
/** 数量限制 */
limit?: number
/** 开始时间 */
startTime?: string
/** 结束时间 */
endTime?: string
/** 排序字段列表 */
orderFields?: OrderField[]
}
/**
@@ -28,11 +46,4 @@ export interface BaseDTO {
export interface BaseVO extends BaseDTO {
/** 主键ID */
id?: string
orderTypes?: OrderType[]
}
export interface OrderType {
field: string
order: 'ASC' | 'DESC'
}

View File

@@ -34,9 +34,12 @@ export default defineConfig({
exposes: {
// ========== 组件模块 ==========
'./components': './src/components/index.ts',
'./components/FileUpload': './src/components/fileupload/FileUpload.vue',
'./components/file/FileUpload': './src/components/file/fileupload/FileUpload.vue',
'./components/file/FileHistory': './src/components/file/fileHistory/FileHistory.vue',
'./components/DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
'./components/iframe/IframeView.vue': './src/components/iframe/IframeView.vue',
'./components/ai/knowledge/DocumentSegment.vue': './src/components/ai/knowledge/documentSegment/DocumentSegment.vue',
'./components/ai/knowledge/DocumentDetail.vue': './src/components/ai/knowledge/documentDetail/DocumentDetail.vue',
// ========== API 模块 ==========
'./api': './src/api/index.ts',
@@ -72,7 +75,7 @@ export default defineConfig({
vue: {},
'vue-router': {},
'element-plus': {},
'@element-plus/icons-vue': {},
'lucide-vue-next': {},
axios: {}
}
})