feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"perf-progress-bar": "/components/perf-progress-bar/perf-progress-bar",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"ai-inline-icon": "/components/ai-inline-icon/ai-inline-icon",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"abandon-modal": "/components/abandon-modal/abandon-modal",
|
||||
"dev-fab": "/components/dev-fab/dev-fab",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
701
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.ts
Normal file
701
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.ts
Normal file
@@ -0,0 +1,701 @@
|
||||
/* 任务列表页 — 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 })
|
||||
},
|
||||
})
|
||||
412
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.wxml
Normal file
412
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.wxml
Normal file
@@ -0,0 +1,412 @@
|
||||
<!-- 任务列表页 — 2026-03-13 全量重写,1:1 对齐 H5 原型 task-list.html -->
|
||||
<!-- AI_CHANGELOG
|
||||
| 日期 | Prompt | 变更 |
|
||||
|------|--------|------|
|
||||
| 2026-03-13 | 重写 task-list 页面 1:1 还原 H5 原型 | 全量重写 WXML |
|
||||
| 2026-03-13 | banner 错位重做 | 移除通用 banner 组件,页面内实现完整 banner |
|
||||
| 2026-03-13 | 背景+纹理合并 SVG | 渐变+纹理+光晕合并为 SVG |
|
||||
| 2026-03-13 | 5项修复+精确还原 | 恢复纹理CSS层、盖戳改回CSS实现、头像引用修复、abandoned标签恢复CSS灰化、全量line-height校准 |
|
||||
-->
|
||||
<view class="page-task-list">
|
||||
|
||||
<!-- ====== 顶部 Banner 区域 — 对齐 H5 .banner-bg.theme-blue.texture-aurora ====== -->
|
||||
<!-- CHANGE 2026-03-13 | banner 背景:SVG 做渐变底图 + CSS 做纹理叠加(SVG pattern 在小程序中不渲染) -->
|
||||
<view class="banner-area">
|
||||
<image class="banner-bg-img" src="/assets/images/banner-bg-combined.svg" mode="aspectFill" />
|
||||
<!-- 纹理层:CSS repeating-linear-gradient 实现斜线网格,SVG pattern 在小程序 image 中不生效 -->
|
||||
<view class="banner-texture"></view>
|
||||
|
||||
<!-- 用户信息区 — H5: .px-5.pt-10.pb-3 -->
|
||||
<view class="user-info-section">
|
||||
<view class="user-info-row">
|
||||
<!-- 头像 — H5: w-14 h-14 rounded-2xl bg-white/20 -->
|
||||
<view class="avatar-wrap">
|
||||
<image src="/assets/images/avatar-coach.png" mode="aspectFill" class="avatar-img" />
|
||||
</view>
|
||||
<!-- 姓名+标签+门店 -->
|
||||
<view class="user-detail">
|
||||
<view class="user-name-row">
|
||||
<text class="user-name">{{userName}}</text>
|
||||
<text class="user-role-tag">{{userRole}}</text>
|
||||
</view>
|
||||
<view class="user-store-row">
|
||||
<text class="user-store">{{storeName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 业绩进度卡片 — H5: .mx-4 > .bg-white/15.backdrop-blur-md.rounded-2xl -->
|
||||
<view class="perf-card">
|
||||
<!-- L1: 跳档提示 -->
|
||||
<view class="perf-l1">
|
||||
<view class="perf-l1-left">
|
||||
<text class="perf-label">距离{{perfData.nextTierHours}}小时仅剩</text>
|
||||
<text class="perf-accent">{{perfData.remainHours}}小时</text>
|
||||
</view>
|
||||
<view class="perf-l1-right" bindtap="onPerformanceTap">
|
||||
<text class="perf-secondary">查看详情</text>
|
||||
<t-icon name="chevron-right" size="22rpx" color="rgba(255,255,255,0.7)" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- L2: 5段档位进度条组件 -->
|
||||
<view class="perf-l2">
|
||||
<perf-progress-bar
|
||||
filledPct="{{perfData.filledPct}}"
|
||||
clampedSparkPct="{{perfData.clampedSparkPct}}"
|
||||
currentTier="{{perfData.currentTier}}"
|
||||
ticks="{{perfData.ticks}}"
|
||||
shineRunning="{{perfData.shineRunning}}"
|
||||
sparkRunning="{{perfData.sparkRunning}}"
|
||||
shineDurMs="{{perfData.shineDurMs}}"
|
||||
sparkDurMs="{{perfData.sparkDurMs}}"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- L3: 课时 + 红戳 + 奖金 -->
|
||||
<view class="perf-l3">
|
||||
<view class="perf-l3-left">
|
||||
<view class="perf-hours-wrap">
|
||||
<view class="perf-hours-row">
|
||||
<text class="hours-green">{{perfData.basicHours}}</text>
|
||||
<text class="hours-sep">|</text>
|
||||
<text class="hours-yellow">{{perfData.bonusHours}}</text>
|
||||
<text class="hours-sep">|</text>
|
||||
<text class="hours-white">{{perfData.totalHours}}</text>
|
||||
</view>
|
||||
<view class="hours-label-row">
|
||||
<text class="hours-label">基础课 | 激励课 | 全部</text>
|
||||
</view>
|
||||
<!-- 红戳徽章 — SVG 实现 -->
|
||||
<image
|
||||
class="stamp-badge {{stampAnimated ? 'stamp-animate' : ''}}"
|
||||
wx:if="{{perfData.tierCompleted}}"
|
||||
src="/assets/images/stamp-badge.svg"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="perf-l3-right">
|
||||
<view class="bonus-wrap">
|
||||
<text class="bonus-amount">{{perfData.bonusMoney}}</text>
|
||||
<text class="bonus-unit">元</text>
|
||||
</view>
|
||||
<view class="bonus-label-row">
|
||||
<text class="bonus-label">达{{perfData.nextTierHours}}h即得</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- L4: 预计收入 -->
|
||||
<view class="perf-l4">
|
||||
<text class="perf-l4-label">{{perfData.incomeMonth}}预计收入 | 比{{perfData.prevMonth}}同期</text>
|
||||
<view class="perf-l4-right" bindtap="onPerformanceTap">
|
||||
<text class="income-value">¥{{perfData.incomeFormatted}}</text>
|
||||
<text class="income-trend {{perfData.incomeTrendDir === 'down' ? 'trend-down' : ''}}">{{perfData.incomeTrend}}</text>
|
||||
<t-icon name="chevron-right" size="28rpx" color="rgba(255,255,255,0.7)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== Loading 骨架屏 ====== -->
|
||||
<view class="state-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<view class="loading-placeholder" wx:for="{{[1,2,3]}}" wx:key="*this">
|
||||
<view class="ph-line ph-line--title"></view>
|
||||
<view class="ph-line ph-line--body"></view>
|
||||
<view class="ph-line ph-line--short"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== 空状态 ====== -->
|
||||
<view class="state-empty" wx:if="{{pageState === 'empty'}}">
|
||||
<text class="empty-text">暂无待办任务</text>
|
||||
</view>
|
||||
|
||||
<!-- ====== Error 状态 ====== -->
|
||||
<view class="state-error" wx:if="{{pageState === 'error'}}">
|
||||
<text class="error-text">加载失败,请重试</text>
|
||||
<view class="retry-btn" bindtap="onRetry">
|
||||
<text class="retry-btn-text">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== 任务列表区域 ====== -->
|
||||
<view class="task-section" wx:if="{{pageState === 'normal'}}">
|
||||
<!-- 标题行 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日 客户维护</text>
|
||||
<text class="section-count">共 {{taskCount}} 项</text>
|
||||
</view>
|
||||
|
||||
<!-- 📌 置顶区域 -->
|
||||
<view class="task-group" wx:if="{{pinnedTasks.length > 0}}">
|
||||
<view class="group-label-row">
|
||||
<text class="group-label group-label--pinned">📌 置顶</text>
|
||||
<text class="group-count">{{pinnedTasks.length}}项</text>
|
||||
</view>
|
||||
<view class="task-card-list">
|
||||
<view
|
||||
class="task-card {{item.isPinned ? 'task-card--pinned' : ''}} task-card--{{item.taskType}}"
|
||||
wx:for="{{pinnedTasks}}" wx:key="id"
|
||||
hover-class="task-card--hover" hover-stay-time="100"
|
||||
data-id="{{item.id}}" data-tasktype="{{item.taskType}}"
|
||||
data-group="pinned" data-index="{{index}}"
|
||||
bindtap="onTaskTap" bindlongpress="onTaskLongPress"
|
||||
>
|
||||
<view class="card-body">
|
||||
<view class="card-row-1">
|
||||
<view class="task-type-tag task-type-tag--{{item.taskType}}">
|
||||
<text class="tag-text">{{item.taskTypeLabel}}</text>
|
||||
</view>
|
||||
<text class="customer-name">{{item.customerName}}</text>
|
||||
<heart-icon score="{{item.heartScore}}" size="small" />
|
||||
<text class="note-indicator" wx:if="{{item.hasNote}}">📝</text>
|
||||
<text class="overdue-badge" wx:if="{{item.deadlineStyle === 'danger'}}">{{item.deadlineLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-2">
|
||||
<text class="visit-text">最近到店:{{item.lastVisitDays}}天前 · 余额:{{item.balanceLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-deadline" wx:if="{{item.deadlineLabel && item.deadlineLabel !== '--' && item.deadlineStyle !== 'danger'}}">
|
||||
<text class="deadline-text deadline-text--{{item.deadlineStyle}}">{{item.deadlineLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-3">
|
||||
<ai-inline-icon color="{{aiColor}}" />
|
||||
<text class="ai-suggestion-text">{{item.aiSuggestion}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一般任务区域 -->
|
||||
<view class="task-group" wx:if="{{normalTasks.length > 0}}">
|
||||
<view class="group-label-row">
|
||||
<text class="group-label group-label--normal">正常任务</text>
|
||||
<text class="group-count">{{normalTasks.length}}项</text>
|
||||
</view>
|
||||
<view class="task-card-list">
|
||||
<view
|
||||
class="task-card task-card--{{item.taskType}}"
|
||||
wx:for="{{normalTasks}}" wx:key="id"
|
||||
hover-class="task-card--hover" hover-stay-time="100"
|
||||
data-id="{{item.id}}" data-tasktype="{{item.taskType}}"
|
||||
data-group="normal" data-index="{{index}}"
|
||||
bindtap="onTaskTap" bindlongpress="onTaskLongPress"
|
||||
>
|
||||
<view class="card-body">
|
||||
<view class="card-row-1">
|
||||
<view class="task-type-tag task-type-tag--{{item.taskType}}">
|
||||
<text class="tag-text">{{item.taskTypeLabel}}</text>
|
||||
</view>
|
||||
<text class="customer-name">{{item.customerName}}</text>
|
||||
<heart-icon score="{{item.heartScore}}" size="small" />
|
||||
<text class="note-indicator" wx:if="{{item.hasNote}}">📝</text>
|
||||
<text class="overdue-badge" wx:if="{{item.deadlineStyle === 'danger'}}">{{item.deadlineLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-2">
|
||||
<text class="visit-text">最近到店:{{item.lastVisitDays}}天前 · 余额:{{item.balanceLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-deadline" wx:if="{{item.deadlineLabel && item.deadlineLabel !== '--' && item.deadlineStyle !== 'danger'}}">
|
||||
<text class="deadline-text deadline-text--{{item.deadlineStyle}}">{{item.deadlineLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-3">
|
||||
<ai-inline-icon color="{{aiColor}}" />
|
||||
<text class="ai-suggestion-text">{{item.aiSuggestion}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已放弃区域 -->
|
||||
<view class="task-group" wx:if="{{abandonedTasks.length > 0}}">
|
||||
<view class="group-label-row">
|
||||
<text class="group-label group-label--abandoned">已放弃</text>
|
||||
<text class="group-count">{{abandonedTasks.length}}项</text>
|
||||
</view>
|
||||
<view class="task-card-list">
|
||||
<view
|
||||
class="task-card task-card--abandoned"
|
||||
wx:for="{{abandonedTasks}}" wx:key="id"
|
||||
hover-class="task-card--hover" hover-stay-time="100"
|
||||
data-id="{{item.id}}" data-tasktype="{{item.taskType}}"
|
||||
data-group="abandoned" data-index="{{index}}"
|
||||
bindtap="onTaskTap" bindlongpress="onTaskLongPress"
|
||||
>
|
||||
<view class="card-body">
|
||||
<view class="card-row-1">
|
||||
<!-- CHANGE 2026-03-13 | abandoned 标签保留原始类型标签,通过 CSS 灰化(对齐 H5 行为) -->
|
||||
<view class="task-type-tag task-type-tag--{{item.taskType}} task-type-tag--abandoned">
|
||||
<text class="tag-text">{{item.taskTypeLabel}}</text>
|
||||
</view>
|
||||
<text class="customer-name customer-name--abandoned">{{item.customerName}}</text>
|
||||
<heart-icon score="{{item.heartScore}}" size="small" />
|
||||
</view>
|
||||
<view class="card-row-2">
|
||||
<text class="visit-text visit-text--abandoned">最近到店:{{item.lastVisitDays}}天前 · 余额:{{item.balanceLabel}}</text>
|
||||
</view>
|
||||
<view class="card-row-abandon" wx:if="{{item.abandonReason}}">
|
||||
<text class="abandon-reason">放弃原因:{{item.abandonReason}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" wx:if="{{!hasMore}}">
|
||||
<text class="load-more-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== P3: 长按上下文菜单 ====== -->
|
||||
<view class="ctx-overlay {{contextMenuVisible ? 'ctx-overlay--active' : ''}}" bindtap="onCloseContextMenu"></view>
|
||||
<view class="ctx-menu {{contextMenuVisible ? 'ctx-menu--active' : ''}}"
|
||||
style="left:{{contextMenuX}}px;top:{{contextMenuY}}px" catchtap="noop">
|
||||
<!-- 已放弃任务:显示"取消放弃" -->
|
||||
<block wx:if="{{contextMenuTarget.isAbandoned}}">
|
||||
<view class="ctx-item" hover-class="ctx-item--hover" bindtap="onCtxCancelAbandon">
|
||||
<text class="ctx-emoji">↩️</text>
|
||||
<text class="ctx-text">取消放弃</text>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 一般/置顶任务:显示标准菜单 -->
|
||||
<block wx:else>
|
||||
<view class="ctx-item" hover-class="ctx-item--hover" bindtap="onCtxPin">
|
||||
<text class="ctx-emoji">📌</text>
|
||||
<text class="ctx-text">{{contextMenuTarget.isPinned ? '取消置顶' : '置顶'}}</text>
|
||||
</view>
|
||||
<view class="ctx-item" hover-class="ctx-item--hover" bindtap="onCtxNote">
|
||||
<text class="ctx-emoji">📝</text>
|
||||
<text class="ctx-text">备注</text>
|
||||
</view>
|
||||
<view class="ctx-item" hover-class="ctx-item--hover" bindtap="onCtxAI">
|
||||
<text class="ctx-emoji">🤖</text>
|
||||
<text class="ctx-text">问问AI助手</text>
|
||||
</view>
|
||||
<view class="ctx-item" hover-class="ctx-item--hover" bindtap="onCtxAbandon">
|
||||
<text class="ctx-emoji">🗑️</text>
|
||||
<text class="ctx-text">放弃任务</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- ====== P4: 放弃弹窗 ====== -->
|
||||
<abandon-modal
|
||||
visible="{{abandonModalVisible}}"
|
||||
customerName="{{abandonTarget.customerName || ''}}"
|
||||
bind:confirm="onAbandonConfirm"
|
||||
bind:cancel="onAbandonCancel"
|
||||
/>
|
||||
|
||||
<!-- 备注弹窗 -->
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{noteTarget.customerName || ''}}"
|
||||
showExpandBtn="{{true}}"
|
||||
showRating="{{true}}"
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button />
|
||||
|
||||
<!-- 开发调试 FAB -->
|
||||
<dev-fab />
|
||||
|
||||
<!-- ====== 调试面板 ====== -->
|
||||
<view class="debug-panel {{showDebugPanel ? 'debug-panel--visible' : ''}}" catchtap="noop">
|
||||
<view class="debug-header">
|
||||
<text class="debug-title">🔧 调试工具</text>
|
||||
<view class="debug-close" bindtap="toggleDebugPanel" hover-class="debug-close--hover">
|
||||
<t-icon name="close" size="32rpx" color="#5e5e5e" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 课时进度控制 -->
|
||||
<view class="debug-section">
|
||||
<view class="debug-label-row">
|
||||
<text class="debug-label">📊 当前课时总量:</text>
|
||||
<text class="debug-value-chip">{{debugTotalHours}}h</text>
|
||||
</view>
|
||||
<slider
|
||||
class="debug-slider"
|
||||
min="0" max="220" step="1"
|
||||
value="{{debugTotalHours}}"
|
||||
bindchange="onDebugTotalHours"
|
||||
activeColor="#10b981"
|
||||
backgroundColor="#e7e7e7"
|
||||
block-size="24"
|
||||
/>
|
||||
<view class="debug-tick-row">
|
||||
<text class="debug-tick">0</text>
|
||||
<text class="debug-tick debug-tick--key">100</text>
|
||||
<text class="debug-tick debug-tick--key debug-tick--current">130</text>
|
||||
<text class="debug-tick debug-tick--key">160</text>
|
||||
<text class="debug-tick debug-tick--key">190</text>
|
||||
<text class="debug-tick">220</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基础 / 激励课时分配 -->
|
||||
<view class="debug-section">
|
||||
<view class="debug-label-row">
|
||||
<text class="debug-label">🟢 基础课时:</text>
|
||||
<text class="debug-value-chip debug-chip--green">{{debugBasicHours}}h</text>
|
||||
</view>
|
||||
<slider
|
||||
class="debug-slider"
|
||||
min="0" max="220" step="0.5"
|
||||
value="{{debugBasicHours}}"
|
||||
bindchange="onDebugBasicHours"
|
||||
activeColor="#6ee7b7"
|
||||
backgroundColor="#e7e7e7"
|
||||
block-size="22"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="debug-section">
|
||||
<view class="debug-label-row">
|
||||
<text class="debug-label">🟡 激励课时:</text>
|
||||
<text class="debug-value-chip debug-chip--yellow">{{debugBonusHours}}h</text>
|
||||
</view>
|
||||
<slider
|
||||
class="debug-slider"
|
||||
min="0" max="80" step="0.5"
|
||||
value="{{debugBonusHours}}"
|
||||
bindchange="onDebugBonusHours"
|
||||
activeColor="#fbbf24"
|
||||
backgroundColor="#e7e7e7"
|
||||
block-size="22"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 档位进度预设 -->
|
||||
<view class="debug-section">
|
||||
<text class="debug-label">🎯 快速预设档位:</text>
|
||||
<view class="debug-btn-group">
|
||||
<view class="debug-btn {{debugPreset === 0 ? 'debug-btn--active' : ''}}" bindtap="onDebugPreset" data-preset="0">未完成</view>
|
||||
<view class="debug-btn {{debugPreset === 1 ? 'debug-btn--active' : ''}}" bindtap="onDebugPreset" data-preset="1">达100h</view>
|
||||
<view class="debug-btn {{debugPreset === 2 ? 'debug-btn--active' : ''}}" bindtap="onDebugPreset" data-preset="2">达130h</view>
|
||||
<view class="debug-btn {{debugPreset === 3 ? 'debug-btn--active' : ''}}" bindtap="onDebugPreset" data-preset="3">达160h</view>
|
||||
<view class="debug-btn {{debugPreset === 4 ? 'debug-btn--active' : ''}}" bindtap="onDebugPreset" data-preset="4">满档220h</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 调试触发按钮 -->
|
||||
<view class="debug-trigger {{showDebugPanel ? 'debug-trigger--active' : ''}}" bindtap="toggleDebugPanel" hover-class="debug-trigger--hover">
|
||||
<text class="debug-trigger-icon">🔧</text>
|
||||
</view>
|
||||
</view>
|
||||
1415
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.wxss
Normal file
1415
tmp/DEMO-miniprogram/miniprogram/pages/task-list/task-list.wxss
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user