过鱼设施数据填报导入修改

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
扈兆增 2026-04-27 09:28:16 +08:00
parent 9d0fbd2cb0
commit 875f674f60
10 changed files with 794 additions and 212 deletions

View File

@ -81,13 +81,6 @@ export function cancelImportTask(data:any) {
data data
}); });
} }
// 查询导入任务
export function getImportTask() {
return request({
url: '/data/importTask/list',
method: 'get'
});
}
//检测用户导入状态 //检测用户导入状态
export function checkImportStatus() { export function checkImportStatus() {
return request({ return request({
@ -95,3 +88,20 @@ export function checkImportStatus() {
method: 'get' method: 'get'
}); });
} }
// 查询用户导入导入结果
export function getLastImportResult() {
return request({
url: '/data/fishDraft/getLastImportResult',
method: 'get'
});
}
// 批量保存草稿
export function batchSaveDraft(data:any) {
return request({
url: '/data/fishDraft/batchSaveDraft',
method: 'post',
data
});
}

View File

@ -78,12 +78,15 @@
:value="formData.baseId" :value="formData.baseId"
placeholder="请选择" placeholder="请选择"
@change="dataDimensionDataChange" @change="dataDimensionDataChange"
show-search
:filter-option="filterOption"
style="width: 135px" style="width: 135px"
> >
<a-select-option <a-select-option
v-for="opt in shuJuTianBaoStore.baseOption" v-for="opt in shuJuTianBaoStore.baseOption"
:key="opt.baseid" :key="opt.baseid"
:value="opt.baseid" :value="opt.baseid"
:label="opt.basename"
> >
{{ opt.basename }} {{ opt.basename }}
</a-select-option> </a-select-option>
@ -92,12 +95,15 @@
:value="formData.rstcd" :value="formData.rstcd"
placeholder="请选择电站" placeholder="请选择电站"
@change="stcdIdChange" @change="stcdIdChange"
show-search
:filter-option="filterOption"
style="width: 135px" style="width: 135px"
> >
<a-select-option <a-select-option
v-for="opt in shuJuTianBaoStore.engOption" v-for="opt in shuJuTianBaoStore.engOption"
:key="opt.stcd" :key="opt.stcd"
:value="opt.stcd" :value="opt.stcd"
:label="opt.ennm"
> >
{{ opt.ennm }} {{ opt.ennm }}
</a-select-option> </a-select-option>
@ -113,11 +119,14 @@
:allow-clear="item.fieldProps?.allowClear" :allow-clear="item.fieldProps?.allowClear"
:style="{ width: item.width ? item.width + 'px' : '200px' }" :style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(val) => triggerManualValuesChange(item.name, val)" @change="(val) => triggerManualValuesChange(item.name, val)"
show-search
:filter-option="filterOption"
> >
<a-select-option <a-select-option
v-for="opt in item.options" v-for="opt in item.options"
:key="opt[item.values?.value] || opt.value || opt.itemCode" :key="opt[item.values?.value] || opt.value || opt.itemCode"
:value="opt[item.values?.value] || opt.value || opt.itemCode" :value="opt[item.values?.value] || opt.value || opt.itemCode"
:label="opt[item.values?.name] || opt.label || opt.dictName"
> >
{{ opt[item.values?.name] || opt.label || opt.dictName }} {{ opt[item.values?.name] || opt.label || opt.dictName }}
</a-select-option> </a-select-option>
@ -207,6 +216,10 @@ const formRef = ref<any>();
const formData = reactive<any>({}); const formData = reactive<any>({});
const rules = reactive<Record<string, any>>({}); const rules = reactive<Record<string, any>>({});
const filterOption = (inputValue: string, option: any) => {
if (!option.label) return false;
return option.label.indexOf(inputValue) !== -1;
};
// 2. false/null/undefined // 2. false/null/undefined
const validSearchList = computed(() => { const validSearchList = computed(() => {
return props.searchList.filter((item) => item); return props.searchList.filter((item) => item);

View File

@ -70,6 +70,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed, watch } from "vue"; import { ref, onMounted, computed, watch } from "vue";
import { getFishDictoryDropdown } from "@/api/select"; import { getFishDictoryDropdown } from "@/api/select";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
import { init } from "echarts";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- Props & Emits --- // --- Props & Emits ---
interface Props { interface Props {
@ -83,7 +86,7 @@ const props = withDefaults(defineProps<Props>(), {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: string | string[]): void; (e: "update:modelValue", value: string | string[], opt: any): void;
}>(); }>();
// --- State --- // --- State ---
@ -149,15 +152,15 @@ const handleSelectOption = (opt: any) => {
} else { } else {
newValues.push(opt.id); // newValues.push(opt.id); //
} }
emit("update:modelValue", newValues); emit("update:modelValue", newValues, opt);
} else { } else {
// --- --- // --- ---
// ID // ID
// //
if (props.modelValue === opt.id) { if (props.modelValue === opt.id) {
emit("update:modelValue", ""); // emit("update:modelValue", "", opt); //
} else { } else {
emit("update:modelValue", opt.id); // emit("update:modelValue", opt.id, opt); //
// //
open.value = false; open.value = false;
} }
@ -167,7 +170,7 @@ const handleSelectOption = (opt: any) => {
const handleChange = (val: any) => { const handleChange = (val: any) => {
// a-select change Tag // a-select change Tag
// //
emit("update:modelValue", val); emit("update:modelValue", val, opt);
}; };
const getFishNameById = (id: string) => { const getFishNameById = (id: string) => {
@ -182,25 +185,34 @@ watch(
(newVal) => { (newVal) => {
// modelValue // modelValue
if (!newVal && Array.isArray(props.modelValue)) { if (!newVal && Array.isArray(props.modelValue)) {
emit("update:modelValue", props.modelValue || null); emit("update:modelValue", props.modelValue || null, options.value);
} }
// modelValue // modelValue
if (newVal && typeof props.modelValue === "string") { if (newVal && typeof props.modelValue === "string") {
emit("update:modelValue", props.modelValue ? [props.modelValue] : []); emit("update:modelValue", props.modelValue ? [props.modelValue] : [], {});
} }
} }
); );
const init = () => {
onMounted(() => { let data = shuJuTianBaoStore.getFishOption();
if (data.length === 0) {
loading.value = true; loading.value = true;
getFishDictoryDropdown() getFishDictoryDropdown()
.then((res) => { .then((res) => {
options.value = res.data || []; options.value = res.data || [];
loading.value = false; loading.value = false;
shuJuTianBaoStore.setFishOption(options.value);
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
} else {
options.value = data;
}
};
onMounted(() => {
init();
}); });
</script> </script>

View File

@ -1,12 +1,14 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; // 使用 ref 更简单直观 import { ref } from 'vue'; // 使用 ref 更简单直观
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from '@/api/select'; import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from '@/api/select';
import { set } from 'lodash';
export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => { export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
// 1. 直接使用 ref 定义状态,确保响应式 // 1. 直接使用 ref 定义状态,确保响应式
const fpssOption = ref<any[]>([]); const fpssOption = ref<any[]>([]);
const baseOption = ref<any[]>([]); const baseOption = ref<any[]>([]);
const engOption = ref<any[]>([]); const engOption = ref<any[]>([]);
const fishOption = ref([]);
// 2. 业务逻辑方法 // 2. 业务逻辑方法
const getBaseOption = async () => { const getBaseOption = async () => {
@ -48,14 +50,23 @@ export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
console.log(error); console.log(error);
} }
}; };
const getFishOption = () => {
return fishOption.value;
};
const setFishOption = (data: any[]) => {
fishOption.value = data;
};
// 3. 直接返回 ref 和方法 // 3. 直接返回 ref 和方法
// 在组件中使用时store.baseOption 会自动解包为数组 // 在组件中使用时store.baseOption 会自动解包为数组
return { return {
fpssOption, fpssOption,
baseOption, baseOption,
engOption, engOption,
fishOption,
getBaseOption, getBaseOption,
getEngOption, getEngOption,
getFpssOption, getFpssOption,
getFishOption,
setFishOption
}; };
}); });

View File

@ -18,18 +18,21 @@
> >
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="水电基地" name="baseId"> <a-form-item label="流域" name="baseId">
<a-select <a-select
v-model:value="formData.baseId" v-model:value="formData.baseId"
:loading="baseLoading" :loading="baseLoading"
placeholder="请选择水电基地" placeholder="请选择流域"
:disabled="isView" :disabled="isView"
show-search
:filter-option="filterOption"
@change="baseChange" @change="baseChange"
> >
<a-select-option <a-select-option
v-for="opt in baseOption" v-for="opt in baseOption"
:key="opt.baseid" :key="opt.baseid"
:value="opt.baseid" :value="opt.baseid"
:label="opt.basename"
> >
{{ opt.basename }} {{ opt.basename }}
</a-select-option> </a-select-option>
@ -43,9 +46,16 @@
:loading="engLoading" :loading="engLoading"
placeholder="请选择电站名称" placeholder="请选择电站名称"
:disabled="isView" :disabled="isView"
show-search
:filter-option="filterOption"
@change="engChange" @change="engChange"
> >
<a-select-option v-for="opt in engOption" :key="opt.stcd" :value="opt.stcd"> <a-select-option
v-for="opt in engOption"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.ennm"
>
{{ opt.ennm }} {{ opt.ennm }}
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -61,11 +71,14 @@
:loading="fpssLoading" :loading="fpssLoading"
placeholder="请选择过鱼设施" placeholder="请选择过鱼设施"
:disabled="isView" :disabled="isView"
show-search
:filter-option="filterOption"
> >
<a-select-option <a-select-option
v-for="opt in fpssOption" v-for="opt in fpssOption"
:key="opt.stcd" :key="opt.stcd"
:value="opt.stcd" :value="opt.stcd"
:label="opt.stnm"
> >
{{ opt.stnm }} {{ opt.stnm }}
</a-select-option> </a-select-option>
@ -76,8 +89,9 @@
<a-form-item label="过鱼时间" name="strdt"> <a-form-item label="过鱼时间" name="strdt">
<a-date-picker <a-date-picker
v-model:value="formData.strdt" v-model:value="formData.strdt"
show-time
style="width: 100%" style="width: 100%"
format="YYYY-MM-DD" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期" placeholder="选择日期"
:disabled="isView" :disabled="isView"
@ -246,15 +260,13 @@ const props = withDefaults(defineProps<Props>(), {
initialValues: null, initialValues: null,
loading: false, loading: false,
}); });
// 1.
const getBaseDropdownSelect = async () => { const getBaseDropdownSelect = async () => {
try { try {
baseLoading.value = true; baseLoading.value = true;
const res = await getBaseDropdown({}); const res = await getBaseDropdown({});
baseOption.value = res.data; baseOption.value = res.data;
} catch (error) { } catch (error) {
console.error("获取水电基地列表失败:", error); console.error("获取流域列表失败:", error);
} finally { } finally {
baseLoading.value = false; baseLoading.value = false;
} }
@ -291,7 +303,6 @@ const getFpssDropdownSelect = async (rstcd: string, baseId: string) => {
fpssLoading.value = false; fpssLoading.value = false;
} }
}; };
// --- v-model ---
const modalVisible = computed({ const modalVisible = computed({
get: () => props.visible, get: () => props.visible,
set: (val) => emit("update:visible", val), set: (val) => emit("update:visible", val),
@ -331,16 +342,19 @@ const defaultFormData = reactive({
weightMax: undefined, weightMax: undefined,
}); });
const formData: any = reactive({ ...defaultFormData }); const formData: any = reactive({ ...defaultFormData });
const filterOption = (inputValue: string, option: any) => {
if (!option.label) return false;
return option.label.indexOf(inputValue) !== -1;
};
// //
const rules: Record<string, Rule[]> = { const rules: Record<string, Rule[]> = {
// baseId: [{ required: true, message: "", trigger: "change" }], baseId: [{ required: true, message: "请选择流域", trigger: "change" }],
// rstcd: [{ required: true, message: "", trigger: "change" }], rstcd: [{ required: true, message: "请选择电站", trigger: "change" }],
// stcd: [{ required: true, message: "", trigger: "change" }], stcd: [{ required: true, message: "请选择过鱼设施", trigger: "change" }],
// strdt: [{ required: true, message: "", trigger: "change" }], strdt: [{ required: true, message: "请选择过鱼时间", trigger: "change" }],
// ftp: [{ required: true, message: "", trigger: "change" }], ftp: [{ required: true, message: "请选择鱼种类", trigger: "change" }],
// direction: [{ required: true, message: "", trigger: "change" }], direction: [{ required: true, message: "请选择游向", trigger: "change" }],
// fcnt: [{ required: true, message: "", trigger: "change" }], fcnt: [{ required: true, message: "请输入过鱼数量", trigger: "change" }],
}; };
// //
@ -409,8 +423,6 @@ const initForm = () => {
if (props.initialValues) { if (props.initialValues) {
// --- --- // --- ---
const values = props.initialValues; const values = props.initialValues;
//
if (values.fwet) { if (values.fwet) {
const weights = values.fwet.split("~"); const weights = values.fwet.split("~");
if (weights.length === 2) { if (weights.length === 2) {
@ -456,12 +468,10 @@ const initForm = () => {
} }
}); });
} else { } else {
// --- ---
resetForm(); resetForm();
} }
}; };
// 2. watch visible
watch( watch(
() => props.visible, () => props.visible,
(newVisible) => { (newVisible) => {
@ -471,20 +481,11 @@ watch(
getEngInfoDropdownSelect(formData.baseId); getEngInfoDropdownSelect(formData.baseId);
getFpssDropdownSelect(formData.rstcd, formData.baseId); getFpssDropdownSelect(formData.rstcd, formData.baseId);
initForm(); initForm();
} else {
//
// initialValues
} }
}, },
{ immediate: false } // immediate false { immediate: false } // immediate false
); );
// 3. watch
//
// watch(
// () => [props.visible, props.initialValues],
// ...
// );
// //
const resetForm = () => { const resetForm = () => {
if (formRef.value) { if (formRef.value) {
@ -549,7 +550,7 @@ const handleOk = async () => {
fwet: fwet, fwet: fwet,
fsz: fsz, fsz: fsz,
}; };
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss") if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(submitValues); console.log(submitValues);
emit("ok", submitValues); emit("ok", submitValues);
} catch (error) { } catch (error) {
@ -560,5 +561,4 @@ const handleOk = async () => {
</script> </script>
<style scoped> <style scoped>
/* 如有需要,添加局部样式 */
</style> </style>

View File

@ -11,7 +11,6 @@
<template #ftp="{ onChange }"> <template #ftp="{ onChange }">
<fishSearch v-model="localTypeDate" width="280px" @update:modelValue="onChange" /> <fishSearch v-model="localTypeDate" width="280px" @update:modelValue="onChange" />
</template> </template>
<!-- 自定义重置及操作按钮区域 -->
<template #actions> <template #actions>
<a-tooltip title="新增"> <a-tooltip title="新增">
<a-button @click="props.handleAdd"> 新增 </a-button> <a-button @click="props.handleAdd"> 新增 </a-button>
@ -65,11 +64,10 @@ import { checkPerm } from "@/directive/permission";
import fishSearch from "@/components/fishSearch/index.vue"; import fishSearch from "@/components/fishSearch/index.vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao"; import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
// --- Props & Emits ---
interface Props { interface Props {
direction: any[]; direction: any[];
guoyuStatus: any[]; guoyuStatus: any[];
importBtn: (file: File) => void; importBtn: () => void;
batchDelBtn: () => void; batchDelBtn: () => void;
submitBtn: () => void; submitBtn: () => void;
successBtn: () => void; successBtn: () => void;
@ -85,11 +83,8 @@ const emit = defineEmits<{
(e: "searchFinish", values: any): void; (e: "searchFinish", values: any): void;
}>(); }>();
const localTypeDate = ref<string>(null); const localTypeDate = ref<string>(null);
// --- State --- const basicSearchRef = ref<any>();
const basicSearchRef = ref<any>(); // 1. ref
const fileInputRef = ref<HTMLInputElement>();
// initSearchData
const initSearchData = { const initSearchData = {
baseId: "all", baseId: "all",
stcd: null, stcd: null,
@ -104,14 +99,11 @@ const initSearchData = {
}; };
const searchData = ref<any>({ ...initSearchData }); const searchData = ref<any>({ ...initSearchData });
// --- Search List Configuration ---
// STCDSTRDTENDDTDIRECTION012
const searchList: any = computed(() => [ const searchList: any = computed(() => [
{ {
type: "waterStation", type: "waterStation",
name: "baseId", name: "baseId",
label: "水电基地", label: "流域",
fieldProps: { fieldProps: {
allowClear: true, allowClear: true,
}, },
@ -166,33 +158,18 @@ const searchList: any = computed(() => [
format: "YYYY-MM-DD", format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD", valueFormat: "YYYY-MM-DD",
allowClear: false, allowClear: false,
// disabledDate: disabledDateFn, //
}, },
presets: DateSetting.RangeButton.days, presets: DateSetting.RangeButton.days,
}, },
]); ]);
// --- Methods --- // --- Methods ---
const triggerFileInput = () => {
fileInputRef.value?.click();
};
// 2. // 2.
const onSearchFinish = (values: any) => { const onSearchFinish = (values: any) => {
console.log(values);
// label dmStcd
// React label options.find(...)
// ID
// const params: any = {};
// if (values.strdt) {
// params.startDate = values.strdt[0].format("YYYY-MM-DD");
// params.endDate = values.strdt[1].format("YYYY-MM-DD");
// }
emit("searchFinish", values); emit("searchFinish", values);
}; };
const onValuesChange = (changedValues: any, allValues: any) => { const onValuesChange = (changedValues: any, allValues: any) => {
console.log(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues }; searchData.value = { ...searchData.value, ...allValues };
// searchData便使 // searchData便使
if (changedValues.rstcd || changedValues.baseId) { if (changedValues.rstcd || changedValues.baseId) {
@ -207,7 +184,7 @@ const onValuesChange = (changedValues: any, allValues: any) => {
const handleReset = () => { const handleReset = () => {
localTypeDate.value = null; localTypeDate.value = null;
emit("reset", searchData.value); emit("reset", initSearchData);
}; };
watch( watch(
() => initSearchData.ftp, () => initSearchData.ftp,
@ -216,9 +193,7 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
// --- Lifecycle ---
onMounted(() => { onMounted(() => {
//
emit("searchFinish", initSearchData); emit("searchFinish", initSearchData);
shuJuTianBaoStore.getFpssOption("", ""); shuJuTianBaoStore.getFpssOption("", "");

View File

@ -0,0 +1,502 @@
<template>
<a-table
size="small"
:loading="fileLoading"
:data-source="fileTableData"
:columns="modalColumns"
height="500"
:scroll="{ y: 500, x: '100%' }"
:row-key="(record, index) => index"
>
<template #bodyCell="{ column, record, index }">
<!-- 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) &&
column.dataIndex &&
record._warnings &&
record._warnings.includes(column.dataIndex)
"
>
<div style="color: red; display: flex; align-items: center">
<span>{{ getDisplayValue(column.dataIndex, record) }}</span>
<exclamation-circle-outlined style="margin-left: 4px" />
</div>
</template>
<!-- 3. 编辑状态下的单元格 (绑定到 editingData) -->
<template v-else-if="isEditing(index)">
<template v-if="column.dataIndex === 'baseName'">
<a-select
v-model:value="editingData.baseId"
placeholder="请选择"
show-search
:filter-option="filterOption"
:loading="rowStates[index]?.baseLoading"
style="width: 100%"
@change="(val) => handleBaseChange(val, index)"
>
<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-else-if="column.dataIndex === 'ennm'">
<a-select
v-model:value="editingData.rstcd"
placeholder="请选择"
show-search
:filter-option="filterOption"
:loading="rowStates[index]?.engLoading"
style="width: 100%"
:disabled="!editingData.baseId"
@change="(val) => handleEngChange(val, index)"
>
<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
: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"
/>
</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%"
>
<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', 'wt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
:min="0"
/>
</template>
<!-- 是否鱼苗 -->
<template v-else-if="column.dataIndex === 'isfs'">
<a-radio-group v-model:value="editingData.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 v-else>
<a-input v-model:value="editingData[column.dataIndex]" size="small" />
</template>
</template>
<!-- 4. 普通展示状态 (直接显示 record) -->
<!-- <template v-else>
{{ getDisplayValue(column.dataIndex, record) }}
</template> -->
</template>
</a-table>
</template>
<script setup lang="ts">
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 { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
import dayjs from "dayjs";
const props: any = defineProps({
fileTableData: { type: Array, default: () => [] },
fileLoading: { type: Boolean, default: false },
direction: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:fileTableData"]);
// --- ---
const editingRowIndex = ref<number | null>(null);
const baseOptions = ref<any[]>([]);
const rowStates = reactive<Record<number, any>>({});
// 使
const editingData = ref<any>(null);
const modalColumns = ref([
{ dataIndex: "baseName", key: "baseName", title: "流域", width: 140 },
{ dataIndex: "ennm", key: "ennm", title: "电站名称", width: 140 },
{ dataIndex: "stnm", key: "stnm", title: "过鱼设施名称", width: 150 },
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 190 },
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
{
dataIndex: "isfs",
key: "isfs",
title: "是否鱼苗",
width: 130,
customRender: ({ text }: any) => {
const isYes = text === 1 || text === "1";
return h(Tag, { color: isYes ? "success" : "error", style: { margin: 0 } }, () =>
isYes ? "是" : "否"
);
},
},
{
dataIndex: "direction",
key: "direction",
title: "游向",
width: 120,
customRender: ({ text }: any) =>
props.direction.find((item: any) => item.itemCode === text)?.dictName || "-",
},
{ dataIndex: "fcnt", key: "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: "level5", title: "图片", width: 100 },
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 },
{
title: "操作",
key: "action",
dataIndex: "action",
fixed: "right",
width: 100,
align: "center",
},
]);
// --- ---
onMounted(() => {
loadBaseOptions();
});
const loadBaseOptions = async () => {
try {
const res = await getBaseDropdown({});
baseOptions.value = res.data || [];
} catch (e) {
console.error("Load base options failed", e);
}
};
const ensureRowState = (index: number) => {
if (!rowStates[index]) {
rowStates[index] = {
engOptions: [],
fpssOptions: [],
baseLoading: false,
engLoading: false,
fpssLoading: false,
};
}
return rowStates[index];
};
// --- ( editingData) ---
const handleBaseChange = async (baseId: string, index: number) => {
console.log(baseId)
editingData.value.baseName = baseOptions.value.find(
(item: any) => item.baseid == baseId
)?.basename;
if (baseId && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'baseName');
}
const state = ensureRowState(index);
//
editingData.value.rstcd = undefined;
editingData.value.stcd = undefined;
state.engOptions = [];
state.fpssOptions = [];
if (!baseId) return;
state.engLoading = true;
try {
const res = await getEngInfoDropdown({ baseId });
state.engOptions = res.data || [];
} catch (e) {
message.error("获取电站列表失败");
} finally {
state.engLoading = false;
}
};
const handleEngChange = async (rstcd: string, index: number) => {
const state = ensureRowState(index);
if (rstcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'ennm');
}
editingData.value.ennm = state.engOptions.find(
(item: any) => item.stcd === rstcd
)?.ennm;
editingData.value.stcd = undefined;
state.fpssOptions = [];
if (!rstcd || !editingData.value.baseId) return;
state.fpssLoading = true;
try {
const res = await getFpssDropdown({ rstcd, baseId: editingData.value.baseId });
state.fpssOptions = res.data || [];
} catch (e) {
message.error("获取设施列表失败");
} finally {
state.fpssLoading = false;
}
};
const handleFpssChange = (stcd: string, index: number) => {
const state = ensureRowState(index);
if (stcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'stnm');
}
editingData.value.stnm = state.fpssOptions.find(
(item: any) => item.stcd === stcd
)?.stnm;
};
// --- ---
const isEditing = (index: number) => editingRowIndex.value === index;
const startEdit = (index: number) => {
const originalRecord = props.fileTableData[index];
// 1.
editingData.value = JSON.parse(JSON.stringify(originalRecord));
// 2. fsz/fwet Min/Max 使
processStringToMinMax(editingData.value);
editingRowIndex.value = index;
// 3. ( editingData )
if (editingData.value.baseId && !editingData.value.rstcd) {
handleBaseChange(editingData.value.baseId, index);
} else if (editingData.value.baseId && editingData.value.rstcd) {
handleBaseChange(editingData.value.baseId, index).then(() => {
handleEngChange(editingData.value.rstcd, index);
});
}
};
// 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 = (index: number) => {
// 1. Min/Max fsz/fwet
processMinMaxToString(editingData.value);
// 2.
const newData = [...props.fileTableData];
newData[index] = { ...editingData.value };
// 3.
emit("update:fileTableData", newData);
// 4.
editingRowIndex.value = null;
editingData.value = null;
message.success("保存成功");
};
const cancelEdit = () => {
editingRowIndex.value = null;
editingData.value = null;
// props.fileTableData退
};
const handlePreviewDelete = (index: number) => {
const newData = [...props.fileTableData];
newData.splice(index, 1);
emit("update:fileTableData", newData);
if (editingRowIndex.value === index) {
editingRowIndex.value = null;
editingData.value = null;
} else if (editingRowIndex.value !== null && editingRowIndex.value > index) {
editingRowIndex.value--;
}
message.success("删除成功");
};
//
const handleFtpChange = (val: any, opt: any) => {
editingData.value.ftpName = opt.name;
};
// --- ---
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
//
const getDisplayValue = (dataIndex: string, record: any) => {
const val = record[dataIndex];
if (val === undefined || val === null) return "-";
if (dataIndex === "isfs") return val === 1 ? "是" : "否";
if (dataIndex === "strdt") return val ? dayjs(val).format("YYYY-MM-DD HH:mm:ss") : "-";
return val;
};
defineExpose({
editingRowIndex,
editingData,
});
</script>

View File

@ -1,6 +1,5 @@
<template> <template>
<div class="guoYuSheShiShuJuTianBao-page"> <div class="guoYuSheShiShuJuTianBao-page">
<!-- 搜索区域组件具体 props 需根据实际子组件调整 -->
<GuoYuSheShiShuJuTianBaoSearch <GuoYuSheShiShuJuTianBaoSearch
ref="searchRef" ref="searchRef"
:guoyuStatus="guoyuStatus" :guoyuStatus="guoyuStatus"
@ -14,7 +13,6 @@
@reset="handleReset" @reset="handleReset"
@search-finish="handleSearchFinish" @search-finish="handleSearchFinish"
/> />
<!-- 主表格 --> <!-- 主表格 -->
<BasicTable <BasicTable
ref="tableRef" ref="tableRef"
@ -39,7 +37,11 @@
type="link" type="link"
size="small" size="small"
@click="handleEdit(record, 'edit')" @click="handleEdit(record, 'edit')"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'|| record.status === 'SUBMITTED'" v-if="
record.status === 'DRAFT' ||
record.status === 'REJECTED' ||
record.status === 'SUBMITTED'
"
>编辑</a-button >编辑</a-button
> >
<a-button <a-button
@ -76,7 +78,6 @@
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<!-- <BasicTable :columns="columns" :listUrl="getFishDraftPage" /> -->
<!-- 隐藏的文件输入框 --> <!-- 隐藏的文件输入框 -->
<input <input
ref="fileInputRef" ref="fileInputRef"
@ -92,20 +93,31 @@
cancel-text="取消导入" cancel-text="取消导入"
:width="1500" :width="1500"
v-model:open="visible" v-model:open="visible"
maskClosable="false"
@cancel="handleCancel"
:confirm-loading="fileLoading" :confirm-loading="fileLoading"
@cancel="handleModalCancel"
@ok="handleModalOk"
> >
<a-table <GuoYuSheShiShuJuTianBaoTable
size="small" ref="modalTableRef"
:fileLoading="fileLoading"
:fileTableData="fileTableData"
:direction="direction"
@update:file-table-data="(val) => fileTableData = val"
/>
<template #footer>
<a-button key="back" @click="handleCustomCancel">取消导入</a-button>
<a-button
key="submit"
type="primary"
:loading="fileLoading" :loading="fileLoading"
:data-source="fileTableData" @click="handleModalOk"
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
row-key="index"
> >
<!-- 如果需要复杂的行内编辑插槽可在此定义但目前逻辑主要在 column render 中处理 --> 提交导入
</a-table> </a-button>
</template>
</a-modal> </a-modal>
<!-- 新增/编辑 Modal (对应 React EditModal) --> <!-- 新增/编辑 Modal (对应 React EditModal) -->
@ -142,10 +154,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted, h } from "vue"; import { ref, computed, onMounted, h, nextTick } from "vue";
import { message, Modal } from "ant-design-vue"; // 使 ant-design-vue import { message, Modal } from "ant-design-vue"; // 使 ant-design-vue
import BasicTable from "@/components/BasicTable/index.vue"; import BasicTable from "@/components/BasicTable/index.vue";
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue"; import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue";
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue"; import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
import { import {
getFishDraftPage, getFishDraftPage,
@ -158,11 +171,13 @@ import {
importFishZip, importFishZip,
submitImportTask, submitImportTask,
cancelImportTask, cancelImportTask,
getImportTask, checkImportStatus,
checkImportStatus batchSaveDraft,
getLastImportResult,
} from "@/api/guoYuSheShiShuJuTianBao"; } from "@/api/guoYuSheShiShuJuTianBao";
import { Tag } from "ant-design-vue"; // Tag import { Tag } from "ant-design-vue"; // Tag
import { getDictItemsByCode } from "@/api/dict"; import { getDictItemsByCode } from "@/api/dict";
import { m } from "vue-router/dist/router-CWoNjPRp.mjs";
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue' // import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
// --- --- // --- ---
@ -188,7 +203,7 @@ const baseColumnsConfig: ColumnConfig[] = [
{ {
dataIndex: "baseName", dataIndex: "baseName",
key: "baseName", key: "baseName",
title: "水电基地", title: "流域",
width: 120, width: 120,
fixed: "left", fixed: "left",
}, },
@ -260,10 +275,12 @@ const videoPreviewVisible = ref(false);
const currentVideoUrl = ref<string>(""); const currentVideoUrl = ref<string>("");
// //
const tableData = ref<any[]>([]);
const fileTableData = ref<any[]>([]); const fileTableData = ref<any[]>([]);
const orgFileTableData = ref<any[]>([]);
const warnings = ref<any[]>([]);
const batchData = ref<any[]>([]); const batchData = ref<any[]>([]);
const modalTableRef = ref<any>(null);
const fileLoading = ref(false); const fileLoading = ref(false);
// Key () // Key ()
@ -278,25 +295,7 @@ const editingKey = ref<string | number>("");
// if (!file) return ""; // if (!file) return "";
// const blob = await file.async("blob"); // const blob = await file.async("blob");
// return URL.createObjectURL(blob); // return URL.createObjectURL(blob);
// } catch (e) { const taskId = ref<string>("");
// 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 // Columns
const columns = computed(() => { const columns = computed(() => {
@ -334,59 +333,6 @@ const columns = computed(() => {
]; ];
}); });
// 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 handleAdd = () => { const handleAdd = () => {
@ -563,40 +509,111 @@ const handleEditSubmit = async (values: FormData) => {
submitLoading.value = false; submitLoading.value = false;
} }
}; };
const isRowDataEqual = (oldRow: any, newRow: any, fields: string[]): boolean => {
for (const field of fields) {
// undefined/null
let oldVal = oldRow?.[field];
let newVal = newRow?.[field];
//
if ((oldVal === undefined || oldVal === null || oldVal === '') &&
(newVal === undefined || newVal === null || newVal === '')) {
continue;
}
//
if (oldVal !== newVal) {
return false;
}
}
return true;
};
const checkTableDataChanges = () => {
// 1. ( Form )
const oldData = orgFileTableData.value;
const newData = fileTableData.value;
// 2.
if (oldData.length !== newData.length) {
return { hasChanged: true, changedCount: newData.length };
}
let changedCount = 0;
// 3.
for (let i = 0; i < newData.length; i++) {
const oldRow = oldData[i];
const newRow = newData[i];
// +1
if (!isRowDataEqual(oldRow, newRow, ['baseId'])) {
changedCount++;
}
}
return { hasChanged: changedCount > 0, changedCount };
};
//
const handleModalOk = () => { const handleModalOk = () => {
Modal.confirm({ console.log(orgFileTableData.value)
title: "是否提交导入数据?", console.log(fileTableData.value)
onOk: async () => { console.log(
// tableData.value = [...fileTableData.value]; modalTableRef.value.editingData)
visible.value = false; if (modalTableRef.value.editingData != undefined) {
editingKey.value = ""; message.warning("请点击保存后提交数据!");
message.success("数据已导入至列表"); return
}
const { hasChanged, changedCount } = checkTableDataChanges();
// 3.
if (!hasChanged) {
message.info("数据未发生任何变化,无需提交");
return;
}
console.log(123)
// if (warnings.value.length > 0) {
// message.warning("");
// return;
// }
// Modal.confirm({
// title: "?",
// onOk: async () => {
// // tableData.value = [...fileTableData.value];
// let ids = fileTableData.value.map((item: any) => item.id);
// visible.value = false;
// editingKey.value = "";
// let res: any = await submitImportTask(ids); // let res: any = await submitImportTask(ids);
// if (res && res?.code == 0) { // if (res && res?.code == 0) {
// message.success(""); // message.success("");
// tableRef.value?.getList(); // tableRef.value?.getList();
// } else {
// message.error(",");
// } // }
}, // },
}); // });
}; };
const handleModalCancel = () => { //
const handleCustomCancel = () => {
Modal.confirm({ Modal.confirm({
title: "是否取消导入数据?", title: "是否取消导入数据?",
content: "未提交的数据将丢失",
okText: "确定",
onOk: async () => { onOk: async () => {
visible.value = false; visible.value = false;
editingKey.value = ""; editingKey.value = "";
// let res: any = await cancelImportTask(ids); //
// if (res && res?.code == 0) { let res: any = await cancelImportTask({taskId: taskId.value});
// message.success(""); if (res && res?.code == 0) {
// tableRef.value?.getList(); message.success("取消成功");
// } tableRef.value?.getList();
}
}, },
}); });
}; };
const handleFileSelect = (e: Event) => { const handleFileSelect = (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const file = target.files?.[0]; const file = target.files?.[0];
@ -621,8 +638,7 @@ const handleFileSelect = (e: Event) => {
resetFileInput(); resetFileInput();
return; return;
} }
fileChange(file);
// props.importBtn(file);
resetFileInput(); resetFileInput();
}; };
@ -631,23 +647,66 @@ const resetFileInput = () => {
fileInputRef.value.value = ""; fileInputRef.value.value = "";
} }
}; };
const importBtn = async (file: File) => { const fileChange = async (file: File) => {
try {
fileLoading.value = true;
const formData = new FormData();
formData.append("file", file);
let res: any = await importFishZip(formData);
const { code } = res.data || {};
if (code == 1) {
message.error("导入失败");
} else {
message.success("导入成功");
fileTableaAnalysis(res, "file");
}
} catch (error) {
} finally {
}
};
// -
const importBtn = async () => {
let res: any = await checkImportStatus(); let res: any = await checkImportStatus();
console.log(res) taskId.value = "";
// fileInputRef.value?.click(); if (res?.code == 0) {
// fileLoading.value = true; const { hasImportingTask ,currentTask} = res?.data || {};
taskId.value = currentTask.id;
if (hasImportingTask) {
visible.value = true;
nextTick(async () => {
fileLoading.value = true;
modalTableRef.value.editingRowIndex = null;
let res1: any = await getLastImportResult();
fileTableaAnalysis(res1, "get");
});
} else {
fileInputRef.value?.click();
}
} else {
message.error("导入查询失败");
}
// editingKey.value = ""; // editingKey.value = "";
};
// try { const fileTableaAnalysis = (res: any, type: string) => {
// const formData = new FormData(); let data = [];
// formData.append("file", file); // let warningsList = [];
// let res: any = await importFishZip(formData); let list = [];
// console.log(res); if (type == "file") {
// visible.value = true; list = res.data.failedRows;
// } catch (error) { } else {
// } finally { list = res.data.result.failedRowDetails;
// // fileLoading.value = false; }
// } console.log(list);
list.forEach((item) => {
data.push({
...item.data,
_warnings: item.warnings,
});
});
fileTableData.value = data || [];
orgFileTableData.value = JSON.parse(JSON.stringify(fileTableData.value));
// warnings.value = warningsList || [];
fileLoading.value = false;
}; };
const handleReset = (values) => { const handleReset = (values) => {
handleSearchFinish(values); handleSearchFinish(values);

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"noUnusedLocals": true, "noUnusedLocals": false,
"noUnusedParameters": true, "noUnusedParameters": false,
"target": "esnext", "target": "esnext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "esnext", "module": "esnext",