<template>
  <div class="yak-management">
    <map-scene
      v-show="!isYakOlMapView"
      :key="yakMapSceneKey"
      ref="mapSceneRef"
      class="yak-map-scene"
      :active-layer="threeYakMapLayer"
      :map-ranking="threeYakMapRanking"
      :tile-url-template="yakMapTileUrlTemplate"
      :admin-boundary-line-scale="2.4"
      @layer-status="handleYakLayerStatus"
      @tile-status="handleYakTileStatus"
    />
    <yak-ol-map
      v-if="isYakOlMapView"
      :key="yakOlMapKey"
      ref="yakOlMapRef"
      class="yak-ol-map-layer"
      :active="props.active && isYakOlMapView"
      :tile-url-template="yakMapTileUrlTemplate"
      :center="yakOlMapCenter"
      :max-zoom="21"
      @ready="handleYakOlReady"
      @layer-status="handleYakLayerStatus"
      @tile-status="handleYakTileStatus"
    />
    <div class="yak-map-scrim" :class="{ 'is-ol-map': isYakOlMapView }"></div>
    <div v-if="yakMapLoadingVisible" class="yak-map-loading">{{ yakMapLoadingText }}</div>
    <div class="yak-management-ui">
      <div v-if="activeView === 'trade' && tradeYears.length" class="trade-year-switcher" aria-label="交易年份切换">
        <span>交易年份</span>
        <button
          v-for="year in tradeYears"
          :key="year"
          type="button"
          :class="{ active: Number(year) === Number(activeTradeYear) }"
          @click="handleTradeYearSelect(year)"
        >
          {{ year }}
        </button>
      </div>
      <div v-if="activeView === 'epidemic' && epidemicYears.length" class="epidemic-year-switcher" aria-label="防疫年份切换">
        <span>防疫年份</span>
        <button
          v-for="year in epidemicYears"
          :key="year"
          type="button"
          :class="{ active: Number(year) === Number(activeEpidemicYear) }"
          @click="handleEpidemicYearSelect(year)"
        >
          {{ year }}
        </button>
      </div>
      <div v-if="activeView === 'forecast'" class="forecast-year-switcher" aria-label="预测年份切换">
        <span>预测年份</span>
        <button
          v-for="item in forecastPeriods"
          :key="item.key"
          type="button"
          :class="{ active: item.key === activePeriodKey }"
          @click="handlePeriodSelect(item.key)"
        >
          {{ item.shortLabel }}
        </button>
      </div>

      <div class="yak-left">
        <div class="yak-side-stack is-left">
          <template v-if="activeView === 'structure'">
            <m-card class="yak-side-card left-card" title="繁殖基础评估" :width="sideCardWidth" :height="sideCardHeight">
              <div class="breeding-health-card">
                <div class="breeding-health-head">
                  <strong>{{ breedingHealthStatus.title }}，{{ breedingHealthStatus.summary }}</strong>
                </div>
                <div class="breeding-health-grid">
                  <div
                    v-for="item in breedingHealthRows"
                    :key="item.key"
                    class="breeding-health-item"
                    :style="{ '--health-color': item.color, '--health-width': `${Math.max(5, Math.min(100, item.percent))}%` }"
                  >
                    <span>{{ item.name }}</span>
                    <strong>{{ formatPercent(item.percent) }}</strong>
                    <i><b></b></i>
                    <em>{{ item.desc }}</em>
                  </div>
                </div>
              </div>
            </m-card>

            <m-card class="yak-side-card left-card" title="畜群结构占比" :width="sideCardWidth" :height="sideCardHeight">
              <div class="structure-chart-card">
                <div class="structure-ratio-card">
                  <div class="structure-ratio-chart">
                    <svg viewBox="0 0 150 150" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
                      <g class="structure-rose-guides">
                        <line
                          v-for="segment in structureRoseSegments"
                          :key="`guide-${segment.key}`"
                          :x1="segment.lineStart.x"
                          :y1="segment.lineStart.y"
                          :x2="segment.lineEnd.x"
                          :y2="segment.lineEnd.y"
                          :stroke="segment.color"
                        />
                      </g>
                      <g class="structure-rose-segments">
                        <path
                          v-for="segment in structureRoseSegments"
                          :key="segment.key"
                          class="structure-rose-segment"
                          :class="{ 'is-active': segment.index === activeStructureIndex }"
                          :d="segment.path"
                          :fill="segment.color"
                          :stroke="segment.color"
                        />
                      </g>
                      <circle class="structure-rose-glow" cx="75" cy="75" r="24" />
                      <circle class="structure-rose-hole" cx="75" cy="75" r="16" />
                      <g class="structure-rose-labels">
                        <text
                          v-for="segment in structureRoseSegments"
                          :key="`label-${segment.key}`"
                          :x="segment.label.x"
                          :y="segment.label.y"
                          :text-anchor="segment.anchor"
                        >
                          {{ segment.item.name }}
                        </text>
                      </g>
                    </svg>
                  </div>
                  <div class="structure-ratio-list">
                    <div
                      v-for="(item, index) in structureRatioRows"
                      :key="item.key || item.name"
                      class="structure-ratio-row"
                      :class="{ 'is-active': index === activeStructureIndex }"
                      :style="{ '--row-color': item.color, '--row-percent': `${Math.max(4, item.percent)}%` }"
                    >
                      <i :style="{ background: item.color, color: item.color }"></i>
                      <span>{{ item.name }}</span>
                      <strong>{{ formatCompact(item.count) }}</strong>
                      <em>头</em>
                    </div>
                  </div>
                </div>
                <div v-if="structureLoading" class="yak-data-status">结构数据加载中</div>
                <div v-else-if="structureError" class="yak-data-status is-warning">{{ structureError }}</div>
              </div>
            </m-card>

            <m-card class="yak-side-card left-card" title="母牛/公牛年龄结构" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="genderAgeOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="牦牛乡镇分布" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="townStackOption" :autoresize="true" />
            </m-card>
          </template>

          <template v-else-if="activeView === 'forecast'">
            <m-card class="yak-side-card left-card" title="存栏变化拆解" :width="sideCardWidth" :height="sideCardHeight">
              <div class="forecast-waterfall-card">
                <div class="forecast-waterfall-head">
                  <span>{{ activePeriod.label }}变化路径</span>
                  <strong :class="{ positive: activeForecastChange > 0, negative: activeForecastChange < 0 }">
                    净变化 {{ formatSigned(activeForecastChange) }}<em>头</em>
                  </strong>
                </div>
                <div class="forecast-waterfall-list">
                  <div
                    v-for="item in forecastWaterfallRows"
                    :key="item.key"
                    class="forecast-waterfall-row"
                    :class="[`is-${item.type}`, { positive: item.delta > 0, negative: item.delta < 0 }]"
                    :style="{
                      '--waterfall-left': `${item.left}%`,
                      '--waterfall-width': `${item.width}%`,
                      '--waterfall-color': item.color,
                    }"
                  >
                    <span>{{ item.name }}</span>
                    <i><b></b></i>
                    <strong>{{ item.display }}</strong>
                  </div>
                </div>
              </div>
            </m-card>

            <m-card class="yak-side-card left-card" title="未来存栏趋势" :width="sideCardWidth" :height="sideCardHeight">
              <div class="forecast-trend-card">
                <div class="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 class="yak-chart forecast-kline-chart" :option="forecastTrendOption" :autoresize="true" />
              </div>
            </m-card>

            <m-card class="yak-side-card left-card" title="月度数量预测" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="monthlyForecastOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="预测因子雷达" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="forecastFactorOption" :autoresize="true" />
            </m-card>
          </template>

          <template v-else-if="activeView === 'epidemic'">
            <m-card class="yak-side-card left-card" title="防疫统计" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="epidemicPreventionStatOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="养殖户防疫排行" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="epidemicOwnerOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="防疫作业趋势" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="epidemicTrendOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="疫苗类型分布" :width="sideCardWidth" :height="sideCardHeight">
              <div v-if="epidemicMedicinePieRows.length" class="epidemic-three-pie-card">
                <div class="epidemic-three-pie-visual">
                  <MPie
                    :key="epidemicMedicinePieKey"
                    :data="epidemicMedicinePieRows"
                    :delay="3000"
                    :colors="epidemicPieColors"
                    :opacity="0.62"
                    class="epidemic-three-pie"
                  >
                    <template #default="{ data }">
                      <div class="epidemic-three-pie-center">
                        <strong>{{ formatCompact(data?.count || epidemicRecordTotal) }}</strong>
                        <span>{{ data?.name || "疫苗类型" }}</span>
                      </div>
                    </template>
                  </MPie>
                </div>
                <div class="epidemic-three-pie-legend">
                  <div
                    v-for="(item, index) in epidemicMedicinePieRows"
                    :key="item.key || item.name"
                    class="epidemic-three-pie-row"
                  >
                    <i :style="{ borderColor: epidemicPieColors[index % epidemicPieColors.length] }"></i>
                    <span>{{ item.name }}</span>
                    <strong>{{ formatCompact(item.value) }}</strong>
                    <em>{{ item.unit }}</em>
                  </div>
                </div>
              </div>
              <div v-else class="epidemic-empty">暂无疫苗类型数据</div>
            </m-card>
          </template>

          <template v-else-if="activeView === 'trade'">
            <m-card class="yak-side-card left-card" title="交易异常关注" :width="sideCardWidth" :height="sideCardHeight">
              <div class="trade-risk-card">
                <div class="trade-risk-head">
                  <span>{{ activeTradeYear }}年流通监管线索</span>
                  <strong>{{ tradeRiskSummary }}</strong>
                </div>
                <div class="trade-risk-list">
                  <div
                    v-for="item in tradeRiskRows"
                    :key="item.key"
                    class="trade-risk-row"
                    :class="`is-${item.level}`"
                    :style="{ '--risk-color': item.color }"
                  >
                    <i></i>
                    <span>
                      <em><b>{{ item.badge }}</b>{{ item.name }}</em>
                      <small>{{ item.value }}｜{{ item.desc }}</small>
                    </span>
                  </div>
                </div>
              </div>
            </m-card>

            <m-card class="yak-side-card left-card" title="交易流通趋势" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="tradeTrendOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="来源乡镇分布" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="tradeTownOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card left-card" title="用途结构统计" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="tradeUsageOption" :autoresize="true" />
            </m-card>
          </template>
        </div>
      </div>

      <div class="yak-right">
        <div class="yak-side-stack is-right">
          <template v-if="activeView === 'structure'">
            <m-card class="yak-side-card right-card" title="乡镇存栏排行" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="townRankOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="母牛与幼畜分布" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="foundationStructureOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="出栏/交易影响" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="tradeImpactOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="性别年龄汇总" :width="sideCardWidth" :height="sideCardHeight">
              <div class="structure-chart-card">
                <div class="structure-summary-card">
                  <div
                    v-for="item in structureSummaryRows"
                    :key="item.key || item.name"
                    class="structure-summary-row"
                    :style="{ '--structure-color': item.color, '--structure-width': `${structureSummaryBarWidth(item)}%` }"
                  >
                    <span><i></i>{{ item.name }}</span>
                    <b><em></em></b>
                    <strong>{{ formatCompact(item.count) }}<em>头</em></strong>
                    <small>{{ formatPercent(item.percent) }}</small>
                  </div>
                </div>
                <div v-if="structureLoading" class="yak-data-status">结构数据加载中</div>
                <div v-else-if="structureError" class="yak-data-status is-warning">{{ structureError }}</div>
              </div>
            </m-card>
          </template>

          <template v-else-if="activeView === 'forecast'">
            <m-card class="yak-side-card right-card" title="乡镇预测排行" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="forecastTownOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="期末结构预测" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="forecastStructureOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="补充/流出对比" :width="sideCardWidth" :height="sideCardHeight">
              <v-chart class="yak-chart" :option="flowComparisonOption" :autoresize="true" />
            </m-card>

            <m-card class="yak-side-card right-card" title="预测变化热力" :width="sideCardWidth" :height="sideCardHeight">
              <div class="forecast-heat-list">
                <div
                  v-for="item in forecastHeatRows"
                  :key="item.name"
                  class="heat-row"
                  :class="{ 'is-negative': item.change < 0 }"
                  :style="forecastHeatStyle(item)"
                  :title="forecastHeatTitle(item)"
                >
                  <span>{{ item.name }}</span>
                  <b><i></i></b>
                  <strong>{{ formatSigned(item.change) }}</strong>
                </div>
              </div>
            </m-card>
          </template>

          <template v-else-if="activeView === 'epidemic'">
            <m-card class="yak-side-card right-card epidemic-ledger-card" title="防疫记录台账" :width="sideCardWidth" :height="epidemicLedgerCardHeight">
              <div class="epidemic-record-table is-full">
                <div class="epidemic-record-toolbar">
                  <span>{{ activeEpidemicYear }}年 共 {{ formatNumber(epidemicTotal) }} 条</span>
                  <div>
                    <input v-model="epidemicRecordKeyword" type="search" placeholder="耳标/养殖户/疫苗" @keyup.enter="searchEpidemicRecords" />
                    <button type="button" @click="searchEpidemicRecords">搜索</button>
                    <button type="button" class="is-more" @click="openEpidemicLedgerModal()">查看更多</button>
                  </div>
                </div>
                <div class="epidemic-record-header">
                  <span>电子耳标</span>
                  <span>养殖户</span>
                  <span>镇/乡</span>
                  <span>疫苗</span>
                  <span>时间</span>
                </div>
                <div class="epidemic-record-rows">
                  <button
                    v-for="row in epidemicLedgerPreviewRows"
                    :key="row.epidemicId || row.earTag"
                    type="button"
                    @click="openEpidemicLedgerModal(row)"
                  >
                    <span>{{ displayValue(row.earTag) }}</span>
                    <span>{{ displayValue(row.owner) }}</span>
                    <span>{{ displayValue(row.town) }}</span>
                    <strong>{{ displayValue(row.medicine) }}</strong>
                    <span>{{ displayDate(row.operateTime) }}</span>
                  </button>
                  <div v-if="!epidemicRecords.length" class="epidemic-empty">{{ epidemicLoading ? "防疫台账读取中" : epidemicError || "暂无防疫记录" }}</div>
                </div>
                <div class="epidemic-record-pager">
                  <span>第 {{ epidemicRecordPage }} / {{ epidemicRecordPageCount }} 页</span>
                  <button type="button" :disabled="epidemicRecordPage <= 1" @click="changeEpidemicRecordPage(epidemicRecordPage - 1)">上一页</button>
                  <button type="button" :disabled="epidemicRecordPage >= epidemicRecordPageCount" @click="changeEpidemicRecordPage(epidemicRecordPage + 1)">下一页</button>
                </div>
              </div>
            </m-card>
          </template>

          <template v-else-if="activeView === 'trade'">
            <m-card class="yak-side-card right-card" title="出证结构统计" :width="sideCardWidth" :height="sideCardHeight">
              <div class="trade-cert-card">
                <div class="trade-cert-main">
                  <span>
                    <em>省外流通 A证</em>
                    <strong>{{ formatNumber(tradeMetricValue("outProvinceHeads", "aCertHeads")) }}<b>头</b></strong>
                    <small>{{ formatNumber(tradeMetricValue("outProvinceCount", "aCertCount")) }}条记录</small>
                  </span>
                  <span>
                    <em>省内流通 B证</em>
                    <strong>{{ formatNumber(tradeMetricValue("inProvinceHeads", "bCertHeads")) }}<b>头</b></strong>
                    <small>{{ formatNumber(tradeMetricValue("inProvinceCount", "bCertCount")) }}条记录</small>
                  </span>
                </div>
                <v-chart class="trade-cert-chart" :option="tradeCertOption" :autoresize="true" />
              </div>
            </m-card>

            <m-card class="yak-side-card right-card trade-source-card" title="出栏来源排行" :width="sideCardWidth" :height="tradeSourceCardHeight">
              <div class="trade-dest-list">
                <div
                  v-for="(item, index) in tradeTownSourceRows"
                  :key="item.name"
                  class="trade-dest-row"
                  :style="{ '--trade-ratio': `${item.ratio}%` }"
                >
                  <span>{{ index + 1 }}</span>
                  <strong>{{ item.name }}</strong>
                  <em>{{ formatCompact(item.headCount) }}头</em>
                </div>
                <div v-if="!tradeTownSourceRows.length" class="epidemic-empty">{{ tradeLoading ? "交易数据读取中" : tradeError || "暂无出栏来源数据" }}</div>
              </div>
            </m-card>

            <m-card class="yak-side-card right-card trade-ledger-card" title="交易出证台账" :width="sideCardWidth" :height="tradeLedgerCardHeight">
              <div class="trade-ledger-table">
                <div class="trade-ledger-toolbar">
                  <span>共 {{ formatNumber(tradeRecordTotal) }} 条</span>
                  <div>
                    <input v-model="tradeRecordKeyword" type="search" placeholder="货主/车辆/检疫号/地区" @keyup.enter="searchTradeRecords" />
                    <button type="button" @click="searchTradeRecords">搜索</button>
                    <button type="button" class="is-more" @click="openTradeLedgerModal()">查看更多</button>
                  </div>
                </div>
                <div class="trade-ledger-header">
                  <span>货主</span>
                  <span>车牌</span>
                  <span>数量</span>
                  <span>来源</span>
                  <span>用途</span>
                  <span>时间</span>
                </div>
                <div class="trade-ledger-rows">
                  <button
                    v-for="row in tradeLedgerRows"
                    :key="row.certId"
                    type="button"
                    @click="locateTradeRecord(row)"
                  >
                    <span>{{ displayValue(row.ownerName) }}</span>
                    <span>{{ displayValue(row.transportNumber) }}</span>
                    <strong>{{ formatNumber(row.amount) }}头</strong>
                    <span>{{ displayValue(row.startTown) }}</span>
                    <span>{{ displayValue(row.usageTypeName) }}</span>
                    <span>{{ displayDate(row.issueTime) }}</span>
                  </button>
                  <div v-if="!tradeLedgerRows.length" class="epidemic-empty">{{ tradeLoading ? "交易台账读取中" : tradeError || "暂无交易记录" }}</div>
                </div>
                <div class="trade-ledger-pager">
                  <span>第 {{ tradeRecordPage }} / {{ tradeRecordPageCount }} 页</span>
                  <button type="button" :disabled="tradeRecordPage <= 1" @click="changeTradeRecordPage(tradeRecordPage - 1)">上一页</button>
                  <button type="button" :disabled="tradeRecordPage >= tradeRecordPageCount" @click="changeTradeRecordPage(tradeRecordPage + 1)">下一页</button>
                </div>
              </div>
            </m-card>
          </template>
        </div>
      </div>

      <div class="yak-bottom-topic">
        <topic-switcher
          :model-value="activeView"
          :topics="viewTopics"
          @update:model-value="activeView = $event"
        />
      </div>
    </div>

    <div v-if="epidemicLedgerModalVisible" class="epidemic-ledger-modal" @click.self="closeEpidemicLedgerModal">
      <div class="epidemic-ledger-dialog">
        <div class="epidemic-ledger-dialog-head">
          <div>
            <span>牦牛防疫台账</span>
            <strong>{{ activeEpidemicYear }}年 {{ formatNumber(epidemicLedgerTotal) }} 条记录</strong>
          </div>
          <button type="button" @click="closeEpidemicLedgerModal">关闭</button>
        </div>

        <div class="epidemic-ledger-filters">
          <input v-model="epidemicLedgerFilters.keyword" type="search" placeholder="耳标 / 养殖户 / 药品" aria-label="耳标 / 养殖户 / 药品" @keyup.enter="searchEpidemicLedger" />
          <select v-model="epidemicLedgerFilters.town">
            <option value="">全部镇/乡</option>
            <option v-for="item in epidemicTownFilterOptions" :key="item" :value="item">{{ item }}</option>
          </select>
          <select v-model="epidemicLedgerFilters.medicine">
            <option value="">全部疫苗</option>
            <option v-for="item in epidemicMedicineFilterOptions" :key="item" :value="item">{{ item }}</option>
          </select>
          <input v-model="epidemicLedgerFilters.startDate" type="date" />
          <input v-model="epidemicLedgerFilters.endDate" type="date" />
          <button type="button" @click="searchEpidemicLedger">检索</button>
          <button type="button" class="is-ghost" @click="resetEpidemicLedgerFilters">重置</button>
        </div>

        <div class="epidemic-ledger-workspace">
          <div class="epidemic-ledger-list">
            <div class="epidemic-ledger-table-head">
              <span>电子耳标</span>
              <span>养殖户</span>
              <span>镇/乡</span>
              <span>村</span>
              <span>疫苗</span>
              <span>免疫时间</span>
              <span>图片</span>
            </div>
            <button
              v-for="row in epidemicLedgerRows"
              :key="row.epidemicId || row.earTag"
              type="button"
              :class="{ active: (epidemicLedgerSelectedRecord?.epidemicId || epidemicLedgerSelectedRecord?.earTag) === (row.epidemicId || row.earTag) }"
              @click="selectEpidemicLedgerRecord(row)"
            >
              <span>{{ displayValue(row.earTag) }}</span>
              <span>{{ displayValue(row.owner) }}</span>
              <span>{{ displayValue(row.town) }}</span>
              <span>{{ displayValue(row.village) }}</span>
              <strong>{{ displayValue(row.medicine) }}</strong>
              <span>{{ displayDate(row.operateTime) }}</span>
              <span>{{ getImagePaths(row).length ? `${getImagePaths(row).length}张` : "无" }}</span>
            </button>
            <div v-if="!epidemicLedgerRows.length" class="epidemic-empty">{{ epidemicLedgerLoading ? "台账读取中" : epidemicLedgerError || "暂无匹配记录" }}</div>
          </div>

          <div class="epidemic-ledger-detail trade-ledger-detail epidemic-ledger-record-detail">
            <div class="epidemic-detail-head trade-detail-head">
              <div>
                <span>{{ displayValue(activeLedgerDetail.earTag) }}</span>
                <em>{{ displayValue(activeLedgerDetail.medicine || activeLedgerDetail.preventionType) }}</em>
              </div>
              <strong>{{ epidemicLedgerDetailLoading ? "加载中" : displayDate(activeLedgerDetail.operateTime) }}</strong>
            </div>
            <div class="epidemic-detail-grid is-modal trade-detail-grid epidemic-detail-flat-grid">
              <span v-for="field in activeLedgerDetailFields" :key="field.label" :class="{ 'is-wide': field.wide }">
                <em>{{ field.label }}</em>
                <strong>{{ field.value }}</strong>
              </span>
            </div>
            <div v-if="activeLedgerImages.length" class="epidemic-detail-photos">
              <div class="epidemic-detail-photo-title">现场照片</div>
              <div class="epidemic-modal-photo-strip">
                <button
                  v-for="path in activeLedgerImages.slice(0, 6)"
                  :key="path"
                  type="button"
                  class="epidemic-modal-photo-thumb"
                  @click.stop="openEpidemicImagePreview(path)"
                >
                  <img
                    :src="getEpidemicImageUrl(path)"
                    alt="防疫照片"
                  />
                </button>
              </div>
            </div>
          </div>
        </div>

        <div class="epidemic-ledger-pager">
          <span>第 {{ epidemicLedgerPage }} / {{ epidemicLedgerPageCount }} 页</span>
          <button type="button" :disabled="epidemicLedgerPage <= 1" @click="changeEpidemicLedgerPage(epidemicLedgerPage - 1)">上一页</button>
          <button type="button" :disabled="epidemicLedgerPage >= epidemicLedgerPageCount" @click="changeEpidemicLedgerPage(epidemicLedgerPage + 1)">下一页</button>
        </div>
      </div>
    </div>

    <div v-if="tradeLedgerModalVisible" class="epidemic-ledger-modal trade-ledger-modal" @click.self="closeTradeLedgerModal">
      <div class="epidemic-ledger-dialog trade-ledger-dialog">
        <div class="epidemic-ledger-dialog-head">
          <div>
            <span>牦牛交易出证台账</span>
            <strong>{{ activeTradeYear }}年 {{ formatNumber(tradeLedgerModalTotal) }} 条记录</strong>
          </div>
          <button type="button" @click="closeTradeLedgerModal">关闭</button>
        </div>

        <div class="epidemic-ledger-filters trade-ledger-filters">
          <input v-model="tradeLedgerModalFilters.keyword" type="search" placeholder="货主 / 车辆 / 检疫号 / 地区" @keyup.enter="searchTradeLedgerModal" />
          <select v-model="tradeLedgerModalFilters.startTown">
            <option value="">全部来源乡镇</option>
            <option v-for="item in tradeTownFilterOptions" :key="item" :value="item">{{ item }}</option>
          </select>
          <select v-model="tradeLedgerModalFilters.usageType">
            <option value="">全部用途</option>
            <option v-for="item in tradeUsageFilterOptions" :key="item" :value="item">{{ item }}</option>
          </select>
          <select v-model="tradeLedgerModalFilters.certType">
            <option value="">全部证书</option>
            <option v-for="item in tradeCertFilterOptions" :key="item.value" :value="item.value">{{ item.label }}</option>
          </select>
          <button type="button" @click="searchTradeLedgerModal">检索</button>
          <button type="button" class="is-ghost" @click="resetTradeLedgerModalFilters">重置</button>
        </div>

        <div class="epidemic-ledger-workspace trade-ledger-workspace">
          <div class="epidemic-ledger-list trade-ledger-modal-list">
            <div class="trade-ledger-modal-table-head">
              <span>货主</span>
              <span>证书</span>
              <span>车牌号</span>
              <span>数量</span>
              <span>来源</span>
              <span>用途</span>
              <span>出证时间</span>
              <span>状态</span>
            </div>
            <button
              v-for="row in tradeLedgerModalRows"
              :key="row.certId || row.factoryCode"
              type="button"
              :class="{ active: activeTradeLedgerRecordKey === (row.certId || row.factoryCode) }"
              @click="selectTradeLedgerRecord(row)"
            >
              <span>{{ displayValue(row.ownerName) }}</span>
              <span>{{ displayValue(row.certType || row.certScopeName) }}</span>
              <span>{{ displayValue(row.transportNumber) }}</span>
              <strong>{{ formatNumber(row.amount) }}头</strong>
              <span>{{ displayValue(row.startTown) }}</span>
              <span>{{ displayValue(row.usageTypeName) }}</span>
              <span>{{ displayDate(row.issueTime) }}</span>
              <span>{{ displayValue(row.statusName) }}</span>
            </button>
            <div v-if="!tradeLedgerModalRows.length" class="epidemic-empty">{{ tradeLedgerModalLoading ? "交易台账读取中" : tradeLedgerModalError || "暂无匹配记录" }}</div>
          </div>

          <div class="epidemic-ledger-detail trade-ledger-detail">
            <div class="epidemic-detail-head trade-detail-head">
              <div>
                <span>{{ displayValue(activeTradeLedgerDetail.factoryCode || activeTradeLedgerDetail.certId) }}</span>
                <em>{{ displayTradeCertScope(activeTradeLedgerDetail) }}</em>
              </div>
              <strong>{{ displayDate(activeTradeLedgerDetail.issueTime) }}</strong>
            </div>
            <div class="epidemic-detail-grid is-modal trade-detail-grid">
              <span class="is-wide"><em>货主</em><strong>{{ displayValue(activeTradeLedgerDetail.ownerName) }}</strong></span>
              <span><em>联系方式</em><strong>{{ maskPhone(activeTradeLedgerDetail.ownerPhone) }}</strong></span>
              <span><em>证书类型</em><strong>{{ displayTradeCertScope(activeTradeLedgerDetail) }}</strong></span>
              <span><em>车牌号</em><strong>{{ displayValue(activeTradeLedgerDetail.transportNumber) }}</strong></span>
              <span><em>牦牛数量</em><strong>{{ formatNumber(activeTradeLedgerDetail.amount) }}头</strong></span>
              <span><em>用途</em><strong>{{ displayValue(activeTradeLedgerDetail.usageTypeName) }}</strong></span>
              <span><em>来源乡镇</em><strong>{{ displayValue(activeTradeLedgerDetail.startTown) }}</strong></span>
              <span><em>目的省份</em><strong>{{ displayValue(activeTradeLedgerDetail.destProvince) }}</strong></span>
              <span><em>检疫员</em><strong>{{ displayValue(activeTradeLedgerDetail.quarantineOfficerName) }}</strong></span>
              <span class="is-wide"><em>状态</em><strong>{{ displayValue(activeTradeLedgerDetail.statusName) }}</strong></span>
              <span class="is-wide"><em>来源地区</em><strong>{{ displayValue(activeTradeLedgerDetail.startRegionFullName) }}</strong></span>
              <span class="is-wide"><em>目的地</em><strong>{{ displayValue(activeTradeLedgerDetail.destRegionFullName) }}</strong></span>
            </div>
            <button
              type="button"
              class="trade-detail-locate"
              :disabled="!activeTradeLedgerDetail.startTown"
              @click="locateTradeRecord(activeTradeLedgerDetail)"
            >
              查看乡镇位置
            </button>
          </div>
        </div>

        <div class="epidemic-ledger-pager">
          <span>第 {{ tradeLedgerModalPage }} / {{ tradeLedgerModalPageCount }} 页</span>
          <button type="button" :disabled="tradeLedgerModalPage <= 1" @click="changeTradeLedgerModalPage(tradeLedgerModalPage - 1)">上一页</button>
          <button type="button" :disabled="tradeLedgerModalPage >= tradeLedgerModalPageCount" @click="changeTradeLedgerModalPage(tradeLedgerModalPage + 1)">下一页</button>
        </div>
      </div>
    </div>

    <div v-if="epidemicImagePreviewVisible" class="epidemic-image-preview-modal" @click.self="closeEpidemicImagePreview">
      <div class="epidemic-image-preview-dialog">
        <div class="epidemic-image-preview-head">
          <div>
            <span>防疫照片</span>
            <strong>{{ displayValue(activeLedgerDetail.earTag) }}</strong>
          </div>
          <button type="button" @click="closeEpidemicImagePreview">关闭</button>
        </div>
        <div class="epidemic-image-preview-body">
          <div class="epidemic-image-preview-frame">
            <img :src="epidemicImagePreviewUrl" alt="防疫照片预览" />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import * as echarts from "echarts"
