Files
orc-order-v2/web/frontend/src/views/files/Orders.vue
T
2026-05-12 21:45:28 +08:00

320 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 type="primary" :disabled="!selected.length" @click="batchMerge">
合并选中 ({{ selected.length }})
</el-button>
<el-button :disabled="!selected.length" @click="batchDownload">
批量下载
</el-button>
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
批量删除
</el-button>
</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="采购单文件名" min-width="200">
<template #default="{ row }">
<span class="file-name primary">{{ row.result_purchase || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="" width="40" align="center">
<template #default="{ row }">
<el-icon :color="row.output_exists ? '#52C41A' : '#d1d5db'" :size="16">
<Right />
</el-icon>
</template>
</el-table-column>
<el-table-column label="Excel处理文件" min-width="180">
<template #default="{ row }">
<span class="file-name secondary">{{ row.output_excel || '--' }}</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="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" @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 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"
: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')
// Detail
const showDetailDlg = ref(false)
const detailLogs = ref<string[]>([])
async function loadData() {
loading.value = true
try {
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 {
ElMessage.error('加载文件列表失败')
} finally {
loading.value = false
}
}
function onSelect(rows: any[]) { selected.value = rows }
async function previewFile(row: any) {
const fname = row.result_purchase || row.output_excel || row.input_image
const dir = row.result_purchase ? 'result' : row.output_excel ? 'output' : 'input'
await openPreview(dir, fname)
}
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')
}
async function deleteFile(row: any) {
try {
await ElMessageBox.confirm(`确定删除 ${row.result_purchase}`, '确认')
await api.delete(`/files/result/${encodeURIComponent(row.result_purchase)}`)
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 batchMerge() {
if (!selected.value.length) return
try {
const filenames = selected.value.map(r => r.result_purchase).filter(Boolean)
const res = await api.post('/processing/merge-batch', { filenames })
ElMessage.success(`合并任务已创建: ${res.data.task_id}`)
} catch (err: any) {
ElMessage.error(err.response?.data?.detail || '合并失败')
}
}
async function batchDownload() {
const token = localStorage.getItem('token')
for (const row of selected.value) {
if (row.result_purchase) {
window.open(`/api/files/download/result/${encodeURIComponent(row.result_purchase)}?token=${token}`, '_blank')
}
}
}
async function batchDelete() {
try {
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
const files = selected.value
.filter(r => r.result_purchase)
.map(r => ({ directory: 'result', filename: r.result_purchase }))
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('批量删除失败')
}
}
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>