<script lang="ts" setup>
import { IotDeviceApi } from '@/api/pms/device'
import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
import { useTableComponents } from '@/components/ZmTable/useTableComponents'
import { useMqtt } from '@/utils/useMqtt'
import {
  Dimensions,
  formatChartValue,
  IotTdItem,
  mapToDimension,
  normalizeRangeValue,
  RangeSettingDraft,
  withDisplayStyle
} from '@/utils/useSocketBus'
import { useDebounceFn } from '@vueuse/core'
import { Setting } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'

const props = defineProps({
  id: {
    type: Number,
    required: true
  },
  deviceCode: {
    type: String,
    required: true
  },
  deviceName: {
    type: String,
    required: true
  },
  otherName: {
    type: String,
    default: ''
  },
  mqttUrl: {
    type: String,
    required: true
  },
  ifInline: {
    type: String,
    required: true
  },
  lastInlineTime: {
    type: String,
    required: true
  },
  deptName: {
    type: String,
    required: true
  },
  vehicleName: {
    type: String,
    required: true
  },
  carOnline: {
    type: String,
    required: true
  },
  isRealTime: {
    type: Boolean,
    default: true
  },
  date: {
    type: Array as PropType<Array<string>>,
    required: true
  },
  token: {
    type: String,
    required: true
  }
})

const emit = defineEmits<{
  (event: 'other-name-updated', payload: { id: number; otherName: string }): void
}>()

const dimensions = ref<Dimensions[]>([])
const selectedDimension = ref<Record<string, boolean>>({})
const message = useMessage()

const currentOtherName = ref(props.otherName)

const displayDeviceName = computed(() => currentOtherName.value || props.deviceName)

watch(
  () => props.otherName,
  (value) => {
    currentOtherName.value = value
  }
)

const { connect, destroy, isConnected, subscribe } = useMqtt()

const REALTIME_WINDOW_MS = 10 * 60 * 1000
const REALTIME_RIGHT_BLANK_MS = 60 * 1000

function getPointTimestamp(value: unknown) {
  if (value === null || value === undefined || value === '') {
    return Date.now()
  }

  const numberValue = Number(value)

  if (Number.isFinite(numberValue) && numberValue > 0) {
    return numberValue > 1_000_000_000_000 ? numberValue : dayjs.unix(numberValue).valueOf()
  }

  const parsedValue = dayjs(value as string).valueOf()

  return Number.isFinite(parsedValue) ? parsedValue : Date.now()
}

const handleMessageUpdate = (_topic: string, data: any) => {
  const list = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []
  const valueMap = new Map<string, number>()
  const updatedNames = new Set<string>()

  for (const item of list) {
    const identity = String(item.id ?? item.identity ?? '')
    const value = Number(item.value ?? item.logValue ?? 0)

    if (!identity || !Number.isFinite(value)) continue

    valueMap.set(identity, value)

    const modelName = dimensions.value.find((item) => item.identifier === identity)?.name

    if (modelName && chartData.value[modelName]) {
      const ts = getPointTimestamp(item.remark || item.readTime || item.ts || item.time)

      appendChartPoint(modelName, {
        ts,
        value
      })

      updatedNames.add(modelName)
    }
  }

  if (!updatedNames.size) return

  updateDimensionValues(valueMap)
  scheduleRealtimeChartUpdate(Array.from(updatedNames))
}

watch(isConnected, (newVal) => {
  if (newVal) {
    // subscribe(`/636/${props.deviceCode}/property/post`)

    // switch (props.deviceCode) {
    //   case 'YF1539':
    //     subscribe(`/656/${props.deviceCode}/property/post`)
    //   case 'YF325':
    //   case 'YF288':
    //   case 'YF671':
    //   case 'YF459':
    //     subscribe(`/635/${props.deviceCode}/property/post`)
    //   case 'YF649':
    //     subscribe(`/636/${props.deviceCode}/property/post`)
    //   default:
    //     subscribe(`/636/${props.deviceCode}/property/post`)
    // }

    subscribe(props.mqttUrl)
    // subscribe('/636/YF649/property/post')
  }
})