import VChart from "vue-echarts"
import mCard from "@/components/mCard/index.vue"
import MPie from "@/components/mPie/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import YakOlMap from "./YakOlMap.vue"
import {
  getYakEpidemicAnalysis,
  getYakEpidemicDetail,
  getYakEpidemicRecords,
  getYakEpidemicStats,
  getYakEpidemicVillageStats,
  getYakForecastRows,
  getYakStructureRows,
  getYakTownRows,
  getYakTradeOptions,
  getYakTradeOverview,
  getYakTradeRecords,
} from "@/services/yakApi"
import {
  buildForecastSeries,
  aggregateTownRows,
  canonicalTownName,
  estimatedStock,
  expertCorrection,
  forecastFactorRows,
  forecastPeriods,
  genderAgeRows,
  getForecastPeriod,
  getForecastStructureRows,
  getTownForecastRows,
  recognitionTotal,
  scaleTownRowsToTotal,
  structureRows as modelStructureRows,
  townRows,
  tradeDeduction,
  tradeImpactRows,
  yakForecastRows,
} from "./data"

const props = defineProps({
  active: {
    type: Boolean,
    default: true,
  },
})
const emit = defineEmits(["module-stats"])
const sideCardWidth = 398
const sideCardGap = 12
const screenViewportHeight = ref(typeof window === "undefined" ? 1080 : window.innerHeight)
const sidePanelTop = 128
const sidePanelBottomGap = 8
const sideCardMinHeight = 227
const sideCardHeight = computed(() => {
  const viewportHeight = Math.max(1080, Number(screenViewportHeight.value) || 1080)
  const fittedHeight = Math.floor((viewportHeight - sidePanelTop - sidePanelBottomGap - sideCardGap * 3) / 4)
  return Math.max(sideCardMinHeight, fittedHeight)
})
const epidemicLedgerCardHeight = computed(() => sideCardHeight.value * 4 + sideCardGap * 3)
const tradeSourceCardHeight = computed(() => sideCardHeight.value + 24)
const tradeLedgerCardHeight = computed(() => sideCardHeight.value * 2 - sideCardGap)
const epidemicImageBaseUrl = "https://qkl-hy.oss-cn-chengdu.aliyuncs.com/yak-vaccine-images"
const yakHighResolutionTileUrlTemplate = "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png"
const yakStructureMapCenter = [103.07984, 33.029058]
const yakForecastMapCenter = [102.682809, 33.131561]
const yakTradeMapView = {
  targetLngLat: [102.58, 32.69],
  targetLocalZ: 0.72,
  distanceScale: 1.12,
  duration: 0.75,
}
const yakMapSceneRevision = "hy-result-dynamic-zoom21-v4"
const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"]
const epidemicPieColors = ["#24f6c4", "#32dcff", "#ffe06b", "#7ee269", "#ff9a62", "#9ea7ff"]
const tradePalette = ["#30dcff", "#ffd76c", "#27f0c0", "#7aa7ff", "#ff8f6b", "#a6f06d", "#9ea7ff"]
const yakTradeMarket = {
  name: "红原牦牛交易市场",
  lng: 102.562157,
  lat: 32.820484,
}

const viewTopics = [
  { key: "structure", name: "畜群结构", color: "#30dcff" },
  { key: "forecast", name: "畜群预测", color: "#ffd76c" },
  { key: "epidemic", name: "牦牛防疫", color: "#27f0c0" },
  { key: "trade", name: "牦牛交易", color: "#ffcf62" },
]

const activeView = ref("structure")
const activePeriodKey = ref("2027")
const activeForecastGranularity = ref("month")
const activeFoundationPage = ref(0)
const apiTownRows = ref([])
const apiMonthlyRows = ref([])
const apiStructureRows = ref([])
const structureLoading = ref(false)
const structureError = ref("")
const epidemicYears = [2025, 2026]
const activeEpidemicYear = ref(2026)
const epidemicAnalysis = ref({ metrics: {} })
const epidemicStats = ref({ rows: [], year: null })
const epidemicVillageStats = ref({ rows: [] })
const epidemicRecords = ref([])
const epidemicTotal = ref(0)
const epidemicRecordPage = ref(1)
const epidemicRecordPageSize = 18
const epidemicRecordKeyword = ref("")
const epidemicRecordQueryKeyword = ref("")
const epidemicLoading = ref(false)
const epidemicError = ref("")
const epidemicLedgerModalVisible = ref(false)
const epidemicLedgerRows = ref([])
const epidemicLedgerTotal = ref(0)
const epidemicLedgerPage = ref(1)
const epidemicLedgerPageSize = 14
const epidemicLedgerLoading = ref(false)
const epidemicLedgerError = ref("")
const epidemicLedgerSelectedRecord = ref(null)
const epidemicLedgerSelectedDetail = ref(null)
const epidemicLedgerDetailLoading = ref(false)
const epidemicImagePreviewVisible = ref(false)
const epidemicImagePreviewUrl = ref("")
const epidemicLedgerFilters = ref(createEpidemicLedgerFilters())
const tradeOptions = ref({ years: [], latestYear: null })
const tradeOverview = ref({ metrics: {}, hasData: false })
const tradeRecords = ref([])
const tradeRecordTotal = ref(0)
const activeTradeYear = ref(null)
const tradeLoading = ref(false)
const tradeError = ref("")
const tradeRecordPage = ref(1)
const tradeRecordPageSize = 12
const tradeRecordKeyword = ref("")
const tradeRecordQueryKeyword = ref("")
const tradeLedgerModalVisible = ref(false)
const tradeLedgerModalRows = ref([])
const tradeLedgerModalTotal = ref(0)
const tradeLedgerModalPage = ref(1)
const tradeLedgerModalPageSize = 14
const tradeLedgerModalLoading = ref(false)
const tradeLedgerModalError = ref("")
const tradeLedgerModalSelectedRecord = ref(null)
const tradeLedgerModalFilters = ref({
  keyword: "",
  startTown: "",
  usageType: "",
  certType: "",
})
const mapSceneRef = ref(null)
const yakOlMapRef = ref(null)
const mapReady = ref(false)
const olMapReady = ref(false)
const yakMapSceneResetRevision = ref(0)
const yakOlMapKey = ref(0)
const tileStatus = ref({ type: "idle", text: "" })
const topicLayerStatus = ref({ type: "idle", text: "" })
let mapAssets = null
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
let destroyed = false
let forecastRotationTimer = null
let foundationRotationTimer = null
let epidemicLedgerDetailToken = 0
const forecastRotationInterval = 5000
const foundationPageSize = 6
const foundationRotationInterval = 4200
const forecastGranularityOptions = [
  { key: "day", label: "日", unit: "日", stepDays: 1 },
  { key: "week", label: "周", unit: "周", stepDays: 7 },
  { key: "month", label: "月", unit: "月", stepDays: 30 },
]

const activePeriod = computed(() => getForecastPeriod(activePeriodKey.value))
const yakMapTileUrlTemplate = computed(() => (activeView.value === "trade" ? "" : yakHighResolutionTileUrlTemplate))
const yakMapTileModeKey = computed(() => (yakMapTileUrlTemplate.value ? "hy-result" : "no-hy-result"))
const yakMapSceneKey = computed(() => `yak-management-map-${yakMapTileModeKey.value}-${yakMapSceneRevision}-${yakMapSceneResetRevision.value}`)
const stockNetAdjustment = computed(() => expertCorrection + tradeDeduction)
const activeForecastChange = computed(() => activePeriod.value.stock - estimatedStock)
const forecastWaterfallRows = computed(() => buildForecastWaterfallRows(activePeriod.value))
const structureChartRows = computed(() => normalizeStructureChartRows(apiStructureRows.value, modelStructureRows))
const structureChartTotal = computed(() => structureChartRows.value.reduce((sum, item) => sum + Number(item.count || 0), 0))
const structureDisplayRows = computed(() => {
  const total = structureChartTotal.value || 1
  return structureChartRows.value.map((item, index) => {
    const count = Number(item.count || 0)
    const ratio = Number(item.ratio)
    return {
      ...item,
      count,
      color: palette[index % palette.length],
      percent: Number.isFinite(ratio) ? ratio : (count / total) * 100,
    }
  })
})
const structureSummaryRows = computed(() => buildStructureSummaryRows(structureDisplayRows.value))
const structureDonutRows = computed(() => buildStructureCompositionRows(structureChartRows.value, modelStructureRows))
const structureRatioRows = computed(() => structureDonutRows.value)
const breedingHealthRows = computed(() => buildBreedingHealthRows(structureChartRows.value, modelStructureRows))
const breedingHealthStatus = computed(() => buildBreedingHealthStatus(breedingHealthRows.value))
const structureSummaryMaxCount = computed(() => Math.max(...structureSummaryRows.value.map((item) => Number(item.count || 0)), 1))
const activeStructureIndex = computed(() => structureDisplayRows.value.length ? 0 : -1)
const structureRoseSegments = computed(() => {
  const data = structureRatioRows.value
  const count = data.length
  if (!count) return []
  const max = Math.max(...data.map((item) => Number(item.count || 0)), 1)
  const step = 360 / count
  const gap = Math.min(8, step * 0.16)
  return data.map((item, index) => {
    const value = Number(item.count || 0)
    const isActive = index === activeStructureIndex.value
    const radius = 36 + Math.sqrt(value / max) * 31 + (isActive ? 5 : 0)
    const innerRadius = 17
    const start = -90 + index * step + gap / 2
    const end = -90 + (index + 1) * step - gap / 2
    const mid = (start + end) / 2
    const lineStart = polarPoint(75, 75, radius + 3, mid)
    const lineEnd = polarPoint(75, 75, radius + 15, mid)
    const label = polarPoint(75, 75, radius + 20, mid)
    return {
      key: item.key || item.name || index,
      index,
      item,
      color: item.color || palette[index % palette.length],
      path: describeRoseSegment(75, 75, innerRadius, radius, start, end),
      lineStart,
      lineEnd,
      label,
      anchor: Math.cos((mid * Math.PI) / 180) >= 0 ? "start" : "end",
    }
  })
})
const adultCowRatio = computed(() => modelStructureRows.find((item) => item.key === "adultCow")?.ratio || 0)
const calfRatio = computed(() => modelStructureRows.find((item) => item.key === "calf")?.ratio || 0)
const displayTownRows = computed(() => {
  return scaleTownRowsToTotal(mergeTownRows(apiTownRows.value.length ? apiTownRows.value : townRows, townRows), estimatedStock)
})
const monthlyRows = computed(() => yakForecastRows)
const forecastTownRows = computed(() => getTownForecastRows(activePeriod.value))
const forecastStructureRows = computed(() => getForecastStructureRows(activePeriod.value))
const epidemicMetrics = computed(() => epidemicAnalysis.value?.metrics || {})
const epidemicTrendRows = computed(() => normalizeAnalysisRows(epidemicAnalysis.value?.dateTrend).sort(sortMonthDayRows))
const epidemicMedicineRows = computed(() => normalizeAnalysisRows(epidemicAnalysis.value?.medicineCounts))
const epidemicMedicinePieRows = computed(() =>
  epidemicMedicineRows.value
    .filter((item) => item.value > 0)
    .map((item, index) => ({
      ...item,
      key: item.key || `${item.name}-${index}`,
      unit: "条",
    }))
)
const epidemicMedicinePieKey = computed(() => epidemicMedicinePieRows.value.map((item) => `${item.name}:${item.value}`).join("|") || "epidemic-medicine-empty")
const epidemicOwnerRows = computed(() => normalizeAnalysisRows(epidemicAnalysis.value?.ownerRanks).slice(0, 8))
const epidemicVaccineTypeCount = computed(() => getEpidemicVaccineTypeCount(epidemicAnalysis.value))
const epidemicTopVaccineName = computed(() => epidemicMedicineRows.value.find((item) => item.name && item.name !== "未填写")?.name || "-")
const epidemicRecordTotal = computed(() => getMetricNumber(epidemicMetrics.value, ["total", "vaccine"]))
const epidemicOwnerTotal = computed(() => getMetricNumber(epidemicMetrics.value, ["owners"]))
const epidemicVillageTotal = computed(() => getMetricNumber(epidemicMetrics.value, ["villages"]))
const epidemicTownCount = computed(() => epidemicTownRows.value.length)
const epidemicTopTownName = computed(() => epidemicTownRows.value[0]?.name || "-")
const epidemicLatestDate = computed(() => {
  const dates = epidemicRecords.value.map((item) => item.operateTime).filter(Boolean).sort()
  return dates.length ? displayDate(dates[dates.length - 1]) : "-"
})
const epidemicEarTagTotal = computed(() => {
  const medicineMax = Math.max(...epidemicMedicineRows.value.map((item) => Number(item.value || 0)), 0)
  if (medicineMax > 0) return medicineMax
  return Math.round(epidemicRecordTotal.value / epidemicVaccineTypeCount.value)
})
const epidemicStatRows = computed(() => {
  const statRows = normalizeEpidemicStatRows(epidemicStats.value?.rows)
  if (statRows.length) return statRows
  return epidemicTownRows.value.map((item) => ({
    name: item.name,
    recordCount: Number(item.recordCount || 0),
    earTagCount: Number(item.earTagCount || 0),
    ratio: item.ratio,
    source: "ledger",
  }))
})
const epidemicVillageMapRows = computed(() => {
  const usedKeys = new Set()
  return normalizeEpidemicVillageRows(epidemicVillageStats.value?.rows)
    .filter((row) => Number.isFinite(row.lng) && Number.isFinite(row.lat))
    .map((row, index) => {
      const pointKey = `${row.lng.toFixed(4)},${row.lat.toFixed(4)}`
      const duplicateOffset = usedKeys.has(pointKey) ? ((index % 3) - 1) * 0.012 : 0
      usedKeys.add(pointKey)
      return {
        ...row,
        name: row.village,
        value: row.earTagCount,
        unit: "万头",
        lng: row.lng + duplicateOffset,
        lat: row.lat - duplicateOffset * 0.65,
        displayValue: Number((row.earTagCount / 10000).toFixed(2)),
        displayUnit: "万头",
        rankLabel: `第${index + 1}名`,
      }
    })
})
const epidemicTownRows = computed(() => {
  const townRecordMap = new Map()
  normalizeAnalysisRows(epidemicAnalysis.value?.townCounts).forEach((row) => {
    const name = normalizeEpidemicTownName(row.name)
    if (!name) return
    townRecordMap.set(name, Number(townRecordMap.get(name) || 0) + Number(row.value || 0))
  })
  return [...townRecordMap.entries()]
    .map(([name, recordCount]) => {
      const earTagCount = Math.round(recordCount / epidemicVaccineTypeCount.value)
      const ratio = epidemicRecordTotal.value ? (recordCount / epidemicRecordTotal.value) * 100 : 0
      return { name, recordCount, earTagCount, ratio }
    })
    .sort((a, b) => Number(b.recordCount || 0) - Number(a.recordCount || 0))
})
const epidemicTotalTownCount = computed(() => Math.max(displayTownRows.value.length, epidemicTownCount.value))
const epidemicCoveredTownNames = computed(() => new Set(epidemicTownRows.value.map((item) => normalizeEpidemicTownName(item.name)).filter(Boolean)))
const epidemicMissingTownNames = computed(() => {
  const covered = epidemicCoveredTownNames.value
  return displayTownRows.value
    .map((item) => normalizeEpidemicTownName(item.name))
    .filter((name) => name && !covered.has(name))
})
const epidemicTownCoverageRate = computed(() => {
  const total = epidemicTotalTownCount.value || epidemicTownCount.value || 1
  return (epidemicTownCount.value / total) * 100
})
const epidemicEarTagCoverageRate = computed(() => estimatedStock ? (epidemicEarTagTotal.value / estimatedStock) * 100 : 0)
const epidemicWeakTarget = computed(() => buildEpidemicWeakTarget())
const epidemicWeakTownCount = computed(() => {
  if (!epidemicRecordTotal.value) return 0
  if (epidemicMissingTownNames.value.length) return epidemicMissingTownNames.value.length
  return epidemicWeakTarget.value.name && epidemicWeakTarget.value.name !== "暂无" ? 1 : 0
})
const epidemicConcernTownRows = computed(() => buildEpidemicConcernTownRows())
const epidemicTownBalanceRows = computed(() => buildEpidemicTownBalanceRows())
const epidemicVillageCoverageRows = computed(() => buildEpidemicVillageCoverageRows())
const epidemicOverviewStatus = computed(() => buildEpidemicOverviewStatus())
const epidemicOverviewRows = computed(() => buildEpidemicOverviewRows())
const epidemicOverviewLiteRows = computed(() => epidemicOverviewRows.value.slice(0, 3))
const epidemicOverviewFocusDesc = computed(() => {
  if (!epidemicRecordTotal.value) return "年度计划待核查"
  if (epidemicMissingTownNames.value.length) return "年度免疫进度需核查"
  return epidemicWeakTarget.value.desc
})
const epidemicRecordPageCount = computed(() => Math.max(Math.ceil(epidemicTotal.value / epidemicRecordPageSize), 1))
const epidemicLedgerPreviewRows = computed(() => epidemicRecords.value.slice(0, epidemicRecordPageSize))
const epidemicLedgerPageCount = computed(() => Math.max(Math.ceil(epidemicLedgerTotal.value / epidemicLedgerPageSize), 1))
const activeLedgerDetail = computed(() => epidemicLedgerSelectedDetail.value || epidemicLedgerSelectedRecord.value || {})
const activeLedgerImages = computed(() => getImagePaths(activeLedgerDetail.value))
const activeLedgerDetailFields = computed(() => {
  const detail = activeLedgerDetail.value || {}
  return [
    buildDetailField("台账编号", detail.epidemicId, { wide: true }),
    buildDetailField("养殖户", detail.owner, { wide: true }),
    buildDetailField("联系电话", maskPhone(detail.phone)),
    buildDetailField("证件类型", detail.idType),
    buildDetailField("证件号码", maskCertificateNo(detail.idNo)),
    buildDetailField("镇/乡", detail.town),
    buildDetailField("村", detail.village),
    buildDetailField("详细地址", detail.location, { wide: true }),
    buildDetailField("畜种", detail.livestockType),
    buildDetailField("性别", detail.sex),
    buildDetailField("畜龄", detail.age),
    buildDetailField("体重", formatWeight(detail.weight)),
    buildDetailField("疫苗/药品", detail.medicine),
    buildDetailField("防疫类型", detail.preventionType),
    buildDetailField("免疫方式", detail.method),
    buildDetailField("剂量", displayDose(detail.dose)),
    buildDetailField("免疫时间", displayDate(detail.operateTime)),
    buildDetailField("操作员", detail.operator),
  ].filter(Boolean)
})
const epidemicTownFilterOptions = computed(() => normalizeAnalysisRows(epidemicAnalysis.value?.townCounts).map((item) => item.name))
const epidemicMedicineFilterOptions = computed(() => epidemicMedicineRows.value.map((item) => item.name).filter((item) => item && item !== "未填写"))
const tradeMetrics = computed(() => tradeOverview.value?.metrics || {})
const tradeYears = computed(() => normalizeTradeYears(
  tradeOptions.value?.years,
  tradeOverview.value?.availableYears,
  tradeOverview.value?.available_years,
  tradeOverview.value?.yearSummary,
  tradeOverview.value?.year_summary,
  tradeOverview.value?.latestYear,
  tradeOverview.value?.latest_year,
  tradeOverview.value?.year,
))
const tradeMonthRows = computed(() => normalizeTradeCountRows(tradeOverview.value?.monthTrend || tradeOverview.value?.month_trend, "name"))
const tradeYearSummaryRows = computed(() => normalizeTradeYearRows(tradeOverview.value?.yearSummary || tradeOverview.value?.year_summary))
const tradeTownRows = computed(() => normalizeTradeCountRows(tradeOverview.value?.startTownCounts || tradeOverview.value?.start_town_counts, "name"))
const tradeUsageRows = computed(() => normalizeTradeCountRows(tradeOverview.value?.usageCounts || tradeOverview.value?.usage_counts, "name"))
const tradeVehicleRows = computed(() => normalizeTradeCountRows(
  tradeOverview.value?.transportNumberCounts
    || tradeOverview.value?.transport_number_counts
    || tradeOverview.value?.vehicleCounts
    || tradeOverview.value?.vehicle_counts
    || tradeOverview.value?.transportCounts
    || tradeOverview.value?.transport_counts,
  "transportNumber",
))
const tradeTownSourceRows = computed(() => {
  const rows = tradeTownRows.value
    .map((item) => {
      const rawName = String(item.name || "").trim()
      const mapTownName = normalizeEpidemicTownName(rawName)
      const displayName = normalizeBusinessTownName(mapTownName || rawName)
      return {
        ...item,
        rawName,
        name: displayName,
        displayName,
        town: mapTownName,
        mapTownName,
      }
    })
    .filter((item) => item.name && item.name !== "未填写")
    .slice(0, 10)
  const max = Math.max(...rows.map((item) => item.headCount), 1)
  return rows.map((item, index) => ({
    ...item,
    key: item.key || `${item.mapTownName || item.name}-${index}`,
    value: item.headCount,
    unit: "头",
    ratio: Math.max(8, Math.round((item.headCount / max) * 100)),
  }))
})
const tradeLedgerRows = computed(() => tradeRecords.value.slice(0, tradeRecordPageSize))
const tradeRecordPageCount = computed(() => Math.max(Math.ceil(tradeRecordTotal.value / tradeRecordPageSize), 1))
const tradeLedgerModalPageCount = computed(() => Math.max(Math.ceil(tradeLedgerModalTotal.value / tradeLedgerModalPageSize), 1))
const activeTradeLedgerDetail = computed(() => tradeLedgerModalSelectedRecord.value || {})
const activeTradeLedgerRecordKey = computed(() => activeTradeLedgerDetail.value.certId || activeTradeLedgerDetail.value.factoryCode || "")
const tradeTownFilterOptions = computed(() => {
  const optionRows = normalizeOptionValues(tradeOptions.value?.startTowns || tradeOptions.value?.start_towns)
  if (optionRows.length) return optionRows
  return uniqueStrings(tradeTownRows.value.map((item) => item.name))
})
const tradeUsageFilterOptions = computed(() => {
  const optionRows = normalizeOptionValues(tradeOptions.value?.usageTypes || tradeOptions.value?.usage_types)
  if (optionRows.length) return optionRows
  return uniqueStrings(tradeUsageRows.value.map((item) => item.name))
})
const tradeCertFilterOptions = computed(() => {
  const optionRows = normalizeLabeledOptions(tradeOptions.value?.certTypeOptions || tradeOptions.value?.cert_type_options)
  if (optionRows.length) return optionRows
  return normalizeOptionValues(tradeOptions.value?.certTypes || tradeOptions.value?.cert_types).map((item) => ({
    value: item,
    label: item,
  }))
})
const tradeFlowRows = computed(() => {
  return tradeTownSourceRows.value.map((item, index) => ({
    key: item.key || `${item.name}-${index}`,
    name: item.displayName || item.name,
    displayName: item.displayName || item.name,
    rawName: item.rawName,
    town: item.mapTownName || item.town || normalizeEpidemicTownName(item.name),
    mapTownName: item.mapTownName || item.town || normalizeEpidemicTownName(item.name),
    value: item.headCount,
    headCount: item.headCount,
    recordCount: item.recordCount,
    ratio: item.ratio,
    unit: "头",
  }))
})
const tradeRiskRows = computed(() => buildTradeRiskRows())
const tradeRiskSummary = computed(() => {
  const highRows = tradeRiskRows.value.filter((item) => item.level === "high")
  if (highRows.length >= 2) return `${highRows.length}项重点`
  if (highRows.length === 1) return `重点：${highRows[0].name}`
  return "整体平稳"
})
const townFoundationRows = computed(() => {
  return displayTownRows.value.map((item) => ({
    ...item,
    adultCowRatio: adultCowRatio.value,
    calfRatio: calfRatio.value,
    adultCow: Math.round((item.count * adultCowRatio.value) / 100),
    calf: Math.round((item.count * calfRatio.value) / 100),
  }))
})
const foundationPageMeta = computed(() => {
  const rows = townFoundationRows.value
  const total = rows.length
  const pageCount = Math.max(Math.ceil(total / foundationPageSize), 1)
  const page = activeFoundationPage.value % pageCount
  const start = page * foundationPageSize
  const end = Math.min(start + foundationPageSize, total)
  return {
    page,
    pageCount,
    start,
    end,
    total,
    rows: rows.slice(start, end),
  }
})
const activeMapRows = computed(() => {
  if (activeView.value === "forecast") return forecastTownRows.value
  return displayTownRows.value
})
const activeMapValueKey = computed(() => {
  if (activeView.value === "forecast") return "forecast"
  return "count"
})
const centerMapRows = computed(() => buildCenterMapRows(activeMapRows.value, activeMapValueKey.value))
const yakRecognitionLayer = computed(() => ({
  key: "yak-marks-layer",
  layerName: "ne:daping_yak_marks",
  styles: "yak_marks",
  cqlFilter: "gov_show = true",
  name: "牦牛识别标记",
  opacity: 0.96,
  queryBuffer: 10,
  mapView: {
    targetLngLat: [102.58, 32.72],
    distanceScale: 0.82,
    targetLocalZ: 0.76,
    duration: 0.85,
  },
  legends: [
    {
      key: "yak-marks",
      name: "牦牛",
      fillColor: "#FFFF00",
      strokeColor: "#FFFF00",
      selectable: false,
    },
  ],
}))
const epidemicVillageBoundaryLayer = computed(() => {
  const rows = normalizeEpidemicVillageRows(epidemicVillageStats.value?.rows)
  const filters = rows
    .map((row) => buildVillageBoundaryFilter(row.town, row.village))
    .filter(Boolean)
  return {
    key: "yak-epidemic-village-boundary",
    sourceType: "wfs",
    layerName: "ne:t_pastures_map",
    styles: "牧场样式",
    name: "防疫村边界",
    opacity: 0.86,
    queryBuffer: 8,
    cqlFilter: filters.length ? filters.join(" OR ") : "EXCLUDE",
    legends: [
      {
        key: "yak-epidemic-village-boundary",
        name: "防疫村边界",
        fillColor: "#24F6C4",
        strokeColor: "#FFF06A",
        selectable: false,
      },
    ],
  }
})
const activeYakMapLayer = computed(() => {
  if (activeView.value === "epidemic") return epidemicVillageBoundaryLayer.value
  if (activeView.value === "trade") return null
  return yakRecognitionLayer.value
})
const isYakOlMapView = computed(() => activeView.value === "structure" || activeView.value === "forecast")
const yakOlMapCenter = computed(() => (activeView.value === "forecast" ? yakForecastMapCenter : yakStructureMapCenter))
const threeYakMapLayer = computed(() => {
  if (isYakOlMapView.value) return null
  return activeYakMapLayer.value
})
const threeYakMapRanking = computed(() => {
  if (isYakOlMapView.value) return null
  return mapRanking.value
})
const yakMapLoadingVisible = computed(() => {
  if (isYakOlMapView.value) return !olMapReady.value
  return !mapReady.value
})
const yakMapLoadingText = computed(() => (isYakOlMapView.value ? "影像地图加载中" : "3D地图加载中"))
const mapRanking = computed(() => {
  if (activeView.value === "trade") {
    return {
      scope: "trade-flow",
      title: `${activeTradeYear.value || ""}出栏来源`,
      metricLabel: "来源",
      unit: "头",
      source: {
        ...yakTradeMarket,
        total: tradeMetricValue("headCount"),
      },
      rows: tradeFlowRows.value,
      showFlowLines: false,
      mapView: yakTradeMapView,
    }
  }
  return {
    scope: activeView.value === "epidemic" ? "village" : "town",
    hideTownNames: activeView.value === "epidemic",
    title: activeView.value === "forecast"
      ? `${activePeriod.value.shortLabel}预测存栏`
      : activeView.value === "epidemic"
        ? `${activeEpidemicYear.value}年村级防疫信息`
        : "乡镇估算存栏",
    metricLabel: activeView.value === "forecast" ? "预测" : activeView.value === "epidemic" ? "防疫" : "存栏",
    unit: "万头",
    rows: activeView.value === "epidemic"
      ? epidemicVillageMapRows.value
      : centerMapRows.value.map((item) => ({
          name: item.name,
          value: Number((item.value / 10000).toFixed(2)),
          unit: "万头",
        })),
  }
})

