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