/* 任务列表页 — 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 渐变实现） |
| 2026-03-21 | P13 前端打磨审查 | G1:头像从fetchMe获取+默认占位图; G2:isCurrentMonth预估判断; T1.2:incomeTrend用formatTrendValue+↑↓箭头; T1.3:abandonReason从task对象获取不硬编码空字符串; T1.4:盖戳动画始终播放移除tierCompleted死代码; B:金额用fmt.money/课时用fmt.hours/计数用fmt.count/空值用fmt.safe/天数用fmt.days; perfData数值字段改为number类型 |
| 2026-03-23 | 角色路由+页面权限守卫 | onShow 添加 checkPageAccess 权限守卫 |
| 2026-03-23 | banner 数据为空修复 | loadData 中将后端 performance 字段完整映射到 perfData（totalHours/basicHours/bonusHours/currentTier/nextTierHours/filledPct/ticks/bonusMoney/incomeMonth/prevMonth 等）；移除未使用的 formatTrendValue import |
| 2026-03-24 | 盖戳+进度条动画等数据加载完成 | onReady 不再提前触发动画；onLoad 用 Promise.all 等 _loadUserInfo+loadData 都完成后再启动盖戳和进度条动画；下拉刷新同理 |
| 2026-03-24 | 助教标签显示等级 | _loadUserInfo 中助教角色显示"X级助教"（如"初级助教"），其他角色显示角色名；数据来自后端 coach_level 字段 |
| 2026-03-24 | 角色标签显示修复 | role 是英文 code（coach/staff/…），之前 `role==='助教'` 永远不匹配；改为 `role==='coach'` + ROLE_LABELS 映射中文 |
| 2026-03-27 | 卡片新增近60天数据+days(0)修复 | enrichTask 新增 recent60dHours/recent60dIncome 字段映射；card-row-2 追加"近60天 Xh | ¥X"展示 |
*/
import { checkPageAccess } from '../../utils/auth-guard'
import type { Task } from '../../utils/mock-data'
import { fetchTasks, fetchMe, pinTask, unpinTask, abandonTask, restoreTask, createNote } from '../../services/api'
import { formatMoney } from '../../utils/money'
import { formatDeadline } from '../../utils/time'
import { formatStorageLevel } from '../../utils/storage-level'
// CHANGE 2026-05-02 | 业务时钟：sandbox 模式下用 business_year/month 替代 new Date()
import { getBusinessClock } from '../../utils/runtime-clock'
// CHANGE 2026-03-27 | 头像：需要 API_BASE 构建头像完整 URL
import { API_BASE } from '../../utils/config'
import {
  SPARK_DELAY_MS, SPARK_DUR_MS, NEXT_LOOP_DELAY_MS,
  calcShineDur, buildTicks, buildProgressBarData,
  type TickItem,
} from '../../utils/perf-progress'

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

/** 扩展任务字段 */
interface EnrichedTask extends Task {
  lastVisitDays: number
  balanceLabel: string
  aiSuggestion: string
  abandonReason?: string
  isAbandoned: boolean
  deadlineLabel: string
  deadlineStyle: 'normal' | 'warning' | 'danger' | 'muted'
  expectedDays: number | null
  idealIntervalDays: number | null
  // CHANGE 2026-03-27 | 近60天服务汇总（口径同 task-detail）
  recent60dHours: number
  recent60dIncome: number
}

/** 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: number
  bonusHours: number
  totalHours: number
  tierCompleted: boolean
  bonusMoney: number
  incomeMonth: string
  prevMonth: string
  incomeFormatted: string
  incomeTrend: string
  incomeTrendDir: 'up' | 'down'
  incomeTrendValue: number | null  // 趋势纯数字（从 incomeTrend 解析，用于千分位格式化）
}

/** Mock: 为任务附加扩展字段 */
function enrichTask(task: Task): EnrichedTask {
  const balance = (task as any).balance
  // CHANGE 2026-03-27 | 储值展示对齐 task-detail：模糊范围（无/少/一般/多/非常多）
  const balanceLabel = formatStorageLevel(balance)
  const lastVisitDays = (task as any).lastVisitDays ?? (task as any).last_visit_days ?? 0

  return {
    ...task,
    lastVisitDays,
    balanceLabel,
    aiSuggestion: (task as any).aiSuggestion ?? (task as any).ai_suggestion ?? '',
    isAbandoned: task.status === 'abandoned',
    abandonReason: task.status === 'abandoned' ? (task as any).abandonReason ?? (task as any).abandon_reason : undefined,
    deadlineLabel: formatDeadline((task as any).deadline).text,
    deadlineStyle: formatDeadline((task as any).deadline).style,
    // CHANGE 2026-03-24 | 预期天数标签：来自后端 expected_days（ideal_interval_days - last_visit_days）
    expectedDays: (task as any).expectedDays ?? (task as any).expected_days ?? null,
    idealIntervalDays: (task as any).idealIntervalDays ?? (task as any).ideal_interval_days ?? null,
    // CHANGE 2026-03-27 | 近60天服务汇总：传原始数字，WXML 用 WXS 格式化
    recent60dHours: (task as any).recent60dHours ?? (task as any).recent60d_hours ?? 0,
    recent60dIncome: (task as any).recent60dIncome ?? (task as any).recent60d_income ?? 0,
  }
}