const genderAgeOption = computed(() => ({
  color: ["#ffd76c", "#30dcff"],
  tooltip: {
    ...chartTooltip(),
    trigger: "axis",
    axisPointer: { type: "shadow" },
    formatter(params) {
      const lines = params.map((item) => `${item.marker}${item.seriesName}：${formatNumber(Math.abs(item.value))}头`)
      return `${params[0].name}<br/>${lines.join("<br/>")}`
    },
  },
  legend: {
    top: 4,
    right: 12,
    itemWidth: 12,
    itemHeight: 8,
    textStyle: { color: "rgba(222, 250, 255, 0.8)", fontSize: 11 },
  },
  grid: { left: 22, right: 22, top: 32, bottom: 12, containLabel: true },
  xAxis: {
    type: "value",
    axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, formatter: (value) => formatCompact(Math.abs(value)) },
    splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
    axisLine: { show: false },
    axisTick: { show: false },
  },
  yAxis: {
    type: "category",
    data: genderAgeRows.map((item) => item.age),
    axisLabel: { color: "rgba(223, 250, 255, 0.82)", fontSize: 11 },
    axisLine: { show: false },
    axisTick: { show: false },
  },
  series: [
    {
      name: "母牛",
      type: "bar",
      stack: "gender",
      barWidth: 12,
      data: genderAgeRows.map((item) => -item.cow),
      itemStyle: { borderRadius: [7, 0, 0, 7] },
    },
    {
      name: "公牛",
      type: "bar",
      stack: "gender",
      barWidth: 12,
      data: genderAgeRows.map((item) => item.bull),
      itemStyle: { borderRadius: [0, 7, 7, 0] },
    },
  ],
}))

const townStackOption = computed(() => {
  const rows = displayTownRows.value
  const totals = rows.map((item) => item.count)
  return {
    color: palette,
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const lines = params.map((item) => `${item.marker}${item.seriesName}：${formatNumber(item.value)}头`)
        return `${params[0].name}<br/>${lines.join("<br/>")}<br/>估算存栏：${formatNumber(totals[params[0].dataIndex])}头<br/>结构依据：专家共识比例`
      },
    },
    legend: {
      top: -2,
      right: 0,
      itemWidth: 8,
      itemHeight: 6,
      itemGap: 6,
      textStyle: { color: "rgba(222, 250, 255, 0.75)", fontSize: 10 },
    },
    grid: { left: 54, right: 30, top: 22, bottom: 8 },
    xAxis: {
      type: "value",
      axisLabel: { color: "rgba(223, 250, 255, 0.56)", fontSize: 10, formatter: formatCompact },
      splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    yAxis: {
      type: "category",
      inverse: true,
      data: rows.map((item) => item.name),
      axisLabel: { color: "rgba(223, 250, 255, 0.82)", fontSize: 10, interval: 0 },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    series: modelStructureRows.map((structure) => ({
      name: structure.name.replace("（≤1岁）", "").replace("（1-2岁）", ""),
      type: "bar",
      stack: "town",
      barWidth: rows.length > 8 ? 6 : 11,
      data: rows.map((town) => Math.round((town.count * structure.ratio) / 100)),
    })),
  }
})

const townRankOption = computed(() =>
  buildHorizontalBarOption(displayTownRows.value, "count", "头", {
    style: "ladder",
    tooltipFormatter(params) {
      const row = displayTownRows.value[params.dataIndex]
      return `${row.name}<br/>估算存栏：${formatNumber(row.count)}头<br/>来源：无人机识别`
    },
  }),
)

const foundationStructureOption = computed(() => {
  const meta = foundationPageMeta.value
  const rows = meta.rows
  const maxTotal = Math.max(...rows.map((item) => item.adultCow + item.calf), 1)
  return {
    color: ["#30dcff", "#ffd76c"],
    animationDurationUpdate: 700,
    animationEasingUpdate: "cubicOut",
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const row = rows[params[0].dataIndex]
        const lines = params
          .filter((item) => ["成年母牛", "幼畜"].includes(item.seriesName))
          .map((item) => `${item.marker}${item.seriesName}：${formatNumber(item.value)}头`)
        return `${row.name}<br/>母幼合计：${formatNumber(row.adultCow + row.calf)}头<br/>${lines.join("<br/>")}<br/>依据：无人机识别 + 专家共识模型`
      },
    },
    legend: {
      data: ["成年母牛", "幼畜"],
      top: 2,
      left: 2,
      itemWidth: 10,
      itemHeight: 7,
      itemGap: 9,
      textStyle: { color: "rgba(222, 250, 255, 0.78)", fontSize: 10 },
    },
    graphic: meta.total > foundationPageSize
      ? {
          type: "text",
          right: 4,
          top: 3,
          silent: true,
          style: {
            text: `${meta.start + 1}-${meta.end} / ${meta.total}`,
            fill: "rgba(222, 250, 255, 0.62)",
            fontSize: 10,
            fontWeight: 600,
          },
        }
      : undefined,
    grid: { left: 18, right: 18, top: 42, bottom: 44, containLabel: false },
    xAxis: {
      type: "category",
      data: rows.map((item) => item.name),
      axisLabel: {
        color: "rgba(223, 250, 255, 0.78)",
        fontSize: 10,
        lineHeight: 12,
        interval: 0,
        margin: 8,
        formatter: formatPlaceAxisLabel,
      },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    yAxis: {
      type: "value",
      max: Math.ceil(maxTotal * 1.28),
      axisLabel: { show: false },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.08)", type: "dashed" } },
    },
    series: [
      {
        name: "基础群规模",
        type: "bar",
        barWidth: 15,
        barGap: "-100%",
        data: rows.map((item) => item.adultCow + item.calf),
        tooltip: { show: false },
        itemStyle: {
          borderRadius: [8, 8, 1, 1],
          color: "rgba(48, 220, 255, 0.07)",
        },
        silent: true,
      },
      {
        name: "成年母牛",
        type: "bar",
        stack: "foundation",
        barWidth: 15,
        data: rows.map((item) => item.adultCow),
        itemStyle: {
          borderRadius: [1, 1, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(48, 220, 255, 0.34)" },
            { offset: 1, color: "#30dcff" },
          ]),
        },
      },
      {
        name: "幼畜",
        type: "bar",
        stack: "foundation",
        barWidth: 15,
        data: rows.map((item) => item.calf),
        itemStyle: {
          borderRadius: [8, 8, 1, 1],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#ffd76c" },
            { offset: 1, color: "rgba(255, 215, 108, 0.2)" },
          ]),
        },
        label: {
          show: true,
          position: "top",
          formatter: ({ dataIndex }) => formatCompact(rows[dataIndex].adultCow + rows[dataIndex].calf),
          color: "#ffffff",
          fontSize: 10,
          fontWeight: 700,
        },
      },
    ],
  }
})

const tradeImpactOption = computed(() => {
  const rows = tradeImpactRows.slice().sort((a, b) => b.value - a.value)
  const total = tradeImpactRows.reduce((sum, item) => sum + item.value, 0)
  const yoyValues = rows.map((item) => item.yoy || 0)
  return {
    color: rows.map((item) => item.color),
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const row = rows[params[0].dataIndex]
        return `${row.label}<br/>数量：${formatNumber(row.value)}头<br/>占比：${formatPercent((row.value / total) * 100)}<br/>同比：${row.yoy > 0 ? "+" : ""}${row.yoy}%`
      },
    },
    grid: { left: 36, right: 30, top: 20, bottom: 34 },
    xAxis: {
      type: "category",
      data: rows.map((item) => item.label),
      axisLabel: { color: "rgba(223, 250, 255, 0.76)", fontSize: 10, interval: 0, margin: 10 },
      axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.24)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        axisLabel: { color: "rgba(223, 250, 255, 0.52)", fontSize: 10, formatter: formatCompact },
        splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.09)", type: "dashed" } },
      },
      {
        type: "value",
        min: Math.min(-2, Math.floor(Math.min(...yoyValues) - 1)),
        max: Math.max(8, Math.ceil(Math.max(...yoyValues) + 1)),
        axisLabel: { color: "rgba(255, 215, 108, 0.68)", fontSize: 10, formatter: "{value}%" },
        splitLine: { show: false },
        axisLine: { show: false },
        axisTick: { show: false },
      },
    ],
    series: [
      {
        name: "数量",
        type: "bar",
        barWidth: 16,
        data: rows.map((item, index) => ({
          value: item.value,
          itemStyle: {
            borderRadius: [8, 8, 2, 2],
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: item.color },
              { offset: 0.72, color: `${item.color}cc` },
              { offset: 1, color: `${item.color}18` },
            ]),
            shadowBlur: index === 0 ? 12 : 7,
            shadowColor: item.color,
          },
          label: {
            show: true,
            position: "top",
            color: index === 0 ? "#ffd76c" : "#ffffff",
            fontSize: 10,
            formatter: ({ value }) => formatCompact(value),
          },
        })),
      },
      {
        name: "同比",
        type: "line",
        yAxisIndex: 1,
        smooth: true,
        symbol: "circle",
        symbolSize: 6,
        data: yoyValues,
        lineStyle: { width: 2, color: "#ffd76c" },
        itemStyle: { color: "#ffd76c", borderColor: "rgba(3, 18, 28, 0.9)", borderWidth: 2 },
        label: {
          show: true,
          position: "top",
          color: "#ffd76c",
          fontSize: 10,
          formatter: ({ value }) => `${value > 0 ? "+" : ""}${value}%`,
        },
      },
    ],
  }
})

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

const monthlyForecastOption = computed(() => ({
  color: ["#30dcff", "#ffd76c"],
  tooltip: { ...chartTooltip(), trigger: "axis" },
  legend: {
    top: 4,
    right: 12,
    itemWidth: 12,
    itemHeight: 8,
    textStyle: { color: "rgba(222, 250, 255, 0.78)", fontSize: 10 },
  },
  grid: { left: 40, right: 42, top: 34, bottom: 28 },
  xAxis: {
    type: "category",
    data: monthlyRows.value.map((item) => item.month || item.name),
    axisLabel: { color: "rgba(223, 250, 255, 0.68)", fontSize: 10, interval: 1 },
    axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.26)" } },
    axisTick: { show: false },
  },
  yAxis: [
    {
      type: "value",
      axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10 },
      splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
    },
    {
      type: "value",
      axisLabel: { color: "rgba(223, 250, 255, 0.48)", fontSize: 10 },
      splitLine: { show: false },
    },
  ],
  series: [
    {
      name: "数量",
      type: "bar",
      barWidth: 12,
      data: monthlyRows.value.map((item) => item.count || item.value),
      itemStyle: {
        borderRadius: [8, 8, 0, 0],
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: "#30dcff" },
          { offset: 1, color: "rgba(48, 220, 255, 0.08)" },
        ]),
      },
    },
    {
      name: "增速",
      type: "line",
      yAxisIndex: 1,
      smooth: true,
      symbolSize: 5,
      data: monthlyRows.value.map((item) => item.speed || item.growthRate || 0),
      lineStyle: { width: 2.4, color: "#ffd76c" },
      itemStyle: { color: "#ffd76c" },
    },
  ],
}))

const forecastFactorOption = computed(() => ({
  tooltip: chartTooltip(),
  radar: {
    center: ["50%", "54%"],
    radius: "68%",
    splitNumber: 4,
    indicator: forecastFactorRows.map((item) => ({ name: item.name, max: 100 })),
    axisName: { color: "rgba(223, 250, 255, 0.76)", fontSize: 11 },
    splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.22)" } },
    splitArea: {
      areaStyle: {
        color: ["rgba(48, 220, 255, 0.04)", "rgba(39, 240, 192, 0.02)"],
      },
    },
    axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.18)" } },
  },
  series: [
    {
      type: "radar",
      data: [
        {
          name: "预测因子",
          value: forecastFactorRows.map((item) => item.value),
          areaStyle: { color: "rgba(48, 220, 255, 0.22)" },
          lineStyle: { color: "#30dcff", width: 2 },
          itemStyle: { color: "#ffd76c" },
        },
      ],
    },
  ],
}))

const forecastTownOption = computed(() => buildHorizontalBarOption(forecastTownRows.value, "forecast", "头"))

const forecastStructureOption = computed(() => {
  const rows = forecastStructureRows.value
  return {
    color: palette,
    tooltip: chartTooltip((params) => {
      const row = rows.find((item) => item.name === params.name)
      return `${params.name}<br/>预测占比：${formatPercent(row?.ratio ?? params.percent)}<br/>预测数量：${formatNumber(row?.count || 0)}头`
    }),
    legend: {
      orient: "vertical",
      right: 4,
      top: "middle",
      itemWidth: 8,
      itemHeight: 8,
      itemGap: 6,
      formatter(name) {
        const row = rows.find((item) => item.name === name)
        return row ? `${name}  ${formatPercent(row.ratio)}` : name
      },
      textStyle: { color: "rgba(222, 250, 255, 0.8)", fontSize: 10 },
    },
    series: [
      {
        name: "期末结构预测",
        type: "pie",
        radius: ["38%", "66%"],
        center: ["32%", "54%"],
        startAngle: 118,
        avoidLabelOverlap: false,
        label: { show: false },
        labelLine: { show: false },
        itemStyle: { borderColor: "rgba(3, 18, 31, 0.92)", borderWidth: 3 },
        data: rows.map((item) => ({ name: item.name, value: item.count })),
      },
    ],
  }
})

const flowComparisonOption = computed(() => ({
  color: ["#ffd76c", "#ff8f6b", "#7aa7ff"],
  tooltip: { ...chartTooltip(), trigger: "axis", axisPointer: { type: "shadow" } },
  legend: {
    top: 4,
    right: 8,
    itemWidth: 12,
    itemHeight: 8,
    textStyle: { color: "rgba(222, 250, 255, 0.78)", fontSize: 10 },
  },
  grid: { left: 44, right: 26, top: 34, bottom: 28 },
  xAxis: {
    type: "category",
    data: forecastPeriods.map((item) => item.shortLabel),
    axisLabel: { color: "rgba(223, 250, 255, 0.76)", fontSize: 11 },
    axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.26)" } },
    axisTick: { show: false },
  },
  yAxis: {
    type: "value",
    axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, formatter: (value) => formatWan(value, 1) },
    splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" } },
  },
  series: [
    {
      name: "繁殖补充",
      type: "bar",
      barWidth: 12,
      data: forecastPeriods.map((item) => item.births),
      itemStyle: { borderRadius: [7, 7, 0, 0] },
    },
    {
      name: "出栏流出",
      type: "bar",
      barWidth: 12,
      data: forecastPeriods.map((item) => -item.outflow),
      itemStyle: { borderRadius: [0, 0, 7, 7] },
    },
    {
      name: "稳态校正",
      type: "line",
      smooth: true,
      symbolSize: 6,
      data: forecastPeriods.map((item) => item.correction),
      lineStyle: { width: 2.4 },
    },
  ],
}))

const forecastHeatRows = computed(() => {
  const rows = forecastTownRows.value
  const max = Math.max(...rows.map((item) => Math.abs(item.change)), 1)
  return rows.map((item) => ({
    ...item,
    width: Math.max(8, Math.round((Math.abs(item.change) / max) * 100)),
  }))
})

function forecastHeatStyle(item) {
  const positive = Number(item?.change || 0) >= 0
  return {
    "--heat-width": `${item.width}%`,
    "--heat-color": positive ? "#24f6c4" : "#ff8f6b",
    "--heat-color-soft": positive ? "rgba(36, 246, 196, 0.28)" : "rgba(255, 143, 107, 0.26)",
    "--heat-color-dim": positive ? "rgba(36, 246, 196, 0.08)" : "rgba(255, 143, 107, 0.08)",
  }
}

function forecastHeatTitle(item) {
  return `${item.name}：${formatSigned(item.change)}头`
}

const epidemicTrendOption = computed(() => {
  const rows = epidemicTrendRows.value
  const values = rows.map((item) => item.value)
  const maxValue = Math.max(...values, 1)
  return {
    color: ["#27f0c0", "#ffd76c"],
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "line", lineStyle: { color: "rgba(39, 240, 192, 0.34)" } },
      formatter(params) {
        const item = params[0]
        return `${item.name}<br/>防疫记录：${formatNumber(item.value)}条`
      },
    },
    grid: { left: 42, right: 24, top: 20, bottom: 28 },
    xAxis: {
      type: "category",
      data: rows.map((item) => item.name),
      axisLabel: { color: "rgba(223, 250, 255, 0.62)", fontSize: 10, interval: Math.max(Math.floor(rows.length / 7), 0) },
      axisLine: { lineStyle: { color: "rgba(39, 240, 192, 0.22)" } },
      axisTick: { show: false },
    },
    yAxis: {
      type: "value",
      axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, formatter: formatCompact },
      splitLine: { lineStyle: { color: "rgba(39, 240, 192, 0.1)", type: "dashed" } },
    },
    series: [
      {
        name: "防疫记录",
        type: "bar",
        barMaxWidth: 12,
        data: values,
        itemStyle: {
          borderRadius: [7, 7, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#27f0c0" },
            { offset: 1, color: "rgba(39, 240, 192, 0.08)" },
          ]),
        },
        label: {
          show: true,
          position: "top",
          color: "rgba(230, 255, 249, 0.92)",
          fontSize: 10,
          formatter: ({ value }) => (value >= maxValue * 0.42 ? formatCompact(value) : ""),
        },
      },
      {
        name: "作业趋势",
        type: "line",
        smooth: true,
        showSymbol: false,
        data: values,
        lineStyle: { width: 2.4, color: "#ffd76c" },
      },
    ],
  }
})

const epidemicOverviewOption = computed(() => {
  const trendRows = epidemicTrendRows.value.length
    ? epidemicTrendRows.value.slice(-6)
    : [{ name: "待开展", value: 0 }]
  const trendValues = trendRows.map((item) => Number(item.value || 0))
  const concernRows = epidemicConcernTownRows.value
  const concernValues = concernRows.map((item) => Number(item.value || 0))
  const maxTrend = Math.max(...trendValues, 1)
  const trimTownName = (name) => String(name || "").replace(/(镇|乡|村)$/, "")

  return {
    color: ["#27f0c0", "#ffd76c"],
    title: [
      {
        text: "作业节奏",
        left: 4,
        top: 0,
        textStyle: { color: "rgba(226, 252, 255, 0.82)", fontSize: 11, fontWeight: 800 },
      },
      {
        text: "补免关注",
        left: "55%",
        top: 0,
        textStyle: { color: "rgba(226, 252, 255, 0.82)", fontSize: 11, fontWeight: 800 },
      },
    ],
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      formatter(params) {
        const first = Array.isArray(params) ? params[0] : params
        if (first?.axisIndex === 1) {
          const row = concernRows[first.dataIndex] || {}
          return `${row.name}<br/>补免关注：${row.label || first.value}`
        }
        const row = trendRows[first.dataIndex] || {}
        return `${row.name}<br/>防疫记录：${formatNumber(row.value)}条`
      },
    },
    grid: [
      { left: 6, right: "52%", top: 26, bottom: 15 },
      { left: "56%", right: 12, top: 26, bottom: 15 },
    ],
    xAxis: [
      {
        type: "category",
        gridIndex: 0,
        data: trendRows.map((item) => item.name),
        axisLabel: {
          color: "rgba(226, 252, 255, 0.56)",
          fontSize: 9,
          interval: 0,
          formatter: (value) => String(value || "").replace(/^(\d{1,2})\D+(\d{1,2}).*$/, "$1/$2"),
        },
        axisLine: { lineStyle: { color: "rgba(39, 240, 192, 0.22)" } },
        axisTick: { show: false },
      },
      {
        type: "value",
        gridIndex: 1,
        min: 0,
        max: Math.max(...concernValues, 100),
        show: false,
      },
    ],
    yAxis: [
      {
        type: "value",
        gridIndex: 0,
        show: false,
      },
      {
        type: "category",
        gridIndex: 1,
        inverse: true,
        data: concernRows.map((item) => item.name),
        axisLabel: {
          color: "rgba(226, 252, 255, 0.68)",
          fontSize: 9,
          formatter: (value) => trimTownName(value),
        },
        axisLine: { show: false },
        axisTick: { show: false },
      },
    ],
    series: [
      {
        name: "防疫记录",
        type: "bar",
        xAxisIndex: 0,
        yAxisIndex: 0,
        barMaxWidth: 9,
        data: trendValues,
        itemStyle: {
          borderRadius: [6, 6, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#27f0c0" },
            { offset: 1, color: "rgba(39, 240, 192, 0.1)" },
          ]),
        },
        label: {
          show: true,
          position: "top",
          color: "rgba(230, 255, 249, 0.86)",
          fontSize: 8,
          formatter: ({ value }) => (value >= maxTrend * 0.55 ? formatCompact(value) : ""),
        },
      },
      {
        name: "节奏线",
        type: "line",
        xAxisIndex: 0,
        yAxisIndex: 0,
        smooth: true,
        showSymbol: false,
        data: trendValues,
        lineStyle: { width: 1.8, color: "#ffd76c" },
        tooltip: { show: false },
      },
      {
        name: "补免关注",
        type: "bar",
        xAxisIndex: 1,
        yAxisIndex: 1,
        barWidth: 8,
        data: concernValues,
        itemStyle: {
          borderRadius: [0, 7, 7, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
            { offset: 0, color: "rgba(255, 215, 108, 0.24)" },
            { offset: 1, color: "#ffd76c" },
          ]),
        },
        label: {
          show: true,
          position: "right",
          color: "rgba(255, 235, 176, 0.9)",
          fontSize: 8,
          formatter: ({ dataIndex }) => concernRows[dataIndex]?.label || "",
        },
      },
    ],
  }
})

const epidemicTownBalanceOption = computed(() => {
  const rows = epidemicTownBalanceRows.value
  const values = rows.map((item) => Number(item.value || 0))
  const maxValue = Math.max(...values, 1)
  const avgValue = values.filter(Boolean).reduce((sum, value) => sum + value, 0) / Math.max(values.filter(Boolean).length, 1)
  const displayMax = Math.ceil(maxValue * 1.18)

  return {
    color: ["#27f0c0", "#ffd76c"],
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const item = rows[params[0]?.dataIndex] || {}
        if (item.isMissing) return `${item.name}<br/>年度防疫：待补强`
        return `${item.name}<br/>覆盖量：${formatNumber(item.value)}头<br/>防疫记录：${formatNumber(item.recordCount)}条`
      },
    },
    grid: { left: 56, right: 46, top: 16, bottom: 12 },
    xAxis: {
      type: "value",
      min: 0,
      max: displayMax,
      show: false,
    },
    yAxis: {
      type: "category",
      inverse: true,
      data: rows.map((item) => item.name),
      axisLabel: {
        color: "rgba(226, 252, 255, 0.82)",
        fontSize: 11,
        interval: 0,
        formatter: (value) => String(value || "").replace(/(镇|乡)$/, ""),
      },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    series: [
      {
        name: "均衡参照",
        type: "bar",
        barWidth: 11,
        silent: true,
        barGap: "-100%",
        data: rows.map(() => displayMax),
        itemStyle: {
          borderRadius: [0, 8, 8, 0],
          borderColor: "rgba(48, 220, 255, 0.12)",
          borderWidth: 1,
          color: "rgba(48, 220, 255, 0.045)",
        },
      },
      {
        name: "覆盖量",
        type: "bar",
        barWidth: 11,
        barGap: "-100%",
        z: 3,
        data: rows.map((item) => {
          const rawValue = Number(item.value || 0)
          return {
            value: rawValue > 0 ? rawValue : Math.max(displayMax * 0.035, 1),
            rawValue,
            itemStyle: {
              borderRadius: [0, 8, 8, 0],
              color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
                { offset: 0, color: item.isMissing ? "rgba(255, 143, 107, 0.24)" : "rgba(39, 240, 192, 0.16)" },
                { offset: 0.72, color: item.isMissing ? "#ff8f6b" : "#27f0c0" },
                { offset: 1, color: item.isMissing ? "#ffd76c" : "#8fffe9" },
              ]),
              shadowBlur: item.isMissing ? 8 : 10,
              shadowColor: item.isMissing ? "rgba(255, 143, 107, 0.36)" : "rgba(39, 240, 192, 0.34)",
            },
          }
        }),
        label: {
          show: true,
          position: "right",
          color: "rgba(239, 255, 255, 0.92)",
          fontSize: 11,
          fontWeight: 800,
          formatter: ({ dataIndex }) => {
            const row = rows[dataIndex] || {}
            return row.isMissing ? "待补强" : `${formatCompact(row.value)}头`
          },
        },
        markLine: avgValue > 0
          ? {
              symbol: "none",
              silent: true,
              lineStyle: { color: "rgba(255, 215, 108, 0.46)", type: "dashed", width: 1 },
              label: {
                color: "rgba(255, 232, 164, 0.8)",
                fontSize: 10,
                formatter: "均衡线",
              },
              data: [{ xAxis: avgValue }],
            }
          : undefined,
      },
    ],
  }
})

