微信小程序页面迁移校验之前 P5任务处理之前
@@ -20,7 +20,14 @@ apps/miniprogram/
|
||||
│ ├── pages/ # 页面目录
|
||||
│ │ ├── mvp/ # MVP 全链路验证页
|
||||
│ │ ├── index/ # 首页
|
||||
│ │ ├── login/ # 登录页
|
||||
│ │ ├── apply/ # 入驻申请页
|
||||
│ │ ├── reviewing/ # 审核中等待页
|
||||
│ │ ├── no-permission/ # 无权限提示页
|
||||
│ │ ├── dev-tools/ # 开发调试面板(仅 develop 环境)
|
||||
│ │ └── logs/ # 日志页
|
||||
│ ├── components/ # 全局组件
|
||||
│ │ └── dev-fab/ # 浮动调试按钮(仅 develop 环境显示)
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── config.ts # 环境配置(API 地址自动切换)
|
||||
│ │ └── util.ts # 通用工具(日期格式化等)
|
||||
@@ -52,6 +59,11 @@ apps/miniprogram/
|
||||
|------|------|
|
||||
| `pages/mvp/mvp` | MVP 全链路验证(从后端读取测试数据) |
|
||||
| `pages/index/index` | 首页(待开发) |
|
||||
| `pages/login/login` | 登录页 |
|
||||
| `pages/apply/apply` | 入驻申请页 |
|
||||
| `pages/reviewing/reviewing` | 审核中等待页 |
|
||||
| `pages/no-permission/no-permission` | 无权限提示页 |
|
||||
| `pages/dev-tools/dev-tools` | 开发调试面板(仅 develop 环境,通过 dev-fab 浮动按钮进入) |
|
||||
| `pages/logs/logs` | 日志页(框架默认) |
|
||||
|
||||
## 后端 API 集成
|
||||
@@ -73,9 +85,9 @@ apps/miniprogram/
|
||||
```
|
||||
wx.login() 获取 code
|
||||
↓
|
||||
POST /api/xcx-auth/login → 获取 JWT(受限令牌)
|
||||
POST /api/xcx-auth/login → 获取 JWT(受限令牌,status=new)
|
||||
↓
|
||||
POST /api/xcx-auth/apply → 提交入驻申请(球房ID + 身份 + 手机号)
|
||||
POST /api/xcx-auth/apply → 提交入驻申请(球房ID + 身份 + 手机号,status → pending)
|
||||
↓
|
||||
管理员在后台审批
|
||||
↓
|
||||
@@ -86,20 +98,44 @@ POST /api/xcx-auth/login → 重新登录获取完整令牌(含 site_id + ro
|
||||
正常使用业务功能
|
||||
```
|
||||
|
||||
用户状态流转:
|
||||
- `new`:新用户,尚未提交申请
|
||||
- `pending`:已提交申请,等待审批
|
||||
- `approved`:审批通过,可正常使用
|
||||
- `rejected`:审批拒绝,可重新申请
|
||||
- `disabled`:账号禁用
|
||||
|
||||
令牌类型:
|
||||
- 受限令牌(`limited=True`):pending 用户,仅可访问申请和状态查询端点
|
||||
- 受限令牌(`limited=True`):new/pending/rejected 用户,仅可访问申请和状态查询端点
|
||||
- 完整令牌:approved 用户,包含 `user_id` + `site_id` + `roles`
|
||||
|
||||
### 开发模式
|
||||
|
||||
后端支持开发模式(`WX_DEV_MODE=true`),提供 mock 登录端点跳过微信 code2Session:
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/xcx-auth/dev-login` | POST | 开发模式 mock 登录(仅开发环境) |
|
||||
|
||||
参数:
|
||||
- `openid`:模拟的微信 openid
|
||||
- `status`:可选,指定用户状态(new/pending/approved/rejected)
|
||||
|
||||
### 关键 API 端点
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/xcx-auth/login` | POST | 微信登录(code → JWT) |
|
||||
| `/api/xcx-auth/dev-login` | POST | 开发模式 mock 登录(仅开发环境) |
|
||||
| `/api/xcx-auth/apply` | POST | 提交入驻申请 |
|
||||
| `/api/xcx-auth/status` | GET | 查询用户状态和申请记录 |
|
||||
| `/api/xcx-auth/sites` | GET | 获取关联门店列表 |
|
||||
| `/api/xcx-auth/switch-site` | POST | 切换当前门店 |
|
||||
| `/api/xcx-auth/refresh` | POST | 刷新令牌 |
|
||||
| `/api/xcx/tasks` | GET | 获取任务列表 |
|
||||
| `/api/xcx/tasks/{task_id}/pin` | POST | 置顶/取消置顶任务 |
|
||||
| `/api/xcx/tasks/{task_id}/abandon` | POST | 放弃/取消放弃任务 |
|
||||
| `/api/xcx/notes` | GET/POST/DELETE | 备注 CRUD |
|
||||
| `/api/xcx-test` | GET | MVP 全链路验证 |
|
||||
|
||||
> 完整 API 文档见 [`apps/backend/docs/API-REFERENCE.md`](../backend/docs/API-REFERENCE.md)
|
||||
@@ -130,9 +166,33 @@ POST /api/xcx-auth/login → 重新登录获取完整令牌(含 site_id + ro
|
||||
- H5 原型设计稿位于 `docs/h5_ui/`
|
||||
- 认证数据存储在 `zqyy_app` 数据库的 `auth` Schema
|
||||
|
||||
## 开发调试面板(dev-tools)
|
||||
|
||||
仅在 develop 环境可用的调试工具,通过页面底部浮动按钮(dev-fab 组件)进入。
|
||||
|
||||
功能:
|
||||
- 展示当前用户上下文(角色、权限、绑定关系、门店信息)
|
||||
- 一键切换角色(coach / staff / site_admin / tenant_admin),后端真实修改 `user_site_roles` 并重签 token
|
||||
- 一键切换用户状态(new / pending / approved / rejected / disabled),后端真实修改 `users.status` 并重签 token
|
||||
- 页面跳转列表,点击可跳转到任意已注册页面
|
||||
|
||||
安全保障:
|
||||
- dev-fab 组件通过 `wx.getAccountInfoSync().miniProgram.envVersion` 判断环境,仅 `develop` 时渲染
|
||||
- 后端 dev 端点仅在 `WX_DEV_MODE=true` 时注册路由,生产环境不可访问
|
||||
|
||||
依赖的后端端点(均需 JWT):
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/xcx/dev-context` | GET | 获取当前用户调试上下文 |
|
||||
| `/api/xcx/dev-switch-role` | POST | 切换角色 |
|
||||
| `/api/xcx/dev-switch-status` | POST | 切换用户状态 |
|
||||
| `/api/xcx/dev-switch-binding` | POST | 切换绑定关系 |
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] 完善认证流程页面(登录 → 申请 → 等待审批 → 首页)
|
||||
- [ ] 任务管理页面(任务列表、置顶、放弃、备注)
|
||||
- [ ] 数据看板页面(助教业绩、客户分析)
|
||||
- [ ] 会员中心页面
|
||||
- [ ] 助教预约功能
|
||||
|
||||
@@ -1,997 +0,0 @@
|
||||
|
||||
|
||||
# 一、文档信息
|
||||
|
||||
* 产品名称:球房运营助手(微信小程序)
|
||||
* 版本:V1.0(原型版,全权限视角)
|
||||
* 撰写日期:YYYY-MM-DD
|
||||
* 适用平台:微信小程序(iOS / Android 手机竖屏)
|
||||
* 文档范围:仅描述小程序前端界面与交互行为,不包含后端服务和接口字段定义。
|
||||
|
||||
---
|
||||
|
||||
# 二、背景与目标
|
||||
|
||||
本小程序用于提升台球厅经营管理效率,为店长、助教管理、助教等内部人员提供任务管理、业绩查看、运营看板和智能助手对话能力。
|
||||
|
||||
当前阶段目标:
|
||||
|
||||
* 交付一套基于“全功能、全权限角色视角”的微信小程序前端原型。
|
||||
* 明确各页面布局、组件及交互行为,便于前端和原型工具直接实现。
|
||||
* 角色权限控制、数据口径、字段来源均由后端与后续迭代处理,原型仅展示有权限时的页面样式。
|
||||
|
||||
---
|
||||
|
||||
# 三、范围与约束说明
|
||||
|
||||
1. **设备与环境**
|
||||
|
||||
* 仅面向手机端微信小程序(iOS / Android),竖屏使用。
|
||||
* 暂不考虑 iPad 等大屏适配。
|
||||
|
||||
2. **门店范围**
|
||||
|
||||
* 当前仅支持一个店铺场景,后端如扩展多门店,在后续版本处理。
|
||||
|
||||
3. **权限与角色**
|
||||
|
||||
* 原型以“全功能视角”展示所有模块与入口。
|
||||
* 实际上线时,不同角色(店长/管理层/助教管理/助教)的权限由后端接口控制,对无权限功能采取“入口隐藏”的方式。
|
||||
* 原型中不绘制模块级“无权限访问”占位状态。
|
||||
|
||||
4. **接口与数据**
|
||||
|
||||
* 本文不描述具体接口、字段名、数据结构。
|
||||
* 各类展示字段以接口实际返回为准,本文若举例字段,仅为示意,不代表完整字段列表。
|
||||
|
||||
5. **登录/申请流程的权限提示**
|
||||
|
||||
* 登录后如账号未通过审核或无访问权限,将展示对应状态页(审核中、无权限),这属于整体访问控制,不属于“模块权限占位”,在原型中需要体现。
|
||||
|
||||
---
|
||||
|
||||
# 四、角色说明(仅用于理解,不做权限逻辑)
|
||||
|
||||
* 店长 / 公司管理层:实际场景中拥有全功能权限。
|
||||
* 助教管理:看板中财务板块不可见(上线时通过隐藏入口实现)。
|
||||
* 助教:看板中财务板块和助教板块不可见(上线时通过隐藏入口实现)。
|
||||
|
||||
原型中统一以“全功能视角”展示,不做差异。
|
||||
|
||||
---
|
||||
|
||||
# 五、全局设计规范
|
||||
|
||||
## 5.1 语言与格式
|
||||
|
||||
* 语言:简体中文。
|
||||
* 金额单位:
|
||||
|
||||
* 元:取整,不显示小数。
|
||||
* 万元:保留两位小数。
|
||||
* 时间显示格式:
|
||||
|
||||
* 标准格式:`YYYY-MM-DD HH:mm:ss`
|
||||
* 在不影响理解情况下,可根据页面需要简化为 `YYYY-MM-DD` 或 `MM-DD HH:mm` 等,具体由设计与前端协商。
|
||||
|
||||
## 5.2 导航与返回规则
|
||||
|
||||
* 底部一级导航(TabBar):
|
||||
|
||||
* Tab 顺序:任务 / 看板 / 我的
|
||||
* 文字:`任务`、`看板`、`我的`
|
||||
* 每个 Tab 对应一个一级页面,点击 Tab 时:
|
||||
|
||||
* 若当前已在该 Tab 内的子页面,点击 Tab 返回该 Tab 的根页面,并滚动至顶部。
|
||||
* 顶部导航:
|
||||
|
||||
* 除特别说明外,二级/详情页隐藏微信原生导航栏,使用自定义头部,左上角为返回图标,行为为返回上一页面。
|
||||
* 弹窗与浮层:
|
||||
|
||||
* 使用标准底部弹出或中部弹窗,与微信交互习惯一致。
|
||||
|
||||
## 5.3 悬浮助手按钮
|
||||
|
||||
* 悬浮按钮在所有业务页面(任务、看板、我的及其子页面)显示,不在“登录/申请/审核中/无权限”页面显示。
|
||||
* 默认位置:页面右下角(不遮挡底部 TabBar),随页面滚动悬浮。
|
||||
* 点击行为:进入“助手对话页面”,默认打开最近一次会话(若有)。
|
||||
|
||||
## 5.4 提示、错误与加载状态
|
||||
|
||||
* **网络异常 / 接口错误(列表/卡片区域)**
|
||||
|
||||
* 在对应数据区域显示文字:`加载失败,请点击重试`
|
||||
* 下方提供“重试”按钮,点击重新请求该区域数据。
|
||||
* 作为所有列表/卡片区域的统一错误样式。
|
||||
|
||||
* **空数据状态**
|
||||
|
||||
* 统一使用简单文字:
|
||||
|
||||
* 列表类统一为:`暂无数据` 或根据场景显示 `暂无任务` 等。
|
||||
* 不使用插画或占位图。
|
||||
|
||||
* **加载状态**
|
||||
|
||||
* 使用区域加载:在列表或卡片区域显示文字:`加载中...`
|
||||
* 不做骨架屏和复杂动画。
|
||||
|
||||
---
|
||||
|
||||
# 六、信息架构与页面列表
|
||||
|
||||
## 6.1 顶层结构
|
||||
|
||||
* 登录相关
|
||||
|
||||
* 登录页
|
||||
* 账号申请页
|
||||
* 审核中页
|
||||
* 无权限页
|
||||
* Tab 1:任务
|
||||
|
||||
* 任务列表页(默认首页)
|
||||
* 任务详情页
|
||||
* 业绩详情页
|
||||
* Tab 2:看板
|
||||
|
||||
* 看板首页(含:财务 / 客户 / 助教 三级视图)
|
||||
* 客户详情页
|
||||
* 助教详情页
|
||||
* Tab 3:我的
|
||||
|
||||
* 我的首页
|
||||
* 备注记录页
|
||||
* 助手对话记录页
|
||||
* 首页设置页
|
||||
* 退出账号(确认弹窗)
|
||||
* 全局
|
||||
|
||||
* 助手对话页
|
||||
|
||||
---
|
||||
|
||||
# 七、关键流程说明
|
||||
|
||||
## 7.1 登录与申请流程
|
||||
|
||||
1. 用户打开小程序 → 登录页。
|
||||
2. 点击“使用微信登录”,完成微信授权。
|
||||
3. 登录后:
|
||||
|
||||
* 若查无此用户,也无此用户提交过申请 → 进入账号申请页。
|
||||
* 若查到该用户提交过申请,状态为“审核中” → 进入“审核中”状态页。
|
||||
* 若查到该用户提交过申请,状态为“拒绝/未通过” → 进入“无权限”状态页。
|
||||
* 若查到该用户申请已通过 → 跳转至用户设置的默认首页(初始为“任务”页)。
|
||||
|
||||
## 7.2 默认首页配置流程
|
||||
|
||||
* 初始默认首页为“任务”。
|
||||
* 用户可在“我的 → 首页设置”中将首页设置为:任务 / 看板。
|
||||
* 设置为“切换即保存”,与账号绑定(不因退出登录而重置)。
|
||||
|
||||
---
|
||||
|
||||
# 八、页面级需求
|
||||
|
||||
以下各页面按【页面名称 / 入口 / 布局结构 / 功能与交互 / 状态】描述。
|
||||
|
||||
---
|
||||
|
||||
## 8.1 登录与访问控制相关
|
||||
|
||||
### 8.1.1 登录页
|
||||
|
||||
**入口**
|
||||
|
||||
* 小程序启动未登录状态。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 顶部:App Logo 占位 + 应用名称(球房运营助手)。
|
||||
* 中部:一句产品描述文案,例如:
|
||||
|
||||
* `为台球厅提升运营效率的内部管理工具`
|
||||
* 底部区域:
|
||||
|
||||
* 主按钮:`使用微信登录`(微信授权登录入口)。
|
||||
* 下方文案 + 勾选框:
|
||||
|
||||
* 复选框 + 文案:`我已阅读并同意《用户协议》和《隐私政策》`
|
||||
* 协议名称为可点击文本(具体跳转页面可后续补充)。
|
||||
|
||||
**功能与交互**
|
||||
|
||||
* 用户必须勾选协议复选框才能点击“使用微信登录”,否则按钮为禁用态。
|
||||
* 点击“使用微信登录”调用微信授权流程,登录成功后进入流程 7.1 所述分支。
|
||||
* 登录失败时,在底部弹出错误提示(Toast),重试留在本页。
|
||||
|
||||
---
|
||||
|
||||
### 8.1.2 账号申请页
|
||||
|
||||
**入口**
|
||||
|
||||
* 登录成功后,系统查无该用户及其申请记录时。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 顶部:标题 `申请访问权限`。
|
||||
* 主体:
|
||||
|
||||
* 文本说明:简单说明需要申请原因,例如:
|
||||
|
||||
* `请填写申请说明,审核通过后即可使用小程序功能。`
|
||||
* 表单区:
|
||||
|
||||
* 字段 1:
|
||||
|
||||
* 标签:`申请说明`,带红色星号(必填)。
|
||||
* 多行文本输入框,用于填写自我介绍、岗位、所属门店等说明(具体内容由用户自由填写)。
|
||||
* 底部:
|
||||
|
||||
* 主按钮:`提交申请`
|
||||
|
||||
**功能与交互**
|
||||
|
||||
* “申请说明”为必填,如为空则点击“提交申请”时在输入框下方显示错误提示:`申请说明不能为空`。
|
||||
* 提交成功后,进入“审核中”页。
|
||||
* 接口错误时,弹出错误提示,停留在本页。
|
||||
|
||||
---
|
||||
|
||||
### 8.1.3 审核中页
|
||||
|
||||
**入口**
|
||||
|
||||
* 登录后发现该用户有申请记录,状态为“审核中”。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 居中展示:
|
||||
|
||||
* 图标(等待/审核中占位图标)。
|
||||
* 标题文案:`申请审核中`
|
||||
* 说明文案:例如:`您的访问申请正在审核中,请稍后再试或联系管理员。`
|
||||
* 不提供其他操作按钮,保持不可操作状态。
|
||||
|
||||
**功能与交互**
|
||||
|
||||
* 用户可关闭小程序或退出;再次进入时仍按登录逻辑判断状态。
|
||||
|
||||
---
|
||||
|
||||
### 8.1.4 无权限页
|
||||
|
||||
**入口**
|
||||
|
||||
* 登录后发现该用户申请状态为“拒绝/未通过”,或无访问权限。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 居中展示:
|
||||
|
||||
* 图标(禁止/无权限占位图标)。
|
||||
* 标题文案:`无访问权限`
|
||||
* 说明文案:例如:`您的访问申请未通过,或当前账号无访问权限。如需使用,请联系管理员。`
|
||||
* 不提供操作按钮,不可操作状态。
|
||||
|
||||
**功能与交互**
|
||||
|
||||
* 用户可关闭小程序或退出;如后续权限变更,再次登录时可进入首页。
|
||||
|
||||
---
|
||||
|
||||
## 8.2 Tab:任务
|
||||
|
||||
### 8.2.1 任务列表页(默认首页)
|
||||
|
||||
**入口**
|
||||
|
||||
* 底部 TabBar 点击“任务”。
|
||||
* 登录通过后,如未设置其他默认首页,则默认进入本页。
|
||||
|
||||
**整体布局**
|
||||
|
||||
* 顶部:自定义导航栏(标题:`任务`),左侧无返回按钮。
|
||||
* Banner 区:当前用户信息与业绩概览。
|
||||
* 任务列表:按紧急程度排序的任务列表。
|
||||
* 悬浮助手按钮:右下角。
|
||||
|
||||
**Banner 区内容**
|
||||
|
||||
* 展示内容示例:
|
||||
|
||||
* 第一行:`用户名` + `身份`(例如:张三 / 助教)
|
||||
* 第二行:一句聚合文案,例如:
|
||||
`本月目标 5 万,已完成 3 万,任务 50 个,完成进度 60%`
|
||||
* 第三行:`X 月预计收入:12345 元`(单位为元,取整)
|
||||
* Banner 整块区域可点击,跳转至“业绩详情页”。
|
||||
|
||||
**任务列表结构**
|
||||
|
||||
* 列表为单列列表,不按任务类型分组,仅通过排序和颜色区分。
|
||||
* 排序规则:按紧急程度从高到低排序,类型依次为:
|
||||
|
||||
* 高优先召回(红)
|
||||
* 优先召回(橙)
|
||||
* 关系构建(粉)
|
||||
* 客户回访(蓝)
|
||||
|
||||
**单条任务卡片布局**
|
||||
|
||||
* 第一行(标题行):
|
||||
|
||||
* 左侧:任务类型标签(带背景色的颜色块或 icon),颜色按类型区分(红/橙/粉/蓝)。
|
||||
* 紧随其后:客户姓名。
|
||||
* 右侧:`>` 箭头图标,提示可点击进入详情。
|
||||
* 第二行(补充行):
|
||||
|
||||
* 核心信息 + 召回说明,具体字段根据当前任务类型与接口返回内容展示,例如:最近到店时间、召回原因、优先级说明等。
|
||||
* 其他:
|
||||
|
||||
* 不提供搜索框和筛选组件,任务集合由接口控制。
|
||||
|
||||
**交互说明**
|
||||
|
||||
* 点击整条任务卡片:进入“任务详情页”。
|
||||
* 长按任务卡片:在长按位置上方弹出黑底浮层菜单,样式类似微信对话长按菜单,菜单项:
|
||||
|
||||
* `任务置底`
|
||||
* `问问助手`
|
||||
* `备注`
|
||||
* “任务置底”:前端仅调用接口,排序规则由后端控制;前端不单独维护生命周期状态。
|
||||
* “问问助手”:跳转至“助手对话页”,以该任务信息为引用,开启新对话主题。
|
||||
* “备注”:弹出底部浮层,输入备注内容并保存,备注按时间排序纳入“备注记录”。
|
||||
|
||||
**空状态**
|
||||
|
||||
* 当列表为空时,在列表区域居中显示文案:`暂无任务`。
|
||||
|
||||
---
|
||||
|
||||
### 8.2.2 任务详情页
|
||||
|
||||
**入口**
|
||||
|
||||
* 任务列表页点击某条任务。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 顶部:自定义导航栏
|
||||
|
||||
* 左:返回按钮 `<`
|
||||
* 中:标题,例如 `任务详情`
|
||||
* 主体内容区:
|
||||
|
||||
* 模块一:客户基本信息
|
||||
|
||||
* 示例字段:姓名、手机号、会员编号、性别、标签(如 VIP/新客)、所属门店等(以接口为准)。
|
||||
* 模块二:消费习惯
|
||||
|
||||
* 文本描述形式,例如:“偏好晚间 21:00 后到店,喜欢中式台球,平均消费 300 元/月”等。
|
||||
* 模块三:与我的关系
|
||||
|
||||
* 等级:很好 / 好 / 一般 / 较陌生
|
||||
* 每个等级附带一段文字说明(例如“最近 3 个月每周均有1次课程”等)。
|
||||
* 模块四:任务建议
|
||||
|
||||
* 纯文本内容,给出执行建议、沟通话术提示等。
|
||||
* 底部固定操作栏:
|
||||
|
||||
* 左按钮:`问问助手`
|
||||
* 右按钮:`备注`
|
||||
|
||||
**交互说明**
|
||||
|
||||
* `问问助手`:
|
||||
|
||||
* 跳转至助手对话页。
|
||||
* 以当前任务的关键信息(任务类型、客户名、任务说明等)作为引用内容,显示为灰底卡片,用户在其下输入文本发送。
|
||||
* 通过此入口固定新建一个新对话主题。
|
||||
* `备注`:
|
||||
|
||||
* 底部弹出浮层,包含备注输入框和“保存”按钮。
|
||||
* 保存后生成一条备注记录,类型标记为“任务备注”,记入“备注记录”,按照创建时间倒序展示。
|
||||
|
||||
---
|
||||
|
||||
### 8.2.3 业绩详情页
|
||||
|
||||
**入口**
|
||||
|
||||
* 任务列表页 Banner 区点击。
|
||||
|
||||
**布局结构**
|
||||
|
||||
* 顶部 Banner:
|
||||
|
||||
* 展示:用户名 + 身份 + 本月业绩进度 + 本月预计收入。
|
||||
* 示例:
|
||||
|
||||
* 第一行:`张三(助教)`
|
||||
* 第二行:`本月目标:5 万,已完成:3 万,任务:50 个,完成进度:60%`
|
||||
* 第三行:`本月预计收入:1.23 万元`
|
||||
* 下方内容区:多组指标,以两列卡片布局展示。
|
||||
|
||||
**指标分组示意**
|
||||
|
||||
* 分组一:`收入构成`
|
||||
* 分组二:`台球助教业绩`
|
||||
* 分组三:`充值业绩`
|
||||
* 分组四:`酒水业绩`
|
||||
|
||||
每组都有组标题一行,下面为两列卡片网格。
|
||||
|
||||
**单个指标卡片内容**
|
||||
|
||||
* 布局:
|
||||
|
||||
* 卡片内上下两行,可视为“名称行 + 数据行”。
|
||||
* 字段:
|
||||
|
||||
* 指标名称
|
||||
* 当前值
|
||||
* 目标值
|
||||
* 完成度(百分比)
|
||||
* 对齐与单位:
|
||||
|
||||
* 数值区居中对齐。
|
||||
* 单位规则:
|
||||
|
||||
* 元:整数,无小数。
|
||||
* 万元:保留 2 位小数。
|
||||
* 完成度:使用 `%`。
|
||||
|
||||
**交互**
|
||||
|
||||
* 页面整体可滚动。
|
||||
* 卡片本身无需额外交互(本期不跳转、不长按)。
|
||||
|
||||
**时间范围**
|
||||
|
||||
* 本页仅展示当前“本月”的业绩数据,不提供时间周期切换。
|
||||
|
||||
---
|
||||
|
||||
## 8.3 Tab:看板
|
||||
|
||||
### 8.3.1 看板首页(含财务/客户/助教)
|
||||
|
||||
**入口**
|
||||
|
||||
* 底部 TabBar 点击“看板”。
|
||||
|
||||
**顶部区域**
|
||||
|
||||
* 一级标签(顶部 Tab):
|
||||
|
||||
* `财务` / `客户` / `助教`
|
||||
* 默认选中:`财务`(原型中展示全功能视角)
|
||||
|
||||
**筛选区域**
|
||||
|
||||
* 位置:一级标签下方。
|
||||
* 展示方式:多标签筛选按钮,每个按钮点击后展开下拉菜单,交互类似外卖/点评类应用。
|
||||
* 联动规则:
|
||||
|
||||
* 更改任一筛选条件后,立即刷新当前视图数据(无需额外“确定”按钮)。
|
||||
* 不提供“重置筛选”按钮。
|
||||
|
||||
**滚动行为**
|
||||
|
||||
* 当用户向上滚动列表内容时,筛选区域保持吸顶显示。
|
||||
* 当用户向下快速滚动时,可自动收起/隐藏筛选区域,仅保留一级 Tab,增强可视区域。
|
||||
* 向上滚动时再次展示筛选区域。
|
||||
|
||||
---
|
||||
|
||||
### 8.3.2 看板 – 财务视图
|
||||
|
||||
**入口**
|
||||
|
||||
* 看板顶部 Tab 选择“财务”(默认)。
|
||||
|
||||
**筛选条件**
|
||||
|
||||
* 条件 1:时间月份
|
||||
|
||||
* 选项:
|
||||
|
||||
1. 本月(默认)
|
||||
2. 上个月
|
||||
3. 最近 3 个月
|
||||
4. 最近半年
|
||||
5. 本季度
|
||||
6. 上个季度
|
||||
7. 本周
|
||||
8. 上周
|
||||
9. 指定时间周期
|
||||
* 选择“指定时间周期”时:
|
||||
|
||||
* 打开日期区间选择组件,可选择开始日期与结束日期。
|
||||
* 最大跨度:366 天。
|
||||
* 当用户选择的时间跨度超过 366 天时,非模态提示:例如 `时间跨度不可超过 366 天`,并阻止该选择生效。
|
||||
|
||||
* 条件 2:区域
|
||||
|
||||
* 选项:
|
||||
|
||||
1. 全部(默认)
|
||||
2. 大厅(子级:A 区、B 区、C 区)
|
||||
3. 麻将房
|
||||
4. 团建房
|
||||
5. 具体房间台桌
|
||||
* 选择“具体房间台桌”时:
|
||||
|
||||
* 弹出选择弹窗,列表单选。
|
||||
* 列表按“大厅 / 麻将房 / 团建房”分组展示具体房间或台桌。
|
||||
* 如接口获取失败或为空,在弹窗中显示:`网络错误,请重试`,并提供“重试”入口。
|
||||
|
||||
**财务汇总行**
|
||||
|
||||
* 展示位置:筛选区域下方第一行。
|
||||
* 分为三列:
|
||||
|
||||
* 当前筛选条件下实际收入
|
||||
* 当前筛选条件下实际支出
|
||||
* 当前筛选条件下净利润
|
||||
* 显隐与“预计”字样:
|
||||
|
||||
* 某些筛选条件下不显示支出与净利润,由接口控制。
|
||||
* 某些时间维度(例如本月、本周等)可显示“预计”字样:`12345 元(预计)`,由接口在数据中标记。
|
||||
|
||||
**内容分区**
|
||||
|
||||
分为四个部分,依次:
|
||||
|
||||
1. 营业数据
|
||||
2. 收入构成
|
||||
3. 支出构成
|
||||
4. 利润构成
|
||||
|
||||
每一部分包含:
|
||||
|
||||
* 标题行:如 `营业数据`
|
||||
* 指标卡片区:每行 3 个卡片,自动换行。
|
||||
|
||||
**指标卡片结构**
|
||||
|
||||
* 每个卡片:
|
||||
|
||||
* 第一行(标题行):左侧图标(简单占位)、右侧为指标名称(例如“总流水”、“客单价”等)。
|
||||
* 第二行(详情行):文字 + 数值,或文字 / 数值单独展示:
|
||||
|
||||
* 例如:`本期:12345 元`,或 `毛利率:35%`。
|
||||
* 指标列表(示意,实际由接口控制):
|
||||
|
||||
* 营业数据:总流水、客单价、开台数、场次、平均停留时长等。
|
||||
* 收入构成:桌费、助教费、酒水、餐饮、包房费、其他。
|
||||
* 支出构成:房租、水电、人工、耗材、推广等。
|
||||
* 利润构成:毛利、净利、毛利率、净利率等。
|
||||
|
||||
**交互**
|
||||
|
||||
* 长按任意指标卡片:
|
||||
|
||||
* 启动助手对话,跳转至“助手对话页”,以该指标为引用内容(来源:财务看板 + 指标名 + 当前数值等),开启新对话主题。
|
||||
* 列表下拉刷新,重新拉取数据。
|
||||
|
||||
---
|
||||
|
||||
### 8.3.3 看板 – 客户视图
|
||||
|
||||
**入口**
|
||||
|
||||
* 看板顶部 Tab 选择“客户”。
|
||||
|
||||
**筛选条件**
|
||||
|
||||
* 条件 1:客户类型
|
||||
|
||||
* 最近到店:按最近到店时间由近到远。
|
||||
* 最应召回:按当天召回因子由高到低(默认)。
|
||||
* 最近充值:按充值时间由近到远。
|
||||
* 最高消费:最近 60 天到店消费金额由高到低(不含充值)。
|
||||
* 最高余额:按单个客户所有会员卡金额总计由高到低。
|
||||
* 最频繁:最近 60 天到店次数由多到少。
|
||||
* 潜力股:最近 60 天到店间隔有缩短趋势的客户。
|
||||
* 最专一:最近 60 天使用助教服务 ≥10 次,且 ≥8 次为同一助教,最近 2 次均为该助教。
|
||||
|
||||
* 条件 2:偏爱项目
|
||||
|
||||
* 不限(默认)
|
||||
* 中式/追分
|
||||
* 斯诺克
|
||||
* 麻将
|
||||
* 团建
|
||||
|
||||
**助教身份默认筛选(后台行为,前端不显式展示)**
|
||||
|
||||
* 当登录用户身份为“助教”时,后台默认增加过滤条件:仅显示最近 14 天内该助教提供过课程服务的客户。
|
||||
* 前端不提供取消或修改该条件的开关,也不在 UI 中单独标识。
|
||||
|
||||
**客户列表卡片布局**
|
||||
|
||||
* 第一行:
|
||||
|
||||
* 左侧:
|
||||
|
||||
* 客户名称
|
||||
* 等级标(如等级图标或字母 A/B/C)
|
||||
* VIP 标识(如“VIP”标签),有则显示。
|
||||
* 右侧:最喜欢的助教列表,文字形式展示,例如:
|
||||
|
||||
* `💖 助教A、💖 助教B、💛 助教C...`
|
||||
* 最多展示前三,超过则以省略号表示。
|
||||
|
||||
* 第二行:
|
||||
|
||||
* 当前排序条件对应的核心指标(如召回因子、储值金额、累计消费等)。
|
||||
* 最近到店时间(副文案)。
|
||||
* 可在末尾增加一句简短说明,例如:`最近 30 天到店 5 次` 等。
|
||||
|
||||
**其他字段**
|
||||
|
||||
* 在“最高余额”等维度时,应显示该客户当前余额字段(由接口提供),格式按金额规则显示。
|
||||
|
||||
**交互**
|
||||
|
||||
* 点击客户卡片:进入“客户详情页”。
|
||||
* 长按客户卡片(可选):可考虑后续扩展为快速备注或助手入口,本期可不实现。
|
||||
|
||||
---
|
||||
|
||||
### 8.3.4 客户详情页
|
||||
|
||||
**入口**
|
||||
|
||||
* 看板 – 客户视图列表点击某客户。
|
||||
|
||||
**布局**
|
||||
|
||||
* 顶部导航:标题 `客户详情`,左侧返回按钮。
|
||||
* 模块一:客户基本信息
|
||||
|
||||
* 示例字段:姓名、手机号、会员编号、性别、等级、VIP 标识、所属门店等。
|
||||
* 手机号可支持点击拨号(后续实现时决定)。
|
||||
* 模块二:消费习惯
|
||||
|
||||
* 标签 + 文本说明的形式:
|
||||
|
||||
* 标签示例:`常来夜场`、`偏爱中式`、`高客单价` 等。
|
||||
* 文本说明:简要描述消费偏好、时段、频率等。
|
||||
* 模块三:与我的关系
|
||||
|
||||
* 等级:很好 / 好 / 一般 / 较陌生。
|
||||
* 等级下附文字说明,描述互动频率、最近服务情况等。
|
||||
* 底部固定操作栏:
|
||||
|
||||
* `问问助手`
|
||||
* `备注`
|
||||
|
||||
**交互**
|
||||
|
||||
* `问问助手`:
|
||||
|
||||
* 跳转到助手对话页。
|
||||
* 引用当前客户的关键信息(客户名、ID、最近消费等)作为灰底引用卡片。
|
||||
* 开启新对话主题。
|
||||
* `备注`:
|
||||
|
||||
* 底部弹出备注输入浮层,类型标记为“客户备注”,保存后进入“备注记录”。
|
||||
|
||||
---
|
||||
|
||||
### 8.3.5 看板 – 助教学视图
|
||||
|
||||
**入口**
|
||||
|
||||
* 看板顶部 Tab 选择“助教”。
|
||||
|
||||
**筛选条件**
|
||||
|
||||
* 条件 1:排序维度
|
||||
|
||||
* 创收最多(默认):按该助教带来的球房流水由高到低。
|
||||
* 创收最低:按球房流水由低到高。
|
||||
* 业绩最高:按业绩完成百分比由高到低。
|
||||
* 业绩最低:按业绩完成百分比由低到高.
|
||||
* 工资最高:按工资由高到低。
|
||||
* 工资最低:按工资由低到高。
|
||||
* 潜在客源储值:按该助教客户关系 >0.7 的所有客户储值金额总和由高到低。
|
||||
|
||||
* 条件 2:擅长项目
|
||||
|
||||
* 不限(默认)
|
||||
* 中式/追分
|
||||
* 斯诺克
|
||||
* 麻将
|
||||
* 团建
|
||||
|
||||
* 条件 3:时间月份
|
||||
|
||||
* 同财务视图:本月(默认)、上月、最近 3 个月、最近半年、本季度、上个季度、本周、上周、指定时间周期。
|
||||
* “指定时间周期”同样使用日期区间选择组件,并限制最大跨度 366 天,超出时非模态提示。
|
||||
|
||||
**助教列表卡片布局**
|
||||
|
||||
* 第一行:
|
||||
|
||||
* 左侧:
|
||||
|
||||
* 助教姓名
|
||||
* 等级标(如星级/等级)
|
||||
* 擅长项目(标签形式,展示主擅长方向)。
|
||||
* 右侧:
|
||||
|
||||
* 关系最好的客户列表,展示客户名称和关系指数(例如:`客户A 0.98、客户B 0.92、客户C 0.89...`),最多展示前三,超过以省略号表示。
|
||||
|
||||
* 第二行:
|
||||
|
||||
* 当前排序维度对应的数值信息,附单位/说明:
|
||||
|
||||
* 如创收最多:`本期流水:12345 元`
|
||||
* 业绩最高:`完成度:87%`
|
||||
* 工资:`本期工资:8000 元`
|
||||
* 上课时长等(小时)。
|
||||
|
||||
* 不显示头像。
|
||||
|
||||
**交互**
|
||||
|
||||
* 点击助教卡片:进入“助教详情页”。
|
||||
|
||||
---
|
||||
|
||||
### 8.3.6 助教详情页
|
||||
|
||||
**入口**
|
||||
|
||||
* 看板 – 助教视图列表点击某助教。
|
||||
|
||||
**布局**
|
||||
|
||||
* 顶部导航:标题 `助教详情`,左侧返回按钮。
|
||||
|
||||
* 模块一:助教基本信息
|
||||
|
||||
* 字段示例:姓名、工号、所属门店、擅长项目、等级等。
|
||||
|
||||
* 模块二:流水与业绩
|
||||
|
||||
* 本月带来的球房流水(数值,单位元或万元)。
|
||||
* 最近 3 个月带来的球房流水(数值)。
|
||||
* 综合业绩完成度(一个总进度百分比)。
|
||||
|
||||
* 模块三:工资与上课时长
|
||||
|
||||
* 本月工资总额。
|
||||
* 对应时间段的上课总时长(小时)。
|
||||
|
||||
* 模块四:前 10 个客户指数最高的客户列表
|
||||
|
||||
* 列表项字段:客户名 + 指数数值(0~1 或百分比展示)。
|
||||
|
||||
* 底部固定操作栏:
|
||||
|
||||
* `问问助手`
|
||||
* `备注`
|
||||
|
||||
**交互**
|
||||
|
||||
* `问问助手`:以助教信息和主要指标为引用,开启新对话主题。
|
||||
* `备注`:对该助教添加备注记录,类型为“助教备注”。
|
||||
|
||||
---
|
||||
|
||||
## 8.4 Tab:我的
|
||||
|
||||
### 8.4.1 我的首页
|
||||
|
||||
**入口**
|
||||
|
||||
* 底部 TabBar 点击“我的”。
|
||||
|
||||
**布局**
|
||||
|
||||
* 顶部:用户信息区域
|
||||
|
||||
* 用户名、身份(店长/助教等)、所属门店等信息。
|
||||
* 列表菜单项:
|
||||
|
||||
* `备注记录`
|
||||
* `助手对话记录`
|
||||
* `首页设置`
|
||||
* `退出账号`
|
||||
|
||||
**交互**
|
||||
|
||||
* 点击各行进入对应子页面或触发弹窗。
|
||||
|
||||
---
|
||||
|
||||
### 8.4.2 备注记录页
|
||||
|
||||
**入口**
|
||||
|
||||
* “我的”首页点击 `备注记录`。
|
||||
|
||||
**布局**
|
||||
|
||||
* 标题:`备注记录`
|
||||
* 列表按时间倒序(由近到远)平铺,不按日期分组。
|
||||
* 每条记录显示:
|
||||
|
||||
* 备注全文(不做截断或只做必要的单行/多行控制)。
|
||||
* 关联对象:例如 `客户:张三` / `任务:XXX` / `助教:李四`。
|
||||
* 创建时间。
|
||||
|
||||
**交互**
|
||||
|
||||
* 点击备注记录:不进入详情页(即本页即为详情展示),不支持编辑/删除。
|
||||
* 列表为空时:显示 `暂无数据`。
|
||||
|
||||
---
|
||||
|
||||
### 8.4.3 助手对话记录页
|
||||
|
||||
**入口**
|
||||
|
||||
* “我的”首页点击 `助手对话记录`。
|
||||
|
||||
**布局**
|
||||
|
||||
* 标题:`助手对话记录`
|
||||
* 列表项字段:
|
||||
|
||||
* 对话标题:由接口返回(一般为首条消息摘要)。
|
||||
* 最近一次对话时间。
|
||||
* 消息条数概览(例如:`共 25 条消息`)。
|
||||
* 列表按最近更新时间倒序。
|
||||
|
||||
**交互**
|
||||
|
||||
* 点击某条记录:
|
||||
|
||||
* 进入“助手对话页”,直接打开该会话。
|
||||
* 默认滚动到该对话的最后一条消息位置。
|
||||
* 不提供删除对话能力。
|
||||
* 列表为空时:显示 `暂无数据`。
|
||||
|
||||
---
|
||||
|
||||
### 8.4.4 首页设置页
|
||||
|
||||
**入口**
|
||||
|
||||
* “我的”首页点击 `首页设置`。
|
||||
|
||||
**布局**
|
||||
|
||||
* 标题:`首页设置`
|
||||
* 内容:
|
||||
|
||||
* 单选列表:
|
||||
|
||||
* `任务`
|
||||
* `看板`
|
||||
* 每项前有单选圆点,当前选中项高亮。
|
||||
* 底部:返回按钮。
|
||||
|
||||
**交互**
|
||||
|
||||
* 用户点击某一选项后立即生效,作为新的默认首页设置(切换即保存,不需要额外保存按钮)。
|
||||
* 退出账号不会清除该设置,再次登录仍使用该默认首页。
|
||||
|
||||
---
|
||||
|
||||
### 8.4.5 退出账号(确认弹窗)
|
||||
|
||||
**入口**
|
||||
|
||||
* “我的”首页点击 `退出账号`。
|
||||
|
||||
**交互**
|
||||
|
||||
* 弹出确认弹窗:
|
||||
|
||||
* 标题:`确认退出`
|
||||
* 文案:`确认退出当前账号吗?`
|
||||
* 按钮:
|
||||
|
||||
* 取消
|
||||
* 退出
|
||||
* 点击“退出”:
|
||||
|
||||
* 清除登录态。
|
||||
* 不清理由于当前账号相关的本地配置(如首页设置、筛选条件等)。
|
||||
* 跳转回“登录页”。
|
||||
* 点击“取消”:关闭弹窗,留在“我的”页。
|
||||
|
||||
---
|
||||
|
||||
## 8.5 全局:助手对话页
|
||||
|
||||
### 8.5.1 入口
|
||||
|
||||
* 悬浮助手按钮。
|
||||
* 任务详情页底部按钮“问问助手”。
|
||||
* 客户详情页底部按钮“问问助手”。
|
||||
* 助教详情页底部按钮“问问助手”。
|
||||
* 看板各视图中长按指标启动助手。
|
||||
* “助手对话记录”页点击某一条历史记录。
|
||||
|
||||
### 8.5.2 聊天形式
|
||||
|
||||
* 对话双方:用户(“我”)与“助手”。
|
||||
* UI 全面仿微信对话界面:
|
||||
|
||||
* 左侧:助手气泡,显示助手头像(固定)和名称。
|
||||
* 右侧:用户气泡,显示用户头像和名称。
|
||||
* 对话记录:
|
||||
|
||||
* 最近 50 条消息默认加载。
|
||||
* 上拉加载更早记录。
|
||||
|
||||
### 8.5.3 引用内容展示
|
||||
|
||||
* 从任务/客户/助教/看板等入口进入助手时,将引用上下文:
|
||||
|
||||
* 引用内容显示为一块灰底小卡片,位于即将发送的消息气泡上方。
|
||||
* 卡片内容包括:
|
||||
|
||||
* 来源类型:任务 / 客户 / 助教 / 看板(具体子模块如财务/客户看板等)。
|
||||
* 标题或名称(例如客户名、任务标题、指标名)。
|
||||
* 部分摘要文案或关键数据。
|
||||
* 引用内容不可编辑,用户只能在下方输入框中补充自己的提问文本后发送。
|
||||
|
||||
### 8.5.4 会话管理
|
||||
|
||||
* 每个“新对话主题”形成一个独立会话,出现在“助手对话记录”列表中。
|
||||
* 来源:
|
||||
|
||||
* 从“助手对话记录”进入:
|
||||
|
||||
* 直接打开对应会话,加载历史记录,滚动到最后一条消息。
|
||||
* 如距最后一条消息超过 1 小时,在输入框区域上方显示横条提示,提供两个按钮:
|
||||
|
||||
* `新对话主题`
|
||||
* `继续对话`
|
||||
* 选择“新对话主题”:清空当前对话展示区域,开始新的对话会话,该会话作为新条目记录在“助手对话记录”中,历史对话仍保留在原会话条目中。
|
||||
* 选择“继续对话”:在当前会话中继续发送消息,对话标题不变。
|
||||
* 从任务/客户/助教/看板入口的“问问助手”或长按启动:
|
||||
|
||||
* 固定开启“新对话主题”,不受 1 小时规则影响,始终新建会话,并带入引用内容。
|
||||
|
||||
### 8.5.5 输入与发送
|
||||
|
||||
* 输入区包含:
|
||||
|
||||
* 文本输入框。
|
||||
* “按住说话”按钮(语音转文字)。
|
||||
* 发送按钮。
|
||||
|
||||
**语音转文字交互**
|
||||
|
||||
* 点击“按住说话”按钮并按住:
|
||||
|
||||
* 显示录音状态动画,松开后结束录音并开始识别。
|
||||
* 识别结果展示在输入框中,用户可编辑后再点击“发送”。
|
||||
* 识别失败时,弹出提示:`识别失败,请重试`,不发送消息。
|
||||
|
||||
**键盘与滚动行为**
|
||||
|
||||
* 键盘弹出时,列表自动滚动到底部,确保最新消息和输入框可见。
|
||||
* 发送消息后,自动滚动到底部。
|
||||
|
||||
---
|
||||
|
||||
# 九、非功能性要求
|
||||
|
||||
* 关键页面(任务列表、看板页、助手对话页)首次可接受加载时间:≤ 10 秒(在普通网络环境下)。
|
||||
* 看板页面数据:
|
||||
|
||||
* 各数据块采用懒加载策略,优先加载当前视图及首屏必要数据,其他部分可在滚动时或后台加载,避免一次性加载过多影响首屏体验。
|
||||
* 本阶段不做埋点与统计需求设计。
|
||||
|
||||
12
apps/miniprogram/jest.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
testMatch: ['**/tests/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': ['ts-jest', {
|
||||
tsconfig: 'tsconfig.test.json',
|
||||
}],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/miniprogram/$1',
|
||||
},
|
||||
}
|
||||
@@ -1,14 +1,65 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/mvp/mvp",
|
||||
"pages/login/login",
|
||||
"pages/apply/apply",
|
||||
"pages/reviewing/reviewing",
|
||||
"pages/no-permission/no-permission",
|
||||
"pages/task-list/task-list",
|
||||
"pages/board-finance/board-finance",
|
||||
"pages/my-profile/my-profile",
|
||||
"pages/task-detail/task-detail",
|
||||
"pages/task-detail-callback/task-detail-callback",
|
||||
"pages/task-detail-priority/task-detail-priority",
|
||||
"pages/task-detail-relationship/task-detail-relationship",
|
||||
"pages/notes/notes",
|
||||
"pages/performance/performance",
|
||||
"pages/performance-records/performance-records",
|
||||
"pages/board-customer/board-customer",
|
||||
"pages/board-coach/board-coach",
|
||||
"pages/customer-detail/customer-detail",
|
||||
"pages/customer-service-records/customer-service-records",
|
||||
"pages/coach-detail/coach-detail",
|
||||
"pages/chat/chat",
|
||||
"pages/chat-history/chat-history",
|
||||
"pages/index/index",
|
||||
"pages/logs/logs"
|
||||
"pages/dev-tools/dev-tools",
|
||||
"pages/logs/logs",
|
||||
"pages/mvp/mvp"
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#8b8b8b",
|
||||
"selectedColor": "#0052d9",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/task-list/task-list",
|
||||
"text": "任务",
|
||||
"iconPath": "assets/icons/tab-task.png",
|
||||
"selectedIconPath": "assets/icons/tab-task-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/board-finance/board-finance",
|
||||
"text": "看板",
|
||||
"iconPath": "assets/icons/tab-board.png",
|
||||
"selectedIconPath": "assets/icons/tab-board-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my-profile/my-profile",
|
||||
"text": "我的",
|
||||
"iconPath": "assets/icons/tab-my.png",
|
||||
"selectedIconPath": "assets/icons/tab-my-active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "Weixin",
|
||||
"navigationBarTitleText": "球房运营助手",
|
||||
"navigationBarBackgroundColor": "#ffffff"
|
||||
},
|
||||
"usingComponents": {
|
||||
"dev-fab": "/components/dev-fab/dev-fab"
|
||||
},
|
||||
"componentFramework": "glass-easel",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,70 @@
|
||||
// app.ts
|
||||
// 应用入口 — 启动时检查登录状态并路由到对应页面
|
||||
import { request } from "./utils/request"
|
||||
|
||||
App<IAppOption>({
|
||||
globalData: {},
|
||||
onLaunch() {
|
||||
// 展示本地存储能力
|
||||
const logs = wx.getStorageSync('logs') || []
|
||||
logs.unshift(Date.now())
|
||||
wx.setStorageSync('logs', logs)
|
||||
|
||||
// 登录
|
||||
wx.login({
|
||||
success: res => {
|
||||
console.log(res.code)
|
||||
// 发送 res.code 到后台换取 openId, sessionKey, unionId
|
||||
},
|
||||
})
|
||||
onLaunch() {
|
||||
// 从 Storage 恢复 token 和用户信息
|
||||
const token = wx.getStorageSync("token")
|
||||
const refreshToken = wx.getStorageSync("refreshToken")
|
||||
const userId = wx.getStorageSync("userId")
|
||||
if (token) {
|
||||
this.globalData.token = token
|
||||
this.globalData.refreshToken = refreshToken
|
||||
if (userId) {
|
||||
this.globalData.authUser = {
|
||||
userId,
|
||||
status: wx.getStorageSync("userStatus") || "new",
|
||||
}
|
||||
}
|
||||
// 有 token → 查询最新用户状态并路由
|
||||
this.checkAuthStatus()
|
||||
}
|
||||
// 无 token → 停留在 login 页(首页已设为 login)
|
||||
},
|
||||
})
|
||||
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/api/xcx/me",
|
||||
method: "GET",
|
||||
needAuth: true,
|
||||
})
|
||||
|
||||
// 持久化用户信息
|
||||
this.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.status,
|
||||
nickname: data.nickname,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.status)
|
||||
|
||||
// 根据状态路由
|
||||
switch (data.status) {
|
||||
case "approved":
|
||||
wx.reLaunch({ url: "/pages/task-list/task-list" })
|
||||
break
|
||||
case "pending":
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
break
|
||||
case "new":
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
break
|
||||
case "rejected":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
case "disabled":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
default:
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// token 无效或网络错误 → 停留在 login 页
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,3 +8,104 @@
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 设计 Token 全局变量(基于 design-tokens.json)
|
||||
* ============================================ */
|
||||
page {
|
||||
/* 颜色 */
|
||||
--color-primary: #0052d9;
|
||||
--color-primary-light: #ecf2fe;
|
||||
--color-success: #00a870;
|
||||
--color-warning: #ed7b2f;
|
||||
--color-error: #e34d59;
|
||||
--color-gray-1: #f3f3f3;
|
||||
--color-gray-2: #eeeeee;
|
||||
--color-gray-3: #e7e7e7;
|
||||
--color-gray-4: #dcdcdc;
|
||||
--color-gray-5: #c5c5c5;
|
||||
--color-gray-6: #a6a6a6;
|
||||
--color-gray-7: #8b8b8b;
|
||||
--color-gray-8: #777777;
|
||||
--color-gray-9: #5e5e5e;
|
||||
--color-gray-10: #4b4b4b;
|
||||
--color-gray-11: #393939;
|
||||
--color-gray-12: #2c2c2c;
|
||||
--color-gray-13: #242424;
|
||||
|
||||
/* 字号 */
|
||||
--font-xs: 24rpx;
|
||||
--font-sm: 28rpx;
|
||||
--font-base: 32rpx;
|
||||
--font-lg: 36rpx;
|
||||
--font-xl: 40rpx;
|
||||
--font-2xl: 48rpx;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 8rpx;
|
||||
--radius-md: 16rpx;
|
||||
--radius-lg: 24rpx;
|
||||
--radius-xl: 32rpx;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-lg: 0 8rpx 32rpx rgba(0,0,0,0.06);
|
||||
--shadow-xl: 0 16rpx 48rpx rgba(0,0,0,0.08);
|
||||
|
||||
/* 间距基准 */
|
||||
--spacing-base: 8rpx;
|
||||
|
||||
/* TDesign 主题覆盖 */
|
||||
--td-brand-color: #0052d9;
|
||||
--td-brand-color-light: #ecf2fe;
|
||||
--td-success-color: #00a870;
|
||||
--td-warning-color: #ed7b2f;
|
||||
--td-error-color: #e34d59;
|
||||
|
||||
/* 页面默认样式 */
|
||||
background-color: var(--color-gray-1);
|
||||
font-size: var(--font-base);
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 通用工具类
|
||||
* ============================================ */
|
||||
|
||||
/* 安全区适配 */
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.safe-area-top {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
/* 文本省略 */
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Flex 布局 */
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
21
apps/miniprogram/miniprogram/assets/icons/ai-robot.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 机器人头部 -->
|
||||
<rect x="5" y="7" width="14" height="12" rx="4" fill="white"/>
|
||||
<!-- 天线 -->
|
||||
<path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="3" r="1.5" fill="white"/>
|
||||
<!-- 眼睛 -->
|
||||
<circle cx="9" cy="11.5" r="2" fill="#667eea"/>
|
||||
<circle cx="15" cy="11.5" r="2" fill="#667eea"/>
|
||||
<!-- 眼睛高光 -->
|
||||
<circle cx="9.5" cy="11" r="0.7" fill="white"/>
|
||||
<circle cx="15.5" cy="11" r="0.7" fill="white"/>
|
||||
<!-- 微笑嘴巴 -->
|
||||
<path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<!-- 腮红 -->
|
||||
<circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/>
|
||||
<circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/>
|
||||
<!-- 耳朵 -->
|
||||
<rect x="3" y="10" width="2" height="4" rx="1" fill="white"/>
|
||||
<rect x="19" y="10" width="2" height="4" rx="1" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1001 B |
3
apps/miniprogram/miniprogram/assets/icons/arrow-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#4b4b4b" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 153 B |
3
apps/miniprogram/miniprogram/assets/icons/chart.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a870">
|
||||
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
3
apps/miniprogram/miniprogram/assets/icons/chat-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#a6a6a6">
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 185 B |
3
apps/miniprogram/miniprogram/assets/icons/chat.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ed7b2f">
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 185 B |
3
apps/miniprogram/miniprogram/assets/icons/check-bold.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 150 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
4
apps/miniprogram/miniprogram/assets/icons/clock.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 187 B |
4
apps/miniprogram/miniprogram/assets/icons/forbidden.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#a6a6a6">
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
BIN
apps/miniprogram/miniprogram/assets/icons/icon-ai-float.png
Normal file
|
After Width: | Height: | Size: 70 B |
BIN
apps/miniprogram/miniprogram/assets/icons/icon-ai-inline.png
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 187 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.32.32 0 0 0 .165-.054l1.9-1.106a.96.96 0 0 1 .465-.116.94.94 0 0 1 .272.04 10.6 10.6 0 0 0 2.822.384c.136 0 .271-.002.405-.009a6.9 6.9 0 0 1-.315-2.053c0-3.694 3.614-6.69 8.076-6.69.233 0 .463.01.691.027C16.964 4.837 13.132 2.188 8.691 2.188zm-2.97 5.28a1.03 1.03 0 1 1 0-2.06 1.03 1.03 0 0 1 0 2.06zm5.96 0a1.03 1.03 0 1 1 0-2.06 1.03 1.03 0 0 1 0 2.06zM24 14.2c0-3.355-3.4-6.08-7.59-6.08s-7.59 2.725-7.59 6.08c0 3.356 3.4 6.08 7.59 6.08.772 0 1.515-.094 2.215-.268a.77.77 0 0 1 .224-.033.79.79 0 0 1 .382.095l1.565.912a.26.26 0 0 0 .135.044c.13 0 .238-.108.238-.242 0-.06-.024-.117-.04-.175l-.32-1.218a.48.48 0 0 1 .175-.547C22.95 17.89 24 16.165 24 14.2zm-10.14-.426a.85.85 0 1 1 0-1.7.85.85 0 0 1 0 1.7zm5.1 0a.85.85 0 1 1 0-1.7.85.85 0 0 1 0 1.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 998 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#0052d9">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 194 B |
3
apps/miniprogram/miniprogram/assets/icons/info-error.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e34d59">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 194 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ed7b2f">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 194 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<circle cx="12" cy="12" r="10" fill="white" opacity="0.3"/>
|
||||
<circle cx="12" cy="12" r="6" fill="white"/>
|
||||
<circle cx="12" cy="12" r="2" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
5
apps/miniprogram/miniprogram/assets/icons/logout.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#5e5e5e" stroke-width="2">
|
||||
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 246 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-board-active.png
Normal file
|
After Width: | Height: | Size: 66 B |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="13" width="4" height="8" rx="1" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
<rect x="10" y="8" width="4" height="13" rx="1" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
<rect x="16" y="3" width="4" height="18" rx="1" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="13" width="4" height="8" rx="1" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
<rect x="10" y="8" width="4" height="13" rx="1" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
<rect x="16" y="3" width="4" height="18" rx="1" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-board.png
Normal file
|
After Width: | Height: | Size: 66 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-my-active.png
Normal file
|
After Width: | Height: | Size: 66 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="7" r="4" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
<path d="M5.5 21a6.5 6.5 0 0113 0h-13z" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
4
apps/miniprogram/miniprogram/assets/icons/tab-my-nav.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="7" r="4" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
<path d="M5.5 21a6.5 6.5 0 0113 0h-13z" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 229 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-my.png
Normal file
|
After Width: | Height: | Size: 66 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-task-active.png
Normal file
|
After Width: | Height: | Size: 66 B |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="4" width="14" height="17" rx="2" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" fill="#0052d9" stroke="#0052d9" stroke-width="1"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="4" width="14" height="17" rx="2" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" stroke="#8b8b8b" stroke-width="1.5"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="#8b8b8b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
BIN
apps/miniprogram/miniprogram/assets/icons/tab-task.png
Normal file
|
After Width: | Height: | Size: 66 B |
3
apps/miniprogram/miniprogram/assets/icons/task.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#0052d9">
|
||||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
3
apps/miniprogram/miniprogram/assets/icons/wechat.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.32.32 0 0 0 .165-.054l1.9-1.106a.96.96 0 0 1 .465-.116.94.94 0 0 1 .272.04 10.6 10.6 0 0 0 2.822.384c.136 0 .271-.002.405-.009a6.9 6.9 0 0 1-.315-2.053c0-3.694 3.614-6.69 8.076-6.69.233 0 .463.01.691.027C16.964 4.837 13.132 2.188 8.691 2.188zm-2.97 5.28a1.03 1.03 0 1 1 0-2.06 1.03 1.03 0 0 1 0 2.06zm5.96 0a1.03 1.03 0 1 1 0-2.06 1.03 1.03 0 0 1 0 2.06zM24 14.2c0-3.355-3.4-6.08-7.59-6.08s-7.59 2.725-7.59 6.08c0 3.356 3.4 6.08 7.59 6.08.772 0 1.515-.094 2.215-.268a.77.77 0 0 1 .224-.033.79.79 0 0 1 .382.095l1.565.912a.26.26 0 0 0 .135.044c.13 0 .238-.108.238-.242 0-.06-.024-.117-.04-.175l-.32-1.218a.48.48 0 0 1 .175-.547C22.95 17.89 24 16.165 24 14.2zm-10.14-.426a.85.85 0 1 1 0-1.7.85.85 0 0 1 0 1.7zm5.1 0a.85.85 0 1 1 0-1.7.85.85 0 0 1 0 1.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 998 B |
BIN
apps/miniprogram/miniprogram/assets/images/banner-blue.png
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 是否显示 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
/** 跳转目标页面 */
|
||||
targetUrl: {
|
||||
type: String,
|
||||
value: '/pages/chat/chat',
|
||||
},
|
||||
/** 可选:携带客户 ID 参数 */
|
||||
customerId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 距底部距离(rpx),TabBar 页面用 200,非 TabBar 页面用 120 */
|
||||
bottom: {
|
||||
type: Number,
|
||||
value: 200,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
let url = this.data.targetUrl
|
||||
if (this.data.customerId) {
|
||||
url += `?customerId=${this.data.customerId}`
|
||||
}
|
||||
wx.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
wx.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
<!-- AI 悬浮对话按钮 — SVG 机器人 + 渐变流动背景 -->
|
||||
<view
|
||||
class="ai-float-btn-container"
|
||||
wx:if="{{visible}}"
|
||||
style="bottom: {{bottom}}rpx"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="ai-float-btn">
|
||||
<!-- 高光叠加层 -->
|
||||
<view class="ai-float-btn-highlight"></view>
|
||||
<!-- 机器人 SVG(小程序用 image 引用) -->
|
||||
<image class="ai-icon-svg" src="/assets/icons/ai-robot.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,57 @@
|
||||
/* AI 悬浮按钮 — 忠于 H5 原型:渐变流动背景 + 机器人 SVG */
|
||||
/* H5: 56px → 56×2×0.875 = 98rpx */
|
||||
|
||||
.ai-float-btn-container {
|
||||
position: fixed;
|
||||
right: 28rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.ai-float-btn {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* 渐变动画背景 — 忠于 H5 原型 */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #f5576c 75%, #667eea 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 8s ease infinite;
|
||||
box-shadow: 0 8rpx 40rpx rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.ai-float-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 高光叠加层 */
|
||||
.ai-float-btn-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.2) 0%, transparent 50%, rgba(0,0,0,0.1) 100%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 背景渐变流动动画 */
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 50% 100%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
75% { background-position: 50% 0%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* SVG 图标 H5: 30px → 52rpx */
|
||||
.ai-icon-svg {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
26
apps/miniprogram/miniprogram/components/banner/banner.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** Banner 主题色 */
|
||||
theme: {
|
||||
type: String,
|
||||
value: 'blue',
|
||||
},
|
||||
/** Banner 标题 */
|
||||
title: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 指标列表 [{label, value}] */
|
||||
metrics: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 背景图加载失败时降级为纯渐变色(CSS 已处理) */
|
||||
onBgError() {
|
||||
// 背景图加载失败,CSS 渐变色自动降级,无需额外处理
|
||||
},
|
||||
},
|
||||
})
|
||||
12
apps/miniprogram/miniprogram/components/banner/banner.wxml
Normal file
@@ -0,0 +1,12 @@
|
||||
<view class="banner banner--{{theme}}">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<text class="banner-title">{{title}}</text>
|
||||
<view class="banner-metrics">
|
||||
<view class="metric-item" wx:for="{{metrics}}" wx:key="label">
|
||||
<text class="metric-value">{{item.value}}</text>
|
||||
<text class="metric-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
66
apps/miniprogram/miniprogram/components/banner/banner.wxss
Normal file
@@ -0,0 +1,66 @@
|
||||
.banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 主题渐变降级(背景图加载失败时) */
|
||||
.banner--blue { background: linear-gradient(135deg, #0052d9, #0080ff); }
|
||||
.banner--red { background: linear-gradient(135deg, #e34d59, #ff6b6b); }
|
||||
.banner--orange { background: linear-gradient(135deg, #ed7b2f, #ffaa44); }
|
||||
.banner--pink { background: linear-gradient(135deg, #d94da0, #ff6bcc); }
|
||||
.banner--teal { background: linear-gradient(135deg, #00a870, #00d68f); }
|
||||
.banner--coral { background: linear-gradient(135deg, #e06c5a, #ff8a7a); }
|
||||
.banner--dark-gold { background: linear-gradient(135deg, #8b6914, #c9a227); }
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.banner-metrics {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 自定义底部导航栏 — 非 TabBar 页面模拟系统导航
|
||||
Component({
|
||||
properties: {
|
||||
/** 当前激活的 tab: task / board / my */
|
||||
active: {
|
||||
type: String,
|
||||
value: 'board',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === this.data.active) return
|
||||
|
||||
if (tab === 'task') {
|
||||
wx.switchTab({ url: '/pages/task-list/task-list' })
|
||||
} else if (tab === 'board') {
|
||||
wx.switchTab({ url: '/pages/board-finance/board-finance' })
|
||||
} else if (tab === 'my') {
|
||||
wx.switchTab({ url: '/pages/my-profile/my-profile' })
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 自定义底部导航栏 — 用于非 TabBar 的看板子页面,SVG icon 忠于 H5 原型 -->
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
class="tab-bar-item {{active === 'task' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="task"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'task' ? '/assets/icons/tab-task-nav-active.svg' : '/assets/icons/tab-task-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">任务</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-bar-item {{active === 'board' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="board"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'board' ? '/assets/icons/tab-board-nav-active.svg' : '/assets/icons/tab-board-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">看板</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-bar-item {{active === 'my' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="my"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'my' ? '/assets/icons/tab-my-nav-active.svg' : '/assets/icons/tab-my-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,39 @@
|
||||
/* 自定义底部导航栏 — 模拟系统 TabBar 外观 */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 100rpx;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid #eeeeee;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.tab-bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.tab-bar-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.tab-bar-label {
|
||||
font-size: 20rpx;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.tab-bar-item--active .tab-bar-label {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
28
apps/miniprogram/miniprogram/components/dev-fab/dev-fab.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 开发调试浮动按钮组件
|
||||
*
|
||||
* 仅在 develop 环境下显示,点击跳转到 dev-tools 页面。
|
||||
* 使用 movable-view 实现可拖拽。
|
||||
*/
|
||||
Component({
|
||||
data: {
|
||||
visible: false,
|
||||
x: 580, // 初始位置:右下角附近(rpx 换算后的 px 近似值)
|
||||
y: 1100,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 仅 develop 环境显示
|
||||
const accountInfo = wx.getAccountInfoSync()
|
||||
const env = accountInfo.miniProgram.envVersion
|
||||
this.setData({ visible: env === "develop" })
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
goDevTools() {
|
||||
wx.navigateTo({ url: "/pages/dev-tools/dev-tools" })
|
||||
},
|
||||
},
|
||||
})
|
||||
15
apps/miniprogram/miniprogram/components/dev-fab/dev-fab.wxml
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
开发调试浮动按钮 — 仅 develop 环境渲染
|
||||
可拖拽,点击跳转到 dev-tools 页面
|
||||
-->
|
||||
<movable-area wx:if="{{visible}}" class="fab-area">
|
||||
<movable-view
|
||||
class="fab-btn"
|
||||
direction="all"
|
||||
x="{{x}}"
|
||||
y="{{y}}"
|
||||
bindtap="goDevTools"
|
||||
>
|
||||
<text class="fab-icon">🛠</text>
|
||||
</movable-view>
|
||||
</movable-area>
|
||||
29
apps/miniprogram/miniprogram/components/dev-fab/dev-fab.wxss
Normal file
@@ -0,0 +1,29 @@
|
||||
/* 浮动按钮覆盖全屏,不阻挡页面交互 */
|
||||
.fab-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(24, 144, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.25);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 40rpx;
|
||||
line-height: 96rpx;
|
||||
text-align: center;
|
||||
width: 96rpx;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 筛选标签文字 */
|
||||
label: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 选项列表 */
|
||||
options: {
|
||||
type: Array,
|
||||
value: [] as Array<{ value: string; text: string }>,
|
||||
},
|
||||
/** 当前选中值 */
|
||||
value: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
'value, options'(val: string, opts: Array<{ value: string; text: string }>) {
|
||||
const matched = (opts || []).find((o) => o.value === val)
|
||||
this.setData({ selectedText: matched ? matched.text : '' })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
expanded: false,
|
||||
selectedText: '',
|
||||
panelTop: 0,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const { value, options } = this.data
|
||||
const matched = (options as Array<{ value: string; text: string }>).find(
|
||||
(o) => o.value === value,
|
||||
)
|
||||
this.setData({ selectedText: matched ? matched.text : '' })
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
if (!this.data.options || (this.data.options as any[]).length === 0) return
|
||||
|
||||
if (!this.data.expanded) {
|
||||
// 展开时计算按钮底部位置,作为面板 top
|
||||
this.createSelectorQuery()
|
||||
.select('.filter-dropdown')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
this.setData({
|
||||
panelTop: rect.bottom,
|
||||
expanded: true,
|
||||
})
|
||||
} else {
|
||||
this.setData({ expanded: true })
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
} else {
|
||||
this.setData({ expanded: false })
|
||||
}
|
||||
},
|
||||
|
||||
onSelect(e: WechatMiniprogram.TouchEvent) {
|
||||
const val = e.currentTarget.dataset.value as string
|
||||
const opts = this.data.options as Array<{ value: string; text: string }>
|
||||
const matched = opts.find((o) => o.value === val)
|
||||
this.setData({
|
||||
expanded: false,
|
||||
selectedText: matched ? matched.text : '',
|
||||
})
|
||||
this.triggerEvent('change', { value: val })
|
||||
},
|
||||
|
||||
/** 点击遮罩层关闭 */
|
||||
onMaskTap() {
|
||||
this.setData({ expanded: false })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- 筛选下拉组件 — 全屏宽度面板 + 遮罩层 -->
|
||||
<view class="filter-dropdown-wrap" wx:if="{{options && options.length > 0}}">
|
||||
<view class="filter-dropdown {{expanded ? 'filter-dropdown--active' : ''}}" bindtap="toggleDropdown">
|
||||
<text class="filter-label">{{selectedText || label}}</text>
|
||||
<t-icon name="caret-down-small" size="32rpx" class="filter-arrow {{expanded ? 'filter-arrow--up' : ''}}" />
|
||||
</view>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<view class="dropdown-mask" wx:if="{{expanded}}" catchtap="onMaskTap" />
|
||||
|
||||
<!-- 全屏宽度下拉面板 -->
|
||||
<view
|
||||
class="dropdown-panel {{expanded ? 'dropdown-panel--show' : ''}}"
|
||||
style="top: {{panelTop}}px"
|
||||
>
|
||||
<view
|
||||
class="dropdown-item {{item.value === value ? 'dropdown-item--active' : ''}}"
|
||||
wx:for="{{options}}"
|
||||
wx:key="value"
|
||||
data-value="{{item.value}}"
|
||||
bindtap="onSelect"
|
||||
>
|
||||
<text>{{item.text}}</text>
|
||||
<t-icon wx:if="{{item.value === value}}" name="check" size="32rpx" color="var(--color-primary)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,107 @@
|
||||
.filter-dropdown-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 触发按钮 */
|
||||
.filter-dropdown {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2rpx solid var(--color-gray-1);
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.filter-dropdown--active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-gray-12);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-dropdown--active .filter-label {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 箭头旋转动画 */
|
||||
.filter-arrow {
|
||||
transition: transform 0.25s ease;
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
.filter-arrow--up {
|
||||
transform: rotate(180deg);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 遮罩层 — 半透明黑色背景,忠于 H5 原型 */
|
||||
.dropdown-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 下拉面板 — 全屏宽度,固定定位,从筛选栏下方展开 */
|
||||
.dropdown-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 28rpx 28rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(-16rpx);
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
pointer-events: none;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.dropdown-panel--show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 选项项 — 更大的 padding,忠于 H5 原型 */
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 34rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-gray-12, #2c2c2c);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.dropdown-item:active {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
|
||||
.dropdown-item--active {
|
||||
color: var(--color-primary, #0052d9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item + .dropdown-item {
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 评分 0-10,超出范围自动 clamp */
|
||||
score: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
heartEmoji: '💙',
|
||||
},
|
||||
|
||||
observers: {
|
||||
score(val: number) {
|
||||
const s = val < 0 ? 0 : val > 10 ? 10 : val
|
||||
let emoji: string
|
||||
if (s > 8.5) {
|
||||
emoji = '💖'
|
||||
} else if (s > 7) {
|
||||
emoji = '🧡'
|
||||
} else if (s > 5) {
|
||||
emoji = '💛'
|
||||
} else {
|
||||
emoji = '💙'
|
||||
}
|
||||
this.setData({ heartEmoji: emoji })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
<text class="heart-icon">{{heartEmoji}}</text>
|
||||
@@ -0,0 +1,6 @@
|
||||
.heart-icon {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
top: -4rpx;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** type → Emoji + 标签文字映射 */
|
||||
const TAG_MAP: Record<string, { emoji: string; label: string }> = {
|
||||
chinese: { emoji: '🎱', label: '中式' },
|
||||
snooker: { emoji: '斯', label: '斯诺克' },
|
||||
mahjong: { emoji: '🀅', label: '麻将' },
|
||||
karaoke: { emoji: '🎤', label: 'K歌' },
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
/** 喜好类型 */
|
||||
type: {
|
||||
type: String,
|
||||
value: 'chinese',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
type(val: string) {
|
||||
const tag = TAG_MAP[val] || { emoji: '❓', label: val }
|
||||
this.setData({ emoji: tag.emoji, label: tag.label })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
emoji: '🎱',
|
||||
label: '中式',
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const tag = TAG_MAP[this.data.type] || { emoji: '❓', label: this.data.type }
|
||||
this.setData({ emoji: tag.emoji, label: tag.label })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
<view class="hobby-tag">
|
||||
<text class="tag-emoji">{{emoji}}</text>
|
||||
<text class="tag-label">{{label}}</text>
|
||||
</view>
|
||||
@@ -0,0 +1,19 @@
|
||||
.hobby-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--color-gray-1);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-9);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tag-emoji {
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.tag-label {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 指标名称 */
|
||||
title: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 指标数值(已格式化字符串) */
|
||||
value: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
/** 单位(元/次/人) */
|
||||
unit: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 环比趋势 */
|
||||
trend: {
|
||||
type: String,
|
||||
value: 'flat', // 'up' | 'down' | 'flat'
|
||||
},
|
||||
/** 环比数值(如 "+12.5%") */
|
||||
trendValue: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 帮助说明文字 */
|
||||
helpText: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
value(val: string | null | undefined) {
|
||||
// null/undefined 显示 "--"
|
||||
this.setData({
|
||||
displayValue: val == null || val === '' ? '--' : val,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
displayValue: '--',
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const val = this.data.value
|
||||
this.setData({
|
||||
displayValue: val == null || val === '' ? '--' : val,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap')
|
||||
},
|
||||
onHelpTap(e: WechatMiniprogram.TouchEvent) {
|
||||
// 阻止冒泡到卡片 tap
|
||||
e.stopPropagation?.()
|
||||
this.triggerEvent('helpTap')
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<view class="metric-card" bindtap="onTap">
|
||||
<view class="metric-header">
|
||||
<text class="metric-title">{{title}}</text>
|
||||
<view class="metric-help" wx:if="{{helpText}}" catchtap="onHelpTap">
|
||||
<t-icon name="help-circle" size="32rpx" color="#a6a6a6" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="metric-body">
|
||||
<text class="metric-value">{{displayValue}}</text>
|
||||
<text class="metric-unit" wx:if="{{unit}}">{{unit}}</text>
|
||||
</view>
|
||||
|
||||
<view class="metric-trend" wx:if="{{trendValue}}">
|
||||
<view class="trend-tag trend-{{trend}}">
|
||||
<t-icon wx:if="{{trend === 'up'}}" name="arrow-up" size="24rpx" />
|
||||
<t-icon wx:elif="{{trend === 'down'}}" name="arrow-down" size="24rpx" />
|
||||
<text wx:else class="trend-flat-icon">-</text>
|
||||
<text class="trend-text">{{trendValue}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,82 @@
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.metric-help {
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.metric-body {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
/* 上升 → 绿色 */
|
||||
.trend-up {
|
||||
color: var(--color-success);
|
||||
background: rgba(0, 168, 112, 0.08);
|
||||
}
|
||||
|
||||
/* 下降 → 红色 */
|
||||
.trend-down {
|
||||
color: var(--color-error);
|
||||
background: rgba(227, 77, 89, 0.08);
|
||||
}
|
||||
|
||||
/* 持平 → 灰色 */
|
||||
.trend-flat {
|
||||
color: var(--color-gray-7);
|
||||
background: rgba(139, 139, 139, 0.08);
|
||||
}
|
||||
|
||||
.trend-flat-icon {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.trend-text {
|
||||
font-size: var(--font-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-rate": "tdesign-miniprogram/rate/rate",
|
||||
"t-textarea": "tdesign-miniprogram/textarea/textarea",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 控制弹窗显示/隐藏 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
/** 客户名(弹窗标题显示) */
|
||||
customerName: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 初始评分 0-10 */
|
||||
initialScore: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
/** 初始备注内容 */
|
||||
initialContent: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
'visible, initialScore, initialContent'(visible: boolean) {
|
||||
if (visible) {
|
||||
const clamped = Math.max(0, Math.min(10, this.data.initialScore))
|
||||
this.setData({
|
||||
starValue: clamped / 2,
|
||||
content: this.data.initialContent,
|
||||
score: clamped,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
/** 星星值 0-5(半星制) */
|
||||
starValue: 0,
|
||||
/** 内部评分 0-10 */
|
||||
score: 0,
|
||||
/** 备注内容 */
|
||||
content: '',
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 星星评分变化 */
|
||||
onRateChange(e: WechatMiniprogram.CustomEvent<{ value: number }>) {
|
||||
const starVal = e.detail.value
|
||||
this.setData({
|
||||
starValue: starVal,
|
||||
score: starVal * 2,
|
||||
})
|
||||
},
|
||||
|
||||
/** 文本内容变化 */
|
||||
onContentChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ content: e.detail.value })
|
||||
},
|
||||
|
||||
/** 确认提交 */
|
||||
onConfirm() {
|
||||
if (!this.data.content.trim()) return
|
||||
this.triggerEvent('confirm', {
|
||||
score: this.data.score,
|
||||
content: this.data.content.trim(),
|
||||
})
|
||||
},
|
||||
|
||||
/** 取消关闭 */
|
||||
onCancel() {
|
||||
this.triggerEvent('cancel')
|
||||
},
|
||||
|
||||
/** 阻止冒泡 */
|
||||
noop() {},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
<view class="modal-mask" wx:if="{{visible}}" catchtap="onCancel">
|
||||
<view class="modal-content" catchtap="noop">
|
||||
<!-- 头部 -->
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">添加备注{{customerName ? ' - ' + customerName : ''}}</text>
|
||||
<view class="modal-close" bindtap="onCancel">
|
||||
<t-icon name="close" size="40rpx" color="#a6a6a6" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评分区域 -->
|
||||
<view class="rating-section">
|
||||
<text class="rating-label">评分</text>
|
||||
<t-rate
|
||||
value="{{starValue}}"
|
||||
count="{{5}}"
|
||||
allow-half="{{true}}"
|
||||
size="48rpx"
|
||||
bind:change="onRateChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<view class="textarea-section">
|
||||
<t-textarea
|
||||
value="{{content}}"
|
||||
placeholder="请输入备注内容..."
|
||||
maxlength="{{500}}"
|
||||
autosize="{{true}}"
|
||||
bind:change="onContentChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<t-button theme="default" size="large" block bindtap="onCancel">取消</t-button>
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="large"
|
||||
block
|
||||
disabled="{{!content.trim()}}"
|
||||
bindtap="onConfirm"
|
||||
>确认</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,68 @@
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
padding: 40rpx 40rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
|
||||
.rating-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.rating-label {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.textarea-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.modal-footer t-button {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-rate": "tdesign-miniprogram/rate/rate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 评分 0-10,内部转换为 0-5 星 */
|
||||
score: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
/** 星星尺寸 */
|
||||
size: {
|
||||
type: String,
|
||||
value: '40rpx',
|
||||
},
|
||||
/** 是否只读 */
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
score(val: number) {
|
||||
// score(0-10) → star(0-5)
|
||||
const clamped = Math.max(0, Math.min(10, val))
|
||||
this.setData({ starValue: clamped / 2 })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
starValue: 0,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const clamped = Math.max(0, Math.min(10, this.data.score))
|
||||
this.setData({ starValue: clamped / 2 })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
<t-rate
|
||||
value="{{starValue}}"
|
||||
count="{{5}}"
|
||||
allow-half
|
||||
size="{{size}}"
|
||||
disabled="{{readonly}}"
|
||||
/>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
8
apps/miniprogram/miniprogram/pages/apply/apply.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "申请访问权限",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
146
apps/miniprogram/miniprogram/pages/apply/apply.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { request } from "../../utils/request"
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
siteCode: "",
|
||||
role: "",
|
||||
phone: "",
|
||||
employeeNumber: "",
|
||||
nickname: "",
|
||||
submitting: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const { statusBarHeight } = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._checkAccess()
|
||||
},
|
||||
|
||||
/** 校验用户身份:无 token 跳登录,非 new/rejected 跳对应页 */
|
||||
async _checkAccess() {
|
||||
const token = wx.getStorageSync("token")
|
||||
if (!token) {
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/api/xcx/me",
|
||||
method: "GET",
|
||||
needAuth: true,
|
||||
})
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.status,
|
||||
nickname: data.nickname,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.status)
|
||||
|
||||
switch (data.status) {
|
||||
case "new":
|
||||
break
|
||||
case "rejected":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
case "approved":
|
||||
wx.reLaunch({ url: "/pages/mvp/mvp" })
|
||||
break
|
||||
case "pending":
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
break
|
||||
case "disabled":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// 网络错误不阻塞,允许用户继续填表
|
||||
}
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack({ fail: () => wx.reLaunch({ url: "/pages/login/login" }) })
|
||||
},
|
||||
|
||||
/* 原生 input 的 bindinput 事件 */
|
||||
onSiteCodeInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ siteCode: e.detail.value })
|
||||
},
|
||||
|
||||
onRoleInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ role: e.detail.value })
|
||||
},
|
||||
|
||||
onPhoneInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ phone: e.detail.value })
|
||||
},
|
||||
|
||||
onEmployeeNumberInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ employeeNumber: e.detail.value })
|
||||
},
|
||||
|
||||
onNicknameInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
async onSubmit() {
|
||||
if (this.data.submitting) return
|
||||
|
||||
const { siteCode, role, phone, nickname, employeeNumber } = this.data
|
||||
|
||||
if (!siteCode.trim()) {
|
||||
wx.showToast({ title: "请输入球房ID", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!role.trim()) {
|
||||
wx.showToast({ title: "请输入申请身份", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!/^\d{11}$/.test(phone)) {
|
||||
wx.showToast({ title: "请输入11位手机号", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!nickname.trim()) {
|
||||
wx.showToast({ title: "请输入昵称", icon: "none" })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ submitting: true })
|
||||
|
||||
try {
|
||||
await request({
|
||||
url: "/api/xcx/apply",
|
||||
method: "POST",
|
||||
data: {
|
||||
site_code: siteCode.trim(),
|
||||
applied_role_text: role.trim(),
|
||||
phone,
|
||||
employee_number: employeeNumber.trim() || undefined,
|
||||
nickname: nickname.trim(),
|
||||
},
|
||||
needAuth: true,
|
||||
})
|
||||
|
||||
wx.showToast({ title: "申请已提交", icon: "success" })
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
}, 800)
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
err?.data?.detail ||
|
||||
(err?.statusCode === 409
|
||||
? "您已有待审核的申请"
|
||||
: err?.statusCode === 422
|
||||
? "表单信息有误,请检查"
|
||||
: "提交失败,请稍后重试")
|
||||
wx.showToast({ title: msg, icon: "none" })
|
||||
} finally {
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
107
apps/miniprogram/miniprogram/pages/apply/apply.wxml
Normal file
@@ -0,0 +1,107 @@
|
||||
<!-- pages/apply/apply.wxml — 按 H5 原型结构迁移 -->
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="navbar">
|
||||
<view class="navbar-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="35rpx" color="#4b4b4b" />
|
||||
</view>
|
||||
<text class="navbar-title">申请访问权限</text>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="main">
|
||||
<!-- 欢迎卡片 + 审核流程(整合) -->
|
||||
<view class="welcome-card">
|
||||
<view class="welcome-header">
|
||||
<view class="welcome-icon-box">
|
||||
<t-icon name="check-circle-filled" size="42rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="welcome-text">
|
||||
<text class="welcome-title">欢迎加入球房运营助手</text>
|
||||
<text class="welcome-desc">请填写申请信息,等待管理员审核</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 审核流程步骤条 -->
|
||||
<view class="steps-bar">
|
||||
<view class="steps-row">
|
||||
<view class="step-item">
|
||||
<view class="step-circle step-circle--active">1</view>
|
||||
<text class="step-label step-label--active">提交申请</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">2</view>
|
||||
<text class="step-label">等待审核</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">3</view>
|
||||
<text class="step-label">审核通过</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">4</view>
|
||||
<text class="step-label">开始使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-card">
|
||||
<!-- 球房ID -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>球房ID</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入球房ID" value="{{siteCode}}" maxlength="5" bindinput="onSiteCodeInput" />
|
||||
</view>
|
||||
<!-- 申请身份 -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>申请身份</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入申请身份(如:助教、店长等)" value="{{role}}" bindinput="onRoleInput" />
|
||||
</view>
|
||||
<!-- 手机号 -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>手机号</text>
|
||||
</view>
|
||||
<input class="form-input" type="number" placeholder="请输入手机号" value="{{phone}}" maxlength="11" bindinput="onPhoneInput" />
|
||||
</view>
|
||||
<!-- 编号(选填) -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text>编号</text>
|
||||
<text class="optional-tag">选填</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入编号" value="{{employeeNumber}}" maxlength="50" bindinput="onEmployeeNumberInput" />
|
||||
</view>
|
||||
<!-- 昵称 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>昵称</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入昵称" value="{{nickname}}" maxlength="50" bindinput="onNicknameInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-area">
|
||||
<text class="form-tip">请认真填写,信息不完整可能导致审核不通过</text>
|
||||
<view class="submit-btn {{submitting ? 'submit-btn--disabled' : ''}}" bindtap="onSubmit">
|
||||
<t-loading wx:if="{{submitting}}" theme="circular" size="32rpx" color="#fff" />
|
||||
<text wx:else class="submit-btn-text">提交申请</text>
|
||||
</view>
|
||||
<text class="bottom-tip">审核通常需要 1-3 个工作日</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
271
apps/miniprogram/miniprogram/pages/apply/apply.wxss
Normal file
@@ -0,0 +1,271 @@
|
||||
/* pages/apply/apply.wxss — H5 px × 2 × 0.875 精确转换 */
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 50%, #ecfeff 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---- 顶部导航栏 h-11=44px→78rpx ---- */
|
||||
.navbar {
|
||||
height: 78rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-bottom: 1rpx solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
.navbar-back {
|
||||
position: absolute;
|
||||
left: 28rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.navbar-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* ---- 主体内容 p-4=16px→28rpx ---- */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 28rpx;
|
||||
padding-bottom: 380rpx;
|
||||
}
|
||||
|
||||
/* ---- 欢迎卡片 p-5=20px→36rpx, rounded-2xl=16px→28rpx ---- */
|
||||
.welcome-card {
|
||||
background: linear-gradient(135deg, #0052d9, #60a5fa);
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
margin-bottom: 28rpx;
|
||||
box-shadow: 0 14rpx 36rpx rgba(0, 82, 217, 0.2);
|
||||
}
|
||||
|
||||
/* gap-4=16px→28rpx, mb-4=16px→28rpx */
|
||||
.welcome-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
/* w-12 h-12=48px→84rpx, rounded-xl=12px→22rpx */
|
||||
.welcome-icon-box {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
min-width: 84rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
/* text-lg=18px→32rpx */
|
||||
.welcome-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx */
|
||||
.welcome-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 审核流程步骤条 p-4=16px→28rpx, rounded-xl=12px→22rpx ---- */
|
||||
.steps-bar {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 22rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.steps-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
/* w-7 h-7=28px→50rpx, text-xs=12px→22rpx */
|
||||
.step-circle {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.step-circle--active {
|
||||
background: #ffffff;
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx */
|
||||
.step-label {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-label--active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* h-0.5=2px→4rpx, mx-2=8px→14rpx */
|
||||
.step-line {
|
||||
flex: 1;
|
||||
height: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 10rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* ---- 表单卡片 rounded-2xl=16px→28rpx ---- */
|
||||
.form-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* px-5=20px→36rpx, py-4=16px→28rpx+2rpx 视觉补偿 */
|
||||
.form-item {
|
||||
padding: 30rpx 36rpx;
|
||||
}
|
||||
|
||||
.form-item--border {
|
||||
border-bottom: 2rpx solid #f3f3f3;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx, mb-2=8px→14rpx */
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 14rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx */
|
||||
.required {
|
||||
color: #e34d59;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx */
|
||||
.optional-tag {
|
||||
font-size: 20rpx;
|
||||
color: #a6a6a6;
|
||||
font-weight: 400;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* px-4=16px→28rpx, py-3=12px→22rpx, rounded-xl=12px→22rpx, text-sm=14px→24rpx
|
||||
小程序 input 组件内部有压缩,py 加 4rpx 补偿到视觉等高 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 28rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 22rpx;
|
||||
border: 2rpx solid #f3f3f3;
|
||||
font-size: 24rpx;
|
||||
font-weight: 300;
|
||||
color: #242424;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #c5c5c5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 表单提示(移入底部固定区) ---- */
|
||||
.form-tip {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20rpx;
|
||||
color: #a6a6a6;
|
||||
margin-bottom: 18rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 底部提交 p-4=16px→28rpx, pb-8=32px→56rpx ---- */
|
||||
.bottom-area {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28rpx;
|
||||
padding-bottom: calc(56rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-top: 2rpx solid #f3f3f3;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* py-4=16px→28rpx (用padding代替固定高度), rounded-xl=12px→22rpx, text-base=16px→28rpx */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 28rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
border-radius: 22rpx;
|
||||
box-shadow: 0 10rpx 28rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.submit-btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx, mt-3=12px→22rpx */
|
||||
.bottom-tip {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
margin-top: 22rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "助教看板",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
263
apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
// 助教看板页 — 排序×技能×时间三重筛选,4 种维度卡片
|
||||
// TODO: 联调时替换 mock 数据为真实 API 调用
|
||||
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> = {
|
||||
'星级': '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
|
||||
levelClass: string
|
||||
skills: Array<{ text: string; cls: string }>
|
||||
topCustomers: string[]
|
||||
// 定档业绩维度
|
||||
perfHours: string
|
||||
perfHoursBefore?: string
|
||||
perfGap?: string
|
||||
perfReached: boolean
|
||||
// 工资维度
|
||||
salary: string
|
||||
salaryPerfHours: string
|
||||
salaryPerfBefore?: string
|
||||
// 客源储值维度
|
||||
svAmount: string
|
||||
svCustomerCount: string
|
||||
svConsume: string
|
||||
// 任务维度
|
||||
taskRecall: string
|
||||
taskCallback: string
|
||||
}
|
||||
|
||||
/** Mock 数据(忠于 H5 原型 6 位助教) */
|
||||
const MOCK_COACHES: CoachItem[] = [
|
||||
{
|
||||
id: 'c1', name: '小燕', initial: '小',
|
||||
avatarGradient: 'avatar--blue',
|
||||
level: '星级', levelClass: LEVEL_CLASS['星级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
|
||||
topCustomers: ['💖 王先生', '💖 李女士', '💛 赵总'],
|
||||
perfHours: '86.2h', perfHoursBefore: '92.0h', perfGap: '距升档 13.8h', perfReached: false,
|
||||
salary: '¥12,680', salaryPerfHours: '86.2h', salaryPerfBefore: '92.0h',
|
||||
svAmount: '¥45,200', svCustomerCount: '18', svConsume: '¥8,600',
|
||||
taskRecall: '18', taskCallback: '14',
|
||||
},
|
||||
{
|
||||
id: 'c2', name: '泡芙', initial: '泡',
|
||||
avatarGradient: 'avatar--green',
|
||||
level: '高级', levelClass: LEVEL_CLASS['高级'],
|
||||
skills: [{ text: '斯', cls: SKILL_CLASS['斯'] }],
|
||||
topCustomers: ['💖 陈先生', '💛 刘女士', '💛 黄总'],
|
||||
perfHours: '72.5h', perfHoursBefore: '78.0h', perfGap: '距升档 7.5h', perfReached: false,
|
||||
salary: '¥10,200', salaryPerfHours: '72.5h', salaryPerfBefore: '78.0h',
|
||||
svAmount: '¥38,600', svCustomerCount: '15', svConsume: '¥6,200',
|
||||
taskRecall: '15', taskCallback: '13',
|
||||
},
|
||||
{
|
||||
id: 'c3', name: 'Amy', initial: 'A',
|
||||
avatarGradient: 'avatar--pink',
|
||||
level: '星级', levelClass: LEVEL_CLASS['星级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }, { text: '斯', cls: SKILL_CLASS['斯'] }],
|
||||
topCustomers: ['💖 张先生', '💛 周女士', '💛 吴总'],
|
||||
perfHours: '68.0h', perfHoursBefore: '72.5h', perfGap: '距升档 32.0h', perfReached: false,
|
||||
salary: '¥9,800', salaryPerfHours: '68.0h', salaryPerfBefore: '72.5h',
|
||||
svAmount: '¥32,100', svCustomerCount: '14', svConsume: '¥5,800',
|
||||
taskRecall: '12', taskCallback: '13',
|
||||
},
|
||||
{
|
||||
id: 'c4', name: 'Mia', initial: 'M',
|
||||
avatarGradient: 'avatar--amber',
|
||||
level: '中级', levelClass: LEVEL_CLASS['中级'],
|
||||
skills: [{ text: '🀄', cls: SKILL_CLASS['🀄'] }],
|
||||
topCustomers: ['💛 赵先生', '💛 吴女士', '💛 孙总'],
|
||||
perfHours: '55.0h', perfGap: '距升档 5.0h', perfReached: false,
|
||||
salary: '¥7,500', salaryPerfHours: '55.0h',
|
||||
svAmount: '¥28,500', svCustomerCount: '12', svConsume: '¥4,100',
|
||||
taskRecall: '10', taskCallback: '10',
|
||||
},
|
||||
{
|
||||
id: 'c5', name: '糖糖', initial: '糖',
|
||||
avatarGradient: 'avatar--purple',
|
||||
level: '初级', levelClass: LEVEL_CLASS['初级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
|
||||
topCustomers: ['💛 钱先生', '💛 孙女士', '💛 周总'],
|
||||
perfHours: '42.0h', perfHoursBefore: '45.0h', perfReached: true,
|
||||
salary: '¥6,200', salaryPerfHours: '42.0h', salaryPerfBefore: '45.0h',
|
||||
svAmount: '¥22,000', svCustomerCount: '10', svConsume: '¥3,500',
|
||||
taskRecall: '8', taskCallback: '10',
|
||||
},
|
||||
{
|
||||
id: 'c6', name: '露露', initial: '露',
|
||||
avatarGradient: 'avatar--cyan',
|
||||
level: '中级', levelClass: LEVEL_CLASS['中级'],
|
||||
skills: [{ text: '🎤', cls: SKILL_CLASS['🎤'] }],
|
||||
topCustomers: ['💛 郑先生', '💛 冯女士', '💛 陈总'],
|
||||
perfHours: '38.0h', perfGap: '距升档 22.0h', perfReached: false,
|
||||
salary: '¥5,100', salaryPerfHours: '38.0h',
|
||||
svAmount: '¥18,300', svCustomerCount: '9', svConsume: '¥2,800',
|
||||
taskRecall: '6', taskCallback: '9',
|
||||
},
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
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
|
||||
}
|
||||
this.setData({ allCoaches: data, coaches: data, 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 })
|
||||
},
|
||||
|
||||
onCoachTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string
|
||||
wx.navigateTo({ url: '/pages/coach-detail/coach-detail?id=' + id })
|
||||
},
|
||||
})
|
||||
153
apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml
Normal file
@@ -0,0 +1,153 @@
|
||||
<!-- 助教看板页 — 忠于 H5 原型结构 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<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" 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"
|
||||
>
|
||||
<view class="card-row">
|
||||
<!-- 头像 -->
|
||||
<view class="card-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>
|
||||
<text class="level-tag {{item.levelClass}}">{{item.level}}</text>
|
||||
<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.perfHours}}</text></text>
|
||||
<text class="right-sub" wx:if="{{item.perfHoursBefore}}">折前 <text class="right-sub-val">{{item.perfHoursBefore}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 工资维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'salary'}}">
|
||||
<text class="salary-tag">预估</text>
|
||||
<text class="salary-amount">{{item.salary}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 客源储值维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'sv'}}">
|
||||
<text class="right-sub">储值</text>
|
||||
<text class="salary-amount">{{item.svAmount}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'task'}}">
|
||||
<text class="right-text">召回 <text class="right-highlight">{{item.taskRecall}}</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.salaryPerfHours}}</text></text>
|
||||
<text class="bottom-sub" wx:if="{{item.salaryPerfBefore}}">折前 <text class="bottom-sub-val">{{item.salaryPerfBefore}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 客源储值:客户数 | 消耗 -->
|
||||
<view class="bottom-right-group" wx:elif="{{dimType === 'sv'}}">
|
||||
<text class="bottom-sub">客户 <text class="bottom-perf-val">{{item.svCustomerCount}}</text>人</text>
|
||||
<text class="bottom-divider">|</text>
|
||||
<text class="bottom-sub">消耗 <text class="bottom-perf-val">{{item.svConsume}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 任务:回访数 -->
|
||||
<text class="bottom-sub" wx:elif="{{dimType === 'task'}}">回访 <text class="bottom-perf-val">{{item.taskCallback}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区(为自定义导航栏留空间) -->
|
||||
<view class="safe-bottom"></view>
|
||||
</block>
|
||||
|
||||
<!-- 自定义底部导航栏 -->
|
||||
<board-tab-bar active="board" />
|
||||
|
||||
<!-- AI 悬浮按钮 — 在导航栏上方 -->
|
||||
<ai-float-button bottom="{{220}}" />
|
||||
|
||||
<dev-fab />
|
||||
330
apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxss
Normal file
@@ -0,0 +1,330 @@
|
||||
/* 助教看板页 — H5 px × 2 × 0.875 精确转换 */
|
||||
|
||||
/* ===== 三态 ===== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
/* ===== 看板 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* w-24px→42rpx, h-3px→5rpx(H5 实际渲染偏细) */
|
||||
.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 ===== */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 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: 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;
|
||||
}
|
||||
|
||||
/* ===== 助教列表 p-4=16px→28rpx, space-y-3=12px→20rpx ===== */
|
||||
.coach-list {
|
||||
padding: 24rpx 28rpx;
|
||||
}
|
||||
|
||||
/* ===== 助教卡片 p-4=16px→28rpx, rounded-2xl=16px→28rpx ===== */
|
||||
.coach-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 30rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.coach-card:active {
|
||||
opacity: 0.96;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* gap-3=12px→20rpx(视觉校准紧凑) */
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* ===== 头像 w-11 h-11=44px→78rpx, text-base=16px→28rpx ===== */
|
||||
.card-avatar {
|
||||
width: 78rpx;
|
||||
height: 78rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 头像渐变色(忠于 H5 原型 6 种) */
|
||||
.avatar--blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
|
||||
.avatar--green { background: linear-gradient(135deg, #4ade80, #10b981); }
|
||||
.avatar--pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
|
||||
.avatar--amber { background: linear-gradient(135deg, #fbbf24, #f97316); }
|
||||
.avatar--purple { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
||||
.avatar--cyan { background: linear-gradient(135deg, #22d3ee, #14b8a6); }
|
||||
|
||||
/* ===== 信息区 ===== */
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* gap-1.5=6px→10rpx */
|
||||
.card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.card-name {
|
||||
font-size: 28rpx;
|
||||
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: 6rpx;
|
||||
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: 6rpx;
|
||||
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 推到右边) ===== */
|
||||
.card-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
/* "折前"更淡更细 */
|
||||
.right-sub {
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
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 — 储值维度缩小避免挤压客户列表 */
|
||||
.salary-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
/* ===== 第二行 mt-1.5=6px→12rpx, text-xs=12px→22rpx ===== */
|
||||
.card-bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* gap-2=8px→12rpx */
|
||||
.customer-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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-10,heart-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 })
|
||||
},
|
||||
})
|
||||
@@ -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 />
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"navigationBarTitleText": "看板",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"metric-card": "/components/metric-card/metric-card",
|
||||
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// 财务看板页 — 忠于 H5 原型结构,内联 mock 数据
|
||||
// TODO: 联调时替换 mock 数据为真实 API 调用
|
||||
|
||||
/** 目录板块定义 */
|
||||
interface TocItem {
|
||||
emoji: string
|
||||
title: string
|
||||
sectionId: string
|
||||
}
|
||||
|
||||
/** 指标解释映射 */
|
||||
const tipContents: Record<string, { title: string; content: string }> = {
|
||||
occurrence: {
|
||||
title: '发生额/正价',
|
||||
content: '所有消费项目按标价计算的总金额,不扣除任何优惠。\n\n即"如果没有任何折扣,客户应付多少"。',
|
||||
},
|
||||
discount: {
|
||||
title: '总优惠',
|
||||
content: '包含会员折扣、赠送卡抵扣、团购差价等所有优惠金额。\n\n优惠越高,实际收入越低。',
|
||||
},
|
||||
confirmed: {
|
||||
title: '成交/确认收入',
|
||||
content: '发生额减去总优惠后的实际入账金额。\n\n成交收入 = 发生额 - 总优惠',
|
||||
},
|
||||
cashIn: {
|
||||
title: '实收/现金流入',
|
||||
content: '实际到账的现金,包含消费直接支付和储值充值。\n\n往期为已结算金额,本期为截至当前的发生额。',
|
||||
},
|
||||
cashOut: {
|
||||
title: '现金支出',
|
||||
content: '包含人工、房租、水电、进货等所有经营支出。',
|
||||
},
|
||||
balance: {
|
||||
title: '现金结余',
|
||||
content: '现金流入减去现金支出。\n\n现金结余 = 现金流入 - 现金支出',
|
||||
},
|
||||
rechargeActual: {
|
||||
title: '储值卡充值实收',
|
||||
content: '会员储值卡首充和续费的实际到账金额。\n\n不含赠送金额。',
|
||||
},
|
||||
firstCharge: {
|
||||
title: '首充',
|
||||
content: '新会员首次充值的金额。',
|
||||
},
|
||||
renewCharge: {
|
||||
title: '续费',
|
||||
content: '老会员续费充值的金额。',
|
||||
},
|
||||
consume: {
|
||||
title: '消耗',
|
||||
content: '会员使用储值卡消费的金额。',
|
||||
},
|
||||
cardBalance: {
|
||||
title: '储值卡总余额',
|
||||
content: '所有储值卡的剩余可用余额。',
|
||||
},
|
||||
allCardBalance: {
|
||||
title: '全类别会员卡余额合计',
|
||||
content: '储值卡 + 赠送卡(酒水卡、台费卡、抵用券)的总余额。\n\n仅供经营参考,非财务属性。',
|
||||
},
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'normal' as 'loading' | 'empty' | 'normal',
|
||||
|
||||
/** 时间筛选 */
|
||||
selectedTime: 'month',
|
||||
timeOptions: [
|
||||
{ value: 'month', text: '本月' },
|
||||
{ value: 'lastMonth', text: '上月' },
|
||||
{ value: 'week', text: '本周' },
|
||||
{ value: 'lastWeek', text: '上周' },
|
||||
{ value: 'quarter3', text: '前3个月 不含本月' },
|
||||
{ value: 'quarter', text: '本季度' },
|
||||
{ value: 'lastQuarter', text: '上季度' },
|
||||
{ value: 'half6', text: '最近6个月不含本月' },
|
||||
],
|
||||
|
||||
/** 区域筛选 */
|
||||
selectedArea: 'all',
|
||||
areaOptions: [
|
||||
{ value: 'all', text: '全部区域' },
|
||||
{ value: 'hall', text: '大厅' },
|
||||
{ value: 'hallA', text: 'A区' },
|
||||
{ value: 'hallB', text: 'B区' },
|
||||
{ value: 'hallC', text: 'C区' },
|
||||
{ value: 'mahjong', text: '麻将房' },
|
||||
{ value: 'teamBuilding', text: '团建房' },
|
||||
],
|
||||
|
||||
/** 环比开关 */
|
||||
compareEnabled: false,
|
||||
|
||||
/** 目录导航 */
|
||||
tocVisible: false,
|
||||
tocItems: [
|
||||
{ emoji: '📈', title: '经营一览', sectionId: 'section-overview' },
|
||||
{ emoji: '💳', title: '预收资产', sectionId: 'section-recharge' },
|
||||
{ emoji: '💰', title: '应计收入确认', sectionId: 'section-revenue' },
|
||||
{ emoji: '🧾', title: '现金流入', sectionId: 'section-cashflow' },
|
||||
{ emoji: '📤', title: '现金流出', sectionId: 'section-expense' },
|
||||
{ emoji: '🎱', title: '助教分析', sectionId: 'section-coach' },
|
||||
] as TocItem[],
|
||||
currentSectionIndex: 0,
|
||||
scrollIntoView: '',
|
||||
|
||||
/** 提示弹窗 */
|
||||
tipVisible: false,
|
||||
tipTitle: '',
|
||||
tipContent: '',
|
||||
|
||||
/** 经营一览 */
|
||||
overview: {
|
||||
occurrence: '¥823,456',
|
||||
occurrenceCompare: '12.5%',
|
||||
discount: '-¥113,336',
|
||||
discountCompare: '3.2%',
|
||||
discountRate: '13.8%',
|
||||
discountRateCompare: '1.5%',
|
||||
confirmedRevenue: '¥710,120',
|
||||
confirmedCompare: '8.7%',
|
||||
cashIn: '¥698,500',
|
||||
cashInCompare: '5.3%',
|
||||
cashOut: '¥472,300',
|
||||
cashOutCompare: '2.1%',
|
||||
cashBalance: '¥226,200',
|
||||
cashBalanceCompare: '15.2%',
|
||||
balanceRate: '32.4%',
|
||||
balanceRateCompare: '3.8%',
|
||||
},
|
||||
|
||||
/** 预收资产 */
|
||||
recharge: {
|
||||
actualIncome: '¥352,800',
|
||||
actualCompare: '18.5%',
|
||||
firstCharge: '¥188,500',
|
||||
firstChargeCompare: '12.3%',
|
||||
renewCharge: '¥164,300',
|
||||
renewChargeCompare: '8.7%',
|
||||
consumed: '¥238,200',
|
||||
consumedCompare: '5.2%',
|
||||
cardBalance: '¥642,600',
|
||||
cardBalanceCompare: '11.4%',
|
||||
giftRows: [
|
||||
{
|
||||
label: '新增', total: '¥108,600', totalCompare: '9.8%',
|
||||
wine: '¥43,200', wineCompare: '11.2%',
|
||||
table: '¥54,100', tableCompare: '8.5%',
|
||||
coupon: '¥11,300', couponCompare: '6.3%',
|
||||
},
|
||||
{
|
||||
label: '消费', total: '¥75,800', totalCompare: '7.2%',
|
||||
wine: '¥32,100', wineCompare: '8.1%',
|
||||
table: '¥32,800', tableCompare: '6.5%',
|
||||
coupon: '¥10,900', couponCompare: '5.8%',
|
||||
},
|
||||
{
|
||||
label: '余额', total: '¥243,900', totalCompare: '4.5%',
|
||||
wine: '¥118,500', wineCompare: '5.2%',
|
||||
table: '¥109,200', tableCompare: '3.8%',
|
||||
coupon: '¥16,200', couponCompare: '2.5%',
|
||||
},
|
||||
],
|
||||
allCardBalance: '¥586,500',
|
||||
allCardBalanceCompare: '6.2%',
|
||||
},
|
||||
|
||||
/** 应计收入确认 */
|
||||
revenue: {
|
||||
structureRows: [
|
||||
{ name: '开台与包厢', amount: '¥358,600', discount: '-¥45,200', booked: '¥313,400', bookedCompare: '9.2%' },
|
||||
{ name: 'A区', amount: '¥118,200', discount: '-¥11,600', booked: '¥106,600', bookedCompare: '12.1%', isSub: true },
|
||||
{ name: 'B区', amount: '¥95,800', discount: '-¥11,200', booked: '¥84,600', bookedCompare: '8.5%', isSub: true },
|
||||
{ name: 'C区', amount: '¥72,600', discount: '-¥11,100', booked: '¥61,500', bookedCompare: '6.3%', isSub: true },
|
||||
{ name: '团建区', amount: '¥48,200', discount: '-¥6,800', booked: '¥41,400', bookedCompare: '5.8%', isSub: true },
|
||||
{ name: '麻将区', amount: '¥23,800', discount: '-¥4,500', booked: '¥19,300', bookedCompare: '-2.1%', isSub: true },
|
||||
{ name: '助教', desc: '基础课', amount: '¥232,500', discount: '-', booked: '¥232,500', bookedCompare: '15.3%' },
|
||||
{ name: '助教', desc: '激励课', amount: '¥112,800', discount: '-', booked: '¥112,800', bookedCompare: '8.2%' },
|
||||
{ name: '食品酒水', amount: '¥119,556', discount: '-¥68,136', booked: '¥51,420', bookedCompare: '6.5%' },
|
||||
],
|
||||
priceItems: [
|
||||
{ name: '开台消费', value: '¥358,600', compare: '9.2%' },
|
||||
{ name: '酒水商品', value: '¥186,420', compare: '18.5%' },
|
||||
{ name: '包厢费用', value: '¥165,636', compare: '12.1%' },
|
||||
{ name: '助教服务', value: '¥112,800', compare: '15.3%' },
|
||||
],
|
||||
totalOccurrence: '¥823,456',
|
||||
totalOccurrenceCompare: '12.5%',
|
||||
discountItems: [
|
||||
{ name: '会员折扣', value: '-¥45,200', compare: '3.1%' },
|
||||
{ name: '赠送卡抵扣', value: '-¥42,016', compare: '2.5%' },
|
||||
{ name: '团购差价', value: '-¥26,120', compare: '5.2%' },
|
||||
],
|
||||
confirmedTotal: '¥710,120',
|
||||
confirmedTotalCompare: '8.7%',
|
||||
channelItems: [
|
||||
{ name: '储值卡结算冲销', value: '¥238,200', compare: '11.2%' },
|
||||
{ name: '现金/线上支付', value: '¥345,800', compare: '7.8%' },
|
||||
{ name: '团购核销确认收入', desc: '团购成交价', value: '¥126,120', compare: '5.3%' },
|
||||
],
|
||||
},
|
||||
|
||||
/** 现金流入 */
|
||||
cashflow: {
|
||||
consumeItems: [
|
||||
{ name: '纸币现金', desc: '柜台现金收款', value: '¥85,600', compare: '12.3%', isDown: true },
|
||||
{ name: '线上收款', desc: '微信/支付宝/刷卡 已扣除平台服务费', value: '¥260,200', compare: '8.5%', isDown: false },
|
||||
{ name: '团购平台', desc: '美团/抖音回款 已扣除平台服务费', value: '¥126,120', compare: '15.2%', isDown: false },
|
||||
],
|
||||
rechargeItems: [
|
||||
{ name: '会员充值到账', desc: '首充/续费实收', value: '¥352,800', compare: '18.5%' },
|
||||
],
|
||||
total: '¥824,720',
|
||||
totalCompare: '10.2%',
|
||||
},
|
||||
|
||||
/** 现金流出 */
|
||||
expense: {
|
||||
operationItems: [
|
||||
{ name: '食品饮料', value: '¥108,200', compare: '4.5%', isDown: false },
|
||||
{ name: '耗材', value: '¥21,850', compare: '2.1%', isDown: true },
|
||||
{ name: '报销', value: '¥10,920', compare: '6.8%', isDown: false },
|
||||
],
|
||||
fixedItems: [
|
||||
{ name: '房租', value: '¥125,000', compare: '持平', isFlat: true },
|
||||
{ name: '水电', value: '¥24,200', compare: '3.2%', isFlat: false },
|
||||
{ name: '物业', value: '¥11,500', compare: '持平', isFlat: true },
|
||||
{ name: '人员工资', value: '¥112,000', compare: '持平', isFlat: true },
|
||||
],
|
||||
coachItems: [
|
||||
{ name: '基础课分成', value: '¥116,250', compare: '8.2%', isDown: false },
|
||||
{ name: '激励课分成', value: '¥23,840', compare: '5.6%', isDown: false },
|
||||
{ name: '充值提成', value: '¥12,640', compare: '12.3%', isDown: false },
|
||||
{ name: '额外奖金', value: '¥11,500', compare: '3.1%', isDown: true },
|
||||
],
|
||||
platformItems: [
|
||||
{ name: '汇来米', value: '¥10,680', compare: '1.5%' },
|
||||
{ name: '美团', value: '¥11,240', compare: '2.8%' },
|
||||
{ name: '抖音', value: '¥10,580', compare: '3.5%' },
|
||||
],
|
||||
total: '¥600,400',
|
||||
totalCompare: '2.1%',
|
||||
},
|
||||
|
||||
/** 助教分析 */
|
||||
coachAnalysis: {
|
||||
basic: {
|
||||
totalPay: '¥232,500',
|
||||
totalPayCompare: '15.3%',
|
||||
totalShare: '¥116,250',
|
||||
totalShareCompare: '15.3%',
|
||||
avgHourly: '¥25/h',
|
||||
avgHourlyCompare: '4.2%',
|
||||
rows: [
|
||||
{ level: '初级', pay: '¥68,600', payCompare: '12.5%', share: '¥34,300', shareCompare: '12.5%', hourly: '¥20/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
{ level: '中级', pay: '¥82,400', payCompare: '18.2%', share: '¥41,200', shareCompare: '18.2%', hourly: '¥25/h', hourlyCompare: '8.7%' },
|
||||
{ level: '高级', pay: '¥57,800', payCompare: '14.6%', share: '¥28,900', shareCompare: '14.6%', hourly: '¥30/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
{ level: '星级', pay: '¥23,700', payCompare: '3.2%', payDown: true, share: '¥11,850', shareCompare: '3.2%', shareDown: true, hourly: '¥35/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
],
|
||||
},
|
||||
incentive: {
|
||||
totalPay: '¥112,800',
|
||||
totalPayCompare: '8.2%',
|
||||
totalShare: '¥33,840',
|
||||
totalShareCompare: '8.2%',
|
||||
avgHourly: '¥15/h',
|
||||
avgHourlyCompare: '2.1%',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// mock 数据已内联,直接显示
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 500)
|
||||
},
|
||||
|
||||
/** 看板 Tab 切换 */
|
||||
onTabChange(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === 'customer') {
|
||||
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
|
||||
} else if (tab === 'coach') {
|
||||
wx.navigateTo({ url: '/pages/board-coach/board-coach' })
|
||||
}
|
||||
},
|
||||
|
||||
/** 时间筛选变更 */
|
||||
onTimeChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedTime: e.detail.value })
|
||||
},
|
||||
|
||||
/** 区域筛选变更 */
|
||||
onAreaChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedArea: e.detail.value })
|
||||
},
|
||||
|
||||
/** 环比开关切换 */
|
||||
toggleCompare() {
|
||||
this.setData({ compareEnabled: !this.data.compareEnabled })
|
||||
},
|
||||
|
||||
/** 目录导航开关 */
|
||||
toggleToc() {
|
||||
this.setData({ tocVisible: !this.data.tocVisible })
|
||||
},
|
||||
|
||||
closeToc() {
|
||||
this.setData({ tocVisible: false })
|
||||
},
|
||||
|
||||
/** 目录项点击 → 滚动到对应板块 */
|
||||
onTocItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = e.currentTarget.dataset.index as number
|
||||
const sectionId = this.data.tocItems[index]?.sectionId
|
||||
if (sectionId) {
|
||||
this.setData({
|
||||
tocVisible: false,
|
||||
currentSectionIndex: index,
|
||||
scrollIntoView: sectionId,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 帮助图标点击 → 弹出说明 */
|
||||
onHelpTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const key = e.currentTarget.dataset.key as string
|
||||
const tip = tipContents[key]
|
||||
if (tip) {
|
||||
this.setData({
|
||||
tipVisible: true,
|
||||
tipTitle: tip.title,
|
||||
tipContent: tip.content,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 关闭提示弹窗 */
|
||||
closeTip() {
|
||||
this.setData({ tipVisible: false })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,734 @@
|
||||
<!-- 财务看板页 — 忠于 H5 原型结构 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" 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 board-tab--active" data-tab="finance">
|
||||
<text>财务</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="customer" bindtap="onTabChange">
|
||||
<text>客户</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="coach" bindtap="onTabChange">
|
||||
<text>助教</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-bar">
|
||||
<view class="filter-bar-inner">
|
||||
<!-- 目录按钮 -->
|
||||
<view class="toc-btn" bindtap="toggleToc">
|
||||
<t-icon name="view-list" size="40rpx" color="#ffffff" />
|
||||
</view>
|
||||
|
||||
<!-- 时间筛选 -->
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="本月"
|
||||
options="{{timeOptions}}"
|
||||
value="{{selectedTime}}"
|
||||
bind:change="onTimeChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 区域筛选 -->
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="全部区域"
|
||||
options="{{areaOptions}}"
|
||||
value="{{selectedArea}}"
|
||||
bind:change="onAreaChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 环比开关 -->
|
||||
<view class="compare-switch" bindtap="toggleCompare">
|
||||
<text class="compare-label">环比</text>
|
||||
<view class="compare-toggle {{compareEnabled ? 'compare-toggle--active' : ''}}">
|
||||
<view class="compare-toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 滚动内容区 -->
|
||||
<scroll-view
|
||||
class="board-content"
|
||||
scroll-y
|
||||
scroll-into-view="{{scrollIntoView}}"
|
||||
scroll-with-animation
|
||||
>
|
||||
|
||||
<!-- ===== 板块 1: 经营一览(深色) ===== -->
|
||||
<view id="section-overview" class="card-section section-dark">
|
||||
<view class="card-header-dark">
|
||||
<text class="card-header-emoji">📈</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-dark">经营一览</text>
|
||||
<text class="card-header-desc-dark">快速了解收入与现金流的整体健康度</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收入概览 -->
|
||||
<view class="sub-section-label">
|
||||
<text class="sub-label-text">收入概览</text>
|
||||
<text class="sub-label-desc">记账口径收入与优惠</text>
|
||||
</view>
|
||||
|
||||
<view class="overview-grid-3">
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">发生额/正价</text>
|
||||
<view class="help-icon-light" data-key="occurrence" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white">{{overview.occurrence}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.occurrenceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">总优惠</text>
|
||||
<view class="help-icon-light" data-key="discount" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-red">{{overview.discount}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down">↓{{overview.discountCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">优惠占比</text>
|
||||
</view>
|
||||
<text class="cell-value-gray">{{overview.discountRate}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down">↓{{overview.discountRateCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成交/确认收入 -->
|
||||
<view class="confirmed-row">
|
||||
<view class="confirmed-left">
|
||||
<text class="confirmed-label">成交/确认收入</text>
|
||||
<view class="help-icon-light" data-key="confirmed" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="confirmed-right">
|
||||
<text class="confirmed-value">{{overview.confirmedRevenue}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.confirmedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-divider-light"></view>
|
||||
|
||||
<!-- 现金流水概览 -->
|
||||
<view class="sub-section-label">
|
||||
<text class="sub-label-text">现金流水概览</text>
|
||||
<text class="sub-label-desc">往期为已结算 本期为截至当前的发生额</text>
|
||||
</view>
|
||||
|
||||
<view class="overview-grid-2">
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">实收/现金流入</text>
|
||||
<view class="help-icon-light" data-key="cashIn" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.cashIn}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashInCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">现金支出</text>
|
||||
<view class="help-icon-light" data-key="cashOut" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-gray-sm">{{overview.cashOut}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashOutCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">现金结余</text>
|
||||
<view class="help-icon-light" data-key="balance" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.cashBalance}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">结余率</text>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.balanceRate}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.balanceRateCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 洞察 -->
|
||||
<view class="ai-insight-section">
|
||||
<view class="ai-insight-header">
|
||||
<view class="ai-insight-icon">🤖</view>
|
||||
<text class="ai-insight-title">AI 智能洞察</text>
|
||||
</view>
|
||||
<view class="ai-insight-body">
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">优惠率Top:</text>团购(5.2%) / 大客户(3.1%) / 赠送卡(2.5%)</text>
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">差异最大:</text>酒水(+18%) / 台桌(-5%) / 包厢(+12%)</text>
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">建议关注:</text>充值高但消耗低,会员活跃度需提升</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 2: 预收资产 ===== -->
|
||||
<view id="section-recharge" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">💳</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">预收资产</text>
|
||||
<text class="card-header-desc-light">会员卡充值与余额 掌握资金沉淀</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 储值卡统计 -->
|
||||
<view class="section-body">
|
||||
<text class="card-section-title">储值卡统计</text>
|
||||
<view class="table-bordered">
|
||||
<!-- 行1:储值卡充值实收 -->
|
||||
<view class="table-row table-row--highlight">
|
||||
<view class="table-row-left">
|
||||
<text class="table-row-label-bold">储值卡充值实收</text>
|
||||
<view class="help-icon-dark" data-key="rechargeActual" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="table-row-right">
|
||||
<text class="table-row-value-lg">{{recharge.actualIncome}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.actualCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 行2:首充/续费/消耗 三列 -->
|
||||
<view class="table-row-grid3">
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">首充</text>
|
||||
<view class="help-icon-dark-sm" data-key="firstCharge" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.firstCharge}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.firstChargeCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">续费</text>
|
||||
<view class="help-icon-dark-sm" data-key="renewCharge" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.renewCharge}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.renewChargeCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">消耗</text>
|
||||
<view class="help-icon-dark-sm" data-key="consume" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.consumed}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.consumedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 行3:储值卡总余额 -->
|
||||
<view class="table-row table-row--footer">
|
||||
<view class="table-row-left">
|
||||
<text class="table-row-label-bold">储值卡总余额</text>
|
||||
<view class="help-icon-dark" data-key="cardBalance" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="table-row-right">
|
||||
<text class="table-row-value-lg">{{recharge.cardBalance}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.cardBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 赠送卡统计详情 -->
|
||||
<text class="card-section-title" style="margin-top: 28rpx;">赠送卡统计详情</text>
|
||||
<view class="table-bordered">
|
||||
<!-- 表头 -->
|
||||
<view class="gift-table-header">
|
||||
<text class="gift-col gift-col--name">类型</text>
|
||||
<text class="gift-col">酒水卡</text>
|
||||
<text class="gift-col">台费卡</text>
|
||||
<text class="gift-col">抵用券</text>
|
||||
</view>
|
||||
<!-- 新增行 -->
|
||||
<view class="gift-table-row" wx:for="{{recharge.giftRows}}" wx:key="label">
|
||||
<view class="gift-col gift-col--name">
|
||||
<text class="gift-row-label">{{item.label}}</text>
|
||||
<text class="gift-row-total">{{item.total}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.wine}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.wineCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.table}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.tableCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.coupon}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.couponCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 全类别会员卡余额合计 -->
|
||||
<view class="total-balance-row">
|
||||
<view class="total-balance-left">
|
||||
<text class="total-balance-label">全类别会员卡余额合计</text>
|
||||
<view class="help-icon-dark" data-key="allCardBalance" bindtap="onHelpTap">?</view>
|
||||
<text class="total-balance-note">仅经营参考,非财务属性</text>
|
||||
</view>
|
||||
<view class="total-balance-right">
|
||||
<text class="total-balance-value">{{recharge.allCardBalance}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.allCardBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 3: 应计收入确认 ===== -->
|
||||
<view id="section-revenue" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">💰</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【记账】应计收入确认</text>
|
||||
<text class="card-header-desc-light">从发生额到入账收入的全流程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 收入结构 -->
|
||||
<view class="sub-title-row">
|
||||
<text class="sub-title-text">收入结构</text>
|
||||
<text class="sub-title-desc">按业务查看各项应计收入的构成</text>
|
||||
</view>
|
||||
<view class="table-bordered">
|
||||
<!-- 表头 -->
|
||||
<view class="rev-table-header">
|
||||
<text class="rev-col rev-col--name">项目</text>
|
||||
<text class="rev-col">发生额</text>
|
||||
<text class="rev-col">优惠</text>
|
||||
<text class="rev-col">入账</text>
|
||||
</view>
|
||||
<!-- 数据行 -->
|
||||
<block wx:for="{{revenue.structureRows}}" wx:key="name">
|
||||
<view class="rev-table-row {{item.isSub ? 'rev-table-row--sub' : ''}}">
|
||||
<view class="rev-col rev-col--name">
|
||||
<text class="{{item.isSub ? 'rev-name-sub' : 'rev-name'}}">{{item.name}}</text>
|
||||
<text class="rev-name-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<text class="rev-col rev-val">{{item.amount}}</text>
|
||||
<text class="rev-col rev-val {{item.discount !== '-' ? 'rev-val--red' : 'rev-val--muted'}}">{{item.discount}}</text>
|
||||
<view class="rev-col">
|
||||
<text class="rev-val {{item.isSub ? '' : 'rev-val--bold'}}">{{item.booked}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled && item.bookedCompare}}">
|
||||
<text class="compare-text-up-xs">↑{{item.bookedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 收入确认(损益链) -->
|
||||
<view class="sub-title-row" style="margin-top: 28rpx;">
|
||||
<text class="sub-title-text">收入确认</text>
|
||||
<text class="sub-title-desc">从正价到收款方式的损益链</text>
|
||||
</view>
|
||||
<view class="table-bordered">
|
||||
<!-- 项目正价 标题 -->
|
||||
<view class="flow-header">
|
||||
<text class="flow-header-title">项目正价</text>
|
||||
<text class="flow-header-desc">即标价测算</text>
|
||||
</view>
|
||||
<!-- 正价明细(左侧竖线) -->
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.priceItems}}" wx:key="name">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 发生额合计 -->
|
||||
<view class="flow-total-row">
|
||||
<view class="flow-total-left">
|
||||
<text class="flow-total-label">发生额</text>
|
||||
<text class="flow-total-desc">即上列正价合计</text>
|
||||
</view>
|
||||
<view class="flow-total-right">
|
||||
<text class="flow-total-value">{{revenue.totalOccurrence}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{revenue.totalOccurrenceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 优惠扣减 -->
|
||||
<view class="flow-header flow-header--deduct">
|
||||
<text class="flow-header-title">优惠扣减</text>
|
||||
</view>
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.discountItems}}" wx:key="name">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val flow-detail-val--red">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down-xs">↓{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 成交收入 -->
|
||||
<view class="flow-total-row flow-total-row--accent">
|
||||
<view class="flow-total-left">
|
||||
<text class="flow-total-label">成交收入</text>
|
||||
<text class="flow-total-desc">发生额 - 优惠</text>
|
||||
</view>
|
||||
<view class="flow-total-right">
|
||||
<text class="flow-total-value">{{revenue.confirmedTotal}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{revenue.confirmedTotalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 收款渠道 -->
|
||||
<view class="flow-header">
|
||||
<text class="flow-header-title">收款渠道明细</text>
|
||||
</view>
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.channelItems}}" wx:key="name">
|
||||
<view class="flow-detail-name-group">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<text class="flow-detail-name-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 4: 现金流入 ===== -->
|
||||
<view id="section-cashflow" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">🧾</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【现金流水】流入</text>
|
||||
<text class="card-header-desc-light">实际到账的资金来源明细</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 消费收入 -->
|
||||
<text class="flow-group-label">消费收入</text>
|
||||
<view class="flow-item-list">
|
||||
<view class="flow-item" wx:for="{{cashflow.consumeItems}}" wx:key="name">
|
||||
<view class="flow-item-left">
|
||||
<text class="flow-item-name">{{item.name}}</text>
|
||||
<text class="flow-item-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-item-right">
|
||||
<text class="flow-item-value">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值收入 -->
|
||||
<text class="flow-group-label" style="margin-top: 20rpx;">充值收入</text>
|
||||
<view class="flow-item-list">
|
||||
<view class="flow-item" wx:for="{{cashflow.rechargeItems}}" wx:key="name">
|
||||
<view class="flow-item-left">
|
||||
<text class="flow-item-name">{{item.name}}</text>
|
||||
<text class="flow-item-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-item-right">
|
||||
<text class="flow-item-value">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合计 -->
|
||||
<view class="flow-sum-row">
|
||||
<text class="flow-sum-label">现金流入合计</text>
|
||||
<view class="flow-sum-right">
|
||||
<text class="flow-sum-value">{{cashflow.total}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{cashflow.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 5: 现金流出 ===== -->
|
||||
<view id="section-expense" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">📤</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【现金流水】流出</text>
|
||||
<text class="card-header-desc-light">清晰呈现各类开销与结构</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 进货与运营 3列 -->
|
||||
<text class="expense-group-label">进货与运营</text>
|
||||
<view class="expense-grid-3">
|
||||
<view class="expense-cell" wx:for="{{expense.operationItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 固定支出 2×2 -->
|
||||
<text class="expense-group-label">固定支出</text>
|
||||
<view class="expense-grid-2">
|
||||
<view class="expense-cell" wx:for="{{expense.fixedItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isFlat ? 'compare-text-flat-xs' : 'compare-text-up-xs'}}">{{item.isFlat ? '' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 助教薪资 2×2 -->
|
||||
<text class="expense-group-label">助教薪资</text>
|
||||
<view class="expense-grid-2">
|
||||
<view class="expense-cell" wx:for="{{expense.coachItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 平台服务费 3列 -->
|
||||
<text class="expense-group-label">平台服务费</text>
|
||||
<text class="expense-group-note">服务费在流水流入时,平台已经扣除。不产生支出流水。</text>
|
||||
<view class="expense-grid-3">
|
||||
<view class="expense-cell" wx:for="{{expense.platformItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支出合计 -->
|
||||
<view class="flow-sum-row">
|
||||
<text class="flow-sum-label">支出合计</text>
|
||||
<view class="flow-sum-right">
|
||||
<text class="flow-sum-value">{{expense.total}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{expense.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 6: 助教分析 ===== -->
|
||||
<view id="section-coach" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">🎱</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">助教分析</text>
|
||||
<text class="card-header-desc-light">全部助教服务收入与分成的平均值</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 基础课 -->
|
||||
<text class="card-section-title">助教 <text class="card-section-title-sub">(基础课)</text></text>
|
||||
<view class="table-bordered">
|
||||
<view class="coach-fin-header">
|
||||
<text class="coach-fin-col coach-fin-col--name">级别</text>
|
||||
<text class="coach-fin-col">客户支付</text>
|
||||
<text class="coach-fin-col">球房抽成</text>
|
||||
<text class="coach-fin-col">小时平均</text>
|
||||
</view>
|
||||
<!-- 合计行 -->
|
||||
<view class="coach-fin-row coach-fin-row--total">
|
||||
<text class="coach-fin-col coach-fin-col--name coach-fin-bold">合计</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.basic.totalPay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.totalPayCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.basic.totalShare}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.totalShareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{coachAnalysis.basic.avgHourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.avgHourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 明细行 -->
|
||||
<view class="coach-fin-row" wx:for="{{coachAnalysis.basic.rows}}" wx:key="level">
|
||||
<text class="coach-fin-col coach-fin-col--name">{{item.level}}</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val">{{item.pay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.payDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.payDown ? '↓' : '↑'}}{{item.payCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val">{{item.share}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.shareDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.shareDown ? '↓' : '↑'}}{{item.shareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{item.hourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.hourlyFlat ? 'compare-text-flat-xs' : 'compare-text-up-xs'}}">{{item.hourlyFlat ? '' : '↑'}}{{item.hourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 激励课 -->
|
||||
<text class="card-section-title" style="margin-top: 28rpx;">助教 <text class="card-section-title-sub">(激励课)</text></text>
|
||||
<view class="table-bordered">
|
||||
<view class="coach-fin-header">
|
||||
<text class="coach-fin-col coach-fin-col--name">级别</text>
|
||||
<text class="coach-fin-col">客户支付</text>
|
||||
<text class="coach-fin-col">球房抽成</text>
|
||||
<text class="coach-fin-col">小时平均</text>
|
||||
</view>
|
||||
<view class="coach-fin-row">
|
||||
<text class="coach-fin-col coach-fin-col--name coach-fin-bold">合计</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.incentive.totalPay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.totalPayCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.incentive.totalShare}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.totalShareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{coachAnalysis.incentive.avgHourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.avgHourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- ===== 目录导航遮罩 ===== -->
|
||||
<view class="toc-overlay" wx:if="{{tocVisible}}" catchtap="closeToc"></view>
|
||||
|
||||
<!-- ===== 目录导航面板 ===== -->
|
||||
<view class="toc-panel {{tocVisible ? 'toc-panel--show' : ''}}">
|
||||
<view class="toc-header">
|
||||
<text class="toc-header-text">📊 财务看板导航</text>
|
||||
</view>
|
||||
<view class="toc-list">
|
||||
<view
|
||||
class="toc-item {{currentSectionIndex === index ? 'toc-item--active' : ''}}"
|
||||
wx:for="{{tocItems}}"
|
||||
wx:key="sectionId"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTocItemTap"
|
||||
>
|
||||
<text class="toc-item-emoji">{{item.emoji}}</text>
|
||||
<text class="toc-item-text">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 指标说明弹窗 ===== -->
|
||||
<view class="tip-overlay" wx:if="{{tipVisible}}" catchtap="closeTip"></view>
|
||||
<view class="tip-toast {{tipVisible ? 'tip-toast--show' : ''}}">
|
||||
<view class="tip-toast-header">
|
||||
<text class="tip-toast-title">{{tipTitle}}</text>
|
||||
<view class="tip-toast-close" bindtap="closeTip">
|
||||
<t-icon name="close" size="36rpx" color="#8b8b8b" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="tip-toast-content">{{tipContent}}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{200}}" />
|
||||
|
||||
<dev-fab />
|
||||
1237
apps/miniprogram/miniprogram/pages/board-finance/board-finance.wxss
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "对话历史",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mockChatHistory } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 带展示标签的对话历史项 */
|
||||
interface ChatHistoryDisplay {
|
||||
id: string
|
||||
title: string
|
||||
lastMessage: string
|
||||
timestamp: string
|
||||
customerName?: string
|
||||
/** 格式化后的时间标签 */
|
||||
timeLabel: string
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态:loading / empty / normal */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 对话历史列表 */
|
||||
list: [] as ChatHistoryDisplay[],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
/** 加载数据 */
|
||||
loadData() {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const sorted = sortByTimestamp(mockChatHistory)
|
||||
const list: ChatHistoryDisplay[] = sorted.map((item) => ({
|
||||
...item,
|
||||
timeLabel: this.formatTime(item.timestamp),
|
||||
}))
|
||||
|
||||
this.setData({
|
||||
list,
|
||||
pageState: list.length === 0 ? 'empty' : 'normal',
|
||||
})
|
||||
}, 400)
|
||||
},
|
||||
|
||||
/** 格式化时间为相对标签 */
|
||||
formatTime(timestamp: string): string {
|
||||
const now = new Date()
|
||||
const target = new Date(timestamp)
|
||||
const diffMs = now.getTime() - target.getTime()
|
||||
const diffMin = Math.floor(diffMs / 60000)
|
||||
const diffHour = Math.floor(diffMs / 3600000)
|
||||
const diffDay = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMin < 1) return '刚刚'
|
||||
if (diffMin < 60) return `${diffMin}分钟前`
|
||||
if (diffHour < 24) return `${diffHour}小时前`
|
||||
if (diffDay < 7) return `${diffDay}天前`
|
||||
|
||||
const month = target.getMonth() + 1
|
||||
const day = target.getDate()
|
||||
return `${month}月${day}日`
|
||||
},
|
||||
|
||||
/** 点击对话记录 → 跳转 chat 页面 */
|
||||
onItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: '/pages/chat/chat?historyId=' + id })
|
||||
},
|
||||
|
||||
/** 下拉刷新 */
|
||||
onPullDownRefresh() {
|
||||
this.loadData()
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 600)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无对话记录" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- 对话列表 -->
|
||||
<view class="chat-list">
|
||||
<view
|
||||
class="chat-item"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<view class="chat-icon-box">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-top">
|
||||
<text class="chat-title text-ellipsis">{{item.title}}</text>
|
||||
<text class="chat-time">{{item.timeLabel}}</text>
|
||||
</view>
|
||||
<view class="chat-bottom">
|
||||
<text class="chat-summary text-ellipsis" wx:if="{{item.customerName}}">{{item.customerName}} · {{item.lastMessage}}</text>
|
||||
<text class="chat-summary text-ellipsis" wx:else>{{item.lastMessage}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="list-footer">
|
||||
<text class="footer-text">— 已加载全部记录 —</text>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||