<template>
  <div class="home-dashboard">
    <map-scene
      ref="mapSceneRef"
      class="home-map-scene"
      :active-layer="homeMapLayer"
      :admin-boundary-line-scale="2.4"
      @feature-loading="featureLoading = $event"
      @feature-info="handleFeatureInfo"
    />
    <div class="home-map-scrim"></div>

    <div class="home-layer-tools">
      <m-card title="图层控制" :width="248" :height="430">
        <div class="home-layer-tree-panel">
          <div class="home-layer-tree-list">
            <div
              v-for="topic in homeLayerTree"
              :key="topic.key"
              class="home-layer-tree-group"
              :class="{ 'is-open': topic.key === expandedHomeTopicKey }"
            >
              <button
                class="home-layer-tree-parent"
                :class="{ 'is-active': topic.key === activeHomeTopicKey }"
                type="button"
                @click="toggleHomeLayerTopic(topic.key)"
              >
                <span>{{ topic.name }}</span>
                <i></i>
              </button>
              <div v-if="topic.key === expandedHomeTopicKey" class="home-layer-tree-children">
                <button
                  v-for="layer in topic.layers"
                  :key="layer.key"
                  class="home-layer-tree-child"
                  :class="{ 'is-active': layer.key === activeHomeLayerKey }"
                  type="button"
                  @click="toggleHomeLayer(layer.key)"
                >
                  <span>{{ layer.name }}</span>
                  <i></i>
                </button>
              </div>
            </div>
          </div>

          <div class="home-layer-tree-legend">
            <div class="home-layer-tree-title">图例</div>
            <div
              v-if="homeLayerLegends.length"
              class="home-layer-legend-grid"
              :class="{ 'is-single': homeLegendSingle }"
            >
              <button
                v-for="item in homeLayerLegends"
                :key="item.key || item.name"
                class="home-layer-legend-row"
                :class="{
                  'is-selected': isHomeLegendItemSelectable(item) && item.key === selectedHomeLegendKey,
                  'is-checkable': isHomeLegendItemSelectable(item),
                  'is-disabled': !isHomeLegendItemSelectable(item),
                }"
                type="button"
                :disabled="!isHomeLegendItemSelectable(item)"
                :title="item.name"
                @click="toggleHomeLegend(item)"
              >
                <span
                  class="home-layer-legend-swatch"
                  :style="{
                    backgroundColor: item.fillColor,
                    borderColor: item.strokeColor || item.fillColor,
                  }"
                ></span>
                <span class="home-layer-legend-name">{{ item.name }}</span>
                <i v-if="isHomeLegendItemSelectable(item)" class="home-layer-legend-check"></i>
              </button>
            </div>
            <div v-else class="home-layer-tree-empty">选择图层后显示图例</div>
          </div>
        </div>
      </m-card>
    </div>

    <div v-if="featurePanelVisible" class="home-map-feature-info" :style="featurePanelStyle">
      <feature-info-panel
        :feature="selectedFeature"
        :empty="featureEmpty"
        :loading="featureLoading"
        :width="460"
        :height="featurePanelHeight"
      />
    </div>

    <div class="home-left">
      <div class="home-side-stack is-left">
        <m-card
          v-for="panel in leftPanels"
          :key="panel.key"
          class="home-card left-card"
          :title="panel.title"
          :width="sidePanelWidth"
          :height="panel.height"
        >
          <v-chart
            v-if="panel.option"
            :ref="(el) => setHomeChartRef(panel.key, el)"
            class="home-chart home-town-column-chart"
            :option="panel.option"
            :autoresize="true"
          />
          <business-panel-renderer
            v-else
            :panel="panel"
            compact
            :column-label-standard="isHomeStandardColumnPanel(panel)"
          />
        </m-card>
      </div>
    </div>

    <div class="home-right">
      <div class="home-side-stack is-right">
        <m-card
          v-for="panel in rightPanels"
          :key="panel.key"
          :class="['home-card', 'right-card', { 'home-video-card': panel.type === 'video' }]"
          :title="panel.title"
          :width="sidePanelWidth"
          :height="panel.height"
        >
        <div v-if="panel.type === 'video'" class="home-video-monitor">
          <button
            class="home-video-nav home-video-prev"
            type="button"
            aria-label="上一路监控"
            :disabled="homeVideoRows.length <= 1"
            @click="showPrevVideo"
          >‹</button>
          <div
            :key="panel.live.key"
            :ref="setHomeVideoScreenRef"
            class="home-video-screen"
            :class="{ 'is-playing': videoPlaying, 'is-loading': videoLoading }"
            :style="videoScreenStyle(panel.live)"
            @click="playCurrentVideo"
          >
            <div :id="videoElementId" class="home-video-player"></div>
            <div v-if="!videoPlaying" class="home-video-scan"></div>
            <div v-if="!videoPlaying && panel.live.hdPlayUrl" class="home-video-play"></div>
            <div v-if="videoLoading" class="home-video-loading">加载中</div>
            <div v-if="videoError" class="home-video-error">{{ videoError }}</div>
            <div class="home-video-point-name">{{ panel.live.title }}</div>
          </div>
          <button
            class="home-video-nav home-video-next"
            type="button"
            aria-label="下一路监控"
            :disabled="homeVideoRows.length <= 1"
            @click="showNextVideo"
          >›</button>
        </div>
        <div v-else-if="panel.key === 'yak-forecast-trend'" class="home-forecast-trend-card">
          <div class="home-forecast-density-switcher" aria-label="预测粒度切换">
            <button
              v-for="item in forecastGranularityOptions"
              :key="item.key"
              type="button"
              :class="{ active: item.key === activeForecastGranularity }"
              @click="activeForecastGranularity = item.key"
            >
              {{ item.label }}
            </button>
          </div>
          <v-chart
            :ref="(el) => setHomeChartRef(panel.key, el)"
            class="home-chart home-forecast-kline-chart"
            :option="panel.option"
            :autoresize="true"
          />
        </div>
        <v-chart
          v-else-if="panel.option"
          :ref="(el) => setHomeChartRef(panel.key, el)"
          class="home-chart"
          :option="panel.option"
          :autoresize="true"
        />
        <div v-else-if="panel.key === 'home-town-balance-gauge'" class="home-balance-gauge-grid">
          <div
            v-for="(item, index) in homeBalanceGaugeRows"
            :key="item.key"
            class="home-balance-gauge-card"
            :class="{ 'is-active': index === homeBalanceActiveGaugeIndex }"
            :style="homeBalanceGaugeStyle(item)"
          >
            <div class="home-balance-gauge-head">
              <strong>{{ item.status }}</strong>
            </div>
            <div class="home-balance-gauge-ring">
              <div class="home-balance-gauge-value">
                <strong>{{ formatNumber(item.value) }}</strong>
                <em>%</em>
              </div>
            </div>
            <div class="home-balance-gauge-name">{{ item.name }}</div>
            <div class="home-balance-gauge-extra">{{ item.extra }}</div>
          </div>
        </div>
        <business-panel-renderer
          v-else
          :panel="panel"
          compact
          :column-label-standard="isHomeStandardColumnPanel(panel)"
        />
        </m-card>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import * as echarts from "echarts"
import EZUIKit from "ezuikit-js"
import VChart from "vue-echarts"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import FeatureInfoPanel from "@/views/forestGrassWet/components/FeatureInfoPanel.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { forestGrassWetTopics, getLayersByTopic, yakIndustryChainTopics } from "@/config/layers"
import { getHomeMonitorings } from "@/services/homeVideoApi"
import { getTopicBusinessStats } from "@/services/openApi"
import { chartTooltip, withChartDefaults } from "@/utils/chartTheme"
import {
  buildForecastSeries,
  estimatedStock,
  forecastPeriods,
  getForecastPeriod,
  structureRows,
} from "@/views/yakManagement/data"
import { balanceTownRows } from "@/views/grassLivestockBalance/data"
import {
  homeTopStats,
} from "./data"
import liveSceneImage from "@/assets/images/gd-live/live-scene-1.png"

