添加增值放流

This commit is contained in:
扈兆增 2026-06-12 18:42:54 +08:00
parent 9c34ab5afe
commit a9fde1edc5
18 changed files with 9486 additions and 53 deletions

View File

@ -22,6 +22,16 @@ export function getIdUrl(params: any) {
params
});
}
export function postIdUrl(data: any) {
return request({
url: import.meta.env.VITE_APP_ATTACHMENT_URL + '/get',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
// 全景影像
export function getPanoramaList(data: any) {
return request({
@ -175,11 +185,118 @@ export function getMonitorDataOnline(data: any) {
data
});
}
// 运行状态
export function getRunState(data: any) {
// 没接
export function getMonitorDataFpq(data: any) {
return request({
url: '/wmp-env-server/env/fp/query/qgc/sdfpssr/GetKendoListCust',
url: '/fp/msstbprpt/GetKendoList',
method: 'post',
data
});
}
// 运行状态
export function getRunState(data: any) {
return request({
url: '/fp/query/qgc/sdfpssr/GetKendoListCust',
method: 'post',
data
});
}
export function getRunStateSecondPlan(data: any) {
return request({
url: '/fp/run/secondPlan/qgc/GetKendoListCust',
method: 'post',
data
});
}
// 综合分析
export function getFishZHFX(data: any) {
return request({
url: 'fp/run/fish/select',
method: 'post',
data
});
}
export function getRunStateAnalysis(data: any) {
return request({
url: '/fp/run/analysis/GetKendoListCust',
method: 'post',
data
});
}
// ai运行识别 没接
export function getAIYXSB(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/getStInfoByStcd',
method: 'get',
params: data
});
}
// ai运行识别 视频/表格 没接
export function getAIComList(data: any) {
return request({
url: '/wmp-env-server/env/ai/com/GetKendoListCust',
method: 'post',
data
});
}
// ai运行识别 日历 没接
export function getAIComCalendar(data: any) {
return request({
url: '/wmp-env-server/env/ai/com/qgc/aiRecord/GetKendoListCust',
method: 'post',
data
});
}
// ai运行 日历点击 没接
export function getAIComCalendarClick(data: any) {
return request({
url: '/wmp-env-server/env/ai/com/qgc/aiFile/GetKendoListCust',
method: 'post',
data
});
}
// 鱼类增殖站 - 运行数据 -获取年
export function getFishProgressionYear(data: any) {
return request({
url: '/wmp-env-server/fb/fbrdmr/getFbRelatedYrByStcd',
method: 'get',
params: data
});
}
//鱼类增殖站 - 运行数据 -获取鱼
export function getFishProgressionFish(data: any) {
return request({
url: '/wmp-env-server/env/fishDic/stcd/GetKendoList',
method: 'post',
data
});
}
//增值放流情况 获取年 没接
export function getNormalAddedSituationYear(data: any) {
return request({
url: '/wmp-env-server/fb/msfbrdm/fbFlData/GetKendoListCust',
method: 'post',
data
});
}
//增值放流情况 列表 没接
export function getNormalAddedSituation(data: any) {
return request({
url: '/wmp-env-server/fb/msfbrdm/qgc/GetKendoListCust',
method: 'post',
data
});
}
// 科研情况 没接
export function getNormalResearchSituation(data: any) {
return request({
url: '/wmp-env-server/base/researchb/GetKendoList',
method: 'post',
data
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,250 @@
<template>
<div class="attachment-preview" v-loading="loading">
<!-- 左侧媒体展示 -->
<div class="viewer-main">
<div
v-if="activeMedia.type === 'image' && activeMedia.src"
class="media-display"
>
<img
:src="activeMedia.src"
style="width: 100%; height: 100%; object-fit: contain"
/>
</div>
<div
v-else-if="activeMedia.type === 'video' && 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 class="viewer-sidebar">
<div class="sidebar-list" v-if="attachmentList.length > 0">
<div
v-for="item in attachmentList"
:key="item.fid"
class="sidebar-item"
:class="{ active: activeFid === item.fid }"
@click="handleSelectAttachment(item)"
>
<div class="item-thumb">
<img v-if="item.isImage && item.thumbUrl" :src="item.thumbUrl" />
<div v-else-if="item.isVideo" class="thumb-placeholder">
<img :src="videoCover" />
</div>
<span v-else class="no-media">无媒体</span>
</div>
</div>
</div>
<a-empty v-else description="暂无附件" class="sidebar-empty" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
import { postIdUrl } from '@/api/mapModal';
import videoCover from '@/assets/images/videFm.jpg';
const props = defineProps<{
fid: string; // fid
}>();
const loading = ref(false);
const attachmentList = ref<any[]>([]);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
//
const fetchAttachmentList = async () => {
if (!props.fid) return;
loading.value = true;
try {
const fids = props.fid
.split(',')
.map((f: string) => f.trim())
.filter(Boolean);
// postIdUrl
let mediaMap: Record<string, any> = {};
if (fids.length > 0) {
const formData = new FormData();
formData.append('id', fids.join(','));
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
//
const list: any[] = [];
fids.forEach((fid: string) => {
const m = mediaMap[fid];
const ext = m?.ext || '';
const isVideo = ext === '.mp4';
const isImage = ext === '.jpg';
list.push({
fid,
media: m || null,
thumbUrl: isImage ? m?.fullpath || '' : '',
isVideo,
isImage
});
});
attachmentList.value = list;
//
if (attachmentList.value.length > 0 && !activeFid.value) {
handleSelectAttachment(attachmentList.value[0]);
}
} catch (error) {
console.error('获取附件列表失败:', error);
attachmentList.value = [];
} finally {
loading.value = false;
}
};
//
const handleSelectAttachment = (item: any) => {
activeFid.value = item.fid;
const { media, isVideo, isImage } = item;
if (isImage && media?.fullpath) {
activeMedia.value = { type: 'image', src: media.fullpath };
} else if (isVideo && media?.fullpath) {
activeMedia.value = { type: 'video', src: media.fullpath };
} else {
activeMedia.value = { type: '', src: '' };
}
};
// fid
watch(
() => props.fid,
() => {
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
attachmentList.value = [];
fetchAttachmentList();
}
);
//
onMounted(() => {
fetchAttachmentList();
});
//
defineExpose({
fetchAttachmentList
});
</script>
<style lang="scss" scoped>
.attachment-preview {
display: flex;
gap: 12px;
height: 100%;
.viewer-main {
flex: 0 0 80%;
background: #000;
border-radius: 4px;
overflow: hidden;
.media-display {
width: 100%;
height: 100%;
}
.media-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
.viewer-sidebar {
flex: 0 0 20%;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
.sidebar-list {
flex: 1;
overflow-y: auto;
.sidebar-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
}
.item-thumb {
width: 100%;
height: 110px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.no-media {
font-size: 12px;
color: #999;
}
}
}
}
.sidebar-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>

View File

@ -0,0 +1,407 @@
<template>
<div class="media-viewer" v-loading="loading">
<!-- 左侧媒体展示 -->
<div class="viewer-main">
<div
v-if="activeMedia.type === 'image' && activeMedia.src"
class="media-display"
>
<img
:src="activeMedia.src"
style="width: 100%; height: 100%; object-fit: contain"
/>
</div>
<div
v-else-if="activeMedia.type === 'video' && 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 class="viewer-sidebar">
<div class="sidebar-list" v-if="mediaList.length > 0">
<div
v-for="item in mediaList"
:key="item.fid"
class="sidebar-item"
:class="{ active: activeRstcd === item.fid }"
@click="handleSelectMedia(item)"
>
<div class="item-thumb">
<img v-if="item.isImage && item.thumbUrl" :src="item.thumbUrl" />
<div v-else-if="item.isVideo" class="thumb-placeholder">
<img :src="videoCover" />
</div>
<span v-else class="no-media">无媒体</span>
</div>
<div class="item-time">{{ item.ennm }} {{ item.tm }}</div>
</div>
</div>
<a-empty v-else description="暂无数据" class="sidebar-empty" />
<div class="sidebar-pagination">
<a-pagination
v-model:current="currentPage"
v-model:pageSize="currentPageSize"
:page-size-options="pageSizeOptions"
:total="total"
show-size-changer
size="small"
@change="handlePageChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import dayjs from 'dayjs';
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
import { getAIComList, getAIComCalendarClick, postIdUrl } from '@/api/mapModal';
import videoCover from '@/assets/images/videFm.jpg';
const props = withDefaults(
defineProps<{
stcd: string;
type: string;
startDate: string;
endDate: string;
pageSizeOptions?: string[];
pageSize?: number;
dataType?: string; // '2' '1'
}>(),
{
pageSizeOptions: () => ['20', '50', '100'],
pageSize: 20,
dataType: ''
}
);
const emit = defineEmits<{
load: [];
}>();
const loading = ref(false);
const currentPage = ref(1);
const currentPageSize = ref(props.pageSize);
const total = ref(0);
const mediaList = ref<any[]>([]);
const activeRstcd = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
//
const resetState = () => {
currentPage.value = 1;
activeRstcd.value = '';
activeMedia.value = { type: '', src: '' };
mediaList.value = [];
};
//
const fetchMediaList = async () => {
if (!props.stcd || !props.type) return;
loading.value = true;
try {
const skip = (currentPage.value - 1) * currentPageSize.value;
const filters: any[] = [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: props.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: props.startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: props.endDate
},
{
field: 'type',
operator: 'eq',
dataType: 'string',
value: props.type
}
];
const params: any = {
take: currentPageSize.value,
skip,
filter: {
logic: 'and',
filters
},
group: []
};
// dataType
if (props.dataType) {
filters.push({
field: 'dataType',
operator: 'eq',
dataType: 'string',
value: props.dataType
});
} else {
//
params.sort = [
{ field: 'rstcdStepSort', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
];
}
console.log(props.dataType);
// 使 getAIComCalendarClick 使 getAIComList
const apiFunc = props.dataType ? getAIComCalendarClick : getAIComList;
console.log(apiFunc);
const res: any = await apiFunc(params);
const rawData = res?.data?.data || [];
total.value = res?.data?.total || 0;
// fid
const allFids: string[] = [];
rawData.forEach((item: any) => {
if (item.fid) {
item.fid.split(',').forEach((f: string) => {
const trimmed = f.trim();
if (trimmed) allFids.push(trimmed);
});
}
});
// postIdUrl
let mediaMap: Record<string, any> = {};
if (allFids.length > 0) {
const formData = new FormData();
formData.append('id', allFids.join(','));
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
// fid
const flatList: any[] = [];
rawData.forEach((item: any) => {
const fids = item.fid
? item.fid
.split(',')
.map((f: string) => f.trim())
.filter(Boolean)
: [];
const tm = item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
fids.forEach((fid: string) => {
const m = mediaMap[fid];
const ext = m?.ext || '';
const isVideo = ext === '.mp4';
const isImage = ext === '.jpg';
flatList.push({
...item,
fid,
media: m || null,
thumbUrl: isImage ? m?.fullpath || '' : '',
isVideo,
isImage,
tm
});
});
});
mediaList.value = flatList;
//
if (mediaList.value.length > 0 && !activeRstcd.value) {
handleSelectMedia(mediaList.value[0]);
}
} catch (error) {
console.error('获取媒体列表失败:', error);
mediaList.value = [];
} finally {
loading.value = false;
}
emit('load');
};
//
const handleSelectMedia = (item: any) => {
activeRstcd.value = item.fid;
const { media, isVideo, isImage } = item;
if (isImage && media?.fullpath) {
activeMedia.value = { type: 'image', src: media.fullpath };
} else if (isVideo && media?.fullpath) {
activeMedia.value = { type: 'video', src: media.fullpath };
} else {
activeMedia.value = { type: '', src: '' };
}
};
//
const handlePageChange = (page: number) => {
currentPage.value = page;
activeRstcd.value = '';
activeMedia.value = { type: '', src: '' };
fetchMediaList();
};
//
watch(
() => [
props.stcd,
props.type,
props.startDate,
props.endDate,
props.dataType
],
() => {
resetState();
fetchMediaList();
}
);
// pageSize
watch(
() => props.pageSize,
val => {
currentPageSize.value = val;
}
);
//
onMounted(() => {
fetchMediaList();
});
//
defineExpose({
fetchMediaList,
resetState
});
</script>
<style lang="scss" scoped>
.media-viewer {
display: flex;
gap: 12px;
height: 100%;
.viewer-main {
flex: 0 0 80%;
background: #000;
border-radius: 4px;
overflow: hidden;
.media-display {
width: 100%;
height: 100%;
}
.media-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
.viewer-sidebar {
flex: 0 0 20%;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
.sidebar-list {
flex: 1;
overflow-y: auto;
.sidebar-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
}
.item-thumb {
width: 100%;
height: 110px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.no-media {
font-size: 12px;
color: #999;
}
}
.item-time {
margin-top: 6px;
font-size: 12px;
color: #666;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.sidebar-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.sidebar-pagination {
padding: 8px;
text-align: center;
border-top: 1px solid #e8e8e8;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,9 @@
:title="
!isEdit
? item.vlsr || String(item.value).length > 10
? item.value
? item.vlsr
? '数据来源:' + item.vlsr
: item.value
: ''
: ''
"

View File

@ -7,6 +7,7 @@
type="daterange"
:allow-clear="false"
value-format="YYYY-MM-DD HH:mm:ss"
:presets="DateSetting.RangeButton.year1"
/>
<a-button class="ml-2" type="primary" @click="initData">查询</a-button>
</div>
@ -14,6 +15,9 @@
ref="tableRef"
:scrollY="500"
:columns="columns"
:searchParams="{
sort: [{ field: 'yr', dir: 'desc' }]
}"
:list-url="getMonitorDataOnline"
/>
<!-- 图片预览弹框 -->
@ -48,8 +52,9 @@
import { ref, onMounted, watch, computed, h } from 'vue';
import { Button } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getMonitorDataOnline } from '@/api/mapModal';
import { getMonitorDataFpq, getMonitorDataOnline } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import { DateSetting } from '@/utils/enumeration';
import dayjs from 'dayjs';
const loading = ref(false);
@ -81,8 +86,36 @@ const openVideoPreview = (url: string) => {
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const isCz = ref(false);
const columns = [
// isCzfalse
const baseColumns = [
{
title: '年份',
dataIndex: 'yr',
width: 120,
customRender: ({ text }: any) =>
text && text !== '-' ? dayjs(text).format('YYYY') : '-'
},
{
title: '鱼种类',
dataIndex: 'ftp',
width: 100
},
{
title: '鱼类规格(cm)',
dataIndex: 'fsz',
width: 100
},
{
title: '过鱼数量(尾)',
dataIndex: 'fcnt',
width: 100
}
];
// isCztrue
const detailColumns = [
{
title: '测站名称',
dataIndex: 'stnm',
@ -94,11 +127,11 @@ const columns = [
dataIndex: 'tm',
width: 120,
customRender: ({ text }: any) =>
text ? dayjs(text).format('YYYY-MM-DD') : '-'
text && text !== '-' ? dayjs(text).format('YYYY-MM-DD') : '-'
},
{
title: '鱼种类',
dataIndex: 'ftp',
dataIndex: 'ftpName',
width: 100
},
{
@ -114,8 +147,7 @@ const columns = [
{
title: '鱼速度(m/s)',
dataIndex: 'fishspeed',
width: 100,
customRender: ({ text }: any) => (text ? Number(text).toFixed(2) : '-')
width: 100
},
{
title: '鱼游向',
@ -131,7 +163,6 @@ const columns = [
return isNaN(num) ? '-' : num.toFixed(1);
}
},
{
title: '流速(m/s)',
dataIndex: 'speed',
@ -178,7 +209,6 @@ const columns = [
align: 'center',
width: 100,
customRender: ({ text }: any) => {
console.log(text);
const hasValidUrl =
text &&
text !== '-' &&
@ -205,7 +235,61 @@ const columns = [
}
}
];
//
const columns = computed(() => {
return isCz.value ? detailColumns : baseColumns;
});
const initData = async () => {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stCode',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{ field: 'sttpCode', operator: 'eq', dataType: 'string', value: 'FPQ' }
]
},
select: ['mway', 'stcd']
};
const params1 = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
},
select: ['mway', 'stcd']
};
isCz.value = false;
const res1 = await getMonitorDataFpq(params1);
const res = await getMonitorDataFpq(params);
let stcd = '';
console.log(res1);
if (res) {
if (res?.data?.data?.length > 0) {
stcd = res.data.data.map(item => item.stcd);
}
}
if (res1) {
if (res1?.data?.data?.length > 0) {
if (res1?.data?.data[0].mway == '2') {
isCz.value = true;
} else if (res1?.data?.data[0].mway == '1') {
isCz.value = false;
}
}
}
console.log(stcd);
const filter = {
logic: 'and',
filters: [
@ -225,10 +309,13 @@ const initData = async () => {
field: 'stcd',
operator: 'in',
dataType: 'string',
value: ['008640203900001049', '008640203900011009']
value: [modelStore.params.stcd]
}
]
};
if (stcd != '') {
filter.filters[2].value = filter.filters[2].value.concat(stcd);
}
tableRef.value?.getList(filter);
};

View File

@ -10,14 +10,11 @@
class="w-[160px]"
:loading="loadFtp"
allowClear
show-search
:fieldNames="{ label: 'name', value: 'id' }"
:filter-option="filterFtpOption"
:options="ftpList"
>
<a-select-option
v-for="item in ftpList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-select-option>
</a-select>
<span class="ml-2"> 游向</span>
<a-select
@ -35,10 +32,22 @@
>
</a-select>
<span class="ml-2"> 过鱼目标</span>
<a-tooltip title="暂无数据" placement="top">
<a-tooltip placement="top" :color="'#fff'">
<template #title>
<div v-if="planFtp" class="p-2">
<div
class="text-[#000] pb-2"
style="border-bottom: 1px solid #f5f5f5"
>
过鱼目标
</div>
<div class="text-[#000] mt-2">{{ planFtp }}</div>
</div>
<div v-else class="text-[#000]">暂无数据</div>
</template>
<a-button size="small"><i class="iconfont icon-yulei"></i></a-button>
</a-tooltip>
<a-button class="ml-2" type="primary" @click="initData">查询</a-button>
<a-button class="ml-2" type="primary" @click="queryBtn">查询</a-button>
</div>
<div class="flex justify-around mt-[20px] mb-[10px]">
<div>计划开始运行时间暂无数据</div>
@ -63,13 +72,32 @@
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
</div>
</a-modal>
<!-- 视频播放弹窗 -->
<a-modal
v-model:open="videoPreviewVisible"
title="视频预览"
:width="800"
:footer="null"
>
<div class="video-container">
<video
v-if="currentVideoUrl"
:src="currentVideoUrl"
controls
autoplay
class="video-player"
/>
<a-empty v-else description="暂无视频" />
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed, h } from 'vue';
import { ref, onMounted, watch, nextTick, computed, h } from 'vue';
import { Button } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getRunState } from '@/api/mapModal';
import { getRunState, getRunStateSecondPlan } from '@/api/mapModal';
import { getDictItemsByCode } from '@/api/dict';
import { getFishDictoryDropdown } from '@/api/select';
import { useModelStore } from '@/store/modules/model';
@ -94,11 +122,27 @@ const ftpList = ref([]);
//
const imagePreviewVisible = ref(false);
const previewImageUrl = ref('');
//
const videoPreviewVisible = ref(false);
const currentVideoUrl = ref('');
//
const filterFtpOption = (input: string, option: any) => {
console.log('filterOption', input, option);
if (!option.name) return false;
return option.name.indexOf(input) !== -1;
};
const openImagePreview = (url: string) => {
previewImageUrl.value = url;
imagePreviewVisible.value = true;
};
const openVideoPreview = (url: string) => {
currentVideoUrl.value = url;
videoPreviewVisible.value = true;
};
const props = defineProps({
isActive: { type: Boolean, default: false }
});
@ -116,7 +160,7 @@ const columns = [
},
{
title: '游向',
dataIndex: 'direction',
dataIndex: 'directionName',
width: 100,
customRender: ({ text }: any) => (text ? text : '-')
},
@ -125,18 +169,18 @@ const columns = [
dataIndex: 'strdt',
width: 120,
customRender: ({ text }: any) =>
text ? dayjs(text).format('YYYY-MM-DD') : '-'
text && text !== '-' ? dayjs(text).format('YYYY-MM-DD') : '-'
},
{
title: '结束时间',
dataIndex: 'enddt',
width: 120,
customRender: ({ text }: any) =>
text ? dayjs(text).format('YYYY-MM-DD') : '-'
text && text !== '-' ? dayjs(text).format('YYYY-MM-DD') : '-'
},
{
title: '附件',
dataIndex: 'firstimgurl',
title: '图片',
dataIndex: 'picpth',
align: 'center',
width: 100,
customRender: ({ text }: any) => {
@ -161,12 +205,44 @@ const columns = [
}
}
},
() => '查看附件'
() => '查看图片'
);
}
},
{
title: '视频',
dataIndex: 'vdpth',
align: 'center',
width: 100,
customRender: ({ text }: any) => {
const hasValidUrl =
text &&
text !== '-' &&
text.trim() !== '' &&
text !== 'null' &&
text !== 'undefined';
return h(
Button,
{
type: 'link',
disabled: !hasValidUrl,
style: {
color: !hasValidUrl ? '#ccc' : '#1890ff',
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
},
onClick: () => {
if (hasValidUrl) {
openVideoPreview(text);
}
}
},
() => '查看视频'
);
}
}
];
const initData = async () => {
const queryBtn = () => {
console.log('5');
const filter = {
logic: 'and',
filters: [
@ -190,37 +266,107 @@ const initData = async () => {
field: 'stcd',
operator: 'in',
dataType: 'string',
value: ['008640203900011010']
value: [modelStore.params.stcd]
}
]
};
if (searchParams.value.direction) {
filter.filters.push({
field: 'direction',
operator: 'eq',
dataType: 'string',
value: searchParams.value.direction
});
}
if (searchParams.value.ftp) {
filter.filters.push({
field: 'ftp',
operator: 'eq',
dataType: 'string',
value: searchParams.value.ftp
});
}
console.log(filter);
nextTick(() => {
tableRef.value?.getList(filter);
});
// const params = {
// filter: {
// logic: 'and',
// filters: [
// {
// field: 'stcd',
// operator: 'eq',
// dataType: 'string',
// value: modelStore.params.stcd
// },
// {
// field: 'yr',
// operator: 'eq',
// dataType: 'string',
// value: searchParams.value.tm.format('YYYY')
// },
// { field: 'direction', operator: 'eq', dataType: 'string', value: '0' }
// ]
// }
// };
// getRunStateSecondPlan(params).then(res => {
// console.log(res);
// planFtp.value = res.data.ftp || '';
// });
};
const initData = async () => {
queryBtn();
hasLoaded.value = true;
};
const getOptions = async () => {
loadDirection.value = true;
getDictItemsByCode({ dictCode: 'direction' }).then(res => {
if (res) {
direction.value = res.data || [];
loadDirection.value = false;
}
});
loadFtp.value = true;
getFishDictoryDropdown().then(res => {
getFishDictoryDropdown()
.then(res => {
ftpList.value = res.data || [];
loadFtp.value = false;
})
.catch(() => {
loadFtp.value = false;
});
};
watch(
() => props.isActive,
active => {
if (active) {
console.log('active', active);
if (active && !hasLoaded.value) {
getOptions();
initData();
hasLoaded.value = true;
}
}
},
{ immediate: true }
);
</script>
<style scoped>
.fish-facility-monitor-data {
padding: 20px;
}
.video-container {
width: 100%;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
width: 100%;
max-height: 600px;
border-radius: 4px;
}
</style>

View File

@ -1,11 +1,584 @@
<template>
<div>
<h2>综合分析</h2>
<div class="fish-zhfx">
<!-- 搜索条件 -->
<div class="search-bar mb-4">
<span>年份</span>
<a-date-picker
v-model:value="searchParams.tm"
picker="year"
style="width: 160px"
/>
<span class="ml-4">鱼类</span>
<a-select
v-model:value="searchParams.ftp"
placeholder="请选择"
class="w-[160px]"
:loading="loadFtp"
:fieldNames="{ label: 'name', value: 'id' }"
:filter-option="filterFtpOption"
:options="ftpList"
allowClear
>
</a-select>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</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 || 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"></script>
<style scoped>
.fish-facility-monitor-data {
padding: 20px;
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import dayjs from 'dayjs';
import * as echarts from 'echarts';
import { getFishZHFX, getRunStateAnalysis } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const isLoading = ref(false);
const tableData = ref<any[]>([]);
const chartData = ref<any[]>([]);
const chartColumns = ref<any[]>([]);
const chartRef = ref<HTMLElement>();
const hasLoaded = ref(false);
let chartInstance: echarts.ECharts | null = null;
//
const searchParams = ref({
tm: dayjs(),
ftp: null
});
//
const loadFtp = ref(false);
const ftpList = ref([]);
//
const tableColumns = ref<any[]>([]);
const tableScrollX = ref(0);
//
const filterFtpOption = (input: string, option: any) => {
if (!option.name) return false;
return option.name.indexOf(input) !== -1;
};
//
const getFtpOptions = async () => {
loadFtp.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(searchParams.value.tm)
.startOf('year')
.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(searchParams.value.tm)
.endOf('year')
.format('YYYY-MM-DD HH:mm:ss')
}
]
}
};
const res = await getFishZHFX(params);
ftpList.value = res?.data?.data || [];
if (ftpList.value.length > 0) {
searchParams.value.ftp = ftpList.value[0].id;
initData();
}
} catch (error) {
console.error('获取鱼类列表失败:', error);
} finally {
loadFtp.value = false;
}
};
//
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
updateChart(chartData.value, chartColumns.value);
};
//
const updateChart = (data: any[], columns: any[]) => {
if (!chartInstance) return;
if (!data || data.length === 0) {
chartInstance.clear();
return;
}
//
const dataColumns = columns.filter(
col => col.dataIndex !== 'dt' && col.visible !== false
);
const sorted = [...data].sort(
(a, b) => new Date(a.dt).getTime() - new Date(b.dt).getTime()
);
const xAxisData = sorted.map(item => dayjs(item.dt).format('YYYY-MM-DD'));
//
const colorList = [
'#009DFF',
'#FEB157',
'#76513C',
'#A0FF8A',
'#7399C6',
'#FEB157',
'#35A9FF',
'#FFAF55',
'#FF8760',
'#FE5658'
];
//
const getCleanTitle = (title: string) => {
return title?.replace(/[()℃尾]/g, '').trim() || '';
};
// Y
const yAxisColumnMap: Record<string, string[]> = {
'数量(尾)': ['数量', 'fcnt'],
'水温(℃)': ['水温', '出库水温', '入库水温'],
'(mg/L)': ['化学需氧量', '氨氮', '总氮', '总磷'],
水位: ['水位', '坝上水位', '坝下水位'],
流量: ['入库流量', '出库流量', '流量']
};
// Y
const getYAxisIndex = (col: any): number => {
// () dataIndex === 'fcnt'
if (col.dataIndex === 'fcnt') return 0;
const cleanTitle = getCleanTitle(col.title);
// YY
const yAxisNames = Object.keys(yAxisColumnMap);
for (let i = 0; i < yAxisNames.length; i++) {
const yAxisName = yAxisNames[i];
// ()dataIndex
if (yAxisName === '数量(尾)') continue;
const columnNames = yAxisColumnMap[yAxisName];
// Y
if (
columnNames.some(
name => cleanTitle.includes(name) || name.includes(cleanTitle)
)
) {
return i;
}
}
// Y
return 0;
};
// Y
const yAxisConfig = [
// 3Y
{
name: '数量(尾)',
type: 'value',
position: 'left',
axisLine: { show: true, lineStyle: { color: '#FF6B35' } },
axisLabel: { color: '#FF6B35' },
nameTextStyle: { color: '#FF6B35' },
splitLine: { show: true, lineStyle: { color: '#e5e5e5' } }
},
{
name: '水温(℃)',
type: 'value',
position: 'left',
axisLine: { show: true, lineStyle: { color: '#56C2E3' } },
axisLabel: { color: '#56C2E3' },
nameTextStyle: { color: '#56C2E3' },
splitLine: { show: false },
offset: 50
},
{
name: '(mg/L)',
type: 'value',
position: 'left',
axisLine: { show: true, lineStyle: { color: '#FF9500' } },
axisLabel: { color: '#FF9500' },
nameTextStyle: { color: '#FF9500' },
splitLine: { show: false },
offset: 100
},
// 2Y
{
name: '水位m',
type: 'value',
position: 'right',
axisLine: { show: true, lineStyle: { color: '#73C0DE' } },
axisLabel: { color: '#73C0DE' },
nameTextStyle: { color: '#73C0DE' },
splitLine: { show: false }
},
{
name: '流量m³/s',
type: 'value',
position: 'right',
axisLine: { show: true, lineStyle: { color: '#91CC75' } },
axisLabel: { color: '#91CC75' },
nameTextStyle: { color: '#91CC75' },
splitLine: { show: false },
offset: 70
}
];
// series
const series = dataColumns.map((col, index) => {
const isBar = col.dataIndex === 'fcnt';
const yAxisIndex = getYAxisIndex(col);
const colData = sorted.map(item => {
return item[col.dataIndex];
});
return {
name: col.title,
type: isBar ? 'bar' : 'line',
yAxisIndex,
data: colData,
smooth: !isBar,
symbol: isBar ? undefined : 'circle',
symbolSize: isBar ? undefined : 4,
itemStyle: { color: colorList[index % colorList.length] },
lineStyle: { width: 2 },
barMaxWidth: 30
};
});
console.log(sorted[0]);
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
textStyle: { color: '#fff', fontSize: 12 },
axisPointer: { type: 'cross' },
formatter: (params: any) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const fullTime = xAxisData[dataIndex] || '';
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
params.forEach((param: any) => {
if (param.value == null) return;
html += `
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${param.value}</strong></span>
</div>
`;
});
return html;
}
},
legend: {
type: 'scroll',
top: 10,
data: dataColumns.map(col => col.title),
textStyle: { fontSize: 12 }
},
grid: {
left: 120,
right: 120,
top: 80,
bottom: 60
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#000000' } },
axisTick: { show: false },
axisLabel: {
fontSize: 12
},
splitLine: {
show: true,
lineStyle: { color: '#bfbfbf', type: 'solid' }
}
},
yAxis: yAxisConfig,
series,
dataZoom: [
{
type: 'inside',
xAxisIndex: [0],
throttle: 50,
start: 0,
end: 100
}
],
toolbox: {
show: true,
feature: {
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
},
right: 20,
top: 10
}
};
chartInstance.setOption(option, true);
};
//
const fetchData = async () => {
isLoading.value = true;
try {
const filterParams = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(searchParams.value.tm)
.startOf('year')
.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(searchParams.value.tm)
.endOf('year')
.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'ftp',
operator: 'eq',
dataType: 'string',
// value: ''
value: searchParams.value.ftp
}
].filter(Boolean)
},
sort: [{ field: 'dt', dir: 'asc' }]
};
const res = await getRunStateAnalysis(filterParams);
console.log(res);
const result = res?.data?.data[0] || {};
console.log(result);
//
const columns = result.columns || [];
chartColumns.value = columns;
tableColumns.value = columns
.filter((col: any) => col.visible !== false)
.map((col: any) => ({
title: col.title,
dataIndex: col.dataIndex,
width: 120,
customRender: ({ text }: any) => {
if (col.dataType === 'date' && text) {
return dayjs(text).format('YYYY-MM-DD');
}
return text != null ? text : '-';
}
}));
tableScrollX.value = tableColumns.value.reduce(
(sum, col) => sum + (col.width || 100),
0
);
//
const dataSource = result.dataSource || [];
console.log(dataSource);
tableData.value = [...dataSource].reverse();
chartData.value = dataSource;
nextTick(() => updateChart(dataSource, columns));
} catch (error) {
console.error('获取数据失败:', error);
tableData.value = [];
chartData.value = [];
if (chartInstance) chartInstance.clear();
} finally {
isLoading.value = false;
}
};
//
const handleSearch = () => fetchData();
//
const initData = () => {
nextTick(() => initChart());
fetchData();
};
// isActive
watch(
() => props.isActive,
active => {
if (active) {
getFtpOptions();
}
},
{ immediate: true }
);
// stcd
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
fetchData();
}
}
);
// resize
let resizeObserver: ResizeObserver | null = null;
watch(
() => props.isActive,
active => {
if (active && chartRef.value) {
setTimeout(() => {
if (chartInstance) chartInstance.resize();
}, 200);
if (!resizeObserver) {
resizeObserver = new ResizeObserver(() => {
if (chartInstance) chartInstance.resize();
});
resizeObserver.observe(chartRef.value);
}
}
}
);
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<style lang="scss" scoped>
.fish-zhfx {
width: 100%;
min-height: 600px;
position: relative;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
.content-body {
width: 100%;
display: flex;
height: 600px;
position: relative;
.chart-wrapper {
flex: 1;
height: 100%;
position: relative;
box-sizing: border-box;
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;
height: 100%;
box-sizing: border-box;
padding: 10px;
overflow: hidden;
overflow-x: auto;
border-left: 1px solid #dcdfe6;
margin-left: 10px;
}
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="normal-added-situation">
<!-- 搜索条件 -->
<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"
/>
<span class="ml-4">鱼类</span>
<a-select
v-model:value="searchParams.ftp"
placeholder="请选择"
class="w-[160px]"
:loading="loadFtp"
:fieldNames="{ label: 'name', value: 'id' }"
:filter-option="filterFtpOption"
:options="ftpList"
allowClear
>
</a-select>
<a-button class="ml-4" type="primary" @click="handleSearch">
查询
</a-button>
</div>
<!-- 表格 -->
<BasicTable
ref="tableRef"
:scrollY="500"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getNormalAddedSituation"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import {
getNormalAddedSituationYear,
getNormalAddedSituation
} from '@/api/mapModal';
import { getFishDictoryDropdown } from '@/api/select';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const hasLoaded = ref(false);
const tableRef = ref();
//
const searchParams = ref({
tm: null as string | null,
ftp: null
});
//
const loadFtp = ref(false);
const ftpList = ref<any[]>([]);
//
const tableColumns = ref([
{
title: '养殖鱼类',
dataIndex: 'ftpName',
width: 150
},
{
title: '计划养殖规模(万尾)',
dataIndex: 'fcntjh',
width: 150,
customRender: ({ text }: any) => {
if (text == null || text === '-') return '-';
return (Number(text) / 10000).toFixed(4);
}
},
{
title: '实际完成数量(万尾)',
dataIndex: 'fcntjc',
width: 150,
customRender: ({ text }: any) => {
if (text == null || text === '-') return '-';
return (Number(text) / 10000).toFixed(4);
}
},
{
title: '放流去向',
dataIndex: 'directionVoList',
width: 150,
customRender: ({ text }: any) => {
if (text?.length > 0) {
return text[0].flstnm + '' + Number(text[0].flfnct) / 10000;
} else {
return '-';
}
}
},
{
title: '平均规格',
children: [
{
title: '全长(cm)',
dataIndex: 'fsz',
width: 150,
customRender: ({ text }: any) => {
if (text == null || text === '-') return '-';
return Number(text).toFixed(2);
}
},
{
title: '体重(kg)',
dataIndex: 'fwet',
width: 150,
customRender: ({ text }: any) => {
if (text == null || text === '-') return '-';
return Number(text).toFixed(2);
}
}
]
// customRender: ({ text }: any) => {
// if (text?.length > 0) {
// return text[0].flstnm + '' + Number(text[0].flfnct) / 10000;
// } else {
// return '-';
// }
// }
}
]);
const tableScrollX = ref(300);
//
const filterFtpOption = (input: string, option: any) => {
if (!option.name) return false;
return option.name.indexOf(input) !== -1;
};
//
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
}
]
},
group: [{ dir: 'desc', field: 'yr' }],
groupResultFlat: true
};
const res: any = await getNormalAddedSituationYear(params);
const yearData = res?.data?.data || [];
if (yearData.length > 0) {
searchParams.value.tm = yearData[0].yr;
getFtpOptions();
} else {
if (tableRef.value) {
tableRef.value.loading = false;
}
}
} catch (error) {
console.error('获取年份列表失败:', error);
if (tableRef.value) {
tableRef.value.loading = false;
}
}
};
//
const getFtpOptions = async () => {
loadFtp.value = true;
getFishDictoryDropdown()
.then(res => {
ftpList.value = res.data || [];
loadFtp.value = false;
refreshTable();
})
.catch(() => {
loadFtp.value = false;
});
};
//
const refreshTable = () => {
const year = searchParams.value.tm;
const startDate = `${year}-01-01 00:00:00`;
const endDate = `${year}-12-31 23:59:59`;
const filter = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endDate
},
...(searchParams.value.ftp
? [
{
field: 'ftp',
operator: 'eq',
dataType: 'string',
value: searchParams.value.ftp
}
]
: [])
]
};
tableRef.value?.getList(filter);
};
//
const handleSearch = () => {
refreshTable();
};
// isActive
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
hasLoaded.value = true;
}
},
{ immediate: true }
);
// stcd
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
ftpList.value = [];
searchParams.value = { tm: null, ftp: null };
getYearList();
}
}
);
</script>
<style lang="scss" scoped>
.normal-added-situation {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,695 @@
<template>
<div class="normal-operation-data">
<div class="search-bar">
<div class="search-item select-item1">
<a-select
v-model:value="searchForm.firstKey"
class="search-control select-control"
placeholder="请选择一级"
:options="firstLevelOptions"
/>
</div>
<div class="search-item input-item1">
<a-select
v-model:value="searchForm.secondKey"
class="search-control input-control"
placeholder="请选择二级"
:options="secondLevelOptions"
/>
</div>
<div class="search-item select-item2">
<a-date-picker
v-model:value="searchForm.year"
class="search-control select-control"
picker="year"
format="YYYY"
value-format="YYYY"
placeholder="请选择年份"
:disabled="loadYear"
:allow-clear="false"
/>
</div>
<template v-for="field in dynamicFilters" :key="field.name">
<div
class="search-item"
:class="field.type === 'select' ? 'select-item' : 'input-item'"
:style="getFieldStyle(field)"
>
<a-select
v-if="field.type === 'select'"
v-model:value="extraFilters[field.name]"
class="search-control select-control"
:placeholder="`请搜索${field.label}`"
:options="getFieldOptions(field)"
:loading="getSelectLoading(field)"
:filter-option="filterSelectOption"
show-search
allow-clear
/>
<a-input
v-else
v-model:value="extraFilters[field.name]"
class="search-control input-control"
:placeholder="field.placeholder || `请输入${field.label}`"
allow-clear
/>
</div>
</template>
<div class="search-actions">
<a-button type="primary" @click="handleSearch">查询</a-button>
</div>
</div>
<BasicTable
ref="tableRef"
:key="currentTable?.key || 'empty-table'"
:columns="tableColumns"
:list-url="getTableList"
:scrollY="500"
:scrollX="tableScrollX"
>
<template #fid="{ record }">
<a-button
type="link"
class="!p-0"
:disabled="!hasAttachment(record?.fid)"
@click="handleViewAttachment(record)"
>
查看附件
</a-button>
</template>
</BasicTable>
<a-modal
v-model:open="attachmentModalVisible"
title="查看附件"
width="80vw"
:footer="null"
:destroy-on-close="true"
>
<div class="attachment-modal-body">
<AttachmentPreview :fid="currentAttachmentFid" />
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref, watch } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import AttachmentPreview from '../AIYXSB/AttachmentPreview.vue';
import { getFishProgressionFish, getFishProgressionYear } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import request from '@/utils/request';
import {
operationStepList,
type OperationFilterOption,
type OperationFilterField,
type OperationTableItem
} from './operationConfig';
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const DEFAULT_YEAR = String(new Date().getFullYear());
const DEFAULT_TABLE_PAGE_SIZE = 20;
const DEFAULT_TABLE_SCROLL_X = 900;
const DEFAULT_SELECT_WIDTH = 150;
const DEFAULT_INPUT_WIDTH = 250;
const EMPTY_TABLE_RESPONSE = {
data: {
records: [],
total: 0
}
};
const modelStore = useModelStore();
const searchForm = reactive({
firstKey: operationStepList[0]?.key || '',
secondKey: '',
year: ''
});
const extraFilters = reactive<Record<string, string | undefined>>({});
const ftpList = ref<OperationFilterOption[]>([]);
const loadYear = ref(false);
const loadFtp = ref(false);
const tableRef = ref();
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
const fieldOptionsMap = reactive<Record<string, OperationFilterOption[]>>({});
const selectLoadingMap = reactive<Record<string, boolean>>({});
const currentFirstLevel = computed(() =>
operationStepList.find(item => item.key === searchForm.firstKey)
);
const secondLevelTables = computed<OperationTableItem[]>(() => {
const groups = currentFirstLevel.value?.textList || [];
return groups.flatMap(group => group.proTable);
});
const currentTable = computed<OperationTableItem | undefined>(() =>
secondLevelTables.value.find(item => item.key === searchForm.secondKey)
);
const dynamicFilters = computed<OperationFilterField[]>(
() => currentTable.value?.filter || []
);
const firstLevelOptions = computed(() =>
operationStepList.map(item => ({
label: item.title,
value: item.key
}))
);
const secondLevelOptions = computed(() =>
secondLevelTables.value.map(item => ({
label: item.title,
value: item.key
}))
);
const getDefaultSecondKey = () => secondLevelOptions.value[0]?.value || '';
const resolveFirstLevelKey = (value?: string) =>
operationStepList.some(item => item.key === value)
? String(value)
: operationStepList[0]?.key || '';
const resolveSecondLevelKey = (value?: string) =>
secondLevelTables.value.some(item => item.key === value)
? String(value)
: getDefaultSecondKey();
const formatDateText = (value: unknown) => {
if (value === undefined || value === null || value === '') {
return '-';
}
const text = String(value).trim();
if (!text || text === '-') {
return '-';
}
return text.length >= 10 ? text.slice(0, 10) : text;
};
const renderEllipsisCell = (value: unknown) => {
const content =
value === undefined || value === null || value === '' ? '-' : String(value);
return h(
'div',
{
title: content,
style: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
}
},
content
);
};
const normalizeColumns = (columns: any[] = []): any[] =>
columns
.filter(column => column?.visible !== false)
.map(column => ({
title: column.title,
dataIndex: column.dataIndex,
key: column.key || column.dataIndex || column.title,
width: column.width,
fixed: column.fixed || undefined,
customRender:
column.dataType === 'date'
? ({ text }: { text: unknown }) =>
renderEllipsisCell(formatDateText(text))
: undefined,
children:
Array.isArray(column.children) && column.children.length > 0
? normalizeColumns(column.children)
: undefined
}));
const tableColumns = computed(() =>
normalizeColumns(currentTable.value?.columns)
);
const getLeafColumns = (columns: any[] = []): any[] =>
columns.flatMap(column => {
if (Array.isArray(column.children) && column.children.length > 0) {
return getLeafColumns(column.children);
}
return [column];
});
const estimateColumnMinWidth = (column: any) => {
if (typeof column.width === 'number') {
return column.width;
}
const titleText = String(column.title || '');
const titleWidth = titleText.length * 16 + 36;
if (column.dataType === 'date') {
return Math.max(120, titleWidth);
}
if (column.dataIndex === 'recorderName' || column.dataIndex === 'fid') {
return Math.max(110, titleWidth);
}
return Math.max(100, Math.min(220, titleWidth));
};
const tableScrollX = computed(() => {
const leafColumns = getLeafColumns(tableColumns.value);
if (leafColumns.length === 0) {
return DEFAULT_TABLE_SCROLL_X;
}
return leafColumns.reduce(
(total, column) => total + estimateColumnMinWidth(column),
0
);
});
const getDefaultFieldWidth = (field: OperationFilterField) =>
field.type === 'select' ? DEFAULT_SELECT_WIDTH : DEFAULT_INPUT_WIDTH;
const getFieldCacheKey = (field: OperationFilterField) =>
`${currentTable.value?.key || 'default'}-${field.name}`;
const getFieldStyle = (field: OperationFilterField) => ({
flexBasis:
typeof field.width === 'number'
? `${field.width}px`
: field.width || `${getDefaultFieldWidth(field)}px`
});
const buildFishOptionItem = (item: any): OperationFilterOption => ({
label: item?.name || item?.label || '-',
value: item?.id ?? item?.value ?? '',
name: item?.name || item?.label || '-',
id: item?.id ?? item?.value ?? ''
});
const buildDictOptionItem = (item: any): OperationFilterOption => ({
label:
item?.dictMeaning || item?.label || item?.text || item?.dictLabel || '-',
value: item?.dictValue ?? item?.value ?? item?.code ?? '',
name:
item?.dictMeaning || item?.label || item?.text || item?.dictLabel || '-',
id: item?.dictValue ?? item?.value ?? item?.code ?? ''
});
const getFieldOptions = (field: OperationFilterField) => {
if (field.name === 'ftp') {
return ftpList.value;
}
return fieldOptionsMap[getFieldCacheKey(field)] || field.options || [];
};
const getSelectLoading = (field: OperationFilterField) => {
if (field.name === 'ftp') {
return loadFtp.value;
}
return !!selectLoadingMap[getFieldCacheKey(field)];
};
const filterSelectOption = (input: string, option: any) => {
const label = String(option?.label ?? option?.name ?? '');
return label.includes(input);
};
const resetExtraFilters = () => {
Object.keys(extraFilters).forEach(key => {
delete extraFilters[key];
});
dynamicFilters.value.forEach(field => {
extraFilters[field.name] = undefined;
});
};
const isEmptyFilterValue = (value: unknown) =>
value === undefined || value === null || String(value).trim() === '';
const hasAttachment = (fid: unknown) =>
!!fid && String(fid).trim() !== '' && String(fid).trim() !== '-';
const handleViewAttachment = (record: any) => {
if (!hasAttachment(record?.fid)) {
return;
}
currentAttachmentFid.value = String(record.fid);
attachmentModalVisible.value = true;
};
const buildInputFilter = (field: OperationFilterField, value: string) => {
const filterKeys = Array.isArray(field.key)
? field.key
: field.key
? [field.key]
: [field.name];
if (filterKeys.length > 1) {
return {
logic: 'or',
filters: filterKeys.map(item => ({
field: item,
operator: 'contains',
dataType: 'string',
value
}))
};
}
return {
field: filterKeys[0],
operator: 'contains',
dataType: 'string',
value
};
};
const buildYearRangeFilters = () => {
if (!modelStore.params.stcd || !searchForm.year) {
return null;
}
return [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'data',
value: `${searchForm.year}-01-01 00:00:00`
},
{
field: 'tm',
operator: 'lte',
dataType: 'data',
value: `${searchForm.year}-12-31 23:59:59`
}
];
};
const buildTableFilter = () => {
const baseFilters = buildYearRangeFilters();
if (!baseFilters) {
return null;
}
const filters: any[] = [...baseFilters];
dynamicFilters.value.forEach(field => {
const fieldValue = extraFilters[field.name];
if (isEmptyFilterValue(fieldValue)) {
return;
}
if (field.type === 'select') {
filters.push({
field: field.name,
operator: 'eq',
dataType: 'string',
value: fieldValue
});
return;
}
filters.push(buildInputFilter(field, String(fieldValue).trim()));
});
return {
logic: 'and',
filters
};
};
const syncSecondLevel = () => {
searchForm.secondKey = resolveSecondLevelKey(searchForm.secondKey);
};
const resetDefaultCascade = () => {
searchForm.firstKey = resolveFirstLevelKey();
searchForm.secondKey = getDefaultSecondKey();
};
const refreshTable = () => {
const filter = buildTableFilter();
if (!filter || !currentTable.value?.url) {
return;
}
tableRef.value?.getList(filter);
};
const loadYearList = async () => {
loadYear.value = true;
try {
if (!modelStore.params.stcd) {
searchForm.year = DEFAULT_YEAR;
resetDefaultCascade();
return;
}
const res: any = await getFishProgressionYear({
stcd: modelStore.params.stcd
});
const records = res?.data?.data || [];
if (records.length === 0) {
searchForm.year = DEFAULT_YEAR;
resetDefaultCascade();
return;
}
const firstItem = records[0];
searchForm.year = firstItem?.yr || DEFAULT_YEAR;
searchForm.firstKey = resolveFirstLevelKey(firstItem?.type);
syncSecondLevel();
searchForm.secondKey = resolveSecondLevelKey(firstItem?.subType);
} catch (error) {
console.error('获取运行数据年份失败:', error);
searchForm.year = DEFAULT_YEAR;
resetDefaultCascade();
} finally {
loadYear.value = false;
}
};
const loadFtpOptions = async () => {
if (!modelStore.params.stcd) {
return;
}
loadFtp.value = true;
try {
const res: any = await getFishProgressionFish({
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
}
]
},
select: ['id', 'name']
});
const records = res?.data?.data || res?.data?.records || res?.data || [];
ftpList.value = records.map((item: any) => buildFishOptionItem(item));
} catch (error) {
console.error('获取鱼类选项失败:', error);
ftpList.value = [];
} finally {
loadFtp.value = false;
}
};
const loadRemoteSelectOptions = async () => {
const remoteFields = dynamicFilters.value.filter(
field => field.type === 'select' && field.name !== 'ftp' && field.url
);
await Promise.all(
remoteFields.map(async field => {
const cacheKey = getFieldCacheKey(field);
selectLoadingMap[cacheKey] = true;
try {
const method = field.method || 'post';
const payload = field.params || {};
const res: any = await request({
url: field.url,
method,
...(method === 'get' ? { params: payload } : { data: payload })
});
const records =
res?.data?.data || res?.data?.records || res?.data || [];
fieldOptionsMap[cacheKey] = records
.filter(
(item: any) =>
item?.level === undefined || String(item.level) === '2'
)
.map((item: any) => buildDictOptionItem(item));
} catch (error) {
console.error(`获取下拉选项失败: ${field.label}`, error);
fieldOptionsMap[cacheKey] = [];
} finally {
selectLoadingMap[cacheKey] = false;
}
})
);
};
const loadBaseData = async () => {
if (!modelStore.params.stcd) {
return;
}
await Promise.all([loadYearList(), loadFtpOptions()]);
};
const getTableList = async (params: any) => {
if (!currentTable.value?.url) {
return EMPTY_TABLE_RESPONSE;
}
return request({
url: currentTable.value.url,
method: 'post',
data: {
current: params?.skip || 1,
size: params?.take || DEFAULT_TABLE_PAGE_SIZE,
filter: params?.filter,
sort: [
{
field: 'tm',
dir: 'desc'
}
]
}
});
};
const handleSearch = () => {
refreshTable();
};
watch(
() => searchForm.firstKey,
() => {
syncSecondLevel();
},
{ immediate: true }
);
watch(
() => searchForm.secondKey,
async () => {
resetExtraFilters();
await loadRemoteSelectOptions();
refreshTable();
},
{ immediate: true }
);
watch(
() => props.isActive,
async active => {
if (!active) {
return;
}
await loadBaseData();
syncSecondLevel();
},
{ immediate: true }
);
watch(
() => modelStore.params.stcd,
async (newStcd, oldStcd) => {
if (!props.isActive || !newStcd || newStcd === oldStcd) {
return;
}
await loadBaseData();
syncSecondLevel();
resetExtraFilters();
await loadRemoteSelectOptions();
refreshTable();
}
);
</script>
<style lang="scss" scoped>
.normal-operation-data {
width: 100%;
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-bottom: 16px;
}
.search-item {
display: flex;
align-items: center;
flex: 0 1 auto;
min-width: 0;
}
.search-control {
width: 100%;
min-width: 0;
}
.select-item {
flex-basis: 150px;
}
.select-item1 {
flex-basis: 160px;
}
.select-item2 {
flex-basis: 80px;
}
.input-item {
flex-basis: 250px;
}
.input-item1 {
flex-basis: 210px;
}
.select-control {
width: 100%;
}
.input-control {
width: 100%;
}
.search-actions {
display: flex;
align-items: center;
gap: 12px;
flex: 0 0 auto;
}
.attachment-modal-body {
height: 60vh;
}
}
</style>

