/* 任务列表页 — 2026-03-13 全量重写，1:1 对齐 H5 原型 task-list.html */
/* AI_CHANGELOG
| 日期 | Prompt | 变更 |
|------|--------|------|
| 2026-03-13 | 重写 task-list 1:1 还原 H5 | 对齐 H5 四种任务类型、border-left 彩条颜色、圆形红戳、mock 数据贴近原型 |
| 2026-03-13 | banner 错位重做 | 添加 userName/userRole/storeName 到 page data；移除 banner 组件引用 |
| 2026-03-13 | 任务类型标签做成 SVG | 添加 tagSvgMap 映射 taskType→SVG 路径，供 WXML image 组件使用 |
| 2026-03-13 | 5项修复+精确还原 | 移除 tagSvgMap（标签恢复 CSS 渐变实现） |
*/
import { mockTasks, mockPerformance } from '../../utils/mock-data'
import type { Task } from '../../utils/mock-data'
import { formatMoney } from '../../utils/money'
import { formatDeadline } from '../../utils/time'

/** CHANGE 2026-03-14 | 所有任务类型统一跳转到 task-detail，由详情页根据 taskId 动态展示内容 */
const DETAIL_ROUTE = '/pages/task-detail/task-detail'

/* ╔══════════════════════════════════════════════════════╗
 * ║           进度条动画参数 — 在此调节                    ║
 * ╚══════════════════════════════════════════════════════╝
 *
 * 动画由 JS 状态机驱动，每轮循环重新读取实际进度条宽度：
 *
 *  ┌─────────────┐  SPARK_DELAY_MS  ┌─────────────┐  NEXT_LOOP_DELAY_MS  ┌─────────────┐
 *  │  高光匀速扫过 │ ───────────────▶ │  火花迸发    │ ──────────────────▶ │  下一轮      │
 *  │ 时长由速度决定│                  │ SPARK_DUR_MS│                    │（重新读进度） │
 *  └─────────────┘                  └─────────────┘                    └─────────────┘
 *
 *  SHINE_SPEED      : 高光移动速度，范围 1~100
 *                     1   = 最慢，最宽进度条（100%）下 5 秒走完
 *                     100 = 最快，最宽进度条（100%）下 0.05 秒走完
 *                     实际时长 = 基准时长 × (filledPct/100)
 *                     基准时长 = 5s - (SHINE_SPEED-1)/(100-1) × (5-0.05)s
 *
 *  SPARK_DELAY_MS   : 高光到达最右端后，光柱点亮+火花迸发的延迟（毫秒）
 *                     正数 = 高光结束后停顿再点亮
 *                     负数 = 高光还未结束，提前点亮（高光末尾与火花重叠）
 *
 *  SPARK_DUR_MS     : 火花迸发到完全消散的时长（毫秒）
 *
 *  NEXT_LOOP_DELAY_MS: 火花消散后到下一轮高光启动的延迟（毫秒）
 *                      正数 = 停顿一段时间
 *                      负数 = 火花还未消散完，高光已从左端启动
 */
const SHINE_SPEED         = 70    // 1~100，速度值
const SPARK_DELAY_MS      = -200     // 毫秒，高光结束→光柱点亮+火花（负=提前）
const SPARK_DUR_MS        = 1400  // 毫秒，火花持续时长
const NEXT_LOOP_DELAY_MS  = 400   // 毫秒，火花结束→下轮高光（负=提前）

/* 根据速度值和进度百分比计算高光时长
 * 高光宽度固定（SHINE_WIDTH_RPX），需要走过的距离 = 填充条宽度 + 高光宽度
 * 轨道宽度约 634rpx（750 - 左右padding各58rpx），高光宽度约占轨道 19%
 * 时长正比于需要走过的总距离，保证视觉速度恒定
 *
 * 速度1  → baseDur=5000ms（最慢），速度100 → baseDur=50ms（最快）
 * shineDurMs = baseDur × (filledPct + SHINE_WIDTH_PCT) / (100 + SHINE_WIDTH_PCT)
 */
