基础弹框模块添加

This commit is contained in:
扈兆增 2026-06-15 09:18:00 +08:00
parent 65c4f4260c
commit d4054ef786
19 changed files with 3906 additions and 51 deletions

View File

@ -303,7 +303,7 @@ export function getAIComCalendarClick(data: any) {
}); });
} }
// 鱼类增殖站 - 运行数据 -获取年 // 鱼类增殖站 - 运行数据 -获取年 没接
export function getFishProgressionYear(data: any) { export function getFishProgressionYear(data: any) {
return request({ return request({
url: '/wmp-env-server/fb/fbrdmr/getFbRelatedYrByStcd', url: '/wmp-env-server/fb/fbrdmr/getFbRelatedYrByStcd',
@ -311,7 +311,7 @@ export function getFishProgressionYear(data: any) {
params: data params: data
}); });
} }
//鱼类增殖站 - 运行数据 -获取鱼 //鱼类增殖站 - 运行数据 -获取鱼 没接
export function getFishProgressionFish(data: any) { export function getFishProgressionFish(data: any) {
return request({ return request({
url: '/wmp-env-server/env/fishDic/stcd/GetKendoList', url: '/wmp-env-server/env/fishDic/stcd/GetKendoList',
@ -319,6 +319,33 @@ export function getFishProgressionFish(data: any) {
data data
}); });
} }
// 过程图 - 亲鱼
export function getProcessDiagramParentFish(data: any) {
return request({
url: '/wmp-env-server/fb/bsmfr/fish/GetKendoListCust',
method: 'post',
data
});
}
// 过程图 - 产卵/孵化
export function getProcessDiagramHatch(data: any, type: 1 | 2) {
return request({
url: `/wmp-env-server/fb/fishhatchrecr/fish/GetKendoListCust?type=${type}`,
method: 'post',
data
});
}
// 过程图 - 鱼苗
export function getProcessDiagramFishFry(data: any) {
return request({
url: '/wmp-env-server/fb/fishbreedr/fish/GetKendoListCust',
method: 'post',
data
});
}
//增值放流情况 获取年 没接 //增值放流情况 获取年 没接
export function getNormalAddedSituationYear(data: any) { export function getNormalAddedSituationYear(data: any) {
return request({ return request({
@ -344,3 +371,125 @@ export function getNormalResearchSituation(data: any) {
data data
}); });
} }
// 珍稀植物园 - 运行数据 没接
export function getNormalDataMonitoring2Year(data: any) {
return request({
url: '/wmp-env-server/env/vpr/year/GetKendoListCust',
method: 'post',
data
});
}
// 珍稀植物园 - 运行数据 列表 没接
export function getNormalDataMonitoring2List(data: any) {
return request({
url: '/wmp-env-server/env/vpr/basinVpIntDetail',
method: 'post',
data
});
}
// 珍稀动物园 - 监测数据 时间 没接
export function getNormal2Year(data: any) {
return request({
url: '/wmp-env-server/env/var/year/GetKendoListCust',
method: 'post',
data
});
}
// 珍稀动物园 - 监测数据 列表 没接
export function getNormal2List(data: any) {
return request({
url: '/wmp-env-server/env/var/GetKendoListCust',
method: 'post',
data
});
}
// 水生生态调查断面 - 监测数据 年份 没接
export function getEcologyYear(data: any) {
return request({
url: '/wmp-env-server/we/wer/getWeYr',
method: 'get',
params: data
});
}
// 水生生态调查断面 - 监测数据 调查批次 没接
export function getEcologyBatch(data: any) {
return request({
url: '/wmp-env-server/env/we/fisht/GetKendoListCust',
method: 'post',
data
});
}
// 水生生态调查断面 - 监测数据 保护类型 没接
export function getEcologyProtectType(data: any) {
return request({
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: data
});
}
// 水生生态调查断面 - 监测数据 调查鱼类列表 没接
export function getEcologyList(data: any) {
return request({
url: '/wmp-env-server/env/we/fisht/qgc/GetKendoListCust',
method: 'post',
data
});
}
// 水生生态调查断面 - 监测数据 水温列表 没接
export function getEcologyList1(data: any) {
return request({
url: '/wmp-env-server/we/wewtr/GetKendoListCust',
method: 'post',
data
});
}
// 水生生态调查断面 - 监测数据 水质列表 没接
export function getEcologyList2(data: any) {
return request({
url: '/wmp-env-server/we/wewqr/GetKendoListCust',
method: 'post',
data
});
}
// 水生生态调查断面 - 监测数据 流速列表 没接
export function getEcologyList3(data: any) {
return request({
url: '/wmp-env-server/env/we/fvR/GetKendoListCust',
method: 'post',
data
});
}
// 野生动物检测 -现场录像 - 年份 - 没接
export function getLiveFootageYear(data: any) {
return request({
url: '/wmp-env-server/env/wva/default/GetKendoListCust',
method: 'post',
data
});
}
// 野生动物检测 -现场录像 - 没接
export function getLiveFootageList(data: any) {
return request({
url: '/wmp-env-server/env/wva/GetKendoListCust',
method: 'post',
data
});
}
// 鱼类分布明细 - 年 没接
export function getFishDistributionYear(data: any) {
return request({
url: '/wmp-env-server/we/wer/getWeYr',
method: 'get',
params: data
});
}
// 鱼类分布明细 - 列表 没接
export function getFishDistributionList(data: any) {
return request({
url: '/wmp-env-server/env/we/fisht/qgc/GetKendoListCust',
method: 'post',
data
});
}

View File

@ -0,0 +1,262 @@
<template>
<a-modal
:open="open"
:title="title"
width="80vw"
:footer="null"
:destroy-on-close="true"
@update:open="handleOpenChange"
>
<a-spin :spinning="loading">
<div class="attachment-modal">
<div v-if="fileList.length > 0" class="file-list">
<div
v-for="file in fileList"
:key="file.id"
:class="['file-item', { active: selectedFile?.id === file.id }]"
@click="selectFile(file)"
>
<span class="file-name" :title="file.name">{{ file.name }}</span>
</div>
</div>
<div class="viewer-panel">
<template v-if="selectedFile">
<div v-if="selectedFile.fileType === 'image'" class="image-viewer">
<img :src="selectedFile.url" :alt="selectedFile.name" />
</div>
<div v-else-if="selectedFile.fileType === 'video'" class="video-viewer">
<video :src="selectedFile.url" controls class="video-player"></video>
</div>
<div v-else-if="selectedFile.fileType === 'pdf'" class="pdf-viewer">
<PdfView :file-id="String(selectedFile.id)" />
</div>
<div v-else class="empty-state">
<a-empty description="暂不支持该附件预览" />
</div>
</template>
<div v-else class="empty-state">
<a-empty description="暂无附件" />
</div>
</div>
</div>
</a-spin>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { getIdUrl } from '@/api/mapModal';
import PdfView from '@/components/pdf/pdfView.vue';
interface AttachmentFileItem {
id: string;
name: string;
url: string;
ext: string;
fileType: 'pdf' | 'image' | 'video' | 'unknown';
}
const props = defineProps({
open: {
type: Boolean,
default: false
},
fid: {
type: String,
default: ''
},
title: {
type: String,
default: '查看附件'
}
});
const emit = defineEmits<{
(e: 'update:open', value: boolean): void;
}>();
const loading = ref(false);
const fileList = ref<AttachmentFileItem[]>([]);
const selectedFile = ref<AttachmentFileItem | null>(null);
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.avi', '.wmv', '.m3u8'];
const getFileType = (ext: string): AttachmentFileItem['fileType'] => {
const normalizedExt = String(ext || '').toLowerCase();
if (normalizedExt === '.pdf') return 'pdf';
if (IMAGE_EXTENSIONS.includes(normalizedExt)) return 'image';
if (VIDEO_EXTENSIONS.includes(normalizedExt)) return 'video';
return 'unknown';
};
const resetState = () => {
loading.value = false;
fileList.value = [];
selectedFile.value = null;
};
const selectFile = (file: AttachmentFileItem) => {
selectedFile.value = file;
};
const fetchAttachmentList = async () => {
if (!props.fid) {
resetState();
return;
}
loading.value = true;
try {
const fids = props.fid
.split(',')
.map(item => item.trim())
.filter(Boolean);
if (fids.length === 0) {
resetState();
return;
}
const res: any = await getIdUrl({
id: fids.join(',')
});
const data = res?.data || {};
const files = fids
.map(fid => {
const item = data?.[fid];
if (!item) return null;
const ext = String(item.ext || '').toLowerCase();
return {
id: fid,
name: item.name || fid,
url: item.fullpath || '',
ext,
fileType: getFileType(ext)
} as AttachmentFileItem;
})
.filter(Boolean) as AttachmentFileItem[];
fileList.value = files;
selectedFile.value = files[0] || null;
} catch (error) {
console.error('获取附件列表失败:', error);
resetState();
} finally {
loading.value = false;
}
};
const handleOpenChange = (value: boolean) => {
emit('update:open', value);
};
watch(
() => [props.open, props.fid],
([open]) => {
if (!open) {
resetState();
return;
}
fetchAttachmentList();
},
{ immediate: true }
);
</script>
<style scoped lang="scss">
.attachment-modal {
display: flex;
width: 100%;
height: 600px;
border: 1px solid #e4e7ed;
border-radius: 3px;
overflow: hidden;
}
.file-list {
width: 220px;
height: 100%;
overflow-y: auto;
background-color: #f8f9fa;
border-right: 1px solid #e4e7ed;
}
.file-item {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #e4e7ed;
&:hover {
background-color: #e8f4fd;
}
&.active {
background-color: #005293;
color: #fff;
}
}
.file-name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.viewer-panel {
flex: 1;
height: 100%;
position: relative;
background: #f5f5f5;
}
.pdf-viewer,
.video-viewer,
.image-viewer,
.empty-state {
width: 100%;
height: 100%;
}
.image-viewer {
display: flex;
align-items: center;
justify-content: center;
background: #000;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.video-viewer {
display: flex;
align-items: center;
justify-content: center;
background: #000;
}
.video-player {
width: 100%;
height: 100%;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,316 @@
<template>
<div class="dzgj">
<div class="search-bar mb-4">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.year"
picker="year"
style="width: 160px"
format="YYYY"
value-format="YYYY"
/>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</div>
<a-tabs v-model:activeKey="activeTabKey" @change="handleTabChange">
<a-tab-pane v-for="tab in tabsItems" :key="tab.key" :tab="tab.name" />
</a-tabs>
<a-spin :spinning="requestLoading">
<BasicTable
ref="tableRef"
:key="activeTabKey"
:scrollY="500"
:scrollX="tableScrollX"
:columns="currentColumns"
:list-url="fetchTableData"
row-key="_rowKey"
:transform-data="transformTableData"
/>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { queryPostUrlList } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
type TabItem = {
name: string;
key: string;
type?: string;
tableUrl: string;
filterProps?: {
params?: Record<string, any>;
};
};
const props = defineProps<{
tabsItems: TabItem[];
isActive: boolean;
}>();
const modelStore = useModelStore();
const DEFAULT_PAGE_SIZE = 20;
const tableRef = ref();
const hasLoaded = ref(false);
const requestLoading = ref(false);
const activeTabKey = ref(props.tabsItems?.[0]?.key || '');
const searchParams = ref({
year: String(new Date().getFullYear())
});
const baseColumns = [
{
title: '序号',
dataIndex: 'rowId',
width: 60
},
{
title: '告警设施',
dataIndex: 'stnm',
width: 220
},
{
title: '告警时间',
dataIndex: 'tms',
width: 220,
customRender: ({ text }: any) => (text ? text : '-')
},
{
title: '异常值',
dataIndex: 'ssz',
width: 120
},
{
title: '监测要素',
dataIndex: 'ysName',
width: 120
},
{
title: '判断标准',
dataIndex: 'bzz',
width: 120
},
{
title: '所属电站',
dataIndex: 'ennm',
width: 120
}
];
const ecologicalFlowColumns = [
{
title: '序号',
dataIndex: '_index',
width: 80
},
{
title: '告警时间',
dataIndex: 'btm',
width: 220,
customRender: ({ text }: any) => (text ? text : '-')
},
{
title: '异常值',
dataIndex: 'av',
width: 140
},
{
title: '限值(m³/s)',
dataIndex: 'val',
width: 140
},
{
title: '所属电站',
dataIndex: 'stnm',
width: 220
}
];
const columnsMap: Record<string, any[]> = {
SZ: baseColumns,
STLL: ecologicalFlowColumns
};
const currentTab = computed(
() => props.tabsItems.find(item => item.key === activeTabKey.value) || null
);
const currentColumns = computed(
() => columnsMap[activeTabKey.value] || baseColumns
);
const tableScrollX = computed(() =>
Math.max(
currentColumns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
const getYearRange = (year: string) => ({
startDate: `${year}-01-01 00:00:00`,
endDate: `${year}-12-31 23:59:59`
});
const getStationField = () => (activeTabKey.value === 'SZ' ? 'rstcd' : 'stcd');
const buildFilter = () => {
const year = searchParams.value.year;
const stcd = modelStore.params.stcd;
if (!year || !stcd) {
return {
logic: 'and',
filters: []
};
}
const { startDate, endDate } = getYearRange(String(year));
return {
logic: 'and',
filters: [
{
field: getStationField(),
operator: 'in',
dataType: 'string',
value: [stcd]
},
{
field: 'btm',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'btm',
operator: 'lte',
dataType: 'date',
value: endDate
}
]
};
};
const buildRequestData = (params: any) => {
const tab = currentTab.value;
return {
...params,
take: Number(params?.take || DEFAULT_PAGE_SIZE),
skip: params?.skip || 1,
params: {
...(tab?.filterProps?.params || {})
},
filter: params?.filter || buildFilter()
};
};
const fetchTableData = async (params: any) => {
const tab = currentTab.value;
if (!tab?.tableUrl || !modelStore.params.stcd || !searchParams.value.year) {
return { data: { records: [], total: 0 } };
}
requestLoading.value = true;
try {
return await queryPostUrlList(tab.tableUrl, buildRequestData(params));
} finally {
requestLoading.value = false;
}
};
const transformTableData = (res: any) => {
const records = res?.data?.records || res?.data?.data || res?.data || [];
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
return {
records: records.map((item: any, index: number) => ({
...item,
_index: index + 1,
_rowKey:
item?.id ||
item?.warnId ||
`${activeTabKey.value}-${
item?.stcd || modelStore.params.stcd || 'stcd'
}-${item?.btm || index}`
})),
total
};
};
const refreshTable = () => {
if (!modelStore.params.stcd || !searchParams.value.year) return;
tableRef.value?.getList(buildFilter());
};
const handleSearch = () => {
if (!searchParams.value.year) {
message.warning('请选择年份');
return;
}
refreshTable();
};
const handleTabChange = async (key: string) => {
activeTabKey.value = key;
await nextTick();
refreshTable();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
refreshTable();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => props.tabsItems,
newTabs => {
if (newTabs?.length) {
activeTabKey.value = newTabs.some(item => item.key === activeTabKey.value)
? activeTabKey.value
: newTabs[0].key;
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd && props.isActive) {
refreshTable();
}
}
);
</script>
<style lang="scss" scoped>
.dzgj {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,583 @@
<template>
<div class="ecology">
<div class="search-bar mb-4">
<div class="search-item">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.tm"
picker="year"
style="width: 160px"
format="YYYY"
value-format="YYYY"
/>
</div>
<div class="search-item">
<span>调查批次</span>
<a-select
v-model:value="searchParams.dcpc"
style="width: 180px"
:options="batchOptions"
placeholder="请选择调查批次"
:loading="batchLoading"
/>
</div>
<div class="search-item">
<span>保护类型</span>
<a-select
v-model:value="searchParams.ty"
style="width: 180px"
:options="protectTypeOptions"
placeholder="请选择保护类型"
:loading="protectTypeLoading"
/>
</div>
<div class="search-item">
<span>渔获物种类</span>
<a-input
v-model:value="searchParams.name"
style="width: 180px"
placeholder="请输入渔获物种类"
allow-clear
/>
</div>
<a-button type="primary" @click="handleSearch">查询</a-button>
</div>
<a-tabs v-model:activeKey="activeTabKey" @change="handleTabChange">
<a-tab-pane v-for="tab in tabsList" :key="tab.key" :tab="tab.name" />
</a-tabs>
<BasicTable
:key="activeTabKey"
ref="tableRef"
:scrollY="500"
:scrollX="tableScrollX"
:columns="currentColumns"
:list-url="getTableList"
row-key="_rowKey"
:transform-data="transformTableData"
>
<template #fid="{ record }">
<a-button
type="link"
class="!p-0"
:disabled="!hasAttachment(record?.fid)"
@click="handleViewAttachment(record)"
>
查看附件
</a-button>
</template>
</BasicTable>
<CommonAttachmentModal
v-model:open="attachmentModalVisible"
:fid="currentAttachmentFid"
title="查看附件"
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import {
getEcologyBatch,
getEcologyList,
getEcologyList1,
getEcologyList2,
getEcologyList3,
getEcologyProtectType,
getEcologyYear
} from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
import CommonAttachmentModal from './CommonAttachmentModal.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const DEFAULT_PAGE_SIZE = 20;
const ALL_VALUE = '';
const hasLoaded = ref(false);
const tableRef = ref();
const batchLoading = ref(false);
const protectTypeLoading = ref(false);
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
const searchParams = ref({
tm: null as string | null,
dcpc: ALL_VALUE,
ty: ALL_VALUE,
name: ''
});
const activeTabKey = ref('fish');
const tabsList = [
{ key: 'fish', name: '调查鱼类' },
{ key: 'waterTemp', name: '水温' },
{ key: 'waterQuality', name: '水质' },
{ key: 'flowRate', name: '流速m/s' }
];
const columnsMap: Record<string, any[]> = {
fish: [
{
title: '调查鱼类',
dataIndex: 'tm',
width: 160
},
{
title: '调查批次',
dataIndex: 'dcpc',
width: 160
},
{
title: '渔获物种类',
dataIndex: 'name',
width: 160
},
{
title: '鱼类规格',
dataIndex: 'fsz',
width: 140
},
{
title: '渔获物数量(尾)',
dataIndex: 'fcnt',
width: 140
},
{
title: '性腺发育期',
dataIndex: 'fwgh',
width: 140
},
{
title: '早期资源量和种类',
dataIndex: 'fwgh',
width: 220
},
{
title: '现场影像',
dataIndex: 'fid',
width: 120,
slots: { customRender: 'fid' }
}
],
waterTemp: [
{
title: '时间',
dataIndex: 'tm',
width: 220
},
{
title: '水温(℃)',
dataIndex: 'wt',
width: 160
}
],
waterQuality: [
{
title: '时间',
dataIndex: 'tm',
width: 220
},
{
title: 'PH',
dataIndex: 'ph',
width: 140
},
{
title: '溶解氧(mg/L)',
dataIndex: 'dox',
width: 180
},
{
title: '电导率(μS/cm)',
dataIndex: 'tm',
width: 180
}
],
flowRate: [
{
title: '时间',
dataIndex: 'tm',
width: 220
},
{
title: '流速(m/s)',
dataIndex: 'flowrate',
width: 160
}
]
};
const listUrlMap: Record<string, (params: any) => Promise<any>> = {
fish: getEcologyList,
waterTemp: getEcologyList1,
waterQuality: getEcologyList2,
flowRate: getEcologyList3
};
const currentColumns = computed(() => columnsMap[activeTabKey.value] || []);
const currentListUrl = computed(
() => listUrlMap[activeTabKey.value] || getEcologyList
);
const tableScrollX = computed(() =>
Math.max(
currentColumns.value.reduce(
(sum: number, col: any) => sum + (col.width || 120),
0
),
800
)
);
const batchOptions = ref([
{
label: '全部',
value: ALL_VALUE
}
]);
const protectTypeOptions = ref([
{
label: '全部',
value: ALL_VALUE
}
]);
const getYearRange = (year: string) => ({
startDate: `${year}-01-01 00:00:00`,
endDate: `${year}-12-31 23:59:59`
});
const getBatchDateRange = (year: string) => ({
startDate: `${year}-01-01`,
endDate: `${year}-12-31`
});
const getDefaultYear = (yearList: any[]) => {
const firstItem = yearList?.[0];
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
return String(firstItem);
}
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
};
const hasAttachment = (fid: unknown) =>
!!fid && String(fid).trim() !== '' && String(fid).trim() !== '-';
const handleViewAttachment = (record: any) => {
if (!hasAttachment(record?.fid)) {
message.warning('该记录无附件');
return;
}
currentAttachmentFid.value = String(record.fid);
attachmentModalVisible.value = true;
};
const buildFilter = () => {
const filters: any[] = [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).endDate
}
];
if (searchParams.value.dcpc) {
filters.push({
field: 'dcpc',
operator: 'eq',
dataType: 'string',
value: searchParams.value.dcpc
});
}
if (searchParams.value.ty) {
filters.push({
field: 'ty',
operator: 'eq',
dataType: 'string',
value: searchParams.value.ty
});
}
if (
searchParams.value.name &&
String(searchParams.value.name).trim() !== ''
) {
filters.push({
field: 'name',
operator: 'contains',
dataType: 'string',
value: searchParams.value.name.trim()
});
}
return {
logic: 'and',
filters
};
};
const transformTableData = (res: any) => {
const records = res?.data?.records || res?.data?.data || res?.data || [];
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
return {
records: records.map((item: any, index: number) => ({
...item,
_rowKey:
item?.id ||
item?.tm ||
`${activeTabKey.value}-${
item?.stcd || modelStore.params.stcd || 'stcd'
}-${index}`
})),
total
};
};
const getTableList = (params: any) => {
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
const currentPage = Number(params?.skip || 1);
const skip = Math.max(currentPage - 1, 0) * take;
return currentListUrl.value({
...params,
take,
skip,
filter: params?.filter || buildFilter()
});
};
const refreshTable = () => {
if (!modelStore.params.stcd || !searchParams.value.tm) return;
tableRef.value?.getList(buildFilter());
};
const loadYearList = async () => {
const currentYear = String(new Date().getFullYear());
if (!modelStore.params.stcd) {
searchParams.value.tm = currentYear;
return;
}
try {
const res: any = await getEcologyYear({
stcd: modelStore.params.stcd
});
const yearData = Array.isArray(res?.data?.data)
? res.data.data
: Array.isArray(res?.data?.records)
? res.data.records
: Array.isArray(res?.data)
? res.data
: [];
const defaultYear = getDefaultYear(yearData) || currentYear;
searchParams.value.tm = String(defaultYear);
} catch (error) {
console.error('获取年份列表失败:', error);
searchParams.value.tm = currentYear;
}
};
const loadBatchOptions = async () => {
if (!modelStore.params.stcd || !searchParams.value.tm) return;
batchLoading.value = true;
try {
const yearRange = getBatchDateRange(String(searchParams.value.tm));
const res: any = await getEcologyBatch({
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: yearRange.startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: yearRange.endDate
}
]
},
group: [{ dir: 'des', field: 'dcpc' }]
});
const records = res?.data?.data || res?.data?.records || res?.data || [];
batchOptions.value = [
{
label: '全部',
value: ALL_VALUE
},
...records
.map((item: any) => {
const value = item?.items?.[0]?.DCPC || item?.key || item?.dcpc || '';
return value
? {
label: value,
value
}
: null;
})
.filter(Boolean)
];
searchParams.value.dcpc = ALL_VALUE;
} catch (error) {
console.error('获取调查批次失败:', error);
batchOptions.value = [
{
label: '全部',
value: ALL_VALUE
}
];
searchParams.value.dcpc = ALL_VALUE;
} finally {
batchLoading.value = false;
}
};
const loadProtectTypeOptions = async () => {
protectTypeLoading.value = true;
try {
const res: any = await getEcologyProtectType({
dictCode: 'FISHTY'
});
const records = res?.data?.data || res?.data?.records || res?.data || [];
protectTypeOptions.value = [
{
label: '全部',
value: ALL_VALUE
},
...records
.filter((item: any) => String(item?.level) !== '1')
.map((item: any) => ({
label: item?.dictMeaning,
value: item?.dictValue
}))
.filter((item: any) => item.label && item.value)
];
searchParams.value.ty = ALL_VALUE;
} catch (error) {
console.error('获取保护类型失败:', error);
protectTypeOptions.value = [
{
label: '全部',
value: ALL_VALUE
}
];
searchParams.value.ty = ALL_VALUE;
} finally {
protectTypeLoading.value = false;
}
};
const initData = async () => {
await loadYearList();
await Promise.all([loadBatchOptions(), loadProtectTypeOptions()]);
refreshTable();
};
const handleSearch = () => {
if (!searchParams.value.tm) {
message.warning('请选择年份');
return;
}
refreshTable();
};
const handleTabChange = async () => {
await nextTick();
refreshTable();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
initData();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => searchParams.value.tm,
(newYear, oldYear) => {
if (newYear && newYear !== oldYear) {
loadBatchOptions();
}
}
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
searchParams.value = {
tm: null,
dcpc: ALL_VALUE,
ty: ALL_VALUE,
name: ''
};
initData();
}
}
);
</script>
<style lang="scss" scoped>
.ecology {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.search-item {
display: flex;
align-items: center;
gap: 8px;
}
}
}
</style>

