Merge branch 'wxk-main'
This commit is contained in:
commit
678caccbd7
@ -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({
|
||||
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
298
frontend-sjgl/src/components/OperationLogModal/index.vue
Normal file
298
frontend-sjgl/src/components/OperationLogModal/index.vue
Normal 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>
|
||||
@ -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 };
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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); };
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<div class="export-container body_one">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:labelCol="{ span: 6 }"
|
||||
:model="formData"
|
||||
: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="请输入显示维度统计"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="内容" name="content">
|
||||
<a-input
|
||||
v-model:value="formData.content"
|
||||
placeholder="请输入内容"
|
||||
allow-clear
|
||||
/>
|
||||
>
|
||||
<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
|
||||
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}` });
|
||||
}
|
||||
if (years.length > 0) {
|
||||
decades.push({
|
||||
title: `${d}s`,
|
||||
value: `decade_${d}`,
|
||||
children: years
|
||||
});
|
||||
}
|
||||
// 统计年份选项(当前年往前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}` });
|
||||
}
|
||||
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 === 'month') {
|
||||
console.log('月度统计导出参数:', {
|
||||
monitorElements: formData.monitorElements,
|
||||
rvcd: formData.rvcd,
|
||||
stations: formData.stations,
|
||||
statMonths: formData.statMonths,
|
||||
dataSource: formData.dataSource
|
||||
});
|
||||
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') {
|
||||
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) {
|
||||
|
||||
404
frontend-sjgl/src/views/monitorData/FishData/FishDataAuto.vue
Normal file
404
frontend-sjgl/src/views/monitorData/FishData/FishDataAuto.vue
Normal 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>
|
||||
369
frontend-sjgl/src/views/monitorData/FishData/FishDataManual.vue
Normal file
369
frontend-sjgl/src/views/monitorData/FishData/FishDataManual.vue
Normal 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>
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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('获取设施列表失败');
|
||||
|
||||
@ -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: {
|
||||
|
||||
207
frontend/src/api/dianzhanzhuantiMode/index.ts
Normal file
207
frontend/src/api/dianzhanzhuantiMode/index.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@ -4,7 +4,7 @@ export function wbsbGetKendoList(data: any) {
|
||||
return request({
|
||||
url: '/env/hydrobase/wbsb/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,12 +130,13 @@
|
||||
:picker="scopeDate.picker"
|
||||
:allowClear="false"
|
||||
:style="{
|
||||
width:
|
||||
scopeDate.picker === 'year'
|
||||
? '80px'
|
||||
: scopeDate.picker === 'month'
|
||||
? '180px'
|
||||
: ''
|
||||
width: scopeDate.width
|
||||
? scopeDate.width
|
||||
: scopeDate.picker === 'year'
|
||||
? '80px'
|
||||
: scopeDate.picker === 'month'
|
||||
? '180px'
|
||||
: ''
|
||||
}"
|
||||
:format="scopeDate.format"
|
||||
:range-separator="' 至 '"
|
||||
@ -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);
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 到 modelStore,VideoInfo 组件依赖 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 |
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
446
frontend/src/modules/dianzhanzhuantiMod/guoyuzongliang/index.vue
Normal file
446
frontend/src/modules/dianzhanzhuantiMod/guoyuzongliang/index.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 }
|
||||
);
|
||||
|
||||
// ==================== 生命周期钩子 ====================
|
||||
|
||||
// 页面加载时执行
|
||||
|
||||
@ -129,3 +129,8 @@ svg {
|
||||
.ant-table-pagination.ant-pagination {
|
||||
padding: 0 4px !important;
|
||||
}
|
||||
.chart_baowei{
|
||||
.ant-spin-nested-loading{
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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="请输入显示维度统计"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="内容" name="content">
|
||||
<a-input
|
||||
v-model:value="formData.content"
|
||||
placeholder="请输入内容"
|
||||
allow-clear
|
||||
/>
|
||||
>
|
||||
<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
|
||||
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>
|
||||
|
||||
@ -150,51 +250,63 @@
|
||||
导出
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-form>
|
||||
</div>
|
||||
</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}` });
|
||||
}
|
||||
if (years.length > 0) {
|
||||
decades.push({
|
||||
title: `${d}s`,
|
||||
value: `decade_${d}`,
|
||||
children: years
|
||||
});
|
||||
}
|
||||
// 统计年份选项(当前年往前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}` });
|
||||
}
|
||||
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 === 'month') {
|
||||
console.log('月度统计导出参数:', {
|
||||
monitorElements: formData.monitorElements,
|
||||
rvcd: formData.rvcd,
|
||||
stations: formData.stations,
|
||||
statMonths: formData.statMonths,
|
||||
dataSource: formData.dataSource
|
||||
});
|
||||
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') {
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -202,7 +202,7 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@ -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 为 @/modulePath,value 为 { 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 {}
|
||||
}
|
||||
//处理返回来的路径
|
||||
const getModuleComponenturl = (url: string) => {
|
||||
return defineAsyncComponent(
|
||||
() => import(url)
|
||||
)
|
||||
//处理返回来的路径(按 code 缓存组件,避免每次渲染都新建导致面板反复卸载重挂载)
|
||||
const moduleComponentCache = new Map<string, any>();
|
||||
const getModuleComponenturl = (url: string) => {
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user