const props = defineProps({
  active: {
    type: Boolean,
    default: true,
  },
})
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const mapSceneRef = ref(null)
const homeVideoScreenRef = ref(null)
const forestGrassWetStats = ref({})
const homeChartRefs = new Map()
let mapAssets = null
let destroyed = false
let chartMotionTimer = null
let chartMotionKickTimer = null
let videoPlayer = null
let videoPlayerReady = false
let videoPlayRun = 0
let videoLoadTimeoutTimer = null
let homeIdleWorkHandle = null
let homeIdleWorkType = ""
let homeIdleWorkLoaded = false
const chartMotionTick = ref(0)
const homeVideoRows = ref([])
const activeVideoManualIndex = ref(0)
const activeForecastGranularity = ref("month")
const videoPlaying = ref(false)
const videoLoading = ref(false)
const videoError = ref("")
const videoElementId = `home-video-player-${Math.random().toString(36).slice(2)}`
const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"]
const sidePanelWidth = 398
const homeViewportHeight = ref(typeof window === "undefined" ? 1080 : window.innerHeight)
const sidePanelTop = 128
const sidePanelGap = 12
const sidePanelBottomGap = 8
const sidePanelMinHeight = 227
const sidePanelHeight = computed(() => {
  const viewportHeight = Math.max(1080, Number(homeViewportHeight.value) || 1080)
  const fittedHeight = Math.floor((viewportHeight - sidePanelTop - sidePanelBottomGap - sidePanelGap * 3) / 4)
  return Math.max(sidePanelMinHeight, fittedHeight)
})
const rightChartPanelHeight = computed(() => sidePanelHeight.value)
const videoPanelHeight = rightChartPanelHeight
const videoLoadTimeoutMs = 12000
const forecastTrendPeriod = getForecastPeriod("2028")
const forecastGranularityOptions = [
  { key: "day", label: "日", unit: "日" },
  { key: "week", label: "周", unit: "周" },
  { key: "month", label: "月", unit: "月" },
]
const homeLayerTopics = [
  ...forestGrassWetTopics.map(({ key, name, color }) => ({ key, name: name.replace("资源", ""), color })),
  ...yakIndustryChainTopics.map(({ key, name, color }) => ({ key, name: "牦牛", color })),
]
const activeHomeTopicKey = ref("")
const expandedHomeTopicKey = ref("")
const activeHomeLayerKey = ref("")
const selectedHomeLegendKey = ref("")
const selectedFeature = ref(null)
const featureEmpty = ref(false)
const featureLoading = ref(false)
const featurePanelPoint = ref(null)
const activeHomeTopicLayers = computed(() => getHomeTopicLayers(activeHomeTopicKey.value))
const homeLayerTree = computed(() =>
  homeLayerTopics.map((topic) => ({
    ...topic,
    layers: getHomeTopicLayers(topic.key),
  }))
)
const baseHomeLayer = computed(() =>
  activeHomeLayerKey.value
    ? activeHomeTopicLayers.value.find((layer) => layer.key === activeHomeLayerKey.value) || null
    : null
)
const homeLayerLegends = computed(() => baseHomeLayer.value?.legends || [])
const homeLegendSelectable = computed(() => homeLayerLegends.value.filter(isHomeSelectableLegend).length > 1)
const homeLegendSingle = computed(() => homeLayerLegends.value.length === 1)
const selectedHomeLegend = computed(() =>
  (baseHomeLayer.value?.legends || []).find((item) => item.key === selectedHomeLegendKey.value) || null
)
const homeMapLayer = computed(() => {
  const layer = baseHomeLayer.value
  if (!layer) return null
  if (!selectedHomeLegend.value?.cqlFilter) return layer
  return {
    ...layer,
    cqlFilter: selectedHomeLegend.value.cqlFilter,
    activeLegend: selectedHomeLegend.value,
    name: selectedHomeLegend.value.name || layer.name,
  }
})
const featurePanelVisible = computed(() => Boolean(featurePanelPoint.value))
const featurePanelHeight = computed(() => {
  if (featureLoading.value || featureEmpty.value || !selectedFeature.value) return 186
  const rowCount = estimateFeatureRowCount(selectedFeature.value)
  return Math.min(480, Math.max(300, 86 + Math.min(rowCount, 12) * 34))
})
const featurePanelStyle = computed(() => {
  if (!featurePanelPoint.value) return {}
  const panelWidth = 460
  const panelHeight = featurePanelHeight.value
  const mapSafeLeft = 430
  const mapSafeRight = 430
  const point = toHomeScreenPoint(featurePanelPoint.value)
  const x = Math.min(
    Math.max(point.x - panelWidth / 2, mapSafeLeft),
    1920 - panelWidth - mapSafeRight,
  )
  const y = Math.min(Math.max(point.y - panelHeight - 36, 188), 1080 - panelHeight - 132)
  return {
    left: `${x}px`,
    top: `${y}px`,
    right: "auto",
    bottom: "auto",
  }
})
const activeStructureIndex = computed(() => getLoopIndex(structureRows.length))
const activeForecastYearIndex = computed(() => getLoopIndex(forecastPeriods.length))
const activeForecastGranularityMeta = computed(() => getForecastGranularity(activeForecastGranularity.value))
const trendSeries = computed(() => buildForecastSeriesByGranularity(forecastTrendPeriod, activeForecastGranularityMeta.value.key))
const activeForecastTrendIndex = computed(() => getLoopIndex(trendSeries.value.labels.length))
const homeBalanceRows = computed(() => {
  const rows = balanceTownRows.filter((item) => item?.name)
  return rows.map((item, index) => ({
    key: `home-balance-${item.areaCode || item.key || item.name}`,
    sourceIndex: index,
    name: item.name,
    value: item.overloadRate,
    unit: "%",
    status: item.level || "总体可控",
    extra: `载畜 ${formatNumber(item.livestockCapacity)}头`,
  }))
})
const homeBalanceGaugeRows = computed(() => {
  const rows = homeBalanceRows.value
  const pageSize = 3
  const start = rows.length ? (Math.floor(chartMotionTick.value / 2) * pageSize) % rows.length : 0
  return Array.from({ length: Math.min(pageSize, rows.length) }, (_, index) => rows[(start + index) % rows.length])
})
const homeBalanceActiveGaugeIndex = computed(() => {
  const length = homeBalanceGaugeRows.value.length
  if (!length) return 0
  return chartMotionTick.value % length
})
const emptyHomeVideo = {
  key: "home-empty-video",
  title: "暂无在线可播放摄像头",
  town: "红原县",
  scene: "牧场监控",
  device: "",
  channel: "",
  cameraCount: 0,
  status: "待接入",
  onlineRate: 0,
  updateTime: "",
  operator: "等待在线摄像头",
  progress: 0,
  image: liveSceneImage,
  playUrl: "",
  hdPlayUrl: "",
  token: "",
}
const activeVideoIndex = computed(() => {
  const length = homeVideoRows.value.length
  if (!length) return 0
  if (videoPlaying.value) return activeVideoManualIndex.value % length
  return (chartMotionTick.value + activeVideoManualIndex.value) % length
})
const currentHomeVideo = computed(() => homeVideoRows.value[activeVideoIndex.value] || emptyHomeVideo)

const leftPanels = computed(() => [
  pickTopicPanel("grassland", "grassland-town"),
  pickTopicPanel("grassland", "grassland-type-proportion"),
  pickTopicPanel("wetland", "wetland-yield"),
  pickTopicPanel("woodland", "woodland-type"),
].filter(Boolean).map((panel, index) => normalizeHomePanel(panel, `forest-grass-wet-${index}`)))

const rightPanels = computed(() => [
  {
    key: "home-video-monitor",
    title: "牧场实时监控",
    type: "video",
    height: videoPanelHeight.value,
    live: currentHomeVideo.value,
  },
  {
    key: "yak-structure",
    title: "畜群结构占比",
    height: rightChartPanelHeight.value,
    option: structureDonutOption.value,
  },
  {
    key: "home-town-balance-gauge",
    title: "草畜平衡超载率",
    type: "gauge",
    height: rightChartPanelHeight.value,
    max: 100,
    rows: homeBalanceGaugeRows.value,
  },
  {
    key: "yak-forecast-trend",
    title: "未来存栏趋势",
    height: rightChartPanelHeight.value,
    option: forecastTrendOption.value,
  },
])

const structureDonutOption = computed(() => withChartDefaults({
  animation: true,
  animationDuration: 900,
  animationDurationUpdate: 700,
  animationEasing: "cubicOut",
  color: palette,
  tooltip: chartTooltip((params) => `${params.name}<br/>占比：${params.percent}%<br/>估算数量：${formatNumber(params.value)}头`),
  legend: {
    orient: "vertical",
    right: 0,
    top: "middle",
    itemWidth: 8,
    itemHeight: 8,
    itemGap: 5,
    formatter(name) {
      const row = structureRows.find((item) => item.name === name)
      return row ? `${name}  ${formatCompact(row.count)}头` : name
    },
    textStyle: { color: "rgba(222, 250, 255, 0.78)", fontSize: 10 },
  },
  series: [
    {
      name: "畜群结构",
      type: "pie",
      selectedMode: "single",
      selectedOffset: 8,
      radius: ["36%", "62%"],
      center: ["30%", "54%"],
      avoidLabelOverlap: true,
      label: { color: "#fff", fontSize: 10, formatter: "{d}%" },
      labelLine: { length: 7, length2: 4, lineStyle: { color: "rgba(223, 250, 255, 0.35)" } },
      itemStyle: { borderColor: "rgba(3, 18, 31, 0.92)", borderWidth: 3 },
      emphasis: {
        scale: true,
        scaleSize: 8,
        itemStyle: { shadowBlur: 16, shadowColor: "rgba(48, 220, 255, 0.58)" },
      },
      data: structureRows.map((item, index) => ({
        name: item.name,
        value: item.count,
        selected: index === activeStructureIndex.value,
      })),
    },
  ],
}))