async function loadDimensions() {
  if (!props.id) return
  try {
    dimensions.value = (
      ((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as IotTdItem[]) ?? []
    )
      .sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
      .map(mapToDimension)
      .filter((item) => item.isText === false)
      .map(withDisplayStyle)

    selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
  } catch (error) {
    console.error(error)
  }
}

interface ChartPoint {
  ts: number
  value: number
}

interface ChartData {
  [key: Dimensions['name']]: ChartPoint[]
}

const chartData = ref<ChartData>({})

const chartRef = ref<HTMLDivElement | null>(null)
let chart: echarts.ECharts | null = null

const chartRealtimeMode = ref(true)
let chartLoadVersion = 0
let realtimeUpdateFrame: number | null = null
const pendingRealtimeSeriesNames = new Set<string>()

const TREND_AXIS_MIN = 0
const TREND_AXIS_MAX = 100

const resizeChart = useDebounceFn(() => {
  chart?.resize()
}, 100)

function getDimensionByName(name: string) {
  return dimensions.value.find((item) => item.name === name)
}

function getDatasetMaxValue(name: string) {
  const dataset = chartData.value[name] || []
  let maxValue = -Infinity

  dataset.forEach(({ value }) => {
    const numberValue = Number(value)
    if (Number.isFinite(numberValue) && numberValue > maxValue) {
      maxValue = numberValue
    }
  })

  const currentValue = normalizeRangeValue(getDimensionByName(name)?.value)
  if (currentValue !== undefined && currentValue > maxValue) {
    maxValue = currentValue
  }

  return Number.isFinite(maxValue) ? maxValue : 0
}

function getSeriesRange(name: string) {
  const dimension = getDimensionByName(name)
  const configuredMin = normalizeRangeValue(dimension?.minValue)
  const configuredMax = normalizeRangeValue(dimension?.maxValue)
  const min = configuredMin ?? 0
  const max = configuredMax ?? getDatasetMaxValue(name)
  const normalizedMax = max > min ? max : min + 1

  return {
    min,
    max: normalizedMax,
    labelMin: min,
    labelMax: normalizedMax,
    configured: configuredMin !== undefined && configuredMax !== undefined
  }
}

const chartLegendItems = computed(() =>
  dimensions.value.map((item) => {
    const range = getSeriesRange(item.name)

    return {
      name: item.name,
      color: item.color,
      bgHover: item.bgHover,
      selected: selectedDimension.value[item.name] !== false,
      max: formatChartValue(range.labelMax),
      min: formatChartValue(range.labelMin)
    }
  })
)

function getTrendYAxisOptions() {
  return {
    type: 'value',
    min: TREND_AXIS_MIN,
    max: TREND_AXIS_MAX,
    interval: 10,
    show: true,
    axisLabel: {
      show: false
    },
    axisLine: {
      show: false
    },
    axisTick: {
      show: false
    },
    splitLine: {
      show: true,
      lineStyle: {
        color: 'rgba(255, 255, 255, 0.08)',
        width: 1
      }
    }
  }
}

function getLatestTimestamp() {
  let latest: number | null = null

  Object.values(chartData.value).forEach((series) => {
    series.forEach((point) => {
      if (latest === null || point.ts > latest) {
        latest = point.ts
      }
    })
  })

  return latest
}

function getSelectedDateRangeTimestamps() {
  const fallbackEnd = Date.now()
  const startTs = dayjs(props.date[0]).valueOf()
  const endTs = dayjs(props.date[1]).valueOf()

  if (Number.isFinite(startTs) && Number.isFinite(endTs) && startTs < endTs) {
    return { startTs, endTs }
  }

  return {
    startTs: fallbackEnd - REALTIME_WINDOW_MS,
    endTs: fallbackEnd
  }
}

function getRealtimeXAxisMax(anchorTs = Date.now()) {
  return anchorTs + REALTIME_RIGHT_BLANK_MS
}

function applyRealtimeWindow(anchorTs = Date.now()) {
  chart?.setOption({
    xAxis: {
      min: anchorTs - REALTIME_WINDOW_MS,
      max: getRealtimeXAxisMax(anchorTs)
    }
  })
}

function applySelectedDateWindow() {
  const { startTs, endTs } = getSelectedDateRangeTimestamps()

  chart?.setOption({
    xAxis: {
      min: startTs,
      max: endTs
    }
  })
}

function trimChartDataToRealtimeWindow(anchorTs = Date.now()) {
  const startTs = anchorTs - REALTIME_WINDOW_MS

  Object.keys(chartData.value).forEach((name) => {
    const series = chartData.value[name] || []
    const windowStartIndex = series.findIndex((point) => point.ts >= startTs)

    if (windowStartIndex === -1) {
      chartData.value[name] = series.slice(-1)
      return
    }

    if (windowStartIndex === 0) return

    chartData.value[name] = series.slice(windowStartIndex - 1)
  })
}

function appendChartPoint(name: string, point: ChartPoint) {
  const series = chartData.value[name] ?? []
  const samePointIndex = series.findIndex((item) => item.ts === point.ts)

  if (samePointIndex >= 0) {
    series[samePointIndex] = point
  } else {
    series.push(point)
  }

  series.sort((a, b) => a.ts - b.ts)
  chartData.value[name] = series
}

function mapChartPoint({ ts, value }: ChartPoint, name: string) {
  const numberValue = Number(value)

  if (!Number.isFinite(numberValue)) return [ts, null, null]

  const { min, max } = getSeriesRange(name)
  const denominator = max - min || 1
  const mappedValue = ((numberValue - min) / denominator) * 100
  const displayValue = Math.min(TREND_AXIS_MAX, Math.max(TREND_AXIS_MIN, mappedValue))

  return [ts, Number(displayValue.toFixed(2)), numberValue]
}

function buildSeriesData(name: string) {
  const dataset = chartData.value[name] || []

  return dataset.map((point) => mapChartPoint(point, name))
}

function updateDimensionValues(valueMap: Map<string, number>) {
  dimensions.value.forEach((item) => {
    const value = valueMap.get(item.identifier)
    if (value !== undefined) {
      item.value = value
    }
  })
}

function getChartAnimationOptions() {
  return {
    animation: !chartRealtimeMode.value,
    animationDuration: chartRealtimeMode.value ? 0 : 200,
    animationEasing: 'linear',
    animationDurationUpdate: chartRealtimeMode.value ? 0 : 200,
    animationEasingUpdate: 'linear'
  }
}

function updateSeriesByNames(names: string[]) {
  if (!chart) render()
  if (!chart || !names.length) return

  chart.setOption(
    {
      ...getChartAnimationOptions(),
      legend: {
        selected: selectedDimension.value
      },
      yAxis: getTrendYAxisOptions(),
      series: names.map((name) => ({
        id: name,
        name,
        data: buildSeriesData(name)
      }))
    },
    {
      lazyUpdate: true
    }
  )
}

function flushRealtimeChartUpdate() {
  realtimeUpdateFrame = null

  const names = Array.from(pendingRealtimeSeriesNames)
  pendingRealtimeSeriesNames.clear()

  if (!chart) render()
  if (!chart || !names.length) return

  const latestTs = getLatestTimestamp() ?? Date.now()
  trimChartDataToRealtimeWindow(latestTs)

  chart.setOption(
    {
      ...getChartAnimationOptions(),
      xAxis: {
        min: latestTs - REALTIME_WINDOW_MS,
        max: getRealtimeXAxisMax(latestTs)
      },
      legend: {
        selected: selectedDimension.value
      },
      yAxis: getTrendYAxisOptions(),
      series: names.map((name) => ({
        id: name,
        name,
        data: buildSeriesData(name)
      }))
    },
    {
      lazyUpdate: true
    }
  )
}

function scheduleRealtimeChartUpdate(names: string[]) {
  names.forEach((name) => pendingRealtimeSeriesNames.add(name))

  if (realtimeUpdateFrame !== null) return

  realtimeUpdateFrame = window.requestAnimationFrame(flushRealtimeChartUpdate)
}

function cancelRealtimeChartUpdate() {
  if (realtimeUpdateFrame !== null) {
    window.cancelAnimationFrame(realtimeUpdateFrame)
    realtimeUpdateFrame = null
  }

  pendingRealtimeSeriesNames.clear()
}

function handleClickSpec(modelName: string) {
  selectedDimension.value[modelName] = !selectedDimension.value[modelName]
  chart?.setOption({
    legend: {
      selected: selectedDimension.value
    }
  })

  if (selectedDimension.value[modelName]) {
    updateSeriesByNames([modelName])
  }

  nextTick(() => {
    chart?.resize()
  })
}

function chartInit() {
  if (!chart) return

  window.removeEventListener('resize', resizeChart)
  window.addEventListener('resize', resizeChart)
}

function render() {
  if (!chartRef.value) return

  if (!chart) chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })

  chartInit()

  const { startTs, endTs } = getSelectedDateRangeTimestamps()

  chart.setOption(
    {
      color: dimensions.value.map((item) => item.color),
      ...getChartAnimationOptions(),
      grid: {
        left: '2%',
        top: '5%',
        right: '2%',
        bottom: '12%'
      },
      tooltip: {
        trigger: 'axis',
        confine: true,
        backgroundColor: 'rgba(11, 17, 33, 0.95)',
        borderColor: '#22d3ee',
        borderWidth: 1,
        textStyle: {
          color: '#e2e8f0'
        },
        axisPointer: {
          type: 'line',
          lineStyle: {
            color: 'rgba(255, 255, 255, 0.3)',
            type: 'dashed'
          }
        },
        formatter: (params: any[]) => {
          const list = Array.isArray(params) ? params : [params]
          const firstPoint = list[0]
          const title = firstPoint?.axisValue
            ? dayjs(Number(firstPoint.axisValue)).format('YYYY-MM-DD HH:mm:ss')
            : ''
          const existed: string[] = []
          const rows = list
            .filter((item) => {
              if (existed.includes(item.seriesName)) return false
              existed.push(item.seriesName)
              return true
            })
            .map(
              (
                item
              ) => `<div style="display:grid;grid-template-columns:max-content max-content;column-gap:16px;align-items:center;margin-top:6px;">
              <span style="white-space:nowrap;color:#cbd5e1;">${item.marker} ${item.seriesName}</span>
              <span style="white-space:nowrap;text-align:right;color:#fff;font-weight:700;font-variant-numeric:tabular-nums;">${formatChartValue(item.value?.[2])}</span>
            </div>`
            )

          return [title ? `<div style="margin-bottom:2px;">${title}</div>` : '', ...rows]
            .filter(Boolean)
            .join('')
        }
      },
      xAxis: {
        type: 'time',
        min: chartRealtimeMode.value ? endTs - REALTIME_WINDOW_MS : startTs,
        max: chartRealtimeMode.value ? getRealtimeXAxisMax(endTs) : endTs,
        boundaryGap: false,
        axisLabel: {
          formatter: (value: number) => dayjs(value).format('YYYY-MM-DD\nHH:mm:ss'),
          rotate: 0,
          align: 'left',
          color: '#94a3b8'
        },
        axisLine: {
          lineStyle: {
            color: 'rgba(34, 211, 238, 0.32)'
          }
        },
        axisTick: {
          show: false
        },
        splitLine: {
          show: false
        }
      },
      dataZoom: [
        {
          type: 'inside',
          xAxisIndex: 0,
          filterMode: 'none'
        },
        {
          type: 'slider',
          xAxisIndex: 0,
          height: 18,
          bottom: 8,
          filterMode: 'none',
          borderColor: 'rgba(34, 211, 238, 0.24)',
          backgroundColor: 'rgba(255,255,255,0.05)',
          fillerColor: 'rgba(34,211,238,0.2)',
          handleSize: 12,
          handleStyle: {
            color: '#22d3ee',
            borderColor: '#22d3ee'
          },
          labelFormatter: (value: any) => dayjs(value).format('YYYY-MM-DD\nHH:mm:ss'),
          textStyle: {
            color: '#94a3b8',
            fontSize: 10,
            lineHeight: 12
          }
        }
      ],
      yAxis: getTrendYAxisOptions(),
      legend: {
        data: dimensions.value.map((item) => item.name),
        selected: selectedDimension.value,
        show: false
      },
      series: dimensions.value.map((item) => ({
        id: item.name,
        name: item.name,
        type: 'line',
        smooth: 0.24,
        showSymbol: false,
        connectNulls: true,
        endLabel: {
          show: true,
          formatter: (params: any) => formatChartValue(params.value?.[2]),
          offset: [6, 0],
          color: item.color,
          fontSize: 12,
          fontWeight: 'bold'
        },
        emphasis: {
          focus: 'series',
          lineStyle: {
            width: 3
          }
        },
        lineStyle: {
          width: 2,
          shadowColor: 'rgba(15, 23, 42, 0.16)',
          shadowBlur: 8,
          shadowOffsetY: 4
        },
        color: item.color,
        data: buildSeriesData(item.name)
      }))
    },
    true
  )
}

