知识库历史文件
This commit is contained in:
@@ -1 +1 @@
|
||||
export {default as DocumentDetail} from './DocumentDetail/DocumentDetail.vue'
|
||||
export * from './knowledge'
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as DocumentSegment } from './documentSegment/DocumentSegment.vue'
|
||||
export { default as DocumentDetail } from './documentDetail/DocumentDetail.vue'
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as FileUpload } from './fileupload/FileUpload.vue'
|
||||
export { default as FileHistory } from './fileHistory/FileHistory.vue'
|
||||
@@ -1 +0,0 @@
|
||||
export { default as FileUpload } from './FileUpload.vue'
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user