const SHINE_WIDTH_RPX  = 120  // rpx，需与 WXSS 的 --shine-width 保持一致
const TRACK_WIDTH_RPX  = 634  // rpx，进度条轨道宽度（750 - padding 116rpx）
const SHINE_WIDTH_PCT  = (SHINE_WIDTH_RPX / TRACK_WIDTH_RPX) * 100  // ≈19%

function calcShineDur(filledPct: number): number {
  const t = (SHINE_SPEED - 1) / 99  // 0(最慢) ~ 1(最快)
  const baseDur = 5000 - t * (5000 - 50)  // ms，走完100%进度条所需时长
  // 实际距离 = 填充条 + 高光自身，相对于(100% + 高光宽度%)归一化
  const distRatio = (filledPct + SHINE_WIDTH_PCT) / (100 + SHINE_WIDTH_PCT)
  return Math.max(50, Math.round(baseDur * distRatio))
}

/** 扩展任务字段 */
interface EnrichedTask extends Task {
  lastVisitDays: number
  balanceLabel: string
  aiSuggestion: string
  abandonReason?: string
  isAbandoned: boolean
  deadlineLabel: string
  deadlineStyle: 'normal' | 'warning' | 'danger' | 'muted'
}

/** 刻度项 */
interface TickItem {
  value: number    // 刻度数值（如 100）
  label: string    // 显示文字（如 '100'）
  left: string     // 百分比位置字符串（如 '45.45%'），首尾项忽略此字段
  highlight: boolean // 是否加粗高亮
}

/** Mock: 根据档位节点数组生成刻度数据 */
function buildTicks(tierNodes: number[], maxHours: number): TickItem[] {
  return tierNodes.map((v, i) => ({
    value: v,
    label: String(v),
    left: `${Math.round((v / maxHours) * 10000) / 100}%`,
    highlight: i === 2, // 第3个档位（如130h）高亮，可由接口控制
  }))
}

/** P0: 业绩进度卡片数据 */
interface PerfData {
  nextTierHours: number
  remainHours: number
  currentTier: number
  tierProgress: number
  filledPct: number
  clampedSparkPct: number
  ticks: TickItem[]        // 刻度数组，由接口传入（Mock 时由 buildTicks 生成）
  shineDurMs: number
  sparkDurMs: number
  shineRunning: boolean
  sparkRunning: boolean
  basicHours: string
  bonusHours: string
  totalHours: string
  tierCompleted: boolean
  bonusMoney: string
  incomeMonth: string
  prevMonth: string
  incomeFormatted: string
  incomeTrend: string
  incomeTrendDir: 'up' | 'down'
}

/** Mock: 为任务附加扩展字段 */
function enrichTask(task: Task): EnrichedTask {
  const daysSeed = (task.id.charCodeAt(task.id.length - 1) % 15) + 1
  const balanceSeedNum = ((task.id.charCodeAt(task.id.length - 1) * 137) % 5000) + 200
  const suggestions = [
    '建议推荐斯诺克进阶课程，提升客户粘性',
    '客户近期消费下降，建议电话关怀了解原因',
    '适合推荐周末球友赛活动，增强社交体验',
    '高价值客户，建议维护关系并推荐VIP权益',
    '新客户首次体验后未续费，建议跟进意向',
  ]
  const suggIdx = task.id.charCodeAt(task.id.length - 1) % suggestions.length

  return {
    ...task,
    lastVisitDays: daysSeed,
    balanceLabel: formatMoney(balanceSeedNum),
    aiSuggestion: suggestions[suggIdx],
    isAbandoned: task.status === 'abandoned',
    abandonReason: task.status === 'abandoned' ? '客户已转至其他门店' : undefined,
    deadlineLabel: formatDeadline((task as any).deadline).text,
    deadlineStyle: formatDeadline((task as any).deadline).style,
  }
}

