<template>
  <div class="home-dashboard">
    <three-map-scene
      v-show="threeHomeMapVisible"
      ref="threeMapSceneRef"
      class="home-map-scene home-three-map-scene"
      :show-data-overlays="false"
      :max-pixel-ratio="1.35"
      @play-complete="handleThreeHomeMapReady"
    />
    <map-scene
      v-if="homeRemoteSensingVisible"
      v-show="homeRemoteSensingVisible"
      ref="mapSceneRef"
      class="home-map-scene home-remote-map-scene"
      :active="props.active && homeRemoteSensingVisible"
      :active-layer="homeMapLayer"
      :admin-boundary-line-scale="1.65"
      :initial-zoom="homeMapInitialZoom"
      :minimum-zoom-distance="1"
      :auto-apply-layer-map-view="false"
      :max-zoom="21"
      :enable-imagery-feature-pick="false"
      hide-town-label-stats
      @ready="handleHomeMapReady"
      @feature-loading="featureLoading = $event"
      @feature-info="handleFeatureInfo"
      @map-click="handleHomeMapClick"
      @view-change="refreshHomeFeaturePanelAnchor"
    />
    <div class="home-map-scrim"></div>

    <div class="home-bottom-switcher">
      <topic-switcher
        :model-value="activeHomeToolbarKey"
        :topics="homeToolbarTopics"
        @update:model-value="handleHomeToolbarSelect"
      />
    </div>

    <div v-if="featurePanelVisible" class="home-map-feature-info" :style="featurePanelStyle">
      <feature-info-panel
        :title="featurePanelTitle"
        :feature="selectedFeature"
        :empty="featureEmpty"
        :loading="featureLoading"
        :width="featurePanelWidth"
        :height="featurePanelHeight"
        closable
        @close="closeFeaturePanel"
      />
    </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)"
            visual-version="v21"
          />
        </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"
        >
        <template #extra>
          <button
            v-if="panel.type === 'video' || panel.key === 'home-town-balance-gauge'"
            class="home-card-more"
            type="button"
            :aria-label="panel.type === 'video' ? '查看全部基地监控' : '查看全部乡镇草畜平衡'"
            @click="panel.type === 'video' ? openHomeVideoModal() : openHomeBalanceModal()"
          >
            更多
            <span aria-hidden="true">›</span>
          </button>
        </template>
        <div v-if="panel.type === 'video'" class="home-video-monitor">
          <button
            class="home-video-nav home-video-prev"
            type="button"
            aria-label="上一路监控"
            :disabled="homeVideoCarouselRows.length <= 1"
            @click="showPrevVideo"
          >‹</button>
          <div class="home-video-grid" :class="{ 'is-single': visibleHomeVideos.length <= 1 }">
            <div
              v-for="item in visibleHomeVideos"
              :key="item.key"
              :ref="(el) => setHomeVideoItemRef(el, item)"
              :data-video-index="item.index"
              class="home-video-screen"
              :class="{
                'is-active': item.isActive,
                'is-playing': isHomeVideoPlaying(item.index),
                'is-loading': isHomeVideoLoading(item.index),
              }"
              :style="videoScreenStyle(item.live, item.isActive, item.index)"
              role="button"
              tabindex="0"
              @click="handleHomeVideoScreenClick(item.index, $event)"
              @keydown.enter.prevent="playHomeVideoAt(item.index)"
              @keydown.space.prevent="playHomeVideoAt(item.index)"
            >
              <div class="home-video-player-host"></div>
              <div v-if="isHomeVideoLoading(item.index)" class="home-video-loading">加载中</div>
              <div v-if="homeVideoErrorFor(item.index)" class="home-video-error">{{ homeVideoErrorFor(item.index) }}</div>
              <button
                v-if="item.live.hdPlayUrl && (!isHomeVideoMounted(item.index) || homeVideoErrorFor(item.index))"
                class="home-video-hit"
                type="button"
                aria-label="播放监控"
                @click.stop="playHomeVideoAt(item.index)"
              ></button>
              <div class="home-video-point-name">{{ item.live.title }}</div>
            </div>
          </div>
          <button
            class="home-video-nav home-video-next"
            type="button"
            aria-label="下一路监控"
            :disabled="homeVideoCarouselRows.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)"
          visual-version="v21"
        />
        </m-card>
      </div>
    </div>

    <div
      v-if="homeVideoModalVisible"
      class="home-more-modal"
      role="dialog"
      aria-modal="true"
      aria-label="全部基地监控"
      @click.self="closeHomeVideoModal"
    >
      <section class="home-more-dialog home-video-dialog">
        <header class="home-more-dialog-head">
          <div>
            <h2>基地监控</h2>
            <p>共 {{ homeVideoRows.length }} 路，在线 {{ homeVideoOnlineCount }} 路</p>
          </div>
          <button type="button" title="关闭" aria-label="关闭" @click="closeHomeVideoModal">×</button>
        </header>
        <div
          v-if="homeVideoModalRows.length"
          class="home-video-modal-grid"
        >
          <div
            v-for="item in homeVideoModalRows"
            :key="item.key"
            :ref="(el) => setHomeVideoItemRef(el, item)"
            :data-video-index="item.index"
            class="home-video-modal-item home-video-screen"
            :class="{
              'is-active': item.index === activeVideoIndex,
              'is-playing': isHomeVideoPlaying(item.index),
              'is-loading': isHomeVideoLoading(item.index),
              'is-offline': item.live.status !== '直播中',
            }"
            :style="videoScreenStyle(item.live, item.index === activeVideoIndex, item.index)"
            role="button"
            tabindex="0"
            @click="handleHomeVideoScreenClick(item.index, $event)"
            @keydown.enter.prevent="playHomeVideoAt(item.index)"
            @keydown.space.prevent="playHomeVideoAt(item.index)"
          >
            <div class="home-video-player-host"></div>
            <div v-if="isHomeVideoLoading(item.index)" class="home-video-loading">加载中</div>
            <div v-if="homeVideoErrorFor(item.index)" class="home-video-error">{{ homeVideoErrorFor(item.index) }}</div>
            <button
              v-if="item.live.hdPlayUrl && (!isHomeVideoMounted(item.index) || homeVideoErrorFor(item.index))"
              class="home-video-hit"
              type="button"
              aria-label="播放监控"
              @click.stop="playHomeVideoAt(item.index)"
            ></button>
            <span class="home-video-point-name">{{ item.live.title }}</span>
          </div>
        </div>
        <div v-else class="home-more-empty">暂无监控数据</div>
        <footer v-if="homeVideoModalPageCount > 1" class="home-more-pager">
          <button type="button" :disabled="homeVideoModalPage <= 1" @click="changeHomeVideoModalPage(homeVideoModalPage - 1)">上一页</button>
          <span>{{ homeVideoModalPage }} / {{ homeVideoModalPageCount }}</span>
          <button type="button" :disabled="homeVideoModalPage >= homeVideoModalPageCount" @click="changeHomeVideoModalPage(homeVideoModalPage + 1)">下一页</button>
        </footer>
      </section>
    </div>

    <Teleport to="body">
      <div
        v-if="homeVideoExpandedIndex !== null"
        class="home-video-web-overlay"
        role="dialog"
        aria-modal="true"
        aria-label="基地监控网页全屏"
      >
        <div ref="homeVideoWebStageRef" class="home-video-screen home-video-web-stage is-active is-playing">
          <div class="home-video-web-player-slot"></div>
        </div>
      </div>
    </Teleport>

    <div
      v-if="homeBalanceModalVisible"
      class="home-more-modal"
      role="dialog"
      aria-modal="true"
      aria-label="全部乡镇草畜平衡"
      @click.self="closeHomeBalanceModal"
    >
      <section class="home-more-dialog home-balance-dialog">
        <header class="home-more-dialog-head">
          <div>
            <h2>全县乡镇草畜平衡</h2>
            <p>全部 {{ homeBalanceRows.length }} 个乡镇，超载率沿用草畜平衡结果库口径</p>
          </div>
          <button type="button" title="关闭" aria-label="关闭" @click="closeHomeBalanceModal">×</button>
        </header>
        <div class="home-balance-modal-grid">
          <article
            v-for="item in homeBalanceRows"
            :key="item.key"
            class="home-balance-modal-item"
            :style="homeBalanceGaugeStyle(item)"
          >
            <div class="home-balance-modal-title">
              <strong>{{ item.name }}</strong>
              <span>{{ item.status }}</span>
            </div>
            <div class="home-balance-modal-value">
              <strong>{{ formatNumber(item.value) }}</strong>
              <em>%</em>
            </div>
            <div class="home-balance-modal-track"><i></i></div>
            <p>{{ item.extra }}</p>
          </article>
        </div>
      </section>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, 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 "@/components/CesiumMap/index.vue"
import ThreeMapScene from "@/views/gdMap/map.vue"
import { Assets as ThreeMapAssets } from "@/views/gdMap/assets"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import FeatureInfoPanel from "@/views/forestGrassWet/components/FeatureInfoPanel.vue"
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import {
  forestGrassWetTopics,
  getLayerByKey,
  getLayersByTopic,
} from "@/config/layers"
import { getHomeMonitorings } from "@/services/homeVideoApi"
import { getTopicBusinessStats, getWoodlandClickInfoByWkt } from "@/services/openApi"
import { getLandClickInfo, getWetlandClickInfo } from "@/services/pastureApi"
import { getYakTradeOverview } from "@/services/yakApi"
import { chartTooltip, withChartDefaults } from "@/utils/chartTheme"
import { SCREEN2_LAYOUT, getScreen2SideCardHeight } from "@/utils/screen2Standard"
import { createFeaturePanelLayout, FEATURE_PANEL_WIDTH } from "@/utils/featurePanelLayout"
import {
  buildForecastSeries,
  estimatedStock,
  formatForecastDayLabel,
  forecastPeriods,
  getForecastPeriod,
  getForecastMonthCount,
  structureRows,
} from "@/views/yakManagement/data"
import { balanceTownRows } from "@/views/grassLivestockBalance/data"
import {
  homeTopStats,
  yakTownRows,
} from "./data"
import liveSceneImage from "@/assets/images/gd-live/live-scene-1.png"

const referenceLinePointImage =
  "image://data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAQ5JREFUOE+1la1OQ0EQRs8kCFIQdVCCxIBpCK1AInkDHC+B4QkwvASub4GsaE0NGCRpKhGFVDQZ+Mjem9vNbkphmWTMfjNnd3b2x0iYuxuwH7wNbIewBfAGzORm5nG6ElfM3QU6AXZSkzXG3oEnMxO8thWgux8DR2tAsfxiZs/VYA38Jazi1NBvYCizv+HK4vCRyrfQgIsf7Nm6+bSnjwIeAGeZaGnXQDfoE+ABmGbixwKeAoeJAMHuv3w30ubATQb6KqDKjZPEuAXOMysZAncJbS7gJbCVEAdAKwP8AK4S2vJfgMVLLt6UDtAreWx0W8od7OJXryq16OPwR2j6+WpAyz2wDagape7vARt9AZ+G3HmhiKS3xwAAAABJRU5ErkJggg=="

const magicDetailTitleFieldMap = {
  图斑编号: "patch_no",
  图斑号: "patch_no",
  湿地编号: "land_number",
  草地编号: "land_number",
  林地类型: "dlmc",
  地类: "landUseName",
  地类名称: "landUseName",
  所在乡镇: "townName",
  所在区域: "full_name",
  面积: "acreage",
  图斑面积: "acreage",
  湿地面积: "acreage",
  草场面积: "acreage",
  草原类: "grassTypeName",
  草原型: "grassMinType",
  草原等级: "grasslandLevel",
  草原类型: "basic_type_name",
  小斑干草产量: "xbGcYield",
  小斑鲜草产量: "xbXcYield",
  鲜草产量: "xcYield",
  干草产量: "gcYield",
  海拔: "elevation",
  地貌: "landformName",
  权属单位: "qsdwmc",
  坐落单位: "zldwmc",
  林班: "lin_ban",
  小班: "xiao_ban",
  森林类别: "sen_lin_lb",
  林种: "lin_zhong",
  公益林等级: "gjgyl_bhdj",
  保护等级: "bh_dj",
  质量等级: "zl_dj",
  优势树种: "you_shi_sz",
  起源: "qi_yuan",
  龄组: "ling_zu",
  郁闭度: "yu_bi_du",
  林地权属: "ld_qs",
  土地使用权属: "tdsyqs",
  林木权属: "lmqs",
  生态区位: "stqwmc",
  坡度等级: "slope",
}

