微信小程序页面迁移校验之前 P5任务处理之前
2
apps/miniprogram - 副本/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
miniprogram_npm
|
||||
0
apps/miniprogram - 副本/.gitkeep
Normal file
202
apps/miniprogram - 副本/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# apps/miniprogram — 微信小程序
|
||||
|
||||
微信小程序前端项目,基于 Donut 多端框架 + TDesign 组件库,为台球门店会员提供移动端服务入口。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 微信小程序原生 + Donut 多端(`projectArchitecture: multiPlatform`)
|
||||
- TDesign 小程序版(`tdesign-miniprogram ^1.12.2`)
|
||||
- TypeScript
|
||||
- 类型定义:`miniprogram-api-typings`
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
apps/miniprogram/
|
||||
├── miniprogram/ # 小程序主体代码
|
||||
│ ├── app.ts # 应用入口(wx.login 获取 code)
|
||||
│ ├── app.json # 全局配置(页面路由、窗口样式)
|
||||
│ ├── app.wxss # 全局样式
|
||||
│ ├── pages/ # 页面目录
|
||||
│ │ ├── mvp/ # MVP 全链路验证页
|
||||
│ │ ├── index/ # 首页
|
||||
│ │ ├── login/ # 登录页
|
||||
│ │ ├── apply/ # 入驻申请页
|
||||
│ │ ├── reviewing/ # 审核中等待页
|
||||
│ │ ├── no-permission/ # 无权限提示页
|
||||
│ │ ├── dev-tools/ # 开发调试面板(仅 develop 环境)
|
||||
│ │ └── logs/ # 日志页
|
||||
│ ├── components/ # 全局组件
|
||||
│ │ └── dev-fab/ # 浮动调试按钮(仅 develop 环境显示)
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── config.ts # 环境配置(API 地址自动切换)
|
||||
│ │ └── util.ts # 通用工具(日期格式化等)
|
||||
│ ├── miniprogram_npm/ # 构建后的 npm 包(TDesign 组件)
|
||||
│ ├── i18n/ # 国际化资源
|
||||
│ └── miniapp/ # Donut 多端原生资源
|
||||
├── typings/ # TypeScript 类型定义
|
||||
├── project.config.json # 微信开发者工具项目配置
|
||||
├── project.miniapp.json # Donut 多端配置
|
||||
├── tsconfig.json # TypeScript 编译配置
|
||||
├── package.json # npm 依赖声明
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 环境准备
|
||||
|
||||
1. 安装微信开发者工具
|
||||
2. 打开本目录(`apps/miniprogram/`)
|
||||
3. 首次打开后,在工具中执行"构建 npm"以生成 `miniprogram_npm/`
|
||||
4. AppID:`wx7c07793d82732921`
|
||||
|
||||
### 页面路由
|
||||
|
||||
当前注册页面(`app.json`):
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `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 集成
|
||||
|
||||
### API 地址配置
|
||||
|
||||
`utils/config.ts` 根据小程序运行环境自动切换 API 地址:
|
||||
|
||||
| 环境 | API 地址 |
|
||||
|------|----------|
|
||||
| develop(开发版) | `http://127.0.0.1:8000` |
|
||||
| trial(体验版) | `https://api.langlangzhuoqiu.cn` |
|
||||
| release(正式版) | `https://api.langlangzhuoqiu.cn` |
|
||||
|
||||
### 认证流程
|
||||
|
||||
小程序用户的完整生命周期:
|
||||
|
||||
```
|
||||
wx.login() 获取 code
|
||||
↓
|
||||
POST /api/xcx-auth/login → 获取 JWT(受限令牌,status=new)
|
||||
↓
|
||||
POST /api/xcx-auth/apply → 提交入驻申请(球房ID + 身份 + 手机号,status → pending)
|
||||
↓
|
||||
管理员在后台审批
|
||||
↓
|
||||
GET /api/xcx-auth/status → 查询审批结果
|
||||
↓
|
||||
POST /api/xcx-auth/login → 重新登录获取完整令牌(含 site_id + roles)
|
||||
↓
|
||||
正常使用业务功能
|
||||
```
|
||||
|
||||
用户状态流转:
|
||||
- `new`:新用户,尚未提交申请
|
||||
- `pending`:已提交申请,等待审批
|
||||
- `approved`:审批通过,可正常使用
|
||||
- `rejected`:审批拒绝,可重新申请
|
||||
- `disabled`:账号禁用
|
||||
|
||||
令牌类型:
|
||||
- 受限令牌(`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)
|
||||
|
||||
## MVP 页面
|
||||
|
||||
`pages/mvp/mvp` 是全链路验证页面,从后端 `/api/xcx-test` 读取 `test."xcx-test"` 表数据并显示,用于验证:
|
||||
- 小程序 → 后端 API → 数据库 的完整链路
|
||||
- 网络请求、错误处理、加载状态
|
||||
|
||||
## 权限模型
|
||||
|
||||
小程序用户通过 RBAC 模型控制功能访问:
|
||||
|
||||
| 角色 | 可见功能 |
|
||||
|------|----------|
|
||||
| coach(助教) | 查看任务、助教看板 |
|
||||
| staff(员工) | 查看任务、数据看板 |
|
||||
| site_admin(店铺管理员) | 全部看板 |
|
||||
| tenant_admin(租户管理员) | 全部权限 |
|
||||
|
||||
多门店支持:用户可关联多个门店,通过 `/api/xcx-auth/switch-site` 切换。
|
||||
|
||||
## 与 Monorepo 的关系
|
||||
|
||||
- 本项目为独立前端工程,不参与 Python uv workspace
|
||||
- 通过 FastAPI 后端(`apps/backend/`)与数据层交互
|
||||
- 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
|
||||
|
||||
- [ ] 完善认证流程页面(登录 → 申请 → 等待审批 → 首页)
|
||||
- [ ] 任务管理页面(任务列表、置顶、放弃、备注)
|
||||
- [ ] 数据看板页面(助教业绩、客户分析)
|
||||
- [ ] 会员中心页面
|
||||
- [ ] 助教预约功能
|
||||
- [ ] 订单查询功能
|
||||
- [ ] 多门店切换 UI
|
||||
- [ ] 消息通知(微信订阅消息)
|
||||
- [ ] CI/CD(代码检查、自动上传体验版)
|
||||
214
apps/miniprogram - 副本/doc/auth-integration-guide.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# 小程序认证系统联调指南
|
||||
|
||||
本文档说明如何在本地环境中完成小程序认证系统的前后端联调测试,覆盖从微信登录到管理端审核的完整流程。
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
### 1.1 后端服务
|
||||
|
||||
1. 在项目根目录 `.env.local` 中添加以下配置:
|
||||
|
||||
```env
|
||||
WX_DEV_MODE=true # 开启开发模式,跳过真实微信 code2Session 调用
|
||||
JWT_SECRET_KEY=dev-secret # 开发环境 JWT 密钥(生产环境请使用强随机值)
|
||||
```
|
||||
|
||||
2. 确保数据库 `test_zqyy_app` 已执行认证相关迁移脚本:
|
||||
|
||||
```bash
|
||||
# 建表脚本
|
||||
psql -d test_zqyy_app -f db/zqyy_app/migrations/2026-02-26__p3_create_auth_tables.sql
|
||||
|
||||
# 种子数据(角色、权限、角色-权限映射)
|
||||
psql -d test_zqyy_app -f db/zqyy_app/migrations/2026-02-26__p3_seed_roles_permissions.sql
|
||||
```
|
||||
|
||||
3. 启动后端服务:
|
||||
|
||||
```bash
|
||||
cd apps/backend && uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
服务默认监听 `http://localhost:8000`。
|
||||
|
||||
### 1.2 微信开发者工具
|
||||
|
||||
1. **导入项目**:打开微信开发者工具 → 导入项目 → 选择 `apps/miniprogram/` 目录
|
||||
2. **AppID**:使用测试号,或在项目设置中选择「不使用云服务」
|
||||
3. **项目设置**:
|
||||
- 勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」(开发环境必须)
|
||||
- 勾选「不校验 HTTPS 证书」
|
||||
4. **编译模式**(可选):点击编译模式下拉 → 添加编译模式 → 指定启动页面(如 `pages/login/login`)方便调试特定页面
|
||||
|
||||
---
|
||||
|
||||
## 2. 联调测试流程
|
||||
|
||||
### Step 1:Mock 登录
|
||||
|
||||
开发模式下使用 `POST /api/xcx/dev-login` 端点,绕过微信 `code2Session` 调用。
|
||||
|
||||
```bash
|
||||
# 默认创建 pending 状态用户
|
||||
curl -X POST http://localhost:8000/api/xcx/dev-login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"openid": "test_user_001"}'
|
||||
|
||||
# 指定用户状态(可选值:pending / approved / rejected / disabled)
|
||||
curl -X POST http://localhost:8000/api/xcx/dev-login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"openid": "test_user_001", "status": "pending"}'
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ...",
|
||||
"token_type": "bearer",
|
||||
"user_status": "pending",
|
||||
"user_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
小程序端会自动将 token 存储到 Storage 和 globalData 中。
|
||||
|
||||
### Step 2:提交申请
|
||||
|
||||
在申请页面(`pages/apply/apply`)填写以下信息:
|
||||
|
||||
| 字段 | 示例值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 球房ID(site_code) | `AB123` | 格式:2 字母 + 3 数字 |
|
||||
| 申请身份 | `助教` 或 `员工` | 下拉选择 |
|
||||
| 手机号 | `13800138000` | 11 位数字 |
|
||||
| 编号(选填) | `EMP001` | 员工编号,用于辅助匹配 |
|
||||
| 昵称 | `张三` | 显示名称 |
|
||||
|
||||
提交后自动跳转到审核等待页(`pages/reviewing/reviewing`)。
|
||||
|
||||
也可通过 API 直接提交:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/xcx/apply \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"site_code": "AB123",
|
||||
"applied_role_text": "助教",
|
||||
"phone": "13800138000",
|
||||
"nickname": "张三"
|
||||
}'
|
||||
```
|
||||
|
||||
### Step 3:管理端审核
|
||||
|
||||
使用管理端 API 审核申请:
|
||||
|
||||
```bash
|
||||
# 1. 查看申请列表
|
||||
curl http://localhost:8000/api/admin/applications \
|
||||
-H "Authorization: Bearer <admin_token>"
|
||||
|
||||
# 2. 查看申请详情(含候选匹配)
|
||||
curl http://localhost:8000/api/admin/applications/1 \
|
||||
-H "Authorization: Bearer <admin_token>"
|
||||
|
||||
# 3a. 批准申请(需提供 role_id,可选 binding 信息)
|
||||
curl -X POST http://localhost:8000/api/admin/applications/1/approve \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"role_id": 1,
|
||||
"binding": {"assistant_id": 100, "binding_type": "assistant"},
|
||||
"review_note": "信息核实通过"
|
||||
}'
|
||||
|
||||
# 3b. 拒绝申请(需提供拒绝原因)
|
||||
curl -X POST http://localhost:8000/api/admin/applications/1/reject \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"review_note": "信息不匹配,请重新申请"}'
|
||||
```
|
||||
|
||||
> **提示**:管理端 API 需要 `site_admin` 或 `tenant_admin` 角色的 token。可通过 `dev-login` 创建一个 approved 状态的管理员用户,再手动在数据库中为其分配管理角色。
|
||||
|
||||
### Step 4:重新登录验证
|
||||
|
||||
审核通过后,在小程序中重新登录(或使用 `dev-login`):
|
||||
|
||||
- 用户状态应变为 `approved`
|
||||
- 小程序应自动跳转到主页(mvp 页面)
|
||||
- 可通过 `GET /api/xcx/me` 验证用户状态和关联信息
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/xcx/me \
|
||||
-H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点汇总
|
||||
|
||||
### 小程序端
|
||||
|
||||
| 端点 | 方法 | 说明 | 认证要求 |
|
||||
|------|------|------|---------|
|
||||
| `/api/xcx/login` | POST | 微信登录 | 无 |
|
||||
| `/api/xcx/dev-login` | POST | 开发模式 mock 登录 | 无(仅 `WX_DEV_MODE=true`) |
|
||||
| `/api/xcx/apply` | POST | 提交申请 | JWT(含 pending) |
|
||||
| `/api/xcx/me` | GET | 查询用户状态 | JWT(含 pending) |
|
||||
| `/api/xcx/me/sites` | GET | 查询关联店铺 | JWT(approved) |
|
||||
| `/api/xcx/switch-site` | POST | 切换店铺 | JWT(approved) |
|
||||
| `/api/xcx/refresh` | POST | 刷新令牌 | refresh_token |
|
||||
|
||||
### 管理端
|
||||
|
||||
| 端点 | 方法 | 说明 | 认证要求 |
|
||||
|------|------|------|---------|
|
||||
| `/api/admin/applications` | GET | 查询申请列表 | JWT + site_admin/tenant_admin |
|
||||
| `/api/admin/applications/{id}` | GET | 申请详情 + 候选匹配 | JWT + site_admin/tenant_admin |
|
||||
| `/api/admin/applications/{id}/approve` | POST | 批准申请 | JWT + site_admin/tenant_admin |
|
||||
| `/api/admin/applications/{id}/reject` | POST | 拒绝申请 | JWT + site_admin/tenant_admin |
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见问题排查
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
- **原因**:token 过期或无效
|
||||
- **排查**:检查小程序 Storage 中的 `access_token` 是否存在;尝试使用 `dev-login` 重新获取 token
|
||||
- **注意**:access_token 默认有效期较短,过期后小程序会自动尝试用 refresh_token 刷新
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
- **原因**:用户状态为 `disabled`,或权限不足
|
||||
- **排查**:通过 `GET /api/xcx/me` 检查用户 `status` 字段;确认用户在目标店铺下有对应角色
|
||||
|
||||
### 409 Conflict
|
||||
|
||||
- **原因**:已有待审核申请(重复提交),或审核目标申请状态非 `pending`
|
||||
- **排查**:通过 `GET /api/xcx/me` 查看现有申请列表及状态
|
||||
|
||||
### 422 Validation Error
|
||||
|
||||
- **原因**:表单数据格式错误
|
||||
- **常见情况**:
|
||||
- `site_code` 不符合 `2字母+3数字` 格式(如 `AB123`)
|
||||
- `phone` 不是 11 位纯数字
|
||||
- 必填字段为空
|
||||
|
||||
### 网络请求失败
|
||||
|
||||
- **检查后端是否启动**:访问 `http://localhost:8000/docs` 确认 Swagger 文档可访问
|
||||
- **检查域名配置**:小程序 `utils/request.ts` 中的 `BASE_URL` 应指向 `http://localhost:8000`
|
||||
- **「不校验合法域名」未勾选**:开发环境必须在微信开发者工具项目设置中勾选此选项,否则 `localhost` 请求会被拦截
|
||||
|
||||
### Mock 登录不可用
|
||||
|
||||
- **原因**:`WX_DEV_MODE` 未设置为 `true`
|
||||
- **排查**:检查 `.env.local` 中是否有 `WX_DEV_MODE=true`;重启后端服务使配置生效
|
||||
404
apps/miniprogram - 副本/doc/h5-input-material-guide.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# H5 → 小程序转换:输入材料准备指南
|
||||
|
||||
> 本文档规范了每次将 H5 原型页面转换为微信小程序时,应提供的输入材料清单、格式要求和操作步骤。
|
||||
> 目标:提高 AI 转换还原度,减少颜色/间距换算错误、交互逻辑遗漏、图标处理失误。
|
||||
|
||||
---
|
||||
|
||||
## 一、HTML 文件规范
|
||||
|
||||
### 问题背景
|
||||
|
||||
当前 H5 原型是 Tailwind CDN + 内联 SVG + 原生 JS 的单文件结构,存在以下转换障碍:
|
||||
|
||||
- Tailwind 工具类需逐个手动展开为 WXSS,容易遗漏或换算错误
|
||||
- 内联 SVG 在小程序中完全不可用,但混在 HTML 里容易被"直译"
|
||||
- JS 交互逻辑和 DOM 操作对小程序没有参考价值,反而干扰判断
|
||||
|
||||
### 推荐做法
|
||||
|
||||
1. **提供渲染后的最终 DOM**(而非模板源码)
|
||||
- 浏览器中打开页面 → 右键 `<body>` → Copy → Copy outerHTML
|
||||
- 原因:Tailwind `tailwind.config` 中自定义颜色/间距在源码中只是类名,无法确定最终计算值
|
||||
- 保存为 `docs/h5_ui/rendered/<page-name>.html`
|
||||
|
||||
2. **去掉所有 `<script>` 标签**
|
||||
- JS 交互逻辑应单独用结构化文本描述(见第三节"交互描述规范")
|
||||
- 不要让 AI 从 DOM 操作代码中反推意图
|
||||
|
||||
3. **内联关键样式**(二选一)
|
||||
|
||||
**方式 A:手动记录(少量元素时)**
|
||||
```
|
||||
Chrome DevTools 操作:
|
||||
1. 打开 H5 页面
|
||||
2. 右键目标元素 → 检查
|
||||
3. 在 Styles 面板中,点击 Computed 标签
|
||||
4. 勾选 "Show all"
|
||||
5. 记录以下关键属性的计算值:
|
||||
- width / height / padding / margin / gap
|
||||
- font-size / font-weight / line-height / color
|
||||
- background / border-radius / box-shadow
|
||||
- display / flex-direction / align-items / justify-content
|
||||
```
|
||||
|
||||
**方式 B:脚本批量导出(推荐)**
|
||||
在 Chrome Console 中执行以下脚本,批量导出关键元素的计算样式:
|
||||
|
||||
```javascript
|
||||
function exportStyles() {
|
||||
const props = [
|
||||
'width','height','padding','margin','gap',
|
||||
'fontSize','fontWeight','lineHeight','color',
|
||||
'background','backgroundColor','borderRadius',
|
||||
'boxShadow','display','flexDirection','alignItems',
|
||||
'justifyContent','position','opacity'
|
||||
];
|
||||
const elements = document.querySelectorAll('[class]');
|
||||
const result = [];
|
||||
elements.forEach((el, i) => {
|
||||
const cs = getComputedStyle(el);
|
||||
const styles = {};
|
||||
props.forEach(p => {
|
||||
const v = cs[p];
|
||||
if (v && v !== 'none' && v !== 'normal' && v !== '0px' && v !== 'auto') {
|
||||
styles[p] = v;
|
||||
}
|
||||
});
|
||||
if (Object.keys(styles).length > 0) {
|
||||
result.push({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
classes: el.className,
|
||||
text: el.textContent?.trim().slice(0, 30),
|
||||
styles
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
exportStyles();
|
||||
```
|
||||
|
||||
将输出追加到 `docs/h5_ui/computed-styles.json`(按页面名分 key 存放)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 二、CSS / 样式规范
|
||||
|
||||
### 当前状况
|
||||
|
||||
CSS 分两部分:Tailwind 工具类(占 90%)+ 少量自定义 `<style>` 块。无 SCSS/LESS 预处理器。
|
||||
|
||||
### 设计 Token 文件(做一次,全局复用)
|
||||
|
||||
将 Tailwind 自定义主题提取为设计 Token,保存为 `docs/h5_ui/design-tokens.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#0052d9",
|
||||
"primary-light": "#ecf2fe",
|
||||
"success": "#00a870",
|
||||
"warning": "#ed7b2f",
|
||||
"error": "#e34d59",
|
||||
"gray-1": "#f3f3f3",
|
||||
"gray-2": "#eeeeee",
|
||||
"gray-3": "#e7e7e7",
|
||||
"gray-4": "#dcdcdc",
|
||||
"gray-5": "#c5c5c5",
|
||||
"gray-6": "#a6a6a6",
|
||||
"gray-7": "#8b8b8b",
|
||||
"gray-8": "#777777",
|
||||
"gray-9": "#5e5e5e",
|
||||
"gray-10": "#4b4b4b",
|
||||
"gray-11": "#393939",
|
||||
"gray-12": "#2c2c2c",
|
||||
"gray-13": "#242424"
|
||||
},
|
||||
"spacing": {
|
||||
"comment": "Tailwind 默认 1 unit = 4px = 8rpx(基于 750rpx 设计稿)",
|
||||
"base": 8,
|
||||
"unit": "rpx"
|
||||
},
|
||||
"borderRadius": {
|
||||
"sm": "8rpx",
|
||||
"md": "16rpx",
|
||||
"lg": "24rpx",
|
||||
"xl": "32rpx",
|
||||
"2xl": "32rpx",
|
||||
"3xl": "48rpx"
|
||||
},
|
||||
"fontSize": {
|
||||
"xs": "24rpx",
|
||||
"sm": "28rpx",
|
||||
"base": "32rpx",
|
||||
"lg": "36rpx",
|
||||
"xl": "40rpx",
|
||||
"2xl": "48rpx"
|
||||
},
|
||||
"shadows": {
|
||||
"lg": "0 8rpx 32rpx rgba(0,0,0,0.06)",
|
||||
"xl": "0 16rpx 48rpx rgba(0,0,0,0.08)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义动画标注
|
||||
|
||||
对 `<style>` 块中的自定义 CSS,需标注处理方式:
|
||||
|
||||
| CSS 特性 | 小程序支持 | 处理方式 |
|
||||
|----------|-----------|----------|
|
||||
| `@keyframes` + `animation` | ✅ 支持 | 直接迁移 |
|
||||
| `transition` | ✅ 支持 | 直接迁移 |
|
||||
| `linear-gradient` | ✅ 支持 | 直接迁移 |
|
||||
| `backdrop-filter: blur()` | ❌ 不支持 | 改为半透明背景色 `rgba()` |
|
||||
| `blur-xl`(Tailwind) | ❌ 不支持 | 去掉或改为纯色 |
|
||||
| CSS 变量 `var()` | ✅ 支持 | 直接迁移(TDesign 大量使用) |
|
||||
|
||||
### 不需要做的事
|
||||
|
||||
- 不需要展开 SCSS/LESS(当前未使用预处理器)
|
||||
- 不需要合并 CSS 文件(小程序每个页面有独立 `.wxss`)
|
||||
|
||||
---
|
||||
|
||||
## 三、交互描述规范
|
||||
|
||||
### 为什么需要单独描述
|
||||
|
||||
H5 原型中的 JS 交互(`addEventListener`、`classList.toggle`、`innerHTML` 等)在小程序中完全不可用。AI 需要从"用户意图"层面理解交互,而非从"DOM 操作"层面翻译。
|
||||
|
||||
### 格式模板
|
||||
|
||||
每个页面提供一份交互说明,保存为 `docs/h5_ui/interactions/<page-name>.md`:
|
||||
|
||||
```markdown
|
||||
# 页面名:<page-name>
|
||||
|
||||
## 状态变量
|
||||
| 变量名 | 类型 | 初始值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| agreed | boolean | false | 协议勾选状态 |
|
||||
| loading | boolean | false | 登录请求中 |
|
||||
|
||||
## 用户操作 → 响应
|
||||
| 操作 | 触发条件 | 响应行为 | 目标状态 |
|
||||
|------|----------|----------|----------|
|
||||
| 点击协议勾选框 | 无 | 切换 agreed | agreed=!agreed |
|
||||
| 点击"使用微信登录" | agreed=true | 调用登录API | loading=true → 跳转 |
|
||||
| 点击"使用微信登录" | agreed=false | 无响应(按钮禁用态) | 不变 |
|
||||
| 登录成功 | API返回 | 按用户状态跳转 | 见流程7.1 |
|
||||
| 登录失败 | API报错 | Toast提示错误 | loading=false |
|
||||
|
||||
## 页面状态枚举
|
||||
| 状态名 | 视觉表现 | 触发条件 |
|
||||
|--------|----------|----------|
|
||||
| 默认态 | 按钮灰色禁用 | agreed=false |
|
||||
| 可登录态 | 按钮蓝色渐变+阴影 | agreed=true |
|
||||
| 加载中 | 按钮显示loading | loading=true |
|
||||
```
|
||||
|
||||
### 必须覆盖的状态(每个页面都要考虑)
|
||||
|
||||
| 状态 | 说明 | 视觉表现参考 |
|
||||
|------|------|-------------|
|
||||
| 正常态(有数据) | 页面主要功能正常展示 | 默认截图 |
|
||||
| 空数据态 | 列表/卡片区域无数据 | 居中文案 `暂无数据` / `暂无任务` |
|
||||
| 加载中态 | 数据请求中 | 区域文案 `加载中...` |
|
||||
| 错误态 | 接口请求失败 | 文案 `加载失败,请点击重试` + 重试按钮 |
|
||||
| 登录态差异 | 如果页面因登录状态有不同表现 | 按实际描述 |
|
||||
|
||||
---
|
||||
|
||||
## 四、视觉截图规范
|
||||
|
||||
### 截图是校验还原度的唯一视觉参考
|
||||
|
||||
### 分辨率
|
||||
|
||||
- 使用 iPhone 6/7/8 尺寸(375×667)
|
||||
- 这是小程序 750rpx 设计基准的 1:1 对应
|
||||
- Chrome DevTools → Toggle device toolbar → 选择 iPhone 6/7/8
|
||||
|
||||
### 每个页面至少截以下状态
|
||||
|
||||
| 状态 | 文件命名示例 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 默认/正常态 | `login--default.png` | 有数据的主要展示 |
|
||||
| 关键交互态 | `login--agreed.png` | 勾选协议后按钮变化 |
|
||||
| 空数据态 | `task-list--empty.png` | 如果适用 |
|
||||
| 弹窗/浮层 | `task-list--longpress-menu.png` | 弹窗打开状态 |
|
||||
| 筛选展开 | `board-customer--filter-open.png` | 下拉筛选展开 |
|
||||
|
||||
### 命名规范
|
||||
|
||||
```
|
||||
<page-name>--<state>.png
|
||||
```
|
||||
|
||||
- 页面名用小写连字符:`board-customer`、`task-detail`、`my-profile`
|
||||
- 状态名用小写连字符:`default`、`empty`、`loading`、`error`、`filter-open`、`longpress-menu`
|
||||
|
||||
### 存放目录
|
||||
|
||||
```
|
||||
docs/h5_ui/screenshots/
|
||||
```
|
||||
|
||||
### 长页面处理
|
||||
|
||||
如果页面有滚动内容,提供长截图:
|
||||
- Chrome DevTools → Ctrl+Shift+P → 输入 "Capture full size screenshot"
|
||||
|
||||
### 不需要的截图
|
||||
|
||||
- 不需要暗色模式截图(PRD 无暗色模式需求)
|
||||
- 不需要多设备截图(仅面向手机竖屏)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 五、资源文件规范
|
||||
|
||||
### 当前状况
|
||||
|
||||
`docs/h5_ui/img/` 只有 2 张图片,大部分图标是内联 SVG。
|
||||
|
||||
### 图标处理优先级
|
||||
|
||||
1. **优先使用 TDesign 内置图标**(`<t-icon name="xxx">`),不需要提供文件
|
||||
2. TDesign 没有的图标,提取为独立 SVG 文件
|
||||
3. 复杂图形/Logo 使用 PNG/JPG 图片
|
||||
|
||||
### 图标文件规范
|
||||
|
||||
| 项目 | 规范 |
|
||||
|------|------|
|
||||
| 格式 | SVG(图标)、PNG(带透明图片)、JPG(照片类) |
|
||||
| 命名 | `<category>-<name>.svg`,如 `icon-billiard.svg`、`icon-wechat.svg` |
|
||||
| 目录 | `apps/miniprogram/miniprogram/assets/icons/`(图标) |
|
||||
| | `apps/miniprogram/miniprogram/assets/images/`(图片) |
|
||||
| 尺寸 | 图片提供 2x 版本(750px 宽设计稿对应的实际像素) |
|
||||
|
||||
### 图标映射表(每个页面提供)
|
||||
|
||||
```markdown
|
||||
| H5 中的图标描述 | 处理方式 | 小程序引用 |
|
||||
|----------------|----------|-----------|
|
||||
| Logo 台球图标 | 自定义SVG | /assets/icons/icon-billiard.svg |
|
||||
| 任务管理图标 | TDesign | <t-icon name="task" /> |
|
||||
| 数据看板图标 | TDesign | <t-icon name="chart-bar" /> |
|
||||
| 智能助手图标 | TDesign | <t-icon name="chat" /> |
|
||||
| 微信图标 | 自定义SVG | /assets/icons/icon-wechat.svg |
|
||||
| 返回箭头 | TDesign | <t-icon name="chevron-left" /> |
|
||||
| 右箭头 | TDesign | <t-icon name="chevron-right" /> |
|
||||
```
|
||||
|
||||
保存为 `docs/h5_ui/icon-mapping.md`,或在每个页面的交互说明中附带。
|
||||
|
||||
---
|
||||
|
||||
## 六、目录结构总览
|
||||
|
||||
准备完成后,`docs/h5_ui/` 目录结构应如下:
|
||||
|
||||
```
|
||||
docs/h5_ui/
|
||||
├── css/ # 原有:自定义 CSS 文件
|
||||
├── img/ # 原有:图片资源
|
||||
├── js/ # 原有:JS 文件
|
||||
├── pages/ # 原有:H5 原型页面
|
||||
│ ├── login.html
|
||||
│ ├── board-customer.html
|
||||
│ └── ...
|
||||
├── rendered/ # 新增:渲染后的 DOM(可选)
|
||||
│ ├── login.html
|
||||
│ └── ...
|
||||
├── computed-styles.json # 新增:计算样式(可选,按页面名分 key)
|
||||
├── screenshots/ # 新增:页面截图(必须)
|
||||
│ ├── login--default.png
|
||||
│ ├── login--agreed.png
|
||||
│ ├── task-list--default.png
|
||||
│ ├── task-list--empty.png
|
||||
│ └── ...
|
||||
├── interactions/ # 新增:交互说明(必须)
|
||||
│ ├── login.md
|
||||
│ ├── task-list.md
|
||||
│ └── ...
|
||||
├── design-tokens.json # 新增:设计 Token(必须,做一次)
|
||||
├── icon-mapping.md # 新增:图标映射表(必须,做一次)
|
||||
└── index.html # 原有:入口页
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、操作步骤——"怎么喂料"
|
||||
|
||||
### 一次性准备(全局)
|
||||
|
||||
1. 创建 `docs/h5_ui/design-tokens.json`(从 `tailwind.config` 提取,见第二节)
|
||||
2. 创建 `docs/h5_ui/icon-mapping.md`(全局图标映射表,见第五节)
|
||||
3. 创建目录:`screenshots/`、`interactions/`、`rendered/`(可选)
|
||||
|
||||
### 每个页面的准备流程
|
||||
|
||||
#### Step 1:准备 HTML + 计算样式
|
||||
1. 在 Chrome 中打开 `docs/h5_ui/pages/<page>.html`
|
||||
2. 切换到 iPhone 6/7/8 设备模式(375×667)
|
||||
3. 在 Console 中运行 `exportStyles()` 脚本(见第一节)
|
||||
4. 将输出追加到 `docs/h5_ui/computed-styles.json`(以页面名为 key)
|
||||
5. (可选)复制渲染后 DOM,保存为 `docs/h5_ui/rendered/<page>.html`
|
||||
|
||||
#### Step 2:截图
|
||||
1. Chrome DevTools → Ctrl+Shift+P → "Capture screenshot"
|
||||
2. 每个状态截一张,命名为 `<page>--<state>.png`
|
||||
3. 放入 `docs/h5_ui/screenshots/`
|
||||
|
||||
#### Step 3:写交互说明
|
||||
1. 按第三节格式模板,写出状态变量、操作→响应表、状态枚举
|
||||
2. 保存为 `docs/h5_ui/interactions/<page>.md`
|
||||
|
||||
#### Step 4:标注图标/图片
|
||||
1. 检查页面中的所有图标和图片
|
||||
2. 在图标映射表中补充新图标的处理方式
|
||||
3. 自定义资源放入小程序 `assets/` 对应目录
|
||||
|
||||
#### Step 5:喂给 AI
|
||||
在对话中提供以下信息:
|
||||
|
||||
```
|
||||
请将 <page-name> 页面转换为小程序。
|
||||
- HTML:docs/h5_ui/pages/<page>.html
|
||||
- 计算样式:docs/h5_ui/computed-styles.json(页面 key: <page>)
|
||||
- 截图:docs/h5_ui/screenshots/<page>__default.png(拖入对话)
|
||||
- 交互说明:docs/h5_ui/interactions/<page>.md
|
||||
- PRD 参考:P<N> spec <章节号>
|
||||
- 图标映射:全部使用 TDesign / 见 icon-mapping.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、最低限度清单
|
||||
|
||||
如果时间有限,以下三项是"必须提供"的底线:
|
||||
|
||||
| 优先级 | 材料 | 作用 | 频率 |
|
||||
|--------|------|------|------|
|
||||
| P0 | 截图(默认态,375px 宽) | 校验还原度的唯一视觉参考 | 每页 |
|
||||
| P0 | 交互说明(状态变量 + 操作响应表) | 避免逻辑错误 | 每页 |
|
||||
| P0 | 设计 Token JSON | 避免颜色/间距换算错误 | 一次 |
|
||||
| P1 | 计算样式 JSON | 显著提高还原度 | 每页(可选) |
|
||||
| P1 | 图标映射表 | 避免图标处理失误 | 一次 + 增量 |
|
||||
| P2 | 渲染后 DOM | 复杂页面时有帮助 | 按需 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文档
|
||||
|
||||
- [H5 → 小程序转换避坑指南](./h5-to-miniprogram-pitfalls.md) — 标签映射、样式差异、事件系统、高频踩坑清单
|
||||
- [小程序认证系统联调指南](./auth-integration-guide.md) — 前后端联调测试流程
|
||||
- [产品需求文档](./prd.md) — 完整 PRD(页面级需求)
|
||||
- PRD Specs — `docs/prd/specs/P1~P11`
|
||||
- H5 原型 — `docs/h5_ui/pages/`
|
||||
476
apps/miniprogram - 副本/doc/h5-to-miniprogram-pitfalls.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# H5 → 微信小程序转换避坑指南
|
||||
|
||||
> 基于本项目 `docs/h5_ui/` 原型与 `apps/miniprogram/miniprogram/pages/` 已转换页面的实际对比,结合微信小程序官方文档整理。
|
||||
> 适用于后续页面开发(如 board-coach、task-list、customer-detail 等)的快速参考。
|
||||
|
||||
---
|
||||
|
||||
## 一、WXML vs HTML — 标签与结构
|
||||
|
||||
### 1.1 标签映射表
|
||||
|
||||
| H5 (HTML) | 小程序 (WXML) | 说明 |
|
||||
|---------------------|----------------------|------|
|
||||
| `<div>` | `<view>` | 最基础的容器 |
|
||||
| `<span>` / `<p>` | `<text>` | 文本必须用 `<text>` 包裹才可选中/换行 |
|
||||
| `<a href="...">` | `<navigator url="">` | 或用 `wx.navigateTo()` 编程式跳转 |
|
||||
| `<img src="...">` | `<image src="" mode="">` | 必须指定 `mode`,默认 320×240 |
|
||||
| `<input>` | `<input>` 或 `<t-input>` | 原生 input 事件名不同;推荐 TDesign |
|
||||
| `<textarea>` | `<textarea>` 或 `<t-textarea>` | 同上 |
|
||||
| `<button>` | `<button>` 或 `<t-button>` | 小程序 button 有 `open-type` 能力 |
|
||||
| `<ul>/<li>` | `<view wx:for="{{list}}">` | 没有列表语义标签 |
|
||||
| `<select>` | `<picker>` 或 `<t-picker>` | 完全不同的交互模式 |
|
||||
| `<label for="id">` | `<label for="id">` | 支持,但 for 只能绑定 checkbox/radio/switch |
|
||||
| `<svg>` 内联 | `<image src="xx.svg">` | 不支持内联 SVG,只能作为图片引用 |
|
||||
| `<iframe>` | `<web-view>` | 需配置业务域名白名单 |
|
||||
|
||||
### 1.2 实际对比:login 页面
|
||||
|
||||
**H5 原型** — 内联 SVG 图标:
|
||||
```html
|
||||
<svg class="w-14 h-14 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" .../>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**小程序转换** — 改为 image 引用:
|
||||
```xml
|
||||
<image class="logo-icon" src="/assets/icons/logo-billiard.svg" mode="aspectFit" />
|
||||
```
|
||||
|
||||
> **坑**:小程序不支持内联 SVG。所有 SVG 图标需提取为独立 `.svg` 文件放到 `assets/icons/`,通过 `<image>` 引用。
|
||||
|
||||
### 1.3 不存在的标签/属性
|
||||
|
||||
| H5 特性 | 小程序替代方案 |
|
||||
|---------|---------------|
|
||||
| `<h1>`~`<h6>` | `<text>` + 样式类 |
|
||||
| `<table>` | `<view>` 手动布局 |
|
||||
| `<form>` + `<input name>` | 小程序 `<form>` 或直接 `setData` 收集 |
|
||||
| `onclick="fn()"` | `bindtap="fn"` 或 `bind:tap="fn"` |
|
||||
| `class="a b c"` 动态 | `class="base {{condition ? 'a' : 'b'}}"` |
|
||||
| `innerHTML` | `<rich-text nodes="{{html}}">` |
|
||||
|
||||
---
|
||||
|
||||
## 二、WXSS vs CSS — 样式差异
|
||||
|
||||
### 2.1 支持的选择器(有限)
|
||||
|
||||
| 选择器 | 支持 | 说明 |
|
||||
|--------|------|------|
|
||||
| `.class` | ✅ | |
|
||||
| `#id` | ✅ | |
|
||||
| `element` (如 `view`) | ✅ | |
|
||||
| `element, element` | ✅ | 群组选择器 |
|
||||
| `::before` / `::after` | ✅ | |
|
||||
| `*` 通配符 | ❌ | Tailwind 的 `*` reset 全部失效 |
|
||||
| `>` 子选择器 | ⚠️ | 部分版本支持,不推荐依赖 |
|
||||
| `+` / `~` 兄弟选择器 | ⚠️ | 同上 |
|
||||
| `:nth-child()` | ⚠️ | 部分支持 |
|
||||
| `@media` | ✅ | 支持,但用 rpx 更好 |
|
||||
|
||||
### 2.2 rpx 单位 — 最大差异
|
||||
|
||||
H5 用 `px`/`rem`/`vw`,小程序用 `rpx`(responsive pixel):
|
||||
- 屏幕宽度固定 = 750rpx
|
||||
- iPhone6 上 1rpx = 0.5px,即 1px = 2rpx
|
||||
- 设计稿以 750px 宽为基准时,数值直接写 rpx
|
||||
|
||||
**实际对比:login 页面**
|
||||
|
||||
H5 原型(Tailwind):
|
||||
```html
|
||||
<div class="w-24 h-24 rounded-3xl"> <!-- 96px × 96px -->
|
||||
```
|
||||
|
||||
小程序转换:
|
||||
```css
|
||||
.logo-box {
|
||||
width: 192rpx; /* 96px × 2 = 192rpx */
|
||||
height: 192rpx;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
```
|
||||
|
||||
> **换算规则**:H5 的 px 值 × 2 = rpx 值(基于 750 宽设计稿)。
|
||||
|
||||
### 2.2.1 H5 → 小程序全局缩放比例(87.5%)
|
||||
|
||||
H5 原型基于 375px 视口设计(iPhone SE/6/7/8),直接 ×2 转 rpx 后在大屏手机(iPhone 15 Pro Max,430pt 宽)上元素偏大。经实测对比,对所有 rpx 值统一乘以 0.875 缩放系数后视觉效果最佳。
|
||||
|
||||
**规则**:
|
||||
- 尺寸、间距、圆角、阴影偏移:`H5 px × 2 × 0.875` = 最终 rpx,取偶数
|
||||
- 字号:同上规则,如 `text-2xl`(24px) → 48rpx × 0.875 = 42rpx
|
||||
- t-icon size:同上规则,如 `w-5`(20px) → 40rpx × 0.875 = 35rpx
|
||||
- `max-width` 等约束宽度:同上规则
|
||||
- 背景纹理间距(如十字纹 `bg-pattern`):不缩放,保持原值
|
||||
|
||||
**换算速查**(常用 Tailwind 值):
|
||||
|
||||
| Tailwind | H5 px | 原始 rpx | ×0.875 | 取整 rpx |
|
||||
|----------|-------|----------|--------|----------|
|
||||
| `text-xs` (12px) | 12 | 24 | 21 | 22 |
|
||||
| `text-sm` (14px) | 14 | 28 | 24.5 | 24 |
|
||||
| `text-base` (16px) | 16 | 32 | 28 | 28 |
|
||||
| `text-lg` (18px) | 18 | 36 | 31.5 | 32 |
|
||||
| `text-xl` (20px) | 20 | 40 | 35 | 36 |
|
||||
| `text-2xl` (24px) | 24 | 48 | 42 | 42 |
|
||||
| `gap-2` / `p-2` (8px) | 8 | 16 | 14 | 14 |
|
||||
| `gap-3` / `p-3` (12px) | 12 | 24 | 21 | 22 |
|
||||
| `gap-4` / `p-4` (16px) | 16 | 32 | 28 | 28 |
|
||||
| `gap-5` / `p-5` (20px) | 20 | 40 | 35 | 36 |
|
||||
| `gap-6` / `p-6` (24px) | 24 | 48 | 42 | 42 |
|
||||
| `gap-8` / `p-8` (32px) | 32 | 64 | 56 | 56 |
|
||||
| `w-10` / `h-10` (40px) | 40 | 80 | 70 | 70 |
|
||||
| `w-14` / `h-14` (56px) | 56 | 112 | 98 | 98 |
|
||||
| `w-24` / `h-24` (96px) | 96 | 192 | 168 | 168 |
|
||||
| `w-28` / `h-28` (112px) | 112 | 224 | 196 | 196 |
|
||||
| `rounded-xl` (12px) | 12 | 24 | 21 | 22 |
|
||||
| `rounded-2xl` (16px) | 16 | 32 | 28 | 28 |
|
||||
| `rounded-3xl` (24px) | 24 | 48 | 42 | 42 |
|
||||
|
||||
> **来源**:no-permission 页面实测确定。先后尝试了非统一缩放、80%、87.5% 三种方案,87.5% 在 iPhone 15 Pro Max 上与 H5 原型视觉一致度最高。后续所有页面转换统一使用此系数。
|
||||
|
||||
### 2.3 Tailwind CSS → 手写 WXSS
|
||||
|
||||
小程序不支持 Tailwind CSS(无构建链集成),所有 Tailwind 工具类必须手写为 WXSS。
|
||||
|
||||
| Tailwind 类 | WXSS 等价写法 |
|
||||
|-------------|--------------|
|
||||
| `flex flex-col items-center` | `display: flex; flex-direction: column; align-items: center;` |
|
||||
| `gap-4` | `gap: 32rpx;`(4 × 8px × 2rpx) |
|
||||
| `p-5` | `padding: 40rpx;` |
|
||||
| `rounded-2xl` | `border-radius: 32rpx;` |
|
||||
| `text-sm` | `font-size: 28rpx;` |
|
||||
| `text-gray-7` | `color: #8b8b8b;` |
|
||||
| `bg-white/60` | `background: rgba(255,255,255,0.6);` |
|
||||
| `backdrop-blur-sm` | ❌ 不支持 `backdrop-filter` |
|
||||
| `shadow-lg` | `box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);` |
|
||||
| `min-h-screen` | `min-height: 100vh;` |
|
||||
|
||||
### 2.4 不支持/有差异的 CSS 特性
|
||||
|
||||
| CSS 特性 | 小程序支持情况 | 替代方案 |
|
||||
|----------|---------------|---------|
|
||||
| `backdrop-filter: blur()` | ❌ 不支持 | 用半透明背景色模拟 |
|
||||
| `position: fixed` | ⚠️ 部分场景异常 | 用 `position: sticky` 或组件自带吸顶 |
|
||||
| `@import url()` 远程 | ❌ | 只支持本地 `@import "path.wxss"` |
|
||||
| `@font-face` 远程字体 | ⚠️ | 需 `wx.loadFontFace()` 动态加载 |
|
||||
| CSS 变量 `var()` | ✅ 支持 | TDesign 大量使用 |
|
||||
| `linear-gradient` | ✅ 支持 | 正常使用 |
|
||||
| `animation` / `@keyframes` | ✅ 支持 | 正常使用 |
|
||||
| `transition` | ✅ 支持 | 正常使用 |
|
||||
| `env(safe-area-inset-*)` | ✅ 支持 | 刘海屏适配必用 |
|
||||
|
||||
### 2.5 样式作用域
|
||||
|
||||
- `app.wxss` = 全局样式
|
||||
- 页面 `.wxss` = 仅当前页面生效(自动隔离)
|
||||
- 组件 `.wxss` = 默认隔离(`styleIsolation: 'isolated'`)
|
||||
|
||||
> **坑**:H5 的全局 CSS reset(如 `* { box-sizing: border-box; }`)在小程序中无效。需要在每个需要的元素上手动设置 `box-sizing`。
|
||||
|
||||
---
|
||||
|
||||
## 三、事件系统 — 最容易踩坑
|
||||
|
||||
### 3.1 事件绑定对比
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `onclick="fn()"` | `bindtap="fn"` | 不能传参! |
|
||||
| `onclick="fn(1)"` | `data-id="1" bindtap="fn"` | 通过 dataset 传参 |
|
||||
| `addEventListener` | 不支持 | 只能在 WXML 中声明式绑定 |
|
||||
| `event.target.value` | `e.detail.value` | 取值路径不同 |
|
||||
| `event.preventDefault()` | `catchtap` | 用 catch 前缀阻止冒泡 |
|
||||
| `event.stopPropagation()` | `catchtap` | 同上 |
|
||||
|
||||
### 3.2 传参方式
|
||||
|
||||
**H5**:
|
||||
```html
|
||||
<button onclick="handleClick(item.id, item.name)">点击</button>
|
||||
```
|
||||
|
||||
**小程序**:
|
||||
```xml
|
||||
<view data-id="{{item.id}}" data-name="{{item.name}}" bindtap="handleClick">点击</view>
|
||||
```
|
||||
```typescript
|
||||
handleClick(e: WechatMiniprogram.TouchEvent) {
|
||||
const { id, name } = e.currentTarget.dataset
|
||||
}
|
||||
```
|
||||
|
||||
> **坑**:`data-` 属性名会自动转换 — 连字符转驼峰(`data-user-id` → `dataset.userId`),大写转小写(`data-userId` → `dataset.userid`)。
|
||||
|
||||
### 3.3 实际对比:login 页面的协议勾选
|
||||
|
||||
**H5 原型**:
|
||||
```html
|
||||
<input type="checkbox" id="agreeCheckbox" onchange="updateButtonState()">
|
||||
<script>
|
||||
checkbox.addEventListener('change', updateButtonState);
|
||||
</script>
|
||||
```
|
||||
|
||||
**小程序转换**:
|
||||
```xml
|
||||
<view class="agreement" bindtap="onAgreeChange">
|
||||
<view class="checkbox {{agreed ? 'checkbox--checked' : ''}}">
|
||||
<t-icon wx:if="{{agreed}}" name="check" size="20rpx" color="#fff" />
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
```typescript
|
||||
onAgreeChange() {
|
||||
this.setData({ agreed: !this.data.agreed })
|
||||
}
|
||||
```
|
||||
|
||||
> **坑**:小程序没有原生 checkbox 的 `checked` 双向绑定,需要手动用 `setData` + 条件样式类模拟。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据绑定与渲染
|
||||
|
||||
### 4.1 模板语法对比
|
||||
|
||||
| 功能 | H5 (原生/框架) | 小程序 WXML |
|
||||
|------|----------------|-------------|
|
||||
| 插值 | `${variable}` / `{{variable}}` | `{{variable}}` |
|
||||
| 条件渲染 | `if/else` + DOM 操作 | `wx:if` / `wx:elif` / `wx:else` |
|
||||
| 列表渲染 | `forEach` + `innerHTML` | `wx:for="{{list}}" wx:key="id"` |
|
||||
| 显示/隐藏 | `style.display = 'none'` | `hidden="{{!show}}"` 或 `wx:if` |
|
||||
| 动态 class | `classList.toggle()` | `class="base {{active ? 'on' : ''}}"` |
|
||||
| 动态 style | `element.style.color = 'red'` | `style="color: {{color}};"` |
|
||||
|
||||
### 4.2 wx:if vs hidden
|
||||
|
||||
```xml
|
||||
<!-- wx:if:条件为 false 时不渲染 DOM,切换时销毁/重建 -->
|
||||
<view wx:if="{{status === 'pending'}}">审核中</view>
|
||||
|
||||
<!-- hidden:始终渲染,只切换 display -->
|
||||
<view hidden="{{status !== 'pending'}}">审核中</view>
|
||||
```
|
||||
|
||||
- 频繁切换 → 用 `hidden`(避免重复创建销毁)
|
||||
- 初始条件不太可能变 → 用 `wx:if`(减少初始渲染量)
|
||||
|
||||
### 4.3 实际对比:reviewing 页面的条件渲染
|
||||
|
||||
**H5 原型**:只有一种状态(审核中),用静态 HTML。
|
||||
|
||||
**小程序转换**:支持 pending/rejected 两种状态,用 `wx:if` 动态切换:
|
||||
```xml
|
||||
<view class="top-gradient top-gradient--{{status}}"></view>
|
||||
<t-icon wx:if="{{status === 'pending'}}" name="time" size="112rpx" color="#fff" />
|
||||
<t-icon wx:else name="close-circle" size="112rpx" color="#fff" />
|
||||
<text class="main-title">{{status === 'pending' ? '申请审核中' : '申请未通过'}}</text>
|
||||
```
|
||||
|
||||
> **坑**:`wx:if` 中的表达式必须在 `{{}}` 内,且不支持复杂 JS 表达式(如函数调用)。需要复杂逻辑时用 WXS 或在 JS 中预处理好数据。
|
||||
|
||||
---
|
||||
|
||||
## 五、路由与导航
|
||||
|
||||
### 5.1 对比
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `window.location.href = 'xx.html'` | `wx.navigateTo({ url: '/pages/xx/xx' })` | 保留当前页,跳新页 |
|
||||
| `window.location.replace()` | `wx.redirectTo()` | 关闭当前页,跳新页 |
|
||||
| `history.back()` | `wx.navigateBack()` | 返回上一页 |
|
||||
| 无直接等价 | `wx.reLaunch()` | 关闭所有页面,打开新页 |
|
||||
| 无直接等价 | `wx.switchTab()` | 跳转 tabBar 页面 |
|
||||
|
||||
### 5.2 实际对比:reviewing 页面的"更换账号"
|
||||
|
||||
**H5 原型**:
|
||||
```javascript
|
||||
function switchAccount() {
|
||||
localStorage.clear();
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
```
|
||||
|
||||
**小程序转换**:
|
||||
```typescript
|
||||
onSwitchAccount() {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = undefined
|
||||
wx.removeStorageSync("token")
|
||||
wx.removeStorageSync("refreshToken")
|
||||
wx.reLaunch({ url: "/pages/login/login" }) // 清空页面栈
|
||||
}
|
||||
```
|
||||
|
||||
> **坑**:
|
||||
> - 页面栈最多 10 层,`navigateTo` 超过会静默失败
|
||||
> - 跳转 tabBar 页面必须用 `switchTab`,用 `navigateTo` 会报错
|
||||
> - 路径必须以 `/` 开头,且不带 `.wxml` 后缀
|
||||
> - `reLaunch` 会销毁所有页面,适合登录/登出等场景
|
||||
|
||||
---
|
||||
|
||||
## 六、存储与网络
|
||||
|
||||
### 6.1 本地存储
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `localStorage.setItem(k, v)` | `wx.setStorageSync(k, v)` | 同步写入 |
|
||||
| `localStorage.getItem(k)` | `wx.getStorageSync(k)` | 同步读取 |
|
||||
| `localStorage.removeItem(k)` | `wx.removeStorageSync(k)` | 同步删除 |
|
||||
| `localStorage.clear()` | `wx.clearStorageSync()` | 清空全部 |
|
||||
| 上限 ~5MB | 上限 10MB | 小程序更大 |
|
||||
|
||||
> **坑**:小程序没有 Cookie,登录态必须自行通过 Storage + header token 管理。
|
||||
|
||||
### 6.2 网络请求
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `fetch()` / `XMLHttpRequest` | `wx.request()` | 需配置域名白名单 |
|
||||
| 无限制 | 并发上限 10 个 | 超出排队 |
|
||||
| 任意域名 | 必须 HTTPS + 白名单 | 开发时可关闭校验 |
|
||||
|
||||
---
|
||||
|
||||
## 七、TDesign 组件替代 H5 原生元素
|
||||
|
||||
本项目使用 TDesign 小程序组件库,以下是常见替代关系:
|
||||
|
||||
| H5 原型元素 | TDesign 组件 | 注意事项 |
|
||||
|-------------|-------------|---------|
|
||||
| `<button>` | `<t-button>` | 用 CSS 变量定制样式,如 `--td-button-large-height` |
|
||||
| `<input>` | `<t-input>` | 事件是 `bind:change` 而非 `bindinput` |
|
||||
| `<textarea>` | `<t-textarea>` | 同上 |
|
||||
| `<select>` | `<t-picker>` | 完全不同的交互 |
|
||||
| `<checkbox>` | `<t-checkbox>` | 或手动实现(如 login 页) |
|
||||
| `<radio>` | `<t-radio>` / `<t-radio-group>` | `bind:change` 取值 |
|
||||
| SVG 图标 | `<t-icon name="xxx">` | TDesign 内置图标库 |
|
||||
| 加载动画 | `<t-loading>` | 替代 CSS spinner |
|
||||
| 弹窗 | `<t-dialog>` / `<t-toast>` | 替代 `alert()` / `confirm()` |
|
||||
| 下拉刷新 | 页面 `onPullDownRefresh` | 在 page.json 中 `enablePullDownRefresh: true` |
|
||||
|
||||
### TDesign 样式覆盖 4 种方式
|
||||
|
||||
1. **CSS 变量**(推荐):`--td-button-large-height: 96rpx`
|
||||
2. **外部样式类**:`t-class="my-class"` + `.my-class { ... !important }`
|
||||
3. **解除隔离**:TDesign 已开启 `addGlobalClass`,页面样式可直接覆盖
|
||||
4. **style 属性**:`style="background: #f5f5f5; border-radius: 16rpx;"`
|
||||
|
||||
**实际示例**(login 页面按钮定制):
|
||||
```css
|
||||
.login-btn {
|
||||
--td-button-large-height: 96rpx !important;
|
||||
--td-button-large-font-size: 32rpx !important;
|
||||
--td-button-border-radius: 24rpx !important;
|
||||
}
|
||||
.login-btn--active {
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6) !important;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、高频踩坑清单
|
||||
|
||||
### 8.1 结构层
|
||||
|
||||
| # | 坑 | 说明 | 解决方案 |
|
||||
|---|-----|------|---------|
|
||||
| 1 | 内联 SVG 不支持 | WXML 不能写 `<svg>` 标签 | 提取为 `.svg` 文件,用 `<image>` 或 `<t-icon>` |
|
||||
| 2 | 没有 DOM API | `document.getElementById` 等全部不可用 | 用 `this.setData()` 驱动视图更新 |
|
||||
| 3 | `<text>` 内只能嵌套 `<text>` | 不能在 `<text>` 内放 `<view>` | 需要块级布局时外层用 `<view>` |
|
||||
| 4 | `checked="false"` 是 true | 字符串 `"false"` 是 truthy | 必须写 `checked="{{false}}"` |
|
||||
| 5 | `wx:key` 必须提供 | 列表渲染不加 key 会警告且性能差 | `wx:key="id"` 或 `wx:key="*this"` |
|
||||
| 6 | `<block>` 不渲染 DOM | 只是逻辑包裹,不产生真实节点 | 需要样式时改用 `<view>` |
|
||||
|
||||
### 8.2 样式层
|
||||
|
||||
| # | 坑 | 说明 | 解决方案 |
|
||||
|---|-----|------|---------|
|
||||
| 7 | `*` 选择器无效 | 全局 reset 失效 | 逐个元素设置 `box-sizing` |
|
||||
| 8 | `backdrop-filter` 不支持 | 毛玻璃效果无法实现 | 用半透明背景色 `rgba()` 近似 |
|
||||
| 9 | `image` 默认 320×240 | 不设宽高会变形 | 始终指定 `width`/`height` + `mode` |
|
||||
| 10 | rpx 小数精度 | 1rpx 在某些设备上不显示 | 边框最小用 2rpx |
|
||||
| 11 | 组件样式隔离 | 页面样式穿不进自定义组件 | 用外部样式类或 `styleIsolation: 'shared'` |
|
||||
| 12 | `!important` 滥用 | TDesign 组件内部样式优先级高 | 优先用 CSS 变量覆盖 |
|
||||
|
||||
### 8.3 逻辑层
|
||||
|
||||
| # | 坑 | 说明 | 解决方案 |
|
||||
|---|-----|------|---------|
|
||||
| 13 | `setData` 性能 | 大数据量传输卡顿 | 只传变化字段:`'list[2].name': 'new'` |
|
||||
| 14 | 没有 Cookie | 登录态不能靠 Cookie | Storage + header token |
|
||||
| 15 | `eval()` 不可用 | 动态代码执行被禁止 | 预编译逻辑 |
|
||||
| 16 | 页面栈 10 层限制 | `navigateTo` 超过 10 层静默失败 | 合理使用 `redirectTo` / `reLaunch` |
|
||||
| 17 | `alert()` 不存在 | 没有浏览器弹窗 API | `wx.showToast()` / `wx.showModal()` |
|
||||
| 18 | `window` / `document` 不存在 | 所有 Web API 不可用 | 用 `wx.*` API 替代 |
|
||||
|
||||
### 8.4 TDesign 相关
|
||||
|
||||
| # | 坑 | 说明 | 解决方案 |
|
||||
|---|-----|------|---------|
|
||||
| 19 | `style: v2` 冲突 | app.json 中的 `"style": "v2"` 导致样式错乱 | 删除该配置 |
|
||||
| 20 | npm 构建遗忘 | 安装新包后忘记构建 npm | 每次 `npm install` 后在开发者工具中"构建 npm" |
|
||||
| 21 | 事件名差异 | TDesign 用 `bind:change`,原生用 `bindinput` | 查阅组件文档确认事件名 |
|
||||
| 22 | 外部样式类命名 | `t-class` / `t-class-input` 等各组件不同 | 查阅组件文档的 External Classes |
|
||||
|
||||
---
|
||||
|
||||
## 九、转换 Checklist(新页面开发用)
|
||||
|
||||
开发新页面时,按此清单逐项检查:
|
||||
|
||||
- [ ] HTML 标签全部替换为 WXML 组件(`div→view`、`span→text`、`img→image`)
|
||||
- [ ] 内联 SVG 提取为文件,改用 `<image>` 或 `<t-icon>`
|
||||
- [ ] Tailwind 类全部手写为 WXSS(px × 2 × 0.875 = rpx,见 §2.2.1 缩放规则)
|
||||
- [ ] `backdrop-filter` 等不支持的 CSS 改为替代方案
|
||||
- [ ] 事件绑定改为 `bindtap` / `bind:change`,传参用 `data-*`
|
||||
- [ ] `alert/confirm` 改为 `wx.showToast` / `wx.showModal`
|
||||
- [ ] `localStorage` 改为 `wx.setStorageSync`
|
||||
- [ ] 路由跳转改为 `wx.navigateTo` / `wx.reLaunch` 等
|
||||
- [ ] 表单收集改为 `setData` + 事件回调
|
||||
- [ ] 图片设置 `mode` 属性(`aspectFit` / `aspectFill` / `widthFix`)
|
||||
- [ ] 列表渲染加 `wx:key`
|
||||
- [ ] 布尔属性用 `{{}}` 包裹(`checked="{{true}}"`)
|
||||
- [ ] TDesign 组件在页面 `.json` 中注册 `usingComponents`
|
||||
- [ ] 安全区适配:`padding-top: env(safe-area-inset-top)`
|
||||
- [ ] 页面配置:`enablePullDownRefresh`、`navigationBarTitleText` 等
|
||||
|
||||
---
|
||||
|
||||
## 十、board-customer 迁移经验补充
|
||||
|
||||
> 来源:board-customer 页面 8 维度卡片迁移实战(2026-03-07)
|
||||
|
||||
### 10.1 复杂维度用独立布局
|
||||
|
||||
最专一维度的助教明细表不适合用通用的 `card-mid-row` 或 `card-grid`,直接用独立的 `loyal-table` 布局(左侧竖线 `border-left: 4rpx solid #eee` + 表头 + 数据行)。同时在助教行的 `wx:if` 中排除 `dimType !== 'loyal'`,避免信息重复。
|
||||
|
||||
**关键**:当某个维度的卡片结构与其他维度差异过大时,不要硬套通用模板,直接写独立布局更清晰。
|
||||
|
||||
### 10.2 heart-icon 组件:TS observer 替代 WXS
|
||||
|
||||
小程序 WXS 不支持 emoji surrogate pair(如 `\uD83D\uDC96`),渲染为乱码。解决方案:
|
||||
- 用 TS `observers` 监听 `score` 属性变化,计算对应 emoji 字符串
|
||||
- WXML 中用 `{{heartEmoji}}` 数据绑定渲染
|
||||
- 样式:`font-size: 22rpx; line-height: 1; position: relative; top: -4rpx` 和文字对齐
|
||||
|
||||
### 10.3 助教字体颜色三态 + badge 渐变
|
||||
|
||||
助教名字颜色规则(通过 CSS class 控制):
|
||||
- 跟 badge → `.assistant--assignee`:`color: #e34d59; font-weight: 700`(红色加粗)
|
||||
- 弃 badge → `.assistant--abandoned`:`color: #a6a6a6`(灰色)
|
||||
- 无 badge → `.assistant--normal`:`color: #242424`(黑色)
|
||||
|
||||
Badge 样式(白字 + 渐变背景 + 阴影):
|
||||
- 跟:`background: linear-gradient(135deg, #e34d59, #f26a76); box-shadow: 0 2rpx 6rpx rgba(227,77,89,0.28)`
|
||||
- 弃:`background: linear-gradient(135deg, #d4d4d4, #b6b6b6); box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.14)`
|
||||
97
apps/miniprogram - 副本/doc/howtodo.md
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
|
||||
1) 迁移前审计与准备(强烈建议先跑这个)
|
||||
你是一名资深微信小程序前端架构师+CSS布局专家。我要把一套 Web 页面(HTML/CSS/少量JS)迁移为“原生微信小程序”(WXML/WXSS/JS/JSON),要求结构与样式细节还原度尽可能高。
|
||||
|
||||
输入(我将分段提供):
|
||||
- 页面HTML(可含多页面)
|
||||
- 全量CSS(含reset/公共样式)
|
||||
- 资源清单(图片/字体/图标svg等路径)
|
||||
- 若有:目标效果截图/设计稿
|
||||
|
||||
请先不要写最终代码,先做《迁移审计与准备报告》,输出必须包含以下部分,并使用清晰的编号标题:
|
||||
|
||||
A. 页面结构清单:列出每个页面的主要区域(header/hero/list/card/footer等)及建议组件化边界(哪些抽组件、哪些保留页面内)
|
||||
B. CSS复杂度审计:列出会影响小程序还原的高风险点(如 position: sticky、复杂选择器、伪元素、滤镜、背景混合、滚动容器、字体/line-height差异、overflow裁切、z-index层叠等),并给出在小程序里的替代策略
|
||||
C. HTML→WXML映射规则:语义标签映射、事件绑定映射、表单控件映射、列表渲染(wx:for)策略
|
||||
D. CSS→WXSS映射规则:单位策略(px→rpx换算方案、断点策略)、选择器扁平化策略、样式隔离策略(页面/组件wxss组织)、必要时是否引入原子化类(如仅用基础class,不引第三方)
|
||||
E. 资源处理:图片/字体/图标(svg转png或iconfont方案)、2x/3x图、网络图片策略、包体与分包建议
|
||||
F. 产物目录规划:给出建议的最终目录树(pages/、components/、styles/、assets/),并说明每个文件职责
|
||||
G. 最小补充信息清单:如果缺少信息,列出“为了高还原必须补充的最小信息”(例如:页面宽度基准、设计稿字号、是否允许自适应裁切等)
|
||||
|
||||
输出要求:表格+要点并存;每个高风险点给出“原因 + 影响 + 处理方案 + 验收方式”。
|
||||
|
||||
2) 生成第一版代码(要求目录树+逐文件输出)
|
||||
基于你刚才的《迁移审计与准备报告》,现在开始生成可运行的小程序第一版代码(优先保证结构与样式还原、其次是交互完整度)。
|
||||
|
||||
硬性要求:
|
||||
A.输出完整目录树(含 pages、components、styles、assets)
|
||||
B.按文件逐个输出内容:.wxml / .wxss / .js / .json
|
||||
C.样式组织:公共样式抽到 styles/common.wxss(或等价方案),页面只放页面差异
|
||||
D.尽量避免依赖第三方库;如必须引入,必须说明理由、替代方案、以及如何降低包体影响
|
||||
E.对所有“Web里有但小程序不完全支持”的效果:必须写清替代实现与预期差异
|
||||
|
||||
我将提供:
|
||||
- <PAGE_NAME_1> 的HTML:<<<...>>>
|
||||
- 全量CSS:<<<...>>>
|
||||
- 可选:截图/设计稿要点:<<<...>>>
|
||||
|
||||
请输出:
|
||||
I. 目录树
|
||||
II. 逐文件代码(从 app.json/app.wxss/app.js 开始)
|
||||
III. 编译与运行说明(微信开发者工具中需注意的设置项)
|
||||
IV. 第一版“已知差异清单”(按严重度排序:P0/P1/P2)
|
||||
|
||||
3) 高保真样式“对齐增强”专用提示词(第二轮开始用)
|
||||
现在进入“高保真样式对齐”阶段。目标:在不破坏现有结构的前提下,把视觉细节尽量贴近 Web/设计稿。
|
||||
|
||||
我会提供:
|
||||
- 当前小程序代码片段(相关 wxml/wxss)
|
||||
- Web 参考(对应HTML/CSS片段)或截图差异描述
|
||||
- 具体差异点列表(例如:间距、对齐、字体、行高、圆角、阴影、图片裁切、列表间距、按钮高度等)
|
||||
|
||||
你的任务:
|
||||
A. 对每个差异点,给出:根因判断(布局/单位/行高/盒模型/层叠/渲染差异)+ 最小修改方案 + 修改后风险
|
||||
B. 输出“补丁式修改”:只给出需要改动的文件与改动段落(像 git diff 一样),不要整文件重贴
|
||||
C. 给出验收步骤:在微信开发者工具与真机预览分别怎么验证
|
||||
|
||||
约束:
|
||||
- 优先使用 flex/盒模型的确定性方案,尽量减少依赖“碰运气”的魔法数
|
||||
- px→rpx 的换算要统一,严禁同一类间距一会儿 rpx 一会儿 px
|
||||
4) 结构还原与组件化质量提升(防止“越改越乱”)
|
||||
请对当前小程序实现做一次“结构与可维护性重构”,目标是在不改变 UI 效果的情况下,提高组件化与样式可控性,从而提升后续对齐效率。
|
||||
|
||||
输出必须包含:
|
||||
A.现状问题清单(重复样式、选择器过深、耦合、命名不一致、难以复用的结构)
|
||||
B.组件拆分方案(组件名、props、slot策略、事件、数据流)
|
||||
C.样式治理方案(BEM/命名规范、公共变量、间距/字号/圆角/阴影的设计token化)
|
||||
D.重构后的目录树
|
||||
E.逐文件补丁(diff风格),并说明每个改动为何不影响UI
|
||||
|
||||
约束:不引第三方UI库;不改变页面路由与业务数据接口(若有)。
|
||||
|
||||
|
||||
5) 细致对比与验收清单(你要的“对比更细致”)
|
||||
请为本次迁移输出《像素级对比与验收清单》,用于我逐项对照 Web 版本与小程序版本。
|
||||
|
||||
要求:
|
||||
- 按页面输出(每页一个小节)
|
||||
- 每页包含:布局结构、间距系统、字体系统、颜色、边框/圆角、阴影、图片裁切、交互态(hover/active/disabled等在小程序里等价态)、滚动与吸顶、列表与空状态
|
||||
- 每条验收项必须可操作:说明“怎么看”“合格标准是什么”“常见失败表现是什么”
|
||||
- 额外输出《差异追踪表》:字段包括【差异描述|截图/位置|严重度P0/P1/P2|可能原因|建议修复方案|修复代价|回归点】
|
||||
|
||||
输出用表格为主,保证我可以直接复制到文档里当验收用例。
|
||||
|
||||
|
||||
6) 编译报错/真机差异快速修复(把 Opus 当“修复代理”)
|
||||
你是微信小程序调试与兼容性专家。我会贴出:
|
||||
- 微信开发者工具编译报错/警告日志
|
||||
- 真机预览与模拟器表现差异描述
|
||||
- 相关代码片段
|
||||
|
||||
请你:
|
||||
A.先定位根因(按概率排序列出 1~3 个最可能原因)
|
||||
B.给出最小修复补丁(diff风格)
|
||||
C.给出回归测试点(防止修A坏B)
|
||||
D.如果是基础库兼容问题,说明需要的最低基础库版本或降级方案
|
||||
|
||||
560
apps/miniprogram - 副本/doc/migration-guide.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# H5 → 微信小程序迁移实施指南
|
||||
|
||||
> 本文档是"规则化迁移 + AI 辅助 + 视觉验收"的完整实施方案。
|
||||
> 适用于将 `docs/h5_ui/pages/*.html`(Tailwind CSS + 原生 JS)迁移为原生微信小程序页面。
|
||||
> 试点页面:`notes`(备注记录)— 中低复杂度,验证流程可行性。
|
||||
|
||||
---
|
||||
|
||||
## 一、核心原则
|
||||
|
||||
**这是迁移工程,不是生成工程。**
|
||||
|
||||
- 规则先行:标签映射、样式换算、事件转换有明确规则表,AI 按规则执行
|
||||
- 原型忠实:H5 源码 + computed-styles + 截图是唯一视觉真相,不凭想象
|
||||
- 组件化承接:TDesign 组件优先,自定义组件按 design.md 接口定义
|
||||
- 视觉回归兜底:每个页面转换后必须与 H5 截图逐项对比
|
||||
- 增量验证:一个页面走完全流程再推进下一个
|
||||
|
||||
---
|
||||
|
||||
## 二、迁移流水线(6 步)
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Step 1 │ │ Step 2 │ │ Step 3 │
|
||||
│ 输入物冻结 │───▶│ 迁移审计 │───▶│ 规则化转换 │
|
||||
│ (HTML/CSS/ │ │ (风险点+ │ │ (标签/样式/ │
|
||||
│ 截图/交互) │ │ 替代方案) │ │ 事件/路由) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌─────────────┐ ┌─────────────┐ ▼
|
||||
│ Step 6 │ │ Step 5 │ ┌─────────────┐
|
||||
│ 验收签收 │◀───│ 差异修复 │◀───│ Step 4 │
|
||||
│ (通过/打回) │ │ (截图diff+ │ │ 编译验证 │
|
||||
│ │ │ 定点修复) │ │ (开发者工具) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Step 1:输入物冻结
|
||||
|
||||
每个页面转换前,AI 必须加载以下材料(缺一不可):
|
||||
|
||||
| 序号 | 材料 | 路径 | 作用 |
|
||||
|------|------|------|------|
|
||||
| 1 | H5 源码 | `docs/h5_ui/pages/<page>.html` | 结构与样式的唯一真相 |
|
||||
| 2 | 自定义 CSS | `docs/h5_ui/css/<page>.css`(如有) | 非 Tailwind 的自定义样式 |
|
||||
| 3 | 交互说明 | `docs/h5_ui/interactions/<page>.md` | 状态变量、操作响应、页面状态枚举 |
|
||||
| 4 | 设计 Token | `docs/h5_ui/design-tokens.json` | 颜色、间距、圆角、字号、阴影 |
|
||||
| 5 | 图标映射表 | `docs/h5_ui/icon-mapping.md` | 图标处理方案(TDesign/自定义/Emoji) |
|
||||
| 6 | 计算样式 | `docs/h5_ui/computed-styles.json` 中对应 key | 精确的 px 数值(如有) |
|
||||
| 7 | 截图 | `docs/h5_ui/screenshots/<page>--*.png` | 视觉校验基线 |
|
||||
| 8 | 转换规范 | `.kiro/steering/miniprogram-h5-conversion.md` | 强制规则 |
|
||||
| 9 | 避坑指南 | `apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md` | 22 个高频坑点 |
|
||||
|
||||
### 加载顺序
|
||||
|
||||
```
|
||||
1. 转换规范 + 避坑指南(规则层)
|
||||
2. 设计 Token + 图标映射(全局资源层)
|
||||
3. H5 源码 + 自定义 CSS + 计算样式(页面源码层)
|
||||
4. 交互说明(行为层)
|
||||
5. 截图(视觉校验层)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Step 2:迁移审计
|
||||
|
||||
读取 H5 源码后,先输出《迁移审计报告》,不写代码。
|
||||
|
||||
### 审计清单
|
||||
|
||||
| 审计项 | 输出内容 |
|
||||
|--------|---------|
|
||||
| A. 页面结构 | 主要区域划分(header/list/card/footer),组件化边界建议 |
|
||||
| B. CSS 风险点 | 不支持的 CSS 特性清单 + 替代方案(伪元素、backdrop-filter、clip-path 等) |
|
||||
| C. Tailwind 展开 | 关键元素的 Tailwind 类 → WXSS 属性映射(取 computed-styles 精确值) |
|
||||
| D. SVG/图标 | 内联 SVG 清单 + 处理方式(TDesign/导出图片/Emoji) |
|
||||
| E. JS 交互 | DOM 操作 → setData 映射表 |
|
||||
| F. 外部依赖 | CDN 资源(Tailwind/Google Fonts)的本地化方案 |
|
||||
| G. 缺失信息 | 需要用户补充的材料清单 |
|
||||
|
||||
### 审计报告格式
|
||||
|
||||
```markdown
|
||||
## <page-name> 迁移审计报告
|
||||
|
||||
### A. 页面结构
|
||||
- 顶部导航栏(sticky,含返回按钮 + 标题)
|
||||
- Tab 切换区(客户备注 / 助教备注)
|
||||
- 备注列表(wx:for 渲染,每条含文本 + 标签 + 时间)
|
||||
|
||||
### B. CSS 风险点
|
||||
| 风险点 | 原因 | 影响 | 替代方案 | 验收方式 |
|
||||
|--------|------|------|---------|---------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### C. 关键样式映射
|
||||
| 元素 | Tailwind 类 | computed 值 | WXSS |
|
||||
|------|------------|-------------|------|
|
||||
| 备注卡片 | bg-white rounded-2xl p-4 shadow-sm | ... | ... |
|
||||
|
||||
### D. 图标处理
|
||||
| 图标 | H5 实现 | 小程序方案 |
|
||||
|------|---------|-----------|
|
||||
| 返回箭头 | 内联 SVG | <t-icon name="chevron-left" /> |
|
||||
|
||||
### E. 交互映射
|
||||
| H5 操作 | DOM 实现 | 小程序实现 |
|
||||
|---------|---------|-----------|
|
||||
| Tab 切换 | classList.toggle | setData({ activeTab }) + wx:if |
|
||||
|
||||
### F. 外部依赖
|
||||
- Tailwind CDN → 手写 WXSS
|
||||
- Google Fonts → 系统字体(-apple-system)
|
||||
|
||||
### G. 缺失信息
|
||||
- (列出需要用户补充的内容)
|
||||
```
|
||||
|
||||
用户确认审计报告后,才进入 Step 3。
|
||||
|
||||
---
|
||||
|
||||
## 五、Step 3:规则化转换
|
||||
|
||||
### 5.0 SVG 导出规则(强制,在转换前执行)
|
||||
|
||||
H5 原型中所有内联 `<svg>` 必须单独导出为 `.svg` 文件,小程序中通过 `<view>` + `<image>` 引用。
|
||||
|
||||
#### 规则
|
||||
|
||||
1. 扫描目标页面 H5 源码中的所有 `<svg>` 标签
|
||||
2. 每个 SVG 导出为独立文件,存放路径:`apps/miniprogram/miniprogram/assets/icons/<name>.svg`
|
||||
3. 命名规则:`icon-<用途>.svg`(如 `icon-wechat.svg`、`icon-clipboard.svg`);Logo 类用 `logo-<名称>.svg`
|
||||
4. 导出时保留原始 `viewBox`、`fill`、`path` 等属性,确保渲染一致
|
||||
5. 小程序中引用方式:`<image src="/assets/icons/<name>.svg" mode="aspectFit" />`,必须指定宽高
|
||||
6. 如果 TDesign 图标库有语义等价的图标(如返回箭头 → `chevron-left`),优先用 `<t-icon>`,不导出 SVG
|
||||
7. 审计报告的「D. 图标处理」栏必须列出每个 SVG 的处理决策(TDesign / 导出 SVG / Emoji)
|
||||
|
||||
#### 为什么不用 TDesign icon 属性
|
||||
|
||||
TDesign `<t-button icon="xxx">` 的 `icon` 属性只支持 TDesign 内置图标名。微信 logo 等第三方品牌图标不在 TDesign 图标库中,传入无效名称会导致图标不显示。因此品牌图标、复杂自定义图标一律导出 SVG 文件,用 `<image>` 引用。
|
||||
|
||||
#### 已导出清单
|
||||
|
||||
| 文件名 | 来源页面 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `logo-billiard.svg` | login | 台球 Logo(已有) |
|
||||
| `icon-wechat.svg` | login | 微信品牌图标 |
|
||||
| `icon-clock-circle.svg` | reviewing | 时钟主图标(stroke 风格,TDesign 无等价) |
|
||||
| `icon-forbidden.svg` | no-permission | 禁止符号主图标(stroke 风格,TDesign 无等价) |
|
||||
|
||||
> 每次迁移新页面时,将新导出的 SVG 追加到此清单。
|
||||
|
||||
### 5.1 标签映射(硬性规则)
|
||||
|
||||
| HTML | WXML | 说明 |
|
||||
|------|------|------|
|
||||
| `<div>` | `<view>` | 容器 |
|
||||
| `<span>` / `<p>` | `<text>` | 文本必须用 `<text>` 包裹 |
|
||||
| `<a>` | `<navigator>` 或 `bindtap` + `wx.navigateTo` | |
|
||||
| `<img>` | `<image mode="">` | 必须指定 mode 和宽高 |
|
||||
| `<svg>` 内联 | `<image src="xx.svg">` 或 `<t-icon>` | 不支持内联 SVG |
|
||||
| `<ul>/<li>` | `<view wx:for>` | 无列表语义标签 |
|
||||
| `<button>` | `<t-button>` | TDesign 优先 |
|
||||
| `<input>` | `<t-input>` | TDesign 优先 |
|
||||
| `<select>` | `<t-picker>` | 完全不同的交互 |
|
||||
| `<h1>`~`<h6>` | `<text>` + 样式类 | 无语义标题标签 |
|
||||
|
||||
**严禁在 WXML 中使用 HTML 标签。**
|
||||
|
||||
### 5.2 样式转换规则
|
||||
|
||||
#### 一屏页面布局模式(强制)
|
||||
|
||||
对于 H5 原型中一屏显示完毕、不需要滚动的页面(如 login、reviewing、no-permission 等),必须使用以下布局模式:
|
||||
|
||||
```css
|
||||
.page {
|
||||
height: 100vh; /* 固定一屏高度,不用 min-height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box; /* padding-top 从 100vh 中扣除 */
|
||||
overflow: hidden; /* 防止内容溢出产生滚动 */
|
||||
/* padding-top 由 JS statusBarHeight 动态设置 */
|
||||
}
|
||||
|
||||
.hero { flex: 1; } /* 主内容区域占满剩余空间,内部垂直居中 */
|
||||
.bottom-area { /* 固定高度,不参与 flex 伸缩 */ }
|
||||
```
|
||||
|
||||
关键点:
|
||||
- `height: 100vh` + `box-sizing: border-box` → padding-top(状态栏)从总高度中扣除,不会导致底部溢出
|
||||
- 主内容区域用 `flex: 1` 自适应剩余空间,内部用 `justify-content: center` 垂直居中
|
||||
- 底部操作区固定高度,不设 `flex`
|
||||
- 如果页面内容可能超过一屏(如 apply 的表单页),改用 `min-height: 100vh` + 允许滚动
|
||||
|
||||
#### 状态栏适配(强制)
|
||||
|
||||
所有 `navigationStyle: "custom"` 的页面,必须:
|
||||
1. TS 中 `onLoad` 获取 `wx.getSystemInfoSync().statusBarHeight`
|
||||
2. WXML 中 `.page` 加 `style="padding-top: {{statusBarHeight}}px;"`
|
||||
3. WXSS 中 `.page` 加 `box-sizing: border-box;`(确保 padding 不增加总高度)
|
||||
4. 禁止使用 `env(safe-area-inset-top)`(部分机型不生效)
|
||||
|
||||
#### rpx 换算
|
||||
```
|
||||
H5 px 值 × 2 = rpx 值(基于 375px → 750rpx)
|
||||
Tailwind spacing: 1 unit = 4px = 8rpx
|
||||
```
|
||||
|
||||
#### Tailwind → WXSS 速查表
|
||||
|
||||
| Tailwind | WXSS |
|
||||
|----------|------|
|
||||
| `p-4` | `padding: 32rpx;` |
|
||||
| `px-4` | `padding-left: 32rpx; padding-right: 32rpx;` |
|
||||
| `m-3` | `margin: 24rpx;` |
|
||||
| `gap-3` | `gap: 24rpx;` |
|
||||
| `space-y-3` | 子元素 `margin-top: 24rpx;`(首个除外) |
|
||||
| `rounded-2xl` | `border-radius: 32rpx;` |
|
||||
| `text-sm` | `font-size: 28rpx;` |
|
||||
| `text-base` | `font-size: 32rpx;` |
|
||||
| `text-xs` | `font-size: 24rpx;` |
|
||||
| `font-medium` | `font-weight: 500;` |
|
||||
| `font-semibold` | `font-weight: 600;` |
|
||||
| `leading-relaxed` | `line-height: 1.625;` |
|
||||
| `shadow-sm` | `box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);` |
|
||||
| `flex` | `display: flex;` |
|
||||
| `flex-col` | `flex-direction: column;` |
|
||||
| `items-center` | `align-items: center;` |
|
||||
| `justify-between` | `justify-content: space-between;` |
|
||||
| `flex-1` | `flex: 1;` |
|
||||
| `min-h-screen` | `min-height: 100vh;` |
|
||||
| `sticky top-0` | `position: sticky; top: 0;` |
|
||||
| `z-10` | `z-index: 10;` |
|
||||
| `bg-white` | `background-color: #ffffff;` |
|
||||
| `bg-gray-1` | `background-color: #f3f3f3;` |
|
||||
| `text-gray-13` | `color: #242424;` |
|
||||
| `text-gray-6` | `color: #a6a6a6;` |
|
||||
| `border-b border-gray-2` | `border-bottom: 2rpx solid #eeeeee;` |
|
||||
| `backdrop-blur-sm` | ❌ 不支持,改为 `rgba()` 半透明 |
|
||||
|
||||
#### 颜色值参照 design-tokens.json
|
||||
|
||||
```
|
||||
primary: #0052d9 primary-light: #ecf2fe
|
||||
success: #00a870 warning: #ed7b2f error: #e34d59
|
||||
gray-1 ~ gray-13: 见 design-tokens.json
|
||||
```
|
||||
|
||||
#### 不支持的 CSS 替代方案
|
||||
|
||||
| CSS 特性 | 替代方案 |
|
||||
|----------|---------|
|
||||
| `backdrop-filter: blur()` | `background: rgba(255,255,255,0.95);` |
|
||||
| `*` 通配符选择器 | 逐个元素设置 |
|
||||
| `::before` / `::after` | 额外 `<view>` 元素模拟(小程序部分支持伪元素,复杂场景用 DOM) |
|
||||
| 远程 `@font-face` | `wx.loadFontFace()` 或系统字体 |
|
||||
| `clip-path` | 改为图片或 CSS 渐变近似 |
|
||||
| `blur-xl`(Tailwind) | 去掉或改为纯色 |
|
||||
|
||||
### 5.3 事件转换规则
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `onclick="fn()"` | `bindtap="fn"` | 不能传参 |
|
||||
| `onclick="fn(id)"` | `data-id="{{id}}" bindtap="fn"` | dataset 传参 |
|
||||
| `addEventListener` | 不支持 | 只能声明式绑定 |
|
||||
| `event.target.value` | `e.detail.value` | 取值路径不同 |
|
||||
| `event.preventDefault()` | `catchtap` | catch 前缀阻止冒泡 |
|
||||
| `classList.toggle('active')` | `setData({ active: !this.data.active })` + `class="{{active ? 'on' : ''}}"` | |
|
||||
| `innerHTML = '...'` | `setData({ content: '...' })` + WXML 数据绑定 | |
|
||||
| `history.back()` | `wx.navigateBack()` | |
|
||||
| `window.location.href` | `wx.navigateTo({ url: '...' })` | |
|
||||
| `localStorage.setItem` | `wx.setStorageSync` | |
|
||||
| `alert()` / `confirm()` | `wx.showToast()` / `wx.showModal()` | |
|
||||
|
||||
### 5.4 路由转换规则
|
||||
|
||||
| 场景 | 小程序 API | 说明 |
|
||||
|------|-----------|------|
|
||||
| 普通页面跳转 | `wx.navigateTo` | 保留当前页,页面栈 +1 |
|
||||
| TabBar 页面跳转 | `wx.switchTab` | 必须用 switchTab,navigateTo 会报错 |
|
||||
| 替换当前页 | `wx.redirectTo` | 关闭当前页 |
|
||||
| 清空页面栈 | `wx.reLaunch` | 登录/登出场景 |
|
||||
| 返回上一页 | `wx.navigateBack` | 页面栈 -1 |
|
||||
|
||||
**TabBar 页面:task-list、board-finance、my-profile**
|
||||
|
||||
### 5.5 转换执行顺序
|
||||
|
||||
```
|
||||
1. 创建页面 4 文件骨架(.wxml / .wxss / .ts / .json)
|
||||
2. 在 .json 中注册 usingComponents(TDesign 组件 + 自定义组件)
|
||||
3. 转换 WXML 结构(HTML 标签 → WXML 标签,保持层级一致)
|
||||
4. 转换 WXSS 样式(Tailwind → 手写 WXSS,取 computed-styles 精确值)
|
||||
5. 转换 TS 逻辑(DOM 操作 → setData,事件绑定 → bindtap)
|
||||
6. 设置 Mock 数据(贴近真实 API 格式,标记 TODO)
|
||||
7. 处理三态(loading / empty / normal / error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Step 4:编译验证
|
||||
|
||||
在微信开发者工具中检查:
|
||||
|
||||
| 检查项 | 合格标准 |
|
||||
|--------|---------|
|
||||
| WXML 编译 | 无编译错误(特别注意 `.toFixed()` 等 JS 方法不能在 WXML 中使用) |
|
||||
| WXSS 编译 | 无警告(检查不支持的选择器) |
|
||||
| 控制台 | 无 JS 运行时错误 |
|
||||
| 图片加载 | 无 404/500 错误(所有 `/assets/` 引用的文件必须存在) |
|
||||
| 组件注册 | 无 "component not found" 警告 |
|
||||
| 路由跳转 | 无 "navigateTo:fail" 错误 |
|
||||
|
||||
---
|
||||
|
||||
## 七、Step 5:差异修复
|
||||
|
||||
### 7.1 截图对比
|
||||
|
||||
用户在微信开发者工具中截图,与 `docs/h5_ui/screenshots/<page>--*.png` 逐项对比。
|
||||
|
||||
### 7.2 差异追踪表
|
||||
|
||||
| 差异描述 | 位置 | 严重度 | 可能原因 | 修复方案 | 修复代价 |
|
||||
|---------|------|--------|---------|---------|---------|
|
||||
| 卡片圆角偏小 | 备注卡片 | P1 | rpx 换算错误 | 改为 32rpx | 低 |
|
||||
| 标签颜色偏差 | tag-coach | P1 | 颜色值不准确 | 取 computed-styles 精确值 | 低 |
|
||||
| 间距不一致 | 列表间距 | P0 | space-y-3 未正确转换 | 改为 margin-top: 24rpx | 低 |
|
||||
|
||||
### 7.3 修复原则
|
||||
|
||||
- 只输出需要改动的文件和改动段落(diff 风格),不整文件重贴
|
||||
- 优先使用 flex/盒模型的确定性方案,不用"碰运气"的魔法数
|
||||
- rpx 换算统一,严禁同一类间距混用 rpx 和 px
|
||||
- 每次修复后重新编译验证,防止修 A 坏 B
|
||||
|
||||
---
|
||||
|
||||
## 八、Step 6:验收签收
|
||||
|
||||
### 逐项验收清单
|
||||
|
||||
| 验收项 | 怎么看 | 合格标准 | 常见失败表现 |
|
||||
|--------|--------|---------|-------------|
|
||||
| 布局结构 | 对比截图整体布局 | 区域划分、层级关系一致 | 元素错位、层级混乱 |
|
||||
| 间距系统 | 对比元素间距 | 与 H5 截图一致(±4rpx 容差) | 间距过大/过小 |
|
||||
| 字体系统 | 对比字号、字重、行高 | 与 design-tokens 一致 | 字号偏差、行高不对 |
|
||||
| 颜色 | 对比背景色、文字色、边框色 | 与 design-tokens 一致 | 颜色偏差 |
|
||||
| 圆角 | 对比卡片、按钮圆角 | 与 design-tokens 一致 | 圆角过大/过小 |
|
||||
| 阴影 | 对比卡片阴影 | 有阴影且不突兀 | 无阴影或阴影过重 |
|
||||
| 图标 | 对比图标位置、大小、颜色 | TDesign 图标正确显示 | 图标缺失或错位 |
|
||||
| 交互完整性 | 按交互说明逐项操作 | 所有操作有正确响应 | 点击无反应、状态不切换 |
|
||||
| 三态处理 | 切换 loading/empty/error | 三种状态均有对应 UI | 缺少空状态或加载态 |
|
||||
| 安全区 | 刘海屏设备检查 | 内容不被刘海遮挡 | 顶部内容被裁切 |
|
||||
|
||||
---
|
||||
|
||||
## 九、试点页面:notes(备注记录)
|
||||
|
||||
### 选择理由
|
||||
- 中低复杂度:简单列表 + Tab 切换 + 标签样式
|
||||
- 覆盖核心转换场景:Tailwind → WXSS、事件绑定、列表渲染、三态处理
|
||||
- 有完整材料:HTML + CSS + 交互说明 + 截图
|
||||
- 验证周期短:预计 1 轮即可完成
|
||||
|
||||
### 需要加载的材料
|
||||
```
|
||||
docs/h5_ui/pages/notes.html
|
||||
docs/h5_ui/css/notes.css
|
||||
docs/h5_ui/interactions/notes.md
|
||||
docs/h5_ui/design-tokens.json
|
||||
docs/h5_ui/icon-mapping.md
|
||||
docs/h5_ui/screenshots/notes--*.png
|
||||
docs/h5_ui/computed-styles.json(notes key,如有)
|
||||
```
|
||||
|
||||
### 预期产出
|
||||
```
|
||||
miniprogram/pages/notes/notes.wxml
|
||||
miniprogram/pages/notes/notes.wxss
|
||||
miniprogram/pages/notes/notes.ts
|
||||
miniprogram/pages/notes/notes.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、页面迁移优先级
|
||||
|
||||
按模块分组,每组内按复杂度从低到高排列:
|
||||
|
||||
| 批次 | 页面 | 复杂度 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 试点 | notes | 低 | 验证流程 |
|
||||
| 第 1 批 | login, apply, reviewing, no-permission | 低 | 认证流程(已有实现,需重写) |
|
||||
| 第 2 批 | task-list, task-detail | 中-高 | 任务模块核心 |
|
||||
| 第 3 批 | task-detail-callback, task-detail-priority, task-detail-relationship | 中 | 任务详情变体 |
|
||||
| 第 4 批 | performance, performance-records | 中 | 绩效模块 |
|
||||
| 第 5 批 | board-finance, board-customer, board-coach | 高 | 看板模块(筛选+吸顶+复杂布局) |
|
||||
| 第 6 批 | customer-detail, customer-service-records, coach-detail | 中 | 详情模块 |
|
||||
| 第 7 批 | chat, chat-history | 中 | 对话模块 |
|
||||
| 第 8 批 | my-profile | 中 | 个人中心 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、需要用户补充的内容
|
||||
|
||||
### P0(阻塞试点)
|
||||
|
||||
| 材料 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| notes 页面截图 | ⚠️ 待确认 | 确认 `docs/h5_ui/screenshots/notes--*.png` 是否存在 |
|
||||
| notes 的 computed-styles | ⚠️ 待确认 | 确认 `computed-styles.json` 中是否有 notes key |
|
||||
|
||||
### P1(提升还原度)
|
||||
|
||||
| 材料 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| Banner 背景图片 | ❌ 缺失 | icon-mapping.md 规划了用 Playwright 截取 Banner,但实际图片未导出 |
|
||||
| AI 图标图片 | ❌ 缺失 | icon-ai-float.png / icon-ai-inline.png / icon-ai-badge.png 需要从 H5 截取 |
|
||||
| 其他页面的 computed-styles | ⚠️ 部分缺失 | 当前仅 5 个页面有数据,其余 19 个缺失 |
|
||||
|
||||
### P2(后续批次需要)
|
||||
|
||||
| 材料 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| home-settings 交互说明 | ❌ 缺失 | |
|
||||
| ai-icon-demo 交互说明 | ❌ 缺失 | |
|
||||
|
||||
---
|
||||
|
||||
## 十二、与现有文档的关系
|
||||
|
||||
| 文档 | 职责 | 本指南的关系 |
|
||||
|------|------|-------------|
|
||||
| `miniprogram-h5-conversion.md`(steering) | 强制转换规则 | 本指南引用其规则,不重复 |
|
||||
| `h5-to-miniprogram-pitfalls.md` | 避坑清单 | 本指南引用其坑点,不重复 |
|
||||
| `h5-input-material-guide.md` | 输入材料准备规范 | 本指南引用其格式要求 |
|
||||
| `howtodo.md` | 6 阶段工作流提示词 | 本指南是其具体化实施版本 |
|
||||
| `design.md`(spec) | 组件接口 + 数据模型 | 本指南引用其组件定义 |
|
||||
|
||||
---
|
||||
|
||||
## 十三、实战踩坑记录
|
||||
|
||||
> 本节记录迁移过程中实际遇到的坑和解决方案,按发现时间倒序排列。每次迁移新页面遇到新坑时追加。
|
||||
|
||||
### P1:WXML 中不能调用 JS 方法
|
||||
|
||||
- 触发页面:所有页面
|
||||
- 现象:`{{price.toFixed(2)}}` 编译报错
|
||||
- 原因:WXML 模板表达式不支持 JS 方法调用(`.toFixed()`、`.map()`、`.filter()` 等)
|
||||
- 解决:创建 WXS 模块 `utils/format.wxs`,在 WXML 中 `<wxs src="..." module="fmt" />`,用 `{{fmt.toFixed(price, 2)}}`
|
||||
|
||||
### P2:TabBar 页面不能用 navigateTo
|
||||
|
||||
- 触发页面:task-list、board-finance、my-profile
|
||||
- 现象:`navigateTo:fail` 静默失败
|
||||
- 原因:TabBar 页面必须用 `wx.switchTab`,`navigateTo` / `redirectTo` 均无效
|
||||
- 解决:跳转前判断目标是否 TabBar 页面,是则用 `switchTab`
|
||||
|
||||
### P3:图片 500 错误(资源文件不存在)
|
||||
|
||||
- 触发页面:所有引用 `/assets/images/*.png` 和 `/assets/icons/icon-ai-*.png` 的页面
|
||||
- 现象:控制台大量 500 错误
|
||||
- 原因:代码引用了不存在的图片文件
|
||||
- 解决:用 CSS 渐变、emoji 文本、`<t-icon>` 替代所有不存在的图片引用;仅保留确实存在的文件(如 `logo-billiard.svg`)
|
||||
|
||||
### P4:env(safe-area-inset-top) 部分机型不生效
|
||||
|
||||
- 触发页面:notes、login(所有 `navigationStyle: "custom"` 的页面)
|
||||
- 现象:iPhone 刘海屏顶部内容被状态栏遮挡,部分安卓机型也有此问题
|
||||
- 原因:`env(safe-area-inset-top)` 在部分机型/基础库版本下返回 0
|
||||
- 解决:TS 中 `onLoad` 获取 `wx.getSystemInfoSync().statusBarHeight`,WXML 中动态设置 `style="padding-top: {{statusBarHeight}}px;"`
|
||||
- 标准模式:见 5.2 节「状态栏适配」
|
||||
|
||||
### P5:statusBarHeight padding 导致一屏页面底部溢出
|
||||
|
||||
- 触发页面:login(所有一屏不滚动的页面)
|
||||
- 现象:底部按钮和协议文字被推到屏幕外
|
||||
- 原因:`min-height: 100vh` + `padding-top: Xpx` = 实际高度 > 100vh
|
||||
- 解决:改为 `height: 100vh` + `box-sizing: border-box`,padding 从总高度中扣除
|
||||
- 标准模式:见 5.2 节「一屏页面布局模式」
|
||||
|
||||
### P6:TDesign Button icon 属性不支持自定义图标
|
||||
|
||||
- 触发页面:login
|
||||
- 现象:`<t-button icon="logo-wechat">` 微信图标不显示
|
||||
- 原因:TDesign `icon` 属性只接受内置图标名,微信 logo 不在内置库中
|
||||
- 解决:放弃 `t-button`,改为原生 `<view>` + `<image src="/assets/icons/icon-wechat.svg">` 手动组合按钮
|
||||
- 规则:品牌图标、复杂自定义图标一律导出 SVG 文件,用 `<image>` 引用(见 5.0 节)
|
||||
|
||||
### P7:TDesign Button 默认样式覆盖自定义 WXSS
|
||||
|
||||
- 触发页面:login
|
||||
- 现象:按钮圆角过大(接近胶囊形)、禁用态颜色不是预期的 `#dcdcdc`
|
||||
- 原因:TDesign Button 内部样式优先级高于外部 `t-class` 覆盖,`!important` 也不一定生效
|
||||
- 解决:对于需要高度定制的按钮,直接用原生 `<view>` 实现,完全绕开 TDesign 样式干扰
|
||||
- 原则:TDesign 组件适合"接近默认样式"的场景;与原型差异大时,原生实现更可控
|
||||
|
||||
### P8:小程序 WXSS 不支持 `::before` / `::after` 伪元素
|
||||
|
||||
- 触发页面:reviewing
|
||||
- 现象:尝试用 `::before` 添加背景图案层,编译无报错但不渲染
|
||||
- 原因:微信小程序 WXSS 不支持 CSS 伪元素
|
||||
- 解决:用实际的 `<view class="bg-pattern">` 替代伪元素,absolute 定位覆盖全屏
|
||||
- 规则:所有需要伪元素的场景(装饰层、分隔线、角标等),一律用 WXML `<view>` 实现
|
||||
|
||||
### P9:小程序 WXSS 不支持 `url("data:image/svg+xml,...")` 内联 SVG
|
||||
|
||||
- 触发页面:reviewing
|
||||
- 现象:H5 用 `background-image: url("data:image/svg+xml,...")` 实现十字纹背景,小程序中不渲染
|
||||
- 原因:小程序 WXSS 的 `background-image` 不支持 data URI(仅支持网络图片和 base64 图片)
|
||||
- 解决:用 `repeating-linear-gradient` 组合模拟纹理图案;或用实际图片文件
|
||||
- 规则:H5 中的 SVG 背景图案,迁移时优先用 CSS 渐变模拟;效果不佳时导出为 PNG/base64
|
||||
|
||||
### P10:小程序不支持 `filter: blur()` CSS 属性
|
||||
|
||||
- 触发页面:reviewing
|
||||
- 现象:H5 用 `blur-xl`(Tailwind)实现图标背景光晕的模糊效果,小程序中无效
|
||||
- 原因:微信小程序 WXSS 不支持 `filter` 属性
|
||||
- 解决:用更大尺寸的 `radial-gradient` 模拟模糊扩散效果(扩大元素尺寸 + 调整渐变衰减曲线)
|
||||
- 参数参考:原始 256rpx → 扩大到 320rpx,gradient 从 `0.18 → 0.06 → transparent` 三段衰减
|
||||
|
||||
### P11:Tailwind `max-w-sm` 在小程序中的换算需考虑容器 padding
|
||||
|
||||
- 触发页面:reviewing
|
||||
- 现象:进度卡片 `max-w-sm = 384px = 768rpx` 直接换算后卡片过宽
|
||||
- 原因:H5 中 `max-w-sm` 受外层 `px-8`(32px padding)约束,实际可用宽度 ≈ 320px;小程序中 content 区域的 padding 和 max-width 叠加效果不同
|
||||
- 解决:实测调整 `max-width` 值(本例最终为 550rpx),不能机械换算
|
||||
- 规则:涉及 `max-w-*` 的元素,迁移后必须在真机/模拟器中目测确认宽度,按实际效果微调
|
||||
|
||||
### P12:H5 Tailwind 颜色变体需逐一核对(如 `bg-amber-300` ≠ `bg-warning`)
|
||||
|
||||
- 触发页面:reviewing
|
||||
- 现象:装饰点 dot-2 在 H5 中用 `bg-amber-300`(#fcd34d),迁移时误用了 `bg-warning`(#ed7b2f)
|
||||
- 原因:Tailwind 的 amber/orange/yellow 色系有多个变体,不能一律映射为 design-tokens 中的 `warning`
|
||||
- 解决:逐个检查 H5 中的颜色类名,查 Tailwind 色板取精确 hex 值
|
||||
- 规则:迁移时遇到非 design-tokens 定义的 Tailwind 颜色,必须查 Tailwind 官方色板确认精确值
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文档索引
|
||||
|
||||
- 转换规范:`.kiro/steering/miniprogram-h5-conversion.md`
|
||||
- 避坑指南:`apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md`
|
||||
- 输入材料指南:`apps/miniprogram/doc/h5-input-material-guide.md`
|
||||
- 工作流提示词:`apps/miniprogram/doc/howtodo.md`
|
||||
- 设计文档:`.kiro/specs/p52-miniapp-fe-all-pages/design.md`
|
||||
- 设计 Token:`docs/h5_ui/design-tokens.json`
|
||||
- 图标映射:`docs/h5_ui/icon-mapping.md`
|
||||
- H5 原型:`docs/h5_ui/pages/`
|
||||
- 交互说明:`docs/h5_ui/interactions/`
|
||||
- 截图:`docs/h5_ui/screenshots/`
|
||||
337
apps/miniprogram - 副本/doc/migration-method-full-path.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# H5 → 微信小程序迁移:实战全路径方法论
|
||||
|
||||
> 基于 board-finance 等 7 个页面的实际迁移经验总结。
|
||||
> 本文档是 `migration-guide.md`(理论流水线)的实战补充,聚焦工具链和操作细节。
|
||||
> 更新日期:2026-03-08
|
||||
|
||||
---
|
||||
|
||||
## 一、全路径概览
|
||||
|
||||
```
|
||||
H5 原型 HTML/CSS
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Phase 1: 结构转换(wxml + ts + wxss 三件套) │
|
||||
│ 输入: H5 源码 + 交互说明 + design-tokens │
|
||||
│ 规则: 标签映射 + 87.5% 缩放 + 事件转换 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Phase 2: 编译验证(getDiagnostics + IDE 检查) │
|
||||
│ 消除 TS 类型错误、wxss 语法警告 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Phase 3: 像素级视觉对比(自动化工具链) │
|
||||
│ H5 截图 ←→ 小程序截图 → diff → 定点修复 → 循环 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Phase 4: 交互态验证(3 种交互态截图对比) │
|
||||
│ filter-dropdown / tip-modal / toc-panel 等 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Phase 5: 收尾(tracker 更新 + dev-tools 状态同步) │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Phase 1: 结构转换
|
||||
|
||||
### 2.1 输入物
|
||||
|
||||
| 材料 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| H5 源码 | `docs/h5_ui/pages/<page>.html` | 结构与样式唯一真相 |
|
||||
| 交互说明 | `docs/h5_ui/interactions/<page>.md` | 状态变量、事件响应 |
|
||||
| Design Tokens | `docs/h5_ui/design-tokens.json` | 颜色、间距、字号 |
|
||||
| 共享组件规范 | `apps/miniprogram/doc/shared-component-specs.md` | filter-dropdown 等 |
|
||||
| 避坑文档 | `apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md` | 22+ 个已知坑点 |
|
||||
|
||||
### 2.2 核心缩放公式
|
||||
|
||||
```
|
||||
最终 rpx = H5 px × 2 × 0.875
|
||||
```
|
||||
|
||||
- 结果取偶数(向最近偶数取整)
|
||||
- 来源:iPhone 15 Pro Max 430px 宽 → 小程序 750rpx 基准,87.5% = 750/430/2
|
||||
- 示例:`16px → 16 × 2 × 0.875 = 28rpx`,`12px → 12 × 2 × 0.875 = 21 → 22rpx`
|
||||
|
||||
### 2.3 标签映射速查
|
||||
|
||||
| H5 | 小程序 | 注意 |
|
||||
|----|--------|------|
|
||||
| `<div>` | `<view>` | |
|
||||
| `<span>` | `<text>` | 小程序 text 不支持嵌套 view |
|
||||
| `<img>` | `<image>` | 必须设 mode,默认 scaleToFill |
|
||||
| `<a>` | `<navigator>` 或 `bindtap` | 小程序无超链接 |
|
||||
| `<input>` | `<input>` 或 `<t-input>` | TDesign 优先 |
|
||||
| `<button>` | `<t-button>` | TDesign 优先 |
|
||||
| `<select>` | `<t-picker>` | 无原生 select |
|
||||
| `<svg>` | `<t-icon>` 或 `<image>` | 小程序不支持内联 SVG |
|
||||
| `<section>` | `<view>` | 语义标签统一用 view |
|
||||
| `scroll 容器` | `<scroll-view>` | 必须设固定高度 |
|
||||
|
||||
### 2.4 样式转换要点
|
||||
|
||||
- Tailwind class → 手写 wxss(小程序不支持 Tailwind)
|
||||
- `vh/vw` → `rpx` 或 `calc()`
|
||||
- `position: fixed` → 正常工作,但注意安全区 `env(safe-area-inset-bottom)`
|
||||
- `backdrop-filter` → 小程序不支持,用纯色半透明替代
|
||||
- `gap` → flexbox gap 在小程序基础库 2.30+ 支持,低版本用 margin
|
||||
- CSS 变量 `var(--xxx)` → 小程序不支持,直接写值
|
||||
- `::before/::after` → 支持,但不能用 `content: attr()`
|
||||
- 渐变 → `linear-gradient` 支持,`radial-gradient` 部分支持
|
||||
|
||||
### 2.5 事件转换
|
||||
|
||||
| H5 | 小程序 | 说明 |
|
||||
|----|--------|------|
|
||||
| `onclick` | `bindtap` | |
|
||||
| `onchange` | `bind:change` | TDesign 组件用 `bind:change` |
|
||||
| `oninput` | `bindinput` | |
|
||||
| `onscroll` | `bindscroll` | 在 scroll-view 上 |
|
||||
| `addEventListener` | 无 | 用 wxml 声明式绑定 |
|
||||
| `e.target.dataset` | `e.currentTarget.dataset` | 注意 currentTarget |
|
||||
| `e.preventDefault()` | `catchtap`(阻止冒泡) | |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三、Phase 2: 编译验证
|
||||
|
||||
### 3.1 TypeScript 检查
|
||||
|
||||
```
|
||||
getDiagnostics → board-finance.ts
|
||||
```
|
||||
|
||||
- 确保 `Page<IData>()` 类型定义完整
|
||||
- data 中所有字段有初始值
|
||||
- 事件处理函数签名正确(`e: WechatMiniprogram.TouchEvent`)
|
||||
|
||||
### 3.2 常见 TS 问题
|
||||
|
||||
- TDesign 组件事件类型:用 `WechatMiniprogram.CustomEvent` 而非 `TouchEvent`
|
||||
- `wx:if` 条件中的变量必须在 data 中声明
|
||||
- `setData` 的 key 路径必须与 data 结构一致
|
||||
|
||||
### 3.3 wxss 检查
|
||||
|
||||
微信 IDE 会报 CSS 警告(非阻塞),常见:
|
||||
- 内联 style 中的空格(`style="width: 100%"` → `style="width:100%"`)
|
||||
- 不支持的属性(`backdrop-filter` 等)
|
||||
|
||||
---
|
||||
|
||||
## 四、Phase 3: 像素级视觉对比(核心工具链)
|
||||
|
||||
这是整个迁移流程中最关键的环节。我们建立了一套完整的自动化对比工具链。
|
||||
|
||||
### 4.1 前置准备
|
||||
|
||||
#### H5 截图(一次性)
|
||||
|
||||
1. 用户启动 Go Live(VS Code 插件),端口 5500
|
||||
2. Playwright MCP 导航到 `http://127.0.0.1:5500/docs/h5_ui/pages/<page>.html`
|
||||
3. 设置视口 `430×932`(iPhone 15 Pro Max)
|
||||
4. 全页面截图 → `docs/h5_ui/screenshots/<page>.png`(1290px 宽,DPR 3)
|
||||
5. 交互态截图(下拉、弹窗、面板等)→ `<page>--<state>.png`
|
||||
|
||||
#### 微信开发者工具连接
|
||||
|
||||
```bash
|
||||
# 用户手动启动自动化端口(或 AI 用 controlPwshProcess)
|
||||
& "C:\dev\WechatDevtools\cli.bat" auto --project "C:\NeoZQYY\apps\miniprogram" --auto-port 9420
|
||||
```
|
||||
|
||||
连接规范:
|
||||
- 只能用 `wsEndpoint` 策略,`ws://127.0.0.1:9420`
|
||||
- 禁止 auto/launch/connect/discover 策略
|
||||
- 导航到 tabbar 页面必须用 `relaunch`,路径前加 `/`
|
||||
|
||||
### 4.2 对比流程(每轮迭代)
|
||||
|
||||
```
|
||||
Step 1: 截图
|
||||
mcp_weixin_devtools_mcp_relaunch → /pages/<page>/<page>
|
||||
mcp_weixin_devtools_mcp_waitFor → 2000ms
|
||||
mcp_weixin_devtools_mcp_screenshot → mp-<page>.png
|
||||
|
||||
Step 2: 尺寸统一
|
||||
python scripts/ops/resize_and_compare_v2.py
|
||||
→ H5 保持 1290px 宽
|
||||
→ MP 截图 ×2 缩放到 1290px
|
||||
→ H5 裁剪到 MP 逻辑高度对应的物理高度
|
||||
→ 输出 cmp-h5.png + cmp-mp.png(同尺寸)
|
||||
|
||||
Step 3: 像素对比
|
||||
mcp_image_compare_compare_images
|
||||
→ image1: cmp-h5.png
|
||||
→ image2: cmp-mp.png
|
||||
→ threshold: 0.1
|
||||
→ 输出 diff 图 + 差异百分比
|
||||
|
||||
Step 4: 差异分析
|
||||
python scripts/ops/analyze_diff.py
|
||||
→ 按 150px 条带分析差异密度
|
||||
→ 定位差异最大的 5 个区域
|
||||
|
||||
Step 5: 精确位置对比
|
||||
mcp_weixin_devtools_mcp_get_page_snapshot(MP 元素位置)
|
||||
Playwright browser_evaluate(H5 元素位置)
|
||||
→ 逐元素对比 Y 坐标和高度
|
||||
|
||||
Step 6: wxss 微调
|
||||
根据位置差异调整 padding/margin/gap
|
||||
→ 回到 Step 1 循环
|
||||
```
|
||||
|
||||
### 4.3 DPR 换算关系
|
||||
|
||||
| 平台 | 物理宽度 | DPR | 逻辑宽度 |
|
||||
|------|---------|-----|---------|
|
||||
| H5 截图 | 1290px | 3 | 430px |
|
||||
| MP 截图 | 645px | 1.5 | 430px |
|
||||
|
||||
统一对比宽度:1290px(MP ×2 缩放)
|
||||
|
||||
高度对齐:
|
||||
- MP 逻辑高度 = MP 物理高度 / 1.5
|
||||
- H5 裁剪高度 = MP 逻辑高度 × 3
|
||||
|
||||
### 4.4 差异阈值参考
|
||||
|
||||
| 差异% | 评价 | 行动 |
|
||||
|-------|------|------|
|
||||
| < 5% | 优秀 | 字体渲染级差异,可接受 |
|
||||
| 5-10% | 良好 | 检查是否有结构性差异(底部内容不同) |
|
||||
| 10-15% | 需调整 | 定位差异区域,微调间距 |
|
||||
| > 15% | 较大 | 可能有布局错误,需逐元素排查 |
|
||||
|
||||
注意:底部区域(MP 只截一屏)的差异是结构性的,不算样式问题。评估时应关注前半屏的差异。
|
||||
|
||||
### 4.5 工具脚本
|
||||
|
||||
| 脚本 | 路径 | 功能 |
|
||||
|------|------|------|
|
||||
| resize_and_compare_v2.py | `scripts/ops/` | 统一尺寸 + 裁剪 |
|
||||
| analyze_diff.py | `scripts/ops/` | 按条带分析差异分布 |
|
||||
|
||||
### 4.6 实战数据(board-finance)
|
||||
|
||||
| 轮次 | 差异% | 主要调整 |
|
||||
|------|-------|---------|
|
||||
| 初始 | 15.29% | 首次截图对比 |
|
||||
| 第一轮 | 12.56% | 10 处间距微调(padding/margin/gap) |
|
||||
| 第二轮 | 9.04% | 2 处调整(header→sub-label 间距、divider 非对称 margin) |
|
||||
|
||||
前半屏(Y<450px)最终差异约 3.5%,达到字体渲染级别。
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase 4: 交互态验证
|
||||
|
||||
### 5.1 常见交互态
|
||||
|
||||
| 交互态 | 触发方式 | 截图命名 |
|
||||
|--------|---------|---------|
|
||||
| 筛选下拉 | 点击 filter-dropdown | `<page>--filter-dropdown.png` |
|
||||
| 指标弹窗 | 点击 help-icon | `<page>--tip-modal.png` |
|
||||
| 目录导航 | 点击 toc-btn | `<page>--toc-panel.png` |
|
||||
|
||||
### 5.2 小程序交互态截图方法
|
||||
|
||||
```
|
||||
# 打开 toc-panel
|
||||
mcp_weixin_devtools_mcp_click → uid=view.toc-btn
|
||||
mcp_weixin_devtools_mcp_waitFor → 500ms
|
||||
mcp_weixin_devtools_mcp_screenshot → mp-<page>--toc-panel.png
|
||||
|
||||
# 关闭(通过 evaluate_script 或点击 overlay)
|
||||
mcp_weixin_devtools_mcp_evaluate_script → page.setData({ tocVisible: false })
|
||||
|
||||
# 打开 tip-modal
|
||||
mcp_weixin_devtools_mcp_click → uid=view.help-icon-light
|
||||
mcp_weixin_devtools_mcp_waitFor → 500ms
|
||||
mcp_weixin_devtools_mcp_screenshot → mp-<page>--tip-modal.png
|
||||
```
|
||||
|
||||
### 5.3 交互态对比
|
||||
|
||||
交互态的对比更多是视觉检查(弹窗位置、遮罩透明度、动画效果),不需要像默认态那样精确到像素。重点关注:
|
||||
- 弹窗/面板的圆角、阴影、背景色
|
||||
- 遮罩层透明度
|
||||
- 列表项的间距和对齐
|
||||
- 关闭按钮的位置
|
||||
|
||||
---
|
||||
|
||||
## 六、Phase 5: 收尾
|
||||
|
||||
### 6.1 更新 migration-tracker.md
|
||||
|
||||
- 将页面从"正在迁移"移到"已完成"
|
||||
- 记录最终差异百分比和关键调整
|
||||
|
||||
### 6.2 更新 dev-tools 页面
|
||||
|
||||
- `dev-tools.ts` 中将页面从 `MIGRATING_PAGES` 移到 `DONE_PAGES`
|
||||
|
||||
### 6.3 截图归档
|
||||
|
||||
所有截图保存在 `docs/h5_ui/screenshots/`:
|
||||
- `<page>.png` — H5 默认态
|
||||
- `<page>--<state>.png` — H5 交互态
|
||||
- `mp-<page>.png` — 小程序默认态
|
||||
- `mp-<page>--<state>.png` — 小程序交互态
|
||||
- `diff-<page>-v2.png` — 最终 diff 图
|
||||
- `cmp-h5.png` / `cmp-mp.png` — 对比用中间文件
|
||||
|
||||
---
|
||||
|
||||
## 七、经验教训
|
||||
|
||||
### 7.1 高频坑点(详见 h5-to-miniprogram-pitfalls.md)
|
||||
|
||||
1. `scroll-view` 必须设固定高度,否则不滚动
|
||||
2. `wx:if` vs `hidden`:频繁切换用 hidden,否则用 wx:if
|
||||
3. 小程序 text 组件不支持嵌套 view
|
||||
4. CSS `gap` 需要基础库 2.30+
|
||||
5. 内联 style 中的空格会触发 IDE 警告
|
||||
6. tabbar 页面只能用 `switchTab` 或 `reLaunch` 导航
|
||||
|
||||
### 7.2 效率提升点
|
||||
|
||||
- 先完成整体结构转换,再做像素级微调(不要边转边调)
|
||||
- 用 `get_page_snapshot` 获取精确元素位置,比目测高效 10 倍
|
||||
- `evaluate_script` 可以直接操作页面 data,比 UI 操作更可靠
|
||||
- 每轮微调控制在 2-5 处修改,避免一次改太多难以定位效果
|
||||
|
||||
### 7.3 差异收敛规律
|
||||
|
||||
- 第一轮调整通常能降 3-5 个百分点(修复明显的间距错误)
|
||||
- 第二轮再降 2-3 个百分点(精细间距对齐)
|
||||
- 低于 10% 后继续调整收益递减(剩余差异多为字体渲染和平台差异)
|
||||
- 前半屏 < 5% 即可视为达标
|
||||
|
||||
---
|
||||
|
||||
## 八、文档索引
|
||||
|
||||
| 文档 | 路径 | 内容 |
|
||||
|------|------|------|
|
||||
| 迁移指南(理论) | `apps/miniprogram/doc/migration-guide.md` | 6 步流水线 |
|
||||
| 本文档(实战) | `apps/miniprogram/doc/migration-method-full-path.md` | 工具链全路径 |
|
||||
| 避坑文档 | `apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md` | 22+ 个坑点 |
|
||||
| 共享组件规范 | `apps/miniprogram/doc/shared-component-specs.md` | 组件接口定义 |
|
||||
| 迁移追踪 | `apps/miniprogram/doc/migration-tracker.md` | 页面进度 |
|
||||
65
apps/miniprogram - 副本/doc/migration-tracker.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 小程序页面迁移追踪
|
||||
|
||||
> 更新日期:2026-03-07
|
||||
> 迁移规范:`migration-guide.md` + `h5-to-miniprogram-pitfalls.md` §2.2.1(87.5% 缩放)
|
||||
> H5 原型目录:`docs/h5_ui/pages/`
|
||||
> 交互说明目录:`docs/h5_ui/interactions/`
|
||||
|
||||
## 已完成(5 页)
|
||||
|
||||
| 页面 | 小程序路径 | H5 原型 | 状态 | 备注 |
|
||||
|------|-----------|---------|------|------|
|
||||
| no-permission | `pages/no-permission/` | `no-permission.html` | ✅ 已完成 | 参考实现,87.5% 缩放基准页 |
|
||||
| login | `pages/login/` | `login.html` | ✅ 已完成 | 87.5% 缩放已应用 |
|
||||
| reviewing | `pages/reviewing/` | `reviewing.html` | ✅ 已完成 | 87.5% 缩放已应用 |
|
||||
| apply | `pages/apply/` | `apply.html` | ✅ 已完成 | 87.5% 缩放 + 像素级微调 |
|
||||
| board-coach | `pages/board-coach/` | `board-coach.html` | ✅ 已完成 | 87.5% 缩放 + 4 维度卡片 + 组件规范见 `shared-component-specs.md` |
|
||||
| board-customer | `pages/board-customer/` | `board-customer.html` | ✅ 已完成 | 8 维度卡片 + 最专一独立表格布局 + heart-icon 组件 |
|
||||
|
||||
## 正在迁移(1 页)
|
||||
|
||||
| 页面 | 小程序路径 | H5 原型 | 状态 | 备注 |
|
||||
|------|-----------|---------|------|------|
|
||||
| board-finance | `pages/board-finance/` | `board-finance.html` | 🔧 迁移中 | 6 板块完整重写:经营一览(深色)+预收资产(储值卡+赠送卡表格)+应计收入(收入结构+损益链)+现金流入+现金流出(4类网格)+助教分析(基础课+激励课表格) |
|
||||
|
||||
## 待迁移(14 页)
|
||||
|
||||
| 页面 | 小程序路径 | H5 原型 | 交互说明 | 优先级 | 备注 |
|
||||
|------|-----------|---------|----------|--------|------|
|
||||
| task-list | `pages/task-list/` | `task-list.html` | ✅ | 高 | 任务列表(主页 Tab) |
|
||||
| my-profile | `pages/my-profile/` | `my-profile.html` | ✅ | 高 | 个人中心(主页 Tab) |
|
||||
| task-detail | `pages/task-detail/` | `task-detail.html` | ✅ | 高 | 任务详情 |
|
||||
| task-detail-callback | `pages/task-detail-callback/` | `task-detail-callback.html` | ✅ | 中 | 任务详情-回访 |
|
||||
| task-detail-priority | `pages/task-detail-priority/` | `task-detail-priority.html` | ✅ | 中 | 任务详情-优先级 |
|
||||
| task-detail-relationship | `pages/task-detail-relationship/` | `task-detail-relationship.html` | ✅ | 中 | 任务详情-关系 |
|
||||
| coach-detail | `pages/coach-detail/` | `coach-detail.html` | ✅ | 中 | 助教详情 |
|
||||
| customer-detail | `pages/customer-detail/` | `customer-detail.html` | ✅ | 中 | 客户详情 |
|
||||
| customer-service-records | `pages/customer-service-records/` | `customer-service-records.html` | ✅ | 中 | 客户服务记录 |
|
||||
| performance | `pages/performance/` | `performance.html` | ✅ | 中 | 业绩总览 |
|
||||
| performance-records | `pages/performance-records/` | `performance-records.html` | ✅ | 中 | 业绩明细 |
|
||||
| chat | `pages/chat/` | `chat.html` | ✅ | 中 | AI 对话 |
|
||||
| chat-history | `pages/chat-history/` | `chat-history.html` | ✅ | 中 | 对话历史 |
|
||||
| notes | `pages/notes/` | `notes.html` | ✅ | 低 | 备忘录 |
|
||||
|
||||
## 无需 H5 迁移的页面
|
||||
|
||||
| 页面 | 小程序路径 | 说明 |
|
||||
|------|-----------|------|
|
||||
| mvp | `pages/mvp/` | 临时入口/路由分发页,无 H5 原型 |
|
||||
| index | `pages/index/` | 框架默认页,无 H5 原型 |
|
||||
| logs | `pages/logs/` | 框架默认日志页,无 H5 原型 |
|
||||
| dev-tools | `pages/dev-tools/` | 开发调试工具页,无 H5 原型 |
|
||||
|
||||
|
||||
| H5 原型 | 交互说明 | 说明 |
|
||||
|---------|----------|------|
|
||||
| `home-settings.html` | 无 | 首页设置,待确认是否需要 |
|
||||
|
||||
## 迁移流程参考
|
||||
|
||||
1. 输入物冻结(H5 html + interactions md + design-tokens.json)
|
||||
2. 迁移审计(对比当前小程序代码与 H5 原型差异)
|
||||
3. 规则化转换(标签映射 + 87.5% 缩放 + 事件绑定)
|
||||
4. 编译验证(开发者工具无报错)
|
||||
5. 真机差异修复(截图对比像素级微调)
|
||||
6. 验收签收(用户确认)
|
||||
997
apps/miniprogram - 副本/doc/prd.md
Normal file
@@ -0,0 +1,997 @@
|
||||
|
||||
|
||||
# 一、文档信息
|
||||
|
||||
* 产品名称:球房运营助手(微信小程序)
|
||||
* 版本: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 秒(在普通网络环境下)。
|
||||
* 看板页面数据:
|
||||
|
||||
* 各数据块采用懒加载策略,优先加载当前视图及首屏必要数据,其他部分可在滚动时或后台加载,避免一次性加载过多影响首屏体验。
|
||||
* 本阶段不做埋点与统计需求设计。
|
||||
|
||||
114
apps/miniprogram - 副本/doc/shared-component-specs.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 共享组件样式规范(已验证)
|
||||
|
||||
> 来源:board-coach 页面迁移验收通过后提取
|
||||
> 用途:board-customer、board-finance 等看板页面复用
|
||||
> 更新日期:2026-03-07
|
||||
|
||||
## 1. AI 悬浮按钮(ai-float-button)
|
||||
|
||||
- 组件路径:`components/ai-float-button/`
|
||||
- 机器人 SVG icon + 渐变流动动画背景
|
||||
- 默认 bottom 220rpx(在自定义底部导航栏上方)
|
||||
|
||||
## 2. 自定义底部导航栏(board-tab-bar)
|
||||
|
||||
- 组件路径:`components/board-tab-bar/`
|
||||
- 用于非 TabBar 的看板子页面(board-coach、board-customer)
|
||||
- SVG icon 从 H5 原型提取,路径 `/assets/icons/tab-*-nav*.svg`
|
||||
|
||||
### 样式参数
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 高度 | 100rpx |
|
||||
| 背景 | #ffffff |
|
||||
| 边框 | 1rpx solid #eeeeee |
|
||||
| icon 尺寸 | 44rpx × 44rpx |
|
||||
| label 字号 | 20rpx |
|
||||
| label 颜色 | #8b8b8b(默认)/ #0052d9(active) |
|
||||
| active 字重 | 500 |
|
||||
| gap(icon↔label) | 4rpx |
|
||||
| safe-area | padding-bottom: env(safe-area-inset-bottom) |
|
||||
|
||||
## 3. 筛选下拉组件(filter-dropdown)
|
||||
|
||||
- 组件路径:`components/filter-dropdown/`
|
||||
- 全屏宽度面板 + 半透明遮罩 + 动态 top 计算
|
||||
|
||||
### 触发按钮样式
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| padding | 16rpx 20rpx |
|
||||
| 背景 | #ffffff |
|
||||
| 边框 | 2rpx solid var(--color-gray-1) |
|
||||
| 圆角 | var(--radius-md) |
|
||||
| 文字+箭头 | justify-content: center |
|
||||
| label 字号 | 24rpx |
|
||||
| label 字重 | 600 |
|
||||
| active 边框色 | var(--color-primary) |
|
||||
| active 背景 | var(--color-primary-light) |
|
||||
|
||||
### 下拉面板样式
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 定位 | fixed, left:0, right:0 |
|
||||
| 最大高度 | 60vh |
|
||||
| 圆角 | 0 0 28rpx 28rpx |
|
||||
| 阴影 | 0 16rpx 48rpx rgba(0,0,0,0.15) |
|
||||
| 选项 padding | 34rpx 32rpx |
|
||||
| 选项字号 | 28rpx |
|
||||
| 分隔线 | 1rpx solid rgba(0,0,0,0.03) |
|
||||
| active 颜色 | var(--color-primary) |
|
||||
| active 字重 | 500 |
|
||||
|
||||
### 遮罩层
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 背景 | rgba(0,0,0,0.5) |
|
||||
| z-index | 999(遮罩)/ 1000(面板) |
|
||||
|
||||
## 4. 顶部看板 Tab 栏
|
||||
|
||||
- 直接写在页面 wxml 中(非独立组件)
|
||||
- sticky top: 0, z-index: 20
|
||||
|
||||
### 样式参数
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 背景 | #ffffff |
|
||||
| 边框 | 2rpx solid #eeeeee |
|
||||
| tab padding | 24rpx 0 |
|
||||
| tab 字号 | 26rpx |
|
||||
| tab 字重 | 500(默认)/ 600(active) |
|
||||
| tab 颜色 | #8b8b8b(默认)/ #0052d9(active) |
|
||||
| 下划线宽 | 42rpx |
|
||||
| 下划线高 | 5rpx |
|
||||
| 下划线渐变 | linear-gradient(90deg, #0052d9, #5b9cf8) |
|
||||
|
||||
## 5. 筛选栏容器
|
||||
|
||||
- sticky top: 70rpx, z-index: 15
|
||||
- 滚动隐藏/显示(220ms ease 过渡)
|
||||
|
||||
### 样式参数
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 背景 | #f3f3f3 |
|
||||
| padding | 14rpx 28rpx |
|
||||
| 内框背景 | #ffffff |
|
||||
| 内框圆角 | 14rpx |
|
||||
| 内框 padding | 10rpx |
|
||||
| 内框 gap | 14rpx |
|
||||
| 内框边框 | 2rpx solid #eeeeee |
|
||||
| 第一个筛选项 | flex: 1.8 |
|
||||
| 其他筛选项 | flex: 1 |
|
||||
|
||||
## 6. 筛选选项内容(board-coach 专用,供参考)
|
||||
|
||||
### 排序维度
|
||||
- 定档业绩最高 / 定档业绩最低 / 工资最高 / 工资最低 / 客源储值最高 / 任务完成最多
|
||||
|
||||
### 技能筛选
|
||||
- 不限 / 🎱 中式追分 / 斯诺克 / 🀄 麻将棋牌 / 🎤团建K歌
|
||||
|
||||
### 时间筛选
|
||||
- 本月 / 本季度 / 上月 / 前3个月(不含本月) / 上季度 / 最近6个月(不含本月,不支持客源储值最高)
|
||||
11
apps/miniprogram - 副本/i18n/base.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ios": {
|
||||
"name": "桌球运营助手"
|
||||
},
|
||||
"android": {
|
||||
"name": "桌球运营助手"
|
||||
},
|
||||
"common": {
|
||||
"name": "桌球运营助手"
|
||||
}
|
||||
}
|
||||
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',
|
||||
},
|
||||
}
|
||||
65
apps/miniprogram - 副本/miniprogram/app.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"pages": [
|
||||
"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/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": "球房运营助手",
|
||||
"navigationBarBackgroundColor": "#ffffff"
|
||||
},
|
||||
"usingComponents": {
|
||||
"dev-fab": "/components/dev-fab/dev-fab"
|
||||
},
|
||||
"componentFramework": "glass-easel",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
5
apps/miniprogram - 副本/miniprogram/app.miniapp.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"adapteByMiniprogram": {
|
||||
"userName": "gh_521029c3a9c7"
|
||||
}
|
||||
}
|
||||
70
apps/miniprogram - 副本/miniprogram/app.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// app.ts
|
||||
// 应用入口 — 启动时检查登录状态并路由到对应页面
|
||||
import { request } from "./utils/request"
|
||||
|
||||
App<IAppOption>({
|
||||
globalData: {},
|
||||
|
||||
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 页
|
||||
}
|
||||
},
|
||||
})
|
||||
111
apps/miniprogram - 副本/miniprogram/app.wxss
Normal file
@@ -0,0 +1,111 @@
|
||||
/**app.wxss**/
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
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 |
@@ -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 |
@@ -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 |
|
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 |
@@ -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 |
|
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 |
@@ -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": {}
|
||||
}
|
||||
@@ -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 渐变色自动降级,无需额外处理
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
11
apps/miniprogram - 副本/miniprogram/i18n/base.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ios": {
|
||||
"name": "桌球运营助手"
|
||||
},
|
||||
"android": {
|
||||
"name": "桌球运营助手"
|
||||
},
|
||||
"common": {
|
||||
"name": "桌球运营助手"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
},
|
||||
})
|
||||