const epidemicVillageCoverageOption = computed(() => {
  const rows = epidemicVillageCoverageRows.value
  const colors = ["#27f0c0", "#30dcff", "#ffd76c", "#7aa7ff", "#86dc62", "#ff8f6b", "#67ffe4", "#40b8ff", "#c7e86b"]
  const data = rows.length
    ? rows.map((item, index) => ({
        name: String(item.name || "").replace(/(村|社区|牧委会)$/, ""),
        value: Math.max(Number(item.value || 0), 1),
        rawValue: Number(item.value || 0),
        town: item.town || "",
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
            { offset: 0, color: colors[index % colors.length] },
            { offset: 1, color: "rgba(17, 70, 92, 0.38)" },
          ]),
        },
      }))
    : [{ name: "待形成", value: 1, rawValue: 0, town: "", itemStyle: { color: "rgba(48, 220, 255, 0.14)" } }]

  return {
    tooltip: {
      ...chartTooltip(),
      formatter(params) {
        const dataItem = params.data || {}
        if (!dataItem.rawValue) return `${dataItem.name}<br/>暂无村级覆盖量`
        return `${dataItem.town ? `${dataItem.town} ` : ""}${dataItem.name}<br/>覆盖量：${formatNumber(dataItem.rawValue)}头`
      },
    },
    series: [
      {
        type: "treemap",
        roam: false,
        nodeClick: false,
        breadcrumb: { show: false },
        left: 6,
        right: 6,
        top: 7,
        bottom: 5,
        visibleMin: 1,
        data,
        label: {
          show: true,
          color: "rgba(235, 254, 255, 0.9)",
          fontSize: 10,
          fontWeight: 800,
          lineHeight: 14,
          formatter(params) {
            const rawValue = Number(params.data?.rawValue || 0)
            return rawValue ? `${params.name}\n${formatCompact(rawValue)}头` : params.name
          },
        },
        upperLabel: { show: false },
        itemStyle: {
          borderColor: "rgba(4, 24, 36, 0.88)",
          borderWidth: 2,
          gapWidth: 2,
        },
        emphasis: {
          label: { color: "#ffffff" },
          itemStyle: { shadowBlur: 14, shadowColor: "rgba(39, 240, 192, 0.4)" },
        },
        levels: [
          {
            itemStyle: {
              borderColor: "rgba(14, 75, 98, 0.72)",
              borderWidth: 1,
              gapWidth: 2,
            },
          },
        ],
      },
    ],
  }
})

const epidemicPreventionStatOption = computed(() => {
  const rows = epidemicStatRows.value
  const barWidth = rows.length > 8 ? 10 : 12
  const frameWidth = barWidth + 8
  const recordValues = rows.map((item) => Number(item.recordCount || 0))
  const earTagValues = rows.map((item) => Number(item.earTagCount || 0))
  const maxRecord = Math.max(...recordValues, 1)
  const frameValues = rows.map(() => Math.ceil(maxRecord * 1.08))
  const frameGradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
    { offset: 0, color: "rgba(48, 220, 255, 0.18)" },
    { offset: 0.48, color: "rgba(48, 220, 255, 0.06)" },
    { offset: 1, color: "rgba(48, 220, 255, 0.02)" },
  ])
  const recordGradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
    { offset: 0, color: "#31f7ff" },
    { offset: 0.36, color: "#1cbcff" },
    { offset: 1, color: "#266dff" },
  ])

  return {
    color: ["#31f7ff", "#ffe06b"],
    title: {
      text: `台账总量：{value|${formatCompact(epidemicRecordTotal.value)}条}`,
      right: 0,
      top: 0,
      textStyle: {
        color: "#fff",
        fontSize: 12,
        fontWeight: 500,
        rich: {
          value: {
            color: "#22f1ff",
            borderColor: "#14bdf4",
            borderWidth: 1,
            borderRadius: 2,
            padding: [2, 6, 3, 6],
            fontSize: 12,
            fontWeight: 700,
          },
        },
      },
    },
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const row = rows[params[0].dataIndex]
        return [
          `${row.name}`,
          `防疫记录：${formatNumber(row.recordCount)}条`,
          `耳标数量：${formatNumber(row.earTagCount)}头`,
          `数据来源：防疫台账`,
        ].join("<br/>")
      },
    },
    legend: [
      {
        data: ["防疫记录"],
        top: 0,
        left: 0,
        itemWidth: 10,
        itemHeight: 10,
        icon: "circle",
        textStyle: { color: "#fff", fontSize: 11 },
        selectedMode: false,
      },
      {
        data: ["耳标数"],
        top: 0,
        left: 82,
        itemWidth: 10,
        itemHeight: 10,
        icon: "circle",
        textStyle: { color: "#fff", fontSize: 11 },
        selectedMode: false,
      },
    ],
    grid: { left: 42, right: 12, top: 46, bottom: 68 },
    xAxis: {
      type: "category",
      data: rows.map((item) => item.name),
      axisLabel: {
        color: "#fff",
        fontSize: 10,
        interval: 0,
        lineHeight: 11,
        margin: 8,
        formatter: verticalTownAxisLabel,
      },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
    yAxis: {
      type: "value",
      name: "数量/条",
      max: Math.ceil(maxRecord * 1.2),
      nameTextStyle: { color: "rgba(223, 250, 255, 0.62)", fontSize: 10, align: "left" },
      axisLabel: { color: "rgba(223, 250, 255, 0.58)", fontSize: 10, formatter: formatCompact },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { lineStyle: { color: "rgba(51, 89, 113, 0.16)", type: "dashed" } },
    },
    series: [
      {
        name: "统计底柱",
        type: "bar",
        barWidth: frameWidth,
        barGap: "-100%",
        data: frameValues,
        tooltip: { show: false },
        silent: true,
        z: 1,
        itemStyle: {
          borderRadius: [8, 8, 1, 1],
          borderColor: "rgba(48, 220, 255, 0.22)",
          borderWidth: 1,
          color: frameGradient,
        },
      },
      {
        name: "防疫记录",
        type: "bar",
        barGap: "-100%",
        barWidth,
        barMinHeight: 2,
        z: 3,
        data: recordValues.map((value) => ({
          value,
          itemStyle: {
            borderRadius: [8, 8, 1, 1],
            borderColor: "rgba(200, 252, 255, 0.22)",
            borderWidth: 1,
            color: recordGradient,
            shadowBlur: 10,
            shadowColor: "rgba(49, 247, 255, 0.28)",
          },
        })),
      },
      {
        name: "顶部高光",
        type: "pictorialBar",
        symbol: "diamond",
        symbolSize: [barWidth + 7, 5],
        symbolOffset: [0, -3],
        symbolPosition: "end",
        z: 8,
        tooltip: { show: false },
        data: recordValues.map((value) => ({
          value,
          itemStyle: {
            color: "rgba(207, 252, 255, 0.88)",
            shadowBlur: 8,
            shadowColor: "rgba(49, 247, 255, 0.72)",
          },
        })),
      },
      {
        name: "底座光影",
        type: "pictorialBar",
        symbol: "diamond",
        symbolSize: [frameWidth + 3, 5],
        symbolOffset: [0, 3],
        z: 2,
        tooltip: { show: false },
        data: recordValues,
        itemStyle: {
          color: "rgba(48, 220, 255, 0.24)",
          shadowBlur: 10,
          shadowColor: "rgba(48, 220, 255, 0.45)",
        },
      },
      {
        name: "耳标数",
        type: "line",
        smooth: true,
        showSymbol: false,
        z: 9,
        data: earTagValues,
        lineStyle: { width: 2.2, color: "#ffe06b", shadowBlur: 8, shadowColor: "rgba(255, 224, 107, 0.5)" },
        itemStyle: { color: "#ffe06b" },
      },
    ],
  }
})

const epidemicOwnerOption = computed(() => {
  const rows = epidemicOwnerRows.value
  return {
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const row = rows[params[0].dataIndex]
        return `${row.name}<br/>防疫记录：${formatNumber(row.value)}条`
      },
    },
    grid: { left: 74, right: 42, top: 12, bottom: 8 },
    xAxis: {
      type: "value",
      axisLabel: { color: "rgba(223, 250, 255, 0.52)", fontSize: 10, formatter: formatCompact },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { lineStyle: { color: "rgba(39, 240, 192, 0.08)", type: "dashed" } },
    },
    yAxis: {
      type: "category",
      inverse: true,
      data: rows.map((item) => item.name),
      axisLabel: { color: "rgba(223, 250, 255, 0.82)", fontSize: 10, interval: 0 },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    series: [
      {
        name: "记录",
        type: "bar",
        barWidth: 9,
        data: rows.map((item, index) => ({
          value: item.value,
          itemStyle: {
            borderRadius: [0, 8, 8, 0],
            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
              { offset: 0, color: "rgba(48, 220, 255, 0.12)" },
              { offset: 0.62, color: ["#27f0c0", "#30dcff", "#ffd76c"][index % 3] },
              { offset: 1, color: "#ffffff" },
            ]),
          },
        })),
        label: {
          show: true,
          position: "right",
          color: "#eaffff",
          fontSize: 10,
          formatter: ({ value }) => formatCompact(value),
        },
      },
      {
        type: "scatter",
        symbolSize: 7,
        data: rows.map((item) => item.value),
        itemStyle: { color: "#ffd76c", shadowBlur: 8, shadowColor: "#ffd76c" },
      },
    ],
  }
})

const tradeTrendOption = computed(() => {
  const rows = tradeMonthRows.value
  return {
    color: ["#30dcff", "#ffd76c"],
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        const head = params.find((item) => item.seriesName === "流通头数")
        const record = params.find((item) => item.seriesName === "出证记录")
        return `${head?.axisValue || ""}<br/>流通头数：${formatNumber(head?.value || 0)}头<br/>出证记录：${formatNumber(record?.value || 0)}条`
      },
    },
    legend: {
      top: 4,
      right: 10,
      itemWidth: 12,
      itemHeight: 8,
      textStyle: { color: "rgba(255, 255, 255, 0.88)", fontSize: 10 },
    },
    grid: { left: 42, right: 38, top: 34, bottom: 28 },
    xAxis: {
      type: "category",
      data: rows.map((item) => item.name),
      axisLabel: { color: "rgba(255, 255, 255, 0.8)", fontSize: 10, interval: 1 },
      axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.24)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        axisLabel: { color: "rgba(255, 255, 255, 0.76)", fontSize: 10, formatter: formatCompact },
        splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.09)", type: "dashed" } },
      },
      {
        type: "value",
        axisLabel: { color: "rgba(255, 255, 255, 0.74)", fontSize: 10, formatter: formatCompact },
        splitLine: { show: false },
      },
    ],
    series: [
      {
        name: "流通头数",
        type: "bar",
        barMaxWidth: 13,
        data: rows.map((item) => item.headCount),
        itemStyle: {
          borderRadius: [7, 7, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#30dcff" },
            { offset: 1, color: "rgba(48, 220, 255, 0.08)" },
          ]),
        },
      },
      {
        name: "出证记录",
        type: "line",
        yAxisIndex: 1,
        smooth: true,
        symbolSize: 5,
        data: rows.map((item) => item.recordCount),
        lineStyle: { width: 2.2, color: "#ffd76c" },
        itemStyle: { color: "#ffd76c", borderColor: "rgba(3, 18, 28, 0.9)", borderWidth: 2 },
      },
    ],
  }
})

const tradeTownOption = computed(() =>
  buildHorizontalBarOption(tradeTownRows.value.slice(0, 10), "headCount", "头", {
    colors: tradePalette,
    style: "ladder",
    axisTextColor: "rgba(255, 255, 255, 0.88)",
    dimAxisTextColor: "rgba(255, 255, 255, 0.74)",
    tooltipFormatter(params) {
      const row = tradeTownRows.value[params.dataIndex]
      return `${row.name}<br/>流通头数：${formatNumber(row.headCount)}头<br/>出证记录：${formatNumber(row.recordCount)}条`
    },
  }),
)

const tradeUsageOption = computed(() => {
  const rows = tradeUsageRows.value
  return {
    color: tradePalette,
    tooltip: chartTooltip((params) => {
      const row = rows.find((item) => item.name === params.name)
      return `${params.name}<br/>占比：${params.percent}%<br/>流通头数：${formatNumber(params.value)}头<br/>出证记录：${formatNumber(row?.recordCount || 0)}条`
    }),
    legend: {
      orient: "vertical",
      right: 4,
      top: "middle",
      itemWidth: 8,
      itemHeight: 8,
      itemGap: 6,
      textStyle: { color: "rgba(255, 255, 255, 0.88)", fontSize: 10 },
      formatter(name) {
        const row = rows.find((item) => item.name === name)
        return row ? `${name}  ${formatCompact(row.headCount)}头` : name
      },
    },
    series: [
      {
        name: "用途结构",
        type: "pie",
        radius: ["38%", "64%"],
        center: ["32%", "54%"],
        startAngle: 110,
        label: { show: false },
        labelLine: { show: false },
        itemStyle: { borderColor: "rgba(3, 18, 31, 0.92)", borderWidth: 3 },
        data: rows.map((item) => ({ name: item.name, value: item.headCount })),
      },
    ],
  }
})

const tradeCertOption = computed(() => {
  const rows = [
    { name: "省外A证", value: tradeMetricValue("outProvinceHeads", "aCertHeads") },
    { name: "省内B证", value: tradeMetricValue("inProvinceHeads", "bCertHeads") },
  ]
  return {
    color: ["#ffd76c", "#30dcff"],
    tooltip: chartTooltip((params) => `${params.name}<br/>流通头数：${formatNumber(params.value)}头<br/>占比：${params.percent}%`),
    series: [
      {
        type: "pie",
        radius: ["48%", "72%"],
        center: ["50%", "52%"],
        label: { show: false },
        labelLine: { show: false },
        itemStyle: { borderColor: "rgba(3, 18, 31, 0.92)", borderWidth: 3 },
        data: rows,
      },
    ],
  }
})

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

function refreshYakMapView() {
  if (isYakOlMapView.value) {
    yakOlMapRef.value?.refreshView?.()
    return
  }
  mapSceneRef.value?.resume?.()
  mapSceneRef.value?.refreshView?.()
}

function clearYakMapLoadTimer() {
  if (!mapLoadTimer) return
  window.clearTimeout(mapLoadTimer)
  mapLoadTimer = null
}

function handleYakMapReset() {
  tileStatus.value = { type: "loading", text: "地图正在复位" }
  topicLayerStatus.value = { type: "idle", text: "" }

  if (isYakOlMapView.value) {
    if (yakOlMapRef.value?.hasMap?.()) {
      olMapReady.value = true
      yakOlMapRef.value?.resetView?.()
    } else {
      olMapReady.value = false
      yakOlMapKey.value += 1
      nextTick(() => {
        window.setTimeout(() => yakOlMapRef.value?.refreshView?.(), 160)
      })
    }
    return
  }

  if (mapSceneRef.value?.hasMap?.()) {
    mapReady.value = true
    refreshYakMapView()
    return
  }
  clearYakMapLoadTimer()
  mapReady.value = false
  mapLoadStarted = false
  yakMapSceneResetRevision.value += 1
  nextTick(() => {
    scheduleYakMapLoad()
  })
}

defineExpose({
  refreshMapView: refreshYakMapView,
  resetMap: handleYakMapReset,
})

onMounted(() => {
  updateScreenViewportHeight()
  window.addEventListener("resize", updateScreenViewportHeight)
  initYakMap()
  loadRemoteRows()
  loadEpidemicData()
  loadTradeData()
  syncForecastRotation()
  syncFoundationRotation()
})

onBeforeUnmount(() => {
  destroyed = true
  window.removeEventListener("resize", updateScreenViewportHeight)
  if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
  stopForecastRotation()
  stopFoundationRotation()
  mapSceneRef.value?.destroyMap?.()
})

watch(
  [activeView, activePeriod, displayTownRows, epidemicEarTagTotal, epidemicRecordTotal, activeEpidemicYear, activeTradeYear, tradeMetrics],
  () => {
    emitModuleStats()
  },
  { immediate: true },
)

watch(activeView, (view, previousView) => {
  const nextUsesOlMap = view === "structure" || view === "forecast"
  const previousUsesOlMap = previousView === "structure" || previousView === "forecast"
  const changesThreeMapTileMode = !nextUsesOlMap && !previousUsesOlMap && (view === "trade" || previousView === "trade")
  if (nextUsesOlMap && !previousUsesOlMap) {
    olMapReady.value = false
    tileStatus.value = { type: "idle", text: "" }
    topicLayerStatus.value = { type: "idle", text: "" }
  }
  if (!nextUsesOlMap && previousUsesOlMap) {
    clearYakMapLoadTimer()
    mapReady.value = false
    mapLoadStarted = false
    tileStatus.value = { type: "idle", text: "" }
    topicLayerStatus.value = { type: "idle", text: "" }
    nextTick(() => {
      scheduleYakMapLoad()
      mapSceneRef.value?.resume?.()
      mapSceneRef.value?.refreshView?.()
      window.setTimeout(() => mapSceneRef.value?.refreshView?.(), 180)
    })
  }
  if (changesThreeMapTileMode) {
    clearYakMapLoadTimer()
    mapReady.value = false
    mapLoadStarted = false
    tileStatus.value = { type: "idle", text: "" }
    topicLayerStatus.value = { type: "idle", text: "" }
    nextTick(() => {
      scheduleYakMapLoad()
    })
  }
  syncForecastRotation()
  syncFoundationRotation()
})

watch(
  () => props.active,
  (active) => {
    if (active) {
      scheduleYakMapLoad()
      mapSceneRef.value?.resume?.()
      syncForecastRotation()
      syncFoundationRotation()
    } else {
      mapSceneRef.value?.pause?.()
      stopForecastRotation()
      stopFoundationRotation()
    }
  },
)

watch(foundationPageMeta, (meta) => {
  if (activeFoundationPage.value >= meta.pageCount) activeFoundationPage.value = 0
  syncFoundationRotation()
})

function handlePeriodSelect(key) {
  if (key === activePeriodKey.value) return
  activePeriodKey.value = key
  syncForecastRotation()
}

function syncForecastRotation() {
  stopForecastRotation()
  if (!props.active || activeView.value !== "forecast" || destroyed) return
  forecastRotationTimer = window.setInterval(rotateForecastPeriod, forecastRotationInterval)
}

function rotateForecastPeriod() {
  const currentIndex = forecastPeriods.findIndex((item) => item.key === activePeriodKey.value)
  const nextIndex = currentIndex >= 0 ? (currentIndex + 1) % forecastPeriods.length : 0
  activePeriodKey.value = forecastPeriods[nextIndex].key
}

function stopForecastRotation() {
  if (!forecastRotationTimer) return
  window.clearInterval(forecastRotationTimer)
  forecastRotationTimer = null
}

function syncFoundationRotation() {
  stopFoundationRotation()
  if (!props.active || activeView.value !== "structure" || destroyed || foundationPageMeta.value.pageCount <= 1) return
  foundationRotationTimer = window.setInterval(rotateFoundationPage, foundationRotationInterval)
}

function rotateFoundationPage() {
  const count = foundationPageMeta.value.pageCount
  if (count <= 1) return
  activeFoundationPage.value = (activeFoundationPage.value + 1) % count
}

function stopFoundationRotation() {
  if (!foundationRotationTimer) return
  window.clearInterval(foundationRotationTimer)
  foundationRotationTimer = null
}

function mergeTownRows(primaryRows = [], fallbackRows = []) {
  const map = new Map()
  fallbackRows.forEach((row) => {
    const name = normalizeBusinessTownName(row.name)
    if (!name) return
    map.set(name, { ...row, name, count: Number(row.count || 0) })
  })
  primaryRows.forEach((row) => {
    const name = normalizeBusinessTownName(row.name)
    if (!name) return
    const fallback = map.get(name) || {}
    map.set(name, {
      ...fallback,
      ...row,
      name,
      count: Number.isFinite(Number(row.count)) ? Number(row.count) : Number(fallback.count || 0),
    })
  })
  return [...map.values()].sort((a, b) => Number(b.count || 0) - Number(a.count || 0))
}

function normalizeBusinessTownName(name) {
  return canonicalTownName(name || "")
}

function normalizeEpidemicTownName(name) {
  return String(name || "").trim()
}

function resolveEpidemicYear(year) {
  const numericYear = Number(year)
  if (Number.isFinite(numericYear) && epidemicYears.includes(numericYear)) return numericYear
  return epidemicYears[epidemicYears.length - 1]
}

function getEpidemicYearRange(year = activeEpidemicYear.value) {
  const targetYear = resolveEpidemicYear(year)
  return {
    startDate: `${targetYear}-01-01`,
    endDate: `${targetYear}-12-31`,
  }
}

function createEpidemicLedgerFilters() {
  return {
    keyword: "",
    town: "",
    medicine: "",
    startDate: "",
    endDate: "",
  }
}

function normalizeAnalysisRows(rows = []) {
  if (!Array.isArray(rows)) return []
  return rows
    .map((item, index) => ({
      key: item.key || item.id || `${item.name || item.label || "item"}-${index}`,
      name: String(item.name || item.label || item.town || item.village || "未填写"),
      value: Number(item.value ?? item.count ?? item.total ?? 0),
    }))
    .filter((item) => item.name && Number.isFinite(item.value))
}

function getEpidemicVaccineTypeCount(analysis = {}) {
  const rows = normalizeAnalysisRows(analysis?.medicineCounts)
  return Math.max(rows.filter((item) => item.name !== "未填写").length, 1)
}

function normalizeTradeYears(...sources) {
  const years = sources
    .flatMap((source) => Array.isArray(source) ? source : [source])
    .map((item) => {
      if (item && typeof item === "object") return item.year ?? item.value ?? item.latestYear ?? item.latest_year
      return item
    })
    .map(Number)
    .filter((year) => Number.isFinite(year) && year > 0)
  return [...new Set(years)].sort((a, b) => a - b)
}

function normalizeTradeCountRows(rows = [], nameKey = "name") {
  if (!Array.isArray(rows)) return []
  return rows
    .map((item, index) => ({
      key: item.key || item.id || `${item[nameKey] || item.name || item.label || "trade"}-${index}`,
      name: String(item[nameKey] || item.name || item.label || item.town || "未填写"),
      recordCount: Number(item.recordCount ?? item.record_count ?? item.count ?? 0),
      headCount: Number(item.headCount ?? item.head_count ?? item.amount ?? item.value ?? 0),
      lng: toFiniteNumber(item.lng ?? item.longitude ?? item.destLng ?? item.dest_lng ?? item.destLongitude ?? item.dest_longitude),
      lat: toFiniteNumber(item.lat ?? item.latitude ?? item.destLat ?? item.dest_lat ?? item.destLatitude ?? item.dest_latitude),
    }))
    .filter((item) => item.name && Number.isFinite(item.headCount))
}

function normalizeTradeYearRows(rows = []) {
  if (!Array.isArray(rows)) return []
  return rows
    .map((item) => ({
      year: Number(item.year),
      recordCount: Number(item.recordCount ?? item.record_count ?? 0),
      headCount: Number(item.headCount ?? item.head_count ?? item.amount ?? item.value ?? 0),
    }))
    .filter((item) => Number.isFinite(item.year) && Number.isFinite(item.headCount))
}

function normalizeOptionValues(source = []) {
  if (!Array.isArray(source)) return []
  return uniqueStrings(source.map((item) => {
    if (item && typeof item === "object") return item.value ?? item.label ?? item.name ?? ""
    return item
  }))
}

function normalizeLabeledOptions(source = []) {
  if (!Array.isArray(source)) return []
  return source
    .map((item) => {
      if (item && typeof item === "object") {
        const value = item.value ?? item.name ?? item.label ?? ""
        const label = item.label ?? item.name ?? item.value ?? value
        return value ? { value: String(value), label: String(label || value) } : null
      }
      return item ? { value: String(item), label: String(item) } : null
    })
    .filter(Boolean)
}

function uniqueStrings(values = []) {
  return [...new Set(values.map((item) => String(item || "").trim()).filter(Boolean))]
}

function normalizeEpidemicStatRows(rows = []) {
  if (!Array.isArray(rows)) return []
  const map = new Map()
  rows.forEach((item, index) => {
    const name = normalizeEpidemicTownName(item.name || item.areaName || item.area_name || item.town || item.label || "")
    if (!name) return
    const value = toFiniteNumber(item.value ?? item.number ?? item.count ?? item.total)
    if (!Number.isFinite(value)) return
    const current = map.get(name) || {
      key: item.areaCode || item.area_code || `${name}-${index}`,
      name,
      recordCount: 0,
      earTagCount: 0,
      source: "stats",
    }
    current.recordCount += value
    const earTagCount = toFiniteNumber(item.earTagCount ?? item.ear_tag_count ?? item.headCount ?? item.head_count)
    current.earTagCount += Number.isFinite(earTagCount) ? earTagCount : 0
    map.set(name, current)
  })
  const totalRecordCount = [...map.values()].reduce((sum, item) => sum + Number(item.recordCount || 0), 0)
  return [...map.values()]
    .map((item) => ({
      ...item,
      earTagCount: item.earTagCount || Math.round(Number(item.recordCount || 0) / epidemicVaccineTypeCount.value),
      ratio: totalRecordCount ? (Number(item.recordCount || 0) / totalRecordCount) * 100 : 0,
    }))
    .sort((a, b) => Number(b.recordCount || 0) - Number(a.recordCount || 0))
}

function normalizeEpidemicStatsPayload(data = {}) {
  const payload = Array.isArray(data) ? { rows: data } : data || {}
  const result = payload.result || {}
  const rows = payload.rows || payload.data || payload.list || payload.records || []
  return {
    rows,
    year: payload.year ?? result.year ?? null,
  }
}

function normalizeEpidemicVillageRows(rows = []) {
  if (!Array.isArray(rows)) return []
  return rows
    .map((item, index) => {
      const town = normalizeEpidemicTownName(item.town || item.townName || item.town_name || "")
      const village = String(item.village || item.villageName || item.village_name || item.name || "").trim()
      const recordCount = toFiniteNumber(item.recordCount ?? item.record_count ?? item.records ?? item.total)
      const earTagCount = toFiniteNumber(item.earTagCount ?? item.ear_tag_count ?? item.headCount ?? item.head_count ?? item.value)
      const imageCount = toFiniteNumber(item.imageCount ?? item.image_count ?? item.images)
      const imageRate = toFiniteNumber(item.imageRate ?? item.image_rate)
      const lng = toFiniteNumber(item.lng ?? item.longitude)
      const lat = toFiniteNumber(item.lat ?? item.latitude)
      return {
        key: item.key || item.id || `${town}-${village}-${index}`,
        town,
        village,
        recordCount: Number.isFinite(recordCount) ? recordCount : 0,
        earTagCount: Number.isFinite(earTagCount) ? earTagCount : 0,
        imageCount: Number.isFinite(imageCount) ? imageCount : 0,
        imageRate: Number.isFinite(imageRate) ? imageRate : 0,
        lng,
        lat,
      }
    })
    .filter((item) => item.town && item.village && Number.isFinite(item.earTagCount))
    .sort((a, b) => Number(b.earTagCount || 0) - Number(a.earTagCount || 0))
}

function buildYearScopedEpidemicVillageStats(analysis = {}, villageStats = {}) {
  const payload = Array.isArray(villageStats) ? { rows: villageStats } : villageStats || {}
  const result = payload.result || {}
  const baseRows = normalizeEpidemicVillageRows(payload.rows || payload.data || payload.list || payload.records || result.rows || [])
  const villageLocationMap = new Map()
  baseRows.forEach((row) => {
    getEpidemicVillageMatchKeys(row.village).forEach((key) => {
      if (!villageLocationMap.has(key)) villageLocationMap.set(key, row)
    })
  })

  const townRows = normalizeAnalysisRows(analysis?.townCounts)
  const fallbackTown = townRows.length === 1 ? normalizeEpidemicTownName(townRows[0].name) : ""
  const vaccineTypeCount = getEpidemicVaccineTypeCount(analysis)
  const rows = normalizeAnalysisRows(analysis?.villageCounts)
    .map((item, index) => {
      const village = String(item.name || "").trim()
      const locationRow = getEpidemicVillageMatchKeys(village)
        .map((key) => villageLocationMap.get(key))
        .find(Boolean) || {}
      const recordCount = Number(item.value || 0)
      return {
        key: locationRow.key || `${locationRow.town || fallbackTown || "防疫"}-${village}-${index}`,
        town: locationRow.town || fallbackTown,
        village,
        recordCount,
        earTagCount: Math.round(recordCount / vaccineTypeCount),
        imageCount: locationRow.imageCount || 0,
        imageRate: locationRow.imageRate || 0,
        lng: locationRow.lng,
        lat: locationRow.lat,
      }
    })
    .filter((row) => row.village && row.recordCount > 0)

  return {
    ...payload,
    rows,
  }
}