const nonBusinessFeatureKeys = new Set([
  "code",
  "msg",
  "message",
  "success",
  "data",
  "result",
  "grassland",
  "wetland",
  "woodland",
  "basicGrassland",
  "dyColumn",
  "geom",
  "wkt",
  "geometry",
  "the_geom",
  "theGeom",
  "shape",
  "shape_leng",
  "shape_length",
  "length",
  "perimeter",
  "gid",
  "id",
  "objectid",
  "object_id",
  "fid",
  "key_uid",
  "bsm",
  "ysdm",
].map((key) => key.toLowerCase()))

const props = defineProps({
  active: {
    type: Boolean,
    default: true,
  },
})
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const mapSceneRef = ref(null)
const threeMapSceneRef = ref(null)
const homeVideoScreenRef = ref(null)
const homeVideoWebStageRef = ref(null)
const homeVideoScreenRefs = new Map()
const forestGrassWetStats = ref({})
const homeChartRefs = new Map()
let destroyed = false
let homeMapReadyReported = false
let chartMotionTimer = null
let chartMotionKickTimer = null
const videoPlayers = new Map()
const videoPlayRuns = new Map()
const videoPlayStarts = new Set()
const videoLoadTimeoutTimers = new Map()
let homeIdleWorkHandle = null
let homeIdleWorkType = ""
let homeIdleWorkLoaded = false
let featureQueryRequestId = 0
let threeHomeAssets = null
let threeHomeAssetLoading = false
let threeHomeAssetLoaded = false
const homeVideoFitRafs = new Map()
const chartMotionTick = ref(0)
const homeVideoRows = ref([])
const todayOutflowHeads = ref(0)
const activeVideoManualIndex = ref(0)
const HOME_VIDEO_AUTO_TICK_SPAN = 4
const homeVideoModalVisible = ref(false)
const homeVideoModalPage = ref(1)
const homeVideoRotationPaused = ref(false)
const homeVideoExpandedIndex = ref(null)
const homeBalanceModalVisible = ref(false)
const activeForecastGranularity = ref("month")
const videoPlaying = ref(false)
const videoLoading = ref(false)
const videoError = ref("")
const videoElementIdPrefix = `home-video-player-${Math.random().toString(36).slice(2)}`
const videoStates = reactive({})
const palette = ["#30dcff", "#27f0c0", "#8bf7ff", "#48cfff", "#48cfff", "#24f6c4"]
const HOME_CHART_TEXT_COLOR = "#ffffff"
const HOME_CHART_LABEL_COLOR = "#ffffff"
const HOME_CHART_MUTED_COLOR = "rgba(246, 253, 255, 0.92)"
const HOME_CHART_AXIS_COLOR = "rgba(242, 253, 255, 0.9)"
const HOME_CHART_GRID_COLOR = "rgba(218, 250, 255, 0.28)"
const sidePanelWidth = SCREEN2_LAYOUT.sideCardWidth
const homeViewportHeight = ref(typeof window === "undefined" ? 1080 : window.innerHeight)
const sidePanelGap = SCREEN2_LAYOUT.sideCardGap
const sidePanelHeight = computed(() => getScreen2SideCardHeight(homeViewportHeight.value))
const rightChartPanelHeight = computed(() => sidePanelHeight.value)
const videoPanelHeight = rightChartPanelHeight
const videoLoadTimeoutMs = 12000
const homeMapInitialZoom = 9
const HOME_3D_TOPIC_KEY = "home-3d"
const forecastTrendPeriod = getForecastPeriod("2028")
const forecastGranularityOptions = [
  { key: "day", label: "日", unit: "日" },
  { key: "week", label: "周", unit: "周" },
  { key: "month", label: "月", unit: "月" },
]
const homeToolbarTopics = [
  { key: "grassland", name: "草地", color: "#A7FF4F", width: 92 },
  { key: "wetland", name: "湿地", color: "#36C8FF", width: 92 },
  { key: "woodland", name: "林地", color: "#31F084", width: 92 },
  { key: "yak", name: "牦牛", color: "#30DCFF", width: 92 },
]
const homeSolidFillStyles = {
  grassland: {
    fill: "#A7FF4F",
    fillOpacity: 0.98,
    opacity: 1,
    maxOpacity: 1,
    stroke: "#F4FFCF",
    strokeOpacity: 0.95,
    strokeWidth: 1.35,
    glowColor: "#DFFF86",
    glowBlur: 18,
    glowOpacity: 0.44,
  },
  wetland: {
    fill: "#36C8FF",
    fillOpacity: 0.97,
    opacity: 1,
    maxOpacity: 1,
    stroke: "#EBFCFF",
    strokeOpacity: 0.95,
    strokeWidth: 1.35,
    glowColor: "#9AF2FF",
    glowBlur: 18,
    glowOpacity: 0.48,
  },
  woodland: {
    fill: "#31F084",
    fillOpacity: 0.97,
    opacity: 1,
    maxOpacity: 1,
    stroke: "#D9FFF0",
    strokeOpacity: 0.94,
    strokeWidth: 1.3,
    glowColor: "#7CFFBF",
    glowBlur: 18,
    glowOpacity: 0.44,
  },
}
const homeToolbarLayerMap = {
  grassland: { topicKey: "grassland", layerKey: "grassland-distribution", type: "raster" },
  wetland: { topicKey: "wetland", layerKey: "wetland-distribution", type: "raster" },
  woodland: { topicKey: "woodland", layerKey: "woodland-distribution", type: "raster" },
  yak: { topicKey: "yak-industry-chain", layerKey: "yak-recognition-layer", type: "vector", opacity: 0.94 },
  pasture: {
    topicKey: "yak-industry-chain",
    layerKey: "yak-industry-points",
    legendKey: "yak-industry-family-pasture",
    type: "vector",
    opacity: 0.96,
  },
  base: {
    topicKey: "yak-industry-chain",
    layerKey: "yak-industry-points",
    legendKeys: [
      "yak-industry-artificial-grassland",
      "yak-industry-scale-breeding",
      "yak-industry-breeding-selection",
    ],
    type: "vector",
    opacity: 0.96,
  },
}
const activeHomeToolbarKey = ref(HOME_3D_TOPIC_KEY)
const selectedFeature = ref(null)
const featureEmpty = ref(false)
const featureLoading = ref(false)
const featurePanelPoint = ref(null)
const homeRemoteSensingVisible = computed(() => activeHomeToolbarKey.value !== HOME_3D_TOPIC_KEY)
const threeHomeMapVisible = computed(() => props.active && !homeRemoteSensingVisible.value)
const homeMapLayer = computed(() => {
  const topicKey = activeHomeToolbarKey.value
  const layerMeta = homeToolbarLayerMap[topicKey]
  if (!layerMeta) return null
  const topic = forestGrassWetTopics.find((item) => item.key === layerMeta.topicKey)
  const layer = getHomeToolbarLayer(layerMeta)
  if (!layer?.layerName && !layer?.dataPath) return null
  const style = homeSolidFillStyles[topicKey]
  const activeLegend = layerMeta.legendKey
    ? (layer.legends || []).find((item) => item.key === layerMeta.legendKey)
    : null
  const activeLegends = Array.isArray(layerMeta.legendKeys)
    ? layerMeta.legendKeys
        .map((key) => (layer.legends || []).find((item) => item.key === key))
        .filter(Boolean)
    : []
  if (layerMeta.type === "vector") {
    return {
      ...layer,
      activeLegend: activeLegend || layer.activeLegend,
      activeLegends: activeLegends.length ? activeLegends : layer.activeLegends,
      name: `${homeToolbarTopics.find((item) => item.key === topicKey)?.name || layer.name}图层`,
      opacity: layerMeta.opacity ?? layer.opacity,
      homePresentationMaxOpacity: layerMeta.opacity ?? 0.96,
      showPointLabels: false,
      hidePointFeatures: false,
    }
  }
  if (!style) return null
  return {
    ...layer,
    name: `${topic?.name || layer.name}分布`,
    styles: "",
    opacity: style.opacity,
    homePresentationMaxOpacity: style.maxOpacity,
    softMask: false,
    homeGlowColor: style.glowColor,
    homeGlowBlur: style.glowBlur,
    homeGlowOpacity: style.glowOpacity,
    sldBody: createHomeSolidFillSld(layer.layerName, style),
  }
})
const featurePanelVisible = computed(() => Boolean(featurePanelPoint.value))
const featurePanelWidth = FEATURE_PANEL_WIDTH
const featurePanelLayout = computed(() => createFeaturePanelLayout({
  point: featurePanelPoint.value ? toHomeScreenPoint(featurePanelPoint.value) : null,
  rowCount: selectedFeature.value ? estimateFeatureRowCount(selectedFeature.value) : 0,
  loading: featureLoading.value,
  empty: featureEmpty.value || !selectedFeature.value,
}))
const featurePanelHeight = computed(() => featurePanelLayout.value.height)
const featurePanelStyle = computed(() => featurePanelLayout.value.style)
const featurePanelTitle = computed(() => {
  const titleMap = {
    grassland: "草地详情",
    wetland: "湿地详情",
    woodland: "林地详情",
    yak: "牦牛识别详情",
  }
  return titleMap[activeHomeToolbarKey.value] || `${homeToolbarTopics.find((item) => item.key === activeHomeToolbarKey.value)?.name || "地图"}详情`
})
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: "",
}
function isOnlineHomeVideo(live) {
  return live?.status === "直播中" && Boolean(live?.hdPlayUrl && live?.token)
}
const homeVideoCarouselRows = computed(() => homeVideoRows.value
  .map((live, index) => ({ key: `home-video-carousel-${live.key || live.title || index}`, index, live }))
  .filter((item) => isOnlineHomeVideo(item.live)))
const activeHomeVideoCarouselIndex = computed(() => {
  const rows = homeVideoCarouselRows.value
  if (!rows.length) return 0
  const manualIndex = normalizeHomeVideoIndex(activeVideoManualIndex.value)
  const manualPosition = rows.findIndex((item) => item.index === manualIndex)
  const basePosition = manualPosition >= 0 ? manualPosition : 0
  if (homeVideoRotationPaused.value || videoPlaying.value || homeVideoModalVisible.value) {
    return basePosition
  }
  return (Math.floor(chartMotionTick.value / HOME_VIDEO_AUTO_TICK_SPAN) + basePosition) % rows.length
})
const activeVideoIndex = computed(() => {
  if (!homeVideoRows.value.length) return 0
  if (homeVideoModalVisible.value) {
    return normalizeHomeVideoIndex(activeVideoManualIndex.value)
  }
  return homeVideoCarouselRows.value[activeHomeVideoCarouselIndex.value]?.index ?? 0
})
const currentHomeVideo = computed(() => homeVideoRows.value[activeVideoIndex.value] || emptyHomeVideo)
const visibleHomeVideos = computed(() => buildVideoPreviewRows(homeVideoCarouselRows.value, activeHomeVideoCarouselIndex.value, emptyHomeVideo))
const homeVideoOnlineCount = computed(() => homeVideoRows.value.filter((item) => isOnlineHomeVideo(item)).length)
const homeVideoModalPageSize = 9
const homeVideoModalSourceRows = computed(() => homeVideoRows.value
  .map((live, index) => ({ index, live }))
  .sort((a, b) => Number(isOnlineHomeVideo(b.live)) - Number(isOnlineHomeVideo(a.live)) || a.index - b.index))
const homeVideoModalPageCount = computed(() => Math.max(1, Math.ceil(homeVideoModalSourceRows.value.length / homeVideoModalPageSize)))
const homeVideoModalRows = computed(() => {
  const start = (homeVideoModalPage.value - 1) * homeVideoModalPageSize
  return homeVideoModalSourceRows.value.slice(start, start + homeVideoModalPageSize).map((item) => ({
    key: `home-video-modal-${item.live.key || item.live.title}-${item.index}`,
    index: item.index,
    live: item.live,
  }))
})
const homeHeaderStats = computed(() => homeTopStats.map((item) => (
  item.key === "today-outflow"
    ? { ...item, value: todayOutflowHeads.value }
    : item
)))
const homePastureProductivityPanel = computed(() => {
  const rows = buildHomePastureProductivityRows()
  if (!rows.length) return null
  return {
    key: "home-pasture-productivity",
    title: "草地生产力",
    rows,
    option: buildHomeReferenceLineOption(rows, "生产力"),
    height: sidePanelHeight.value,
  }
})
const yakStockDistributionPanel = computed(() => ({
  key: "yak-stock-distribution",
  title: "牦牛存栏分布",
  height: rightChartPanelHeight.value,
  option: buildHomeYakStockWaveOption(yakTownRows, "牦牛存栏"),
}))

