feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
335
frontend/src/views/dataset/DatasetCreateView.vue
Normal file
335
frontend/src/views/dataset/DatasetCreateView.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
|
||||
import PageCard from '@/components/PageCard.vue'
|
||||
import {
|
||||
getDataset,
|
||||
createDataset,
|
||||
updateDataset,
|
||||
uploadDatasetFiles,
|
||||
} from '@/api/modules/dataset'
|
||||
import type { DatasetItem } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const editId = computed(() => route.params.id as string | undefined)
|
||||
|
||||
/** 允许的文件类型 */
|
||||
const acceptTypes = '.json,.jsonl'
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
dataset_type: 'train' as 'train' | 'eval',
|
||||
storage: 'local' as 'local' | 'cloud' | 'minio',
|
||||
// MinIO 配置
|
||||
minio_endpoint: '',
|
||||
minio_bucket: '',
|
||||
minio_access_key: '',
|
||||
minio_secret_key: '',
|
||||
minio_ssl: false,
|
||||
})
|
||||
|
||||
const files = ref<File[]>([])
|
||||
const fileCount = ref(0)
|
||||
const formatValid = ref<boolean | null>(null)
|
||||
const formatMessage = ref('')
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入数据集名称', trigger: 'blur' },
|
||||
{ max: 20, message: '不超过 20 字符', trigger: 'blur' },
|
||||
],
|
||||
description: [{ max: 50, message: '不超过 50 字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
/** 处理文件选择(替换模式:新文件覆盖旧文件) */
|
||||
async function handleFileChange(uploadFile: UploadFile) {
|
||||
const raw = uploadFile.raw
|
||||
if (!raw) return
|
||||
const ext = raw.name.split('.').pop()?.toLowerCase()
|
||||
if (ext !== 'json' && ext !== 'jsonl') {
|
||||
ElMessage.warning('仅支持 JSON/JSONL 格式')
|
||||
return
|
||||
}
|
||||
if (raw.size > 200 * 1024 * 1024) {
|
||||
ElMessage.warning('单文件不能超过 200MB')
|
||||
return
|
||||
}
|
||||
files.value = [raw] // 替换模式
|
||||
await analyzeFile(raw)
|
||||
}
|
||||
|
||||
/** 前端解析文件统计条数并校验 Alpaca 格式 */
|
||||
async function analyzeFile(file: File) {
|
||||
try {
|
||||
const text = await file.text()
|
||||
const lines = text.trim().split('\n').filter(Boolean)
|
||||
fileCount.value = lines.length
|
||||
|
||||
// Alpaca 格式校验:每行 JSON 须含 instruction 字段
|
||||
let validCount = 0
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line)
|
||||
if (obj.instruction !== undefined) validCount++
|
||||
} catch {
|
||||
// 非 JSON 行(如纯 JSONL 多行结构)
|
||||
}
|
||||
}
|
||||
if (validCount > 0 && validCount === lines.length) {
|
||||
formatValid.value = true
|
||||
formatMessage.value = `符合 Alpaca 格式(含 instruction 字段)`
|
||||
} else if (validCount > 0) {
|
||||
formatValid.value = true
|
||||
formatMessage.value = `部分符合 Alpaca 格式(${validCount}/${lines.length})`
|
||||
} else {
|
||||
formatValid.value = false
|
||||
formatMessage.value = '未检测到标准 Alpaca 格式(缺少 instruction 字段),仍可上传'
|
||||
}
|
||||
} catch {
|
||||
fileCount.value = 0
|
||||
formatValid.value = null
|
||||
formatMessage.value = '文件解析失败'
|
||||
}
|
||||
}
|
||||
|
||||
/** 自定义上传(阻止自动上传,仅收集文件) */
|
||||
function customUpload() {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
files.value = []
|
||||
fileCount.value = 0
|
||||
formatValid.value = null
|
||||
formatMessage.value = ''
|
||||
}
|
||||
|
||||
async function loadEditData() {
|
||||
if (!editId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const ds: any = await getDataset(editId.value)
|
||||
Object.assign(form, {
|
||||
name: ds.name || '',
|
||||
description: ds.description || '',
|
||||
dataset_type: ds.type === 'eval' ? 'eval' : 'train',
|
||||
storage: ds.storage_type || 'local',
|
||||
})
|
||||
fileCount.value = ds.count || 0
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (!isEdit.value && files.value.length === 0) {
|
||||
ElMessage.warning('请至少上传一个数据集文件')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const baseData: Partial<DatasetItem> = {
|
||||
name: form.name,
|
||||
type: form.dataset_type,
|
||||
storage_type: form.storage,
|
||||
description: form.description,
|
||||
count: fileCount.value,
|
||||
}
|
||||
// MinIO 配置
|
||||
if (form.storage === 'minio') {
|
||||
;(baseData as any).minio_config = {
|
||||
endpoint: form.minio_endpoint,
|
||||
bucket: form.minio_bucket,
|
||||
access_key: form.minio_access_key,
|
||||
secret_key: form.minio_secret_key,
|
||||
ssl: form.minio_ssl,
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit.value && editId.value) {
|
||||
// 编辑:更新记录 + 可选上传新文件
|
||||
await updateDataset(editId.value, baseData)
|
||||
if (files.value.length > 0) {
|
||||
await uploadDatasetFiles(editId.value, files.value)
|
||||
}
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
// 新建:创建记录 → 上传文件 → 更新 count
|
||||
const res: any = await createDataset(baseData)
|
||||
const newId = res?.id || res
|
||||
if (files.value.length > 0) {
|
||||
await uploadDatasetFiles(newId, files.value)
|
||||
await updateDataset(newId, { count: fileCount.value })
|
||||
}
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
router.push('/dataset')
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(loadEditData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageCard :title="isEdit ? '编辑数据集' : '上传数据集'" v-loading="loading">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
style="max-width: 640px"
|
||||
>
|
||||
<el-form-item label="数据集名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入数据集名称" maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据集描述">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据集类型">
|
||||
<el-radio-group v-model="form.dataset_type">
|
||||
<el-radio value="train">训练集</el-radio>
|
||||
<el-radio value="eval">评测集</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="存储位置">
|
||||
<el-radio-group v-model="form.storage">
|
||||
<el-radio value="local">本地</el-radio>
|
||||
<el-radio value="cloud">云平台</el-radio>
|
||||
<el-radio value="minio">MinIO</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- MinIO 配置 -->
|
||||
<template v-if="form.storage === 'minio'">
|
||||
<el-form-item label="Endpoint">
|
||||
<el-input v-model="form.minio_endpoint" placeholder="如:http://minio:9000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket">
|
||||
<el-input v-model="form.minio_bucket" placeholder="请输入 Bucket 名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Access Key">
|
||||
<el-input v-model="form.minio_access_key" placeholder="请输入 Access Key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Secret Key">
|
||||
<el-input v-model="form.minio_secret_key" type="password" show-password placeholder="请输入 Secret Key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用 SSL">
|
||||
<el-switch v-model="form.minio_ssl" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 文件上传(仅本地存储) -->
|
||||
<el-form-item v-if="form.storage === 'local'" label="上传文件">
|
||||
<div style="width: 100%">
|
||||
<el-upload
|
||||
drag
|
||||
:accept="acceptTypes"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:http-request="customUpload"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<i class="fa fa-cloud-upload" style="font-size: 32px; color: #1890ff" />
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅支持 JSON/JSONL 格式,单文件不超过 200MB</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<!-- 已选文件列表 -->
|
||||
<div v-for="f in files" :key="f.name" class="file-item">
|
||||
<div class="file-info">
|
||||
<i class="fa fa-file-code-o" />
|
||||
<span class="file-name">{{ f.name }}</span>
|
||||
<span class="file-size">{{ (f.size / 1024).toFixed(1) }} KB</span>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="handleRemove">
|
||||
<i class="fa fa-times" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 格式校验提示 -->
|
||||
<el-alert
|
||||
v-if="formatMessage"
|
||||
:title="formatMessage"
|
||||
:type="formatValid ? 'success' : 'warning'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-top: 8px"
|
||||
/>
|
||||
<div v-if="fileCount" class="record-count">解析到 {{ fileCount }} 条记录</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||
{{ isEdit ? '保存' : '上传' }}
|
||||
</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</PageCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-name {
|
||||
color: #303133;
|
||||
}
|
||||
.file-size {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.record-count {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
168
frontend/src/views/dataset/DatasetListView.vue
Normal file
168
frontend/src/views/dataset/DatasetListView.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataTablePage from '@/components/DataTablePage.vue'
|
||||
import { getDatasetList, deleteDataset, downloadDatasetUrl } from '@/api/modules/dataset'
|
||||
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
|
||||
import type { DatasetItem } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const dataList = ref<DatasetItem[]>([])
|
||||
const activeTab = ref('upload')
|
||||
|
||||
const filteredDataList = computed(() => {
|
||||
if (activeTab.value === 'upload') {
|
||||
return dataList.value
|
||||
} else {
|
||||
// 假设数据任务产生的数据集可以通过某个字段区分,目前 mock 数据没有该字段,所以暂为空
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
dataList.value = (await getDatasetList()) || []
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await deleteDataset(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
|
||||
function handlePreview(row: any) {
|
||||
router.push(`/dataset/${row.id}/preview`)
|
||||
}
|
||||
|
||||
function handleDownload(row: any) {
|
||||
window.open(downloadDatasetUrl(row.id), '_blank')
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTablePage
|
||||
title="数据集管理"
|
||||
:data="filteredDataList"
|
||||
:loading="loading"
|
||||
searchable
|
||||
:search-fields="['name', 'description']"
|
||||
:create-text="activeTab === 'upload' ? '上传数据集' : ''"
|
||||
create-to="/dataset/create"
|
||||
:delete-fn="handleDelete"
|
||||
row-key="id"
|
||||
@refresh="loadData"
|
||||
>
|
||||
<template #title>
|
||||
<div class="capsule-tabs">
|
||||
<button
|
||||
class="capsule-tab-item"
|
||||
:class="{ active: activeTab === 'upload' }"
|
||||
@click="activeTab = 'upload'"
|
||||
>
|
||||
本地上传
|
||||
</button>
|
||||
<button
|
||||
class="capsule-tab-item"
|
||||
:class="{ active: activeTab === 'task' }"
|
||||
@click="activeTab = 'task'"
|
||||
>
|
||||
数据任务
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #columns>
|
||||
<el-table-column label="数据集名称" prop="name" align="center" />
|
||||
<el-table-column label="数据类型" align="center" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="primary" size="small">
|
||||
{{ DATASET_TYPE_MAP[String(row.type).toLowerCase()] || row.type || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="存储位置" align="center" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" size="small">
|
||||
{{ STORAGE_MAP[row.storage_type] || row.storage_type || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大小" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.size && row.size !== '0 B' && row.size !== '0' ? row.size : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据条数" align="center" width="100">
|
||||
<template #default="{ row }">{{ row.count || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" align="center" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.description || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time ? new Date(row.create_time).toLocaleString('zh-CN') : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link size="small" @click="handlePreview(row)">
|
||||
<i class="fa fa-eye" style="margin-right: 4px" />预览
|
||||
</el-button>
|
||||
<el-button type="success" link size="small" @click="handleDownload(row)">
|
||||
<i class="fa fa-download" style="margin-right: 4px" />下载
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
<i class="fa fa-trash-o" style="margin-right: 4px" />删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTablePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 胶囊切换栏样式 */
|
||||
.capsule-tabs {
|
||||
display: flex;
|
||||
background: #f1f5f9;
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
gap: 2px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.capsule-tab-item {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 6px 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #4f46e5;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
157
frontend/src/views/dataset/DatasetPreviewView.vue
Normal file
157
frontend/src/views/dataset/DatasetPreviewView.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import PageCard from '@/components/PageCard.vue'
|
||||
import {
|
||||
getDataset,
|
||||
previewDatasetFile,
|
||||
deleteDataset,
|
||||
downloadDatasetUrl,
|
||||
downloadFileUrl,
|
||||
} from '@/api/modules/dataset'
|
||||
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
|
||||
import type { DatasetItem, DatasetFile } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const datasetId = route.params.id as string
|
||||
|
||||
const loading = ref(false)
|
||||
const dataset = ref<DatasetItem | null>(null)
|
||||
const selectedFileId = ref<string>('')
|
||||
const previewContent = ref('')
|
||||
|
||||
const files = computed<DatasetFile[]>(() => dataset.value?.files || [])
|
||||
|
||||
const previewLines = computed(() => previewContent.value.split('\n').slice(0, 100))
|
||||
const totalLines = computed(() => previewContent.value.split('\n').length)
|
||||
|
||||
async function loadDataset() {
|
||||
loading.value = true
|
||||
try {
|
||||
dataset.value = await getDataset(datasetId)
|
||||
if (files.value.length > 0) {
|
||||
selectedFileId.value = String(files.value[0].id || files.value[0].name)
|
||||
loadPreview()
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
if (!selectedFileId.value) return
|
||||
try {
|
||||
const res = await previewDatasetFile(selectedFileId.value)
|
||||
previewContent.value = res.content || ''
|
||||
} catch {
|
||||
previewContent.value = '加载失败'
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadFile(file: any) {
|
||||
const fid = file.id || file.name
|
||||
window.open(downloadFileUrl(datasetId, fid), '_blank')
|
||||
}
|
||||
|
||||
function handleDownloadAll() {
|
||||
window.open(downloadDatasetUrl(datasetId), '_blank')
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await ElMessageBox.confirm('确定要删除这个数据集吗?', '确认删除', { type: 'warning' })
|
||||
await deleteDataset(datasetId)
|
||||
ElMessage.success('删除成功')
|
||||
router.push('/dataset')
|
||||
}
|
||||
|
||||
onMounted(loadDataset)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageCard title="数据集预览" v-loading="loading">
|
||||
<template #extra>
|
||||
<el-button @click="handleDownloadAll"><i class="fa fa-download" /> 打包下载</el-button>
|
||||
<el-button type="danger" @click="handleDelete"><i class="fa fa-trash" /> 删除</el-button>
|
||||
<el-button @click="router.back()">返回</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 数据集信息 -->
|
||||
<el-descriptions :column="3" border style="margin-bottom: 20px">
|
||||
<el-descriptions-item label="名称">{{ dataset?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ DATASET_TYPE_MAP[String(dataset?.type).toLowerCase()] || dataset?.type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="存储位置">
|
||||
{{ STORAGE_MAP[dataset?.storage_type || ''] || dataset?.storage_type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据条数">{{ dataset?.count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ dataset?.create_time ? new Date(dataset.create_time).toLocaleString('zh-CN') : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述">{{ dataset?.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="file-section">
|
||||
<h3 class="section-title">文件列表</h3>
|
||||
<el-table :data="files" style="width: 100%">
|
||||
<el-table-column label="文件名" prop="name" />
|
||||
<el-table-column label="大小" prop="size" width="120" />
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="selectedFileId = String(row.id || row.name); loadPreview()">预览</el-button>
|
||||
<el-button link type="success" @click="handleDownloadFile(row)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 预览内容 -->
|
||||
<div class="preview-section">
|
||||
<h3 class="section-title">内容预览</h3>
|
||||
<pre class="preview-pre">{{ previewLines.join('\n') }}</pre>
|
||||
<div v-if="totalLines > 100" class="preview-footer">
|
||||
... 共 {{ totalLines }} 条记录,已显示前 100 条 ...
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.file-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.preview-pre {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.preview-footer {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user