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,297 @@
# Performance 页面迁移说明
## 迁移版本
- **H5 原型基准**`docs/h5_ui/pages/performance.html`
- **桥文档版本**v3.0
- **迁移日期**2026-03-15
- **更新日期**2026-03-15Banner背景改用灵活的SVG实现
---
## 样式调整清单
### 1. 单位转换规则
遵循桥文档 §2.3 的混合单位策略:
- **主要使用 `rpx`**:页面宽度、横向布局、卡片尺寸、间距
- **使用 `px`**导航栏高度44px、图标点击区域
- **换算基准**H5 CSS px → 小程序 rpx750宽基准1px = 1.8204rpx
### 2. 自定义导航栏(.custom-nav
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| height | 44px | 44px | 标准导航栏高度使用px |
| font-size | text-base (16px) | 32rpx | 标题字号 |
| icon size | w-5 h-5 (20px) | 24px | 返回按钮图标 |
### 3. Banner 区域(.banner-section
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| 背景方案 | CSS 渐变 | SVG 图片 | 使用 `/assets/images/banner-bg-combined.svg` |
| height | - | 480rpx | Banner 高度(可通过 WXSS 调整) |
| padding | px-5 pt-2 pb-2 (20px 8px) | 24rpx 40rpx 32rpx | 内容区内边距 |
#### Banner SVG 背景实现(灵活可调)
- **方案**SVG 做渐变底图(与 task-detail/task-list 保持一致)
- **原因**SVG pattern 在小程序 image 组件中不渲染
- **WXML 实现**
```wxml
<view class="banner-section">
<image class="banner-bg-img" src="/assets/images/banner-bg-combined.svg" mode="aspectFill" />
<view class="banner-content">
<!-- 内容 -->
</view>
</view>
```
- **WXSS 实现**(可灵活调整):
```wxss
.banner-section {
position: relative;
width: 100%;
overflow: hidden;
height: 480rpx; /* 调整此值改变 Banner 高度 */
}
.banner-bg-img {
position: absolute;
top: 0; /* 调整此值改变背景垂直位置 */
left: 0; /* 调整此值改变背景水平位置 */
width: 100%;
height: 100%;
z-index: 0;
}
.banner-content {
position: relative;
z-index: 2;
padding: 24rpx 40rpx 32rpx; /* 调整此值改变内容区内边距 */
height: 100%;
display: flex;
flex-direction: column;
}
```
- **调整方式**
- 修改 `.banner-section` 的 `height` 调整 Banner 高度
- 修改 `.banner-bg-img` 的 `top/left` 调整背景位置
- 修改 `.banner-bg-img` 的 `width/height` 调整背景缩放
- 修改 `.banner-content` 的 `padding` 调整内容区域
#### 个人信息卡片
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| avatar size | w-14 h-14 (56px) | 112rpx | 头像尺寸 |
| avatar radius | rounded-2xl (16px) | 24rpx | 头像圆角 |
| name font-size | text-xl (20px) | 40rpx | 姓名字号 |
| role tag padding | px-2 py-0.5 (8px 2px) | 4rpx 16rpx | 角色标签内边距 |
#### 收入概览卡片
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| grid gap | gap-3 (12px) | 24rpx | 卡片间距 |
| card padding | p-4 (16px) | 24rpx | 卡片内边距 |
| card radius | rounded-xl (12px) | 24rpx | 卡片圆角 |
| label font-size | text-xs (12px) | 22rpx | 标签字号 |
| value font-size | text-2xl (24px) | 44rpx | 数值字号 |
### 4. Section 卡片(.section-card
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| border-radius | rounded-2xl (16px) | 32rpx | 卡片圆角 |
| padding | p-4 (16px) | 32rpx | 卡片内边距 |
| margin | m-4 (16px) | 24rpx 24rpx 0 | 卡片外边距 |
| box-shadow | shadow-sm | 0 1px 2px rgba(0,0,0,0.05) | 阴影 |
### 5. 收入档位卡片(.tier-card
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| padding | p-3 (12px) | 24rpx | 内边距 |
| border-radius | rounded-xl (12px) | 24rpx | 圆角 |
| border | 1px solid | 2rpx solid | 边框 |
| emoji size | text-2xl (24px) | 48rpx | 表情图标 |
| rate value | text-lg (18px) | 36rpx | 费率数值 |
| rate unit | text-xs (12px) | 22rpx | 单位字号 |
| rate desc | text-[10px] | 20rpx | 描述字号 |
#### 档位徽章
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| top | -8px | -16rpx | 顶部偏移 |
| padding | 2px 10px | 4rpx 20rpx | 内边距 |
| font-size | 10px | 20rpx | 字号 |
### 6. 升级提示(.upgrade-hint
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| padding | p-3 (12px) | 24rpx | 内边距 |
| border-radius | rounded-xl (12px) | 24rpx | 圆角 |
| emoji size | text-lg (18px) | 36rpx | 表情大小 |
| label font-size | text-xs (12px) | 22rpx | 标签字号 |
| hours font-size | text-sm (14px) | 26rpx | 小时数字号 |
| bonus value | text-lg (18px) | 36rpx | 奖金数值 |
### 7. 业绩明细列表(.income-list
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| icon box size | w-8 h-8 (32px) | 64rpx | 图标容器 |
| icon box radius | rounded-lg (8px) | 16rpx | 图标圆角 |
| label font-size | text-sm (14px) | 26rpx | 标签字号 |
| desc font-size | text-[10px] | 20rpx | 描述字号 |
| value font-size | text-base (16px) | 30rpx | 数值字号 |
| total value | text-xl (20px) | 40rpx | 合计数值 |
### 8. 服务记录(.service-records-section
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| avatar size | w-[38px] h-[38px] | 76rpx | 头像尺寸 |
| avatar radius | rounded-lg (8px) | 16rpx | 头像圆角 |
| name font-size | text-sm (14px) | 26rpx | 姓名字号 |
| time font-size | text-xs (12px) | 22rpx | 时间字号 |
| hours font-size | text-sm (14px) | 26rpx | 小时数字号 |
| tag padding | px-1.5 py-px (6px 1px) | 2rpx 12rpx | 标签内边距 |
| tag font-size | text-[11px] | 22rpx | 标签字号 |
#### 日期分隔线
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| date font-size | 11px | 22rpx | 日期字号 |
| line height | 1px | 2rpx | 分隔线高度 |
| padding | 14px 16px 4px | 20rpx 0 8rpx | 内边距 |
### 9. 客户列表(.customer-list
| 属性 | H5 值 | 小程序值 | 说明 |
|------|------|--------|------|
| avatar size | w-[38px] h-[38px] | 76rpx | 头像尺寸 |
| avatar radius | rounded-lg (8px) | 16rpx | 头像圆角 |
| name font-size | text-sm (14px) | 26rpx | 姓名字号 |
| detail font-size | text-xs (12px) | 22rpx | 详情字号 |
| icon size | - | 24px | 右侧箭头图标 |
### 10. 颜色映射附录C
| 用途 | 色值 | 说明 |
|------|------|------|
| 主品牌色 | #0052d9 | primary |
| 成功色 | #00a870 | success |
| 背景色 | #f3f3f3 | gray-1 |
| 正文色 | #242424 | gray-13 |
| 次级文字 | #a6a6a6 | gray-6 |
| 三级文字 | #8b8b8b | gray-7 |
| 四级文字 | #c5c5c5 | gray-5 |
| 分割线 | #f3f3f3 | gray-1 |
### 11. 渐变色系统
#### 头像渐变8种
- `from-blue`: #60a5fa → #6366f1
- `from-pink`: #f472b6 → #f43f5e
- `from-teal`: #2dd4bf → #10b981
- `from-green`: #4ade80 → #14b8a6
- `from-orange`: #fb923c → #f59e0b
- `from-purple`: #c084fc → #8b5cf6
- `from-violet`: #a78bfa → #7c3aed
- `from-amber`: #fbbf24 → #eab308
#### 档位卡片渐变
- 当前档位:#f0fdf4 → #dcfce7绿色系
- 下一阶段:#fefce8 → #fef9c3黄色系
- 升级提示:#eff6ff → #eef2ff蓝色系
- 奖金按钮:#fbbf24 → #f97316橙色系
---
## 关键调整点
### 1. 全局 line-height 设置
```wxss
page {
line-height: 1.5;
}
view {
line-height: inherit;
}
```
**原因**:微信小程序 `<text>` 组件不能直接设置 `line-height`,必须在外层 `<view>` 设置附录B.2.3
### 2. 单位统一为 rpx除特殊情况
- 主要尺寸使用 `rpx`,确保不同设备自适应
- 导航栏高度、图标点击区域使用 `px`
- 换算公式1px = 1.8204rpx750宽基准
### 3. 移除 CSS 变量依赖
- 原:`var(--color-gray-2, #eeeeee)`
- 新:直接使用 `#eeeeee`
- 原因:确保样式独立,不依赖全局变量
### 4. Icon 尺寸调整
- 导航栏返回按钮:`size="24px"`(标准点击区域)
- 列表箭头图标:`size="24px"`
- 加载/错误图标:`size="40px"`
### 5. 复杂背景还原
- **Banner 背景**:使用 SVG 图片 `/assets/images/banner-bg-combined.svg`(与 task-detail/task-list 保持一致)
- 原因SVG pattern 在小程序 image 组件中不渲染
- **灵活可调**:通过 WXSS 可调整 Banner 高度、背景位置、内容区内边距
- 档位卡片使用双色渐变背景
- 头像使用 8 种预设渐变色
- 升级提示使用渐变背景 + 渐变按钮
### 6. 数值字体特性
```wxss
.perf-value {
font-variant-numeric: tabular-nums;
}
```
确保数字等宽对齐
---
## 验收清单
- [x] 导航栏高度与H5对齐44px
- [x] Banner SVG背景与H5对齐
- [x] Banner 高度可通过 WXSS 调整480rpx
- [x] Banner 背景位置可通过 WXSS 调整
- [x] 个人信息卡片尺寸与H5对齐
- [x] 收入概览卡片样式与H5对齐
- [x] 档位卡片渐变背景与H5对齐
- [x] 档位徽章位置与H5对齐
- [x] 升级提示样式与H5对齐
- [x] 业绩明细列表与H5对齐
- [x] 服务记录样式与H5对齐
- [x] 日期分隔线与H5对齐
- [x] 客户列表样式与H5对齐
- [x] 头像渐变色与H5对齐
- [x] 所有字号与H5对齐
- [x] 所有间距与H5对齐
- [x] 所有圆角与H5对齐
- [x] 所有颜色与H5对齐
- [x] 行高设置正确在view上设置
- [x] Banner背景使用灵活的SVG实现与task-detail保持一致
---
## 已知限制
1. **动画效果**H5 中的 `bonus-highlight` 闪烁动画未实现(需要 CSS animation
2. **backdrop-filter**:收入卡片的 `backdrop-blur` 效果在部分安卓设备可能不支持
3. **SVG兼容性**banner-bg-combined.svg 需要确保存在于 `/assets/images/` 目录
---
## 后续维护
- 若真机测试发现尺寸偏差,优先检查 line-height 设置
- 若需要调整 Banner 高度,修改 `.banner-section` 的 `height` 属性
- 若需要调整 Banner 背景位置,修改 `.banner-bg-img` 的 `top/left` 属性
- 若需要调整间距参考附录A的spacing表
- 若需要调整字号参考附录B的字体表
- 颜色调整参考附录C的颜色字典
- Banner背景问题参考task-detail页面的实现方式

View File

@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "业绩详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {
"ai-float-button": "/components/ai-float-button/ai-float-button",
"dev-fab": "/components/dev-fab/dev-fab",
"metric-card": "/components/metric-card/metric-card",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}