const threeYearForecastOption = computed(() => withChartDefaults({
  animation: true,
  animationDuration: 1100,
  animationDurationUpdate: 900,
  animationEasing: "cubicOut",
  color: ["#30dcff"],
  tooltip: {
    ...chartTooltip(),
    trigger: "axis",
    axisPointer: { type: "shadow" },
    formatter(params) {
      const row = forecastPeriods[params[0].dataIndex]
      return `${row.label}<br/>预测存栏：${formatNumber(row.stock)}头`
    },
  },
  grid: { left: 50, right: 18, top: 16, bottom: 24 },
  xAxis: {
    type: "category",
    data: forecastPeriods.map((item) => item.shortLabel),
    axisLabel: { color: "rgba(223, 250, 255, 0.76)", fontSize: 11 },
    axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.26)" } },
    axisTick: { show: false },
  },
  yAxis: {
    type: "value",
    min(value) {
      return Math.max(0, Math.floor(value.min / 10000) * 10000)
    },
    axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, margin: 4, formatter: (value) => formatHeadWan(value, 1) },
    splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
  },
  series: [
    {
      name: "预测存栏",
      type: "bar",
      barWidth: 24,
      data: forecastPeriods.map((item, index) => ({
        value: item.stock,
        itemStyle: {
          borderRadius: [9, 9, 1, 1],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: index === activeForecastYearIndex.value ? "#ffd76c" : "#30dcff" },
            { offset: 0.72, color: index === activeForecastYearIndex.value ? "rgba(255, 215, 108, 0.7)" : "rgba(48, 220, 255, 0.62)" },
            { offset: 1, color: index === activeForecastYearIndex.value ? "rgba(255, 215, 108, 0.1)" : "rgba(48, 220, 255, 0.08)" },
          ]),
          shadowBlur: index === activeForecastYearIndex.value ? 16 : 8,
          shadowColor: index === activeForecastYearIndex.value ? "rgba(255, 215, 108, 0.56)" : "rgba(48, 220, 255, 0.28)",
        },
      })),
      itemStyle: {
        borderRadius: [9, 9, 1, 1],
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: "#30dcff" },
          { offset: 0.72, color: "rgba(48, 220, 255, 0.62)" },
          { offset: 1, color: "rgba(48, 220, 255, 0.08)" },
        ]),
      },
      label: {
        show: true,
        position: "top",
        color: "#ffffff",
        fontSize: 10,
        formatter: ({ value }) => formatHeadWan(value),
      },
    },
  ],
}))

const forecastTrendOption = computed(() => {
  const granularity = activeForecastGranularityMeta.value
  const series = trendSeries.value
  const rows = buildForecastKLineRows(series)
  const axisInterval = getForecastAxisInterval(rows.length, granularity.key)
  return withChartDefaults({
    animation: true,
    animationDuration: 1200,
    animationDurationUpdate: 900,
    animationEasing: "cubicOut",
    color: ["#30dcff", "#ffd76c", "#ff8f6b"],
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      triggerOn: "mousemove|click",
      axisPointer: {
        type: "cross",
        crossStyle: { color: "rgba(126, 251, 246, 0.42)" },
        label: {
          color: "#eaffff",
          backgroundColor: "rgba(5, 31, 51, 0.96)",
        },
      },
      formatter(params) {
        const items = Array.isArray(params) ? params : [params]
        const candle = items.find((item) => item.seriesType === "candlestick")
        const row = candle?.data || rows[candle?.dataIndex ?? items[0]?.dataIndex ?? 0]
        if (!row) return ""
        const change = row.close - row.open
        return [
          `${row.label}｜${granularity.unit}度预测`,
          `预测存栏：${formatNumber(row.close)}头`,
          `较上期：${formatSigned(change)}头`,
          `繁殖补充：${formatNumber(row.birth)}头`,
          `出栏扣减：${formatNumber(row.outflow)}头`,
        ].join("<br/>")
      },
    },
    legend: {
      top: 2,
      right: 2,
      data: ["预测存栏", "繁殖补充", "出栏扣减"],
      itemWidth: 10,
      itemHeight: 7,
      itemGap: 7,
      textStyle: { color: "rgba(222, 250, 255, 0.76)", fontSize: 10 },
    },
    grid: { left: 42, right: 32, top: 30, bottom: 24 },
    xAxis: {
      type: "category",
      boundaryGap: true,
      data: series.labels,
      axisLabel: { color: "rgba(223, 250, 255, 0.62)", fontSize: 10, interval: axisInterval },
      axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.28)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        scale: true,
        axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, formatter: formatCompact },
        splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
      },
      {
        type: "value",
        axisLabel: { color: "rgba(223, 250, 255, 0.48)", fontSize: 10, formatter: formatCompact },
        splitLine: { show: false },
      },
    ],
    series: [
      {
        name: "预测存栏",
        type: "candlestick",
        yAxisIndex: 0,
        barWidth: getForecastBarWidth(rows.length, granularity.key),
        data: rows,
        itemStyle: {
          color: "rgba(48, 220, 255, 0.82)",
          color0: "rgba(255, 143, 107, 0.82)",
          borderColor: "#30dcff",
          borderColor0: "#ff8f6b",
          borderWidth: 1.2,
        },
      },
      {
        name: "预测存栏线",
        type: "line",
        yAxisIndex: 0,
        smooth: true,
        symbol: "circle",
        symbolSize: 3,
        data: rows.map((item, index) => ({
          value: item.close,
          symbolSize: index === activeForecastTrendIndex.value ? 7 : 3,
          itemStyle: { color: index === activeForecastTrendIndex.value ? "#ffffff" : "#30dcff" },
        })),
        lineStyle: { width: 1.6, color: "rgba(48, 220, 255, 0.86)" },
        itemStyle: { color: "#30dcff" },
        tooltip: { show: false },
      },
      buildLineSeries("繁殖补充", series.births, "#ffd76c", 1, false, activeForecastTrendIndex.value),
      buildLineSeries("出栏扣减", series.outflow, "#ff8f6b", 1, false, activeForecastTrendIndex.value),
    ],
  })
})

onMounted(() => {
  updateHomeViewportHeight()
  window.addEventListener("resize", updateHomeViewportHeight)
  document.addEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
  emit("module-stats", {
    moduleKey: "home",
    items: homeTopStats,
  })
  initHomeMap()
})

onBeforeUnmount(() => {
  destroyed = true
  window.removeEventListener("resize", updateHomeViewportHeight)
  document.removeEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
  stopVideoPlayer()
  cancelHomeIdleWork()
  stopChartMotion()
  homeChartRefs.clear()
  mapSceneRef.value?.destroyMap?.()
})

watch(activeVideoIndex, () => {
  if (!videoPlaying.value && !videoLoading.value && !videoPlayer) {
    videoError.value = ""
    return
  }
  stopVideoPlayer({ resetError: true })
})

watch(activeHomeLayerKey, () => {
  selectedHomeLegendKey.value = ""
  closeFeaturePanel()
})

watch(selectedHomeLegendKey, () => {
  closeFeaturePanel()
})

watch(
  () => props.active,
  (active) => {
    if (active) {
      mapSceneRef.value?.resume?.()
      startChartMotion()
      if (!homeIdleWorkLoaded) scheduleHomeIdleWork()
    } else {
      mapSceneRef.value?.pause?.()
      cancelHomeIdleWork()
      stopChartMotion()
      stopVideoPlayer({ resetError: true })
    }
  },
)

function initHomeMap() {
  mapAssets = new Assets()
  emit("loading-progress", { progress: 0 })
  mapAssets.instance?.on?.("onProgress", (path, itemsLoaded, itemsTotal) => {
    const total = itemsTotal || 1
    emit("loading-progress", {
      progress: Math.floor((itemsLoaded / total) * 100),
      path,
      itemsLoaded,
      itemsTotal,
    })
  })
  mapAssets.ready?.then(() => loadHomeMapOnce()).catch(() => {
    emit("map-ready")
    scheduleHomeIdleWork()
  })
}