const chartLoading = ref(false)

async function initLoadChartData(real_time: boolean = true) {
  if (!dimensions.value.length) return

  const loadVersion = ++chartLoadVersion
  chartRealtimeMode.value = real_time
  chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
  chartLoading.value = true

  try {
    await Promise.all(
      dimensions.value.map(async (item) => {
        item.response = true
        try {
          const res = await IotStatApi.getDeviceInfoChartly(
            props.deviceCode,
            item.identifier,
            props.date[0],
            props.date[1]
          )

          chartData.value[item.name] = (((res as any[]) ?? []) as any[])
            .sort((a, b) => a.ts - b.ts)
            .map((item) => ({ ts: item.ts, value: item.value }))
        } catch (error) {
          console.error(error)
        } finally {
          item.response = false
        }
      })
    )

    if (loadVersion !== chartLoadVersion) return

    const latestTs = getLatestTimestamp() ?? dayjs(props.date[1]).valueOf()
    if (real_time) {
      trimChartDataToRealtimeWindow(latestTs)
      applyRealtimeWindow(latestTs)
    } else {
      applySelectedDateWindow()
    }
    updateSeriesByNames(dimensions.value.map((item) => item.name))
  } finally {
    chartLoading.value = false
  }

  if (real_time && loadVersion === chartLoadVersion) {
    connect(`wss://aims.deepoil.cc/mqtt`, { password: props.token }, handleMessageUpdate)
  }
}

