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:
Neo
2026-03-20 09:02:10 +08:00
parent 3d2e5f8165
commit beb88d5bea
388 changed files with 6436 additions and 25458 deletions

View File

@@ -0,0 +1,17 @@
{
"navigationBarTitleText": "助教看板",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"usingComponents": {
"coach-level-tag": "/components/coach-level-tag/coach-level-tag",
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
"ai-float-button": "/components/ai-float-button/ai-float-button",
"board-tab-bar": "/custom-tab-bar/index",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-tag": "tdesign-miniprogram/tag/tag",
"dev-fab": "/components/dev-fab/dev-fab"
}
}

View File

@@ -0,0 +1,289 @@
// 助教看板页 — 排序×技能×时间三重筛选4 种维度卡片
// TODO: 联调时替换 mock 数据为真实 API 调用
import { formatMoney, formatCount } from '../../utils/money'
import { formatHours } from '../../utils/time'
export {}
/** 排序维度 → 卡片模板映射 */
type DimType = 'perf' | 'salary' | 'sv' | 'task'
const SORT_TO_DIM: Record<string, DimType> = {
perf_desc: 'perf',
perf_asc: 'perf',
salary_desc: 'salary',
salary_asc: 'salary',
sv_desc: 'sv',
task_desc: 'task',
}
const SORT_OPTIONS = [
{ value: 'perf_desc', text: '定档业绩最高' },
{ value: 'perf_asc', text: '定档业绩最低' },
{ value: 'salary_desc', text: '工资最高' },
{ value: 'salary_asc', text: '工资最低' },
{ value: 'sv_desc', text: '客源储值最高' },
{ value: 'task_desc', text: '任务完成最多' },
]
const SKILL_OPTIONS = [
{ value: 'all', text: '不限' },
{ value: 'chinese', text: '🎱 中式/追分' },
{ value: 'snooker', text: '斯诺克' },
{ value: 'mahjong', text: '🀄 麻将/棋牌' },
{ value: 'karaoke', text: '🎤 团建/K歌' },
]
const TIME_OPTIONS = [
{ value: 'month', text: '本月' },
{ value: 'quarter', text: '本季度' },
{ value: 'last_month', text: '上月' },
{ value: 'last_3m', text: '前3个月(不含本月)' },
{ value: 'last_quarter', text: '上季度' },
{ value: 'last_6m', text: '最近6个月(不含本月,不支持客源储值最高)' },
]
/** 等级 → 样式类映射 */
const LEVEL_CLASS: Record<string, string> = {
star: 'level--star',
senior: 'level--high',
middle: 'level--mid',
junior: 'level--low',
// 兼容旧中文 key 过渡期
'星级': 'level--star',
'高级': 'level--high',
'中级': 'level--mid',
'初级': 'level--low',
}
/** 技能 → 样式类映射 */
const SKILL_CLASS: Record<string, string> = {
'🎱': 'skill--chinese',
'斯': 'skill--snooker',
'🀄': 'skill--mahjong',
'🎤': 'skill--karaoke',
}
interface CoachItem {
id: string
name: string
initial: string
avatarGradient: string
level: string // 英文 key: star | senior | middle | junior
levelClass: string
skills: Array<{ text: string; cls: string }>
topCustomers: string[]
// 定档业绩维度
perfHours: number
perfHoursBefore?: number
perfGap?: string
perfReached: boolean
// 工资维度
salary: number
salaryPerfHours: number
salaryPerfBefore?: number
// 客源储值维度
svAmount: number
svCustomerCount: number
svConsume: number
// 任务维度
taskRecall: number
taskCallback: number
}
/** Mock 数据(忠于 H5 原型 6 位助教) */
const MOCK_COACHES: CoachItem[] = [
{
id: 'c1', name: '小燕', initial: '小',
avatarGradient: 'blue',
level: 'star', levelClass: LEVEL_CLASS['star'],
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
topCustomers: ['💖 王先生', '💖 李女士', '💛 赵总'],
perfHours: 86.2, perfHoursBefore: 92.0, perfGap: '距升档 13.8h', perfReached: false,
salary: 12680, salaryPerfHours: 86.2, salaryPerfBefore: 92.0,
svAmount: 45200, svCustomerCount: 18, svConsume: 8600,
taskRecall: 18, taskCallback: 14,
},
{
id: 'c2', name: '泡芙', initial: '泡',
avatarGradient: 'green',
level: 'senior', levelClass: LEVEL_CLASS['senior'],
skills: [{ text: '斯', cls: SKILL_CLASS['斯'] }],
topCustomers: ['💖 陈先生', '💛 刘女士', '💛 黄总'],
perfHours: 72.5, perfHoursBefore: 78.0, perfGap: '距升档 7.5h', perfReached: false,
salary: 10200, salaryPerfHours: 72.5, salaryPerfBefore: 78.0,
svAmount: 38600, svCustomerCount: 15, svConsume: 6200,
taskRecall: 15, taskCallback: 13,
},
{
id: 'c3', name: 'Lucy', initial: 'A',
avatarGradient: 'pink',
level: 'star', levelClass: LEVEL_CLASS['star'],
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }, { text: '斯', cls: SKILL_CLASS['斯'] }],
topCustomers: ['💖 张先生', '💛 周女士', '💛 吴总'],
perfHours: 68.0, perfHoursBefore: 72.5, perfGap: '距升档 32.0h', perfReached: false,
salary: 9800, salaryPerfHours: 68.0, salaryPerfBefore: 72.5,
svAmount: 32100, svCustomerCount: 14, svConsume: 5800,
taskRecall: 12, taskCallback: 13,
},
{
id: 'c4', name: 'Mia', initial: 'M',
avatarGradient: 'amber',
level: 'middle', levelClass: LEVEL_CLASS['middle'],
skills: [{ text: '🀄', cls: SKILL_CLASS['🀄'] }],
topCustomers: ['💛 赵先生', '💛 吴女士', '💛 孙总'],
perfHours: 55.0, perfGap: '距升档 5.0h', perfReached: false,
salary: 7500, salaryPerfHours: 55.0,
svAmount: 28500, svCustomerCount: 12, svConsume: 4100,
taskRecall: 10, taskCallback: 10,
},
{
id: 'c5', name: '糖糖', initial: '糖',
avatarGradient: 'violet',
level: 'junior', levelClass: LEVEL_CLASS['junior'],
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
topCustomers: ['💛 钱先生', '💛 孙女士', '💛 周总'],
perfHours: 42.0, perfHoursBefore: 45.0, perfReached: true,
salary: 6200, salaryPerfHours: 42.0, salaryPerfBefore: 45.0,
svAmount: 22000, svCustomerCount: 10, svConsume: 3500,
taskRecall: 8, taskCallback: 10,
},
{
id: 'c6', name: '露露', initial: '露',
avatarGradient: 'cyan',
level: 'middle', levelClass: LEVEL_CLASS['middle'],
skills: [{ text: '🎤', cls: SKILL_CLASS['🎤'] }],
topCustomers: ['💛 郑先生', '💛 冯女士', '💛 陈总'],
perfHours: 38.0, perfGap: '距升档 22.0h', perfReached: false,
salary: 5100, salaryPerfHours: 38.0,
svAmount: 18300, svCustomerCount: 9, svConsume: 2800,
taskRecall: 6, taskCallback: 9,
},
]
Page({
data: {
pageState: 'loading' as 'loading' | 'empty' | 'normal' | 'error',
activeTab: 'coach' as 'finance' | 'customer' | 'coach',
selectedSort: 'perf_desc',
sortOptions: SORT_OPTIONS,
selectedSkill: 'all',
skillOptions: SKILL_OPTIONS,
selectedTime: 'month',
timeOptions: TIME_OPTIONS,
/** 当前维度类型,控制卡片模板 */
dimType: 'perf' as DimType,
coaches: [] as CoachItem[],
allCoaches: [] as CoachItem[],
/** 筛选栏可见性(滚动隐藏/显示) */
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_COACHES
if (!data || data.length === 0) {
this.setData({ pageState: 'empty' })
return
}
// 格式化数字字段为展示字符串
const enriched = data.map((c) => ({
...c,
perfHoursLabel: formatHours(c.perfHours),
perfHoursBeforeLabel: c.perfHoursBefore ? formatHours(c.perfHoursBefore) : undefined,
salaryLabel: formatMoney(c.salary),
salaryPerfHoursLabel: formatHours(c.salaryPerfHours),
salaryPerfBeforeLabel: c.salaryPerfBefore ? formatHours(c.salaryPerfBefore) : undefined,
svAmountLabel: formatMoney(c.svAmount),
svCustomerCountLabel: formatCount(c.svCustomerCount, '人'),
svConsumeLabel: formatMoney(c.svConsume),
taskRecallLabel: formatCount(c.taskRecall, '次'),
taskCallbackLabel: formatCount(c.taskCallback, '次'),
}))
this.setData({ allCoaches: enriched, coaches: enriched, 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 === 'customer') {
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
}
},
onSortChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
const val = e.detail.value
this.setData({
selectedSort: val,
dimType: SORT_TO_DIM[val] || 'perf',
})
},
onSkillChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
this.setData({ selectedSkill: e.detail.value })
},
onTimeChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
this.setData({ selectedTime: e.detail.value })
},
onRetry() {
this.loadData()
},
onCoachTap(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
wx.navigateTo({ url: '/pages/coach-detail/coach-detail?id=' + id })
},
})

