WholeProcessPlatform/frontend/src/modules/shuidianhuangjingjieruMod/TwoLayer/ShuiDianKaiFQKTwoLayer.vue

1367 lines
44 KiB
Vue
Raw Normal View History

2026-05-15 08:51:17 +08:00
<script setup lang="ts">
// ==================== 导入依赖 ====================
2026-05-15 18:08:29 +08:00
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
2026-05-15 08:51:17 +08:00
import * as echarts from 'echarts'
import type { EChartsOption } from 'echarts'
import type { ColumnsType } from 'ant-design-vue/es/table/interface'
import { DownloadOutlined } from '@ant-design/icons-vue'
import BasicSearch from '@/components/BasicSearch/index.vue'
import BasicTable from '@/components/BasicTable/index.vue'
import { message } from 'ant-design-vue'
2026-05-15 18:08:29 +08:00
import { useModelStore } from '@/store/modules/model'
import { handleTabs } from '@/components/MapModal/setting.config'
import { getKendoList, getKendoListCust, getIntroduce } from '@/api/shuidiankaifa'
2026-06-09 11:17:07 +08:00
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
2026-05-15 18:08:29 +08:00
// ==================== 开发配置 ====================
const USE_MOCK_DATA = true // 开发时使用模拟数据,生产时改为 false
2026-05-15 08:51:17 +08:00
// ==================== Props 定义 ====================
const props = defineProps<{
defaultJidiInfo?: Record<string, string | number | boolean> // 默认基地信息
seriesName?: string // 系列名称(如:"装机容量(万kW)" 或 "数量(座)"
stateName?: string // 状态名称
themeecList?: any[] // 主题颜色列表(用于饼图颜色自定义)
defaultTab?: string // 默认显示的Tab值'已建'/'在建'
}>()
// ==================== 类型定义 ====================
interface TabItem {
key: string
value: string
col: string
unit: string
count: number
}
interface PieDataItem {
key: string
name: string
value: number
unit: string
dataDimensionRecursive?: string
index?: number
}
interface TableFilterParams {
baseId?: string | Array<{ field: string; operator: string; dataType: string; value: string[] }>
hbrvcd?: string | Array<{ field: string; operator: string; dataType: string; value: string[] }>
bldsttCcode?: string
stnmContains?: string | null
prsc?: Array<{ field: string; operator: string; dataType: string; value: string[] }>
ttpwr?: Array<{ field: string; operator: string }>
sttpCode?: string
}
interface ModalData {
sttp: string
stcd: string
titleName: string
}
// ==================== 响应式状态 ====================
2026-05-15 18:08:29 +08:00
// 引入 modelStore 用于控制全局 MapModal
const modelStore = useModelStore()
2026-06-09 11:17:07 +08:00
const JidiSelectEventStore = useJidiSelectEventStore();
2026-05-15 08:51:17 +08:00
// 基地相关
const qid = ref<string>('')
const jiDiList = ref<any[]>([])
const defJD = ref<string>(props.defaultJidiInfo?.basename as string || '')
// Tab栏相关
const tabIndex = ref<string>('')
const tabKey = ref<string>('')
const tabCol = ref<string>('')
const tabList = ref<TabItem[]>([])
// 搜索相关
const stnm = ref<string | null>(null)
const formRef = ref<any>()
// 饼图相关
const pieData = ref<PieDataItem[]>([])
const pieCode = ref<string[]>([])
const pikData = ref<PieDataItem[]>([])
const chartRef = ref<HTMLElement | null>(null)
const chartInstance = ref<any>(null)
2026-05-15 18:08:29 +08:00
const chartInitialized = ref<boolean>(false) // 关键:图表初始化标志
2026-05-15 08:51:17 +08:00
const dataLoading = ref<boolean>(false)
2026-05-15 18:08:29 +08:00
// 弹窗相关(已废弃,改用全局 MapModal
// const mapModal = ref<boolean>(false)
// const modalData = ref<ModalData>({ sttp: '', stcd: '', titleName: '' })
2026-05-15 08:51:17 +08:00
// 单位配置
const unit = computed(() => getUnitConfigByCode('Other', 'ZJRL').unit)
// 颜色配置优先使用themeecList否则使用默认颜色
const jdColor = ['#5B8FF9', '#5AD8A6', '#F6BD16', '#7262FD', '#FF6B6B', '#9556a4', '#30b7b9']
// 表格引用(用于导出功能)
const tableRef = ref<any>(null)
// ==================== API 服务层(待完善) ====================
/**
* 获取Tab统计数据
*/
async function fetchTabData(params: any): Promise<TabItem[]> {
2026-05-15 18:08:29 +08:00
console.log('【API】获取Tab数据参数:', params)
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 开发模式:返回模拟数据
if (USE_MOCK_DATA) {
return new Promise((resolve) => {
setTimeout(() => {
const _name = `装机容量(${unit.value})`
const useTtpwrFilter = props.seriesName === _name
resolve([
{
2026-05-15 08:51:17 +08:00
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
2026-05-15 18:08:29 +08:00
key: 'build',
2026-05-15 08:51:17 +08:00
unit: useTtpwrFilter ? unit.value : '座',
2026-05-15 18:08:29 +08:00
value: '已建',
count: useTtpwrFilter ? 4568.50 : 120
},
{
2026-05-15 08:51:17 +08:00
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'building',
unit: useTtpwrFilter ? unit.value : '座',
value: '在建',
2026-05-15 18:08:29 +08:00
count: useTtpwrFilter ? 2345.30 : 85
},
{
2026-05-15 08:51:17 +08:00
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
2026-05-15 18:08:29 +08:00
key: 'noBuild',
2026-05-15 08:51:17 +08:00
unit: useTtpwrFilter ? unit.value : '座',
2026-05-15 18:08:29 +08:00
value: '未建',
count: useTtpwrFilter ? 1230.20 : 60
}
])
}, 300)
})
}
// 生产模式:调用真实 API
try {
const _name = `装机容量(${unit.value})`
const useTtpwrFilter = props.seriesName === _name
// 构建排序配置
const aggregates = [{ field: 'ttpwr', aggregate: 'sum' }]
const sortConfig = useTtpwrFilter
? [{ dir: 'desc', field: 'bldsttCcode', aggregates }]
: [{ dir: 'desc', field: 'bldsttCcode' }]
// 调用真实 API
const res = await getKendoList(
useTtpwrFilter
? { ...params, ttpwr: [{ field: 'ttpwr', operator: 'isnotnull' }] }
: params,
sortConfig
)
if (!res?.data || !Array.isArray(res.data)) {
console.warn('【API】Tab数据返回格式异常:', res)
return []
}
// 数据处理逻辑与React版本一致
let list: TabItem[] = []
res.data.reduce((acc: any, cur: any) => {
if (cur.key === '0') {
list.push({
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'noBuild',
unit: useTtpwrFilter ? unit.value : '座',
value: '未建',
count: (cur?.items[0]?.SUM_TTPWR && transUnit(cur?.items[0]?.SUM_TTPWR, 'Other', 'ZJRL')) || cur.items[0].COUNT_ENGID
})
} else if (cur.key === '1') {
list.push({
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'building',
unit: useTtpwrFilter ? unit.value : '座',
value: '在建',
count: (cur?.items[0]?.SUM_TTPWR && transUnit(cur?.items[0]?.SUM_TTPWR, 'Other', 'ZJRL')) || cur.items[0].COUNT_ENGID
})
} else if (cur.key === '2') {
list.push({
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'build',
unit: useTtpwrFilter ? unit.value : '座',
value: '已建',
count: (cur?.items[0]?.SUM_TTPWR && transUnit(cur?.items[0]?.SUM_TTPWR, 'Other', 'ZJRL')) || cur.items[0].COUNT_ENGID
})
}
return list
}, [])
console.log('【API】Tab数据处理结果:', list)
return list
} catch (error) {
console.error('【API】获取Tab数据失败:', error)
message.error('获取Tab数据失败')
return []
}
2026-05-15 08:51:17 +08:00
}
/**
* 获取饼图分组数据完整实现React版本的复杂逻辑
*/
async function fetchPieData(params: any): Promise<PieDataItem[]> {
2026-05-15 18:08:29 +08:00
console.log('【API】获取饼图数据参数:', params)
console.log('【API】USE_MOCK_DATA:', USE_MOCK_DATA, 'qid.value:', qid.value)
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 开发模式:返回模拟数据
if (USE_MOCK_DATA) {
return new Promise((resolve) => {
setTimeout(() => {
const _name = `装机容量(${unit.value})`
const useTtpwrFilter = props.seriesName === _name
console.log('【模拟数据】生成饼图数据useTtpwrFilter:', useTtpwrFilter, 'qid.value:', qid.value)
let mockData: PieDataItem[]
if (qid.value === 'all') {
// 全基地模式:按基地分组
mockData = [
{ key: 'cj', name: '长江流域', value: useTtpwrFilter ? 4568.50 : 120, unit: '', dataDimensionRecursive: 'true' },
{ key: 'hh', name: '黄河流域', value: useTtpwrFilter ? 2345.30 : 85, unit: '', dataDimensionRecursive: 'true' },
{ key: 'zj', name: '珠江流域', value: useTtpwrFilter ? 1230.20 : 60, unit: '', dataDimensionRecursive: 'true' },
{ key: 'hh2', name: '淮河流域', value: useTtpwrFilter ? 890.10 : 45, unit: '', dataDimensionRecursive: 'true' },
{ key: 'hj', name: '海河流域', value: useTtpwrFilter ? 567.80 : 30, unit: '', dataDimensionRecursive: 'true' }
]
console.log('【模拟数据】全基地模式,生成 5 条数据')
} else if (qid.value === 'other') {
// other 模式:按河段分组
mockData = [
{ key: 'river1', name: '长江干流', value: useTtpwrFilter ? 890.50 : 25, unit: '', dataDimensionRecursive: 'true', index: 1 },
{ key: 'river2', name: '黄河干流', value: useTtpwrFilter ? 567.30 : 18, unit: '', dataDimensionRecursive: 'true', index: 2 },
{ key: 'river3', name: '珠江干流', value: useTtpwrFilter ? 345.20 : 12, unit: '', dataDimensionRecursive: 'true', index: 3 }
]
console.log('【模拟数据】other 模式,生成 3 条数据')
} else {
// 单个基地模式:按河段分组
mockData = [
{ key: 'cjgl', name: '长江干流', value: useTtpwrFilter ? 2345.60 : 50, unit: '', dataDimensionRecursive: 'true' },
{ key: 'cjzl', name: '长江支流', value: useTtpwrFilter ? 1234.50 : 35, unit: '', dataDimensionRecursive: 'true' },
{ key: 'cjxz', name: '长江下游', value: useTtpwrFilter ? 987.40 : 25, unit: '', dataDimensionRecursive: 'true' }
]
console.log('【模拟数据】单基地模式,生成 3 条数据')
}
console.log('【模拟数据】✅ 饼图数据生成成功:', mockData.length, '条')
console.log('【模拟数据】数据详情:', mockData)
resolve(mockData)
}, 300)
})
}
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 生产模式:调用真实 API
try {
const _name = `装机容量(${unit.value})`
const useTtpwrFilter = props.seriesName === _name
// 构建排序配置
const aggregates = [{ field: 'ttpwr', aggregate: 'sum' }]
const sortConfig = [
qid.value === 'all' || qid.value === 'other' ? null : { dir: 'asc', field: 'rvcdStepSort' },
qid.value === 'all' ? { dir: 'asc', field: 'baseStepSort' } : null,
{
dir: 'asc',
field: qid.value === 'all' ? 'baseId' : 'hbrvcd'
},
{
dir: 'desc',
field: qid.value === 'all' ? 'baseName' : 'hbrvcdName'
},
{
dir: 'desc',
field: 'bldsttCcode',
aggregates
2026-05-15 08:51:17 +08:00
}
2026-05-15 18:08:29 +08:00
].filter(Boolean)
// 调用真实 API
const res = await getKendoList(
useTtpwrFilter
? { ...params, ttpwr: [{ field: 'ttpwr', operator: 'isnotnull' }] }
: params,
sortConfig as any[]
)
if (!res?.data || !Array.isArray(res.data)) {
console.warn('【API】饼图数据返回格式异常:', res)
return []
}
// 数据处理逻辑完全复刻React版本的flatMap逻辑
let list = res.data.flatMap((item: any) => {
let arr: PieDataItem[] = []
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 关键:根据 qid 决定数据提取路径
const itmeData = (qid.value === 'all' || qid.value === 'other')
? item?.items
: item?.items[0]?.items
itmeData?.map((outItem: any) => {
const itemList = outItem?.items?.[0]?.items?.[0]
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 关键:根据 qid 决定 name 的来源
const name = qid.value === 'all'
? outItem?.items?.[0]?.items?.[0]?.items?.[0]?.BASENAME
: (outItem?.items?.[0]?.items?.[0]?.BASENAME || outItem?.items?.[0]?.items?.[0]?.HBRVCDNAME)
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
if (name) {
arr.push({
key: qid.value === 'all'
? item?.items[0].key
: (qid.value === 'other' ? item?.key : item?.items?.[0]?.key),
dataDimensionRecursive: 'true',
name: name,
unit: '',
value: qid.value === 'all' && useTtpwrFilter
? (itemList?.SUM_TTPWR && +transUnit(itemList?.SUM_TTPWR, 'Other', 'ZJRL'))
: (qid.value === 'all' && !useTtpwrFilter
? itemList?.COUNT_BASEID
: ((outItem?.items?.[0]?.items?.[0]?.SUM_TTPWR && +transUnit(outItem?.items?.[0]?.items?.[0]?.SUM_TTPWR, 'Other', 'ZJRL'))
|| outItem?.items?.[0]?.items?.[0]?.COUNT_BLDSTTCCODE))
})
}
2026-05-15 08:51:17 +08:00
})
2026-05-15 18:08:29 +08:00
return arr
})
// 特殊处理other 类型需要调用 getIntroduce 接口排序
if (qid.value === 'other' && list.length > 0) {
const introRes = await getIntroduce(
[
{ field: 'wbsType', operator: 'eq', value: 'PSB_RVCD' },
{ field: 'objId', operator: 'eq', value: 'other' },
{ field: 'orderIndex', operator: 'isnotnull' }
],
[{ field: 'orderIndex', dir: 'asc' }],
false,
['wbsCode', 'wbsName', 'orderIndex']
)
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
if (introRes?.data?.length && list?.length) {
list = list.map((el: any) => {
const found = introRes.data.find((item: any) => el?.key === item?.wbsCode)
return {
...el,
index: found?.orderIndex
}
}).sort((a: any, b: any) => a?.index - b?.index)
}
}
console.log('【API】饼图数据处理结果:', list)
return list
} catch (error) {
console.error('【API】获取饼图数据失败:', error)
message.error('获取饼图数据失败')
return []
}
2026-05-15 08:51:17 +08:00
}
/**
* 获取other类型的排序信息
*/
async function fetchIntroduce(params: any): Promise<any[]> {
// TODO: 替换为实际API调用
console.log('【模拟】获取排序信息,参数:', params)
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ wbsCode: 'river1', orderIndex: 1 },
{ wbsCode: 'river2', orderIndex: 2 },
{ wbsCode: 'river3', orderIndex: 3 }
])
}, 300)
})
}
/**
* 表格数据请求函数
*/
2026-05-15 18:08:29 +08:00
async function fetchTableData(params: any): Promise<any> {
console.log('【API】获取表格数据参数:', params)
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 开发模式:返回模拟数据
if (USE_MOCK_DATA) {
return new Promise((resolve) => {
setTimeout(() => {
const buildStatus = params.bldsttCcode
const hbrvcdFilter = Array.isArray(params.hbrvcd)
? params.hbrvcd[0]?.value || []
: []
// 电站基础数据模板
const stationTemplates = [
{ name: '三峡', river: 'cjgl', baseName: '长江流域', province: '湖北省' },
{ name: '葛洲坝', river: 'cjgl', baseName: '长江流域', province: '湖北省' },
{ name: '向家坝', river: 'cjgl', baseName: '长江流域', province: '四川省' },
{ name: '溪洛渡', river: 'cjgl', baseName: '长江流域', province: '云南省' },
{ name: '白鹤滩', river: 'cjgl', baseName: '长江流域', province: '四川省' },
{ name: '乌东德', river: 'cjgl', baseName: '长江流域', province: '云南省' },
{ name: '小浪底', river: 'hhgl', baseName: '黄河流域', province: '河南省' },
{ name: '刘家峡', river: 'hhgl', baseName: '黄河流域', province: '甘肃省' },
{ name: '龙羊峡', river: 'hhgl', baseName: '黄河流域', province: '青海省' },
{ name: '拉西瓦', river: 'hhgl', baseName: '黄河流域', province: '青海省' },
{ name: '天生桥', river: 'zjgl', baseName: '珠江流域', province: '贵州省' },
{ name: '岩滩', river: 'zjgl', baseName: '珠江流域', province: '广西区' },
{ name: '大化', river: 'zjgl', baseName: '珠江流域', province: '广西区' },
{ name: '响洪甸', river: 'hhgl2', baseName: '淮河流域', province: '安徽省' },
{ name: '梅山', river: 'hhgl2', baseName: '淮河流域', province: '安徽省' },
{ name: '潘家口', river: 'hjgl', baseName: '海河流域', province: '河北省' },
{ name: '大黑汀', river: 'hjgl', baseName: '海河流域', province: '河北省' },
{ name: '丰满', river: 'sjgl', baseName: '松花江流域', province: '吉林省' },
{ name: '白山', river: 'sjgl', baseName: '松花江流域', province: '吉林省' },
{ name: '水丰', river: 'ljgl', baseName: '辽河流域', province: '辽宁省' }
]
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 根据建设状态筛选电站
let filteredStations = stationTemplates
2026-05-15 08:51:17 +08:00
2026-05-15 18:08:29 +08:00
// 如果有河段过滤,进一步筛选
if (hbrvcdFilter.length > 0) {
filteredStations = stationTemplates.filter(s => hbrvcdFilter.includes(s.river))
2026-05-15 08:51:17 +08:00
}
2026-05-15 18:08:29 +08:00
// 如果没有匹配的河段,返回空数组
if (filteredStations.length === 0) {
// 关键:返回 BasicTable 期望的格式 { data: { records: [], total: 0 } }
resolve({
data: {
records: [],
total: 0
}
})
return
2026-05-15 08:51:17 +08:00
}
2026-05-15 18:08:29 +08:00
// 生成模拟记录
const mockRecords = filteredStations.map((station, index) => {
// 根据不同状态生成不同的装机容量范围
let ttpwrRange
if (buildStatus === '2') {
// 已建:较大容量
ttpwrRange = [5000, 30000]
} else if (buildStatus === '1') {
// 在建:中等容量
ttpwrRange = [2000, 15000]
} else {
// 未建:较小容量或规划中
ttpwrRange = [1000, 8000]
}
const ttpwr = Math.floor(Math.random() * (ttpwrRange[1] - ttpwrRange[0]) + ttpwrRange[0])
const yrge = (ttpwr * 0.04).toFixed(2) // 年发电量约为装机容量的4%
const ttcp = (Math.random() * 200 + 50).toFixed(2)
// 投产时间根据状态设置
let piodt, aiodt
if (buildStatus === '2') {
// 已建:过去的时间
const year = 2000 + Math.floor(Math.random() * 20)
piodt = `${year}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
aiodt = `${year + Math.floor(Math.random() * 5 + 1)}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
} else if (buildStatus === '1') {
// 在建未来1-3年
const year = 2026 + Math.floor(Math.random() * 3)
piodt = `${year}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
aiodt = `${year + Math.floor(Math.random() * 3 + 1)}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
} else {
// 未建:更远的未来
const year = 2028 + Math.floor(Math.random() * 7)
piodt = `${year}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
aiodt = `${year + Math.floor(Math.random() * 5 + 2)}-0${Math.floor(Math.random() * 9 + 1)}-${Math.floor(Math.random() * 28 + 1)}`
}
return {
id: `station_${buildStatus}_${index}`,
stcd: `ST${String(index + 1).padStart(4, '0')}`,
stnm: `${station.name}水电站`,
dvtp: ['堤坝式', '引水式', '混合式'][index % 3],
dvtpName: ['堤坝式', '引水式', '混合式'][index % 3],
ttpwr: ttpwr,
yrge: yrge,
ttcp: ttcp,
baseName: station.baseName,
addvcdName: station.province,
hbrvcdName: station.name === '三峡' ? '长江干流' :
station.name === '葛洲坝' ? '长江干流' :
station.name === '小浪底' ? '黄河干流' :
station.name === '刘家峡' ? '黄河干流' :
station.name === '天生桥' ? '珠江干流' :
station.name === '响洪甸' ? '淮河干流' :
station.name === '潘家口' ? '海河干流' :
station.name === '丰满' ? '松花江干流' :
station.name === '水丰' ? '辽河干流' : `${station.name.substring(0, 2)}河段`,
piodt: piodt,
aiodt: aiodt
}
})
console.log('【调试】模拟表格数据:', {
records: mockRecords.length,
total: mockRecords.length,
firstRecord: mockRecords[0]
})
// 关键:返回 BasicTable 期望的格式 { data: { records: [], total: 0 } }
resolve({
data: {
records: mockRecords,
total: mockRecords.length
}
})
}, 400)
})
}
// 生产模式:调用真实 API
try {
// 根据 qid 决定使用哪个接口
const url = qid.value === 'all'
? '/dec-lygk-base-server/base/vmsstbprpt/GetKendoListCust'
: '/dec-lygk-base-server/base/vmsstbprpt/GetKendoList'
const apiFunc = qid.value === 'all' ? getKendoListCust : getKendoList
// 调用真实 API
const res = await apiFunc(params, [])
// 关键:确保返回格式为 { data: { records: [], total: 0 } }
return {
data: {
records: res?.data?.records || res?.data?.items || [],
total: res?.data?.total || 0
}
}
} catch (error) {
console.error('【API】获取表格数据失败:', error)
message.error('获取表格数据失败')
return {
data: {
records: [],
total: 0
}
}
}
2026-05-15 08:51:17 +08:00
}
// ==================== 工具函数 ====================
/**
2026-05-15 18:08:29 +08:00
* 单位转换函数
2026-05-15 08:51:17 +08:00
*/
function transUnit(value: number, from: string, to: string): number {
2026-05-15 18:08:29 +08:00
// 根据实际业务规则实现
2026-05-15 08:51:17 +08:00
// 当前假设:接口返回的单位是 kW需要转换为万kW
if (from === 'Other' && to === 'ZJRL') {
return value / 10000 // kW → 万kW
}
return value
}
/**
* 获取单位配置
*/
function getUnitConfigByCode(code: string, type: string): { unit: string } {
// TODO: 从配置文件或API获取
// 根据项目实际情况,装机容量通常使用"万kW"作为单位
return { unit: '万kW' }
}
/**
* 处理导出功能
*/
function handleExport() {
if (tableRef.value && typeof tableRef.value.export === 'function') {
// 假设 BasicTable 暴露了 export 方法,请根据实际组件 API 调整
tableRef.value.export()
} else {
message.info('导出功能待实现:请确认 BasicTable 组件是否暴露了导出方法')
console.log('Table Ref:', tableRef.value)
}
}
// ==================== ECharts 饼图配置 ====================
/**
* 计算饼图配置增强版支持themeecList自定义颜色
*/
const chartSetting = computed<EChartsOption>(() => {
2026-05-15 18:08:29 +08:00
console.log('【chartSetting】计算饼图配置pieData.value 长度:', pieData.value.length)
console.log('【chartSetting】pieData.value 内容:', pieData.value)
2026-05-15 08:51:17 +08:00
const total = pieData.value.reduce((acc, item) => acc + item.value, 0).toFixed(2)
const flag = props.seriesName === '数量(座)'
const bbbb = flag ? '电站总数量' : '总装机容量'
const dddd = flag ? '电站数量' : '装机容量'
const cccc = flag ? ' (座)' : ` (${unit.value})`
2026-05-15 18:08:29 +08:00
console.log('【chartSetting】配置信息:', { total, bbbb, dddd, cccc, seriesDataLength: pieData.value.length })
2026-05-15 08:51:17 +08:00
// 关键:根据 themeecList 动态设置颜色与React版本一致
const colors = pieData.value.map((item: any, index: any) => {
if (props.themeecList && props.themeecList.length > 0) {
const themeColor = props.themeecList.find((i: any) => i.name === item.name)
return themeColor?.color || jdColor[index % jdColor.length]
}
return jdColor[index % jdColor.length]
})
return {
color: colors, // 添加颜色配置
tooltip: {
trigger: 'item',
formatter: (params: any) =>
`${dddd}<div>${params.marker}${params.name}: &nbsp;&nbsp;&nbsp;&nbsp;<span style="float:right">${params.value}${cccc}</span></div>`
},
title: [
{
text: String(total || 0), // 修复确保text为string类型
left: 'center',
top: '40%',
textStyle: { fontSize: 24, fontWeight: 'bold', color: '#333' }
},
{
text: bbbb,
left: 'center',
top: '50%',
textStyle: { fontSize: 14, color: '#666' }
},
{
text: cccc,
left: 'center',
top: '55%',
textStyle: { fontSize: 12, color: '#999' }
}
],
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
textStyle: { fontSize: 12 }
},
series: [
{
name: dddd,
type: 'pie',
radius: ['40%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside',
formatter: '{b}\n{c}',
fontSize: 12
},
emphasis: {
scale: true,
scaleSize: 10
},
data: pieData.value.map((item, index) => ({
...item
// 颜色已在顶层color中统一设置
}))
}
]
}
})
/**
* 初始化图表
*/
function initChart() {
2026-05-15 18:08:29 +08:00
console.log('【图表初始化】尝试初始化chartRef.value:', chartRef.value)
console.log('【图表初始化】当前 pieData.value 长度:', pieData.value.length)
if (!chartRef.value) {
console.warn('【图表初始化】容器未就绪,延迟重试')
setTimeout(initChart, 200)
return
}
// 验证容器尺寸
const rect = chartRef.value.getBoundingClientRect()
const containerWidth = chartRef.value.offsetWidth
const containerHeight = chartRef.value.offsetHeight
console.log('【图表初始化】容器信息:', {
offsetWidth: containerWidth,
offsetHeight: containerHeight,
getBoundingClientRect: {
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left
},
display: window.getComputedStyle(chartRef.value).display,
visibility: window.getComputedStyle(chartRef.value).visibility,
parentDisplay: window.getComputedStyle(chartRef.value.parentElement).display
})
// 临时添加红色边框验证容器可见性
chartRef.value.style.border = '3px solid red'
chartRef.value.style.background = 'rgba(255, 0, 0, 0.1)'
if (containerWidth === 0 || containerHeight === 0) {
console.error('【图表初始化】❌ 容器尺寸为0无法初始化')
console.log('【图表初始化】⏰ 200ms 后重试...')
setTimeout(initChart, 200)
return
}
console.log('【图表初始化】✅ 容器尺寸正常,开始初始化 ECharts')
2026-05-15 08:51:17 +08:00
// 关键使用固定的图表尺寸541px × 412px
chartInstance.value = echarts.init(chartRef.value, null, {
width: 541,
height: 412
})
2026-05-15 18:08:29 +08:00
console.log('【图表初始化】ECharts 实例创建成功')
// 绑定图例选择事件
chartInstance.value.on('legendselectchanged', handleLegendSelectChanged)
// 绑定点击事件
chartInstance.value.on('click', handlePieClick)
// 关键:只有当有数据时才调用 updateChart
if (pieData.value.length > 0) {
console.log('【图表初始化】✅ 数据已就绪,渲染图表')
updateChart()
// 关键:延迟调用 resize 确保渲染完成
setTimeout(() => {
chartInstance.value?.resize()
console.log('【图表初始化】✅ resize 完成')
console.log('【图表初始化】图表数据:', pieData.value.length, '条')
chartInitialized.value = true
}, 100)
} else {
console.log('【图表初始化】⏰ 数据未就绪,标记为已初始化,等待 watch 触发更新')
// 设置标志,数据加载后会触发渲染
chartInitialized.value = true
}
2026-05-15 08:51:17 +08:00
}
/**
* 更新图表
*/
function updateChart() {
if (!chartInstance.value) return
chartInstance.value.setOption(chartSetting.value, true)
}
/**
* 处理窗口大小变化
*/
function handleResize() {
chartInstance.value?.resize()
}
/**
* 饼图点击事件完全复刻React版本逻辑
*/
function handlePieClick(params: any) {
if (qid.value !== 'all') {
return // 仅在全基地模式下支持点击联动
}
// 关键根据点击的扇区名称查找对应的基地ID
const matchedOption = searchList.value[0]?.options?.find(
(value: any) => value.label === params.name
)
if (matchedOption) {
qid.value = matchedOption.qid || 'other'
// 关键:获取 dataDimensionRecursive 标志(用于控制表格刷新行为)
const matchedData = pieData.value.find((item: any) => item.name === params.name)
console.log('【饼图点击】切换到基地:', params.name, 'qid:', qid.value, 'dataDimensionRecursive:', matchedData?.dataDimensionRecursive)
}
}
/**
* 图例选择变化事件完全复刻React版本逻辑
*/
function handleLegendSelectChanged(e: any) {
const trueRivers = Object.keys(e.selected).filter(key => e.selected[key])
const allTrue = Object.values(e.selected).every(value => value === false)
const selectedKeys = pikData.value
.filter((item: any) => trueRivers.includes(item.name))
.map((item: any) => item.key)
// 关键与React版本保持一致全选时设置为 ["0"]
pieCode.value = allTrue ? ['0'] : selectedKeys
console.log('【图例联动】allTrue:', allTrue, 'selectedKeys:', selectedKeys, 'pieCode:', pieCode.value)
}
// ==================== 表格列配置 ====================
const columns: ColumnsType = [
{
title: '电站名称',
dataIndex: 'stnm',
key: 'stnm',
fixed: 'left',
ellipsis: true,
width: 150,
customRender: ({ record }: any) => ({
children: record.stnm,
attrs: {
onClick: () => handleRowClick(record)
},
style: {
color: '#1890ff',
cursor: 'pointer'
}
})
},
{
title: '开发方式',
dataIndex: 'dvtp',
key: 'dvtp',
ellipsis: true,
customRender: ({ record }: any) => record.dvtpName || '-'
},
{
title: `装机容量(${unit.value})`,
dataIndex: 'ttpwr',
key: 'ttpwr',
ellipsis: true,
customRender: ({ text }: any) => text ? transUnit(text, 'Other', 'ZJRL').toFixed(2) : '-'
},
{
title: '设计年发电量(亿kW·h)',
dataIndex: 'yrge',
key: 'yrge',
ellipsis: true,
},
{
title: '总库容(亿m³)',
dataIndex: 'ttcp',
key: 'ttcp',
ellipsis: true
},
{
title: '所属基地',
dataIndex: 'baseName',
key: 'baseName',
ellipsis: true
},
{
title: '所属行政区',
dataIndex: 'addvcdName',
key: 'addvcdName',
ellipsis: true
},
{
title: '所在河段',
dataIndex: 'hbrvcdName',
key: 'hbrvcdName',
ellipsis: true
},
{
title: '第一台机组投产时间',
dataIndex: 'piodt',
key: 'piodt',
ellipsis: true,
width: 170
},
{
title: '全部机组投产时间',
dataIndex: 'aiodt',
key: 'aiodt',
ellipsis: true
}
]
/**
* 处理表格行点击打开详情弹窗
*/
function handleRowClick(record: any) {
2026-05-15 18:08:29 +08:00
// 使用全局 MapModal 组件
modelStore.params = {
2026-05-15 08:51:17 +08:00
sttp: 'ENG',
stcd: record.stcd,
titleName: record.stnm
2026-05-15 18:08:29 +08:00
} as any // 临时类型断言,因为 store 定义不完整
modelStore.title = record.stnm ? `${record.stnm} 详情信息` : '详情信息'
modelStore.tabList = handleTabs(modelStore.params)
modelStore.modalVisible = true
console.log('【打开详情弹窗】参数:', modelStore.params)
2026-05-15 08:51:17 +08:00
}
// ==================== 搜索表单配置 ====================
const searchList = computed(() => [
{
type: 'Select',
name: 'Select',
label: '基地选择',
options: jiDiList.value?.map((item: any) => ({
qid: item.baseid,
value: item.basename,
label: item.basename
})) || []
},
{
type: 'Input',
name: 'stnm',
label: '',
fieldProps: {
allowClear: true,
placeholder: '请输入电站名称'
}
}
])
const initialValues = computed(() => ({
Select: defJD.value,
stnm: null
}))
/**
* 处理搜索提交完全复刻React版本逻辑
*/
function handleSearch(values: any) {
// 关键根据选中的基地名称查找对应的qid
const matchedOption = searchList.value[0]?.options?.find(
(value: any) => value.label === values.Select
)
qid.value = matchedOption?.qid || 'other'
stnm.value = values.stnm
console.log('【搜索】基地:', values.Select, 'qid:', qid.value, '电站名称:', stnm.value)
}
/**
* 处理重置
*/
function handleReset() {
qid.value = ''
stnm.value = null
defJD.value = props.defaultJidiInfo?.basename as string || ''
}
// ==================== 数据监听与副作用 ====================
/**
* 表格过滤条件计算完全复刻React版本逻辑
*/
const tableFilter = computed<TableFilterParams>(() => {
let res: TableFilterParams = {
// 关键:根据 pieCode 和 qid 动态决定 baseId 的格式
baseId: (pieCode.value?.length > 0 && qid.value === 'all')
? [{
field: 'baseId',
operator: 'in',
dataType: 'string',
value: pieCode.value
}]
: (qid.value || (props.defaultJidiInfo?.baseid as string)),
sttpCode: 'ENG',
// 关键:根据 pieCode 和 qid 动态决定 hbrvcd 的格式
hbrvcd: (pieCode.value?.length > 0 && qid.value !== 'all')
? [{
field: 'hbrvcd',
operator: 'in',
dataType: 'string',
value: pieCode.value
}]
: undefined,
bldsttCcode: tabKey.value === 'noBuild' ? '0' : tabKey.value === 'building' ? '1' : '2',
stnmContains: stnm.value,
prsc: [
{ field: 'prsc', operator: 'in', dataType: 'string', value: ['1', '2', '3'] }
]
}
// 根据 seriesName 决定是否添加 ttpwr 过滤与React版本保持一致但注释掉
// if (props.seriesName === `装机容量(${unit.value})`) {
// res = { ...res, ttpwr: [{ field: 'ttpwr', operator: 'isnotnull' }] }
// }
// 关键清理无效字段与React版本一致
if (res.baseId === 'all') {
delete res.baseId
}
if (!res.hbrvcd) {
delete res.hbrvcd
}
if (stnm.value === null) {
delete res.stnmContains
}
return res
})
/**
* 监听基地变化获取Tab数据
*/
watch(qid, async (newQid) => {
if (!newQid) return
dataLoading.value = true
try {
const params = {
baseId: newQid,
sttpCode: 'ENG',
prsc: [{ field: 'prsc', operator: 'in', dataType: 'string', value: ['1', '2', '3'] }]
}
const result = await fetchTabData(params)
tabList.value = result
if (result.length > 0) {
// 关键:根据 defaultTab 参数设置默认选中的 Tab
if (props.defaultTab) {
const matchedTab = result.find(item => item.value === props.defaultTab)
if (matchedTab) {
tabIndex.value = matchedTab.value
tabKey.value = matchedTab.key
tabCol.value = matchedTab.col
} else {
// 如果没匹配到,使用第一个 Tab
tabIndex.value = result[0].value
tabKey.value = result[0].key
tabCol.value = result[0].col
}
} else {
// 没有传入 defaultTab使用默认行为第一个 Tab
tabIndex.value = result[0].value
tabKey.value = result[0].key
tabCol.value = result[0].col
}
}
} catch (error) {
console.error('获取Tab数据失败:', error)
message.error('获取Tab数据失败')
} finally {
dataLoading.value = false
}
}, { immediate: true })
/**
* 监听 defaultTab 变化动态切换 Tab解决第二次进入页面时失效的问题
*/
watch(
() => props.defaultTab,
(newDefaultTab) => {
if (!newDefaultTab || tabList.value.length === 0) return
const matchedTab = tabList.value.find(item => item.value === newDefaultTab)
if (matchedTab) {
tabIndex.value = matchedTab.value
tabKey.value = matchedTab.key
tabCol.value = matchedTab.col
}
},
{ immediate: true }
)
/**
* 监听Tab和搜索条件变化获取饼图数据
*/
watch(
[tabKey, tabCol, stnm, qid],
async () => {
2026-05-15 18:08:29 +08:00
console.log('【Watch触发】tabKey:', tabKey.value, 'tabCol:', tabCol.value, 'stnm:', stnm.value, 'qid:', qid.value)
if (!tabKey.value || !tabCol.value) {
console.log('【Watch触发】跳过tabKey 或 tabCol 为空')
return
}
2026-05-15 08:51:17 +08:00
dataLoading.value = true
try {
const params: any = {
stnmContains: stnm.value,
baseId: qid.value,
sttpCode: 'ENG',
bldsttCcode: tabKey.value === 'noBuild' ? '0' : tabKey.value === 'building' ? '1' : '2',
prsc: [{ field: 'prsc', operator: 'in', dataType: 'string', value: ['1', '2', '3'] }]
}
if (props.seriesName === `装机容量(${unit.value})`) {
params.ttpwr = [{ field: 'ttpwr', operator: 'isnotnull' }]
}
if (stnm.value === null) {
delete params.stnmContains
}
2026-05-15 18:08:29 +08:00
console.log('【Watch触发】调用 fetchPieData参数:', params)
2026-05-15 08:51:17 +08:00
const result = await fetchPieData(params)
2026-05-15 18:08:29 +08:00
console.log('【Watch触发】✅ fetchPieData 返回结果:', result.length, '条')
console.log('【Watch触发】数据内容:', result)
2026-05-15 08:51:17 +08:00
// 特殊处理 other 类型
if (qid.value === 'other' && result.length > 0) {
const introRes = await fetchIntroduce({
logic: 'and',
filters: [
{ field: 'wbsType', operator: 'eq', value: 'PSB_RVCD' },
{ field: 'objId', operator: 'eq', value: 'other' },
{ field: 'orderIndex', operator: 'isnotnull' }
]
})
if (introRes?.length) {
result.forEach((item: any) => {
const found = introRes.find((i: any) => item.key === i.wbsCode)
if (found) {
item.index = found.orderIndex
}
})
result.sort((a: any, b: any) => (a.index || 0) - (b.index || 0))
}
}
2026-05-15 18:08:29 +08:00
console.log('【Watch触发】更新 pieData数据量:', result.length)
2026-05-15 08:51:17 +08:00
pikData.value = result
pieData.value = result
pieCode.value = []
2026-05-15 18:08:29 +08:00
console.log('【Watch触发】pieData.value 更新后的值:', pieData.value)
// 关键:等待 DOM 更新
await nextTick()
// 关键:如果图表未初始化,先初始化;否则直接更新
if (!chartInitialized.value) {
console.log('【Watch触发】图表未初始化开始初始化图表')
// 延迟初始化,确保容器完全渲染
setTimeout(() => {
initChart()
}, 200)
} else {
// 图表已初始化,直接更新数据
console.log('【Watch触发】图表已初始化更新数据')
updateChart()
chartInstance.value?.resize()
console.log('【图表更新】数据变化,重新渲染')
}
2026-05-15 08:51:17 +08:00
} catch (error) {
2026-05-15 18:08:29 +08:00
console.error('【API】获取饼图数据失败:', error)
2026-05-15 08:51:17 +08:00
message.error('获取饼图数据失败')
} finally {
dataLoading.value = false
}
},
{ deep: true }
)
/**
* 生命周期钩子
*/
onMounted(() => {
// 模拟加载基地列表(更真实的基地数据)
2026-06-09 11:17:07 +08:00
JidiSelectEventStore.jidiData.forEach(element => {
jiDiList.value.push({
label: element.wbsName,
value: element.wbsCode
})
});
2026-05-15 08:51:17 +08:00
// 设置默认选中"全部基地"
if (!qid.value) {
qid.value = 'all'
}
2026-05-15 18:08:29 +08:00
// 关键:不再在 onMounted 中初始化图表
// 图表初始化由 watch 监听数据加载后触发,避免竞态条件
console.log('【生命周期】onMounted 触发,等待数据加载后初始化图表')
2026-05-15 08:51:17 +08:00
window.addEventListener('resize', handleResize)
})
2026-05-15 18:08:29 +08:00
// 监听 Tab 切换,确保图表在 Tab 激活时正确渲染
watch(tabIndex, async (newTab, oldTab) => {
console.log('【Tab切换】从', oldTab, '切换到', newTab)
// 等待 DOM 更新
await nextTick()
setTimeout(() => {
if (chartInstance.value && chartInitialized.value) {
console.log('【Tab切换】重新调整图表尺寸')
chartInstance.value.resize()
updateChart()
} else if (!chartInitialized.value) {
console.log('【Tab切换】图表未初始化等待数据加载后初始化')
// 不在此处初始化,由 watch([tabKey, tabCol...]) 处理
}
}, 300)
})
2026-05-15 08:51:17 +08:00
onUnmounted(() => {
chartInstance.value?.dispose()
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div class="shuidian-kaifa-container">
<!-- 搜索表单 -->
<BasicSearch
ref="formRef"
:searchList="searchList"
:initialValues="initialValues"
@finish="handleSearch"
@reset="handleReset"
>
<template #actions>
<!-- TODO: 集成ExportBtnFileWithSelect组件 -->
<!-- <ExportBtnFileWithSelect
authCode="sdkfzk-export"
tableRef="tableRef"
exportUrl="/dec-lygk-base-server/base/msstbprpt/export"
:select="['stnm', 'baseName', 'addvcdName', 'rvcdName', 'reachcdName', 'dvtp', 'ttpwr', 'yrge', 'ttcp', 'piodt', 'aiodt']"
:useDefaultColumnSelect="false"
fileName="水电开发情况数据"
/> -->
<a-button @click="handleExport">
<template #icon><DownloadOutlined /></template>
导出
</a-button>
</template>
</BasicSearch>
<!-- Tab切换 -->
<a-tabs v-model:activeKey="tabIndex" class="dwsta-table-tu">
<a-tab-pane
v-for="item in tabList"
:key="item.value"
:tab="`${item.value}(${item.count}${item.unit})`"
>
<!-- 图表+表格布局 -->
<div class="chart-table-layout">
<!-- 左侧饼图 -->
<div class="chart-section">
<div
ref="chartRef"
class="pie-chart"
style="width: 541px; height: 412px;"
></div>
</div>
<!-- 右侧表格 -->
<div class="table-section">
<BasicTable
ref="tableRef"
:columns="columns"
:listUrl="fetchTableData"
:searchParams="tableFilter"
:defaultPageSize="20"
:scrollY="380"
/>
</div>
</div>
</a-tab-pane>
</a-tabs>
2026-05-15 18:08:29 +08:00
<!-- 详情弹窗已改用全局 MapModal 组件 AppMain.vue 中统一管理 -->
<!-- 无需在此处添加 a-modal点击表格行会自动触发 modelStore.modalVisible -->
2026-05-15 08:51:17 +08:00
</div>
</template>
<style scoped lang="scss">
.shuidian-kaifa-container {
display: flex;
flex-direction: column;
height: 700px;
padding: 16px;
background: #fff;
border-radius: 4px;
}
.dwsta-table-tu {
flex: 1;
display: flex;
flex-direction: column;
:deep(.ant-tabs-content) {
height: 100%;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
}
:deep(.ant-tabs-nav) {
margin-bottom: 16px;
}
}
.chart-table-layout {
display: flex;
gap: 16px;
height: calc(100% - 40px);
min-height: 450px;
.chart-section {
flex: 0 0 541px;
background: #fafafa;
border-radius: 4px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
.pie-chart {
width: 541px;
height: 412px;
}
}
.table-section {
flex: 1;
overflow: hidden;
background: #fff;
border-radius: 4px;
}
}
.modal-content {
padding: 16px;
:deep(.ant-descriptions) {
margin-bottom: 16px;
}
}
</style>