WholeProcessPlatform/frontend/src/views/shuJuTianBao/guoYuSheShiShuJuTianBao.vue

547 lines
17 KiB
Vue
Raw Normal View History

<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>
</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(() => {});
</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>