View File

@ -0,0 +1,259 @@
<template>
<div class="normal">
<div class="search-bar mb-4">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.tm"
picker="year"
style="width: 160px"
format="YYYY"
value-format="YYYY"
/>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</div>
<BasicTable
ref="tableRef"
:scrollY="500"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getTableList"
row-key="_rowKey"
:transform-data="transformTableData"
>
<template #fid="{ record }">
<a-button
type="link"
class="!p-0"
:disabled="!hasAttachment(record?.fid)"
@click="handleViewAttachment(record)"
>
查看附件
</a-button>
</template>
</BasicTable>
<CommonAttachmentModal
v-model:open="attachmentModalVisible"
:fid="currentAttachmentFid"
title="查看附件"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { getNormal2List, getNormal2Year } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
import CommonAttachmentModal from './CommonAttachmentModal.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const DEFAULT_PAGE_SIZE = 20;
const hasLoaded = ref(false);
const tableRef = ref();
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
const searchParams = ref({
tm: null as string | null
});
const tableColumns = ref([
{
title: '种类',
dataIndex: 'tetp',
width: 180
},
{
title: '放生日期',
dataIndex: 'tm',
width: 180
},
{
title: '救助数量(只)',
dataIndex: 'tecnt',
width: 180
},
{
title: '放生地点',
dataIndex: 'stlc',
width: 180
},
{
title: '放生照片',
dataIndex: 'fid',
width: 120,
slots: { customRender: 'fid' }
}
]);
const tableScrollX = ref(320);
const getDefaultYear = (yearList: any[]) => {
const firstItem = yearList?.[0];
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
return String(firstItem);
}
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
};
const getYearRange = (year: string) => ({
startDate: `${year}-01-01 00:00:00`,
endDate: `${year}-12-31 23:59:59`
});
const hasAttachment = (fid: unknown) =>
!!fid && String(fid).trim() !== '' && String(fid).trim() !== '-';
const handleViewAttachment = (record: any) => {
if (!hasAttachment(record?.fid)) {
message.warning('该记录无附件');
return;
}
currentAttachmentFid.value = String(record.fid);
attachmentModalVisible.value = true;
};
const transformTableData = (res: any) => {
const records = res?.data?.records || res?.data?.data || res?.data || [];
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
return {
records: records.map((item: any, index: number) => ({
...item,
_rowKey:
item?.id ||
item?.fid ||
`${item?.stcd || 'stcd'}-${
item?.tm || searchParams.value.tm || 'tm'
}-${index}`
})),
total
};
};
const getYearList = async () => {
if (!modelStore.params.stcd) return;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
}
};
const res: any = await getNormal2Year(params);
const yearData = res?.data?.data || res?.data?.records || res?.data || [];
const defaultYear =
getDefaultYear(yearData) || String(new Date().getFullYear());
searchParams.value.tm = String(defaultYear);
refreshTable();
} catch (error) {
console.error('获取年份列表失败:', error);
if (tableRef.value) {
tableRef.value.loading = false;
}
}
};
const buildFilter = () => ({
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).endDate
}
]
});
const getTableList = (params: any) => {
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
const currentPage = Number(params?.skip || 1);
const skip = Math.max(currentPage - 1, 0) * take;
return getNormal2List({
...params,
take,
skip,
filter: params?.filter || buildFilter()
});
};
const refreshTable = () => {
if (!modelStore.params.stcd || !searchParams.value.tm) return;
tableRef.value?.getList(buildFilter());
};
const handleSearch = () => {
if (!searchParams.value.tm) {
message.warning('请选择年份');
return;
}
refreshTable();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
searchParams.value = { tm: null };
getYearList();
}
}
);
</script>
<style lang="scss" scoped>
.normal {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@ -39,7 +39,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { h, ref, watch } from 'vue';
import { import {
getNormalAddedSituationYear, getNormalAddedSituationYear,
getNormalAddedSituation getNormalAddedSituation
@ -67,6 +67,33 @@ const searchParams = ref({
const loadFtp = ref(false); const loadFtp = ref(false);
const ftpList = ref<any[]>([]); const ftpList = ref<any[]>([]);
const isEmptyValue = (value: unknown) =>
value == null || value === '-' || String(value).trim() === '';
const formatRangeOrDecimal = (value: unknown, digits: number) => {
if (isEmptyValue(value)) return '-';
const text = String(value).trim();
if (text.includes('~')) {
return text;
}
const numericValue = Number(text);
return Number.isNaN(numericValue) ? text : numericValue.toFixed(digits);
};
const formatDirectionList = (list: any[]) => {
if (!Array.isArray(list) || list.length === 0) return '-';
return list.map(item => {
const name = item?.flstnm || '-';
const count = isEmptyValue(item?.flfnct)
? '-'
: `${(Number(item.flfnct) / 10000).toFixed(4)}万尾`;
return `${name}${count}`;
});
};
// //
const tableColumns = ref([ const tableColumns = ref([
{ {
@ -97,11 +124,33 @@ const tableColumns = ref([
dataIndex: 'directionVoList', dataIndex: 'directionVoList',
width: 150, width: 150,
customRender: ({ text }: any) => { customRender: ({ text }: any) => {
if (text?.length > 0) { const lines = formatDirectionList(text);
return text[0].flstnm + '' + Number(text[0].flfnct) / 10000; if (lines === '-') return '-';
} else {
return '-'; return h(
} 'div',
{
style: {
whiteSpace: 'normal',
lineHeight: '22px',
wordBreak: 'break-all'
}
},
lines.map((line, index) =>
h(
'div',
{
key: `${line}-${index}`,
style: {
padding: '6px 0',
borderBottom:
index === lines.length - 1 ? 'none' : '1px solid #f0f0f0'
}
},
line
)
)
);
} }
}, },
{ {
@ -112,8 +161,7 @@ const tableColumns = ref([
dataIndex: 'fsz', dataIndex: 'fsz',
width: 150, width: 150,
customRender: ({ text }: any) => { customRender: ({ text }: any) => {
if (text == null || text === '-') return '-'; return formatRangeOrDecimal(text, 2);
return Number(text).toFixed(2);
} }
}, },
{ {
@ -121,8 +169,7 @@ const tableColumns = ref([
dataIndex: 'fwet', dataIndex: 'fwet',
width: 150, width: 150,
customRender: ({ text }: any) => { customRender: ({ text }: any) => {
if (text == null || text === '-') return '-'; return text == null || text === '-' ? '-' : text;
return Number(text).toFixed(2);
} }
} }
] ]

View File

@ -0,0 +1,307 @@
<template>
<div class="normal-data-monitoring2">
<div class="search-bar mb-4">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.tm"
picker="year"
style="width: 160px"
format="YYYY"
value-format="YYYY"
/>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</div>
<BasicTable
ref="tableRef"
:scrollY="500"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getTableList"
row-key="_rowKey"
:transform-data="transformTableData"
>
<template #fid="{ record }">
<a-button
type="link"
class="!p-0"
:disabled="!hasAttachment(record?.fid)"
@click="handleViewAttachment(record)"
>
查看附件
</a-button>
</template>
</BasicTable>
<CommonAttachmentModal
v-model:open="attachmentModalVisible"
:fid="currentAttachmentFid"
title="查看附件"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import {
getNormalDataMonitoring2List,
getNormalDataMonitoring2Year
} from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
import CommonAttachmentModal from './CommonAttachmentModal.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const DEFAULT_PAGE_SIZE = 20;
const hasLoaded = ref(false);
const tableRef = ref();
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
const searchParams = ref({
tm: null as string | null
});
const tableColumns = ref([
{
title: '植物园名称',
dataIndex: 'stnm',
width: 180
},
{
title: '年份',
dataIndex: 'tm',
width: 80
},
{
title: '保护种类',
dataIndex: 'tetp',
width: 120
},
{
title: '单位',
dataIndex: 'unit',
width: 120
},
{
title: '实际种植/种植要求',
dataIndex: 'surnum',
width: 160,
customRender: ({ record }: any) => {
const tecnt =
record?.tecnt !== undefined &&
record?.tecnt !== null &&
String(record.tecnt).trim() !== ''
? record.tecnt
: '-';
const transplant =
record?.transplant !== undefined &&
record?.transplant !== null &&
String(record.transplant).trim() !== ''
? record.transplant
: '-';
return `${tecnt}/${transplant}`;
}
},
{
title: '存活数量/移栽规模',
dataIndex: 'treeNum',
width: 160,
customRender: ({ record }: any) => {
const surnum =
record?.surnum !== undefined &&
record?.surnum !== null &&
String(record.surnum).trim() !== ''
? record.surnum
: '-';
const plantask =
record?.plantask !== undefined &&
record?.plantask !== null &&
String(record.plantask).trim() !== ''
? record.plantask
: '-';
return `${surnum}/${plantask}`;
}
},
{
title: '存活率(%)',
dataIndex: 'rate',
width: 120,
customRender: ({ text }: any) => {
if (
text === undefined ||
text === null ||
String(text).trim() === '' ||
Number.isNaN(Number(text))
) {
return '-';
}
return Number(text).toFixed(2);
}
},
{
title: '附件',
dataIndex: 'fid',
width: 120,
slots: { customRender: 'fid' }
}
]);
const tableScrollX = ref(300);
const getDefaultYear = (yearList: any[]) => {
const firstItem = yearList?.[0];
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
return String(firstItem);
}
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
};
const hasAttachment = (fid: unknown) =>
!!fid && String(fid).trim() !== '' && String(fid).trim() !== '-';
const handleViewAttachment = (record: any) => {
if (!hasAttachment(record?.fid)) {
message.warning('该记录无附件');
return;
}
currentAttachmentFid.value = String(record.fid);
attachmentModalVisible.value = true;
};
const transformTableData = (res: any) => {
const records = res?.data?.records || res?.data?.data || res?.data || [];
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
return {
records: records.map((item: any, index: number) => ({
...item,
_rowKey:
item?.id ||
item?.fid ||
`${item?.stcd || 'stcd'}-${
item?.tm || searchParams.value.tm || 'tm'
}-${index}`
})),
total
};
};
const getYearList = async () => {
if (!modelStore.params.stcd) return;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
}
};
const res: any = await getNormalDataMonitoring2Year(params);
const yearData = res?.data?.data || res?.data?.records || res?.data || [];
const defaultYear =
getDefaultYear(yearData) || String(new Date().getFullYear());
searchParams.value.tm = String(defaultYear);
refreshTable();
} catch (error) {
console.error('获取年份列表失败:', error);
if (tableRef.value) {
tableRef.value.loading = false;
}
}
};
const buildFilter = () => ({
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: [modelStore.params.stcd]
},
{
field: 'tm',
operator: 'eq',
dataType: 'string',
value: searchParams.value.tm
}
]
});
const getTableList = (params: any) => {
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
const currentPage = Number(params?.skip || 1);
const skip = Math.max(currentPage - 1, 0) * take;
return getNormalDataMonitoring2List({
...params,
take,
skip,
filter: params?.filter || buildFilter()
});
};
const refreshTable = () => {
if (!modelStore.params.stcd || !searchParams.value.tm) return;
tableRef.value?.getList(buildFilter());
};
const handleSearch = () => {
if (!searchParams.value.tm) {
message.warning('请选择年份');
return;
}
refreshTable();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
searchParams.value = { tm: null };
getYearList();
}
}
);
</script>
<style lang="scss" scoped>
.normal-data-monitoring2 {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@ -359,12 +359,12 @@ export const operationStepList: OperationStepItem[] = [
}, },
{ {
type: 'select', type: 'select',
name: 'fpitocintype', name: 'fishsrc',
label: '催产剂种类', label: '亲鱼来源',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue', url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get', method: 'get',
params: { params: {
dictCode: 'FPITOCINTYPE' dictCode: 'BSSR'
} }
} }
] ]

View File

@ -1550,9 +1550,9 @@ export const qinYuPeiYuTouWei = [
}, },
{ {
dataIndexRaw: '', dataIndexRaw: '',
key: 'ftype', key: 'ftypeName',
title: '饲料种类', title: '饲料种类',
dataIndex: 'ftype', dataIndex: 'ftypeName',
children: [], children: [],
visible: true, visible: true,
fixed: '', fixed: '',
@ -3868,9 +3868,9 @@ export const miaoZhongJiLu = [
}, },
{ {
dataIndexRaw: '', dataIndexRaw: '',
key: 'feedtp', key: 'ftypeName',
title: '饲料种类', title: '饲料种类',
dataIndex: 'feedtp', dataIndex: 'ftypeName',
children: [], children: [],
visible: true, visible: true,
fixed: '', fixed: '',

View File

@ -0,0 +1,710 @@
<template>
<div class="process-diagram">
<div class="top-panel">
<div class="stage-panel">
<div class="panel-header panel-inline">
<div class="panel-title w-[44px]">过程:</div>
<a-slider
class="stage-slider"
:min="0"
:max="stageList.length - 1"
:step="1"
:marks="sliderMarks"
:value="activeStageIndex"
:tooltip-open="false"
@change="handleStageClick"
/>
</div>
</div>
<div class="month-panel">
<div class="panel-title">月份:</div>
<a-range-picker
v-model:value="selectedRange"
class="month-picker"
:allow-clear="false"
picker="month"
format="YYYY-MM"
@change="handleRangeChange"
/>
</div>
</div>
<a-spin :spinning="isLoading" tip="加载中...">
<div class="content-body">
<div class="chart-wrapper">
<div ref="chartRef" class="chart-container"></div>
<a-empty
v-if="chartData.length === 0"
description="暂无数据"
class="chart-empty"
/>
</div>
<div class="table-wrapper">
<BasicTable
:scrollY="480"
:scrollX="tableScrollX"
:columns="tableColumns"
:data="tableData"
/>
</div>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch
} from 'vue';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import * as echarts from 'echarts';
import {
getProcessDiagramFishFry,
getProcessDiagramHatch,
getProcessDiagramParentFish
} from '@/api/mapModal';
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
type StageKey = 'parentFish' | 'spawning' | 'hatching' | 'fishFry';
interface StageItem {
key: StageKey;
label: string;
value: number;
fetcher: (params: any) => Promise<any>;
}
interface RemoteColumn {
title: string;
dataIndex: string;
key?: string;
visible?: boolean;
dataType?: string;
dataFormat?: string | null;
}
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const modelStore = useModelStore();
const stageList: StageItem[] = [
{
key: 'parentFish',
label: '亲鱼',
value: 0,
fetcher: params => getProcessDiagramParentFish(params)
},
{
key: 'spawning',
label: '产卵',
value: 1,
fetcher: params => getProcessDiagramHatch(params, 1)
},
{
key: 'hatching',
label: '孵化',
value: 2,
fetcher: params => getProcessDiagramHatch(params, 2)
},
{
key: 'fishFry',
label: '鱼苗',
value: 3,
fetcher: params => getProcessDiagramFishFry(params)
}
];
const sliderMarks = stageList.reduce<Record<number, string>>((result, item) => {
result[item.value] = item.label;
return result;
}, {});
const activeStageIndex = ref(0);
const createDefaultRange = () =>
[dayjs().startOf('month'), dayjs().startOf('month')] as [Dayjs, Dayjs];
const selectedRange = ref<[Dayjs, Dayjs]>(createDefaultRange());
const isLoading = ref(false);
const tableData = ref<Record<string, any>[]>([]);
const chartData = ref<Record<string, any>[]>([]);
const tableColumns = ref<any[]>([]);
const tableScrollX = ref(0);
const chartRef = ref<HTMLElement>();
let chartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
let currentRequestId = 0;
const currentStage = computed(
() =>
stageList.find(item => item.value === activeStageIndex.value) ||
stageList[0]
);
const colorList = [
'#1677ff',
'#52c41a',
'#fa8c16',
'#722ed1',
'#13c2c2',
'#eb2f96',
'#2f54eb',
'#fa541c'
];
const clearData = () => {
tableData.value = [];
chartData.value = [];
tableColumns.value = [];
tableScrollX.value = 0;
if (chartInstance) {
chartInstance.clear();
}
};
const buildParams = () => ({
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: selectedRange.value[0]
.startOf('month')
.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: selectedRange.value[1]
.endOf('month')
.format('YYYY-MM-DD HH:mm:ss')
}
]
},
sort: [{ field: 'dt', dir: 'desc' }]
});
const normalizeResult = (response: any) => {
const data = response?.data?.data;
if (Array.isArray(data)) {
return data[0] || {};
}
return data || response?.data || {};
};
const isValidDateValue = (value: unknown) => dayjs(value as any).isValid();
const isNumericValue = (value: unknown) => {
if (value == null || value === '') return false;
if (typeof value === 'number') return Number.isFinite(value);
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) return false;
return !Number.isNaN(Number(trimmed));
}
return false;
};
const guessTimeField = (
columns: RemoteColumn[],
dataSource: Record<string, any>[]
) => {
const priorityColumns = columns.find(column =>
['dt', 'tm', 'date', 'time'].includes(
String(column.dataIndex).toLowerCase()
)
);
if (priorityColumns) {
return priorityColumns.dataIndex;
}
const dateTypeColumn = columns.find(
column => column.dataType === 'date' || column.dataType === 'datetime'
);
if (dateTypeColumn) {
return dateTypeColumn.dataIndex;
}
const firstRow = dataSource[0];
if (!firstRow) return '';
return (
Object.keys(firstRow).find(key => {
const lowerKey = key.toLowerCase();
return (
['dt', 'tm', 'date', 'time'].includes(lowerKey) ||
isValidDateValue(firstRow[key])
);
}) || ''
);
};
const resolveChartColumns = (
columns: RemoteColumn[],
dataSource: Record<string, any>[],
timeField: string
) => {
return columns.filter(column => {
if (column.visible === false || column.dataIndex === timeField)
return false;
if (['date', 'datetime'].includes(String(column.dataType).toLowerCase())) {
return false;
}
return dataSource.some(row => isNumericValue(row[column.dataIndex]));
});
};
const formatAxisLabel = (value: unknown) => {
const parsed = dayjs(value as any);
if (!parsed.isValid()) return String(value ?? '');
return parsed.format('YYYY-MM');
};
const formatTooltipTime = (value: unknown) => {
const parsed = dayjs(value as any);
if (!parsed.isValid()) return String(value ?? '');
return parsed.format('YYYY-MM');
};
const getSeriesName = (column: RemoteColumn) =>
column.title || column.key || column.dataIndex;
const aggregateChartDataByMonth = (
dataSource: Record<string, any>[],
timeField: string,
metricColumns: RemoteColumn[]
) => {
const groupedMap = new Map<string, Record<string, any>>();
dataSource.forEach(item => {
const monthKey = formatAxisLabel(item[timeField]);
const current = groupedMap.get(monthKey) || { [timeField]: monthKey };
metricColumns.forEach(column => {
const rawValue = item[column.dataIndex];
const nextValue = isNumericValue(rawValue) ? Number(rawValue) : 0;
current[column.dataIndex] =
Number(current[column.dataIndex] || 0) + nextValue;
});
groupedMap.set(monthKey, current);
});
return Array.from(groupedMap.entries())
.sort((a, b) => dayjs(a[0]).valueOf() - dayjs(b[0]).valueOf())
.map(([, value]) => value);
};
const normalizeDateFormat = (format?: string | null) => {
if (!format) return 'YYYY-MM-DD HH:mm:ss';
return format
.replace(/yyyy/g, 'YYYY')
.replace(/dd/g, 'DD')
.replace(/hh/g, 'HH');
};
const formatTableValue = (value: unknown, column: RemoteColumn) => {
if (value == null || value === '') return '-';
if (column.dataType === 'date' || column.dataType === 'datetime') {
const parsed = dayjs(value as any);
if (!parsed.isValid()) return String(value);
return parsed.format(normalizeDateFormat(column.dataFormat));
}
return value;
};
const buildTableColumns = (columns: RemoteColumn[]) => {
const visibleColumns = columns.filter(column => column.visible !== false);
tableColumns.value = visibleColumns.map(column => ({
title: column.title,
dataIndex: column.dataIndex,
key: column.key || column.dataIndex,
width: column.dataType === 'date' ? 120 : 180,
customRender: ({ record, text }: any) => {
const cellValue =
record && column.dataIndex in record ? record[column.dataIndex] : text;
return formatTableValue(cellValue, column);
}
}));
tableScrollX.value = tableColumns.value.reduce(
(total, item) => total + (item.width || 120),
0
);
};
const updateChart = (
dataSource: Record<string, any>[],
columns: RemoteColumn[]
) => {
if (!chartInstance) return;
if (!dataSource.length || !columns.length) {
chartInstance.clear();
return;
}
const timeField = guessTimeField(columns, dataSource);
if (!timeField) {
chartInstance.clear();
return;
}
const metricColumns = resolveChartColumns(columns, dataSource, timeField);
if (!metricColumns.length) {
chartInstance.clear();
return;
}
const chartRows = aggregateChartDataByMonth(
dataSource,
timeField,
metricColumns
);
const option = {
color: colorList,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
formatter: (params: any[]) => {
if (!params?.length) return '';
const index = params[0].dataIndex;
let total = 0;
let html = `<div style="margin-bottom:8px;">${formatTooltipTime(
chartRows[index]?.[timeField]
)}</div>`;
params.forEach(param => {
if (param.value == null || param.value === '') return;
total += Number(param.value) || 0;
html += `
<div style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${param.color};"></span>
<span>${param.seriesName}${param.value}</span>
</div>
`;
});
html += `
<div style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.2);font-weight:600;">
总计${total}
</div>
`;
return html;
}
},
legend: {
type: 'scroll',
width: '80%',
right: 60,
top: 10,
data: metricColumns.map(getSeriesName)
},
grid: {
left: 70,
right: 30,
top: 70,
bottom: 60
},
xAxis: {
type: 'category',
data: chartRows.map(item => item[timeField]),
axisTick: { show: false }
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: { color: '#f0f0f0' }
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
}
],
toolbox: {
show: true,
right: 20,
feature: {
saveAsImage: { title: '保存为图片' }
}
},
series: metricColumns.map((column, index) => ({
name: getSeriesName(column),
type: 'bar',
stack: 'total',
barMaxWidth: 24,
itemStyle: {
color: colorList[index % colorList.length]
},
data: chartRows.map(item => {
const value = item[column.dataIndex];
return isNumericValue(value) ? Number(value) : 0;
})
}))
};
chartInstance.setOption(option, true);
};
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(chartRef.value);
};
const ensureChartReady = async () => {
await nextTick();
if (!chartInstance) {
initChart();
}
if (!resizeObserver && chartRef.value) {
resizeObserver = new ResizeObserver(() => {
chartInstance?.resize();
});
resizeObserver.observe(chartRef.value);
}
};
const fetchData = async () => {
if (!modelStore.params.stcd) {
clearData();
return;
}
const requestId = ++currentRequestId;
isLoading.value = true;
try {
const response = await currentStage.value.fetcher(buildParams());
if (requestId !== currentRequestId) return;
const result = normalizeResult(response);
const columns = Array.isArray(result.columns) ? result.columns : [];
const dataSource = Array.isArray(result.dataSource)
? result.dataSource
: [];
tableData.value = dataSource;
chartData.value = dataSource;
buildTableColumns(columns);
await ensureChartReady();
updateChart(dataSource, columns);
} catch (error) {
if (requestId !== currentRequestId) return;
clearData();
console.error('获取过程图数据失败:', error);
} finally {
if (requestId === currentRequestId) {
isLoading.value = false;
}
}
};
const handleStageClick = (value: number) => {
if (value === activeStageIndex.value) return;
activeStageIndex.value = value;
if (props.isActive) {
fetchData();
}
};
const handleRangeChange = (value: [Dayjs, Dayjs] | null) => {
if (!value || value.length !== 2 || !value[0] || !value[1]) {
selectedRange.value = createDefaultRange();
}
if (props.isActive) {
fetchData();
}
};
const handleResize = () => {
chartInstance?.resize();
};
watch(
() => props.isActive,
async active => {
if (!active) return;
await ensureChartReady();
fetchData();
setTimeout(() => {
chartInstance?.resize();
}, 200);
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newValue, oldValue) => {
if (!props.isActive || !newValue || newValue === oldValue) return;
fetchData();
}
);
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
currentRequestId += 1;
window.removeEventListener('resize', handleResize);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
.process-diagram {
width: 100%;
min-height: 640px;
.top-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-bottom: 16px;
padding: 16px 20px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #262626;
flex-shrink: 0;
}
.stage-panel {
width: 50%;
min-width: 0;
}
.panel-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.panel-inline {
gap: 12px;
margin-bottom: 0;
}
.stage-slider {
width: 100%;
}
:deep(.ant-slider) {
.ant-slider-mark-text {
margin-top: 10px;
}
.ant-slider-handle {
background-color: rgb(18, 92, 223);
}
.ant-slider-track {
background-color: rgb(18, 92, 223);
}
.ant-slider-dot {
border-color: rgb(18, 92, 223);
}
.ant-slider-mark {
.ant-slider-mark-text-active:not(:has(~ .ant-slider-mark-text-active)) {
color: rgb(18, 92, 223);
font-size: 15px;
font-weight: 600;
}
}
.ant-slider-handle {
&::after {
box-shadow: 0 0 0 2px #125cdf;
background-color: rgb(18, 92, 223);
}
}
}
.month-panel {
display: flex;
justify-content: end;
align-items: center;
width: 50%;
min-width: 0;
gap: 12px;
}
.month-picker {
width: 280px;
}
.content-body {
display: flex;
height: 500px;
}
.chart-wrapper {
position: relative;
flex: 1;
padding-top: 10px;
}
.chart-container {
width: 100%;
height: 100%;
}
.chart-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.table-wrapper {
flex: 1;
min-width: 500px;
margin-left: 10px;
padding: 10px;
overflow: hidden;
overflow-x: auto;
border-left: 1px solid #dcdfe6;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,435 @@
<template>
<div class="live-footage">
<div class="search-bar mb-4">
<span class="mr-2">时间</span>
<a-spin :spinning="monthLoading" size="small" class="mr-4">
<a-range-picker
v-model:value="selectedMonthRange"
picker="month"
style="width: 260px"
format="YYYY-MM"
value-format="YYYY-MM"
:allowClear="false"
:disabled="monthLoading"
/>
</a-spin>
<a-button
class="ml-2"
type="primary"
:loading="monthLoading && !loading"
@click="handleSearch"
>
查询
</a-button>
</div>
<a-spin :spinning="loading" class="video-mode-spin">
<div v-if="mediaList.length > 0" class="video-mode">
<div class="viewer-spin">
<div class="video-main">
<div v-if="activeMedia.src" class="media-display">
<LiveVideoBox
:videoData="{ url: activeMedia.src }"
height="100%"
:showTitle="false"
/>
</div>
<div v-else class="media-empty">
<a-empty description="请选择右侧数据" />
</div>
</div>
</div>
<div class="sidebar-spin">
<div class="video-sidebar">
<div class="sidebar-list">
<div
v-for="item in mediaList"
:key="item._itemKey"
class="sidebar-item"
:class="{ active: activeMediaKey === item._itemKey }"
@click="handleSelectMedia(item)"
>
<div class="item-thumb">
<img v-if="item.thumbUrl" :src="item.thumbUrl" />
<div v-else class="thumb-placeholder">
<img :src="videoCover" />
</div>
</div>
<div class="item-text">
{{ item.flnm }}
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="shared-empty">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
import { getLiveFootageList, getLiveFootageYear } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import videoCover from '@/assets/images/videFm.jpg';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
type MonthRange = [string, string];
const hasLoaded = ref(false);
const loading = ref(false);
const monthLoading = ref(false);
const selectedMonthRange = ref<MonthRange>([
dayjs().format('YYYY-MM'),
dayjs().format('YYYY-MM')
]);
const mediaList = ref<any[]>([]);
const activeMediaKey = ref('');
const activeMedia = ref<{ src: string }>({ src: '' });
const attachmentBaseUrl = `${import.meta.env.VITE_APP_ATTACHMENT_URL}/?`;
const getMonthRange = (monthRange: MonthRange) => ({
startDate: dayjs(monthRange[0], 'YYYY-MM')
.startOf('month')
.format('YYYY-MM-DD HH:mm:ss'),
endDate: dayjs(monthRange[1], 'YYYY-MM')
.endOf('month')
.format('YYYY-MM-DD HH:mm:ss')
});
const getCurrentMonthRange = (): MonthRange => {
const currentMonth = dayjs().format('YYYY-MM');
return [currentMonth, currentMonth];
};
const buildAttachmentUrl = (path: string) => {
const value = String(path || '').trim();
if (!value) return '';
return `${attachmentBaseUrl}${value}`;
};
const resetActiveMedia = () => {
activeMediaKey.value = '';
activeMedia.value = { src: '' };
};
const handleSelectMedia = (item: any) => {
activeMediaKey.value = item._itemKey;
activeMedia.value = { src: item.mediaUrl || '' };
};
const buildFilter = () => {
const stcd = modelStore.params.stcd;
const { startDate, endDate } = getMonthRange(selectedMonthRange.value);
return {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: endDate
}
]
};
};
const buildMediaList = async (rawData: any[]) => {
return rawData
.filter((item: any) => String(item?.flpth || '').trim() !== '')
.map((item: any, index: number) => {
const formattedTm = item.tm
? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss')
: '-';
return {
...item,
tm: formattedTm,
thumbUrl: buildAttachmentUrl(item?.imgPath),
mediaUrl: buildAttachmentUrl(item?.flpth),
_itemKey: `${item?.flpth}-${index}`
};
});
};
const loadDefaultMonthRange = async () => {
if (!modelStore.params.stcd) {
selectedMonthRange.value = getCurrentMonthRange();
return;
}
monthLoading.value = true;
try {
const res: any = await getLiveFootageYear({
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
}
});
const records = res?.data?.records || res?.data?.data || res?.data || [];
const defaultTm = records?.[0]?.tm;
if (defaultTm) {
const currentMonth = dayjs(defaultTm).format('YYYY-MM');
const previousMonth = dayjs(defaultTm)
.subtract(1, 'month')
.format('YYYY-MM');
selectedMonthRange.value = [previousMonth, currentMonth];
return;
}
} catch (error) {
console.error('获取现场录像默认月份失败:', error);
} finally {
monthLoading.value = false;
}
if (!selectedMonthRange.value?.[0] || !selectedMonthRange.value?.[1]) {
selectedMonthRange.value = getCurrentMonthRange();
}
};
const fetchData = async () => {
if (
!modelStore.params.stcd ||
!selectedMonthRange.value?.[0] ||
!selectedMonthRange.value?.[1]
) {
return;
}
loading.value = true;
try {
resetActiveMedia();
const res: any = await getLiveFootageList({
filter: buildFilter()
});
const rawData = res?.data?.records || res?.data?.data || res?.data || [];
mediaList.value = await buildMediaList(rawData);
if (mediaList.value.length > 0) {
handleSelectMedia(mediaList.value[0]);
}
} catch (error) {
console.error('获取现场录像失败:', error);
mediaList.value = [];
} finally {
loading.value = false;
}
};
const initData = async () => {
await loadDefaultMonthRange();
await fetchData();
};
const handleSearch = () => {
if (!selectedMonthRange.value?.[0] || !selectedMonthRange.value?.[1]) {
message.warning('请选择时间范围');
return;
}
fetchData();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
initData();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd && props.isActive) {
initData();
}
}
);
</script>
<style lang="scss" scoped>
.live-footage {
width: 100%;
.search-bar {
display: flex;
align-items: center;
}
.video-mode-spin {
display: block;
height: 600px;
}
.video-mode {
display: flex;
gap: 12px;
height: 600px;
}
.viewer-spin {
width: 100%;
flex: 1;
height: 100%;
}
.viewer-spin :deep(.ant-spin-container),
.sidebar-spin :deep(.ant-spin-container) {
height: 100%;
}
.sidebar-spin {
width: 320px;
height: 100%;
flex: 0 0 320px;
}
.video-main {
width: 100%;
height: 100%;
background: #000;
border-radius: 4px;
overflow: hidden;
.media-display {
width: 100%;
height: 100%;
}
.media-empty {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #fff;
}
}
.video-sidebar {
display: flex;
flex-direction: column;
height: 100%;
padding: 8px;
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.sidebar-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.sidebar-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
margin-bottom: 8px;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover,
&.active {
background: #f5f5f5;
}
&.active {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
}
}
.item-thumb {
width: 100%;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
background: #fafafa;
overflow: hidden;
border-radius: 4px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.thumb-placeholder,
.no-media {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.no-media {
color: #999;
font-size: 12px;
}
.item-text {
line-height: 1.6;
word-break: break-all;
text-align: center;
}
.sidebar-item.active .item-text {
color: #1677ff;
font-weight: 500;
}
.shared-empty {
height: 600px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,404 @@
<template>
<div class="on-site-photo">
<div class="search-bar mb-4">
<span class="mr-2">时间</span>
<a-spin :spinning="monthLoading" size="small" class="mr-4">
<a-range-picker
v-model:value="selectedMonthRange"
picker="month"
style="width: 260px"
format="YYYY-MM"
value-format="YYYY-MM"
:allowClear="false"
:disabled="monthLoading"
/>
</a-spin>
<a-button
class="ml-2"
type="primary"
:loading="monthLoading && !loading"
@click="handleSearch"
>
查询
</a-button>
</div>
<a-spin :spinning="loading" class="photo-spin">
<div v-if="photoList.length > 0" class="photo-carousel-wrapper">
<a-button
v-show="photoList.length > 1"
class="carousel-arrow carousel-arrow-left"
@click="handlePrev"
>
<span><left-circle-outlined /></span>
</a-button>
<a-carousel ref="carouselRef">
<div
v-for="item in photoList"
:key="item._itemKey"
class="carousel-item"
>
<div class="image-container">
<img :src="item.imageUrl" :alt="item.flnm || '现场图片'" />
</div>
</div>
</a-carousel>
<a-button
v-show="photoList.length > 1"
class="carousel-arrow carousel-arrow-right"
@click="handleNext"
>
<span><right-circle-outlined /></span>
</a-button>
</div>
<div v-else class="shared-empty">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
import {
getLiveFootageList,
getLiveFootageYear,
postIdUrl
} from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
type MonthRange = [string, string];
const hasLoaded = ref(false);
const loading = ref(false);
const monthLoading = ref(false);
const selectedMonthRange = ref<MonthRange>([
dayjs().format('YYYY-MM'),
dayjs().format('YYYY-MM')
]);
const photoList = ref<any[]>([]);
const carouselRef = ref();
const getMonthRange = (monthRange: MonthRange) => ({
startDate: dayjs(monthRange[0], 'YYYY-MM')
.startOf('month')
.format('YYYY-MM-DD HH:mm:ss'),
endDate: dayjs(monthRange[1], 'YYYY-MM')
.endOf('month')
.format('YYYY-MM-DD HH:mm:ss')
});
const getCurrentMonthRange = (): MonthRange => {
const currentMonth = dayjs().format('YYYY-MM');
return [currentMonth, currentMonth];
};
const isImageExt = (ext: string) =>
['.jpg', '.jpeg', '.png', '.webp', '.gif'].includes(
String(ext || '').toLowerCase()
);
const buildFilter = () => {
const stcd = modelStore.params.stcd;
const { startDate, endDate } = getMonthRange(selectedMonthRange.value);
return {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: endDate
}
]
};
};
const buildPhotoList = async (rawData: any[]) => {
const rowsWithFid = rawData.filter(
(item: any) => !!String(item?.fid || '').trim()
);
const allFids: string[] = [];
rowsWithFid.forEach((item: any) => {
String(item.fid)
.split(',')
.map((fid: string) => fid.trim())
.filter(Boolean)
.forEach((fid: string) => allFids.push(fid));
});
if (allFids.length === 0) {
return [];
}
const formData = new FormData();
formData.append('id', allFids.join(','));
const mediaRes: any = await postIdUrl(formData);
const mediaMap = mediaRes?.data || {};
const list: any[] = [];
rowsWithFid.forEach((item: any, index: number) => {
const formattedTm = item.tm
? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss')
: '-';
String(item.fid)
.split(',')
.map((fid: string) => fid.trim())
.filter(Boolean)
.forEach((fid: string, fidIndex: number) => {
const media = mediaMap[fid];
if (!media?.fullpath || !isImageExt(media?.ext)) return;
list.push({
...item,
fid,
tm: formattedTm,
imageUrl: media.fullpath,
_itemKey: `${fid}-${index}-${fidIndex}`
});
});
});
return list;
};
const loadDefaultMonthRange = async () => {
if (!modelStore.params.stcd) {
selectedMonthRange.value = getCurrentMonthRange();
return;
}
monthLoading.value = true;
try {
const res: any = await getLiveFootageYear({
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
}
});
const records = res?.data?.records || res?.data?.data || res?.data || [];
const defaultTm = records?.[0]?.tm;
if (defaultTm) {
const currentMonth = dayjs(defaultTm).format('YYYY-MM');
const previousMonth = dayjs(defaultTm)
.subtract(1, 'month')
.format('YYYY-MM');
selectedMonthRange.value = [previousMonth, currentMonth];
return;
}
} catch (error) {
console.error('获取现场图片默认月份失败:', error);
} finally {
monthLoading.value = false;
}
if (!selectedMonthRange.value?.[0] || !selectedMonthRange.value?.[1]) {
selectedMonthRange.value = getCurrentMonthRange();
}
};
const fetchData = async () => {
if (
!modelStore.params.stcd ||
!selectedMonthRange.value?.[0] ||
!selectedMonthRange.value?.[1]
) {
return;
}
loading.value = true;
try {
const res: any = await getLiveFootageList({
filter: buildFilter()
});
const rawData = res?.data?.records || res?.data?.data || res?.data || [];
photoList.value = await buildPhotoList(rawData);
} catch (error) {
console.error('获取现场图片失败:', error);
photoList.value = [];
} finally {
loading.value = false;
}
};
const initData = async () => {
await loadDefaultMonthRange();
await fetchData();
};
const handleSearch = () => {
if (!selectedMonthRange.value?.[0] || !selectedMonthRange.value?.[1]) {
message.warning('请选择时间范围');
return;
}
fetchData();
};
const handlePrev = () => {
carouselRef.value?.prev();
};
const handleNext = () => {
carouselRef.value?.next();
};
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
initData();
hasLoaded.value = true;
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd && props.isActive) {
initData();
}
}
);
</script>
<style lang="scss" scoped>
.on-site-photo {
width: 100%;
.search-bar {
display: flex;
align-items: center;
}
.photo-spin {
display: block;
height: 600px;
}
.photo-spin :deep(.ant-spin-container) {
height: 100%;
}
.photo-carousel-wrapper {
position: relative;
height: 100%;
padding: 0 48px;
}
.carousel-arrow {
position: absolute;
top: 50%;
z-index: 2;
width: 36px;
height: 36px;
border-radius: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
border: none;
color: #fff;
font-size: 20px;
&:hover,
&:focus {
color: #fff;
}
&:disabled {
background: rgba(0, 0, 0, 0.25);
color: rgba(255, 255, 255, 0.6);
cursor: not-allowed;
}
}
.carousel-arrow-left {
left: 4px;
}
.carousel-arrow-right {
right: 4px;
}
:deep(.ant-carousel),
:deep(.slick-slider),
:deep(.slick-list),
:deep(.slick-track) {
height: 100%;
}
:deep(.slick-dots li button) {
background: rgba(0, 0, 0, 0.3);
}
:deep(.slick-dots li.slick-active button) {
background: rgba(0, 0, 0, 0.6);
}
.carousel-item {
height: 600px;
}
.image-container {
width: 100%;
height: 600px;
display: flex !important;
align-items: center;
justify-content: center;
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.shared-empty {
height: 600px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
}
}
</style>

View File

@ -20,7 +20,13 @@
8. 栖息地-流量监测 没有水位视频和流量视频 字段没数据 8. 栖息地-流量监测 没有水位视频和流量视频 字段没数据
9. 栖息地 水温水质流量没有数据没法测 9. 栖息地 水温水质流量没有数据没法测
10.鱼类增殖站 - 运行数据 要添加字典 10.鱼类增殖站 - 运行数据 要添加字典
11.珍惜植物园 - 种植要求字段不知道
12.水生调查断面 - 监测数据 不知道电导率 性腺发育期早期资源量和种类
13.水电告警情况 生态流量 不知道字段 接口传参有问题
没弄 环保设施
--> -->
<div v-if="modelStore.showStcdSelector" class="stcd-selector-wrapper"> <div v-if="modelStore.showStcdSelector" class="stcd-selector-wrapper">
<a-select <a-select
@ -123,6 +129,12 @@
" "
:wq-show="modelStore.wqShow" :wq-show="modelStore.wqShow"
/> />
<!-- 监测数据 - 水电站告警情况 -->
<DZGJ
v-show="currentActiveKey === 'DZGJ'"
:is-active="currentActiveKey === 'DZGJ'"
:tabs-items="getTabChildren('DZGJ')"
/>
<!-- 流量监测 --> <!-- 流量监测 -->
<FlowMeasure <FlowMeasure
v-show="currentActiveKey === 'FlowMeasure'" v-show="currentActiveKey === 'FlowMeasure'"
@ -171,12 +183,16 @@
v-show="currentActiveKey === 'WaterTemperatureRep'" v-show="currentActiveKey === 'WaterTemperatureRep'"
:is-active="currentActiveKey === 'WaterTemperatureRep'" :is-active="currentActiveKey === 'WaterTemperatureRep'"
/> />
<!-- 鱼类增殖站 - 运行数据 --> <!-- 鱼类增殖站 - 运行数据 -->
<NormalOperationData <NormalOperationData
v-show="currentActiveKey === 'NormalOperationData'" v-show="currentActiveKey === 'NormalOperationData'"
:is-active="currentActiveKey === 'NormalOperationData'" :is-active="currentActiveKey === 'NormalOperationData'"
/> />
<!-- 过程图 -->
<ProcessDiagram
v-show="currentActiveKey === 'ProcessDiagram'"
:is-active="currentActiveKey === 'ProcessDiagram'"
/>
<!-- 增值放流情况 --> <!-- 增值放流情况 -->
<NormalAddedSituation <NormalAddedSituation
v-show="currentActiveKey === 'NormalAddedSituation'" v-show="currentActiveKey === 'NormalAddedSituation'"
@ -187,6 +203,31 @@
v-show="currentActiveKey === 'NormalResearchSituation'" v-show="currentActiveKey === 'NormalResearchSituation'"
:is-active="currentActiveKey === 'NormalResearchSituation'" :is-active="currentActiveKey === 'NormalResearchSituation'"
/> />
<!-- 珍稀植物园 - 运行数据 -->
<NormalDataMonitoring2
v-show="currentActiveKey === 'NormalDataMonitoring2'"
:is-active="currentActiveKey === 'NormalDataMonitoring2'"
/>
<!-- 动物救助站 - 监测数据 -->
<Normal
v-show="currentActiveKey === 'Normal'"
:is-active="currentActiveKey === 'Normal'"
/>
<!-- 水生生态调查断面 - 监测数据 -->
<Ecology
v-show="currentActiveKey === 'Ecology'"
:is-active="currentActiveKey === 'Ecology'"
/>
<!-- 野生动物检测 -现场录像 -->
<liveFootage
v-show="currentActiveKey === 'liveFootage'"
:is-active="currentActiveKey === 'liveFootage'"
/>
<!-- 野生动物检测 -现场照片 -->
<onSitePhoto
v-show="currentActiveKey === 'onSitePhoto'"
:is-active="currentActiveKey === 'onSitePhoto'"
/>
<!-- 查看报告 --> <!-- 查看报告 -->
<Attachment <Attachment
v-show="currentActiveKey === 'attachment'" v-show="currentActiveKey === 'attachment'"
@ -225,8 +266,20 @@ import FishFacilityMonitorData from './components/FishFacilityMonitorData.vue';
import FishFacilityRunState from './components/FishFacilityRunState.vue'; // import FishFacilityRunState from './components/FishFacilityRunState.vue'; //
// //
import NormalOperationData from './components/NormalOperationData/index.vue'; // import NormalOperationData from './components/NormalOperationData/index.vue'; //
import ProcessDiagram from './components/ProcessDiagram.vue'; //
import NormalAddedSituation from './components/NormalAddedSituation.vue'; // import NormalAddedSituation from './components/NormalAddedSituation.vue'; //
import NormalResearchSituation from './components/NormalResearchSituation.vue'; // import NormalResearchSituation from './components/NormalResearchSituation.vue'; //
//
import NormalDataMonitoring2 from './components/NormalDataMonitoring2.vue'; //
// -
import liveFootage from './components/liveFootage.vue'; //
import onSitePhoto from './components/onSitePhoto.vue'; //
///
import Normal from './components/Normal.vue'; //
//
import Ecology from './components/Ecology.vue'; //
//
import DZGJ from './components/DZGJ.vue'; //
import FishZHFX from './components/FishZHFX.vue'; // import FishZHFX from './components/FishZHFX.vue'; //
import EcologicalFlow from './components/EcologicalFlow.vue'; // import EcologicalFlow from './components/EcologicalFlow.vue'; //
@ -294,8 +347,7 @@ const checkPowerStationTopicData = async (stcd: string) => {
] ]
} }
}); });
hasPowerStatData.value = hasPowerStatData.value = res?.data?.data && res?.data?.data?.length > 0;
res?.data?.data && res?.data?.data?.length > 0;
} catch (error) { } catch (error) {
console.error('查询电站专题配置失败:', error); console.error('查询电站专题配置失败:', error);
hasPowerStatData.value = false; hasPowerStatData.value = false;
@ -605,10 +657,10 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
] ]
}; };
const res2 = await getVideoList(params); const res2 = await getVideoList(params);
console.log('', modelStore.params.sttp); console.log('', modelStore.params.sttp);
debugger
if ( if (
(res2?.data?.data?.length > 0 || res2?.data?.records?.length > 0) || res2?.data?.data?.length > 0 ||
res2?.data?.records?.length > 0 ||
modelStore.params.sttp == 'WQ' modelStore.params.sttp == 'WQ'
) { ) {
tabsConfig.value = newTabs; tabsConfig.value = newTabs;

View File

@ -147,11 +147,16 @@ export class MapOl implements MapInterface {
this.popupManager.showPopup(undefined, undefined); this.popupManager.showPopup(undefined, undefined);
this.popupManager.handleMapClick(evt.pixel, detectedFeature => { this.popupManager.handleMapClick(evt.pixel, detectedFeature => {
console.log(detectedFeature); console.log(detectedFeature);
modelStore.modalVisible = true; if (detectedFeature.values_.sttpMap == 'ylfb') {
modelStore.params = detectedFeature.values_; modelStore.ylfbModalVisible = true;
modelStore.title = modelStore.params = detectedFeature.values_;
detectedFeature.values_.titleName || } else {
detectedFeature.values_.stnm + ' 详情信息'; modelStore.modalVisible = true;
modelStore.params = detectedFeature.values_;
modelStore.title =
detectedFeature.values_.titleName ||
detectedFeature.values_.stnm + ' 详情信息';
}
}); });
}); });
@ -242,6 +247,12 @@ export class MapOl implements MapInterface {
} }
const dynamicScale = this.getDynamicPointScale(currentZoom); const dynamicScale = this.getDynamicPointScale(currentZoom);
const fontSize = this.getPointFontSize(dynamicScale); const fontSize = this.getPointFontSize(dynamicScale);
const formattedLabelText = this.formatPointLabelText(labelText);
const labelLineCount = formattedLabelText
? formattedLabelText.split('\n').length
: 1;
const labelOffsetY =
labelLineCount > 1 ? -30 * dynamicScale : -22 * dynamicScale;
const styleOptions: any = { const styleOptions: any = {
image: new Icon({ image: new Icon({
@ -254,8 +265,8 @@ export class MapOl implements MapInterface {
}; };
styleOptions.text = new Text({ styleOptions.text = new Text({
text: labelText, text: formattedLabelText,
offsetY: -20 * dynamicScale, offsetY: labelOffsetY,
font: `${fontSize}px sans-serif`, font: `${fontSize}px sans-serif`,
fill: new Fill({ color: '#fff' }), fill: new Fill({ color: '#fff' }),
stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }), stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }),
@ -265,6 +276,34 @@ export class MapOl implements MapInterface {
return new Style(styleOptions); return new Style(styleOptions);
} }
/**
* 12
*/
private formatPointLabelText(text: string): string {
const normalizedText = String(text || '')
.replace(/[()]/g, '')
.trim();
if (!normalizedText) {
return '';
}
const maxLineLength = 12;
if (normalizedText.length <= maxLineLength) {
return normalizedText;
}
const firstLine = normalizedText.slice(0, maxLineLength);
if (normalizedText.length <= maxLineLength * 2) {
return `${firstLine}\n${normalizedText.slice(maxLineLength)}`;
}
const secondLine = `${normalizedText.slice(
maxLineLength,
maxLineLength + 9
)}...`;
return `${firstLine}\n${secondLine}`;
}
/** /**
* *
*/ */

View File

@ -269,10 +269,12 @@ export class PointLayerManager {
...item, ...item,
_layerKey: targetLayerKey _layerKey: targetLayerKey
}); });
feature.setId(item.stcd || item._id); feature.setId(item.stcd || item._id);
feature.set('_iconUrl', iconUrl); feature.set('_iconUrl', iconUrl);
feature.set('_labelText', titleName || stnm || ennm || ''); feature.set(
'_labelText',
item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || ''
);
feature.set('_legendVisible', true); feature.set('_legendVisible', true);
feature.set('_sttpMap', item.sttpMap); feature.set('_sttpMap', item.sttpMap);
if (item.popupHtml) { if (item.popupHtml) {

View File

@ -0,0 +1,284 @@
<template>
<a-modal
:open="visible"
title="鱼类分布明细"
width="80vw"
:footer="null"
:closable="true"
@cancel="handleClose"
:destroyOnClose="true"
class="ylfb-modal"
>
<div class="ylfb-content">
<div class="search-bar mb-4">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.tm"
picker="year"
style="width: 160px"
format="YYYY"
value-format="YYYY"
/>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</div>
<BasicTable
ref="tableRef"
:scrollY="500"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getTableList"
row-key="_rowKey"
:transform-data="transformTableData"
/>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getFishDistributionList,
getFishDistributionYear
} from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const DEFAULT_PAGE_SIZE = 20;
const hasLoaded = ref(false);
const tableRef = ref();
const searchParams = ref({
tm: null as string | null
});
const tableColumns = ref([
{
title: '调查日期',
dataIndex: 'tm'
},
{
title: '科属',
dataIndex: 'family',
customRender: ({ record }: any) => {
const family =
record?.family !== undefined &&
record?.family !== null &&
String(record.family).trim() !== '' &&
String(record.family).trim() !== '-'
? String(record.family).trim()
: '';
const genus =
record?.genus !== undefined &&
record?.genus !== null &&
String(record.genus).trim() !== '' &&
String(record.genus).trim() !== '-'
? String(record.genus).trim()
: '';
if (!family && !genus) {
return '-';
}
if (!family) {
return genus;
}
if (!genus) {
return family;
}
return `${family}-${genus}`;
}
},
{
title: '渔获物种类',
dataIndex: 'name'
},
{
title: '鱼类规格(cm)',
dataIndex: 'fsz'
},
{
title: '渔获物数量(尾)',
dataIndex: 'fcnt'
},
{
title: '渔获物重量(kg)',
dataIndex: 'fwgh'
}
]);
const tableScrollX = ref(800);
const getDefaultYear = (yearList: any[]) => {
const firstItem = yearList?.[0];
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
return String(firstItem);
}
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
};
const getYearRange = (year: string) => ({
startDate: `${year}-01-01 00:00:00`,
endDate: `${year}-12-31 23:59:59`
});
const buildFilter = () => {
const filters: any[] = [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: getYearRange(String(searchParams.value.tm)).endDate
}
];
return {
logic: 'and',
filters
};
};
const transformTableData = (res: any) => {
const records = res?.data?.records || res?.data?.data || res?.data || [];
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
return {
records: records.map((item: any, index: number) => ({
...item,
_rowKey:
item?.id ||
`${item?.stcd || modelStore.params.stcd || 'stcd'}-${
item?.tm || searchParams.value.tm || 'tm'
}-${index}`
})),
total
};
};
const getTableList = (params: any) => {
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
const currentPage = Number(params?.skip || 1);
const skip = Math.max(currentPage - 1, 0) * take;
return getFishDistributionList({
...params,
take,
skip,
filter: params?.filter || buildFilter()
});
};
const refreshTable = () => {
if (!modelStore.params.stcd || !searchParams.value.tm) return;
tableRef.value?.getList(buildFilter());
};
const getYearList = async () => {
const currentYear = String(new Date().getFullYear());
if (!modelStore.params.stcd) {
searchParams.value.tm = currentYear;
return;
}
try {
const res: any = await getFishDistributionYear({
stcd: modelStore.params.stcd
});
const yearData = Array.isArray(res?.data?.data)
? res.data.data
: Array.isArray(res?.data?.records)
? res.data.records
: Array.isArray(res?.data)
? res.data
: [];
searchParams.value.tm = getDefaultYear(yearData) || currentYear;
refreshTable();
} catch (error) {
console.error('获取年份列表失败:', error);
searchParams.value.tm = currentYear;
refreshTable();
}
};
const handleSearch = () => {
if (!searchParams.value.tm) {
message.warning('请选择调查日期');
return;
}
refreshTable();
};
const handleClose = () => {
emit('update:visible', false);
};
watch(
() => props.visible,
visible => {
if (visible && !hasLoaded.value) {
getYearList();
hasLoaded.value = true;
}
if (!visible) {
hasLoaded.value = false;
searchParams.value = {
tm: null
};
}
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (props.visible && newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
searchParams.value = {
tm: null
};
getYearList();
hasLoaded.value = true;
}
}
);
</script>
<style lang="scss" scoped>
.ylfb-modal {
.ylfb-content {
min-height: 600px;
}
.search-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
}
}
</style>

