微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -0,0 +1,14 @@
{
"navigationBarTitleText": "客户看板",
"enablePullDownRefresh": true,
"usingComponents": {
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
"heart-icon": "/components/heart-icon/heart-icon",
"ai-float-button": "/components/ai-float-button/ai-float-button",
"board-tab-bar": "/components/board-tab-bar/board-tab-bar",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}

View File

@@ -0,0 +1,300 @@
// 客户看板页 — 8 个维度查看前 100 名客户
// TODO: 联调时替换 mock 数据为真实 API 调用
export {}
/** 维度类型 → 卡片模板映射 */
type DimType = 'recall' | 'potential' | 'balance' | 'recharge' | 'recent' | 'spend60' | 'freq60' | 'loyal'
const DIMENSION_TO_DIM: Record<string, DimType> = {
recall: 'recall',
potential: 'potential',
balance: 'balance',
recharge: 'recharge',
recent: 'recent',
spend60: 'spend60',
freq60: 'freq60',
loyal: 'loyal',
}
const DIMENSION_OPTIONS = [
{ value: 'recall', text: '最应召回' },
{ value: 'potential', text: '最大消费潜力' },
{ value: 'balance', text: '最高余额' },
{ value: 'recharge', text: '最近充值' },
{ value: 'recent', text: '最近到店' },
{ value: 'spend60', text: '最高消费 近60天' },
{ value: 'freq60', text: '最频繁 近60天' },
{ value: 'loyal', text: '最专一 近60天' },
]
const PROJECT_OPTIONS = [
{ value: 'all', text: '全部' },
{ value: 'chinese', text: '🎱 中式/追分' },
{ value: 'snooker', text: '斯诺克' },
{ value: 'mahjong', text: '🀄 麻将/棋牌' },
{ value: 'karaoke', text: '🎤 团建/K歌' },
]
interface AssistantInfo {
name: string
cls: string // assistant--assignee / assistant--abandoned / assistant--normal
heartScore: number // 0-10heart-icon 组件用
badge?: string // 跟 / 弃
badgeCls?: string // assistant-badge--follow / assistant-badge--drop
}
interface CustomerItem {
id: string
name: string
initial: string
avatarCls: string
// 召回维度
idealDays: number
elapsedDays: number
overdueDays: number
visits30d: number
balance: string
recallIndex: string
// 消费潜力维度
potentialTags: Array<{ text: string; theme: string }>
spend30d: string
avgVisits: string
avgSpend: string
// 余额维度
lastVisit: string
monthlyConsume: string
availableMonths: string
// 充值维度
lastRecharge: string
rechargeAmount: string
recharges60d: string
currentBalance: string
// 消费60天
spend60d: string
visits60d: string
highSpendTag: boolean
// 频率60天
avgInterval: string
weeklyVisits: Array<{ val: number; pct: number }>
// 专一度
intimacy: string
coachName: string
coachRatio: string
topCoachName: string
topCoachHeart: number
topCoachScore: string
coachDetails: Array<{
name: string
cls: string
heartScore: number
badge?: string
avgDuration: string
serviceCount: string
coachSpend: string
relationIdx: number
}>
// 最近到店
visitFreq: string
daysAgo: number
// 助教
assistants: AssistantInfo[]
}
/** Mock 数据(忠于 H5 原型 3 位客户) */
const MOCK_CUSTOMERS: CustomerItem[] = [
{
id: 'u1', name: '王先生', initial: '王', avatarCls: 'avatar--amber',
idealDays: 7, elapsedDays: 15, overdueDays: 8,
visits30d: 3, balance: '¥2,680', recallIndex: '9.2',
potentialTags: [
{ text: '高频', theme: 'primary' },
{ text: '高客单', theme: 'warning' },
],
spend30d: '¥4,200', avgVisits: '6.2次', avgSpend: '¥680',
lastVisit: '3天前', monthlyConsume: '¥3,500', availableMonths: '0.8月',
lastRecharge: '2月15日', rechargeAmount: '¥5,000', recharges60d: '2次', currentBalance: '¥2,680',
spend60d: '¥8,400', visits60d: '12', highSpendTag: true,
avgInterval: '5.0天', intimacy: '92',
topCoachName: '小燕', topCoachHeart: 9.2, topCoachScore: '9.2',
coachDetails: [
{ name: '小燕', cls: 'assistant--assignee', heartScore: 9.2, badge: '跟', avgDuration: '2.3h', serviceCount: '14', coachSpend: '¥4,200', relationIdx: 9.2 },
{ name: '泡芙', cls: 'assistant--normal', heartScore: 6.5, avgDuration: '1.5h', serviceCount: '8', coachSpend: '¥2,100', relationIdx: 7.2 },
],
weeklyVisits: [
{ val: 2, pct: 60 }, { val: 2, pct: 60 }, { val: 1, pct: 30 }, { val: 3, pct: 100 },
{ val: 2, pct: 60 }, { val: 3, pct: 100 }, { val: 1, pct: 30 }, { val: 3, pct: 100 },
], coachName: '小燕', coachRatio: '78%',
visitFreq: '6.2次/月',
daysAgo: 3,
assistants: [
{ name: '小燕', cls: 'assistant--assignee', heartScore: 9.2, badge: '跟', badgeCls: 'assistant-badge--follow' },
{ name: '泡芙', cls: 'assistant--normal', heartScore: 6.5 },
],
},
{
id: 'u2', name: '李女士', initial: '李', avatarCls: 'avatar--pink',
idealDays: 10, elapsedDays: 22, overdueDays: 12,
visits30d: 1, balance: '¥8,200', recallIndex: '8.5',
potentialTags: [
{ text: '高余额', theme: 'success' },
{ text: '低频', theme: 'gray' },
],
spend30d: '¥1,800', avgVisits: '2.1次', avgSpend: '¥860',
lastVisit: '12天前', monthlyConsume: '¥1,800', availableMonths: '4.6月',
lastRecharge: '1月20日', rechargeAmount: '¥10,000', recharges60d: '1次', currentBalance: '¥8,200',
spend60d: '¥3,600', visits60d: '4', highSpendTag: false,
avgInterval: '15.0天', intimacy: '68',
topCoachName: 'Amy', topCoachHeart: 8.5, topCoachScore: '8.5',
coachDetails: [
{ name: 'Amy', cls: 'assistant--assignee', heartScore: 8.5, badge: '跟', avgDuration: '1.8h', serviceCount: '10', coachSpend: '¥3,600', relationIdx: 8.5 },
{ name: '小燕', cls: 'assistant--abandoned', heartScore: 4.2, badge: '弃', avgDuration: '0.8h', serviceCount: '3', coachSpend: '¥600', relationIdx: 4.2 },
],
weeklyVisits: [
{ val: 1, pct: 40 }, { val: 1, pct: 40 }, { val: 0, pct: 10 }, { val: 1, pct: 40 },
{ val: 1, pct: 40 }, { val: 0, pct: 10 }, { val: 1, pct: 40 }, { val: 1, pct: 40 },
], coachName: 'Amy', coachRatio: '62%',
visitFreq: '2.1次/月',
daysAgo: 12,
assistants: [
{ name: 'Amy', cls: 'assistant--assignee', heartScore: 8.5, badge: '跟', badgeCls: 'assistant-badge--follow' },
{ name: '小燕', cls: 'assistant--abandoned', heartScore: 4.2, badge: '弃', badgeCls: 'assistant-badge--drop' },
],
},
{
id: 'u3', name: '张先生', initial: '张', avatarCls: 'avatar--blue',
idealDays: 5, elapsedDays: 8, overdueDays: 3,
visits30d: 5, balance: '¥1,200', recallIndex: '7.8',
potentialTags: [
{ text: '高频', theme: 'primary' },
],
spend30d: '¥3,500', avgVisits: '8.0次', avgSpend: '¥440',
lastVisit: '1天前', monthlyConsume: '¥3,200', availableMonths: '0.4月',
lastRecharge: '3月1日', rechargeAmount: '¥3,000', recharges60d: '3次', currentBalance: '¥1,200',
spend60d: '¥7,000', visits60d: '16', highSpendTag: true,
avgInterval: '3.8天', intimacy: '95',
topCoachName: '泡芙', topCoachHeart: 9.5, topCoachScore: '9.5',
coachDetails: [
{ name: '泡芙', cls: 'assistant--assignee', heartScore: 9.5, badge: '跟', avgDuration: '2.1h', serviceCount: '11', coachSpend: '¥3,300', relationIdx: 9.5 },
{ name: '小燕', cls: 'assistant--normal', heartScore: 6.8, avgDuration: '1.3h', serviceCount: '6', coachSpend: '¥1,800', relationIdx: 6.8 },
{ name: 'Amy', cls: 'assistant--abandoned', heartScore: 3.2, badge: '弃', avgDuration: '0.5h', serviceCount: '2', coachSpend: '¥400', relationIdx: 3.2 },
],
weeklyVisits: [
{ val: 2, pct: 50 }, { val: 3, pct: 75 }, { val: 2, pct: 50 }, { val: 4, pct: 100 },
{ val: 3, pct: 75 }, { val: 3, pct: 75 }, { val: 2, pct: 50 }, { val: 3, pct: 75 },
], coachName: '泡芙', coachRatio: '85%',
visitFreq: '8.0次/月',
daysAgo: 1,
assistants: [
{ name: '泡芙', cls: 'assistant--assignee', heartScore: 9.5, badge: '跟', badgeCls: 'assistant-badge--follow' },
],
},
]
Page({
data: {
pageState: 'loading' as 'loading' | 'empty' | 'normal',
activeTab: 'customer' as 'finance' | 'customer' | 'coach',
selectedDimension: 'recall',
dimensionOptions: DIMENSION_OPTIONS,
selectedProject: 'all',
projectOptions: PROJECT_OPTIONS,
/** 当前维度类型,控制卡片模板 */
dimType: 'recall' as DimType,
customers: [] as CustomerItem[],
allCustomers: [] as CustomerItem[],
totalCount: 0,
/** 筛选栏可见性 */
filterBarVisible: true,
},
_lastScrollTop: 0,
_scrollAcc: 0,
_scrollDir: null as 'up' | 'down' | null,
onLoad() {
this.loadData()
},
onPullDownRefresh() {
this.loadData()
setTimeout(() => wx.stopPullDownRefresh(), 500)
},
/** 滚动隐藏/显示筛选栏 */
onPageScroll(e: { scrollTop: number }) {
const y = e.scrollTop
const delta = y - this._lastScrollTop
this._lastScrollTop = y
if (y <= 8) {
if (!this.data.filterBarVisible) this.setData({ filterBarVisible: true })
this._scrollAcc = 0
this._scrollDir = null
return
}
if (Math.abs(delta) <= 2) return
const dir = delta > 0 ? 'down' : 'up'
if (dir !== this._scrollDir) {
this._scrollDir = dir
this._scrollAcc = 0
}
this._scrollAcc += Math.abs(delta)
const threshold = dir === 'up' ? 12 : 24
if (this._scrollAcc < threshold) return
const visible = dir === 'up'
if (this.data.filterBarVisible !== visible) {
this.setData({ filterBarVisible: visible })
}
this._scrollAcc = 0
},
loadData() {
this.setData({ pageState: 'loading' })
setTimeout(() => {
const data = MOCK_CUSTOMERS
if (!data || data.length === 0) {
this.setData({ pageState: 'empty' })
return
}
this.setData({
allCustomers: data,
customers: data,
totalCount: data.length,
pageState: 'normal',
})
}, 400)
},
onTabChange(e: WechatMiniprogram.TouchEvent) {
const tab = e.currentTarget.dataset.tab as string
if (tab === 'finance') {
wx.switchTab({ url: '/pages/board-finance/board-finance' })
} else if (tab === 'coach') {
wx.navigateTo({ url: '/pages/board-coach/board-coach' })
}
},
onDimensionChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
const val = e.detail.value
this.setData({
selectedDimension: val,
dimType: DIMENSION_TO_DIM[val] || 'recall',
})
},
onProjectChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
this.setData({ selectedProject: e.detail.value })
},
onCustomerTap(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
wx.navigateTo({ url: '/pages/customer-detail/customer-detail?id=' + id })
},
})

