基础弹框模块添加
This commit is contained in:
parent
65c4f4260c
commit
d4054ef786
@ -303,7 +303,7 @@ export function getAIComCalendarClick(data: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// 鱼类增殖站 - 运行数据 -获取年
|
||||
// 鱼类增殖站 - 运行数据 -获取年 没接
|
||||
export function getFishProgressionYear(data: any) {
|
||||
return request({
|
||||
url: '/wmp-env-server/fb/fbrdmr/getFbRelatedYrByStcd',
|
||||
@ -311,7 +311,7 @@ export function getFishProgressionYear(data: any) {
|
||||
params: data
|
||||
});
|
||||
}
|
||||
//鱼类增殖站 - 运行数据 -获取鱼
|
||||
//鱼类增殖站 - 运行数据 -获取鱼 没接
|
||||
export function getFishProgressionFish(data: any) {
|
||||
return request({
|
||||
url: '/wmp-env-server/env/fishDic/stcd/GetKendoList',
|
||||
@ -319,6 +319,33 @@ export function getFishProgressionFish(data: any) {
|
||||
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) {
|
||||
return request({
|
||||
@ -344,3 +371,125 @@ export function getNormalResearchSituation(data: any) {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>
|
||||
316
frontend/src/components/MapModal/components/DZGJ.vue
Normal file
316
frontend/src/components/MapModal/components/DZGJ.vue
Normal 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>
|
||||
583
frontend/src/components/MapModal/components/Ecology.vue
Normal file
583
frontend/src/components/MapModal/components/Ecology.vue
Normal 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>
|
||||
259
frontend/src/components/MapModal/components/Normal.vue
Normal file
259
frontend/src/components/MapModal/components/Normal.vue
Normal 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>
|
||||
@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { h, ref, watch } from 'vue';
|
||||
import {
|
||||
getNormalAddedSituationYear,
|
||||
getNormalAddedSituation
|
||||
@ -67,6 +67,33 @@ const searchParams = ref({
|
||||
const loadFtp = ref(false);
|
||||
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([
|
||||
{
|
||||
@ -97,11 +124,33 @@ const tableColumns = ref([
|
||||
dataIndex: 'directionVoList',
|
||||
width: 150,
|
||||
customRender: ({ text }: any) => {
|
||||
if (text?.length > 0) {
|
||||
return text[0].flstnm + ':' + Number(text[0].flfnct) / 10000;
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
const lines = formatDirectionList(text);
|
||||
if (lines === '-') 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',
|
||||
width: 150,
|
||||
customRender: ({ text }: any) => {
|
||||
if (text == null || text === '-') return '-';
|
||||
return Number(text).toFixed(2);
|
||||
return formatRangeOrDecimal(text, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -121,8 +169,7 @@ const tableColumns = ref([
|
||||
dataIndex: 'fwet',
|
||||
width: 150,
|
||||
customRender: ({ text }: any) => {
|
||||
if (text == null || text === '-') return '-';
|
||||
return Number(text).toFixed(2);
|
||||
return text == null || text === '-' ? '-' : text;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -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>
|
||||
@ -359,12 +359,12 @@ export const operationStepList: OperationStepItem[] = [
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'fpitocintype',
|
||||
label: '催产剂种类',
|
||||
name: 'fishsrc',
|
||||
label: '亲鱼来源',
|
||||
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
|
||||
method: 'get',
|
||||
params: {
|
||||
dictCode: 'FPITOCINTYPE'
|
||||
dictCode: 'BSSR'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -1550,9 +1550,9 @@ export const qinYuPeiYuTouWei = [
|
||||
},
|
||||
{
|
||||
dataIndexRaw: '',
|
||||
key: 'ftype',
|
||||
key: 'ftypeName',
|
||||
title: '饲料种类',
|
||||
dataIndex: 'ftype',
|
||||
dataIndex: 'ftypeName',
|
||||
children: [],
|
||||
visible: true,
|
||||
fixed: '',
|
||||
@ -3868,9 +3868,9 @@ export const miaoZhongJiLu = [
|
||||
},
|
||||
{
|
||||
dataIndexRaw: '',
|
||||
key: 'feedtp',
|
||||
key: 'ftypeName',
|
||||
title: '饲料种类',
|
||||
dataIndex: 'feedtp',
|
||||
dataIndex: 'ftypeName',
|
||||
children: [],
|
||||
visible: true,
|
||||
fixed: '',
|
||||
|
||||
710
frontend/src/components/MapModal/components/ProcessDiagram.vue
Normal file
710
frontend/src/components/MapModal/components/ProcessDiagram.vue
Normal 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>
|
||||
435
frontend/src/components/MapModal/components/liveFootage.vue
Normal file
435
frontend/src/components/MapModal/components/liveFootage.vue
Normal 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>
|
||||
404
frontend/src/components/MapModal/components/onSitePhoto.vue
Normal file
404
frontend/src/components/MapModal/components/onSitePhoto.vue
Normal 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>
|
||||
@ -20,7 +20,13 @@
|
||||
8. 栖息地-流量监测 没有水位视频和流量视频 字段没数据
|
||||
9. 栖息地 水温、水质、流量没有数据没法测
|
||||
10.鱼类增殖站 - 运行数据 要添加字典
|
||||
11.珍惜植物园 - 种植要求字段不知道
|
||||
12.水生调查断面 - 监测数据 不知道电导率 ,性腺发育期,早期资源量和种类
|
||||
13.水电告警情况 生态流量 不知道字段 接口传参有问题
|
||||
|
||||
|
||||
|
||||
没弄: 环保设施
|
||||
-->
|
||||
<div v-if="modelStore.showStcdSelector" class="stcd-selector-wrapper">
|
||||
<a-select
|
||||
@ -123,6 +129,12 @@
|
||||
"
|
||||
:wq-show="modelStore.wqShow"
|
||||
/>
|
||||
<!-- 监测数据 - 水电站告警情况 -->
|
||||
<DZGJ
|
||||
v-show="currentActiveKey === 'DZGJ'"
|
||||
:is-active="currentActiveKey === 'DZGJ'"
|
||||
:tabs-items="getTabChildren('DZGJ')"
|
||||
/>
|
||||
<!-- 流量监测 -->
|
||||
<FlowMeasure
|
||||
v-show="currentActiveKey === 'FlowMeasure'"
|
||||
@ -171,12 +183,16 @@
|
||||
v-show="currentActiveKey === 'WaterTemperatureRep'"
|
||||
:is-active="currentActiveKey === 'WaterTemperatureRep'"
|
||||
/>
|
||||
|
||||
<!-- 鱼类增殖站 - 运行数据 -->
|
||||
<NormalOperationData
|
||||
v-show="currentActiveKey === 'NormalOperationData'"
|
||||
:is-active="currentActiveKey === 'NormalOperationData'"
|
||||
/>
|
||||
<!-- 过程图 -->
|
||||
<ProcessDiagram
|
||||
v-show="currentActiveKey === 'ProcessDiagram'"
|
||||
:is-active="currentActiveKey === 'ProcessDiagram'"
|
||||
/>
|
||||
<!-- 增值放流情况 -->
|
||||
<NormalAddedSituation
|
||||
v-show="currentActiveKey === 'NormalAddedSituation'"
|
||||
@ -187,6 +203,31 @@
|
||||
v-show="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
|
||||
v-show="currentActiveKey === 'attachment'"
|
||||
@ -225,8 +266,20 @@ import FishFacilityMonitorData from './components/FishFacilityMonitorData.vue';
|
||||
import FishFacilityRunState from './components/FishFacilityRunState.vue'; // 运行情况
|
||||
// 鱼类增殖站
|
||||
import NormalOperationData from './components/NormalOperationData/index.vue'; // 运行数据
|
||||
import ProcessDiagram from './components/ProcessDiagram.vue'; // 过程图
|
||||
import NormalAddedSituation from './components/NormalAddedSituation.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 EcologicalFlow from './components/EcologicalFlow.vue'; // 生态流量
|
||||
@ -294,8 +347,7 @@ const checkPowerStationTopicData = async (stcd: string) => {
|
||||
]
|
||||
}
|
||||
});
|
||||
hasPowerStatData.value =
|
||||
res?.data?.data && res?.data?.data?.length > 0;
|
||||
hasPowerStatData.value = res?.data?.data && res?.data?.data?.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查询电站专题配置失败:', error);
|
||||
hasPowerStatData.value = false;
|
||||
@ -605,10 +657,10 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
|
||||
]
|
||||
};
|
||||
const res2 = await getVideoList(params);
|
||||
console.log('', modelStore.params.sttp);
|
||||
debugger
|
||||
console.log('', modelStore.params.sttp);
|
||||
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'
|
||||
) {
|
||||
tabsConfig.value = newTabs;
|
||||
|
||||
@ -147,11 +147,16 @@ export class MapOl implements MapInterface {
|
||||
this.popupManager.showPopup(undefined, undefined);
|
||||
this.popupManager.handleMapClick(evt.pixel, detectedFeature => {
|
||||
console.log(detectedFeature);
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = detectedFeature.values_;
|
||||
modelStore.title =
|
||||
detectedFeature.values_.titleName ||
|
||||
detectedFeature.values_.stnm + ' 详情信息';
|
||||
if (detectedFeature.values_.sttpMap == 'ylfb') {
|
||||
modelStore.ylfbModalVisible = true;
|
||||
modelStore.params = detectedFeature.values_;
|
||||
} else {
|
||||
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 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 = {
|
||||
image: new Icon({
|
||||
@ -254,8 +265,8 @@ export class MapOl implements MapInterface {
|
||||
};
|
||||
|
||||
styleOptions.text = new Text({
|
||||
text: labelText,
|
||||
offsetY: -20 * dynamicScale,
|
||||
text: formattedLabelText,
|
||||
offsetY: labelOffsetY,
|
||||
font: `${fontSize}px sans-serif`,
|
||||
fill: new Fill({ color: '#fff' }),
|
||||
stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }),
|
||||
@ -265,6 +276,34 @@ export class MapOl implements MapInterface {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据缩放级别计算锚点图标缩放比例,统一复用,减少样式函数中的重复判断。
|
||||
*/
|
||||
|
||||
@ -269,10 +269,12 @@ export class PointLayerManager {
|
||||
...item,
|
||||
_layerKey: targetLayerKey
|
||||
});
|
||||
|
||||
feature.setId(item.stcd || item._id);
|
||||
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('_sttpMap', item.sttpMap);
|
||||
if (item.popupHtml) {
|
||||
|
||||
284
frontend/src/components/ylfbModal/index.vue
Normal file
284
frontend/src/components/ylfbModal/index.vue
Normal 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>
|
||||
@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import { useRoute } from 'vue-router';
|
||||
import MapModal from '@/components/MapModal/index.vue';
|
||||
import ylfbModal from '@/components/ylfbModal/index.vue';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import GisView from '@/components/gis/GisView.vue';
|
||||
|
||||
@ -32,6 +33,9 @@ const routeKey = computed(() => router.path + Math.random());
|
||||
:title="modelStore.title"
|
||||
:tabs-config="modelStore.tabList"
|
||||
/>
|
||||
<!-- 鱼类分布 -->
|
||||
|
||||
<ylfbModal v-model:visible="modelStore.ylfbModalVisible" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@ -734,7 +734,7 @@ export const useMapStore = defineStore('map', () => {
|
||||
if (ylfbKeys.includes(layerKey)) {
|
||||
if (yearTime) {
|
||||
requestParams.filters.push({
|
||||
field: 'tm',
|
||||
field: 'startTime',
|
||||
operator: 'eq',
|
||||
dataType: 'date',
|
||||
value: moment(yearTime)
|
||||
@ -742,7 +742,7 @@ export const useMapStore = defineStore('map', () => {
|
||||
.format('YYYY-MM-DD 00:00:00')
|
||||
});
|
||||
requestParams.filters.push({
|
||||
field: 'tm',
|
||||
field: 'endTime',
|
||||
operator: 'eq',
|
||||
dataType: 'date',
|
||||
value: moment(yearTime).endOf('year').format('YYYY-MM-DD 23:59:59')
|
||||
|
||||
@ -4,12 +4,13 @@ import { ref } from 'vue';
|
||||
export const useModelStore = defineStore('model', () => {
|
||||
// state
|
||||
const modalVisible = ref(false);
|
||||
const currentTabKey = ref("basicInfo");
|
||||
const ylfbModalVisible = ref(false); // 鱼类分布弹窗显示状态
|
||||
const currentTabKey = ref('basicInfo');
|
||||
const tabList = ref([]);
|
||||
const title = ref('');
|
||||
const isBasicEdit = 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
|
||||
@ -26,8 +27,8 @@ export const useModelStore = defineStore('model', () => {
|
||||
stcd: any;
|
||||
date: any;
|
||||
show: any;
|
||||
enfc:any;
|
||||
eqtp:any;
|
||||
enfc: any;
|
||||
eqtp: any;
|
||||
baseId?: string; // 新增:基地 ID
|
||||
sttpCode?: string; // 新增:站点类型代码
|
||||
sttpMap?: string; // 新增:站点类型映射
|
||||
@ -38,24 +39,25 @@ export const useModelStore = defineStore('model', () => {
|
||||
stcd: '',
|
||||
date: '',
|
||||
show: true,
|
||||
enfc:'0',
|
||||
eqtp:'',
|
||||
enfc: '0',
|
||||
eqtp: '',
|
||||
baseId: '',
|
||||
sttpCode: '',
|
||||
sttpMap: '',
|
||||
dtinType:'',
|
||||
bldstt:'',
|
||||
dtinType: '',
|
||||
bldstt: ''
|
||||
});
|
||||
const filterStll= ref('')
|
||||
const filterStll = ref('');
|
||||
return {
|
||||
params,
|
||||
modalVisible,
|
||||
ylfbModalVisible, // 鱼类分布弹窗显示状态
|
||||
currentTabKey,
|
||||
tabList,
|
||||
title,
|
||||
isBasicEdit,
|
||||
showStcdSelector, // 新增
|
||||
stcdOptions, // 新增
|
||||
showStcdSelector, // 新增
|
||||
stcdOptions, // 新增
|
||||
filterStll,
|
||||
// 新增返回字段
|
||||
baseId,
|
||||
@ -65,6 +67,6 @@ export const useModelStore = defineStore('model', () => {
|
||||
aiSites,
|
||||
wtConfig,
|
||||
wqShow,
|
||||
fishTabInfo,
|
||||
fishTabInfo
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user