async function loadHomeMapOnce(attempt = 0) {
  if (destroyed) return
  await nextTick()
  const scene = mapSceneRef.value
  const mapLoaded = Boolean(scene?.loadMap?.(mapAssets) || scene?.hasMap?.())
  if (mapLoaded) {
    emit("loading-progress", { progress: 100 })
    emit("map-ready")
    scheduleHomeIdleWork()
    if (props.active) scene.play?.()
    return
  }
  if (attempt < 24) {
    window.setTimeout(() => loadHomeMapOnce(attempt + 1), 120)
    return
  }
  emit("map-ready")
  scheduleHomeIdleWork()
}

function refreshMapView() {
  mapSceneRef.value?.resume?.()
  mapSceneRef.value?.refreshView?.()
}

function resetMap() {
  closeFeaturePanel()
  if (mapSceneRef.value?.hasMap?.()) {
    refreshMapView()
    return
  }
  nextTick(() => {
    loadHomeMapOnce()
  })
}

defineExpose({
  refreshMapView,
  resetMap,
})

function scheduleHomeIdleWork() {
  cancelHomeIdleWork()
  const run = () => {
    homeIdleWorkHandle = null
    homeIdleWorkType = ""
    if (destroyed || !props.active) return
    homeIdleWorkLoaded = true
    void loadForestGrassWetPanels()
    void loadHomeMonitorings()
    if (props.active) startChartMotion()
  }
  if (typeof window.requestIdleCallback === "function") {
    homeIdleWorkType = "idle"
    homeIdleWorkHandle = window.requestIdleCallback(run, { timeout: 1800 })
    return
  }
  homeIdleWorkType = "timeout"
  homeIdleWorkHandle = window.setTimeout(run, 650)
}

function cancelHomeIdleWork() {
  if (!homeIdleWorkHandle) return
  if (homeIdleWorkType === "idle" && typeof window.cancelIdleCallback === "function") {
    window.cancelIdleCallback(homeIdleWorkHandle)
  } else {
    window.clearTimeout(homeIdleWorkHandle)
  }
  homeIdleWorkHandle = null
  homeIdleWorkType = ""
}

function setHomeChartRef(key, chart) {
  if (!key) return
  if (chart) {
    homeChartRefs.set(key, chart)
  } else {
    homeChartRefs.delete(key)
  }
}

function setHomeVideoScreenRef(el) {
  homeVideoScreenRef.value = el || null
}

function startChartMotion() {
  if (!props.active || destroyed) return
  stopChartMotion()
  chartMotionTimer = window.setInterval(playChartMotion, 2600)
  chartMotionKickTimer = window.setTimeout(playChartMotion, 800)
}

function stopChartMotion() {
  if (chartMotionTimer) {
    window.clearInterval(chartMotionTimer)
    chartMotionTimer = null
  }
  if (chartMotionKickTimer) {
    window.clearTimeout(chartMotionKickTimer)
    chartMotionKickTimer = null
  }
}

function playChartMotion() {
  if (destroyed || !props.active) return
  chartMotionKickTimer = null
  chartMotionTick.value += 1
  playYakStructureMotion()
  playThreeYearForecastMotion()
  playForecastTrendMotion()
}

async function loadHomeMonitorings() {
  try {
    const rows = await getHomeMonitorings()
    if (destroyed) return
    homeVideoRows.value = rows
    activeVideoManualIndex.value = 0
    videoError.value = rows.length ? "" : "暂无在线可播放摄像头"
  } catch (error) {
    if (destroyed) return
    homeVideoRows.value = []
    videoError.value = "监控接口暂不可用"
    window.setTimeout(() => {
      if (videoError.value === "监控接口暂不可用") videoError.value = ""
    }, 2400)
  }
}

function videoScreenStyle(live) {
  const image = live?.image || liveSceneImage
  return videoPlaying.value
    ? undefined
    : {
        backgroundImage: `linear-gradient(180deg, rgba(0, 16, 26, 0.08), rgba(0, 16, 26, 0.32)), url(${image})`,
      }
}

function showPrevVideo() {
  stepVideo(-1)
}

function showNextVideo() {
  stepVideo(1)
}

function stepVideo(step) {
  const length = homeVideoRows.value.length
  if (!length) return
  stopVideoPlayer({ resetError: true })
  const current = activeVideoIndex.value
  activeVideoManualIndex.value = (current + step + length) % length
  chartMotionTick.value = 0
}

function homeBalanceGaugeProgress(item) {
  const value = Math.max(0, Number(item?.value || 0))
  const maxValue = Math.max(5, ...homeBalanceRows.value.map((row) => Math.max(0, Number(row.value || 0))))
  return Math.max(6, Math.min(100, (value / maxValue) * 100))
}

function homeBalanceGaugeColor(item) {
  const value = Number(item?.value)
  const status = String(item?.status || item?.level || "")
  if (!Number.isFinite(value)) return "#30dcff"
  if (value > 20 || /严重|重度|高超载/.test(status)) return "#ff5a64"
  if (value > 10 || /中度|预警/.test(status)) return "#ff9a5c"
  if (value > 5 || /轻度/.test(status)) return "#ffe06b"
  if (value <= 0 || /总体可控|合理|正常|达标|无超载/.test(status)) return "#24f6c4"
  return "#30dcff"
}

function homeBalanceGaugeStyle(item) {
  const color = homeBalanceGaugeColor(item)
  return {
    "--balance-color": color,
    "--balance-progress": `${homeBalanceGaugeProgress(item)}%`,
  }
}

async function playCurrentVideo() {
  const live = currentHomeVideo.value
  if (!live?.hdPlayUrl || !live?.token) {
    videoError.value = "暂无在线可播放摄像头"
    return
  }
  if (videoPlaying.value || videoLoading.value) return

  activeVideoManualIndex.value = activeVideoIndex.value
  chartMotionTick.value = 0
  if (videoPlayer) stopVideoPlayer({ resetError: true })
  videoError.value = ""
  videoLoading.value = true
  await nextTick()

  const playRun = ++videoPlayRun
  let player = null
  startVideoLoadTimeout(playRun)
  try {
    const screenRect = homeVideoScreenRef.value?.getBoundingClientRect?.()
    const playerWidth = Math.max(300, Math.floor(screenRect?.width || 340))
    const playerHeight = Math.max(80, Math.floor(screenRect?.height || 92))
    player = new EZUIKit.EZUIKitPlayer({
      id: videoElementId,
      accessToken: live.token,
      url: live.hdPlayUrl,
      audio: 0,
      width: playerWidth,
      height: playerHeight,
      themeData: buildEzvizThemeData(),
      handleError(error) {
        if (playRun !== videoPlayRun) return
        clearVideoLoadTimeout()
        console.warn("EZUIKit player error", error)
        videoError.value = "视频加载失败"
        videoLoading.value = false
        videoPlaying.value = false
        videoPlayRun += 1
        videoPlayer = null
        videoPlayerReady = false
        clearVideoElement()
      },
      handleSuccess() {
        if (playRun !== videoPlayRun) return
        clearVideoLoadTimeout()
        videoError.value = ""
        videoPlayerReady = true
        videoLoading.value = false
      },
      fullScreenChangeCallBack() {},
    })
    if (playRun !== videoPlayRun) {
      stopEzvizPlayer(player)
      clearVideoElement()
      return
    }
    videoPlayer = player
    videoPlaying.value = true
  } catch (error) {
    clearVideoLoadTimeout()
    console.warn("EZUIKit init error", error)
    videoError.value = "视频加载失败"
    stopVideoPlayer()
  }
}

function stopVideoPlayer(options = {}) {
  const { clearElement = true, resetError = false } = options
  clearVideoLoadTimeout()
  const player = videoPlayer
  const ready = videoPlayerReady
  videoPlayer = null
  videoPlayerReady = false
  videoPlayRun += 1
  if (ready) stopEzvizPlayer(player)
  if (clearElement) clearVideoElement()
  videoPlaying.value = false
  videoLoading.value = false
  if (resetError) videoError.value = ""
}

function startVideoLoadTimeout(playRun) {
  clearVideoLoadTimeout()
  videoLoadTimeoutTimer = window.setTimeout(() => {
    if (playRun !== videoPlayRun || !videoLoading.value) return
    videoError.value = "视频加载超时"
    stopVideoPlayer({ resetError: false })
  }, videoLoadTimeoutMs)
}

function clearVideoLoadTimeout() {
  if (!videoLoadTimeoutTimer) return
  window.clearTimeout(videoLoadTimeoutTimer)
  videoLoadTimeoutTimer = null
}