View File

@@ -0,0 +1,226 @@
// TODO: 联调时替换为真实 API 调用
import { initPageAiColor } from '../../utils/ai-color-manager'
/** 业绩明细项(本月/上月) */
interface IncomeItem {
icon: string
label: string
desc: string
value: string
}
/** 服务记录(按日期分组后的展示结构) */
interface ServiceRecord {
customerName: string
avatarChar: string
avatarColor: string
timeRange: string
hours: string
courseType: string
courseTypeClass: string
location: string
income: string
}
interface DateGroup {
date: string
totalHours?: string
totalIncome?: string
records: ServiceRecord[]
}
Page({
data: {
pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal',
/** Banner 数据 */
coachName: '小燕',
coachRole: '助教',
storeName: '广州朗朗桌球',
monthlyIncome: '¥6,206',
lastMonthIncome: '¥16,880',
/** 收入档位 */
currentTier: {
basicRate: 80,
incentiveRate: 95,
},
nextTier: {
basicRate: 90,
incentiveRate: 114,
},
upgradeHoursNeeded: 15,
upgradeBonus: 800,
/** 本月业绩明细 */
incomeItems: [] as IncomeItem[],
monthlyTotal: '¥6,950.5',
/** 服务记录 */
thisMonthRecords: [] as DateGroup[],
thisMonthRecordsExpanded: false,
/** 默认显示前 N 条日期组 */
visibleRecordGroups: 2,
/** 新客列表 */
newCustomers: [] as Array<{ name: string; avatarChar: string; avatarColor: string; lastService: string; count: number }>,
newCustomerExpanded: false,
/** 常客列表 */
regularCustomers: [] as Array<{ name: string; avatarChar: string; avatarColor: string; hours: number; income: string; count: number }>,
regularCustomerExpanded: false,
},
onLoad() {
// 初始化 AI 图标配色(蓝色 - 数据分析感)
const { aiColor } = initPageAiColor('performance')
this.setData({ aiColor })
this.loadData()
},
loadData() {
this.setData({ pageState: 'loading' })
setTimeout(() => {
try {
// TODO: 替换为真实 API
const incomeItems: IncomeItem[] = [
{ icon: '🎱', label: '基础课', desc: '80元/h × 75h', value: '¥6,000' },
{ icon: '⭐', label: '激励课', desc: '95.05元/h × 10h', value: '¥950.5' },
{ icon: '💰', label: '充值激励', desc: '客户充值返佣', value: '¥500' },
{ icon: '🏆', label: 'TOP3 销冠奖', desc: '全店业绩前三名奖励', value: '继续努力' },
]
const gradients = [
'blue', 'pink', 'teal', 'green',
'orange', 'purple', 'violet', 'amber',
]
// 模拟服务记录按日期分组
const thisMonthRecords: DateGroup[] = [
{
date: '2月7日',
totalHours: '4.0h',
totalIncome: '¥350',
records: [
{ customerName: '王先生', avatarChar: '王', avatarColor: gradients[0], timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: '¥160' },
{ customerName: '李女士', avatarChar: '李', avatarColor: gradients[1], timeRange: '16:00-18:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: '¥190' },
],
},
{
date: '2月6日',
totalHours: '2.0h',
totalIncome: '¥160',
records: [
{ customerName: '张先生', avatarChar: '张', avatarColor: gradients[2], timeRange: '19:00-21:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: '¥160' },
],
},
{
date: '2月5日',
totalHours: '4.0h',
totalIncome: '¥320',
records: [
{ customerName: '陈女士', avatarChar: '陈', avatarColor: gradients[2], timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
{ customerName: '赵先生', avatarChar: '赵', avatarColor: gradients[5], timeRange: '14:00-16:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: '¥160' },
],
},
{
date: '2月4日',
totalHours: '4.0h',
totalIncome: '¥350',
records: [
{ customerName: '孙先生', avatarChar: '孙', avatarColor: gradients[6], timeRange: '19:00-21:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: '¥190' },
{ customerName: '吴女士', avatarChar: '吴', avatarColor: gradients[2], timeRange: '15:00-17:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: '¥160' },
],
},
]
const newCustomers = [
{ name: '王先生', avatarChar: '王', avatarColor: gradients[0], lastService: '2月7日', count: 2 },
{ name: '李女士', avatarChar: '李', avatarColor: gradients[1], lastService: '2月7日', count: 1 },
{ name: '刘先生', avatarChar: '刘', avatarColor: gradients[4], lastService: '2月6日', count: 1 },
{ name: '周女士', avatarChar: '周', avatarColor: gradients[3], lastService: '2月5日', count: 1 },
{ name: '吴先生', avatarChar: '吴', avatarColor: gradients[7], lastService: '2月4日', count: 1 },
{ name: '郑女士', avatarChar: '郑', avatarColor: gradients[5], lastService: '2月3日', count: 1 },
{ name: '钱先生', avatarChar: '钱', avatarColor: gradients[6], lastService: '2月2日', count: 1 },
{ name: '冯女士', avatarChar: '冯', avatarColor: gradients[2], lastService: '2月1日', count: 1 },
]
const regularCustomers = [
{ name: '张先生', avatarChar: '张', avatarColor: gradients[2], hours: 12, income: '¥960', count: 6 },
{ name: '陈女士', avatarChar: '陈', avatarColor: gradients[2], hours: 10, income: '¥800', count: 5 },
{ name: '赵先生', avatarChar: '赵', avatarColor: gradients[5], hours: 8, income: '¥640', count: 4 },
{ name: '孙先生', avatarChar: '孙', avatarColor: gradients[6], hours: 6, income: '¥570', count: 3 },
{ name: '杨女士', avatarChar: '杨', avatarColor: 'pink', hours: 6, income: '¥480', count: 3 },
{ name: '黄先生', avatarChar: '黄', avatarColor: 'amber', hours: 4, income: '¥380', count: 2 },
{ name: '林女士', avatarChar: '林', avatarColor: 'green', hours: 4, income: '¥320', count: 2 },
{ name: '徐先生', avatarChar: '徐', avatarColor: 'orange', hours: 2, income: '¥190', count: 1 },
]
this.setData({
pageState: 'normal',
incomeItems,
thisMonthRecords,
newCustomers,
regularCustomers,
})
} catch (_e) {
this.setData({ pageState: 'error' })
}
}, 500)
},
/** 加载失败重试 */
onRetry() {
this.loadData()
},
/** 自定义导航栏返回 */
onNavBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/task-list/task-list' }) })
},
/** 展开/收起本月服务记录 */
toggleThisMonthRecords() {
this.setData({ thisMonthRecordsExpanded: !this.data.thisMonthRecordsExpanded })
},
/** 查看全部 → 跳转业绩明细 */
goToRecords() {
wx.navigateTo({ url: '/pages/performance-records/performance-records' })
},
/** 展开/收起新客列表 */
toggleNewCustomer() {
this.setData({ newCustomerExpanded: !this.data.newCustomerExpanded })
},
/** 展开/收起常客列表 */
toggleRegularCustomer() {
this.setData({ regularCustomerExpanded: !this.data.regularCustomerExpanded })
},
/** 点击客户卡片 → 跳转任务详情 */
onCustomerTap(e: WechatMiniprogram.TouchEvent) {
const { name } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/task-detail/task-detail?customerName=${name}`,
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
})
},
/** 点击服务记录 → 跳转任务详情 */
onRecordTap(e: WechatMiniprogram.TouchEvent) {
const { customerName, taskId } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/task-detail/task-detail?customerName=${customerName}${taskId ? `&taskId=${taskId}` : ''}`,
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
})
},
/** 点击收入概览卡片 → 跳转业绩记录 */
onIncomeCardTap() {
wx.navigateTo({ url: '/pages/performance-records/performance-records' })
},
})

View File

@@ -0,0 +1,294 @@
<!-- pages/performance/performance.wxml — 业绩总览 -->
<!-- 加载态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-icon name="chart-bar" size="40px" color="#dcdcdc" />
<text class="empty-text">暂无业绩数据</text>
</view>
<!-- 错误态 -->
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-icon name="close-circle" size="40px" color="#e34d59" />
<text class="error-text">加载失败,请点击重试</text>
<view class="retry-btn" hover-class="retry-btn--hover" bindtap="onRetry">
<text>重试</text>
</view>
</view>
<!-- 正常态 -->
<block wx:else>
<!-- Banner 区域 — SVG 做渐变底图 -->
<view class="banner-section">
<image class="banner-bg-img" src="/assets/images/banner-bg-blue-light-aurora.svg" mode="widthFix" />
<view class="banner-content">
<!-- 个人信息 -->
<view class="coach-info">
<view class="coach-avatar">
<image src="/assets/images/avatar-coach.png" class="avatar-img" mode="aspectFill" />
</view>
<view class="coach-meta">
<view class="coach-name-row">
<text class="coach-name">{{coachName}}</text>
<text class="coach-role-tag">{{coachRole}}</text>
</view>
<text class="coach-store">{{storeName}}</text>
</view>
</view>
<!-- 核心收入数据 -->
<view class="income-overview">
<view class="income-card">
<text class="income-label">本月预计收入</text>
<text class="income-value">{{monthlyIncome}}</text>
</view>
<view class="income-card">
<text class="income-label">上月收入</text>
<text class="income-value income-highlight">{{lastMonthIncome}}</text>
</view>
</view>
</view>
</view>
<!-- 收入情况 -->
<view class="section-card">
<view class="section-title">
<view class="title-dot dot-primary"></view>
<text>收入情况</text>
</view>
<!-- 当前档位 -->
<view class="tier-card tier-current">
<view class="tier-badge badge-current">当前档位</view>
<view class="tier-row">
<view class="tier-icon-label">
<text class="tier-emoji">📊</text>
<text class="tier-label tier-label-green">当前档位</text>
</view>
<view class="tier-rates">
<view class="rate-item">
<view class="rate-value-row">
<text class="rate-value rate-green">{{currentTier.basicRate}}</text>
<text class="rate-unit rate-green-light">元/h</text>
</view>
<text class="rate-desc rate-green-light">基础课到手</text>
</view>
<view class="rate-divider"></view>
<view class="rate-item">
<view class="rate-value-row">
<text class="rate-value rate-green">{{currentTier.incentiveRate}}</text>
<text class="rate-unit rate-green-light">元/h</text>
</view>
<text class="rate-desc rate-green-light">激励课到手</text>
</view>
</view>
</view>
</view>
<!-- 下一阶段 -->
<view class="tier-card tier-next">
<view class="tier-badge badge-next">下一阶段</view>
<view class="tier-row">
<view class="tier-icon-label">
<text class="tier-emoji">🎯</text>
<text class="tier-label tier-label-yellow">下一阶段</text>
</view>
<view class="tier-rates">
<view class="rate-item">
<view class="rate-value-row">
<text class="rate-value rate-yellow">{{nextTier.basicRate}}</text>
<text class="rate-unit rate-yellow-light">元/h</text>
</view>
<text class="rate-desc rate-yellow-light">基础课到手</text>
</view>
<view class="rate-divider rate-divider-yellow"></view>
<view class="rate-item">
<view class="rate-value-row">
<text class="rate-value rate-yellow">{{nextTier.incentiveRate}}</text>
<text class="rate-unit rate-yellow-light">元/h</text>
</view>
<text class="rate-desc rate-yellow-light">激励课到手</text>
</view>
</view>
</view>
</view>
<!-- 升级提示 -->
<view class="upgrade-hint">
<view class="upgrade-left">
<text class="upgrade-emoji">⏱️</text>
<view class="upgrade-text">
<text class="upgrade-label">距离下一阶段</text>
<view class="upgrade-hours">
<text>需完成 </text>
<text class="upgrade-hours-num">{{upgradeHoursNeeded}}</text>
<text> 小时</text>
</view>
</view>
</view>
<view class="upgrade-bonus">
<text class="bonus-label">到达即得</text>
<text class="bonus-value">{{upgradeBonus}}元</text>
</view>
</view>
</view>
<!-- 本月业绩 -->
<view class="section-card">
<view class="section-title">
<view class="title-dot dot-success"></view>
<text>本月业绩 预估</text>
</view>
<view class="income-list">
<view class="income-row" wx:for="{{incomeItems}}" wx:key="label">
<view class="income-row-left">
<view class="income-icon-box">
<text>{{item.icon}}</text>
</view>
<view class="income-info">
<text class="income-item-label">{{item.label}}</text>
<text class="income-item-desc">{{item.desc}}</text>
</view>
</view>
<text class="income-item-value">{{item.value}}</text>
</view>
</view>
<!-- 合计 -->
<view class="income-total">
<text class="total-label">本月合计 预估</text>
<text class="total-value">{{monthlyTotal}}</text>
</view>
<!-- 服务记录明细 -->
<view class="service-records-section">
<view class="service-records-header">
<text class="service-records-emoji">📋</text>
<text class="service-records-title">我的服务记录明细</text>
</view>
<block wx:for="{{thisMonthRecords}}" wx:key="date" wx:if="{{thisMonthRecordsExpanded || index < visibleRecordGroups}}">
<view class="date-divider">
<text decode class="dd-date">{{item.date}}&nbsp;—</text>
<text decode class="dd-stats" wx:if="&nbsp;{{item.totalHours}}">{{item.totalHours}}&nbsp;·&nbsp;{{item.totalIncome}}&nbsp;&nbsp;</text>
<view class="dd-line"></view>
</view>
<view class="record-item" wx:for="{{item.records}}" wx:for-item="rec" wx:key="customerName" bindtap="onRecordTap" data-customer-name="{{rec.customerName}}" data-task-id="{{rec.taskId}}">
<view class="record-avatar avatar-{{rec.avatarColor}}">
<text>{{rec.avatarChar}}</text>
</view>
<view class="record-content">
<view class="record-top">
<view class="record-name-time">
<text class="record-name">{{rec.customerName}}</text>
<text class="record-time">{{rec.timeRange}}</text>
</view>
<text class="record-hours">{{rec.hours}}</text>
</view>
<view class="record-bottom">
<view class="record-tags">
<text class="course-tag {{rec.courseTypeClass}}">{{rec.courseType}}</text>
<text class="record-location">{{rec.location}}</text>
</view>
<text class="record-income">我的预估收入 <text class="record-income-val">{{rec.income}}</text></text>
</view>
</view>
</view>
</block>
<view class="records-actions">
<view class="records-toggle" hover-class="toggle-btn--hover" bindtap="toggleThisMonthRecords" wx:if="{{thisMonthRecords.length > visibleRecordGroups}}">
<text>{{thisMonthRecordsExpanded ? '收起' : '展开更多'}}</text>
<t-icon name="{{thisMonthRecordsExpanded ? 'chevron-up' : 'chevron-down'}}" size="24px" />
</view>
<view class="records-view-all" hover-class="view-all--hover" bindtap="goToRecords">
<text>查看全部</text>
<t-icon name="chevron-right" size="24px" color="#0052d9" />
</view>
</view>
</view>
</view>
<!-- 我的新客 -->
<view class="section-card">
<view class="section-title">
<view class="title-dot dot-cyan"></view>
<text>我的新客</text>
</view>
<view class="customer-list">
<view
class="customer-item"
hover-class="customer-item--hover"
wx:for="{{newCustomers}}"
wx:key="name"
wx:if="{{newCustomerExpanded ? index < 20 : index < 5}}"
data-name="{{item.name}}"
bindtap="onCustomerTap"
>
<view class="customer-avatar avatar-{{item.avatarColor}}">
<text>{{item.avatarChar}}</text>
</view>
<view class="customer-info">
<text class="customer-name">{{item.name}}</text>
<text class="customer-detail">最近服务: {{item.lastService}} · {{item.count}}次</text>
</view>
<t-icon name="chevron-right" size="24px" color="#c5c5c5" />
</view>
</view>
<view class="toggle-btn" hover-class="toggle-btn--hover" bindtap="toggleNewCustomer" wx:if="{{newCustomers.length > 5}}">
<text>{{newCustomerExpanded ? '收起 ↑' : '查看更多 ↓'}}</text>
</view>
</view>
<!-- 我的常客 -->
<view class="section-card">
<view class="section-title">
<view class="title-dot dot-pink"></view>
<text>我的常客</text>
</view>
<view class="customer-list">
<view
class="customer-item"
hover-class="customer-item--hover"
wx:for="{{regularCustomers}}"
wx:key="name"
wx:if="{{regularCustomerExpanded ? index < 20 : index < 5}}"
data-name="{{item.name}}"
bindtap="onCustomerTap"
>
<view class="customer-avatar avatar-{{item.avatarColor}}">
<text>{{item.avatarChar}}</text>
</view>
<view class="customer-info">
<text class="customer-name">{{item.name}}</text>
<text class="customer-detail">{{item.count}}次 · {{item.hours}}h · {{item.income}}</text>
</view>
<t-icon name="chevron-right" size="24px" color="#c5c5c5" />
</view>
</view>
<view class="toggle-btn" hover-class="toggle-btn--hover" bindtap="toggleRegularCustomer" wx:if="{{regularCustomers.length > 5}}">
<text>{{regularCustomerExpanded ? '收起 ↑' : '查看更多 ↓'}}</text>
</view>
</view>
</block>
<!-- AI 悬浮按钮 -->
<ai-float-button />
<dev-fab />

View File

@@ -0,0 +1,833 @@
/* pages/performance/performance.wxss — 业绩总览 */
/* ========== 全局设置 ========== */
page {
background-color: #f3f3f3;
line-height: 1.5;
}
view {
line-height: inherit;
}
/* ========== 加载态 / 空态 / 错误态 ========== */
.page-loading,
.page-empty,
.page-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
gap: 24rpx;
}
.empty-text,
.error-text {
font-size: 26rpx;
color: #a6a6a6;
line-height: 36rpx;
}
.retry-btn {
padding: 16rpx 48rpx;
background-color: #0052d9;
color: #ffffff;
border-radius: 16rpx;
font-size: 26rpx;
line-height: 36rpx;
}
.retry-btn--hover {
opacity: 0.7;
}
/* ========== Banner 区域 ========== */
/* 方案SVG 做渐变底图(与 task-detail 保持一致)
* 特点:通过 WXSS 可灵活调整位置、高度、缩放
*/
.banner-section {
position: relative;
width: 100%;
overflow: hidden;
height: 380rpx;
}
/* 渐变底图 — SVG 包含渐变 + 光晕 */
/* 方案SVG 图片完整显示,通过调整位置和尺寸来控制显示区域
* 超出 banner-section 的部分会被 overflow: hidden 裁剪
* 调整方式:
* - 修改 top/left 来移动图片位置选择显示SVG的哪个部分
* - 修改 width/height 来缩放图片
* - 负的 top 值:图片向上移,显示下半部分
* - 正的 top 值:图片向下移,显示上半部分
*/
.banner-bg-img {
position: absolute;
top: -50rpx; /* 调整此值移动图片:负值向上移,正值向下移 */
left: 0; /* 调整此值左右移动图片 */
width: 100%; /* 调整此值缩放图片宽度 */
height: auto; /* 保持图片比例,或设置具体值如 600rpx 来缩放 */
z-index: 0;
}
.banner-content {
position: relative;
z-index: 2;
padding: 40rpx;
height: 100%;
display: flex;
flex-direction: column;
}
/* 自定义导航栏 */
.custom-nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin-bottom: 16rpx;
}
.nav-back {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.nav-back--hover {
background-color: rgba(255, 255, 255, 0.2);
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
line-height: 44rpx;
}
.nav-placeholder {
width: 44px;
}
/* 个人信息 */
.coach-info {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 40rpx;
}
.coach-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.2);
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-img {
width: 100%;
height: 100%;
}
.coach-meta {
flex: 1;
}
.coach-name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.coach-name {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
line-height: 56rpx;
}
.coach-role-tag {
padding: 4rpx 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 24rpx;
font-size: 22rpx;
color: #ffffff;
line-height: 29rpx;
}
.coach-store {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 36rpx;
}
/* 收入概览卡片 */
.income-overview {
display: flex;
gap: 24rpx;
}
.income-card {
flex: 1;
background: rgba(255, 255, 255, 0.2);
border-radius: 24rpx;
padding: 30rpx;
text-align: center;
backdrop-filter: blur(4px);
}
.income-label {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 8rpx;
line-height: 29rpx;
}
.income-value {
display: block;
font-size: 44rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
line-height: 58rpx;
}
.income-highlight {
color: #a7f3d0;
}
/* ========== 通用 Section 卡片 ========== */
.section-card {
background: #ffffff;
border-radius: 32rpx;
padding: 32rpx;
margin: 24rpx 24rpx 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 600;
color: #242424;
line-height: 40rpx;
}
.title-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex-shrink: 0;
}
.dot-primary { background: #0052d9; }
.dot-success { background: #00a870; }
.dot-cyan { background: #06b6d4; }
.dot-pink { background: #ec4899; }
/* ========== 收入档位 ========== */
.tier-card {
position: relative;
padding: 24rpx;
border-radius: 24rpx;
margin-bottom: 20rpx;
}
.tier-current {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border: 2rpx solid #86efac;
}
.tier-next {
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
border: 2rpx solid #fde047;
}
.tier-badge {
position: absolute;
top: -16rpx;
right: 24rpx;
padding: 4rpx 20rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 600;
color: #ffffff;
line-height: 29rpx;
}
.badge-current {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
}
.badge-next {
background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%);
}
.tier-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.tier-icon-label {
display: flex;
align-items: center;
gap: 12rpx;
}
.tier-emoji {
font-size: 48rpx;
line-height: 1;
}
.tier-label {
font-size: 26rpx;
font-weight: 500;
line-height: 36rpx;
}
.tier-label-green { color: #15803d; }
.tier-label-yellow { color: #a16207; }
.tier-rates {
display: flex;
align-items: center;
gap: 24rpx;
}
.rate-item {
text-align: center;
width: 128rpx;
}
.rate-value-row {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4rpx;
}
.rate-value {
font-size: 36rpx;
font-weight: 700;
line-height: 51rpx;
}
.rate-unit {
font-size: 22rpx;
line-height: 29rpx;
}
.rate-desc {
font-size: 20rpx;
margin-top: 4rpx;
display: block;
line-height: 29rpx;
}
.rate-green { color: #15803d; }
.rate-green-light { color: #16a34a; }
.rate-yellow { color: #a16207; }
.rate-yellow-light { color: #ca8a04; }
.rate-divider {
width: 2rpx;
height: 64rpx;
background: #bbf7d0;
}
.rate-divider-yellow {
background: #fef08a;
}
/* 升级提示 */
.upgrade-hint {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(to right, #eff6ff, #eef2ff);
border-radius: 24rpx;
padding: 24rpx;
border: 2rpx solid #bfdbfe;
margin-top: 20rpx;
}
.upgrade-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.upgrade-emoji {
font-size: 48rpx;
line-height: 1;
}
.upgrade-text {
display: flex;
flex-direction: column;
}
.upgrade-label {
font-size: 22rpx;
color: #5e5e5e;
display: block;
line-height: 29rpx;
}
.upgrade-hours {
font-size: 26rpx;
font-weight: 600;
color: #1d4ed8;
line-height: 36rpx;
}
.upgrade-hours-num {
font-size: 30rpx;
line-height: 44rpx;
}
.upgrade-bonus {
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(to right, #fbbf24, #f97316);
color: #ffffff;
padding: 12rpx 24rpx;
border-radius: 16rpx;
}
.bonus-label {
font-size: 20rpx;
opacity: 0.9;
line-height: 29rpx;
}
.bonus-value {
font-size: 36rpx;
font-weight: 700;
line-height: 51rpx;
}
/* ========== 本月业绩明细 ========== */
.income-list {
margin-bottom: 16rpx;
}
.income-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 2rpx solid #f3f3f3;
}
.income-row:last-child {
border-bottom: none;
}
.income-row-left {
display: flex;
align-items: center;
gap: 20rpx;
flex: 1;
}
.income-icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
background: #f3f3f3;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
flex-shrink: 0;
}
.income-info {
display: flex;
flex-direction: column;
}
.income-item-label {
font-size: 26rpx;
font-weight: 500;
color: #242424;
line-height: 36rpx;
}
.income-item-desc {
font-size: 22rpx;
color: #c5c5c5;
margin-top: 4rpx;
line-height: 29rpx;
}
.income-item-value {
font-size: 30rpx;
font-weight: 700;
color: #242424;
font-variant-numeric: tabular-nums;
line-height: 44rpx;
}
.income-total {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16rpx;
border-top: 2rpx solid #f3f3f3;
}
.total-label {
font-size: 26rpx;
font-weight: 500;
color: #8b8b8b;
line-height: 36rpx;
}
.total-value {
font-size: 40rpx;
font-weight: 700;
color: #00a870;
font-variant-numeric: tabular-nums;
line-height: 58rpx;
}
/* ========== 服务记录 ========== */
.service-records-section {
margin-top: 32rpx;
margin: 32rpx -32rpx -32rpx;
padding: 32rpx;
background: rgba(243, 243, 243, 0.7);
border-radius: 0 0 32rpx 32rpx;
}
.service-records-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 8rpx;
}
.service-records-emoji {
font-size: 26rpx;
line-height: 1;
}
.service-records-title {
font-size: 26rpx;
font-weight: 600;
color: #242424;
line-height: 36rpx;
}
.date-divider {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 0 8rpx;
}
.dd-date {
font-size: 22rpx;
color: #8b8b8b;
font-weight: 500;
line-height: 29rpx;
}
.dd-line {
flex: 1;
height: 2rpx;
background: #dcdcdc;
}
.dd-stats {
font-size: 22rpx;
color: #a6a6a6;
font-variant-numeric: tabular-nums;
line-height: 29rpx;
}
.record-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 16rpx 0;
cursor: pointer;
}
.record-item:active {
opacity: 0.7;
}
.record-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 30rpx;
font-weight: 500;
flex-shrink: 0;
}
/* 头像渐变色 */
.avatar-from-blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
.avatar-from-pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
.avatar-from-teal { background: linear-gradient(135deg, #2dd4bf, #10b981); }
.avatar-from-green { background: linear-gradient(135deg, #4ade80, #14b8a6); }
/* 头像渐变色由 app.wxss 全局 .avatar-{key} 统一提供VI §8 */
.avatar-from-purple { background: linear-gradient(135deg, #c084fc, #8b5cf6); }
.avatar-from-violet { background: linear-gradient(135deg, #a78bfa, #7c3aed); }
.avatar-from-amber { background: linear-gradient(135deg, #fbbf24, #eab308); }
.record-content {
flex: 1;
min-width: 0;
}
.record-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.record-name-time {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.record-name {
font-size: 26rpx;
font-weight: 500;
color: #242424;
flex-shrink: 0;
line-height: 36rpx;
}
.record-time {
font-size: 22rpx;
color: #a6a6a6;
line-height: 29rpx;
}
.record-hours {
font-size: 26rpx;
font-weight: 700;
color: #059669;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
line-height: 36rpx;
}
.record-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8rpx;
}
.record-tags {
display: flex;
align-items: center;
gap: 12rpx;
}
.course-tag {
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
line-height: 29rpx;
}
.tag-basic {
background: #ecfdf5;
color: #15803d;
}
.tag-vip {
background: #eff6ff;
color: #1d4ed8;
}
.tag-tip {
background: #fffbeb;
color: #a16207;
}
.record-location {
font-size: 22rpx;
color: #8b8b8b;
line-height: 29rpx;
}
.record-income {
font-size: 22rpx;
color: #c5c5c5;
flex-shrink: 0;
line-height: 29rpx;
}
.record-income-val {
font-weight: 500;
color: #5e5e5e;
}
.records-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 16rpx 0;
font-size: 26rpx;
color: #8b8b8b;
line-height: 36rpx;
}
.records-view-all {
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
padding: 12rpx 0;
font-size: 26rpx;
color: #0052d9;
font-weight: 500;
line-height: 36rpx;
}
/* ========== 新客 / 常客列表 ========== */
.customer-list {
margin-bottom: 8rpx;
}
.customer-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 16rpx 0;
border-bottom: 2rpx solid #f3f3f3;
}
.customer-item:last-child {
border-bottom: none;
}
.customer-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 30rpx;
font-weight: 500;
flex-shrink: 0;
}
.customer-info {
flex: 1;
min-width: 0;
}
.customer-name {
display: block;
font-size: 26rpx;
font-weight: 500;
color: #242424;
line-height: 36rpx;
}
.customer-detail {
display: block;
font-size: 22rpx;
color: #a6a6a6;
margin-top: 4rpx;
line-height: 29rpx;
}
.customer-item--hover {
background-color: #f3f3f3;
}
/* ========== 服务记录按钮 ========== */
/* 并列按钮容器 */
.records-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 16rpx 120rpx;
}
.records-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 26rpx;
color: #0052d9;
line-height: 36rpx;
flex: 1;
font-weight: 500;
}
.records-view-all {
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
font-size: 26rpx;
color: #0052d9;
font-weight: 500;
line-height: 36rpx;
flex: 1;
}
.toggle-btn--hover {
opacity: 0.6;
}
.view-all--hover {
opacity: 0.6;
}
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 0;
font-size: 26rpx;
color: #0052d9;
line-height: 36rpx;
}
.toggle-btn--hover {
opacity: 0.6;
}
.view-all--hover {
opacity: 0.6;
}