const leftPanels = computed(() => [
  withHomePanelTitle(pickTopicPanel("grassland", "grassland-town"), "草地面积"),
  withHomePanelTitle(pickTopicPanel("wetland", "wetland-town-area"), "湿地面积"),
  withHomePanelTitle(pickTopicPanel("grassland", "grassland-type-proportion"), "草原类别"),
  homePastureProductivityPanel.value,
].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,
  },
  yakStockDistributionPanel.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,
  },
])

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: HOME_CHART_MUTED_COLOR, fontSize: 10 },
  },
  series: [
    {
      name: "畜群结构",
      type: "pie",
      selectedMode: "single",
      selectedOffset: 8,
      radius: ["36%", "62%"],
      center: ["30%", "54%"],
      avoidLabelOverlap: true,
      label: { color: HOME_CHART_LABEL_COLOR, 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: 5, shadowColor: "rgba(48, 220, 255, 0.2)" },
      },
      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: HOME_CHART_AXIS_COLOR, 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: HOME_CHART_AXIS_COLOR, fontSize: 10, margin: 4, formatter: (value) => formatHeadWan(value, 1) },
    splitLine: { lineStyle: { color: HOME_CHART_GRID_COLOR, 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 ? "#8bf7ff" : "#30dcff" },
            { offset: 0.72, color: index === activeForecastYearIndex.value ? "rgba(139, 247, 255, 0.7)" : "rgba(48, 220, 255, 0.62)" },
            { offset: 1, color: index === activeForecastYearIndex.value ? "rgba(139, 247, 255, 0.1)" : "rgba(48, 220, 255, 0.08)" },
          ]),
          shadowBlur: index === activeForecastYearIndex.value ? 16 : 8,
          shadowColor: index === activeForecastYearIndex.value ? "rgba(139, 247, 255, 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: HOME_CHART_LABEL_COLOR,
        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", "#8bf7ff", "#48cfff"],
    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: HOME_CHART_MUTED_COLOR, fontSize: 10 },
    },
    grid: { left: 42, right: 32, top: 30, bottom: 24 },
    xAxis: {
      type: "category",
      boundaryGap: true,
      data: series.labels,
      axisLabel: { color: HOME_CHART_AXIS_COLOR, fontSize: 10, interval: axisInterval },
      axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.28)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        scale: true,
        axisLabel: { color: HOME_CHART_AXIS_COLOR, fontSize: 10, formatter: formatCompact },
        splitLine: { lineStyle: { color: HOME_CHART_GRID_COLOR, type: "dashed" } },
      },
      {
        type: "value",
        axisLabel: { color: HOME_CHART_MUTED_COLOR, 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(72, 207, 255, 0.82)",
          borderColor: "#30dcff",
          borderColor0: "#48cfff",
          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, "#8bf7ff", 1, false, activeForecastTrendIndex.value),
      buildLineSeries("出栏扣减", series.outflow, "#48cfff", 1, false, activeForecastTrendIndex.value),
    ],
  })
})

onMounted(() => {
  updateHomeViewportHeight()
  window.addEventListener("resize", updateHomeViewportHeight)
  window.addEventListener("keydown", handleHomeKeyDown)
  document.addEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
  document.addEventListener("click", handleHomeVideoDocumentClickCapture, true)
  emitHomeTopStats()
  initHomeMap()
})

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

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

watch(activeHomeToolbarKey, (topicKey) => {
  closeFeaturePanel()
  if (topicKey === HOME_3D_TOPIC_KEY) {
    mapSceneRef.value?.pause?.()
    nextTick(() => {
      initThreeHomeMap()
      resumeThreeHomeMap()
    })
    return
  }
  pauseThreeHomeMap()
  nextTick(() => {
    mapSceneRef.value?.resume?.()
  })
})

watch(
  () => props.active,
  (active) => {
    if (active) {
      initHomeMap()
      restoreHomeMapOnActivate()
      startChartMotion()
      if (!homeIdleWorkLoaded) scheduleHomeIdleWork()
    } else {
      homeMapReadyReported = false
      mapSceneRef.value?.pause?.()
      pauseThreeHomeMap()
      cancelHomeIdleWork()
      stopChartMotion()
      stopVideoPlayer({ resetError: true })
      closeHomeVideoModal()
      closeHomeBalanceModal()
    }
  },
)

function initHomeMap() {
  homeMapReadyReported = false
  emit("loading-progress", { progress: 0 })
  nextTick(() => {
    if (homeRemoteSensingVisible.value) {
      if (mapSceneRef.value?.hasMap?.()) handleHomeMapReady()
      else mapSceneRef.value?.loadMap?.()
      return
    }
    initThreeHomeMap()
  })
}

function initThreeHomeMap() {
  if (destroyed || !props.active || homeRemoteSensingVisible.value) return
  if (getThreeHomeCanvasMap()) {
    resumeThreeHomeMap()
    handleHomeMapReady()
    return
  }
  if (threeHomeAssetLoaded) {
    mountThreeHomeMap()
    return
  }
  if (threeHomeAssetLoading) return
  threeHomeAssetLoading = true
  threeHomeAssets = new ThreeMapAssets({ showDataOverlays: false })
  threeHomeAssets.instance.on("onProgress", handleThreeHomeAssetProgress)
  threeHomeAssets.instance.on("onLoad", handleThreeHomeAssetsLoaded)
  threeHomeAssets.instance.on("onError", handleThreeHomeAssetsError)
  if (threeHomeAssets.instance.loaded) handleThreeHomeAssetsLoaded()
}

function handleThreeHomeAssetProgress(path, itemsLoaded, itemsTotal) {
  if (destroyed || homeRemoteSensingVisible.value) return
  const total = Math.max(1, Number(itemsTotal) || 1)
  const loaded = Math.max(0, Number(itemsLoaded) || 0)
  emit("loading-progress", { progress: Math.min(98, Math.floor((loaded / total) * 92)) })
}

function handleThreeHomeAssetsLoaded() {
  if (destroyed) return
  threeHomeAssetLoading = false
  threeHomeAssetLoaded = true
  emit("loading-progress", { progress: 96 })
  nextTick(mountThreeHomeMap)
}

function handleThreeHomeAssetsError(error) {
  threeHomeAssetLoading = false
  console.warn("[home] 3D map assets load failed", error)
  handleHomeMapReady()
}

function mountThreeHomeMap() {
  if (destroyed || !props.active || homeRemoteSensingVisible.value || !threeHomeAssets) return
  threeMapSceneRef.value?.loadMap?.(threeHomeAssets)
  resumeThreeHomeMap()
  threeMapSceneRef.value?.play?.()
  handleHomeMapReady()
}

function resumeThreeHomeMap() {
  const scene = threeMapSceneRef.value
  scene?.resume?.()
  const canvasMap = getThreeHomeCanvasMap()
  canvasMap?.sizes?.init?.()
  canvasMap?.resize?.()
  canvasMap?.label3d?.resize?.()
  canvasMap?.label3d?.update?.()
}

function pauseThreeHomeMap() {
  threeMapSceneRef.value?.pause?.()
}

function getThreeHomeCanvasMap() {
  const canvasMap = threeMapSceneRef.value?.canvasMap
  return canvasMap?.value || canvasMap || null
}

function handleThreeHomeMapReady() {
  if (!homeRemoteSensingVisible.value) handleHomeMapReady()
}

function destroyThreeHomeAssets() {
  threeHomeAssets?.instance?.destroy?.()
  threeHomeAssets = null
  threeHomeAssetLoading = false
  threeHomeAssetLoaded = false
}

function handleHomeMapReady() {
  if (destroyed || homeMapReadyReported) return
  homeMapReadyReported = true
  emit("loading-progress", { progress: 100 })
  emit("map-ready")
  scheduleHomeIdleWork()
}

function refreshMapView(options = {}) {
  if (!homeRemoteSensingVisible.value) {
    resumeThreeHomeMap()
    return
  }
  mapSceneRef.value?.resume?.()
  if (options.reset) {
    mapSceneRef.value?.resetView?.()
    if (!mapSceneRef.value?.resetView) mapSceneRef.value?.refreshView?.()
    return
  }
  mapSceneRef.value?.refreshView?.()
}

function restoreHomeMapOnActivate() {
  if (activeHomeToolbarKey.value !== HOME_3D_TOPIC_KEY && !homeToolbarLayerMap[activeHomeToolbarKey.value]) {
    activeHomeToolbarKey.value = HOME_3D_TOPIC_KEY
  }
  const restore = () => {
    if (destroyed || !props.active) return
    if (homeRemoteSensingVisible.value) {
      refreshMapView({ reset: true })
      return
    }
    initThreeHomeMap()
    resumeThreeHomeMap()
  }
  nextTick(() => {
    restore()
    window.setTimeout(restore, 180)
  })
}

function resetMap() {
  closeFeaturePanel()
  if (!homeRemoteSensingVisible.value) {
    resumeThreeHomeMap()
    return
  }
  if (mapSceneRef.value?.hasMap?.()) {
    refreshMapView({ reset: true })
    return
  }
  initHomeMap()
  nextTick(() => {
    mapSceneRef.value?.resume?.()
  })
}

function showDefaultThreeMap() {
  closeFeaturePanel()
  if (activeHomeToolbarKey.value === HOME_3D_TOPIC_KEY) {
    nextTick(() => {
      initThreeHomeMap()
      resumeThreeHomeMap()
    })
    return
  }
  activeHomeToolbarKey.value = HOME_3D_TOPIC_KEY
}

defineExpose({
  refreshMapView,
  resetMap,
  showDefaultThreeMap,
})

function scheduleHomeIdleWork() {
  cancelHomeIdleWork()
  const run = () => {
    homeIdleWorkHandle = null
    homeIdleWorkType = ""
    if (destroyed || !props.active) return
    homeIdleWorkLoaded = true
    void loadForestGrassWetPanels()
    void loadHomeMonitorings()
    void loadTodayOutflow()
    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 setHomeVideoItemRef(el, item) {
  const rawIndex = Number(item?.index)
  if (Number.isFinite(rawIndex)) {
    if (el) {
      homeVideoScreenRefs.set(rawIndex, el)
    } else {
      homeVideoScreenRefs.delete(rawIndex)
    }
  }
  if (item?.isActive) homeVideoScreenRef.value = el || null
}

function buildVideoPreviewRows(rows = [], activeIndex = 0, emptyRow = emptyHomeVideo) {
  const sourceRows = Array.isArray(rows) ? rows : []
  if (!sourceRows.length) {
    return [{ key: `empty-${emptyRow.key || "video"}`, index: 0, live: emptyRow, isActive: true }]
  }
  const index = (activeIndex + sourceRows.length) % sourceRows.length
  const item = sourceRows[index]
  const live = item?.live || item || emptyRow
  const sourceIndex = Number.isFinite(Number(item?.index)) ? Number(item.index) : index
  return [{
    key: `${live.key || live.title || "video"}-${sourceIndex}`,
    index: sourceIndex,
    live,
    isActive: true,
  }]
}

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()
}

function emitHomeTopStats() {
  emit("module-stats", {
    moduleKey: "home",
    items: homeHeaderStats.value,
  })
}

async function loadTodayOutflow() {
  const today = formatLocalDate(new Date())
  try {
    const result = await getYakTradeOverview({
      year: Number(today.slice(0, 4)),
      startDate: today,
      endDate: today,
    })
    const metrics = result?.metrics || {}
    const value = Number(metrics.headCount ?? metrics.head_count ?? 0)
    todayOutflowHeads.value = Number.isFinite(value) ? value : 0
  } catch (error) {
    todayOutflowHeads.value = 0
  }
  if (!destroyed) emitHomeTopStats()
}

function formatLocalDate(date) {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, "0")
  const day = String(date.getDate()).padStart(2, "0")
  return `${year}-${month}-${day}`
}

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

function normalizeHomeVideoIndex(index) {
  const length = homeVideoRows.value.length
  if (!length) return 0
  return ((Number(index) || 0) + length) % length
}

function homeVideoStateKey(index) {
  return String(normalizeHomeVideoIndex(index))
}

function ensureHomeVideoState(index) {
  const key = homeVideoStateKey(index)
  if (!videoStates[key]) {
    videoStates[key] = {
      playing: false,
      loading: false,
      ready: false,
      error: "",
    }
  }
  return videoStates[key]
}