View File

@@ -0,0 +1,301 @@
<!-- 客户看板页 — 忠于 H5 原型8 维度卡片模板 -->
<!-- 加载态 -->
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
<t-loading theme="circular" size="70rpx" text="加载中..." />
</view>
<!-- 空状态 -->
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
<t-empty description="暂无客户数据" />
</view>
<!-- 正常态 -->
<block wx:else>
<!-- 顶部看板 Tab -->
<view class="board-tabs">
<view class="board-tab" data-tab="finance" bindtap="onTabChange">
<text>财务</text>
</view>
<view class="board-tab board-tab--active" data-tab="customer">
<text>客户</text>
</view>
<view class="board-tab" data-tab="coach" bindtap="onTabChange">
<text>助教</text>
</view>
</view>
<!-- 筛选区域 -->
<view class="filter-bar {{filterBarVisible ? '' : 'filter-bar--hidden'}}">
<view class="filter-bar-inner">
<view class="filter-item filter-item--wide">
<filter-dropdown
label="最应召回"
options="{{dimensionOptions}}"
value="{{selectedDimension}}"
bind:change="onDimensionChange"
/>
</view>
<view class="filter-item">
<filter-dropdown
label="全部"
options="{{projectOptions}}"
value="{{selectedProject}}"
bind:change="onProjectChange"
/>
</view>
</view>
</view>
<!-- 列表头部 -->
<view class="list-header">
<view class="list-header-left">
<text class="list-header-title">客户列表</text>
<text class="list-header-sub">· 前100名</text>
</view>
<text class="list-header-count">共{{totalCount}}名客户</text>
</view>
<!-- 客户列表 -->
<view class="customer-list">
<view
class="customer-card"
wx:for="{{customers}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onCustomerTap"
>
<!-- ===== 卡片头部:头像 + 姓名 + 右侧指标 ===== -->
<view class="card-header">
<view class="card-avatar {{item.avatarCls}}">
<text class="avatar-text">{{item.initial}}</text>
</view>
<text class="card-name" wx:if="{{dimType !== 'freq60'}}">{{item.name}}</text>
<!-- 最频繁:姓名 + 下方小字垂直排列 -->
<view class="card-name-group" wx:if="{{dimType === 'freq60'}}">
<text class="card-name">{{item.name}}</text>
<view class="card-name-sub">
<text class="mid-text" style="white-space: nowrap;">平均间隔 <text class="mid-primary">{{item.avgInterval}}</text></text>
<text class="mid-text" style="margin-left: 16rpx; white-space: nowrap;">60天消费 <text class="mid-dark">{{item.spend60d}}</text></text>
</view>
</view>
<view class="card-header-spacer"></view>
<!-- 最应召回:理想/已过/超期 -->
<view class="header-metrics" wx:if="{{dimType === 'recall'}}">
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
<text class="metric-gray">已过 <text class="metric-error">{{item.elapsedDays}}天</text></text>
<view class="overdue-tag {{item.overdueDays > 7 ? 'overdue-tag--danger' : 'overdue-tag--warn'}}">
<text>超期 {{item.overdueDays}}天</text>
</view>
</view>
<!-- 最大消费潜力:频率/客单/余额标签 -->
<view class="header-metrics" wx:elif="{{dimType === 'potential'}}">
<view class="potential-tag potential-tag--{{tag.theme}}" wx:for="{{item.potentialTags}}" wx:for-item="tag" wx:key="text">
<text>{{tag.text}}</text>
</view>
</view>
<!-- 最高余额:最近到店/理想 -->
<view class="header-metrics" wx:elif="{{dimType === 'balance'}}">
<text class="metric-gray">最近到店 <text class="metric-dark">{{item.lastVisit}}</text></text>
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
</view>
<!-- 最近充值:最近到店/理想 -->
<view class="header-metrics" wx:elif="{{dimType === 'recharge'}}">
<text class="metric-gray">最近到店 <text class="metric-dark">{{item.lastVisit}}</text></text>
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
</view>
<!-- 最频繁近60天右上角大字到店次数 -->
<view class="header-metrics header-metrics--freq" wx:elif="{{dimType === 'freq60'}}">
<view class="freq-big-num">
<text class="freq-big-val">{{item.visits60d}}</text>
<text class="freq-big-unit">次</text>
</view>
<text class="freq-big-label">60天到店</text>
</view>
<!-- 最专一近60天右上角 ❤️ 助教名 + 关系指数 -->
<view class="header-metrics header-metrics--loyal" wx:elif="{{dimType === 'loyal'}}">
<heart-icon score="{{item.topCoachHeart}}" />
<text class="loyal-top-name">{{item.topCoachName}}</text>
<text class="loyal-top-score">{{item.topCoachScore}}</text>
</view>
<!-- 最高消费近60天高消费标签 -->
<view class="header-metrics" wx:elif="{{dimType === 'spend60'}}">
<view class="potential-tag potential-tag--warning" wx:if="{{item.highSpendTag}}">
<text>高消费</text>
</view>
</view>
<!-- 最近到店:右上角大字 X天前到店 -->
<view class="header-metrics header-metrics--recent" wx:elif="{{dimType === 'recent'}}">
<view class="recent-big-num">
<text class="recent-big-val">{{item.daysAgo}}</text>
<text class="recent-big-unit">天前到店</text>
</view>
</view>
</view>
<!-- ===== 卡片中间行:维度特定数据 ===== -->
<!-- 最应召回30天到店 / 余额 / 召回指数 -->
<view class="card-mid-row" wx:if="{{dimType === 'recall'}}">
<text class="mid-text">30天到店 <text class="mid-dark">{{item.visits30d}}次</text></text>
<text class="mid-text mid-ml">余额 <text class="mid-dark">{{item.balance}}</text></text>
<text class="mid-text mid-right">召回指数 <text class="mid-primary-bold">{{item.recallIndex}}</text></text>
</view>
<!-- 最大消费潜力4 列网格30天消费用橙色大字和最高余额的余额值一致 -->
<view class="card-grid card-grid--4" wx:elif="{{dimType === 'potential'}}">
<view class="grid-cell">
<text class="grid-label">30天消费</text>
<text class="grid-val grid-val--warning grid-val--lg">{{item.spend30d}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">月均到店</text>
<text class="grid-val">{{item.avgVisits}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">余额</text>
<text class="grid-val grid-val--success">{{item.balance}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">次均消费</text>
<text class="grid-val">{{item.avgSpend}}</text>
</view>
</view>
<!-- 最高余额3 列网格 -->
<view class="card-grid card-grid--3" wx:elif="{{dimType === 'balance'}}">
<view class="grid-cell">
<text class="grid-label">余额</text>
<text class="grid-val grid-val--warning grid-val--lg">{{item.balance}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">月均消耗</text>
<text class="grid-val">{{item.monthlyConsume}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">可用</text>
<text class="grid-val grid-val--success">{{item.availableMonths}}</text>
</view>
</view>
<!-- 最近充值4 列网格 -->
<view class="card-grid card-grid--4" wx:elif="{{dimType === 'recharge'}}">
<view class="grid-cell">
<text class="grid-label">最后充值</text>
<text class="grid-val">{{item.lastRecharge}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">充值</text>
<text class="grid-val grid-val--success">{{item.rechargeAmount}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">60天充值</text>
<text class="grid-val">{{item.recharges60d}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">当前余额</text>
<text class="grid-val grid-val--warning">{{item.currentBalance}}</text>
</view>
</view>
<!-- 最高消费近60天3 列网格 -->
<view class="card-grid card-grid--3" wx:elif="{{dimType === 'spend60'}}">
<view class="grid-cell">
<text class="grid-label">近60天消费</text>
<text class="grid-val grid-val--warning grid-val--lg">{{item.spend60d}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">到店次数</text>
<text class="grid-val">{{item.visits60d}}</text>
</view>
<view class="grid-cell">
<text class="grid-label">次均消费</text>
<text class="grid-val">{{item.avgSpend}}</text>
</view>
</view>
<!-- 最频繁近60天无中间行数据已在头部 -->
<!-- 最频繁迷你柱状图8 周) -->
<view class="mini-chart" wx:if="{{dimType === 'freq60'}}">
<view class="mini-chart-header">
<text class="mini-chart-label">8周前</text>
<text class="mini-chart-label">本周</text>
</view>
<view class="mini-chart-bars">
<view class="mini-bar-col" wx:for="{{item.weeklyVisits}}" wx:for-item="wv" wx:for-index="wIdx" wx:key="wIdx">
<view class="mini-bar" style="height:{{wv.pct}}%;opacity:{{0.2 + wv.pct * 0.006}}"></view>
</view>
</view>
<view class="mini-chart-nums">
<text class="mini-chart-num {{wIdx === item.weeklyVisits.length - 1 ? 'mini-chart-num--active' : ''}}" wx:for="{{item.weeklyVisits}}" wx:for-item="wv" wx:for-index="wIdx" wx:key="wIdx">{{wv.val}}</text>
</view>
</view>
<!-- 最专一近60天助教服务明细表 -->
<view class="loyal-table" wx:elif="{{dimType === 'loyal'}}">
<!-- 表头 -->
<view class="loyal-row loyal-row--header">
<text class="loyal-col loyal-col--name">助教</text>
<text class="loyal-col">次均时长</text>
<text class="loyal-col">服务次数</text>
<text class="loyal-col">助教消费</text>
<text class="loyal-col">关系指数</text>
</view>
<!-- 数据行 -->
<view class="loyal-row" wx:for="{{item.coachDetails}}" wx:for-item="cd" wx:key="name">
<view class="loyal-col loyal-col--name">
<heart-icon score="{{cd.heartScore}}" />
<text class="loyal-coach-name {{cd.cls}}">{{cd.name}}</text>
<text class="assistant-badge assistant-badge--follow" wx:if="{{cd.badge === '跟'}}">跟</text>
<text class="assistant-badge assistant-badge--drop" wx:elif="{{cd.badge === '弃'}}">弃</text>
</view>
<text class="loyal-col loyal-val">{{cd.avgDuration}}</text>
<text class="loyal-col loyal-val">{{cd.serviceCount}}</text>
<text class="loyal-col loyal-val">{{cd.coachSpend}}</text>
<text class="loyal-col {{cd.relationIdx >= 8 ? 'loyal-val--primary' : 'loyal-val--gray'}}">{{cd.relationIdx}}</text>
</view>
</view>
<!-- 最近到店:理想间隔 / 60天到店 / 次均消费 -->
<view class="card-mid-row" wx:elif="{{dimType === 'recent'}}">
<text class="mid-text">理想间隔 <text class="mid-dark">{{item.idealDays}}天</text></text>
<text class="mid-text mid-ml">60天到店 <text class="mid-dark">{{item.visits60d}}天</text></text>
<text class="mid-text mid-ml">次均消费 <text class="mid-dark">{{item.avgSpend}}</text></text>
</view>
<!-- ===== 卡片底部:助教行(最专一维度不显示,因为助教信息在表格中) ===== -->
<view class="card-assistant-row" wx:if="{{item.assistants && item.assistants.length > 0 && dimType !== 'loyal'}}">
<text class="assistant-label">助教:</text>
<block wx:for="{{item.assistants}}" wx:for-item="ast" wx:for-index="astIdx" wx:key="name">
<text class="assistant-sep" wx:if="{{astIdx > 0}}">|</text>
<view class="assistant-tag">
<heart-icon score="{{ast.heartScore}}" />
<text class="assistant-name {{ast.cls}}">{{ast.name}}</text>
<text class="assistant-badge assistant-badge--follow" wx:if="{{ast.badge === '跟'}}">跟</text>
<text class="assistant-badge assistant-badge--drop" wx:elif="{{ast.badge === '弃'}}">弃</text>
</view>
</block>
</view>
</view>
</view>
<!-- 底部安全区 -->
<view class="safe-bottom"></view>
</block>
<!-- 自定义底部导航栏 -->
<board-tab-bar active="board" />
<!-- AI 悬浮按钮 -->
<ai-float-button bottom="{{220}}" />
<dev-fab />

View File

@@ -0,0 +1,637 @@
/* 客户看板页 — 忠于 H5 原型87.5% 缩放 */
/* ===== 三态 ===== */
.page-loading,
.page-empty {
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
}
/* ===== 看板 Tab对齐 board-coach 规范) ===== */
.board-tabs {
display: flex;
background: #ffffff;
border-bottom: 2rpx solid #eeeeee;
position: sticky;
top: 0;
z-index: 20;
}
.board-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 26rpx;
font-weight: 500;
color: #8b8b8b;
position: relative;
}
.board-tab--active {
color: #0052d9;
font-weight: 600;
}
.board-tab--active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 42rpx;
height: 5rpx;
background: linear-gradient(90deg, #0052d9, #5b9cf8);
border-radius: 3rpx;
}
/* ===== 筛选区域(对齐 board-coach 规范) ===== */
.filter-bar {
background: #f3f3f3;
padding: 14rpx 28rpx;
position: sticky;
top: 70rpx;
z-index: 15;
border-bottom: 2rpx solid #eeeeee;
transition: transform 220ms ease, opacity 220ms ease;
}
.filter-bar--hidden {
opacity: 0;
transform: translateY(-110%);
pointer-events: none;
}
.filter-bar-inner {
display: flex;
align-items: center;
gap: 14rpx;
background: #ffffff;
border-radius: 14rpx;
padding: 10rpx;
border: 2rpx solid #eeeeee;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
}
.filter-item {
flex: 1;
min-width: 0;
}
.filter-item--wide {
flex: 1.8;
}
/* ===== 列表头部 ===== */
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 28rpx 12rpx;
}
.list-header-left {
display: flex;
align-items: center;
gap: 8rpx;
}
.list-header-title {
font-size: 28rpx;
font-weight: 600;
color: #242424;
}
.list-header-sub {
font-size: 24rpx;
color: #a6a6a6;
}
.list-header-count {
font-size: 24rpx;
color: #c5c5c5;
}
/* ===== 客户列表 ===== */
.customer-list {
padding: 0 28rpx 24rpx;
margin-top: 4rpx;
}
/* ===== 客户卡片 ===== */
.customer-card {
background: #ffffff;
border-radius: 28rpx;
padding: 30rpx 28rpx 26rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.customer-card:active {
opacity: 0.96;
transform: scale(0.98);
}
/* ===== 卡片头部 ===== */
.card-header {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 6rpx;
}
.card-avatar {
width: 66rpx;
height: 66rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-text {
color: #ffffff;
font-size: 26rpx;
font-weight: 600;
}
/* 头像渐变色 */
.avatar--amber { background: linear-gradient(135deg, #fbbf24, #f97316); }
.avatar--pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
.avatar--blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
.avatar--green { background: linear-gradient(135deg, #4ade80, #10b981); }
.avatar--purple { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
.avatar--cyan { background: linear-gradient(135deg, #22d3ee, #14b8a6); }
.avatar--rose { background: linear-gradient(135deg, #fb7185, #e11d48); }
.avatar--indigo { background: linear-gradient(135deg, #818cf8, #4f46e5); }
.card-name {
font-size: 28rpx;
font-weight: 600;
color: #242424;
flex-shrink: 0;
}
/* 最频繁:姓名+小字垂直排列 */
.card-name-group {
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.card-name-sub {
display: flex;
align-items: center;
margin-top: 2rpx;
white-space: nowrap;
overflow: hidden;
}
.card-header-spacer {
flex: 1;
}
/* 头部右侧指标区 */
.header-metrics {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
/* 最频繁维度:右上角大字到店次数 */
.header-metrics--freq {
flex-direction: column;
align-items: flex-end;
gap: 0;
}
.freq-big-num {
display: flex;
align-items: baseline;
}
.freq-big-val {
font-size: 36rpx;
font-weight: 700;
color: #0052d9;
line-height: 1;
}
.freq-big-unit {
font-size: 22rpx;
font-weight: 400;
color: #a6a6a6;
margin-left: 2rpx;
}
.freq-big-label {
font-size: 20rpx;
color: #a6a6a6;
margin-top: -2rpx;
}
/* 最近到店维度:右上角大字 X天前到店 */
.header-metrics--recent {
gap: 4rpx;
}
.recent-big-num {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.recent-big-val {
font-size: 40rpx;
font-weight: 700;
color: #00a870;
line-height: 1;
}
.recent-big-unit {
font-size: 22rpx;
font-weight: 400;
color: #a6a6a6;
}
.metric-gray {
font-size: 22rpx;
color: #a6a6a6;
}
.metric-dark {
color: #393939;
font-weight: 600;
}
.metric-error {
color: #e34d59;
font-weight: 700;
}
/* 超期标签 */
.overdue-tag {
padding: 4rpx 10rpx;
font-size: 22rpx;
font-weight: 700;
border-radius: 6rpx;
}
.overdue-tag--danger {
background: rgba(227, 77, 89, 0.1);
color: #e34d59;
}
.overdue-tag--warn {
background: rgba(237, 123, 47, 0.1);
color: #ed7b2f;
}
/* 消费潜力标签 */
.potential-tag {
padding: 4rpx 10rpx;
font-size: 22rpx;
border-radius: 6rpx;
}
.potential-tag--primary {
background: rgba(0, 82, 217, 0.1);
color: #0052d9;
}
.potential-tag--warning {
background: rgba(237, 123, 47, 0.1);
color: #ed7b2f;
}
.potential-tag--success {
background: rgba(0, 168, 112, 0.1);
color: #00a870;
}
.potential-tag--gray {
background: #eeeeee;
color: #777777;
}
/* ===== 卡片中间行flex 布局,左对齐名字位置) ===== */
.card-mid-row {
display: flex;
align-items: center;
padding: 6rpx 0 4rpx 80rpx;
}
.mid-text {
font-size: 24rpx;
color: #c5c5c5;
}
.mid-dark {
color: #393939;
font-weight: 600;
}
.mid-primary {
color: #0052d9;
font-weight: 500;
}
.mid-primary-bold {
color: #0052d9;
font-weight: 700;
}
.mid-ml {
margin-left: 20rpx;
}
.mid-right {
margin-left: auto;
}
.mid-error {
color: #e34d59;
font-weight: 700;
}
/* ===== 网格布局 ===== */
.card-grid {
display: grid;
gap: 12rpx;
padding: 6rpx 0 4rpx 80rpx;
}
.card-grid--3 {
grid-template-columns: repeat(3, 1fr);
text-align: center;
}
.card-grid--4 {
grid-template-columns: repeat(4, 1fr);
text-align: center;
}
.grid-cell {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.grid-label {
font-size: 18rpx;
color: #a6a6a6;
}
.grid-val {
font-size: 24rpx;
font-weight: 600;
color: #393939;
}
.grid-val--success {
color: #00a870;
}
.grid-val--warning {
color: #ed7b2f;
}
.grid-val--lg {
font-size: 28rpx;
font-weight: 700;
}
/* ===== 迷你柱状图(最频繁维度) ===== */
.mini-chart {
padding: 8rpx 0 4rpx 80rpx;
}
.mini-chart-header {
display: flex;
justify-content: space-between;
margin-bottom: 4rpx;
}
.mini-chart-label {
font-size: 18rpx;
color: #c5c5c5;
}
.mini-chart-bars {
display: flex;
align-items: flex-end;
gap: 6rpx;
height: 48rpx;
}
.mini-bar-col {
flex: 1;
display: flex;
align-items: flex-end;
height: 100%;
}
.mini-bar {
width: 100%;
background: rgba(0, 82, 217, 0.3);
border-radius: 4rpx 4rpx 0 0;
min-height: 4rpx;
}
.mini-chart-nums {
display: flex;
gap: 6rpx;
margin-top: 4rpx;
}
.mini-chart-num {
flex: 1;
text-align: center;
font-size: 18rpx;
color: #c5c5c5;
}
.mini-chart-num--active {
color: #0052d9;
font-weight: 700;
}
/* ===== 助教行 ===== */
.card-assistant-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 10rpx;
margin-left: 80rpx;
padding-top: 10rpx;
border-top: 2rpx solid rgba(0, 0, 0, 0.04);
}
.assistant-label {
font-size: 22rpx;
color: #a6a6a6;
flex-shrink: 0;
}
.assistant-tag {
display: flex;
align-items: center;
gap: 4rpx;
}
.assistant-heart {
width: 24rpx;
height: 24rpx;
}
.assistant-name {
font-size: 22rpx;
font-weight: 500;
}
.assistant-name.assistant--assignee {
color: #e34d59;
font-weight: 700;
}
.assistant-name.assistant--abandoned {
color: #a6a6a6;
}
.assistant-name.assistant--normal {
color: #242424;
}
.assistant-sep {
font-size: 20rpx;
color: #c5c5c5;
margin: 0 6rpx;
}
/* 跟/弃 badge — 忠于 H5 原型:白字 + 渐变背景 + 阴影 */
.assistant-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28rpx;
height: 24rpx;
padding: 0 8rpx;
border-radius: 10rpx;
font-size: 18rpx;
line-height: 1;
font-weight: 700;
letter-spacing: 0.5rpx;
margin-left: 4rpx;
color: #ffffff;
}
.assistant-badge--follow {
background: linear-gradient(135deg, #e34d59 0%, #f26a76 100%);
border: 1rpx solid rgba(227, 77, 89, 0.28);
box-shadow: 0 2rpx 6rpx rgba(227, 77, 89, 0.28);
}
.assistant-badge--drop {
background: linear-gradient(135deg, #d4d4d4 0%, #b6b6b6 100%);
border: 1rpx solid rgba(120, 120, 120, 0.18);
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.14);
}
/* ===== 最专一维度:助教服务明细表 ===== */
.loyal-table {
padding: 6rpx 0 4rpx 80rpx;
border-left: 4rpx solid #eeeeee;
margin-left: 80rpx;
padding-left: 14rpx;
margin-top: 4rpx;
}
.loyal-row {
display: flex;
align-items: center;
gap: 4rpx;
padding: 6rpx 0;
}
.loyal-row--header {
padding-bottom: 8rpx;
}
.loyal-row--header .loyal-col {
font-size: 20rpx;
color: #c5c5c5;
}
.loyal-col {
flex: 1;
text-align: right;
font-size: 24rpx;
}
.loyal-col--name {
width: 140rpx;
flex: none;
text-align: left;
display: flex;
align-items: center;
gap: 4rpx;
}
.loyal-coach-name {
font-size: 22rpx;
font-weight: 500;
}
.loyal-coach-name.assistant--assignee {
color: #e34d59;
font-weight: 700;
}
.loyal-coach-name.assistant--abandoned {
color: #a6a6a6;
}
.loyal-coach-name.assistant--normal {
color: #242424;
}
.loyal-val {
font-weight: 600;
color: #393939;
}
.loyal-val--primary {
font-weight: 700;
color: #0052d9;
}
.loyal-val--gray {
color: #8b8b8b;
}
/* 最专一头部右侧 */
.header-metrics--loyal {
gap: 6rpx;
}
.loyal-top-name {
font-size: 24rpx;
font-weight: 600;
color: #e34d59;
}
.loyal-top-score {
font-size: 24rpx;
font-weight: 700;
color: #0052d9;
}
/* ===== 底部安全区 ===== */
.safe-bottom {
height: 200rpx;
padding-bottom: env(safe-area-inset-bottom);
}