function stopEzvizPlayer(player) {
  if (!player || typeof player.stop !== "function") return
  try {
    const stopped = player.stop()
    if (stopped && typeof stopped.catch === "function") {
      stopped.catch(() => {})
    }
  } catch (error) {
    console.warn("EZUIKit stop error", error)
  }
}

function clearVideoElement() {
  if (typeof document === "undefined") return
  const container = document.getElementById(videoElementId)
  if (container) {
    container.replaceChildren()
  }
}

function buildEzvizThemeData() {
  return {
    header: {
      color: "#1890ff",
      activeColor: "#ffffff",
      backgroundColor: "#000000",
      btnList: [
        { iconId: "deviceID", part: "left", defaultActive: 0, memo: "顶部设备序列号", isrender: 0 },
        { iconId: "deviceName", part: "left", defaultActive: 0, memo: "顶部设备名称", isrender: 0 },
        { iconId: "cloudRec", part: "right", defaultActive: 0, memo: "云存储", isrender: 0 },
        { iconId: "rec", part: "right", defaultActive: 0, memo: "SD卡回放", isrender: 0 },
      ],
    },
    footer: {
      color: "#ffffff",
      activeColor: "#1890ff",
      backgroundColor: "#00000021",
      btnList: [
        { iconId: "play", part: "left", defaultActive: 1, memo: "播放", isrender: 1 },
        { iconId: "capturePicture", part: "left", defaultActive: 0, memo: "截屏按钮", isrender: 0 },
        { iconId: "sound", part: "left", defaultActive: 0, memo: "声音按钮", isrender: 1 },
        { iconId: "pantile", part: "left", defaultActive: 0, memo: "云台控制按钮", isrender: 1 },
        { iconId: "recordvideo", part: "left", defaultActive: 0, memo: "录制按钮", isrender: 0 },
        { iconId: "zoom", part: "left", defaultActive: 0, memo: "电子放大", isrender: 1 },
        { iconId: "hd", part: "right", defaultActive: 0, memo: "清晰度切换按钮", isrender: 1 },
        { iconId: "expend", part: "right", defaultActive: 0, memo: "全局全屏按钮", isrender: 1 },
      ],
    },
  }
}

function playYakStructureMotion() {
  const chart = homeChartRefs.get("yak-structure")
  playChartAction(chart, "畜群结构", activeStructureIndex.value)
}

function playThreeYearForecastMotion() {
  const chart = homeChartRefs.get("yak-three-year-forecast")
  playChartAction(chart, "预测存栏", activeForecastYearIndex.value)
}

function playForecastTrendMotion() {
  const chart = homeChartRefs.get("yak-forecast-trend")
  playChartAction(chart, "预测存栏", activeForecastTrendIndex.value)
}

function playChartAction(chart, seriesName, dataIndex) {
  const instance = typeof chart?.dispatchAction === "function" ? chart : chart?.chart
  if (!instance || dataIndex < 0) return
  instance.dispatchAction({ type: "downplay", seriesName })
  instance.dispatchAction({ type: "hideTip" })
  instance.dispatchAction({ type: "highlight", seriesName, dataIndex })
  instance.dispatchAction({ type: "showTip", seriesName, dataIndex })
}

function getLoopIndex(length) {
  if (!length) return 0
  return chartMotionTick.value % length
}

async function loadForestGrassWetPanels() {
  const [grassland, wetland, woodland] = await Promise.all([
    getTopicBusinessStats("grassland", "grassland-distribution"),
    getTopicBusinessStats("wetland", "wetland-distribution"),
    getTopicBusinessStats("woodland", "woodland-distribution"),
  ])
  if (destroyed) return
  forestGrassWetStats.value = { grassland, wetland, woodland }
}

function pickTopicPanel(topicKey, panelKey) {
  const stats = forestGrassWetStats.value[topicKey]
  const panels = [
    ...(stats?.leftPanels || []),
    ...(stats?.rightPanels || []),
  ]
  return panels.find((panel) => panel?.key === panelKey)
}

function normalizeHomePanel(panel, prefix) {
  const homeTownColumnMetrics = {
    "wetland-yield": "湿地生产力",
  }
  if (homeTownColumnMetrics[panel.key]) {
    return {
      ...panel,
      key: `home-${prefix}-${panel.key}`,
      height: sidePanelHeight.value,
      option: buildHomeTownColumnOption(panel, homeTownColumnMetrics[panel.key]),
    }
  }
  const displayOverrides = {
    "woodland-type": { type: "structure", variant: undefined },
  }
  const override = displayOverrides[panel.key] || {}
  return {
    ...panel,
    ...override,
    key: `home-${prefix}-${panel.key}`,
    height: sidePanelHeight.value,
  }
}

function buildHomeTownColumnOption(panel, metricName) {
  const rows = (panel.rows || []).filter((item) => Number.isFinite(Number(item.value))).slice(0, 10)
  const unit = rows.find((item) => item.unit)?.unit || "万亩"
  const values = rows.map((item) => Number(item.value) || 0)
  const maxValue = Math.max(...values, 1)
  const topValue = Math.ceil(maxValue * 1.08)
  const colors = getHomeTownColumnPalette(panel.key)

  return withChartDefaults({
    animation: true,
    animationDuration: 1000,
    animationDurationUpdate: 800,
    animationEasing: "cubicOut",
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: {
        type: "shadow",
        shadowStyle: { color: "rgba(48, 220, 255, 0.08)" },
      },
      formatter(params) {
        const items = Array.isArray(params) ? params : [params]
        const point = items.find((item) => item.seriesName === metricName) || items[0]
        const row = rows[point?.dataIndex ?? 0]
        if (!row) return ""
        return `${row.name}<br/>${metricName}：${formatNumber(row.value)}${row.unit || unit}`
      },
    },
    grid: { left: 24, right: 14, top: 26, bottom: 58, containLabel: false },
    graphic: [
      {
        type: "text",
        right: 8,
        top: 3,
        style: {
          text: `单位：${unit}`,
          fill: "rgba(224, 246, 255, 0.72)",
          fontSize: 11,
          fontWeight: 600,
        },
      },
    ],
    xAxis: {
      type: "category",
      data: rows.map((item) => item.name),
      axisLabel: {
        interval: 0,
        hideOverlap: false,
        color: "rgba(226, 250, 255, 0.82)",
        fontSize: 11,
        lineHeight: 13,
        margin: 8,
        formatter: formatShortVerticalTownName,
      },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    yAxis: {
      type: "value",
      min: 0,
      max: topValue,
      axisLabel: { show: false },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)" } },
    },
    series: [
      {
        name: "上限",
        type: "bar",
        silent: true,
        barWidth: 13,
        barGap: "-100%",
        data: rows.map(() => topValue),
        itemStyle: {
          borderRadius: [8, 8, 3, 3],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(48, 220, 255, 0.18)" },
            { offset: 1, color: "rgba(48, 220, 255, 0.03)" },
          ]),
        },
        tooltip: { show: false },
        z: 1,
      },
      {
        name: metricName,
        type: "bar",
        barWidth: 13,
        data: rows.map((item, index) => ({
          value: Number(item.value) || 0,
          itemStyle: {
            borderRadius: [8, 8, 3, 3],
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: colors[index % colors.length].top },
              { offset: 0.72, color: colors[index % colors.length].bottom },
              { offset: 1, color: "rgba(24, 114, 185, 0.22)" },
            ]),
            shadowBlur: 10,
            shadowColor: colors[index % colors.length].shadow,
          },
        })),
        label: { show: false },
        emphasis: {
          itemStyle: { shadowBlur: 18 },
        },
        z: 3,
      },
      {
        name: "柱顶",
        type: "pictorialBar",
        symbol: "circle",
        symbolSize: [15, 6],
        symbolOffset: [0, -2],
        symbolPosition: "end",
        data: rows.map((item, index) => ({
          value: Number(item.value) || 0,
          itemStyle: {
            color: colors[index % colors.length].cap,
            opacity: 0.9,
            shadowBlur: 8,
            shadowColor: colors[index % colors.length].capShadow,
          },
        })),
        tooltip: { show: false },
        z: 4,
      },
    ],
  })
}

