Files
orc-order-v2/web/frontend/src/views/files/Tables.vue
T
2026-05-12 21:49:58 +08:00

322 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="file-page animate-in">
<div class="page-header">
<div class="header-left">
<h3>表格处理</h3>
<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>
<el-button :disabled="!selected.length" @click="batchDelete">
批量删除
</el-button>
<el-button type="danger" @click="clearAll">
删除全部
</el-button>
<input
ref="uploadInput"
type="file"
multiple
accept=".xls,.xlsx"
hidden
@change="handleUpload"
/>
</div>
</div>
<el-table
:data="items"
v-loading="loading"
@selection-change="onSelect"
@sort-change="onSortChange"
stripe
style="width: 100%"
>
<el-table-column type="selection" width="45" />
<el-table-column label="Excel处理文件" min-width="200">
<template #default="{ row }">
<span class="file-name primary">{{ row.output_excel || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="" width="40" align="center">
<template #default="{ row }">
<el-icon :color="row.result_exists ? '#52C41A' : '#d1d5db'" :size="16">
<Right />
</el-icon>
</template>
</el-table-column>
<el-table-column label="采购单文件" min-width="180">
<template #default="{ row }">
<span class="file-name secondary">{{ row.result_purchase || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="" width="40" align="center">
<template #default="{ row }">
<el-icon :color="row.input_exists ? '#52C41A' : '#d1d5db'" :size="16">
<Right />
</el-icon>
</template>
</el-table-column>
<el-table-column label="Input图片" min-width="180">
<template #default="{ row }">
<span class="file-name secondary">{{ row.input_image || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170" align="center" sortable="custom" prop="created_at">
<template #default="{ row }">
<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" @close="cleanupPreview">
<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"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="loadData"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Right } from '@element-plus/icons-vue'
import { useProcessingStore } from '../../stores/processing'
import { statusType, statusText, fmtTime } from '../../composables/useFileUtils'
import { useFilePreview } from '../../composables/useFilePreview'
import api from '../../api'
const processingStore = useProcessingStore()
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
const items = ref<any[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = 50
const loading = ref(true)
const selected = ref<any[]>([])
const sortBy = ref('created_at')
const sortOrder = ref('desc')
const uploadInput = ref<HTMLInputElement>()
const showDetailDlg = ref(false)
const detailLogs = ref<string[]>([])
async function loadData() {
loading.value = true
try {
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 {
ElMessage.error('加载文件列表失败')
} finally {
loading.value = false
}
}
function onSelect(rows: any[]) { selected.value = rows }
async function previewFile(row: any) {
const fname = row.output_excel || row.result_purchase || row.input_image
const dir = row.output_excel ? 'output' : row.result_purchase ? 'result' : 'input'
await openPreview(dir, fname)
}
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 })
ElMessage.success(`处理任务已创建: ${res.data.task_id}`)
} catch (err: any) {
ElMessage.error(err.response?.data?.detail || '处理失败')
}
}
async function deleteFile(row: any) {
try {
await ElMessageBox.confirm(`确定删除 ${row.output_excel}`, '确认')
await api.delete(`/files/output/${encodeURIComponent(row.output_excel)}`)
if (row.id) await api.delete('/files/relations', { data: { ids: [row.id] } })
ElMessage.success('已删除')
loadData()
} catch (err: any) {
if (err !== 'cancel') ElMessage.error('删除失败')
}
}
async function batchProcess() {
try {
const filenames = selected.value.map(r => r.output_excel).filter(Boolean)
const res = await api.post('/processing/excel', { files: filenames })
ElMessage.success(`批量处理任务已创建: ${res.data.task_id}`)
} catch (err: any) {
ElMessage.error(err.response?.data?.detail || '处理失败')
}
}
async function batchDelete() {
try {
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
const files = selected.value
.filter(r => r.output_excel)
.map(r => ({ directory: 'output', filename: r.output_excel }))
const res = await api.post('/files/batch-delete', { files })
if (res.data.errors?.length) {
ElMessage.warning(`删除完成,${res.data.errors.length} 个文件失败`)
} else {
ElMessage.success('批量删除完成')
}
loadData()
} catch (err: any) {
if (err !== 'cancel') ElMessage.error('批量删除失败')
}
}
async function clearAll() {
try {
await ElMessageBox.confirm('确定清空所有 Excel 处理文件?此操作不可恢复。', '确认')
await api.post('/files/clear/output')
await api.post('/files/relations/sync')
ElMessage.success('已清空')
loadData()
} catch (err: any) {
if (err !== 'cancel') ElMessage.error('清空失败')
}
}
onMounted(loadData)
</script>
<style scoped>
.file-page {
width: 100%;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-left h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.header-actions {
display: flex;
gap: 8px;
}
.file-name.primary {
font-weight: 600;
color: var(--text-primary);
}
.file-name.secondary {
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>