/** Mock: 构造业绩进度卡片数据 — 对齐 H5 原型数值 */
function buildPerfData(): PerfData {
  const total = 87.5
  const filledPct = Math.min(100, parseFloat(((total / 220) * 100).toFixed(1)))
  // Mock 档位节点：实际由接口返回，格式为 number[]
  const tierNodes = [0, 100, 130, 160, 190, 220]
  return {
    nextTierHours: 100,
    remainHours: 12.5,
    currentTier: 1,
    tierProgress: 58,
    filledPct,
    clampedSparkPct: Math.max(0, Math.min(100, filledPct)),
    ticks: buildTicks(tierNodes, 220),
    shineDurMs: calcShineDur(filledPct),
    sparkDurMs: SPARK_DUR_MS,
    shineRunning: false,
    sparkRunning: false,
    basicHours: '77.5',
    bonusHours: '12',
    totalHours: String(total),
    tierCompleted: true,
    bonusMoney: '800',
    incomeMonth: '2月',
    prevMonth: '1月',
    incomeFormatted: '6,206',
    incomeTrend: '↓368',
    incomeTrendDir: 'down',
  }
}

Page({
  data: {
    pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal',
    pinnedTasks: [] as EnrichedTask[],
    normalTasks: [] as EnrichedTask[],
    abandonedTasks: [] as EnrichedTask[],
    taskCount: 0,
    bannerMetrics: [] as Array<{ label: string; value: string }>,
    bannerTitle: '',
    /* CHANGE 2026-03-13 | banner 重做：添加用户信息字段，对齐 H5 原型 */
    userName: '小燕',
    userRole: '助教',
    storeName: '广州朗朗桌球',
    avatarUrl: '/assets/images/avatar-coach.png', // MOCK 头像地址
    /* CHANGE 2026-03-13 | tagSvgMap 已移除，标签恢复 CSS 渐变实现 */
    perfData: {
      nextTierHours: 0,
      remainHours: 0,
      currentTier: 0,
      tierProgress: 0,
      filledPct: 0,
      clampedSparkPct: 0,
      ticks: [],
      shineDurMs: 1000,
      sparkDurMs: 1400,
      shineRunning: false,
      sparkRunning: false,
      basicHours: '0',
      bonusHours: '0',
      totalHours: '0',
      tierCompleted: false,
      bonusMoney: '0',
      incomeMonth: '',
      prevMonth: '',
      incomeFormatted: '0',
      incomeTrend: '',
      incomeTrendDir: 'up' as 'up' | 'down',
    } as PerfData,
    stampAnimated: false,
    hasMore: true,

    // --- 调试面板 ---
    showDebugPanel: false,
    debugTotalHours: 87.5,
    debugBasicHours: 77.5,
    debugBonusHours: 12,
    debugPreset: -1,

    contextMenuVisible: false,
    contextMenuX: 0,
    contextMenuY: 0,
    contextMenuTarget: {} as EnrichedTask,

    abandonModalVisible: false,
    abandonTarget: {} as EnrichedTask,

    noteModalVisible: false,
    noteTarget: {} as EnrichedTask,
    aiColor: 'indigo', // 随机 AI 配色，页面加载时随机选取
  },

  _longPressed: false,
  _animTimer: null as ReturnType<typeof setTimeout> | null,

  onLoad() {
    const colors = ['red', 'orange', 'yellow', 'blue', 'indigo', 'purple']
    const aiColor = colors[Math.floor(Math.random() * colors.length)]
    this.setData({ aiColor })
    this.loadData()
  },

  onReady() {
    // 页面渲染完成后启动动画循环
    setTimeout(() => {
      this.setData({ stampAnimated: true })
      this._startAnimLoop()
    }, 100)
  },

  onShow() {
    // 每次显示页面时重新随机 AI 配色，并恢复动画循环
    const colors = ['red', 'orange', 'yellow', 'blue', 'indigo', 'purple']
    const aiColor = colors[Math.floor(Math.random() * colors.length)]
    this.setData({ aiColor })
    const tabBar = this.getTabBar?.()
    if (tabBar) tabBar.setData({ active: 'task' })
    // 若页面从后台恢复，重启动画循环
    if (this.data.pageState === 'normal' && !this._animTimer) {
      this._startAnimLoop()
    }
  },

  onHide() {
    // 页面不可见时停止动画，节省性能
    this._stopAnimLoop()
  },

  onUnload() {
    this._stopAnimLoop()
  },

  /* ──────────────────────────────────────────────────────
   * JS 动画状态机
   * 每轮流程：
   *   1. 读当前 filledPct，重新计算 shineDurMs
   *   2. 启动高光（shineRunning=true），等 shineDurMs
   *   3. 等 SPARK_DELAY_MS（可为负，即与高光末尾重叠）
   *   4. 启动火花（sparkRunning=true），等 SPARK_DUR_MS
   *   5. 停止火花（sparkRunning=false）
   *   6. 等 NEXT_LOOP_DELAY_MS（可为负，即提前启动下轮）
   *   7. 回到第1步
   * ────────────────────────────────────────────────────── */
  _startAnimLoop() {
    this._stopAnimLoop() // 防止重复启动
    this._runAnimStep()
  },

  _stopAnimLoop() {
    if (this._animTimer !== null) {
      clearTimeout(this._animTimer)
      this._animTimer = null
    }
    // 停止时重置动画状态
    this.setData({
      'perfData.shineRunning': false,
      'perfData.sparkRunning': false,
    })
  },

  _runAnimStep() {
    // 每轮开始时重新读当前进度，重新计算高光时长
    const filledPct = this.data.perfData.filledPct ?? 0
    const shineDurMs = calcShineDur(filledPct)

    console.log(`[动画] filledPct=${filledPct}% shineDurMs=${shineDurMs}ms`)

    // 阶段1：启动高光
    this.setData({
      'perfData.shineRunning': true,
      'perfData.sparkRunning': false,
      'perfData.shineDurMs': shineDurMs,
    })

    // 阶段2：高光结束后 + SPARK_DELAY_MS → 点亮光柱+火花
    // 若 SPARK_DELAY_MS 为负，高光还未结束就提前点火
    const sparkTriggerDelay = Math.max(0, shineDurMs + SPARK_DELAY_MS)
    this._animTimer = setTimeout(() => {
      // 阶段3：火花迸发
      this.setData({ 'perfData.sparkRunning': true })

      // 阶段4：火花持续 SPARK_DUR_MS 后熄灭
      this._animTimer = setTimeout(() => {
        this.setData({
          'perfData.shineRunning': false,
          'perfData.sparkRunning': false,
        })

        // 阶段5：等 NEXT_LOOP_DELAY_MS 后启动下一轮
        const nextDelay = Math.max(0, NEXT_LOOP_DELAY_MS)
        this._animTimer = setTimeout(() => {
          this._runAnimStep()
        }, nextDelay)

      }, SPARK_DUR_MS)
    }, sparkTriggerDelay)
  },

  onPullDownRefresh() {
    this.loadData(() => {
      wx.stopPullDownRefresh()
    })
  },

  onReachBottom() {
    if (!this.data.hasMore) return
    this.setData({ hasMore: false })
    wx.showToast({ title: '没有更多了', icon: 'none' })
  },

  loadData(cb?: () => void) {
    this.setData({ pageState: 'loading', stampAnimated: false })

    setTimeout(() => {
      /* CHANGE 2026-03-13 | mock 数据贴近 H5 原型：7 条任务（2 置顶 + 3 一般 + 2 已放弃） */
      const allTasks: Task[] = [
        ...mockTasks,
        {
          id: 'task-007',
          customerName: '孙丽',
          customerAvatar: '/assets/images/avatar-default.png',
          taskType: 'callback',
          taskTypeLabel: '客户回访',
          deadline: '2026-03-06',
          heartScore: 3.5,
          hobbies: [],
          isPinned: false,
          hasNote: false,
          status: 'abandoned',
        },
      ]

      const enriched = allTasks.map(enrichTask)

      const pinnedTasks = enriched.filter((t) => t.isPinned && !t.isAbandoned)
      const normalTasks = enriched.filter((t) => !t.isPinned && !t.isAbandoned && t.status === 'pending')
      const abandonedTasks = enriched.filter((t) => t.isAbandoned)
      const totalCount = pinnedTasks.length + normalTasks.length + abandonedTasks.length

      const perfData = buildPerfData()
      const perf = mockPerformance
      const bannerTitle = `${perf.currentTier}`
      const bannerMetrics: Array<{ label: string; value: string }> = []

      this.setData({
        pageState: totalCount > 0 ? 'normal' : 'empty',
        pinnedTasks,
        normalTasks,
        abandonedTasks,
        taskCount: totalCount,
        bannerTitle,
        bannerMetrics,
        perfData,
        hasMore: true,
      })

      if (perfData.tierCompleted) {
        setTimeout(() => {
          this.setData({ stampAnimated: true })
        }, 300)
      }

      cb?.()
    }, 600)
  },

  onRetry() {
    this.loadData()
  },

  onTaskTap(e: WechatMiniprogram.TouchEvent) {
    if (this._longPressed) {
      this._longPressed = false
      return
    }
    const { id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `${DETAIL_ROUTE}?id=${id}`,
      fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
    })
  },

  onPerformanceTap() {
    wx.navigateTo({
      url: '/pages/performance/performance',
      fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
    })
  },

  onTaskLongPress(e: WechatMiniprogram.TouchEvent) {
    this._longPressed = true
    const { group, index } = e.currentTarget.dataset
    const groupKey = group as 'pinned' | 'normal' | 'abandoned'
    const groupMap: Record<string, EnrichedTask[]> = {
      pinned: this.data.pinnedTasks,
      normal: this.data.normalTasks,
      abandoned: this.data.abandonedTasks,
    }
    const target = groupMap[groupKey]?.[index]
    if (!target) return

    const touch = e.touches[0]
    const sysInfo = wx.getSystemInfoSync()
    const menuW = 175
    const menuH = 220
    let x = touch.clientX
    let y = touch.clientY

    if (x + menuW > sysInfo.windowWidth - 16) x = sysInfo.windowWidth - menuW - 16
    if (x < 16) x = 16
    if (y + menuH > sysInfo.windowHeight - 16) y = sysInfo.windowHeight - menuH - 16
    if (y < 16) y = 16

    this.setData({
      contextMenuVisible: true,
      contextMenuX: x,
      contextMenuY: y,
      contextMenuTarget: target,
    })
  },

  onCloseContextMenu() {
    this.setData({ contextMenuVisible: false })
  },

  noop() {},

  onCtxPin() {
    const target = this.data.contextMenuTarget
    const isPinned = !target.isPinned
    wx.showToast({
      title: isPinned ? `已置顶「${target.customerName}」` : `已取消置顶「${target.customerName}」`,
      icon: 'none',
    })
    this.setData({ contextMenuVisible: false })
    this._updateTaskPin(target.id, isPinned)
  },

  onCtxNote() {
    const target = this.data.contextMenuTarget
    this.setData({
      contextMenuVisible: false,
      noteModalVisible: true,
      noteTarget: target,
    })
  },

  onCtxAI() {
    const target = this.data.contextMenuTarget
    this.setData({ contextMenuVisible: false })
    wx.navigateTo({
      url: `/pages/ai-chat/ai-chat?taskId=${target.id}&customerName=${target.customerName}`,
      fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
    })
  },

  onCtxAbandon() {
    const target = this.data.contextMenuTarget
    this.setData({
      contextMenuVisible: false,
      abandonModalVisible: true,
      abandonTarget: target,
    })
  },

  /** 放弃弹窗 - 确认 */
  onAbandonConfirm(e: WechatMiniprogram.CustomEvent<{ reason: string }>) {
    const { reason } = e.detail
    const target = this.data.abandonTarget
    wx.showToast({ title: `已放弃「${target.customerName}」`, icon: 'none' })
    this.setData({ abandonModalVisible: false })
    this._updateTaskAbandon(target.id, reason)
  },

  /** 放弃弹窗 - 取消 */
  onAbandonCancel() {
    this.setData({ abandonModalVisible: false })
  },

  /** 长按菜单 - 取消放弃（已放弃任务） */
  onCtxCancelAbandon() {
    const target = this.data.contextMenuTarget
    this.setData({ contextMenuVisible: false })
    wx.showLoading({ title: '处理中...' })
    setTimeout(() => {
      wx.hideLoading()
      wx.showToast({ title: `已取消放弃「${target.customerName}」`, icon: 'success' })
      this._updateTaskCancelAbandon(target.id)
    }, 500)
  },

  onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string }>) {
    const { score, content } = e.detail
    const target = this.data.noteTarget
    wx.showToast({ title: `已保存「${target.customerName}」备注`, icon: 'success' })
    this.setData({ noteModalVisible: false })
    console.log('[note]', target.id, score, content)
  },

  onNoteCancel() {
    this.setData({ noteModalVisible: false })
  },

  _updateTaskPin(taskId: string, isPinned: boolean) {
    const allTasks = [
      ...this.data.pinnedTasks,
      ...this.data.normalTasks,
      ...this.data.abandonedTasks,
    ].map((t) => (t.id === taskId ? { ...t, isPinned } : t))

    this.setData({
      pinnedTasks: allTasks.filter((t) => t.isPinned && !t.isAbandoned),
      normalTasks: allTasks.filter((t) => !t.isPinned && !t.isAbandoned),
      abandonedTasks: allTasks.filter((t) => t.isAbandoned),
    })
  },

  _updateTaskAbandon(taskId: string, reason: string) {
    const allTasks = [
      ...this.data.pinnedTasks,
      ...this.data.normalTasks,
      ...this.data.abandonedTasks,
    ].map((t) =>
      t.id === taskId
        ? { ...t, isAbandoned: true, isPinned: false, status: 'abandoned' as const, abandonReason: reason }
        : t,
    )

    const pinnedTasks = allTasks.filter((t) => t.isPinned && !t.isAbandoned)
    const normalTasks = allTasks.filter((t) => !t.isPinned && !t.isAbandoned)
    const abandonedTasks = allTasks.filter((t) => t.isAbandoned)

    this.setData({ pinnedTasks, normalTasks, abandonedTasks, taskCount: allTasks.length })
  },

  /** 切换调试面板 */
  toggleDebugPanel() {
    this.setData({ showDebugPanel: !this.data.showDebugPanel })
  },

  /** 调试 - 拖动总课时滑块 */
  onDebugTotalHours(e: WechatMiniprogram.SliderChange) {
    const total = e.detail.value
    this.setData({ debugTotalHours: total, debugPreset: -1 })
    this._applyDebugHours(this.data.debugBasicHours, this.data.debugBonusHours, total)
  },

  /** 调试 - 拖动基础课时滑块 */
  onDebugBasicHours(e: WechatMiniprogram.SliderChange) {
    const basic = e.detail.value
    const total = basic + this.data.debugBonusHours
    this.setData({ debugBasicHours: basic, debugTotalHours: total, debugPreset: -1 })
    this._applyDebugHours(basic, this.data.debugBonusHours, total)
  },

  /** 调试 - 拖动激励课时滑块 */
  onDebugBonusHours(e: WechatMiniprogram.SliderChange) {
    const bonus = e.detail.value
    const total = this.data.debugBasicHours + bonus
    this.setData({ debugBonusHours: bonus, debugTotalHours: total, debugPreset: -1 })
    this._applyDebugHours(this.data.debugBasicHours, bonus, total)
  },

  /** 调试 - 快速预设档位 */
  onDebugPreset(e: WechatMiniprogram.BaseEvent) {
    const preset = e.currentTarget.dataset.preset as number
    const presets: Array<{ basic: number; bonus: number; total: number }> = [
      { basic: 45,   bonus: 5,  total: 50   },  // 未完成 (段0中间)
      { basic: 90,   bonus: 10, total: 100  },  // 恰好达100h
      { basic: 115,  bonus: 15, total: 130  },  // 恰好达130h
      { basic: 145,  bonus: 15, total: 160  },  // 恰好达160h
      { basic: 195,  bonus: 25, total: 220  },  // 满档220h
    ]
    const p = presets[preset]
    if (!p) return
    this.setData({ debugBasicHours: p.basic, debugBonusHours: p.bonus, debugTotalHours: p.total, debugPreset: preset })
    this._applyDebugHours(p.basic, p.bonus, p.total)
  },

  /** 调试 - 切换盖戳动画 */
  onDebugToggleStamp() {
    const completed = !this.data.perfData.tierCompleted
    this.setData({
      'perfData.tierCompleted': completed,
      stampAnimated: false,
    })
    if (completed) {
      setTimeout(() => this.setData({ stampAnimated: true }), 50)
    }
  },

  /** 内部：根据课时数值重新计算档位进度并更新 perfData */
  _applyDebugHours(basic: number, bonus: number, total: number) {
    // 档位刻度：[0, 100, 130, 160, 190, 220]
    const tiers = [0, 100, 130, 160, 190, 220]
    let currentTier = 0
    for (let i = 1; i < tiers.length; i++) {
      if (total >= tiers[i]) currentTier = i
      else break
    }
    // 当前段内进度百分比
    const segStart = tiers[currentTier]
    const segEnd = tiers[currentTier + 1] ?? tiers[tiers.length - 1]
    const tierProgress = segEnd > segStart
      ? Math.min(100, Math.round(((total - segStart) / (segEnd - segStart)) * 100))
      : 100
    const nextTierHours = tiers[currentTier + 1] ?? 220
    const remainHours = Math.max(0, nextTierHours - total)
    const tierCompleted = total >= 220
    const filledPct = Math.min(100, Math.round((total / 220) * 1000) / 10)
    const tierNodes = [0, 100, 130, 160, 190, 220]  // Mock，实际由接口返回
    this.setData({
      'perfData.totalHours': String(total),
      'perfData.basicHours': String(basic),
      'perfData.bonusHours': String(bonus),
      'perfData.currentTier': currentTier,
      'perfData.tierProgress': tierProgress,
      'perfData.filledPct': filledPct,
      'perfData.clampedSparkPct': Math.max(0, Math.min(100, filledPct)),
      'perfData.ticks': buildTicks(tierNodes, 220),
      'perfData.shineDurMs': calcShineDur(filledPct),
      'perfData.sparkDurMs': SPARK_DUR_MS,
      'perfData.nextTierHours': nextTierHours,
      'perfData.remainHours': remainHours,
      'perfData.tierCompleted': tierCompleted,
      stampAnimated: false,
    })
    // 进度变化后重启动画循环，使下一轮立即用新进度重新计算高光时长
    this._startAnimLoop()
    if (tierCompleted) {
      setTimeout(() => this.setData({ stampAnimated: true }), 50)
    }
  },

  /** 取消放弃任务 - 将任务从已放弃列表移出至一般任务 */
  _updateTaskCancelAbandon(taskId: string) {
    const allTasks = [
      ...this.data.pinnedTasks,
      ...this.data.normalTasks,
      ...this.data.abandonedTasks,
    ].map((t) =>
      t.id === taskId
        ? { ...t, isAbandoned: false, isPinned: false, status: 'pending' as const, abandonReason: undefined }
        : t,
    )

    const pinnedTasks = allTasks.filter((t) => t.isPinned && !t.isAbandoned)
    const normalTasks = allTasks.filter((t) => !t.isPinned && !t.isAbandoned)
    const abandonedTasks = allTasks.filter((t) => t.isAbandoned)

    this.setData({ pinnedTasks, normalTasks, abandonedTasks, taskCount: allTasks.length })
  },
})