function getHomeTownColumnPalette(panelKey) {
  if (panelKey === "grassland-town") {
    return [
      { top: "#9BEA55", bottom: "#29DFA7", shadow: "rgba(41, 223, 167, 0.4)", cap: "#9BEA55", capShadow: "rgba(155, 234, 85, 0.52)" },
      { top: "#7FEA6A", bottom: "#24D5C0", shadow: "rgba(36, 213, 192, 0.4)", cap: "#7FEA6A", capShadow: "rgba(127, 234, 106, 0.52)" },
      { top: "#B6E84C", bottom: "#47D982", shadow: "rgba(71, 217, 130, 0.4)", cap: "#B6E84C", capShadow: "rgba(182, 232, 76, 0.52)" },
    ]
  }
  return ["#24f6c4", "#32dcff", "#ffe06b", "#7ee269", "#ff9a62", "#9ea7ff"].map((color) => ({
    top: color,
    bottom: "rgba(50, 220, 255, 0.12)",
    shadow: `${color}66`,
    cap: color,
    capShadow: `${color}88`,
  }))
}

function isHomeStandardColumnPanel(panel) {
  return String(panel?.key || "").includes("grassland-town")
}

function buildLineSeries(name, data, color, yAxisIndex = 0, area = false, activeIndex = -1) {
  return {
    name,
    type: "line",
    yAxisIndex,
    smooth: true,
    symbolSize: 5,
    data: data.map((value, index) => ({
      value,
      symbolSize: index === activeIndex ? 10 : 5,
      itemStyle: { color: index === activeIndex ? "#ffffff" : color },
      label: {
        show: index === activeIndex,
        position: "top",
        color,
        fontSize: 10,
        formatter: ({ value: labelValue }) => formatCompact(labelValue),
      },
    })),
    lineStyle: { width: 2.4, color },
    itemStyle: { color },
    areaStyle: area
      ? {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: `${color}44` },
            { offset: 1, color: `${color}05` },
          ]),
        }
      : undefined,
  }
}

function getForecastGranularity(key) {
  return forecastGranularityOptions.find((item) => item.key === key) || forecastGranularityOptions[2]
}

function buildForecastSeriesByGranularity(period, granularityKey = "month") {
  if (granularityKey === "month") return buildForecastSeries(period)

  const dayCount = Math.max(1, Math.round(Number(period.months || 0) * 30))
  const stepDays = granularityKey === "week" ? 7 : 1
  const count = Math.ceil(dayCount / stepDays) + 1
  const labels = Array.from({ length: count }, (_, index) => {
    const day = Math.min(dayCount, index * stepDays)
    if (index === 0) return "2026-06"
    if (day >= dayCount) return period.label
    return granularityKey === "week" ? `第${index}周` : `第${day}日`
  })
  const stock = labels.map((_, index) => {
    const day = Math.min(dayCount, index * stepDays)
    const ratio = day / dayCount
    const seasonal = Math.sin(index / (granularityKey === "week" ? 1.8 : 8.5)) * Math.sin(Math.PI * ratio) * Math.min(360, period.months * 18)
    return Math.round(estimatedStock + (period.stock - estimatedStock) * ratio + seasonal)
  })
  const births = labels.map((_, index) => Math.round(period.births * (Math.min(dayCount, index * stepDays) / dayCount)))
  const outflow = labels.map((_, index) => Math.round(period.outflow * (Math.min(dayCount, index * stepDays) / dayCount)))
  return { labels, stock, births, outflow }
}

function getForecastAxisInterval(count, granularityKey) {
  if (granularityKey === "day") return Math.max(10, Math.ceil(count / 6))
  if (granularityKey === "week") return Math.max(2, Math.ceil(count / 7))
  return count > 24 ? 4 : count > 14 ? 2 : 1
}

function getForecastBarWidth(count, granularityKey) {
  if (granularityKey === "day") return count > 700 ? 2 : count > 360 ? 3 : 4
  if (granularityKey === "week") return count > 110 ? 4 : 6
  return count > 24 ? 7 : 10
}

function buildForecastKLineRows(series) {
  return (series.labels || []).map((label, index) => {
    const open = Number(series.stock?.[Math.max(index - 1, 0)] || 0)
    const close = Number(series.stock?.[index] || open)
    const birth = Math.max(0, Number(series.births?.[index] || 0) - Number(series.births?.[Math.max(index - 1, 0)] || 0))
    const outflow = Math.max(0, Number(series.outflow?.[index] || 0) - Number(series.outflow?.[Math.max(index - 1, 0)] || 0))
    const wickPadding = Math.max(120, Math.round((birth + outflow) * 0.18))
    const high = Math.max(open, close) + Math.max(wickPadding, Math.round(birth * 0.32))
    const low = Math.max(0, Math.min(open, close) - Math.max(wickPadding, Math.round(outflow * 0.32)))
    return {
      label,
      open,
      close,
      low,
      high,
      birth,
      outflow,
      value: [open, close, low, high],
    }
  })
}

function formatNumber(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return number.toLocaleString("zh-CN", {
    maximumFractionDigits: Number.isInteger(number) ? 0 : 2,
  })
}

function formatSigned(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return `${number > 0 ? "+" : ""}${formatNumber(number)}`
}

function formatCompact(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  if (Math.abs(number) >= 10000) return `${(number / 10000).toFixed(Math.abs(number) >= 100000 ? 0 : 1)}万`
  return Math.round(number).toLocaleString("zh-CN")
}

function formatHeadWan(value, digits = 2) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  if (Math.abs(number) >= 10000) return `${(number / 10000).toFixed(digits)}万`
  return Math.round(number).toLocaleString("zh-CN")
}

function formatVerticalTownName(name) {
  return String(name || "").split("").join("\n")
}

function formatShortVerticalTownName(name) {
  const shortName = String(name || "")
    .replace(/\s+/g, "")
    .slice(0, 3)
  return formatVerticalTownName(shortName)
}

function selectHomeLayerTopic(topicKey) {
  activeHomeTopicKey.value = topicKey
  activeHomeLayerKey.value = ""
  selectedHomeLegendKey.value = ""
}

function toggleHomeLayerTopic(topicKey) {
  if (expandedHomeTopicKey.value === topicKey) {
    expandedHomeTopicKey.value = ""
    return
  }
  expandedHomeTopicKey.value = topicKey
  if (activeHomeTopicKey.value !== topicKey) {
    selectHomeLayerTopic(topicKey)
  }
}

function getHomeTopicLayers(topicKey) {
  const layers = getLayersByTopic(topicKey)
  if (topicKey !== "yak-industry-chain") return layers
  return layers.filter((layer) => layer.key === "yak-recognition-layer")
}

function toggleHomeLayer(layerKey) {
  activeHomeLayerKey.value = activeHomeLayerKey.value === layerKey ? "" : layerKey
}

function toggleHomeLegend(item) {
  if (!isHomeLegendItemSelectable(item)) return
  selectedHomeLegendKey.value = selectedHomeLegendKey.value === item.key ? "" : item.key
}

function isHomeSelectableLegend(item) {
  if (item?.selectable === false) return false
  return Boolean(item?.key && (item.cqlFilter || item.rankingValueKey))
}

function isHomeLegendItemSelectable(item) {
  return homeLegendSelectable.value && isHomeSelectableLegend(item)
}

function updateHomeViewportHeight() {
  homeViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight
}

function toHomeScreenPoint(point) {
  const screen = document.querySelector(".home-dashboard")
  const rect = screen?.getBoundingClientRect()
  if (!rect?.width || !rect?.height) return point
  return {
    x: ((point.x - rect.left) / rect.width) * 1920,
    y: ((point.y - rect.top) / rect.height) * 1080,
  }
}

function estimateFeatureRowCount(feature) {
  const properties = feature?.properties || {}
  const keys = Object.keys(properties).filter((key) => {
    const value = properties[key]
    if (value === undefined || value === null || value === "") return false
    const normalized = key.toLowerCase()
    if (["geom", "wkt", "geometry", "the_geom", "shape_leng", "shape_length", "length", "perimeter"].includes(normalized)) return false
    if (["gid", "id", "objectid", "object_id", "fid", "key_uid", "bsm", "ysdm"].includes(normalized)) return false
    return true
  })
  const hasGeometry = Boolean(feature?.feature?.geometry || feature?.geometry)
  return keys.length + (hasGeometry ? 1 : 0)
}

function handleFeatureInfo(payload) {
  const feature = payload?.feature ?? payload ?? null
  if (!feature) {
    closeFeaturePanel()
    return
  }
  selectedFeature.value = feature
  featureEmpty.value = false
  if (payload?.screen) {
    featurePanelPoint.value = payload.screen
  }
}

function closeFeaturePanel() {
  selectedFeature.value = null
  featureEmpty.value = false
  featurePanelPoint.value = null
  mapSceneRef.value?.clearSelectedFeature?.()
}

function handleOutsideFeaturePointerDown(event) {
  if (!featurePanelVisible.value) return
  const panel = document.querySelector(".home-map-feature-info")
  const target = event.target
  if (panel && target instanceof Node && panel.contains(target)) return
  closeFeaturePanel()
}