function getHomeVideoState(index) {
  return videoStates[homeVideoStateKey(index)] || {}
}

function homeVideoElementId(index) {
  return `${videoElementIdPrefix}-${homeVideoStateKey(index)}`
}

function isHomeVideoPlaying(index) {
  return Boolean(getHomeVideoState(index).playing)
}

function isHomeVideoLoading(index) {
  return Boolean(getHomeVideoState(index).loading)
}

function homeVideoErrorFor(index) {
  const stateError = getHomeVideoState(index).error
  if (stateError) return stateError
  return normalizeHomeVideoIndex(index) === activeVideoIndex.value ? videoError.value : ""
}

function isHomeVideoMounted(index) {
  const state = getHomeVideoState(index)
  return Boolean(state.playing || state.loading)
}

function syncVideoPlaybackSummary() {
  const states = Object.values(videoStates)
  videoPlaying.value = states.some((state) => state.playing)
  videoLoading.value = states.some((state) => state.loading)
}

function videoScreenStyle(live, isActive = true, index = activeVideoIndex.value) {
  const image = live?.image || liveSceneImage
  return isHomeVideoPlaying(index) && isActive
    ? 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 openHomeVideoModal() {
  const currentIndex = activeVideoIndex.value
  stopVideoPlayer({ resetError: true })
  activeVideoManualIndex.value = currentIndex
  homeVideoRotationPaused.value = true
  chartMotionTick.value = 0
  const modalIndex = homeVideoModalSourceRows.value.findIndex((item) => item.index === currentIndex)
  homeVideoModalPage.value = Math.floor(Math.max(0, modalIndex) / homeVideoModalPageSize) + 1
  homeVideoModalVisible.value = true
  void nextTick(() => resizeMountedHomeVideoPlayers())
}

function closeHomeVideoModal() {
  if (!homeVideoModalVisible.value) return
  stopVideoPlayer({ resetError: true })
  homeVideoModalVisible.value = false
}

function changeHomeVideoModalPage(page) {
  const nextPage = Math.max(1, Math.min(homeVideoModalPageCount.value, Number(page) || 1))
  if (nextPage === homeVideoModalPage.value) return
  stopVideoPlayer({ resetError: true })
  homeVideoModalPage.value = nextPage
  const pageStart = (nextPage - 1) * homeVideoModalPageSize
  activeVideoManualIndex.value = homeVideoModalSourceRows.value[pageStart]?.index ?? 0
  void nextTick(() => resizeMountedHomeVideoPlayers())
}

function openHomeBalanceModal() {
  homeBalanceModalVisible.value = true
}

function closeHomeBalanceModal() {
  homeBalanceModalVisible.value = false
}

function stepVideo(step) {
  const rows = homeVideoCarouselRows.value
  const length = rows.length
  if (!length) return
  stopVideoPlayer({ resetError: true })
  const current = activeHomeVideoCarouselIndex.value
  activeVideoManualIndex.value = rows[(current + step + length) % length]?.index ?? 0
  homeVideoRotationPaused.value = true
  chartMotionTick.value = 0
}

function playHomeVideoAt(index) {
  const length = homeVideoRows.value.length
  if (!length) {
    void playCurrentVideo()
    return
  }
  const targetIndex = normalizeHomeVideoIndex(index)
  const key = homeVideoStateKey(targetIndex)
  const state = getHomeVideoState(targetIndex)
  activeVideoManualIndex.value = targetIndex
  homeVideoRotationPaused.value = true
  chartMotionTick.value = 0
  if (videoPlayStarts.has(key) || videoPlayers.has(key) || ((state.playing || state.loading) && !state.error)) {
    fitVideoPlayerToScreen(targetIndex)
    return
  }
  videoPlayStarts.add(key)
  void nextTick(async () => {
    try {
      await playHomeVideoByIndex(targetIndex, { stopOthers: !homeVideoModalVisible.value })
    } finally {
      videoPlayStarts.delete(key)
    }
  })
}

function handleHomeVideoScreenClick(index, event) {
  if (isHomeVideoSdkControlTarget(event?.target)) return
  playHomeVideoAt(index)
}

function handleHomeVideoDocumentClickCapture(event) {
  const webFullscreenControl = event?.target?.closest?.('[id$="-webExpend-content"]')
    || event?.composedPath?.().find((node) => String(node?.id || "").endsWith("-webExpend-content"))
  if (!webFullscreenControl) return
  const screenIndex = Number(webFullscreenControl.closest?.("[data-video-index]")?.dataset?.videoIndex)
  const targetIndex = homeVideoExpandedIndex.value ?? (Number.isFinite(screenIndex) ? screenIndex : activeVideoIndex.value)
  event.preventDefault?.()
  event.stopPropagation?.()
  event.stopImmediatePropagation?.()
  void toggleHomeVideoWebExpanded(targetIndex)
}

async function toggleHomeVideoWebExpanded(index) {
  const targetIndex = normalizeHomeVideoIndex(index)
  if (homeVideoExpandedIndex.value === targetIndex) {
    closeHomeVideoWebExpanded()
    return
  }
  if (homeVideoExpandedIndex.value !== null) closeHomeVideoWebExpanded()
  const wrap = document.getElementById(`${homeVideoElementId(targetIndex)}-wrap`)
  if (!wrap) return
  homeVideoExpandedIndex.value = targetIndex
  await nextTick()
  const slot = homeVideoWebStageRef.value?.querySelector?.(".home-video-web-player-slot")
  if (!slot) {
    homeVideoExpandedIndex.value = null
    return
  }
  slot.appendChild(wrap)
  fitVideoPlayerToScreen(targetIndex)
  scheduleVideoFit(targetIndex)
}

function closeHomeVideoWebExpanded(options = {}) {
  const { restore = true } = options
  const targetIndex = homeVideoExpandedIndex.value
  if (targetIndex === null) return
  const wrap = document.getElementById(`${homeVideoElementId(targetIndex)}-wrap`)
  if (restore && wrap) {
    const screen = resolveHomeVideoScreen(targetIndex) || homeVideoScreenRef.value
    const host = screen?.querySelector?.(".home-video-player-host")
    if (host) host.appendChild(wrap)
  }
  homeVideoExpandedIndex.value = null
  if (restore) {
    void nextTick(() => {
      fitVideoPlayerToScreen(targetIndex)
      scheduleVideoFit(targetIndex)
    })
  }
}

function isHomeVideoSdkControlTarget(target) {
  return Boolean(target?.closest?.('[id$="-audioControls"], [id$="-ez-iframe-footer-container"], .footer-controls, .theme-icon-item'))
}

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 "#8bf7ff"
  if (value > 10 || /中度|预警/.test(status)) return "#48cfff"
  if (value > 5 || /轻度/.test(status)) return "#8bf7ff"
  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() {
  return playHomeVideoByIndex(activeVideoIndex.value, { stopOthers: !homeVideoModalVisible.value })
}

async function playHomeVideoByIndex(index, options = {}) {
  const { stopOthers = true } = options
  const targetIndex = normalizeHomeVideoIndex(index)
  const live = homeVideoRows.value[targetIndex] || emptyHomeVideo
  const key = homeVideoStateKey(targetIndex)
  let state = ensureHomeVideoState(targetIndex)
  if (live?.status !== "直播中" || !live?.hdPlayUrl || !live?.token) {
    state.error = live?.status === "离线" ? "当前监控离线" : "暂无在线可播放摄像头"
    state.loading = false
    state.playing = false
    state.ready = false
    if (targetIndex === activeVideoIndex.value) videoError.value = state.error
    syncVideoPlaybackSummary()
    return
  }
  if (videoPlayers.has(key) || ((state.playing || state.loading) && !state.error)) return

  activeVideoManualIndex.value = targetIndex
  chartMotionTick.value = 0
  if (stopOthers) {
    stopVideoPlayer({ resetError: true })
  } else if (videoPlayers.has(key) || state.loading) {
    stopVideoPlayerAt(targetIndex, { resetError: true })
  }
  state = ensureHomeVideoState(targetIndex)
  state.error = ""
  state.loading = true
  state.playing = false
  state.ready = false
  if (targetIndex === activeVideoIndex.value) videoError.value = ""
  syncVideoPlaybackSummary()
  await nextTick()

  const playRun = (videoPlayRuns.get(key) || 0) + 1
  videoPlayRuns.set(key, playRun)
  let player = null
  startVideoLoadTimeout(targetIndex, playRun)
  try {
    const screen = resolveHomeVideoScreen(targetIndex) || homeVideoScreenRef.value
    const elementId = homeVideoElementId(targetIndex)
    if (!mountHomeVideoElement(screen, elementId)) throw new Error("视频容器未就绪")
    const { width: playerWidth, height: playerHeight } = getHomeVideoScreenSize(screen, 340, 92)
    player = new EZUIKit.EZUIKitPlayer({
      id: elementId,
      accessToken: live.token,
      url: live.hdPlayUrl,
      audio: 0,
      width: playerWidth,
      height: playerHeight,
      themeData: buildEzvizThemeData(),
      handleError(error) {
        if (playRun !== videoPlayRuns.get(key)) return
        clearVideoLoadTimeout(targetIndex)
        console.warn("EZUIKit player error", error)
        stopEzvizPlayer(player)
        state.error = "视频加载失败"
        state.loading = false
        state.playing = false
        state.ready = false
        videoPlayRuns.set(key, playRun + 1)
        videoPlayers.delete(key)
        clearVideoElement(targetIndex)
        if (targetIndex === activeVideoIndex.value) videoError.value = state.error
        syncVideoPlaybackSummary()
      },
      handleSuccess() {
        if (playRun !== videoPlayRuns.get(key)) return
        clearVideoLoadTimeout(targetIndex)
        state.error = ""
        state.ready = true
        state.loading = false
        if (targetIndex === activeVideoIndex.value) videoError.value = ""
        fitVideoPlayerToScreen(targetIndex)
        scheduleVideoFit(targetIndex)
        keepEzvizControlsVisible(elementId)
        syncVideoPlaybackSummary()
      },
      fullScreenChangeCallBack() {},
    })
    if (playRun !== videoPlayRuns.get(key)) {
      stopEzvizPlayer(player)
      clearVideoElement(targetIndex)
      return
    }
    videoPlayers.set(key, player)
    state.playing = true
    syncVideoPlaybackSummary()
    void nextTick(() => {
      fitVideoPlayerToScreen(targetIndex)
      scheduleVideoFit(targetIndex)
      keepEzvizControlsVisible(elementId)
    })
  } catch (error) {
    clearVideoLoadTimeout(targetIndex)
    console.warn("EZUIKit init error", error)
    state.error = "视频加载失败"
    if (targetIndex === activeVideoIndex.value) videoError.value = state.error
    stopVideoPlayerAt(targetIndex, { resetError: false })
  }
}

function keepEzvizControlsVisible(id) {
  if (typeof document === "undefined" || !id) return
  const footer = document.getElementById(`${id}-ez-iframe-footer-container`)
  const controls = document.getElementById(`${id}-audioControls`)
  if (footer) {
    Object.assign(footer.style, {
      position: "absolute",
      left: "0",
      right: "0",
      bottom: "0",
      zIndex: "20",
      width: "100%",
      marginTop: "0",
      pointerEvents: "auto",
      visibility: "visible",
      opacity: "1",
    })
  }
  if (!controls) return
  Object.assign(controls.style, {
    opacity: "1",
    pointerEvents: "all",
    visibility: "visible",
  })
}

function scheduleVideoFit(index = activeVideoIndex.value, frame = 0) {
  if (typeof window === "undefined") return
  const targetIndex = normalizeHomeVideoIndex(index)
  const key = homeVideoStateKey(targetIndex)
  const previousRaf = homeVideoFitRafs.get(key)
  if (frame === 0 && previousRaf) {
    window.cancelAnimationFrame(previousRaf)
  }
  const raf = window.requestAnimationFrame(() => {
    fitVideoPlayerToScreen(targetIndex)
    if (frame < 8) {
      scheduleVideoFit(targetIndex, frame + 1)
    } else {
      homeVideoFitRafs.delete(key)
    }
  })
  homeVideoFitRafs.set(key, raf)
}

function clearHomeVideoFitFrames(index) {
  if (typeof window === "undefined") return
  if (index !== undefined) {
    const key = homeVideoStateKey(index)
    const raf = homeVideoFitRafs.get(key)
    if (raf) window.cancelAnimationFrame(raf)
    homeVideoFitRafs.delete(key)
    return
  }
  homeVideoFitRafs.forEach((raf) => window.cancelAnimationFrame(raf))
  homeVideoFitRafs.clear()
}

function resizeMountedHomeVideoPlayers() {
  Array.from(videoPlayers.keys()).forEach((key) => {
    const index = Number(key)
    if (Number.isFinite(index)) scheduleVideoFit(index)
  })
}

function getHomeVideoScreenSize(screen, fallbackWidth = 1, fallbackHeight = 1) {
  if (!screen) {
    return {
      width: Math.max(1, Math.floor(fallbackWidth || 1)),
      height: Math.max(1, Math.floor(fallbackHeight || 1)),
    }
  }
  const rect = screen.getBoundingClientRect?.()
  const width = screen.clientWidth || screen.offsetWidth || rect?.width || fallbackWidth
  const height = screen.clientHeight || screen.offsetHeight || rect?.height || fallbackHeight
  return {
    width: Math.max(1, Math.floor(width || 1)),
    height: Math.max(1, Math.floor(height || 1)),
  }
}

function resolveHomeVideoScreen(index = activeVideoIndex.value) {
  if (typeof document === "undefined") return null
  const targetIndex = normalizeHomeVideoIndex(index)
  const scope = homeVideoModalVisible.value ? ".home-video-modal-item" : ".home-video-card .home-video-screen"
  const scopedScreen = document.querySelector(`${scope}[data-video-index="${targetIndex}"]`)
  if (scopedScreen) return scopedScreen
  const mappedScreen = homeVideoScreenRefs.get(targetIndex)
  if (mappedScreen?.isConnected) return mappedScreen
  return null
}

function mountHomeVideoElement(screen, elementId) {
  const host = screen?.querySelector?.(".home-video-player-host")
  if (!host) return null
  host.replaceChildren()
  const container = document.createElement("div")
  container.id = elementId
  container.className = "home-video-player"
  host.appendChild(container)
  return container
}

function fitVideoPlayerToScreen(index = activeVideoIndex.value) {
  if (typeof document === "undefined") return
  const targetIndex = normalizeHomeVideoIndex(index)
  const key = homeVideoStateKey(targetIndex)
  const elementId = homeVideoElementId(targetIndex)
  const screen = homeVideoExpandedIndex.value === targetIndex && homeVideoWebStageRef.value
    ? homeVideoWebStageRef.value
    : resolveHomeVideoScreen(targetIndex) || homeVideoScreenRef.value
  const wrap = document.getElementById(`${elementId}-wrap`)
  const player = document.getElementById(elementId)
  const root = wrap || player
  if (!screen || !root) return

  const { width, height } = getHomeVideoScreenSize(screen)
  const ezvizPlayer = videoPlayers.get(key)
  try {
    if (wrap && player && ezvizPlayer && typeof ezvizPlayer.reSize === "function") {
      ezvizPlayer.reSize(width, height)
    } else if (wrap && player && ezvizPlayer && typeof ezvizPlayer.autoResize === "function") {
      ezvizPlayer.autoResize()
    }
  } catch (error) {
    console.warn("EZUIKit resize failed", error)
  }

  const fillStyle = {
    position: "absolute",
    left: "0",
    top: "0",
    right: "0",
    bottom: "0",
    width: `${width}px`,
    height: `${height}px`,
  }

  Object.assign(root.style, fillStyle, {
    display: "block",
    verticalAlign: "top",
  })

  if (wrap && player) {
    Object.assign(player.style, {
      position: "absolute",
      left: "0",
      top: "0",
      right: "0",
      bottom: "0",
      display: "block",
      width: "100%",
      height: "100%",
      maxWidth: "none",
      maxHeight: "none",
      overflow: "hidden",
      verticalAlign: "top",
    })
  }

  const iframe = document.getElementById(`EZUIKitPlayer-${elementId}`)
  if (iframe) {
    iframe.setAttribute("width", String(width))
    iframe.setAttribute("height", String(height))
  }
  const pluginPlayer = document.getElementById(`${elementId}-player`)
  const canvas = document.getElementById(`${elementId}canvas0`)
  const canvasDraw = document.getElementById(`${elementId}canvas_draw0`)

  const nodes = [
    root,
    player,
    pluginPlayer,
    canvas?.parentElement,
    canvas,
    canvasDraw?.parentElement,
    canvasDraw,
    ...Array.from(player?.children || []),
    ...Array.from(root?.querySelectorAll?.("[id$='-wrap'], .play-window, video, canvas, iframe, object, embed") || []),
    ...Array.from(player?.querySelectorAll?.(".play-window, video, canvas, iframe, object, embed") || []),
    iframe,
  ].filter(Boolean)
  const uniqueNodes = Array.from(new Set(nodes))

  uniqueNodes.forEach((node) => {
    node.setAttribute?.("width", String(width))
    node.setAttribute?.("height", String(height))
    Object.assign(node.style, {
      position: "absolute",
      left: "0",
      top: "0",
      right: "0",
      bottom: "0",
      width: "100%",
      height: "100%",
      maxWidth: "none",
      maxHeight: "none",
      margin: "0",
      objectFit: "fill",
    })
  })
  keepEzvizControlsVisible(elementId)
}

function stopVideoPlayer(options = {}) {
  Array.from(videoPlayers.keys()).forEach((key) => {
    stopVideoPlayerAt(Number(key), options)
  })
  Object.keys(videoStates).forEach((key) => {
    if (!videoPlayers.has(key)) {
      stopVideoPlayerAt(Number(key), options)
    }
  })
  syncVideoPlaybackSummary()
}

function stopVideoPlayerAt(index, options = {}) {
  const { clearElement = true, resetError = false } = options
  const targetIndex = normalizeHomeVideoIndex(index)
  const key = homeVideoStateKey(targetIndex)
  clearHomeVideoFitFrames(targetIndex)
  clearVideoLoadTimeout(targetIndex)
  const player = videoPlayers.get(key)
  videoPlayers.delete(key)
  videoPlayRuns.set(key, (videoPlayRuns.get(key) || 0) + 1)
  if (player) stopEzvizPlayer(player)
  if (clearElement) clearVideoElement(targetIndex)
  const state = videoStates[key]
  if (state) {
    state.playing = false
    state.loading = false
    state.ready = false
    if (resetError) state.error = ""
    if (!state.error) delete videoStates[key]
  }
  if (targetIndex === activeVideoIndex.value && resetError) videoError.value = ""
  syncVideoPlaybackSummary()
}

function startVideoLoadTimeout(index, playRun) {
  const targetIndex = normalizeHomeVideoIndex(index)
  const key = homeVideoStateKey(targetIndex)
  clearVideoLoadTimeout(targetIndex)
  const timer = window.setTimeout(() => {
    const state = videoStates[key]
    if (playRun !== videoPlayRuns.get(key) || !state?.loading) return
    state.error = "视频加载超时"
    if (targetIndex === activeVideoIndex.value) videoError.value = state.error
    stopVideoPlayerAt(targetIndex, { resetError: false })
  }, videoLoadTimeoutMs)
  videoLoadTimeoutTimers.set(key, timer)
}

function clearVideoLoadTimeout(index) {
  const key = homeVideoStateKey(index)
  const timer = videoLoadTimeoutTimers.get(key)
  if (!timer) return
  window.clearTimeout(timer)
  videoLoadTimeoutTimers.delete(key)
}

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(index = activeVideoIndex.value) {
  if (typeof document === "undefined") return
  const targetIndex = normalizeHomeVideoIndex(index)
  const elementId = homeVideoElementId(targetIndex)
  const wrap = document.getElementById(`${elementId}-wrap`)
  const container = document.getElementById(elementId)
  const screen = resolveHomeVideoScreen(targetIndex) || homeVideoScreenRef.value
  const host = screen?.querySelector?.(".home-video-player-host")
  if (homeVideoExpandedIndex.value === targetIndex) closeHomeVideoWebExpanded({ restore: false })
  if (host) host.replaceChildren()
  if (wrap?.isConnected) wrap.remove()
  if (container?.isConnected) container.remove()
}

function buildEzvizThemeData() {
  return {
    autoFocus: 0,
    header: {
      color: "#30dcff",
      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: "#30dcff",
      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: 0 },
        { iconId: "hd", part: "right", defaultActive: 0, memo: "清晰度切换按钮", isrender: 1 },
        { iconId: "webExpend", 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] = await Promise.all([
    getTopicBusinessStats("grassland", "grassland-distribution"),
    getTopicBusinessStats("wetland", "wetland-distribution"),
  ])
  if (destroyed) return
  forestGrassWetStats.value = { grassland, wetland }
}

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

function withHomePanelTitle(panel, title) {
  return panel ? { ...panel, title } : null
}

function buildHomePastureProductivityRows() {
  const grassAreaRows = pickTopicPanel("grassland", "grassland-town")?.rows || []
  const wetlandAreaRows = pickTopicPanel("wetland", "wetland-town-area")?.rows || []
  const grassProductivityRows = pickTopicPanel("grassland", "grassland-productivity-town")?.rows || []
  const wetlandProductivityRows = pickTopicPanel("wetland", "wetland-mu-yield")?.rows || []
  const grassAreaMap = toHomeMetricMap(grassAreaRows)
  const wetlandAreaMap = toHomeMetricMap(wetlandAreaRows)
  const grassProductivityMap = toHomeMetricMap(grassProductivityRows)
  const wetlandProductivityMap = toHomeMetricMap(wetlandProductivityRows)
  const names = [...new Set([
    ...grassAreaRows,
    ...wetlandAreaRows,
    ...grassProductivityRows,
    ...wetlandProductivityRows,
  ].map((item) => item?.name).filter(Boolean))]

  return names.map((name, index) => {
    const grassArea = grassAreaMap.get(name) || 0
    const wetlandArea = wetlandAreaMap.get(name) || 0
    const grassProductivity = grassProductivityMap.get(name) || 0
    const wetlandProductivity = wetlandProductivityMap.get(name) || 0
    let weightedTotal = 0
    let weightedArea = 0
    if (grassArea > 0 && grassProductivity > 0) {
      weightedTotal += grassArea * grassProductivity
      weightedArea += grassArea
    }
    if (wetlandArea > 0 && wetlandProductivity > 0) {
      weightedTotal += wetlandArea * wetlandProductivity
      weightedArea += wetlandArea
    }
    const fallbackValues = [grassProductivity, wetlandProductivity].filter((value) => value > 0)
    const value = weightedArea > 0
      ? weightedTotal / weightedArea
      : fallbackValues.reduce((sum, item) => sum + item, 0) / Math.max(fallbackValues.length, 1)
    return {
      key: `home-pasture-productivity-${name}`,
      name,
      value: Number(value.toFixed(2)),
      unit: "斤/亩",
      extra: "草地与湿地加权亩产",
      sourceIndex: index,
    }
  }).filter((item) => item.value > 0)
}

function toHomeMetricMap(rows = []) {
  return new Map(rows.map((item) => [item.name, Number(item.value) || 0]))
}

function normalizeHomePanel(panel, prefix) {
  const key = String(panel?.key || "")
  if (key.includes("grassland-town")) {
    return {
      ...panel,
      key: `home-${prefix}-${panel.key}`,
      height: sidePanelHeight.value,
      option: buildHomeTownColumnOption(panel, panel.title),
    }
  }
  if (key.includes("wetland-town-area")) {
    return {
      ...panel,
      key: `home-${prefix}-${panel.key}`,
      height: sidePanelHeight.value,
      option: buildHomeTownRankBarOption(panel, panel.title),
    }
  }
  return {
    ...panel,
    key: `home-${prefix}-${panel.key}`,
    height: sidePanelHeight.value,
  }
}

function buildHomeTownColumnOption(panel, metricName, options = {}) {
  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.14)
  const showTrendLine = options.showTrendLine === true

  return withChartDefaults({
    animation: true,
    animationDuration: 900,
    animationDurationUpdate: 700,
    animationEasing: "cubicOut",
    title: {
      text: `单位：${unit}`,
      left: "5%",
      top: "8%",
      textStyle: {
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 10,
        fontWeight: 400,
      },
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
        shadowStyle: { opacity: 0 },
      },
      backgroundColor: "rgba(0, 0, 0, 0.92)",
      borderWidth: 1,
      borderColor: "rgba(255, 255, 255, 0.82)",
      padding: [8, 10],
      textStyle: {
        color: "#ffffff",
        fontSize: 10,
      },
      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: "12%",
      top: "25%",
      width: "82%",
      height: "55%",
    },
    xAxis: [
      {
        type: "category",
        data: rows.map((item) => item.name),
        axisLabel: {
          interval: 0,
          hideOverlap: false,
          color: HOME_CHART_TEXT_COLOR,
          fontSize: 10,
          fontWeight: 700,
          lineHeight: 12,
          margin: 8,
          padding: [0, 0, 0, 0],
          formatter: formatReferenceTownName,
        },
        axisLine: { show: false },
        axisTick: { show: false },
        splitLine: { show: false },
      },
      {
        type: "category",
        data: rows.map((item) => item.name),
        axisLine: {
          show: false,
          lineStyle: { color: "rgba(0, 0, 0, 0)" },
        },
        axisTick: { show: false },
        axisLabel: { show: false },
      },
    ],
    yAxis: {
      type: "value",
      min: 0,
      max: topValue,
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: {
        show: true,
        lineStyle: {
          type: [2, 2],
          dashOffset: 5,
          color: HOME_CHART_GRID_COLOR,
          opacity: 1,
          width: 0.3,
        },
      },
      axisLabel: {
        color: HOME_CHART_AXIS_COLOR,
        fontSize: 10,
        formatter: formatCompact,
      },
    },
    series: [
      {
        name: metricName,
        type: "bar",
        barWidth: 7,
        stack: "b",
        z: 3,
        data: rows.map((item) => Number(item.value) || 0),
        label: {
          show: true,
          position: "top",
          distance: 9,
          color: HOME_CHART_LABEL_COLOR,
          fontSize: 10,
          fontWeight: 700,
          formatter: ({ value }) => formatCompact(value),
        },
        itemStyle: {
          borderRadius: 2,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#43d9e8" },
            { offset: 0.58, color: "rgba(67, 217, 232, 0.72)" },
            { offset: 1, color: "rgba(67, 217, 232, 0.16)" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 4,
            shadowColor: "rgba(67, 217, 232, 0.18)",
          },
        },
      },
      ...(showTrendLine ? [
        {
          name: `${metricName}趋势`,
          type: "line",
          smooth: true,
          symbol: "circle",
          symbolSize: 5,
          showSymbol: true,
          data: values,
          label: { show: false },
          lineStyle: {
            width: 1.3,
            color: "rgba(139, 247, 255, 0.82)",
            shadowBlur: 0,
            shadowColor: "transparent",
          },
          itemStyle: {
            color: HOME_CHART_LABEL_COLOR,
            borderColor: "#8bf7ff",
            borderWidth: 1,
            shadowBlur: 0,
            shadowColor: "transparent",
          },
          emphasis: {
            scale: true,
            lineStyle: { width: 2 },
          },
          tooltip: { show: false },
          z: 5,
        },
      ] : []),
      {
        type: "bar",
        barWidth: 18,
        xAxisIndex: 1,
        barGap: "-220%",
        data: rows.map(() => topValue),
        emphasis: {
          focus: "none",
          itemStyle: {
            color: "rgba(255, 255, 255, 0.8)",
          },
        },
        itemStyle: {
          color: "rgba(67, 217, 232, 0.3)",
          opacity: 0.08,
        },
        tooltip: { show: false },
        z: 0,
      },
    ],
  })
}

