WholeProcessPlatform/frontend/src/modules/shuidianhuangjingjieruMod/TwoLayer/ShuiDianKaiFQKTwoLayer.vue
2026-06-12 16:59:35 +08:00

1367 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
// ==================== 导入依赖 ====================
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
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'
import { useModelStore } from '@/store/modules/model'
import { handleTabs } from '@/components/MapModal/setting.config'
import { getKendoList, getKendoListCust, getIntroduce } from '@/api/shuidiankaifa'
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
// ==================== 开发配置 ====================
const USE_MOCK_DATA = true // 开发时使用模拟数据,生产时改为 false
// ==================== 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
}
// ==================== 响应式状态 ====================
// 引入 modelStore 用于控制全局 MapModal
const modelStore = useModelStore()
const JidiSelectEventStore = useJidiSelectEventStore();
// 基地相关
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)
const chartInitialized = ref<boolean>(false) // 关键:图表初始化标志
const dataLoading = ref<boolean>(false)
// 弹窗相关(已废弃,改用全局 MapModal
// const mapModal = ref<boolean>(false)
// const modalData = ref<ModalData>({ sttp: '', stcd: '', titleName: '' })
// 单位配置
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[]> {
console.log('【API】获取Tab数据参数:', params)
// 开发模式:返回模拟数据
if (USE_MOCK_DATA) {
return new Promise((resolve) => {
setTimeout(() => {
const _name = `装机容量(${unit.value})`
const useTtpwrFilter = props.seriesName === _name
resolve([
{
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'build',
unit: useTtpwrFilter ? unit.value : '座',
value: '已建',
count: useTtpwrFilter ? 4568.50 : 120
},
{
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'building',
unit: useTtpwrFilter ? unit.value : '座',
value: '在建',
count: useTtpwrFilter ? 2345.30 : 85
},
{
col: useTtpwrFilter ? 'ttpwr' : 'engTotal',
key: 'noBuild',
unit: useTtpwrFilter ? unit.value : '座',
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 []
}
}
/**
* 获取饼图分组数据完整实现React版本的复杂逻辑
*/
async function fetchPieData(params: any): Promise<PieDataItem[]> {
console.log('【API】获取饼图数据参数:', params)
console.log('【API】USE_MOCK_DATA:', USE_MOCK_DATA, 'qid.value:', qid.value)
// 开发模式:返回模拟数据
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)
})
}
// 生产模式:调用真实 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
}
].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[] = []
// 关键:根据 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]
// 关键:根据 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)
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))
})
}
})
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']
)
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 []
}
}
/**
* 获取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)
})
}
/**
* 表格数据请求函数
*/
async function fetchTableData(params: any): Promise<any> {
console.log('【API】获取表格数据参数:', params)
// 开发模式:返回模拟数据
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: '辽宁省' }
]
// 根据建设状态筛选电站
let filteredStations = stationTemplates
// 如果有河段过滤,进一步筛选
if (hbrvcdFilter.length > 0) {
filteredStations = stationTemplates.filter(s => hbrvcdFilter.includes(s.river))
}
// 如果没有匹配的河段,返回空数组
if (filteredStations.length === 0) {
// 关键:返回 BasicTable 期望的格式 { data: { records: [], total: 0 } }
resolve({
data: {
records: [],
total: 0
}
})
return
}
// 生成模拟记录
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
}
}
}
}
// ==================== 工具函数 ====================
/**
* 单位转换函数
*/
function transUnit(value: number, from: string, to: string): number {
// 根据实际业务规则实现
// 当前假设:接口返回的单位是 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>(() => {
console.log('【chartSetting】计算饼图配置pieData.value 长度:', pieData.value.length)
console.log('【chartSetting】pieData.value 内容:', pieData.value)
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})`
console.log('【chartSetting】配置信息:', { total, bbbb, dddd, cccc, seriesDataLength: pieData.value.length })
// 关键:根据 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() {
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')
// 关键使用固定的图表尺寸541px × 412px
chartInstance.value = echarts.init(chartRef.value, null, {
width: 541,
height: 412
})
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
}
}
/**
* 更新图表
*/
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) {
// 使用全局 MapModal 组件
modelStore.params = {
sttp: 'ENG',
stcd: record.stcd,
titleName: record.stnm
} as any // 临时类型断言,因为 store 定义不完整
modelStore.title = record.stnm ? `${record.stnm} ` : ''
modelStore.tabList = handleTabs(modelStore.params)
modelStore.modalVisible = true
console.log('【打开详情弹窗】参数:', modelStore.params)
}
// ==================== 搜索表单配置 ====================
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 () => {
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
}
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
}
console.log('【Watch触发】调用 fetchPieData参数:', params)
const result = await fetchPieData(params)
console.log('【Watch触发】✅ fetchPieData 返回结果:', result.length, '条')
console.log('【Watch触发】数据内容:', result)
// 特殊处理 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))
}
}
console.log('【Watch触发】更新 pieData数据量:', result.length)
pikData.value = result
pieData.value = result
pieCode.value = []
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('【图表更新】数据变化,重新渲染')
}
} catch (error) {
console.error('【API】获取饼图数据失败:', error)
message.error('获取饼图数据失败')
} finally {
dataLoading.value = false
}
},
{ deep: true }
)
/**
* 生命周期钩子
*/
onMounted(() => {
// 模拟加载基地列表(更真实的基地数据)
JidiSelectEventStore.jidiData.forEach(element => {
jiDiList.value.push({
label: element.wbsName,
value: element.wbsCode
})
});
// 设置默认选中"全部基地"
if (!qid.value) {
qid.value = 'all'
}
// 关键:不再在 onMounted 中初始化图表
// 图表初始化由 watch 监听数据加载后触发,避免竞态条件
console.log('【生命周期】onMounted 触发,等待数据加载后初始化图表')
window.addEventListener('resize', handleResize)
})
// 监听 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)
})
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>
<!-- 详情弹窗已改用全局 MapModal 组件,在 AppMain.vue 中统一管理 -->
<!-- 无需在此处添加 a-modal点击表格行会自动触发 modelStore.modalVisible -->
</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>