<script setup lang="ts">
/**
 * 后台折线图：旧版 canvas，金色描边，适配暗色工作台。
 * 多系列时按同色相不同透明度区分；下方 HTML 图例标明发文/UV，避免「UV 没展示」的误解。
 * 小程序里 v-if 挂载后首帧 boundingClientRect 常为 0，需重试并设置原生宽高。
 * getCurrentInstance 仅在 setup/同步生命周期有效，必须在此处缓存 proxy。
 */
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'

const props = withDefaults(defineProps<{
  /** 横轴标签 */
  categories?: string[]
  /** 系列：{ name, data } */
  series?: { name: string, data: number[] }[]
  height?: number
}>(), {
  categories: () => [],
  series: () => [],
  height: 220,
})

const SERIES_COLORS = ['#d4b383', 'rgba(120,180,255,0.9)', 'rgba(160,220,160,0.9)']

/** setup 阶段同步缓存，供异步 draw 使用 */
const compProxy = getCurrentInstance()?.proxy as never
const canvasId = `line-${Math.random().toString(36).slice(2, 8)}`
/** 原生 canvas 缓冲像素宽（含 dpr），绑定到 width 属性才能在小程序上正常绘制 */
const canvasW = ref(300)
const canvasH = ref(220)
let retryTimer: ReturnType<typeof setTimeout> | null = null
let retryCount = 0
const MAX_RETRY = 5

const legendItems = computed(() =>
  (props.series || []).map((s, i) => ({
    name: s.name || `系列${i + 1}`,
    color: SERIES_COLORS[i % SERIES_COLORS.length],
  })),
)

/** 优先新 API，降级 getSystemInfoSync，减少弃用警告 */
function fallbackWidth(): number {
  try {
    const win = typeof uni.getWindowInfo === 'function' ? uni.getWindowInfo() : null
    if (win?.windowWidth) return Math.max(200, win.windowWidth - 48)
    const sys = uni.getSystemInfoSync()
    return Math.max(200, (sys.windowWidth || 375) - 48)
  } catch {
    return 300
  }
}

function getDpr(): number {
  try {
    const device = typeof uni.getDeviceInfo === 'function' ? uni.getDeviceInfo() : null
    if (device && 'pixelRatio' in device && Number(device.pixelRatio)) {
      return Number(device.pixelRatio)
    }
    return uni.getSystemInfoSync().pixelRatio || 2
  } catch {
    return 2
  }
}

/**
 * 构建选择器：有组件实例则限定在组件内，否则页面级查询（canvasId 已全局唯一）。
 * 禁止对 null 调用 .in()，否则 mp-weixin 读 $scope 直接报错。
 */
function createQuery() {
  const q = uni.createSelectorQuery()
  if (compProxy) return q.in(compProxy)
  return q
}

/**
 * 在逻辑像素坐标下绘制折线（调用前需已设好原生宽高并 scale）。
 */
function paint(ctx: UniApp.CanvasContext, w: number, h: number, dpr: number) {
  const cats = props.categories
  const series = props.series
  ctx.scale(dpr, dpr)
  const padL = 36
  const padR = 12
  const padT = 16
  const padB = 28
  const plotW = w - padL - padR
  const plotH = h - padT - padB

  ctx.clearRect(0, 0, w, h)
  const all = series.flatMap(s => s.data)
  const max = Math.max(1, ...all)
  const min = Math.min(0, ...all)
  const span = max - min || 1

  ctx.setStrokeStyle('rgba(255,255,255,0.08)')
  ctx.setLineWidth(1)
  for (let i = 0; i <= 3; i++) {
    const y = padT + (plotH * i) / 3
    ctx.beginPath()
    ctx.moveTo(padL, y)
    ctx.lineTo(w - padR, y)
    ctx.stroke()
  }

  series.forEach((s, si) => {
    const color = SERIES_COLORS[si % SERIES_COLORS.length]
    ctx.setStrokeStyle(color)
    ctx.setLineWidth(2)
    ctx.beginPath()
    s.data.forEach((v, i) => {
      const x = padL + (cats.length <= 1 ? plotW / 2 : (plotW * i) / (cats.length - 1))
      const y = padT + plotH - ((v - min) / span) * plotH
      if (i === 0) ctx.moveTo(x, y)
      else ctx.lineTo(x, y)
    })
    ctx.stroke()
    s.data.forEach((v, i) => {
      const x = padL + (cats.length <= 1 ? plotW / 2 : (plotW * i) / (cats.length - 1))
      const y = padT + plotH - ((v - min) / span) * plotH
      ctx.beginPath()
      ctx.arc(x, y, 3, 0, Math.PI * 2)
      ctx.setFillStyle(color)
      ctx.fill()
    })
  })

  ctx.setFillStyle('rgba(136,136,136,1)')
  ctx.setFontSize(10)
  const step = Math.max(1, Math.ceil(cats.length / 5))
  cats.forEach((c, i) => {
    if (i % step !== 0 && i !== cats.length - 1) return
    const x = padL + (cats.length <= 1 ? plotW / 2 : (plotW * i) / (cats.length - 1))
    ctx.fillText(String(c).slice(-5), x - 12, h - 8)
  })
  ctx.draw()
}

/**
 * 测量并绘制。宽度为 0 时用系统宽度兜底并短延迟重试；
 * 先写原生宽高再 nextTick 绘制，避免属性未同步导致空白。
 */
const draw = (isRetry = false) => {
  const cats = props.categories
  const series = props.series
  if (!cats.length || !series.length) return
  if (!isRetry) retryCount = 0

  createQuery()
    .select(`#${canvasId}`)
    .boundingClientRect((rect) => {
      const box = Array.isArray(rect) ? rect[0] : rect
      let w = box?.width || 0
      const measuredOk = !!w
      if (!w) w = fallbackWidth()
      if (!measuredOk && retryCount < MAX_RETRY) {
        retryCount += 1
        if (retryTimer) clearTimeout(retryTimer)
        retryTimer = setTimeout(() => draw(true), 80)
      } else if (measuredOk) {
        retryCount = 0
      }
      if (!w) return

      const h = props.height
      const dpr = getDpr()
      canvasW.value = Math.floor(w * dpr)
      canvasH.value = Math.floor(h * dpr)
      nextTick(() => {
        const ctx = uni.createCanvasContext(canvasId, compProxy)
        paint(ctx, w, h, dpr)
      })
    })
    .exec()
}

onMounted(() => nextTick(() => draw()))
onUnmounted(() => {
  if (retryTimer) clearTimeout(retryTimer)
})
watch(() => [props.categories, props.series], () => nextTick(() => draw()), { deep: true })
</script>

<template>
  <view class="line-wrap">
    <canvas
      :id="canvasId"
      :canvas-id="canvasId"
      class="chart"
      :width="canvasW"
      :height="canvasH"
      :style="{ width: '100%', height: `${height}px` }"
    />
    <view v-if="legendItems.length" class="legend">
      <view v-for="(item, i) in legendItems" :key="i" class="legend-item">
        <view class="dot" :style="{ background: item.color }" />
        <text class="legend-text">{{ item.name }}</text>
      </view>
    </view>
  </view>
</template>

<style scoped>
.line-wrap { width: 100%; }
.chart { width: 100%; display: block; }
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 20rpx;
  margin-top: 12rpx;
  padding: 0 8rpx;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 8rpx;
}
.dot {
  width: 16rpx;
  height: 16rpx;
  border-radius: 4rpx;
}
.legend-text {
  font-size: 22rpx;
  color: rgba(255, 255, 255, 0.55);
}
</style>