</script>

<style lang="scss">
.home-dashboard {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  color: #fff;
  background: #03121c;
}

.home-map-scene {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.home-map-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(circle at 50% 52%, transparent 0%, transparent 42%, rgba(1, 9, 15, 0.22) 74%, rgba(1, 8, 13, 0.58) 100%),
    linear-gradient(90deg, rgba(0, 12, 20, 0.36), transparent 24%, transparent 76%, rgba(0, 12, 20, 0.36));
}

.home-layer-tools {
  position: absolute;
  top: auto;
  right: 458px;
  bottom: 72px;
  left: auto;
  z-index: 7;
  width: 248px;
  box-sizing: border-box;
  pointer-events: auto;

  .m-card {
    width: 100% !important;
    height: 430px !important;
    background: rgba(3, 18, 30, 0.54);
    backdrop-filter: blur(2px);
  }
}

.home-map-feature-info {
  position: absolute;
  z-index: 8;
  pointer-events: auto;

  .m-card {
    background: rgba(3, 18, 30, 0.58);
    backdrop-filter: blur(2px);
  }
}

.home-layer-tree-panel {
  display: grid;
  grid-template-rows: minmax(150px, 210px) minmax(0, 1fr);
  gap: 12px;
  height: 100%;
  box-sizing: border-box;
  padding: 12px 12px 14px;
  overflow: hidden;
}

.home-layer-tree-list {
  display: flex;
  flex-direction: column;
  gap: 7px;
  min-height: 0;
  padding-right: 4px;
  overflow-y: auto;
  scrollbar-width: none;

  &::-webkit-scrollbar {
    display: none;
  }
}

.home-layer-tree-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.home-layer-tree-parent,
.home-layer-tree-child {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-width: 0;
  height: 27px;
  flex: 0 0 27px;
  border: 1px solid rgba(48, 220, 255, 0.22);
  color: rgba(255, 255, 255, 0.74);
  font-size: 12px;
  cursor: pointer;
  background: rgba(3, 20, 32, 0.58);

  span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.home-layer-tree-parent {
  padding: 0 9px;
  font-weight: 700;

  i {
    width: 7px;
    height: 7px;
    border-right: 1px solid rgba(255, 255, 255, 0.62);
    border-bottom: 1px solid rgba(255, 255, 255, 0.62);
    transform: rotate(-45deg);
    transition: transform 0.18s ease;
  }
}

.home-layer-tree-group.is-open .home-layer-tree-parent i {
  transform: rotate(45deg) translateY(-2px);
}

.home-layer-tree-parent.is-active {
  color: #ffffff;
  border-color: rgba(48, 220, 255, 0.66);
  background: linear-gradient(90deg, rgba(21, 121, 130, 0.5), rgba(3, 20, 32, 0.78));
}

.home-layer-tree-children {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-left: 12px;
}

.home-layer-tree-child {
  padding: 0 9px;

  i {
    width: 8px;
    height: 8px;
    border: 1px solid rgba(255, 255, 255, 0.42);
    border-radius: 50%;
  }
}

.home-layer-tree-child.is-active {
  color: #ffffff;
  border-color: rgba(48, 220, 255, 0.7);
  background: linear-gradient(90deg, rgba(21, 121, 130, 0.52), rgba(3, 20, 32, 0.78));

  i {
    border-color: #30dcff;
    background: #30dcff;
    box-shadow: 0 0 12px rgba(48, 220, 255, 0.88);
  }
}

.home-layer-tree-legend {
  min-height: 0;
  padding-top: 9px;
  border-top: 1px solid rgba(48, 220, 255, 0.18);
  overflow: visible;
}

.home-layer-tree-title {
  position: relative;
  margin-bottom: 7px;
  padding-left: 10px;
  color: rgba(219, 249, 255, 0.9);
  font-size: 13px;
  font-weight: 600;
  line-height: 16px;

  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 4px;
    width: 4px;
    height: 8px;
    background: #30dcff;
    box-shadow: 0 0 10px rgba(48, 220, 255, 0.7);
  }
}

.home-layer-legend-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 96px));
  justify-content: center;
  align-content: start;
  gap: 7px 8px;
  max-height: calc(100% - 24px);
  padding-right: 4px;
  overflow-y: auto;
  scrollbar-width: none;

  &::-webkit-scrollbar {
    display: none;
  }

  &.is-single {
    grid-template-columns: minmax(0, 128px);
  }
}

.home-layer-legend-row {
  display: grid;
  grid-template-columns: 15px minmax(0, 1fr);
  align-items: start;
  column-gap: 6px;
  min-width: 0;
  min-height: 22px;
  padding: 3px 2px;
  border: 1px solid transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
  background: transparent;

  &:disabled {
    opacity: 1;
  }

  &.is-checkable {
    grid-template-columns: 15px minmax(0, 1fr) 12px;
  }

  &.is-selected {
    border-color: rgba(48, 220, 255, 0.46);
    background: rgba(48, 220, 255, 0.08);
  }

  &.is-disabled {
    cursor: default;
  }
}

.home-layer-legend-swatch {
  width: 12px;
  height: 12px;
  margin-top: 2px;
  border: 1px solid currentColor;
}

.home-layer-legend-name {
  min-width: 0;
  color: rgba(236, 251, 255, 0.74);
  font-size: 11px;
  line-height: 16px;
  word-break: break-all;
}

.home-layer-legend-check {
  width: 7px;
  height: 7px;
  margin-top: 4px;
  border: 1px solid rgba(255, 255, 255, 0.42);
  border-radius: 50%;
}

.home-layer-legend-row.is-selected .home-layer-legend-check {
  border-color: #30dcff;
  background: #30dcff;
  box-shadow: 0 0 10px rgba(48, 220, 255, 0.72);
}

.home-layer-tree-empty {
  padding-top: 18px;
  color: rgba(219, 249, 255, 0.46);
  font-size: 12px;
  text-align: center;
}

.home-left,
.home-right {
  position: absolute;
  top: 128px;
  bottom: 8px;
  z-index: 5;
  width: 398px;
  perspective: 700px;
  perspective-origin: 50% 50%;
  pointer-events: all;
}

.home-left {
  left: 32px;
}

.home-right {
  right: 32px;
}

.home-side-stack {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  transform-style: preserve-3d;
}

.home-side-stack.is-left {
  transform: translate3d(0, 0, 0) rotateY(6deg);
  transform-origin: left center;
}

.home-side-stack.is-right {
  transform: translate3d(0, 0, 0) rotateY(-6deg);
  transform-origin: right center;
}

.home-card {
  flex: 0 0 auto;

  .m-card-bd-content {
    top: 40px;
    bottom: 10px;
    color: #f3feff;
  }

  .m-card-bd-bg {
    filter: brightness(1.08) saturate(1.04);
  }

  .m-card-bd-bg::after {
    content: "";
    position: absolute;
    inset: 4px 5px 6px;
    z-index: 0;
    pointer-events: none;
    border: 1px solid rgba(48, 220, 255, 0.14);
    background:
      linear-gradient(180deg, rgba(17, 73, 92, 0.16), rgba(6, 29, 44, 0.14)),
      linear-gradient(90deg, rgba(48, 220, 255, 0.1), transparent 42%, rgba(48, 220, 255, 0.06));
    box-shadow:
      inset 0 0 0 1px rgba(48, 220, 255, 0.06),
      inset 0 12px 28px rgba(48, 220, 255, 0.04);
  }
}

.home-dashboard .m-card-hd-title {
  color: #ffffff !important;
  background: none !important;
  -webkit-text-fill-color: #ffffff !important;
  font-weight: 800 !important;
  filter: brightness(1.2) contrast(1.08) !important;
  text-shadow:
    0 0 1px rgba(255, 255, 255, 0.96),
    0 0 8px rgba(255, 255, 255, 0.22) !important;
}

.home-chart {
  width: 100%;
  height: 100%;
}

.home-balance-gauge-grid {
  display: grid;
  height: 100%;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  padding: 8px 12px 10px;
  box-sizing: border-box;
  overflow: hidden;
}

