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"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
:batchData="batchData"
|
|
|
|
|
|
:batchDel="batchDel"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
@search-finish="handleSearchFinish"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主表格 -->
|
2026-04-22 17:53:20 +08:00
|
|
|
|
<BasicTable
|
|
|
|
|
|
ref="tableRef"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
:columns="columns"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
:list-url="getFishDraftPage"
|
|
|
|
|
|
:search-params="{}"
|
|
|
|
|
|
:enable-row-selection="true"
|
|
|
|
|
|
@selection-change="handleSelectionChange"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
|
|
|
|
|
|
<div class="flex">
|
|
|
|
|
|
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
|
|
|
|
|
<a-button type="link" danger size="small" @click="handleDelete([record.id])"
|
|
|
|
|
|
>删除</a-button
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</BasicTable>
|
|
|
|
|
|
<!-- <BasicTable :columns="columns" :listUrl="getFishDraftPage" /> -->
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 导入预览 Modal -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
title="导入数据预览"
|
|
|
|
|
|
ok-text="提交导入"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
:width="1500"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
v-model:open="visible"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
:confirm-loading="fileLoading"
|
|
|
|
|
|
@cancel="handleModalCancel"
|
|
|
|
|
|
@ok="handleModalOk"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:loading="fileLoading"
|
|
|
|
|
|
:data-source="fileTableData"
|
|
|
|
|
|
:columns="modalColumns"
|
|
|
|
|
|
:scroll="{ y: 500, x: '100%' }"
|
|
|
|
|
|
row-key="index"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 如果需要复杂的行内编辑插槽,可在此定义,但目前逻辑主要在 column render 中处理 -->
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新增/编辑 Modal (对应 React 的 EditModal) -->
|
|
|
|
|
|
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
|
|
|
|
|
|
<EditModal
|
2026-04-22 17:53:20 +08:00
|
|
|
|
v-model:visible="editModalVisible"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
:initial-values="currentRecord"
|
|
|
|
|
|
:loading="submitLoading"
|
|
|
|
|
|
@cancel="editModalCancel"
|
|
|
|
|
|
@ok="handleEditSubmit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 视频预览 Modal -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
title="视频预览"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
v-model:open="videoPreviewVisible"
|
2026-04-20 16:57:54 +08:00
|
|
|
|
:footer="null"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
@cancel="closeVideoPreview"
|
|
|
|
|
|
>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
<video
|
|
|
|
|
|
v-if="currentVideoUrl"
|
|
|
|
|
|
controls
|
|
|
|
|
|
autoplay
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:src="currentVideoUrl"
|
|
|
|
|
|
>
|
2026-04-20 16:57:54 +08:00
|
|
|
|
您的浏览器不支持视频播放
|
|
|
|
|
|
</video>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
2026-04-20 09:07:03 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
import { ref, computed, onMounted, h } from "vue";
|
|
|
|
|
|
import { message, Modal } from "ant-design-vue"; // 假设使用 ant-design-vue
|
|
|
|
|
|
import JSZip from "jszip";
|
|
|
|
|
|
import * as XLSX from "xlsx";
|
|
|
|
|
|
import BasicTable from "@/components/BasicTable/index.vue";
|
|
|
|
|
|
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
|
|
|
|
|
|
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
|
|
|
|
|
|
import {
|
|
|
|
|
|
getFishDraftPage,
|
|
|
|
|
|
addFishDraft,
|
|
|
|
|
|
editFishDraft,
|
|
|
|
|
|
delFishDraft,
|
|
|
|
|
|
} from "@/api/guoYuSheShiShuJuTianBao";
|
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
import { Tag } from 'ant-design-vue'; // 确保导入 Tag
|
2026-04-20 16:57:54 +08:00
|
|
|
|
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
|
|
|
|
|
|
|
// --- 类型定义 ---
|
|
|
|
|
|
interface FormData {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
[key: string]: any;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnConfig {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
dataIndex: string;
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
width?: number;
|
|
|
|
|
|
customRender?: (text: any, record: any) => any;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const tableRef = ref<any>(null);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
// --- 基础配置 ---
|
|
|
|
|
|
const baseColumnsConfig: ColumnConfig[] = [
|
2026-04-22 17:53:20 +08:00
|
|
|
|
{ dataIndex: "engName", key: "engName", title: "水电基地", width: 100 },
|
|
|
|
|
|
{ dataIndex: "baseName", key: "baseName", title: "电站名称", width: 120 },
|
|
|
|
|
|
{ dataIndex: "fpname", key: "fpname", title: "过鱼设施名称", width: 150 },
|
|
|
|
|
|
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
|
|
|
|
|
|
{ dataIndex: "ftp", key: "ftp", title: "鱼种类", width: 120 },
|
|
|
|
|
|
{
|
|
|
|
|
|
dataIndex: "isfs",
|
|
|
|
|
|
key: "isfs",
|
|
|
|
|
|
title: "是否鱼苗",
|
|
|
|
|
|
width: 74,
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
|
|
|
|
|
const isYes = text === 1 || text === '1';
|
|
|
|
|
|
return h(
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
{
|
|
|
|
|
|
color: isYes ? 'success' : 'error', // Antdv Tag 的颜色预设
|
|
|
|
|
|
style: { margin: 0 } // 去除默认 margin,使其在表格中对齐更好
|
|
|
|
|
|
},
|
|
|
|
|
|
() => isYes ? '是' : '否'
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{ dataIndex: "direction", key: "direction", title: "游向", width: 80 },
|
|
|
|
|
|
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
|
|
|
|
|
|
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
|
|
|
|
|
|
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
|
|
|
|
|
|
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
|
|
|
|
|
|
{ dataIndex: "picpth", key: "level5", title: "图片", width: 100 },
|
|
|
|
|
|
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 },
|
|
|
|
|
|
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
|
|
|
|
|
|
{ dataIndex: "status", key: "status", title: "状态", width: 100 },
|
|
|
|
|
|
];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 状态定义 ---
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const visible = ref(false); // 导入预览 Modal
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 编辑相关状态
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const editModalVisible = ref(false);
|
|
|
|
|
|
const currentRecord = ref<FormData | null>(null);
|
|
|
|
|
|
const submitLoading = ref(false);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 视频预览相关状态
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const videoPreviewVisible = ref(false);
|
|
|
|
|
|
const currentVideoUrl = ref<string>("");
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 表格数据
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const tableData = ref<any[]>([]);
|
|
|
|
|
|
const fileTableData = ref<any[]>([]);
|
|
|
|
|
|
const batchData = ref<any[]>([]);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const fileLoading = ref(false);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 行内编辑 Key (用于导入预览表格)
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const editingKey = ref<string | number>("");
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- 辅助函数 ---
|
|
|
|
|
|
|
|
|
|
|
|
// 从 Zip 获取 Blob URL
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// 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 "";
|
|
|
|
|
|
// }
|
|
|
|
|
|
// };
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 渲染媒体单元格 (返回 VNode 或简单结构,实际在 Antdv columns render 中处理)
|
|
|
|
|
|
// 在 Vue Antdv 中,render 函数接收 (text, record, index)
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// const createMediaRender = (type: "image" | "video") => {
|
|
|
|
|
|
// return (text: string) => {
|
|
|
|
|
|
// if (!text) return "-";
|
|
|
|
|
|
// // 这里简化处理,实际项目中可能需要使用 h 函数渲染图标和点击事件
|
|
|
|
|
|
// // 由于无法直接在这里绑定 click 事件到简单的字符串返回,建议在 columns 定义中使用 slots 或 h 函数
|
|
|
|
|
|
// // 为了保持逻辑清晰,这里仅返回文本提示,实际 UI 需结合 Antdv 的 customRender
|
|
|
|
|
|
// return type === "image" ? "查看图片" : "播放视频";
|
|
|
|
|
|
// };
|
|
|
|
|
|
// };
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Columns 定义 ---
|
|
|
|
|
|
|
|
|
|
|
|
// 主表格 Columns
|
|
|
|
|
|
const columns = computed(() => {
|
|
|
|
|
|
return [
|
|
|
|
|
|
...baseColumnsConfig.map((col) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (col.dataIndex === "level5") {
|
2026-04-20 16:57:54 +08:00
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (!text) return "-";
|
|
|
|
|
|
// 实际应渲染 Icon 和点击事件,此处简化
|
|
|
|
|
|
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`;
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (col.dataIndex === "level6") {
|
2026-04-20 16:57:54 +08:00
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (!text) return "-";
|
|
|
|
|
|
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`;
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
return { ...col, visible: true };
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}),
|
|
|
|
|
|
{
|
2026-04-22 17:53:20 +08:00
|
|
|
|
title: "操作",
|
|
|
|
|
|
key: "action",
|
|
|
|
|
|
dataIndex: "action",
|
|
|
|
|
|
fixed: "right",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 导入预览表格 Columns (包含行内编辑逻辑)
|
|
|
|
|
|
const modalColumns = computed(() => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
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" ? "查看图片" : "播放视频";
|
|
|
|
|
|
}
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// 普通列
|
2026-04-20 16:57:54 +08:00
|
|
|
|
if (editing) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// 返回 Input 组件标识
|
|
|
|
|
|
return "Input编辑中";
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
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" },
|
|
|
|
|
|
// });
|
|
|
|
|
|
});
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 业务逻辑方法 ---
|
|
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
currentRecord.value = null;
|
|
|
|
|
|
editModalVisible.value = true;
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const handleEdit = (record: any) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
currentRecord.value = { ...record };
|
|
|
|
|
|
editModalVisible.value = true;
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// 删除过鱼数据
|
|
|
|
|
|
const handleDelete = (ids: any[]) => {
|
|
|
|
|
|
console.log(ids)
|
2026-04-20 16:57:54 +08:00
|
|
|
|
Modal.confirm({
|
2026-04-22 17:53:20 +08:00
|
|
|
|
title: "是否确认删除选中数据吗?",
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
let res: any = await delFishDraft(ids);
|
|
|
|
|
|
if (res && res?.code == 0) {
|
|
|
|
|
|
message.success("删除成功");
|
|
|
|
|
|
tableRef.value?.getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
// 批量删除
|
|
|
|
|
|
const batchDel = () => {
|
|
|
|
|
|
handleDelete(batchData.value);
|
|
|
|
|
|
};
|
|
|
|
|
|
// 多选
|
|
|
|
|
|
const handleSelectionChange = (keys: any) => {
|
|
|
|
|
|
batchData.value = keys;
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
const editModalCancel = () => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
editModalVisible.value = false;
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const handleEditSubmit = async (values: FormData) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
submitLoading.value = true;
|
|
|
|
|
|
console.log(values);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
// 模拟异步请求
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// setTimeout(() => {
|
|
|
|
|
|
if (currentRecord.value) {
|
|
|
|
|
|
// 编辑逻辑
|
|
|
|
|
|
|
|
|
|
|
|
let res: any = await editFishDraft({
|
|
|
|
|
|
...values
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res && res?.code == 0) {
|
|
|
|
|
|
message.success("编辑成功");
|
|
|
|
|
|
editModalVisible.value = false;
|
|
|
|
|
|
tableRef.value?.getList();
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
submitLoading.value = false;
|
|
|
|
|
|
// 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 };
|
|
|
|
|
|
// }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 新增逻辑
|
|
|
|
|
|
let res: any = await addFishDraft({
|
|
|
|
|
|
...values,
|
|
|
|
|
|
tm: dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
|
|
|
|
|
stcd: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res && res?.code == 0) {
|
|
|
|
|
|
message.success("新增成功");
|
|
|
|
|
|
editModalVisible.value = false;
|
|
|
|
|
|
tableRef.value?.getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
submitLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// }, 500);
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const parseExcelFile = async (fileName: string, arrayBuffer: ArrayBuffer) => {
|
|
|
|
|
|
try {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const workbook = XLSX.read(arrayBuffer, {
|
|
|
|
|
|
type: "array",
|
|
|
|
|
|
cellDates: true, // 【关键】将日期单元格解析为 JS Date 对象
|
|
|
|
|
|
dateNF: "yyyy-mm-dd", // 【关键】指定日期输出格式
|
|
|
|
|
|
});
|
|
|
|
|
|
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;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
} catch (error) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
console.error(`解析文件 ${fileName} 失败:`, error);
|
|
|
|
|
|
message.error(`文件 ${fileName} 解析失败`);
|
|
|
|
|
|
return [];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const handleModalOk = () => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
tableData.value = [...fileTableData.value];
|
|
|
|
|
|
visible.value = false;
|
|
|
|
|
|
message.success("数据已导入至列表");
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const handleModalCancel = () => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
visible.value = false;
|
|
|
|
|
|
editingKey.value = "";
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const importBtn = async (file: File) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
fileLoading.value = true;
|
|
|
|
|
|
editingKey.value = "";
|
|
|
|
|
|
const hideMessage = message.loading("正在解析压缩包...", 0);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const zip = await JSZip.loadAsync(file);
|
|
|
|
|
|
const zipPathMap: Record<string, string> = {};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 构建路径映射
|
|
|
|
|
|
zip.forEach((relativePath, zipEntry) => {
|
|
|
|
|
|
if (!zipEntry.dir) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const lowerPath = relativePath.toLowerCase();
|
|
|
|
|
|
zipPathMap[lowerPath] = relativePath;
|
|
|
|
|
|
const pathParts = relativePath.split("/");
|
2026-04-20 16:57:54 +08:00
|
|
|
|
for (let i = 0; i < pathParts.length; i++) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const subPath = pathParts.slice(i).join("/");
|
|
|
|
|
|
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
});
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const fileNames = Object.keys(zip.files);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
if (fileNames.length === 0) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
hideMessage();
|
|
|
|
|
|
message.warning("压缩包为空");
|
|
|
|
|
|
fileLoading.value = false;
|
|
|
|
|
|
return;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
let allExcelData: any[] = [];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
for (const fileName of fileNames) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const zipEntry = zip.files[fileName];
|
|
|
|
|
|
if (zipEntry.dir) continue;
|
|
|
|
|
|
if (!fileName.match(/\.(xls|xlsx)$/i)) continue;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const arrayBuffer = await zipEntry.async("arraybuffer");
|
|
|
|
|
|
const data = await parseExcelFile(fileName, arrayBuffer);
|
|
|
|
|
|
if (!data || data.length === 0) continue;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const transformedData = await Promise.all(
|
|
|
|
|
|
data.map(async (item: any) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const newObj: any = {};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
for (const excelKey in item) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue;
|
|
|
|
|
|
const value = item[excelKey];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
// 模糊匹配列标题
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const matchedCol = baseColumnsConfig.find(
|
|
|
|
|
|
(col) => excelKey.includes(col.title) || col.title.includes(excelKey)
|
|
|
|
|
|
);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
if (matchedCol) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
let finalValue = value;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
// 处理图片和视频路径提取
|
2026-04-22 17:53:20 +08:00
|
|
|
|
if (
|
|
|
|
|
|
(matchedCol.dataIndex === "level5" ||
|
|
|
|
|
|
matchedCol.dataIndex === "level6") &&
|
|
|
|
|
|
value &&
|
|
|
|
|
|
typeof value === "string"
|
|
|
|
|
|
) {
|
|
|
|
|
|
const trimPath = value.trim().replace(/\\/g, "/");
|
2026-04-20 16:57:54 +08:00
|
|
|
|
if (trimPath) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const searchKey = trimPath.toLowerCase();
|
|
|
|
|
|
const realPath = zipPathMap[searchKey];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
if (realPath) {
|
|
|
|
|
|
try {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const zipFile = zip.file(realPath);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
if (zipFile) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const blob = await zipFile.async("blob");
|
|
|
|
|
|
finalValue = URL.createObjectURL(blob);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
console.error(`Failed to extract blob for: ${realPath}`, e);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
newObj[matchedCol.dataIndex] = finalValue;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
return newObj;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
})
|
2026-04-22 17:53:20 +08:00
|
|
|
|
);
|
|
|
|
|
|
allExcelData = [...allExcelData, ...transformedData];
|
2026-04-20 16:57:54 +08:00
|
|
|
|
} catch (err) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
console.error(`读取文件 ${fileName} 失败`, err);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
fileTableData.value = allExcelData;
|
|
|
|
|
|
visible.value = true;
|
|
|
|
|
|
hideMessage();
|
|
|
|
|
|
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`);
|
2026-04-20 16:57:54 +08:00
|
|
|
|
} catch (error) {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
hideMessage();
|
|
|
|
|
|
console.error("ZIP 解析失败:", error);
|
|
|
|
|
|
message.error("文件格式错误或解析失败");
|
2026-04-20 16:57:54 +08:00
|
|
|
|
} finally {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
fileLoading.value = false;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const saveBtn = async () => {
|
|
|
|
|
|
// TODO: 实现保存逻辑
|
2026-04-22 17:53:20 +08:00
|
|
|
|
console.log("Save button clicked");
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const handleSearchFinish = (values: any) => {
|
|
|
|
|
|
console.log(values);
|
|
|
|
|
|
// const newSearchData = { ...searchData.value, ...e };
|
|
|
|
|
|
// searchData.value = newSearchData;
|
|
|
|
|
|
// getData(newSearchData, label);
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
const closeVideoPreview = () => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
videoPreviewVisible.value = false;
|
|
|
|
|
|
currentVideoUrl.value = "";
|
|
|
|
|
|
};
|
2026-04-20 16:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 生命周期 ---
|
2026-04-22 17:53:20 +08:00
|
|
|
|
onMounted(() => {});
|
2026-04-20 09:07:03 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-04-20 16:57:54 +08:00
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.guoYuSheShiShuJuTianBao-page {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
padding: 20px;
|
2026-04-20 16:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|