View File

@@ -0,0 +1,136 @@
<!-- 助教看板页 — 忠于 H5 原型结构 -->
<!-- 加载态toast 浮层,不白屏) -->
<view class="g-toast-loading" wx:if="{{pageState === 'loading'}}">
<view class="g-toast-loading-inner">
<t-loading theme="circular" size="40rpx" />
<text class="g-toast-loading-text">加载中...</text>
</view>
</view>
<!-- 空状态 -->
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
<t-empty description="暂无助教数据" />
</view>
<!-- 错误态 -->
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-empty description="加载失败" />
<view class="retry-btn" bindtap="onRetry">点击重试</view>
</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" data-tab="customer" bindtap="onTabChange">
<text>客户</text>
</view>
<view class="board-tab board-tab--active" data-tab="coach">
<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="{{sortOptions}}" value="{{selectedSort}}" bind:change="onSortChange" />
</view>
<view class="filter-item">
<filter-dropdown label="不限" options="{{skillOptions}}" value="{{selectedSkill}}" bind:change="onSkillChange" />
</view>
<view class="filter-item">
<filter-dropdown label="本月" options="{{timeOptions}}" value="{{selectedTime}}" bind:change="onTimeChange" />
</view>
</view>
</view>
<!-- 助教列表 -->
<view class="coach-list">
<view class="coach-card" wx:for="{{coaches}}" wx:key="id" data-id="{{item.id}}" bindtap="onCoachTap" hover-class="coach-card--hover">
<view class="card-row">
<!-- 头像 -->
<view class="card-avatar avatar-{{item.avatarGradient}}">
<text class="avatar-text">{{item.initial}}</text>
</view>
<!-- 信息区 -->
<view class="card-info">
<!-- 第一行:姓名 + 等级 + 技能 + 右侧指标 -->
<view class="card-name-row">
<text class="card-name">{{item.name}}</text>
<coach-level-tag level="{{item.level}}" shadowColor="rgba(0,0,0,0)" />
<text class="skill-tag {{skill.cls}}" wx:for="{{item.skills}}" wx:for-item="skill" wx:key="text">{{skill.text}}</text>
<!-- 定档业绩维度 -->
<view class="card-right" wx:if="{{dimType === 'perf'}}">
<text class="right-text">定档 <text class="right-highlight">{{item.perfHoursLabel}}</text></text>
<text class="right-sub" wx:if="{{item.perfHoursBeforeLabel}}">折前 <text class="right-sub-val">{{item.perfHoursBeforeLabel}}</text></text>
</view>
<!-- 工资维度 -->
<view class="card-right" wx:elif="{{dimType === 'salary'}}">
<text class="salary-tag">预估</text>
<text class="salary-amount">{{item.salaryLabel}}</text>
</view>
<!-- 客源储值维度 -->
<view class="card-right" wx:elif="{{dimType === 'sv'}}">
<text class="right-sub">储值</text>
<text class="salary-amount">{{item.svAmountLabel}}</text>
</view>
<!-- 任务维度 -->
<view class="card-right" wx:elif="{{dimType === 'task'}}">
<text class="right-text">召回 <text class="right-highlight">{{item.taskRecallLabel}}</text></text>
</view>
</view>
<!-- 第二行:客户列表 + 右侧补充 -->
<view class="card-bottom-row">
<view class="customer-list">
<block wx:for="{{item.topCustomers}}" wx:for-item="cust" wx:for-index="custIdx" wx:key="*this">
<text class="customer-item" wx:if="{{dimType !== 'sv' || custIdx < 2}}">{{cust}}</text>
</block>
</view>
<!-- 定档业绩:距升档/已达标 -->
<text class="bottom-right bottom-right--warning" wx:if="{{dimType === 'perf' && !item.perfReached}}">{{item.perfGap}}</text>
<text class="bottom-right bottom-right--success" wx:elif="{{dimType === 'perf' && item.perfReached}}">✅ 已达标</text>
<!-- 工资:定档/折前 -->
<view class="bottom-right-group" wx:elif="{{dimType === 'salary'}}">
<text class="bottom-perf">定档 <text class="bottom-perf-val">{{item.salaryPerfHoursLabel}}</text></text>
<text class="bottom-sub" wx:if="{{item.salaryPerfBeforeLabel}}">折前 <text class="bottom-sub-val">{{item.salaryPerfBeforeLabel}}</text></text>
</view>
<!-- 客源储值:客户数 | 消耗 -->
<view class="bottom-right-group" wx:elif="{{dimType === 'sv'}}">
<text class="bottom-sub">客户 <text class="bottom-perf-val">{{item.svCustomerCountLabel}}</text></text>
<text class="bottom-divider">|</text>
<text class="bottom-sub">消耗 <text class="bottom-perf-val">{{item.svConsumeLabel}}</text></text>
</view>
<!-- 任务:回访数 -->
<text class="bottom-sub" wx:elif="{{dimType === 'task'}}">回访 <text class="bottom-perf-val">{{item.taskCallbackLabel}}</text></text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部安全区(为自定义导航栏留空间) -->
<view class="safe-bottom"></view>
</block>
<!-- 自定义底部导航栏 -->
<board-tab-bar active="board" />
<!-- AI 悬浮按钮 — 在导航栏上方 -->
<ai-float-button />
<dev-fab wx:if="{{false}}" />