function getEpidemicVillageMatchKeys(value) {
  const text = String(value || "").trim()
  const withoutSuffix = text.replace(/(村|社区|牧委会)$/, "")
  return [...new Set([text, withoutSuffix].filter(Boolean))]
}

function toFiniteNumber(value) {
  if (value === undefined || value === null || value === "") return NaN
  const number = Number(String(value).replace(/,/g, "").replace(/%/g, ""))
  return Number.isFinite(number) ? number : NaN
}

function escapeCqlLike(value) {
  return String(value || "").replace(/'/g, "''").replace(/%/g, "\\%").replace(/_/g, "\\_")
}

function buildVillageBoundaryFilter(town, village) {
  const cleanTown = normalizeEpidemicTownName(town)
  const cleanVillage = String(village || "").trim()
  if (!cleanTown || !cleanVillage) return ""
  const fullName = `${cleanTown}${cleanVillage}`
  const escapedFullName = escapeCqlLike(fullName)
  const escapedVillage = escapeCqlLike(cleanVillage)
  return `(full_name LIKE '%${escapedFullName}%' OR name = '${escapedFullName}' OR name = '${escapedVillage}')`
}

function verticalAxisLabel(value) {
  return formatPlaceAxisLabel(value)
}

function verticalTownAxisLabel(value) {
  const text = String(value || "").trim().replace(/\s+/g, "")
  return text ? Array.from(text).join("\n") : ""
}

function formatPlaceAxisLabel(value) {
  const text = String(value || "").trim()
  if (!text) return ""
  const placeMatch = text.match(/^(.+)(镇|乡|村)$/)
  if (placeMatch) return `${placeMatch[1]}\n${placeMatch[2]}`
  return text
}

function sortMonthDayRows(prev, next) {
  return parseMonthDayKey(prev.name) - parseMonthDayKey(next.name)
}

function parseMonthDayKey(value) {
  const match = String(value || "").match(/(\d{1,2})\D+(\d{1,2})/)
  if (!match) return Number.MAX_SAFE_INTEGER
  return Number(match[1]) * 100 + Number(match[2])
}

function normalizeRecordRows(rows = []) {
  return Array.isArray(rows) ? rows.map(normalizeRecordRow).filter(Boolean) : []
}

function normalizeRecordRow(row) {
  if (!row) return null
  return {
    epidemicId: row.epidemicId || row.epidemic_id || row.id || "",
    earTag: row.earTag || row.ear_tag || "",
    owner: row.owner || "",
    idType: row.idType || row.id_type || "",
    idNo: row.idNo || row.id_no || "",
    phone: row.phone || "",
    town: row.town || "",
    village: row.village || "",
    location: row.location || "",
    livestockType: row.livestockType || row.livestock_type || "",
    age: row.age || "",
    sex: row.sex || "",
    weight: row.weight || row.weightKg || row.weight_kg || "",
    preventionType: row.preventionType || row.prevention_type || "",
    method: row.method || "",
    medicine: row.medicine || "",
    dose: row.dose || "",
    operator: row.operator || "",
    operateTime: row.operateTime || row.operate_time || "",
    imagePaths: row.imagePaths || row.image_paths || "",
  }
}

function normalizeTradeRecordRows(rows = []) {
  return Array.isArray(rows) ? rows.map(normalizeTradeRecordRow).filter(Boolean) : []
}

function normalizeTradeRecordRow(row) {
  if (!row) return null
  return {
    certId: row.certId || row.cert_id || row.id || "",
    certType: row.certType || row.cert_type || "",
    certScopeName: row.certScopeName || row.cert_scope_name || "",
    factoryCode: row.factoryCode || row.factory_code || "",
    ownerName: row.ownerName || row.owner_name || "",
    ownerPhone: row.ownerPhone || row.owner_phone || "",
    transportNumber: row.transportNumber || row.transport_number || "",
    amount: Number(row.amount ?? row.headCount ?? row.head_count ?? row.value ?? 0),
    unitName: row.unitName || row.unit_name || "头",
    animalTypeName: row.animalTypeName || row.animal_type_name || "",
    usageTypeName: row.usageTypeName || row.usage_type_name || "",
    startTown: row.startTown || row.start_town || "",
    startRegionFullName: row.startRegionFullName || row.start_region_full_name || "",
    destProvince: row.destProvince || row.dest_province || "",
    destRegionFullName: row.destRegionFullName || row.dest_region_full_name || "",
    quarantineOfficerName: row.quarantineOfficerName || row.quarantine_officer_name || "",
    issueTime: row.issueTime || row.issue_time || "",
    statusName: row.statusName || row.status_name || "",
  }
}

function getMetricNumber(metrics, keys) {
  for (const key of keys) {
    const value = Number(metrics?.[key])
    if (Number.isFinite(value)) return value
  }
  return 0
}

function tradeMetricValue(...keys) {
  return getMetricNumber(tradeMetrics.value, keys)
}

function formatShortTownNames(names = [], limit = 2) {
  const list = [...new Set(names.map((item) => String(item || "").trim()).filter(Boolean))]
  if (!list.length) return "暂无"
  if (list.length <= limit) return list.join("、")
  return `${list.slice(0, limit).join("、")}等${list.length}个乡镇`
}

function buildEpidemicWeakTarget() {
  const missingNames = epidemicMissingTownNames.value
  if (missingNames.length) {
    return {
      name: formatShortTownNames(missingNames, 1),
      desc: "年度防疫覆盖需补强",
      level: "watch",
    }
  }

  const rows = epidemicTownRows.value
    .filter((item) => Number(item.earTagCount || item.recordCount || 0) > 0)
    .sort((a, b) => Number(a.earTagCount || a.recordCount || 0) - Number(b.earTagCount || b.recordCount || 0))
  const weakTown = rows[0]
  if (!weakTown) {
    return {
      name: "暂无",
      desc: "等待年度防疫记录形成",
      level: "normal",
    }
  }

  return {
    name: weakTown.name,
    desc: `${formatCompact(weakTown.earTagCount || weakTown.recordCount)}头，建议补免回访`,
    level: rows.length >= 2 ? "watch" : "normal",
  }
}

function buildEpidemicConcernTownRows() {
  const missingNames = epidemicMissingTownNames.value
  if (missingNames.length) {
    return missingNames.slice(0, 4).map((name, index) => ({
      key: `missing-${name}`,
      name,
      value: Math.max(25, 100 - index * 12),
      label: "待补免",
      level: "watch",
    }))
  }

  const rows = epidemicTownRows.value.filter((item) => Number(item.earTagCount || item.recordCount || 0) > 0)
  if (!rows.length) {
    return [{ key: "empty", name: "暂无", value: 0, label: "待开展", level: "watch" }]
  }

  const avg = rows.reduce((sum, item) => sum + Number(item.earTagCount || 0), 0) / rows.length || 1
  return [...rows]
    .sort((a, b) => Number(a.earTagCount || 0) - Number(b.earTagCount || 0))
    .slice(0, 4)
    .map((item) => {
      const count = Number(item.earTagCount || item.recordCount || 0)
      const gap = Math.max(8, Math.round(((avg - count) / avg) * 100))
      return {
        key: `weak-${item.name}`,
        name: item.name,
        value: Math.min(100, gap),
        label: `${formatCompact(count)}头`,
        level: gap >= 35 ? "watch" : "normal",
      }
    })
}

function buildEpidemicTownBalanceRows() {
  const townRows = epidemicStatRows.value.length ? epidemicStatRows.value : epidemicTownRows.value
  const measuredRows = townRows
    .map((item, index) => {
      const name = normalizeEpidemicTownName(item.name)
      const earTagCount = Number(item.earTagCount || 0)
      const recordCount = Number(item.recordCount || item.value || 0)
      const value = earTagCount || Math.round(recordCount / epidemicVaccineTypeCount.value)
      return {
        key: item.key || `${name}-${index}`,
        name,
        value,
        recordCount,
        isMissing: false,
      }
    })
    .filter((item) => item.name && Number.isFinite(item.value))

  const missingRows = epidemicMissingTownNames.value.map((name, index) => ({
    key: `missing-${name}-${index}`,
    name,
    value: 0,
    recordCount: 0,
    isMissing: true,
  }))

  const merged = [...missingRows, ...measuredRows]
  if (!merged.length) {
    return [{ key: "empty", name: "待开展", value: 0, recordCount: 0, isMissing: true }]
  }

  return merged
    .sort((a, b) => {
      if (a.isMissing !== b.isMissing) return a.isMissing ? -1 : 1
      return Number(a.value || 0) - Number(b.value || 0)
    })
    .slice(0, 7)
}

function buildEpidemicVillageCoverageRows() {
  const locationRows = epidemicVillageMapRows.value
    .map((item, index) => ({
      key: item.key || `${item.town}-${item.village}-${index}`,
      name: item.village || item.name,
      town: item.town,
      value: Number(item.earTagCount || item.value || 0),
    }))
    .filter((item) => item.name && Number.isFinite(item.value) && item.value > 0)

  if (locationRows.length) return locationRows.sort((a, b) => b.value - a.value).slice(0, 9)

  return normalizeAnalysisRows(epidemicAnalysis.value?.villageCounts)
    .map((item, index) => ({
      key: item.key || `${item.name}-${index}`,
      name: item.name,
      town: "",
      value: Math.round(Number(item.value || 0) / epidemicVaccineTypeCount.value),
    }))
    .filter((item) => item.name && Number.isFinite(item.value) && item.value > 0)
    .sort((a, b) => b.value - a.value)
    .slice(0, 9)
}

function buildEpidemicOverviewStatus() {
  if (!epidemicRecordTotal.value) {
    return {
      level: "watch",
      title: "年度待开展",
      desc: "年度免疫记录暂未形成",
      advice: "建议先核查年度免疫计划与乡镇推进节奏。",
    }
  }

  const townCoverage = epidemicTownCoverageRate.value
  const weakTarget = epidemicWeakTarget.value
  if (epidemicMissingTownNames.value.length) {
    return {
      level: "watch",
      title: "补免待核查",
      desc: `乡镇覆盖${formatPercent(townCoverage)}，${weakTarget.name}需补强`,
      advice: `优先核查${formatShortTownNames(epidemicMissingTownNames.value, 1)}免疫进度。`,
    }
  }

  if (townCoverage >= 90 && epidemicVaccineTypeCount.value >= 4) {
    return {
      level: "stable",
      title: "运行平稳",
      desc: `乡镇覆盖${formatPercent(townCoverage)}，疫苗谱较完整`,
      advice: "建议保持当前免疫节奏，持续跟踪补免回访。",
    }
  }

  return {
    level: "normal",
    title: "均衡回访",
    desc: `${weakTarget.name}相对偏低，适合继续跟进`,
    advice: `关注${weakTarget.name}补免回访，保持覆盖均衡。`,
  }
}

function buildEpidemicOverviewRows() {
  return [
    {
      key: "latest",
      name: "最近免疫",
      value: epidemicLatestDate.value,
      desc: "最后作业时间",
      level: epidemicLatestDate.value === "-" ? "watch" : "stable",
      color: "#27f0c0",
    },
    {
      key: "top-town",
      name: "高频乡镇",
      value: epidemicTopTownName.value,
      desc: "年度作业集中区",
      level: "normal",
      color: "#30dcff",
    },
    {
      key: "vaccine",
      name: "主用疫苗",
      value: epidemicTopVaccineName.value,
      desc: `${epidemicVaccineTypeCount.value}类疫苗`,
      level: "normal",
      color: "#ffd76c",
    },
  ]
}

function buildTradeRiskRows() {
  const totalHeads = tradeMetricValue("headCount", "totalHeads", "total_heads")
  const townRows = tradeTownSourceRows.value
  const sourceTotal = townRows.reduce((sum, item) => sum + Number(item.headCount || 0), 0) || totalHeads || 1
  const topTown = townRows[0]
  const topTownShare = topTown ? (Number(topTown.headCount || 0) / sourceTotal) * 100 : 0

  const usageRows = tradeUsageRows.value
    .filter((item) => item.name && item.name !== "未填写")
    .sort((a, b) => Number(b.headCount || 0) - Number(a.headCount || 0))
  const usageTotal = usageRows.reduce((sum, item) => sum + Number(item.headCount || 0), 0) || totalHeads || 1
  const topUsage = usageRows[0]
  const topUsageShare = topUsage ? (Number(topUsage.headCount || 0) / usageTotal) * 100 : 0

  const vehicleRows = tradeVehicleRows.value
    .filter((item) => item.name && item.name !== "未填写")
    .sort((a, b) => Number(b.headCount || b.recordCount || 0) - Number(a.headCount || a.recordCount || 0))
  const topVehicle = vehicleRows[0]
  const vehicleCount = tradeMetricValue("vehicleCount", "vehicle_count", "transportVehicleCount", "transport_vehicle_count")
  const avgHeadsPerVehicle = vehicleCount > 0 ? totalHeads / vehicleCount : 0

  const outProvinceHeads = tradeMetricValue("outProvinceHeads", "out_province_heads", "aCertHeads", "a_cert_heads")
  const inProvinceHeads = tradeMetricValue("inProvinceHeads", "in_province_heads", "bCertHeads", "b_cert_heads")
  const certTotal = outProvinceHeads + inProvinceHeads || totalHeads || 1
  const outProvinceShare = outProvinceHeads ? (outProvinceHeads / certTotal) * 100 : 0

  const rows = [
    {
      key: "town",
      name: "高频出栏乡镇",
      value: topTown?.displayName || topTown?.name || "暂无",
      desc: topTown
        ? `${formatCompact(topTown.headCount)}头，占${formatPercent(topTownShare)}`
        : "等待来源乡镇数据",
      level: topTownShare >= 35 ? "high" : topTownShare >= 24 ? "watch" : "normal",
      color: "#ffcf62",
    },
    {
      key: "usage",
      name: "交易用途集中度",
      value: topUsage?.name || "暂无",
      desc: topUsage ? `占${formatPercent(topUsageShare)}` : "等待用途结构数据",
      level: topUsageShare >= 60 ? "high" : topUsageShare >= 45 ? "watch" : "normal",
      color: "#30dcff",
    },
    {
      key: "vehicle",
      name: topVehicle ? "运输车辆高频" : "车辆周转强度",
      value: topVehicle
        ? `${formatCompact(topVehicle.headCount || topVehicle.recordCount)}${topVehicle.headCount ? "头" : "条"}`
        : vehicleCount ? `车均${formatNumber(avgHeadsPerVehicle)}头` : "--",
      desc: topVehicle
        ? "已接入车辆聚合，明细在台账核查"
        : vehicleCount ? `${formatNumber(vehicleCount)}辆参与流通` : "等待车辆聚合数据",
      level: topVehicle || avgHeadsPerVehicle >= 260 ? "watch" : "normal",
      color: "#7aa7ff",
    },
    {
      key: "cert-dest",
      name: "证照/去向线索",
      value: outProvinceHeads ? formatPercent(outProvinceShare) : "--",
      desc: outProvinceHeads
        ? `省外流通${formatCompact(outProvinceHeads)}头`
        : "等待证照去向数据",
      level: outProvinceShare >= 55 ? "high" : outProvinceShare >= 35 ? "watch" : "normal",
      color: "#ff8f6b",
    },
  ]

  return rows.map((item) => ({
    ...item,
    badge: item.level === "high" ? "重点" : item.level === "watch" ? "关注" : "平稳",
  }))
}

function getImagePaths(row = {}) {
  return String(row.imagePaths || "")
    .split(";")
    .map((item) => item.trim())
    .filter(Boolean)
}

function getEpidemicImageUrl(path) {
  const cleanPath = String(path || "").replace(/^images\//, "")
  const encodedPath = cleanPath.split("/").map(encodeURIComponent).join("/")
  return `${epidemicImageBaseUrl}/${encodedPath}`
}

function openEpidemicImagePreview(path) {
  epidemicImagePreviewUrl.value = getEpidemicImageUrl(path)
  epidemicImagePreviewVisible.value = true
}

function closeEpidemicImagePreview() {
  epidemicImagePreviewVisible.value = false
  epidemicImagePreviewUrl.value = ""
}

function displayValue(value) {
  return value === undefined || value === null || value === "" ? "-" : value
}

function hasDisplayValue(value) {
  const text = String(value ?? "").trim()
  return Boolean(text && text !== "-")
}

function buildDetailField(label, value, options = {}) {
  if (!hasDisplayValue(value)) return null
  return { label, value, ...options }
}

function displayDate(value) {
  return value ? String(value).slice(0, 10) : "-"
}

function displayTownVillage(row = {}) {
  return [row.town, row.village].filter(Boolean).join("/") || "-"
}

function maskPhone(phone) {
  const text = String(phone || "").trim()
  if (!text) return "-"
  if (text.length < 7) return text
  return `${text.slice(0, 3)}****${text.slice(-4)}`
}

function maskCertificateNo(value) {
  const text = String(value || "").trim()
  if (!text) return "-"
  if (text.length <= 4) return text
  return `****${text.slice(-4)}`
}

function formatWeight(value) {
  const text = String(value ?? "").trim()
  if (!text) return "-"
  if (/kg|公斤|千克/i.test(text)) return text
  const number = Number(text)
  return Number.isFinite(number) ? `${formatNumber(number)}kg` : text
}

function displayDose(value) {
  const text = String(value ?? "").trim()
  return text || "-"
}

function displayTradeCertScope(row = {}) {
  return row.certScopeName || row.certType || "-"
}

async function loadRemoteRows() {
  structureLoading.value = true
  structureError.value = ""
  const [townResult, forecastResult, structureResult] = await Promise.allSettled([
    getYakTownRows(),
    getYakForecastRows(),
    getYakStructureRows(),
  ])
  if (townResult.status === "fulfilled" && townResult.value?.length) {
    apiTownRows.value = aggregateTownRows(townResult.value
      .map((item) => ({
        name: canonicalTownName(item.name),
        count: Number(item.value),
      }))
      .filter((item) => item.name && Number.isFinite(item.count)))
  }
  if (forecastResult.status === "fulfilled" && forecastResult.value?.length) {
    apiMonthlyRows.value = forecastResult.value
      .map((item) => ({
        month: item.name,
        count: Number(item.value),
        speed: parseExtraNumber(item.extra),
      }))
      .filter((item) => item.month && Number.isFinite(item.count))
  }
  if (structureResult.status === "fulfilled") {
    apiStructureRows.value = normalizeStructureApiRows(structureResult.value)
    if (!apiStructureRows.value.length) {
      structureError.value = "结构数据暂无数据"
    }
  } else {
    structureError.value = formatLoadError(structureResult.reason, "结构数据异常")
    apiStructureRows.value = []
  }
  structureLoading.value = false
}

async function loadTradeData(preferredYear = activeTradeYear.value) {
  tradeLoading.value = true
  tradeError.value = ""
  try {
    if (!tradeOptions.value?.years?.length) {
      tradeOptions.value = await getYakTradeOptions()
    }
    const optionYears = normalizeTradeYears(tradeOptions.value?.years)
    const latestYear = Number(tradeOptions.value?.latestYear || tradeOptions.value?.latest_year || optionYears[optionYears.length - 1])
    const targetYear = Number(preferredYear || activeTradeYear.value || latestYear || optionYears[optionYears.length - 1])
    activeTradeYear.value = targetYear
    let overview = await getYakTradeOverview({ year: targetYear })
    const hasData = overview?.hasData ?? overview?.has_data
    const fallbackYear = Number(overview?.latestYear || overview?.latest_year || latestYear)
    if (hasData === false && fallbackYear && fallbackYear !== targetYear) {
      activeTradeYear.value = fallbackYear
      overview = await getYakTradeOverview({ year: fallbackYear })
    }
    tradeOverview.value = overview || { metrics: {}, hasData: false }
    await loadTradeRecords(1)
  } catch (error) {
    tradeError.value = "交易数据接口异常"
    tradeOverview.value = { metrics: {}, hasData: false }
    tradeRecords.value = []
    tradeRecordTotal.value = 0
  } finally {
    tradeLoading.value = false
  }
}

async function loadTradeRecords(page = tradeRecordPage.value) {
  if (!activeTradeYear.value) return
  const params = {
    year: activeTradeYear.value,
    page,
    pageSize: tradeRecordPageSize,
    keyword: tradeRecordQueryKeyword.value,
  }
  try {
    const data = await getYakTradeRecords(params)
    tradeRecords.value = normalizeTradeRecordRows(data.rows)
    tradeRecordTotal.value = Number(data.page?.totalCount || data.totalCount || tradeRecords.value.length || 0)
    tradeRecordPage.value = Number(data.page?.page || page || 1)
  } catch (error) {
    tradeError.value = tradeError.value || "交易台账接口异常"
    tradeRecords.value = []
    tradeRecordTotal.value = 0
  }
}

function handleTradeYearSelect(year) {
  const nextYear = Number(year)
  if (!Number.isFinite(nextYear) || nextYear === Number(activeTradeYear.value)) return
  activeTradeYear.value = nextYear
  tradeRecordPage.value = 1
  tradeRecordKeyword.value = ""
  tradeRecordQueryKeyword.value = ""
  loadTradeData(nextYear)
}

function handleEpidemicYearSelect(year) {
  const nextYear = resolveEpidemicYear(year)
  if (nextYear === Number(activeEpidemicYear.value)) return
  activeEpidemicYear.value = nextYear
  epidemicRecordPage.value = 1
  epidemicRecordKeyword.value = ""
  epidemicRecordQueryKeyword.value = ""
  epidemicLedgerPage.value = 1
  epidemicLedgerFilters.value = createEpidemicLedgerFilters()
  loadEpidemicData(nextYear)
}

function searchTradeRecords() {
  tradeRecordQueryKeyword.value = tradeRecordKeyword.value.trim()
  loadTradeRecords(1)
}

function changeTradeRecordPage(page) {
  const nextPage = Math.min(Math.max(Number(page) || 1, 1), tradeRecordPageCount.value)
  loadTradeRecords(nextPage)
}

function openTradeLedgerModal(row = null) {
  tradeLedgerModalVisible.value = true
  if (row) tradeLedgerModalSelectedRecord.value = row
  loadTradeLedgerModalRows(1, row)
}

function closeTradeLedgerModal() {
  tradeLedgerModalVisible.value = false
}

function searchTradeLedgerModal() {
  loadTradeLedgerModalRows(1)
}

function resetTradeLedgerModalFilters() {
  tradeLedgerModalFilters.value = {
    keyword: "",
    startTown: "",
    usageType: "",
    certType: "",
  }
  loadTradeLedgerModalRows(1)
}

function changeTradeLedgerModalPage(page) {
  const nextPage = Math.min(Math.max(Number(page) || 1, 1), tradeLedgerModalPageCount.value)
  loadTradeLedgerModalRows(nextPage)
}

async function loadTradeLedgerModalRows(page = tradeLedgerModalPage.value, preferredRow = null) {
  if (!activeTradeYear.value) return
  tradeLedgerModalLoading.value = true
  tradeLedgerModalError.value = ""
  const filters = tradeLedgerModalFilters.value
  const params = {
    year: activeTradeYear.value,
    page,
    pageSize: tradeLedgerModalPageSize,
    keyword: filters.keyword.trim(),
    startTown: filters.startTown,
    usageType: filters.usageType,
    certType: filters.certType,
  }
  try {
    const data = await getYakTradeRecords(params)
    tradeLedgerModalRows.value = normalizeTradeRecordRows(data.rows)
    tradeLedgerModalTotal.value = Number(data.page?.totalCount || data.totalCount || tradeLedgerModalRows.value.length || 0)
    tradeLedgerModalPage.value = Number(data.page?.page || page || 1)
    const preferredKey = preferredRow ? preferredRow.certId || preferredRow.factoryCode : ""
    const selected = preferredKey
      ? tradeLedgerModalRows.value.find((item) => (item.certId || item.factoryCode) === preferredKey) || preferredRow
      : tradeLedgerModalRows.value[0] || null
    selectTradeLedgerRecord(selected)
  } catch (error) {
    tradeLedgerModalError.value = "交易台账接口异常"
    tradeLedgerModalRows.value = []
    tradeLedgerModalTotal.value = 0
    tradeLedgerModalSelectedRecord.value = null
  } finally {
    tradeLedgerModalLoading.value = false
  }
}

function selectTradeLedgerRecord(row) {
  tradeLedgerModalSelectedRecord.value = row || null
}

function locateTradeRecord(row) {
  if (!row?.startTown) return
  activeView.value = "trade"
  mapSceneRef.value?.focusTown?.(normalizeEpidemicTownName(row.startTown), {
    distanceScale: 0.78,
    duration: 0.8,
  })
}

async function loadEpidemicData(preferredYear = activeEpidemicYear.value) {
  const targetYear = resolveEpidemicYear(preferredYear)
  const yearRange = getEpidemicYearRange(targetYear)
  activeEpidemicYear.value = targetYear
  epidemicLoading.value = true
  epidemicError.value = ""
  const [analysisResult, recordsResult, statsResult, villageStatsResult] = await Promise.allSettled([
    getYakEpidemicAnalysis(yearRange),
    getYakEpidemicRecords({
      page: epidemicRecordPage.value,
      pageSize: epidemicRecordPageSize,
      keyword: epidemicRecordQueryKeyword.value,
      ...yearRange,
    }),
    getYakEpidemicStats({ year: targetYear }),
    getYakEpidemicVillageStats(),
  ])

  if (analysisResult.status === "fulfilled") {
    epidemicAnalysis.value = analysisResult.value || { metrics: {} }
  } else {
    epidemicError.value = "防疫统计接口异常"
    epidemicAnalysis.value = { metrics: {} }
  }

  if (recordsResult.status === "fulfilled") {
    const data = recordsResult.value || {}
    epidemicRecords.value = normalizeRecordRows(data.rows)
    epidemicTotal.value = Number(data.page?.totalCount || data.totalCount || epidemicRecords.value.length || 0)
  } else {
    epidemicError.value = epidemicError.value || "防疫台账接口异常"
    epidemicRecords.value = []
    epidemicTotal.value = 0
  }

  if (statsResult.status === "fulfilled") {
    epidemicStats.value = normalizeEpidemicStatsPayload(statsResult.value)
  } else {
    epidemicStats.value = { rows: [], year: null }
  }

  if (villageStatsResult.status === "fulfilled") {
    epidemicVillageStats.value = buildYearScopedEpidemicVillageStats(epidemicAnalysis.value, villageStatsResult.value)
  } else {
    epidemicVillageStats.value = { rows: [] }
  }

  epidemicLoading.value = false
}

async function loadEpidemicPreviewRecords(page = epidemicRecordPage.value) {
  epidemicLoading.value = true
  epidemicError.value = ""
  const nextPage = Math.min(Math.max(Number(page) || 1, 1), epidemicRecordPageCount.value)
  const yearRange = getEpidemicYearRange(activeEpidemicYear.value)
  try {
    const data = await getYakEpidemicRecords({
      page: nextPage,
      pageSize: epidemicRecordPageSize,
      keyword: epidemicRecordQueryKeyword.value,
      ...yearRange,
    })
    epidemicRecords.value = normalizeRecordRows(data.rows)
    epidemicTotal.value = Number(data.page?.totalCount || data.totalCount || epidemicRecords.value.length || 0)
    epidemicRecordPage.value = Number(data.page?.page || nextPage || 1)
  } catch (error) {
    epidemicError.value = "防疫台账接口异常"
    epidemicRecords.value = []
    epidemicTotal.value = 0
    epidemicRecordPage.value = 1
  } finally {
    epidemicLoading.value = false
  }
}

function searchEpidemicRecords() {
  epidemicRecordQueryKeyword.value = epidemicRecordKeyword.value.trim()
  epidemicLedgerFilters.value = {
    ...createEpidemicLedgerFilters(),
    keyword: epidemicRecordQueryKeyword.value,
  }
  loadEpidemicPreviewRecords(1)
}

function changeEpidemicRecordPage(page) {
  loadEpidemicPreviewRecords(page)
}

function openEpidemicLedgerModal(row = null) {
  epidemicLedgerModalVisible.value = true
  if (row) {
    epidemicLedgerSelectedRecord.value = row
    epidemicLedgerSelectedDetail.value = null
  }
  loadEpidemicLedgerRows(1, row)
}

function closeEpidemicLedgerModal() {
  epidemicLedgerModalVisible.value = false
  closeEpidemicImagePreview()
}

function searchEpidemicLedger() {
  loadEpidemicLedgerRows(1)
}

function resetEpidemicLedgerFilters() {
  epidemicLedgerFilters.value = createEpidemicLedgerFilters()
  loadEpidemicLedgerRows(1)
}

function changeEpidemicLedgerPage(page) {
  loadEpidemicLedgerRows(page)
}

async function loadEpidemicLedgerRows(page = epidemicLedgerPage.value, preferredRow = null) {
  epidemicLedgerLoading.value = true
  epidemicLedgerError.value = ""
  const filters = epidemicLedgerFilters.value
  const yearRange = getEpidemicYearRange(activeEpidemicYear.value)
  const params = {
    keyword: filters.keyword.trim(),
    town: filters.town,
    medicine: filters.medicine,
    startDate: filters.startDate || yearRange.startDate,
    endDate: filters.endDate || yearRange.endDate,
    page,
    pageSize: epidemicLedgerPageSize,
  }
  try {
    const data = await getYakEpidemicRecords(params)
    epidemicLedgerRows.value = normalizeRecordRows(data.rows)
    epidemicLedgerTotal.value = Number(data.page?.totalCount || data.totalCount || epidemicLedgerRows.value.length || 0)
    epidemicLedgerPage.value = Number(data.page?.page || page || 1)
    const selected = preferredRow || epidemicLedgerRows.value[0] || null
    if (selected) {
      selectEpidemicLedgerRecord(selected)
    } else {
      epidemicLedgerSelectedRecord.value = null
      epidemicLedgerSelectedDetail.value = null
    }
  } catch (error) {
    epidemicLedgerError.value = "台账接口异常"
    epidemicLedgerRows.value = []
    epidemicLedgerTotal.value = 0
  } finally {
    epidemicLedgerLoading.value = false
  }
}

async function selectEpidemicLedgerRecord(row) {
  epidemicLedgerSelectedRecord.value = row
  epidemicLedgerSelectedDetail.value = null
  if (!row?.earTag && !row?.epidemicId) return
  const token = ++epidemicLedgerDetailToken
  epidemicLedgerDetailLoading.value = true
  try {
    const detail = await getYakEpidemicDetail(row.earTag || row.epidemicId)
    if (token === epidemicLedgerDetailToken) {
      epidemicLedgerSelectedDetail.value = normalizeRecordRow(detail) || row
    }
  } catch (error) {
    if (token === epidemicLedgerDetailToken) {
      epidemicLedgerSelectedDetail.value = row
    }
  } finally {
    if (token === epidemicLedgerDetailToken) {
      epidemicLedgerDetailLoading.value = false
    }
  }
}

function initYakMap() {
  mapAssets = new Assets()
  mapAssetsReady = false
  mapAssets.ready?.then(() => {
    mapAssetsReady = true
    scheduleYakMapLoad()
  }).catch(() => {
    mapReady.value = true
  })
}

function scheduleYakMapLoad() {
  if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return
  mapLoadStarted = true
  if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
  mapLoadTimer = window.setTimeout(() => {
    mapLoadTimer = null
    loadYakMapOnce()
  }, 600)
}

async function loadYakMapOnce(attempt = 0) {
  if (destroyed) return
  if (!props.active) {
    mapLoadStarted = false
    return
  }
  await nextTick()
  if (!props.active) {
    mapLoadStarted = false
    return
  }
  const scene = mapSceneRef.value
  scene?.loadMap?.(mapAssets)
  if (scene?.canvasMap) {
    mapLoadStarted = false
    mapReady.value = true
    scene.play?.()
    return
  }
  if (attempt < 24) {
    window.setTimeout(() => loadYakMapOnce(attempt + 1), 120)
    return
  }
  mapLoadStarted = false
}

function handleYakLayerStatus(status = {}) {
  topicLayerStatus.value = {
    type: status.type || "idle",
    text: status.text || "",
  }
}

function handleYakOlReady() {
  olMapReady.value = true
}

function handleYakTileStatus(status = {}) {
  tileStatus.value = {
    type: status.type || "idle",
    text: status.text || "",
    loaded: status.loaded,
    failed: status.failed,
    total: status.total,
    zoom: status.zoom,
    tileUrlTemplate: status.tileUrlTemplate,
  }
}

function emitModuleStats() {
  if (activeView.value === "epidemic") {
    emit("module-stats", {
      moduleKey: "yak-management",
      items: [
        { zh: "防疫记录", value: epidemicRecordTotal.value, unit: "条" },
        { zh: "防疫耳标", value: epidemicEarTagTotal.value, unit: "头" },
        { zh: "疫苗类型", value: epidemicVaccineTypeCount.value, unit: "类" },
        { zh: "涉及村数", value: epidemicVillageTotal.value, unit: "个" },
      ],
    })
    return
  }
  if (activeView.value === "forecast") {
    const change = activePeriod.value.stock - estimatedStock
    emit("module-stats", {
      moduleKey: "yak-management",
      items: [
        { zh: "预测期末存栏", value: activePeriod.value.stock, unit: "头" },
        { zh: "繁殖补充", value: activePeriod.value.births, unit: "头" },
        { zh: "出栏流出", value: activePeriod.value.outflow, unit: "头" },
        { zh: "预测净变化", value: change, unit: "头" },
      ],
    })
    return
  }
  if (activeView.value === "trade") {
    emit("module-stats", {
      moduleKey: "yak-management",
      items: [
        { zh: "流通总量", value: tradeMetricValue("headCount"), unit: "头" },
        { zh: "出证记录", value: tradeMetricValue("recordCount"), unit: "条" },
        { zh: "交易销售", value: tradeMetricValue("saleHeadCount"), unit: "头" },
        { zh: "运输车辆", value: tradeMetricValue("vehicleCount"), unit: "辆" },
      ],
    })
    return
  }
  emit("module-stats", {
    moduleKey: "yak-management",
    items: [
      { zh: "无人机识别总数", value: recognitionTotal, unit: "头" },
      { zh: "专家共识模型修正", value: stockNetAdjustment.value, unit: "头" },
      { zh: "当前存栏预估数量", value: estimatedStock, unit: "头" },
    ],
  })
}

function buildHorizontalBarOption(rows, valueKey, unit, options = {}) {
  const labelKey = options.labelKey || "name"
  const colors = options.colors || palette
  const maxValue = Math.max(...rows.map((item) => Number(item[valueKey] || 0)), 1)
  const isDense = rows.length > 8
  const isLadder = options.style === "ladder"
  const barWidth = isDense ? (isLadder ? 7 : 6) : rows.length > 6 ? 11 : 13
  const rankColors = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"]
  const axisTextColor = options.axisTextColor || "rgba(223, 250, 255, 0.82)"
  const dimAxisTextColor = options.dimAxisTextColor || "rgba(223, 250, 255, 0.56)"
  return {
    tooltip: {
      ...chartTooltip(),
      trigger: "axis",
      axisPointer: { type: "shadow" },
      formatter(params) {
        if (options.tooltipFormatter) return options.tooltipFormatter(params[0])
        const row = rows[params[0].dataIndex]
        return `${row[labelKey]}<br/>数值：${formatNumber(row[valueKey])}${unit}`
      },
    },
    grid: { left: isDense ? 58 : 74, right: isDense ? 50 : 54, top: isDense ? 3 : 14, bottom: isDense ? 5 : 14 },
    xAxis: {
      type: "value",
      axisLabel: { color: dimAxisTextColor, fontSize: 10, formatter: unit === "%" ? "{value}%" : formatCompact },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
    yAxis: {
      type: "category",
      inverse: true,
      data: rows.map((item) => item[labelKey]),
      axisLabel: { color: axisTextColor, fontSize: isDense ? 9 : 11, interval: 0 },
      axisLine: { show: false },
      axisTick: { show: false },
    },
    series: [
      {
        type: "bar",
        barWidth,
        barGap: "-100%",
        data: rows.map((item, index) => ({
          value: item[valueKey],
          itemStyle: {
            borderRadius: isLadder ? [1, 10, 10, 1] : [0, 7, 7, 0],
            color: buildBarGradient(isLadder ? rankColors[index % rankColors.length] : colors[index % colors.length], isLadder),
            shadowBlur: isLadder ? 10 : 0,
            shadowColor: isLadder ? colors[index % colors.length] : "transparent",
          },
        })),
        showBackground: true,
        backgroundStyle: {
          color: isLadder ? "rgba(48, 220, 255, 0.035)" : "rgba(48, 220, 255, 0.055)",
          borderRadius: isLadder ? [1, 10, 10, 1] : [0, 7, 7, 0],
        },
        label: {
          show: true,
          position: "right",
          color: "#ffffff",
          fontSize: isDense ? 9 : 10,
          formatter: ({ value }) => `${unit === "%" ? formatPercent(value) : formatCompact(value)}${unit === "%" ? "" : ""}`,
        },
        markLine: options.showAverage === false
          ? undefined
          : {
              silent: true,
              symbol: "none",
              label: { show: false },
              lineStyle: { color: "rgba(255, 215, 108, 0.22)", type: "dashed", width: 1 },
              data: [{ xAxis: Math.round(maxValue * 0.72) }],
            },
      },
      ...(isLadder
        ? [
            {
              type: "pictorialBar",
              symbol: "rect",
              symbolSize: [2, barWidth + 5],
              symbolOffset: [0, 0],
              symbolPosition: "end",
              z: 4,
              data: rows.map((item, index) => ({
                value: item[valueKey],
                itemStyle: {
                  color: "#ffffff",
                  opacity: index < 3 ? 0.82 : 0.5,
                  shadowBlur: 8,
                  shadowColor: rankColors[index % rankColors.length],
                },
              })),
            },
          ]
        : []),
    ],
  }
}

function buildBarGradient(color, bright = false) {
  return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
    { offset: 0, color: bright ? `${color}22` : "rgba(48, 220, 255, 0.12)" },
    { offset: 0.64, color: bright ? color : `${color}cc` },
    { offset: 1, color },
  ])
}

function buildLineSeries(name, data, color, yAxisIndex = 0, area = false) {
  return {
    name,
    type: "line",
    yAxisIndex,
    smooth: true,
    symbolSize: 5,
    data,
    lineStyle: { width: 2.5, color },
    itemStyle: { color },
    areaStyle: area
      ? {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: `${color}44` },
            { offset: 1, color: `${color}05` },
          ]),
        }
      : undefined,
  }
}

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

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

  const dayCount = Math.max(1, Math.round(Number(period.months || 0) * 30))
  const stepDays = granularityKey === "week" ? 7 : 1
  const count = Math.ceil(dayCount / stepDays) + 1
  const labels = Array.from({ length: count }, (_, index) => {
    const day = Math.min(dayCount, index * stepDays)
    if (index === 0) return "2026-06"
    if (day >= dayCount) return period.label
    return granularityKey === "week" ? `第${index}周` : `第${day}日`
  })
  const stock = labels.map((_, index) => {
    const day = Math.min(dayCount, index * stepDays)
    const ratio = day / dayCount
    const seasonal = Math.sin(index / (granularityKey === "week" ? 1.8 : 8.5)) * Math.sin(Math.PI * ratio) * Math.min(360, period.months * 18)
    return Math.round(estimatedStock + (period.stock - estimatedStock) * ratio + seasonal)
  })
  const births = labels.map((_, index) => {
    const ratio = Math.min(dayCount, index * stepDays) / dayCount
    return Math.round(period.births * ratio)
  })
  const outflow = labels.map((_, index) => {
    const ratio = Math.min(dayCount, index * stepDays) / dayCount
    return Math.round(period.outflow * ratio)
  })
  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 buildCenterMapRows(rows, valueKey) {
  const rowMap = new Map()
  rows.forEach((row) => {
    const name = toSystemMapTownName(row.name)
    if (!name) return
    const current = rowMap.get(name) || { name, value: 0 }
    current.value += Number(row[valueKey] || 0)
    rowMap.set(name, current)
  })
  return [...rowMap.values()].sort((a, b) => b.value - a.value)
}

