WholeProcessPlatform/frontend-sjtb/src/views/shuJuTianBao/guoYuSheShiShuJuTianBao/guoYuSheShiShuJuTianBaoTable.vue

791 lines
22 KiB
Vue
Raw Normal View History

<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 === 'rvnm'">
<a-select
v-model:value="editingData.rvcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.hbrvcdLoading"
style="width: 100%"
@change="val => handleHbrvcdChange(val, index, 'input')"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
minWidth: '300px'
}"
>
<a-select-option
v-for="opt in hbrvcdOptions"
:key="opt.rvcd"
:value="opt.rvcd"
:label="opt.rvnm"
>
{{ opt.rvnm }}
</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.rvcd"
@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 === 'strdtStr'">
<a-date-picker
v-model:value="editingData.strdtStr"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:allow-clear="false"
@change="val => delWarning(val, 'strdtStr')"
/>
</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, 'delete')"
>
{{ 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, 'delete')"
>
{{ 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: 'rvnm',
key: 'rvnm',
dataIndexKey: 'rvcd',
title: '流域',
width: 140
},
{
dataIndex: 'ennm',
key: 'ennm',
dataIndexKey: 'rstcd',
title: '电站名称',
width: 140
},
{
dataIndex: 'stnm',
key: 'stnm',
dataIndexKey: 'stcd',
title: '过鱼设施名称',
width: 150
},
{
dataIndex: 'strdtStr',
key: 'strdtStr',
dataIndexKey: 'strdtStr',
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.rvcd !== 'all');
}
hbrvcdOptions.value = list;
} catch (e) {
console.error('Load rvcd 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 (
// rvcd: string,
// index: number,
// type: string = "start"
// ) => {
// const state = ensureRowState(index);
// editingData.value.baseName = hbrvcdOptions.value.find(
// (item: any) => item.rvcd == rvcd
// )?.basename;
// delWarning(rvcd, "rvcd");
// // 清空后续字段
// 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({ rvcd });
// state.engOptions = res.data || [];
// } catch (e) {
// message.error("获取电站列表失败");
// } finally {
// state.engLoading = false;
// }
// };
const handleHbrvcdChange = async (
rvcd: string,
index: number,
type = 'start'
) => {
const state = ensureRowState(index);
editingData.value.rvnm = hbrvcdOptions.value.find(
(item: any) => item.rvcd == rvcd
)?.rvnm;
delWarning(rvcd, 'rvcd');
if (rvcd == null) {
delWarning(null, 'rvcd');
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({ rvcd });
state.engOptions = res.data || [];
} catch (e) {
message.error('获取电站列表失败');
} finally {
state.engLoading = false;
}
};
const handleEngChange = async (
rstcd: string,
index: number,
type = '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, rvcd: editingData.value.rvcd });
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('rvcd')) {
editingData.value.rvcd = null;
editingData.value.rvnm = 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');
}
if (editingData.value.warnings.includes('strdt')) {
editingData.value.strdt = null;
delWarning(null, 'strdt');
}
// 3. 预加载下拉选项 (基于 editingData 的值)
if (editingData.value.rvcd == '' || editingData.value.rvcd == undefined) {
if (editingData.value.rstcd) {
handleHbrvcdChange('', index, 'start').then(() => {
handleEngChange(editingData.value.rstcd, index, 'start');
});
} else {
handleEngChange('', index, 'start');
}
} else if (
editingData.value.rvcd != '' &&
editingData.value.rvcd != undefined
) {
handleHbrvcdChange(editingData.value.rvcd, 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. 验证 strdtStr 不能为空
if (!editingData.value.strdtStr) {
message.error('过鱼时间不能为空');
return;
}
// 3. 创建新数组,替换对应索引的数据
const newData = [...props.fileTableData];
newData[index] = { ...editingData.value };
newData[index].strdt = newData[index].strdtStr
? newData[index].strdtStr + ' 00:00:00'
: newData[index].strdtStr;
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>