View File

@ -0,0 +1,522 @@
import {
miaoZhongJiLu,
miaoZhongPeiYu,
qinYuPeiYuFangShi,
qinYuPeiYuShuiWen,
qinYuPeiYuShuiZi,
qinYuPeiYuTouWei,
qinYuSiWanJiLu,
qinYuXinXi,
taoTaiQinYu,
yuBingFangZhi,
yuLeiRenGo,
yuLeiShouJin,
yuleifuhua
} from './proliferationColumns';
export interface OperationFilterOption {
label: string;
value: string | number;
name?: string;
id?: string | number;
}
export interface OperationFilterField {
type: 'select' | 'input';
name: string;
label: string;
placeholder?: string;
width?: string | number;
key?: string | string[];
url?: string;
method?: 'get' | 'post';
params?: Record<string, any>;
options?: OperationFilterOption[];
}
export interface OperationTableItem {
title: string;
key: string;
url: string;
columns: any[];
filter: OperationFilterField[];
}
export interface OperationTextGroup {
ctitle: string;
key: string;
proTable: OperationTableItem[];
disable?: boolean;
}
export interface OperationStepItem {
title: string;
key: string;
typeIcon?: string;
arrowIcon?: string;
textList: OperationTextGroup[];
stepIcon?: string;
}
export const operationStepList: OperationStepItem[] = [
{
title: '亲鱼选配与培育',
typeIcon: 'fishStation2',
arrowIcon: 'arrowDownLine2',
key: '1',
textList: [
{
ctitle: '亲鱼选配',
key: '1-1',
proTable: [
{
title: '亲鱼信息',
key: '1-1',
url: '/wmp-env-server/fb/bsmfr/qgc/GetKendoListCust',
columns: qinYuXinXi,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'sex',
label: '性别',
width: 120,
url: '',
options: [
{ label: '雄鱼', value: 1 },
{ label: '雌鱼', value: 2 }
]
},
{
type: 'input',
name: 'signnum',
label: '标记编号',
key: ['signnum', 'length'],
placeholder: '搜索标记编号、全长',
url: '',
options: []
}
]
},
{
title: '亲鱼死亡记录',
key: '1-2',
url: '/wmp-env-server/fb/bsdeathr/qgc/GetKendoListCust',
columns: qinYuSiWanJiLu,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'input',
name: 'signnum',
label: '标记编号',
key: ['spod', 'deathd', 'length'],
placeholder: '搜索死亡症状、死亡诊断、全长',
url: '',
options: []
}
]
}
],
disable: false
},
{
ctitle: '亲鱼培育',
key: '1-2',
proTable: [
{
title: '亲鱼培育方式',
key: '1-3',
url: '/wmp-env-server/fb/bsctmob/qgc/GetKendoListCust',
columns: qinYuPeiYuFangShi,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'ctmo',
label: '培育方式',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'CTMO'
}
},
{
type: 'input',
name: 'fpnum',
label: '标记编号',
placeholder: '搜索鱼池名称、全长',
key: ['fpName', 'length'],
url: '',
options: []
}
]
},
{
title: '亲鱼培育水温观测记录',
key: '1-4',
url: '/wmp-env-server/fb/fbbwtr/GetKendoListCust',
columns: qinYuPeiYuShuiWen,
filter: [
{
type: 'input',
name: 'fpName',
label: '鱼池名称',
placeholder: '搜索鱼池名称',
url: '',
options: []
}
]
},
{
title: '亲鱼培育水质监测记录',
key: '1-5',
url: '/wmp-env-server/fb/fbcwqr/GetKendoListCust',
columns: qinYuPeiYuShuiZi,
filter: [
{
type: 'input',
name: 'fpName',
label: '鱼池名称',
placeholder: '搜索鱼池名称',
url: '',
options: []
}
]
},
{
title: '亲鱼培育投喂记录',
key: '1-6',
url: '/wmp-env-server/fb/bsrfir/qgc/GetKendoListCust',
columns: qinYuPeiYuTouWei,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'ftype',
label: '饲料种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'FEEDTP'
}
},
{
type: 'input',
name: 'fpName',
label: '鱼池名称',
placeholder: '搜索鱼池名称',
url: '',
options: []
}
]
}
],
disable: false
},
{
ctitle: '亲鱼补充与淘汰',
key: '1-3',
proTable: [
{
title: '淘汰亲鱼信息',
key: '1-7',
url: '/wmp-env-server/fb/outfishr/qgc/GetKendoListCust',
columns: taoTaiQinYu,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'sex',
label: '性别',
width: 120,
url: '',
options: [
{ label: '雄鱼', value: 1 },
{ label: '雌鱼', value: 2 }
]
},
{
type: 'input',
name: 'length',
label: '全长',
placeholder: '搜索全长'
}
]
}
],
disable: false
}
],
stepIcon: ''
},
{
title: '人工繁殖',
key: '2',
typeIcon: 'fishStation2',
arrowIcon: 'arrowDownLine2',
textList: [
{
ctitle: '亲本选择',
key: '2-1',
proTable: [],
disable: true
},
{
ctitle: '人工催产',
key: '2-2',
proTable: [
{
title: '鱼类人工催产记录',
key: '2-1',
url: '/wmp-env-server/fb/fishartinlr/qgc/GetKendoListCust',
columns: yuLeiRenGo,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'fpitocintype',
label: '催产剂种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'FPITOCINTYPE'
}
},
{
type: 'select',
name: 'sex',
label: '性别',
width: 120,
url: '',
options: [
{ label: '雄鱼', value: 1 },
{ label: '雌鱼', value: 2 }
]
}
]
}
],
disable: false
},
{
ctitle: '人工授精',
key: '2-3',
proTable: [],
disable: true
},
{
ctitle: '孵化',
key: '2-4',
proTable: [
{
title: '鱼类受精卵孵化过程记录',
key: '2-2',
url: '/wmp-env-server/fb/fishhatchrecr/qgc/GetKendoListCust',
columns: yuLeiShouJin,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'fpitocintype',
label: '催产剂种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'FPITOCINTYPE'
}
}
]
},
{
title: '鱼类孵化过程巡查记录',
key: '2-3',
url: '/wmp-env-server/fb/fishhatchpror/qgc/GetKendoListCust',
columns: yuleifuhua,
filter: [
{
type: 'input',
name: 'devno',
label: '孵化设施编号',
placeholder: '搜索孵化设施编号'
}
]
}
],
disable: false
}
],
stepIcon: ''
},
{
title: '鱼苗和鱼种的培育',
key: '3',
typeIcon: 'fishStation2',
arrowIcon: 'arrowDownLine2',
textList: [
{
ctitle: '鱼苗和鱼种的培育',
key: '3-1',
proTable: [
{
title: '苗种培育水环境监测记录',
key: '3-1',
url: '/wmp-env-server/fb/scwemr/GetKendoListCust',
columns: miaoZhongPeiYu,
filter: [
{
type: 'input',
name: 'fpName',
label: '鱼池名称',
placeholder: '搜索鱼池名称'
}
]
},
{
title: '苗种培育记录',
key: '3-2',
url: '/wmp-env-server/fb/fishbreedr/qgc/GetKendoListCust',
columns: miaoZhongJiLu,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'ftype',
label: '饲料种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'FEEDTP'
}
},
{
type: 'input',
name: 'farmno',
label: '鱼池编号',
placeholder: '搜索鱼池名称、摄食情况',
key: ['fpName', 'fin']
}
]
}
],
disable: false
}
],
stepIcon: ''
},
{
title: '鱼病防治',
key: '4',
typeIcon: 'fishStation2',
arrowIcon: 'arrowDownLine2',
textList: [
{
ctitle: '鱼病防治',
key: '4-1',
proTable: [
{
title: '鱼病防治记录',
key: '4-1',
url: '/wmp-env-server/fb/fishdpacr/qgc/GetKendoListCust',
columns: yuBingFangZhi,
filter: [
{
type: 'select',
name: 'ftp',
label: '鱼类',
url: '',
options: []
},
{
type: 'select',
name: 'method',
label: '用药方式',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'method'
}
},
{
type: 'select',
name: 'treateffect',
label: '治疗效果',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'TREATEFFECT'
}
},
{
type: 'select',
name: 'fishdrugtype',
label: '鱼药种类',
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictTreeValue',
method: 'get',
params: {
dictCode: 'FISHDRUGTYPE'
}
},
{
type: 'input',
name: 'fpName',
label: '鱼池名称',
placeholder: '搜索鱼池名称、发病症状',
key: ['fpName', 'symptoms']
}
]
}
],
disable: false
}
],
stepIcon: ''
}
];