function toSystemMapTownName(name) {
  const aliases = {
    色地镇: "色地镇",
    瓦切镇: "瓦切镇",
    安曲镇: "安曲镇",
    龙日镇: "龙日镇",
    邛溪镇: "邛溪镇",
    刷经寺镇: "刷经寺镇",
    阿木乡: "阿木乡",
    查尔玛乡: "查尔玛乡",
    江茸乡: "江茸乡",
    麦洼乡: "麦洼乡",
  }
  return aliases[name] || ""
}

function chartTooltip(formatter) {
  return {
    trigger: "item",
    backgroundColor: "rgba(5, 31, 51, 0.96)",
    borderColor: "rgba(0, 210, 255, 0.54)",
    borderWidth: 1,
    textStyle: { color: "#e7fbff", fontSize: 12 },
    extraCssText: "box-shadow:0 0 18px rgba(0,210,255,.2);",
    formatter,
  }
}

function normalizeStructureApiRows(rows = []) {
  return rows
    .map((item, index) => {
      const name = String(item.name || item.label || item.type || `结构${index + 1}`).trim()
      const count = toFiniteCount(item.value ?? item.count ?? item.number)
      return {
        key: item.key || item.id || `${name}-${index}`,
        name,
        count,
        ratio: Number(item.ratio ?? item.percent ?? 0),
        source: item.source || "结构统计",
      }
    })
    .filter((item) => item.name && item.count > 0)
}

function normalizeStructureChartRows(apiRows = [], fallbackRows = []) {
  const sourceRows = apiRows.length ? apiRows : fallbackRows
  const rows = sourceRows
    .map((item, index) => {
      const name = String(item.name || item.label || `结构${index + 1}`).trim()
      const count = toFiniteCount(item.count ?? item.value ?? item.number)
      return {
        ...item,
        key: item.key || `${name}-${index}`,
        name,
        count,
      }
    })
    .filter((item) => item.name && item.count > 0)
  const total = rows.reduce((sum, item) => sum + item.count, 0)
  return rows.map((item) => ({
    ...item,
    ratio: total > 0 ? Number(((item.count / total) * 100).toFixed(1)) : Number(item.ratio || 0),
  }))
}

function toFiniteCount(value) {
  const number = Number(String(value ?? "").replace(/,/g, "").replace(/%/g, ""))
  return Number.isFinite(number) && number > 0 ? Math.round(number) : 0
}

function formatLoadError(error, fallback) {
  const message = String(error?.message || error || "")
  if (/未登录|401|HTTP_401/.test(message)) return "结构数据未就绪"
  if (/超时|timeout/i.test(message)) return "结构数据加载超时"
  return fallback
}

function parseExtraNumber(value) {
  const match = String(value || "").match(/-?\d+(?:\.\d+)?/)
  return match ? Number(match[0]) : 0
}

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

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 formatWan(value, digits = 2) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return `${(number / 10000).toFixed(digits)}万`
}

function formatPercent(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return `${Number.isInteger(number) ? number : number.toFixed(1)}%`
}

function polarPoint(cx, cy, radius, angle) {
  const radian = (angle * Math.PI) / 180
  return {
    x: Number((cx + radius * Math.cos(radian)).toFixed(2)),
    y: Number((cy + radius * Math.sin(radian)).toFixed(2)),
  }
}

function describeRoseSegment(cx, cy, innerRadius, outerRadius, startAngle, endAngle) {
  const outerStart = polarPoint(cx, cy, outerRadius, startAngle)
  const outerEnd = polarPoint(cx, cy, outerRadius, endAngle)
  const innerEnd = polarPoint(cx, cy, innerRadius, endAngle)
  const innerStart = polarPoint(cx, cy, innerRadius, startAngle)
  const largeArc = endAngle - startAngle > 180 ? 1 : 0
  return [
    `M ${outerStart.x} ${outerStart.y}`,
    `A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${outerEnd.x} ${outerEnd.y}`,
    `L ${innerEnd.x} ${innerEnd.y}`,
    `A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${innerStart.x} ${innerStart.y}`,
    "Z",
  ].join(" ")
}

function buildStructureSummaryRows(rows = []) {
  const total = rows.reduce((sum, item) => sum + Number(item.count || 0), 0) || 1
  const groups = [
    {
      key: "female",
      group: "性别",
      name: "母牛合计",
      color: "#27f0c0",
      matcher: (name) => name.includes("母"),
    },
    {
      key: "male",
      group: "性别",
      name: "公牛合计",
      color: "#30dcff",
      matcher: (name) => name.includes("公"),
    },
    {
      key: "young",
      group: "年龄",
      name: "1岁以下",
      color: "#ffd76c",
      matcher: (name) => /0-1|≤1|幼/.test(name),
    },
    {
      key: "adult",
      group: "年龄",
      name: "3岁以上",
      color: "#ff8f6b",
      matcher: (name) => /3-4|4-5|5\+|成年/.test(name),
    },
  ]

  return groups.map((group) => {
    const count = rows
      .filter((item) => group.matcher(String(item.name || "")))
      .reduce((sum, item) => sum + Number(item.count || 0), 0)
    return {
      key: group.key,
      group: group.group,
      name: group.name,
      color: group.color,
      count,
      percent: (count / total) * 100,
    }
  })
}

function buildStructureCompositionRows(rows = [], fallbackRows = []) {
  const hasCoreRows = rows.some((item) => isCoreStructureCategory(item))
  const sourceRows = hasCoreRows ? rows : fallbackRows
  const total = sourceRows.reduce((sum, item) => sum + Number(item.count || 0), 0) || estimatedStock || 1
  const definitions = [
    { key: "adultCow", name: "成年母牛", color: "#27f0c0", matcher: (key, name) => key === "adultCow" || name === "成年母牛" },
    { key: "otherAdult", name: "其他成年牛", color: "#7aa7ff", matcher: (key, name) => key === "otherAdult" || name.includes("其他成年") },
    { key: "calf", name: "幼畜", color: "#30dcff", matcher: (key, name) => key === "calf" || name.includes("幼畜") },
    { key: "yearling", name: "育成牛", color: "#ffd76c", matcher: (key, name) => key === "yearling" || name.includes("育成牛") },
    { key: "bull", name: "公牛", color: "#a6f06d", matcher: (key, name) => key === "bull" || name === "公牛" },
  ]

  return definitions.map((definition) => {
    const matchedRows = sourceRows.filter((item) => definition.matcher(String(item.key || ""), String(item.name || "")))
    const count = matchedRows.reduce((sum, item) => sum + Number(item.count || 0), 0)
    const ratio = matchedRows.length === 1 ? Number(matchedRows[0].ratio) : Number.NaN
    return {
      key: definition.key,
      name: definition.name,
      color: definition.color,
      count,
      percent: Number.isFinite(ratio) && ratio > 0 ? ratio : (count / total) * 100,
    }
  }).filter((item) => item.count > 0 || item.percent > 0)
}

function isCoreStructureCategory(item = {}) {
  const key = String(item.key || "")
  const name = String(item.name || "")
  return ["adultCow", "otherAdult", "calf", "yearling", "bull"].includes(key)
    || name === "成年母牛"
    || name.includes("其他成年")
    || name.includes("幼畜")
    || name.includes("育成牛")
    || name === "公牛"
}

function buildBreedingHealthRows(rows = [], fallbackRows = []) {
  const hasCoreRows = rows.some((item) => isCoreBreedingCategory(item))
  const sourceRows = hasCoreRows ? rows : fallbackRows
  const total = sourceRows.reduce((sum, item) => sum + Number(item.count || 0), 0) || 1
  const buildMetric = (key, name, color, matcher, desc) => {
    const matchedRows = sourceRows.filter((item) => matcher(String(item.key || ""), String(item.name || "")))
    const count = matchedRows
      .reduce((sum, item) => sum + Number(item.count || 0), 0)
    const directRatio = matchedRows.length === 1 ? Number(matchedRows[0].ratio) : Number.NaN
    return {
      key,
      name,
      color,
      count,
      percent: Number.isFinite(directRatio) && directRatio > 0 ? directRatio : (count / total) * 100,
      desc,
    }
  }

  return [
    buildMetric(
      "adult-cow",
      "成年母牛",
      "#27f0c0",
      (key, name) => key === "adultCow" || name === "成年母牛",
      "繁殖基础",
    ),
    buildMetric(
      "calf",
      "幼畜",
      "#ffd76c",
      (key, name) => key === "calf" || name.includes("幼畜"),
      "新生补充",
    ),
    buildMetric(
      "yearling",
      "育成牛",
      "#30dcff",
      (key, name) => key === "yearling" || name.includes("育成牛"),
      "后备梯队",
    ),
    buildMetric(
      "bull",
      "公牛配比",
      "#7aa7ff",
      (key, name) => key === "bull" || name === "公牛",
      "配种支撑",
    ),
  ]
}

function isCoreBreedingCategory(item = {}) {
  const key = String(item.key || "")
  const name = String(item.name || "")
  return ["adultCow", "calf", "yearling", "bull"].includes(key)
    || name === "成年母牛"
    || name.includes("幼畜")
    || name.includes("育成牛")
    || name === "公牛"
}

function buildBreedingHealthStatus(rows = []) {
  const valueOf = (key) => Number(rows.find((item) => item.key === key)?.percent || 0)
  const adultCow = valueOf("adult-cow")
  const calf = valueOf("calf")
  const yearling = valueOf("yearling")
  const bull = valueOf("bull")
  const reserve = calf + yearling

  if (adultCow >= 32 && reserve >= 18 && bull >= 3 && bull <= 12) {
    return {
      label: "结构健康",
      title: "繁殖基础稳定",
      summary: "母牛基础足，后备衔接稳",
      note: "保持繁殖群与后备梯队，持续观察季节波动。",
    }
  }

  if (reserve < 18) {
    return {
      label: "结构健康",
      title: "后备梯队稳中补强",
      summary: "基础稳定，后备可继续优化",
      note: "关注幼畜成活与育成牛补充，保持生产连续性。",
    }
  }

  if (bull < 3) {
    return {
      label: "结构健康",
      title: "繁殖基础总体稳定",
      summary: "母牛与后备梯队较稳",
      note: "结合实际配种需求，平稳优化公牛配置。",
    }
  }

  return {
    label: "结构健康",
    title: "畜群梯队较均衡",
    summary: "繁殖群和后备牛连续支撑",
    note: "延续结构管理，优先保障母牛与后备牛质量。",
  }
}

function structureSummaryBarWidth(item) {
  const value = Number(item?.count || 0)
  return Math.max(8, Math.min(100, (value / structureSummaryMaxCount.value) * 100))
}

function buildForecastWaterfallRows(period = {}) {
  const startStock = estimatedStock
  const deltas = [
    { key: "births", name: "繁殖补充", delta: Number(period.births || 0), color: "#27f0c0" },
    { key: "outflow", name: "出栏流出", delta: -Number(period.outflow || 0), color: "#ff8f6b" },
    { key: "correction", name: "修正损耗", delta: Number(period.correction || 0), color: "#ffd76c" },
  ]
  let cursor = startStock
  const rows = [
    {
      key: "current",
      name: "当前存栏",
      type: "anchor",
      value: startStock,
      magnitude: startStock,
      from: startStock,
      to: startStock,
      color: "#30dcff",
      display: `${formatWan(startStock, 2)}头`,
    },
  ]

  deltas.forEach((item) => {
    const from = cursor
    const to = cursor + item.delta
    cursor = to
    rows.push({
      ...item,
      name: `${item.delta >= 0 ? "+" : "-"}${item.name}`,
      type: "delta",
      from,
      to,
      value: item.delta,
      magnitude: Math.abs(item.delta),
      display: `${formatSignedWan(item.delta)}头`,
    })
  })

  const finalStock = Number(period.stock || cursor)
  rows.push({
    key: "final",
    name: "=期末预测",
    type: "final",
    value: finalStock,
    magnitude: finalStock,
    from: finalStock,
    to: finalStock,
    color: "#7aa7ff",
    display: `${formatWan(finalStock, 2)}头`,
  })

  const maxMagnitude = Math.max(...rows.map((item) => Number(item.magnitude) || 0), 1)
  return rows.map((item) => {
    const width = ((Number(item.magnitude) || 0) / maxMagnitude) * 100
    return {
      ...item,
      left: 0,
      width: Math.max(item.type === "delta" ? 5 : 10, Math.min(100, width)),
    }
  })
}

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

function formatSignedWan(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return `${number >= 0 ? "+" : "-"}${formatWan(Math.abs(number))}`
}
</script>

<style lang="scss">
.yak-management {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  color: #fff;
  background: #03121c;
  height: 100%;
  overscroll-behavior: none;
}

.yak-map-scene {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  background: transparent !important;

  > canvas {
    display: block;
    width: 100% !important;
    height: 100% !important;
  }

  > div {
    width: 100% !important;
    height: 100% !important;
  }

  .china-label .town-stat-label {
    transform: translateY(-50%) scale(0.92);
  }
}

