Merge branch 'wxk-main'

This commit is contained in:
王兴凯 2026-08-14 08:57:18 +08:00
commit 678caccbd7
80 changed files with 10555 additions and 1399 deletions

View File

@ -1080,6 +1080,22 @@ export function updateFishDictoryInfo(data) {
});
}
// 操作日志
export function getOperationLogList(data) {
return request({
url: '/base/operationLog/GetKendoListCust',
method: 'post',
data
});
}
export function getDetailByMainId(data) {
return request({
url: '/base/operationLog/getDetailByMainId',
method: 'get',
params:data
});
}
// 鱼类字典 - 删除
export function deleteFishDictoryInfo(data) {
return request({

View File

@ -138,3 +138,29 @@ export function noAuthGetKendoPage(data: any = {}) {
data
});
}
//综合导出
//taskId
export function qgcExportCreate (data: any = {}) {
return request({
url: '/qgcExport/create',
method: 'post',
params:data
});
}
//查看没有没准备完成
export function qgcExportstatus (data: any = {}) {
return request({
url: '/qgcExport/status',
method: 'get',
params:data
});
}
// 下载zip文件
export function qgcExportdownload (data: any = {}) {
return request({
url: '/qgcExport/download',
method: 'post',
params:data,
responseType: 'blob'
});
}

View File

@ -0,0 +1,298 @@
<template>
<a-modal
v-model:open="visible"
title="操作日志"
width="90vw"
:footer="null"
@cancel="handleCancel"
>
<!-- 搜索区域 -->
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@finish="onSearchFinish"
@reset="onReset"
/>
<!-- 表格 -->
<BasicTable
ref="tableRef"
:enableEllipsis="true"
:scrollX="tableScrollX"
:scrollY="500"
:columns="columns"
:list-url="getOperationLogList"
:transform-data="customTransform"
>
<template #action="{ record }">
<a-button
type="link"
class="text-[#2f6b98]"
size="small"
@click="handleDetail(record)"
>
查看详情
</a-button>
</template>
</BasicTable>
<!-- 查看详情 Modal -->
<a-modal
v-model:open="detailVisible"
title="操作日志详情"
width="800px"
:footer="null"
>
<a-spin :spinning="detailLoading">
<div style="max-height: 50vh; overflow-y: auto">
<a-descriptions bordered :column="1" size="small">
<a-descriptions-item
v-for="item in diffList"
:key="item.field"
:label="item.label"
>
<template v-if="item.operationType === '01'">
<span style="color: #52c41a">{{ item.newValue }}</span>
</template>
<template v-else-if="item.operationType === '03'">
<span style="color: #ff4d4f">{{ item.oldValue }}</span>
</template>
<template v-else>
<span style="color: #ff4d4f">{{ item.oldValue }}</span>
<span style="margin: 0 8px"></span>
<span style="color: #52c41a">{{ item.newValue }}</span>
</template>
</a-descriptions-item>
</a-descriptions>
</div>
</a-spin>
</a-modal>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue";
import dayjs from "dayjs";
import BasicSearch from "@/components/BasicSearch/index.vue";
import BasicTable from "@/components/BasicTable/index.vue";
import { getOperationLogList, getDetailByMainId } from "@/api/DataQueryMenuModule";
import { useDraggable } from "@/utils/drag";
defineOptions({ name: "OperationLogModal" });
const props = defineProps<{
open: boolean;
tableName: string;
}>();
const emit = defineEmits(["update:open"]);
const visible = computed({
get: () => props.open,
set: (val) => emit("update:open", val),
});
useDraggable(visible, { boundary: true, resetOnOpen: true });
const basicSearchRef = ref();
const tableRef = ref();
const initSearchData = { rvcd: 'all' };
//
const searchList: any = [
{
type: "waterStation",
name: "rvcd",
label: "流域",
placeholder: "请输入流域",
fieldProps: { allowClear: true },
options: [],
},
{
type: "Input",
name: "operator",
label: "修改人",
placeholder: "请输入修改人",
fieldProps: { allowClear: true },
},
{
type: "RangePicker",
name: "operateTime",
label: "修改时间",
fieldProps: {
allowClear: true,
valueFormat: "YYYY-MM-DD",
},
},
];
//
const columns = ref<any[]>([
{
key: "recordId",
title: "电站",
dataIndex: "recordId",
visible: true,
width: 120,
ellipsis: true,
},
{
key: "tableName",
title: "数据类型/所属表名",
dataIndex: "tableName",
visible: true,
width: 160,
ellipsis: true,
},
{
key: "operator",
title: "修改人",
dataIndex: "operator",
visible: true,
width: 120,
},
{
key: "operateTime",
title: "修改时间",
dataIndex: "operateTime",
visible: true,
width: 160,
customRender: ({ text }: { text: string }) => {
if (!text || text === "-") return "-";
const date = dayjs(text);
return date.isValid() ? date.format("YYYY-MM-DD") : "-";
},
},
{
key: "source",
title: "来源依据",
dataIndex: "source",
visible: true,
ellipsis: true,
},
{
key: "remark",
title: "备注",
dataIndex: "remark",
visible: true,
ellipsis: true,
},
{
key: "action",
title: "操作",
dataIndex: "action",
fixed: "right",
width: 120,
},
]);
const tableScrollX = computed(() =>
Math.max(
columns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
const buildSearchParams = (values: any) => {
return {
logic: "and",
filters: [
values.rstcd
? {
field: "recordId",
operator: "eq",
dataType: "string",
value: values.rstcd,
}
: null,
values.operator
? {
field: "operator",
operator: "contains",
dataType: "string",
value: values.operator,
}
: null,
values.operateTime?.[0]
? {
field: "operateTime",
operator: "gte",
dataType: "date",
value: values.operateTime[0],
}
: null,
values.operateTime?.[1]
? {
field: "operateTime",
operator: "lte",
dataType: "date",
value: values.operateTime[1],
}
: null,
{
field: "tableName",
operator: "eq",
dataType: "string",
value: props.tableName,
},
].filter(Boolean),
};
};
const onSearchFinish = (values: any) => {
const params = buildSearchParams(values);
tableRef.value.getList(params);
};
const onReset = () => {
tableRef.value?.getList(buildSearchParams({}));
};
const handleDetail = async (record: any) => {
detailVisible.value = true;
detailLoading.value = true;
try {
const res = await getDetailByMainId({ mainId: record.id });
const data = res?.data || [];
diffList.value = data.map((item: any) => ({
field: item.fieldCode,
label: item.fieldMeaning,
operationType: record.operationType,
oldValue: item.oldValueName || item.oldValueCode || '-',
newValue: item.newValueName || item.newValueCode ||'-'
}));
} finally {
detailLoading.value = false;
}
};
const detailVisible = ref(false);
const detailLoading = ref(false);
const diffList = ref<{ field: string; label: string; operationType: string; oldValue: string; newValue: string }[]>([]);
const handleCancel = () => {
visible.value = false;
};
const customTransform = (res: any) => {
return {
records: res?.data?.records || [],
total: res?.data?.total || 0
};
};
watch(
() => props.open,
(val) => {
if (val) {
nextTick(() => {
tableRef.value?.getList(buildSearchParams({}));
});
}
}
);
</script>
<style scoped lang="less"></style>

View File

@ -75,6 +75,7 @@
</template>
<template #actions>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
<a-button :loading="btnLoading" @click="exportBtn">导出</a-button>
<a-button @click="customFilterBtn">自定义筛选列</a-button>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
@ -96,6 +97,7 @@ const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
interface Props {
@ -290,6 +292,9 @@ const exportBtn = () => {
const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };

View File

@ -1184,6 +1184,42 @@ const resolveSelectLabel = (field: string, value: any): string => {
return String(value);
};
// select label
const selectLabelFieldMap: Record<string, string> = {
baseId: 'baseName',
addvcd: 'addvcdName',
reachcd: 'reachcdName',
hycd: 'hynm',
topHycd: 'topHynm'
};
// select code label
const resolveLabelForSubmit = (field: string, value: any): string | null => {
if (value === null || value === undefined || value === '') return null;
// { value, label }
const getOptions = selectOptionsMap[field];
if (getOptions) {
const opt = getOptions().find(
(o: any) => String(o.value) === String(value)
);
if (opt) return opt.label;
}
// hycd / topHycd 使 { hycd, hynm }
if (field === 'hycd' && props.hycdList) {
const opt = props.hycdList.find(
(o: any) => String(o.hycd) === String(value)
);
if (opt) return opt.hynm;
}
if (field === 'topHycd' && props.topHynmList) {
const opt = props.topHynmList.find(
(o: any) => String(o.hycd) === String(value)
);
if (opt) return opt.hynm;
}
return null;
};
//
const changeOrder = ref<
{ field: string; label: string; oldValue: string; newValue: string }[]
@ -1353,10 +1389,24 @@ const handleConfirmSubmit = async (source: string) => {
confirmLoading.value = true;
try {
let res: any;
// select label engInfo
const appendSelectLabels = (engInfo: Record<string, any>, fields: string[]) => {
for (const field of fields) {
const labelField = selectLabelFieldMap[field];
if (labelField) {
const label = resolveLabelForSubmit(field, formData.value[field]);
if (label !== null) {
engInfo[labelField] = label;
}
}
}
};
if (props.isAdd) {
//
const engInfo: Record<string, any> = {};
Object.assign(engInfo, formData.value);
appendSelectLabels(engInfo, Object.keys(formData.value));
res = await addPowerInfo({
engInfo,
source: source.trim() || '新增'
@ -1369,6 +1419,7 @@ const handleConfirmSubmit = async (source: string) => {
changeOrder.value.forEach(item => {
engInfo[item.field] = formData.value[item.field];
});
appendSelectLabels(engInfo, changeOrder.value.map(c => c.field));
res = await updatePowerInfo({
engInfo,
source: source.trim()

View File

@ -12,6 +12,7 @@
@custom-filter-btn="customFilterBtn"
@search-finish="onSearchFinish"
@reset="onReset"
@seeEdit = "handleSeeEdit"
ref="searchRef"
/>
@ -70,6 +71,12 @@
<!-- 删除确认 Modal -->
<DeleteConfirmModal ref="deleteModalRef" @success="handleEditSuccess" />
<!-- 操作日志 Modal -->
<OperationLogModal
v-model:open="operationLogVisible"
:table-name="'SD_ENGINFO_B_H'"
/>
</div>
</template>
<script setup lang="ts">
@ -80,6 +87,7 @@ import BasicTable from '@/components/BasicTable/index.vue';
import CustomColumnModal from './CustomColumnModal.vue';
import EditPowerModal from './EditPowerModal.vue';
import DeleteConfirmModal from './DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import {
getCompanyList,
getPowerTableList,
@ -141,6 +149,7 @@ const topHynmLoading = ref(false);
const hycdLoading = ref(false);
const isEdit = ref(true);
const operationLogVisible = ref(false);
const dvtpList = ref<any[]>([
{ label: '堤坝式', value: '0' },
@ -264,6 +273,9 @@ const onReset = (values: any) => {
currentSearchParams.value = values;
initTable(values);
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const exportBtn = () => {

View File

@ -11,6 +11,7 @@
>
<template #actions>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
<a-button @click="exportBtn" :loading="exportLoading">导出</a-button>
</template>
</BasicSearch>
@ -35,6 +36,7 @@ const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const localTypeDate = ref<string>(null);
@ -121,6 +123,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
const handleReset = () => {
localTypeDate.value = null;
emit('reset', initSearchData);

View File

@ -11,6 +11,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
<a-row type="flex" class="w-full h-full" style="height: calc(100% - 80px)">
@ -121,6 +122,8 @@
title="删除鱼类"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FISHDICTORY_B'" />
</div>
</template>
<script setup lang="ts">
@ -146,6 +149,7 @@ import {
import { calcTableScrollY } from '@/utils/index';
import EditFishDictoryModal from './EditFishDictoryModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
const fishResource = ref<any>({
name: '',
logoList: [],
@ -484,6 +488,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
//
const exportBtn = () => {
@ -616,6 +621,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -22,6 +23,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -72,6 +74,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
emit('search-finish', { ...initSearchData });
});

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除AI边缘计算盒子"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_AIBOX_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import AiBoxSearch from './AiBoxSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditAiBoxModal from './EditAiBoxModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getAiboxPageList, deleteAiboxInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -244,6 +249,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除其他水生生态保护工程"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_OTTE_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import OtweSearch from './OtweSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditOtweModal from './EditOtweModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getOtwePageList, deleteOtweInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -69,6 +73,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -235,6 +240,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
const handleEdit = (record: any) => {
isAdd.value = false;
editRecord.value = { ...record };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除鱼类调查"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_AIMONITOR_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import FishSearch from './FishSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditFishModal from './EditFishModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getAiPageList, deleteAiInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -252,6 +257,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -3,6 +3,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -12,7 +13,7 @@ import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void }>();
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
const basicSearchRef = ref();
const isHide = ref(false);
const sttpOption = ref([]);
@ -36,6 +37,7 @@ const onSearchFinish = (values: any) => emit('search-finish', { ...values });
const hideBtn = () => { isHide.value = !isHide.value; };
const handleReset = () => emit('reset', initSearchData);
const handleAdd = () => emit('add');
const seeEdit = () => emit('seeEdit');
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
defineExpose({ basicSearchRef });

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<FpSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<FpSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getFpPageList">
<template #action="{ record }">
@ -12,6 +12,8 @@
<EditFpModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteFpFn" :label="'过鱼设施'" title="删除过鱼设施" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FPSS_B_H'" />
</div>
</template>
@ -22,6 +24,7 @@ import FpSearch from './FpSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditFpModal from './EditFpModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getFpPageList, deleteFpInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -33,6 +36,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const bldsttMap: Record<number, string> = { 0: '规划', 1: '在建', 2: '已建' };
@ -72,6 +76,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除陆生生态调查断面"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_TE_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import TeSearch from './TeSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditTeModal from './EditTeModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getTePageList, deleteTeInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -222,6 +227,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -22,6 +23,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -68,6 +70,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
emit('search-finish', { ...initSearchData });
});

View File

@ -4,6 +4,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -35,6 +36,8 @@
title="删除栖息地"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FHBT_B_H'" />
</div>
</template>
@ -44,6 +47,7 @@ import FhSearch from './FhSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditFhModal from './EditFhModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getFhPageList, deleteFhInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -56,6 +60,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{ key: 'stnm', title: '栖息地名称', dataIndex: 'stnm', visible: true, width: 260, ellipsis: true },
@ -94,6 +99,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<VaSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<VaSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getVaPageList">
<template #action="{ record }">
<a-button type="link" class="text-[#2f6b98]" size="small" @click="handleEdit(record)">编辑</a-button>
@ -9,6 +9,7 @@
</BasicTable>
<EditVaModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteVaFn" :label="'动物救助站'" title="删除动物救助站" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_VA_B_H'" />
</div>
</template>
<script setup lang="ts">
@ -18,10 +19,11 @@ import VaSearch from './VaSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditVaModal from './EditVaModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getVaPageList, deleteVaInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const tableRef = ref(), searchRef = ref(), tableScrollY = ref<string | number>(0), currentSearchParams = ref<any>({});
const editVisible = ref(false), editRecord = ref<any>(null), isAdd = ref(false), deleteModalRef = ref();
const editVisible = ref(false), editRecord = ref<any>(null), isAdd = ref(false), deleteModalRef = ref(), operationLogVisible = ref(false);
const columns = ref<any[]>([
{ key: 'stnm', title: '救助站名称', dataIndex: 'stnm', visible: true, width: 200, fixed: 'left', ellipsis: true },
{ key: 'stcd', title: '救助站编码', dataIndex: 'stcd', visible: true, width: 160, ellipsis: true },
@ -41,6 +43,7 @@ const onSearchFinish = (values: any) => { currentSearchParams.value = values; in
const onReset = (values: any) => { currentSearchParams.value = values; initTable(values); };
const initTable = (values: any) => { const params = buildSearchParams(values); 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 deleteVaFn = async (record: any, reason: string) => deleteVaInfo({ ids: [record.stcd], source: reason });

View File

@ -3,6 +3,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -12,7 +13,7 @@ import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void }>();
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
const basicSearchRef = ref();
const isHide = ref(false);
const sttpOption = ref([]);
@ -36,6 +37,7 @@ const onSearchFinish = (values: any) => emit('search-finish', { ...values });
const hideBtn = () => { isHide.value = !isHide.value; };
const handleReset = () => emit('reset', initSearchData);
const handleAdd = () => emit('add');
const seeEdit = () => emit('seeEdit');
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
defineExpose({ basicSearchRef });

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<EqSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<EqSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getEqPageList">
<template #action="{ record }">
@ -12,6 +12,8 @@
<EditEqModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteEqFn" :label="'生态流量设施'" title="删除生态流量设施" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_EQ_B_H'" />
</div>
</template>
@ -22,6 +24,7 @@ import EqSearch from './EqSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditEqModal from './EditEqModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getEqPageList, deleteEqInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -33,6 +36,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const bldsttMap: Record<number, string> = { 0: '规划', 1: '在建', 2: '已建' };
@ -69,6 +73,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -3,6 +3,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -12,7 +13,7 @@ import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void }>();
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
const basicSearchRef = ref();
const isHide = ref(false);
const sttpOption = ref([]);
@ -36,6 +37,7 @@ const onSearchFinish = (values: any) => emit('search-finish', { ...values });
const hideBtn = () => { isHide.value = !isHide.value; };
const handleReset = () => emit('reset', initSearchData);
const handleAdd = () => emit('add');
const seeEdit = () => emit('seeEdit');
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
defineExpose({ basicSearchRef });

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<VdSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<VdSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getVdPageList">
<template #action="{ record }">
@ -12,6 +12,8 @@
<EditVdModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteVdFn" :label="'视频监控'" title="删除视频监控点" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_VDINFO_B'" />
</div>
</template>
@ -21,6 +23,7 @@ import VdSearch from './VdSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditVdModal from './EditVdModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getVdPageList, deleteVdInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -32,6 +35,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{ key: 'stnm', title: '监控点名', dataIndex: 'stnm', visible: true, width: 200, ellipsis: true },
@ -62,6 +66,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除水生生态调查断面"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_WE_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import WeSearch from './WeSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditWeModal from './EditWeModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getWePageList, deleteWeInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -222,6 +227,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,12 @@
title="删除水温测站"
@success="handleEditSuccess"
/>
<!-- 操作日志 Modal -->
<OperationLogModal
v-model:open="operationLogVisible"
:table-name="'SD_WT_B_H'"
/>
</div>
</template>
@ -57,6 +64,7 @@ import WtSearch from './WtSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditWtModal from './EditWtModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getWtPageList, deleteWtInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +78,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -226,6 +235,11 @@ const handleAdd = () => {
editVisible.value = true;
};
//
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -22,6 +23,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -85,6 +87,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
const getBaseList = async () => {
let params = {
"filter":

View File

@ -4,6 +4,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -40,6 +41,12 @@
title="删除水质测站"
@success="handleEditSuccess"
/>
<!-- 操作日志 Modal -->
<OperationLogModal
v-model:open="operationLogVisible"
:table-name="'SD_WQ_B_H'"
/>
</div>
</template>
@ -50,6 +57,7 @@ import WqSearch from './WqSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditWqModal from './EditWqModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getWqPageList, deleteWqInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -62,6 +70,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -211,6 +220,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
const handleEdit = (record: any) => {
isAdd.value = false;
editRecord.value = { ...record };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除声呐及水下摄像头"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_SONAR_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import SonarSearch from './SonarSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditSonarModal from './EditSonarModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getSonarPageList, deleteSonarInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -69,6 +73,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -235,6 +240,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
const handleEdit = (record: any) => {
isAdd.value = false;
editRecord.value = { ...record };

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除低温水减缓设施"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_DW_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import DwSearch from './DwSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditDwModal from './EditDwModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getDwPageList, deleteDwInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -237,6 +242,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -110,6 +112,10 @@ const handleAdd = () => {
emit('add');
};
const seeEdit = () => {
emit('seeEdit');
};
onMounted(() => {
getBaseList();
emit('search-finish', { ...initSearchData });

View File

@ -5,6 +5,7 @@
@search-finish="onSearchFinish"
@reset="onReset"
@add="handleAdd"
@seeEdit="handleSeeEdit"
ref="searchRef"
/>
@ -47,6 +48,8 @@
title="删除其他陆生生态保护工程"
@success="handleEditSuccess"
/>
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_OTWE_B_H'" />
</div>
</template>
@ -57,6 +60,7 @@ import OtteSearch from './OtteSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditOtteModal from './EditOtteModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getOttePageList, deleteOtteInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -70,6 +74,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{
@ -237,6 +242,10 @@ const handleAdd = () => {
editVisible.value = true;
};
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
//
const handleEdit = (record: any) => {
isAdd.value = false;

View File

@ -10,6 +10,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -23,6 +24,7 @@ const emit = defineEmits<{
(e: 'search-finish', values: any): void;
(e: 'reset', values: any): void;
(e: 'add'): void;
(e: 'seeEdit'): void;
}>();
const basicSearchRef = ref();
@ -83,6 +85,7 @@ const onSearchFinish = (values: any) => emit('search-finish', { ...values });
const hideBtn = () => { isHide.value = !isHide.value; };
const handleReset = () => emit('reset', initSearchData);
const handleAdd = () => emit('add');
const seeEdit = () => emit('seeEdit');
onMounted(() => {
getBaseList();

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<FbSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<FbSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getFbPageList">
<template #action="{ record }">
@ -12,6 +12,8 @@
<EditFbModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteFbFn" :label="'鱼类增殖站'" title="删除鱼类增殖站" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_FBRD_B_H'" />
</div>
</template>
@ -22,6 +24,7 @@ import FbSearch from './FbSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditFbModal from './EditFbModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getFbPageList, deleteFbInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -34,6 +37,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{ key: 'stnm', title: '增殖站名称', dataIndex: 'stnm', visible: true, width: 160,fixed: 'left', ellipsis: true },
@ -79,6 +83,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -3,6 +3,7 @@
<template #actions>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" @click="seeEdit">查看修改</a-button>
</template>
</BasicSearch>
</template>
@ -12,7 +13,7 @@ import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { sttpGetKendoList } from '@/api/system/map/ConfigManagement';
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void }>();
const emit = defineEmits<{ (e: 'search-finish', values: any): void; (e: 'reset', values: any): void; (e: 'add'): void; (e: 'seeEdit'): void }>();
const basicSearchRef = ref();
const isHide = ref(false);
const sttpOption = ref([]);
@ -36,6 +37,7 @@ const onSearchFinish = (values: any) => emit('search-finish', { ...values });
const hideBtn = () => { isHide.value = !isHide.value; };
const handleReset = () => emit('reset', initSearchData);
const handleAdd = () => emit('add');
const seeEdit = () => emit('seeEdit');
onMounted(() => { getBaseList(); emit('search-finish', { ...initSearchData }); });
defineExpose({ basicSearchRef });

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<VpSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" ref="searchRef" />
<VpSearch @search-finish="onSearchFinish" @reset="onReset" @add="handleAdd" @seeEdit="handleSeeEdit" ref="searchRef" />
<BasicTable ref="tableRef" :enableEllipsis="true" :scrollX="tableScrollX" :scrollY="tableScrollY" :columns="columns" :list-url="getVpPageList">
<template #action="{ record }">
@ -12,6 +12,8 @@
<EditVpModal v-model:open="editVisible" :record="editRecord" :is-add="isAdd" @success="handleEditSuccess" />
<DeleteConfirmModal ref="deleteModalRef" :delete-fn="deleteVpFn" :label="'珍稀植物园'" title="删除珍稀植物园" @success="handleEditSuccess" />
<OperationLogModal v-model:open="operationLogVisible" :table-name="'SD_VP_B_H'" />
</div>
</template>
@ -22,6 +24,7 @@ import VpSearch from './VpSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import EditVpModal from './EditVpModal.vue';
import DeleteConfirmModal from '@/views/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import { getVpPageList, deleteVpInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
@ -33,6 +36,7 @@ const editVisible = ref(false);
const editRecord = ref<any>(null);
const isAdd = ref(false);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const columns = ref<any[]>([
{ key: 'stnm', title: '植物园名称', dataIndex: 'stnm', visible: true, width: 150, ellipsis: true },
@ -67,6 +71,7 @@ const onReset = (values: any) => { currentSearchParams.value = values; initTable
const initTable = (values: any) => { tableRef.value.getList(buildSearchParams(values)); };
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); };

View File

@ -3,7 +3,7 @@
<a-form
ref="formRef"
:model="formData"
:labelCol="{ span: 6 }"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 18 }"
class="export-form"
>
@ -12,24 +12,29 @@
<a-radio-group v-model:value="formData.timeDimension">
<a-radio value="month">月度统计</a-radio>
<a-radio value="year">年统计</a-radio>
<a-radio value="day">日统计</a-radio>
<a-radio value="hour">时统计</a-radio>
</a-radio-group>
</a-form-item>
<!-- 月度统计 -->
<template v-if="formData.timeDimension === 'month'">
<template v-if="formData.timeDimension !== 'year'">
<a-form-item label="统计监测要素" name="monitorElements">
<a-checkbox-group v-model:value="formData.monitorElements">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
<a-checkbox
v-for="item in dataFieldOption"
:value="item.itemCode"
>{{ item.dictName }}</a-checkbox
>
</a-checkbox-group>
</a-form-item>
<a-form-item label="选择水电站">
<a-row :gutter="16">
<a-col :span="12">
<a-col :span="6">
<a-select
v-model:value="formData.rvcd"
placeholder="请选择流域"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
show-search
@ -37,6 +42,7 @@
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 100%"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
@ -48,7 +54,7 @@
</a-select-option>
</a-select>
</a-col>
<a-col :span="12">
<a-col :span="6">
<a-select
v-model:value="formData.stations"
placeholder="请选择水电站"
@ -58,6 +64,7 @@
:filter-option="filterOption"
:loading="stationLoading"
style="width: 100%"
:max-tag-count="2"
>
<a-select-option
v-for="item in stationOptions"
@ -86,14 +93,14 @@
tree-node-filter-prop="title"
show-search
allow-clear
style="width: 100%"
style="width: 400px"
/>
</a-form-item>
<a-form-item label="数据来源" name="dataSource">
<a-form-item label="数据来源" name="dataSource" v-if="formData.timeDimension === 'month'">
<a-radio-group v-model:value="formData.dataSource">
<a-radio value="day">日统计表</a-radio>
<a-radio value="hour">小时表</a-radio>
<a-radio value="true">日统计表</a-radio>
<a-radio value="false">小时表</a-radio>
</a-radio-group>
</a-form-item>
</template>
@ -102,45 +109,138 @@
<template v-if="formData.timeDimension === 'year'">
<a-form-item label="统计内容" name="yearStatContent">
<a-checkbox-group v-model:value="formData.yearStatContent">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
<a-checkbox value="verticalTemp">垂向水温</a-checkbox>
<a-checkbox value="surfaceTemp">表层水温</a-checkbox>
<a-checkbox value="wq">水质</a-checkbox>
<a-checkbox value="qec">生态流量达标率</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="统计年份" name="statYears">
<a-tree-select
<a-select
v-model:value="formData.statYears"
:tree-data="yearTreeData"
placeholder="请选择统计年份"
tree-checkable
:fieldNames="{
label: 'title',
value: 'value',
children: 'children'
}"
tree-node-filter-prop="title"
mode="multiple"
allow-clear
show-search
allow-clear
:filter-option="filterOption"
style="width: 300px"
:max-tag-count="2"
>
<a-select-option
v-for="item in yearOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="统计维度与内容" name="dimensionStat">
<a-checkbox-group
v-model:value="formData.yearDimContent"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="显示维度统计" name="dimensionStat">
<a-input
v-model:value="formData.dimensionStat"
placeholder="请输入显示维度统计"
>
<a-row>
<a-col :span="24" style="margin-bottom: 10px">
<div style="display: flex; align-items: center">
<a-checkbox value="stcd">电站</a-checkbox>
<a-select
v-model:value="formData.rvcd"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
/>
</a-form-item>
<a-form-item label="内容" name="content">
<a-input
v-model:value="formData.content"
placeholder="请输入内容"
show-search
:filter-option="filterOption"
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
<a-select
v-model:value="formData.stations"
placeholder="请选择水电站"
mode="multiple"
allow-clear
/>
show-search
:filter-option="filterOption"
:loading="stationLoading"
style="width: 300px"
:max-tag-count="2"
>
<a-select-option
v-for="item in stationOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
<a-col :span="24" style="margin-bottom: 10px">
<div style="display: flex; align-items: center">
<a-checkbox value="baseId">基地</a-checkbox>
<a-select
v-model:value="formData.rvcd"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
<a-col :span="24">
<div style="display: flex; align-items: center">
<a-checkbox value="ly">流域</a-checkbox>
<a-select
v-model:value="formData.lyRvcd"
placeholder="请选择流域"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="lyLoading"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in lyOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</template>
@ -155,46 +255,58 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { message } from 'ant-design-vue';
import { getSelectForDropdown, getEngInfoDropdown } from '@/api/select';
import {
getSelectForDropdown,
getEngInfoDropdown,
qgcExportCreate,
qgcExportstatus,
qgcExportdownload
} from '@/api/select';
import { getDictItemsByCode } from '@/api/dict';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
const formRef = ref<any>();
const exportLoading = ref(false);
const rvcdLoading = ref(false);
const stationLoading = ref(false);
const JidiSelectEventStore = useJidiSelectEventStore();
//
const rvcdOptions = ref<{ label: string; value: string }[]>([]);
//
const stationOptions = ref<{ label: string; value: string }[]>([]);
//
const lyOptions = ref<{ label: string; value: string }[]>([]);
const lyLoading = ref(false);
//
const formData = reactive({
timeDimension: 'month' as 'month' | 'year',
timeDimension: 'month',
//
monitorElements: [] as string[],
rvcd: [] as string[],
stations: [] as string[],
statMonths: [] as string[],
dataSource: 'day' as 'day' | 'hour',
dataSource: 'true',
//
yearStatContent: [] as string[],
statYears: [] as string[],
yearDimContent: [] as string[],
lyRvcd: [] as string[],
dimensionStat: '',
content: ''
});
//
// 26
const monthTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const years: any[] = [];
for (let y = currentYear; y >= currentYear - 5; y--) {
for (let y = currentYear; y >= currentYear - 26; y--) {
years.push({
title: `${y}`,
title: `${y}`,
value: `year_${y}`,
children: Array.from({ length: 12 }, (_, i) => ({
title: `${y}${i + 1}`,
title: `${y}-${i + 1}`,
value: `${y}-${String(i + 1).padStart(2, '0')}`
}))
});
@ -202,27 +314,22 @@ const monthTreeData = computed(() => {
return years;
});
//
const yearTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const decades: any[] = [];
const startDecade = Math.floor((currentYear - 20) / 10) * 10;
const endDecade = Math.floor(currentYear / 10) * 10;
//
const getDefaultStatMonths = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
return [`${year}-${String(month).padStart(2, '0')}`];
};
for (let d = endDecade; d >= startDecade; d -= 10) {
const years: any[] = [];
for (let y = d; y < d + 10 && y <= currentYear; y++) {
years.push({ title: `${y}`, value: `${y}` });
// 26
const yearOptions = computed(() => {
const currentYear = new Date().getFullYear();
const years: { label: string; value: string }[] = [];
for (let y = currentYear; y >= currentYear - 26; y--) {
years.push({ label: `${y}`, value: `${y}` });
}
if (years.length > 0) {
decades.push({
title: `${d}s`,
value: `decade_${d}`,
children: years
});
}
}
return decades;
return years;
});
//
@ -230,38 +337,18 @@ const filterOption = (input: string, option: any) => {
return (option?.label || '').toLowerCase().includes(input.toLowerCase());
};
//
const loadRvcdOptions = async () => {
rvcdLoading.value = true;
try {
const res = await getSelectForDropdown({});
if (res?.data) {
rvcdOptions.value = (res.data || []).map((item: any) => ({
label: item.rvnm || item.label || item.name,
value: item.rvcd || item.value || item.id
}));
}
} catch (e) {
console.error('获取流域列表失败', e);
} finally {
rvcdLoading.value = false;
}
};
//
const loadStationOptions = async (rvcds?: string[]) => {
stationLoading.value = true;
try {
const params: any = {};
if (rvcds && rvcds.length > 0) {
params.rvcds = rvcds;
}
const res = await getEngInfoDropdown(params);
const res = await getEngInfoDropdown({ baseIds: rvcds || [] });
if (res?.data) {
stationOptions.value = (res.data || []).map((item: any) => ({
label: item.engnm || item.stnm || item.label || item.name,
label: item.ennm || item.stnm || item.label || item.name,
value: item.stcd || item.engId || item.value || item.id
}));
//
formData.stations = stationOptions.value.map(item => item.value);
}
} catch (e) {
console.error('获取水电站列表失败', e);
@ -270,31 +357,118 @@ const loadStationOptions = async (rvcds?: string[]) => {
}
};
// store
const initDone = ref(false);
watch(
() => JidiSelectEventStore.jidiData,
(val) => {
if (initDone.value) return;
const data = (val || []).filter((item: any) => item.wbsCode !== 'all');
if (data.length === 0) return;
rvcdOptions.value = data.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
formData.rvcd = [rvcdOptions.value[0].value];
loadStationOptions(formData.rvcd);
initDone.value = true;
},
{ immediate: true }
);
//
const handleRvcdChange = (values: string[]) => {
formData.stations = [];
loadStationOptions(values);
};
//
const loadLyOptions = async () => {
lyLoading.value = true;
try {
const res = await getSelectForDropdown({});
if (res?.data) {
lyOptions.value = (res.data || []).map((item: any) => ({
label: item.rvnm,
value: item.rvcd
}));
//
if (lyOptions.value.length > 0) {
formData.lyRvcd = [lyOptions.value[0].value];
}
}
} catch (e) {
console.error('获取流域列表失败', e);
} finally {
lyLoading.value = false;
}
};
const getExstatus = async (taskId, exportParams) => {
let res1: any = await qgcExportstatus({ taskId: taskId });
if (res1.status == 'PROCESSING') {
setTimeout(() => {
getExstatus(taskId, exportParams);
}, 5000);
} else if (res1.status == 'DONE') {
const blob = await qgcExportdownload({ taskId: taskId, ...exportParams });
//
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export_${taskId}.zip`;
a.click();
window.URL.revokeObjectURL(url);
exportLoading.value = false;
message.success('导出成功');
} else {
message.error('导出失败');
}
};
//
const handleExport = () => {
const handleExport = async () => {
exportLoading.value = true;
try {
if (formData.timeDimension !== 'year') {
const params: any = {
tmDimension: formData.timeDimension,
dataField: formData.monitorElements.join(','),
months: formData.statMonths.join(','),
stcd: formData.stations.join(','),
sysId: 'qgc',
isCalcQec: true
};
if (formData.timeDimension === 'month') {
console.log('月度统计导出参数:', {
monitorElements: formData.monitorElements,
rvcd: formData.rvcd,
stations: formData.stations,
statMonths: formData.statMonths,
dataSource: formData.dataSource
});
params.isDailyDimension = formData.dataSource === 'day';
}
console.log(`${formData.timeDimension}统计导出参数:`, params);
let res: any = await qgcExportCreate(params);
if (res.taskId) {
getExstatus(res.taskId, params);
}
} else {
console.log('年统计导出参数:', {
yearStatContent: formData.yearStatContent,
statYears: formData.statYears,
dimensionStat: formData.dimensionStat,
content: formData.content
});
const params: any = {
tmDimension: 'year',
yearStatType: formData.yearStatContent.join(','),
year: formData.statYears.join(','),
sysId: 'qgc'
};
//
if (formData.yearDimContent.includes('stcd')) {
params.stcd = formData.stations.join(',');
}
if (formData.yearDimContent.includes('baseId')) {
params.baseId = formData.rvcd.join(',');
}
if (formData.yearDimContent.includes('ly')) {
params.hbRvcd = formData.lyRvcd.join(',');
}
console.log('年统计导出参数:', params);
let res: any = await qgcExportCreate(params);
if (res.taskId) {
getExstatus(res.taskId, params);
}
}
message.success('导出任务已提交');
} catch (e) {
@ -303,17 +477,28 @@ const handleExport = () => {
exportLoading.value = false;
}
};
const dataFieldOption = ref([]);
const getDict = async () => {
let res = await getDictItemsByCode({ dictCode: 'dataField' });
dataFieldOption.value = res.data;
formData.monitorElements = dataFieldOption.value.map(item => item.itemCode);
// debugger
};
onMounted(() => {
loadRvcdOptions();
loadStationOptions();
getDict();
formData.statMonths = getDefaultStatMonths();
formData.yearStatContent = ['wq', 'qec'];
formData.yearDimContent = ['stcd', 'baseId'];
formData.statYears = [String(new Date().getFullYear())];
loadLyOptions();
});
</script>
<style scoped lang="scss">
.export-container {
padding: 20px;
max-width: 800px;
// max-width: 800px;
width: 100%;
.export-form {
:deep(.ant-form-item) {

View File

@ -0,0 +1,404 @@
<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_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: '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);
}
}
},
() => '查看视频'
);
}
}
]);
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>

View File

@ -0,0 +1,369 @@
<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_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);
}
}
},
() => '查看视频'
);
}
}
]);
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>

View File

@ -82,12 +82,13 @@ const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
mway: string;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: '0086601020VP000006',
mway: '1'
mway: props.mway
};
const searchData = ref<any>({ ...initSearchData });
@ -135,22 +136,6 @@ 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',

View File

@ -1,551 +1,22 @@
<template>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<!-- 搜索组件 -->
<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
}"
>
</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 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>
</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';
import { ref } from 'vue';
import FishDataManual from './FishDataManual.vue';
import FishDataAuto from './FishDataAuto.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 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);
}
}
},
() => '查看视频'
);
}
}
];
//
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);
}
}
},
() => '查看视频'
);
}
}
];
//
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();
});
});
const activeTab = ref('manual');
</script>
<style scoped lang="scss">
@ -553,4 +24,21 @@ onMounted(() => {
position: relative;
z-index: 900;
pointer-events: all;
}</style>
}
.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>

View File

@ -150,6 +150,11 @@
style="width: 100%"
:disabled="!editingData.rstcd"
@change="val => handleFpssChange(val, index)"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
minWidth: '200px'
}"
>
<a-select-option
v-for="opt in rowStates[index]?.fpssOptions || []"
@ -520,7 +525,7 @@ const handleHbrvcdChange = async (
}
state.engLoading = true;
try {
const res = await getEngInfoDropdown({ rvcd });
const res = await getEngInfoDropdown({ reachcd:rvcd });
state.engOptions = res.data || [];
} catch (e) {
message.error('获取电站列表失败');
@ -550,7 +555,7 @@ const handleEngChange = async (
}
state.fpssLoading = true;
try {
const res = await getFpssDropdown({ rstcd, rvcd: editingData.value.rvcd });
const res = await getFpssDropdown({ rstcd, reachcd: editingData.value.rvcd });
state.fpssOptions = res.data || [];
} catch (e) {
message.error('获取设施列表失败');

View File

@ -7,7 +7,7 @@ import request from '@/utils/request';
*/
export function getBotanicalGardenList(data: any) {
return request({
url: '/wmp-env-server/env/vp/basin/GetKendoListCust',
url: '/vap/basin/GetKendoListCust',
method: 'post',
data: data,
});
@ -21,7 +21,7 @@ export function getBotanicalGardenList(data: any) {
export function getBotanicalGardenStatistic(params: any) {
const { filter, group, groupResultFlat } = params;
return request({
url: '/wmp-env-server/env/vp/bcount/GetKendoListCust',
url: '/vap/bcount/GetKendoListCust',
method: 'post',
data: {
data: {

View File

@ -0,0 +1,207 @@
import request from '@/utils/request';
// 电站基本信息统计 - GetKendoList
export function baseMsstbprptGetKendoList(data: any) {
return request({
url: '/overview/msstbprpt/GetKendoList',
method: 'post',
data: data
});
}
// 监测点水温变化 - 获取测站树形列表
export function getCeZhanTreeList(params: any) {
const data: any = {
filter: params,
select: ['id', 'stcd', 'rstcd', 'ennm', 'stnm', 'sttpCode', 'lgtd', 'lttd', 'orderIndex'],
orders: { rstcdStepSort: 'asc' }
};
return request({
url: '/eng/base/engTree/GetKendoList',
method: 'post',
data: data
});
}
// 监测点水温变化 - 获取有水温数据的测站点
export function getCeZhan(data: any) {
return request({
url: '/wt/wtrv/default/stcd',
method: 'post',
data: data
});
}
// 监测点水温变化 - 获取水温变化时序数据
export function getChangeInWaterTemperature(data: any) {
return request({
url: '/wt/wtrv/GetKendoListCust',
method: 'post',
data
});
}
// 水质监测 - 获取测站树形列表
export function getWaterQualityStationTreeList(params: any) {
const data: any = {
filter: params,
select: ['stcd', 'stnm', 'lgtd', 'lttd'],
sort: [
{ field: 'rstcdStepSort', sort: 'desc' },
{ field: 'orderIndex', sort: 'asc' }
]
};
return request({
url: '/eng/base/vmsstbprpt/engTree/GetKendoList',
method: 'post',
data
});
}
// 水质监测 - 获取元素配置(图例名称、颜色、单位)
export function getWaterQualityElementConfig(params: { stcd: string; startTime: string; endTime: string }) {
return request({
url: '/wq/stbprp/GetStbprpYsByStcd',
method: 'get',
params: { ...params, tbCode: 'WQ_R' }
});
}
// 水质监测 - 获取水质时序数据
export function getWaterQualityData(params: any) {
const data: any = { filter: params, sort: [{ field: 'tm', dir: 'asc' }] };
return request({
url: '/wq/data/noAuth/GetKendoListCust',
method: 'post',
data
});
}
// 水质达标率 - 获取测站列表扁平数组按baseId分组构建树
export function getShuiZhiDaBiaoLvStationList(params: any) {
const data: any = {
filter: params,
sort: [
{ field: 'hbrvcd', dir: 'asc' },
{ field: 'rstcdStepSort', dir: 'asc' }
],
select: []
};
return request({
url: '/wq/vmsstbprpt/GetKendoList',
method: 'post',
data
});
}
// 水质达标率 - 获取默认测站
export function getShuiZhiDaBiaoLvDefaultStcd(params: any) {
return request({
url: '/wq/default/stcd',
method: 'post',
data: { filter: params }
});
}
// 水质达标率 - 获取月度达标率数据
export function getShuiZhiDaBiaoLvData(params: any) {
return request({
url: '/wq/GetKendoListCust',
method: 'post',
data: { filter: params }
});
}
// 垂向水温变化 - 获取测站下拉列表
export function getChuiXiangShuiWenStationList(params: any) {
const data: any = {
filter: params,
select: ['id', 'stcd', 'rstcd', 'ennm', 'stnm', 'sttpCode', 'lgtd', 'lttd', 'orderIndex']
};
return request({
url: '/wt/msstbprpt/GetKendoList',
method: 'post',
data
});
}
// 垂向水温变化 - 获取默认站码
export function getChuiXiangShuiWenDefaultStcd(params: any) {
return request({
url: '/wt/sdrvwts/default/stcd',
method: 'post',
data: { filter: params }
});
}
// 垂向水温变化 - 获取垂向水温数据(按月分组)
export function getChuiXiangShuiWenData(params: any) {
return request({
url: '/wt/dzCxList/GetKendoListCust',
method: 'post',
data: { filter: params }
});
}
// 生态流量数据分析 - 获取日数据
export function getEcologicalFlowData(params: any) {
const data: any = { filter: params, sort: [{ field: 'tm', dir: 'asc' }] };
return request({
url: '/eq/GetKendoListCust',
method: 'post',
data
});
}
// 过鱼总量 - 获取设施树
export function getGuoYuZongLiangFacilityTree(params: any) {
return request({
url: '/fp/query/getTreeStcdList',
method: 'post',
data: { filter: params }
});
}
// 过鱼总量 - 获取饼图数据(按鱼类分组统计)
export function getGuoYuZongLiangPieData(params: any) {
const group = [
{
dir: 'asc',
field: 'ftp',
aggregates: [{ field: 'fcnt', aggregate: 'sum' }]
}
];
return request({
url: '/fp/query/qgc/GetKendoListCust',
method: 'post',
data: { filter: params, group }
});
}
// 过鱼总量 - 人工站表格数据(年份/鱼类/规格/数量)
export function getOverfishTotal(params: any) {
return request({
url: '/fp/query/qgc/getOverfishTotal',
method: 'post',
data: params
});
}
// 过鱼总量 - 自动站日数据(图表+表格)
export function getGuoYuShuLiangDay(params: any) {
return request({
url: '/fp/fpssrlQdays/GetKendoListCust',
method: 'post',
data: params
});
}
// 过鱼总量 - 自动站月数据(图表+表格)
export function getGuoYuShuLiangMonth(params: any) {
return request({
url: '/fp/fpssrlQdrtps/GetKendoListCust',
method: 'post',
data: params
});
}

View File

@ -130,3 +130,29 @@ export function noAuthGetKendoPage(data: any = {}) {
data
});
}
//综合导出
//taskId
export function qgcExportCreate (data: any = {}) {
return request({
url: '/qgcExport/create',
method: 'post',
params:data
});
}
//查看没有没准备完成
export function qgcExportstatus (data: any = {}) {
return request({
url: '/qgcExport/status',
method: 'get',
params:data
});
}
// 下载zip文件
export function qgcExportdownload (data: any = {}) {
return request({
url: '/qgcExport/download',
method: 'post',
params:data,
responseType: 'blob'
});
}

View File

@ -58,11 +58,11 @@
v-model:tree-expanded-keys="treeExpandedKeys"
show-search
:size="'small'"
style="width: 110px"
:style="{ width: moreSelect.width || '110px' }"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
minWidth: '200px'
minWidth: moreSelect.minWidth ? moreSelect.minWidth : '200px'
}"
placeholder=" "
:tree-data="processedMoreSelectOptions"
@ -130,8 +130,9 @@
:picker="scopeDate.picker"
:allowClear="false"
:style="{
width:
scopeDate.picker === 'year'
width: scopeDate.width
? scopeDate.width
: scopeDate.picker === 'year'
? '80px'
: scopeDate.picker === 'month'
? '180px'
@ -160,7 +161,7 @@
</div>
</div>
</div>
<div class="body">
<div class="body" :style="{ marginBottom: props.bodyMarginBottom }">
<slot v-if="isExpand" />
</div>
</div>
@ -259,7 +260,9 @@ const props = defineProps({
default: () => ({
show: false,
value: undefined,
options: []
options: [],
width: '',
minWidth: ''
})
},
datetimePicker: {
@ -282,7 +285,8 @@ const props = defineProps({
show: false,
value: undefined,
format: null, //YYYY-MM-DD HH
picker: 'month' //date | week | month | quarter | year
picker: 'month', //date | week | month | quarter | year
width: ''
})
},
tabs: {
@ -291,6 +295,10 @@ const props = defineProps({
show: false,
value: 'one'
})
},
bodyMarginBottom: {
type: String,
default: '16px'
}
});
const emit = defineEmits(['tab-change', 'update-values']);
@ -972,6 +980,7 @@ onUnmounted(() => {
<style lang="scss">
.qgc-side-pannel-item {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
flex-direction: column;
@ -984,7 +993,7 @@ onUnmounted(() => {
font-size: 16px;
color: #2f6b98;
line-height: 36px;
padding-left: 16px;
padding-left: 10px;
padding-right: 8px;
display: flex;
justify-content: space-between;
@ -1062,7 +1071,7 @@ onUnmounted(() => {
font-size: 14px;
line-height: 22px;
padding: 16px 0 0;
margin-bottom: 16px;
text-overflow: ellipsis;
overflow: hidden;
height: calc(100% - 36px);

View File

@ -102,6 +102,7 @@ const getData = async () => {
const res1Data = res1?.data?.data || [];
const res2Data = res2?.data?.data || [];
// debugger
if (res1Data.length > 0 || res2Data.length > 0) {
const ttpwrlist = res1Data.map((item: any) => ({

View File

@ -58,14 +58,7 @@
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onUnmounted,
nextTick,
watch,
defineOptions
} from 'vue';
import { ref, onMounted, onUnmounted, nextTick, watch, inject,defineOptions, computed, type Ref } from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -73,6 +66,7 @@ import { yearGetYearFpStatistics } from '@/api/gyss';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import type { ColumnsType } from 'ant-design-vue/es/table/Table';
import { useDraggable } from '@/utils/drag';
import dayjs from 'dayjs';
import FishResource from '@/components/FishResource/index.vue';
// API
@ -835,6 +829,47 @@ watch(
},
{ deep: true, immediate: true }
);
// ==================== ppbclTagList ====================
const MODULE_PATH = '@/modules/GYZLLB/index.vue';
const ppbclConfigMap = inject<Ref<Record<string, { isShow: number; tagValue: string }>>>('ppbclConfigMap', ref({}));
/** 根据 tagValue 计算默认年份YYYY 格式) */
function getYearFromTagValue(tagValue: string): string | null {
const now = dayjs();
switch (tagValue) {
case '1y':
return now.subtract(1, 'year').format('YYYY');
case '3y':
return now.subtract(3, 'year').format('YYYY');
case '5y':
return now.subtract(5, 'year').format('YYYY');
default:
return null;
}
}
watch(
() => ppbclConfigMap?.value?.[MODULE_PATH],
(config) => {
if (config) {
// isShow
datetimePicker.value.show = config.isShow !== 0;
// tagValue
if (config.tagValue) {
const year = getYearFromTagValue(config.tagValue);
if (year) {
datetimePicker.value.value = year;
}
}
}
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,457 @@
<!-- 垂向水温变化 -->
<template>
<SidePanelItem
title="垂向水温变化"
:bodyMarginBottom="'2px'"
:select="selectConfig"
:datetimePicker="(datetimePickerConfig as any)"
@update-values="onUpdateValues"
>
<div class="cxsw-wrapper">
<a-spin :spinning="loading" tip="加载中..." class="chart-container">
<div v-show="hasData" ref="chartRef" class="chart" style="height: 100%;"></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useModelStore } from '@/store/modules/model';
import {
getChuiXiangShuiWenStationList,
getChuiXiangShuiWenDefaultStcd,
getChuiXiangShuiWenData
} from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'DZChuiXiangShuiWenBianHua' });
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const selectOptions = ref<{ label: string; value: string }[]>([]);
const selectData = ref('');
const yearValue = ref<Dayjs>(dayjs());
const modelStore = useModelStore();
// aggregates { "1": [{wthg, vwt}], "2": [...] }
const chartAggregates = ref<Record<
string,
{ wthg: string; vwt: string }[]
> | null>(null);
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== SidePanelItem ====================
/** 测站下拉选择器 */
const selectConfig = computed(() => ({
show: true,
value: selectData.value,
options: selectOptions.value,
width: '158px',
minWidth: '200px'
}));
/** 年份选择器 */
const datetimePickerConfig = computed(() => ({
show: true,
value: yearValue.value,
picker: 'year' as const,
format: 'YYYY'
}));
// ==================== ====================
const MONTH_LABELS = [
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月'
];
const buildOption = () => {
const aggs = chartAggregates.value;
if (!aggs) return {};
const legend: string[] = [];
const seriesData: any[] = [];
MONTH_LABELS.forEach((label, idx) => {
const monthKey = String(idx + 1);
const monthData = aggs[monthKey];
if (monthData && monthData.length > 0) {
legend.push(label);
seriesData.push({
name: label,
data: monthData.map((item: any) => [
parseFloat(item.vwt),
parseFloat(item.wthg)
]),
type: 'line',
smooth: true,
connectNulls: true,
symbolSize: 2
});
}
});
return {
tooltip: {
show: true,
trigger: 'item' as const,
formatter: (params: any) => {
return `<span>${params.seriesName}</span><br /><span>水温(℃)${params.data[0]}</span><br /><span>深度(m)${params.data[1]}</span>`;
}
},
grid: {
top: '23%',
right: '15%',
bottom: '20%'
},
legend: {
type: 'scroll' as const,
data: legend,
top: 0,
left: 'center'
},
xAxis: {
name: '水温(°C)',
type: 'value' as const,
position: 'top' as const,
nameLocation: 'end' as const,
boundaryGap: false,
axisTick: { show: false },
min: 0,
max: 25,
splitNumber: 5,
interval: 5
},
yAxis: {
type: 'value' as const,
name: '深度(m)',
min: 0,
inverse: true
},
dataZoom: [{ type: 'inside' as const }],
series: seriesData
};
};
// ==================== ECharts ====================
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', handleResize);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
const stnm = selectOptions.value.find(o => o.value === selectData.value)?.label || '';
modelStore.modalVisible = true;
modelStore.params.sttp = 'WTRV';
modelStore.params.enfc = '1';
modelStore.title = stnm;
modelStore.params.stcd = selectData.value || '';
modelStore.filter.rangeTm = [
yearValue.value.endOf('year').format('YYYY-MM-DD HH:mm:ss'),
yearValue.value.startOf('year').format('YYYY-MM-DD HH:mm:ss')
];
}
});
if (hasData.value && chartAggregates.value) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 1. 获取测站下拉列表 + 2. 默认站码 */
const fetchStationList = async () => {
if (!stcd.value) return;
loading.value = true;
const params: any = {
logic: 'and',
filters: [
{ field: 'sttpCode', operator: 'eq', dataType: 'string', value: 'WTVT' },
{ field: 'rstcd', operator: 'eq', dataType: 'string', value: stcd.value }
]
};
try {
const res = await getChuiXiangShuiWenStationList(params);
const list = res?.data?.data ?? [];
if (list.length > 0) {
//
const options = list.map((item: any) => ({
label: item.stnm,
value: item.stcd
}));
selectOptions.value = options;
selectData.value = options[0]?.value ?? '';
//
await fetchDefaultStcd();
} else {
selectOptions.value = [];
selectData.value = '';
chartAggregates.value = null;
hasData.value = false;
loading.value = false;
}
} catch (err) {
console.error('获取测站列表失败', err);
loading.value = false;
}
};
/** 2. 获取默认站码 */
const fetchDefaultStcd = async () => {
const params: any = {
logic: 'and',
filters: [
{ field: 'mway', operator: 'eq', dataType: 'string', value: 2 },
{ field: 'rstcd', operator: 'eq', dataType: 'string', value: stcd.value },
{
field: 'year',
operator: 'eq',
dataType: 'string',
value: yearValue.value.format('YYYY')
}
]
};
try {
const res = await getChuiXiangShuiWenDefaultStcd(params);
const list = res?.data ?? [];
if (list.length > 0) {
selectData.value = list[0].stcd || selectOptions.value[0]?.value || '';
} else {
// fallback:
selectData.value = selectOptions.value[0]?.value ?? '';
}
if (!selectData.value) {
loading.value = false;
}
} catch (err) {
console.error('获取默认站码失败', err);
// fallback
selectData.value = selectOptions.value[0]?.value ?? '';
if (!selectData.value) {
loading.value = false;
}
}
};
/** 3. 获取垂向水温数据 */
const fetchChartData = async () => {
if (!selectData.value) return;
const requestStcd = selectData.value;
loading.value = true;
const params: any = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: selectData.value
},
{
field: 'year',
operator: 'eq',
dataType: 'string',
value: yearValue.value.format('YYYY')
}
]
};
try {
const res = await getChuiXiangShuiWenData(params);
// selectData
if (requestStcd !== selectData.value) return;
const aggregates =
res?.data?.data?.aggregates ?? res?.data?.aggregates ?? null;
if (aggregates && Object.keys(aggregates).length > 0) {
const hasAnyData = Object.values(aggregates).some(
(arr: any) => Array.isArray(arr) && arr.length > 0
);
if (!hasAnyData) {
chartAggregates.value = null;
hasData.value = false;
loading.value = false;
return;
}
chartAggregates.value = aggregates;
hasData.value = true;
await nextTick();
if (!chartInstance) {
initChart();
}
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
chartAggregates.value = null;
hasData.value = false;
}
} catch (err) {
console.error('获取垂向水温数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
// handleTreeSelect1
if (payload?.select && payload.select.value != null) {
selectData.value = payload.select.value;
return;
}
// handleChange
if (typeof payload === 'string') {
selectData.value = payload;
return;
}
//
// debugger
if (payload?.datetime) {
yearValue.value = dayjs(payload.datetime);
}
};
// ==================== ====================
/** 监听 stcd → 重新获取全部 */
watch(stcd, newStcd => {
if (newStcd) {
destroyChart();
nextTick(() => {
fetchStationList();
});
}
});
/** 监听 selectData → 请求数据 */
watch(selectData, val => {
if (val) {
fetchChartData();
}
});
/** 监听 yearValue → 仅刷新图表数据 */
watch(yearValue, () => {
if (selectData.value) {
destroyChart();
nextTick(() => {
fetchChartData();
});
}
});
// ==================== ====================
onMounted(() => {
if (stcd.value) {
fetchStationList();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.cxsw-wrapper {
width: 100%;
height: 100%;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<SidePanelItem title="社会投资、扶贫就业">
<div class="shtzfp-wrapper">
<a-spin :spinning="loading" tip="加载中...">
<div v-if="hasData" class="card-list">
<a-card
v-for="item in cardItems"
:key="item.title"
class="stat-card"
hoverable
size="small"
>
<div class="card-content">
<i class="icon iconfont" :class="item.icon" />
<span class="card-title">{{ item.title }}</span>
</div>
<span
class="card-value"
:style="item.color ? { color: item.color } : {}"
>
{{ item.totalNum ?? '-' }}
</span>
</a-card>
</div>
<a-empty
v-if="!hasData && !loading"
description="暂无数据"
class="empty-center"
/>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { getSocialInvestmentData } from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'DZSheHuiTouZiFP' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const cardItems = ref<
Array<{ icon: string; title: string; totalNum: number | string | null; color?: string }>
>([]);
// ==================== API ====================
const fetchData = async () => {
if (!stcd.value) return;
loading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd.value }
]
},
group: [{ dir: 'des', field: 'stcd' }],
aggregate: [
{ aggregate: 'sum', field: 'stjtz' },
{ aggregate: 'sum', field: 'dirjob' },
{ aggregate: 'sum', field: 'indjob' },
{ aggregate: 'sum', field: 'poorjobeff' },
{ aggregate: 'sum', field: 'ldtz' }
]
};
const res = await getSocialInvestmentData(params);
const data = res?.data?.data?.[0];
if (data) {
hasData.value = true;
const { dirjob, indjob, poorjobeff, ldtz, stjtz } = data;
cardItems.value = [
{ icon: 'icon-sum', title: '扶贫就业收益(万元)', totalNum: poorjobeff, color: '#FCA900' },
{ icon: 'icon-people', title: '直接就业人口(人)', totalNum: dirjob },
{ icon: 'icon-people', title: '间接就业人口(人)', totalNum: indjob },
{ icon: 'icon-sum', title: '总投资(万元)', totalNum: stjtz, color: '#FCA900' },
{ icon: 'icon-sum', title: '总拉动投资(万元)', totalNum: ldtz, color: '#FCA900' }
];
} else {
hasData.value = false;
}
} catch {
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
watch(stcd, (newStcd) => {
if (newStcd) {
fetchData();
}
});
</script>
<style scoped lang="scss">
.shtzfp-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.card-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow: auto;
height: 100%;
}
.stat-card {
cursor: default;
width: 100%;
}
.stat-card :deep(.ant-card-body) {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 1px 10px;
}
.card-content {
display: flex;
align-items: center;
width: 50%;
overflow: hidden;
}
.card-content .icon {
font-size: 18px;
flex-shrink: 0;
margin-right: 17px;
color: #2f6b98;
}
.card-title {
font-size: 14px;
color: #000000d9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-value {
width: 50%;
font-size: 24px;
/* font-weight: 600; */
flex-shrink: 0;
margin-left: 8px;
text-align: right;
}
</style>

View File

@ -0,0 +1,498 @@
<template>
<div class="dz-video-monitor">
<!-- 标题栏 -->
<div class="video-header">
<span class="video-title">水电站视频监控</span>
<span class="more-btn" @click="openMoreModal">
<i class="iconfont icon-moreVideo" />
</span>
</div>
<!-- 视频缩略图跑马灯 -->
<div class="video-carousel-wrapper" ref="carouselRef">
<div v-if="videoList.length === 0" class="video-empty">
<a-empty description="暂无视频数据" />
</div>
<template v-else>
<!-- 左箭头 -->
<div
v-if="videoList.length > visibleCount"
class="carousel-arrow arrow-left"
@click="prevSlide"
>
<LeftOutlined />
</div>
<!-- 视频轨道 -->
<div
class="carousel-track"
:style="{ transform: `translateX(-${scrollOffset}px)` }"
>
<div
v-for="item in videoList"
:key="item.vdid"
class="video-item"
:style="{ width: itemWidth + 'px' }"
>
<div class="video-thumb">
<img :src="item.imgPath" :alt="item.vdnm || item.flnm" />
<div class="play-overlay" @click="playVideo(item)">
<i class="iconfont icon-play" />
</div>
</div>
<a-tooltip :title="item.vdnm || item.flnm">
<div class="video-name">{{ item.vdnm || item.flnm }}</div>
</a-tooltip>
</div>
</div>
<!-- 右箭头 -->
<div
v-if="videoList.length > visibleCount"
class="carousel-arrow arrow-right"
@click="nextSlide"
>
<RightOutlined />
</div>
</template>
</div>
<!-- 单个视频播放弹窗 -->
<a-modal
v-model:open="playModalVisible"
title="视频播放"
width="900px"
:footer="null"
destroyOnClose
>
<VideoPlayer
:list="videoList"
:activeFid="activeFid"
:activeMedia="activeMedia"
:loading="false"
:page="1"
:pageSize="20"
:pageSizeOptions="['20', '50', '100']"
:total="videoList.length"
@select="handleSelectMedia"
/>
</a-modal>
<!-- 更多视频弹窗 -->
<a-modal
v-model:open="moreModalVisible"
title="视频监控"
width="90%"
:footer="null"
destroyOnClose
>
<VideoInfo :is-active="moreModalVisible" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import {
ref,
inject,
watch,
onMounted,
onUnmounted,
type Ref
} from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import VideoPlayer from '@/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue';
import VideoInfo from '@/components/MapModal/components/videoInfo.vue';
import { videoSurveillance, realTimeVideo } from '@/api/video';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
const modelStore = useModelStore();
defineOptions({
name: 'DZShuiDianZhanSPJK'
});
interface VideoItem {
vdid: string;
vdnm?: string;
flnm?: string;
imgPath: string;
url?: string;
/** VideoPlayer 兼容字段 */
fid: string;
type: string;
flpth: string;
tm: string;
ennm: string;
}
interface DianZhanStation {
wbsCode: string;
stcd: string;
ennm: string;
lgtd: number;
lttd: number;
}
const dianZhanStation = inject<Ref<DianZhanStation | null>>('dianZhanStation');
//
const videoList = ref<VideoItem[]>([]);
//
const carouselRef = ref<HTMLElement | null>(null);
const visibleCount = ref(4);
const itemWidth = ref(160);
const scrollOffset = ref(0);
const currentSlide = ref(0);
//
const playModalVisible = ref(false);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
//
const moreModalVisible = ref(false);
//
const updateVisibleCount = () => {
if (carouselRef.value) {
const width = carouselRef.value.offsetWidth;
visibleCount.value = Math.max(
1,
Math.floor((width - 60) / (itemWidth.value + 8))
);
}
};
//
const nextSlide = () => {
const maxSlide = Math.max(0, videoList.value.length - visibleCount.value);
if (currentSlide.value < maxSlide) {
currentSlide.value++;
scrollOffset.value = currentSlide.value * (itemWidth.value + 8);
}
};
const prevSlide = () => {
if (currentSlide.value > 0) {
currentSlide.value--;
scrollOffset.value = currentSlide.value * (itemWidth.value + 8);
}
};
//
const playVideo = (item: VideoItem) => {
setActiveVideo(item);
playModalVisible.value = true;
};
//
const setActiveVideo = (item: VideoItem) => {
activeFid.value = item.fid;
activeMedia.value = {
type: 'video',
src: item.flpth || item.url || ''
};
};
// VideoPlayer
const handleSelectMedia = (item: any) => {
activeFid.value = item.fid;
activeMedia.value = {
type: 'video',
src: item.flpth || item.src || ''
};
// debugger
};
//
const openMoreModal = () => {
moreModalVisible.value = true;
// stcd modelStoreVideoInfo modelStore.params.stcd
if (dianZhanStation?.value?.stcd) {
modelStore.params.stcd = dianZhanStation.value.stcd;
}
};
//
const fetchVideoData = async (stcd: string) => {
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'sttpCode',
operator: 'in',
dataType: 'string',
value: ['VD_FBFM', 'VD_EQS']
}
]
}
};
//
const surveillanceRes = await videoSurveillance(params);
const stcdList = surveillanceRes?.data?.data || [];
if (!stcdList.length) {
videoList.value = [];
return;
}
// stcd
const stcds = stcdList.map((item: any) => item.stcd).filter(Boolean);
if (!stcds.length) {
videoList.value = [];
return;
}
//
const timeRange = [
dayjs().subtract(7, 'day').format('YYYY-MM-DD 00:00:00'),
dayjs().format('YYYY-MM-DD 23:59:59')
];
const params1 = {
take: 100,
skip: 0,
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcds
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: timeRange[0]
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: timeRange[1]
}
// {
// field: 'tm',
// operator: 'gte',
// dataType: 'date',
// value: '2024-05-01 00:00:00'
// },
// {
// field: 'tm',
// operator: 'lte',
// dataType: 'date',
// value: '2026-05-31 23:59:59'
// }
]
},
sort: [
{
field: 'tm',
dir: 'desc'
}
]
};
const videoRes = await realTimeVideo(params1);
const videoData = videoRes?.data?.data || videoRes?.data || [];
// debugger
const videoItems: VideoItem[] = (
Array.isArray(videoData) ? videoData : []
).map((item: any) => ({
vdid: item.vdid,
vdnm: item.vdnm,
flnm: item.flnm,
imgPath: item.imgPath || '',
url: item.url || item.flv || item.hls,
// VideoPlayer
fid: item.vdid,
type: 'vdsp',
flpth: item.url || item.flv || item.hls || '',
tm: item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-',
ennm: item.ennm || ''
}));
videoList.value = videoItems;
// debugger
currentSlide.value = 0;
scrollOffset.value = 0;
} catch (error) {
console.error('获取视频数据失败:', error);
videoList.value = [];
}
};
//
watch(
() => dianZhanStation?.value,
newVal => {
if (newVal?.stcd) {
fetchVideoData(newVal.stcd);
}
},
{ deep: true, immediate: true }
);
onMounted(() => {
updateVisibleCount();
window.addEventListener('resize', updateVisibleCount);
});
onUnmounted(() => {
window.removeEventListener('resize', updateVisibleCount);
});
</script>
<style lang="scss" scoped>
.dz-video-monitor {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255);
.video-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 12px 3px 0px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
background: #e5edf3;
.video-title {
font-size: 16px;
color: #2f6b98;
font-weight: 500;
display: inline-block;
border-left:2px solid #2f6b98;
padding-left: 16px;
}
.more-btn {
cursor: pointer;
font-size: 18px;
color: #666;
transition: color 0.2s;
&:hover {
color: #005293;
}
}
}
.video-carousel-wrapper {
flex: 1;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
padding: 0 28px;
.video-empty {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.carousel-track {
display: flex;
gap: 8px;
transition: transform 0.3s ease;
align-items: center;
height: 100%;
.video-item {
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
.video-thumb {
position: relative;
width: 100%;
height: 110px;
background: #1a1a1a;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
cursor: pointer;
i {
font-size: 16px;
color: #fff;
}
}
}
.video-name {
padding: 4px 6px;
font-size: 12px;
color: #333;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 5;
width: 24px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
color: #fff;
cursor: pointer;
border-radius: 2px;
transition: background 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.5);
}
&.arrow-left {
left: 2px;
}
&.arrow-right {
right: 2px;
}
}
}
}
</style>

View File

@ -0,0 +1,551 @@
<!-- 水质达标率 -->
<template>
<SidePanelItem
title="水质达标率"
:bodyMarginBottom="'2px'"
:moreSelect="moreSelectConfig"
:datetimePicker="(datetimePickerConfig as any)"
@update-values="onUpdateValues"
>
<div class="szdbl-wrapper">
<div class="tabs_con">
<div
:class="mway == 2 ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange(2)"
>
自动站点
</div>
<div
:class="mway == 1 ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange(1)"
>
人工站点
</div>
</div>
<div class="tabs_body">
<a-spin :spinning="loading" tip="加载中..." class="chart-container chart_baowei">
<div v-show="hasData" ref="chartRef" class="chart" style="height: 100%" ></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useModelStore } from '@/store/modules/model';
import {
getShuiZhiDaBiaoLvStationList,
getShuiZhiDaBiaoLvDefaultStcd,
getShuiZhiDaBiaoLvData
} from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'DZShuiZhiDaBiaoLv' });
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const MONTH_LABELS = [
'1月', '2月', '3月', '4月', '5月', '6月',
'7月', '8月', '9月', '10月', '11月', '12月'
];
// ==================== ====================
const mway = ref<number>(2); // 2=, 1=
const loading = ref(false);
const hasData = ref(false);
const yearValue = ref<Dayjs>(dayjs());
const selectValue = ref<{ value: string; label: string } | null>(null);
const optionsList = ref<any[]>([]);
const monthValue = ref<(string | number)[]>([]);
const modelStore = useModelStore();
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== SidePanelItem ====================
/** 测站树选择器 */
const moreSelectConfig = computed(() => ({
show: true,
value: selectValue.value?.value ?? '',
options: optionsList.value,
width: '120px'
}));
/** 年份选择器 */
const datetimePickerConfig = computed(() => ({
show: true,
value: yearValue.value,
picker: 'year' as const,
format: 'YYYY'
}));
// ==================== ====================
const buildTreeData = (flatList: any[]): any[] => {
if (!Array.isArray(flatList) || flatList.length === 0) return [];
// baseId
const groupMap: Record<string, any[]> = {};
flatList.forEach((item: any) => {
const key = item.baseId || 'other';
if (!groupMap[key]) groupMap[key] = [];
groupMap[key].push(item);
});
//
return Object.keys(groupMap)
.filter(key => key !== 'other')
.sort((a, b) => Number(a) - Number(b))
.map(key => ({
title: groupMap[key][0]?.baseName || key,
value: key,
selectable: false,
children: groupMap[key]
.sort((a, b) => (a.siteStepSort ?? 0) - (b.siteStepSort ?? 0))
.map((item: any) => ({
title: item.stnm,
value: item.stcd
}))
}));
};
// ==================== ====================
const buildOption = () => ({
color: ['#4B79AB'],
legend: {
data: ['达标率'],
itemWidth: 16,
itemHeight: 10,
textStyle: { fontSize: 12 }
},
tooltip: {
show: true,
confine: true,
trigger: 'axis' as const,
axisPointer: { type: 'none' },
formatter: (params: any) => {
const item = Array.isArray(params) ? params[0] : params;
const hasVal = item.value != null && item.value !== '-';
const value = hasVal ? `${item.value} %` : '--';
return `<div>${item.marker}达标率: ${value}</div>`;
}
},
grid: {
top: 30,
bottom: 10,
right: 10,
left: 5,
containLabel: true
},
xAxis: {
type: 'category' as const,
data: MONTH_LABELS,
axisLabel: { fontSize: 11, color: '#000' },
axisTick: { show: false },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } }
},
yAxis: {
type: 'value' as const,
name: '达标率(%)',
nameTextStyle: { fontSize: 12, color: '#000' },
axisLabel: { fontSize: 11, color: '#000' },
axisTick: { show: true },
axisLine: { show: true },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } },
min: 0,
max: 100
},
series: [
{
name: '达标率',
data: monthValue.value,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 2,
showSymbol: true,
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#d5e0eb' },
{ offset: 1, color: '#fbfcfd' }
]
}
}
}
],
dataZoom: [{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true }]
});
// ==================== ECharts ====================
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', handleResize);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
const stnm = selectValue.value?.label || '';
modelStore.modalVisible = true;
modelStore.params.sttp = 'WQ';
modelStore.title = stnm;
modelStore.params.stcd = selectValue.value?.value || '';
modelStore.filter.rangeTm = [
yearValue.value.endOf('year').format('YYYY-MM-DD HH:mm:ss'),
yearValue.value.startOf('year').format('YYYY-MM-DD HH:mm:ss')
];
}
});
if (hasData.value) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 1. 获取测站列表 → 构建树 → 获取默认测站 */
const fetchStationList = async () => {
if (!stcd.value) return;
loading.value = true;
const params: any = {
logic: 'and',
filters: [
{ field: 'sttpCode', operator: 'eq', dataType: 'string', value: 'WQ' },
{ field: 'usfl', operator: 'eq', dataType: 'string', value: '1' },
{ field: 'mway', operator: 'eq', dataType: 'number', value: mway.value },
{ field: 'rstcd', operator: 'eq', dataType: 'string', value: stcd.value }
]
};
try {
const res = await getShuiZhiDaBiaoLvStationList(params);
const list = res?.data?.data ?? [];
if (list.length > 0) {
optionsList.value = buildTreeData(list);
// mway==1
if (mway.value === 1) {
const firstLeaf = findFirstLeaf(optionsList.value);
if (firstLeaf) {
selectValue.value = { value: firstLeaf.value, label: firstLeaf.title };
}
} else {
await fetchDefaultStcd(list);
}
// loading
if (!selectValue.value) {
monthValue.value = [];
hasData.value = false;
loading.value = false;
}
} else {
optionsList.value = [];
selectValue.value = null;
monthValue.value = [];
hasData.value = false;
loading.value = false;
}
} catch (err) {
console.error('获取测站列表失败', err);
loading.value = false;
}
};
/** 2. 获取默认测站 */
const fetchDefaultStcd = async (stationList: any[]) => {
const params: any = {
logic: 'and',
filters: [
{ field: 'drYear', operator: 'eq', dataType: 'string', value: yearValue.value.format('YYYY') },
{ field: 'TYPE', operator: 'eq', dataType: 'string', value: 'MON' },
{ field: 'mway', operator: 'eq', dataType: 'number', value: mway.value },
{ field: 'rstcd', operator: 'eq', dataType: 'string', value: stcd.value }
]
};
try {
const res = await getShuiZhiDaBiaoLvDefaultStcd(params);
const list = res?.data?.data ?? [];
if (list.length > 0) {
selectValue.value = { value: list[0].stcd, label: list[0].stnm };
} else {
// fallback:
const firstLeaf = findFirstLeaf(optionsList.value);
if (firstLeaf) {
selectValue.value = { value: firstLeaf.value, label: firstLeaf.title };
}
}
} catch (err) {
console.error('获取默认测站失败', err);
// fallback
const firstLeaf = findFirstLeaf(optionsList.value);
if (firstLeaf) {
selectValue.value = { value: firstLeaf.value, label: firstLeaf.title };
}
}
};
/** 辅助:找到树中第一个叶子节点 */
const findFirstLeaf = (tree: any[]): any | null => {
for (const node of tree) {
if (node.children?.length) {
return node.children[0];
}
}
return null;
};
/** 3. 获取月度达标率数据 */
const fetchChartData = async () => {
if (!selectValue.value?.value) return;
loading.value = true;
const params: any = {
logic: 'and',
filters: [
{ field: 'drYear', operator: 'eq', dataType: 'string', value: yearValue.value.format('YYYY') },
{ field: 'stcd', operator: 'eq', dataType: 'string', value: selectValue.value.value },
{ field: 'TYPE', operator: 'eq', dataType: 'string', value: 'MON' }
]
};
try {
const res = await getShuiZhiDaBiaoLvData(params);
const list = res?.data?.data ?? [];
if (list.length > 0 && list.every((item: any) => item.wqRate !== null)) {
// 12
const data = Array(12).fill('-');
list.forEach((item: any) => {
const monthIdx = parseInt(dayjs(item.drMonth).format('M'), 10) - 1;
data[monthIdx] = item.wqRate ?? '-';
});
monthValue.value = data;
hasData.value = true;
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
monthValue.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取达标率数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
// value selectedNodeExtra
if (payload.moreSelect) {
const node = payload.selectedNodeExtra || {};
selectValue.value = {
value: payload.moreSelect,
label: node.title || selectValue.value?.label || ''
};
fetchChartData();
}
//
if (payload.datetimePicker) {
yearValue.value = dayjs(payload.datetimePicker);
fetchChartData();
}
};
/** Tab 切换 */
const onTabChange = (key: number) => {
if (key === mway.value) return;
mway.value = key;
};
// ==================== ====================
/** 监听 stcd → 刷新全部 */
watch(stcd, newStcd => {
if (newStcd) {
selectValue.value = null;
destroyChart();
nextTick(() => {
fetchStationList();
});
}
});
/** 监听 mway → 刷新全部 + 重建图表 */
watch(mway, () => {
selectValue.value = null;
destroyChart();
nextTick(() => {
initChart();
fetchStationList();
});
});
/** 监听 selectValue → 图表初始化和数据请求 */
watch(selectValue, (val) => {
if (val?.value) {
if (!chartInstance) {
nextTick(() => initChart());
}
fetchChartData();
}
});
// ==================== ====================
onMounted(() => {
if (stcd.value) {
fetchStationList();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.szdbl-wrapper {
width: 100%;
height: 100%;
padding: 5px;
display: flex;
.tabs_con {
width: 28px;
height: 100%;
border: 2px solid #2f6b98;
border-radius: 5px;
text-align: center;
font-size: 14px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
cursor: pointer;
.tabs_zhong {
color: #fff;
background-color: #2f6b98;
box-sizing: border-box;
padding: 5px 0px;
height:100%;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei {
box-sizing: border-box;
padding: 5px 0px;
height:100%;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei:hover {
color: #2f6b98;
}
}
.tabs_body {
flex: 1;
height: 100%;
margin-left: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
// :deep(.ant-spin-nested-loading) {
// width: 100%;
// // height: 100%;
// }
// :deep(.ant-spin-container) {
// width: 100%;
// height: 100%;
// }
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,761 @@
<!-- 水质监测趋势图 -->
<template>
<SidePanelItem
title="水质监测情况"
:bodyMarginBottom="'2px'"
:moreSelect="moreSelectConfig"
:scopeDate="(scopeDateConfig as any)"
@update-values="onUpdateValues"
>
<!-- {{ moreSelectConfig }} -->
<div class="szjc-wrapper">
<div class="tabs_con">
<div
:class="mway == '2' ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange('2')"
>
自动站点
</div>
<div
:class="mway == '1' ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange('1')"
>
人工站点
</div>
</div>
<!-- 自动站点 -->
<div v-show="mway == '2'" class="tabs_body chart_baowei">
<a-spin
:spinning="loading"
tip="加载中..."
class="chart-container chart_baowei"
>
<div
v-show="hasData"
ref="chartRef"
class="chart"
style="height: 100%"
></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
<!-- 人工站点 -->
<div v-show="mway == '1'" class="tabs_body">
<a-spin
:spinning="loading"
tip="加载中..."
class="chart-container chart_baowei"
>
<div
v-show="hasData"
ref="chartRefManual"
class="chart"
style="height: 100%"
></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useModelStore } from '@/store/modules/model';
import {
getWaterQualityStationTreeList,
getWaterQualityElementConfig,
getWaterQualityData
} from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'DZShuiZhiJianCeQuShiTu' });
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
const ppbclConfigMap: any = inject('ppbclConfigMap', ref({}));
const MODULE_PATH =
'@/modules/dianzhanzhuantiMod/DZShuiZhiJianCeQuShiTu/index.vue';
// ==================== ====================
interface IndicatorConfig {
key: string;
name: string;
unit: string;
color: string;
}
const indicators = ref<IndicatorConfig[]>([]);
const CHART_LINE_NUM = 2; // 线
const modelStore = useModelStore();
// ==================== ====================
const mway = ref<string>('2'); // '2'=, '1'=
const loading = ref(false);
const hasData = ref(false);
const xData = ref<string[]>([]);
const echartsData = ref<any[][]>([]);
const rawDataList = ref<any[]>([]); // tooltip
const selected = ref<string[]>([]); // "PH\n"
//
const optionsList = ref<any[]>([]);
const selectValue = ref<any>(null);
const dateRange = ref<[Dayjs, Dayjs]>([dayjs().subtract(7, 'day'), dayjs()]);
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
const chartRefManual = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const moduleConfig = computed(
() => ppbclConfigMap.value?.[MODULE_PATH] || { isShow: false, tagValue: '' }
);
const defaultDateRange = computed<[Dayjs, Dayjs]>(() => {
const cfg = moduleConfig.value;
if (cfg?.isShow && cfg?.tagValue) {
const d = dayjs(cfg.tagValue);
if (d.isValid()) {
return [d, dayjs()];
}
}
return [dayjs().subtract(7, 'day'), dayjs()];
});
// ==================== SidePanelItem ====================
/** 测站树选择器 */
const moreSelectConfig = computed(() => ({
show: true,
value: selectValue.value?.id ?? '',
options: optionsList.value,
width: '95px',
minWidth: '240px'
}));
/** 日期范围选择器 */
const scopeDateConfig = computed(() => ({
show: true,
value: [defaultDateRange.value[0], defaultDateRange.value[1]] as any,
picker: 'date' as const,
format: 'YYYY-MM-DD',
width: '215px'
}));
// ==================== ====================
const transformTreeData = (nodes: any[]): any[] => {
if (!Array.isArray(nodes)) return [];
return nodes.map((node: any) => ({
title: node.stnm || node.ennm || '',
value: node.stcd || node.id || '',
key: node.stcd || node.id || '',
selectable: node.items ? false : true,
...node,
children: node.items ? transformTreeData(node.items) : undefined
}));
};
// ==================== ====================
const buildOption = () => {
const legendData: string[] = [];
const yAxisList: any[] = [];
const seriesList: any[] = [];
indicators.value.forEach((ind, index) => {
const name = ind.unit ? `${ind.name}(${ind.unit})` : ind.name;
legendData.push(name);
const selIdx = selected.value.indexOf(name);
const show = selIdx > -1;
// y
const vals = (echartsData.value[index] || [])
.filter(v => v != null && v !== '--') as number[];
const dataMin = vals.length ? Math.min(...vals) : 0;
const dataMax = vals.length ? Math.max(...vals) : 0;
const FALLBACK_COLORS = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
const yAxisColor = ind.color || FALLBACK_COLORS[index % FALLBACK_COLORS.length];
yAxisList.push({
type: 'value' as const,
name: `${ind.name}${ind.unit ? `(${ind.unit})` : ''}`,
nameTextStyle: { fontSize: 12, color: yAxisColor },
axisLabel: {
fontSize: 11,
color: yAxisColor,
formatter: (val: number) =>
Number.isInteger(val) ? val : val.toFixed(1)
},
axisTick: { show: true, lineStyle: { color: yAxisColor } },
axisLine: { show: true, lineStyle: { color: yAxisColor } },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } },
splitNumber: 3,
show,
position: selIdx % 2 === 0 ? 'left' : 'right',
alignTicks: true,
min: +(dataMin - dataMin * 0.1).toFixed(2),
max: +(dataMax + dataMax * 0.1).toFixed(2)
});
seriesList.push({
name,
type: 'line',
yAxisIndex: index,
smooth: true,
showSymbol: (params: any) => {
const iv = Math.max(0, Math.ceil(xData.value.length / 9) - 1);
return iv === 0 ? true : params.dataIndex % (iv + 1) === 0;
},
symbol: 'circle',
symbolSize: 2,
connectNulls: true,
itemStyle: { color: yAxisColor },
data: echartsData.value[index] || []
});
});
// legend.selected
const legsel: Record<string, boolean> = {};
legendData.forEach(name => {
legsel[name] = selected.value.includes(name);
});
return {
grid: {
top: 60,
bottom: 30,
right: 45,
left: 35
},
legend: {
data: legendData,
type: 'scroll' as const,
selected: legsel,
itemWidth: 16,
itemHeight: 10,
itemGap: 16,
textStyle: { fontSize: 12 }
},
tooltip: {
show: true,
confine: true,
axisPointer: { type: 'none' },
trigger: 'axis' as const,
formatter: (params: any[]) => {
if (!params?.length) return '';
const item = rawDataList.value[params[0].dataIndex];
const fullTime = item?.tm
? dayjs(item.tm).format('YYYY-MM-DD HH:mm')
: params[0].name;
let html = `<div style="margin-bottom:4px">${fullTime}</div>`;
params.forEach((p: any) => {
const ind = indicators.value.find(
i => (i.unit ? `${i.name}(${i.unit})` : i.name) === p.seriesName
);
const unit = ind?.unit ? ` ${ind.unit}` : '';
const hasVal = p.value != null && p.value !== '--';
const val = hasVal ? `${p.value}${unit}` : '--';
html += `<div>${p.marker}${p.seriesName}: ${val}</div>`;
});
return html;
}
},
xAxis: {
type: 'category' as const,
boundaryGap: false,
data: xData.value,
axisLabel: {
fontSize: 11,
color: '#000',
interval: Math.max(0, Math.ceil(xData.value.length / 9) - 1)
},
axisTick: { show: false },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } }
},
yAxis: yAxisList,
series: seriesList,
dataZoom: [{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true }]
};
};
// ==================== ECharts ====================
const getCurrentChartRef = () =>
mway.value === '2' ? chartRef.value : chartRefManual.value;
const initChart = () => {
const dom = getCurrentChartRef();
if (!dom) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(dom);
window.addEventListener('resize', handleResize);
observeChartResize(dom);
//
chartInstance.on('legendselectchanged', (e: any) => {
const currentSelected = [...selected.value];
const idx = currentSelected.indexOf(e.name);
if (idx >= 0) {
//
currentSelected.splice(idx, 1);
// >=2 3
if (currentSelected.length >= 2 && idx === 2) {
currentSelected.shift();
}
} else if (currentSelected.length >= CHART_LINE_NUM) {
//
currentSelected.shift();
currentSelected.push(e.name);
} else {
currentSelected.push(e.name);
}
selected.value = currentSelected;
chartInstance?.setOption(buildOption(), true);
});
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
const stnm = selectValue.value?.value || '';
modelStore.modalVisible = true;
modelStore.params.sttp = 'WQ';
modelStore.title = stnm;
modelStore.params.stcd = selectValue.value?.id || '';
modelStore.filter.rangeTm = [
dateRange.value[1].format('YYYY-MM-DD HH:mm:ss'),
dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
];
}
});
if (hasData.value) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
let resizeObserver: ResizeObserver | null = null;
const observeChartResize = (dom: HTMLElement) => {
if (typeof ResizeObserver === 'undefined') return;
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(() => chartInstance?.resize());
resizeObserver.observe(dom);
};
const clearResizeObserver = () => {
resizeObserver?.disconnect();
resizeObserver = null;
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
clearResizeObserver();
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 获取测站树列表 */
const fetchStationList = async () => {
if (!stcd.value) return;
const apiMway = Number(mway.value); // 2=, 1=
const params: any = {
logic: 'and',
filters: [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WQ'
},
{
field: 'mway',
operator: 'eq',
dataType: 'number',
value: apiMway
},
{
field: 'dtinType',
operator: 'neq',
dataType: 'number',
value: 1
},
{
field: 'rstcd',
operator: 'in',
dataType: 'string',
value: [stcd.value]
}
]
};
try {
const res = await getWaterQualityStationTreeList(params);
const treeData = res?.data?.data ?? [];
if (treeData?.length > 0) {
optionsList.value = transformTreeData(treeData);
//
const firstItem = treeData[0];
const leafNode = firstItem?.items?.find((ve: any) => ve?.stcd);
if (leafNode) {
selectValue.value = {
value: leafNode.stnm,
id: leafNode.stcd,
opt: { name: leafNode.stnm, id: leafNode.stcd, sttp: 'WQ' }
};
console.log(
'默认选中的节点:',
selectValue.value.id,
selectValue.value,
optionsList.value
);
fetchChartData();
}
} else {
optionsList.value = [];
selectValue.value = null;
loading.value = false;
xData.value = [];
echartsData.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取测站列表失败', err);
loading.value = false;
}
};
/** 获取元素配置(图例名称、颜色、单位) */
const fetchElementConfig = async (): Promise<IndicatorConfig[]> => {
if (!selectValue.value) return [];
const startTime = dateRange.value[0]
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss');
const endTime = dateRange.value[1].endOf('day').format('YYYY-MM-DD HH:mm:ss');
try {
const res = await getWaterQualityElementConfig({
stcd: selectValue.value.id,
startTime,
endTime
});
const list = res?.data?.data ?? [];
return list
.filter((item: any) => item.enable === '1')
.map((item: any) => {
let color = '';
let unit = '';
try {
const ctrl = JSON.parse(item.showControl || '{}');
color = ctrl.lineColor || '';
unit = ctrl.unit || '';
} catch {
// ignore parse error
}
return {
key: item.ys?.toLowerCase() || '',
name: item.ysShowName || item.ys || '',
unit,
color
};
});
} catch (err) {
console.error('获取元素配置失败', err);
return [];
}
};
/** 获取水质图表数据 */
const fetchChartData = async () => {
if (!selectValue.value) return;
loading.value = true;
// 1.
const configList = await fetchElementConfig();
if (configList.length === 0) {
indicators.value = [];
xData.value = [];
echartsData.value = [];
hasData.value = false;
loading.value = false;
return;
}
indicators.value = configList;
// 2.
const params: any = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: selectValue.value.id
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dateRange.value[0].startOf('day').format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dateRange.value[1].endOf('day').format('YYYY-MM-DD HH:mm:ss')
}
]
};
try {
const res = await getWaterQualityData(params);
const list = res?.data?.data ?? [];
rawDataList.value = list;
if (list?.length) {
// : [{ name: tm, data: [] }, ...]
const dataSource = list.map((item: any) => ({
name: item.tm,
data: indicators.value.map(ind => item[ind.key] ?? '--')
}));
xData.value = dataSource.map((item: any) =>
item.name ? dayjs(item.name).format('HH:mm') : '--'
);
// : echartsData[i] = i
const newEchartsData: any[][] = [];
if (dataSource[0]?.data) {
for (let i = 0; i < dataSource[0].data.length; i++) {
newEchartsData.push(dataSource.map(item => item.data[i]));
}
}
echartsData.value = newEchartsData;
// CHART_LINE_NUM
selected.value = indicators.value
.slice(0, CHART_LINE_NUM)
.map(ind => (ind.unit ? `${ind.name}(${ind.unit})` : ind.name));
hasData.value = true;
await nextTick();
if (!chartInstance) {
initChart();
}
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
xData.value = [];
echartsData.value = [];
rawDataList.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取水质数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
// value selectedNodeExtra
if (payload.moreSelect) {
const node = payload.selectedNodeExtra || {};
selectValue.value = {
value: node.title || selectValue.value?.value,
id: payload.moreSelect,
opt: node.title ? { ...node } : selectValue.value?.opt
};
fetchChartData();
}
//
if (
payload.scopeDate &&
Array.isArray(payload.scopeDate) &&
payload.scopeDate.length === 2
) {
const d0 = dayjs(payload.scopeDate[0]);
const d1 = dayjs(payload.scopeDate[1]);
if (d0.isValid() && d1.isValid()) {
dateRange.value = [d0, d1];
fetchChartData();
}
}
};
/** Tab 切换 */
const onTabChange = (key: string) => {
if (key === mway.value) return;
mway.value = key;
};
// ==================== ====================
/** 监听 stcd → 刷新测站列表 */
watch(stcd, newStcd => {
if (newStcd) {
nextTick(() => {
destroyChart();
fetchStationList();
});
}
});
/** 监听 mway → 刷新测站列表 + 重建图表 */
watch(mway, () => {
destroyChart();
nextTick(() => {
fetchStationList();
});
});
// ==================== ====================
onMounted(() => {
// : ppbclConfigMap,
const cfg = moduleConfig.value;
if (cfg?.isShow && cfg?.tagValue) {
const d = dayjs(cfg.tagValue);
if (d.isValid()) {
dateRange.value = [d, dayjs()];
}
}
if (stcd.value) {
fetchStationList();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.szjc-wrapper {
width: 100%;
height: 100%;
padding: 5px;
display: flex;
box-sizing: border-box;
.tabs_con {
width: 28px;
height: 100%;
border: 2px solid #2f6b98;
border-radius: 5px;
text-align: center;
font-size: 14px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
cursor: pointer;
.tabs_zhong {
color: #fff;
background-color: #2f6b98;
box-sizing: border-box;
padding: 5px 0px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei {
box-sizing: border-box;
padding: 5px 0px;
height: 100%;
place-items: center;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei:hover {
color: #2f6b98;
}
}
.tabs_body {
flex: 1;
min-width: 0;
height: 100%;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,772 @@
<!-- 监测点水温变化 -->
<template>
<SidePanelItem
:title="panelTitle"
:bodyMarginBottom="'2px'"
:moreSelect="moreSelectConfig"
:scopeDate="(scopeDateConfig as any)"
@update-values="onUpdateValues"
>
<div class="jcdsw-wrapper">
<div class="tabs_con">
<div
:class="mway == '2' ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange('2')"
>
自动站点
</div>
<!-- <div
v-if="rgAuth"
:class="mway == '1' ? 'tabs_zhong' : 'tabs_mei'"
@click="onTabChange('1')"
>
人工站点
</div> -->
</div>
<!-- 自动站点 -->
<div v-show="mway == '2'" class="tabs_body">
<a-spin
:spinning="loading"
tip="加载中..."
class="chart-container chart_baowei"
>
<div
v-show="hasData"
ref="chartRef"
class="chart"
style="height: 100%"
></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
<!-- 人工站点 -->
<div v-show="mway == '1'" class="tabs_body">
<a-spin
:spinning="loading"
tip="加载中..."
class="chart-container chart_baowei"
>
<div v-show="hasData" ref="chartRefManual" class="chart"></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
getCeZhanTreeList,
getCeZhan,
getChangeInWaterTemperature
} from '@/api/dianzhanzhuantiMode';
import { useModelStore } from '@/store/modules/model';
defineOptions({ name: 'JianCeDianShuiWenBianHua' });
// ==================== store ====================
const modelStore = useModelStore();
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
const ppbclConfigMap: any = inject('ppbclConfigMap', ref({}));
const MODULE_PATH =
'@/modules/dianzhanzhuantiMod/JianCeDianShuiWenBianHua/index.vue';
// ==================== ====================
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
const LINE_COLORS = {
sw: '#9556a4',
avgwt: '#df91ab',
minwt: '#6ca4f7',
maxwt: '#78c300'
};
// ==================== ====================
const panelTitle = computed(() =>
currentBaseId === '07' ? '水温监测情况 ' : '水温监测情况'
);
// ==================== ====================
const rgAuth = ref(true); // TODO:
// ==================== ====================
const mway = ref<string>('2');
const loading = ref(false);
const hasData = ref(false);
const loadingData = ref<any[]>([]);
const xlabel = ref<string[]>([]);
const yvalue = ref<{ sw: any[]; avgwt: any[]; minwt: any[]; maxwt: any[] }>({
sw: [],
avgwt: [],
minwt: [],
maxwt: []
});
//
const optionsList = ref<any[]>([]);
const selectValue = ref<any>(null);
const dateRange = ref<[Dayjs, Dayjs]>([dayjs().subtract(1, 'year'), dayjs()]);
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
const chartRefManual = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const moduleConfig = computed(
() => ppbclConfigMap.value?.[MODULE_PATH] || { isShow: false, tagValue: '' }
);
const defaultDateRange = computed<[Dayjs, Dayjs]>(() => {
const cfg = moduleConfig.value;
if (cfg?.isShow && cfg?.tagValue) {
// tagValue
const d = dayjs(cfg.tagValue);
if (d.isValid()) {
return [d, dayjs()];
}
}
return [dayjs().subtract(1, 'year'), dayjs()];
});
// ==================== SidePanelItem ====================
/** 测站树选择器 */
const moreSelectConfig = computed(() => ({
show: true,
value: selectValue.value?.id ?? '',
options: optionsList.value,
width: '95px',
minWidth: '240px'
}));
/** 日期范围选择器 */
const scopeDateConfig = computed(() => ({
// show: moduleConfig.value?.isShow ?? true,
show: true,
value: [defaultDateRange.value[0], defaultDateRange.value[1]],
picker: 'date' as const,
format: 'YYYY-MM-DD',
width: '218px'
}));
// ==================== ====================
const buildOption = () => ({
grid: {
top: 60,
bottom: 10,
right: 10,
left: 5,
containLabel: true
},
legend: {
itemWidth: 18, //
itemHeight: 10, //
textStyle: { fontSize: 12 }, //
data: [
{ name: '同期最低值' },
{ name: '同期月均值' },
{ name: '水温' },
{ name: '同期最高值' }
]
},
tooltip: {
show: true,
confine: true,
axisPointer: { type: 'none' },
trigger: 'axis' as const,
formatter: (params: any[]) => {
if (!params?.length) return '';
const item = loadingData.value[params[0].dataIndex];
const fullTime = item?.tm
? dayjs(item.tm).format('YYYY-MM-DD HH:mm')
: params[0].name;
let html = `<div style="margin-bottom:4px">${fullTime}</div>`;
params.forEach((p: any) => {
const val = p.value != null ? `${p.value}` : '-';
html += `<div>${p.marker}${p.seriesName}: ${val}</div>`;
});
return html;
}
},
color: [
LINE_COLORS.minwt,
LINE_COLORS.avgwt,
LINE_COLORS.sw,
LINE_COLORS.maxwt
],
xAxis: {
type: 'category' as const,
data: xlabel.value,
axisLabel: {
fontSize: 11,
color: '#000',
interval: Math.max(0, Math.ceil(xlabel.value.length / 9) - 1)
},
axisTick: { show: false },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } }
},
yAxis: (() => {
const allVals = [
...yvalue.value.sw,
...yvalue.value.avgwt,
...yvalue.value.minwt,
...yvalue.value.maxwt
].filter(v => v != null) as number[];
const dataMin = allVals.length ? Math.min(...allVals) : 0;
const dataMax = allVals.length ? Math.max(...allVals) : 0;
return {
type: 'value' as const,
name: '水温(℃)',
nameTextStyle: { fontSize: 12, color: '#000' },
axisLabel: { color: '#000' },
axisTick: { show: true },
axisLine: { show: true },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } },
min: +(dataMin - 0.2).toFixed(1),
max: +(dataMax + 0.1).toFixed(1)
};
})(),
dataZoom: [{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true }],
series: [
{
name: '水温',
type: 'line',
smooth: true,
showSymbol: (params: any) => {
const iv = Math.max(0, Math.ceil(xlabel.value.length / 9) - 1);
return iv === 0 ? true : params.dataIndex % (iv + 1) === 0;
},
symbol: 'circle',
symbolSize: 3,
data: yvalue.value.sw
},
{
name: '同期月均值',
type: 'line',
smooth: true,
showSymbol: (params: any) => {
const iv = Math.max(0, Math.ceil(xlabel.value.length / 9) - 1);
return iv === 0 ? true : params.dataIndex % (iv + 1) === 0;
},
symbol: 'circle',
symbolSize: 2,
data: yvalue.value.avgwt
},
{
name: '同期最低值',
type: 'line',
smooth: true,
showSymbol: (params: any) => {
const iv = Math.max(0, Math.ceil(xlabel.value.length / 9) - 1);
return iv === 0 ? true : params.dataIndex % (iv + 1) === 0;
},
symbol: 'circle',
symbolSize: 2,
data: yvalue.value.minwt
},
{
name: '同期最高值',
type: 'line',
smooth: true,
showSymbol: (params: any) => {
const iv = Math.max(0, Math.ceil(xlabel.value.length / 9) - 1);
return iv === 0 ? true : params.dataIndex % (iv + 1) === 0;
},
symbol: 'circle',
symbolSize: 2,
data: yvalue.value.maxwt
}
]
});
// ==================== ECharts ====================
const getCurrentChartRef = () =>
mway.value === '2' ? chartRef.value : chartRefManual.value;
const initChart = () => {
const dom = getCurrentChartRef();
if (!dom) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(dom);
window.addEventListener('resize', handleResize);
observeChartResize(dom);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
const stnm = selectValue.value?.value || selectValue.value?.opt?.name || '';
modelStore.modalVisible = true;
modelStore.params.sttp = 'WT';
modelStore.title = stnm;
modelStore.params.stcd = selectValue.value?.id || '';
modelStore.filter.rangeTm = [
dateRange.value[1].format('YYYY-MM-DD HH:mm:ss'),
dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
];
}
});
if (hasData.value) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
let resizeObserver: ResizeObserver | null = null;
/** 监听图表容器尺寸变化,避免容器初始为 0 或 v-show 切换后图表只有高度没有宽度 */
const observeChartResize = (dom: HTMLElement) => {
if (typeof ResizeObserver === 'undefined') return;
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(() => chartInstance?.resize());
resizeObserver.observe(dom);
};
const clearResizeObserver = () => {
resizeObserver?.disconnect();
resizeObserver = null;
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
clearResizeObserver();
window.removeEventListener('resize', handleResize);
};
// ==================== ====================
const transformTreeData = (nodes: any[]): any[] => {
if (!Array.isArray(nodes)) return [];
return nodes.map((node: any) => ({
title: node.stnm || node.ennm || '',
value: node.stcd || node.rstcd || '',
key: node.stcd || node.rstcd || '',
...node,
children: node.items ? transformTreeData(node.items) : undefined
}));
};
// ==================== API ====================
/** 获取测站列表 + 有数据的测站 */
const fetchStationList = async () => {
if (!stcd.value) return;
const baseId = currentBaseId;
const params = {
logic: 'and',
filters: [
{
field: 'mway',
operator: 'eq',
dataType: 'string',
value: Number(mway.value)
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTRV'
},
baseId !== '07'
? {
field: 'fhstcd',
operator: 'isnull',
dataType: 'string'
}
: null,
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: stcd.value
}
].filter(Boolean)
};
let params2: any = {
filter: {
logic: 'and',
filters: [
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dateRange.value[0].format('YYYY-MM-DD 00:00:00')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dateRange.value[1].format('YYYY-MM-DD 23:59:59')
},
{
field: 'rstcd',
operator: 'in',
dataType: 'string',
value: [stcd.value]
},
{
field: 'mway',
operator: 'eq',
dataType: 'number',
value: Number(mway.value)
}
]
},
sort: [
{
field: 'orderIndex',
dir: 'asc'
}
]
};
if (baseId !== '07') {
params2.filter.filters.push({
field: 'fhstcd',
operator: 'eq',
dataType: 'string',
value: 'isnull'
});
}
try {
const res = await getCeZhanTreeList(params);
const treeData = res?.data?.data ?? [];
if (treeData?.length > 0) {
const res2 = await getCeZhan(params2);
const selData = res2?.data?.data ?? [];
const sel = selData.map((el: any) => ({
value: el.stnm,
opt: { name: el.stnm, id: el.stcd, sttp: 'WTRV' },
id: el.stcd
}));
const dataList = treeData;
setOptionsList(dataList);
let selVal: any = null;
const firstItem = dataList.find((e: any) =>
e?.items?.some((ve: any) => ve?.stcd)
);
if (sel?.[0]) {
selVal = sel[0];
} else if (firstItem) {
const opt = firstItem.items?.find((ve: any) => ve?.stcd);
selVal = {
value: opt?.stnm,
opt: { name: opt?.stnm, id: opt?.stcd, sttp: opt?.sttpCode },
id: opt?.stcd
};
}
// tree-data a-tree-select
nextTick(() => {
selectValue.value = selVal;
if (selVal) {
fetchChartData();
}
});
} else {
setOptionsList([]);
selectValue.value = null;
loading.value = false;
xlabel.value = [];
yvalue.value = { sw: [], avgwt: [], minwt: [], maxwt: [] };
loadingData.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取测站列表失败', err);
loading.value = false;
}
};
/** 获取水温图表数据 */
const fetchChartData = async () => {
if (!selectValue.value) return;
loading.value = true;
const params: any = {
filter: {
logic: 'and',
filters: [
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dateRange.value[0].startOf('day').format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dateRange.value[1].endOf('day').format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: selectValue.value.id
}
]
}
};
try {
const res = await getChangeInWaterTemperature(params);
const list = res?.data?.data ?? [];
if (list?.length) {
loadingData.value = list;
const dataSource = list.map((item: any) => ({
name: item.tm,
value: item.wt ?? null,
stcd: item.stcd,
avgwt: item.avgwt ?? null,
minwt: item.minwt ?? null,
maxwt: item.maxwt ?? null
}));
xlabel.value = dataSource.map((item: any) =>
item.name ? dayjs(item.name).format('HH:mm') : '--'
);
yvalue.value = {
sw: dataSource.map((item: any) => item.value ?? null),
avgwt: dataSource.map((item: any) => item.avgwt ?? null),
minwt: dataSource.map((item: any) => item.minwt ?? null),
maxwt: dataSource.map((item: any) => item.maxwt ?? null)
};
hasData.value = true;
// v-show display:none canvas 0
await nextTick();
if (!chartInstance) {
initChart();
}
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
xlabel.value = [];
yvalue.value = { sw: [], avgwt: [], minwt: [], maxwt: [] };
loadingData.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取水温数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
/** 设置测站树数据(转换后的格式给 SidePanelItem */
const setOptionsList = (treeData: any[]) => {
optionsList.value = transformTreeData(treeData);
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
// value selectedNodeExtra
if (payload.moreSelect) {
const node = payload.selectedNodeExtra || {};
selectValue.value = {
value: node.title || selectValue.value?.value,
id: payload.moreSelect,
opt: node.title ? { ...node } : selectValue.value?.opt
};
fetchChartData();
}
//
if (
payload.scopeDate &&
Array.isArray(payload.scopeDate) &&
payload.scopeDate.length === 2
) {
const d0 = dayjs(payload.scopeDate[0]);
const d1 = dayjs(payload.scopeDate[1]);
if (d0.isValid() && d1.isValid()) {
dateRange.value = [d0, d1];
fetchChartData();
}
}
};
/** Tab 切换 */
const onTabChange = (key: string) => {
if (key === mway.value) return;
mway.value = key;
};
// ==================== ====================
/** 监听 stcd → 刷新测站列表(图表初始化统一由 fetchChartData 在容器可见后执行) */
watch(stcd, newStcd => {
if (newStcd) {
destroyChart();
nextTick(() => {
fetchStationList();
});
}
});
/** 监听 mway → 刷新测站列表 + 重建图表 */
watch(mway, () => {
destroyChart();
nextTick(() => {
fetchStationList();
});
});
/** 监听图表数据 → 更新 echarts */
watch(
[xlabel, yvalue],
() => {
if (chartInstance && hasData.value) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
},
{ deep: true }
);
// ==================== ====================
onMounted(() => {
// onMounted initChart hasData false display:none 0
// fetchChartData
//
const cfg = moduleConfig.value;
if (cfg?.isShow && cfg?.tagValue) {
const d = dayjs(cfg.tagValue);
if (d.isValid()) {
dateRange.value = [d, dayjs()];
}
}
if (stcd.value) {
fetchStationList();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.jcdsw-wrapper {
width: 100%;
height: 100%;
padding: 5px;
display: flex;
box-sizing: border-box;
.tabs_con {
width: 28px;
height: 100%;
border: 2px solid #2f6b98;
border-radius: 5px;
text-align: center;
font-size: 14px;
box-sizing: border-box;
// padding: 5px 0px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
cursor: pointer;
.tabs_zhong {
color: #fff;
background-color: #2f6b98;
box-sizing: border-box;
padding: 5px 0px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei {
box-sizing: border-box;
padding: 5px 0px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.tabs_mei:hover {
color: #2f6b98;
}
}
.tabs_body {
width: 100%;
height: 100%;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,332 @@
<template>
<SidePanelItem title="水电站介绍" :bodyMarginBottom="'2px'">
<div class="station-intro">
<a-empty v-if="mediaData.length === 0" description="暂无介绍信息" />
<template v-else>
<div
class="container"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
>
<div
class="carousel-track"
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<div
v-for="(item, index) in renderMediaData"
:key="index"
class="carousel-item"
@click="openDetail"
>
<img :src="item.image" :alt="item.title" />
<div class="img-title">{{ item.title }}</div>
</div>
</div>
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in mediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
</div>
<div class="description-text">
{{ currentDescription }}
</div>
</template>
</div>
<!-- 详情弹框 -->
<a-modal
v-model:open="modalVisible"
title="设施详情"
width="80%"
:footer="null"
>
<div v-if="detailDataSource.length" class="detail-container">
<ArtsDetail :dataSource="detailDataSource" :index="0" />
</div>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
inject,
watch,
type Ref,
ref,
computed,
onMounted,
onUnmounted
} from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
import { overviewvmsstbprptGetKendoList } from '@/api/home';
defineOptions({
name: 'ShuiDianZhanJieShao'
});
interface MediaItem {
image: string;
title: string;
description: string;
}
interface DianZhanStation {
wbsCode: string;
stcd: string;
ennm: string;
lgtd: number;
lttd: number;
}
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
const dianZhanStation = inject<Ref<DianZhanStation | null>>('dianZhanStation');
//
const modalVisible = ref(false);
//
const mediaData = ref<MediaItem[]>([]);
const renderMediaData = ref<MediaItem[]>([]);
const currentIndex = ref(1);
const isHovering = ref(false);
const isTransitioning = ref(false);
let timer: any = null;
const detailDataSource = computed(() => {
return mediaData.value.map(item => ({
url: item.image || '',
description: item.description || '',
title: item.title || ''
}));
});
const currentDescription = computed(() => {
const realIndex = getCurrentRealIndex();
return mediaData.value[realIndex]?.description || '';
});
const getCurrentRealIndex = () => {
const len = mediaData.value.length;
if (len === 0) return 0;
let realIndex = currentIndex.value - 1;
if (realIndex < 0) realIndex = len - 1;
if (realIndex >= len) realIndex = 0;
return realIndex;
};
const initRenderData = () => {
const len = mediaData.value.length;
if (len === 0) {
renderMediaData.value = [];
return;
}
if (len === 1) {
renderMediaData.value = [...mediaData.value];
currentIndex.value = 0;
return;
}
renderMediaData.value = [
mediaData.value[len - 1],
...mediaData.value,
mediaData.value[0]
];
currentIndex.value = 1;
};
const goToSlide = (targetIndex: number) => {
if (isTransitioning.value || mediaData.value.length <= 1) return;
currentIndex.value = targetIndex + 1;
};
const startAutoPlay = () => {
if (timer) clearInterval(timer);
if (mediaData.value.length <= 1) return;
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
currentIndex.value++;
}
}, 4000);
};
const openDetail = () => {
if (mediaData.value.length) {
modalVisible.value = true;
}
};
const fetchStationInfo = async (stcd: string) => {
try {
const params = {
filter: {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd },
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'ENG'
}
]
},
sort: [{ field: 'siteStepSort', dir: 'asc' }],
select: ['inffile', 'introduce', 'ennm', 'logo', 'precis']
};
const res = await overviewvmsstbprptGetKendoList(params);
const dataList = res?.data?.data;
if (dataList && Array.isArray(dataList)) {
mediaData.value = dataList
.filter((item: any) => item.inffile || item.introduce)
.map((item: any) => ({
image: item.inffile ? `${baseUrl}?${item.inffile}&view=jpg` : '',
title: item.ennm || '',
description: item.introduce || ''
}));
} else {
mediaData.value = [];
}
initRenderData();
startAutoPlay();
} catch {
mediaData.value = [];
initRenderData();
}
};
watch(
() => dianZhanStation?.value,
newVal => {
if (newVal?.stcd) {
fetchStationInfo(newVal.stcd);
}
},
{ deep: true, immediate: true }
);
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
<style lang="scss" scoped>
.station-intro {
width: 100%;
height: 100%;
padding: 0px 8px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.container {
width: 100%;
height: calc(100% - 44px);
position: relative;
.carousel-track {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out;
&.no-transition {
transition: none;
}
.carousel-item {
min-width: 100%;
height: 100%;
position: relative;
flex-shrink: 0;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: middle;
}
.img-title {
position: absolute;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 2;
line-height: 22px;
padding: 0px 8px;
text-align: left;
text-shadow: rgb(0, 0, 0) 0px 0px 2px;
background-color: rgba(0,0,0,0.35);
color: #fff;
font-size: 14px;
// font-variant:tabilar-nums;
::after {
content: '';
position: absolute;
top: 0px;
z-index: -1;
left: 0px;
width: 100%;
height: 100%;
bottom: 0px;
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(2px);
}
}
}
}
.pagination-dots-fixed {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10;
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #d8d8d8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
}
}
.description-text {
font-size: 14px;
line-height: 1.5;
transition: all 0.3s ease;
margin-top: 2px;
min-height: 44px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
word-break: break-word;
}
.detail-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="61px" height="64px" viewBox="0 0 61 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrowUpLg</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#2F6B98" stop-opacity="0.45" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0.25" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.7637128%" y1="0.23628715%" x2="49.7637128%" y2="99.7637128%" id="linearGradient-2">
<stop stop-color="#2F6B98" stop-opacity="0.25" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0.15" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="-2.48949813e-15%" x2="50%" y2="100%" id="linearGradient-3">
<stop stop-color="#2F6B98" stop-opacity="0.150950612" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g id="5-生态调查" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="5.2-生态调查-陆生生态调查" transform="translate(-1560.000000, -662.000000)">
<g id="编组备份-4" transform="translate(1476.000000, 119.000000)">
<g id="编组-12" transform="translate(16.000000, 412.000000)">
<g id="ArrowUpLg" transform="translate(68.000000, 131.000000)">
<path d="M30.5,0.838886685 L59.7879803,34.4317691 L41.8358974,34.4317691 L41.8358974,63.4487179 L19.1641026,63.4487179 L19.1641026,34.4317691 L1.21201968,34.4317691 L30.5,0.838886685 Z" id="路径" stroke="url(#linearGradient-1)" stroke-width="1.1025641"></path>
<polygon id="路径备份-4" fill="url(#linearGradient-2)" points="56.9333333 33.1756019 30.5 2.75268817 4.06666667 33.1756019 20.3333333 33.1756019 20.3333333 61.9354839 40.6666667 61.9354839 40.6666667 33.1756019"></polygon>
<path d="M40.6666667,61.2473118 L40.6666667,61.9354839 L20.3333333,61.9354839 L20.3333333,61.2473118 L40.6666667,61.2473118 Z M40.6666667,59.8709677 L40.6666667,60.5591398 L20.3333333,60.5591398 L20.3333333,59.8709677 L40.6666667,59.8709677 Z M40.6666667,58.4946237 L40.6666667,59.1827957 L20.3333333,59.1827957 L20.3333333,58.4946237 L40.6666667,58.4946237 Z M40.6666667,57.1182796 L40.6666667,57.8064516 L20.3333333,57.8064516 L20.3333333,57.1182796 L40.6666667,57.1182796 Z M40.6666667,55.7419355 L40.6666667,56.4301075 L20.3333333,56.4301075 L20.3333333,55.7419355 L40.6666667,55.7419355 Z M40.6666667,54.3655914 L40.6666667,55.0537634 L20.3333333,55.0537634 L20.3333333,54.3655914 L40.6666667,54.3655914 Z M40.6666667,52.9892473 L40.6666667,53.6774194 L20.3333333,53.6774194 L20.3333333,52.9892473 L40.6666667,52.9892473 Z M40.6666667,51.6129032 L40.6666667,52.3010753 L20.3333333,52.3010753 L20.3333333,51.6129032 L40.6666667,51.6129032 Z M40.6666667,50.2365591 L40.6666667,50.9247312 L20.3333333,50.9247312 L20.3333333,50.2365591 L40.6666667,50.2365591 Z M40.6666667,48.8602151 L40.6666667,49.5483871 L20.3333333,49.5483871 L20.3333333,48.8602151 L40.6666667,48.8602151 Z M40.6666667,47.483871 L40.6666667,48.172043 L20.3333333,48.172043 L20.3333333,47.483871 L40.6666667,47.483871 Z M40.6666667,46.1075269 L40.6666667,46.7956989 L20.3333333,46.7956989 L20.3333333,46.1075269 L40.6666667,46.1075269 Z M40.6666667,44.7311828 L40.6666667,45.4193548 L20.3333333,45.4193548 L20.3333333,44.7311828 L40.6666667,44.7311828 Z M40.6666667,43.3548387 L40.6666667,44.0430108 L20.3333333,44.0430108 L20.3333333,43.3548387 L40.6666667,43.3548387 Z M40.6666667,41.9784946 L40.6666667,42.6666667 L20.3333333,42.6666667 L20.3333333,41.9784946 L40.6666667,41.9784946 Z M40.6666667,40.6021505 L40.6666667,41.2903226 L20.3333333,41.2903226 L20.3333333,40.6021505 L40.6666667,40.6021505 Z M40.6666667,39.2258065 L40.6666667,39.9139785 L20.3333333,39.9139785 L20.3333333,39.2258065 L40.6666667,39.2258065 Z M40.6666667,37.8494624 L40.6666667,38.5376344 L20.3333333,38.5376344 L20.3333333,37.8494624 L40.6666667,37.8494624 Z M40.6666667,36.4731183 L40.6666667,37.1612903 L20.3333333,37.1612903 L20.3333333,36.4731183 L40.6666667,36.4731183 Z M40.6666667,35.0967742 L40.6666667,35.7849462 L20.3333333,35.7849462 L20.3333333,35.0967742 L40.6666667,35.0967742 Z" id="形状结合" fill="url(#linearGradient-3)"></path>
<polyline id="路径-5备份" transform="translate(30.500000, 13.075269) scale(-1, 1) rotate(-90.000000) translate(-30.500000, -13.075269) " points="20.8655914 -3.86917563 40.1344086 13.0752688 20.8655914 30.0197133"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,480 @@
<template>
<SidePanelItem
title="珍稀植物园运行数据"
:moreSelect="moreSelect"
:select="select"
@update-values="handlePanelChange"
>
<div class="zxzwy-wrapper">
<a-spin :spinning="cardLoading" tip="加载中...">
<!-- 左右卡片 -->
<div v-if="hasCardData" class="card-wrapper" @click="openModal">
<div class="card-item card-left">
<div class="card-icon">
<i class="icon iconfont icon-chengliu"></i>
</div>
<span class="card-name"
>{{ showData.botanyName || ''
}}{{ showData.unitName ? `(${showData.unitName})` : '' }}</span
>
<h5 class="card-count">{{ showData.tamarixChinensis ?? '-' }}</h5>
<img :src="arrowUpSvg" alt="" class="arrow-icon" />
<div class="card-rate">{{ showData.tamarixChinensisLive ?? '-' }}</div>
<p class="card-label">存活率</p>
</div>
<div class="card-item card-right">
<div class="card-icon"> <i class="icon iconfont icon-zhenxizhiwuyuan"></i></div>
<span class="card-name">其他</span>
<h5 class="card-count">{{ showData.other || 0 }}</h5>
<img :src="arrowUpSvg" alt="" class="arrow-icon" />
<div class="card-rate">{{ showData.otherLive ?? '-' }}</div>
<p class="card-label">存活率</p>
</div>
</div>
<a-empty
v-if="!hasCardData && !cardLoading"
description="暂无数据"
class="empty-center"
/>
</a-spin>
</div>
<!-- 详情弹窗 -->
<a-modal
v-model:open="modalVisible"
title="珍稀植物园运行数据列表"
:footer="null"
width="80%"
:destroyOnClose="true"
:centered="true"
class="detail-modal"
>
<div class="search-form">
<a-space size="middle">
<div>年份:</div>
<a-select
v-model:value="modalYear"
style="width: 100px"
:options="select.options"
@change="onModalSearch"
/>
<a-button type="primary" @click="onModalSearch">查询</a-button>
</a-space>
</div>
<BasicTable
ref="tableRef"
:list-url="getBotanicalGardenDetailData"
:columns="detailColumns"
:scrollY="400"
:search-params="modalSearchParams"
:transform-data="detailTransform"
/>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject, nextTick } from 'vue';
import { message } from 'ant-design-vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getBotanicalGardenList,
getBotanicalGardenStatistic,
getBotanicalGardenDetailData
} from '@/api/ZXZWYYunXingShuJu';
import arrowUpSvg from './arrowUpLg.svg';
defineOptions({ name: 'ZXZWYYunXingShuJu' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const apiId = computed(() => dianZhanStation.value?.stcd || '');
// ==================== SidePanelItem ====================
const isInitializing = ref(false);
/** moreSelect: 植物园(标题栏树形选择器) */
const moreSelect = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[]
});
/** select: 年份(标题栏普通选择器) */
const select = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[],
width: '120px',
minWidth: '120px'
});
/** 各地植物园的年份列表:{ [stcd]: year[] } */
const yearMap = ref<Record<string, string[]>>({});
// ==================== ====================
const cardLoading = ref(false);
const hasCardData = ref(false);
const showData = ref({
botanyName: '',
unitName: '',
tamarixChinensis: '-' as string | number,
tamarixChinensisLive: '-' as string | number,
other: 0 as string | number,
otherLive: '-' as string | number
});
// ==================== Modal ====================
const modalVisible = ref(false);
const modalYear = ref<string>();
const tableRef = ref<any>(null);
/** 当前选中的植物园 stcd从 moreSelect.options 中找) */
const currentGardenStcd = computed(() => {
const val = moreSelect.value.value;
const options = moreSelect.value.options || [];
const found = (options as any[]).find((o: any) => o.value === val);
return found?.stcd || '';
});
const modalSearchParams = computed(() => ({
group: [],
groupResultFlat: false
}));
// ==================== ====================
const detailColumns = [
{
key: 'stnm',
title: '植物园名称',
dataIndex: 'stnm',
width: '150px',
ellipsis: true
},
{ key: 'tm', title: '年份', dataIndex: 'tm', width: '80px' },
{ key: 'tetp', title: '保护种类', dataIndex: 'tetp', width: '120px' },
{ key: 'unitName', title: '单位', dataIndex: 'unitName', width: '80px' },
{
key: 'tecntDisplay',
title: '实际种植/种植要求',
dataIndex: 'tecntDisplay',
width: '140px'
},
{
key: 'surnumDisplay',
title: '存活数量/移栽规模',
dataIndex: 'surnumDisplay',
width: '140px'
},
{ key: 'rate', title: '存活率(%)', dataIndex: 'rate', width: '100px' }
];
/** 预处理详情数据:拼接种植/存活字段 */
const detailTransform = (res: any) => {
const records = (res?.data?.data || []).map((item: any) => ({
...item,
tecntDisplay: `${item.tecnt ?? '-'}/${item.plantask ?? '-'}`,
surnumDisplay: `${item.surnum ?? '-'}/${item.transplant ?? '-'}`
}));
return { records, total: res?.data?.total || 0 };
};
// ==================== API ====================
/** 获取植物园下拉列表 */
const fetchGardenList = async () => {
if (!apiId.value) return;
isInitializing.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: apiId.value
}
]
},
group: [
{
dir: 'des',
field: 'year'
},
{
dir: 'des',
field: 'stcd'
},
{
dir: 'des',
field: 'name'
}
],
groupResultFlat: 'true'
};
const res = await getBotanicalGardenList(params);
const list = res?.data?.data || [];
// debugger
if (list.length === 0) return;
const gardenMap = new Map<string, { stcd: string; stnm: string }>();
const yearMapTemp: Record<string, string[]> = {};
list.forEach((item: any) => {
if (!gardenMap.has(item.stcd)) {
gardenMap.set(item.stcd, { stcd: item.stcd, stnm: item.name });
}
if (!yearMapTemp[item.stcd]) {
yearMapTemp[item.stcd] = [];
}
yearMapTemp[item.stcd].push(item.year);
});
yearMap.value = yearMapTemp;
// moreSelect 使field-names label 'title'
const gardens = Array.from(gardenMap.values());
moreSelect.value.options = gardens.map(g => ({
title: g.stnm,
value: g.stnm,
stcd: g.stcd
}));
//
const firstGarden = gardens[0];
moreSelect.value.value = firstGarden?.stnm;
updateYearOptions(firstGarden?.stcd);
} catch {
message.error('获取植物园列表失败');
} finally {
isInitializing.value = false;
}
};
/** 根据植物园 stcd 更新年份选项 */
const updateYearOptions = (stcd: string) => {
const years = yearMap.value[stcd] || [];
select.value.options = years.map(y => ({ label: y, value: y }));
select.value.value = years[0];
};
/** 获取卡片统计数据 */
const fetchStatistic = async () => {
const year = select.value.value;
const stcd = currentGardenStcd.value;
if (!year || !stcd) return;
cardLoading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'tm',
operator: 'eq',
dataType: 'string',
value: year
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
}
};
const res = await getBotanicalGardenStatistic(params);
const list = res?.data?.data || [];
hasCardData.value = list.length > 0;
if (list.length > 0) {
const result = list.reduce((acc: any, cur: any) => {
if (cur.name === '其他') {
acc.other = cur.tecnt;
acc.otherLive = cur.rate;
} else {
acc.botanyName = cur.name;
acc.tamarixChinensis = cur.tecnt;
acc.tamarixChinensisLive = cur.rate;
acc.unitName = cur.unitName;
}
return acc;
}, {});
showData.value = {
botanyName: result.botanyName || '',
unitName: result.unitName || '',
tamarixChinensis: result.tamarixChinensis ?? '-',
tamarixChinensisLive: result.tamarixChinensisLive ?? '-',
other: result.other ?? 0,
otherLive: result.otherLive ?? '-'
};
}
} catch {
hasCardData.value = false;
} finally {
cardLoading.value = false;
}
};
// ==================== ====================
/** SidePanelItem 控件变化回调 */
const handlePanelChange = async (data: any) => {
if (isInitializing.value) return;
//
if (
data.moreSelect !== undefined &&
data.moreSelect !== moreSelect.value.value
) {
moreSelect.value.value = data.moreSelect;
const stcd = (moreSelect.value.options as any[]).find(
(o: any) => o.value === data.moreSelect
)?.stcd;
updateYearOptions(stcd);
return; // select.value.value updateYearOptions handlePanelChange
}
//
if (data.select !== undefined) {
select.value.value = data.select;
fetchStatistic();
}
};
/** 打开详情弹窗 */
const openModal = () => {
modalYear.value = select.value.value;
modalVisible.value = true;
nextTick(() => {
onModalSearch();
});
};
/** 弹窗内搜索 */
const onModalSearch = () => {
if (!modalYear.value || !currentGardenStcd.value) return;
tableRef.value?.getList({
endTime: [{ field: 'tm', operator: 'eq', value: modalYear.value }],
stcd: currentGardenStcd.value
});
};
// ==================== ====================
watch(apiId, newId => {
if (newId) {
fetchGardenList();
}
}, { immediate: true });
</script>
<style lang="scss" scoped>
.zxzwy-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:deep(.ant-spin-nested-loading) {
width: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
}
}
.empty-center {
height: 100%;
}
.card-wrapper {
display: flex;
gap: 8px;
cursor: pointer;
height: 100%;
padding: 0px 5px;
}
.card-icon{
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #2f6b98;
display: flex;
align-items: center;
justify-content: center;
}
.card-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px;
// border: 1px solid #e8e8e8;
// border-radius: 4px;
transition: box-shadow 0.2s;
position: relative;
box-sizing: border-box;
}
.card-item:hover {
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-left {
border-right: 1px solid #e8e8e8;
}
.card-left .icon {
font-size: 20px;
color: #fff;
}
.card-right .icon {
font-size: 20px;
color: #fff;
}
.card-name {
font-size: 14px;
color: #3E3E3E;
margin-top: 2px;
}
.card-count {
font-size: 24px;
line-height: 32px;
color: #2f6b98;
// margin-bottom: 12px;
}
.arrow-icon {
width: 61px;
height: 64px;
margin-top: 2px;
}
.card-rate {
font-size: 14px;
color: #2f6b98;
position: absolute;
left: 50%;
top: 62%;
transform: translate(-50%, 0%);
}
.card-label {
font-size: 14px;
color: #3E3E3E;
text-indent:0 !important;
/* margin: 0; */
}
.detail-modal .search-form {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,292 @@
<template>
<!-- 详情弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="mFilter?.ctitle || '详情'"
width="80%"
:footer="null"
:bodyStyle="{ height: '700px' }"
centered
destroyOnClose
@cancel="handleModalCancel"
>
<!-- Tab 切换 -->
<div class="modal-tabs" v-if="mFilter?.proTable?.length">
<div
v-for="item in mFilter.proTable"
:key="item.key"
class="modal-tabs__item"
:class="{ 'modal-tabs__item--active': activeTab?.key === item.key }"
@click="switchTab(item)"
>
{{ item.title }}
</div>
</div>
<!-- 筛选表单 -->
<BasicSearch
v-if="activeTab && searchList.length"
:key="activeTab.key"
:searchList="searchList"
@finish="handleTableSearch"
@reset="handleTableReset"
/>
<!-- 数据表格 -->
<BasicTable
v-if="activeTab"
:key="activeTab.key"
ref="tableRef"
:columns="tableColumns"
:listUrl="tableListUrl"
:searchParams="tableSearchParams"
:transformData="customTransform"
:defaultPageSize="20"
:scrollY="400"
>
<template #fid="{ record }">
<a v-if="record.fid" @click="handleAttachmentPreview(record.fid)">查看</a>
<span v-else class="no-data-link">查看</span>
</template>
</BasicTable>
</a-modal>
<!-- 附件预览弹窗 -->
<a-modal
v-model:open="attachmentModalVisible"
title="附件预览"
width="80vw"
:footer="null"
destroyOnClose
:bodyStyle="{ height: '70vh' }"
>
<AttachmentPreview :fid="currentAttachmentFid" />
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick } from 'vue';
import request from '@/utils/request';
import BasicSearch from '@/components/BasicSearch/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import AttachmentPreview from '@/components/MapModal/components/AIYXSB/AttachmentPreview.vue';
import { getFishDic } from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'ZenZhiZhanYunXingDetailModal' });
const props = defineProps<{
selectedStcd: string;
}>();
// tab key
const igKeys = ['1-2-1'];
// ==================== ====================
const modalVisible = ref(false);
const activeTab = ref<any>(null);
const mFilter = ref<any>(null);
const searchForms = ref<any>({});
const tableRef = ref();
const fishDic = ref<Array<{ value: string; key: string }>>([]);
// ==================== ====================
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
// ==================== activeTab.filter ====================
const searchList = computed(() => {
if (!activeTab.value?.filter) return [];
return activeTab.value.filter.map((f: any) => {
const item: any = {
type: f.type === 'select' ? 'Select' : 'Input',
name: f.name,
label: f.label,
placeholder: f.placeholder || `请输入${f.label}`
};
if (f.type === 'select' && f.name === 'ftp') {
item.options = fishDic.value.map((d: any) => ({
label: d.key,
value: d.value
}));
}
return item;
});
});
// ==================== proliferationColumns ====================
const buildColumns = (cols: any[]): any[] => {
return cols
.filter((col: any) => col.visible !== false)
.map((col: any) => {
const base: any = {
title: col.title,
dataIndex: col.dataIndex,
key: col.key || col.dataIndex,
width: col.width || 150,
ellipsis: col.ellipsis ?? true
};
if (col.dataIndex === 'fid' || col.key === 'fid') {
base.slots = { customRender: 'fid' };
}
if (col.children?.length) {
base.children = buildColumns(col.children);
}
return base;
});
};
const tableColumns = computed(() => {
if (!activeTab.value?.columns) return [];
return buildColumns(activeTab.value.columns);
});
// ==================== ====================
const tableSearchParams = computed(() => {
const filters: any[] = [];
if (props.selectedStcd) {
filters.push({
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: props.selectedStcd
});
}
if (searchForms.value) {
Object.keys(searchForms.value).forEach(key => {
if (searchForms.value[key] !== undefined && searchForms.value[key] !== '') {
filters.push({
field: key,
operator: 'contains',
dataType: 'string',
value: searchForms.value[key]
});
}
});
}
const result: any = {
filter: { logic: 'and', filters }
};
if (!igKeys.includes(activeTab.value?.key)) {
result.sort = [{ field: 'tm', dir: 'desc' }];
}
return result;
});
// ==================== listUrl ====================
const tableListUrl = computed(() => {
return (params: any) =>
request({
url: activeTab.value?.url || '',
method: 'post',
data: params
});
});
const customTransform = (res: any) => ({
records: res?.data?.data || res?.data?.items || [],
total: res?.data?.total || 0
});
// ==================== ====================
const switchTab = (item: any) => {
searchForms.value = {};
activeTab.value = item;
nextTick(() => {
tableRef.value?.getList(tableSearchParams.value.filter);
});
};
const handleTableSearch = (values: any) => {
searchForms.value = { ...values };
tableRef.value?.getList(tableSearchParams.value.filter);
};
const handleTableReset = () => {
searchForms.value = {};
tableRef.value?.getList(tableSearchParams.value.filter);
};
const handleModalCancel = () => {
modalVisible.value = false;
activeTab.value = null;
mFilter.value = null;
searchForms.value = {};
};
const handleAttachmentPreview = (fid: string) => {
if (fid) {
currentAttachmentFid.value = fid;
attachmentModalVisible.value = true;
}
};
// ==================== API ====================
const fetchFishDic = async () => {
if (fishDic.value.length > 0) return; //
try {
const res = await getFishDic({
filter: { logic: 'and', filters: [] },
select: ['id', 'name']
});
// debugger
const data = res?.data?.data || [];
fishDic.value = data.map((item: any) => ({
value: item.id,
key: item.name
}));
} catch (e) {
console.error('获取鱼类字典失败', e);
}
};
// ==================== ====================
const open = (val: any) => {
searchForms.value = {};
mFilter.value = val;
activeTab.value = val.proTable?.[0] || null;
modalVisible.value = true;
fetchFishDic();
nextTick(() => {
tableRef.value?.getList(tableSearchParams.value.filter);
});
};
defineExpose({ open });
</script>
<style scoped>
.modal-tabs {
display: flex;
gap: 0;
margin-bottom: 16px;
border-bottom: 1px solid #e8e8e8;
}
.modal-tabs__item {
padding: 8px 20px;
cursor: pointer;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.modal-tabs__item:hover {
color: #2f6b98;
}
.modal-tabs__item--active {
color: #2f6b98;
border-bottom-color: #2f6b98;
font-weight: 500;
}
.no-data-link {
color: #d9d9d9;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,622 @@
<template>
<SidePanelItem
title="增殖站运行及评估"
:select="stationSelect"
@update-values="onPanelChange"
>
<div class="zzzyx-wrapper">
<a-spin :spinning="loading" tip="加载中...">
<div v-if="hasData" class="step-card-wrapper">
<div
v-for="(item, index) in stepList"
:key="item.key"
class="step-card"
:class="{
'step-card--active': activeIndex === index,
'step-card--last': index === 4
}"
:style="{ zIndex: activeIndex === index ? 999 : 989 - index * 10 }"
@click="index === 4 ? undefined : setActiveIndex(index)"
>
<!-- 激活状态展开子步骤 -->
<template v-if="activeIndex === index">
<div class="step-card__header">{{ item.title }}</div>
<div class="step-card__icon">
<img :src="fishStation2Svg" alt="" />
</div>
<div class="step-card__arrow">
<img :src="arrowDownLine2Svg" alt="" />
</div>
<div class="step-card__list">
<div
v-for="val in item.textList"
:key="val.key"
class="step-card__list-item"
:class="{
'is-disabled': val.disable,
'is-active': val.curNewSurFace
}"
:style="val.disable ? {} : { cursor: 'pointer' }"
@click="!val.disable && val.proTable?.length && detailModalRef?.open(val)"
>
{{ val.ctitle }}
</div>
</div>
<p class="step-card__step">STEP{{ index + 1 }}</p>
</template>
<!-- 折叠状态 -->
<template v-else>
<div class="step-card__fold-label">{{ item.title }}</div>
<p class="step-card__step">STEP{{ index + 1 }}</p>
</template>
</div>
</div>
<a-empty
v-if="!hasData && !loading"
description="暂无数据"
class="empty-center"
/>
</a-spin>
</div>
<!-- 详情弹窗子组件 -->
<ZenZhiZhanYunXingDetailModal
ref="detailModalRef"
:selected-stcd="selectedStcd"
/>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import ZenZhiZhanYunXingDetailModal from './ZenZhiZhanYunXingDetailModal.vue';
import fishStation2Svg from '@/assets/icons/fishStation2.svg';
import arrowDownLine2Svg from '@/assets/icons/arrowDownLine2.svg';
import {
getHatcheryOperationData
} from '@/api/dianZhanZhuanTi';
import { msstbprptGetKendoList } from '@/api/zzfl';
import {
qinYuXinXi,
qinYuSiWanJiLu,
qinYuPeiYuFangShi,
qinYuPeiYuShuiWen,
qinYuPeiYuShuiZi,
qinYuPeiYuTouWei,
taoTaiQinYu,
yuLeiRenGo,
yuLeiShouJin,
yuleifuhua,
miaoZhongPeiYu,
miaoZhongJiLu,
yuBingFangZhi
} from '@/components/MapModal/components/NormalOperationData/proliferationColumns';
defineOptions({ name: 'ZenZhiZhanYunXingPG' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const apiId = computed(() => dianZhanStation.value?.stcd || '');
// ==================== 5 proTable ====================
const INIT_STEP_LIST = [
{
title: '亲鱼选配与培育',
key: '0',
textList: [
{
ctitle: '亲鱼选配',
key: '1-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '亲鱼信息',
key: '1-1-1',
url: '/fb/bsmfr/GetKendoListCust',
columns: qinYuXinXi,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'signnum', label: '标记编号' }
]
},
{
title: '亲鱼死亡记录',
key: '1-1-2',
url: '/fb/bsdeathr/GetKendoListCust',
columns: qinYuSiWanJiLu,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fpnum', label: '鱼池编号' },
{ type: 'input', name: 'signnum', label: '标记编号' }
]
}
]
},
{
ctitle: '亲鱼培育',
key: '1-2',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '亲鱼培育方式',
key: '1-2-1',
url: '/fb/bsctmob/GetKendoListCust',
columns: qinYuPeiYuFangShi,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育水温观测记录',
key: '1-2-2',
url: '/fb/fbbwtr/GetKendoListCust',
columns: qinYuPeiYuShuiWen,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育水质监测记录',
key: '1-2-3',
url: '/fb/fbcwqr/GetKendoListCust',
columns: qinYuPeiYuShuiZi,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育投喂记录',
key: '1-2-4',
url: '/fb/bsrfir/GetKendoListCust',
columns: qinYuPeiYuTouWei,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
}
]
},
{
ctitle: '亲鱼补充与淘汰',
key: '1-3',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '淘汰亲鱼信息',
key: '1-3-1',
url: '/fb/outfishr/GetKendoListCust',
columns: taoTaiQinYu,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'signno', label: '标记编号' }
]
}
]
}
]
},
{
title: '人工繁殖',
key: '1',
textList: [
{
ctitle: '亲本选择',
key: '2-1',
disable: true,
curNewSurFace: '',
proTable: []
},
{
ctitle: '人工催产',
key: '2-2',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼类人工催产记录',
key: '2-2-1',
url: '/fb/fishartinlr/GetKendoListCust',
columns: yuLeiRenGo,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] }
]
}
]
},
{
ctitle: '人工授精',
key: '2-3',
disable: true,
curNewSurFace: '',
proTable: []
},
{
ctitle: '孵化',
key: '2-4',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼类受精卵孵化过程记录',
key: '2-4-1',
url: '/fb/fishhatchrecr/GetKendoListCust',
columns: yuLeiShouJin,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fishsrc', label: '亲鱼来源' }
]
},
{
title: '鱼类孵化过程巡查记录',
key: '2-4-2',
url: '/fb/fishhatchpror/GetKendoListCust',
columns: yuleifuhua,
filter: [
{ type: 'input', name: 'devno', label: '孵化设施编号' }
]
}
]
}
]
},
{
title: '鱼苗和鱼种的培育',
key: '2',
textList: [
{
ctitle: '鱼苗和鱼种的培育',
key: '3-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '苗种培育水环境监测记录',
key: '3-1-1',
url: '/fb/scwemr/GetKendoListCust',
columns: miaoZhongPeiYu,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '苗种培育记录',
key: '3-1-2',
url: '/fb/fishbreedr/GetKendoListCust',
columns: miaoZhongJiLu,
filter: [
{ type: 'input', name: 'farmno', label: '鱼池编号' }
]
}
]
}
]
},
{
title: '鱼病防治',
key: '3',
textList: [
{
ctitle: '鱼病防治',
key: '4-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼病防治记录',
key: '4-1-1',
url: '/fb/fishdpacr/GetKendoListCust',
columns: yuBingFangZhi,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
}
]
}
]
},
{
title: '放流',
key: '4',
textList: []
}
];
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const stepList = ref<any[]>([]);
const activeIndex = ref(0);
const stationOptions = ref<
Array<{ value: string; label: string; stcd: string }>
>([]);
const selectedStation = ref<string>('');
const selectedStcd = ref<string>('');
// ==================== ref ====================
const detailModalRef = ref();
// ==================== SidePanelItem ====================
const stationSelect = computed(() => ({
show: true,
value: selectedStation.value,
options: stationOptions.value,
width: '160px'
}));
// ==================== API ====================
/** 拉取增殖站下拉列表 */
const fetchStationList = async (stcd: string) => {
if (!stcd) return;
try {
const params: any = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: stcd
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'FB'
}
]
},
select: [
'id',
'stcd',
'rstcd',
'ennm',
'stnm',
'sttpCode',
'lgtd',
'lttd',
'orderIndex'
]
};
const res = await msstbprptGetKendoList(params);
const data = res?.data?.data || [];
if (data.length > 0) {
const list = data.map((item: any) => ({
label: item.stnm,
value: item.stcd,
stcd: item.stcd
}));
stationOptions.value = list.map((item: any) => ({
value: item.value,
label: item.label,
stcd: item.stcd
}));
selectedStation.value = list[0].value;
selectedStcd.value = list[0].stcd;
} else {
stationOptions.value = [];
selectedStation.value = '';
selectedStcd.value = '';
}
} catch (e) {
console.error('获取增殖站列表失败', e);
}
};
/** 拉取各步骤运行状态 */
const fetchStepStatus = async (stcd: string) => {
if (!stcd) return;
loading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
}
};
const stepRes = await getHatcheryOperationData(params);
const stepData = stepRes?.data?.data || [];
if (stepData.length > 0) {
hasData.value = true;
const newStepList = JSON.parse(JSON.stringify(INIT_STEP_LIST));
//
stepData.forEach((item: any) => {
const { firstGrade, newest } = item;
if (newest) {
const idx = newStepList.findIndex((s: any) => s.title === firstGrade);
if (idx !== -1) activeIndex.value = idx;
}
});
//
newStepList.forEach((step: any) => {
step.textList.forEach((val: any) => {
let curNewSurFace = '';
stepData.forEach((dItem: any) => {
const { key, newSurface, value } = dItem;
if (val.ctitle === key) {
val.disable = value === '2';
curNewSurFace = newSurface;
}
});
val.curNewSurFace = curNewSurFace;
});
});
stepList.value = newStepList;
} else {
hasData.value = false;
}
} catch (e) {
console.error('获取增殖站运行状态失败', e);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
const onPanelChange = (data: any) => {
if (data.select) {
const option = stationOptions.value.find(
(item: any) => item.value === data.select
);
if (option) {
selectedStation.value = option.value;
selectedStcd.value = option.stcd;
fetchStepStatus(option.stcd);
}
}
};
const setActiveIndex = (index: number) => {
activeIndex.value = index;
};
// ==================== ====================
watch(apiId, newId => {
if (newId) {
fetchStationList(newId);
}
}, { immediate: true });
//
watch(selectedStcd, newStcd => {
if (newStcd) {
fetchStepStatus(newStcd);
}
});
</script>
<style lang="scss" scoped>
.zzzyx-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
}
/* .empty-center {
height: 100%;
} */
.step-card-wrapper {
display: flex;
height: 100%;
position: relative;
padding: 0px 10px;
align-items: center;
justify-content: space-between;
}
.step-card {
position: relative;
transition: all 0.3s ease;
background: #ffffff40;
border: 1px solid #ddd;
height: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
box-shadow: 3px 3px 3px 3px #2f6b9840;
}
}
.step-card--active {
background: #fff;
border: 0px solid #00000000;
display: block;
&:hover {
box-shadow: 3px 3px 3px 3px #2f6b9840;
}
}
.step-card__header {
font-size: 14px;
color: #000000d9;
margin-bottom: 2px;
width: 126px;
text-align: center;
}
.step-card__icon {
width: 100%;
display: flex;
justify-content: center;
margin: 3px 0px;
img {
width: 45px;
height: 45px;
}
}
.step-card__arrow {
width: 100%;
display: flex;
justify-content: center;
img {
width: 20px;
height: 8px;
}
}
.step-card__list {
display: flex;
flex-direction: column;
align-items: center;
margin: 4px 0;
}
.step-card__list-item {
font-size: 14px;
color: #000000d9;
white-space: nowrap;
}
.step-card__list-item.is-disabled {
color: #bbb;
cursor: default;
}
.step-card__step {
position: absolute;
left: -25px;
bottom: 4px;
font-size: 14px;
color: #999;
margin: 0;
}
.step-card__fold-label {
font-size: 14px;
color: #000000d9;
writing-mode: vertical-lr;
letter-spacing: 2px;
}
</style>

View File

@ -0,0 +1,351 @@
<!-- 生态流量数据分析 -->
<template>
<SidePanelItem
title="生态流量数据分析"
:bodyMarginBottom="'2px'"
:datetimePicker="(datetimePickerConfig as any)"
@update-values="onUpdateValues"
>
<div class="ecoflow-wrapper">
<a-spin
:spinning="loading"
tip="加载中..."
class="chart-container chart_baowei"
>
<div
v-show="hasData"
ref="chartRef"
class="chart"
style="height: 100%"
></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useModelStore } from '@/store/modules/model';
import { getEcologicalFlowData } from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'EcologicalDataMod' });
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const monthValue = ref<Dayjs>(dayjs());
const modelStore = useModelStore();
//
const chartRawData = ref<any[]>([]);
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const SERIES_COLORS: Record<string, string> = {
入库流量: '#4B79AB',
生态流量: '#00A050',
生态流量限值: '#F7A737'
};
// ==================== SidePanelItem ====================
/** 月份选择器 */
const datetimePickerConfig = computed(() => ({
show: true,
value: monthValue.value,
picker: 'month' as const,
format: 'YYYY-MM'
}));
// ==================== ====================
const buildOption = () => {
const data = chartRawData.value;
if (!data || data.length === 0) return {};
const sorted = [...data].sort(
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
);
const xData = sorted.map((item: any) => dayjs(item.tm).format('YYYY-MM-DD'));
const qiData = sorted.map((item: any) => item.qi);
const qecData = sorted.map((item: any) => item.qec);
const qecLimitData = sorted.map((item: any) => item.qecLimit);
const series: any[] = [];
const legend: string[] = [];
const xLen = xData.length;
const interval = Math.max(1, Math.floor(xLen / 9));
const addSeries = (name: string, d: any[]) => {
legend.push(name);
series.push({
name,
type: 'line',
data: d,
smooth: true,
symbol: 'circle',
symbolSize: 2,
showSymbol: (_: any, idx: number) => idx % interval === 0,
lineStyle: { color: SERIES_COLORS[name], width: 2 },
itemStyle: { color: SERIES_COLORS[name] }
});
};
addSeries('入库流量', qiData);
addSeries('生态流量', qecData);
addSeries('生态流量限值', qecLimitData);
return {
tooltip: {
trigger: 'axis' as const,
axisPointer: { type: 'none' },
formatter: (params: any[]) => {
if (!params?.length) return '';
let html = `<div style="margin-bottom:4px">${params[0].name}</div>`;
params.forEach((p: any) => {
let val: string;
if (p.value == null) {
val = '';
} else if (p.seriesName === '入库流量') {
val = Math.round(p.value).toString();
} else {
val = Number(p.value).toFixed(1);
}
html += `<div>${p.marker}${p.seriesName}: ${val}</div>`;
});
return html;
}
},
legend: {
type: 'scroll' as const,
top: 0,
data: legend,
itemWidth: 18,
itemHeight: 10,
textStyle: { fontSize: 12 }
},
grid: {
left: 5,
right: 10,
bottom: 10,
top: 40,
containLabel: true
},
xAxis: {
type: 'category' as const,
data: xData,
splitLine: {
show: true
},
axisLabel: {
formatter: (value: string) => value.replace(' ', '\n'),
color: '#000'
}
// axisLabel: { interval, color: '#000', fontSize: 11 },
},
yAxis: {
type: 'value' as const,
name: '流量(m³/s)',
axisTick: { show: true },
axisLine: { show: true },
axisLabel: { color: '#000' },
nameTextStyle: { color: '#000', fontSize: 12 },
splitLine: { show: true, lineStyle: { color: '#bfbfbf' } }
},
dataZoom: [{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true }],
series
};
};
// ==================== ECharts ====================
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', handleResize);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
const stnm = dianZhanStation.value?.stnm || '';
modelStore.modalVisible = true;
modelStore.params.sttp = 'ENG';
modelStore.params.eqtp = 'QEC';
modelStore.title = stnm;
modelStore.params.stcd = stcd.value || '';
modelStore.filter.rangeTm = [
monthValue.value.endOf('month').format('YYYY-MM-DD HH:mm:ss'),
monthValue.value.startOf('month').format('YYYY-MM-DD HH:mm:ss')
];
}
});
if (hasData.value && chartRawData.value.length > 0) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 获取生态流量日数据 */
const fetchChartData = async () => {
if (!stcd.value) return;
loading.value = true;
const startTime = monthValue.value
.startOf('month')
.format('YYYY-MM-DD HH:mm:ss');
const endTime = monthValue.value.endOf('month').format('YYYY-MM-DD HH:mm:ss');
const params: any = {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd.value },
{ field: 'TYPE', operator: 'eq', dataType: 'string', value: 'DAY' },
{ field: 'tm', operator: 'gte', dataType: 'date', value: startTime },
{ field: 'tm', operator: 'lte', dataType: 'date', value: endTime }
]
};
try {
const res = await getEcologicalFlowData(params);
const list = res?.data?.data ?? [];
if (list.length > 0) {
chartRawData.value = list;
hasData.value = true;
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
chartRawData.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取生态流量数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
// debugger
if (payload?.datetime) {
monthValue.value = dayjs(payload.datetime);
}
};
// ==================== ====================
watch(stcd, newStcd => {
if (newStcd) {
destroyChart();
nextTick(() => {
initChart();
fetchChartData();
});
}
});
watch(monthValue, () => {
if (stcd.value) {
destroyChart();
nextTick(() => {
initChart();
fetchChartData();
});
}
});
// ==================== ====================
onMounted(() => {
if (stcd.value) {
initChart();
fetchChartData();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.ecoflow-wrapper {
width: 100%;
height: 100%;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,505 @@
<template>
<a-modal
:open="visible"
title="过鱼总量详情"
width="90%"
:body-style="{ height: '600px', padding: '16px' }"
:footer="null"
@cancel="handleClose"
>
<!-- 顶部工具条日期 + /月切换 -->
<div class="auto-toolbar">
<a-range-picker
v-model:value="date"
:allow-clear="false"
:disabled-date="disabledDate"
:picker="selType"
:format="dateFormat"
style="width: 240px"
@change="onDateChange"
/>
<a-select
v-model:value="selType"
style="width: 100px"
:options="typeOptions"
@change="onTypeChange"
/>
</div>
<!-- 表格+ 图表 -->
<div class="auto-content">
<div class="auto-table-pane">
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="listUrl"
:search-params="tableSearchParams"
:transform-data="transformData"
:scroll-y="420"
></BasicTable>
</div>
<div class="auto-chart-pane">
<div ref="chartRef" class="auto-chart"></div>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, computed, nextTick, onMounted, onUnmounted } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import * as echarts from 'echarts';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getGuoYuShuLiangDay,
getGuoYuShuLiangMonth
} from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'ModelAutoStation' });
interface Props {
visible: boolean;
stcd: string;
dateRange: [Dayjs, Dayjs];
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
stcd: '',
dateRange: () => [dayjs(), dayjs()]
});
const emit = defineEmits(['update:visible']);
// ==================== ====================
const UNIT_FISH = '尾';
const UNIT_SPEED = 'm/s';
const UNIT_TEMP = '℃';
const typeOptions = [
{ label: '日', value: 'day' },
{ label: '月', value: 'month' }
];
// ==================== ====================
const tableRef = ref();
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const selType = ref<'day' | 'month'>('day');
// ()(线)(线)
const CHART_COLORS = ['#4b79ab', '#78c300', '#f5a623'];
// 4
const date = ref<[Dayjs, Dayjs]>([
dayjs().subtract(4, 'year').startOf('year'),
dayjs()
]);
// ==================== ====================
//
watch(
() => props.dateRange,
(val) => {
const start = val[0].startOf('year');
const endOfYear = val[1].endOf('year');
const today = dayjs();
date.value = [start, endOfYear.isAfter(today) ? today : endOfYear];
},
{ immediate: true }
);
const dateFormat = computed(() => (selType.value === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM'));
/** 禁用未来日期 */
const disabledDate = (current: Dayjs) => {
return current && current > dayjs().endOf('day');
};
// ==================== ====================
const generateArray = (num: number): string[] =>
Array.from({ length: 13 - num }, (_, i) => String(i + num));
const generateArrayMonth = (num: number): string[] => {
const end = num === 12 ? 12 : num;
return Array.from({ length: end }, (_, i) => String(i + 1));
};
const generateArrayYear = (startYear: number, endYear: number): number[] => {
const result: number[] = [];
for (let i = startYear; i <= endYear; i++) result.push(i);
return result.slice(1, -1);
};
/** 构造跨年度的年月过滤条件start 月、中间年份、end 月) */
const buildMonthFilter = () => {
const start = date.value[0];
const end = date.value[1];
const startYear = start.year();
const endYear = end.year();
const startMonth = generateArray(start.month() + 1);
const endMonth = generateArrayMonth(end.month() + 1);
const years = generateArrayYear(startYear, endYear);
return [
{
logic: 'and',
filters: [
{ field: 'year', operator: 'eq', dataType: 'number', value: startYear },
{ field: 'month', operator: 'in', value: startMonth }
]
},
years.length > 0
? {
logic: 'and',
filters: [{ field: 'year', operator: 'in', value: years }]
}
: null,
{
logic: 'and',
filters: [
{ field: 'year', operator: 'eq', dataType: 'number', value: endYear },
{ field: 'month', operator: 'in', value: endMonth }
]
}
].filter((el: any) => el != null);
};
// ==================== ====================
const columns = [
{ title: '时间', dataIndex: 'tm', key: 'tm', width: 120, sort: true },
{ title: `过鱼数量(${UNIT_FISH})`, dataIndex: 'fcnt', key: 'fcnt', width: 120, sort: true },
{ title: `水温(${UNIT_TEMP})`, dataIndex: 'wd', key: 'wd', width: 100, sort: true },
{ title: `流速(${UNIT_SPEED})`, dataIndex: 'ls', key: 'ls', width: 100, sort: true }
];
// / API
const listUrl = computed(() =>
selType.value === 'day' ? getGuoYuShuLiangDay : getGuoYuShuLiangMonth
);
// group / sort / select
const tableSearchParams = computed(() => {
const aggregates = [
{ field: 'fcnt', aggregate: 'sum' },
{ field: 'temperature', aggregate: 'max' },
{ field: 'speed', aggregate: 'max' }
];
if (selType.value === 'day') {
return {
group: [{ dir: 'desc', field: 'dt', aggregates }],
sort: [{ field: 'dt', dir: 'desc' }],
select: []
};
}
return {
group: [
{ dir: 'desc', field: 'year', aggregates },
{ dir: 'desc', field: 'month' }
],
groupResultFlat: true
};
});
//
const buildFilter = computed(() => {
if (selType.value === 'day') {
return {
logic: 'and',
filters: [
{
field: 'dt',
operator: 'gte',
dataType: 'date',
value: date.value[0].format('YYYY-MM-DD')
},
{
field: 'dt',
operator: 'lte',
dataType: 'date',
value: date.value[1].format('YYYY-MM-DD')
},
{ field: 'stcd', operator: 'eq', dataType: 'string', value: props.stcd }
]
};
}
return {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', value: props.stcd },
{ logic: 'or', filters: buildMonthFilter() }
]
};
});
//
const transformData = (res: any) => {
const list = res?.data?.data ?? res?.data ?? [];
let records: any[] = [];
if (selType.value === 'day') {
records = list.map((el: any, index: number) => ({
key: index,
tm: el.key?.split(' ')[0] ?? '-',
fcnt: el?.aggregates?.sum_fcnt ?? '-',
wd: el?.aggregates?.max_temperature ?? '-',
ls: el?.aggregates?.max_speed ?? '-'
}));
} else {
records = list.map((el: any, index: number) => ({
key: index,
tm: el?.year ? `${el.year}-${String(el.month).padStart(2, '0')}` : '-',
fcnt: el?.sum_fcnt ?? '-',
wd: el?.max_temperature ?? '-',
ls: el?.max_speed ?? '-'
}));
}
return { records, total: records.length };
};
// ==================== ====================
const buildChartOption = (records: any[]) => {
const toNum = (v: any) =>
v === null || v === undefined || v === '-' ? null : Number(v);
return {
color: CHART_COLORS,
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 130, top: 60, bottom: 40 },
legend: { data: ['总数', '流速', '水温'] },
dataZoom: [
{
type: 'inside',
xAxisIndex: [0],
zoomOnMouseWheel: false,
moveOnMouseMove: false,
moveOnMouseWheel: true,
minValueSpan: 0,
maxValueSpan: 9,
startValue: 9999
}
],
xAxis: [
{
type: 'category',
data: records.map((el) => el.tm),
splitLine: { show: true }
}
],
yAxis: [
{
type: 'value',
name: `总数(${UNIT_FISH})`,
position: 'left',
axisLine: { show: true, lineStyle: { color: CHART_COLORS[0] } },
axisTick: { show: true },
splitLine: { show: true }
},
{
type: 'value',
name: `流速(${UNIT_SPEED})`,
position: 'right',
offset: 60,
axisLine: { show: true, lineStyle: { color: CHART_COLORS[1] } },
axisTick: { show: true },
splitLine: { show: true }
},
{
type: 'value',
name: `水温(${UNIT_TEMP})`,
position: 'right',
axisLine: { show: true, lineStyle: { color: CHART_COLORS[2] } },
axisTick: { show: true },
splitLine: { show: true }
}
],
series: [
{ name: '总数', type: 'bar', barMaxWidth: 20, data: records.map((el) => toNum(el.fcnt)) },
{ name: '流速', type: 'line', yAxisIndex: 1, data: records.map((el) => toNum(el.ls)) },
{ name: '水温', type: 'line', yAxisIndex: 2, data: records.map((el) => toNum(el.wd)) }
]
};
};
// -> yAxis //
const Y_AXIS_NAME_INDEX: Record<string, number> = { 总数: 0, 流速: 1, 水温: 2 };
// / y
const handleLegendSelectChanged = (params: any) => {
if (!chartInstance) return;
const selected = params.selected || {};
const yAxisUpdate: Record<number, { show: boolean }> = {};
Object.keys(Y_AXIS_NAME_INDEX).forEach((name) => {
yAxisUpdate[Y_AXIS_NAME_INDEX[name]] = { show: selected[name] !== false };
});
chartInstance.setOption({ yAxis: yAxisUpdate } as any);
};
const renderChart = (records: any[]) => {
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
chartInstance.on('legendselectchanged', handleLegendSelectChanged);
}
chartInstance.setOption(buildChartOption(records), true);
chartInstance.resize();
};
const fetchChartData = async () => {
const aggregates = [
{ field: 'fcnt', aggregate: 'sum' },
{ field: 'temperature', aggregate: 'max' },
{ field: 'speed', aggregate: 'max' }
];
let params: any;
if (selType.value === 'day') {
params = {
filter: buildFilter.value,
group: [{ dir: 'asc', field: 'dt', aggregates }],
sort: [{ field: 'dt', dir: 'asc' }]
};
} else {
params = {
filter: buildFilter.value,
group: [
{ dir: 'asc', field: 'year', aggregates },
{ dir: 'asc', field: 'month' }
],
groupResultFlat: true
};
}
const res: any = await (selType.value === 'day'
? getGuoYuShuLiangDay(params)
: getGuoYuShuLiangMonth(params));
const list = res?.data?.data ?? res?.data ?? [];
const records = list.map((el: any) => {
if (selType.value === 'day') {
return {
tm: el.key?.split(' ')[0] ?? '-',
fcnt: el?.aggregates?.sum_fcnt ?? null,
wd: el?.aggregates?.max_temperature ?? null,
ls: el?.aggregates?.max_speed ?? null
};
}
return {
tm: el?.year ? `${el.year}-${String(el.month).padStart(2, '0')}` : '-',
fcnt: el?.sum_fcnt ?? null,
wd: el?.max_temperature ?? null,
ls: el?.max_speed ?? null
};
});
renderChart(records);
};
// ==================== ====================
const refreshAll = () => {
if (tableRef.value) {
tableRef.value.getList(buildFilter.value);
}
if (chartRef.value) {
fetchChartData();
}
};
const onDateChange = () => {
refreshAll();
};
const onTypeChange = () => {
// selType searchParams / listUrl prop filter group
nextTick(() => {
refreshAll();
});
};
const handleClose = () => {
emit('update:visible', false);
};
// ==================== ====================
// tableRef
watch(
tableRef,
(ref) => {
if (ref && props.visible) {
ref.getList(buildFilter.value);
}
},
{ flush: 'post' }
);
watch(
() => props.visible,
(val) => {
if (val) {
refreshAll();
}
}
);
//
watch(
chartRef,
(ref) => {
if (ref && props.visible) {
fetchChartData();
}
},
{ flush: 'post' }
);
// ==================== ====================
const handleResize = () => {
chartInstance?.resize();
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped lang="scss">
.auto-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.auto-content {
display: flex;
gap: 12px;
height: calc(100% - 44px);
min-height: 0;
.auto-table-pane {
flex: 55 55 0;
height: 100%;
min-width: 0;
}
.auto-chart-pane {
flex: 45 45 0;
height: 100%;
min-width: 0;
.auto-chart {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<a-modal
:open="visible"
title="过鱼总量详情"
width="80%"
:body-style="{ height: '600px', padding: '16px' }"
:footer="null"
@cancel="handleClose"
>
<!-- 日期范围选择 -->
<div class="modal-date-range">
<a-range-picker
v-model:value="date"
:allow-clear="false"
:disabled-date="disabledDate"
format="YYYY-MM-DD"
style="width: 240px; margin-bottom: 12px"
@change="onDateChange"
/>
</div>
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="getOverfishTotal"
:search-params="tableSearchParams"
:transform-data="transformData"
:scroll-y="430"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, computed, nextTick } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import BasicTable from '@/components/BasicTable/index.vue';
import { getOverfishTotal } from '@/api/dianzhanzhuantiMode';
defineOptions({ name: 'ModelManualStation' });
interface Props {
visible: boolean;
stcd: string;
dateRange: [Dayjs, Dayjs];
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
stcd: '',
dateRange: () => [dayjs(), dayjs()]
});
const emit = defineEmits(['update:visible']);
const tableRef = ref();
//
const date = ref<[Dayjs, Dayjs]>([dayjs().subtract(4, 'year').startOf('year'), dayjs()]);
// dateRange
// 2022-2026 2022-01-01 ~ 2026-08-12
// 2022-2025 2022-01-01 ~ 2025-12-31
watch(
() => props.dateRange,
(val) => {
const start = val[0].startOf('year');
const endOfYear = val[1].endOf('year');
const today = dayjs();
date.value = [start, endOfYear.isAfter(today) ? today : endOfYear];
},
{ immediate: true }
);
const columns = [
{ title: '年份', dataIndex: 'yr', key: 'yr', width: 100, merge: true },
{ title: '鱼类', dataIndex: 'ftp', key: 'ftp', width: 150 },
{ title: '鱼类规格(g)', dataIndex: 'fsz', key: 'fsz', width: 120 },
{ title: '过鱼数量(尾)', dataIndex: 'fcnt', key: 'fcnt', width: 120 }
];
// group
const tableSearchParams = computed(() => ({
group: [
{
dir: 'desc',
field: 'yr',
aggregates: [
{ field: 'fcnt', aggregate: 'sum' },
{ field: 'fsz', aggregate: 'max' }
]
},
{
dir: 'desc',
field: 'ftp'
}
],
groupResultFlat: true
}));
//
const buildFilter = computed(() => ({
logic: 'and',
filters: [
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: date.value[0].format('YYYY-MM-DD')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: date.value[1].format('YYYY-MM-DD')
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: [props.stcd]
}
]
}));
// API
const transformData = (res: any) => {
const list = res?.data?.data ?? res?.data ?? [];
const records = list.map((item: any, index: number) => ({
key: index,
yr: item.yr ?? '-',
ftp: item.ftp ?? '-',
fsz: item.max_fsz ?? item.fsz ?? '-',
fcnt: item.sum_fcnt ?? item.fcnt ?? '-'
}));
return { records, total: records.length };
};
//
watch(
() => props.visible,
(val) => {
if (val) {
nextTick(() => {
tableRef.value?.getList(buildFilter.value);
});
}
}
);
/** 禁用未来日期 */
const disabledDate = (current: Dayjs) => {
return current && current > dayjs().endOf('day');
};
/** 日期变化 */
const onDateChange = () => {
tableRef.value?.getList(buildFilter.value);
};
const handleClose = () => {
emit('update:visible', false);
};
</script>

View File

@ -0,0 +1,446 @@
<!-- 过鱼总量 -->
<template>
<SidePanelItem
title="过鱼总量"
:bodyMarginBottom="'2px'"
:moreSelect="(moreSelectConfig as any)"
:scopeDate="(scopeDateConfig as any)"
@update-values="onUpdateValues"
>
<div class="gyzl-wrapper">
<a-spin :spinning="loading" tip="加载中..." class="chart-container">
<div v-show="hasData" ref="chartRef" class="chart" style="height: 100%;"></div>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</a-spin>
</div>
<!-- 人工站弹窗 -->
<ModelManualStation
v-if="manualModalVisible"
v-model:visible="manualModalVisible"
:stcd="facility?.value || ''"
:date-range="dateRange"
/>
<!-- 自动站弹窗 -->
<ModelAutoStation
v-if="autoModalVisible"
v-model:visible="autoModalVisible"
:stcd="facility?.value || ''"
:date-range="dateRange"
/>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs, { Dayjs } from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
getGuoYuZongLiangFacilityTree,
getGuoYuZongLiangPieData
} from '@/api/dianzhanzhuantiMode';
import ModelManualStation from './ModelManualStation.vue';
import ModelAutoStation from './ModelAutoStation.vue';
defineOptions({ name: 'GuoYuZongLiang' });
// ==================== ====================
const dianZhanStation: any = inject('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
//
const facilityTree = ref<any[]>([]);
const facilityStcd = ref('');
const facility = ref<any>(null); // mway
//
const manualModalVisible = ref(false);
const autoModalVisible = ref(false);
//
const dateRange = ref<[Dayjs, Dayjs]>([
dayjs().subtract(4, 'year').startOf('year'),
dayjs()
]);
//
const pieData = ref<{ name: string; value: number }[]>([]);
// ECharts
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const UNIT = '尾';
// ==================== ====================
/** 生成随机颜色(与 React generateRandomColor 一致) */
const generateRandomColor = (names: string[]): string[] => {
return names.map(() => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'));
};
// ==================== SidePanelItem ====================
/** 设施树选择器 */
const moreSelectConfig = computed(() => ({
show: true,
value: facilityStcd.value,
options: facilityTree.value,
width: '120px',
minWidth: '200px'
}));
/** 日期范围选择器,格式 YYYY */
const scopeDateConfig = computed(() => ({
show: true,
value: [dateRange.value[0], dateRange.value[1]] as any,
picker: 'year' as const,
format: 'YYYY',
width: '150px'
}));
// ==================== ====================
const buildOption = () => {
const data = pieData.value;
if (!data || data.length === 0) return {};
const colorNames = data.map((item) => item.name);
const colors = generateRandomColor(colorNames);
const totalCount = data.reduce((sum, item) => sum + item.value, 0);
return {
graphic: [
{
type: 'text',
left: '38%',
top: '42%',
style: {
text: `${totalCount}`,
textAlign: 'center',
fill: '#2f6b98',
fontSize: 16,
fontWeight: 'bold'
}
},
{
type: 'text',
left: '35%',
top: '55%',
style: {
text: `总量(${UNIT})`,
textAlign: 'center',
fill: '#000',
fontSize: 14
}
}
],
tooltip: {
trigger: 'item' as const,
formatter: (params: any) => `${params.name}: ${params.value} ${UNIT} (${params.percent}%)`
},
legend: {
orient: 'vertical' as const,
right: 5,
top: 'center',
itemWidth: 14,
itemHeight: 14,
formatter: (name: string) => {
const found = data.find((item) => item.name === name);
const value = found ? found.value : '-';
const truncated = name.length > 5 ? name.substring(0, 5) + '...' : name;
return `${truncated} ${value}${UNIT}`;
},
textStyle: { fontSize: 12 }
},
series: [
{
type: 'pie' as const,
radius: ['60%', '80%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
label: { show: false },
emphasis: {
label: { show: true, fontSize: 14, fontWeight: 'bold' }
},
data: data.map((item, index) => ({
...item,
itemStyle: { color: colors[index] }
}))
}
]
};
};
// ==================== ECharts ====================
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', handleResize);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series' && facility.value) {
if (facility.value.mway === 1) {
manualModalVisible.value = true;
} else if (facility.value.mway === 2) {
autoModalVisible.value = true;
}
}
});
if (hasData.value && pieData.value.length > 0) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
};
const handleResize = () => {
chartInstance?.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 获取设施树 */
const fetchFacilityTree = async () => {
if (!stcd.value) return;
loading.value = true;
try {
const params: any = {
logic: 'and',
filters: [
{ field: 'rstcd', operator: 'in', dataType: 'string', value: [stcd.value] }
]
};
const res = await getGuoYuZongLiangFacilityTree(params);
const list = res?.data?.data ?? [];
// debugger
// moreSelect { title, value, children }
const treeData = list.map((group: any) => ({
title: group.ennm,
value: group.rstcd,
children: (group.items || []).map((v: any) => ({
title: v.stnm,
value: v.stcd,
mway: v.mway
}))
}));
facilityTree.value = treeData;
// debugger
if (list.length > 0) {
// stcdVo
const firstGroup = list.find((g: any) => g?.items?.some((v: any) => v?.stcd));
if (firstGroup) {
const firstChild = firstGroup.items.find((v: any) => v?.stcd);
if (firstChild) {
facility.value = firstChild;
facilityStcd.value = firstChild.stcd;
fetchPieData();
return;
}
}
facilityStcd.value = '';
hasData.value = false;
pieData.value = [];
} else {
facilityTree.value = [];
facilityStcd.value = '';
hasData.value = false;
pieData.value = [];
loading.value = false;
}
} catch (err) {
console.error('获取设施树失败', err);
loading.value = false;
}
};
/** 获取饼图数据 */
const fetchPieData = async () => {
if (!facilityStcd.value) return;
loading.value = true;
const startTime = dateRange.value[0].startOf('year').format('YYYY-MM-DD HH:mm:ss');
const endTime = dateRange.value[1].endOf('year').format('YYYY-MM-DD HH:mm:ss');
const params: any = {
logic: 'and',
filters: [
{ field: 'tm', operator: 'gte', dataType: 'date', value: startTime },
{ field: 'tm', operator: 'lte', dataType: 'date', value: endTime },
{ field: 'stcd', operator: 'eq', dataType: 'string', value: facilityStcd.value }
]
};
try {
const res = await getGuoYuZongLiangPieData(params);
const list = res?.data?.data ?? [];
if (list.length > 0) {
pieData.value = list
.map((el: any) => ({
name: el?.items?.[0]?.FTP ?? el.key ?? '',
value: Number(el?.items?.[0]?.SUM_FCNT ?? 0)
}))
.sort((a: any, b: any) => b.value - a.value);
hasData.value = true;
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
} else {
pieData.value = [];
hasData.value = false;
}
} catch (err) {
console.error('获取过鱼总量数据失败', err);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
/** SidePanelItem @update-values */
const onUpdateValues = (payload: any) => {
if (payload?.moreSelect) {
facilityStcd.value = payload.moreSelect;
//
const found = facilityTree.value
.flatMap((g: any) => g.children || [])
.find((c: any) => c.value === payload.moreSelect);
if (found) {
facility.value = found;
}
}
if (payload?.scopeDate) {
dateRange.value = [
dayjs(payload.scopeDate[0]),
dayjs(payload.scopeDate[1])
];
}
};
// ==================== ====================
watch(stcd, (newStcd) => {
if (newStcd) {
destroyChart();
nextTick(() => {
fetchFacilityTree().then(() => {
initChart();
});
});
}
});
//
watch(facilityStcd, (newVal) => {
if (newVal && chartInstance) {
destroyChart();
nextTick(() => {
fetchPieData().then(() => {
initChart();
});
});
}
});
//
watch(dateRange, () => {
if (facilityStcd.value) {
fetchPieData().then(() => {
if (chartInstance) {
chartInstance.setOption(buildOption(), true);
nextTick(() => chartInstance?.resize());
}
});
}
});
// ==================== ====================
onMounted(() => {
if (stcd.value) {
fetchFacilityTree().then(() => {
initChart();
});
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.gyzl-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.chart-container {
width: 100%;
height: 100%;
.chart {
width: 100%;
height: 100%;
}
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,381 @@
<template>
<div class="detail-container">
<BasicSearch
ref="formRef"
:searchList="searchList"
:initialValues="initialValues"
@finish="handleSearch"
@reset="handleReset"
>
<template #actions>
<a-button @click="handleExport" :loading="exportLoading">
<template #icon><DownloadOutlined /></template>
导出
</a-button>
</template>
</BasicSearch>
<BasicTable
ref="tableRef"
:columns="columns"
:listUrl="getPowerStationReleaseDetail"
:searchParams="tableSearchParams"
:transformData="customTransform"
:defaultPageSize="20"
:scrollY="400"
>
<template #ennm="{ record }">
<a @click="handleStationClick(record)">{{ record.ennm }}</a>
</template>
<template #fid="{ record }">
<a
v-if="record.fid && record.fid.length > 1"
@click="handlePreview(record.fid)"
>查看</a
>
<span v-else class="no-data-link">查看</span>
</template>
</BasicTable>
<!-- 视频预览弹窗 -->
<a-modal
v-model:open="previewVisible"
title="视频资料"
:footer="null"
width="800px"
>
<template v-if="previewData && previewData.length">
<a-empty v-if="!previewVisible" description="暂无视频数据" />
</template>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import BasicSearch from '@/components/BasicSearch/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getPowerStationReleaseDetail } from '@/api/dianZhanZhuanTi';
import { useModelStore } from '@/store/modules/model';
import { message } from 'ant-design-vue';
import { getDictItemsByCode } from '@/api/dict';
const props = defineProps<{
year: string;
stcdSelectOptions: { label: string; value: string }[];
stcd: string;
}>();
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
// ==================== ====================
const runStateList: any = ref([]);
const getdict = async () => {
const res = await getDictItemsByCode({ dictCode: 'EngFl' });
runStateList.value = (res.data || []).map((item: any) => ({
label: item.dictName,
value: item.itemCode
}));
runStateList.value.unshift({
label: '全部',
value: 'all'
});
};
onMounted(() => {
getdict();
nextTick(() => {
tableRef.value?.getList(buildFilter.value);
});
});
// ==================== ====================
const formRef = ref();
const tableRef = ref();
const exportLoading = ref(false);
const state = reactive({
years: props.year,
stcd: props.stcd,
runState: 'all',
search: ''
});
// ==================== ====================
const previewVisible = ref(false);
const previewData = ref<string[]>([]);
function handlePreview(fid: string) {
if (fid && fid.length > 1) {
previewData.value = [fid];
previewVisible.value = true;
}
}
// ==================== ====================
const searchList = computed(() => [
{
type: 'DataPicker',
name: 'years',
label: '年份',
picker: 'year',
fieldProps: {
allowClear: false,
format: 'YYYY',
valueFormat: 'YYYY'
}
},
{
type: 'Select',
name: 'stcd',
label: '所属电站',
options: props.stcdSelectOptions
},
{
type: 'Select',
name: 'runState',
label: '工作开展情况',
options: runStateList.value
},
{
type: 'Input',
name: 'search',
label: '',
placeholder: '请输入电站名称,实际放鱼种类'
}
]);
const initialValues = computed(() => ({
years: props.year,
stcd: props.stcd,
runState: 'all',
search: ''
}));
// ==================== / ====================
function handleSearch(values: any) {
state.years = values.years ? dayjs(values.years).format('YYYY') : '';
state.stcd = values.stcd ?? '';
state.runState = values.runState ?? 'all';
state.search = values.search ?? '';
tableRef.value?.getList(buildFilter.value);
}
function handleReset() {
state.years = props.year;
state.stcd = props.stcd;
state.runState = 'all';
state.search = '';
}
// ==================== ====================
const buildFilter = computed(() => {
const { search, runState, years, stcd } = state;
return {
logic: 'and',
filters: [
currentBaseId && currentBaseId !== 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
}
: null,
runState != 'all'
? {
field: 'runState',
operator: 'eq',
dataType: 'string',
value: runState
}
: null,
years
? {
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: dayjs(years).startOf('year').format('YYYY-MM-DD HH:mm:ss')
}
: null,
years
? {
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: dayjs(years).endOf('year').format('YYYY-MM-DD HH:mm:ss')
}
: null,
stcd
? { field: 'stcd', operator: 'eq', dataType: 'string', value: stcd }
: null,
search
? {
field: 'ennm',
operator: 'contains',
dataType: 'string',
value: search
}
: null,
{
field: 'bldsttCcode',
operator: 'in',
dataType: 'string',
value: [1, 2]
}
].filter(Boolean)
};
});
const tableSearchParams = computed(() => ({
filter: buildFilter.value
}));
const customTransform = (res: any) => ({
records: res?.data?.data || res?.data?.items || [],
total: res?.data?.total || 0
});
// ==================== ====================
const columns = [
{
title: '电站名称',
dataIndex: 'ennm',
key: 'ennm',
width: 150,
ellipsis: true,
merge: true,
slots: { customRender: 'ennm' }
},
{
title: '实际放鱼时间',
dataIndex: 'tm',
key: 'tm',
width: 160,
ellipsis: true
},
{
title: '实际放鱼种类',
dataIndex: 'ftpName',
key: 'ftpName',
width: 160,
ellipsis: true
},
{
title: '实际放鱼数量(万尾)',
dataIndex: 'fcntjc',
key: 'fcntjc',
width: 160,
ellipsis: true
},
{
title: '放鱼平均规格(cm)',
dataIndex: 'fsz',
key: 'fsz',
width: 160,
ellipsis: true
},
{
title: '计划放鱼时段',
dataIndex: 'timeSlot',
key: 'timeSlot',
width: 160,
ellipsis: true,
merge: true
},
{
title: '计划放鱼种类',
dataIndex: 'planFtpName',
key: 'planFtpName',
width: 160,
ellipsis: true,
merge: true
},
{
title: '计划放鱼数量(万尾)',
dataIndex: 'fcntjh',
key: 'fcntjh',
width: 160,
ellipsis: true,
merge: true
},
{
title: '放流地点',
dataIndex: 'rplace',
key: 'rplace',
width: 160,
ellipsis: true
},
{
title: '所在河流',
dataIndex: 'river',
key: 'river',
width: 160,
ellipsis: true
},
{
title: '工作开展情况',
dataIndex: 'runStateName',
key: 'runStateName',
width: 120,
ellipsis: true,
merge: true
},
{
title: '视频资料',
dataIndex: 'fid',
key: 'fid',
width: 100,
ellipsis: true,
fixed: 'right',
slots: { customRender: 'fid' }
}
];
// ==================== ====================
const modelStore = useModelStore();
function handleStationClick(record: any) {
modelStore.params = {
...modelStore.params,
sttp: 'ENG',
stcd: record.stcd,
sttpCode: record.sttpCode || 'ENG'
};
modelStore.title = record.ennm ? `${record.ennm} ` : '';
modelStore.modalVisible = true;
}
// ==================== ====================
function handleExport() {
if (exportLoading.value) return;
exportLoading.value = true;
if (tableRef.value && typeof tableRef.value.exportTable === 'function') {
tableRef.value
.exportTable({
fileName: `电站放流详情_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
} else {
message.info('暂无导出数据');
exportLoading.value = false;
}
}
</script>
<style scoped lang="scss">
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
}
.no-data-link {
color: #d9d9d9;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,449 @@
<template>
<SidePanelItem
title="电站放流统计"
:datetimePicker="datePickerConfig"
@update-values="onDateChange"
>
<div class="release-statistics">
<a-spin
v-show="hasData"
:spinning="loading"
tip="加载中..."
class="chart-container"
>
<div ref="chartRef" class="chart-container"></div>
</a-spin>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</div>
</SidePanelItem>
<!-- 弹窗 -->
<a-modal
v-model:open="showModal"
title="电站放流详情"
width="80vw"
:footer="null"
:destroyOnClose="true"
>
<PowerStationReleasetaStisticsDetail
v-if="showModal"
:year="modalPageData.year"
:stcdSelectOptions="modalPageData.stcdSelectOptions"
:stcd="modalPageData.stcd"
/>
</a-modal>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import PowerStationReleasetaStisticsDetail from './PowerStationReleasetaStisticsDetail.vue';
import {
getPowerStationReleaseYear,
getPowerStationReleaseData
} from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'PowerStationReleasetaStistics' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
const UNIT = '万尾';
const COLORS = ['#6ca4f7', '#91cc75'];
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const searchDate = ref<dayjs.Dayjs | null>(null);
const resData = ref<any[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const showModal = ref(false);
const modalPageData = ref({
year: '',
stcdSelectOptions: [] as { label: string; value: string }[],
stcd: ''
});
// ==================== ====================
const datePickerConfig = computed(() => ({
show: true,
picker: 'year' as const,
value: searchDate.value ? searchDate.value.format('YYYY') : undefined
}));
// ==================== ====================
interface ChartData {
stationList: string[];
planList: number[];
actualList: number[];
}
const chartData = computed<ChartData>(() => {
if (!resData.value?.length)
return { stationList: [], planList: [], actualList: [] };
return {
stationList: resData.value.map((item: any) => item.ennm),
planList: resData.value.map((item: any) =>
parseFloat(((item.fcntjh ?? 0) / 10000).toFixed(4))
),
actualList: resData.value.map((item: any) =>
parseFloat(((item.fcntjc ?? 0) / 10000).toFixed(4))
)
};
});
// ==================== ====================
const stcdSelectOptions = computed(() => {
if (!resData.value?.length) return [];
const options = resData.value.map((item: any) => ({
label: item.ennm,
value: item.stcd
}));
return [{ label: '全部', value: '' }, ...options];
});
// ==================== ECharts ====================
const buildOption = (data: ChartData) => {
const allValues = [...data.planList, ...data.actualList];
const maxVal = Math.max(...allValues, 1);
const minVal = Math.min(0, ...allValues);
// y 4
const rawRange = maxVal - minVal;
const rawStep = rawRange / 4;
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
const normalizedStep = rawStep / magnitude;
let niceStep: number;
if (normalizedStep <= 1) niceStep = 1;
else if (normalizedStep <= 2) niceStep = 2;
else if (normalizedStep <= 5) niceStep = 5;
else niceStep = 10;
niceStep *= magnitude;
const yMin = minVal < 0 ? 0 : minVal;
return {
tooltip: {
trigger: 'axis' as const,
formatter: (params: any) => {
if (!params || params.length === 0) return '';
let html = `<div style="font-size:14px;margin-bottom:6px;">${params[0].name}</div>`;
params.forEach((p: any) => {
const val = p.value ?? 0;
html += `
<div style="display:flex;align-items:center;margin:2px 0;">
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${p.color};margin-right:6px;"></span>
<span>${p.seriesName}: <strong>${val}</strong> ${UNIT}</span>
</div>`;
});
return html;
}
},
legend: {
top: 0,
left: 'right',
orient: 'horizontal' as const,
icon: 'roundRect' as const,
itemGap: 10,
data: [
{ name: '计划放流', itemStyle: { color: COLORS[0] } },
{ name: '实际放流', itemStyle: { color: COLORS[1] } }
],
textStyle: { fontSize: 12 }
},
color: COLORS,
grid: {
top: '15%',
left: '8%',
right: '8%',
bottom: '0%',
containLabel: true
},
xAxis: {
type: 'category' as const,
data: data.stationList,
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value' as const,
name: `数量(${UNIT})`,
nameGap: 15,
nameTextStyle: { fontSize: 12 },
splitNumber: 4,
min: yMin,
max: maxVal + niceStep,
interval: niceStep
},
series: [
{
name: '计划放流',
type: 'bar',
data: data.planList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 12,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
},
{
name: '实际放流',
type: 'bar',
data: data.actualList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 13,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
}
]
};
};
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
nextTick(() => {
chartInstance = echarts.init(chartRef.value);
chartInstance.on('click', handleChartClick);
if (chartData.value.stationList.length > 0) {
chartInstance.setOption(buildOption(chartData.value), true);
}
});
};
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 获取数据年份(第一次加载时确定默认年份) */
const fetchYear = () => {
if (!stcd.value) return;
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseYear(params)
.then((res: any) => {
// debugger
const list = res?.data?.data?.[0] ?? [];
// debugger
if (list.length > 0) {
searchDate.value = dayjs(list[0].plansd);
} else {
searchDate.value = dayjs();
}
})
.catch(() => {
searchDate.value = dayjs();
});
};
/** 获取明细数据 */
const fetchData = () => {
if (!stcd.value || !searchDate.value) return;
loading.value = true;
const startTime = searchDate.value
.startOf('year')
.format('YYYY-MM-DD HH:mm:ss');
const endTime = searchDate.value.endOf('year').format('YYYY-MM-DD HH:mm:ss');
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseData(params)
.then((res: any) => {
loading.value = false;
const list = res?.data?.data ?? [];
resData.value = list;
hasData.value = list.length > 0;
if (chartInstance) {
chartInstance.setOption(buildOption(chartData.value), true);
}
nextTick(() => {
chartInstance?.resize();
});
})
.catch(() => {
loading.value = false;
resData.value = [];
hasData.value = false;
});
};
// ==================== ====================
const onDateChange = (payload: any) => {
if (payload.datetime) {
searchDate.value = dayjs(payload.datetime, 'YYYY');
}
};
/** 图表点击 → 打开弹窗 */
const handleChartClick = (params: any) => {
const name = params?.name;
if (!name) return;
const matched = stcdSelectOptions.value.find(item => item.label === name);
modalPageData.value = {
year: searchDate.value ? searchDate.value.format('YYYY') : '',
stcdSelectOptions: stcdSelectOptions.value,
stcd: matched?.value ?? ''
};
showModal.value = true;
};
// ==================== ====================
watch(stcd, newStcd => {
if (newStcd) {
fetchYear();
}
});
watch(searchDate, newDate => {
if (newDate) {
fetchData();
}
});
watch(
chartData,
newData => {
if (chartInstance && newData.stationList.length > 0) {
chartInstance.setOption(buildOption(newData), true);
nextTick(() => chartInstance?.resize());
}
},
{ deep: true }
);
// ==================== ====================
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
if (stcd.value) {
fetchYear();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped lang="scss">
.release-statistics {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
}
.empty-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>

View File

@ -20,7 +20,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch, inject, computed } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, watch, inject, computed, type Ref } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -649,6 +649,68 @@ watch(
},
{ deep: true, immediate: true }
);
// ==================== ppbclTagList ====================
const MODULE_PATH = '@/modules/qixidishuiwenbianhua/index.vue';
const ppbclConfigMap = inject<Ref<Record<string, { isShow: number; tagValue: string }>>>('ppbclConfigMap', ref({}));
/** 根据 tagValue 计算默认日期范围YY-MM 格式) */
function getDateRangeFromTagValue(tagValue: string): [string, string] | null {
const now = dayjs();
let start: dayjs.Dayjs;
switch (tagValue) {
case '7d':
start = now.subtract(7, 'day');
break;
case '1M':
start = now.subtract(1, 'month');
break;
case 'a1M':
start = now.subtract(1, 'month').startOf('month');
break;
case '3M':
start = now.subtract(3, 'month');
break;
case 'a3M':
start = now.subtract(3, 'month').startOf('month');
break;
case '1y':
start = now.subtract(1, 'year');
break;
case '3y':
start = now.subtract(3, 'year');
break;
case '5y':
start = now.subtract(5, 'year');
break;
default:
return null;
}
return [start.format('YYYY-MM'), now.format('YYYY-MM')];
}
watch(
() => ppbclConfigMap?.value?.[MODULE_PATH],
(config) => {
if (config) {
// isShow
datetimePicker.value.show = config.isShow !== 0;
// tagValue
if (config.tagValue) {
const dateRange = getDateRangeFromTagValue(config.tagValue);
if (dateRange) {
datetimePicker.value.value = dateRange;
}
}
}
},
{ immediate: true }
);
// ==================== ====================
//

View File

@ -129,3 +129,8 @@ svg {
.ant-table-pagination.ant-pagination {
padding: 0 4px !important;
}
.chart_baowei{
.ant-spin-nested-loading{
width: 100% !important;
}
}

View File

@ -1,9 +1,9 @@
<template>
<div class="export-container">
<div class="export-container body_one">
<a-form
ref="formRef"
:model="formData"
:labelCol="{ span: 6 }"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 18 }"
class="export-form"
>
@ -12,24 +12,29 @@
<a-radio-group v-model:value="formData.timeDimension">
<a-radio value="month">月度统计</a-radio>
<a-radio value="year">年统计</a-radio>
<a-radio value="day">日统计</a-radio>
<a-radio value="hour">时统计</a-radio>
</a-radio-group>
</a-form-item>
<!-- 月度统计 -->
<template v-if="formData.timeDimension === 'month'">
<template v-if="formData.timeDimension !== 'year'">
<a-form-item label="统计监测要素" name="monitorElements">
<a-checkbox-group v-model:value="formData.monitorElements">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
<a-checkbox
v-for="item in dataFieldOption"
:value="item.itemCode"
>{{ item.dictName }}</a-checkbox
>
</a-checkbox-group>
</a-form-item>
<a-form-item label="选择水电站">
<a-row :gutter="16">
<a-col :span="12">
<a-col :span="6">
<a-select
v-model:value="formData.rvcd"
placeholder="请选择流域"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
show-search
@ -37,6 +42,7 @@
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 100%"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
@ -48,7 +54,7 @@
</a-select-option>
</a-select>
</a-col>
<a-col :span="12">
<a-col :span="6">
<a-select
v-model:value="formData.stations"
placeholder="请选择水电站"
@ -58,6 +64,7 @@
:filter-option="filterOption"
:loading="stationLoading"
style="width: 100%"
:max-tag-count="2"
>
<a-select-option
v-for="item in stationOptions"
@ -86,14 +93,14 @@
tree-node-filter-prop="title"
show-search
allow-clear
style="width: 100%"
style="width: 400px"
/>
</a-form-item>
<a-form-item label="数据来源" name="dataSource">
<a-form-item label="数据来源" name="dataSource" v-if="formData.timeDimension === 'month'">
<a-radio-group v-model:value="formData.dataSource">
<a-radio value="day">日统计表</a-radio>
<a-radio value="hour">小时表</a-radio>
<a-radio value="true">日统计表</a-radio>
<a-radio value="false">小时表</a-radio>
</a-radio-group>
</a-form-item>
</template>
@ -102,45 +109,138 @@
<template v-if="formData.timeDimension === 'year'">
<a-form-item label="统计内容" name="yearStatContent">
<a-checkbox-group v-model:value="formData.yearStatContent">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
<a-checkbox value="verticalTemp">垂向水温</a-checkbox>
<a-checkbox value="surfaceTemp">表层水温</a-checkbox>
<a-checkbox value="wq">水质</a-checkbox>
<a-checkbox value="qec">生态流量达标率</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="统计年份" name="statYears">
<a-tree-select
<a-select
v-model:value="formData.statYears"
:tree-data="yearTreeData"
placeholder="请选择统计年份"
tree-checkable
:fieldNames="{
label: 'title',
value: 'value',
children: 'children'
}"
tree-node-filter-prop="title"
mode="multiple"
allow-clear
show-search
allow-clear
:filter-option="filterOption"
style="width: 300px"
:max-tag-count="2"
>
<a-select-option
v-for="item in yearOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="统计维度与内容" name="dimensionStat">
<a-checkbox-group
v-model:value="formData.yearDimContent"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="显示维度统计" name="dimensionStat">
<a-input
v-model:value="formData.dimensionStat"
placeholder="请输入显示维度统计"
>
<a-row>
<a-col :span="24" style="margin-bottom: 10px">
<div style="display: flex; align-items: center">
<a-checkbox value="stcd">电站</a-checkbox>
<a-select
v-model:value="formData.rvcd"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
/>
</a-form-item>
<a-form-item label="内容" name="content">
<a-input
v-model:value="formData.content"
placeholder="请输入内容"
show-search
:filter-option="filterOption"
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
<a-select
v-model:value="formData.stations"
placeholder="请选择水电站"
mode="multiple"
allow-clear
/>
show-search
:filter-option="filterOption"
:loading="stationLoading"
style="width: 300px"
:max-tag-count="2"
>
<a-select-option
v-for="item in stationOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
<a-col :span="24" style="margin-bottom: 10px">
<div style="display: flex; align-items: center">
<a-checkbox value="baseId">基地</a-checkbox>
<a-select
v-model:value="formData.rvcd"
placeholder="请选择水电基地"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in rvcdOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
<a-col :span="24">
<div style="display: flex; align-items: center">
<a-checkbox value="ly">流域</a-checkbox>
<a-select
v-model:value="formData.lyRvcd"
placeholder="请选择流域"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="lyLoading"
style="width: 200px; margin-right: 10px"
:max-tag-count="2"
>
<a-select-option
v-for="item in lyOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</template>
@ -155,46 +255,58 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { message } from 'ant-design-vue';
import { getSelectForDropdown, getEngInfoDropdown } from '@/api/select';
import {
getSelectForDropdown,
getEngInfoDropdown,
qgcExportCreate,
qgcExportstatus,
qgcExportdownload
} from '@/api/select';
import { getDictItemsByCode } from '@/api/dict';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
const formRef = ref<any>();
const exportLoading = ref(false);
const rvcdLoading = ref(false);
const stationLoading = ref(false);
const JidiSelectEventStore = useJidiSelectEventStore();
//
const rvcdOptions = ref<{ label: string; value: string }[]>([]);
//
const stationOptions = ref<{ label: string; value: string }[]>([]);
//
const lyOptions = ref<{ label: string; value: string }[]>([]);
const lyLoading = ref(false);
//
const formData = reactive({
timeDimension: 'month' as 'month' | 'year',
timeDimension: 'month',
//
monitorElements: [] as string[],
rvcd: [] as string[],
stations: [] as string[],
statMonths: [] as string[],
dataSource: 'day' as 'day' | 'hour',
dataSource: 'true',
//
yearStatContent: [] as string[],
statYears: [] as string[],
yearDimContent: [] as string[],
lyRvcd: [] as string[],
dimensionStat: '',
content: ''
});
//
// 26
const monthTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const years: any[] = [];
for (let y = currentYear; y >= currentYear - 5; y--) {
for (let y = currentYear; y >= currentYear - 26; y--) {
years.push({
title: `${y}`,
title: `${y}`,
value: `year_${y}`,
children: Array.from({ length: 12 }, (_, i) => ({
title: `${y}${i + 1}`,
title: `${y}-${i + 1}`,
value: `${y}-${String(i + 1).padStart(2, '0')}`
}))
});
@ -202,27 +314,22 @@ const monthTreeData = computed(() => {
return years;
});
//
const yearTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const decades: any[] = [];
const startDecade = Math.floor((currentYear - 20) / 10) * 10;
const endDecade = Math.floor(currentYear / 10) * 10;
//
const getDefaultStatMonths = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
return [`${year}-${String(month).padStart(2, '0')}`];
};
for (let d = endDecade; d >= startDecade; d -= 10) {
const years: any[] = [];
for (let y = d; y < d + 10 && y <= currentYear; y++) {
years.push({ title: `${y}`, value: `${y}` });
// 26
const yearOptions = computed(() => {
const currentYear = new Date().getFullYear();
const years: { label: string; value: string }[] = [];
for (let y = currentYear; y >= currentYear - 26; y--) {
years.push({ label: `${y}`, value: `${y}` });
}
if (years.length > 0) {
decades.push({
title: `${d}s`,
value: `decade_${d}`,
children: years
});
}
}
return decades;
return years;
});
//
@ -230,38 +337,18 @@ const filterOption = (input: string, option: any) => {
return (option?.label || '').toLowerCase().includes(input.toLowerCase());
};
//
const loadRvcdOptions = async () => {
rvcdLoading.value = true;
try {
const res = await getSelectForDropdown({});
if (res?.data) {
rvcdOptions.value = (res.data || []).map((item: any) => ({
label: item.rvnm || item.label || item.name,
value: item.rvcd || item.value || item.id
}));
}
} catch (e) {
console.error('获取流域列表失败', e);
} finally {
rvcdLoading.value = false;
}
};
//
const loadStationOptions = async (rvcds?: string[]) => {
stationLoading.value = true;
try {
const params: any = {};
if (rvcds && rvcds.length > 0) {
params.rvcds = rvcds;
}
const res = await getEngInfoDropdown(params);
const res = await getEngInfoDropdown({ baseIds: rvcds || [] });
if (res?.data) {
stationOptions.value = (res.data || []).map((item: any) => ({
label: item.engnm || item.stnm || item.label || item.name,
label: item.ennm || item.stnm || item.label || item.name,
value: item.stcd || item.engId || item.value || item.id
}));
//
formData.stations = stationOptions.value.map(item => item.value);
}
} catch (e) {
console.error('获取水电站列表失败', e);
@ -270,31 +357,118 @@ const loadStationOptions = async (rvcds?: string[]) => {
}
};
// store
const initDone = ref(false);
watch(
() => JidiSelectEventStore.jidiData,
(val) => {
if (initDone.value) return;
const data = (val || []).filter((item: any) => item.wbsCode !== 'all');
if (data.length === 0) return;
rvcdOptions.value = data.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
formData.rvcd = [rvcdOptions.value[0].value];
loadStationOptions(formData.rvcd);
initDone.value = true;
},
{ immediate: true }
);
//
const handleRvcdChange = (values: string[]) => {
formData.stations = [];
loadStationOptions(values);
};
//
const loadLyOptions = async () => {
lyLoading.value = true;
try {
const res = await getSelectForDropdown({});
if (res?.data) {
lyOptions.value = (res.data || []).map((item: any) => ({
label: item.rvnm,
value: item.rvcd
}));
//
if (lyOptions.value.length > 0) {
formData.lyRvcd = [lyOptions.value[0].value];
}
}
} catch (e) {
console.error('获取流域列表失败', e);
} finally {
lyLoading.value = false;
}
};
const getExstatus = async (taskId, exportParams) => {
let res1: any = await qgcExportstatus({ taskId: taskId });
if (res1.status == 'PROCESSING') {
setTimeout(() => {
getExstatus(taskId, exportParams);
}, 5000);
} else if (res1.status == 'DONE') {
const blob = await qgcExportdownload({ taskId: taskId, ...exportParams });
//
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export_${taskId}.zip`;
a.click();
window.URL.revokeObjectURL(url);
exportLoading.value = false;
message.success('导出成功');
} else {
message.error('导出失败');
}
};
//
const handleExport = () => {
const handleExport = async () => {
exportLoading.value = true;
try {
if (formData.timeDimension !== 'year') {
const params: any = {
tmDimension: formData.timeDimension,
dataField: formData.monitorElements.join(','),
months: formData.statMonths.join(','),
stcd: formData.stations.join(','),
sysId: 'qgc',
isCalcQec: true
};
if (formData.timeDimension === 'month') {
console.log('月度统计导出参数:', {
monitorElements: formData.monitorElements,
rvcd: formData.rvcd,
stations: formData.stations,
statMonths: formData.statMonths,
dataSource: formData.dataSource
});
params.isDailyDimension = formData.dataSource === 'day';
}
console.log(`${formData.timeDimension}统计导出参数:`, params);
let res: any = await qgcExportCreate(params);
if (res.taskId) {
getExstatus(res.taskId, params);
}
} else {
console.log('年统计导出参数:', {
yearStatContent: formData.yearStatContent,
statYears: formData.statYears,
dimensionStat: formData.dimensionStat,
content: formData.content
});
const params: any = {
tmDimension: 'year',
yearStatType: formData.yearStatContent.join(','),
year: formData.statYears.join(','),
sysId: 'qgc'
};
//
if (formData.yearDimContent.includes('stcd')) {
params.stcd = formData.stations.join(',');
}
if (formData.yearDimContent.includes('baseId')) {
params.baseId = formData.rvcd.join(',');
}
if (formData.yearDimContent.includes('ly')) {
params.hbRvcd = formData.lyRvcd.join(',');
}
console.log('年统计导出参数:', params);
let res: any = await qgcExportCreate(params);
if (res.taskId) {
getExstatus(res.taskId, params);
}
}
message.success('导出任务已提交');
} catch (e) {
@ -303,17 +477,28 @@ const handleExport = () => {
exportLoading.value = false;
}
};
const dataFieldOption = ref([]);
const getDict = async () => {
let res = await getDictItemsByCode({ dictCode: 'dataField' });
dataFieldOption.value = res.data;
formData.monitorElements = dataFieldOption.value.map(item => item.itemCode);
// debugger
};
onMounted(() => {
loadRvcdOptions();
loadStationOptions();
getDict();
formData.statMonths = getDefaultStatMonths();
formData.yearStatContent = ['wq', 'qec'];
formData.yearDimContent = ['stcd', 'baseId'];
formData.statYears = [String(new Date().getFullYear())];
loadLyOptions();
});
</script>
<style scoped lang="scss">
.export-container {
padding: 20px;
max-width: 800px;
// max-width: 800px;
width: 100%;
.export-form {
:deep(.ant-form-item) {
@ -321,4 +506,12 @@ onMounted(() => {
}
}
}
.body_one {
position: relative;
z-index: 900;
pointer-events: all;
}
// .export-container{
// }
</style>

View File

@ -0,0 +1,404 @@
<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>

View File

@ -0,0 +1,369 @@
<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>

View File

@ -80,12 +80,13 @@ const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
mway: string;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: '0086601020VP000006',
mway: '1'
mway: props.mway
};
const searchData = ref<any>({ ...initSearchData });
@ -133,22 +134,6 @@ 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',

View File

@ -1,551 +1,42 @@
<template>
<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
}"
<div class="w-full h-full">
<a-tabs
v-model:activeKey="activeTab"
class="fish-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>
<a-tab-pane key="manual" tab="人工站点">
<FishDataManual />
</a-tab-pane>
<a-tab-pane key="auto" tab="自动站点">
<FishDataAuto />
</a-tab-pane>
</a-tabs>
</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';
import { ref } from 'vue';
import FishDataManual from './FishDataManual.vue';
import FishDataAuto from './FishDataAuto.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 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();
});
});
const activeTab = ref('manual');
</script>
<style scoped lang="scss"></style>
<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>

View File

@ -202,7 +202,7 @@ onUnmounted(() => {
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
height: 200px;
position: relative;
overflow: hidden;

View File

@ -27,14 +27,14 @@
:style="{ gridArea: `${item.position}-${item.areaIdx}` }"
class="panel-item"
>
<component :is="getModuleComponent(item.code)" />
<component :is="panelComponents[item.code]" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick, provide } from 'vue';
import { ref, computed, onMounted, onUnmounted, watch, nextTick, provide,defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import { getTreeConfiguredps, getPowerSubjectLayout } from '@/api/dianZhanZhuanTi';
@ -44,7 +44,6 @@ import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import { getModuleComponent } from './moduleMap';
import { LAYOUT_GRID_SKELETONS } from './layoutGridSkeletons';
import { setMapLegendPos } from '@/components/gis/gisUtils';
defineOptions({
name: 'DianZhanZhuanTi'
});
@ -72,6 +71,10 @@ interface DianZhanStation {
const dianZhanStation = ref<DianZhanStation | null>(null);
provide('dianZhanStation', dianZhanStation);
// ppbclTagList key @/modulePathvalue { isShow, tagValue }
const ppbclConfigMap = ref<Record<string, { isShow: number; tagValue: string }>>({});
provide('ppbclConfigMap', ppbclConfigMap);
// ==================== ====================
const treeSelectData = ref<any[]>([]);
const treeSelectValue = ref<string>();
@ -81,6 +84,8 @@ const layoutData = ref<{
type: string;
data: Array<{ code: string; name: string; position: string; index: number }>;
} | null>(null);
/** 布局数据加载完成后统一解析一次模块组件,避免每次渲染都新建组件导致面板反复卸载重挂载 */
const panelComponents = ref<Record<string, any>>({});
// ==================== ====================
@ -141,12 +146,26 @@ function getLayout(stcd: string) {
(el: any) => el.code
);
layoutData.value = { type: layoutConfig.layoutId, data: _data };
// ppbclTagList
const ppbclTagList: any[] = layoutConfig.ppbclTagList || [];
const map: Record<string, { isShow: number; tagValue: string }> = {};
ppbclTagList.forEach((item: any) => {
// ../../modules/xxx/index.vue @/modules/xxx/index.vue
const normalizedPath = item.code?.replace(/^\.\.\/\.\.\//, '@/');
if (normalizedPath) {
map[normalizedPath] = { isShow: item.isShow, tagValue: item.tagValue };
}
});
ppbclConfigMap.value = map;
} else {
layoutData.value = { type: '', data: [] };
ppbclConfigMap.value = {};
}
})
.catch(() => {
layoutData.value = { type: '', data: [] };
ppbclConfigMap.value = {};
});
}
@ -224,11 +243,13 @@ function switchToVector() {
mapViewStore.setActiveBaseLayerKey('s_province_boundaries');
} catch {}
}
//
// code
const moduleComponentCache = new Map<string, any>();
const getModuleComponenturl = (url: string) => {
return defineAsyncComponent(
() => import(url)
)
if (!moduleComponentCache.has(url)) {
moduleComponentCache.set(url, defineAsyncComponent(() => import(url)));
}
return moduleComponentCache.get(url);
};
// ==================== jidiSelectorMod ====================
watch(
@ -271,6 +292,19 @@ watch(
{ deep: true }
);
/** 布局数据加载完成后,统一解析一次各面板模块组件(组件只创建一次,后续渲染复用) */
watch(
() => layoutData.value?.data,
(items) => {
const map: Record<string, any> = {};
(items || []).forEach((item: any) => {
if (item.code) map[item.code] = getModuleComponenturl(item.code);
});
panelComponents.value = map;
},
{ deep: true }
);
// ==================== ====================
function resetMapControlPositions() {
const legend = document.querySelector('#qgc-legendtl') as HTMLElement;