WholeProcessPlatform/frontend/src/views/shuJuTianBao/guoYuSheShiShuJuTianBao/guoYuSheShiShuJuTianBaoTable.vue
2026-05-07 15:40:18 +08:00

739 lines
21 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>
<a-table
size="small"
:loading="fileLoading"
:data-source="fileTableData"
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record, index }">
<!--
<template v-if="column.key === 'index' || column.dataIndex === 'index'">
{{ index + 1 }}
</template> -->
<!-- 1. 操作列 -->
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<template v-if="editingRowIndex === index">
<a-button type="link" size="small" @click="saveEdit(index)">保存</a-button>
<a-button type="link" size="small" @click="cancelEdit">取消</a-button>
</template>
<template v-else>
<a-button type="link" size="small" @click="startEdit(index)">编辑</a-button>
<a-button type="link" danger size="small" @click="handlePreviewDelete(index)"
>删除</a-button
>
</template>
</div>
</template>
<!-- 2. 警告提示列 (非编辑状态) -->
<template
v-else-if="
!isEditing(index) &&
record.warnings &&
record.warnings.includes(column.dataIndexKey)
"
>
<div style="color: red; display: flex; align-items: center">
<span v-if="record[column.dataIndex]">{{ record[column.dataIndex] }}</span>
<span v-else> 请添加{{ column.title }}</span>
<exclamation-circle-outlined style="margin-left: 4px" />
</div>
</template>
<!-- 3. 编辑状态下的单元格 (绑定到 editingData) -->
<template
v-else-if="
isEditing(index) && column.dataIndex != 'picpth' && column.dataIndex != 'vdpth'
"
>
<!-- 基地 -->
<!-- <template v-if="column.dataIndex === 'baseName'">
<a-select
v-model:value="editingData.baseId"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.baseLoading"
style="width: 100%"
@change="(val) => handleBaseChange(val, index, 'input')"
>
<a-select-option
v-for="opt in baseOptions"
:key="opt.baseid"
:value="opt.baseid"
:label="opt.basename"
>
{{ opt.basename }}
</a-select-option>
</a-select>
</template> -->
<!-- 流域名称 -->
<template v-if="column.dataIndex === 'hbrvnm'">
<a-select
v-model:value="editingData.hbrvcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.hbrvcdLoading"
style="width: 100%"
@change="(val) => handleHbrvcdChange(val, index, 'input')"
>
<a-select-option
v-for="opt in hbrvcdOptions"
:key="opt.hbrvcd"
:value="opt.hbrvcd"
:label="opt.hbrvnm"
>
{{ opt.hbrvnm }}
</a-select-option>
</a-select>
</template>
<!-- 电站名称 -->
<template v-else-if="column.dataIndex === 'ennm'">
<a-select
v-model:value="editingData.rstcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.engLoading"
style="width: 100%"
:disabled="!editingData.hbrvcd"
@change="(val) => handleEngChange(val, index, 'input')"
>
<a-select-option
v-for="opt in rowStates[index]?.engOptions || []"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.ennm"
>
{{ opt.ennm }}
</a-select-option>
</a-select>
</template>
<!-- 过鱼设施 -->
<template v-else-if="column.dataIndex === 'stnm'">
<a-select
v-model:value="editingData.stcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.fpssLoading"
style="width: 100%"
:disabled="!editingData.rstcd"
@change="(val) => handleFpssChange(val, index)"
>
<a-select-option
v-for="opt in rowStates[index]?.fpssOptions || []"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.stnm"
>
{{ opt.stnm }}
</a-select-option>
</a-select>
</template>
<!-- 过鱼时间 -->
<template v-else-if="column.dataIndex === 'strdt'">
<a-date-picker
v-model:value="editingData.strdt"
show-time
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="(val) => delWarning(val, 'strdt')"
/>
</template>
<!-- 鱼种类 -->
<template v-else-if="column.dataIndex === 'ftpName'">
<fishSearch
v-model="editingData.ftp"
style="width: 100%"
@update:modelValue="handleFtpChange"
/>
</template>
<!-- 游向 -->
<template v-else-if="column.dataIndex === 'direction'">
<a-select
v-model:value="editingData.direction"
placeholder="请选择"
style="width: 100%"
@change="(val) => delWarning(val, 'direction')"
>
<a-select-option
v-for="item in direction"
:key="item.itemCode"
:value="item.itemCode"
>
{{ item.dictName }}
</a-select-option>
</a-select>
</template>
<!-- 数字输入框 -->
<template v-else-if="['fcnt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
:step="1"
:precision="0"
:min="0"
@change="(val) => delWarning(val, column.dataIndex)"
/>
</template>
<template v-else-if="['wt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
/>
</template>
<!-- 是否鱼苗 -->
<template v-else-if="column.dataIndex === 'isfs'">
<a-radio-group
v-model:value="editingData.isfs"
@change="(val) => delWarning(val, 'isfs')"
>
<a-radio :value="1">是</a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</template>
<!-- 体长 (编辑态使用 Min/Max) -->
<template v-else-if="column.dataIndex === 'fsz'">
<div class="flex">
<a-input-number
v-model:value="editingData.bodyLengthMin"
style="width: 50%"
:min="0"
/>
<span class="px-[2px]">~</span>
<a-input-number
v-model:value="editingData.bodyLengthMax"
style="width: 50%"
:min="0"
/>
</div>
</template>
<!-- 体重 (编辑态使用 Min/Max) -->
<template v-else-if="column.dataIndex === 'fwet'">
<div class="flex align-center">
<a-input-number
v-model:value="editingData.weightMin"
style="width: 50%"
:min="0"
/>
<span class="px-[2px]">~</span>
<a-input-number
v-model:value="editingData.weightMax"
style="width: 50%"
:min="0"
/>
</div>
</template>
</template>
<template v-else-if="column.dataIndex === 'picpth'">
<div class="preview" v-for="(item, index) in record.picpthList">
<div
class="text"
:class="{ text_warning: record.picpthsWarnings.includes(item.name) }"
@click="emit('preview-click', record, 'image', index)"
>
{{ item.name }}
</div>
</div>
<div v-if="record.picpthList.length == 0">暂无图片</div>
</template>
<template v-else-if="column.dataIndex === 'vdpth'">
<div class="preview" v-for="(item, index) in record.vdpthList">
<div
class="text"
:class="{ text_warning: record.vdpthsWarnings.includes(item.name) }"
@click="emit('preview-click', record, 'video', index)"
>
{{ item.name }}
</div>
</div>
<div v-if="record.vdpthList.length == 0">暂无视频</div>
</template>
</template>
</a-table>
</template>
<script setup lang="ts">
import _ from "lodash";
import { ref, reactive, onMounted, h } from "vue";
import { message, Tag } from "ant-design-vue";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import fishSearch from "@/components/fishSearch/index.vue";
import {
getSelectForDropdown,
getEngInfoDropdown,
getFpssDropdown,
revalidateAndUpdateRow,
deleteRowById,
} from "@/api/select";
const props: any = defineProps({
taskId: { type: String, default: "" },
getFileList: { type: Function, default: () => {} },
fileTableData: { type: Array, default: () => [] },
fileLoading: { type: Boolean, default: false },
direction: { type: Array, default: () => [] },
});
const emit = defineEmits(["preview-click", "update:fileLoading"]);
// --- 状态管理 ---
const editingRowIndex = ref<number | null>(null);
const hbrvcdOptions = ref<any[]>([]);
const rowStates = reactive<Record<number, any>>({});
// 【核心】临时编辑数据,只在编辑模式下使用
const editingData = ref<any>(null);
const modalColumns = ref([
{ dataIndex: "rowIndex", key: "rowIndex", title: "序号", width: 60, dataIndexKey: "rowIndex",align: "center",fixed: "left" },
{
dataIndex: "hbrvnm",
key: "hbrvnm",
dataIndexKey: "hbrvcd",
title: "流域",
width: 140,
},
{
dataIndex: "ennm",
key: "ennm",
dataIndexKey: "rstcd",
title: "电站名称",
width: 140,
},
{
dataIndex: "stnm",
key: "stnm",
dataIndexKey: "stcd",
title: "过鱼设施名称",
width: 150,
},
{
dataIndex: "strdt",
key: "strdt",
dataIndexKey: "strdt",
title: "过鱼时间",
width: 190,
},
{
dataIndex: "ftpName",
key: "ftpName",
dataIndexKey: "ftp",
title: "鱼种类",
width: 120,
},
{
dataIndex: "isfs",
key: "isfs",
title: "是否鱼苗",
dataIndexKey: "isfs",
width: 130,
customRender: ({ text }: any) => {
const isYes = text === 1 || text === "1";
return h(Tag, { color: isYes ? "success" : "error", style: { margin: 0 } }, () =>
isYes ? "是" : "否"
);
},
},
{
dataIndex: "direction",
dataIndexKey: "direction",
key: "direction",
title: "游向",
width: 120,
customRender: ({ text }: any) =>
props.direction.find((item: any) => item.itemCode === text)?.dictName || "-",
},
{
dataIndex: "fcnt",
key: "fcnt",
dataIndexKey: "fcnt",
title: "过鱼数量(尾)",
width: 120,
},
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 160 },
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 160 },
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
{ dataIndex: "picpth", key: "picpth", title: "图片", width: 160 },
{ dataIndex: "vdpth", key: "vdpth", title: "视频", width: 160 },
{
title: "操作",
key: "action",
dataIndex: "action",
fixed: "right",
width: 100,
align: "center",
},
]);
// --- 初始化 ---
onMounted(() => {
// loadBaseOptions();
loadHbrvcdOptions();
});
// const loadBaseOptions = async () => {
// try {
// const res = await getBaseDropdown({});
// let list = res.data || [];
// if (list && list.length > 0) {
// list = list.filter((item: any) => item.baseid !== "all");
// }
// baseOptions.value = list;
// } catch (e) {
// console.error("Load base options failed", e);
// }
// };
const loadHbrvcdOptions = async () => {
try {
const res = await getSelectForDropdown({});
let list = res.data || [];
if (list && list.length > 0) {
list = list.filter((item: any) => item.hbrvcd !== "all");
}
hbrvcdOptions.value = list;
} catch (e) {
console.error("Load hbrvcd options failed", e);
}
};
const ensureRowState = (index: number) => {
if (!rowStates[index]) {
rowStates[index] = {
engOptions: [],
fpssOptions: [],
baseLoading: false,
hbrvcdLoading: false,
engLoading: false,
fpssLoading: false,
};
}
return rowStates[index];
};
// --- 级联逻辑 (操作 editingData) ---
// const handleBaseChange = async (
// hbrvcd: string,
// index: number,
// type: string = "start"
// ) => {
// const state = ensureRowState(index);
// editingData.value.baseName = hbrvcdOptions.value.find(
// (item: any) => item.hbrvcd == hbrvcd
// )?.basename;
// delWarning(hbrvcd, "hbrvcd");
// // 清空后续字段
// if (type != "start") {
// editingData.value.rstcd = undefined;
// editingData.value.stcd = undefined;
// editingData.value.ennm = undefined;
// editingData.value.stnm = undefined;
// state.engOptions = [];
// state.fpssOptions = [];
// }
// state.engLoading = true;
// try {
// const res = await getEngInfoDropdown({ hbrvcd });
// state.engOptions = res.data || [];
// } catch (e) {
// message.error("获取电站列表失败");
// } finally {
// state.engLoading = false;
// }
// };
const handleHbrvcdChange = async (
hbrvcd: string,
index: number,
type: string = "start"
) => {
const state = ensureRowState(index);
editingData.value.hbrvnm = hbrvcdOptions.value.find(
(item: any) => item.hbrvcd == hbrvcd
)?.hbrvnm;
delWarning(hbrvcd, "hbrvcd");
if (hbrvcd == null) {
delWarning(null, "hbrvcd");
delWarning(null, "stcd");
}
// 清空后续字段
if (type != "start") {
editingData.value.rstcd = undefined;
editingData.value.stcd = undefined;
editingData.value.ennm = undefined;
editingData.value.stnm = undefined;
state.engOptions = [];
state.fpssOptions = [];
}
state.engLoading = true;
try {
const res = await getEngInfoDropdown({ hbrvcd });
state.engOptions = res.data || [];
} catch (e) {
message.error("获取电站列表失败");
} finally {
state.engLoading = false;
}
};
const handleEngChange = async (rstcd: string, index: number, type: string = "start") => {
const state = ensureRowState(index);
editingData.value.ennm = state.engOptions.find(
(item: any) => item.stcd === rstcd
)?.ennm;
delWarning(rstcd, "rstcd");
if (rstcd == null) {
delWarning(null, "stcd");
}
// 清空后续字段
if (type != "start") {
editingData.value.stcd = undefined;
editingData.value.stnm = undefined;
state.fpssOptions = [];
}
state.fpssLoading = true;
try {
const res = await getFpssDropdown({ rstcd, hbrvcd: editingData.value.hbrvcd });
state.fpssOptions = res.data || [];
} catch (e) {
message.error("获取设施列表失败");
} finally {
state.fpssLoading = false;
}
};
const handleFpssChange = (stcd: string, index: number) => {
const state = ensureRowState(index);
delWarning(stcd, "stcd");
editingData.value.stnm = state.fpssOptions.find(
(item: any) => item.stcd === stcd
)?.stnm;
};
// 消除警告 / 添加警告
const delWarning = (val: any, key: string) => {
// 确保 warnings 数组存在
if (!editingData.value.warnings) {
editingData.value.warnings = [];
}
const warnings = editingData.value.warnings;
const hasWarning = warnings.includes(key);
if (val !== null && val !== undefined && val !== "") {
// 1. 如果有值,且存在警告,则移除警告
if (hasWarning) {
editingData.value.warnings = warnings.filter((w: string) => w !== key);
}
} else {
// 2. 如果值为空 (null/undefined/''),且不存在警告,则添加警告
if (!hasWarning) {
editingData.value.warnings.push(key);
}
}
};
// --- 编辑控制 ---
const isEditing = (index: number) => editingRowIndex.value === index;
const startEdit = (index: number) => {
const originalRecord = props.fileTableData[index];
// 1. 深拷贝原始数据到临时编辑区
let copiedData = JSON.parse(JSON.stringify(originalRecord));
editingData.value = copiedData;
// 2. 预处理:将 fsz/fwet 字符串拆分为 Min/Max 供输入框使用
processStringToMinMax(editingData.value);
editingRowIndex.value = index;
if (editingData.value.warnings.includes("hbrvcd")) {
editingData.value.hbrvcd = null;
editingData.value.hbrvnm = null;
editingData.value.rstcd = null;
editingData.value.stcd = null;
delWarning(null, "rstcd");
delWarning(null, "stcd");
}
if (editingData.value.warnings.includes("rstcd")) {
editingData.value.stcd = null;
delWarning(null, "stcd");
}
// 3. 预加载下拉选项 (基于 editingData 的值)
if (editingData.value.hbrvcd == "" || editingData.value.hbrvcd == undefined) {
if (editingData.value.rstcd) {
handleHbrvcdChange("", index, "start").then(() => {
handleEngChange(editingData.value.rstcd, index, "start");
});
} else {
handleEngChange("", index, "start");
}
} else if (editingData.value.hbrvcd != "" && editingData.value.hbrvcd != undefined) {
handleHbrvcdChange(editingData.value.hbrvcd, index, "start").then(() => {
handleEngChange(editingData.value.rstcd, index, "start");
});
}
};
// 辅助:字符串转 Min/Max
const processStringToMinMax = (data: any) => {
if (data.fsz) {
const sizes = String(data.fsz).split("~");
data.bodyLengthMin = sizes[0] || "";
data.bodyLengthMax = sizes[1] || sizes[0] || "";
} else {
data.bodyLengthMin = "";
data.bodyLengthMax = "";
}
if (data.fwet) {
const weights = String(data.fwet).split("~");
data.weightMin = weights[0] || "";
data.weightMax = weights[1] || weights[0] || "";
} else {
data.weightMin = "";
data.weightMax = "";
}
};
// 辅助Min/Max 转字符串
const processMinMaxToString = (data: any) => {
// 处理体长
if (data.bodyLengthMin !== "" || data.bodyLengthMax !== "") {
if (data.bodyLengthMin == data.bodyLengthMax) {
data.fsz = data.bodyLengthMin;
} else {
data.fsz = `${data.bodyLengthMin}~${data.bodyLengthMax}`;
}
} else {
data.fsz = "";
}
// 处理体重
if (data.weightMin !== "" || data.weightMax !== "") {
if (data.weightMin == data.weightMax) {
data.fwet = data.weightMin;
} else {
data.fwet = `${data.weightMin}~${data.weightMax}`;
}
} else {
data.fwet = "";
}
// 清理临时字段,避免污染原数据
delete data.bodyLengthMin;
delete data.bodyLengthMax;
delete data.weightMin;
delete data.weightMax;
};
const saveEdit = async (index: number) => {
// 1. 后处理:将 Min/Max 合并回 fsz/fwet
processMinMaxToString(editingData.value);
// 2. 创建新数组,替换对应索引的数据
const newData = [...props.fileTableData];
newData[index] = { ...editingData.value };
emit("update:fileLoading", true);
try {
const res: any = await revalidateAndUpdateRow({
taskId: props.taskId,
data: newData[index],
});
if (res && res?.code == 0) {
message.success("保存成功");
props.getFileList();
editingRowIndex.value = null;
editingData.value = null;
}
} catch (e) {
message.error("保存失败");
} finally {
emit("update:fileLoading", false);
}
};
const cancelEdit = () => {
editingRowIndex.value = null;
editingData.value = null;
// 由于我们从未修改过 props.fileTableData所以不需要恢复操作直接退出即可
};
// 删除按钮点击事件
const handlePreviewDelete = async (index: number) => {
const newData = [...props.fileTableData];
emit("update:fileLoading", true);
try {
const res: any = await deleteRowById({
taskId: props.taskId,
data: newData[index],
});
if (res && res?.code == 0) {
message.success("删除成功");
props.getFileList();
editingRowIndex.value = null;
editingData.value = null;
}
} catch (e) {
message.error("删除失败");
} finally {
emit("update:fileLoading", false);
}
};
// 鱼种类编辑 修改名称
const handleFtpChange = (val: any, opt: any) => {
editingData.value.ftpName = opt.name;
delWarning(val, "ftp");
};
// --- 辅助函数 ---
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
defineExpose({
editingRowIndex,
editingData,
});
</script>
<style lang="scss" scoped>
.preview {
width: 100%;
position: relative;
cursor: pointer;
color: #1890ff;
&:hover {
color: #40a9ff;
}
display: flex;
justify-content: justify-between;
align-items: center;
.text {
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text_warning {
color: red;
}
}
</style>