.yak-ol-map-layer {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.yak-management-ui {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

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

  &.is-ol-map {
    background:
      radial-gradient(circle at 50% 52%, transparent 0%, transparent 58%, rgba(1, 9, 15, 0.16) 82%, rgba(1, 8, 13, 0.42) 100%),
      linear-gradient(90deg, rgba(0, 12, 20, 0.24), transparent 22%, transparent 78%, rgba(0, 12, 20, 0.24)),
      linear-gradient(180deg, rgba(2, 16, 27, 0.01), rgba(2, 13, 22, 0.12));
  }
}

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

.yak-left {
  left: 32px;
}

.yak-right {
  right: 32px;
}

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

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

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

.yak-side-card {
  flex: 0 0 auto;

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

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

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

.trade-source-card {
  .m-card-bd-content {
    top: 38px;
    bottom: 8px;
  }
}

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

.yak-chart {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}

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

.structure-chart-card {
  position: relative;
  width: 100%;
  height: 100%;
}

.breeding-health-card {
  display: grid;
  grid-template-rows: 34px minmax(0, 1fr);
  gap: 9px;
  height: 100%;
  padding: 10px 13px 10px;
  overflow: hidden;
  box-sizing: border-box;
}

.breeding-health-head {
  display: flex;
  align-items: center;
  min-width: 0;
  padding: 0 12px;
  border-left: 3px solid rgba(39, 240, 192, 0.82);
  background:
    radial-gradient(circle at 18% 50%, rgba(39, 240, 192, 0.18), transparent 48%),
    linear-gradient(90deg, rgba(39, 240, 192, 0.14), rgba(48, 220, 255, 0.04));
  box-sizing: border-box;

  strong {
    display: block;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #ffffff;
    font-size: 16px;
    line-height: 20px;
    font-weight: 900;
    text-shadow: 0 0 12px rgba(39, 240, 192, 0.22);
  }
}

.breeding-health-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: 9px;
  min-height: 0;
}

.breeding-health-item {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: 18px 14px 6px;
  align-content: center;
  gap: 2px 6px;
  min-width: 0;
  min-height: 0;
  padding: 7px 9px;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.13);
  background:
    linear-gradient(90deg, color-mix(in srgb, var(--health-color) 16%, transparent), rgba(5, 27, 41, 0.32)),
    rgba(3, 25, 40, 0.18);
  box-sizing: border-box;

  span,
  strong,
  em {
    position: relative;
    z-index: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span {
    color: rgba(229, 253, 255, 0.84);
    font-size: 12px;
    line-height: 15px;
    font-weight: 800;
  }

  strong {
    color: color-mix(in srgb, var(--health-color) 82%, #ffffff);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 18px;
    line-height: 18px;
    font-weight: 900;
    text-align: right;
  }

  i {
    grid-column: 1 / 3;
    position: relative;
    z-index: 1;
    display: block;
    height: 4px;
    overflow: hidden;
    background: rgba(48, 220, 255, 0.12);

    b {
      display: block;
      width: var(--health-width);
      height: 100%;
      background: var(--health-color);
      box-shadow: 0 0 10px color-mix(in srgb, var(--health-color) 70%, transparent);
    }
  }

  em {
    grid-column: 1 / 3;
    grid-row: 2;
    color: color-mix(in srgb, var(--health-color) 74%, #ffffff);
    font-size: 11px;
    line-height: 13px;
    font-style: normal;
  }
}

.structure-ratio-card {
  display: grid;
  grid-template-columns: 176px minmax(0, 1fr);
  align-items: center;
  gap: 4px;
  height: 100%;
  overflow: hidden;
  padding: 4px 12px 7px 8px;
  box-sizing: border-box;
}

.structure-ratio-chart {
  position: relative;
  width: 172px;
  height: 142px;
  min-width: 0;

  svg {
    width: 100%;
    height: 100%;
    overflow: visible;
  }
}

.structure-ratio-list {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  height: 100%;
  gap: 8px;
  overflow: hidden;
}

.structure-ratio-row {
  position: relative;
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) 62px 24px;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 28px;
  padding: 0 2px;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.12);
  background: rgba(48, 220, 255, 0.055);
  color: rgba(225, 249, 255, 0.76);
  font-size: 10px;
  line-height: 13px;
  box-sizing: border-box;
  transition: background 0.35s ease, border-color 0.35s ease;

  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: var(--row-percent);
    background: linear-gradient(90deg, var(--row-color), rgba(48, 220, 255, 0));
    opacity: 0.18;
    pointer-events: none;
  }

  i {
    position: relative;
    z-index: 1;
    width: 12px;
    height: 12px;
    border: 1px solid #24f6c4;
    border-radius: 50%;
    box-sizing: border-box;
    box-shadow: 0 0 10px currentColor;
  }

  span {
    position: relative;
    z-index: 1;
    min-width: 0;
    overflow: hidden;
    color: rgba(255, 255, 255, 0.9);
    font-size: 16px;
    line-height: 18px;
    font-weight: 800;
    text-overflow: ellipsis;
    white-space: normal;
  }

  strong {
    position: relative;
    z-index: 1;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 21px;
    font-weight: 800;
    line-height: 23px;
    text-align: right;
    white-space: nowrap;
  }

  em {
    position: relative;
    z-index: 1;
    color: rgba(255, 255, 255, 0.54);
    font-size: 12px;
    font-style: normal;
    white-space: nowrap;
  }

  &.is-active {
    color: #ffffff;
    border-color: rgba(48, 220, 255, 0.22);
    background: linear-gradient(90deg, rgba(48, 220, 255, 0.18), rgba(48, 220, 255, 0.04));
  }
}

.structure-rose-segment {
  opacity: 0.92;
  stroke-width: 1.5;
  stroke-opacity: 0.5;
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 0.3s ease, filter 0.3s ease, transform 0.3s ease;
  filter:
    drop-shadow(0 2px 2px rgba(0, 10, 18, 0.34))
    drop-shadow(0 0 5px rgba(48, 220, 255, 0.18));

  &.is-active {
    opacity: 1;
    transform: scale(1.035);
    filter:
      drop-shadow(0 3px 5px rgba(0, 10, 18, 0.42))
      drop-shadow(0 0 10px rgba(36, 246, 196, 0.42));
  }
}

.structure-rose-guides line {
  stroke-width: 1;
  stroke-opacity: 0.5;
}

.structure-rose-labels text {
  fill: rgba(226, 252, 255, 0.68);
  font-size: 8px;
  font-weight: 600;
  dominant-baseline: middle;
}

.structure-rose-glow {
  fill: rgba(36, 246, 196, 0.1);
  stroke: rgba(48, 220, 255, 0.24);
  stroke-width: 1;
  filter: drop-shadow(0 0 8px rgba(36, 246, 196, 0.26));
}

.structure-rose-hole {
  fill: rgba(3, 18, 30, 0.94);
  stroke: rgba(227, 253, 255, 0.22);
  stroke-width: 1.2;
}

.structure-summary-card {
  display: grid;
  grid-template-rows: repeat(4, minmax(0, 1fr));
  align-content: center;
  gap: 10px;
  height: 100%;
  overflow: hidden;
  padding: 15px 18px 14px 18px;
  box-sizing: border-box;
}

.structure-summary-row {
  position: relative;
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr) 58px;
  grid-template-rows: 22px 14px;
  align-items: center;
  column-gap: 10px;
  min-width: 0;
  min-height: 0;
  padding: 2px 0;
  color: rgba(225, 249, 255, 0.72);

  span {
    grid-row: 1 / 3;
    display: inline-flex;
    min-width: 0;
    align-items: center;
    gap: 8px;
    overflow: hidden;
    color: rgba(242, 254, 255, 0.88);
    font-size: 13px;
    line-height: 17px;
    font-weight: 800;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  i {
    width: 9px;
    height: 9px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--structure-color);
    box-shadow: 0 0 10px var(--structure-color);
  }

  strong {
    grid-column: 3;
    grid-row: 1;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 14px;
    line-height: 15px;
    font-weight: 800;
    text-align: right;
    white-space: nowrap;

    em {
      margin-left: 3px;
      color: rgba(226, 252, 255, 0.68);
      font-family: "PingFang SC", Arial, sans-serif;
      font-size: 9px;
      font-style: normal;
      font-weight: 700;
    }
  }

  b {
    position: relative;
    grid-column: 2;
    grid-row: 1 / 3;
    height: 17px;
    overflow: hidden;
    border: 1px solid rgba(48, 220, 255, 0.08);
    background:
      repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.035) 0 1px, transparent 1px 18px),
      rgba(48, 220, 255, 0.05);
  }

  b em {
    display: block;
    width: var(--structure-width);
    height: 100%;
    background: linear-gradient(90deg, color-mix(in srgb, var(--structure-color) 20%, transparent), var(--structure-color));
    box-shadow: 0 0 12px var(--structure-color);
  }

  small {
    grid-column: 3;
    grid-row: 2;
    color: rgba(226, 252, 255, 0.78);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 10px;
    line-height: 11px;
    font-weight: 800;
    text-align: right;
    white-space: nowrap;
  }
}

.yak-data-status {
  position: absolute;
  left: 12px;
  bottom: 6px;
  z-index: 2;
  max-width: calc(100% - 24px);
  padding: 2px 8px;
  border: 1px solid rgba(48, 220, 255, 0.24);
  border-radius: 3px;
  color: rgba(222, 250, 255, 0.74);
  font-size: 10px;
  line-height: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: rgba(4, 24, 37, 0.72);
  box-shadow: inset 0 0 10px rgba(48, 220, 255, 0.08);

  &.is-warning {
    border-color: rgba(255, 215, 108, 0.28);
    color: rgba(255, 229, 146, 0.86);
    background: rgba(48, 34, 12, 0.58);
  }
}

.forecast-kline-chart {
  padding-top: 16px;
}

.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);
    }
  }
}

.yak-map-loading {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 8;
  color: rgba(223, 250, 255, 0.64);
  font-size: 16px;
  transform: translate(-50%, -50%);
}

.yak-bottom-topic {
  position: absolute;
  left: 50%;
  bottom: 48px;
  z-index: 8;
  transform: translateX(-50%);
  pointer-events: all;
}

.trade-year-switcher,
.forecast-year-switcher,
.epidemic-year-switcher {
  position: absolute;
  left: 462px;
  top: 218px;
  z-index: 8;
  display: flex;
  width: 78px;
  flex-direction: column;
  gap: 8px;
  padding: 10px 8px;
  border: 1px solid rgba(255, 215, 108, 0.28);
  background:
    linear-gradient(180deg, rgba(9, 42, 55, 0.72), rgba(4, 18, 30, 0.82)),
    rgba(3, 18, 31, 0.62);
  box-shadow:
    0 0 18px rgba(255, 215, 108, 0.12),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05);
  pointer-events: all;
  transform: perspective(620px) rotateY(6deg);
  transform-origin: left center;
  box-sizing: border-box;

  span {
    color: rgba(255, 255, 255, 0.86);
    font-size: 12px;
    font-weight: 700;
    line-height: 14px;
    text-align: center;
    white-space: nowrap;
  }

  button {
    width: 100%;
    height: 30px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(48, 220, 255, 0.08);
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 14px;
    font-weight: 900;
    cursor: pointer;
    box-sizing: border-box;

    &.active,
    &:hover {
      border-color: rgba(255, 215, 108, 0.7);
      background: rgba(255, 215, 108, 0.18);
      color: #ffd76c;
      box-shadow: 0 0 14px rgba(255, 215, 108, 0.12);
    }
  }
}

.forecast-year-switcher {
  border-color: rgba(48, 220, 255, 0.28);
  box-shadow:
    0 0 18px rgba(48, 220, 255, 0.12),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05);

  button {
    &.active,
    &:hover {
      border-color: rgba(48, 220, 255, 0.7);
      background: rgba(48, 220, 255, 0.16);
      color: #30dcff;
      box-shadow: 0 0 14px rgba(48, 220, 255, 0.14);
    }
  }
}

.epidemic-year-switcher {
  border-color: rgba(39, 240, 192, 0.28);
  box-shadow:
    0 0 18px rgba(39, 240, 192, 0.12),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05);

  button {
    &.active,
    &:hover {
      border-color: rgba(39, 240, 192, 0.72);
      background: rgba(39, 240, 192, 0.16);
      color: #27f0c0;
      box-shadow: 0 0 14px rgba(39, 240, 192, 0.14);
    }
  }
}

.forecast-waterfall-card {
  display: grid;
  grid-template-rows: 32px minmax(0, 1fr);
  gap: 8px;
  height: 100%;
  padding: 9px 13px 10px;
  overflow: hidden;
  box-sizing: border-box;
}

.forecast-waterfall-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-width: 0;
  padding: 0 10px;
  border-left: 3px solid rgba(48, 220, 255, 0.72);
  background:
    linear-gradient(90deg, rgba(48, 220, 255, 0.13), rgba(48, 220, 255, 0.025)),
    rgba(3, 25, 40, 0.18);
  box-sizing: border-box;

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

  span {
    color: rgba(226, 252, 255, 0.78);
    font-size: 12px;
    line-height: 16px;
    font-weight: 800;
  }

  strong {
    color: #ffd76c;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 15px;
    line-height: 18px;
    font-weight: 900;
    text-align: right;

    &.negative {
      color: #ff8f6b;
    }

    em {
      margin-left: 2px;
      color: rgba(226, 252, 255, 0.6);
      font-family: "PingFang SC", Arial, sans-serif;
      font-size: 10px;
      font-style: normal;
      font-weight: 700;
    }
  }
}

.forecast-waterfall-list {
  display: grid;
  grid-template-rows: repeat(5, minmax(0, 1fr));
  gap: 5px;
  min-height: 0;
}

.forecast-waterfall-row {
  display: grid;
  grid-template-columns: 78px minmax(0, 1fr) 76px;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 0;

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

  span {
    color: rgba(226, 252, 255, 0.78);
    font-size: 12px;
    line-height: 14px;
    font-weight: 800;
  }

  strong {
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 13px;
    line-height: 15px;
    font-weight: 900;
    text-align: right;
  }

  i {
    position: relative;
    display: block;
    height: 18px;
    overflow: hidden;
    border: 1px solid rgba(48, 220, 255, 0.08);
    background:
      repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 18px),
      rgba(48, 220, 255, 0.045);
    box-sizing: border-box;

    &::before {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      top: 50%;
      height: 1px;
      background: rgba(226, 252, 255, 0.12);
      transform: translateY(-50%);
    }

    b {
      position: absolute;
      left: var(--waterfall-left);
      top: 3px;
      width: var(--waterfall-width);
      height: 10px;
      background: linear-gradient(90deg, color-mix(in srgb, var(--waterfall-color) 22%, transparent), var(--waterfall-color));
      box-shadow: 0 0 10px color-mix(in srgb, var(--waterfall-color) 72%, transparent);
    }
  }

  &.is-anchor,
  &.is-final {
    span {
      color: rgba(255, 255, 255, 0.9);
    }

    i b {
      top: 3px;
      height: 10px;
      border-radius: 8px;
      background: var(--waterfall-color);
    }
  }

  &.positive strong {
    color: #27f0c0;
  }

  &.negative strong {
    color: #ff8f6b;
  }

  &.is-final strong {
    color: #ffffff;
  }
}

.estimate-summary-card,
.forecast-summary-card {
  height: 100%;
  padding: 6px 14px 8px;
  box-sizing: border-box;
}

.estimate-summary-card {
  display: grid;
  grid-template-rows: minmax(58px, 0.88fr) minmax(86px, 1.12fr);
  row-gap: 10px;
  padding: 7px 14px 10px;
  overflow: hidden;
}

.estimate-flow-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  min-height: 0;
}

.estimate-flow-item {
  min-width: 0;
  height: 100%;
  padding: 8px 10px 7px;
  border-left: 2px solid rgba(48, 220, 255, 0.64);
  background:
    linear-gradient(90deg, rgba(48, 220, 255, 0.13), rgba(48, 220, 255, 0.025)),
    rgba(3, 25, 40, 0.18);
  box-sizing: border-box;

  em,
  strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(223, 250, 255, 0.6);
    font-style: normal;
    font-size: 11px;
    line-height: 14px;
  }

  strong {
    margin-top: 5px;
    color: #30dcff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 20px;
    font-weight: 900;
    line-height: 24px;
  }

  small {
    margin-left: 3px;
    color: rgba(223, 250, 255, 0.58);
    font-size: 10px;
    font-weight: 600;
  }

  &.is-adjustment {
    border-left-color: rgba(255, 215, 108, 0.64);
    background:
      linear-gradient(90deg, rgba(255, 215, 108, 0.12), rgba(48, 220, 255, 0.025)),
      rgba(3, 25, 40, 0.18);

    strong {
      color: #ffd76c;
    }

    strong.negative {
      color: #ff8f6b;
    }
  }
}

.estimate-result-band {
  position: relative;
  min-width: 0;
  display: flex;
  height: 100%;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
  padding: 10px 14px 9px;
  border: 1px solid rgba(48, 220, 255, 0.18);
  border-left: 3px solid rgba(48, 220, 255, 0.76);
  background:
    radial-gradient(circle at 18% 50%, rgba(48, 220, 255, 0.16), transparent 46%),
    linear-gradient(90deg, rgba(48, 220, 255, 0.18), rgba(48, 220, 255, 0.04)),
    rgba(3, 25, 40, 0.22);
  box-sizing: border-box;

  &::after {
    content: "";
    position: absolute;
    right: -12px;
    bottom: -18px;
    width: 118px;
    height: 76px;
    pointer-events: none;
    background:
      radial-gradient(circle at center, rgba(48, 220, 255, 0.16), transparent 58%),
      linear-gradient(135deg, transparent 0 42%, rgba(48, 220, 255, 0.12) 43% 45%, transparent 46% 100%);
  }

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

  span {
    color: rgba(223, 250, 255, 0.72);
    font-size: 13px;
    line-height: 16px;
  }

  strong {
    position: relative;
    z-index: 1;
    margin-top: 0;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 38px;
    font-weight: 900;
    line-height: 42px;
  }

  em {
    margin-left: 5px;
    color: rgba(223, 250, 255, 0.62);
    font-style: normal;
    font-size: 12px;
  }
}

.forecast-simple-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  margin-top: 9px;

  span {
    min-width: 0;
    height: 38px;
    padding: 5px 7px;
    border-left: 2px solid rgba(48, 220, 255, 0.58);
    background: linear-gradient(90deg, rgba(48, 220, 255, 0.1), rgba(48, 220, 255, 0.02));
    box-sizing: border-box;
  }

  em,
  strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(223, 250, 255, 0.56);
    font-style: normal;
    font-size: 10px;
    line-height: 11px;
  }

  strong {
    margin-top: 3px;
    color: #30dcff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 13px;
    font-weight: 900;
    line-height: 15px;
  }

  .positive {
    color: #ffd76c;
  }

  .negative {
    color: #ff8f6b;
  }
}

.forecast-summary-card {
  display: grid;
  grid-template-rows: minmax(84px, 1fr) minmax(44px, 0.55fr);
  row-gap: 9px;
  padding: 8px 14px 10px;
  overflow: hidden;
}

.forecast-result-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 98px;
  align-items: center;
  column-gap: 8px;
  height: 100%;
  min-height: 0;
}

.forecast-result-main {
  min-width: 0;
  display: flex;
  height: 100%;
  flex-direction: column;
  justify-content: space-between;
  padding: 10px 12px;
  border-left: 2px solid rgba(48, 220, 255, 0.7);
  background:
    radial-gradient(circle at 22% 50%, rgba(48, 220, 255, 0.16), transparent 48%),
    linear-gradient(90deg, rgba(48, 220, 255, 0.16), rgba(48, 220, 255, 0.035)),
    rgba(3, 25, 40, 0.18);
  box-sizing: border-box;

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

  span {
    color: rgba(223, 250, 255, 0.66);
    font-size: 13px;
    line-height: 16px;
  }

  strong {
    margin-top: 0;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 35px;
    font-weight: 900;
    line-height: 39px;

    em {
      margin-left: 4px;
      color: rgba(223, 250, 255, 0.62);
      font-style: normal;
      font-size: 11px;
    }
  }
}

.forecast-change-badge {
  min-width: 0;
  height: 66px;
  padding: 11px 9px 8px;
  border: 1px solid rgba(255, 215, 108, 0.28);
  background: rgba(255, 215, 108, 0.06);
  box-sizing: border-box;

  span,
  strong {
    display: block;
    overflow: hidden;
    text-align: right;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span {
    color: rgba(223, 250, 255, 0.55);
    font-size: 10px;
    line-height: 11px;
  }

  strong {
    margin-top: 2px;
    color: #ffd76c;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 14px;
    font-weight: 900;
    line-height: 16px;

    em {
      margin-left: 2px;
      color: rgba(223, 250, 255, 0.58);
      font-style: normal;
      font-size: 10px;
    }
  }

  &.negative {
    border-color: rgba(255, 143, 107, 0.28);
    background: rgba(255, 143, 107, 0.06);

    strong {
      color: #ff8f6b;
    }
  }
}

.forecast-simple-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 7px;
  margin-top: 0;

  span {
    height: 100%;
    min-height: 42px;
    padding: 6px 8px;
  }

  strong {
    margin-top: 3px;
    font-size: 15px;
    line-height: 17px;
  }
}

.forecast-heat-list {
  display: flex;
  height: 100%;
  flex-direction: column;
  gap: 4px;
  padding: 8px 13px 9px;
  box-sizing: border-box;
}

.heat-row {
  position: relative;
  display: grid;
  grid-template-columns: 70px minmax(0, 1fr) 60px;
  align-items: center;
  gap: 8px;
  min-height: 0;
  flex: 1 1 0;
  padding: 0 8px;
  overflow: visible;
  border: 1px solid rgba(48, 220, 255, 0.14);
  background:
    linear-gradient(90deg, rgba(48, 220, 255, 0.08), transparent 34%, rgba(36, 246, 196, 0.04)),
    rgba(2, 18, 30, 0.58);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.018),
    inset 0 0 12px rgba(48, 220, 255, 0.04);

  &::before {
    content: "";
    position: absolute;
    left: 78px;
    right: 68px;
    top: 50%;
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(48, 220, 255, 0.24), transparent);
    transform: translateY(-50%);
  }

  &::after {
    content: "";
    position: absolute;
    left: 0;
    top: 6px;
    bottom: 6px;
    width: 2px;
    background: var(--heat-color);
    box-shadow: 0 0 10px var(--heat-color);
    opacity: 0.78;
  }

  span,
  strong,
  b {
    position: relative;
    z-index: 1;
    white-space: nowrap;
  }

  span {
    color: #effdff;
    font-size: 12px;
    line-height: 14px;
    font-weight: 900;
    overflow: hidden;
    text-overflow: ellipsis;
    text-shadow:
      0 1px 2px rgba(0, 0, 0, 0.72),
      0 0 8px rgba(48, 220, 255, 0.18);
  }

  b {
    display: block;
    height: 11px;
    padding: 2px;
    border: 1px solid rgba(48, 220, 255, 0.22);
    background:
      linear-gradient(90deg, rgba(48, 220, 255, 0.08), rgba(48, 220, 255, 0.025)),
      rgba(0, 14, 24, 0.66);
    box-shadow:
      inset 0 0 10px rgba(0, 0, 0, 0.34),
      0 0 10px rgba(48, 220, 255, 0.08);

    i {
      position: relative;
      display: block;
      width: var(--heat-width);
      height: 100%;
      min-width: 8px;
      background:
        linear-gradient(90deg, rgba(255, 255, 255, 0.86), var(--heat-color) 16%, rgba(48, 220, 255, 0.72) 78%, rgba(255, 255, 255, 0.9)),
        linear-gradient(90deg, var(--heat-color-soft), transparent);
      box-shadow:
        0 0 10px var(--heat-color-soft),
        0 0 18px var(--heat-color-dim);
      overflow: visible;

      &::after {
        content: "";
        position: absolute;
        right: -4px;
        top: 50%;
        width: 7px;
        height: 7px;
        border-radius: 50%;
        background: #ffffff;
        box-shadow:
          0 0 8px #ffffff,
          0 0 14px var(--heat-color);
        transform: translateY(-50%);
      }

      &::before {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.42), transparent);
        opacity: 0.72;
      }
    }
  }

  strong {
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 15px;
    line-height: 17px;
    font-weight: 900;
    text-align: right;
    text-shadow:
      0 1px 2px rgba(0, 0, 0, 0.72),
      0 0 10px var(--heat-color-soft);
  }

  &.is-negative {
    border-color: rgba(255, 143, 107, 0.18);
  }
}

.epidemic-overview-chart-card {
  height: 100%;
  padding: 7px 9px 5px;
  box-sizing: border-box;
}

.epidemic-overview-chart {
  width: 100%;
  height: 100%;
}

.epidemic-chain-chart-card {
  height: 100%;
  padding: 7px 8px 5px;
  box-sizing: border-box;
}

.epidemic-chain-chart {
  width: 100%;
  height: 100%;
}

.epidemic-chain-card {
  height: 100%;
  padding: 9px 13px;
  box-sizing: border-box;
}

.epidemic-chain-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: center;
  column-gap: 8px;

  span {
    min-width: 0;
    height: 48px;
    padding: 6px 6px;
    border: 1px solid rgba(48, 220, 255, 0.16);
    background: rgba(48, 220, 255, 0.05);
    box-sizing: border-box;
  }

  em,
  strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(223, 250, 255, 0.56);
    font-style: normal;
    font-size: 10px;
  }

  strong {
    margin-top: 5px;
    color: #27f0c0;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 15px;
    font-weight: 900;
  }

  i {
    color: rgba(255, 215, 108, 0.8);
    font-style: normal;
    text-align: center;
  }
}

.epidemic-chain-note {
  margin: 8px 0;
  padding: 7px 9px;
  border-left: 2px solid rgba(255, 215, 108, 0.76);
  background: rgba(255, 215, 108, 0.06);

  span {
    display: block;
    color: rgba(234, 252, 255, 0.84);
    font-size: 11px;
    line-height: 17px;
  }
}

.epidemic-chain-metrics {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;

  span {
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 28px;
    padding: 0 8px;
    border: 1px solid rgba(39, 240, 192, 0.14);
    background: rgba(39, 240, 192, 0.04);
  }

  em {
    color: rgba(223, 250, 255, 0.58);
    font-style: normal;
    font-size: 10px;
  }

  strong {
    overflow: hidden;
    color: #ffffff;
    font-size: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.epidemic-three-pie-card {
  display: grid;
  grid-template-columns: 142px minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  height: 100%;
  padding: 8px 12px 8px 10px;
  box-sizing: border-box;
}

.epidemic-three-pie-visual {
  position: relative;
  width: 136px;
  height: 136px;
  min-width: 0;
}

.epidemic-three-pie {
  width: 100%;
  height: 100%;
}

.epidemic-three-pie-center {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  width: 100%;
  height: 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #fff;
  text-align: center;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.96);
  pointer-events: none;

  strong {
    overflow: hidden;
    max-width: 84px;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 21px;
    line-height: 22px;
    font-weight: 900;
    text-overflow: ellipsis;
    white-space: nowrap;
    transform: translateY(-2px);
  }

  span {
    overflow: hidden;
    max-width: 86px;
    color: rgba(255, 255, 255, 0.96);
    font-size: 10px;
    line-height: 11px;
    font-weight: 700;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.epidemic-three-pie-legend {
  display: flex;
  min-width: 0;
  height: 100%;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
}

.epidemic-three-pie-row {
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr) auto auto;
  align-items: center;
  column-gap: 5px;
  min-height: 17px;

  i {
    width: 8px;
    height: 8px;
    border: 2px solid #24f6c4;
    border-radius: 2px;
    box-sizing: border-box;
    background: rgba(3, 20, 31, 0.84);
  }

  span {
    overflow: hidden;
    color: rgba(226, 250, 255, 0.82);
    font-size: 10px;
    line-height: 13px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  strong {
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
    line-height: 13px;
    font-weight: 800;
    white-space: nowrap;
  }

  em {
    color: rgba(223, 250, 255, 0.54);
    font-style: normal;
    font-size: 10px;
    line-height: 12px;
    white-space: nowrap;
  }
}

.epidemic-record-table {
  display: flex;
  height: 100%;
  flex-direction: column;
  gap: 5px;
  padding: 7px 9px 8px;
  box-sizing: border-box;

  &.is-double {
    gap: 5px;
  }

  &.is-full {
    gap: 6px;
    padding: 9px 10px 10px;
  }
}

.epidemic-record-toolbar {
  display: grid;
  grid-template-columns: 118px minmax(0, 1fr);
  align-items: center;
  column-gap: 7px;
  height: 29px;

  > span {
    overflow: hidden;
    color: rgba(255, 255, 255, 0.9);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  > div {
    display: grid;
    min-width: 0;
    grid-template-columns: minmax(0, 1fr) 44px 62px;
    gap: 5px;
  }

  input,
  button {
    height: 25px;
    min-width: 0;
    border: 1px solid rgba(48, 220, 255, 0.16);
    background: rgba(3, 25, 40, 0.62);
    color: #ffffff;
    font-size: 10px;
    box-sizing: border-box;
  }

  input {
    padding: 0 7px;

    &::placeholder {
      color: rgba(255, 255, 255, 0.58);
    }
  }

  button {
    border-color: rgba(255, 215, 108, 0.42);
    background: rgba(255, 215, 108, 0.12);
    color: #ffffff;
    cursor: pointer;
  }

  button.is-more {
    border-color: rgba(48, 220, 255, 0.34);
    background: rgba(48, 220, 255, 0.12);
  }
}

.epidemic-record-header,
.epidemic-record-rows button {
  display: grid;
  grid-template-columns: 112px 56px 42px 54px 64px;
  column-gap: 4px;
  align-items: center;
}

.epidemic-record-header {
  height: 24px;
  padding: 0 6px;
  border: 1px solid rgba(48, 220, 255, 0.1);
  background: rgba(48, 220, 255, 0.06);
  box-sizing: border-box;

  span {
    color: rgba(255, 255, 255, 0.78);
    font-size: 10px;
  }
}

.epidemic-record-rows {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
  gap: 3px;
  overflow: hidden;

  button {
    min-height: 0;
    flex: 1 1 0;
    padding: 0 6px;
    border: 1px solid rgba(48, 220, 255, 0.08);
    background: rgba(48, 220, 255, 0.035);
    color: rgba(255, 255, 255, 0.88);
    cursor: pointer;
    text-align: left;

    &:hover,
    &.active {
      border-color: rgba(255, 215, 108, 0.36);
      background: linear-gradient(90deg, rgba(255, 215, 108, 0.12), rgba(48, 220, 255, 0.04));
      color: #ffffff;
    }
  }

  span,
  strong {
    color: #ffffff;
    overflow: hidden;
    font-size: 10px;
    line-height: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  strong {
    color: #ffd76c;
    font-weight: 700;
  }
}

.epidemic-record-table.is-full .epidemic-record-rows {
  gap: 4px;
}

.epidemic-record-pager {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 7px;
  height: 24px;

  span {
    margin-right: auto;
    color: rgba(255, 255, 255, 0.84);
    font-size: 10px;
  }

  button {
    height: 22px;
    padding: 0 8px;
    border: 1px solid rgba(48, 220, 255, 0.18);
    background: rgba(48, 220, 255, 0.07);
    color: #ffffff;
    font-size: 10px;
    cursor: pointer;

    &:disabled {
      opacity: 0.36;
      cursor: not-allowed;
    }
  }
}

.epidemic-empty {
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: center;
  color: rgba(223, 250, 255, 0.56);
  font-size: 12px;
}

.epidemic-detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 28px;
  border-bottom: 1px solid rgba(48, 220, 255, 0.14);

  span {
    overflow: hidden;
    color: #27f0c0;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 15px;
    font-weight: 900;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  strong {
    color: rgba(223, 250, 255, 0.72);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
    white-space: nowrap;
  }
}

.epidemic-detail-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  margin-top: 8px;

  span {
    min-width: 0;
    height: 34px;
    padding: 4px 7px;
    border: 1px solid rgba(48, 220, 255, 0.1);
    background: rgba(48, 220, 255, 0.035);
    box-sizing: border-box;
  }

  em,
  strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(223, 250, 255, 0.52);
    font-style: normal;
    font-size: 10px;
    line-height: 10px;
  }

  strong {
    margin-top: 4px;
    color: #ffffff;
    font-size: 11px;
    line-height: 12px;
  }

  &.is-modal {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 7px;
    margin-top: 7px;

    span {
      height: 39px;
      padding: 5px 8px;
    }

    .is-wide {
      grid-column: 1 / -1;
    }
  }
}

.epidemic-ledger-modal {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 8, 14, 0.68);
  pointer-events: all;
}

.epidemic-ledger-dialog {
  width: 1260px;
  height: 724px;
  padding: 18px;
  border: 1px solid rgba(255, 215, 108, 0.36);
  background:
    linear-gradient(180deg, rgba(12, 38, 48, 0.97), rgba(4, 17, 28, 0.99)),
    rgba(3, 18, 31, 0.98);
  box-shadow:
    0 0 34px rgba(255, 215, 108, 0.16),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06);
  box-sizing: border-box;
}

.epidemic-ledger-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 42px;
  border-bottom: 1px solid rgba(48, 220, 255, 0.18);

  span {
    display: block;
    color: #ffffff;
    font-size: 19px;
    font-weight: 800;
  }

  strong {
    display: block;
    margin-top: 3px;
    color: rgba(255, 255, 255, 0.86);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
  }

  button {
    width: 68px;
    height: 28px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    background: rgba(255, 255, 255, 0.08);
    color: #ffffff;
    cursor: pointer;
  }
}