function buildHomeTownRankBarOption(panel, metricName) {
  const rows = (panel.rows || [])
    .filter((item) => Number.isFinite(Number(item.value)))
    .sort((a, b) => (Number(b.value) || 0) - (Number(a.value) || 0))
    .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 axisMax = Math.ceil(maxValue * 1.2)

  return withChartDefaults({
    animation: true,
    animationDuration: 900,
    animationDurationUpdate: 700,
    animationEasing: "cubicOut",
    title: {
      text: `单位：${unit}`,
      left: "5%",
      top: "8%",
      textStyle: {
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 10,
        fontWeight: 400,
      },
    },
    tooltip: {
      trigger: "axis",
      axisPointer: { type: "shadow", shadowStyle: { opacity: 0.08 } },
      backgroundColor: "rgba(0, 0, 0, 0.92)",
      borderWidth: 1,
      borderColor: "rgba(255, 255, 255, 0.82)",
      padding: [8, 10],
      textStyle: { color: "#ffffff", fontSize: 10 },
      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: 58,
      right: 74,
      top: 42,
      bottom: 14,
      containLabel: false,
    },
    xAxis: {
      type: "value",
      min: 0,
      max: axisMax,
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: { show: false },
      splitLine: {
        show: true,
        lineStyle: {
          color: HOME_CHART_GRID_COLOR,
          type: [2, 4],
          width: 0.3,
        },
      },
    },
    yAxis: {
      type: "category",
      inverse: true,
      data: rows.map((item) => item.name),
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: {
        color: HOME_CHART_TEXT_COLOR,
        fontSize: 10,
        fontWeight: 700,
        margin: 8,
      },
    },
    series: [
      {
        name: "背景",
        type: "bar",
        barWidth: 8,
        barGap: "-100%",
        data: rows.map(() => axisMax),
        itemStyle: {
          color: "rgba(74, 145, 165, 0.13)",
          borderColor: "rgba(95, 208, 235, 0.12)",
          borderWidth: 1,
        },
        emphasis: { disabled: true },
        tooltip: { show: false },
        z: 0,
      },
      {
        name: metricName,
        type: "bar",
        barWidth: 8,
        data: values,
        label: {
          show: true,
          position: "right",
          distance: 8,
          color: HOME_CHART_LABEL_COLOR,
          fontSize: 10,
          fontWeight: 700,
          formatter: ({ value }) => `${formatNumber(value)}${unit}`,
        },
        itemStyle: {
          borderRadius: [0, 5, 5, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
            { offset: 0, color: "rgba(50, 170, 255, 0.22)" },
            { offset: 0.45, color: "#35d6ff" },
            { offset: 1, color: "#8dfbff" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 3,
            shadowColor: "rgba(53, 214, 255, 0.16)",
          },
        },
        z: 2,
      },
    ],
  })
}

function buildHomeYakStockWaveOption(rows = [], metricName = "数值") {
  const townOrder = ["邛溪镇", "刷经寺镇", "安曲镇", "龙日镇", "江茸乡", "查尔玛乡", "瓦切镇", "阿木乡", "麦洼乡", "色地镇"]
  const townOrderIndex = new Map(townOrder.map((name, index) => [name, index]))
  const chartRows = rows
    .filter((item) => Number.isFinite(Number(item.value)))
    .sort((a, b) => (townOrderIndex.get(a.name) ?? 99) - (townOrderIndex.get(b.name) ?? 99))
    .slice(0, 10)
  const unit = chartRows.find((item) => item.unit)?.unit || "头"
  const values = chartRows.map((item) => Number(item.value) || 0)
  const maxValue = Math.max(...values, 1)
  const axisMax = Math.ceil((maxValue * 1.16) / 10000) * 10000
  const activeIndex = getLoopIndex(chartRows.length)
  const peakIndexes = new Set(values
    .map((value, index) => ({ value, index }))
    .sort((a, b) => b.value - a.value)
    .slice(0, 3)
    .map((item) => item.index))

  return withChartDefaults({
    animation: true,
    animationDuration: 760,
    animationDurationUpdate: 520,
    animationEasing: "cubicOut",
    title: {
      text: `单位：${unit}`,
      left: 12,
      top: 8,
      textStyle: {
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 10,
        fontWeight: 400,
      },
    },
    tooltip: {
      trigger: "axis",
      axisPointer: { type: "line", lineStyle: { color: "rgba(48, 220, 255, 0.18)" } },
      backgroundColor: "rgba(0, 0, 0, 0.92)",
      borderWidth: 1,
      borderColor: "rgba(255, 255, 255, 0.82)",
      padding: [8, 10],
      textStyle: { color: "#ffffff", fontSize: 10 },
      formatter(params) {
        const items = Array.isArray(params) ? params : [params]
        const point = items.find((item) => item.seriesName === metricName) || items[0]
        const row = chartRows[point?.dataIndex ?? 0]
        if (!row) return ""
        return `${row.name}<br/>${metricName}：${formatNumber(row.value)}${row.unit || unit}`
      },
    },
    grid: {
      left: 46,
      right: 20,
      top: 38,
      bottom: 28,
      containLabel: false,
    },
    xAxis: {
      type: "category",
      boundaryGap: false,
      data: chartRows.map((item) => item.name),
      axisLine: { show: true, lineStyle: { color: "rgba(141, 251, 255, 0.32)" } },
      axisTick: { show: false },
      axisLabel: {
        show: true,
        interval: 0,
        margin: 8,
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 9,
        formatter: (name) => String(name).replace(/[乡镇]$/, ""),
      },
    },
    yAxis: {
      type: "value",
      min: 0,
      max: axisMax,
      splitNumber: 4,
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: {
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 9,
        margin: 7,
        formatter: (value) => formatCompact(value),
      },
      splitLine: {
        show: true,
        lineStyle: {
          color: "rgba(112, 209, 230, 0.25)",
          type: [4, 4],
          width: 0.7,
        },
      },
    },
    series: [
      {
        name: metricName,
        type: "line",
        smooth: 0.16,
        showSymbol: true,
        symbol: "circle",
        symbolSize: (_value, params) => params.dataIndex === activeIndex ? 7 : 4,
        data: values,
        label: {
          show: true,
          position: "top",
          distance: 5,
          color: HOME_CHART_LABEL_COLOR,
          fontSize: 10,
          fontWeight: 700,
          formatter: ({ value, dataIndex }) => peakIndexes.has(dataIndex) ? formatCompact(value) : "",
        },
        lineStyle: {
          width: 2.5,
          color: "#8dfbff",
          shadowBlur: 7,
          shadowColor: "rgba(48, 220, 255, 0.68)",
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(73, 241, 255, 0.72)" },
            { offset: 0.38, color: "rgba(38, 197, 235, 0.26)" },
            { offset: 1, color: "rgba(13, 78, 111, 0.03)" },
          ]),
        },
        itemStyle: {
          color(params) {
            return params.dataIndex === activeIndex ? "#ffffff" : "#38e6f7"
          },
          borderColor: "#8dfbff",
          borderWidth: 1,
          shadowBlur: 5,
          shadowColor: "rgba(48, 220, 255, 0.58)",
        },
        emphasis: {
          scale: true,
          itemStyle: {
            color: "#ffffff",
            shadowBlur: 5,
            shadowColor: "rgba(141, 251, 255, 0.2)",
          },
        },
        z: 2,
      },
    ],
  })
}

