数据填报修改

This commit is contained in:
扈兆增 2026-04-24 15:31:32 +08:00
parent f01e6d926f
commit 6e600d6c72
9 changed files with 1055 additions and 701 deletions

View File

@ -8,27 +8,90 @@ export function getFishDraftPage(data:any) {
data
});
}
//新增目录
//新增过鱼数据
export function addFishDraft(queryParams:any) {
return request({
url: '/data/fishDraft/add',
url: '/data/fishDraft/saveDraft',
method: 'post',
data: queryParams
});
}
//修改目录
//修改过鱼数据
export function editFishDraft(queryParams:any) {
return request({
url: '/data/fishDraft/update',
url: '/data/fishDraft/updateDraft',
method: 'post',
data: queryParams
});
}
//删除
//删除 过鱼数据
export function delFishDraft(data:any) {
return request({
url: '/data/fishDraft/batchDelete',
url: '/data/fishDraft/batchRemoveDraft',
method: 'post',
data
});
}
//提交过鱼数据
export function submitFishDraft(data:any) {
return request({
url: '/data/fishDraft/submitDrafts',
method: 'post',
data
});
}
//审批过鱼数据
export function successFishDraft(data:any) {
return request({
url: '/data/fishDraft/batchApprove',
method: 'post',
data
});
}
//驳回过鱼数据
export function rejectFishDraft(data:any) {
return request({
url: '/data/fishDraft/reject',
method: 'post',
data
});
}
// 导入zip
export function importFishZip(data:FormData) {
return request({
url: '/data/fishDraft/importZip',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data' }
});
}
// 提交导入任务
export function submitImportTask(data:any) {
return request({
url: '/data/fishDraft/submitDrafts',
method: 'post',
data
});
}
// 取消导入任务
export function cancelImportTask(data:any) {
return request({
url: '/data/fishDraft/cancelImport',
method: 'post',
data
});
}
// 查询导入任务
export function getImportTask() {
return request({
url: '/data/importTask/list',
method: 'get'
});
}
//检测用户导入状态
export function checkImportStatus() {
return request({
url: '/data/fishDraft/checkImportStatus',
method: 'get'
});
}

View File

@ -0,0 +1,33 @@
import request from '@/utils/request';
// 基地下拉列表
export function getBaseDropdown(data:any) {
return request({
url: '/env/hydrobase/dropdown',
method: 'get',
data
});
}
//电站下拉列表
export function getEngInfoDropdown(params:any) {
return request({
url: '/env/engInfo/dropdown',
method: 'get',
params
});
}
//过鱼设施下拉列表
export function getFpssDropdown(params:any) {
return request({
url: '/env/fpss/dropdown',
method: 'get',
params
});
}
//鱼类名称下拉列表
export function getFishDictoryDropdown() {
return request({
url: '/env/fishDictory/listByName',
method: 'get'
});
}

View File