.epidemic-ledger-filters {
  display: grid;
  grid-template-columns: 220px 130px 130px 132px 132px 66px 66px;
  gap: 10px;
  align-items: center;
  margin: 16px 0 14px;

  input,
  select,
  button {
    height: 32px;
    min-width: 0;
    border: 1px solid rgba(255, 255, 255, 0.16);
    background: rgba(255, 255, 255, 0.045);
    color: #ffffff;
    font-size: 12px;
    box-sizing: border-box;
  }

  input::placeholder {
    color: rgba(255, 255, 255, 0.62);
  }

  select option {
    color: #2a2415;
    background: #fff7df;
  }

  input,
  select {
    padding: 0 9px;
  }

  button {
    cursor: pointer;
    background: rgba(255, 215, 108, 0.13);
    border-color: rgba(255, 215, 108, 0.36);
    color: #ffffff;
    font-weight: 700;

    &.is-ghost {
      background: rgba(255, 255, 255, 0.06);
      border-color: rgba(255, 255, 255, 0.18);
    }
  }
}

.epidemic-ledger-workspace {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 376px;
  gap: 14px;
  height: 548px;
}

.epidemic-ledger-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  padding: 8px;
  overflow: hidden;
  border: 1px solid rgba(255, 215, 108, 0.16);
  background: rgba(255, 255, 255, 0.035);
  box-shadow: inset 0 0 24px rgba(255, 215, 108, 0.03);
  box-sizing: border-box;
}

.epidemic-ledger-table-head,
.epidemic-ledger-list button {
  display: grid;
  grid-template-columns: minmax(126px, 1.5fr) minmax(76px, 0.9fr) 72px minmax(96px, 1fr) 72px 82px 42px;
  align-items: center;
  column-gap: 8px;
}

.epidemic-ledger-table-head {
  height: 34px;
  padding: 0 12px;
  border: 0;
  border-bottom: 1px solid rgba(255, 215, 108, 0.14);
  border-radius: 0;
  background: rgba(255, 255, 255, 0.07);

  span {
    overflow: hidden;
    color: rgba(255, 255, 255, 0.82);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.epidemic-ledger-list button {
  position: relative;
  width: 100%;
  height: 30px;
  padding: 0 10px 0 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 3px;
  background:
    linear-gradient(90deg, rgba(255, 255, 255, 0.045), rgba(48, 220, 255, 0.018)),
    rgba(255, 255, 255, 0.025);
  color: rgba(255, 255, 255, 0.9);
  cursor: pointer;
  text-align: left;
  transition:
    border-color 0.18s ease,
    background 0.18s ease,
    color 0.18s ease;

  &::before {
    position: absolute;
    left: 0;
    top: 6px;
    width: 2px;
    height: 18px;
    content: "";
    opacity: 0;
    background: #ffd76c;
    box-shadow: 0 0 10px rgba(255, 215, 108, 0.72);
  }

  &.active,
  &:hover {
    border-color: rgba(255, 215, 108, 0.36);
    background:
      linear-gradient(90deg, rgba(255, 215, 108, 0.16), rgba(48, 220, 255, 0.05)),
      rgba(255, 255, 255, 0.045);
    color: #ffffff;

    &::before {
      opacity: 1;
    }
  }

  span,
  strong {
    overflow: hidden;
    font-size: 10px;
    line-height: 12px;
    color: #ffffff;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  strong {
    color: #ffd76c;
    font-weight: 800;
  }
}

.epidemic-ledger-detail {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: 14px;
  border: 1px solid rgba(39, 240, 192, 0.15);
  background: rgba(39, 240, 192, 0.04);
  box-sizing: border-box;
}

.epidemic-detail-scroll {
  flex: 1 1 0;
  min-height: 0;
  margin-top: 10px;
  padding-right: 4px;
  overflow-y: auto;
}

.epidemic-detail-section {
  & + & {
    margin-top: 10px;
  }
}

.epidemic-detail-section-title {
  display: flex;
  align-items: center;
  height: 18px;
  color: #27f0c0;
  font-size: 12px;
  font-weight: 700;
  line-height: 18px;
}

.epidemic-modal-photo-strip {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-top: 7px;
  overflow: hidden;

  .epidemic-modal-photo-thumb {
    position: relative;
    width: 100%;
    height: 96px;
    padding: 0;
    overflow: hidden;
    border: 1px solid rgba(39, 240, 192, 0.24);
    background: rgba(39, 240, 192, 0.04);
    cursor: zoom-in;
    box-sizing: border-box;

    &:hover {
      border-color: rgba(255, 215, 108, 0.58);
      box-shadow: 0 0 14px rgba(255, 215, 108, 0.16);
    }

    &::after {
      position: absolute;
      right: 0;
      bottom: 0;
      left: 0;
      height: 18px;
      content: "";
      background: linear-gradient(180deg, rgba(0, 12, 20, 0), rgba(0, 12, 20, 0.92) 52%, rgba(0, 12, 20, 0.98));
      pointer-events: none;
    }
  }

  img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.18s ease;
  }

  .epidemic-modal-photo-thumb:hover img {
    transform: scale(1.03);
  }

  span {
    grid-column: 1 / -1;
    display: flex;
    height: 96px;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(48, 220, 255, 0.12);
    color: rgba(223, 250, 255, 0.54);
    font-size: 11px;
  }
}

.epidemic-image-preview-modal {
  position: absolute;
  inset: 0;
  z-index: 48;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 46px;
  background: rgba(0, 8, 14, 0.78);
  pointer-events: all;
  box-sizing: border-box;
}

.epidemic-image-preview-dialog {
  display: flex;
  width: min(920px, 78vw);
  max-height: 78vh;
  flex-direction: column;
  border: 1px solid rgba(255, 215, 108, 0.34);
  background:
    linear-gradient(180deg, rgba(9, 38, 50, 0.97), rgba(2, 15, 26, 0.99)),
    rgba(2, 17, 28, 0.98);
  box-shadow:
    0 0 34px rgba(255, 215, 108, 0.14),
    inset 0 0 0 1px rgba(255, 255, 255, 0.055);
}

.epidemic-image-preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: 52px;
  padding: 0 16px 0 18px;
  border-bottom: 1px solid rgba(48, 220, 255, 0.16);
  box-sizing: border-box;

  div {
    min-width: 0;
  }

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

  span {
    color: #ffffff;
    font-size: 16px;
    font-weight: 800;
  }

  strong {
    margin-top: 3px;
    color: rgba(39, 240, 192, 0.82);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
  }

  button {
    width: 68px;
    height: 28px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    background: rgba(255, 255, 255, 0.08);
    color: #ffffff;
    cursor: pointer;
  }
}

.epidemic-image-preview-body {
  display: flex;
  min-height: 0;
  flex: 1;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 10, 17, 0.42);
}

.epidemic-image-preview-frame {
  position: relative;
  display: flex;
  max-width: 100%;
  max-height: calc(78vh - 84px);
  align-items: center;
  justify-content: center;
  overflow: hidden;

  img {
    display: block;
    max-width: 100%;
    max-height: calc(78vh - 84px);
    object-fit: contain;
  }

  &::after {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 38px;
    content: "";
    background: linear-gradient(180deg, rgba(0, 10, 17, 0), rgba(0, 10, 17, 0.96) 58%, rgba(0, 10, 17, 0.99));
    pointer-events: none;
  }
}

.epidemic-ledger-pager {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  height: 42px;

  span {
    color: rgba(255, 255, 255, 0.72);
    font-size: 12px;
  }

  button {
    height: 26px;
    padding: 0 12px;
    border: 1px solid rgba(255, 255, 255, 0.16);
    background: rgba(255, 255, 255, 0.055);
    color: #ffffff;
    cursor: pointer;

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

.trade-ledger-dialog {
  width: 1260px;
  height: 724px;
  border-color: rgba(255, 215, 108, 0.36);
  background:
    linear-gradient(180deg, rgba(12, 38, 48, 0.97), rgba(4, 17, 28, 0.99)),
    rgba(3, 18, 31, 0.98);
  box-shadow:
    0 0 34px rgba(255, 215, 108, 0.16),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06);

  .epidemic-ledger-dialog-head {
    strong {
      color: rgba(255, 255, 255, 0.86);
    }

    button {
      border-color: rgba(255, 255, 255, 0.28);
      background: rgba(255, 255, 255, 0.08);
      color: #ffffff;
    }
  }
}

.trade-ledger-filters {
  grid-template-columns: 260px 150px 142px 142px 70px 70px;

  input,
  select,
  button {
    color: #ffffff;
  }

  button {
    color: #ffffff;
    font-weight: 700;
  }
}

.trade-ledger-workspace {
  grid-template-columns: minmax(0, 1fr) 376px;
}

.trade-ledger-modal-table-head,
.trade-ledger-modal-list button {
  display: grid;
  grid-template-columns: 128px 84px 92px 58px 88px 90px 102px 68px;
  align-items: center;
  column-gap: 10px;
}

.trade-ledger-modal-table-head {
  height: 34px;
  padding: 0 12px;
  border-bottom: 1px solid rgba(255, 215, 108, 0.14);
  background: rgba(255, 255, 255, 0.07);

  span {
    overflow: hidden;
    color: rgba(255, 255, 255, 0.82);
    font-size: 11px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.trade-ledger-modal-list {
  border-color: rgba(255, 215, 108, 0.16);
  background: rgba(255, 255, 255, 0.035);

  button {
    height: 34px;
    color: rgba(255, 255, 255, 0.9);

    &.active,
    &:hover {
      background: linear-gradient(90deg, rgba(255, 215, 108, 0.16), rgba(48, 220, 255, 0.05));
      color: #ffffff;
    }

    span,
    strong {
      color: #ffffff;
    }

    strong {
      color: #ffd76c;
    }
  }
}

.trade-ledger-detail {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  border-color: rgba(48, 220, 255, 0.24);
  background:
    linear-gradient(180deg, rgba(8, 42, 54, 0.84), rgba(3, 19, 30, 0.92)),
    radial-gradient(circle at 10% 0, rgba(255, 215, 108, 0.14), transparent 38%),
    rgba(3, 25, 40, 0.48);
  box-shadow:
    inset 0 0 22px rgba(48, 220, 255, 0.055),
    inset 0 0 0 1px rgba(255, 255, 255, 0.035);

  .trade-detail-head {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 12px;
    align-items: center;
    min-height: 76px;
    padding: 15px 16px 13px;
    border-bottom: 1px solid rgba(48, 220, 255, 0.18);
    background:
      linear-gradient(90deg, rgba(255, 215, 108, 0.16), rgba(48, 220, 255, 0.035)),
      rgba(3, 26, 38, 0.62);
    box-sizing: border-box;

    &::before {
      position: absolute;
      left: 0;
      top: 16px;
      width: 3px;
      height: 44px;
      content: "";
      background: #ffd76c;
      box-shadow: 0 0 14px rgba(255, 215, 108, 0.72);
    }

    div {
      min-width: 0;
    }

    span,
    em {
      display: block;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    span {
      color: #ffffff;
      font-family: D-DIN, Arial, sans-serif;
      font-size: 21px;
      font-weight: 800;
      line-height: 24px;
    }

    em {
      margin-top: 6px;
      color: #ffd76c;
      font-size: 12px;
      font-style: normal;
      font-weight: 800;
      line-height: 16px;
    }

    strong {
      padding: 5px 9px;
      border: 1px solid rgba(48, 220, 255, 0.18);
      background: rgba(5, 27, 38, 0.58);
      color: rgba(237, 253, 255, 0.82);
      font-family: D-DIN, Arial, sans-serif;
      font-size: 13px;
      font-weight: 800;
      white-space: nowrap;
    }
  }
}

.epidemic-ledger-record-detail {
  .trade-detail-head em {
    color: #27f0c0;
  }

  .epidemic-detail-flat-grid {
    padding-bottom: 0;
  }
}

.epidemic-detail-grid.is-modal.trade-detail-grid {
  flex: 0 0 auto;
  align-content: start;
  grid-template-columns: 66px minmax(0, 1fr) 66px minmax(0, 1fr);
  gap: 0;
  margin: 0;
  padding: 14px 16px 0;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.1);
  border-bottom: 0;

  span {
    min-width: 0;
    height: auto;
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
  }

  &::before {
    display: none;
  }

  span:not(.is-wide) {
    display: contents;
  }

  span:not(.is-wide) em,
  span:not(.is-wide) strong,
  .is-wide em,
  .is-wide strong {
    display: flex;
    min-width: 0;
    height: 30px;
    align-items: center;
    padding: 0 8px;
    border-bottom: 1px solid rgba(48, 220, 255, 0.1);
    box-sizing: border-box;
  }

  em {
    justify-content: flex-end;
    color: rgba(205, 239, 246, 0.64);
    font-size: 10px;
    font-style: normal;
    line-height: 14px;
    text-align: right;
    background: rgba(255, 215, 108, 0.055);
    border-right: 1px solid rgba(48, 220, 255, 0.085);
  }

  em::after {
    content: "：";
  }

  strong {
    min-width: 0;
    margin-top: 0;
    overflow: hidden;
    color: #ffffff;
    font-size: 13px;
    line-height: 16px;
    font-weight: 800;
    text-overflow: ellipsis;
    white-space: nowrap;
    background: rgba(255, 255, 255, 0.03);
  }

  span:not(.is-wide):nth-child(odd) strong {
    border-right: 1px solid rgba(48, 220, 255, 0.065);
  }

  .is-wide {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: 66px minmax(0, 1fr);
  }
}

.epidemic-detail-photos {
  min-height: 0;
  margin: 12px 16px 16px;
  padding-top: 10px;
  border-top: 1px solid rgba(48, 220, 255, 0.12);
}

.epidemic-detail-photo-title {
  height: 18px;
  color: #27f0c0;
  font-size: 12px;
  font-weight: 800;
  line-height: 18px;
}

.epidemic-ledger-record-detail .epidemic-modal-photo-strip {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: 8px;

  .epidemic-modal-photo-thumb {
    height: 76px;
    border-color: rgba(48, 220, 255, 0.22);
    border-radius: 2px;
    background: rgba(4, 28, 41, 0.62);
  }
}

.trade-detail-locate {
  width: calc(100% - 32px);
  height: 36px;
  margin: auto 16px 16px;
  border: 1px solid rgba(48, 220, 255, 0.34);
  background:
    linear-gradient(90deg, rgba(48, 220, 255, 0.14), rgba(255, 215, 108, 0.08)),
    rgba(4, 28, 41, 0.74);
  color: #ffffff;
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
  transition:
    border-color 0.18s ease,
    background 0.18s ease,
    box-shadow 0.18s ease;

  &:hover:not(:disabled) {
    border-color: rgba(255, 215, 108, 0.52);
    background:
      linear-gradient(90deg, rgba(255, 215, 108, 0.16), rgba(48, 220, 255, 0.11)),
      rgba(8, 36, 48, 0.84);
    box-shadow: 0 0 14px rgba(255, 215, 108, 0.12);
  }

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

.trade-overview-card {
  display: grid;
  grid-template-rows: minmax(0, 1fr) 46px;
  row-gap: 10px;
  height: 100%;
  padding: 14px 13px 10px;
  box-sizing: border-box;
}

.trade-main-metric {
  display: flex;
  min-width: 0;
  flex-direction: column;
  justify-content: center;

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

  span {
    color: rgba(255, 255, 255, 0.84);
    font-size: 12px;
    line-height: 14px;
  }

  strong {
    margin-top: 3px;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 27px;
    font-weight: 900;
    line-height: 29px;

    em {
      margin-left: 5px;
      color: rgba(255, 255, 255, 0.82);
      font-style: normal;
      font-size: 12px;
    }
  }
}

.trade-metric-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;

  span {
    min-width: 0;
    padding: 5px 7px;
    border-left: 2px solid rgba(255, 215, 108, 0.62);
    background: linear-gradient(90deg, rgba(255, 215, 108, 0.11), rgba(48, 220, 255, 0.03));
    box-sizing: border-box;
  }

  em,
  strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(255, 255, 255, 0.78);
    font-style: normal;
    font-size: 10px;
    line-height: 11px;
  }

  strong {
    margin-top: 4px;
    color: #ffd76c;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 13px;
    line-height: 15px;
    font-weight: 900;
  }
}

.trade-risk-card {
  display: grid;
  grid-template-rows: 30px minmax(0, 1fr);
  gap: 5px;
  height: 100%;
  padding: 9px 12px 10px;
  overflow: hidden;
  box-sizing: border-box;
}

.trade-risk-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
  padding: 0 9px;
  border-left: 3px solid rgba(255, 207, 98, 0.78);
  background:
    linear-gradient(90deg, rgba(255, 207, 98, 0.14), rgba(48, 220, 255, 0.025)),
    rgba(3, 25, 40, 0.2);
  box-sizing: border-box;

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

  span {
    color: rgba(226, 252, 255, 0.78);
    font-size: 12px;
    line-height: 16px;
    font-weight: 800;
  }

  strong {
    flex: 0 0 auto;
    color: #ffd76c;
    font-size: 12px;
    line-height: 16px;
    font-weight: 900;
    text-align: right;
  }
}

.trade-risk-list {
  display: grid;
  grid-template-rows: repeat(4, minmax(0, 1fr));
  gap: 4px;
  min-height: 0;
}

.trade-risk-row {
  position: relative;
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr);
  align-items: center;
  gap: 7px;
  min-width: 0;
  min-height: 0;
  padding: 3px 8px 3px 10px;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.11);
  background:
    linear-gradient(90deg, color-mix(in srgb, var(--risk-color) 12%, transparent), rgba(48, 220, 255, 0.02)),
    rgba(4, 24, 37, 0.2);
  box-sizing: border-box;

  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    width: 2px;
    height: 22px;
    background: var(--risk-color);
    box-shadow: 0 0 10px var(--risk-color);
    transform: translateY(-50%);
  }

  i {
    position: relative;
    z-index: 1;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--risk-color);
    box-shadow: 0 0 9px var(--risk-color);
  }

  span {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-width: 0;
    gap: 8px;
  }

  em,
  small {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
    min-width: 0;
    color: rgba(255, 255, 255, 0.9);
    font-size: 11px;
    line-height: 14px;
    font-style: normal;
    font-weight: 900;

    b {
      flex: 0 0 auto;
      min-width: 28px;
      height: 15px;
      padding: 0 5px;
      border: 1px solid color-mix(in srgb, var(--risk-color) 52%, transparent);
      border-radius: 1px;
      color: var(--risk-color);
      font-size: 10px;
      line-height: 13px;
      font-weight: 900;
      text-align: center;
      background: color-mix(in srgb, var(--risk-color) 13%, rgba(3, 25, 40, 0.62));
      box-sizing: border-box;
    }
  }

  small {
    flex: 1 1 auto;
    min-width: 0;
    color: rgba(226, 252, 255, 0.76);
    font-size: 10px;
    line-height: 12px;
    text-align: right;
  }

  &.is-high {
    border-color: rgba(255, 143, 107, 0.24);
  }

  &.is-high small {
    color: rgba(255, 235, 176, 0.88);
  }

  &.is-watch small {
    color: rgba(255, 215, 108, 0.82);
  }
}

.trade-cert-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 108px;
  gap: 8px;
  height: 100%;
  padding: 9px 12px;
  box-sizing: border-box;
}

.trade-cert-main {
  display: flex;
  min-width: 0;
  flex-direction: column;
  justify-content: center;
  gap: 8px;

  span {
    min-width: 0;
    padding: 7px 8px;
    border: 1px solid rgba(48, 220, 255, 0.12);
    background: rgba(48, 220, 255, 0.045);
  }

  em,
  strong,
  small {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  em {
    color: rgba(255, 255, 255, 0.78);
    font-style: normal;
    font-size: 10px;
  }

  strong {
    margin-top: 4px;
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 18px;
    line-height: 20px;
    font-weight: 900;

    b {
      margin-left: 3px;
      color: rgba(255, 255, 255, 0.82);
      font-size: 10px;
      font-weight: 500;
    }
  }

  small {
    margin-top: 3px;
    color: rgba(255, 255, 255, 0.82);
    font-size: 10px;
  }
}

.trade-cert-chart {
  width: 108px;
  height: 100%;
}

.trade-dest-list {
  display: flex;
  height: 100%;
  flex-direction: column;
  gap: 3px;
  padding: 7px 10px 8px;
  box-sizing: border-box;
}

.trade-dest-row {
  position: relative;
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) 66px;
  align-items: center;
  min-height: 0;
  flex: 1 1 0;
  column-gap: 6px;
  overflow: hidden;
  border: 1px solid rgba(48, 220, 255, 0.08);
  background: rgba(48, 220, 255, 0.035);

  &::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--trade-ratio);
    background: linear-gradient(90deg, rgba(255, 215, 108, 0.22), rgba(48, 220, 255, 0.08));
  }

  span,
  strong,
  em {
    position: relative;
    z-index: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span {
    padding-left: 7px;
    color: #ffd76c;
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
    font-weight: 900;
  }

  strong {
    color: #ffffff;
    font-size: 11px;
    line-height: 13px;
  }

  em {
    color: #ffffff;
    font-family: D-DIN, Arial, sans-serif;
    font-style: normal;
    font-size: 11px;
    line-height: 13px;
    text-align: right;
  }
}

.trade-ledger-table {
  display: flex;
  height: 100%;
  flex-direction: column;
  gap: 5px;
  padding: 7px 9px 8px;
  box-sizing: border-box;
}

.trade-ledger-toolbar {
  display: grid;
  grid-template-columns: 82px minmax(0, 1fr);
  align-items: center;
  column-gap: 7px;
  height: 29px;

  > span {
    overflow: hidden;
    color: rgba(255, 255, 255, 0.9);
    font-family: D-DIN, Arial, sans-serif;
    font-size: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  > div {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 44px 62px;
    gap: 5px;
  }

  input,
  button {
    height: 25px;
    min-width: 0;
    border: 1px solid rgba(48, 220, 255, 0.16);
    background: rgba(3, 25, 40, 0.62);
    color: #ffffff;
    font-size: 10px;
    box-sizing: border-box;
  }

  input {
    padding: 0 7px;

    &::placeholder {
      color: rgba(255, 255, 255, 0.58);
    }
  }

  button {
    border-color: rgba(255, 215, 108, 0.42);
    background: rgba(255, 215, 108, 0.12);
    color: #ffffff;
    cursor: pointer;
  }

  button.is-more {
    border-color: rgba(48, 220, 255, 0.34);
    background: rgba(48, 220, 255, 0.12);
  }
}

.trade-ledger-header,
.trade-ledger-rows button {
  display: grid;
  grid-template-columns: 98px 56px 39px 38px 34px 64px;
  column-gap: 4px;
  align-items: center;
}

.trade-ledger-header {
  height: 24px;
  padding: 0 6px;
  border: 1px solid rgba(48, 220, 255, 0.1);
  background: rgba(48, 220, 255, 0.06);
  box-sizing: border-box;

  span {
    color: rgba(255, 255, 255, 0.78);
    font-size: 10px;
  }
}

.trade-ledger-rows {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
  gap: 3px;
  overflow: hidden;

  button {
    min-height: 0;
    flex: 1 1 0;
    padding: 0 6px;
    border: 1px solid rgba(48, 220, 255, 0.08);
    background: rgba(48, 220, 255, 0.035);
    color: rgba(255, 255, 255, 0.88);
    cursor: pointer;
    text-align: left;

    &:hover {
      border-color: rgba(255, 215, 108, 0.36);
      background: linear-gradient(90deg, rgba(255, 215, 108, 0.12), rgba(48, 220, 255, 0.04));
      color: #ffffff;
    }
  }

  span,
  strong {
    color: #ffffff;
    overflow: hidden;
    font-size: 10px;
    line-height: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  span:first-child {
    line-height: 12px;
    white-space: normal;
    word-break: break-all;
    text-overflow: clip;
  }

  strong {
    color: #ffd76c;
    font-weight: 700;
  }
}

.trade-ledger-pager {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 7px;
  height: 24px;

  span {
    margin-right: auto;
    color: rgba(255, 255, 255, 0.84);
    font-size: 10px;
  }

  button {
    height: 22px;
    padding: 0 8px;
    border: 1px solid rgba(48, 220, 255, 0.18);
    background: rgba(48, 220, 255, 0.07);
    color: #ffffff;
    font-size: 10px;
    cursor: pointer;

    &:disabled {
      opacity: 0.36;
      cursor: not-allowed;
    }
  }
}
</style>