function buildHomeReferenceLineOption(rows = [], metricName = "数值") {
  const chartRows = rows.filter((item) => Number.isFinite(Number(item.value))).slice(0, 10)
  const unit = chartRows.find((item) => item.unit)?.unit || ""
  const values = chartRows.map((item) => Number(item.value) || 0)
  const maxValue = Math.max(...values, 1)
  const topValue = Math.ceil(maxValue * 1.12)

  return withChartDefaults({
    animation: true,
    animationDuration: 900,
    animationDurationUpdate: 700,
    animationEasing: "cubicOut",
    title: {
      text: unit ? `单位：${unit}` : "",
      left: "5%",
      top: "8%",
      textStyle: {
        color: HOME_CHART_MUTED_COLOR,
        fontSize: 10,
        fontWeight: 400,
      },
    },
    grid: {
      left: "12%",
      top: "25%",
      width: "82%",
      height: "55%",
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
        shadowStyle: { opacity: 0.2 },
      },
      backgroundColor: "rgba(0, 0, 0, 0.92)",
      borderWidth: 1,
      borderColor: "rgba(153, 153, 153, 0.95)",
      padding: [8, 10],
      textStyle: {
        color: "#ffffff",
        fontSize: 10,
      },
      formatter(params) {
        const items = Array.isArray(params) ? params : [params]
        const point = items[0]
        const row = chartRows[point?.dataIndex ?? 0]
        if (!row) return ""
        return `${row.name}<br/>${metricName}：${formatNumber(row.value)}${row.unit || unit}`
      },
    },
    color: ["#6BC7F6"],
    xAxis: [
      {
        type: "category",
        data: chartRows.map((item) => item.name),
        axisLine: { show: false },
        axisTick: { show: false },
        splitLine: { show: false },
        axisLabel: {
          color: HOME_CHART_TEXT_COLOR,
          fontSize: 10,
          fontWeight: 700,
          interval: 0,
          padding: [0, 0, 0, 0],
          formatter: formatReferenceTownName,
        },
      },
      {
        data: [],
        axisLine: {
          show: false,
          lineStyle: { color: "rgba(0, 0, 0, 0)" },
        },
        axisTick: { show: false },
        axisLabel: { show: false },
      },
    ],
    yAxis: {
      type: "value",
      min: 0,
      max: topValue,
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: {
        show: true,
        lineStyle: {
          type: [2, 2],
          dashOffset: 5,
          color: HOME_CHART_GRID_COLOR,
          opacity: 1,
          width: 0.3,
        },
      },
      axisLabel: {
        color: HOME_CHART_AXIS_COLOR,
        fontSize: 10,
        formatter: formatCompact,
      },
    },
    series: [
      {
        name: metricName,
        data: values,
        type: "line",
        smooth: true,
        symbol: referenceLinePointImage,
        symbolSize: 10,
        showSymbol: false,
        z: 3,
        emphasis: {
          focus: "none",
          itemStyle: { color: "white" },
        },
        label: {
          show: true,
          position: "top",
          distance: 10,
          color: HOME_CHART_LABEL_COLOR,
          fontSize: 10,
          fontWeight: 700,
          formatter: ({ value }) => formatCompact(value),
        },
        lineStyle: {
          shadowColor: "rgba(0, 0, 0, 0.4)",
          shadowBlur: 3,
          shadowOffsetY: 10,
          width: 3,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(115, 208, 255, 1)" },
            { offset: 1, color: "rgba(18, 60, 98, 1)" },
          ]),
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(25, 255, 236, 0.34)" },
            { offset: 1, color: "rgba(51, 153, 255, 0)" },
          ]),
        },
      },
    ],
  })
}