@ -6,6 +6,7 @@
:rules="rules"
layout="inline"
class="basic-search-form"
@reset="handleReset"
@finish="handleFinish"
@values-change="handleValuesChange"
>
@ -18,14 +19,18 @@
:name="item.name"
style="width: 100%; margin-bottom: 0"
>
<!-- 1. 优先检查是否有具名插槽或者 type custom -->
<slot
v-if="$slots[item.name] || item.type === 'custom'"
:name="item.name"
:value="formData[item.name]"
:onChange="(val:any) => { formData[item.name] = val }"
:formModel="formData"
/>
<!-- 1. 优先检查是否有具名插槽或者 type custom -->
<slot
v-if="$slots[item.name] || item.type === 'custom'"
:name="item.name"
:value="formData[item.name]"
:onChange="(val:any) => {
formData[item.name] = val;
triggerManualValuesChange(item.name, val);
}"
:formModel="formData"
/>
<!-- 普通日期选择器 -->
<a-date-picker
v-else-if="item.type === 'DataPicker'"
@ -38,7 +43,9 @@
:allow-clear="item.fieldProps?.allowClear"
:presets="item.presets"
style="width: 100%"
@change="(val) => triggerManualValuesChange(item.name, val)"
/>
<!-- 日期范围选择器 -->
<a-range-picker
v-else-if="item.type === 'RangePicker'"
@ -51,6 +58,7 @@
:allow-clear="item.fieldProps?.allowClear"
:presets="item.presets"
style="width: 100%"
@change="(val) => triggerManualValuesChange(item.name, val)"
/>
<!-- 普通输入框 -->
@ -60,38 +68,40 @@
:placeholder="item.placeholder || '请输入'"
:allow-clear="item.fieldProps?.allowClear"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
/>
<!-- 电站下拉框 -->
<div class="flex gap-[10px]" v-else-if="item.type === 'waterStation'">
<a-form-item-rest>
<a-select
:value="formData.stcd?.dataDimensionData"
placeholder="请选择"
@change="dataDimensionDataChange"
style="width: 135px"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
<a-select
:value="formData.baseId"
placeholder="请选择"
@change="dataDimensionDataChange"
style="width: 135px"
>
{{ opt.label }}
</a-select-option>
</a-select>
<a-select
:value="formData.stcd?.stcdId"
placeholder="请选择电站"
@change="stcdIdChange"
style="width: 135px"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
<a-select-option
v-for="opt in shuJuTianBaoStore.baseOption"
:key="opt.baseid"
:value="opt.baseid"
>
{{ opt.basename }}
</a-select-option>
</a-select>
<a-select
:value="formData.rstcd"
placeholder="请选择电站"
@change="stcdIdChange"
style="width: 135px"
>
{{ opt.label }}
</a-select-option>
</a-select>
<a-select-option
v-for="opt in shuJuTianBaoStore.engOption"
:key="opt.stcd"
:value="opt.stcd"
>
{{ opt.ennm }}
</a-select-option>
</a-select>
</a-form-item-rest>
</div>
@ -102,27 +112,25 @@
:placeholder="item.placeholder || '请选择'"
:allow-clear="item.fieldProps?.allowClear"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(val) => triggerManualValuesChange(item.name, val)"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
:key="opt[item.values?.value] || opt.value || opt.itemCode"
:value="opt[item.values?.value] || opt.value || opt.itemCode"
>
{{ opt.label }}
{{ opt[item.values?.name] || opt.label || opt.dictName }}
</a-select-option>
</a-select>
<!-- 单选 -->
<!-- 单选-->
<a-radio-group
v-else-if="item.type === 'Radio'"
v-model:value="formData[item.name]"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
>
<a-radio
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
>
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-radio>
</a-radio-group>
@ -149,7 +157,10 @@
</template>
<script lang="ts" setup>
import { ref, computed, reactive, watch, onMounted } from "vue";
import { ref, computed, reactive, watch, onMounted, nextTick } from "vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// import { nextTick } from "process";
// --- ---
export interface SearchItem {
@ -163,7 +174,13 @@ export interface SearchItem {
xlSpan?: number;
width?: number;
presets?: any[];
options?: { label: string; value: any }[];
values?: any;
options?: {
itemCode?: string;
dictName?: string;
label: string;
value: any;
}[];
component?: any;
}
@ -192,8 +209,9 @@ const rules = reactive<Record<string, any>>({});
// 2. false/null/undefined
const validSearchList = computed(() => {
return props.searchList.filter(item => item);
return props.searchList.filter((item) => item);
});
// --- ---
const initForm = () => {
const initial = JSON.parse(JSON.stringify(props.initialValues || {}));
@ -205,8 +223,12 @@ const initForm = () => {
Object.assign(formData, initial);
// 3. searchList false/null/undefined
validSearchList.value.forEach((item) => {
if (item.type == "waterStation") {
//
shuJuTianBaoStore.getBaseOption();
shuJuTianBaoStore.getEngOption(formData.baseId);
}
if (item.fieldProps?.required) {
rules[item.name] = [
{ required: true, message: `${item.label}不能为空`, trigger: "blur" },
@ -214,15 +236,35 @@ const initForm = () => {
}
});
};
/**
* 手动触发 valuesChange 事件
* 用于处理那些没有被 a-form 直接管理的字段 waterStation 内部逻辑
* 或者作为标准控件的备份触发机制
*/
const triggerManualValuesChange = (changedKey: string, newValue: any) => {
//
const changedValues = { [changedKey]: newValue };
//
// 使 {...formData}
emit("valuesChange", changedValues, { ...formData });
};
const dataDimensionDataChange = (value: any) => {
formData.stcd.dataDimensionData = value;
formData.baseId = value;
formData.rstcd = "";
shuJuTianBaoStore.getEngOption(formData.baseId);
// valuesChange a-form-item-rest
triggerManualValuesChange("baseId", formData.baseId);
};
// const hbrvcdChange = (value: any) => {
// formData.stcd.hbrvcd = value;
// };
const stcdIdChange = (value: any) => {
formData.stcd.stcdId = value;
formData.rstcd = value;
// valuesChange
triggerManualValuesChange("rstcd", formData.rstcd);
};
onMounted(() => {
initForm();
});
@ -240,7 +282,8 @@ watch(
// --- ---
const handleFinish = (values: any) => {
emit("finish", values);
const finalValues = { ...formData, ...values };
emit("finish", finalValues);
};
const handleValuesChange = (changedValues: any, allValues: any) => {
@ -250,12 +293,16 @@ const handleValuesChange = (changedValues: any, allValues: any) => {
const handleReset = () => {
if (formRef.value) {
formRef.value.resetFields();
nextTick(() => {
initForm();
});
emit("reset");
}
};
defineExpose({
form: formRef,
formData,
reset: handleReset,
submit: () => formRef.value?.submit(),
});

View File

@ -67,7 +67,7 @@ const rowSelection = computed(() => ({
emit("selection-change", keys, rows);
},
getCheckboxProps: (record: any) => ({
disabled: record.disabled, //
// disabled: record.status === 'SUBMITTED'
}),
}));
@ -88,20 +88,20 @@ const paginationConfig = computed(() => ({
* 获取列表数据
* @param extraParams 额外的临时参数可选
*/
const getList = async (extraParams?: Record<string, any>) => {
const getList = async (filter?: Record<string, any>) => {
loading.value = true;
try {
//
const params = {
...props.searchParams,
...extraParams,
// ...props.searchParams,
skip: page.value,
take: size.value,
filter: {}
filter: filter,
// skip/take
// skip: (page.value - 1) * size.value,
// take: size.value,
};
console.log(params);
const res = await props.listUrl(params);
@ -177,7 +177,6 @@ watch(
// --- Lifecycle ---
onMounted(() => {
getList();
});
</script>

View File

@ -1,24 +1,28 @@
<template>
<a-select
ref="selectRef"
:style="{ width: width }"
:value="modelValue"
:options="options"
:loading="loading"
@change="handleChange"
placeholder="请选择鱼名称"
mode="multiple"
@search="handleSearch"
placeholder="请选择鱼种类"
:mode="multiple ? 'multiple' : undefined"
show-search
:filter-option="filterOption"
:filter-option="false"
class="custom-fish-select"
:dropdownMatchSelectWidth="false"
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
@dropdownVisibleChange="handleDropdownVisibleChange"
:max-tag-count="multiple ? 1 : undefined"
:open="open"
@update:open="open = $event"
:field-names="{ label: 'name', value: 'id' }"
>
<!-- 自定义 Tag 显示名称 -->
<template #tagRender="{ value: tagId, onClose }">
<a-tag
closable
@close="onClose"
style="margin-right: 3px; max-width: 120px"
>
<!-- 自定义 Tag 显示名称 (仅在多选时生效) -->
<template #tagRender="{ value: tagId, onClose }" v-if="multiple">
<a-tag closable @close="onClose" style="margin-right: 3px; max-width: 120px">
{{ getFishNameById(tagId) }}
</a-tag>
</template>
@ -32,7 +36,7 @@
:key="opt.id"
class="dropdown-item"
:class="{
'is-active': Array.isArray(modelValue) && modelValue.includes(opt.id),
'is-active': isSelected(opt.id),
'is-hovered': opt.id === hoveredId,
}"
@click.stop="handleSelectOption(opt)"
@ -40,11 +44,9 @@
>
<span class="item-name">{{ opt.name }}</span>
<!-- 选中对勾 -->
<span v-if="Array.isArray(modelValue) && modelValue.includes(opt.id)" class="check-icon"></span>
</div>
<div v-if="filteredOptions.length === 0" class="empty-tip">
无匹配数据
<span v-if="isSelected(opt.id)" class="check-icon"></span>
</div>
<div v-if="filteredOptions.length === 0" class="empty-tip">无匹配数据</div>
</div>
<!-- 中间分割线 -->
@ -66,30 +68,42 @@
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { ref, onMounted, computed, watch } from "vue";
import { getFishDictoryDropdown } from "@/api/select";
// --- Props & Emits ---
interface Props {
modelValue: string[]; // ID
options: any[];
width: string;
modelValue: string | string[]; // ()()
width?: string;
multiple?: boolean; //
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
multiple: false, //
});
const emit = defineEmits<{
(e: "update:modelValue", value: string[]): void;
(e: "update:modelValue", value: string | string[]): void;
}>();
// --- State ---
// API
const options = ref<any>(props.options || []);
const loading = ref(false);
const options = ref<any[]>([]);
const searchKeyword = ref<string>("");
const hoveredId = ref<string | null>(null);
const open = ref(false); //
// --- Computed ---
const filteredOptions = computed(() => {
return options.value;
if (!searchKeyword.value) {
return options.value;
}
const lowerKeyword = searchKeyword.value.toLowerCase();
return options.value.filter((item: any) => {
const nameMatch = item.name?.toLowerCase().includes(lowerKeyword);
const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword);
return nameMatch || aliasMatch;
});
});
const currentDetailData = computed(() => {
@ -100,39 +114,59 @@ const currentDetailData = computed(() => {
});
// --- Methods ---
const handleDropdownVisibleChange = (open: boolean) => {
if (!open) {
hoveredId.value = null;
//
const isSelected = (id: string) => {
if (props.multiple) {
return Array.isArray(props.modelValue) && props.modelValue.includes(id);
} else {
return props.modelValue === id;
}
};
const filterOption = (input: string, option: any) => {
if (!input) return true;
const targetOpt = options.value.find((item: any) => item.id === option.value);
if (!targetOpt) return false;
const handleSearch = (value: string) => {
searchKeyword.value = value;
};
const lowerInput = input.toLowerCase();
const nameMatch = targetOpt.name?.toLowerCase().includes(lowerInput);
const aliasMatch = targetOpt.alias?.toLowerCase().includes(lowerInput);
return nameMatch || aliasMatch;
const handleDropdownVisibleChange = (val: boolean) => {
open.value = val;
if (!val) {
hoveredId.value = null;
searchKeyword.value = "";
}
};
const handleSelectOption = (opt: any) => {
let newValues: string[] = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
const index = newValues.indexOf(opt.id);
if (props.multiple) {
// --- ---
let newValues: string[] = Array.isArray(props.modelValue)
? [...props.modelValue]
: [];
const index = newValues.indexOf(opt.id);
if (index > -1) {
newValues.splice(index, 1);
if (index > -1) {
newValues.splice(index, 1); //
} else {
newValues.push(opt.id); //
}
emit("update:modelValue", newValues);
} else {
newValues.push(opt.id);
// --- ---
// ID
//
if (props.modelValue === opt.id) {
emit("update:modelValue", ""); //
} else {
emit("update:modelValue", opt.id); //
//
open.value = false;
}
}
//
emit("update:modelValue", newValues);
};
const handleChange = (val: any) => {
// a-select tag
// a-select change Tag
//
emit("update:modelValue", val);
};
@ -141,9 +175,37 @@ const getFishNameById = (id: string) => {
const fish = options.value.find((item: any) => item.id === id);
return fish ? fish.name : id;
};
// multiple
watch(
() => props.multiple,
(newVal) => {
// modelValue
if (!newVal && Array.isArray(props.modelValue)) {
emit("update:modelValue", props.modelValue || null);
}
// modelValue
if (newVal && typeof props.modelValue === "string") {
emit("update:modelValue", props.modelValue ? [props.modelValue] : []);
}
}
);
onMounted(() => {
loading.value = true;
getFishDictoryDropdown()
.then((res) => {
options.value = res.data || [];
loading.value = false;
})
.catch(() => {
loading.value = false;
});
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.custom-fish-select {
:deep(.ant-select-dropdown) {
padding: 0 !important;
@ -157,7 +219,7 @@ const getFishNameById = (id: string) => {
background: #fff;
border-radius: 4px;
overflow: hidden;
height: 300px; /* 固定高度 */
height: 300px;
}
.dropdown-left-list {

View File

@ -0,0 +1,61 @@
import { defineStore } from 'pinia';
import { ref } from 'vue'; // 使用 ref 更简单直观
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from '@/api/select';
export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
// 1. 直接使用 ref 定义状态,确保响应式
const fpssOption = ref<any[]>([]);
const baseOption = ref<any[]>([]);
const engOption = ref<any[]>([]);
// 2. 业务逻辑方法
const getBaseOption = async () => {
try {
const res = await getBaseDropdown({});
if (res.data && Array.isArray(res.data)) {
const list = [...res.data];
list.unshift({
baseid: 'all',
basename: '当前全部'
});
// 直接赋值给 ref触发响应式更新
baseOption.value = list;
}
} catch (error) {
console.error('获取水电基地列表失败:', error);
}
};
const getEngOption = async (baseId: string) => {
try {
const param = baseId === 'all' ? {} : { baseId };
const res = await getEngInfoDropdown(param);
if (res.data && Array.isArray(res.data)) {
// 直接赋值给 ref
engOption.value = res.data;
}
} catch (error) {
console.error('获取电站列表失败:', error);
}
};
const getFpssOption = async (baseId: string, rstcd: string) => {
try {
const res = await getFpssDropdown({ baseId, rstcd });
fpssOption.value = res.data;
} catch (error) {
console.log(error);
}
};
// 3. 直接返回 ref 和方法
// 在组件中使用时store.baseOption 会自动解包为数组
return {
fpssOption,
baseOption,
engOption,
getBaseOption,
getEngOption,
getFpssOption,
};
});

View File

@ -2,11 +2,16 @@
<div class="guoYuSheShiShuJuTianBao-page">
<!-- 搜索区域组件具体 props 需根据实际子组件调整 -->
<GuoYuSheShiShuJuTianBaoSearch
:import-btn="importBtn"
:save-btn="saveBtn"
ref="searchRef"
:guoyuStatus="guoyuStatus"
:direction="direction"
:handle-add="handleAdd"
:batchData="batchData"
:batchDel="batchDel"
:importBtn="importBtn"
:batchDelBtn="batchDelBtn"
:submitBtn="submitBtn"
:successBtn="successBtn"
@reset="handleReset"
@search-finish="handleSearchFinish"
/>
@ -23,21 +28,68 @@
<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
type="link"
size="small"
@click="handleSubmit([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>提交</a-button
>
<a-button
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'|| record.status === 'SUBMITTED'"
>编辑</a-button
>
<a-button
type="link"
danger
size="small"
@click="handleDelete([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>删除</a-button
>
<a-button
type="link"
size="small"
@click="handleEdit(record, 'view')"
v-if="record.status === 'SUBMITTED'"
>查看</a-button
>
<a-button
type="link"
size="small"
@click="handleSuccess([record.id])"
v-if="record.status === 'SUBMITTED'"
>审批</a-button
>
<a-button
type="link"
danger
size="small"
@click="handleReject(record.id)"
v-if="record.status === 'SUBMITTED'"
>驳回</a-button
>
</div>
</template>
</template>
</BasicTable>
<!-- <BasicTable :columns="columns" :listUrl="getFishDraftPage" /> -->
<!-- 隐藏的文件输入框 -->
<input
ref="fileInputRef"
type="file"
accept=".zip,application/zip,application/x-zip-compressed"
style="display: none"
@change="handleFileSelect"
/>
<!-- 导入预览 Modal -->
<a-modal
title="导入数据预览"
ok-text="提交导入"
cancel-text="取消"
cancel-text="取消导入"
:width="1500"
v-model:open="visible"
:confirm-loading="fileLoading"
@ -60,6 +112,8 @@
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
<EditModal
v-model:visible="editModalVisible"
:is-view="isView"
:direction="direction"
:initial-values="currentRecord"
:loading="submitLoading"
@cancel="editModalCancel"
@ -88,10 +142,8 @@
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, h } from "vue";
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";
@ -100,9 +152,17 @@ import {
addFishDraft,
editFishDraft,
delFishDraft,
submitFishDraft,
successFishDraft,
rejectFishDraft,
importFishZip,
submitImportTask,
cancelImportTask,
getImportTask,
checkImportStatus
} from "@/api/guoYuSheShiShuJuTianBao";
import dayjs from "dayjs";
import { Tag } from 'ant-design-vue'; // Tag
import { Tag } from "ant-design-vue"; // Tag
import { getDictItemsByCode } from "@/api/dict";
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
// --- ---
@ -115,34 +175,52 @@ interface ColumnConfig {
key: string;
title: string;
width?: number;
fixed?: string;
customRender?: (text: any, record: any) => any;
}
const fileInputRef = ref<any>(null);
const tableRef = ref<any>(null);
//
const direction = ref<any>([]);
const guoyuStatus = ref<any>([]);
// --- ---
const baseColumnsConfig: ColumnConfig[] = [
{ dataIndex: "engName", key: "engName", title: "水电基地", width: 100 },
{ dataIndex: "baseName", key: "baseName", title: "电站名称", width: 120 },
{ dataIndex: "fpname", key: "fpname", title: "过鱼设施名称", width: 150 },
{
dataIndex: "baseName",
key: "baseName",
title: "水电基地",
width: 120,
fixed: "left",
},
{ dataIndex: "ennm", key: "ennm", title: "电站名称", width: 120, fixed: "left" },
{ dataIndex: "stnm", key: "stnm", title: "过鱼设施名称", width: 150, fixed: "left" },
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
{ dataIndex: "ftp", key: "ftp", title: "鱼种类", width: 120 },
{ dataIndex: "ftpName", key: "ftpName", 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 ? '是' : '否'
);
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,
customRender: ({ text }: any) =>
direction.value.find((item: any) => item.itemCode === text)?.dictName || "-",
},
{ 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 },
@ -150,7 +228,22 @@ const baseColumnsConfig: ColumnConfig[] = [
{ 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 },
{
dataIndex: "status",
key: "status",
title: "状态",
width: 100,
customRender: ({ text }: any) => {
let data = guoyuStatus.value.find((item: any) => item.itemCode === text);
return h(
Tag,
{
color: data?.custom1 || "error", // Antdv Tag
},
() => data?.dictName || "-"
);
},
},
];
// --- ---
@ -158,6 +251,7 @@ const visible = ref(false); // 导入预览 Modal
//
const editModalVisible = ref(false);
const isView = ref(false);
const currentRecord = ref<FormData | null>(null);
const submitLoading = ref(false);
@ -175,7 +269,6 @@ const fileLoading = ref(false);
// Key ()
const editingKey = ref<string | number>("");
// --- ---
// Zip Blob URL
@ -235,7 +328,7 @@ const columns = computed(() => {
key: "action",
dataIndex: "action",
fixed: "right",
width: 100,
width: 200,
align: "center",
},
];
@ -256,43 +349,42 @@ const modalColumns = computed(() => {
// message.success("");
// };
return baseColumnsConfig
.map((col) => ({
...col,
customRender: ({ text, record, index }: any) => {
const editing = isEditing(record, index);
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 (col.dataIndex === "level5" || col.dataIndex === "level6") {
if (editing) {
// Input
// Input VNode slot h
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" },
// });
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" },
// });
});
// --- ---
@ -301,17 +393,21 @@ const handleAdd = () => {
currentRecord.value = null;
editModalVisible.value = true;
};
const handleEdit = (record: any) => {
//
const handleEdit = (record: any, type: string) => {
if (type == "view") {
isView.value = true;
} else {
isView.value = false;
}
currentRecord.value = { ...record };
editModalVisible.value = true;
};
//
// /
const handleDelete = (ids: any[]) => {
console.log(ids)
Modal.confirm({
title: "是否确认删除选中数据吗?",
title: "是否确认 删除 选中数据吗?",
onOk: async () => {
let res: any = await delFishDraft(ids);
if (res && res?.code == 0) {
@ -321,10 +417,118 @@ const handleDelete = (ids: any[]) => {
},
});
};
//
const batchDel = () => {
// -
const batchDelBtn = () => {
handleDelete(batchData.value);
};
///
const handleSubmit = (ids: any[]) => {
Modal.confirm({
title: "是否 提交 选中数据吗?",
onOk: async () => {
let res: any = await submitFishDraft(ids);
if (res && res?.code == 0) {
message.success("提交成功");
tableRef.value?.getList();
}
},
});
};
// -
const submitBtn = async () => {
handleSubmit(batchData.value);
};
// /
const handleSuccess = (ids: any[]) => {
let rejectReason = "";
Modal.confirm({
title: "是否确认 审批通过 选中数据?",
// 使 h
content: h("div", { style: "margin-top: 10px;" }, [
h("div", { style: "margin-bottom: 8px;" }, [
h("span", { style: "color: red;" }, "* "),
h("span", "审批原因:"),
]),
h("textarea", {
class: "ant-input", // 使 antdv 使 Antdv
placeholder: "请输入审批原因",
rows: 4,
style: {
width: "100%",
resize: "none",
border: "1px solid #d9d9d9",
padding: "8px",
borderRadius: "4px",
},
//
onInput: (e: Event) => {
rejectReason = (e.target as HTMLTextAreaElement).value;
},
}),
]),
okText: "确认审批",
cancelText: "取消",
onOk: async () => {
let res: any = await successFishDraft({ ids: ids, approveComment: rejectReason });
if (res && res?.code == 0) {
message.success("审批成功");
tableRef.value?.getList();
}
},
});
};
// -
const successBtn = async () => {
handleSuccess(batchData.value);
};
//
const handleReject = (id: any) => {
let rejectReason = "";
Modal.confirm({
title: "是否确认 驳回 选中数据?",
// 使 h
content: h("div", { style: "margin-top: 10px;" }, [
h("div", { style: "margin-bottom: 8px;" }, [
h("span", { style: "color: red;" }, "* "),
h("span", "驳回原因:"),
]),
h("textarea", {
class: "ant-input", // 使 antdv 使 Antdv
placeholder: "请输入驳回原因",
rows: 4,
style: {
width: "100%",
resize: "none",
border: "1px solid #d9d9d9",
padding: "8px",
borderRadius: "4px",
},
//
onInput: (e: Event) => {
rejectReason = (e.target as HTMLTextAreaElement).value;
},
}),
]),
okText: "确认驳回",
cancelText: "取消",
onOk: async () => {
//
if (!rejectReason || rejectReason.trim() === "") {
message.warning("请输入驳回原因");
return Promise.reject(); // Modal
}
let res: any = await rejectFishDraft({ id: id, rejectReason: rejectReason });
if (res && res?.code == 0) {
message.success("驳回成功");
tableRef.value?.getList();
}
},
});
};
//
const handleSelectionChange = (keys: any) => {
batchData.value = keys;
@ -332,17 +536,13 @@ const handleSelectionChange = (keys: any) => {
const editModalCancel = () => {
editModalVisible.value = false;
};
// /-
const handleEditSubmit = async (values: FormData) => {
submitLoading.value = true;
console.log(values);
//
// setTimeout(() => {
if (currentRecord.value) {
//
let res: any = await editFishDraft({
...values
...values,
});
if (res && res?.code == 0) {
message.success("编辑成功");
@ -350,27 +550,10 @@ const handleEditSubmit = async (values: FormData) => {
tableRef.value?.getList();
}
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("新增成功");
@ -379,152 +562,154 @@ const handleEditSubmit = async (values: FormData) => {
}
submitLoading.value = false;
}
// }, 500);
};
const parseExcelFile = async (fileName: string, arrayBuffer: ArrayBuffer) => {
try {
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;
} catch (error) {
console.error(`解析文件 ${fileName} 失败:`, error);
message.error(`文件 ${fileName} 解析失败`);
return [];
}
};
const handleModalOk = () => {
tableData.value = [...fileTableData.value];
visible.value = false;
message.success("数据已导入至列表");
Modal.confirm({
title: "是否提交导入数据?",
onOk: async () => {
// tableData.value = [...fileTableData.value];
visible.value = false;
editingKey.value = "";
message.success("数据已导入至列表");
// let res: any = await submitImportTask(ids);
// if (res && res?.code == 0) {
// message.success("");
// tableRef.value?.getList();
// }
},
});
};
const handleModalCancel = () => {
visible.value = false;
editingKey.value = "";
Modal.confirm({
title: "是否取消导入数据?",
onOk: async () => {
visible.value = false;
editingKey.value = "";
// let res: any = await cancelImportTask(ids);
// if (res && res?.code == 0) {
// message.success("");
// tableRef.value?.getList();
// }
},
});
};
const importBtn = async (file: File) => {
fileLoading.value = true;
editingKey.value = "";
const hideMessage = message.loading("正在解析压缩包...", 0);
try {
const zip = await JSZip.loadAsync(file);
const zipPathMap: Record<string, string> = {};
const handleFileSelect = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
//
zip.forEach((relativePath, zipEntry) => {
if (!zipEntry.dir) {
const lowerPath = relativePath.toLowerCase();
zipPathMap[lowerPath] = relativePath;
const pathParts = relativePath.split("/");
for (let i = 0; i < pathParts.length; i++) {
const subPath = pathParts.slice(i).join("/");
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath;
}
}
});
// (50MB)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
message.error("文件大小不能超过50MB");
resetFileInput();
return;
}
const fileNames = Object.keys(zip.files);
if (fileNames.length === 0) {
hideMessage();
message.warning("压缩包为空");
fileLoading.value = false;
return;
}
//
const isZip =
file.name.toLowerCase().endsWith(".zip") ||
file.type === "application/zip" ||
file.type === "application/x-zip-compressed";
let allExcelData: any[] = [];
for (const fileName of fileNames) {
const zipEntry = zip.files[fileName];
if (zipEntry.dir) continue;
if (!fileName.match(/\.(xls|xlsx)$/i)) continue;
if (!isZip) {
message.error("请选择.zip格式的压缩包");
resetFileInput();
return;
}
try {
const arrayBuffer = await zipEntry.async("arraybuffer");
const data = await parseExcelFile(fileName, arrayBuffer);
if (!data || data.length === 0) continue;
// props.importBtn(file);
resetFileInput();
};
const transformedData = await Promise.all(
data.map(async (item: any) => {
const newObj: any = {};
for (const excelKey in item) {
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue;
const value = item[excelKey];
//
const matchedCol = baseColumnsConfig.find(
(col) => excelKey.includes(col.title) || col.title.includes(excelKey)
);
if (matchedCol) {
let finalValue = value;
//
if (
(matchedCol.dataIndex === "level5" ||
matchedCol.dataIndex === "level6") &&
value &&
typeof value === "string"
) {
const trimPath = value.trim().replace(/\\/g, "/");
if (trimPath) {
const searchKey = trimPath.toLowerCase();
const realPath = zipPathMap[searchKey];
if (realPath) {
try {
const zipFile = zip.file(realPath);
if (zipFile) {
const blob = await zipFile.async("blob");
finalValue = URL.createObjectURL(blob);
}
} catch (e) {
console.error(`Failed to extract blob for: ${realPath}`, e);
}
}
}
}
newObj[matchedCol.dataIndex] = finalValue;
}
}
return newObj;
})
);
allExcelData = [...allExcelData, ...transformedData];
} catch (err) {
console.error(`读取文件 ${fileName} 失败`, err);
}
}
fileTableData.value = allExcelData;
visible.value = true;
hideMessage();
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`);
} catch (error) {
hideMessage();
console.error("ZIP 解析失败:", error);
message.error("文件格式错误或解析失败");
} finally {
fileLoading.value = false;
const resetFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.value = "";
}
};
const importBtn = async (file: File) => {
let res: any = await checkImportStatus();
console.log(res)
// fileInputRef.value?.click();
// fileLoading.value = true;
// editingKey.value = "";
const saveBtn = async () => {
// TODO:
console.log("Save button clicked");
// try {
// const formData = new FormData();
// formData.append("file", file);
// let res: any = await importFishZip(formData);
// console.log(res);
// visible.value = true;
// } catch (error) {
// } finally {
// // fileLoading.value = false;
// }
};
const handleReset = (values) => {
handleSearchFinish(values);
};
// -
const handleSearchFinish = (values: any) => {
console.log(values);
// const newSearchData = { ...searchData.value, ...e };
// searchData.value = newSearchData;
// getData(newSearchData, label);
const filters = [
values.ftp && {
field: "ftp",
operator: "eq",
dataType: "string",
value: values.ftp,
},
{
field: "TM",
operator: "gte",
dataType: "date",
value: values.strdt[0],
},
{
field: "TM",
operator: "lte",
dataType: "date",
value: values.strdt[1],
},
values.direction && {
field: "direction",
operator: "eq",
dataType: "string",
value: values.direction,
},
values.status && {
field: "status",
operator: "eq",
dataType: "string",
value: values.status,
},
values.stcd && {
field: "stcd",
operator: "eq",
dataType: "string",
value: values.stcd,
},
values.rstcd && {
field: "rstcd",
operator: "eq",
dataType: "string",
value: values.rstcd,
},
values.baseId !== "all" && {
field: "baseId",
operator: "eq",
dataType: "string",
value: values.baseId,
},
].filter(Boolean);
const filter = {
logic: "and",
filters: filters,
};
tableRef.value?.getList(filter);
};
const closeVideoPreview = () => {
@ -533,7 +718,14 @@ const closeVideoPreview = () => {
};
// --- ---
onMounted(() => {});
onMounted(() => {
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
direction.value = res.data;
});
getDictItemsByCode({ dictCode: "guoyuStatus" }).then((res) => {
guoyuStatus.value = res.data;
});
});
</script>
<style lang="scss" scoped>

View File

@ -1,10 +1,11 @@
<template>
<a-modal
:title="isEdit ? '编辑数据' : '新增数据'"
:title="isView ? '查看数据' : isEdit ? '编辑数据' : '新增数据'"
v-model:open="modalVisible"
:confirm-loading="loading"
width="800px"
:destroy-on-close="true"
:footer="isView ? null : undefined"
@cancel="handleCancel"
@ok="handleOk"
>
@ -17,21 +18,58 @@
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="水电基地" name="engName">
<a-input v-model:value="formData.engName" placeholder="请输入水电基地" />
<a-form-item label="水电基地" name="baseId">
<a-select
v-model:value="formData.baseId"
:loading="baseLoading"
placeholder="请选择水电基地"
:disabled="isView"
@change="baseChange"
>
<a-select-option
v-for="opt in baseOption"
:key="opt.baseid"
:value="opt.baseid"
>
{{ opt.basename }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="电站名称" name="baseName">
<a-input v-model:value="formData.baseName" placeholder="请输入电站名称" />
<a-form-item label="电站名称" name="rstcd">
<a-select
v-model:value="formData.rstcd"
:loading="engLoading"
placeholder="请选择电站名称"
:disabled="isView"
@change="engChange"
>
<a-select-option v-for="opt in engOption" :key="opt.stcd" :value="opt.stcd">
{{ opt.ennm }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="过鱼设施名称" name="fpname">
<a-input v-model:value="formData.fpname" placeholder="请输入过鱼设施名称" />
<a-form-item label="过鱼设施" name="stcd">
<a-select
v-model:value="formData.stcd"
:loading="fpssLoading"
placeholder="请选择过鱼设施"
:disabled="isView"
>
<a-select-option
v-for="opt in fpssOption"
:key="opt.stcd"
:value="opt.stcd"
>
{{ opt.stnm }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
@ -42,6 +80,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -50,12 +89,12 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="鱼种类" name="ftp">
<a-input v-model:value="formData.ftp" placeholder="请输入鱼种类" />
<fishSearch v-model="formData.ftp" :disabled="isView" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="是否鱼苗" name="isfs">
<a-radio-group v-model:value="formData.isfs">
<a-radio-group v-model:value="formData.isfs" :disabled="isView">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
@ -70,11 +109,14 @@
v-model:value="formData.direction"
placeholder="请选择游向"
allow-clear
:disabled="isView"
>
<a-select-option value="上行">上行</a-select-option>
<a-select-option value="下行">下行</a-select-option>
<a-select-option value="上行折返">上行折返</a-select-option>
<a-select-option value="下行折返">下行折返</a-select-option>
<a-select-option
v-for="item in direction"
:key="item.itemCode"
:value="item.itemCode"
>{{ item.dictName }}</a-select-option
>
</a-select>
</a-form-item>
</a-col>
@ -85,6 +127,7 @@
style="width: 100%"
placeholder="数量"
:min="0"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -104,6 +147,7 @@
placeholder="请输入"
:min="0"
@change="validateBodyLength"
:disabled="isView"
/>
<span class="px-[10px]">~</span>
<a-input-number
@ -112,6 +156,7 @@
placeholder="请输入"
:min="0"
@change="validateBodyLength"
:disabled="isView"
/>
</div>
</a-form-item>
@ -129,6 +174,7 @@
placeholder="请输入"
:min="0"
@change="validateWeight"
:disabled="isView"
/>
<span class="px-[10px]">~</span>
<a-input-number
@ -137,6 +183,7 @@
placeholder="请输入"
:min="0"
@change="validateWeight"
:disabled="isView"
/>
</div>
</a-form-item>
@ -150,6 +197,7 @@
style="width: 100%"
placeholder="水温"
:min="0"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -172,21 +220,77 @@
<script lang="ts" setup>
import { ref, reactive, watch, computed } from "vue";
import dayjs from "dayjs";
import { message } from "ant-design-vue";
import type { Rule } from "ant-design-vue/es/form";
import fishSearch from "@/components/fishSearch/index.vue";
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
// Props
interface Props {
visible: boolean;
direction: any[];
initialValues?: any | null;
loading?: boolean;
isView?: boolean;
}
const baseLoading = ref(false);
const engLoading = ref(false);
const fpssLoading = ref(false);
const baseOption = ref<any[]>([]);
const engOption = ref<any[]>([]);
const fpssOption = ref<any[]>([]);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false,
});
// 1.
const getBaseDropdownSelect = async () => {
try {
baseLoading.value = true;
const res = await getBaseDropdown({});
baseOption.value = res.data;
} catch (error) {
console.error("获取水电基地列表失败:", error);
} finally {
baseLoading.value = false;
}
};
const baseChange = async (baseId: string) => {
formData.rstcd = undefined;
formData.stcd = undefined;
await getEngInfoDropdownSelect(baseId);
await getFpssDropdownSelect(formData.rstcd, baseId);
};
const getEngInfoDropdownSelect = async (baseId: string) => {
try {
engLoading.value = true;
const res = await getEngInfoDropdown({ baseId });
engOption.value = res.data;
} catch (error) {
console.error("获取电站列表失败", error);
} finally {
engLoading.value = false;
}
};
const engChange = async (rstcd: string) => {
formData.stcd = undefined;
await getFpssDropdownSelect(rstcd, formData.baseId);
};
const getFpssDropdownSelect = async (rstcd: string, baseId: string) => {
try {
fpssLoading.value = true;
const res = await getFpssDropdown({ rstcd, baseId });
fpssOption.value = res.data;
} catch (error) {
console.error("获取流量列表失败", error);
} finally {
fpssLoading.value = false;
}
};
// --- v-model ---
const modalVisible = computed({
get: () => props.visible,
@ -208,10 +312,9 @@ const weightError = ref<string>("");
//
const defaultFormData = reactive({
id: undefined,
engName: undefined,
baseName: undefined,
fpname: undefined,
baseId: undefined,
stcd: undefined,
rstcd: undefined,
strdt: undefined,
ftp: undefined,
isfs: 0,
@ -229,41 +332,15 @@ const defaultFormData = reactive({
});
const formData: any = reactive({ ...defaultFormData });
//
// const validateBodyLengthRange = (rule: any, value: any) => {
// const min = formData.bodyLengthMin;
// const max = formData.bodyLengthMax;
// //
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
// if (Number(min) >= Number(max)) {
// return Promise.reject("");
// }
// }
// return Promise.resolve();
// };
// const validateWeightRange = (rule: any, value: any) => {
// const min = formData.weightMin;
// const max = formData.weightMax;
// //
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
// if (Number(min) >= Number(max)) {
// return Promise.reject("");
// }
// }
// };
//
const rules: Record<string, Rule[]> = {
// engName: [{ required: true, message: "", trigger: "blur" }],
// baseName: [{ required: true, message: "", trigger: "blur" }],
// fpname: [{ required: true, message: "", trigger: "blur" }],
// baseId: [{ required: true, message: "", trigger: "change" }],
// rstcd: [{ required: true, message: "", trigger: "change" }],
// stcd: [{ required: true, message: "", trigger: "change" }],
// strdt: [{ required: true, message: "", trigger: "change" }],
// //
// bodyLengthMin: [{ validator: validateBodyLengthRange, trigger: "change" }],
// bodyLengthMax: [{ validator: validateBodyLengthRange, trigger: "change" }],
// weightMin: [{ validator: validateWeightRange, trigger: "change" }],
// weightMax: [{ validator: validateWeightRange, trigger: "change" }],
// ftp: [{ required: true, message: "", trigger: "change" }],
// direction: [{ required: true, message: "", trigger: "change" }],
// fcnt: [{ required: true, message: "", trigger: "change" }],
};
//
@ -276,14 +353,25 @@ const validateBodyLength = () => {
//
bodyLengthError.value = "";
//
if (min !== undefined && min !== null && max !== undefined && max !== null) {
if (Number(min) >= Number(max)) {
bodyLengthError.value = "最小体长必须小于最大体长";
// handleOk
// ( undefined, null, )
const hasMin = min !== undefined && min !== null && min !== "";
const hasMax = max !== undefined && max !== null && max !== "";
// 1.
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
bodyLengthError.value = "最小体长和最大体长均需填写";
return false;
}
// 2.
if (hasMin && hasMax) {
// min > max
if (Number(min) > Number(max)) {
bodyLengthError.value = "最小体长不能大于最大体长";
return false;
}
}
return true;
};
@ -294,12 +382,25 @@ const validateWeight = () => {
//
weightError.value = "";
if (min !== undefined && min !== null && max !== undefined && max !== null) {
if (Number(min) >= Number(max)) {
weightError.value = "最小体重必须小于最大体重";
//
const hasMin = min !== undefined && min !== null && min !== "";
const hasMax = max !== undefined && max !== null && max !== "";
// 1.
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
weightError.value = "最小体重和最大体重均需填写";
return false;
}
// 2.
if (hasMin && hasMax) {
// min > max
if (Number(min) > Number(max)) {
weightError.value = "最小体重不能大于最大体重";
return false;
}
}
return true;
};
@ -312,8 +413,13 @@ const initForm = () => {
//
if (values.fwet) {
const weights = values.fwet.split("~");
formData.weightMin = weights[0];
formData.weightMax = weights[1];
if (weights.length === 2) {
formData.weightMin = weights[0];
formData.weightMax = weights[1];
} else {
formData.weightMin = weights[0];
formData.weightMax = weights[0];
}
} else {
formData.weightMin = undefined;
formData.weightMax = undefined;
@ -321,8 +427,13 @@ const initForm = () => {
if (values.fsz) {
const sizes = values.fsz.split("~");
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[1];
if (sizes.length === 2) {
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[1];
} else {
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[0];
}
} else {
formData.bodyLengthMin = undefined;
formData.bodyLengthMax = undefined;
@ -356,6 +467,9 @@ watch(
(newVisible) => {
if (newVisible) {
//
getBaseDropdownSelect();
getEngInfoDropdownSelect(formData.baseId);
getFpssDropdownSelect(formData.rstcd, formData.baseId);
initForm();
} else {
//
@ -378,8 +492,8 @@ const resetForm = () => {
}
Object.assign(formData, defaultFormData);
//
bodyLengthError.value = '';
weightError.value = '';
bodyLengthError.value = "";
weightError.value = "";
};
//
@ -402,13 +516,41 @@ const handleOk = async () => {
}
//
await formRef.value.validate();
let fwet = "";
if (
formData.weightMin == formData.weightMax &&
formData.weightMin != undefined &&
formData.weightMax != undefined
) {
fwet = formData.weightMin;
} else if (formData.weightMin == undefined && formData.weightMax == undefined) {
fwet = "-";
} else {
fwet = formData.weightMin + "~" + formData.weightMax;
}
let fsz = "";
if (
formData.bodyLengthMin == formData.bodyLengthMax &&
formData.bodyLengthMin != undefined &&
formData.bodyLengthMax != undefined
) {
fsz = formData.bodyLengthMin;
} else if (
formData.bodyLengthMin == undefined &&
formData.bodyLengthMax == undefined
) {
fsz = "-";
} else {
fsz = formData.bodyLengthMin + "~" + formData.bodyLengthMax;
}
//
const submitValues = {
...formData,
fwet: formData.weightMin + "~" + formData.weightMax,
fsz: formData.bodyLengthMin + "~" + formData.bodyLengthMax,
fwet: fwet,
fsz: fsz,
};
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss")
console.log(submitValues);
emit("ok", submitValues);
} catch (error) {
console.error("Validate Failed:", error);

View File

@ -1,52 +1,38 @@
<template>
<div class="guoYuSheShiShuJuTianBao-search">
<!-- 隐藏的文件输入框 -->
<input
ref="fileInputRef"
type="file"
accept=".zip,application/zip,application/x-zip-compressed"
style="display: none"
@change="handleFileSelect"
/>
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #typeDate="{ onChange }">
<fishSearch
v-model="localTypeDate"
width="280px"
:options="options"
@update:modelValue="onChange"
/>
<template #ftp="{ onChange }">
<fishSearch v-model="localTypeDate" width="280px" @update:modelValue="onChange" />
</template>
<!-- 自定义重置及操作按钮区域 -->
<template #actions>
<a-tooltip title="新增">
<a-button @click="props.handleAdd"> 新增 </a-button>
</a-tooltip>
<a-tooltip title="导入zip">
<a-button v-hasPerm="['sjtb:import-zip']" @click="triggerFileInput">
<a-button v-hasPerm="['sjtb:import-zip']" @click="props.importBtn">
导入zip
</a-button>
</a-tooltip>
<a-button @click="props.batchDel" :disabled="batchData.length === 0">
<a-button @click="props.batchDelBtn" :disabled="batchData.length === 0">
批量删除
</a-button>
<a-tooltip title="提交数据">
<a-button @click="props.saveBtn">
<a-button @click="props.submitBtn" :disabled="batchData.length === 0">
<template #icon><SaveOutlined /></template>
提交数据
</a-button>
</a-tooltip>
<a-tooltip title="批量审批">
<a-button @click="props.saveBtn">
<a-button @click="props.successBtn" :disabled="batchData.length === 0">
<template #icon><CheckSquareOutlined /></template>
批量审批
</a-button>
@ -77,224 +63,43 @@ import BasicSearch from "@/components/BasicSearch/index.vue"; // 确保路径正
import { DateSetting } from "@/utils/enumeration";
import { checkPerm } from "@/directive/permission";
import fishSearch from "@/components/fishSearch/index.vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
// --- Props & Emits ---
interface Props {
direction: any[];
guoyuStatus: any[];
importBtn: (file: File) => void;
batchDel: () => void;
saveBtn: () => void;
batchDelBtn: () => void;
submitBtn: () => void;
successBtn: () => void;
batchData: any[];
handleAdd: () => void;
}
const shuJuTianBaoStore = useShuJuTianBaoStore();
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "searchFinish", values: any, label: string): void;
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
const localTypeDate = ref<string[]>([]);
const localTypeDate = ref<string>(null);
// --- State ---
const basicSearchRef = ref<any>(); // 1. ref
const fileInputRef = ref<HTMLInputElement>();
const options = ref<any>([
{
_tls: {},
id: "00DDF2A72147B2115384F64DDFE26A5E",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 1,
name: "异唇裂腹鱼",
code: null,
nameEn: null,
alias: null,
description: null,
logo: null,
introduce: null,
inffile: null,
genus: null,
family: null,
species: null,
fsz: null,
type: 1,
typeName: "淡水",
rare: null,
specOrigin: null,
specOriginName: null,
ptype: null,
ptypeName: null,
rvcd: "null",
rvcdName: "",
zyFishId: "00DDF2A72147B2115384F64DDFE26A5E",
habitMigrat: null,
feedingHabit: null,
spawnCharact: null,
spawnMonth: null,
food: null,
timeFeed: null,
orignDate: null,
pretemp: null,
flowRate: null,
depth: null,
botmMater: null,
wqtq: null,
habitat: null,
situation: null,
resourceType: null,
shapedesc: null,
protectlvl: null,
habitation: null,
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
{
_tls: {},
id: "0249006974f34c288d6cb4df54e3b19d",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 2,
name: "匙吻鲟",
code: null,
nameEn: "Polyodon spathula",
alias: "美国匙吻鲟、鸭嘴鲟",
description:
"匙吻鲟(Polyodonspathula)亦称匙吻猫鱼(spoonbillcat)。产于北美洲的原始鱼,为桨吻鲟(paddlefish)的一种。属鲟形目、匙吻鲟科是北美洲的一种名贵大型淡水经济鱼类。匙吻鲟的显著特点是吻呈扁平桨状特别长。鱼的体表光滑无鳞背部黑蓝灰色有一些斑点在其间体侧有点状赭色腹部白色。个体大这种大型淡水鱼可以长到220厘米重达90公斤以上。",
logo: "20240527221754634033127655455265",
introduce: null,
inffile:
"20240527221811830658320352201158,20240527221805865127213075311524,20240527221822527347221377607671,20240527221828072460253583084314,20240527221800311481326028334838,20240527221817630761245563388673",
genus: "匙吻鲟属",
family: "匙吻鲟科",
species: "匙吻鲟",
fsz: "85~220",
type: 1,
typeName: "淡水",
rare: null,
specOrigin: 2,
specOriginName: "外来鱼类",
ptype: 4,
ptypeName: "易危",
rvcd: "SJLY148",
rvcdName: "大渡河",
zyFishId: "0249006974f34c288d6cb4df54e3b19d",
habitMigrat: "繁殖洄游",
feedingHabit: "肉食性",
spawnCharact: "粘性卵类型",
spawnMonth: "4-5",
food: "主要以浮游动物,也以甲壳类和双壳类生物为食",
timeFeed: "夜间觅食",
orignDate:
"匙吻鲟在美国密西西比河流域的22个洲均有发现。包括密苏里河到蒙大拿州俄亥俄河和它的主要支流流域。雄鱼在79龄达到性成熟雌鱼晚一年相对怀卵量约为每克体重3.5粒。匙吻鲟多在45月繁殖适宜水温为1618℃繁殖期会游到江河上游产卵受精卵灰黑色直径22.5毫米有黏性往往粘在砾石上孵化孵化期67天。",
pretemp: "037℃",
flowRate: "0.3m/s",
depth: "2~2.5",
botmMater: "泥质",
wqtq: "适宜的pH范围为6.58对溶解氧要求较高应在5毫克/升以上。",
habitat: null,
situation: null,
resourceType: null,
shapedesc:
"匙吻鲟有一个形如匙柄的长吻长约为体长的三分之一。身体流线型体表光滑无鳞。眼小口较大位于吻末端的腹面不能伸缩上颌背面具有粗糙的颗粒感觉器。鳃盖骨大而向后延伸鳃盖膜长达胸鳍至腹鳍的1/2处。头部有一喷水孔和喷水腔。胸鳍较小下位腹鳍腹位背鳍起点在腹鳍之后。尾鳍分叉歪尾型上叶长于下叶尾柄披有梗栉状的甲鳞。背部黑蓝灰色常有一些斑点间于其中两侧逐渐变浅体侧有点状褐色腹部白色。",
protectlvl: null,
habitation: "缓流型;广温性;中上层水域",
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
{
_tls: {},
id: "02A23B169BF240589B2C37C5E81A8DC2",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 3,
name: "南方马口鱼",
code: null,
nameEn: "Chinese hooksnout carp",
alias:
"午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公",
description:
"南方马口鱼Opsariichthys uncirostris bidens Gunther1873是鲤科马口鱼属的一种生活的溪流中的小型鱼类。体长稍侧扁腹部圆。头稍尖头长大于体高。吻钝吻长远比其宽为大。口特大下颌前端突起两侧面各有一凹陷恰与上颌突出部分吻合。下咽齿3行。鳞圆形背鳍条27无硬刺。臀鳍条38-10。背部黑灰色体侧下半部及腹面银白色喉部、口唇及各鳍橙黄背鳍上有黑色的小斑点眼上部有一红色斑点体两侧具有浅蓝色的垂直条纹。生殖季节时雄鱼体色更为鲜艳。",
logo: "20240527192500111683624865306342",
introduce: null,
inffile:
"20240527192505300717052825727341,20240527192533035616525871580354,20240527192510217883087850433201,20240527192516128514164206355182,20240527192522835236402141341053,20240527192527583177528213025212",
genus: "马口鱼属",
family: "鲤科",
species: "南方马口鱼",
fsz: "720",
type: 1,
typeName: "淡水",
rare: null,
specOrigin: 1,
specOriginName: "本土",
ptype: 4,
ptypeName: "易危",
rvcd: "null",
rvcdName: "",
zyFishId: "02A23B169BF240589B2C37C5E81A8DC2",
habitMigrat: "定居型",
feedingHabit: "肉食性",
spawnCharact: "沉性卵类型",
spawnMonth: "6-8",
food: "摄食小型鱼类和水生昆虫。",
timeFeed: "白天觅食",
orignDate:
"产卵期在68月份。第一年生长较迅速可达711厘米。1龄鱼即有繁殖能力系小型鱼类。",
pretemp: "030℃",
flowRate: "0.3m/s",
depth: "1~1.5",
botmMater: "砂砾底质",
wqtq: "pH在7.27.8之内,凉爽清洁、溶氧丰富的水质",
habitat: null,
situation: null,
resourceType: null,
shapedesc:
"背鳍条37臀鳍条39侧线鳞4547下咽齿3行1·4·54·4·1。鳃耙外侧10脊椎骨35。体长为体高的3.14.3倍为头长的3.53.9倍为尾柄长的4.75.2倍为尾柄高的10.211.3倍。头长为吻长的2.73.2倍为眼径的5.06.2倍为眼间距的3.13.3倍。体延长侧扁。吻长其长略大于宽口大端位口裂向上倾斜下颌后端延长达眼前缘其前缘凸起两侧凹陷恰与上颌前端和两侧嵌合。眼中等大位于头侧上方。鳃耙短小而稀疏。下咽齿圆柱性顶端尖而长。侧线完全前端弯向体侧腹方后端向上延至尾柄正中。背鳍无硬刺其起点至吻端稍大于至尾鳍基部的距离胸鳍不达腹鳍其末端可达胸、腹鳍间距的3/5处。腹鳍外缘略钝圆起点约与背鳍不分支鳍条相对。鳔2视后室约为前室的2倍腹腔膜银白色。体背部灰黑色腹部银白色体侧有浅蓝色的垂直条纹胸鳍、腹鳍和臀鳍为橙黄色。雄鱼在生殖期出现婚装头部、吻部和臀部有显眼的珠星臀鳍的第14根分支鳍条特别延长全身具有很鲜艳的婚姻色。",
protectlvl: null,
habitation: "流水型;冷水性;中上水层",
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
]);
// initSearchData
const initSearchData = {
dmStcd: "008660306300000079", // ID
stcd: {
dataDimensionData: "all",
dataDimensionType: "hyBase",
hbrvcd: "",
stcdId: "",
},
mway: "1",
typeDate: [],
baseId: "all",
stcd: null,
rstcd: null,
ftp: null,
status: null,
direction: null,
strdt: [
dayjs().startOf("month").format("YYYY-MM-DD"),
dayjs().endOf("day").format("YYYY-MM-DD"),
dayjs().startOf("month").format("YYYY-MM-DD HH:mm:ss"),
dayjs().endOf("day").format("YYYY-MM-DD HH:mm:ss"),
],
};
@ -305,7 +110,7 @@ const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "waterStation",
name: "engName",
name: "baseId",
label: "水电基地",
fieldProps: {
allowClear: true,
@ -314,24 +119,20 @@ const searchList: any = computed(() => [
},
{
type: "Select",
name: "fpname",
name: "stcd",
label: "过鱼设施",
values: { name: "stnm", value: "rstcd" },
fieldProps: {
allowClear: true,
},
options: [],
options: shuJuTianBaoStore.fpssOption,
},
{
type: "Select",
name: "direction",
label: "游向",
width: 120,
options: [
{ label: "上行", value: "上行" },
{ label: "下行", value: "下行" },
{ label: "上行折返", value: "上行折返" },
{ label: "下行折返", value: "下行折返" },
],
options: props.direction,
fieldProps: {
allowClear: true,
},
@ -352,10 +153,7 @@ const searchList: any = computed(() => [
fieldProps: {
allowClear: true,
},
options: [
{ label: "正常", value: "01" },
{ label: "异常", value: "02" },
],
options: props.guoyuStatus,
},
{
@ -374,43 +172,6 @@ const searchList: any = computed(() => [
},
]);
// --- Methods ---
// 1.
const handleFileSelect = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
// (50MB)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
message.error("文件大小不能超过50MB");
resetFileInput();
return;
}
//
const isZip =
file.name.toLowerCase().endsWith(".zip") ||
file.type === "application/zip" ||
file.type === "application/x-zip-compressed";
if (!isZip) {
message.error("请选择.zip格式的压缩包");
resetFileInput();
return;
}
props.importBtn(file);
resetFileInput();
};
const resetFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.value = "";
}
};
const triggerFileInput = () => {
fileInputRef.value?.click();
};
@ -421,52 +182,46 @@ const onSearchFinish = (values: any) => {
// label dmStcd
// React label options.find(...)
// ID
const label = "默认水温站"; // TODO: values.dmStcd
// 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, label);
emit("searchFinish", values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
// searchData便使
console.log(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
if (changedValues.strdt) {
//
console.log("Time changed:", changedValues.strdt);
// searchData便使
if (changedValues.rstcd || changedValues.baseId) {
shuJuTianBaoStore.getFpssOption(
allValues.baseId == "all" ? "" : allValues.baseId,
allValues.rstcd
);
const formInstance = basicSearchRef.value?.formData;
formInstance.stcd = null;
}
};
// const handleReset = (form: any) => {
// //
// if (form) {
// form.resetFields();
// }
// //
// nextTick(() => {
// if (form) {
// form.setFieldsValue(initSearchData);
// }
// //
// emit("searchFinish", initSearchData, "");
// });
// };
const handleReset = () => {
localTypeDate.value = null;
emit("reset", searchData.value);
};
watch(
() => initSearchData.typeDate,
() => initSearchData.ftp,
(newVal) => {
localTypeDate.value = newVal || [];
localTypeDate.value = newVal || "";
},
{ immediate: true }
);
// --- Lifecycle ---
onMounted(() => {
//
emit("searchFinish", initSearchData, "两河口出库水温站");
emit("searchFinish", initSearchData);
shuJuTianBaoStore.getFpssOption("", "");
});
</script>
<style lang="scss"></style>