Files
Neo-ZQYY/tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.ts

702 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 任务列表页 — 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需要走过的距离 = 填充条宽度 + 高光宽度
* 轨道宽度约 634rpx750 - 左右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 })
},
})