async function initfn(load: boolean = true, real_time: boolean = true) {
  if (load) await loadDimensions()
  await nextTick()
  chartRealtimeMode.value = real_time
  render()
  await initLoadChartData(real_time)
}

onMounted(() => {
  initfn(true, props.isRealTime)
})

watch(
  () => [props.date, props.isRealTime] as const,
  async ([newDate, isRealTime], [oldDate, oldIsRealTime]) => {
    if (!newDate || newDate.length !== 2) return

    if (
      oldDate &&
      newDate[0] === oldDate[0] &&
      newDate[1] === oldDate[1] &&
      isRealTime === oldIsRealTime
    ) {
      return
    }

    await cancelAllRequests()

    destroy()
    cancelRealtimeChartUpdate()
    chartLoadVersion += 1

    if (chart) chart.clear()

    await initfn(false, isRealTime)
  }
)

onUnmounted(() => {
  chartLoadVersion += 1
  destroy()
  cancelRealtimeChartUpdate()

  window.removeEventListener('resize', resizeChart)
  chart?.dispose()
  chart = null
})

const router = useRouter()

function handleDetailClick() {
  router.push({
    name: 'MonitoringDetail',
    query: {
      id: props.id,
      ifInline: props.ifInline,
      carOnline: props.carOnline,
      time: props.lastInlineTime,
      name: displayDeviceName.value,
      code: props.deviceCode,
      dept: props.deptName,
      vehicle: props.vehicleName,
      mqttUrl: props.mqttUrl
    }
  })
}

