feat: 前端补齐批量上传、重新摘要入库与入库进度页
- api/documents.js: 新增 uploadBatch / reingest / taskList 封装 - Ingest.vue: 新增「批量上传」标签页,多文件逐项异步入库并展示结果 - Documents.vue: 文档操作列新增「重新摘要」按钮(读原文件→删旧→新任务) - 新增 Tasks.vue 入库进度页:近期任务列表 + 2s 自动轮询 + 状态徽标 - 路由与菜单新增「入库进度」入口
This commit is contained in:
@@ -52,6 +52,37 @@ export function upload(formData) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* multipart 批量文件上传入库(异步,逐文件创建任务)
|
||||||
|
* @param {File[]} files
|
||||||
|
* @returns {Promise<{tasks: Array<{filename:string, task_id:string}>, failed: Array<{filename:string, error:string}>}>}
|
||||||
|
*/
|
||||||
|
export function uploadBatch(files) {
|
||||||
|
const formData = new FormData()
|
||||||
|
files.forEach((f) => formData.append('files', f))
|
||||||
|
return http.post('/api/v1/documents/upload-batch', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新摘要入库(读原文件→删旧数据→新建任务)
|
||||||
|
* @param {string} docId
|
||||||
|
* @returns {Promise<{task_id:string, status:string}>}
|
||||||
|
*/
|
||||||
|
export function reingest(docId) {
|
||||||
|
return http.post(`/api/v1/documents/${encodeURIComponent(docId)}/reingest`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出近期入库任务(按 updated_at 降序)
|
||||||
|
* @param {number} [limit=20]
|
||||||
|
* @returns {Promise<{items: Array, total: number}>}
|
||||||
|
*/
|
||||||
|
export function taskList(limit = 20) {
|
||||||
|
return http.get('/api/v1/documents/tasks', { params: { limit } })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询入库任务状态
|
* 查询入库任务状态
|
||||||
* @param {string} taskId
|
* @param {string} taskId
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
TeamOutlined
|
TeamOutlined,
|
||||||
|
ClockCircleOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useAuthStore } from '@/stores/useAuthStore'
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
import { changeMyPassword } from '@/api/auth'
|
import { changeMyPassword } from '@/api/auth'
|
||||||
@@ -39,6 +40,7 @@ const menuItems = computed(() => {
|
|||||||
{ key: 'overview', icon: () => h(DashboardOutlined), label: '概览' },
|
{ key: 'overview', icon: () => h(DashboardOutlined), label: '概览' },
|
||||||
{ key: 'documents', icon: () => h(FileTextOutlined), label: '文档管理' },
|
{ key: 'documents', icon: () => h(FileTextOutlined), label: '文档管理' },
|
||||||
{ key: 'ingest', icon: () => h(UploadOutlined), label: '文档入库' },
|
{ key: 'ingest', icon: () => h(UploadOutlined), label: '文档入库' },
|
||||||
|
{ key: 'tasks', icon: () => h(ClockCircleOutlined), label: '入库进度' },
|
||||||
{ key: 'search', icon: () => h(SearchOutlined), label: '检索测试台' },
|
{ key: 'search', icon: () => h(SearchOutlined), label: '检索测试台' },
|
||||||
{ key: 'categories', icon: () => h(AppstoreOutlined), label: '类目列表' },
|
{ key: 'categories', icon: () => h(AppstoreOutlined), label: '类目列表' },
|
||||||
{ key: 'settings', icon: () => h(SettingOutlined), label: '设置' },
|
{ key: 'settings', icon: () => h(SettingOutlined), label: '设置' },
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const routes = [
|
|||||||
component: () => import('@/views/Ingest.vue'),
|
component: () => import('@/views/Ingest.vue'),
|
||||||
meta: { title: '文档入库', requiresAuth: true }
|
meta: { title: '文档入库', requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
name: 'tasks',
|
||||||
|
component: () => import('@/views/Tasks.vue'),
|
||||||
|
meta: { title: '入库进度', requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
name: 'search',
|
name: 'search',
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { FileOutlined } from '@ant-design/icons-vue'
|
|||||||
import {
|
import {
|
||||||
list as fetchDocuments,
|
list as fetchDocuments,
|
||||||
detail as fetchDocumentDetail,
|
detail as fetchDocumentDetail,
|
||||||
remove as deleteDocument
|
remove as deleteDocument,
|
||||||
|
reingest as reingestDocument
|
||||||
} from '@/api/documents'
|
} from '@/api/documents'
|
||||||
import { truncate } from '@/utils/format'
|
import { truncate } from '@/utils/format'
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ function formatSize(bytes) {
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isLoadingDetail = ref(false)
|
const isLoadingDetail = ref(false)
|
||||||
const isDeleting = ref(false)
|
const isDeleting = ref(false)
|
||||||
|
const isReingesting = ref(false)
|
||||||
|
|
||||||
const documents = ref([])
|
const documents = ref([])
|
||||||
const nextOffset = ref(null)
|
const nextOffset = ref(null)
|
||||||
@@ -99,6 +101,26 @@ function handleDelete(doc) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新摘要入库:读原文件→删旧数据→提交新任务
|
||||||
|
function handleReingest(doc) {
|
||||||
|
const title = doc.title || doc.doc_id
|
||||||
|
Modal.confirm({
|
||||||
|
title: '重新摘要入库',
|
||||||
|
content: `将重新读取文档「${title}」(${doc.doc_id}) 的原始文件,删除旧数据后重新生成三级总结并入库。生成新的 doc_id。是否继续?`,
|
||||||
|
okText: '重新入库',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'primary',
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
const data = await reingestDocument(doc.doc_id)
|
||||||
|
message.success(`已提交重新入库任务:${data.task_id}`)
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err?.message || '重新入库失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleCloseDetail() {
|
function handleCloseDetail() {
|
||||||
detailVisible.value = false
|
detailVisible.value = false
|
||||||
detailData.value = null
|
detailData.value = null
|
||||||
@@ -109,7 +131,7 @@ const columns = [
|
|||||||
{ title: '类目', dataIndex: 'category', key: 'category', width: 140 },
|
{ title: '类目', dataIndex: 'category', key: 'category', width: 140 },
|
||||||
{ title: '标签', dataIndex: 'tags', key: 'tags', width: 220 },
|
{ title: '标签', dataIndex: 'tags', key: 'tags', width: 220 },
|
||||||
{ title: 'L1 摘要', dataIndex: 'summary', key: 'summary', ellipsis: true },
|
{ title: 'L1 摘要', dataIndex: 'summary', key: 'summary', ellipsis: true },
|
||||||
{ title: '操作', key: 'action', width: 160, fixed: 'right' }
|
{ title: '操作', key: 'action', width: 200, fixed: 'right' }
|
||||||
]
|
]
|
||||||
|
|
||||||
function getDocTitle(record) {
|
function getDocTitle(record) {
|
||||||
@@ -158,6 +180,14 @@ onMounted(() => {
|
|||||||
<a-button type="link" size="small" @click="handleViewDetail(record.doc_id)">
|
<a-button type="link" size="small" @click="handleViewDetail(record.doc_id)">
|
||||||
详情
|
详情
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:loading="isReingesting"
|
||||||
|
@click="handleReingest(record)"
|
||||||
|
>
|
||||||
|
重新摘要
|
||||||
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { ingest as ingestDocument, upload as uploadDocument } from '@/api/documents'
|
import {
|
||||||
|
ingest as ingestDocument,
|
||||||
|
upload as uploadDocument,
|
||||||
|
uploadBatch
|
||||||
|
} from '@/api/documents'
|
||||||
import { useIngestPolling } from '@/composables/useIngestPolling'
|
import { useIngestPolling } from '@/composables/useIngestPolling'
|
||||||
import {
|
import {
|
||||||
INGEST_STATUS_TEXT,
|
INGEST_STATUS_TEXT,
|
||||||
@@ -34,12 +38,26 @@ const fileForm = reactive({
|
|||||||
const fileList = ref([])
|
const fileList = ref([])
|
||||||
const rawFile = ref(null)
|
const rawFile = ref(null)
|
||||||
|
|
||||||
|
// 批量上传结果:{tasks: [{filename, task_id}], failed: [{filename, error}]}
|
||||||
|
const batchResult = ref(null)
|
||||||
|
const isSubmittingBatch = ref(false)
|
||||||
|
|
||||||
const ACCEPTED_EXTENSIONS = '.txt,.md,.html,.htm,.pdf,.docx'
|
const ACCEPTED_EXTENSIONS = '.txt,.md,.html,.htm,.pdf,.docx'
|
||||||
|
|
||||||
const statusBadgeColor = computed(() => {
|
const statusBadgeColor = computed(() => {
|
||||||
return INGEST_STATUS_COLOR[pollState.status] || 'default'
|
return INGEST_STATUS_COLOR[pollState.status] || 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const batchColumns = [
|
||||||
|
{ title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true },
|
||||||
|
{ title: 'task_id', dataIndex: 'task_id', key: 'task_id', width: 260 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const failedColumns = [
|
||||||
|
{ title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true },
|
||||||
|
{ title: '错误', dataIndex: 'error', key: 'error' }
|
||||||
|
]
|
||||||
|
|
||||||
const statusBadgeText = computed(() => {
|
const statusBadgeText = computed(() => {
|
||||||
return INGEST_STATUS_TEXT[pollState.status] || pollState.status || '-'
|
return INGEST_STATUS_TEXT[pollState.status] || pollState.status || '-'
|
||||||
})
|
})
|
||||||
@@ -60,9 +78,28 @@ function handleFileChange(file) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileRemove() {
|
function handleFileRemove(file) {
|
||||||
rawFile.value = null
|
rawFile.value = null
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
|
batchResult.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量上传:before-upload 返回 false,收集多个 File 到 fileList
|
||||||
|
function handleBatchFileChange(file, fileListArg) {
|
||||||
|
const files = fileListArg.map((f) => f.originFileObj || f)
|
||||||
|
fileList.value = files
|
||||||
|
batchResult.value = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchRemove(file) {
|
||||||
|
fileList.value = fileList.value.filter((f) => f !== file && f.name !== file.name)
|
||||||
|
batchResult.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchClear() {
|
||||||
|
fileList.value = []
|
||||||
|
batchResult.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmitText() {
|
async function handleSubmitText() {
|
||||||
@@ -115,6 +152,33 @@ async function handleSubmitFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSubmitBatch() {
|
||||||
|
if (!fileList.value.length) {
|
||||||
|
message.warning('请选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isSubmittingBatch.value = true
|
||||||
|
try {
|
||||||
|
const data = await uploadBatch(fileList.value)
|
||||||
|
batchResult.value = data
|
||||||
|
const okCount = data.tasks?.length || 0
|
||||||
|
const failCount = data.failed?.length || 0
|
||||||
|
if (okCount > 0) {
|
||||||
|
message.success(`已提交 ${okCount} 个任务${failCount > 0 ? `,${failCount} 个失败` : ''}`)
|
||||||
|
} else if (failCount > 0) {
|
||||||
|
message.error(`全部 ${failCount} 个文件上传失败`)
|
||||||
|
}
|
||||||
|
// 若仅一个成功任务,直接轮询其进度
|
||||||
|
if (data.tasks?.length === 1 && failCount === 0) {
|
||||||
|
startPolling(data.tasks[0].task_id)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err?.message || '批量上传失败')
|
||||||
|
} finally {
|
||||||
|
isSubmittingBatch.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleCancelPolling() {
|
function handleCancelPolling() {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
message.info('已停止轮询')
|
message.info('已停止轮询')
|
||||||
@@ -208,6 +272,85 @@ function handleCancelPolling() {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
|
<a-tab-pane key="batch" tab="批量上传">
|
||||||
|
<a-form :model="fileForm" layout="vertical">
|
||||||
|
<a-form-item label="选择文件(可多选)">
|
||||||
|
<a-upload
|
||||||
|
multiple
|
||||||
|
:file-list="fileList"
|
||||||
|
:accept="ACCEPTED_EXTENSIONS"
|
||||||
|
:before-upload="handleBatchFileChange"
|
||||||
|
@remove="handleBatchRemove"
|
||||||
|
>
|
||||||
|
<a-button>选择文件</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<div class="text-muted" style="margin-top: 4px">
|
||||||
|
支持 .txt/.md/.html/.htm/.pdf/.docx,可多选,逐文件异步入库
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="fileList.length">
|
||||||
|
<a-button type="primary" :loading="isSubmittingBatch" @click="handleSubmitBatch">
|
||||||
|
批量上传入库({{ fileList.length }} 个文件)
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
style="margin-left: 8px"
|
||||||
|
:disabled="isSubmittingBatch"
|
||||||
|
@click="handleBatchClear"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<div v-if="batchResult" class="ingest__result" style="margin-top: 16px">
|
||||||
|
<div class="ingest__result-header">
|
||||||
|
<span>批量上传结果</span>
|
||||||
|
</div>
|
||||||
|
<a-alert
|
||||||
|
v-if="batchResult.failed && batchResult.failed.length"
|
||||||
|
class="ingest__alert"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:message="`${batchResult.failed.length} 个文件上传失败`"
|
||||||
|
/>
|
||||||
|
<a-table
|
||||||
|
v-if="batchResult.tasks && batchResult.tasks.length"
|
||||||
|
:data-source="batchResult.tasks"
|
||||||
|
:columns="batchColumns"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
row-key="task_id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'filename'">
|
||||||
|
<span :title="record.filename">{{ record.filename }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'task_id'">
|
||||||
|
<a-typography-text
|
||||||
|
copyable
|
||||||
|
:style="{ fontSize: '12px' }"
|
||||||
|
>{{ record.task_id }}</a-typography-text>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
<a-table
|
||||||
|
v-if="batchResult.failed && batchResult.failed.length"
|
||||||
|
:data-source="batchResult.failed"
|
||||||
|
:columns="failedColumns"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
row-key="filename"
|
||||||
|
style="margin-top: 12px"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'error'">
|
||||||
|
<span class="text-break">{{ record.error }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|
||||||
<div v-if="pollState.taskId" class="ingest__result">
|
<div v-if="pollState.taskId" class="ingest__result">
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { taskList } from '@/api/documents'
|
||||||
|
import {
|
||||||
|
INGEST_STATUS_TEXT,
|
||||||
|
INGEST_STATUS_COLOR
|
||||||
|
} from '@/constants/ingest'
|
||||||
|
|
||||||
|
const tasks = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isPolling = ref(false)
|
||||||
|
const limit = ref(50)
|
||||||
|
|
||||||
|
let timer = null
|
||||||
|
|
||||||
|
function formatTime(iso) {
|
||||||
|
if (!iso) return '-'
|
||||||
|
const d = new Date(iso)
|
||||||
|
if (Number.isNaN(d.getTime())) return iso
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText(s) {
|
||||||
|
return INGEST_STATUS_TEXT[s] || s || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(s) {
|
||||||
|
return INGEST_STATUS_COLOR[s] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTasks(showSpinner = false) {
|
||||||
|
if (showSpinner) isLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await taskList(limit.value)
|
||||||
|
tasks.value = data.items || []
|
||||||
|
total.value = data.total ?? tasks.value.length
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err?.message || '加载入库任务失败')
|
||||||
|
} finally {
|
||||||
|
if (showSpinner) isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
if (timer) return
|
||||||
|
isPolling.value = true
|
||||||
|
timer = setInterval(async () => {
|
||||||
|
await loadTasks(false)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
isPolling.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePolling() {
|
||||||
|
if (isPolling.value) {
|
||||||
|
stopPolling()
|
||||||
|
} else {
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
loadTasks(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTasks(true)
|
||||||
|
startPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'task_id', dataIndex: 'task_id', key: 'task_id', width: 260 },
|
||||||
|
{ title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 110 },
|
||||||
|
{ title: 'doc_id', dataIndex: 'doc_id', key: 'doc_id', width: 260 },
|
||||||
|
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||||
|
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 170 }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tasks page-section">
|
||||||
|
<div class="tasks__header">
|
||||||
|
<h2 class="tasks__title">入库进度</h2>
|
||||||
|
<div class="tasks__actions">
|
||||||
|
<span class="text-muted" style="margin-right: 8px">共 {{ total }} 条</span>
|
||||||
|
<a-button size="small" :loading="isLoading" @click="handleRefresh">刷新</a-button>
|
||||||
|
<a-button size="small" :type="isPolling ? 'default' : 'primary'" @click="togglePolling">
|
||||||
|
{{ isPolling ? '停止轮询' : '开始轮询' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-alert
|
||||||
|
v-if="isPolling"
|
||||||
|
class="tasks__alert"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
message="每 2 秒自动刷新,展示近期上传文件与摘要入库进度。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tasks"
|
||||||
|
:pagination="false"
|
||||||
|
:loading="isLoading"
|
||||||
|
row-key="task_id"
|
||||||
|
size="small"
|
||||||
|
:scroll="{ x: 1100 }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'task_id'">
|
||||||
|
<a-typography-text copyable :style="{ fontSize: '12px' }">
|
||||||
|
{{ record.task_id }}
|
||||||
|
</a-typography-text>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'filename'">
|
||||||
|
<span :title="record.filename">{{ record.filename || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'doc_id'">
|
||||||
|
<a-typography-text
|
||||||
|
v-if="record.doc_id"
|
||||||
|
copyable
|
||||||
|
:style="{ fontSize: '12px' }"
|
||||||
|
>
|
||||||
|
{{ record.doc_id }}
|
||||||
|
</a-typography-text>
|
||||||
|
<span v-else class="text-muted">-</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'created_at'">
|
||||||
|
{{ formatTime(record.created_at) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'updated_at'">
|
||||||
|
{{ formatTime(record.updated_at) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tasks__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks__title {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks__alert {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user