fix: sync/barcode/memory overhaul + detailed logs + preview + result tracking
- Sync: fix GiteaSync constructor + add push()/pull() methods - Barcode: two-tab layout matching GUI (mapping + special rules) - Memory: spec→specification unification, manual add, confidence/price tracking - Processing: TaskLogHandler captures detailed logs (barcode mapping, unit conversion) - Preview: fullscreen dialog for file preview (image/Excel) in Orders/Tables/Images - Detail: per-file log filtering in file pages - Tasks: result files now per-task, add copy path button - Config: reactive edited state + save_config fix - Dashboard: sync task isolation, log limit 10 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<el-tag type="info" size="small">共 {{ total }} 个</el-tag>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="triggerUpload">上传图片</el-button>
|
||||
<el-button type="primary" :disabled="!selected.length" @click="batchPipeline">
|
||||
批量生成采购单 ({{ selected.length }})
|
||||
</el-button>
|
||||
@@ -18,6 +19,14 @@
|
||||
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
<input
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.bmp"
|
||||
hidden
|
||||
@change="handleUpload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +34,7 @@
|
||||
:data="items"
|
||||
v-loading="loading"
|
||||
@selection-change="onSelect"
|
||||
@sort-change="onSortChange"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
@@ -63,21 +73,37 @@
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<el-table-column label="创建时间" width="170" align="center" sortable="custom" prop="created_at">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="pipelineFile(row)">
|
||||
生成采购单
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="ocrFile(row)">
|
||||
仅OCR
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">
|
||||
删除
|
||||
</el-button>
|
||||
<span class="time-text">{{ fmtTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="previewFile(row)">预览</el-button>
|
||||
<el-button link type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="pipelineFile(row)">生成采购单</el-button>
|
||||
<el-button link type="primary" size="small" @click="ocrFile(row)">仅OCR</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showPreview" title="文件预览" :fullscreen="true" append-to-body :close-on-click-modal="false" class="preview-dialog">
|
||||
<div class="preview-body">
|
||||
<div v-if="previewType === 'image'" class="preview-image-wrap"><img :src="previewSrc" style="max-width:100%;max-height:100%;object-fit:contain" /></div>
|
||||
<div v-else-if="previewType === 'excel'" class="preview-table-wrap"><table class="preview-table"><tr v-for="(row, ri) in previewRows" :key="ri"><td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td></tr></table></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showDetailDlg" title="处理详情" width="70%" :close-on-click-modal="false" top="5vh">
|
||||
<div class="detail-logs">
|
||||
<div v-if="detailLogs.length === 0" style="text-align:center;color:var(--text-muted);padding:40px">暂无该文件的处理日志</div>
|
||||
<div v-for="(line, i) in detailLogs" :key="i" class="detail-line" :class="{err: line.includes('失败')||line.includes('错误'), ok: line.includes('完成')}">{{ line }}</div>
|
||||
</div>
|
||||
<template #footer><el-button @click="showDetailDlg = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
@@ -94,14 +120,27 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Right } from '@element-plus/icons-vue'
|
||||
import { useProcessingStore } from '../../stores/processing'
|
||||
import api from '../../api'
|
||||
|
||||
const processingStore = useProcessingStore()
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const loading = ref(false)
|
||||
const selected = ref<any[]>([])
|
||||
const sortBy = ref('created_at')
|
||||
const sortOrder = ref('desc')
|
||||
const uploadInput = ref<HTMLInputElement>()
|
||||
|
||||
const showPreview = ref(false)
|
||||
const previewType = ref('')
|
||||
const previewSrc = ref('')
|
||||
const previewRows = ref<string[][]>([])
|
||||
const showDetailDlg = ref(false)
|
||||
const detailLogs = ref<string[]>([])
|
||||
|
||||
function statusType(s: string) {
|
||||
const m: Record<string, string> = { done: 'success', merged: 'success', excel_done: 'warning', ocr_done: 'info', pending: 'info' }
|
||||
@@ -112,10 +151,15 @@ function statusText(s: string) {
|
||||
return m[s] || s
|
||||
}
|
||||
|
||||
function fmtTime(t: string): string {
|
||||
if (!t) return '--'
|
||||
return t.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/files/relations', { params: { view: 'images', page: page.value, page_size: pageSize } })
|
||||
const res = await api.get('/files/relations', { params: { view: 'images', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} catch {}
|
||||
@@ -124,6 +168,64 @@ async function loadData() {
|
||||
|
||||
function onSelect(rows: any[]) { selected.value = rows }
|
||||
|
||||
async function previewFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
const fname = row.input_image || row.output_excel || row.result_purchase
|
||||
const dir = row.input_image ? 'input' : row.output_excel ? 'output' : 'result'
|
||||
try {
|
||||
const resp = await fetch(`/api/files/preview/${dir}/${encodeURIComponent(fname)}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
const ct = resp.headers.get('content-type') || ''
|
||||
if (ct.includes('image')) {
|
||||
previewType.value = 'image'
|
||||
const blob = await resp.blob()
|
||||
previewSrc.value = URL.createObjectURL(blob)
|
||||
} else {
|
||||
const data = await resp.json()
|
||||
if (data.type === 'excel') { previewType.value = 'excel'; previewRows.value = data.rows }
|
||||
}
|
||||
showPreview.value = true
|
||||
} catch { ElMessage.error('预览失败') }
|
||||
}
|
||||
|
||||
function showDetail(row: any) {
|
||||
const fname = row.input_image || row.output_excel || row.result_purchase
|
||||
const stem = fname.replace(/\.[^.]+$/, '')
|
||||
detailLogs.value = (processingStore.logs || []).filter((l: string) => l.includes(fname) || l.includes(stem))
|
||||
showDetailDlg.value = true
|
||||
}
|
||||
|
||||
function onSortChange({ prop, order }: any) {
|
||||
if (prop === 'created_at') {
|
||||
sortBy.value = 'created_at'
|
||||
sortOrder.value = order === 'ascending' ? 'asc' : 'desc'
|
||||
} else {
|
||||
sortBy.value = ''
|
||||
sortOrder.value = 'desc'
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
function triggerUpload() {
|
||||
uploadInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleUpload(e: Event) {
|
||||
const el = e.target as HTMLInputElement
|
||||
if (!el.files || !el.files.length) return
|
||||
for (const file of Array.from(el.files)) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
try {
|
||||
await api.post('/files/upload?target=input', fd)
|
||||
ElMessage.success(`已上传: ${file.name}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(`上传失败: ${file.name}`)
|
||||
}
|
||||
}
|
||||
el.value = ''
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function pipelineFile(row: any) {
|
||||
try {
|
||||
const res = await api.post('/processing/pipeline-single', { filename: row.input_image })
|
||||
@@ -241,9 +343,29 @@ onMounted(loadData)
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.preview-table td { border:1px solid var(--border-light);padding:4px 8px;white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis }
|
||||
.preview-table tr:nth-child(even) { background:#fafafa }
|
||||
.preview-table tr:first-child { background:#f0f0f0;font-weight:600 }
|
||||
.detail-logs { max-height:60vh;overflow-y:auto;background:#09090b;border-radius:8px;padding:14px;font-family:var(--font-mono);font-size:12px;line-height:1.8 }
|
||||
.detail-line { color:#a1a1aa }
|
||||
.detail-line.err { color:#ef4444 }
|
||||
.detail-line.ok { color:#22c55e }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) { display:flex;flex-direction:column;width:96vw;height:94vh;margin:3vh 2vw;border-radius:12px!important }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__header { flex-shrink:0 }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__body { flex:1;min-height:0;padding:8px 16px 16px;overflow:hidden;display:flex;flex-direction:column }
|
||||
.preview-body { flex:1;min-height:0;display:flex;flex-direction:column }
|
||||
.preview-image-wrap { flex:1;display:flex;align-items:center;justify-content:center;min-height:0 }
|
||||
.preview-table-wrap { flex:1;overflow:auto;min-height:0;border:1px solid var(--border-light);border-radius:8px }
|
||||
.preview-table { border-collapse:collapse;font-size:12px;width:100% }
|
||||
</style>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
:data="items"
|
||||
v-loading="loading"
|
||||
@selection-change="onSelect"
|
||||
@sort-change="onSortChange"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
@@ -60,18 +61,49 @@
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<el-table-column label="创建时间" width="170" align="center" sortable="custom" prop="created_at">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="downloadFile(row)">
|
||||
下载
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">
|
||||
删除
|
||||
</el-button>
|
||||
<span class="time-text">{{ fmtTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="previewFile(row)">预览</el-button>
|
||||
<el-button link type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="downloadFile(row)">下载</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- Preview dialog -->
|
||||
<el-dialog v-model="showPreview" title="文件预览" :fullscreen="true" append-to-body :close-on-click-modal="false" class="preview-dialog">
|
||||
<div class="preview-body">
|
||||
<div v-if="previewType === 'image'" class="preview-image-wrap">
|
||||
<img :src="previewSrc" style="max-width:100%;max-height:100%;object-fit:contain" />
|
||||
</div>
|
||||
<div v-else-if="previewType === 'excel'" class="preview-table-wrap">
|
||||
<table class="preview-table">
|
||||
<tr v-for="(row, ri) in previewRows" :key="ri">
|
||||
<td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else style="text-align:center;color:var(--text-muted);padding:40px">暂无可预览内容</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Detail dialog -->
|
||||
<el-dialog v-model="showDetailDlg" title="处理详情" width="70%" :close-on-click-modal="false" top="5vh">
|
||||
<div class="detail-logs">
|
||||
<div v-if="detailLogs.length === 0" style="text-align:center;color:var(--text-muted);padding:40px">暂无该文件的处理日志</div>
|
||||
<div v-for="(line, i) in detailLogs" :key="i" class="detail-line" :class="{err: line.includes('失败')||line.includes('错误'), ok: line.includes('完成')}">{{ line }}</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showDetailDlg = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
@@ -88,14 +120,29 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Right } from '@element-plus/icons-vue'
|
||||
import { useProcessingStore } from '../../stores/processing'
|
||||
import api from '../../api'
|
||||
|
||||
const processingStore = useProcessingStore()
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const loading = ref(false)
|
||||
const selected = ref<any[]>([])
|
||||
const sortBy = ref('created_at')
|
||||
const sortOrder = ref('desc')
|
||||
|
||||
// Preview
|
||||
const showPreview = ref(false)
|
||||
const previewType = ref('')
|
||||
const previewSrc = ref('')
|
||||
const previewRows = ref<string[][]>([])
|
||||
|
||||
// Detail
|
||||
const showDetailDlg = ref(false)
|
||||
const detailLogs = ref<string[]>([])
|
||||
|
||||
function statusType(s: string) {
|
||||
const m: Record<string, string> = { done: 'success', merged: 'success', excel_done: 'warning', ocr_done: 'info', pending: 'info' }
|
||||
@@ -106,10 +153,15 @@ function statusText(s: string) {
|
||||
return m[s] || s
|
||||
}
|
||||
|
||||
function fmtTime(t: string): string {
|
||||
if (!t) return '--'
|
||||
return t.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/files/relations', { params: { view: 'orders', page: page.value, page_size: pageSize } })
|
||||
const res = await api.get('/files/relations', { params: { view: 'orders', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} catch {}
|
||||
@@ -118,6 +170,54 @@ async function loadData() {
|
||||
|
||||
function onSelect(rows: any[]) { selected.value = rows }
|
||||
|
||||
async function previewFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
const fname = row.result_purchase || row.output_excel || row.input_image
|
||||
const dir = row.result_purchase ? 'result' : row.output_excel ? 'output' : 'input'
|
||||
const url = `/api/files/preview/${dir}/${encodeURIComponent(fname)}`
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } })
|
||||
const ct = resp.headers.get('content-type') || ''
|
||||
|
||||
if (ct.includes('image')) {
|
||||
previewType.value = 'image'
|
||||
const blob = await resp.blob()
|
||||
previewSrc.value = URL.createObjectURL(blob)
|
||||
} else {
|
||||
const data = await resp.json()
|
||||
if (data.type === 'excel') {
|
||||
previewType.value = 'excel'
|
||||
previewRows.value = data.rows
|
||||
}
|
||||
}
|
||||
showPreview.value = true
|
||||
} catch {
|
||||
ElMessage.error('预览失败')
|
||||
}
|
||||
}
|
||||
|
||||
function showDetail(row: any) {
|
||||
const fname = row.result_purchase || row.output_excel || row.input_image
|
||||
const stem = fname.replace(/\.[^.]+$/, '')
|
||||
// Filter logs from the processing store that mention this file
|
||||
detailLogs.value = (processingStore.logs || []).filter(
|
||||
(l: string) => l.includes(fname) || l.includes(stem)
|
||||
)
|
||||
showDetailDlg.value = true
|
||||
}
|
||||
|
||||
function onSortChange({ prop, order }: any) {
|
||||
if (prop === 'created_at') {
|
||||
sortBy.value = 'created_at'
|
||||
sortOrder.value = order === 'ascending' ? 'asc' : 'desc'
|
||||
} else {
|
||||
sortBy.value = ''
|
||||
sortOrder.value = 'desc'
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function downloadFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
window.open(`/api/files/download/result/${encodeURIComponent(row.result_purchase)}?token=${token}`, '_blank')
|
||||
@@ -204,9 +304,47 @@ onMounted(loadData)
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.preview-table td {
|
||||
border: 1px solid var(--border-light);
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.preview-table tr:nth-child(even) { background: #fafafa; }
|
||||
.preview-table tr:first-child { background: #f0f0f0; font-weight: 600; }
|
||||
|
||||
.detail-logs {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: #09090b;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.detail-line { color: #a1a1aa; }
|
||||
.detail-line.err { color: #ef4444; }
|
||||
.detail-line.ok { color: #22c55e; }
|
||||
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) { display:flex; flex-direction:column; width:96vw; height:94vh; margin:3vh 2vw; border-radius:12px!important }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__header { flex-shrink:0 }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__body { flex:1; min-height:0; padding:8px 16px 16px; overflow:hidden; display:flex; flex-direction:column }
|
||||
.preview-body { flex:1; min-height:0; display:flex; flex-direction:column }
|
||||
.preview-image-wrap { flex:1; display:flex; align-items:center; justify-content:center; min-height:0 }
|
||||
.preview-table-wrap { flex:1; overflow:auto; min-height:0; border:1px solid var(--border-light); border-radius:8px }
|
||||
.preview-table { border-collapse:collapse;font-size:12px;width:100% }
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<el-tag type="info" size="small">共 {{ total }} 个</el-tag>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="triggerUpload">上传Excel</el-button>
|
||||
<el-button type="primary" :disabled="!selected.length" @click="batchProcess">
|
||||
批量处理 ({{ selected.length }})
|
||||
</el-button>
|
||||
@@ -15,6 +16,14 @@
|
||||
<el-button type="danger" @click="clearAll">
|
||||
删除全部
|
||||
</el-button>
|
||||
<input
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".xls,.xlsx"
|
||||
hidden
|
||||
@change="handleUpload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +31,7 @@
|
||||
:data="items"
|
||||
v-loading="loading"
|
||||
@selection-change="onSelect"
|
||||
@sort-change="onSortChange"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
@@ -60,18 +70,36 @@
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<el-table-column label="创建时间" width="170" align="center" sortable="custom" prop="created_at">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="processFile(row)">
|
||||
处理
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">
|
||||
删除
|
||||
</el-button>
|
||||
<span class="time-text">{{ fmtTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="previewFile(row)">预览</el-button>
|
||||
<el-button link type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="processFile(row)">处理</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showPreview" title="文件预览" :fullscreen="true" append-to-body :close-on-click-modal="false" class="preview-dialog">
|
||||
<div class="preview-body">
|
||||
<div v-if="previewType === 'image'" class="preview-image-wrap"><img :src="previewSrc" style="max-width:100%;max-height:100%;object-fit:contain" /></div>
|
||||
<div v-else-if="previewType === 'excel'" class="preview-table-wrap"><table class="preview-table"><tr v-for="(row, ri) in previewRows" :key="ri"><td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td></tr></table></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showDetailDlg" title="处理详情" width="70%" :close-on-click-modal="false" top="5vh">
|
||||
<div class="detail-logs">
|
||||
<div v-if="detailLogs.length === 0" style="text-align:center;color:var(--text-muted);padding:40px">暂无该文件的处理日志</div>
|
||||
<div v-for="(line, i) in detailLogs" :key="i" class="detail-line" :class="{err: line.includes('失败')||line.includes('错误'), ok: line.includes('完成')}">{{ line }}</div>
|
||||
</div>
|
||||
<template #footer><el-button @click="showDetailDlg = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
@@ -88,14 +116,27 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Right } from '@element-plus/icons-vue'
|
||||
import { useProcessingStore } from '../../stores/processing'
|
||||
import api from '../../api'
|
||||
|
||||
const processingStore = useProcessingStore()
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const loading = ref(false)
|
||||
const selected = ref<any[]>([])
|
||||
const sortBy = ref('created_at')
|
||||
const sortOrder = ref('desc')
|
||||
const uploadInput = ref<HTMLInputElement>()
|
||||
|
||||
const showPreview = ref(false)
|
||||
const previewType = ref('')
|
||||
const previewSrc = ref('')
|
||||
const previewRows = ref<string[][]>([])
|
||||
const showDetailDlg = ref(false)
|
||||
const detailLogs = ref<string[]>([])
|
||||
|
||||
function statusType(s: string) {
|
||||
const m: Record<string, string> = { done: 'success', merged: 'success', excel_done: 'warning', ocr_done: 'info', pending: 'info' }
|
||||
@@ -106,10 +147,15 @@ function statusText(s: string) {
|
||||
return m[s] || s
|
||||
}
|
||||
|
||||
function fmtTime(t: string): string {
|
||||
if (!t) return '--'
|
||||
return t.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/files/relations', { params: { view: 'tables', page: page.value, page_size: pageSize } })
|
||||
const res = await api.get('/files/relations', { params: { view: 'tables', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} catch {}
|
||||
@@ -118,6 +164,64 @@ async function loadData() {
|
||||
|
||||
function onSelect(rows: any[]) { selected.value = rows }
|
||||
|
||||
async function previewFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
const fname = row.output_excel || row.result_purchase || row.input_image
|
||||
const dir = row.output_excel ? 'output' : row.result_purchase ? 'result' : 'input'
|
||||
try {
|
||||
const resp = await fetch(`/api/files/preview/${dir}/${encodeURIComponent(fname)}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
const ct = resp.headers.get('content-type') || ''
|
||||
if (ct.includes('image')) {
|
||||
previewType.value = 'image'
|
||||
const blob = await resp.blob()
|
||||
previewSrc.value = URL.createObjectURL(blob)
|
||||
} else {
|
||||
const data = await resp.json()
|
||||
if (data.type === 'excel') { previewType.value = 'excel'; previewRows.value = data.rows }
|
||||
}
|
||||
showPreview.value = true
|
||||
} catch { ElMessage.error('预览失败') }
|
||||
}
|
||||
|
||||
function showDetail(row: any) {
|
||||
const fname = row.output_excel || row.result_purchase || row.input_image
|
||||
const stem = fname.replace(/\.[^.]+$/, '')
|
||||
detailLogs.value = (processingStore.logs || []).filter((l: string) => l.includes(fname) || l.includes(stem))
|
||||
showDetailDlg.value = true
|
||||
}
|
||||
|
||||
function onSortChange({ prop, order }: any) {
|
||||
if (prop === 'created_at') {
|
||||
sortBy.value = 'created_at'
|
||||
sortOrder.value = order === 'ascending' ? 'asc' : 'desc'
|
||||
} else {
|
||||
sortBy.value = ''
|
||||
sortOrder.value = 'desc'
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
function triggerUpload() {
|
||||
uploadInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleUpload(e: Event) {
|
||||
const el = e.target as HTMLInputElement
|
||||
if (!el.files || !el.files.length) return
|
||||
for (const file of Array.from(el.files)) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
try {
|
||||
await api.post('/files/upload?target=output', fd)
|
||||
ElMessage.success(`已上传: ${file.name}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(`上传失败: ${file.name}`)
|
||||
}
|
||||
}
|
||||
el.value = ''
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function processFile(row: any) {
|
||||
try {
|
||||
const res = await api.post('/processing/excel-single', { filename: row.output_excel })
|
||||
@@ -208,9 +312,29 @@ onMounted(loadData)
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.preview-table td { border:1px solid var(--border-light);padding:4px 8px;white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis }
|
||||
.preview-table tr:nth-child(even) { background:#fafafa }
|
||||
.preview-table tr:first-child { background:#f0f0f0;font-weight:600 }
|
||||
.detail-logs { max-height:60vh;overflow-y:auto;background:#09090b;border-radius:8px;padding:14px;font-family:var(--font-mono);font-size:12px;line-height:1.8 }
|
||||
.detail-line { color:#a1a1aa }
|
||||
.detail-line.err { color:#ef4444 }
|
||||
.detail-line.ok { color:#22c55e }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) { display:flex;flex-direction:column;width:96vw;height:94vh;margin:3vh 2vw;border-radius:12px!important }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__header { flex-shrink:0 }
|
||||
:global(.preview-dialog.el-dialog.is-fullscreen) .el-dialog__body { flex:1;min-height:0;padding:8px 16px 16px;overflow:hidden;display:flex;flex-direction:column }
|
||||
.preview-body { flex:1;min-height:0;display:flex;flex-direction:column }
|
||||
.preview-image-wrap { flex:1;display:flex;align-items:center;justify-content:center;min-height:0 }
|
||||
.preview-table-wrap { flex:1;overflow:auto;min-height:0;border:1px solid var(--border-light);border-radius:8px }
|
||||
.preview-table { border-collapse:collapse;font-size:12px;width:100% }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user