const { ZmTable, ZmTableColumn } = useTableComponents<RangeSettingDraft>()
const settingsDialogVisible = ref(false)
const settingsSaving = ref(false)
const deviceNameDraft = ref('')
const rangeSettingDrafts = ref<RangeSettingDraft[]>([])

function buildRangeSettingDrafts() {
  rangeSettingDrafts.value = dimensions.value.map((item) => ({
    name: item.name,
    identifier: item.identifier,
    color: item.color,
    minValue: normalizeRangeValue(item.minValue),
    maxValue: normalizeRangeValue(item.maxValue)
  }))
}

function openSettingsDialog() {
  deviceNameDraft.value = currentOtherName.value || props.deviceName
  buildRangeSettingDrafts()
  settingsDialogVisible.value = true
}

async function saveDeviceName() {
  const nextName = deviceNameDraft.value.trim()

  if (!nextName) {
    throw new Error('设备名称不能为空')
  }

  if (nextName === (currentOtherName.value || props.deviceName)) return

  await IotDeviceApi.createLyDevice({
    deviceId: props.id,
    deviceCode: props.deviceCode,
    deviceName: props.deviceName,
    otherName: nextName
  })

  currentOtherName.value = nextName
  emit('other-name-updated', {
    id: props.id,
    otherName: nextName
  })
}