.home-balance-gauge-card {
  position: relative;
  display: grid;
  min-width: 0;
  min-height: 0;
  grid-template-rows: 32px minmax(72px, 1fr) 22px 16px;
  justify-items: center;
  padding: 10px 8px 9px;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.18);
  background:
    radial-gradient(circle at 50% 46%, color-mix(in srgb, var(--balance-color) 14%, transparent), transparent 55%),
    linear-gradient(180deg, rgba(48, 220, 255, 0.075), rgba(5, 27, 40, 0.46));
  box-shadow:
    inset 0 0 18px rgba(48, 220, 255, 0.07),
    0 0 0 rgba(48, 220, 255, 0);
  transition:
    border-color 0.35s ease,
    box-shadow 0.35s ease,
    background 0.35s ease;

  &::before {
    position: absolute;
    inset: 9px;
    content: "";
    border: 1px solid rgba(255, 255, 255, 0.035);
    pointer-events: none;
  }

  &.is-active {
    border-color: color-mix(in srgb, var(--balance-color) 64%, rgba(48, 220, 255, 0.22));
    background:
      radial-gradient(circle at 50% 46%, color-mix(in srgb, var(--balance-color) 20%, transparent), transparent 58%),
      linear-gradient(180deg, rgba(48, 220, 255, 0.095), rgba(5, 27, 40, 0.52));
    box-shadow:
      inset 0 0 22px rgba(48, 220, 255, 0.09),
      0 0 14px color-mix(in srgb, var(--balance-color) 16%, transparent);
  }
}

.home-balance-gauge-head {
  position: relative;
  z-index: 1;
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;

  span,
  strong {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span {
    color: rgba(238, 253, 255, 0.58);
    font-size: 10px;
    font-weight: 700;
  }

  strong {
    width: 100%;
    color: var(--balance-color);
    font-size: 11px;
    font-weight: 900;
    text-align: center;
    text-shadow: 0 0 8px color-mix(in srgb, var(--balance-color) 34%, transparent);
  }
}

.home-balance-gauge-ring {
  position: relative;
  z-index: 1;
  align-self: center;
  width: min(94px, 78%);
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    conic-gradient(from 218deg, var(--balance-color) 0 var(--balance-progress), rgba(93, 175, 199, 0.18) var(--balance-progress) 100%),
    rgba(3, 23, 35, 0.78);
  box-shadow:
    0 0 18px color-mix(in srgb, var(--balance-color) 22%, transparent),
    inset 0 0 18px rgba(0, 0, 0, 0.42);

  &::before {
    position: absolute;
    inset: 12px;
    content: "";
    border-radius: 50%;
    background:
      radial-gradient(circle at 50% 45%, rgba(48, 220, 255, 0.12), rgba(3, 23, 35, 0.98) 62%),
      rgba(3, 23, 35, 0.96);
    box-shadow:
      inset 0 0 12px rgba(0, 0, 0, 0.42),
      inset 0 0 0 1px rgba(48, 220, 255, 0.08);
  }

  &::after {
    position: absolute;
    left: 50%;
    bottom: 9px;
    width: 2px;
    height: 16px;
    content: "";
    background: rgba(0, 10, 18, 0.8);
    transform: translateX(-50%) rotate(35deg);
    transform-origin: 50% 100%;
    box-shadow: 0 0 5px rgba(0, 10, 18, 0.58);
  }
}

.home-balance-gauge-value {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;

  strong {
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 24px;
    line-height: 28px;
    font-weight: 900;
    text-shadow: 0 0 10px color-mix(in srgb, var(--balance-color) 42%, transparent);
  }

  em {
    margin-left: 2px;
    color: rgba(238, 253, 255, 0.76);
    font-size: 11px;
    font-style: normal;
    font-weight: 800;
  }
}

.home-balance-gauge-name,
.home-balance-gauge-extra {
  position: relative;
  z-index: 1;
  max-width: 100%;
  overflow: hidden;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-balance-gauge-name {
  color: #ffffff;
  font-size: 15px;
  line-height: 21px;
  font-weight: 900;
  text-shadow: 0 0 8px rgba(48, 220, 255, 0.18);
}

.home-balance-gauge-extra {
  color: rgba(238, 253, 255, 0.66);
  font-size: 11px;
  line-height: 16px;
  font-weight: 700;
}

.home-forecast-trend-card {
  position: relative;
  width: 100%;
  height: 100%;
}

.home-forecast-kline-chart {
  padding-top: 14px;
  box-sizing: border-box;
}

.home-forecast-density-switcher {
  position: absolute;
  top: 0;
  left: 4px;
  z-index: 3;
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid rgba(48, 220, 255, 0.22);
  border-radius: 3px;
  background: rgba(4, 24, 37, 0.72);
  box-shadow: inset 0 0 10px rgba(48, 220, 255, 0.08);

  button {
    min-width: 24px;
    height: 18px;
    padding: 0 6px;
    border: 0;
    border-radius: 2px;
    color: rgba(226, 250, 255, 0.66);
    font-size: 10px;
    line-height: 18px;
    font-weight: 800;
    background: transparent;
    cursor: pointer;

    &.active {
      color: #041c2b;
      background: linear-gradient(180deg, #7efbf6, #30dcff);
      box-shadow: 0 0 10px rgba(48, 220, 255, 0.42);
    }
  }
}

.home-video-monitor {
  position: relative;
  display: block;
  height: 100%;
  padding: 2px 18px;
  box-sizing: border-box;
  pointer-events: auto;
}

.home-video-screen {
  position: relative;
  width: 88%;
  height: 100%;
  margin: 0 auto;
  min-width: 0;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid rgba(48, 220, 255, 0.34);
  background-color: #020d14;
  background-position: center;
  background-size: cover;
  box-shadow:
    inset 0 0 0 1px rgba(10, 255, 214, 0.08),
    inset 0 0 26px rgba(48, 220, 255, 0.16),
    0 0 16px rgba(18, 145, 190, 0.14);
}

.home-video-screen.is-playing {
  background-image: none !important;
}

.home-video-player,
.home-video-player > div,
.home-video-player video,
.home-video-player canvas,
.home-video-player iframe {
  position: absolute !important;
  inset: 0 !important;
  z-index: 1;
  width: 100% !important;
  height: 100% !important;
}

.home-video-player {
  pointer-events: auto;
}

.home-video-screen .loading-container,
.home-video-screen .loading-item,
.home-video-screen #loading-icon {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

.home-video-nav {
  position: absolute;
  top: 50%;
  z-index: 6;
  width: 22px;
  height: 42px;
  border: 1px solid rgba(48, 220, 255, 0.26);
  color: rgba(227, 253, 255, 0.88);
  font-size: 20px;
  line-height: 20px;
  cursor: pointer;
  background: linear-gradient(180deg, rgba(48, 220, 255, 0.18), rgba(10, 39, 54, 0.56));
  box-shadow: inset 0 0 12px rgba(48, 220, 255, 0.1);
  transform: translateY(-50%);
}

.home-video-nav:disabled {
  cursor: not-allowed;
  opacity: 0.38;
}

.home-video-prev {
  left: 25px;
}

.home-video-next {
  right: 25px;
}

.home-video-screen::before {
  position: absolute;
  inset: 0;
  z-index: 2;
  content: "";
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 34%, rgba(0, 0, 0, 0.44)),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0, rgba(255, 255, 255, 0.04) 1px, transparent 1px, transparent 5px);
}

.home-video-screen.is-playing::before {
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.36), transparent 28%, rgba(0, 0, 0, 0.24));
}

.home-video-scan {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(126, 251, 246, 0.2), transparent);
  transform: translateX(-100%);
  animation: homeVideoScan 3.2s linear infinite;
}

.home-video-play {
  position: absolute;
  left: 50%;
  top: 46%;
  z-index: 4;
  width: 30px;
  height: 30px;
  border: 1px solid rgba(255, 255, 255, 0.62);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.36);
  transform: translate(-50%, -50%);
  box-shadow: 0 0 12px rgba(48, 220, 255, 0.2);
}

.home-video-play::after {
  position: absolute;
  left: 12px;
  top: 8px;
  width: 0;
  height: 0;
  content: "";
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  border-left: 10px solid #ffffff;
}

.home-video-point-name {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 8px;
  z-index: 4;
  overflow: hidden;
  padding: 4px 8px;
  border-left: 2px solid rgba(69, 233, 164, 0.78);
  color: #ffffff;
  font-size: 12px;
  line-height: 15px;
  font-weight: 800;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: rgba(0, 0, 0, 0.42);
}

.home-video-loading,
.home-video-error {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 5;
  min-width: 72px;
  padding: 4px 8px;
  border: 1px solid rgba(48, 220, 255, 0.32);
  color: #ffffff;
  font-size: 11px;
  line-height: 14px;
  text-align: center;
  background: rgba(0, 0, 0, 0.62);
  transform: translate(-50%, -50%);
}

.home-video-error {
  border-color: rgba(255, 122, 69, 0.44);
  color: #ffb18e;
}

@keyframes homeVideoScan {
  to {
    transform: translateX(100%);
  }
}

</style>