View File

@ -3,6 +3,7 @@ import { computed } from 'vue';
import { useTagsViewStore } from '@/store/modules/tagsView'; import { useTagsViewStore } from '@/store/modules/tagsView';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import MapModal from '@/components/MapModal/index.vue'; import MapModal from '@/components/MapModal/index.vue';
import ylfbModal from '@/components/ylfbModal/index.vue';
import { useModelStore } from '@/store/modules/model'; import { useModelStore } from '@/store/modules/model';
import GisView from '@/components/gis/GisView.vue'; import GisView from '@/components/gis/GisView.vue';
@ -32,6 +33,9 @@ const routeKey = computed(() => router.path + Math.random());
:title="modelStore.title" :title="modelStore.title"
:tabs-config="modelStore.tabList" :tabs-config="modelStore.tabList"
/> />
<!-- 鱼类分布 -->
<ylfbModal v-model:visible="modelStore.ylfbModalVisible" />
</section> </section>
</template> </template>

View File

@ -734,7 +734,7 @@ export const useMapStore = defineStore('map', () => {
if (ylfbKeys.includes(layerKey)) { if (ylfbKeys.includes(layerKey)) {
if (yearTime) { if (yearTime) {
requestParams.filters.push({ requestParams.filters.push({
field: 'tm', field: 'startTime',
operator: 'eq', operator: 'eq',
dataType: 'date', dataType: 'date',
value: moment(yearTime) value: moment(yearTime)
@ -742,7 +742,7 @@ export const useMapStore = defineStore('map', () => {
.format('YYYY-MM-DD 00:00:00') .format('YYYY-MM-DD 00:00:00')
}); });
requestParams.filters.push({ requestParams.filters.push({
field: 'tm', field: 'endTime',
operator: 'eq', operator: 'eq',
dataType: 'date', dataType: 'date',
value: moment(yearTime).endOf('year').format('YYYY-MM-DD 23:59:59') value: moment(yearTime).endOf('year').format('YYYY-MM-DD 23:59:59')

View File

@ -4,13 +4,14 @@ import { ref } from 'vue';
export const useModelStore = defineStore('model', () => { export const useModelStore = defineStore('model', () => {
// state // state
const modalVisible = ref(false); const modalVisible = ref(false);
const currentTabKey = ref("basicInfo"); const ylfbModalVisible = ref(false); // 鱼类分布弹窗显示状态
const currentTabKey = ref('basicInfo');
const tabList = ref([]); const tabList = ref([]);
const title = ref(''); const title = ref('');
const isBasicEdit = ref(false); const isBasicEdit = ref(false);
const showStcdSelector = ref(false); // 是否显示下拉框 const showStcdSelector = ref(false); // 是否显示下拉框
const stcdOptions = ref<Array<{ label: string, value: string }>>([]); // 可选测站列表 const stcdOptions = ref<Array<{ label: string; value: string }>>([]); // 可选测站列表
// 新增:功能迁移所需的状态字段 // 新增:功能迁移所需的状态字段
const baseId = ref<string>(''); // 当前基地 ID const baseId = ref<string>(''); // 当前基地 ID
const hasPowerStatData = ref<boolean>(false); // 电站是否有专题数据 const hasPowerStatData = ref<boolean>(false); // 电站是否有专题数据
@ -20,14 +21,14 @@ export const useModelStore = defineStore('model', () => {
const wtConfig = ref<{ ioWtrv?: boolean; hasRstcdWtvt?: boolean }>({}); // 水温配置 const wtConfig = ref<{ ioWtrv?: boolean; hasRstcdWtvt?: boolean }>({}); // 水温配置
const wqShow = ref<boolean>(false); // 水质 AI 预测显示 const wqShow = ref<boolean>(false); // 水质 AI 预测显示
const fishTabInfo = ref<any>(null); // 鱼类监测数据信息 const fishTabInfo = ref<any>(null); // 鱼类监测数据信息
const params = ref<{ const params = ref<{
sttp: string; sttp: string;
stcd: any; stcd: any;
date: any; date: any;
show: any; show: any;
enfc:any; enfc: any;
eqtp:any; eqtp: any;
baseId?: string; // 新增:基地 ID baseId?: string; // 新增:基地 ID
sttpCode?: string; // 新增:站点类型代码 sttpCode?: string; // 新增:站点类型代码
sttpMap?: string; // 新增:站点类型映射 sttpMap?: string; // 新增:站点类型映射
@ -38,24 +39,25 @@ export const useModelStore = defineStore('model', () => {
stcd: '', stcd: '',
date: '', date: '',
show: true, show: true,
enfc:'0', enfc: '0',
eqtp:'', eqtp: '',
baseId: '', baseId: '',
sttpCode: '', sttpCode: '',
sttpMap: '', sttpMap: '',
dtinType:'', dtinType: '',
bldstt:'', bldstt: ''
}); });
const filterStll= ref('') const filterStll = ref('');
return { return {
params, params,
modalVisible, modalVisible,
ylfbModalVisible, // 鱼类分布弹窗显示状态
currentTabKey, currentTabKey,
tabList, tabList,
title, title,
isBasicEdit, isBasicEdit,
showStcdSelector, // 新增 showStcdSelector, // 新增
stcdOptions, // 新增 stcdOptions, // 新增
filterStll, filterStll,
// 新增返回字段 // 新增返回字段
baseId, baseId,
@ -65,6 +67,6 @@ export const useModelStore = defineStore('model', () => {
aiSites, aiSites,
wtConfig, wtConfig,
wqShow, wqShow,
fishTabInfo, fishTabInfo
}; };
}); });