Files
bj_power/bj_power_mes/frontend/src/pages/Attachment.vue
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

251 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>
<el-card>
<!-- 附件中心集中查阅 + 下载/预览 + 逻辑删除 + 归档清理
物理落盘统一为 <Upload.Dir>////<文件类型>/<uuid>.<ext>库内只存相对路径 -->
<el-alert v-if="disk.level !== 'ok'" :type="disk.level === 'critical' ? 'error' : 'warning'"
:closable="false" show-icon style="margin-bottom:12px"
:title="disk.message || '磁盘空间告警'">
<template #default>
<div style="font-size:12px;line-height:1.7">
附件根目录{{ disk.rootDir }}剩余 {{ freePercent }}%剩余 {{ fmtSize(disk.freeBytes) }} / {{ fmtSize(disk.totalBytes) }}
<el-button v-if="disk.level === 'critical' && can('sys.attachment:manage')" link type="danger"
style="margin-left:8px" @click="doArchive">立即归档清理</el-button>
</div>
</template>
</el-alert>
<el-form inline style="margin-bottom:12px">
<el-form-item label="文件类型">
<el-select v-model="form.fileType" clearable placeholder="全部" style="width:150px">
<el-option v-for="(label, val) in FILE_TYPES" :key="val" :label="label" :value="val" />
</el-select>
</el-form-item>
<el-form-item label="业务类型">
<el-select v-model="form.bizType" clearable placeholder="全部" style="width:150px">
<el-option v-for="(label, val) in BIZ_TYPES" :key="val" :label="label" :value="val" />
</el-select>
</el-form-item>
<el-form-item label="业务ID">
<el-input v-model="form.bizId" placeholder="如 工单号 / 图号 / SN" clearable style="width:190px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="文件名">
<el-input v-model="form.fileName" placeholder="模糊匹配" clearable style="width:160px" @keyup.enter="search" />
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.includeDeleted">含已删除</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
<el-button v-if="can('sys.attachment:manage')" type="warning" plain :loading="archiving"
@click="doArchive">归档清理</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="60" align="center"
:index="(i) => (page.current - 1) * page.size + i + 1" />
<el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column label="文件类型" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" type="info">{{ fileTypeText(row.fileType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="相对路径" min-width="260" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="preview(row)">{{ row.filePath || '-' }}</el-link>
</template>
</el-table-column>
<el-table-column label="大小" width="100" align="right">
<template #default="{ row }">{{ fmtSize(row.fileSize) }}</template>
</el-table-column>
<el-table-column label="业务类型" width="110" align="center">
<template #default="{ row }">{{ bizTypeText(row.bizType) }}</template>
</el-table-column>
<el-table-column prop="bizId" label="业务ID" width="150" show-overflow-tooltip />
<el-table-column prop="uploader" label="上传人" width="100" align="center">
<template #default="{ row }">{{ row.uploader || '-' }}</template>
</el-table-column>
<el-table-column label="上传时间" width="155" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.deleted" size="small" type="danger">已删除</el-tag>
<el-tag v-else-if="row.archived" size="small" type="warning">已归档</el-tag>
<el-tag v-else size="small" type="success">有效</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="preview(row)">预览</el-button>
<el-button link type="success" size="small" @click="download(row)">下载</el-button>
<el-button v-if="!row.deleted && can('sys.attachment:manage')" link type="danger" size="small"
@click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
:current-page="page.current" :page-size="page.size" :page-sizes="[20, 50, 100]"
@current-change="(p) => { page.current = p; load() }"
@size-change="(s) => { page.size = s; page.current = 1; load() }" />
</div>
</el-card>
<PageHelp :data="helpAttachment" page="Attachment" />
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { getToken } from '../utils/auth'
import { can } from '../utils/perm'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpAttachment } from '../help'
// 文件类型(大写枚举)中文名
const FILE_TYPES = {
PROCESS_PDF: '工艺流程',
INSPECTION_PHOTO: '检验照片',
INSPECTION_PDF: '检验报告',
DRAWING: '图纸',
PROCESS_CARD: '流程卡',
PACKAGE_PHOTO: '包装照片',
EXCEL_IMPORT: 'Excel导入',
OTHER: '其他'
}
// 业务类型中文名
const BIZ_TYPES = {
work_order: '工单',
material: '物料',
process_flow: '工艺流程',
inspection: '检验',
quality: '质量',
inbound: '入库',
semi: '半成品',
outbound: '出库',
attachment: '系统'
}
const form = reactive({ fileType: '', bizType: '', bizId: '', fileName: '', includeDeleted: false })
const dateRange = ref([])
const list = ref([])
const total = ref(0)
const page = reactive({ current: 1, size: 20 })
const loading = ref(false)
const archiving = ref(false)
const disk = reactive({ level: 'ok', message: '', rootDir: '', usedPercent: 0, freeBytes: 0, totalBytes: 0 })
const freePercent = computed(() => Math.max(0, 100 - (disk.usedPercent || 0)))
function fileTypeText(v) { return FILE_TYPES[v] || v || '-' }
function bizTypeText(v) { return BIZ_TYPES[v] || v || '-' }
function fmtSize(n) {
n = n || 0
if (n < 1024) return n + ' B'
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB'
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB'
}
async function load() {
loading.value = true
try {
const data = await request.get('/attachment/all', {
params: {
fileType: form.fileType,
bizType: form.bizType,
bizId: form.bizId.trim(),
fileName: form.fileName.trim(),
startDate: dateRange.value?.[0] || '',
endDate: dateRange.value?.[1] || '',
includeDeleted: form.includeDeleted ? 'true' : 'false',
page: page.current,
pageSize: page.size
}
})
list.value = data?.list || []
total.value = data?.total || 0
Object.assign(disk, data?.disk || {})
} finally {
loading.value = false
}
}
function search() { page.current = 1; load() }
function resetFilters() {
form.fileType = ''
form.bizType = ''
form.bizId = ''
form.fileName = ''
form.includeDeleted = false
dateRange.value = []
search()
}
// 预览:走免鉴权直链(UUID 文件名不可枚举),PDF/图片浏览器内联打开
function preview(row) {
if (!row.previewUrl) return ElMessage.warning('无预览地址')
window.open(row.previewUrl, '_blank')
}
// 下载:带鉴权走 id 路由,服务端给出中文文件名
async function download(row) {
const token = getToken()
const resp = await fetch('/api/v1/attachment/file/' + row.id + '?dl=1', { headers: { Authorization: 'Bearer ' + token } })
if (!resp.ok) { ElMessage.error('下载失败'); return }
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = row.fileName || 'file'
a.click()
URL.revokeObjectURL(url)
}
async function remove(row) {
try {
await ElMessageBox.confirm(`确认删除附件「${row.fileName}」吗?删除后可在「含已删除」中追溯。`, '操作确认', {
confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
await request.post('/attachment/delete', { id: row.id })
ElMessage.success('删除成功')
await load()
}
// 归档:先 dryRun 预览数量,确认后执行(早于本地保留年数的附件移动到外部备份目录并标记已归档)
async function doArchive() {
const pre = await request.post('/attachment/archive', { dryRun: true })
if (!pre?.marked) return ElMessage.info('没有需要归档的附件(早于 ' + pre?.cutYear + ' 年的记录)')
const tip = pre.archiveDir
? `将把 ${pre.marked} 个早于 ${pre.cutYear} 年的附件移动到备份目录:\n${pre.archiveDir}`
: `将把 ${pre.marked} 个早于 ${pre.cutYear} 年的附件标记为已归档(未配置备份目录,不移动物理文件)`
try {
await ElMessageBox.confirm(tip + '\n\n确认执行归档吗?', '归档确认', {
confirmButtonText: '确认归档', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
archiving.value = true
try {
const res = await request.post('/attachment/archive', { dryRun: false })
ElMessage.success(`归档完成:移动 ${res?.moved || 0},标记 ${res?.marked || 0},失败 ${res?.failed || 0}`)
await load()
} finally {
archiving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.pager { margin-top: 12px; display: flex; justify-content: flex-end; }
</style>