View File

@ -0,0 +1,183 @@
<template>
<div class="normal-research-situation">
<!-- 搜索条件 -->
<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="getNormalResearchSituation"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import {
getNormalAddedSituationYear,
getNormalResearchSituation
} from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import BasicTable from '@/components/BasicTable/index.vue';
const modelStore = useModelStore();
const props = defineProps({
isActive: { type: Boolean, default: false }
});
const hasLoaded = ref(false);
const tableRef = ref();
//
const searchParams = ref({
tm: null as string | null
});
//
const tableColumns = ref([
{
title: '科研鱼类',
dataIndex: 'ftp',
width: 150
},
{
title: '已开展工作',
dataIndex: 'wco',
width: 150
},
{
title: '工作详情',
dataIndex: 'jd',
width: 150
}
]);
const tableScrollX = ref(450);
//
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
}
]
},
group: [{ dir: 'desc', field: 'yr' }],
groupResultFlat: true
};
const res: any = await getNormalAddedSituationYear(params);
const yearData = res?.data?.data || [];
if (yearData.length > 0) {
searchParams.value.tm = yearData[0].yr;
refreshTable();
} else {
if (tableRef.value) {
tableRef.value.loading = false;
}
}
} catch (error) {
console.error('获取年份列表失败:', error);
if (tableRef.value) {
tableRef.value.loading = false;
}
}
};
//
const refreshTable = () => {
const year = searchParams.value.tm;
const startDate = `${year}-01-01 00:00:00`;
const endDate = `${year}-12-31 23:59:59`;
const filter = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
},
{
field: 'dt',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'dt',
operator: 'lte',
dataType: 'date',
value: endDate
}
]
};
tableRef.value?.getList(filter);
};
//
const handleSearch = () => {
refreshTable();
};
// isActive
watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
hasLoaded.value = true;
}
},
{ immediate: true }
);
// stcd
watch(
() => modelStore.params.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
searchParams.value = { tm: null };
getYearList();
}
}
);
</script>
<style lang="scss" scoped>
.normal-research-situation {
width: 100%;
.search-bar {
width: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,500 @@
export const yyccg: any[] = [
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: '出港时间',
dataIndex: 'tm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
]
export const Gysyx: any[] = [
{
dataIndexRaw: '',
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: '赶鱼栅运行时间',
dataIndex: 'tm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
]
export const Syjyx: any[] = [
{
dataIndexRaw: '',
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: window.__lyConfigs.baseId == '02' ? '升鱼斗运行时间' : '升鱼机运行时间',
dataIndex: 'tm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
]
export const ZWyyh: any[] = [
{
dataIndexRaw: '',
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: '运行时间',
dataIndex: 'tm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
]
export const Ycyd: any[] = [
{
dataIndexRaw: '',
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: '运行时间',
dataIndex: 'tm',
children: [],
visible: true,
sort: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
]
export const dlmcolumns = [
{
dataIndexRaw: '',
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
children: [],
visible: true,
fixed: 'left',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
children: [],
visible: true,
fixed: 'left',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'tm',
title: '操作时间',
dataIndex: 'tm',
dataFormat: 'YYYY-MM-DD',
dataType: 'date',
children: [],
visible: true,
sort: true,
fixed: 'left',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'aiVal',
title: '操作状态',
dataIndex: 'aiVal',
children: [],
visible: true,
sort: true,
fixed: 'left',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false,
render: (res: any) => (
res == "open" ? "开启" : res == "close" ? "关闭" : '-'
)
},
]

View File

@ -19,6 +19,7 @@
7. 出库水温 综合分析 导出没做
8. 栖息地-流量监测 没有水位视频和流量视频 字段没数据
9. 栖息地 水温水质流量没有数据没法测
10.鱼类增殖站 - 运行数据 要添加字典
-->
<div v-if="modelStore.showStcdSelector" class="stcd-selector-wrapper">
@ -134,6 +135,11 @@
v-show="currentActiveKey === 'FishZHFX'"
:is-active="currentActiveKey === 'FishZHFX'"
/>
<!-- AI运行识别 -->
<AIYXSB
v-show="currentActiveKey === 'AIYXSB'"
:is-active="currentActiveKey === 'AIYXSB'"
/>
<!-- 预警提示 -->
<EarlyWarningAlert
v-show="currentActiveKey === 'tableTabs'"
@ -150,6 +156,22 @@
v-show="currentActiveKey === 'WaterTemperatureRep'"
:is-active="currentActiveKey === 'WaterTemperatureRep'"
/>
<!-- 鱼类增殖站 - 运行数据 -->
<NormalOperationData
v-show="currentActiveKey === 'NormalOperationData'"
:is-active="currentActiveKey === 'NormalOperationData'"
/>
<!-- 增值放流情况 -->
<NormalAddedSituation
v-show="currentActiveKey === 'NormalAddedSituation'"
:is-active="currentActiveKey === 'NormalAddedSituation'"
/>
<!-- 科研情况 -->
<NormalResearchSituation
v-show="currentActiveKey === 'NormalResearchSituation'"
:is-active="currentActiveKey === 'NormalResearchSituation'"
/>
<!-- 查看报告 -->
<Attachment
v-show="currentActiveKey === 'attachment'"
@ -181,10 +203,16 @@ import MonitorInfo from './components/MonitorInfo.vue'; // 电站运行过程线
import WaterTemperature from './components/WaterTemperature.vue'; //
import VerticalWaterTemperature from './components/VerticalWaterTemperature.vue'; //
import WaterQuality from './components/WaterQuality.vue'; //
import AIYXSB from './components/AIYXSB/index.vue'; // AI
import EarlyWarningAlert from './components/EarlyWarningAlert.vue'; //
import FlowMeasure from './components/FlowMeasure.vue'; //
import FishFacilityMonitorData from './components/FishFacilityMonitorData.vue'; // 线
import FishFacilityRunState from './components/FishFacilityRunState.vue'; //
//
import NormalOperationData from './components/NormalOperationData/index.vue'; //
import NormalAddedSituation from './components/NormalAddedSituation.vue'; //
import NormalResearchSituation from './components/NormalResearchSituation.vue'; //
import FishZHFX from './components/FishZHFX.vue'; //
import EcologicalFlow from './components/EcologicalFlow.vue'; //
import Attachment from './components/Attachment.vue'; // /
@ -400,8 +428,7 @@ watch(
let params = newVal;
if (['VD', 'WTRV', 'ZQ', 'WQ'].includes(params?.sttpCode)) {
params.sttp = params.sttpCode;
}else
if (['ENG_ALARM'].includes(params?.sttpMap)) {
} else if (['ENG_ALARM'].includes(params?.sttpMap)) {
params.sttp = params.sttpMap;
}
tabsConfig.value = handleTabs({ ...params });

View File

@ -358,6 +358,51 @@ namespace DateSetting {
value: [getStartTime().subtract(10, 'year'), getStartTime()]
}
],
year1: [
{
label: '近七天',
// 注意:如果项目配置了 isoWeek 插件且希望周一为起始,可使用 startOf('isoWeek')
value: [dayjs().subtract(6, 'day').startOf('day'), dayjs().endOf('day')]
},
{
label: '本月',
value: [dayjs().startOf('month'), dayjs().endOf('month')]
},
{
label: '最近一个月',
value: [
dayjs().subtract(1, 'month').startOf('month'),
dayjs().subtract(1, 'month').endOf('month')
]
},
{
label: '最近三个月',
value: [
dayjs().subtract(3, 'month').startOf('month'),
dayjs().endOf('month')
]
},
{
label: '最近六个月',
value: [
dayjs().subtract(6, 'month').startOf('month'),
dayjs().endOf('month')
]
},
{
label: '最近一年',
value: [getStartTime().subtract(1, 'year'), getStartTime()]
},
{
label: '最近两年',
value: [getStartTime().subtract(2, 'year'), getStartTime()]
},
{
label: '近三年',
value: [getStartTime().subtract(3, 'year'), getStartTime()]
}
],
future: [
{
label: '未来一周',

View File

@ -369,7 +369,7 @@ const handleSearchFinish = (values: any) => {
},
values.stcd && {
field: 'stcd',
operator: 'eq',
operator: 'contains',
dataType: 'string',
value: values.stcd
}