Merge branch 'wxk-main'
This commit is contained in:
commit
617bf8edaa
@ -114,6 +114,24 @@ export function getFlowDataMonthList(data) {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 生态流量运行数据 - 删除
|
||||
export function deleteFlowDataInfo(data) {
|
||||
return request({
|
||||
url: '/eq/data/removeKendoByIds',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 生态流量运行数据 - 修改
|
||||
export function updateEqRsData(data) {
|
||||
return request({
|
||||
url: '/eq/data/updateEqRsData',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 水质监测数据 - 断面
|
||||
export function getWaterDataSectionList(data) {
|
||||
return request({
|
||||
@ -331,6 +349,36 @@ export function deleteFpssrlRInfo(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 过鱼设施AI自动数据 - 分页查询
|
||||
export function getFpssrlAiRPageList(data) {
|
||||
return request({
|
||||
url: '/fp/query/qgc/GetKendoListCustAI',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 过鱼设施AI自动数据 - 编辑
|
||||
export function updateFpssrlAiRInfo(data) {
|
||||
return request({
|
||||
url: '/base/fpssrlAiR/update',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 过鱼设施AI自动数据 - 删除
|
||||
export function deleteFpssrlAiRInfo(data) {
|
||||
return request({
|
||||
url: '/base/fpssrlAiR/delete',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 鱼类 放鱼
|
||||
export function getFishReleaseList(data) {
|
||||
return request({
|
||||
|
||||
@ -54,7 +54,7 @@ interface Props {
|
||||
data?: any[];
|
||||
enableRowSelection?: boolean;
|
||||
enableRowHighlight?: boolean; // 是否启用行点击高亮
|
||||
rowKey?: string;
|
||||
rowKey?: string | ((record: any) => string | number);
|
||||
searchParams?: Record<string, any>;
|
||||
defaultPageSize?: number;
|
||||
isPage?: boolean;
|
||||
@ -663,7 +663,20 @@ const enhancedColumns = computed(() => {
|
||||
title: String(text)
|
||||
},
|
||||
{
|
||||
default: () => String(text)
|
||||
default: () =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
},
|
||||
String(text)
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -210,7 +210,7 @@
|
||||
<!-- 图片上传 -->
|
||||
<a-row :gutter="16" class="mt-4">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="图片" name="firstimgurl" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
|
||||
<a-form-item label="图片" name="firstimgurl" :label-col="{ span: 4 }" :wrapper-col="{ span: 21 }">
|
||||
<a-upload
|
||||
v-model:file-list="picFileList"
|
||||
:multiple="false"
|
||||
@ -230,7 +230,7 @@
|
||||
<!-- 视频上传 -->
|
||||
<a-row :gutter="16" class="mt-4">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="视频" name="videourl" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
|
||||
<a-form-item label="视频" name="videourl" :label-col="{ span: 4 }" :wrapper-col="{ span: 21 }">
|
||||
<a-upload
|
||||
v-model:file-list="vdpFileList"
|
||||
:multiple="false"
|
||||
|
||||
@ -1,456 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
mway="1"
|
||||
:exportLoading="exportLoading"
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@add="handleAdd"
|
||||
@seeEdit="handleSeeEdit"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 图片预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
title="图片预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 视频预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="videoPreviewVisible"
|
||||
title="视频预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<video
|
||||
:src="previewVideoUrl"
|
||||
controls
|
||||
style="width: 100%; height: 600px"
|
||||
></video>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 编辑/新增 Modal -->
|
||||
<EditFishDataManualModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
:is-add="isAdd"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteFpssFn"
|
||||
:label="'过鱼人工数据'"
|
||||
title="删除过鱼人工数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FPSS_R'" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseMonitorList, deleteFpssRInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
import EditFishDataManualModal from './EditFishDataManualModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
|
||||
const sort = ref<any>([
|
||||
{
|
||||
field: 'baseStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rvcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rstcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'ttpwr',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'desc'
|
||||
}
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 编辑/新增相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
// 视频预览
|
||||
const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
// 人工列
|
||||
const tableColumns = ref<any[]>([
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'strdt',
|
||||
title: '开始时间',
|
||||
dataIndex: 'strdt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'enddt',
|
||||
title: '结束时间',
|
||||
dataIndex: 'enddt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '填报时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fsz',
|
||||
title: '鱼类规格',
|
||||
dataIndex: 'fsz',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '图片',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
if (exportLoading.value) return;
|
||||
|
||||
exportLoading.value = true;
|
||||
searchRef.value.btnLoading = true;
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_人工_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
? {
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
if (values.stcd) {
|
||||
filters.push({
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isAdd.value = true;
|
||||
editRecord.value = null;
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
isAdd.value = false;
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteFpssFn = async (record: any, reason: string) => {
|
||||
return deleteFpssRInfo({
|
||||
ids: [record.id],
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@ -57,8 +57,10 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button v-if="props.mway === '1'" type="primary" @click="handleAdd">新增</a-button>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button danger :disabled="!selectedCount" @click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -77,8 +79,8 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
@ -86,13 +88,13 @@ const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
mway: string;
|
||||
selectedCount?: number;
|
||||
}>();
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
rstcd: null,
|
||||
stcd: '0086601020VP000006',
|
||||
mway: props.mway
|
||||
mway: '1'
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
@ -140,6 +142,22 @@ const searchList: any = computed(() => [
|
||||
value: item.stcd
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'Radio',
|
||||
name: 'mway',
|
||||
label: '数据类型',
|
||||
width: 140,
|
||||
options: [
|
||||
{
|
||||
label: '人工',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: '自动',
|
||||
value: '2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'jcdt',
|
||||
@ -157,15 +175,13 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const initCrossSectionList = (params, setDefault = false) => {
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [];
|
||||
if (params) {
|
||||
if (params.rvcd != '' && params.rvcd != 'all') {
|
||||
@ -185,13 +201,6 @@ const initCrossSectionList = (params, setDefault = false) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
// 根据人工/自动站过滤过鱼设施
|
||||
filters.push({
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: props.mway
|
||||
});
|
||||
crossSectionListLoading.value = true;
|
||||
getFishReleaseMonitorSectionList({
|
||||
filter: {
|
||||
@ -202,15 +211,6 @@ const initCrossSectionList = (params, setDefault = false) => {
|
||||
}).then(res => {
|
||||
if (res.data?.data) {
|
||||
crossSectionList.value = res.data?.data;
|
||||
// 首次加载时默认选中第一条过鱼设施
|
||||
if (setDefault && crossSectionList.value.length > 0) {
|
||||
const firstStcd = crossSectionList.value[0].stcd;
|
||||
initSearchData.stcd = firstStcd;
|
||||
if (basicSearchRef.value?.formData) {
|
||||
basicSearchRef.value.formData.stcd = firstStcd;
|
||||
}
|
||||
handleSearchFinish({ ...initSearchData, stcd: firstStcd });
|
||||
}
|
||||
}
|
||||
crossSectionListLoading.value = false;
|
||||
});
|
||||
@ -261,8 +261,9 @@ onMounted(() => {
|
||||
min: `${now.subtract(5, 'year').format('YYYY-MM-DD')} 00:00:00`,
|
||||
max: `${now.format('YYYY-MM-DD')} 23:59:59`
|
||||
};
|
||||
// 首次加载:请求设施列表后默认选中第一条并触发搜索
|
||||
initCrossSectionList(undefined, true);
|
||||
initCrossSectionList();
|
||||
// 使用 handleSearchFinish 确保包含 jcdt 等完整参数
|
||||
handleSearchFinish({ ...initSearchData });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,22 +1,697 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col pl-[20px] body_one">
|
||||
<a-tabs v-model:activeKey="activeTab" class="fish-tabs">
|
||||
<a-tab-pane key="manual" tab="人工站点">
|
||||
<FishDataManual />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="auto" tab="自动站点">
|
||||
<FishDataAuto />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 图片预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
title="图片预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 视频预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="videoPreviewVisible"
|
||||
title="视频预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<video
|
||||
:src="previewVideoUrl"
|
||||
controls
|
||||
style="width: 100%; height: 600px"
|
||||
></video>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 编辑 Modal -->
|
||||
<EditFishDataManualModal
|
||||
v-if="isManual"
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
<EditFishDataAutoModal
|
||||
v-else
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteFn"
|
||||
:label="deleteLabel"
|
||||
:title="'删除' + deleteLabel"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import FishDataManual from './FishDataManual.vue';
|
||||
import FishDataAuto from './FishDataAuto.vue';
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import EditFishDataManualModal from './EditFishDataManualModal.vue';
|
||||
import EditFishDataAutoModal from './EditFishDataAutoModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import { getFishReleaseMonitorList, deleteFpssRInfo, deleteFpssrlRInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const activeTab = ref('manual');
|
||||
const sort = ref<any>([
|
||||
{
|
||||
field: 'baseStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rvcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rstcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'ttpwr',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'desc'
|
||||
}
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 编辑/删除相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const isManual = computed(() => currentSearchParams.value.mway !== '2');
|
||||
const deleteLabel = computed(() =>
|
||||
isManual.value ? '过鱼人工数据' : '过鱼自动数据'
|
||||
);
|
||||
const operationTableName = computed(() =>
|
||||
isManual.value ? 'SD_FPSS_R' : 'SD_FPSS_A'
|
||||
);
|
||||
|
||||
// 多选相关
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
// 视频预览
|
||||
const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
// 人工列
|
||||
const columnsManual: any[] = [
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'strdt',
|
||||
title: '开始时间',
|
||||
dataIndex: 'strdt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'enddt',
|
||||
title: '结束时间',
|
||||
dataIndex: 'enddt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '填报时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fsz',
|
||||
title: '鱼类规格',
|
||||
dataIndex: 'fsz',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '图片',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
];
|
||||
|
||||
// 自动列
|
||||
const columnsAuto: any[] = [
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'ennm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'length',
|
||||
title: '鱼长度(cm)',
|
||||
dataIndex: 'length',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'width',
|
||||
title: '鱼宽度(cm)',
|
||||
dataIndex: 'width',
|
||||
sort: true,
|
||||
width: 120,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fishspeed',
|
||||
title: '鱼速度(m/s)',
|
||||
dataIndex: 'fishspeed',
|
||||
sort: true,
|
||||
width: 80,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
title: '水温(℃)',
|
||||
dataIndex: 'temperature',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'speed',
|
||||
title: '流速(m/s)',
|
||||
dataIndex: 'speed',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'channelno',
|
||||
title: '过鱼通道',
|
||||
dataIndex: 'channelno',
|
||||
sort: true,
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '轮廓图',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '短视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
];
|
||||
|
||||
// 根据数据类型动态切换列
|
||||
const tableColumns = computed(() => {
|
||||
const mway = currentSearchParams.value.mway;
|
||||
return mway === '2' ? columnsAuto : columnsManual;
|
||||
});
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
if (exportLoading.value) return;
|
||||
|
||||
exportLoading.value = true;
|
||||
searchRef.value.btnLoading = true;
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
? {
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
if (values.stcd) {
|
||||
filters.push({
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteFn = async (record: any, reason: string) => {
|
||||
const ids = record.ids || [record.id];
|
||||
if (isManual.value) {
|
||||
return deleteFpssRInfo({
|
||||
ids,
|
||||
source: reason
|
||||
});
|
||||
}
|
||||
return deleteFpssrlRInfo({
|
||||
ids,
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
ids: [...selectedRowKeys.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -24,21 +699,4 @@ const activeTab = ref('manual');
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.fish-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content),
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}</style>
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑放鱼数据"
|
||||
width="600px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-empty description="编辑功能开发中,接口待接入" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
</script>
|
||||
@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="放鱼详情"
|
||||
width="90vw"
|
||||
:footer="null"
|
||||
>
|
||||
<!-- 查询区:复用页面搜索项 -->
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleResetWrapper"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #jcdt>
|
||||
<a-date-picker
|
||||
class="w-[160px]"
|
||||
v-model:value="jcdt.min"
|
||||
format="YYYY"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
picker="year"
|
||||
:showTime="timeShowTime"
|
||||
placeholder="起始时间"
|
||||
:showToday="false"
|
||||
>
|
||||
<template #renderExtraFooter>
|
||||
<div class="flex items-center flex-wrap px-2">
|
||||
<span
|
||||
v-for="item in timeDateList"
|
||||
:key="item"
|
||||
@click="handleRangeClick(item)"
|
||||
>
|
||||
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
至
|
||||
<a-form-item-rest>
|
||||
<a-date-picker
|
||||
class="w-[160px]"
|
||||
v-model:value="jcdt.max"
|
||||
format="YYYY"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
picker="year"
|
||||
:showTime="timeShowTime"
|
||||
:disabledDate="disabledEndDate"
|
||||
placeholder="结束时间"
|
||||
:showToday="false"
|
||||
>
|
||||
<template #renderExtraFooter>
|
||||
<div class="flex items-center flex-wrap px-2">
|
||||
<span
|
||||
v-for="item in timeDateList"
|
||||
:key="item"
|
||||
@click="handleRangeClick(item)"
|
||||
>
|
||||
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button
|
||||
danger
|
||||
:disabled="!selectedRowKeys.length"
|
||||
@click="handleBatchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
|
||||
<!-- 表格(无放流总量列) -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="true"
|
||||
:row-key="getRowKey"
|
||||
:columns="columns"
|
||||
:list-url="listUrl"
|
||||
:scrollY="500"
|
||||
:searchParams="{ sort }"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>修改</a-button
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 编辑弹窗(占位) -->
|
||||
<EditFishReleaseModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="handleDeleteFn"
|
||||
:label="deleteLabel"
|
||||
:title="'删除' + deleteLabel"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import EditFishReleaseModal from './EditFishReleaseModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import { useTimeScale } from '@/store/composables/useTimeScale';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
|
||||
const tableRef = ref();
|
||||
const basicSearchRef = ref();
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
rstcd: null,
|
||||
stcd: null
|
||||
};
|
||||
|
||||
const timeDateList = ref([
|
||||
{
|
||||
label: '一年',
|
||||
value: [dayjs().startOf('year'), dayjs().endOf('year')]
|
||||
},
|
||||
{
|
||||
label: '两年',
|
||||
value: [dayjs().subtract(1, 'year').startOf('year'), dayjs().endOf('year')]
|
||||
},
|
||||
{
|
||||
label: '三年',
|
||||
value: [dayjs().subtract(2, 'year').startOf('year'), dayjs().endOf('year')]
|
||||
},
|
||||
{
|
||||
label: '五年',
|
||||
value: [
|
||||
dayjs().subtract(4, 'year').startOf('year'),
|
||||
dayjs().startOf('year')
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const {
|
||||
jcdt,
|
||||
currentTimeScale,
|
||||
timeShowTime,
|
||||
handleRangeClick,
|
||||
disabledEndDate,
|
||||
handleSearchFinish,
|
||||
handleValuesChange,
|
||||
init
|
||||
} = useTimeScale({
|
||||
defaultTimeScale: initSearchData.timeScale,
|
||||
onSearchFinish: values => initTable(values),
|
||||
onReset: () => initTable(initSearchData)
|
||||
});
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'waterStation',
|
||||
name: 'rvcd',
|
||||
label: '流域',
|
||||
placeholder: '请输入流域名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'jcdt',
|
||||
label: '时间',
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
handleSearchFinish(values);
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
handleValuesChange(changedValues, allValues);
|
||||
};
|
||||
|
||||
const handleResetWrapper = () => {
|
||||
currentTimeScale.value = initSearchData.timeScale;
|
||||
const now = dayjs();
|
||||
jcdt.value = {
|
||||
min: `${now
|
||||
.subtract(1, 'year')
|
||||
.startOf('year')
|
||||
.format('YYYY-MM-DD')} 00:00:00`,
|
||||
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
|
||||
};
|
||||
handleSearchFinish({ ...initSearchData });
|
||||
};
|
||||
|
||||
const sort = ref<any>([
|
||||
{ field: 'baseStepSort', dir: 'asc' },
|
||||
{ field: 'rvcdStepSort', dir: 'asc' },
|
||||
{ field: 'siteStepSort', dir: 'asc' },
|
||||
{ field: 'flyr', dir: 'asc' }
|
||||
]);
|
||||
|
||||
// TODO: 弹窗查询接口待接入,暂时返回空数据
|
||||
const listUrl = async () => {
|
||||
console.warn('放鱼详情查询接口待接入');
|
||||
return { data: { records: [], total: 0 } };
|
||||
};
|
||||
|
||||
// 表格列(无放流总量)
|
||||
const columns: any[] = [
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 160,
|
||||
mergeDeps: ['stnm']
|
||||
},
|
||||
{
|
||||
key: 'flyr',
|
||||
title: '放鱼年份',
|
||||
dataIndex: 'flyr',
|
||||
sort: true,
|
||||
width: 160,
|
||||
mergeDeps: ['stnm']
|
||||
},
|
||||
{
|
||||
key: 'release',
|
||||
title: '放流来源',
|
||||
dataIndex: 'release',
|
||||
mergeDeps: ['stnm'],
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
key: 'ftpName',
|
||||
title: '放鱼种类',
|
||||
dataIndex: 'ftpName',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '数量(万尾)',
|
||||
dataIndex: 'fcnt',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) =>
|
||||
(record.fcnt / 10000).toFixed(4) || '-'
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
];
|
||||
|
||||
// 多选相关(唯一标识 stcd+ftpName)
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const getRowKey = (record: any) => `${record.stcd}_${record.ftpName}`;
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const currentSearchParams = ref<any>({});
|
||||
|
||||
const initTable = (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null
|
||||
].filter(Boolean);
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'flyr',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'flyr',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
tableRef.value?.getList({
|
||||
logic: 'and',
|
||||
filters
|
||||
});
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteModalRef = ref();
|
||||
const deleteLabel = '放鱼数据';
|
||||
|
||||
// 删除接口暂未提供,待接入后替换
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
message.error('删除接口暂未提供');
|
||||
throw new Error('删除接口暂未提供');
|
||||
};
|
||||
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
ids: [...selectedRowKeys.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
const now = dayjs();
|
||||
jcdt.value = {
|
||||
min: `${now
|
||||
.subtract(1, 'year')
|
||||
.startOf('year')
|
||||
.format('YYYY-MM-DD')} 00:00:00`,
|
||||
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
|
||||
};
|
||||
handleSearchFinish({ ...initSearchData });
|
||||
});
|
||||
</script>
|
||||
@ -59,6 +59,7 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -97,6 +98,7 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
@ -157,6 +159,9 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
handleValuesChange(changedValues, allValues);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
@ -19,13 +20,36 @@
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleDetail(record)"
|
||||
>详情</a-button
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<FishReleaseDetailModal
|
||||
v-model:open="detailVisible"
|
||||
:record="detailRecord"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FishReleaseSearch from './FishReleaseSearch.vue';
|
||||
import FishReleaseDetailModal from './FishReleaseDetailModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
@ -54,6 +78,11 @@ const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const operationLogVisible = ref(false);
|
||||
// TODO: 操作日志表名待确认
|
||||
const operationTableName = 'SD_RPIMNFISH_R';
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.reduce((sum: number, col: any) => sum + (col.width || 180), 0),
|
||||
@ -122,6 +151,13 @@ const columns: any[] = [
|
||||
dataIndex: 'fcnt',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => (record.fcnt / 10000).toFixed(4) || '-'
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 80
|
||||
}
|
||||
];
|
||||
|
||||
@ -198,6 +234,19 @@ const initTable = (values: any) => {
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
// 详情弹窗
|
||||
const detailVisible = ref(false);
|
||||
const detailRecord = ref<any>(null);
|
||||
const handleDetail = (record: any) => {
|
||||
detailRecord.value = { ...record };
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
|
||||
@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑生态流量数据"
|
||||
width="70vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
:rules="formRules"
|
||||
class="max-h-[70vh] overflow-y-auto pr-4"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="电站名称" name="stnm">
|
||||
<a-input
|
||||
v-model:value="formData.stnm"
|
||||
placeholder="请输入电站名称"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="基地" name="baseName">
|
||||
<a-input
|
||||
v-model:value="formData.baseName"
|
||||
placeholder="请输入基地"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="时间" name="tm">
|
||||
<a-date-picker
|
||||
v-model:value="formData.tm"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择时间"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生态流量是否达标" name="sfdb">
|
||||
<a-select
|
||||
v-model:value="formData.sfdb"
|
||||
placeholder="请选择是否达标"
|
||||
disabled
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in sfdbOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>{{ opt.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水利部要求是否达标" name="mwrSfdb">
|
||||
<a-select
|
||||
v-model:value="formData.mwrSfdb"
|
||||
placeholder="请选择是否达标"
|
||||
disabled
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in sfdbOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>{{ opt.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="多年平均流量*10%是否达标" name="avqSfdb">
|
||||
<a-select
|
||||
v-model:value="formData.avqSfdb"
|
||||
placeholder="请选择是否达标"
|
||||
disabled
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in sfdbOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>{{ opt.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="入库流量(m³/s)" name="qi">
|
||||
<a-input-number
|
||||
v-model:value="formData.qi"
|
||||
placeholder="请输入入库流量"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生态流量值(m³/s)" name="qec">
|
||||
<a-input-number
|
||||
v-model:value="formData.qec"
|
||||
placeholder="请输入生态流量值"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生态环境部限值要求(m³/s)" name="qecLimit">
|
||||
<a-input-number
|
||||
v-model:value="formData.qecLimit"
|
||||
placeholder="请输入生态环境部限值要求"
|
||||
:precision="5"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水利部限值要求(m³/s)" name="mwrLimit">
|
||||
<a-input-number
|
||||
v-model:value="formData.mwrLimit"
|
||||
placeholder="请输入水利部限值要求"
|
||||
:precision="5"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="多年平均流量(m³/s)" name="avqLimit">
|
||||
<a-input-number
|
||||
v-model:value="formData.avqLimit"
|
||||
placeholder="请输入多年平均流量"
|
||||
:precision="5"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<ConfirmModal
|
||||
v-model:open="confirmModalVisible"
|
||||
:diff-list="diffList"
|
||||
:confirm-loading="confirmLoading"
|
||||
@confirm="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import ConfirmModal from '@/components/ConfirmModal/index.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
import { updateEqRsData } from '@/api/DataQueryMenuModule';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref<any>({});
|
||||
const originalRecord = ref<any>({});
|
||||
const originalLabels = ref<Record<string, string>>({});
|
||||
const confirmModalVisible = ref(false);
|
||||
|
||||
const formRules = ref<any>({
|
||||
tm: [{ required: true, message: '请选择时间', trigger: 'change' }]
|
||||
});
|
||||
|
||||
// 字段名 → 中文名映射(与页面表格展示字段对应)
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
stnm: '电站名称',
|
||||
baseName: '基地',
|
||||
tm: '时间',
|
||||
qi: '入库流量(m³/s)',
|
||||
qec: '生态流量值(m³/s)',
|
||||
sfdb: '生态流量是否达标',
|
||||
qecLimit: '生态环境部限值要求(m³/s)',
|
||||
mwrLimit: '水利部限值要求(m³/s)',
|
||||
mwrSfdb: '水利部要求是否达标',
|
||||
avqLimit: '多年平均流量(m³/s)',
|
||||
avqSfdb: '多年平均流量*10%是否达标'
|
||||
};
|
||||
|
||||
// 达标状态选项
|
||||
const sfdbOptions = [
|
||||
{ label: '不达标', value: 0 },
|
||||
{ label: '达标', value: 1 },
|
||||
{ label: '无生态流量数据', value: 2 },
|
||||
{ label: '无生态流量限值要求', value: 3 }
|
||||
];
|
||||
|
||||
// select 选项映射
|
||||
const selectOptionsMap: Record<string, () => any[]> = {
|
||||
sfdb: () => sfdbOptions,
|
||||
mwrSfdb: () => sfdbOptions,
|
||||
avqSfdb: () => sfdbOptions
|
||||
};
|
||||
|
||||
// 按修改顺序记录变更
|
||||
const changeOrder = ref<
|
||||
{ field: string; label: string; oldValue: string; newValue: string }[]
|
||||
>([]);
|
||||
const diffList = changeOrder;
|
||||
|
||||
const resolveSelectLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空';
|
||||
const getOptions = selectOptionsMap[field];
|
||||
if (getOptions) {
|
||||
const opt = getOptions().find(o => String(o.value) === String(value));
|
||||
if (opt) return opt.label;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 监听 formData 变化
|
||||
watch(
|
||||
() => formData.value,
|
||||
newData => {
|
||||
const original = originalRecord.value;
|
||||
for (const key in newData) {
|
||||
const oldVal = original[key];
|
||||
const newVal = newData[key];
|
||||
if (oldVal !== newVal) {
|
||||
const existingIdx = changeOrder.value.findIndex(c => c.field === key);
|
||||
const oldNorm =
|
||||
oldVal === null || oldVal === undefined || oldVal === '-'
|
||||
? null
|
||||
: oldVal;
|
||||
const newNorm = newVal === null || newVal === undefined ? null : newVal;
|
||||
if (oldNorm === newNorm) continue;
|
||||
if (
|
||||
oldNorm !== null &&
|
||||
newNorm !== null &&
|
||||
!isNaN(Number(oldNorm)) &&
|
||||
!isNaN(Number(newNorm)) &&
|
||||
Number(oldNorm) === Number(newNorm)
|
||||
)
|
||||
continue;
|
||||
|
||||
const isSelect = !!selectOptionsMap[key];
|
||||
const oldDisplay = isSelect
|
||||
? originalLabels.value[key] ?? resolveSelectLabel(key, oldNorm)
|
||||
: oldNorm === null
|
||||
? '空'
|
||||
: String(oldNorm);
|
||||
const newDisplay = isSelect
|
||||
? resolveSelectLabel(key, newNorm)
|
||||
: newNorm === null
|
||||
? '空'
|
||||
: String(newNorm);
|
||||
|
||||
const entry = {
|
||||
field: key,
|
||||
label: fieldLabelMap[key] || key,
|
||||
oldValue: oldDisplay,
|
||||
newValue: newDisplay
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(c => c.field !== key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听 record 变化
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (newRecord) {
|
||||
const converted = { ...newRecord };
|
||||
// 将 "-" 还原为 null,避免控件显示异常
|
||||
for (const key in converted) {
|
||||
if (converted[key] === '-') {
|
||||
converted[key] = null;
|
||||
}
|
||||
}
|
||||
// 数字字段字符串转数字,保证 select/input-number 回显匹配
|
||||
[
|
||||
'sfdb',
|
||||
'mwrSfdb',
|
||||
'avqSfdb',
|
||||
'qi',
|
||||
'qec',
|
||||
'qecLimit',
|
||||
'mwrLimit',
|
||||
'avqLimit'
|
||||
].forEach(key => {
|
||||
if (
|
||||
converted[key] !== null &&
|
||||
converted[key] !== undefined &&
|
||||
converted[key] !== ''
|
||||
) {
|
||||
converted[key] = Number(converted[key]);
|
||||
}
|
||||
});
|
||||
originalRecord.value = { ...converted };
|
||||
formData.value = { ...converted };
|
||||
changeOrder.value = [];
|
||||
// 提前解析 select 字段的原始显示名
|
||||
const labels: Record<string, string> = {};
|
||||
for (const key in converted) {
|
||||
if (selectOptionsMap[key]) {
|
||||
labels[key] = resolveSelectLabel(key, converted[key]);
|
||||
}
|
||||
}
|
||||
originalLabels.value = labels;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validateFields();
|
||||
if (changeOrder.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 时间字段选择时仅到年月日,提交时统一补上时分秒
|
||||
const formatDateTime = (val: any): any => {
|
||||
if (val === null || val === undefined || val === '') return val;
|
||||
const str = String(val);
|
||||
return str.length === 10 ? `${str} 00:00:00` : str;
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async (source: string) => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
// 与 EditWaterDataModal 保持一致:updateData 传完整表单,source 传修改依据
|
||||
const updateData: Record<string, any> = { ...formData.value };
|
||||
if (updateData.tm) {
|
||||
updateData.tm = formatDateTime(updateData.tm);
|
||||
}
|
||||
const res = await updateEqRsData({
|
||||
updateData,
|
||||
source: source.trim()
|
||||
});
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
@ -63,6 +63,14 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button
|
||||
v-if="showBatchDelete"
|
||||
danger
|
||||
:disabled="!selectedCount"
|
||||
@click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -78,13 +86,22 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
showBatchDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showBatchDelete: true
|
||||
}
|
||||
);
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
@ -185,6 +202,12 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
handleValuesChange(changedValues, allValues);
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:show-batch-delete="currentSearchParams.timeScale === 'tm'"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
@ -12,6 +16,7 @@
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="currentSearchParams.timeScale === 'tm'"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="displayColumns"
|
||||
@ -19,20 +24,65 @@
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
<a-button
|
||||
type="link"
|
||||
class="text-[#2f6b98]"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDelete(record)"
|
||||
>删除</a-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 编辑生态流量数据 Modal -->
|
||||
<EditFlowDataModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="handleDeleteFn"
|
||||
:label="deleteLabel"
|
||||
:title="'删除' + deleteLabel"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import FlowDataSearch from './FlowDataSearch.vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import EditFlowDataModal from './EditFlowDataModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import { Tooltip, message } from 'ant-design-vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import {
|
||||
getFlowDataList,
|
||||
getFlowDataDayList,
|
||||
getFlowDataMonthList
|
||||
getFlowDataMonthList,
|
||||
deleteFlowDataInfo
|
||||
} from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { buildTimeFilters } from '@/utils/buildTimeFilters';
|
||||
@ -62,6 +112,23 @@ const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 编辑/删除相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const deleteLabel = '生态流量数据';
|
||||
// TODO: 操作日志表名待确认
|
||||
const operationTableName = 'SD_QEC_R';
|
||||
|
||||
// 多选相关
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 根据 timeScale 动态切换 API
|
||||
const currentListUrl = computed(() => {
|
||||
const timeScale = currentSearchParams.value.timeScale;
|
||||
@ -359,18 +426,37 @@ const ruleColumnsMap: any = {
|
||||
const displayColumns = computed(() => {
|
||||
const ruleType = currentSearchParams.value.ruleType ?? '';
|
||||
const ruleColumns = ruleColumnsMap[ruleType] || ruleColumnsMap[''];
|
||||
return [...baseColumns, ...ruleColumns];
|
||||
const columns: any[] = [...baseColumns, ...ruleColumns];
|
||||
// 仅 timeScale === 'tm' 时显示操作列
|
||||
if (currentSearchParams.value.timeScale === 'tm') {
|
||||
columns.push({
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
// debugger
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
@ -456,6 +542,71 @@ const initTable = (values: any) => {
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 删除(兼容单条:record 本身;批量:record.records 数组),仅小时(tm)数据支持删除
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
const list = Array.isArray(record.records) ? record.records : [record];
|
||||
const dataList = list.map((r: any) => ({
|
||||
id: r.stcd,
|
||||
tm: r.tm
|
||||
}));
|
||||
return deleteFlowDataInfo({
|
||||
dataList,
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (currentSearchParams.value.timeScale !== 'tm') {
|
||||
message.info('仅小时数据支持删除');
|
||||
return;
|
||||
}
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
records: [...selectedRows.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
|
||||
@ -61,6 +61,14 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button
|
||||
v-if="showBatchDelete"
|
||||
danger
|
||||
:disabled="!selectedCount"
|
||||
@click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -77,14 +85,23 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
showBatchDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showBatchDelete: true
|
||||
}
|
||||
);
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
@ -184,6 +201,12 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [
|
||||
{
|
||||
|
||||
@ -5,12 +5,18 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:show-batch-delete="currentSearchParams.timeScale === 'tm'"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="currentSearchParams.timeScale === 'tm'"
|
||||
:row-key="getRowKey"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="Columns"
|
||||
@ -18,6 +24,7 @@
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
@ -53,6 +60,12 @@
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -61,7 +74,9 @@ import dayjs from 'dayjs';
|
||||
import FlowStationSearch from './FlowStationSearch.vue';
|
||||
import EditFlowStationModal from './EditFlowStationModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
getFlowStationList,
|
||||
getFlowStationDayList,
|
||||
@ -108,6 +123,17 @@ const exportLoading = ref(false);
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const operationTableName = 'SD_RIVER_R';
|
||||
|
||||
// 多选相关(唯一标识 stcd+tm,日/月模式数据用 dt)
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const getRowKey = (record: any) => `${record.stcd}_${record.tm ?? record.dt}`;
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 根据 timeScale 动态切换 API
|
||||
const currentListUrl = computed(() => {
|
||||
@ -230,6 +256,10 @@ const Columns = computed(() => {
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
@ -237,6 +267,10 @@ const onSearchFinish = async (values: any) => {
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
@ -262,25 +296,60 @@ const handleEdit = (record: any) => {
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除(兼容单条:record 本身;批量:record.records 数组)
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
const list = Array.isArray(record.records) ? record.records : [record];
|
||||
const dataList = list.map((r: any) => ({
|
||||
id: r.stcd,
|
||||
tm: r.tm
|
||||
}));
|
||||
return deleteFlowStationInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd,
|
||||
tm: record.tm
|
||||
}
|
||||
],
|
||||
dataList,
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除(仅小时 tm 模式,与单条删除一致)
|
||||
const handleBatchDelete = () => {
|
||||
if (currentSearchParams.value.timeScale !== 'tm') {
|
||||
message.info('仅小时数据支持删除');
|
||||
return;
|
||||
}
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
records: [...selectedRows.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
@ -61,6 +61,14 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button
|
||||
v-if="showBatchDelete"
|
||||
danger
|
||||
:disabled="!selectedCount"
|
||||
@click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -77,14 +85,23 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
showBatchDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showBatchDelete: true
|
||||
}
|
||||
);
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
@ -184,6 +201,12 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [
|
||||
{
|
||||
|
||||
@ -5,12 +5,18 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:show-batch-delete="currentSearchParams.timeScale === 'tm'"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="currentSearchParams.timeScale === 'tm'"
|
||||
:row-key="getRowKey"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="Columns"
|
||||
@ -18,6 +24,7 @@
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
@ -53,6 +60,12 @@
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -61,7 +74,9 @@ import dayjs from 'dayjs';
|
||||
import SurfaceTempSearch from './SurfaceTempSearch.vue';
|
||||
import EditSurfaceTempModal from './EditSurfaceTempModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
getSurfaceTempList,
|
||||
getSurfaceTempDayList,
|
||||
@ -106,6 +121,17 @@ const exportLoading = ref(false);
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const operationTableName = 'SD_WTRV_R';
|
||||
|
||||
// 多选相关(唯一标识 stcd+tm,日/月模式数据用 dt)
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const getRowKey = (record: any) => `${record.stcd}_${record.tm ?? record.dt}`;
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 根据 timeScale 动态切换 API
|
||||
const currentListUrl = computed(() => {
|
||||
@ -194,6 +220,10 @@ const Columns = computed(() => {
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
@ -201,6 +231,10 @@ const onSearchFinish = async (values: any) => {
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
@ -226,25 +260,60 @@ const handleEdit = (record: any) => {
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除(兼容单条:record 本身;批量:record.records 数组)
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
const list = Array.isArray(record.records) ? record.records : [record];
|
||||
const dataList = list.map((r: any) => ({
|
||||
id: r.stcd,
|
||||
dt: r.tm
|
||||
}));
|
||||
return deleteSurfaceTempInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd,
|
||||
dt: record.tm
|
||||
}
|
||||
],
|
||||
dataList,
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除(仅小时 tm 模式,与单条删除一致)
|
||||
const handleBatchDelete = () => {
|
||||
if (currentSearchParams.value.timeScale !== 'tm') {
|
||||
message.info('仅小时数据支持删除');
|
||||
return;
|
||||
}
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
records: [...selectedRows.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
@ -63,6 +63,10 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button danger :disabled="!selectedCount" @click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -79,6 +83,8 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
@ -86,6 +92,7 @@ const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
}>();
|
||||
|
||||
const initSearchData = {
|
||||
@ -186,6 +193,12 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [
|
||||
{
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:selected-count="currentSearchParams.timeScale === 'tm' ? selectedRowsForChart.length : 0"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
@ -96,6 +99,12 @@
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -113,7 +122,9 @@ import * as echarts from 'echarts';
|
||||
import VerticalTempSearch from './VerticalTempSearch.vue';
|
||||
import EditVerticalTempModal from './EditVerticalTempModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getVerticalList, deleteVerticalInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
@ -132,6 +143,8 @@ const tableData = ref<any[]>([]);
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const operationTableName = 'SD_WTVT_R';
|
||||
|
||||
// 图表相关
|
||||
const chartRef = ref<HTMLElement>();
|
||||
@ -439,25 +452,59 @@ const handleEdit = (record: any) => {
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除(兼容单条:record 本身;批量:record.records 数组)
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
const list = Array.isArray(record.records) ? record.records : [record];
|
||||
const dataList = list.map((r: any) => ({
|
||||
id: r.stcd ?? r.id,
|
||||
tm: r.tm ?? r.dt
|
||||
}));
|
||||
return deleteVerticalInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd ?? record.id,
|
||||
tm: record.tm ?? record.dt
|
||||
}
|
||||
],
|
||||
dataList,
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowsForChart.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除(仅小时 tm 模式,与单条删除一致)
|
||||
const handleBatchDelete = () => {
|
||||
if (currentSearchParams.value.timeScale !== 'tm') {
|
||||
message.info('仅小时数据支持删除');
|
||||
return;
|
||||
}
|
||||
if (selectedRowsForChart.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
records: [...selectedRowsForChart.value],
|
||||
stnm: `选中的 ${selectedRowsForChart.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
@ -63,6 +63,14 @@
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button
|
||||
v-if="showBatchDelete"
|
||||
danger
|
||||
:disabled="!selectedCount"
|
||||
@click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
@ -79,13 +87,22 @@ const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
showBatchDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showBatchDelete: true
|
||||
}
|
||||
);
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
@ -201,6 +218,12 @@ const onSearchFinish = (values: any) => {
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [
|
||||
{
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:show-batch-delete="currentSearchParams.timeScale === 'tm'"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
@ -12,6 +16,8 @@
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="currentSearchParams.timeScale === 'tm'"
|
||||
row-key="stcd"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="Columns"
|
||||
@ -19,6 +25,7 @@
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template v-if="currentSearchParams.timeScale === 'tm'">
|
||||
@ -55,6 +62,12 @@
|
||||
label="数据"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -63,7 +76,8 @@ import dayjs from 'dayjs';
|
||||
import WaterDataSearch from './WaterDataSearch.vue';
|
||||
import EditWaterDataModal from './EditWaterDataModal.vue';
|
||||
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import { Tooltip, message } from 'ant-design-vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import {
|
||||
getWaterDataList,
|
||||
@ -111,6 +125,16 @@ const exportLoading = ref(false);
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const operationTableName = 'SD_WQ_R';
|
||||
|
||||
// 多选相关
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 根据 timeScale 动态切换 API
|
||||
const currentListUrl = computed(() => {
|
||||
@ -340,12 +364,20 @@ const Columns = computed(() => {
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 切换查询条件时清空选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
@ -416,32 +448,65 @@ const handleEdit = (record: any) => {
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除
|
||||
// 删除(兼容单条:record 本身;批量:record.records 数组)
|
||||
const handleDeleteFn = async (record: any, reason: string) => {
|
||||
console.log(record, reason);
|
||||
const list = Array.isArray(record.records) ? record.records : [record];
|
||||
const dataList = list.map((r: any) => ({
|
||||
id: r.stcd,
|
||||
tm: r.tm
|
||||
}));
|
||||
return deleteWaterDataInfo({
|
||||
dataList: [
|
||||
{
|
||||
id: record.stcd,
|
||||
tm: record.tm
|
||||
}
|
||||
],
|
||||
dataList,
|
||||
dataType: 'TIME',
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (currentSearchParams.value.timeScale !== 'tm') {
|
||||
message.info('仅小时数据支持删除');
|
||||
return;
|
||||
}
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
records: [...selectedRows.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 编辑/删除成功回调
|
||||
const handleEditSuccess = () => {
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
// 查看修改(操作日志)
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
|
||||
@ -0,0 +1,841 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="编辑过鱼自动数据"
|
||||
width="70vw"
|
||||
:confirm-loading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
:rules="formRules"
|
||||
class="max-h-[70vh] overflow-y-auto pr-4"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼设施" name="stcd">
|
||||
<a-select
|
||||
v-model:value="formData.stcd"
|
||||
placeholder="请选择过鱼设施"
|
||||
allow-clear
|
||||
show-search
|
||||
:loading="facilityLoading"
|
||||
:filter-option="filterOption"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in facilityOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="时间" name="tm">
|
||||
<a-date-picker
|
||||
v-model:value="formData.tm"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼种类" name="ftp">
|
||||
<fishSearch v-model="formData.ftp" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼数量" name="fcnt">
|
||||
<a-input-number
|
||||
v-model:value="formData.fcnt"
|
||||
placeholder="请输入过鱼数量"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼通道" name="channelno">
|
||||
<a-input
|
||||
v-model:value="formData.channelno"
|
||||
placeholder="请输入过鱼通道"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼游向" name="direction">
|
||||
<a-select
|
||||
v-model:value="formData.direction"
|
||||
placeholder="请选择鱼游向"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option :value="0">上行</a-select-option>
|
||||
<a-select-option :value="1">下行</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼尺寸" name="fsz">
|
||||
<a-select
|
||||
v-model:value="formData.fsz"
|
||||
placeholder="请选择鱼尺寸"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="大">大</a-select-option>
|
||||
<a-select-option value="中">中</a-select-option>
|
||||
<a-select-option value="小">小</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼长度(cm)" name="length">
|
||||
<a-input-number
|
||||
v-model:value="formData.length"
|
||||
placeholder="请输入鱼长度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼宽度(cm)" name="width">
|
||||
<a-input-number
|
||||
v-model:value="formData.width"
|
||||
placeholder="请输入鱼宽度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼速度(m/s)" name="fishspeed">
|
||||
<a-input
|
||||
v-model:value="formData.fishspeed"
|
||||
placeholder="请输入鱼速度"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼位置" name="fishposition">
|
||||
<a-input-number
|
||||
v-model:value="formData.fishposition"
|
||||
placeholder="请输入鱼位置"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水温(℃)" name="temperature">
|
||||
<a-input-number
|
||||
v-model:value="formData.temperature"
|
||||
placeholder="请输入水温"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水位(m)" name="waterlevel">
|
||||
<a-input-number
|
||||
v-model:value="formData.waterlevel"
|
||||
placeholder="请输入水位"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="流速(m/s)" name="speed">
|
||||
<a-input-number
|
||||
v-model:value="formData.speed"
|
||||
placeholder="请输入流速"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="流量(m³/s)" name="q">
|
||||
<a-input-number
|
||||
v-model:value="formData.q"
|
||||
placeholder="请输入流量"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="溶氧(mg/L)" name="dox">
|
||||
<a-input-number
|
||||
v-model:value="formData.dox"
|
||||
placeholder="请输入溶氧"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="浊度(NTU)" name="tu">
|
||||
<a-input-number
|
||||
v-model:value="formData.tu"
|
||||
placeholder="请输入浊度"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="255"
|
||||
show-count
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 图片上传 -->
|
||||
<a-row :gutter="16" class="mt-4">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="图片" name="firstimgurl" :label-col="{ span: 4 }" :wrapper-col="{ span: 21 }">
|
||||
<a-upload
|
||||
v-model:file-list="picFileList"
|
||||
:multiple="false"
|
||||
:max-count="1"
|
||||
list-type="picture-card"
|
||||
:before-upload="handleBeforePicUpload"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="picFileList.length < 1">
|
||||
<plus-outlined />
|
||||
<div class="mt-2">上传图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 视频上传 -->
|
||||
<a-row :gutter="16" class="mt-4">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="视频" name="videourl" :label-col="{ span: 4 }" :wrapper-col="{ span: 21 }">
|
||||
<a-upload
|
||||
v-model:file-list="vdpFileList"
|
||||
:multiple="false"
|
||||
:max-count="1"
|
||||
:before-upload="handleBeforeVdpUpload"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<a-button v-if="vdpFileList.length < 1">
|
||||
<upload-outlined /> 上传视频
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<ConfirmModal
|
||||
v-model:open="confirmModalVisible"
|
||||
:diff-list="diffList"
|
||||
:confirm-loading="confirmLoading"
|
||||
@confirm="handleConfirmSubmit"
|
||||
@cancel="handleConfirmCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { message, Upload } from 'ant-design-vue';
|
||||
import { PlusOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { updateFpssrlAiRInfo } from '@/api/DataQueryMenuModule';
|
||||
import { getFishReleaseMonitorSectionList } from '@/api/DataQueryMenuModule';
|
||||
import ConfirmModal from '@/components/ConfirmModal/index.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
import fishSearch from '@/components/fishSearch/index.vue';
|
||||
import { useShuJuTianBaoStore } from '@/store/modules/shuJuTianBao';
|
||||
|
||||
const shuJuTianBaoStore = useShuJuTianBaoStore();
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:open', 'success']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: val => emit('update:open', val)
|
||||
});
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
|
||||
const formRef = ref();
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref<any>({});
|
||||
const originalRecord = ref<any>({});
|
||||
const originalLabels = ref<Record<string, string>>({});
|
||||
const confirmModalVisible = ref(false);
|
||||
|
||||
// 过鱼设施下拉
|
||||
const facilityLoading = ref(false);
|
||||
const facilityOptions = ref<any[]>([]);
|
||||
|
||||
// 图片上传相关
|
||||
const picFileList = ref<any[]>([]);
|
||||
// 视频上传相关
|
||||
const vdpFileList = ref<any[]>([]);
|
||||
|
||||
const formRules = ref<any>({
|
||||
stcd: [{ required: true, message: '请选择过鱼设施', trigger: 'change' }],
|
||||
tm: [{ required: true, message: '请选择时间', trigger: 'change' }],
|
||||
ftp: [{ required: true, message: '请选择鱼种类', trigger: 'blur' }],
|
||||
fcnt: [{ required: true, message: '请输入过鱼数量', trigger: 'blur' }],
|
||||
channelno: [{ required: true, message: '请输入过鱼通道', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 字段名 → 中文名映射
|
||||
const fieldLabelMap: Record<string, string> = {
|
||||
stcd: '过鱼设施',
|
||||
tm: '时间',
|
||||
ftp: '鱼种类',
|
||||
fcnt: '过鱼数量',
|
||||
fsz: '鱼尺寸',
|
||||
length: '鱼长度(cm)',
|
||||
width: '鱼宽度(cm)',
|
||||
fishspeed: '鱼速度(m/s)',
|
||||
direction: '鱼游向',
|
||||
fishposition: '鱼位置',
|
||||
firstimgurl: '图片',
|
||||
videourl: '视频',
|
||||
temperature: '水温(℃)',
|
||||
waterlevel: '水位(m)',
|
||||
speed: '流速(m/s)',
|
||||
q: '流量(m³/s)',
|
||||
dox: '溶氧(mg/L)',
|
||||
tu: '浊度(NTU)',
|
||||
channelno: '过鱼通道',
|
||||
remark: '备注'
|
||||
};
|
||||
|
||||
// select 选项映射
|
||||
const selectOptionsMap: Record<string, () => any[]> = {
|
||||
stcd: () => facilityOptions.value,
|
||||
ftp: () =>
|
||||
shuJuTianBaoStore.getFishOption().map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
})),
|
||||
direction: () => [
|
||||
{ label: '上行', value: 0 },
|
||||
{ label: '下行', value: 1 }
|
||||
],
|
||||
fsz: () => [
|
||||
{ label: '大', value: '大' },
|
||||
{ label: '中', value: '中' },
|
||||
{ label: '小', value: '小' }
|
||||
]
|
||||
};
|
||||
|
||||
// 按修改顺序记录变更
|
||||
const changeOrder = ref<
|
||||
{ field: string; label: string; oldValue: string; newValue: string }[]
|
||||
>([]);
|
||||
const diffList = changeOrder;
|
||||
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
const label = option.label || '';
|
||||
const keyword = inputValue || '';
|
||||
return label.includes(keyword);
|
||||
};
|
||||
|
||||
const handleBeforePicUpload = (file: any) => {
|
||||
const isImage =
|
||||
file.type === 'image/jpeg' ||
|
||||
file.type === 'image/png' ||
|
||||
file.type === 'image/gif' ||
|
||||
file.type === 'image/webp';
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件(JPG/PNG/GIF/WEBP)');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
message.error('图片大小不能超过 10MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
picFileList.value = [...picFileList.value, file];
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleBeforeVdpUpload = (file: any) => {
|
||||
const isVideo =
|
||||
file.type === 'video/mp4' ||
|
||||
file.type === 'video/quicktime' ||
|
||||
file.type === 'video/x-msvideo' ||
|
||||
file.type === 'video/webm' ||
|
||||
file.type === 'video/x-matroska';
|
||||
if (!isVideo) {
|
||||
message.error('只能上传视频文件(MP4/MOV/AVI/WEBM/MKV)');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||||
if (!isLt100M) {
|
||||
message.error('视频大小不能超过 100MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
vdpFileList.value = [...vdpFileList.value, file];
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// 解析图片/视频 id 列表,过滤 null/-/undefined 等占位字符串
|
||||
const normalizeIds = (val: any): string[] => {
|
||||
if (val === null || val === undefined || val === '') return [];
|
||||
return String(val)
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s && s !== '-' && s !== 'null' && s !== 'undefined');
|
||||
};
|
||||
|
||||
const resolveSelectLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空';
|
||||
const getOptions = selectOptionsMap[field];
|
||||
if (getOptions) {
|
||||
const opt = getOptions().find(
|
||||
(o: any) => String(o.value) === String(value)
|
||||
);
|
||||
if (opt) return opt.label;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 解析原始值的显示名:优先用预解析快照,不依赖实时选项
|
||||
const resolveOriginalLabel = (field: string): string => {
|
||||
return resolveSelectLabel(field, originalRecord.value[field]);
|
||||
};
|
||||
|
||||
// 监听 formData 变化
|
||||
watch(
|
||||
() => formData.value,
|
||||
newData => {
|
||||
const original = originalRecord.value;
|
||||
for (const key in newData) {
|
||||
const oldVal = original[key];
|
||||
const newVal = newData[key];
|
||||
if (oldVal !== newVal) {
|
||||
const existingIdx = changeOrder.value.findIndex(c => c.field === key);
|
||||
const oldNorm =
|
||||
oldVal === null || oldVal === undefined || oldVal === '-'
|
||||
? null
|
||||
: oldVal;
|
||||
const newNorm = newVal === null || newVal === undefined ? null : newVal;
|
||||
if (oldNorm === newNorm) continue;
|
||||
if (
|
||||
oldNorm !== null &&
|
||||
newNorm !== null &&
|
||||
!isNaN(Number(oldNorm)) &&
|
||||
!isNaN(Number(newNorm)) &&
|
||||
Number(oldNorm) === Number(newNorm)
|
||||
)
|
||||
continue;
|
||||
|
||||
const isSelect = !!selectOptionsMap[key];
|
||||
const oldDisplay = isSelect
|
||||
? (originalLabels.value[key] ?? resolveSelectLabel(key, oldNorm))
|
||||
: oldNorm === null
|
||||
? '空'
|
||||
: String(oldNorm);
|
||||
const newDisplay = isSelect
|
||||
? resolveSelectLabel(key, newNorm)
|
||||
: newNorm === null
|
||||
? '空'
|
||||
: String(newNorm);
|
||||
|
||||
const entry = {
|
||||
field: key,
|
||||
label: fieldLabelMap[key] || key,
|
||||
oldValue: oldDisplay,
|
||||
newValue: newDisplay
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(c => c.field !== key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听图片列表变化,生成图片增删 diff
|
||||
watch(
|
||||
() => picFileList.value,
|
||||
newList => {
|
||||
const originalIds = normalizeIds(originalRecord.value.firstimgurl);
|
||||
const currentExistingIds = newList
|
||||
.filter((f: any) => f.isExisting)
|
||||
.map((f: any) => f.uid);
|
||||
const currentNewCount = newList.filter((f: any) => f.originFileObj).length;
|
||||
|
||||
const deletedCount = originalIds.filter(
|
||||
(id: string) => !currentExistingIds.includes(id)
|
||||
).length;
|
||||
const addedCount = currentNewCount;
|
||||
|
||||
if (deletedCount > 0 || addedCount > 0) {
|
||||
const existingIdx = changeOrder.value.findIndex(
|
||||
c => c.field === 'firstimgurl'
|
||||
);
|
||||
const oldDisplay = `${originalIds.length}张图片`;
|
||||
const parts: string[] = [];
|
||||
if (deletedCount > 0) parts.push(`删除${deletedCount}张`);
|
||||
if (addedCount > 0) parts.push(`新增${addedCount}张`);
|
||||
const entry = {
|
||||
field: 'firstimgurl',
|
||||
label: '图片',
|
||||
oldValue: oldDisplay,
|
||||
newValue: parts.join(',')
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(
|
||||
c => c.field !== 'firstimgurl'
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听视频列表变化,生成视频增删 diff
|
||||
watch(
|
||||
() => vdpFileList.value,
|
||||
newList => {
|
||||
const originalIds = normalizeIds(originalRecord.value.videourl);
|
||||
const currentExistingIds = newList
|
||||
.filter((f: any) => f.isExisting)
|
||||
.map((f: any) => f.uid);
|
||||
const currentNewCount = newList.filter((f: any) => f.originFileObj).length;
|
||||
|
||||
const deletedCount = originalIds.filter(
|
||||
(id: string) => !currentExistingIds.includes(id)
|
||||
).length;
|
||||
const addedCount = currentNewCount;
|
||||
|
||||
if (deletedCount > 0 || addedCount > 0) {
|
||||
const existingIdx = changeOrder.value.findIndex(
|
||||
c => c.field === 'videourl'
|
||||
);
|
||||
const oldDisplay = `${originalIds.length}个视频`;
|
||||
const parts: string[] = [];
|
||||
if (deletedCount > 0) parts.push(`删除${deletedCount}个`);
|
||||
if (addedCount > 0) parts.push(`新增${addedCount}个`);
|
||||
const entry = {
|
||||
field: 'videourl',
|
||||
label: '视频',
|
||||
oldValue: oldDisplay,
|
||||
newValue: parts.join(',')
|
||||
};
|
||||
if (existingIdx >= 0) {
|
||||
changeOrder.value[existingIdx] = entry;
|
||||
} else {
|
||||
changeOrder.value.push(entry);
|
||||
}
|
||||
} else {
|
||||
changeOrder.value = changeOrder.value.filter(
|
||||
c => c.field !== 'videourl'
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const loadFacilities = async () => {
|
||||
facilityLoading.value = true;
|
||||
try {
|
||||
const res = await getFishReleaseMonitorSectionList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: '2'
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['stcd', 'stnm', 'mway']
|
||||
});
|
||||
facilityOptions.value = (res.data?.data || []).map((item: any) => ({
|
||||
label: item.stnm,
|
||||
value: item.stcd
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取过鱼设施列表失败:', error);
|
||||
} finally {
|
||||
facilityLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 record 变化
|
||||
watch(
|
||||
() => props.record,
|
||||
newRecord => {
|
||||
if (newRecord) {
|
||||
const converted = { ...newRecord };
|
||||
// 将 "-" 还原为 null,避免控件显示异常
|
||||
for (const key in converted) {
|
||||
if (converted[key] === '-') {
|
||||
converted[key] = null;
|
||||
}
|
||||
}
|
||||
// 数字字段字符串转数字,保证 select/input-number 回显匹配
|
||||
[
|
||||
'direction',
|
||||
'fcnt',
|
||||
'length',
|
||||
'width',
|
||||
'fishposition',
|
||||
'temperature',
|
||||
'waterlevel',
|
||||
'speed',
|
||||
'q',
|
||||
'dox',
|
||||
'tu'
|
||||
].forEach(key => {
|
||||
if (
|
||||
converted[key] !== null &&
|
||||
converted[key] !== undefined &&
|
||||
converted[key] !== ''
|
||||
) {
|
||||
converted[key] = Number(converted[key]);
|
||||
}
|
||||
});
|
||||
// 鱼种类:列表返回名称,fishSearch 需用 fishId(id) 回显
|
||||
const ftpName = converted.ftp;
|
||||
if (converted.fishId) {
|
||||
converted.ftp = converted.fishId;
|
||||
}
|
||||
originalRecord.value = { ...converted };
|
||||
formData.value = { ...converted };
|
||||
changeOrder.value = [];
|
||||
// 提前解析 select 字段的原始显示名,防止联动刷新选项后丢失 label
|
||||
const labels: Record<string, string> = {};
|
||||
for (const key in converted) {
|
||||
if (selectOptionsMap[key]) {
|
||||
labels[key] = resolveOriginalLabel(key);
|
||||
}
|
||||
}
|
||||
// ftp 优先使用返回的名称,避免选项未加载时显示 id
|
||||
if (ftpName && converted.fishId) {
|
||||
labels.ftp = ftpName;
|
||||
}
|
||||
originalLabels.value = labels;
|
||||
// 处理已有图片
|
||||
const picIds = normalizeIds(converted.firstimgurl);
|
||||
if (picIds.length > 0) {
|
||||
picFileList.value = picIds.map((id: string) => ({
|
||||
uid: id,
|
||||
name: id,
|
||||
status: 'done',
|
||||
url: `${baseUrl}/?${id}&view=jpg`,
|
||||
isExisting: true
|
||||
}));
|
||||
} else {
|
||||
picFileList.value = [];
|
||||
}
|
||||
// 处理已有视频
|
||||
const vdpIds = normalizeIds(converted.videourl);
|
||||
if (vdpIds.length > 0) {
|
||||
vdpFileList.value = vdpIds.map((id: string) => ({
|
||||
uid: id,
|
||||
name: id,
|
||||
status: 'done',
|
||||
url: `${baseUrl}/?${id}`,
|
||||
isExisting: true
|
||||
}));
|
||||
} else {
|
||||
vdpFileList.value = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 选项加载完成后重新解析原始显示名
|
||||
watch(facilityOptions, () => {
|
||||
const original = originalRecord.value;
|
||||
if (!original || Object.keys(original).length === 0) return;
|
||||
const labels: Record<string, string> = {};
|
||||
for (const key in original) {
|
||||
if (selectOptionsMap[key]) {
|
||||
labels[key] = resolveOriginalLabel(key);
|
||||
}
|
||||
}
|
||||
// 保留已解析的 ftp 名称,避免选项未加载时显示 id
|
||||
if (originalLabels.value.ftp) {
|
||||
labels.ftp = originalLabels.value.ftp;
|
||||
}
|
||||
originalLabels.value = labels;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
val => {
|
||||
if (val) {
|
||||
loadFacilities();
|
||||
// 编辑时统一初始化图片/视频(无值则清空,效果同新增)
|
||||
const picIds = normalizeIds(props.record?.firstimgurl);
|
||||
picFileList.value = picIds.length
|
||||
? picIds.map((id: string) => ({
|
||||
uid: id,
|
||||
name: id,
|
||||
status: 'done',
|
||||
url: `${baseUrl}/?${id}&view=jpg`,
|
||||
isExisting: true
|
||||
}))
|
||||
: [];
|
||||
const vdpIds = normalizeIds(props.record?.videourl);
|
||||
vdpFileList.value = vdpIds.length
|
||||
? vdpIds.map((id: string) => ({
|
||||
uid: id,
|
||||
name: id,
|
||||
status: 'done',
|
||||
url: `${baseUrl}/?${id}`,
|
||||
isExisting: true
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validateFields();
|
||||
if (changeOrder.value.length === 0) {
|
||||
message.info('未检测到任何修改');
|
||||
return;
|
||||
}
|
||||
confirmModalVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const buildFormData = (engInfo: Record<string, any>, source: string) => {
|
||||
const formData = new FormData();
|
||||
const data = {
|
||||
engInfo,
|
||||
source: source.trim() || '编辑'
|
||||
};
|
||||
formData.append('data', JSON.stringify(data));
|
||||
// 添加新增的图片文件
|
||||
picFileList.value.forEach((file: any) => {
|
||||
if (file.originFileObj) {
|
||||
formData.append('picFiles', file.originFileObj);
|
||||
}
|
||||
});
|
||||
// 添加新增的视频文件
|
||||
vdpFileList.value.forEach((file: any) => {
|
||||
if (file.originFileObj) {
|
||||
formData.append('vdFiles', file.originFileObj);
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
};
|
||||
|
||||
// 时间字段选择时仅到年月日,提交时统一补上时分秒
|
||||
const formatDateTime = (val: any): any => {
|
||||
if (val === null || val === undefined || val === '') return val;
|
||||
const str = String(val);
|
||||
return str.length === 10 ? `${str} 00:00:00` : str;
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async (source: string) => {
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
// 编辑时只传修改过的字段
|
||||
const engInfo: Record<string, any> = {};
|
||||
engInfo.id = formData.value.id;
|
||||
changeOrder.value.forEach(item => {
|
||||
// select 清空后值为 undefined/null,统一转为空字符串,避免序列化时字段被省略
|
||||
let val = formData.value[item.field];
|
||||
if (val === null || val === undefined) {
|
||||
val = '';
|
||||
}
|
||||
engInfo[item.field] = val;
|
||||
});
|
||||
// 保留剩余的已有图片 id
|
||||
const existingPicIds = picFileList.value
|
||||
.filter((f: any) => f.isExisting)
|
||||
.map((f: any) => f.uid);
|
||||
if (existingPicIds.length > 0) {
|
||||
engInfo.firstimgurl = existingPicIds.join(',');
|
||||
} else {
|
||||
delete engInfo.firstimgurl;
|
||||
}
|
||||
// 保留剩余的已有视频 id
|
||||
const existingVdpIds = vdpFileList.value
|
||||
.filter((f: any) => f.isExisting)
|
||||
.map((f: any) => f.uid);
|
||||
if (existingVdpIds.length > 0) {
|
||||
engInfo.videourl = existingVdpIds.join(',');
|
||||
} else {
|
||||
delete engInfo.videourl;
|
||||
}
|
||||
// 时间字段统一补时分秒
|
||||
if (engInfo.tm) {
|
||||
engInfo.tm = formatDateTime(engInfo.tm);
|
||||
}
|
||||
const fd = buildFormData(engInfo, source);
|
||||
const res = await updateFpssrlAiRInfo(fd);
|
||||
if (res?.code == 0 || res?.success) {
|
||||
message.success('编辑成功');
|
||||
confirmModalVisible.value = false;
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} else {
|
||||
message.error(res?.msg || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('提交失败,请重试');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
confirmModalVisible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
confirmModalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="approval-search">
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleResetWrapper"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
><template #jcdt>
|
||||
<a-date-picker
|
||||
class="w-[120px]"
|
||||
v-model:value="jcdt.min"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="起始时间"
|
||||
:allow-clear="false"
|
||||
:showToday="false"
|
||||
>
|
||||
<template #renderExtraFooter>
|
||||
<div class="flex items-center flex-wrap px-2">
|
||||
<span
|
||||
v-for="item in DateSetting.RangeButton.days1"
|
||||
:key="item"
|
||||
@click="handleRangeClick(item)"
|
||||
>
|
||||
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
至
|
||||
<a-form-item-rest>
|
||||
<a-date-picker
|
||||
class="w-[120px]"
|
||||
:allow-clear="false"
|
||||
v-model:value="jcdt.max"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:disabledDate="disabledEndDate"
|
||||
placeholder="结束时间"
|
||||
:showToday="false"
|
||||
>
|
||||
<template #renderExtraFooter>
|
||||
<div class="flex items-center flex-wrap px-2">
|
||||
<span
|
||||
v-for="item in DateSetting.RangeButton.days1"
|
||||
:key="item"
|
||||
@click="handleRangeClick(item)"
|
||||
>
|
||||
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="seeEdit">查看修改</a-button>
|
||||
<a-button danger :disabled="!selectedCount" @click="batchDelete"
|
||||
>批量删除</a-button
|
||||
>
|
||||
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
import { useTimeScale } from '@/store/composables/useTimeScale';
|
||||
import { getFishReleaseMonitorSectionList } from '@/api/DataQueryMenuModule';
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'export-btn'): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
(e: 'seeEdit'): void;
|
||||
(e: 'batch-delete'): void;
|
||||
}>();
|
||||
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
selectedCount?: number;
|
||||
}>();
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
rstcd: null,
|
||||
stcd: null,
|
||||
mway: '2',
|
||||
sttpCode: null
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
|
||||
const {
|
||||
jcdt,
|
||||
currentTimeScale,
|
||||
handleRangeClick,
|
||||
disabledEndDate,
|
||||
handleSearchFinish,
|
||||
handleValuesChange,
|
||||
init,
|
||||
setDefaultTimeRange
|
||||
} = useTimeScale({
|
||||
defaultTimeScale: initSearchData.timeScale,
|
||||
onSearchFinish: values => emit('search-finish', values),
|
||||
onReset: () => emit('reset', initSearchData)
|
||||
});
|
||||
const crossSectionListLoading = ref<any>(false);
|
||||
const crossSectionList = ref<any>([]);
|
||||
const facilityTypeOptions = ref<any[]>([]);
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'waterStation',
|
||||
name: 'rvcd',
|
||||
label: '流域',
|
||||
placeholder: '请输入流域名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Select',
|
||||
name: 'sttpCode',
|
||||
label: '设施类型',
|
||||
width: 140,
|
||||
placeholder: '请选择设施类型',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: facilityTypeOptions.value.map(item => ({
|
||||
label: item.dictName,
|
||||
value: item.itemCode
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'Select',
|
||||
name: 'stcd',
|
||||
label: '设施名称',
|
||||
width: 200,
|
||||
placeholder: '请选择过鱼设施名称',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
loading: crossSectionListLoading.value,
|
||||
options: crossSectionList.value.map(item => ({
|
||||
label: item.stnm,
|
||||
value: item.stcd
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'jcdt',
|
||||
label: '时间',
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
handleSearchFinish(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
emit('export-btn');
|
||||
};
|
||||
const seeEdit = () => {
|
||||
emit('seeEdit');
|
||||
};
|
||||
const batchDelete = () => {
|
||||
emit('batch-delete');
|
||||
};
|
||||
const initCrossSectionList = params => {
|
||||
const filters = [];
|
||||
if (params) {
|
||||
if (params.rvcd != '' && params.rvcd != 'all') {
|
||||
filters.push({
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: params.rvcd
|
||||
});
|
||||
}
|
||||
if (params.rstcd != '' && params.rstcd != null) {
|
||||
filters.push({
|
||||
field: 'rstcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: params.rstcd
|
||||
});
|
||||
}
|
||||
if (params.sttpCode != '' && params.sttpCode != null) {
|
||||
filters.push({
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: params.sttpCode
|
||||
});
|
||||
}
|
||||
}
|
||||
crossSectionListLoading.value = true;
|
||||
return getFishReleaseMonitorSectionList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: filters
|
||||
},
|
||||
select: ['stcd', 'stnm', 'wtDeviceType', 'dtinType', 'mway']
|
||||
}).then(res => {
|
||||
if (res.data?.data) {
|
||||
crossSectionList.value = res.data?.data;
|
||||
}
|
||||
// 设施名称固定选中第一条数据
|
||||
if (crossSectionList.value.length > 0) {
|
||||
if (basicSearchRef.value?.formData) {
|
||||
basicSearchRef.value.formData.stcd = crossSectionList.value[0].stcd;
|
||||
}
|
||||
}
|
||||
crossSectionListLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
// 当流域、断面或设施类型变化时,清空过鱼设施选中并重新请求列表
|
||||
if (
|
||||
'rvcd' in changedValues ||
|
||||
'rstcd' in changedValues ||
|
||||
'sttpCode' in changedValues
|
||||
) {
|
||||
allValues.stcd = null;
|
||||
if (basicSearchRef.value?.formData) {
|
||||
basicSearchRef.value.formData.stcd = null;
|
||||
}
|
||||
initCrossSectionList(allValues);
|
||||
}
|
||||
|
||||
handleValuesChange(changedValues, allValues);
|
||||
searchData.value = { ...searchData.value, ...allValues };
|
||||
};
|
||||
|
||||
const handleResetWrapper = async () => {
|
||||
// 手动重置内部状态,避免触发两次请求
|
||||
currentTimeScale.value = initSearchData.timeScale;
|
||||
const now = dayjs();
|
||||
jcdt.value = {
|
||||
min: `${now
|
||||
.subtract(5, 'year')
|
||||
.startOf('year')
|
||||
.format('YYYY-MM-DD')} 00:00:00`,
|
||||
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
|
||||
};
|
||||
setDefaultTimeRange(initSearchData.timeScale);
|
||||
// 重置后重新加载设施列表并选中第一条
|
||||
await initCrossSectionList();
|
||||
// 触发搜索,使用当前状态而非 initSearchData
|
||||
handleSearchFinish({
|
||||
...initSearchData,
|
||||
...(basicSearchRef.value?.formData || {})
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
btnLoading,
|
||||
searchData
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
init();
|
||||
// 设置默认年份:前一年到当前年
|
||||
const now = dayjs();
|
||||
jcdt.value = {
|
||||
min: `${now.subtract(5, 'year').format('YYYY-MM-DD')} 00:00:00`,
|
||||
max: `${now.format('YYYY-MM-DD')} 23:59:59`
|
||||
};
|
||||
// 设施类型字典
|
||||
getDictItemsByCode({ dictCode: 'facilityType' }).then(res => {
|
||||
facilityTypeOptions.value = res.data || [];
|
||||
});
|
||||
// 先加载设施列表(默认选中第一条),再发起初始查询
|
||||
await initCrossSectionList();
|
||||
// 使用 handleSearchFinish 确保包含 jcdt 等完整参数
|
||||
handleSearchFinish({
|
||||
...initSearchData,
|
||||
...(basicSearchRef.value?.formData || {})
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@ -1,25 +1,28 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
mway="2"
|
||||
:exportLoading="exportLoading"
|
||||
<FishSurveyDataSearch
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
@seeEdit="handleSeeEdit"
|
||||
@batch-delete="handleBatchDelete"
|
||||
:selected-count="selectedRowKeys.length"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:enable-row-selection="true"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:columns="columnsAuto"
|
||||
:list-url="getFpssrlAiRPageList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-button
|
||||
@ -64,7 +67,7 @@
|
||||
</a-modal>
|
||||
|
||||
<!-- 编辑 Modal -->
|
||||
<EditFishDataAutoModal
|
||||
<EditFishSurveyDataModal
|
||||
v-model:open="editVisible"
|
||||
:record="editRecord"
|
||||
@success="handleEditSuccess"
|
||||
@ -73,27 +76,31 @@
|
||||
<!-- 删除确认 Modal -->
|
||||
<DeleteConfirmModal
|
||||
ref="deleteModalRef"
|
||||
:delete-fn="deleteFpssrlFn"
|
||||
:label="'过鱼自动数据'"
|
||||
title="删除过鱼自动数据"
|
||||
:delete-fn="deleteFn"
|
||||
:label="deleteLabel"
|
||||
:title="'删除' + deleteLabel"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FPSS_A'" />
|
||||
<!-- 操作日志弹框 -->
|
||||
<OperationLogModal
|
||||
v-model:open="operationLogVisible"
|
||||
:table-name="operationTableName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import FishSurveyDataSearch from './fishSurveyDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseMonitorList, deleteFpssrlRInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
import EditFishDataAutoModal from './EditFishDataAutoModal.vue';
|
||||
import EditFishSurveyDataModal from './EditFishSurveyDataModal.vue';
|
||||
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
|
||||
import OperationLogModal from '@/components/OperationLogModal/index.vue';
|
||||
import { getFpssrlAiRPageList, deleteFpssrlAiRInfo } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const sort = ref<any>([
|
||||
{
|
||||
@ -123,11 +130,21 @@ const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 编辑/删除/日志相关
|
||||
// 编辑/删除相关
|
||||
const editVisible = ref(false);
|
||||
const editRecord = ref<any>(null);
|
||||
const deleteModalRef = ref();
|
||||
const operationLogVisible = ref(false);
|
||||
const deleteLabel = '过鱼自动数据';
|
||||
const operationTableName = 'SD_FPSSRL_AI_R';
|
||||
|
||||
// 多选相关
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const handleSelectionChange = (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
@ -149,7 +166,7 @@ const openVideoPreview = (url: string) => {
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
columnsAuto.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
@ -158,7 +175,7 @@ const tableScrollX = computed(() =>
|
||||
);
|
||||
|
||||
// 自动列
|
||||
const tableColumns = ref<any[]>([
|
||||
const columnsAuto: any[] = [
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
@ -177,7 +194,7 @@ const tableColumns = ref<any[]>([
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
@ -339,7 +356,7 @@ const tableColumns = ref<any[]>([
|
||||
fixed: 'right',
|
||||
width: 120
|
||||
}
|
||||
]);
|
||||
];
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
@ -362,7 +379,7 @@ const exportBtn = () => {
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_自动_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
fileName: `过鱼监测数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
@ -389,12 +406,12 @@ const initTable = (values: any) => {
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
values.sttpCode
|
||||
? {
|
||||
field: 'mway',
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
value: values.sttpCode
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
@ -441,10 +458,6 @@ const initTable = (values: any) => {
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
editRecord.value = { ...record };
|
||||
@ -457,19 +470,49 @@ const handleEditSuccess = () => {
|
||||
};
|
||||
|
||||
// 删除
|
||||
const deleteFpssrlFn = async (record: any, reason: string) => {
|
||||
return deleteFpssrlRInfo({
|
||||
ids: [record.id],
|
||||
const deleteFn = async (record: any, reason: string) => {
|
||||
const ids = record.ids || [record.id];
|
||||
return deleteFpssrlAiRInfo({
|
||||
ids,
|
||||
source: reason
|
||||
});
|
||||
};
|
||||
|
||||
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
|
||||
const refreshAfterDelete = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
initTable(currentSearchParams.value);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
deleteModalRef.value?.open(record, () => {
|
||||
initTable(currentSearchParams.value);
|
||||
refreshAfterDelete();
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.info('请先勾选要删除的数据');
|
||||
return;
|
||||
}
|
||||
deleteModalRef.value?.open(
|
||||
{
|
||||
ids: [...selectedRowKeys.value],
|
||||
stnm: `选中的 ${selectedRowKeys.value.length} 条数据`
|
||||
},
|
||||
() => {
|
||||
refreshAfterDelete();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSeeEdit = () => {
|
||||
operationLogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
@ -477,4 +520,9 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.body_one {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
}</style>
|
||||
@ -47,6 +47,12 @@ const dialog = ref(false);
|
||||
const treeRef = ref();
|
||||
const fishTreeRef = ref();
|
||||
const infoForm = ref();
|
||||
//所属组织树型下拉框字段映射
|
||||
const treeSelectProps = {
|
||||
value: 'id',
|
||||
label: 'orgname',
|
||||
children: 'childList'
|
||||
};
|
||||
//获取企业树形信息
|
||||
const treeloading = ref(true);
|
||||
function getTree() {
|
||||
@ -99,7 +105,8 @@ function addClick() {
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: []
|
||||
roleinfo: [],
|
||||
orgid: ''
|
||||
};
|
||||
getrole();
|
||||
}
|
||||
@ -178,7 +185,8 @@ const info = ref({
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: [] as any[]
|
||||
roleinfo: [] as any[],
|
||||
orgid: ''
|
||||
});
|
||||
//修改-用户
|
||||
function editdepartment(row: any) {
|
||||
@ -188,6 +196,8 @@ function editdepartment(row: any) {
|
||||
});
|
||||
info.value = JSON.parse(JSON.stringify(row));
|
||||
info.value.roleinfo = selectID;
|
||||
// 回显所属组织
|
||||
info.value.orgid = row.orgid;
|
||||
rolesdata.value = row.roles;
|
||||
title.value = '修改用户';
|
||||
dialogVisible.value = true;
|
||||
@ -207,7 +217,8 @@ function confirmClick(formEl: any) {
|
||||
nickname: info.value.nickname,
|
||||
email: info.value.email,
|
||||
phone: info.value.phone,
|
||||
orgid: orgId.value
|
||||
// 修改时根据所属组织树型下拉框传递
|
||||
orgid: info.value.orgid
|
||||
};
|
||||
const roleids = String(info.value.roleinfo);
|
||||
updataUser(params, roleids).then(() => {
|
||||
@ -787,6 +798,7 @@ function getrole() {
|
||||
rolename: ''
|
||||
};
|
||||
listGroupedByTenant(params).then(res => {
|
||||
// debugger
|
||||
rolesdata.value = res;
|
||||
});
|
||||
}
|
||||
@ -1250,14 +1262,14 @@ function handleClearSelection() {
|
||||
placeholder="请输入登录账号"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item label="所属角色">
|
||||
<el-select
|
||||
v-model="info.roleinfo"
|
||||
placeholder=" "
|
||||
style="width: 100%"
|
||||
multiple
|
||||
filterable
|
||||
multiple
|
||||
>
|
||||
<el-option-group
|
||||
v-for="group in rolesdata"
|
||||
@ -1273,6 +1285,17 @@ function handleClearSelection() {
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属组织" v-if="info.id">
|
||||
<el-tree-select
|
||||
v-model="info.orgid"
|
||||
:data="treedata"
|
||||
:props="treeSelectProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
placeholder="请选择所属组织"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span
|
||||
class="dialog-footer"
|
||||
|
||||
@ -47,6 +47,12 @@ const dialog = ref(false);
|
||||
const treeRef = ref();
|
||||
const fishTreeRef = ref();
|
||||
const infoForm = ref();
|
||||
//所属组织树型下拉框字段映射
|
||||
const treeSelectProps = {
|
||||
value: 'id',
|
||||
label: 'orgname',
|
||||
children: 'childList'
|
||||
};
|
||||
//获取企业树形信息
|
||||
const treeloading = ref(true);
|
||||
function getTree() {
|
||||
@ -99,7 +105,8 @@ function addClick() {
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: []
|
||||
roleinfo: [],
|
||||
orgid: ''
|
||||
};
|
||||
getrole();
|
||||
}
|
||||
@ -178,7 +185,8 @@ const info = ref({
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: [] as any[]
|
||||
roleinfo: [] as any[],
|
||||
orgid: ''
|
||||
});
|
||||
//修改-用户
|
||||
function editdepartment(row: any) {
|
||||
@ -188,6 +196,8 @@ function editdepartment(row: any) {
|
||||
});
|
||||
info.value = JSON.parse(JSON.stringify(row));
|
||||
info.value.roleinfo = selectID;
|
||||
// 回显所属组织
|
||||
info.value.orgid = row.orgid;
|
||||
rolesdata.value = row.roles;
|
||||
title.value = '修改用户';
|
||||
dialogVisible.value = true;
|
||||
@ -207,7 +217,8 @@ function confirmClick(formEl: any) {
|
||||
nickname: info.value.nickname,
|
||||
email: info.value.email,
|
||||
phone: info.value.phone,
|
||||
orgid: orgId.value
|
||||
// 修改时根据所属组织树型下拉框传递
|
||||
orgid: info.value.orgid
|
||||
};
|
||||
const roleids = String(info.value.roleinfo);
|
||||
updataUser(params, roleids).then(() => {
|
||||
@ -787,6 +798,7 @@ function getrole() {
|
||||
rolename: ''
|
||||
};
|
||||
listGroupedByTenant(params).then(res => {
|
||||
// debugger
|
||||
rolesdata.value = res;
|
||||
});
|
||||
}
|
||||
@ -1250,13 +1262,14 @@ function handleClearSelection() {
|
||||
placeholder="请输入登录账号"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属角色">
|
||||
|
||||
<el-form-item label="所属角色">
|
||||
<el-select
|
||||
v-model="info.roleinfo"
|
||||
placeholder=" "
|
||||
style="width: 100%"
|
||||
multiple
|
||||
filterable
|
||||
multiple
|
||||
>
|
||||
<el-option-group
|
||||
v-for="group in rolesdata"
|
||||
@ -1272,6 +1285,17 @@ function handleClearSelection() {
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属组织" v-if="info.id">
|
||||
<el-tree-select
|
||||
v-model="info.orgid"
|
||||
:data="treedata"
|
||||
:props="treeSelectProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
placeholder="请选择所属组织"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span
|
||||
class="dialog-footer"
|
||||
|
||||
@ -4,34 +4,11 @@
|
||||
<a-row class="w-[100%]" v-if="!loading">
|
||||
<a-col
|
||||
:span="!item.visible ? 24 : item.filed == '' ? 12 : 12"
|
||||
v-for="(item, index) in tabledata"
|
||||
v-for="item in tabledata"
|
||||
:key="item.key"
|
||||
>
|
||||
<div class="col-title" v-if="!item.visible">
|
||||
{{ item.name }}:
|
||||
<div v-if="modelStore.isBasicEdit && index == 0">
|
||||
<a-button
|
||||
v-if="!isEdit"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
@click="editBtn"
|
||||
>
|
||||
编辑信息
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
@click="saveBtn"
|
||||
>
|
||||
保存信息
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" @click="recordBtn">
|
||||
修改记录
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-left" v-if="item.visible && item.filed != ''">
|
||||
<a-tooltip :title="item.name.length > 10 ? item.name : ''"
|
||||
@ -41,50 +18,14 @@
|
||||
<div class="col-right" v-if="item.visible">
|
||||
<a-tooltip
|
||||
:title="
|
||||
!isEdit
|
||||
? item.vlsr || String(item.value).length > 10
|
||||
? item.vlsr
|
||||
? '数据来源:' + item.vlsr
|
||||
: item.value
|
||||
: ''
|
||||
item.vlsr || String(item.value).length > 10
|
||||
? item.vlsr
|
||||
? '数据来源:' + item.vlsr
|
||||
: item.value
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<span v-if="!isEdit && item.filed != ''">{{
|
||||
item.value || '-'
|
||||
}}</span>
|
||||
<span v-else>
|
||||
<!-- {{ item.value }} -->
|
||||
<a-input
|
||||
v-if="item.type == 'input'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
></a-input>
|
||||
<a-select
|
||||
v-else-if="item.type == 'select'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
class="w-full"
|
||||
></a-select>
|
||||
<!-- <div v-else-if="item.type == 'date'">{{ item.value }}2222</div> -->
|
||||
<a-date-picker
|
||||
v-else-if="item.type == 'date'"
|
||||
v-model:value="item.value"
|
||||
:format="item.format"
|
||||
:value-format="item.format"
|
||||
:picker="item.picker"
|
||||
class="w-full"
|
||||
></a-date-picker>
|
||||
<a-input-number
|
||||
v-else-if="item.type == 'number'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
class="w-full"
|
||||
></a-input-number>
|
||||
</span>
|
||||
<span>{{ item.value || '-' }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
@ -100,22 +41,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="修改记录"
|
||||
:footer="null"
|
||||
width="900px"
|
||||
:zIndex="2000"
|
||||
>
|
||||
123
|
||||
</a-modal>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs';
|
||||
import { ref, nextTick, watch, computed } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import {
|
||||
BasicColumns,
|
||||
wtPointColumns,
|
||||
@ -152,7 +84,6 @@ import { getStcdDetail } from '@/api/mapModal';
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
const modelStore = useModelStore();
|
||||
const hasLoaded = ref(false); // 标记是否已经请求过
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
@ -163,12 +94,10 @@ const props = defineProps({
|
||||
default: false
|
||||
}
|
||||
});
|
||||
const visible = ref(false);
|
||||
const isLogo = ref(false);
|
||||
const loading = ref(false);
|
||||
const data2: any = ref({});
|
||||
const tabledata = ref([]);
|
||||
useDraggable(visible, { boundary: true, resetOnOpen: true });
|
||||
const columnsConfig = ref([
|
||||
{
|
||||
type: 'ENG',
|
||||
@ -337,22 +266,6 @@ const columnsConfig = ref([
|
||||
|
||||
//DW_1
|
||||
]);
|
||||
const isEdit = ref(false);
|
||||
const editBtn = () => {
|
||||
nextTick(() => {
|
||||
isEdit.value = true;
|
||||
tabledata.value.forEach(item => {
|
||||
if (item.type == 'date')
|
||||
item.value = dayjs(item.value).format(item.format);
|
||||
});
|
||||
});
|
||||
};
|
||||
const saveBtn = () => {
|
||||
isEdit.value = false;
|
||||
};
|
||||
const recordBtn = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
const getData = async () => {
|
||||
if (hasLoaded.value) return;
|
||||
loading.value = true;
|
||||
|
||||
@ -598,7 +598,16 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
|
||||
) {
|
||||
tabsConfig.value = newTabs;
|
||||
} else {
|
||||
tabsConfig.value = newTabs.filter((item: any) => item.type != 'video');
|
||||
let arr = ['DW', 'DW_1', 'DW_2', 'DW_3', 'DW_4', 'DW_5', 'DW_6'];
|
||||
if (arr.includes(modelStore.params.sttp)) {
|
||||
tabsConfig.value = newTabs.filter(
|
||||
(item: any) => item.type == 'basic'
|
||||
);
|
||||
} else {
|
||||
tabsConfig.value = newTabs.filter(
|
||||
(item: any) => item.type != 'video'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('QGC Tab 过滤失败:', error);
|
||||
|
||||
@ -221,7 +221,6 @@ const handleItemClick = (item: MediaItem) => {
|
||||
modelStore.params.sttp = 'WVA';
|
||||
modelStore.params.stcd = item.stcd;
|
||||
modelStore.title = `${item.stnm} 野生动物监测`;
|
||||
modelStore.isBasicEdit = true;
|
||||
};
|
||||
|
||||
// ==================== 获取站点列表 ====================
|
||||
|
||||
@ -550,7 +550,6 @@ const initChart = () => {
|
||||
modelStore.params.sttp = 'WTRV';
|
||||
modelStore.params.enfc = '1';
|
||||
modelStore.title = stnm;
|
||||
// modelStore.isBasicEdit = true;
|
||||
modelStore.params.stcd = stcd;
|
||||
modelStore.filter.rangeTm = [startTime, endTime];
|
||||
|
||||
|
||||
@ -276,7 +276,7 @@ const fetchFacilityStats = async () => {
|
||||
"field": "bldsttCcode",
|
||||
"operator": "eq",
|
||||
"dataType": "string",
|
||||
"value": "2"
|
||||
"value": formState.bldstt
|
||||
} : null,
|
||||
formState.stnm ? {
|
||||
"field": "stnm",
|
||||
@ -334,6 +334,7 @@ const fetchBuildStateList = async () => {
|
||||
value: item.itemCode
|
||||
}))
|
||||
buildStateList.value.unshift({ label: '全部', value: '' })
|
||||
// debugger
|
||||
// 临时模拟数据(实际使用时删除)
|
||||
// buildStateList.value = [
|
||||
// { label: '全部', value: '' },
|
||||
@ -399,7 +400,7 @@ const handleSearch = () => {
|
||||
"field": "bldsttCcode",
|
||||
"operator": "eq",
|
||||
"dataType": "string",
|
||||
"value": "2"
|
||||
"value": formState.bldstt
|
||||
} : null,
|
||||
formState.stnm ? {
|
||||
"field": "stnm",
|
||||
|
||||
@ -347,7 +347,6 @@ const handleViewDetail = (record: any, type: any) => {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params.sttp = 'VD_FBFM';
|
||||
modelStore.title = '视频监控站详情信息';
|
||||
modelStore.isBasicEdit = true;
|
||||
modelStore.params.stcd = record.stcd;
|
||||
}
|
||||
}
|
||||
|
||||
@ -519,7 +519,6 @@ const handleViewDetail = (record: any) => {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params.sttp = "WT";
|
||||
modelStore.title = record.stnm ;
|
||||
// modelStore.isBasicEdit = true;
|
||||
if (tabIndex.value == "27") {
|
||||
modelStore.params.enfc = "1";
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import * as echarts from 'echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { qgcGetKendoListCust } from '@/api/sz';
|
||||
import { getMonitorDataWaterQualityDetail } from '@/api/mapModal/index';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import {
|
||||
@ -101,49 +102,23 @@ const indicatorConfig = ref<
|
||||
}>
|
||||
>([]);
|
||||
|
||||
// ==================== HSL 随机颜色生成 ====================
|
||||
// ==================== 指标颜色 ====================
|
||||
// 参考 MonitorInfo.vue 的固定色板,按指标顺序循环取色,颜色稳定可预期
|
||||
const COLOR_PALETTE = [
|
||||
'#56C2E3',
|
||||
'#7399C6',
|
||||
'#4B79AB',
|
||||
'#78C300',
|
||||
'#00A050',
|
||||
'#F7A737'
|
||||
];
|
||||
const generateRandomColor = (index: number): string => {
|
||||
// 使用黄金角度确保颜色均匀分布
|
||||
const goldenAngle = 137.508;
|
||||
const hue = (index * goldenAngle) % 360;
|
||||
|
||||
// 饱和度控制在 45%-65% 之间
|
||||
const saturation = 45 + Math.random() * 20;
|
||||
|
||||
// 亮度控制在 45%-60% 之间,避免太亮或太暗
|
||||
const lightness = 45 + Math.random() * 15;
|
||||
|
||||
// HSL 转 RGB
|
||||
const c = ((1 - Math.abs((2 * lightness) / 100 - 1)) * saturation) / 100;
|
||||
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
||||
const m = lightness / 100 - c / 2;
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
|
||||
if (hue < 60) {
|
||||
[r, g, b] = [c, x, 0];
|
||||
} else if (hue < 120) {
|
||||
[r, g, b] = [x, c, 0];
|
||||
} else if (hue < 180) {
|
||||
[r, g, b] = [0, c, x];
|
||||
} else if (hue < 240) {
|
||||
[r, g, b] = [0, x, c];
|
||||
} else if (hue < 300) {
|
||||
[r, g, b] = [x, 0, c];
|
||||
} else {
|
||||
[r, g, b] = [c, 0, x];
|
||||
}
|
||||
|
||||
const toHex = (value: number) => {
|
||||
const hex = Math.round((value + m) * 255).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
return COLOR_PALETTE[index % COLOR_PALETTE.length];
|
||||
};
|
||||
|
||||
// 指标颜色映射(来自水质参数接口 showControl.lineColor,key 为指标小写编码)
|
||||
const indicatorColorMap = ref<Record<string, string>>({});
|
||||
|
||||
// ==================== 动态指标筛选和配置生成 ====================
|
||||
const generateIndicatorConfig = (data: StationData[]) => {
|
||||
if (!data || data.length === 0) {
|
||||
@ -216,54 +191,15 @@ const generateIndicatorConfig = (data: StationData[]) => {
|
||||
.filter(ind => validKeysSet.has(ind.key))
|
||||
.sort((a, b) => a.sort - b.sort);
|
||||
|
||||
// 生成最终配置
|
||||
// 生成最终配置:颜色优先取接口配置,缺失时回退到色板
|
||||
indicatorConfig.value = validIndicators.map((ind, index) => ({
|
||||
key: ind.key,
|
||||
name: ind.name,
|
||||
unit: ind.description || '',
|
||||
color: generateRandomColor(index),
|
||||
color: indicatorColorMap.value[ind.key] || generateRandomColor(index),
|
||||
sort: ind.sort
|
||||
}));
|
||||
|
||||
console.log('动态生成指标配置:', indicatorConfig.value);
|
||||
};
|
||||
|
||||
// ==================== 模拟数据 ====================
|
||||
const generateMockData = () => {
|
||||
const stations = [
|
||||
{ stnm: '两河口', stcd: 'ST001', type: '0' },
|
||||
{ stnm: '锦屏一级', stcd: 'ST002', type: '1' },
|
||||
{ stnm: '桐子林', stcd: 'ST003', type: '0' },
|
||||
{ stnm: '溪洛渡', stcd: 'ST004', type: '0' },
|
||||
{ stnm: '向家坝', stcd: 'ST005', type: '1' },
|
||||
{ stnm: '三峡', stcd: 'ST006', type: '0' },
|
||||
{ stnm: '葛洲坝', stcd: 'ST007', type: '0' }
|
||||
];
|
||||
|
||||
const tm = '2024-01-15 08:00:00';
|
||||
|
||||
// 模拟限值数据
|
||||
const mockLimits = {
|
||||
ph: { min: 6, max: 9 },
|
||||
dox: { min: 6 },
|
||||
codmn: { max: 4 },
|
||||
nh3n: { max: 0.5 },
|
||||
tp: { max: 0.1 },
|
||||
tn: { max: 1.0 }
|
||||
};
|
||||
|
||||
return stations.map((station, index) => ({
|
||||
...station,
|
||||
tm,
|
||||
SORT: index + 1,
|
||||
ph: 7.2 + Math.random() * 0.8,
|
||||
dox: 6.5 + Math.random() * 2.5,
|
||||
codmn: 1.5 + Math.random() * 2.0,
|
||||
nh3n: 0.15 + Math.random() * 0.35,
|
||||
tp: 0.05 + Math.random() * 0.15,
|
||||
tn: 0.8 + Math.random() * 1.2,
|
||||
limits: mockLimits // 新增:存储该站点的所有限值
|
||||
}));
|
||||
};
|
||||
|
||||
// 定义数据类型接口
|
||||
@ -273,44 +209,9 @@ interface StationData {
|
||||
type: string;
|
||||
tm: string;
|
||||
SORT: number;
|
||||
// 所有水质指标字段(动态)
|
||||
ph?: number | null;
|
||||
dox?: number | null;
|
||||
codmn?: number | null;
|
||||
codcr?: number | null;
|
||||
bod5?: number | null;
|
||||
nh3n?: number | null;
|
||||
tp?: number | null;
|
||||
tn?: number | null;
|
||||
cu?: number | null;
|
||||
zn?: number | null;
|
||||
f?: number | null;
|
||||
se?: number | null;
|
||||
ars?: number | null;
|
||||
hg?: number | null;
|
||||
cd?: number | null;
|
||||
cr6?: number | null;
|
||||
pb?: number | null;
|
||||
cn?: number | null;
|
||||
vlph?: number | null;
|
||||
oil?: number | null;
|
||||
las?: number | null;
|
||||
s2?: number | null;
|
||||
fcg?: number | null;
|
||||
cl?: number | null;
|
||||
so4?: number | null;
|
||||
no3?: number | null;
|
||||
thrd?: number | null;
|
||||
cond?: number | null;
|
||||
fe?: number | null;
|
||||
mn?: number | null;
|
||||
al?: number | null;
|
||||
chla?: number | null;
|
||||
clarity?: number | null;
|
||||
tu?: number | null;
|
||||
wtmp?: number | null;
|
||||
limits: Record<string, { min?: number; max?: number }>;
|
||||
[key: string]: any; // 允许任意水质指标字段
|
||||
// 水质指标字段为动态生成,通过索引签名访问
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const mockData = ref<StationData[]>([]);
|
||||
@ -637,17 +538,11 @@ const handleLegendSelectChanged = (params: any) => {
|
||||
const clickedName = name;
|
||||
const isNowSelected = selected[clickedName];
|
||||
|
||||
console.log(
|
||||
`图例点击: ${clickedName}, 当前状态: ${isNowSelected ? '显示' : '隐藏'}`
|
||||
);
|
||||
console.log('当前可见队列:', visibleSeriesQueue.value);
|
||||
|
||||
if (isNowSelected) {
|
||||
// 用户想要显示这个系列
|
||||
if (visibleSeriesQueue.value.length >= 2) {
|
||||
// 已满2个,移除最早的(队列头部)
|
||||
const removed = visibleSeriesQueue.value.shift();
|
||||
console.log(`已达上限,移除最早系列: ${removed}`);
|
||||
visibleSeriesQueue.value.shift();
|
||||
}
|
||||
// 添加到队列尾部
|
||||
if (!visibleSeriesQueue.value.includes(clickedName)) {
|
||||
@ -658,135 +553,23 @@ const handleLegendSelectChanged = (params: any) => {
|
||||
const index = visibleSeriesQueue.value.indexOf(clickedName);
|
||||
if (index > -1) {
|
||||
visibleSeriesQueue.value.splice(index, 1);
|
||||
console.log(`从队列中移除: ${clickedName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('更新后可见队列:', visibleSeriesQueue.value);
|
||||
|
||||
// 根据队列构建最终的 selected 状态
|
||||
const finalSelected: Record<string, boolean> = {};
|
||||
indicatorConfig.value.forEach(config => {
|
||||
finalSelected[config.name] = visibleSeriesQueue.value.includes(config.name);
|
||||
});
|
||||
|
||||
// 深拷贝当前配置
|
||||
const currentOption = JSON.parse(JSON.stringify(chartInstance.getOption()));
|
||||
|
||||
// 同步图例选中状态
|
||||
currentOption.legend[0].selected = finalSelected;
|
||||
|
||||
// 根据图例状态更新 Y 轴显隐
|
||||
const newYAxis = currentOption.yAxis.map((item: any) => {
|
||||
let isShow = false;
|
||||
let displayIndex = 0;
|
||||
|
||||
// 计算这是第几个显示的Y轴
|
||||
for (const key in finalSelected) {
|
||||
if (finalSelected[key]) {
|
||||
const config = indicatorConfig.value.find(c => c.name === key);
|
||||
if (config && config.name === item.name) {
|
||||
isShow = true;
|
||||
break;
|
||||
}
|
||||
displayIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// 最多只显示2个Y轴
|
||||
if (!isShow || displayIndex >= 2) {
|
||||
return { ...item, show: false };
|
||||
}
|
||||
|
||||
return { ...item, show: true };
|
||||
});
|
||||
|
||||
currentOption.yAxis = newYAxis;
|
||||
|
||||
// 动态调整 Y 轴布局
|
||||
yAxisShowDynamic(finalSelected, currentOption);
|
||||
|
||||
// 完全替换模式更新
|
||||
// 基于最新队列重新生成完整配置(所有 y 轴都带颜色/formatter/min-max,
|
||||
// 避免从 getOption() 快照激活隐藏 y 轴时丢失样式导致颜色消失、宽度变化)
|
||||
try {
|
||||
chartInstance.setOption(currentOption, true);
|
||||
console.log('图表配置已更新');
|
||||
const option = getChartOption();
|
||||
chartInstance.setOption(option, true);
|
||||
} catch (error) {
|
||||
console.error('图表更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Y轴动态布局调整 ====================
|
||||
const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
|
||||
const allShow = options.yAxis?.filter((item: any) => item.show);
|
||||
const showCount = allShow?.length || 0;
|
||||
|
||||
// 没有显示的Y轴
|
||||
if (showCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有一个Y轴:统一置于左侧
|
||||
if (showCount === 1) {
|
||||
delete options.grid.right;
|
||||
options.grid.left = '80px';
|
||||
|
||||
options.yAxis = options.yAxis.map((item: any) => {
|
||||
if (item.show) {
|
||||
return {
|
||||
...item,
|
||||
position: 'left',
|
||||
offset: 0
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 两个Y轴:左侧1个,右侧1个
|
||||
if (showCount >= 2) {
|
||||
let leftAssigned = false;
|
||||
let rightAssigned = false;
|
||||
|
||||
options.yAxis = options.yAxis.map((item: any) => {
|
||||
if (!item.show) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// 第一个可见的Y轴放在左侧
|
||||
if (!leftAssigned) {
|
||||
leftAssigned = true;
|
||||
delete options.grid.right;
|
||||
options.grid.left = '80px';
|
||||
|
||||
return {
|
||||
...item,
|
||||
position: 'left',
|
||||
offset: 0
|
||||
};
|
||||
} else if (!rightAssigned) {
|
||||
// 第二个Y轴放在右侧
|
||||
rightAssigned = true;
|
||||
options.grid.right = '60px';
|
||||
return {
|
||||
...item,
|
||||
position: 'right',
|
||||
offset: 0
|
||||
};
|
||||
} else {
|
||||
// 第三个及以后的Y轴隐藏(理论上不应该到这里)
|
||||
return {
|
||||
...item,
|
||||
show: false
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 数据点点击事件处理 ====================
|
||||
const handleDataPointClick = (params: any) => {
|
||||
if (!params || !params.dataIndex) return;
|
||||
// dataIndex 为 0 时不能直接取反判断,必须判断 null/undefined
|
||||
if (!params || params.dataIndex == null) return;
|
||||
|
||||
// 获取点击的数据点对应的站点信息
|
||||
const dataIndex = params.dataIndex;
|
||||
@ -796,7 +579,6 @@ const handleDataPointClick = (params: any) => {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params.sttp = 'WQFB';
|
||||
modelStore.title = stationData.stnm;
|
||||
// modelStore.isBasicEdit = true;
|
||||
modelStore.params.stcd = stationData.stcd;
|
||||
modelStore.filter.rangeTm = [
|
||||
dayjs(datetimePicker.value.value)
|
||||
@ -821,8 +603,6 @@ const getecharts = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始获取图表数据,select.value:', select.value.value);
|
||||
|
||||
// 设置 loading 状态
|
||||
loading.value = true;
|
||||
hasData.value = false;
|
||||
@ -882,8 +662,6 @@ const getecharts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('原始数据条数:', apiData.length);
|
||||
|
||||
// 过滤出有水质数据的站点(sttp === 'WQ')并按sort排序
|
||||
const filteredData = apiData
|
||||
.filter((item: any) => item.sttp === 'WQ')
|
||||
@ -931,47 +709,20 @@ const getecharts = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 动态收集指标字段(基于 indicators 配置,避免硬编码 30+ 个字段映射)
|
||||
const indicatorValues: Record<string, number | null> = {};
|
||||
indicators.forEach(ind => {
|
||||
const val = item[ind.key];
|
||||
indicatorValues[ind.key] = val != null ? Number(val) : null;
|
||||
});
|
||||
|
||||
return {
|
||||
stnm: stnm,
|
||||
stcd: item.stcd || '',
|
||||
type: item.rstcd ? '1' : '0', // 有rstcd表示是电站出库站
|
||||
tm: item.tm || '',
|
||||
SORT: index + 1,
|
||||
ph: item.ph != null ? Number(item.ph) : null,
|
||||
dox: item.dox != null ? Number(item.dox) : null,
|
||||
codmn: item.codmn != null ? Number(item.codmn) : null,
|
||||
codcr: item.codcr != null ? Number(item.codcr) : null,
|
||||
bod5: item.bod5 != null ? Number(item.bod5) : null,
|
||||
nh3n: item.nh3n != null ? Number(item.nh3n) : null,
|
||||
tp: item.tp != null ? Number(item.tp) : null,
|
||||
tn: item.tn != null ? Number(item.tn) : null,
|
||||
cu: item.cu != null ? Number(item.cu) : null,
|
||||
zn: item.zn != null ? Number(item.zn) : null,
|
||||
f: item.f != null ? Number(item.f) : null,
|
||||
se: item.se != null ? Number(item.se) : null,
|
||||
ars: item.ars != null ? Number(item.ars) : null,
|
||||
hg: item.hg != null ? Number(item.hg) : null,
|
||||
cd: item.cd != null ? Number(item.cd) : null,
|
||||
cr6: item.cr6 != null ? Number(item.cr6) : null,
|
||||
pb: item.pb != null ? Number(item.pb) : null,
|
||||
cn: item.cn != null ? Number(item.cn) : null,
|
||||
vlph: item.vlph != null ? Number(item.vlph) : null,
|
||||
oil: item.oil != null ? Number(item.oil) : null,
|
||||
las: item.las != null ? Number(item.las) : null,
|
||||
s2: item.s2 != null ? Number(item.s2) : null,
|
||||
fcg: item.fcg != null ? Number(item.fcg) : null,
|
||||
cl: item.cl != null ? Number(item.cl) : null,
|
||||
so4: item.so4 != null ? Number(item.so4) : null,
|
||||
no3: item.no3 != null ? Number(item.no3) : null,
|
||||
thrd: item.thrd != null ? Number(item.thrd) : null,
|
||||
cond: item.cond != null ? Number(item.cond) : null,
|
||||
fe: item.fe != null ? Number(item.fe) : null,
|
||||
mn: item.mn != null ? Number(item.mn) : null,
|
||||
al: item.al != null ? Number(item.al) : null,
|
||||
chla: item.chla != null ? Number(item.chla) : null,
|
||||
clarity: item.clarity != null ? Number(item.clarity) : null,
|
||||
tu: item.tu != null ? Number(item.tu) : null,
|
||||
wtmp: item.wtmp != null ? Number(item.wtmp) : null,
|
||||
...indicatorValues,
|
||||
limits: limits // 新增:存储该站点的所有限值
|
||||
};
|
||||
});
|
||||
@ -992,12 +743,42 @@ const getecharts = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取水质参数配置(showControl.lineColor),用于指标颜色
|
||||
// 参考 WaterQuality.vue:颜色来自接口返回的每个参数配置
|
||||
try {
|
||||
const configStcd = modelStore.params.stcd || chartData[0]?.stcd;
|
||||
if (configStcd) {
|
||||
const detailRes = await getMonitorDataWaterQualityDetail({
|
||||
stcd: configStcd,
|
||||
tbCode: 'WQ_R',
|
||||
startTime: getStartTime(),
|
||||
endTime: getEndTime()
|
||||
});
|
||||
const detailData = detailRes?.data?.data || detailRes?.data || [];
|
||||
const map: Record<string, string> = {};
|
||||
detailData.forEach((param: any) => {
|
||||
if (!param.ys) return;
|
||||
try {
|
||||
const config = JSON.parse(param.showControl || '{}');
|
||||
if (config.lineColor) {
|
||||
map[param.ys.toLowerCase()] = config.lineColor;
|
||||
}
|
||||
} catch {
|
||||
// 解析失败忽略该参数
|
||||
}
|
||||
});
|
||||
indicatorColorMap.value = map;
|
||||
}
|
||||
} catch (error) {
|
||||
// 配置获取失败不影响主流程,颜色走色板回退
|
||||
console.error('获取水质参数配置失败:', error);
|
||||
}
|
||||
|
||||
// 动态生成指标配置
|
||||
generateIndicatorConfig(chartData);
|
||||
|
||||
// 如果没有有效指标,显示空状态
|
||||
if (indicatorConfig.value.length === 0) {
|
||||
console.log('没有有效指标,显示空状态');
|
||||
hasData.value = false;
|
||||
chartInstance?.clear();
|
||||
loading.value = false;
|
||||
@ -1101,7 +882,6 @@ const initChart = async () => {
|
||||
|
||||
// 初始化可见队列:默认只显示第一个系列(按sort排序后的第一个)
|
||||
visibleSeriesQueue.value = [indicatorConfig.value[0].name];
|
||||
console.log('初始化可见队列:', visibleSeriesQueue.value);
|
||||
|
||||
// 初始化 ECharts 实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
@ -1178,7 +958,6 @@ const wbsCode = ref('');
|
||||
watch(
|
||||
() => stationSource.value,
|
||||
newVal => {
|
||||
console.log(newVal);
|
||||
wbsCode.value = newVal.wbsCode;
|
||||
select.value.value = newVal.wbsCode;
|
||||
getecharts();
|
||||
@ -1199,22 +978,7 @@ watch(
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 监听可见系列数量变化,重新设置图表配置以确保 tooltip formatter 使用最新的 visibleSeriesQueue 值
|
||||
watch(
|
||||
() => visibleSeriesQueue.value.length,
|
||||
(newLength, oldLength) => {
|
||||
// 只有当长度真正变化时才重新设置
|
||||
if (
|
||||
newLength !== oldLength &&
|
||||
chartInstance &&
|
||||
indicatorConfig.value.length > 0
|
||||
) {
|
||||
// 重新生成 option(这会创建新的 formatter 闭包,捕获最新的 visibleSeriesQueue)
|
||||
const newOption = getChartOption();
|
||||
chartInstance.setOption(newOption, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
// 图例切换时 handleLegendSelectChanged 已基于最新队列重建完整配置,此处无需再监听
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,404 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
mway="2"
|
||||
:exportLoading="exportLoading"
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 图片预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
title="图片预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 视频预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="videoPreviewVisible"
|
||||
title="视频预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<video
|
||||
:src="previewVideoUrl"
|
||||
controls
|
||||
style="width: 100%; height: 600px"
|
||||
></video>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseMonitorList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const sort = ref<any>([
|
||||
{
|
||||
field: 'baseStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rvcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rstcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'ttpwr',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'desc'
|
||||
}
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
// 视频预览
|
||||
const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
// 自动列
|
||||
const tableColumns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'length',
|
||||
title: '鱼长度(cm)',
|
||||
dataIndex: 'length',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'width',
|
||||
title: '鱼宽度(cm)',
|
||||
dataIndex: 'width',
|
||||
sort: true,
|
||||
width: 120,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fishspeed',
|
||||
title: '鱼速度(m/s)',
|
||||
dataIndex: 'fishspeed',
|
||||
sort: true,
|
||||
width: 80,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
title: '水温(℃)',
|
||||
dataIndex: 'temperature',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'speed',
|
||||
title: '流速(m/s)',
|
||||
dataIndex: 'speed',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'channelno',
|
||||
title: '过鱼通道',
|
||||
dataIndex: 'channelno',
|
||||
sort: true,
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '轮廓图',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '短视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
if (exportLoading.value) return;
|
||||
|
||||
exportLoading.value = true;
|
||||
searchRef.value.btnLoading = true;
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_自动_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
? {
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
if (values.stcd) {
|
||||
filters.push({
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@ -1,369 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
mway="1"
|
||||
:exportLoading="exportLoading"
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 图片预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
title="图片预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 视频预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="videoPreviewVisible"
|
||||
title="视频预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<video
|
||||
:src="previewVideoUrl"
|
||||
controls
|
||||
style="width: 100%; height: 600px"
|
||||
></video>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseMonitorList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const sort = ref<any>([
|
||||
{
|
||||
field: 'baseStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rvcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rstcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'ttpwr',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'desc'
|
||||
}
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
// 视频预览
|
||||
const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
// 人工列
|
||||
const tableColumns = ref<any[]>([
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'strdt',
|
||||
title: '开始时间',
|
||||
dataIndex: 'strdt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'enddt',
|
||||
title: '结束时间',
|
||||
dataIndex: 'enddt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '填报时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fsz',
|
||||
title: '鱼类规格',
|
||||
dataIndex: 'fsz',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '图片',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
if (exportLoading.value) return;
|
||||
|
||||
exportLoading.value = true;
|
||||
searchRef.value.btnLoading = true;
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_人工_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
? {
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
if (values.stcd) {
|
||||
filters.push({
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@ -80,13 +80,12 @@ const btnLoading = ref<boolean>(false);
|
||||
|
||||
const props = defineProps<{
|
||||
exportLoading?: boolean;
|
||||
mway: string;
|
||||
}>();
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
rstcd: null,
|
||||
stcd: '0086601020VP000006',
|
||||
mway: props.mway
|
||||
mway: '1'
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
@ -134,6 +133,22 @@ const searchList: any = computed(() => [
|
||||
value: item.stcd
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'Radio',
|
||||
name: 'mway',
|
||||
label: '数据类型',
|
||||
width: 140,
|
||||
options: [
|
||||
{
|
||||
label: '人工',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: '自动',
|
||||
value: '2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'jcdt',
|
||||
|
||||
@ -1,42 +1,551 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
class="fish-tabs"
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<!-- 搜索组件 -->
|
||||
<FishDataSearch
|
||||
@export-btn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
@reset="onReset"
|
||||
:exportLoading="exportLoading"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="tableColumns"
|
||||
:list-url="getFishReleaseMonitorList"
|
||||
:searchParams="{
|
||||
sort: sort
|
||||
}"
|
||||
>
|
||||
<a-tab-pane key="manual" tab="人工站点">
|
||||
<FishDataManual />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="auto" tab="自动站点">
|
||||
<FishDataAuto />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 图片预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
title="图片预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 视频预览弹框 -->
|
||||
<a-modal
|
||||
v-model:open="videoPreviewVisible"
|
||||
title="视频预览"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<video
|
||||
:src="previewVideoUrl"
|
||||
controls
|
||||
style="width: 100%; height: 600px"
|
||||
></video>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import FishDataManual from './FishDataManual.vue';
|
||||
import FishDataAuto from './FishDataAuto.vue';
|
||||
import { ref, computed, onMounted, nextTick, h } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import FishDataSearch from './FishDataSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { getFishReleaseMonitorList } from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const activeTab = ref('manual');
|
||||
const sort = ref<any>([
|
||||
{
|
||||
field: 'baseStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rvcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'rstcdStepSort',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'ttpwr',
|
||||
dir: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'desc'
|
||||
}
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const searchRef = ref();
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
const currentSearchParams = ref<any>({});
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
// 视频预览
|
||||
const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
tableColumns.value.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 180),
|
||||
0
|
||||
),
|
||||
600
|
||||
)
|
||||
);
|
||||
|
||||
// 人工列
|
||||
const columnsManual: any[] = [
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'strdt',
|
||||
title: '开始时间',
|
||||
dataIndex: 'strdt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'enddt',
|
||||
title: '结束时间',
|
||||
dataIndex: 'enddt',
|
||||
sort: true,
|
||||
width: 100,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '填报时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'fsz',
|
||||
title: '鱼类规格',
|
||||
dataIndex: 'fsz',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '图片',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 自动列
|
||||
const columnsAuto: any[] = [
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '电站名称',
|
||||
dataIndex: 'stnm',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sort: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所属基地',
|
||||
dataIndex: 'baseName',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
dataIndex: 'stnm',
|
||||
sort: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'tm',
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'ftp',
|
||||
title: '鱼种类',
|
||||
dataIndex: 'ftp',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'length',
|
||||
title: '鱼长度(cm)',
|
||||
dataIndex: 'length',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'width',
|
||||
title: '鱼宽度(cm)',
|
||||
dataIndex: 'width',
|
||||
sort: true,
|
||||
width: 120,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fishspeed',
|
||||
title: '鱼速度(m/s)',
|
||||
dataIndex: 'fishspeed',
|
||||
sort: true,
|
||||
width: 80,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'directionName',
|
||||
title: '鱼游向',
|
||||
dataIndex: 'directionName',
|
||||
sort: true,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
title: '水温(℃)',
|
||||
dataIndex: 'temperature',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'speed',
|
||||
title: '流速(m/s)',
|
||||
dataIndex: 'speed',
|
||||
sort: true,
|
||||
width: 160,
|
||||
customRender: ({ text }: any) => {
|
||||
const num = parseFloat(text);
|
||||
return isNaN(num) ? '-' : num.toFixed(2);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fcnt',
|
||||
title: '过鱼数量(尾)',
|
||||
dataIndex: 'fcnt',
|
||||
sort: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'channelno',
|
||||
title: '过鱼通道',
|
||||
dataIndex: 'channelno',
|
||||
sort: true,
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
key: 'firstimgurl',
|
||||
title: '轮廓图',
|
||||
dataIndex: 'firstimgurl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openImagePreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看图片'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'videourl',
|
||||
title: '短视频',
|
||||
dataIndex: 'videourl',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }: any) => {
|
||||
const hasValidUrl =
|
||||
text &&
|
||||
text !== '-' &&
|
||||
text.trim() !== '' &&
|
||||
text !== 'null' &&
|
||||
text !== 'undefined';
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
disabled: !hasValidUrl,
|
||||
style: {
|
||||
color: !hasValidUrl ? '#ccc' : '#1890ff',
|
||||
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
|
||||
},
|
||||
onClick: () => {
|
||||
if (hasValidUrl) {
|
||||
openVideoPreview(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => '查看视频'
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 根据数据类型动态切换列
|
||||
const tableColumns = computed(() => {
|
||||
const mway = currentSearchParams.value.mway;
|
||||
return mway === '2' ? columnsAuto : columnsManual;
|
||||
});
|
||||
|
||||
const onSearchFinish = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
// 等待 prop 更新后再调用 getList
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const onReset = async (values: any) => {
|
||||
currentSearchParams.value = values;
|
||||
await nextTick();
|
||||
initTable(values);
|
||||
};
|
||||
|
||||
const exportBtn = () => {
|
||||
if (exportLoading.value) return;
|
||||
|
||||
exportLoading.value = true;
|
||||
searchRef.value.btnLoading = true;
|
||||
|
||||
tableRef.value
|
||||
.exportTable({
|
||||
fileName: `过鱼监测数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
|
||||
})
|
||||
.finally(() => {
|
||||
exportLoading.value = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initTable = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.rvcd && values.rvcd !== 'all'
|
||||
? {
|
||||
field: 'rvcd',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.rvcd
|
||||
}
|
||||
: null,
|
||||
values.rstcd
|
||||
? {
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
: null,
|
||||
values.mway
|
||||
? {
|
||||
field: 'mway',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.mway
|
||||
}
|
||||
: null
|
||||
].filter(Boolean); // 移除空项
|
||||
if (values.stcd) {
|
||||
filters.push({
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
if (values.jcdt) {
|
||||
filters.push(
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fish-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// margin: 0 !important;
|
||||
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content),
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@ -47,6 +47,12 @@ const dialog = ref(false);
|
||||
const treeRef = ref();
|
||||
const fishTreeRef = ref();
|
||||
const infoForm = ref();
|
||||
//所属组织树型下拉框字段映射
|
||||
const treeSelectProps = {
|
||||
value: 'id',
|
||||
label: 'orgname',
|
||||
children: 'childList'
|
||||
};
|
||||
//获取企业树形信息
|
||||
const treeloading = ref(true);
|
||||
function getTree() {
|
||||
@ -99,7 +105,8 @@ function addClick() {
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: []
|
||||
roleinfo: [],
|
||||
orgid: ''
|
||||
};
|
||||
getrole();
|
||||
}
|
||||
@ -178,7 +185,8 @@ const info = ref({
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleinfo: [] as any[]
|
||||
roleinfo: [] as any[],
|
||||
orgid: ''
|
||||
});
|
||||
//修改-用户
|
||||
function editdepartment(row: any) {
|
||||
@ -188,6 +196,8 @@ function editdepartment(row: any) {
|
||||
});
|
||||
info.value = JSON.parse(JSON.stringify(row));
|
||||
info.value.roleinfo = selectID;
|
||||
// 回显所属组织
|
||||
info.value.orgid = row.orgid;
|
||||
rolesdata.value = row.roles;
|
||||
title.value = '修改用户';
|
||||
dialogVisible.value = true;
|
||||
@ -207,7 +217,8 @@ function confirmClick(formEl: any) {
|
||||
nickname: info.value.nickname,
|
||||
email: info.value.email,
|
||||
phone: info.value.phone,
|
||||
orgid: orgId.value
|
||||
// 修改时根据所属组织树型下拉框传递
|
||||
orgid: info.value.orgid
|
||||
};
|
||||
const roleids = String(info.value.roleinfo);
|
||||
updataUser(params, roleids).then(() => {
|
||||
@ -1274,6 +1285,17 @@ function handleClearSelection() {
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属组织" v-if="info.id">
|
||||
<el-tree-select
|
||||
v-model="info.orgid"
|
||||
:data="treedata"
|
||||
:props="treeSelectProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
placeholder="请选择所属组织"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span
|
||||
class="dialog-footer"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user