async function saveRangeSetting(item: Dimensions, minValue?: number, maxValue?: number) {
  const min = normalizeRangeValue(minValue)
  const max = normalizeRangeValue(maxValue)

  if (min === undefined && max === undefined) {
    if (item.id) {
      await IotDeviceApi.deleteMaxMin({ id: item.id })
    }

    item.minValue = undefined
    item.maxValue = undefined
    item.id = undefined
    return
  }

  if (min === undefined || max === undefined) {
    throw new Error(`${item.name} 的最大值和最小值需要同时填写`)
  }

  if (min > max) {
    throw new Error(`${item.name} 的最小值不能大于最大值`)
  }

  if (min === max) {
    throw new Error(`${item.name} 的最大值和最小值不能相等`)
  }

  const body = {
    minValue: min,
    maxValue: max,
    deviceId: props.id,
    propertyCode: item.identifier,
    alarmProperty: item.name,
    deviceName: props.deviceName,
    id: item.id
  }

  const res = await IotDeviceApi.saveMaxMin(body)

  if (res.id) item.id = res.id
  item.minValue = min
  item.maxValue = max
}

async function handleSettingsSave() {
  settingsSaving.value = true
  try {
    await saveDeviceName()

    for (const draft of rangeSettingDrafts.value) {
      const item = dimensions.value.find((dimension) => dimension.identifier === draft.identifier)
      if (!item) continue

      await saveRangeSetting(item, draft.minValue, draft.maxValue)
    }

    updateSeriesByNames(dimensions.value.map((item) => item.name))
    settingsDialogVisible.value = false
    message.success('设置成功')
  } catch (error: any) {
    message.warning(error?.message || '设置失败')
  } finally {
    settingsSaving.value = false
  }
}

function handleRangeDialogReset() {
  rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
    ...item,
    minValue: undefined,
    maxValue: undefined
  }))
}