/** Mock: 构造业绩进度卡片数据 — 对齐 H5 原型数值 */
function buildPerfData(): PerfData {
  return {
    nextTierHours: 0,
    remainHours: 0,
    currentTier: 0,
    tierProgress: 0,
    filledPct: 0,
    clampedSparkPct: 0,
    ticks: [],
    shineDurMs: 1000,
    sparkDurMs: SPARK_DUR_MS,
    shineRunning: false,
    sparkRunning: false,
    basicHours: 0,
    bonusHours: 0,
    totalHours: 0,
    tierCompleted: false,
    bonusMoney: 0,
    incomeMonth: '',
    prevMonth: '',
    incomeFormatted: '0',
    incomeTrend: '',
    incomeTrendDir: 'up',
  }
}

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: '',
    isCurrentMonth: true,
    /* 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',
      incomeTrendValue: null as number | null,
    } 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 })
    // CHANGE 2026-03-24 | 盖戳+进度条动画等所有数据加载完成后再启动
    this._loadAllThenAnimate()
  },

  onReady() {
    // 动画由 _loadAllThenAnimate 统一控制，onReady 不再提前触发
  },

  onShow() {
    // 权限守卫：检查登录状态、账号禁用、角色权限
    checkPageAccess('pages/task-list/task-list')
    // 每次显示页面时重新随机 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() {
    // CHANGE 2026-03-24 | 下拉刷新：重新加载所有数据后再启动动画
    this._stopAnimLoop()
    this.setData({ stampAnimated: false })
    Promise.all([
      this._loadUserInfo(),
      this.loadData(),
    ]).then(() => {
      wx.stopPullDownRefresh()
      setTimeout(() => {
        this.setData({ stampAnimated: true })
        this._startAnimLoop()
      }, 300)
    })
  },

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

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

    try {
      // CHANGE 2026-03-24 | 一次性加载全部任务（pageSize=200），避免分页导致部分类型任务不显示
      // CHANGE 2026-03-27 | 并行请求 active + abandoned 任务，放弃任务需要单独请求
      const [res, abandonedRes] = await Promise.all([
        fetchTasks({ pageSize: 200 }),
        fetchTasks({ status: 'abandoned', pageSize: 200 }),
      ])
      const allTasks: Task[] = [
        ...(res.tasks || []),
        ...(abandonedRes.tasks || []),
      ]

      const enriched = allTasks.map(enrichTask)

      const pinnedTasks = enriched.filter((t) => t.isPinned && !t.isAbandoned)
      // CHANGE 2026-03-24 | 后端返回 status="active"，前端原先只匹配 "pending"，导致全部被过滤
      const normalTasks = enriched.filter((t) => !t.isPinned && !t.isAbandoned && (t.status === 'pending' || t.status === 'active'))
      const abandonedTasks = enriched.filter((t) => t.isAbandoned)
      const totalCount = pinnedTasks.length + normalTasks.length + abandonedTasks.length

      // CHANGE 2026-03-23 | 修复 banner 数据为空：将后端 performance 字段映射到 perfData
      const perfData = buildPerfData()
      const perf = res.performance as any
      const bannerTitle = perf ? `${perf.currentTier ?? perf.current_tier ?? ''}` : ''
      const bannerMetrics: Array<{ label: string; value: string }> = []

      if (perf) {
        // 后端返回 snake_case（CamelModel 可能已转 camelCase），兼容两种命名
        const totalHours = perf.totalHours ?? perf.total_hours ?? 0
        const basicHours = perf.basicHours ?? perf.basic_hours ?? 0
        const bonusHours = perf.bonusHours ?? perf.bonus_hours ?? 0
        const nextTierHours = perf.nextTierHours ?? perf.next_tier_hours ?? 0
        const tierCompleted = perf.tierCompleted ?? perf.tier_completed ?? false
        const bonusMoney = perf.bonusMoney ?? perf.bonus_money ?? 0
        const monthLabel = perf.monthLabel ?? perf.month_label ?? ''
        const prevMonth = perf.prevMonth ?? perf.prev_month ?? ''
        const totalIncome = perf.totalIncome ?? perf.total_income ?? 0
        const incomeTrend = perf.incomeTrend ?? perf.income_trend ?? ''
        const incomeTrendDir = perf.incomeTrendDir ?? perf.income_trend_dir ?? 'up'

        // 统一使用共用模块计算进度条数据
        const pbData = buildProgressBarData(perf)

        perfData.totalHours = totalHours
        perfData.basicHours = basicHours
        perfData.bonusHours = bonusHours
        perfData.currentTier = pbData.currentTier
        perfData.nextTierHours = nextTierHours
        perfData.remainHours = pbData.remainHours
        perfData.tierCompleted = tierCompleted
        perfData.bonusMoney = bonusMoney
        perfData.incomeMonth = monthLabel
        perfData.prevMonth = prevMonth
        perfData.incomeFormatted = formatMoney(totalIncome)
        perfData.incomeTrend = incomeTrend
        perfData.incomeTrendDir = incomeTrendDir === 'down' ? 'down' : 'up'
        // 从 "^7373" / "v368" 中提取纯数字，用于千分位格式化
        const trendNumMatch = incomeTrend.replace(/[^0-9.]/g, '')
        perfData.incomeTrendValue = trendNumMatch ? parseFloat(trendNumMatch) : null
        perfData.filledPct = pbData.filledPct
        perfData.clampedSparkPct = pbData.clampedSparkPct
        perfData.ticks = pbData.ticks
        perfData.shineDurMs = pbData.shineDurMs
        perfData.sparkDurMs = pbData.sparkDurMs
        perfData.tierProgress = pbData.tierProgress
      }

      // G2: 当月预估判断
      // CHANGE 2026-05-02 | 用业务时钟，sandbox 模式按 sandbox_date 判断
      const clock = await getBusinessClock()
      const nowYear = clock.business_year
      const nowMonth = clock.business_month
      const businessDay = parseInt(clock.business_date.slice(8, 10), 10) || 1
      const incomeMonth = perfData.incomeMonth
      let dataYear = nowYear
      let dataMonth = nowMonth
      if (incomeMonth) {
        const parts = incomeMonth.match(/(\d+)/)
        if (parts) dataMonth = parseInt(parts[1], 10)
      }
      // CHANGE 2026-03-24 | 预估规则：当月且当前日期 ≤ 5号才显示"预估"（全小程序统一）
      const isCurrentMonth = dataYear === nowYear && dataMonth === nowMonth && businessDay <= 5

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

      // 盖戳+进度条动画由 _loadAllThenAnimate 统一触发，此处不再单独启动
    } catch (err) {
      console.error('[task-list] loadData 异常:', err)
      this.setData({ pageState: 'error' })
    }

    cb?.()
  },

  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' }),
    })
  },

  /** 查看我的所有客户 → 跳转业绩页并滚动到底部（常客列表） */
  onViewAllCustomers() {
    wx.navigateTo({
      url: '/pages/performance/performance?scrollToBottom=1',
      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() {},

  // CHANGE 2026-03-27 | 联调：置顶/取消置顶调用后端 API
  async onCtxPin() {
    const target = this.data.contextMenuTarget
    const isPinned = !target.isPinned
    this.setData({ contextMenuVisible: false })
    try {
      if (isPinned) {
        await pinTask(target.id)
      } else {
        await unpinTask(target.id)
      }
      wx.showToast({
        title: isPinned ? `已置顶「${target.customerName}」` : `已取消置顶「${target.customerName}」`,
        icon: 'none',
      })
      this._updateTaskPin(target.id, isPinned)
    } catch (_e) {
      wx.showToast({ title: '操作失败，请重试', icon: 'none' })
    }
  },

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

  onCtxAI() {
    const target = this.data.contextMenuTarget
    this.setData({ contextMenuVisible: false })
    // CHANGE 2026-03-20 | T12.2: 修正路由从 ai-chat 到 chat，传 taskId 使用 task 上下文入口
    wx.navigateTo({
      url: `/pages/chat/chat?taskId=${target.id}`,
      fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
    })
  },

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

  // CHANGE 2026-03-27 | 联调：放弃任务调用后端 API
  async onAbandonConfirm(e: WechatMiniprogram.CustomEvent<{ reason: string }>) {
    const { reason } = e.detail
    const target = this.data.abandonTarget
    this.setData({ abandonModalVisible: false })
    try {
      await abandonTask(target.id, reason)
      wx.showToast({ title: `已放弃「${target.customerName}」`, icon: 'none' })
      this._updateTaskAbandon(target.id, reason)
    } catch (_e) {
      wx.showToast({ title: '操作失败，请重试', icon: 'none' })
    }
  },

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

  // CHANGE 2026-03-27 | 联调：取消放弃调用后端 API
  async onCtxCancelAbandon() {
    const target = this.data.contextMenuTarget
    this.setData({ contextMenuVisible: false })
    wx.showLoading({ title: '处理中...' })
    try {
      await restoreTask(target.id)
      wx.hideLoading()
      wx.showToast({ title: `已取消放弃「${target.customerName}」`, icon: 'success' })
      this._updateTaskCancelAbandon(target.id)
    } catch (_e) {
      wx.hideLoading()
      wx.showToast({ title: '操作失败，请重试', icon: 'none' })
    }
  },

  // CHANGE 2026-03-27 | 联调：备注调用后端 API
  async onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string; serviceScore?: number; returnScore?: number }>) {
    const { score, content, serviceScore, returnScore } = e.detail
    const target = this.data.noteTarget
    this.setData({ noteModalVisible: false })
    const memberId = (target as any).memberId ?? (target as any).member_id
    const taskId = Number(target.id)
    try {
      await createNote({
        targetId: memberId ? Number(memberId) : taskId,
        content,
        taskId,
        score: score || undefined,
        ratingServiceWillingness: serviceScore || undefined,
        ratingRevisitLikelihood: returnScore || undefined,
      })
      wx.showToast({ title: `已保存「${target.customerName}」备注`, icon: 'success' })
    } catch (_e) {
      wx.showToast({ title: '备注保存失败，请重试', icon: 'none' })
    }
  },

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

  /** CHANGE 2026-03-24 | 所有数据加载完成后统一触发盖戳+进度条动画 */
  async _loadAllThenAnimate() {
    // 并行加载用户信息和任务数据，全部完成后再启动动画
    await Promise.all([
      this._loadUserInfo(),
      this.loadData(),
    ])
    // 所有数据渲染完成后启动动画
    setTimeout(() => {
      this.setData({ stampAnimated: true })
      this._startAnimLoop()
    }, 300)
  },

  /** G1: 从全局用户信息或 fetchMe 获取头像等字段 */
  /** CHANGE 2026-03-24 | 助教标签显示"X级助教"，其他角色显示角色名 */
  async _loadUserInfo() {
    const app = getApp<IAppOption>()
    const authUser = app.globalData.authUser
    if (authUser?.nickname) {
      // CHANGE 2026-03-27 | 头像：avatarUrl 是相对路径，需拼接完整 URL
      const userId = (authUser as any).userId
      const hasAvatar = !!(authUser as any).avatarUrl
      this.setData({
        userName: authUser.nickname,
        avatarUrl: hasAvatar && userId ? `${API_BASE}/api/xcx/avatar/${userId}` : '',
      })
    }
    // 兜底：调用 fetchMe 获取完整用户信息
    try {
      const me = await fetchMe()
      if (me) {
        const role = (me as any).role || ''
        const coachLevel = (me as any).coach_level || (me as any).coachLevel || ''
        // CHANGE 2026-03-24 | role 是英文 code（coach/staff/head_coach/manager），需映射为中文
        const ROLE_LABELS: Record<string, string> = { coach: '助教', staff: '员工', head_coach: '主教练', manager: '店长' }
        const roleLabel = ROLE_LABELS[role] || role
        // 助教显示"X级助教"（如"初级助教"），其他角色显示中文角色名
        const displayTag = (role === 'coach' && coachLevel) ? `${coachLevel}助教` : roleLabel
        // CHANGE 2026-03-27 | 头像：读 avatarUrl（CamelModel 转换后的字段名），拼接完整 URL
        const userId = (me as any).userId
        const hasAvatar = !!(me as any).avatarUrl
        this.setData({
          userName: (me as any).nickname || (me as any).name || this.data.userName,
          userRole: displayTag,
          storeName: (me as any).storeName || (me as any).store_name || '',
          avatarUrl: hasAvatar && userId
            ? `${API_BASE}/api/xcx/avatar/${userId}`
            : this.data.avatarUrl,
        })
      }
    } catch {
      // 网络失败时保留 globalData 中的信息
    }
  },

  _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': total,
      'perfData.basicHours': basic,
      'perfData.bonusHours': 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 })
  },
})
