2026-04-20 09:07:03 +08:00
|
|
|
|
<template>
|
2026-04-20 16:57:54 +08:00
|
|
|
|
<div class="guoYuSheShiShuJuTianBao-page">
|
|
|
|
|
|
<!-- 搜索区域组件,具体 props 需根据实际子组件调整 -->
|
|
|
|
|
|
<GuoYuSheShiShuJuTianBaoSearch
|
|
|
|
|
|
:import-btn="importBtn"
|
|
|
|
|
|
:save-btn="saveBtn"
|
|
|
|
|
|
:handle-add="handleAdd"
|
|
|
|
|
|
@search-finish="handleSearchFinish"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主表格 -->
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
:row-selection="rowSelection"
|
|
|
|
|
|
:data-source="tableData"
|
|
|
|
|
|
:columns="columns"
|
|
|
|
|
|
:pagination="paginationConfig"
|
|
|
|
|
|
:scroll="{ x: '100%' }"
|
|
|
|
|
|
row-key="key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 自定义插槽渲染可根据需要扩展,这里主要依赖 columns 中的 render 函数逻辑,但在 Vue中通常使用 slot 或 h 函数 -->
|
|
|
|
|
|
<!-- 注意:Antdv 的 columns render 支持返回 VNode 或字符串 -->
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 导入预览 Modal -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
title="导入数据预览"
|
|
|
|
|
|
ok-text="提交导入"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
:width="1500"
|
|
|
|
|
|
:open="visible"
|
|
|
|
|
|
:confirm-loading="fileLoading"
|
|
|
|
|
|
@cancel="handleModalCancel"
|
|
|
|
|
|
@ok="handleModalOk"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:loading="fileLoading"
|
|
|
|
|
|
:data-source="fileTableData"
|
|
|
|
|
|
:columns="modalColumns"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
:scroll="{ y: 500, x: '100%' }"
|
|
|
|
|
|
row-key="index"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 如果需要复杂的行内编辑插槽,可在此定义,但目前逻辑主要在 column render 中处理 -->
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新增/编辑 Modal (对应 React 的 EditModal) -->
|
|
|
|
|
|
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
|
|
|
|
|
|
<EditModal
|
|
|
|
|
|
v-model:visible="editModalVisible"
|
|
|
|
|
|
:initial-values="currentRecord"
|
|
|
|
|
|
:loading="submitLoading"
|
|
|
|
|
|
@cancel="editModalCancel"
|
|
|
|
|
|
@ok="handleEditSubmit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 视频预览 Modal -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
title="视频预览"
|
|
|
|
|
|
:open="videoPreviewVisible"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
@cancel="closeVideoPreview"
|
|
|
|
|
|
>
|
|
|
|
|
|
<video v-if="currentVideoUrl" controls autoplay style="width: 100%" :src="currentVideoUrl">
|
|
|
|
|
|
您的浏览器不支持视频播放
|
|
|
|
|
|
</video>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
2026-04-20 09:07:03 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2026-04-20 16:57:54 +08:00
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
|
|
import { message, Modal } from 'ant-design-vue' // 假设使用 ant-design-vue
|
|
|
|
|
|
import JSZip from 'jszip'
|
|
|
|
|
|
import * as XLSX from 'xlsx'
|
|
|
|
|
|
import GuoYuSheShiShuJuTianBaoSearch from './guoYuSheShiShuJuTianBaoSearch.vue'
|
|
|
|
|
|
import EditModal from './guoYuSheShiShuJuTianBaoForm.vue'
|
|
|
|
|
|
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
|
|
|
|
|
|
|
// --- 类型定义 ---
|
|
|
|
|
|
interface FormData {
|
|
|
|
|
|
[key: string]: any
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnConfig {
|
|
|
|
|
|
dataIndex: string
|
|
|
|
|
|
key: string
|
|
|
|
|
|
title: string
|
|
|
|
|
|
width?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 基础配置 ---
|
|
|
|
|
|
const baseColumnsConfig: ColumnConfig[] = [
|
|
|
|
|
|
{ dataIndex: 'stcd', key: 'stcd', title: '水电基地', width: 100 },
|
|
|
|
|
|
{ dataIndex: 'title', key: 'title', title: '电站名称', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'office', key: 'office', title: '过鱼设施名称', width: 150 },
|
|
|
|
|
|
{ dataIndex: 'regionName', key: 'regionName', title: '过鱼时间', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'location', key: 'location', title: '鱼种类', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'location11', key: 'location', title: '是否鱼苗', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'DIRECTION', key: 'DIRECTION', title: '游向', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'level1', key: 'level1', title: '过鱼数量(尾)', width: 160 },
|
|
|
|
|
|
{ dataIndex: 'level2', key: 'level2', title: '体长', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'level3', key: 'level3', title: '平均体重', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'level4', key: 'level4', title: '水温', width: 120 },
|
|
|
|
|
|
{ dataIndex: 'level5', key: 'level5', title: '图片', width: 100 },
|
|
|
|
|
|
{ dataIndex: 'level6', key: 'level6', title: '视频', width: 100 },
|
|
|
|
|
|
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// --- 状态定义 ---
|
|
|
|
|
|
const searchData = ref<any>(null)
|
|
|
|
|
|
const visible = ref(false) // 导入预览 Modal
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑相关状态
|
|
|
|
|
|
const editModalVisible = ref(false)
|
|
|
|
|
|
const currentRecord = ref<FormData | null>(null)
|
|
|
|
|
|
const submitLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 视频预览相关状态
|
|
|
|
|
|
const videoPreviewVisible = ref(false)
|
|
|
|
|
|
const currentVideoUrl = ref<string>('')
|
|
|
|
|
|
|
|
|
|
|
|
// 表格数据
|
|
|
|
|
|
const tableData = ref<any[]>([])
|
|
|
|
|
|
const fileTableData = ref<any[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const fileLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 行内编辑 Key (用于导入预览表格)
|
|
|
|
|
|
const editingKey = ref<string | number>('')
|
|
|
|
|
|
|
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
|
const page = ref(1)
|
|
|
|
|
|
const size = ref(10)
|
|
|
|
|
|
|
|
|
|
|
|
// --- 辅助函数 ---
|
|
|
|
|
|
|
|
|
|
|
|
// 从 Zip 获取 Blob URL
|
|
|
|
|
|
const getBlobUrlFromZip = async (zip: JSZip, fileName: string): Promise<string> => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const file = zip.file(fileName)
|
|
|
|
|
|
if (!file) return ''
|
|
|
|
|
|
const blob = await file.async('blob')
|
|
|
|
|
|
return URL.createObjectURL(blob)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Extract file failed', e)
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染媒体单元格 (返回 VNode 或简单结构,实际在 Antdv columns render 中处理)
|
|
|
|
|
|
// 在 Vue Antdv 中,render 函数接收 (text, record, index)
|
|
|
|
|
|
const createMediaRender = (type: 'image' | 'video') => {
|
|
|
|
|
|
return (text: string) => {
|
|
|
|
|
|
if (!text) return '-'
|
|
|
|
|
|
// 这里简化处理,实际项目中可能需要使用 h 函数渲染图标和点击事件
|
|
|
|
|
|
// 由于无法直接在这里绑定 click 事件到简单的字符串返回,建议在 columns 定义中使用 slots 或 h 函数
|
|
|
|
|
|
// 为了保持逻辑清晰,这里仅返回文本提示,实际 UI 需结合 Antdv 的 customRender
|
|
|
|
|
|
return type === 'image' ? '查看图片' : '播放视频'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Columns 定义 ---
|
|
|
|
|
|
|
|
|
|
|
|
// 主表格 Columns
|
|
|
|
|
|
const columns = computed(() => {
|
|
|
|
|
|
return [
|
|
|
|
|
|
...baseColumnsConfig.map((col) => {
|
|
|
|
|
|
if (col.dataIndex === 'level5') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
|
|
|
|
|
if(!text) return '-'
|
|
|
|
|
|
// 实际应渲染 Icon 和点击事件,此处简化
|
|
|
|
|
|
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (col.dataIndex === 'level6') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
|
|
|
|
|
if(!text) return '-'
|
|
|
|
|
|
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return { ...col, visible: true }
|
|
|
|
|
|
}),
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
align: 'center',
|
|
|
|
|
|
customRender: ({ record, index }: any) => {
|
|
|
|
|
|
// 在 Vue 模板中,通常通过 slot #bodyCell 来处理复杂操作列
|
|
|
|
|
|
// 这里仅做逻辑占位,实际需在 template 中定义 <template #bodyCell="{ column, record, index }">
|
|
|
|
|
|
return '操作列'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 导入预览表格 Columns (包含行内编辑逻辑)
|
|
|
|
|
|
const modalColumns = computed(() => {
|
|
|
|
|
|
const isEditing = (record: any, index: number) => index === editingKey.value
|
|
|
|
|
|
|
|
|
|
|
|
const save = async (index: number) => {
|
|
|
|
|
|
// 保存逻辑:实际上 fileTableData 是响应式的,input change 时已经更新
|
|
|
|
|
|
editingKey.value = ''
|
|
|
|
|
|
message.success('行数据已更新')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteRow = (index: number) => {
|
|
|
|
|
|
fileTableData.value = fileTableData.value.filter((_, i) => i !== index)
|
|
|
|
|
|
message.success('行数据已删除')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return baseColumnsConfig.map((col) => ({
|
|
|
|
|
|
...col,
|
|
|
|
|
|
customRender: ({ text, record, index }: any) => {
|
|
|
|
|
|
const editing = isEditing(record, index)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是媒体列
|
|
|
|
|
|
if (col.dataIndex === 'level5' || col.dataIndex === 'level6') {
|
|
|
|
|
|
if (editing) {
|
|
|
|
|
|
// 返回 Input 组件的 VNode 或标识,实际需用 slot 或 h 函数
|
|
|
|
|
|
return 'Input编辑中'
|
|
|
|
|
|
}
|
|
|
|
|
|
return col.dataIndex === 'level5' ? '查看图片' : '播放视频'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 普通列
|
|
|
|
|
|
if (editing) {
|
|
|
|
|
|
// 返回 Input 组件标识
|
|
|
|
|
|
return 'Input编辑中'
|
|
|
|
|
|
}
|
|
|
|
|
|
return text
|
|
|
|
|
|
},
|
|
|
|
|
|
// Antdv 支持通过 slots 自定义单元格内容以实现交互
|
|
|
|
|
|
slots: { customRender: `cell-${col.dataIndex}` }
|
|
|
|
|
|
})).concat({
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
dataIndex: 'operation',
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
align: 'center',
|
|
|
|
|
|
customRender: ({ record, index }: any) => {
|
|
|
|
|
|
const editable = isEditing(record, index)
|
|
|
|
|
|
return editable ? '保存/取消' : '修改/删除'
|
|
|
|
|
|
},
|
|
|
|
|
|
slots: { customRender: 'cell-operation' }
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const rowSelection = {
|
|
|
|
|
|
onChange: (selectedRowKeys: string[], selectedRows: any[]) => {
|
|
|
|
|
|
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
|
|
|
|
|
|
},
|
|
|
|
|
|
getCheckboxProps: (record: any) => ({
|
|
|
|
|
|
disabled: record.name === 'Disabled User', // Column configuration not to be checked
|
|
|
|
|
|
name: record.name,
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
// --- 业务逻辑方法 ---
|
|
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
|
currentRecord.value = null
|
|
|
|
|
|
editModalVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = (record: any) => {
|
|
|
|
|
|
currentRecord.value = { ...record }
|
|
|
|
|
|
editModalVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteMain = (index: number) => {
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确定删除这条数据吗?',
|
|
|
|
|
|
onOk: () => {
|
|
|
|
|
|
tableData.value = tableData.value.filter((_, i) => i !== index)
|
|
|
|
|
|
message.success('删除成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const editModalCancel = () => {
|
|
|
|
|
|
editModalVisible.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleEditSubmit = async (values: FormData) => {
|
|
|
|
|
|
submitLoading.value = true
|
|
|
|
|
|
// 模拟异步请求
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (currentRecord.value) {
|
|
|
|
|
|
// 编辑逻辑
|
|
|
|
|
|
const newData = tableData.value.map(item => {
|
|
|
|
|
|
// 简单比较,实际建议用 ID
|
|
|
|
|
|
if (JSON.stringify(item) === JSON.stringify(currentRecord.value)) { // 注意:浅比较可能不够,需根据实际 ID
|
|
|
|
|
|
return { ...item, ...values }
|
|
|
|
|
|
}
|
|
|
|
|
|
return item
|
|
|
|
|
|
})
|
|
|
|
|
|
// 更稳妥的方式是通过 key 查找
|
|
|
|
|
|
const targetIndex = tableData.value.findIndex(item => item.key === currentRecord.value?.key)
|
|
|
|
|
|
if(targetIndex > -1) {
|
|
|
|
|
|
tableData.value[targetIndex] = { ...tableData.value[targetIndex], ...values }
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success('编辑成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 新增逻辑
|
|
|
|
|
|
const newRecord = { ...values, key: Date.now() }
|
|
|
|
|
|
tableData.value = [newRecord, ...tableData.value]
|
|
|
|
|
|
message.success('新增成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
submitLoading.value = false
|
|
|
|
|
|
editModalVisible.value = false
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getData = async (searchDataParam?: any, label?: string) => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
// TODO: 实现实际的数据获取 API 调用
|
|
|
|
|
|
console.log('Fetching data with:', searchDataParam)
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parseExcelFile = async (fileName: string, arrayBuffer: ArrayBuffer) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
|
|
|
|
|
|
const firstSheetName = workbook.SheetNames[0]
|
|
|
|
|
|
if (!firstSheetName) throw new Error('Excel文件中没有工作表')
|
|
|
|
|
|
const worksheet = workbook.Sheets[firstSheetName]
|
|
|
|
|
|
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet)
|
|
|
|
|
|
return jsonData
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`解析文件 ${fileName} 失败:`, error)
|
|
|
|
|
|
message.error(`文件 ${fileName} 解析失败`)
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleModalOk = () => {
|
|
|
|
|
|
tableData.value = [...fileTableData.value]
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
message.success('数据已导入至列表')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleModalCancel = () => {
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
editingKey.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const importBtn = async (file: File) => {
|
|
|
|
|
|
fileLoading.value = true
|
|
|
|
|
|
editingKey.value = ''
|
|
|
|
|
|
const hideMessage = message.loading('正在解析压缩包...', 0)
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const zip = await JSZip.loadAsync(file)
|
|
|
|
|
|
const zipPathMap: Record<string, string> = {}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建路径映射
|
|
|
|
|
|
zip.forEach((relativePath, zipEntry) => {
|
|
|
|
|
|
if (!zipEntry.dir) {
|
|
|
|
|
|
const lowerPath = relativePath.toLowerCase()
|
|
|
|
|
|
zipPathMap[lowerPath] = relativePath
|
|
|
|
|
|
const pathParts = relativePath.split('/')
|
|
|
|
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
|
|
|
|
const subPath = pathParts.slice(i).join('/')
|
|
|
|
|
|
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const fileNames = Object.keys(zip.files)
|
|
|
|
|
|
if (fileNames.length === 0) {
|
|
|
|
|
|
hideMessage()
|
|
|
|
|
|
message.warning('压缩包为空')
|
|
|
|
|
|
fileLoading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let allExcelData: any[] = []
|
|
|
|
|
|
for (const fileName of fileNames) {
|
|
|
|
|
|
const zipEntry = zip.files[fileName]
|
|
|
|
|
|
if (zipEntry.dir) continue
|
|
|
|
|
|
if (!fileName.match(/\.(xls|xlsx)$/i)) continue
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const arrayBuffer = await zipEntry.async('arraybuffer')
|
|
|
|
|
|
const data = await parseExcelFile(fileName, arrayBuffer)
|
|
|
|
|
|
if (!data || data.length === 0) continue
|
|
|
|
|
|
|
|
|
|
|
|
const transformedData = await Promise.all(
|
|
|
|
|
|
data.map(async (item: any) => {
|
|
|
|
|
|
const newObj: any = {}
|
|
|
|
|
|
for (const excelKey in item) {
|
|
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue
|
|
|
|
|
|
const value = item[excelKey]
|
|
|
|
|
|
// 模糊匹配列标题
|
|
|
|
|
|
const matchedCol = baseColumnsConfig.find((col) =>
|
|
|
|
|
|
excelKey.includes(col.title) || col.title.includes(excelKey)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (matchedCol) {
|
|
|
|
|
|
let finalValue = value
|
|
|
|
|
|
// 处理图片和视频路径提取
|
|
|
|
|
|
if ((matchedCol.dataIndex === 'level5' || matchedCol.dataIndex === 'level6') && value && typeof value === 'string') {
|
|
|
|
|
|
const trimPath = value.trim().replace(/\\/g, '/')
|
|
|
|
|
|
if (trimPath) {
|
|
|
|
|
|
const searchKey = trimPath.toLowerCase()
|
|
|
|
|
|
const realPath = zipPathMap[searchKey]
|
|
|
|
|
|
if (realPath) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const zipFile = zip.file(realPath)
|
|
|
|
|
|
if (zipFile) {
|
|
|
|
|
|
const blob = await zipFile.async('blob')
|
|
|
|
|
|
finalValue = URL.createObjectURL(blob)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error(`Failed to extract blob for: ${realPath}`, e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
newObj[matchedCol.dataIndex] = finalValue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return newObj
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
allExcelData = [...allExcelData, ...transformedData]
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(`读取文件 ${fileName} 失败`, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fileTableData.value = allExcelData
|
|
|
|
|
|
visible.value = true
|
|
|
|
|
|
hideMessage()
|
|
|
|
|
|
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
hideMessage()
|
|
|
|
|
|
console.error('ZIP 解析失败:', error)
|
|
|
|
|
|
message.error('文件格式错误或解析失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
fileLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveBtn = async () => {
|
|
|
|
|
|
// TODO: 实现保存逻辑
|
|
|
|
|
|
console.log('Save button clicked')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearchFinish = (e: any, label: string) => {
|
|
|
|
|
|
const newSearchData = { ...searchData.value, ...e }
|
|
|
|
|
|
searchData.value = newSearchData
|
|
|
|
|
|
getData(newSearchData, label)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const closeVideoPreview = () => {
|
|
|
|
|
|
videoPreviewVisible.value = false
|
|
|
|
|
|
currentVideoUrl.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页配置
|
|
|
|
|
|
const paginationConfig = computed(() => ({
|
|
|
|
|
|
size: 'small' as const,
|
|
|
|
|
|
total: total.value,
|
|
|
|
|
|
showTotal: (total: number) => `共 ${total} 条`,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
pageSize: size.value,
|
|
|
|
|
|
current: page.value,
|
|
|
|
|
|
onChange: (p: number, ps: number) => {
|
|
|
|
|
|
page.value = p
|
|
|
|
|
|
size.value = ps
|
|
|
|
|
|
// 重新获取数据
|
|
|
|
|
|
// getData(searchData.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
// --- 生命周期 ---
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// 初始化加载数据
|
|
|
|
|
|
// getData()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-20 09:07:03 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-04-20 16:57:54 +08:00
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.guoYuSheShiShuJuTianBao-page {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|