function clearRangeDraft(row: RangeSettingDraft) {
  row.minValue = undefined
  row.maxValue = undefined
}
</script>
<template>
  <div class="rounded-lg chart-container flex flex-col">
    <header class="chart-header justify-between">
      <div class="flex items-center">
        <div class="title-icon"></div>
        <div>{{ `${props.deviceCode}-${displayDeviceName}` }}</div>
      </div>
      <div class="chart-header__actions">
        <el-button link type="primary" :icon="Setting" @click.stop="openSettingsDialog">
          设置
        </el-button>
        <el-button link type="primary" class="group" @click.stop="handleDetailClick">
          详情
          <div
            class="i-material-symbols:arrow-right-alt-rounded size-4 transition-transform group-hover:translate-x-1"></div>
        </el-button>
      </div>
    </header>
    <main class="chart-main" v-loading="chartLoading" element-loading-background="transparent">
      <div class="chart-legend">
        <button
          v-for="item in chartLegendItems"
          :key="item.name"
          type="button"
          class="chart-legend__item"
          :class="{ 'is-active': item.selected }"
          :aria-pressed="item.selected"
          :style="{
            '--theme-color': item.color,
            '--theme-bg-hover': item.bgHover
          }"
          @click="handleClickSpec(item.name)">
          <span class="chart-legend__range">{{ item.max }}</span>
          <span class="chart-legend__line"></span>
          <span class="chart-legend__name">{{ item.name }}</span>
          <span class="chart-legend__line"></span>
          <span class="chart-legend__range">{{ item.min }}</span>
        </button>
      </div>

      <div class="chart-stage">
        <div ref="chartRef" class="chart-canvas"></div>
      </div>
    </main>

    <el-dialog
      v-model="settingsDialogVisible"
      title="设备设置"
      width="900px"
      class="board-settings-dialog"
      modal-class="board-settings-overlay"
      append-to-body
      destroy-on-close>
      <el-form label-width="90px" class="board-settings-form">
        <el-form-item label="设备名称" required>
          <el-input
            v-model="deviceNameDraft"
            maxlength="50"
            show-word-limit
            placeholder="请输入设备名称" />
        </el-form-item>
      </el-form>

      <div class="board-settings-section">
        <div class="board-settings-section__title">曲线量程设置</div>
        <ZmTable :data="rangeSettingDrafts" :loading="false" :max-height="420" :show-border="true">
          <ZmTableColumn label="曲线" min-width="180">
            <template #default="{ row }">
              <div class="flex items-center gap-2 min-w-0">
                <span
                  class="inline-block size-2.5 rounded-full shrink-0"
                  :style="{ backgroundColor: row.color }"></span>
                <span class="board-settings-line-name">{{ row.name }}</span>
              </div>
            </template>
          </ZmTableColumn>
          <ZmTableColumn label="最小值" width="180">
            <template #default="{ row }">
              <el-input-number
                v-model="row.minValue"
                class="!w-full"
                :controls="false"
                :placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
            </template>
          </ZmTableColumn>
          <ZmTableColumn label="最大值" width="180">
            <template #default="{ row }">
              <el-input-number
                v-model="row.maxValue"
                class="!w-full"
                :controls="false"
                :placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
            </template>
          </ZmTableColumn>
          <ZmTableColumn label="当前使用范围" width="170">
            <template #default="{ row }">
              <span class="board-settings-range-text">
                {{ formatChartValue(getSeriesRange(row.name).labelMin) }}
                -
                {{ formatChartValue(getSeriesRange(row.name).labelMax) }}
              </span>
            </template>
          </ZmTableColumn>
          <ZmTableColumn label="操作" width="90">
            <template #default="{ row }">
              <el-button size="small" link type="primary" @click="clearRangeDraft(row)">
                清空
              </el-button>
            </template>
          </ZmTableColumn>
        </ZmTable>
      </div>

      <template #footer>
        <div class="flex justify-between">
          <el-button @click="handleRangeDialogReset">全部清空</el-button>
          <div class="flex gap-2">
            <el-button @click="settingsDialogVisible = false">取消</el-button>
            <el-button type="primary" :loading="settingsSaving" @click="handleSettingsSave">
              保存
            </el-button>
          </div>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<style scoped>
.chart-container {
  position: relative;
  min-height: 0;
  overflow: hidden;
  background-color: rgb(11 17 33 / 90%);
  border: 2px solid rgb(34 211 238 / 30%);
  box-shadow:
    0 0 20px rgb(0 0 0 / 80%),
    inset 0 0 15px rgb(34 211 238 / 10%);
  transition:
    border-color 0.3s ease,
    transform 0.3s ease;
}

.chart-container::before {
  position: absolute;
  pointer-events: none;
  background: radial-gradient(
    400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
    rgb(34 211 238 / 5%),
    transparent 40%
  );
  content: '';
  inset: 0;
}