function isHomeStandardColumnPanel(panel) {
  const key = String(panel?.key || "")
  return 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 monthCount = getForecastMonthCount(period)
  const dayCount = Math.max(1, Math.round(Math.max(1, monthCount - 1) * 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)
    return formatForecastDayLabel(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 formatReferenceTownName(name) {
  return String(name || "")
    .replace(/\s+/g, "")
    .replace(/[镇乡]$/u, "")
    .slice(0, 4)
}

function getHomeToolbarLayer(layerMeta) {
  if (!layerMeta) return null
  const matchedLayer = getLayerByKey(layerMeta.topicKey, layerMeta.layerKey)
  if (matchedLayer) return matchedLayer
  const layers = getLayersByTopic(layerMeta.topicKey)
  return layers.find((item) => item.key === layerMeta.layerKey) || layers[0] || null
}

function createHomeSolidFillSld(layerName, style) {
  const fillOpacity = Number.isFinite(Number(style.fillOpacity)) ? Number(style.fillOpacity) : 0.46
  const stroke = style.stroke || style.strokeColor || "#E5FCFF"
  const strokeOpacity = Number.isFinite(Number(style.strokeOpacity)) ? Number(style.strokeOpacity) : 0.86
  const strokeWidth = Number.isFinite(Number(style.strokeWidth)) ? Number(style.strokeWidth) : 1.1
  return `<?xml version="1.0" encoding="UTF-8"?><StyledLayerDescriptor version="1.0.0" xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc"><NamedLayer><Name>${layerName}</Name><UserStyle><FeatureTypeStyle><Rule><PolygonSymbolizer><Fill><CssParameter name="fill">${style.fill}</CssParameter><CssParameter name="fill-opacity">${fillOpacity}</CssParameter></Fill><Stroke><CssParameter name="stroke">${stroke}</CssParameter><CssParameter name="stroke-opacity">${strokeOpacity}</CssParameter><CssParameter name="stroke-width">${strokeWidth}</CssParameter><CssParameter name="stroke-linejoin">round</CssParameter></Stroke></PolygonSymbolizer></Rule></FeatureTypeStyle></UserStyle></NamedLayer></StyledLayerDescriptor>`
}

function handleHomeToolbarSelect(topicKey) {
  if (!homeToolbarTopics.some((item) => item.key === topicKey)) return
  activeHomeToolbarKey.value = topicKey
}

function handleHomeKeyDown(event) {
  if (event.key !== "Escape") return
  if (homeVideoExpandedIndex.value !== null) {
    closeHomeVideoWebExpanded()
    return
  }
  closeHomeVideoModal()
  closeHomeBalanceModal()
}

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

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

function getHomeFeaturePanelPoint(feature, fallback) {
  return mapSceneRef.value?.getVectorFeatureAnchorScreen?.(feature)
    || mapSceneRef.value?.getVectorFeatureScreen?.(feature)
    || fallback
}

function refreshHomeFeaturePanelAnchor() {
  if (!featurePanelVisible.value || !selectedFeature.value) return
  const nextPoint = getHomeFeaturePanelPoint(selectedFeature.value, featurePanelPoint.value)
  if (!nextPoint) return
  const currentPoint = featurePanelPoint.value
  if (
    currentPoint
    && Math.abs(Number(currentPoint.x) - Number(nextPoint.x)) < 0.5
    && Math.abs(Number(currentPoint.y) - Number(nextPoint.y)) < 0.5
  ) {
    return
  }
  featurePanelPoint.value = nextPoint
}

function estimateFeatureRowCount(feature) {
  const properties = feature?.properties || {}
  const keys = Object.keys(properties).filter((key) => isDisplayableFeatureProperty(key, properties[key]))
  return keys.length
}

function isDisplayableFeatureProperty(key, value) {
  if (value === undefined || value === null || value === "") return false
  if (Array.isArray(value) || typeof value === "object") return false
  return !nonBusinessFeatureKeys.has(String(key || "").toLowerCase())
}

function getHomePointQueryType(layer = {}) {
  const topicKey = String(activeHomeToolbarKey.value || "").toLowerCase()
  const signal = `${layer.key || ""} ${layer.layerName || ""} ${topicKey}`.toLowerCase()
  if (topicKey === "woodland" || signal.includes("woodland") || signal.includes("t_woodland")) return "woodland"
  if (topicKey === "wetland" || signal.includes("wetland") || signal.includes("shidi")) return "wetland"
  if (topicKey === "grassland" || signal.includes("grassland") || signal.includes("caodi")) return "grassland"
  return ""
}

function buildHomePointWkt(lngLat = {}) {
  const lng = formatHomeWktCoordinate(lngLat.lng)
  const lat = formatHomeWktCoordinate(lngLat.lat)
  if (lng === "" || lat === "") return ""
  return `POINT(${lng} ${lat})`
}

function formatHomeWktCoordinate(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return ""
  return number.toFixed(8).replace(/\.?0+$/, "")
}

async function handleHomeMapClick(payload = {}) {
  const lng = Number(payload?.lngLat?.lng)
  const lat = Number(payload?.lngLat?.lat)
  const layer = homeMapLayer.value
  const queryType = getHomePointQueryType(layer)
  if (!Number.isFinite(lng) || !Number.isFinite(lat) || !queryType) {
    closeFeaturePanel()
    return
  }

  const requestId = ++featureQueryRequestId
  const clickScreen = payload.screen || { x: 960, y: 540 }
  featurePanelPoint.value = null
  selectedFeature.value = null
  featureEmpty.value = false
  featureLoading.value = true
  mapSceneRef.value?.clearSelectedFeature?.()

  try {
    const feature = await queryHomePointFeature(layer, { lng, lat }, queryType)
    if (requestId !== featureQueryRequestId || destroyed) return
    if (!feature) {
      closeFeaturePanel()
      return
    }
    selectedFeature.value = feature
    featureEmpty.value = false
    mapSceneRef.value?.highlightFeature?.(feature, { highlightTone: "v21" })
    featurePanelPoint.value = getHomeFeaturePanelPoint(feature, clickScreen)
  } catch (error) {
    if (requestId === featureQueryRequestId) closeFeaturePanel()
  } finally {
    if (requestId === featureQueryRequestId) {
      featureLoading.value = false
    }
  }
}

async function queryHomePointFeature(layer, lngLat, queryType = getHomePointQueryType(layer)) {
  const wkt = buildHomePointWkt(lngLat)
  if (!wkt) return null
  let detail = null
  if (queryType === "grassland") {
    detail = await getLandClickInfo({ wkt })
  } else if (queryType === "wetland") {
    detail = await getWetlandClickInfo({ wkt })
  } else if (queryType === "woodland") {
    detail = await getWoodlandClickInfoByWkt(wkt)
  }
  const item = pickHomePointDetail(detail, queryType)
  if (!hasHomePointDetail(item)) return null
  const feature = normalizeHomePointFeature(item, layer, lngLat, queryType)
  return estimateFeatureRowCount(feature) > 0 ? feature : null
}

function pickHomePointDetail(payload, queryType) {
  const candidates = [
    payload?.[queryType],
    payload?.grassland,
    payload?.wetland,
    payload?.woodland,
    payload?.data?.[queryType],
    payload?.data?.grassland,
    payload?.data?.wetland,
    payload?.data?.woodland,
    payload?.data,
    payload?.result,
    payload,
  ]
  for (const candidate of candidates) {
    const item = pickFirstHomeObject(candidate)
    if (item) return item
  }
  return null
}

function pickFirstHomeObject(value) {
  if (Array.isArray(value)) return value.find((item) => item && typeof item === "object") || null
  return value && typeof value === "object" ? value : null
}

function hasHomePointDetail(detail) {
  if (!detail || typeof detail !== "object") return false
  if (Array.isArray(detail.dyColumn) && detail.dyColumn.some(hasHomeColumnValue)) return true
  return Object.keys(detail).some((key) => isDisplayableFeatureProperty(key, detail[key]))
}

function hasHomeColumnValue(column = {}) {
  const value = column?.value
  if (value === undefined || value === null || value === "") return false
  return Boolean(String(column?.field || column?.title || "").trim())
}

function normalizeHomePointFeature(detail, layer, lngLat, queryType) {
  const properties = normalizeHomePointProperties(detail)
  const geometry = pickHomeGeometry(detail)
  return {
    id: detail.id || detail.patchNo || detail.patch_no || detail.landNumber || detail.land_number || `${queryType}-${lngLat.lng}-${lngLat.lat}`,
    properties,
    feature: {
      type: "Feature",
      properties,
      geometry,
    },
    geometry,
    layer,
    lngLat,
  }
}

function pickHomeGeometry(detail = {}) {
  return detail.geometry
    || detail.geom
    || detail.wkt
    || detail.the_geom
    || detail.theGeom
    || detail.geo
    || detail.geometryWkt
    || detail.geometry_wkt
    || detail.shape
    || null
}

function normalizeHomePointProperties(detail = {}) {
  const properties = { ...detail }
  const dyColumns = Array.isArray(detail.dyColumn) ? detail.dyColumn : []
  dyColumns.forEach((column) => {
    const value = column?.value
    if (value === undefined || value === null || value === "") return
    const titleKey = magicDetailTitleFieldMap[String(column?.title || "").trim()]
    const fieldKey = String(column?.field || "").trim()
    const keys = [fieldKey, camelToSnake(fieldKey), titleKey].filter(Boolean)
    keys.forEach((key) => {
      if (properties[key] === undefined || properties[key] === null || properties[key] === "") {
        properties[key] = value
      }
    })
  })
  return properties
}

function camelToSnake(value) {
  const key = String(value || "").trim()
  if (!key) return ""
  return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()
}

function handleFeatureInfo(payload) {
  const feature = payload?.feature ?? payload ?? null
  if (!feature || estimateFeatureRowCount(feature) <= 0) {
    closeFeaturePanel()
    return
  }
  selectedFeature.value = feature
  featureEmpty.value = false
  if (payload?.screen) {
    featurePanelPoint.value = getHomeFeaturePanelPoint(feature, payload.screen)
  }
}

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

function handleOutsideFeaturePointerDown(event) {
  if (!featurePanelVisible.value) return
  const panel = document.querySelector(".home-map-feature-info")
  const map = document.querySelector(".home-dashboard .cesium-map")
  const target = event.target
  if (panel && target instanceof Node && panel.contains(target)) return
  if (map && target instanceof Node && map.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;

  &::before,
  &::after {
    position: absolute;
    inset: 0;
    content: "";
  }

  &::before {
    opacity: 0.12;
    background: url("@/assets/images/bg.png") center / cover no-repeat;
  }

  &::after {
    background:
      radial-gradient(circle at 50% 52%, transparent 0%, transparent 54%, rgba(2, 13, 20, 0.06) 80%, rgba(2, 12, 18, 0.22) 100%),
      linear-gradient(90deg, rgba(0, 14, 24, 0.12), transparent 26%, transparent 74%, rgba(0, 14, 24, 0.12));
  }
}

.home-bottom-switcher {
  position: absolute;
  left: 50%;
  bottom: 8px;
  z-index: 10;
  width: max-content;
  transform: translateX(-50%);
  pointer-events: auto;
}

.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(170px, 236px) 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-child.is-overlay {
  i {
    border-radius: 1px;
  }

  &.is-active i::after {
    content: "";
    display: block;
    width: 4px;
    height: 2px;
    margin: 2px 0 0 1px;
    border-left: 1px solid #08202d;
    border-bottom: 1px solid #08202d;
    transform: rotate(-45deg);
  }
}

.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-origin: 50% 50%;
  pointer-events: all;
}

.home-left {
  left: 32px;
  perspective: var(--screen-side-perspective-left, 500px);
}

.home-right {
  right: 32px;
  perspective: var(--screen-side-perspective-right, 800px);
}

.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) scaleX(1) scaleY(1) rotateX(0deg) rotateY(6deg) rotateZ(0deg) skewX(0deg) skewY(0deg);
  transform-origin: left center;
}

.home-side-stack.is-right {
  transform: translate3d(0, 0, 0) scaleX(1) scaleY(1) rotateX(0deg) rotateY(-6deg) rotateZ(0deg) skewX(0deg) skewY(0deg);
  transform-origin: right center;
}

.home-card {
  flex: 0 0 auto;

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

  .m-card-hd-bg,
  .m-card-bd-bg {
    filter: none;
  }

  .m-card-bd-bg::after {
    content: none;
  }
}

.home-card .m-card-hd-title {
  color: #fff !important;
  background: -webkit-linear-gradient(rgba(219, 249, 255, 1), rgba(169, 240, 255, 1)) !important;
  -webkit-background-clip: text !important;
  -webkit-text-fill-color: transparent !important;
  font-weight: 600 !important;
  filter: none !important;
  text-shadow: none !important;
}

.home-card-more {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 24px;
  padding: 0 4px 0 8px;
  border: 0;
  color: rgba(220, 250, 255, 0.82);
  font-size: 11px;
  line-height: 24px;
  font-weight: 700;
  background: rgba(4, 35, 51, 0.72);
  cursor: pointer;

  span {
    color: #30dcff;
    font-size: 18px;
    line-height: 18px;
  }

  &:hover {
    color: #ffffff;
    background: rgba(14, 83, 106, 0.82);
  }
}

.home-more-modal {
  position: absolute;
  inset: 0;
  z-index: 140;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 8, 15, 0.76);
  backdrop-filter: blur(3px);
  pointer-events: auto;
}