View File

@@ -0,0 +1,338 @@
/* 助教看板页 — H5 px × 2 × 0.875 精确转换 */
/* ===== 三态 ===== */
.page-loading,
.page-empty,
.page-error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 60vh;
}
.retry-btn {
margin-top: 24rpx;
padding: 16rpx 48rpx;
font-size: 28rpx;
color: #0052d9;
border: 2rpx solid #0052d9;
border-radius: 44rpx;
}
/* ===== 看板 Tab py-3=12px→22rpx, text-sm=14px→26rpx(视觉校准+2) ===== */
.board-tabs {
display: flex;
background: #ffffff;
border-bottom: 2rpx solid #eeeeee;
position: sticky;
top: 0;
z-index: 20;
}
/* text-sm=14px→24rpx, line-height 匹配 Tailwind 默认 1.25rem=20px→36rpx */
.board-tab {
flex: 1;
text-align: center;
padding: 22rpx 0;
font-size: 24rpx;
line-height: 34rpx;
font-weight: 500;
color: #8b8b8b;
position: relative;
}
.board-tab--active {
color: #0052d9;
font-weight: 600;
}
/* w-24px→42rpx, h-3px→5rpxH5 实际渲染偏细) */
.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;
}
/* px-4=16px→28rpx, py-2=8px→14rpx, sticky top-[44px]→77rpx */
.filter-bar {
background: #f3f3f3;
padding: 14rpx 28rpx;
position: sticky;
top: 77rpx;
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;
}
/* p-1.5=6px→10rpx, gap-2=8px→14rpx, rounded-lg=8px→14rpx */
.filter-bar-inner {
display: flex;
align-items: center;
gap: 14rpx;
background: #ffffff;
border-radius: 16rpx;
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: 2;
}
/* p-4=16px→28rpx, space-y-3=12px→22rpx — 四边一致 */
.coach-list {
padding: 13rpx 28rpx 28rpx 28rpx;
}
/* p-4=16px→28rpx, rounded-2xl=16px→32rpxborder-radius: px×2不乘0.875 */
.coach-card {
background: #ffffff;
border-radius: 32rpx;
padding: 28rpx;
margin-bottom: 22rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.coach-card--hover {
opacity: 0.96;
transform: scale(0.98);
}
/* gap-3=12px→20rpx, items-center 忠于 H5 原型 */
.card-row {
display: flex;
align-items: center;
gap: 20rpx;
}
/* w-11 h-11=44px→78rpx, text-base=16px→28rpx — items-center 对齐,无需 margin-top */
.card-avatar {
width: 78rpx;
height: 78rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
/* 头像渐变色由 app.wxss 全局 .avatar-{key} 统一提供VI §8 */
/* ===== 信息区 ===== */
.card-info {
flex: 1;
min-width: 0;
}
/* gap-1.5=6px→10rpx */
.card-name-row {
display: flex;
align-items: center;
gap: 10rpx;
flex-wrap: nowrap;
}
/* text-base=16px→28rpx, line-height: 1.5→42rpx匹配 Tailwind 默认) */
.card-name {
font-size: 28rpx;
line-height: 42rpx;
font-weight: 600;
color: #242424;
flex-shrink: 0;
}
/* ===== 等级标签 px-1.5=6px→10rpx, py-0.5=2px→4rpx, text-xs=12px→22rpx ===== */
.level-tag {
padding: 4rpx 10rpx;
font-size: 22rpx;
border-radius: 8rpx;
flex-shrink: 0;
font-weight: 500;
}
.level--star {
background: linear-gradient(to right, #fbbf24, #f97316);
color: #ffffff;
}
.level--high {
background: linear-gradient(to right, #a78bfa, #8b5cf6);
color: #ffffff;
}
.level--mid {
background: linear-gradient(to right, #60a5fa, #6366f1);
color: #ffffff;
}
.level--low {
background: linear-gradient(to right, #9ca3af, #6b7280);
color: #ffffff;
}
/* ===== 技能标签 ===== */
.skill-tag {
padding: 4rpx 10rpx;
font-size: 22rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.skill--chinese { background: rgba(0, 82, 217, 0.1); color: #0052d9; }
.skill--snooker { background: rgba(0, 168, 112, 0.1); color: #00a870; }
.skill--mahjong { background: rgba(237, 123, 47, 0.1); color: #ed7b2f; }
.skill--karaoke { background: rgba(227, 77, 89, 0.1); color: #e34d59; }
/* ml-auto, gap-2=8px→14rpx */
.card-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 14rpx;
flex-shrink: 0;
white-space: nowrap;
}
/* text-xs=12px→22rpx — "定档"标签文字 */
.right-text {
font-size: 22rpx;
color: #8b8b8b;
font-weight: 400;
}
/* text-sm=14px→24rpx — 定档数值加粗 */
.right-highlight {
font-size: 24rpx;
font-weight: 700;
color: #0052d9;
}
/* "折前"/"储值" 辅助文字 — text-xs=12px→22rpx, gray-6=#a6a6a6 */
.right-sub {
font-size: 22rpx;
color: #a6a6a6;
font-weight: 400;
}
.right-sub-val {
color: #8b8b8b;
font-weight: 400;
}
/* 工资维度 */
.salary-tag {
padding: 4rpx 10rpx;
background: rgba(237, 123, 47, 0.1);
color: #ed7b2f;
font-size: 22rpx;
border-radius: 6rpx;
}
/* text-lg=18px→32rpx, line-height: 28px→50rpx匹配 Tailwind text-lg 的 1.75rem */
.salary-amount {
font-size: 32rpx;
line-height: 50rpx;
font-weight: 700;
color: #242424;
}
/* mt-1.5=6px→10rpx, text-xs=12px→22rpx */
.card-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
}
/* gap-2=8px→14rpx */
.customer-list {
display: flex;
align-items: center;
gap: 14rpx;
overflow: hidden;
flex: 1;
min-width: 0;
}
.customer-item {
font-size: 22rpx;
color: #a6a6a6;
white-space: nowrap;
}
.bottom-right {
font-size: 22rpx;
font-weight: 500;
flex-shrink: 0;
}
.bottom-right--warning {
color: #ed7b2f;
}
.bottom-right--success {
color: #00a870;
}
.bottom-right-group {
display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0;
}
.bottom-perf {
font-size: 22rpx;
color: #4b4b4b;
font-weight: 600;
}
.bottom-perf-val {
font-weight: 700;
color: #4b4b4b;
}
.bottom-sub {
font-size: 22rpx;
color: #8b8b8b;
}
.bottom-sub-val {
color: #8b8b8b;
}
.bottom-divider {
font-size: 22rpx;
color: #c5c5c5;
}
/* ===== 底部安全区(自定义导航栏高度 100rpx + safe-area ===== */
.safe-bottom {
height: 200rpx;
padding-bottom: env(safe-area-inset-bottom);
}