.chart-container:hover {
  border-color: rgb(34 211 238 / 60%);
  transform: scale(1.005);
}

.chart-header {
  display: flex;
  padding: 12px 16px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 1px;
  color: #e2e8f0;
  background: rgb(255 255 255 / 3%);
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(to right, rgb(34 211 238 / 50%), transparent) 1;
  align-items: center;
}

.chart-header__actions {
  display: flex;
  flex-shrink: 0;
  gap: 10px;
  align-items: center;
}

.title-icon {
  width: 4px;
  height: 16px;
  margin-right: 10px;
  background: #22d3ee;
  box-shadow: 0 0 8px #22d3ee;
}

.board-settings-form {
  padding-right: 16px;
}

.board-settings-section {
  margin-top: 14px;
}

.board-settings-section__title {
  margin-bottom: 10px;
  font-size: 14px;
  font-weight: 700;
  color: #334155;
}

.chart-main {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  flex: 1;
  gap: 10px;
  min-height: 0;
  padding: 12px;
  background-image: radial-gradient(circle at 50% 50%, rgb(34 211 238 / 10%) 0%, transparent 80%),
    linear-gradient(135deg, rgb(11 17 33 / 90%) 0%, rgb(6 9 18 / 95%) 100%);
  background-size:
    100% 100%,
    100% 100%;
}

.chart-legend {
  display: grid;
  grid-auto-columns: 32px;
  grid-auto-flow: column;
  align-items: stretch;
  width: max-content;
  max-width: min(296px, 28vw);
  min-width: 34px;
  min-height: 0;
  padding-bottom: 4px;
  overflow: auto hidden;
  user-select: none;
  gap: 4px;
}

.chart-legend__item {
  position: relative;
  display: grid;
  width: 32px;
  height: 100%;
  min-height: 128px;
  padding: 8px 4px;
  font: inherit;
  color: #94a3b8;
  cursor: pointer;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  outline: none;
  box-sizing: border-box;
  transition:
    color 0.2s ease,
    background-color 0.2s ease,
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    opacity 0.2s ease;
  appearance: none;
  grid-template-rows: auto 1fr auto 1fr auto;
  place-items: center;
}

.chart-legend__item:hover {
  background: var(--theme-bg-hover);
  border-color: rgb(34 211 238 / 32%);
  box-shadow: 0 0 14px rgb(34 211 238 / 12%);
}

.chart-legend__item:not(.is-active) {
  opacity: 0.48;
}

.chart-legend__item.is-active {
  color: var(--theme-color);
}

.chart-legend__range {
  font-size: 10px;
  line-height: 1;
  color: #94a3b8;
  writing-mode: sideways-lr;
}

.chart-legend__name {
  max-width: 104px;
  overflow: hidden;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  color: currentcolor;
  text-overflow: ellipsis;
  white-space: nowrap;
  writing-mode: sideways-lr;
}

.chart-legend__line {
  width: 2px;
  height: 100%;
  min-height: 18px;
  background: var(--theme-color);
  border-radius: 999px;
  opacity: 0.42;
}

.chart-stage {
  position: relative;
  min-width: 0;
  min-height: 240px;
  overflow: hidden;
  background: rgb(2 6 23 / 22%);
  border: 1px dashed rgb(34 211 238 / 24%);
  border-radius: 8px;
}

.chart-canvas {
  width: 100%;
  height: 100%;
  min-height: 240px;
}

@media (width <= 768px) {
  .chart-main {
    grid-template-columns: minmax(0, 1fr);
  }

  .chart-legend {
    max-width: none;
    min-height: auto;
    overflow-x: auto;
  }

  .chart-legend__item {
    grid-template-rows: none;
    grid-template-columns: auto 1fr auto;
    gap: 6px;
    width: auto;
    min-width: 132px;
    min-height: 32px;
  }

  .chart-legend__name,
  .chart-legend__range {
    writing-mode: horizontal-tb;
  }

  .chart-legend__line {
    display: none;
  }
}
</style>