.home-more-dialog {
  display: flex;
  width: 1240px;
  height: 790px;
  flex-direction: column;
  padding: 20px;
  border: 1px solid rgba(48, 220, 255, 0.42);
  box-sizing: border-box;
  background:
    linear-gradient(180deg, rgba(11, 47, 64, 0.98), rgba(3, 18, 30, 0.99)),
    rgba(3, 18, 30, 0.98);
  box-shadow:
    0 0 34px rgba(48, 220, 255, 0.18),
    inset 0 0 0 1px rgba(255, 255, 255, 0.035);
}

.home-more-dialog-head {
  display: flex;
  height: 56px;
  flex: 0 0 56px;
  align-items: flex-start;
  justify-content: space-between;
  border-bottom: 1px solid rgba(48, 220, 255, 0.2);

  h2,
  p {
    margin: 0;
  }

  h2 {
    color: #ffffff;
    font-size: 21px;
    line-height: 27px;
    font-weight: 800;
  }

  p {
    margin-top: 3px;
    color: rgba(222, 249, 255, 0.65);
    font-size: 12px;
    line-height: 16px;
  }

  > button {
    width: 34px;
    height: 34px;
    padding: 0;
    border: 1px solid rgba(48, 220, 255, 0.32);
    color: rgba(235, 253, 255, 0.9);
    font-size: 25px;
    line-height: 30px;
    background: rgba(48, 220, 255, 0.08);
    cursor: pointer;

    &:hover {
      border-color: rgba(48, 220, 255, 0.72);
      color: #ffffff;
      background: rgba(48, 220, 255, 0.16);
    }
  }
}

.home-video-modal-grid {
  position: relative;
  isolation: isolate;
  display: grid;
  min-height: 0;
  flex: 1;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: 12px;
  padding: 16px 0 12px;
}

.home-video-modal-item {
  position: relative;
  z-index: 2;
  min-width: 0;
  min-height: 0;
  height: auto;
  padding: 0;
  text-align: left;

  &.is-offline {
    cursor: default;
    filter: saturate(0.52);
  }
}

.home-video-modal-item.is-active {
  z-index: 3;
}

.home-video-modal-item:not(.is-active) {
  z-index: 8;
}

.home-video-modal-item:not(.is-active) .home-video-hit {
  z-index: 40;
  pointer-events: auto;
}

.home-video-modal-status {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 5;
  padding: 3px 8px;
  border-left: 2px solid #24f6c4;
  color: #b9ffe9;
  font-size: 11px;
  line-height: 15px;
  font-weight: 800;
  background: rgba(1, 28, 26, 0.76);
}

.home-video-modal-item.is-offline .home-video-modal-status {
  border-left-color: #8bf7ff;
  color: #ffe6a7;
  background: rgba(42, 31, 5, 0.76);
}

.home-more-empty {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  color: rgba(222, 249, 255, 0.58);
  font-size: 15px;
}

.home-more-pager {
  display: flex;
  height: 34px;
  flex: 0 0 34px;
  align-items: center;
  justify-content: center;
  gap: 12px;

  button {
    height: 28px;
    padding: 0 14px;
    border: 1px solid rgba(48, 220, 255, 0.28);
    color: rgba(232, 252, 255, 0.86);
    font-size: 12px;
    background: rgba(48, 220, 255, 0.08);
    cursor: pointer;
  }

  button:disabled {
    cursor: not-allowed;
    opacity: 0.38;
  }

  span {
    min-width: 54px;
    color: rgba(232, 252, 255, 0.72);
    font-family: var(--screen-font-number, D-DIN, Arial, sans-serif);
    font-size: 13px;
    text-align: center;
  }
}

.home-balance-dialog {
  width: 1180px;
  height: 610px;
}

.home-balance-modal-grid {
  display: grid;
  min-height: 0;
  flex: 1;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: 14px;
  padding-top: 18px;
}

.home-balance-modal-item {
  position: relative;
  display: flex;
  min-width: 0;
  flex-direction: column;
  justify-content: center;
  padding: 16px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--balance-color) 44%, rgba(48, 220, 255, 0.16));
  box-sizing: border-box;
  background:
    radial-gradient(circle at 82% 12%, color-mix(in srgb, var(--balance-color) 14%, transparent), transparent 44%),
    linear-gradient(180deg, rgba(15, 57, 72, 0.7), rgba(4, 25, 38, 0.78));
  box-shadow: inset 0 0 20px rgba(48, 220, 255, 0.05);

  &::before {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    content: "";
    background: var(--balance-color);
    box-shadow: 0 0 10px var(--balance-color);
  }

  p {
    margin: 12px 0 0;
    color: rgba(225, 250, 255, 0.68);
    font-size: 12px;
    line-height: 16px;
  }
}

.home-balance-modal-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;

  strong {
    overflow: hidden;
    color: #ffffff;
    font-size: 17px;
    line-height: 22px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span {
    overflow: hidden;
    color: var(--balance-color);
    font-size: 11px;
    line-height: 16px;
    font-weight: 700;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.home-balance-modal-value {
  display: flex;
  align-items: baseline;
  margin-top: 22px;

  strong {
    color: #ffffff;
    font-family: var(--screen-font-number, D-DIN, Arial, sans-serif);
    font-size: 36px;
    line-height: 40px;
    text-shadow: 0 0 12px color-mix(in srgb, var(--balance-color) 38%, transparent);
  }

  em {
    margin-left: 3px;
    color: rgba(235, 253, 255, 0.72);
    font-size: 13px;
    font-style: normal;
  }
}

.home-balance-modal-track {
  height: 5px;
  margin-top: 13px;
  overflow: hidden;
  background: rgba(113, 181, 199, 0.15);

  i {
    display: block;
    width: var(--balance-progress);
    height: 100%;
    background: linear-gradient(90deg, color-mix(in srgb, var(--balance-color) 62%, transparent), var(--balance-color));
    box-shadow: 0 0 8px var(--balance-color);
  }
}

.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: var(--screen-font-number, 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 24px;
  box-sizing: border-box;
  pointer-events: auto;
}

.home-video-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  height: 100%;
  min-height: 0;
}

.home-video-grid.is-single {
  grid-template-columns: minmax(0, 1fr);
}

.home-video-screen {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  min-width: 0;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid rgba(48, 220, 255, 0.25);
  background-color: #020d14;
  background-position: center;
  background-size: cover;
  box-shadow: none;
}

.home-video-screen.is-active {
  border-color: rgba(126, 251, 246, 0.66);
  box-shadow: none;
}

.home-video-screen:not(.is-active) {
  opacity: 0.88;
}

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

.home-video-web-overlay {
  position: fixed;
  inset: 0;
  z-index: 100000;
  overflow: hidden;
  background: #000000;
}

.home-video-web-stage {
  width: 100vw;
  height: 100vh;
  border: 0;
  cursor: default;
}

.home-video-web-player-slot {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.home-video-screen > .home-video-player-host,
.home-video-screen > .home-video-player,
.home-video-screen [id$="-wrap"],
.home-video-screen [id$="-player"],
.home-video-screen [id^="EZUIKitPlayer-"],
.home-video-screen .play-window,
.home-video-screen video,
.home-video-screen canvas,
.home-video-screen iframe,
.home-video-screen object,
.home-video-screen embed,
.home-video-screen [id$="-wrap"] > .home-video-player > div,
.home-video-screen [id$="-wrap"] > .home-video-player .play-window,
.home-video-screen [id$="-wrap"] > .home-video-player [id$="-player"],
.home-video-screen [id$="-wrap"] > .home-video-player video,
.home-video-screen [id$="-wrap"] > .home-video-player canvas,
.home-video-screen [id$="-wrap"] > .home-video-player iframe,
.home-video-screen [id$="-wrap"] > .home-video-player object,
.home-video-screen [id$="-wrap"] > .home-video-player embed {
  position: absolute !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  max-width: none !important;
  max-height: none !important;
  margin: 0 !important;
  object-fit: fill !important;
}

.home-video-screen > .home-video-player-host,
.home-video-screen > .home-video-player,
.home-video-screen [id$="-wrap"] {
  z-index: 3;
  pointer-events: auto;
}

.home-video-screen [id$="-wrap"] {
  z-index: 3 !important;
  overflow: hidden;
  pointer-events: auto;
}

.home-video-screen [id$="-wrap"] > .home-video-player {
  position: absolute !important;
  inset: 0 !important;
  z-index: 1;
  display: block !important;
  width: 100% !important;
  height: 100% !important;
  overflow: hidden;
  pointer-events: auto;
}

.home-video-screen [id$="-wrap"] > .home-video-player > div,
.home-video-screen [id$="-wrap"] > .home-video-player .play-window,
.home-video-screen [id$="-wrap"] > .home-video-player [id$="-player"],
.home-video-screen [id$="-wrap"] > .home-video-player video,
.home-video-screen [id$="-wrap"] > .home-video-player canvas,
.home-video-screen [id$="-wrap"] > .home-video-player iframe {
  z-index: 1;
}

.home-video-screen [id$="-ez-iframe-footer-container"] {
  position: absolute !important;
  right: 0 !important;
  bottom: 0 !important;
  left: 0 !important;
  z-index: 20 !important;
  width: 100% !important;
  margin-top: 0 !important;
  pointer-events: auto !important;
  visibility: visible !important;
  opacity: 1 !important;
}

.home-video-screen [id$="-audioControls"] {
  pointer-events: auto !important;
  visibility: visible !important;
  opacity: 1 !important;
}

.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: 18px;
}

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

.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 {
  z-index: 2;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.14), transparent 28%, rgba(0, 0, 0, 0.18));
}

.home-video-hit {
  position: absolute;
  inset: 0;
  z-index: 7;
  padding: 0;
  border: 0;
  cursor: pointer;
  background: transparent;
}

.home-video-point-name,
.home-video-loading,
.home-video-error {
  pointer-events: none;
}

.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-screen.is-playing .home-video-point-name {
  z-index: 2;
  opacity: 0;
}

.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(139, 247, 255, 0.42);
  color: #c9fbff;
}

</style>
