提交
No files matched your search
@@ -1 +1 @@
|
||||
27977
|
||||
31370
|
||||
@@ -53,6 +53,7 @@ from app.crud.crm.product import (
|
||||
from app.schemas.crm.product import (
|
||||
CategoryDeleteBody,
|
||||
ProductCategoryCreateBody,
|
||||
ProductCategoryTreeBody,
|
||||
ProductCategoryUpdateBody,
|
||||
ProductCreateBody,
|
||||
ProductDeleteBody,
|
||||
@@ -325,23 +326,20 @@ router = APIRouter(prefix="/web/crm", tags=["CRM管理"])
|
||||
|
||||
@router.post("/product-categories/tree")
|
||||
def category_tree(
|
||||
body: dict = None,
|
||||
body: ProductCategoryTreeBody = None,
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取产品分类树"""
|
||||
"""获取产品分类树(名称/状态由服务端过滤)"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_info = crm_user_info(_user)
|
||||
payload = body or {}
|
||||
category_name = payload.get("categoryName") or payload.get("category_name")
|
||||
status = payload.get("status")
|
||||
q = body or ProductCategoryTreeBody()
|
||||
rows = list_category_tree(
|
||||
db,
|
||||
tenant_no,
|
||||
category_name=category_name,
|
||||
status=status,
|
||||
category_name=q.category_name,
|
||||
status=q.status,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(rows)
|
||||
|
||||
@@ -44,6 +44,15 @@ class CategoryDeleteBody(BaseModel):
|
||||
id: str = Field(..., description="分类ID")
|
||||
|
||||
|
||||
class ProductCategoryTreeBody(BaseModel):
|
||||
"""产品分类树查询(密文解密后须用 Pydantic 绑定,裸 dict=None 在部分链路下会丢 body)"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
category_name: Optional[str] = Field(default=None, alias="categoryName", description="分类名称(模糊)")
|
||||
status: Optional[str] = Field(default=None, description="状态")
|
||||
|
||||
|
||||
class ProductUpdateStatusBody(BaseModel):
|
||||
"""产品批量上下架请求体"""
|
||||
|
||||
|
||||
@@ -221,8 +221,22 @@ Tab:首页(占位) | 待办(占位) | 工作台★ | 我的
|
||||
| S9 | 运输 | 运费结算 + 用户选项 | 列表/选人接口正常 | [x] freight 列表 + users/options ok;无待结算 |
|
||||
| S10 | 运输 | 运费待审批列表 | 列表正常 | [x] 列表空态 ok |
|
||||
|
||||
**接口冒烟结论**:S1–S10 **读路径全部通**(0 FAIL)。写路径有 3 处需跟进:① 线索跟进字段;② 报销创建体应用 `details` 而非 `items`;③ 安排运输依赖证书齐备业务数据。
|
||||
**前端 UI**:仍由人工在微信开发者工具/真机验收。
|
||||
**接口冒烟结论**:S1–S10 **读路径全部通**(0 FAIL)。写路径有 3 处需跟进:① 线索跟进字段;② 报销创建体应用 `details` 而非 `items`;③ 安排运输依赖证书齐备业务数据。
|
||||
|
||||
### 7.2 H5 UI 冒烟(Playwright,不改 uni-app 工程)
|
||||
|
||||
> **执行**:2026-07-17,H5 `http://localhost:5173/`,账号 `admin` / `admin123`,后端 `127.0.0.1:8000`。
|
||||
> 脚本:独立目录 `miniprogram-e2e/smoke.mjs`(`cd miniprogram-e2e && node smoke.mjs`);截图与 JSON 在 `miniprogram-e2e/artifacts/`。
|
||||
|
||||
| # | 路径 | 结果 |
|
||||
|---|------|------|
|
||||
| E1 | 登录 → 工作台四模块可见 | [x] PASS |
|
||||
| E2 | 线索管理 | [x] PASS |
|
||||
| E3 | 报销申请 | [x] PASS |
|
||||
| E4 | 地磅仪表盘 | [x] PASS |
|
||||
| E5 | 运输计划 | [x] PASS |
|
||||
|
||||
**UI 冒烟结论**:E1–E5 **全通**(0 FAIL)。微信开发者工具 / 真机验收仍见 §7 未勾项。
|
||||
|
||||
---
|
||||
|
||||
@@ -248,6 +262,8 @@ Tab:首页(占位) | 待办(占位) | 工作台★ | 我的
|
||||
| 2026-07-16 | P1–P4 | CRM / 报销 / 地磅 / 运输业务页落地并接线工作台 |
|
||||
| 2026-07-16 | P4+ | 运费提交审批改为用户选项选人 Sheet;P5 冒烟表写入 §7.1 |
|
||||
| 2026-07-17 | P5 | 接口冒烟 S1–S10 读路径全通(脚本 `mp_api_smoke.py`);写路径 3 处待对齐见 §7.1 |
|
||||
| 2026-07-17 | P5 | H5 Playwright E1–E5 冒烟全通(`miniprogram-e2e/smoke.mjs`,见 §7.2) |
|
||||
| 2026-07-17 | P5 | 按 QA 节奏启动:先全量文字用例(`miniprogram-e2e/cases/`),脚本验证待用例评审后逐步开展 |
|
||||
|
||||
<!-- 每完成一批任务追加一行,例如:
|
||||
| 2026-0x-xx | P0 | TabBar + 工作台落地 |
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 小程序 CRM 产品分类 Implementation Plan
|
||||
|
||||
> **For agentic workers:** Inline execution in this session (user requested 请实现).
|
||||
|
||||
**Goal:** 在小程序工作台落地 CRM「产品分类」缩进展开树管理页,功能对齐 Web 核心 CRUD。
|
||||
|
||||
**Architecture:** 复用 `/web/crm/product-categories/*`;list 页本地 `expandedIds` 控制可见扁平行;form 页独立路由承载新增/编辑/子分类。
|
||||
|
||||
**Tech Stack:** uni-app Vue3、现有 `api/crm.js` + `mp-*` 样式、三墙 Token。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: API + 路由入口
|
||||
|
||||
**Files:**
|
||||
- Modify: `miniprogram/api/crm.js`
|
||||
- Modify: `miniprogram/config/api.js`
|
||||
- Modify: `miniprogram/config/workbench-menus.js`
|
||||
- Modify: `miniprogram/pages.json`
|
||||
|
||||
- [x] 增加 tree/add/update/delete 四个方法
|
||||
- [x] PAGE_PATHS + WORKBENCH_ROUTE_MAP + 菜单 enabled
|
||||
- [x] pages.json 注册 list/form
|
||||
|
||||
### Task 2: 列表页 list.vue
|
||||
|
||||
**Files:**
|
||||
- Create: `miniprogram/pages/crm/product-category/list.vue`
|
||||
|
||||
- [x] 搜索、状态 chips、展开树、行操作、删除确认
|
||||
|
||||
### Task 3: 表单页 form.vue
|
||||
|
||||
**Files:**
|
||||
- Create: `miniprogram/pages/crm/product-category/form.vue`
|
||||
|
||||
- [x] 上级选择、名称/排序/状态/备注、提交
|
||||
|
||||
### Task 4: 收尾
|
||||
|
||||
- [x] 更新 design spec 状态
|
||||
- [x] Demo 标注已采用方案 A
|
||||
@@ -0,0 +1,87 @@
|
||||
# 小程序按钮与弹窗用例补全设计
|
||||
|
||||
> 日期:2026-07-18
|
||||
> 状态:已确认(用户选组织方式 C + 源码静态盘点方案 1)
|
||||
> 范围:`miniprogram` H5/小程序四模块 + 壳层
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
现有 `miniprogram-e2e/cases/00–04` 用例偏重页面与字段,**按钮与弹窗内按钮未做门禁式穷尽覆盖**。
|
||||
人类测试工程师要求:每个界面可点控件都有用例;点击后若出现弹窗 / ActionSheet / Sheet,则**层内每个按钮**也要有独立用例。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
1. 建立总清单门禁:`BUTTON-DIALOG-INVENTORY.md`,无 `TODO` 才算「按钮用例写完整」。
|
||||
2. 详细步骤写在各模块用例文件(`00-shell`…`04-pl`),总清单用用例 ID 回链。
|
||||
3. 仍遵守「一次只测一个界面」的执行节奏(第二步执行时)。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
- 本阶段不执行自动化点测、不改业务代码。
|
||||
- 不覆盖灰卡背后的未实现业务页内部按钮(灰卡仅测 Toast「本期暂未开放」)。
|
||||
|
||||
## 4. 方法(方案 1)
|
||||
|
||||
1. 静态扫描 `pages/**/*.vue` + `components/mp-approver-picker`。
|
||||
2. 识别:页头/底栏/卡片按钮、链接、`uni.showModal`、`uni.showActionSheet`、自定义弹窗、Sheet。
|
||||
3. 写入总清单;弹层内每个按钮单独成行。
|
||||
4. 为每行分配/回填用例 ID;缺则 `TODO`。
|
||||
5. 按界面把 `TODO` 补进模块用例文件后,更新清单状态。
|
||||
|
||||
## 5. 总清单字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| INV-ID | 清单行号,如 `INV-LGN-01` |
|
||||
| 模块 / 界面 / 路径 | 定位 |
|
||||
| 触发控件 | 准确文案 |
|
||||
| 控件类型 | 页头/底栏/卡片/导航/勾选/快捷卡/上传… |
|
||||
| 弹出层类型 | 无 / showModal / ActionSheet / 自定义弹窗 / Sheet |
|
||||
| 弹层标题 | 原文 |
|
||||
| 层内按钮 | 本行关注的那一个按钮文案 |
|
||||
| 显示条件 | 状态/Tab 等 |
|
||||
| 用例 ID | 已写则填;否则 `TODO` |
|
||||
| 最小集 | 见 §6 |
|
||||
|
||||
## 6. 有弹层时的最小用例集(门禁)
|
||||
|
||||
对每个「触发控件 → 弹层」组合,至少包含:
|
||||
|
||||
| # | 用例意图 |
|
||||
|---|----------|
|
||||
| 1 | 点击触发 → 弹层出现(标题/关键文案正确) |
|
||||
| 2 | 点取消/否 → 关闭且业务不变 |
|
||||
| 3 | 点确认/是/通过等 → 成功路径(或校验失败提示) |
|
||||
| 4 | 若遮罩可关 → 点遮罩等同取消(一条) |
|
||||
| 5 | ActionSheet → **每个 item** 至少一条 |
|
||||
| 6 | Sheet → 取消、确定、未选确定校验 各至少一条 |
|
||||
|
||||
无弹层的按钮:至少「点击 → 预期导航/提交/Toast」一条;写操作另补失败/连点等按模块文件既有标准。
|
||||
|
||||
## 7. 交付物
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `docs/superpowers/specs/2026-07-18-mp-button-dialog-cases-design.md` | 本文 |
|
||||
| `miniprogram-e2e/cases/BUTTON-DIALOG-INVENTORY.md` | 总清单门禁 |
|
||||
| `miniprogram-e2e/cases/00–04*.md` | 补齐详细用例(清单驱动,分批) |
|
||||
| `miniprogram-e2e/cases/README.md` / `PROGRESS.md` | 索引更新 |
|
||||
|
||||
## 8. 完成定义(DoD)
|
||||
|
||||
- [ ] 总清单覆盖启用业务页 + 壳层全部可点主操作与全部弹层按钮行
|
||||
- [ ] 清单中无 `TODO`(均已回链用例 ID)
|
||||
- [ ] 模块文件中存在对应该 ID 的可执行步骤(预置/步骤/预期)
|
||||
- [ ] 评审确认后,才进入第二步「按界面执行」
|
||||
|
||||
## 9. 风险
|
||||
|
||||
- 条件按钮(如仅 draft 显示)需造数才能执行,用例仍须先写全。
|
||||
- `showModal` 确认键系统默认为「确定」,用例文案以真机/H5 实际为准。
|
||||
- 费用项目 ActionSheet 的 item 动态来自接口,清单记「动态 item」,用例要求「至少选一项 + 无数据 Toast」。
|
||||
|
||||
## 10. 自检
|
||||
|
||||
- 无 TBD/占位未解释
|
||||
- 与用户已选 C + 方案 1 一致
|
||||
- 不扩大到真机执行本阶段范围
|
||||
@@ -0,0 +1,124 @@
|
||||
# 小程序 · CRM 产品分类(缩进展开树)设计
|
||||
|
||||
> 日期:2026-07-18
|
||||
> 状态:已确认并实现
|
||||
> 视觉 Demo:`mobile-ui/shanxia-mp-crm-product-category-demo.html`(方案 A)
|
||||
> 功能对照:`web-admin/src/views/crm/product-category/index.vue`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
- 小程序工作台 CRM 模块中「产品分类」为灰卡(`enabled: false`),页面未实现。
|
||||
- Web 管理端已具备完整能力:分类树列表、名称/状态筛选、新增/修改/新增子分类/删除、全部展开/折叠。
|
||||
- 后端接口已存在:`/web/crm/product-categories/{tree,add,update,delete}`,小程序可复用(与其它 CRM 页相同,走 admin JWT)。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
1. 小程序工作台可进入「产品分类」管理页,功能对齐 Web 端核心 CRUD。
|
||||
2. 树形数据采用 **方案 A:缩进展开树**(一屏总览 + 行内操作),视觉对齐 Demo 方案 A 与三墙 Token。
|
||||
3. 复用现有后端,不新增接口;不改表结构。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
- 不做 Excel 导入/导出、列显隐、多选批量删除(Web 端工具栏扩展能力本期不做)。
|
||||
- 不做产品管理页(「产品管理」仍为灰卡)。
|
||||
- 不改 Web 端产品分类页。
|
||||
|
||||
## 4. 信息架构与页面
|
||||
|
||||
| 页面 | 路径 | 职责 |
|
||||
|------|------|------|
|
||||
| 分类树列表 | `pages/crm/product-category/list` | 搜索、状态筛选、展开树、行操作入口 |
|
||||
| 新增/编辑表单 | `pages/crm/product-category/form` | 新增顶级 / 新增子分类 / 编辑;选择上级分类 |
|
||||
|
||||
入口:工作台 `产品分类` → `enabled: true`,`code: 'crm-product-category'` → 路由到 list。
|
||||
|
||||
## 5. UI 设计(方案 A)
|
||||
|
||||
### 5.1 列表页骨架
|
||||
|
||||
顺序对齐现有 CRM 子页与 Demo:
|
||||
|
||||
1. Header:返回工作台 · 标题「产品分类」·「全部展开/折叠」·「+ 新增」
|
||||
2. 搜索框:分类名称(确认搜索,调用 tree 接口 `categoryName`)
|
||||
3. 状态 chips:全部 / 正常 / 停用(对应 `status` 空 / `0` / `1`,与 Web 字典 `sys_normal_disable` 一致)
|
||||
4. 树列表:白底圆角容器内的缩进行
|
||||
|
||||
### 5.2 树行
|
||||
|
||||
每行展示:
|
||||
|
||||
- 左侧展开箭头(无子节点隐藏);缩进按层级(约 18px/级)
|
||||
- 分类名称(主文案)
|
||||
- 元信息:状态 Tag(正常=success / 停用=muted)· 排序号 · 子类数量(有子时)
|
||||
- 右侧:`+`(新增子分类)· `⋯`(打开 ActionSheet:编辑 / 新增子分类 / 删除)
|
||||
|
||||
交互:
|
||||
|
||||
- 点箭头:仅展开/折叠该节点(本地 `expandedIds`,默认展开一级有子节点)
|
||||
- 点「全部展开/折叠」:切换整树展开态(对齐 Web)
|
||||
- 点「+ 新增」:进入 form,上级为空(顶级)
|
||||
- 点行内 `+` 或 Sheet「新增子分类」:进入 form,预填 `parentId`
|
||||
- 点 Sheet「编辑」:进入 form,带 `id`
|
||||
- 点 Sheet「删除」:`uni.showModal` 确认后调 delete,成功刷新树
|
||||
|
||||
树为全量接口(不分页),不做触底加载。
|
||||
|
||||
### 5.3 表单页字段
|
||||
|
||||
对齐 Web 弹窗字段(精简交互适配移动端):
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| 上级分类 | 否 | 选择器:树扁平为带缩进/全路径选项;留空或选「顶级分类」→ `parentId=0`;新增子分类时预填且可改 |
|
||||
| 分类名称 | 是 | |
|
||||
| 排序号 | 否 | 数字,默认 0 |
|
||||
| 状态 | 是 | 正常 `0` / 停用 `1`,默认 `0`(与 Web 表单默认一致) |
|
||||
| 备注 | 否 | |
|
||||
|
||||
编辑时禁止把节点设为自己或自己的子孙为上级(前端用当前树校验;后端若已有校验则透传错误 Toast)。
|
||||
|
||||
提交成功后返回列表并刷新。
|
||||
|
||||
## 6. 数据与 API
|
||||
|
||||
在 `miniprogram/api/crm.js` 增加(与 Web 同路径):
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| `listProductCategoryTree` | `POST /web/crm/product-categories/tree` | body: `{ categoryName?, status? }` → 嵌套 `children` |
|
||||
| `addProductCategory` | `POST /web/crm/product-categories/add` | |
|
||||
| `updateProductCategory` | `POST /web/crm/product-categories/update` | |
|
||||
| `deleteProductCategory` | `POST /web/crm/product-categories/delete` | `{ id }` |
|
||||
|
||||
列表页直接消费树结构;表单上级选择器可对同一 tree 做扁平遍历(展示全路径,分隔符 ` / `)。
|
||||
|
||||
删除权限:若接口返回创建人限制类错误,Toast 展示后端文案(与 Web `canDeleteByCreator` 语义一致,小程序侧不做复杂权限 UI,依赖接口)。
|
||||
|
||||
## 7. 接入改动清单
|
||||
|
||||
1. `pages/crm/product-category/list.vue`、`form.vue`(新建)
|
||||
2. `pages.json` 注册两页
|
||||
3. `config/api.js`:`PAGE_PATHS` + `WORKBENCH_ROUTE_MAP`
|
||||
4. `config/workbench-menus.js`:产品分类 `enabled: true`,补 `code`
|
||||
5. `api/crm.js`:四个分类接口
|
||||
6. 样式:优先复用 `mp-*` 全局类;树行专用样式可写在 list 页 scoped(Token 用三墙绿,勿用 Web `#10b981`)
|
||||
7. Demo:保留 `mobile-ui/shanxia-mp-crm-product-category-demo.html` 作为视觉权威;实现后可把方案切换条标注「已采用 A」
|
||||
|
||||
## 8. 错误与空态
|
||||
|
||||
- 加载失败:Toast + 空列表
|
||||
- 树为空:空态文案「暂无产品分类」+ 引导新增
|
||||
- 表单校验失败:字段级 Toast(名称必填)
|
||||
- 删除确认取消:无副作用
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
1. 工作台「产品分类」可点进列表,不再 Toast「本期暂未开放」。
|
||||
2. 树层级、展开折叠、搜索、状态筛选与 Web 数据一致(同租户同账号)。
|
||||
3. 可完成:新增顶级 → 新增子分类 → 编辑 → 删除(有子节点时以后端规则为准,失败有提示)。
|
||||
4. 视觉与 Demo 方案 A 一致:绿渐变头、缩进树、状态 Tag、底部/Sheet 操作。
|
||||
|
||||
## 10. 测试建议
|
||||
|
||||
- 手工:三级树展开/折叠、搜索命中后祖先仍可见(后端 tree 已带祖先)、停用筛选、表单上级选择、删除确认。
|
||||
- 若维护 `miniprogram-e2e` 按钮清单:为本页补 INV 行(列表头按钮、行内 +/⋯、Sheet 三项、删除 Modal、表单提交)。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 测试进度(人类测试工程师节奏)
|
||||
|
||||
## 阶段状态
|
||||
|
||||
| 阶段 | 内容 | 状态 |
|
||||
|------|------|------|
|
||||
| **第一步·功能用例** | 全量文字用例 ~1200+ | 已有全面版 |
|
||||
| **第一步·按钮弹窗门禁** | 设计 + 总清单盘点 | **清单已生成;用例 ID 仍有 TODO** |
|
||||
| **第二步** | 按界面执行验证 | 未开始(等清单 TODO 清零并评审) |
|
||||
|
||||
## 关键交付(2026-07-18)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `docs/superpowers/specs/2026-07-18-mp-button-dialog-cases-design.md` | 设计(方式 C + 源码盘点) |
|
||||
| `miniprogram-e2e/cases/BUTTON-DIALOG-INVENTORY.md` | 按钮/弹窗总清单门禁 |
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 按清单把各行 `TODO` 补进 `00–04`(优先:登录协议弹窗 → 线索转客户/无效 → 公海领取 → 报销审批弹窗 → 地磅作废 → 运输证书/结算 Sheet)。
|
||||
2. 清单无 `TODO` 后,回复「清单 OK,开始测登录页」进入第二步。
|
||||
|
||||
确认前不开始大规模点测执行。
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"base": "http://localhost:5173",
|
||||
"user": "admin",
|
||||
"results": [
|
||||
{
|
||||
"id": "E1",
|
||||
"ok": true,
|
||||
"detail": "工作台=true CRM=true 报销=true 地磅=true 运输=true"
|
||||
},
|
||||
{
|
||||
"id": "E2",
|
||||
"ok": true,
|
||||
"detail": "线索列表页已打开"
|
||||
},
|
||||
{
|
||||
"id": "E3",
|
||||
"ok": true,
|
||||
"detail": "报销申请页已打开"
|
||||
},
|
||||
{
|
||||
"id": "E4",
|
||||
"ok": true,
|
||||
"detail": "地磅仪表盘已打开"
|
||||
},
|
||||
{
|
||||
"id": "E5",
|
||||
"ok": true,
|
||||
"detail": "运输计划页已打开"
|
||||
}
|
||||
],
|
||||
"fail": 0,
|
||||
"ts": "2026-07-17T14:15:50.563Z"
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
# 壳层 — 全面测试用例(登录 / Tab / 工作台 / 我的)
|
||||
|
||||
> 每一检索、每一字段、每一按钮、协议链路均单独覆盖;无业务增删改则写负向确认。
|
||||
|
||||
---
|
||||
|
||||
## A. 登录页 `login/login`
|
||||
|
||||
### A1 页面展示(逐项)
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| LGN-D01 | Logo 区 | P1 | 打开登录页 | 可见 Logo 图 |
|
||||
| LGN-D02 | 应用名 | P0 | — | 「山峡化工」 |
|
||||
| LGN-D03 | 副标题 | P0 | — | 「一体化业务移动端」 |
|
||||
| LGN-D04 | 标签「用户名」 | P0 | — | 可见 |
|
||||
| LGN-D05 | 标签「密码」 | P0 | — | 可见 |
|
||||
| LGN-D06 | 用户名占位 | P0 | 未输入 | 「请输入用户名」 |
|
||||
| LGN-D07 | 密码占位 | P0 | 未输入 | 「请输入密码」 |
|
||||
| LGN-D08 | 登录按钮文案 | P0 | 非 loading | 「账号密码登录」 |
|
||||
| LGN-D09 | 协议文案 | P0 | — | 「登录即表示同意」+《用户服务协议》+「和」+《隐私政策》 |
|
||||
| LGN-D10 | 勾选框默认未选 | P0 | 首次进入 | 未勾选态 |
|
||||
|
||||
### A2 协议勾选与弹窗(逐项)
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| LGN-P01 | 未勾选点登录弹窗 | P0 | 填账号密码→登录 | 弹窗标题「协议确认」 |
|
||||
| LGN-P02 | 弹窗正文 | P0 | — | 「登录前请先同意《用户服务协议》和《隐私政策》」 |
|
||||
| LGN-P03 | 弹窗按钮「否」「是,继续登录」 | P0 | — | 两个按钮都在 |
|
||||
| LGN-P04 | 点「否」 | P0 | — | 弹窗关;仍登录页;未跳转 |
|
||||
| LGN-P05 | 点遮罩等同否 | P1 | 点遮罩 | 弹窗关;未登录成功 |
|
||||
| LGN-P06 | 点「是」继续校验 | P0 | 用户名为空时点是 | 弹窗关;走空用户名校验 |
|
||||
| LGN-P07 | 勾选后不再弹窗 | P0 | 点勾选→填账密→登录 | **不**出现协议确认弹窗 |
|
||||
| LGN-P08 | 勾选再取消仍弹窗 | P0 | 勾选→再点取消勾选→登录 | 再次弹协议确认 |
|
||||
| LGN-P09 | 打开用户服务协议 | P0 | 点《用户服务协议》 | 进入协议页,可滚动正文 |
|
||||
| LGN-P10 | 打开隐私政策 | P0 | 点《隐私政策》 | 进入隐私页 |
|
||||
| LGN-P11 | 协议页返回 | P1 | 系统返回 | 回登录页 |
|
||||
|
||||
### A3 字段校验与登录(逐项)
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| LGN-V01 | 空用户名 | P0 | 已同意协议;用户名空;密码任意→登录 | Toast「请输入用户名」 |
|
||||
| LGN-V02 | 空密码 | P0 | 已同意;仅用户名→登录 | Toast「请输入密码」 |
|
||||
| LGN-V03 | 用户名密码都空 | P0 | 已同意;都空→登录 | 先「请输入用户名」 |
|
||||
| LGN-V04 | 错误密码 | P0 | 正确用户+错误密码 | Toast 失败类;仍登录页 |
|
||||
| LGN-V05 | 错误用户名 | P0 | 不存在用户+任意密码 | Toast 失败类;仍登录页 |
|
||||
| LGN-V06 | 正确登录 | P0 | admin/admin123(已同意) | 进工作台;见「工作台」 |
|
||||
| LGN-V07 | 登录中文案 | P1 | 提交后观察 | 「登录中...」;按钮禁用语义 |
|
||||
| LGN-V08 | 网络失败 | P2 | 停后端再登录 | Toast 网络失败类 |
|
||||
| LGN-V09 | 已登录再进登录页 | P1 | 有 Token 打开登录 | 自动跳转工作台/首页 |
|
||||
| LGN-V10 | 密码可输入掩码 | P1 | 输入密码 | 界面为密文点 |
|
||||
|
||||
### A4 负向(本页无增删改业务)
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| LGN-N01 | 无注册入口 | 页面无「注册」 |
|
||||
| LGN-N02 | 无忘记密码(若未做) | 无该入口或按产品说明 |
|
||||
|
||||
---
|
||||
|
||||
## B. 工作台 `workbench/workbench`
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| WB-D01 | 标题 | P0 | 进入 | 「工作台」 |
|
||||
| WB-D02 | 副标题 | P0 | — | 「选择业务模块,进入对应功能办理」 |
|
||||
| WB-M01 | CRM 模块标题与描述 | P0 | — | 「CRM 管理」;描述含线索·商机… |
|
||||
| WB-M02 | 报销模块标题与描述 | P0 | — | 「报销管理」 |
|
||||
| WB-M03 | 地磅模块标题与描述 | P0 | — | 「地磅管理」 |
|
||||
| WB-M04 | 运输模块标题与描述 | P0 | — | 「运输管理」 |
|
||||
| WB-E01~E21 | 每个启用菜单可进入 | P0 | 逐一点启用菜单 | 进入对应业务页且标题正确(21 个启用项各记一条执行勾选) |
|
||||
| WB-G01 | 产品分类灰卡 | P0 | 点击 | Toast「本期暂未开放」 |
|
||||
| WB-G02 | 产品管理 | P0 | 点击 | 同上 |
|
||||
| WB-G03 | 合同审批 | P0 | 点击 | 同上 |
|
||||
| WB-G04 | 借款发放 | P0 | 点击 | 同上 |
|
||||
| WB-G05 | 全部单据 | P0 | 点击 | 同上 |
|
||||
| WB-G06 | 付款记录 | P0 | 点击 | 同上 |
|
||||
| WB-G07 | 财务发放 | P0 | 点击 | 同上 |
|
||||
| WB-G08 | 车辆充值 | P0 | 点击 | 同上 |
|
||||
| WB-G09 | 车辆维修 | P0 | 点击 | 同上 |
|
||||
| WB-G10 | 运输地址库 | P0 | 点击 | 同上 |
|
||||
| WB-A01 | 未登录拦截 | P0 | 清 Token 打开 | 跳转登录 |
|
||||
| WB-A02 | 无检索框 | P1 | 查看 | 工作台本身无搜索(菜单靠点击) |
|
||||
|
||||
**启用菜单逐条执行清单(WB-E)**
|
||||
线索管理、商机管理、客户管理、公海池、报价管理、合同管理、待办任务、报销申请、报销审批、借款申请、借款审批、我的单据、仪表盘、地磅计划、地磅单管理、地磅出入库、运输计划、计划归档、购买证上传、运费结算、运费审批。
|
||||
|
||||
---
|
||||
|
||||
## C. 首页 / 待办占位
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| HOME-01 | 标题与副标题 | 切首页 | 「首页」;「概览与快捷入口(本期占位)」 |
|
||||
| HOME-02 | 占位主文案 | — | 「首页功能建设中」 |
|
||||
| HOME-03 | 占位说明 | — | 引导去工作台办理业务 |
|
||||
| HOME-04 | 「前往工作台」 | 点击 | 切到工作台 Tab |
|
||||
| HOME-05 | 无业务列表/检索 | — | 无搜索、无业务卡片增删改 |
|
||||
| HOME-06 | 未登录拦截 | 清 Token | 跳登录 |
|
||||
| TODO-01 | 标题与副标题 | 切待办 | 「待办」;跨模块待办占位说明 |
|
||||
| TODO-02 | 占位主文案 | — | 「待办功能建设中」 |
|
||||
| TODO-03 | 无检索/无单据操作 | — | 无列表增删改 |
|
||||
| TODO-04 | 未登录拦截 | — | 跳登录 |
|
||||
|
||||
---
|
||||
|
||||
## D. 我的 `mine/mine`(字段逐项)
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| MINE-H01 | 头像区 | P1 | 进入我的 | 有头像展示 |
|
||||
| MINE-H02 | Hero 姓名 | P0 | — | realName 或 username;未登录「未登录」 |
|
||||
| MINE-H03 | Hero 部门行 | P1 | — | 「山峡化工 · {部门}」类 |
|
||||
| MINE-F01 | 字段「姓名」 | P0 | 对照接口 | 展示一致 |
|
||||
| MINE-F02 | 字段「账号」 | P0 | — | username |
|
||||
| MINE-F03 | 字段「手机号」 | P0 | — | phone/mobile 或「—」 |
|
||||
| MINE-F04 | 字段「邮箱」 | P0 | — | email 或「—」 |
|
||||
| MINE-F05 | 字段「所属部门」 | P0 | — | deptName 或「—」 |
|
||||
| MINE-F06 | 字段「角色」 | P0 | — | userRole 或「—」 |
|
||||
| MINE-B01 | 「保存修改」 | P0 | 点击 | Toast「个人信息修改将在后续开放」;**数据未改** |
|
||||
| MINE-B02 | 确认无真实编辑 | P0 | 尝试改字段 | 字段只读或无法改持久化 |
|
||||
| MINE-B03 | 「退出登录」 | P0 | 点击 | 回登录页;再进工作台需登录 |
|
||||
| MINE-B04 | 退出 loading | P2 | 点击退出 | 有 loading 态 |
|
||||
| MINE-T01 | 底部提示 | P2 | — | 「本期支持查看…资料编辑后续开放」 |
|
||||
| MINE-A01 | 未登录拦截 | P0 | — | 跳登录 |
|
||||
| MINE-N01 | 无删除账号 | P1 | — | 无注销/删除账号入口 |
|
||||
|
||||
---
|
||||
|
||||
## E. TabBar
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| TAB-01 | 四项文案 | 查看底栏 | 首页 / 待办 / 工作台 / 我的 |
|
||||
| TAB-02 | 逐个切换 | 点四个 Tab | 各页正确打开 |
|
||||
| TAB-03 | 切换不掉登录 | 已登录来回切 | 仍登录态 |
|
||||
| TAB-04 | 登录后默认工作台 | 刚登录 | 落在工作台 |
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
| 批次 | 日期 | 结果 | 备注 |
|
||||
|------|------|------|------|
|
||||
| — | — | — | 文字用例阶段 |
|
||||
|
||||
|
||||
## 附录:加厚用例(边界 / 组合 / 回归)
|
||||
|
||||
### 登录 — 输入边界与组合
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| LGN-X001 | 用户名仅空格 | 同意协议后用户名空格密码有→登录 | Toast「请输入用户名」或等价 |
|
||||
| LGN-X002 | 密码仅空格 | 用户名有密码空格 | Toast「请输入密码」 |
|
||||
| LGN-X003 | 用户名超长 | 200+字符用户名 | 失败提示或不崩溃 |
|
||||
| LGN-X004 | 密码超长 | 200+字符密码 | 同上 |
|
||||
| LGN-X005 | 用户名含 SQL 片段 | `admin'--` | 登录失败;无异常页 |
|
||||
| LGN-X006 | 密码含特殊符号 | `!@#$%^&*()` | 按真实密码对错 |
|
||||
| LGN-X007 | 复制粘贴用户名 | 粘贴含换行 | trim 或失败提示 |
|
||||
| LGN-X008 | 切换大小写用户名 | Admin vs admin | 按后端大小写规则 |
|
||||
| LGN-X009 | 正确用户错误密码×3 | 连续3次错密 | 每次失败 Toast;仍可再试(无锁则记录) |
|
||||
| LGN-X010 | 登录成功后返回键 | 进工作台后浏览器返回 | 不回到可重复提交的脏登录态(或需重新登录) |
|
||||
| LGN-X011 | 协议勾选后刷新页 | 勾选后刷新 | 勾选重置为未选 |
|
||||
| LGN-X012 | 填表后刷新 | 已填账密刷新 | 输入清空 |
|
||||
| LGN-X013 | 登录中断网恢复 | 登录中断网 | 失败提示;可重试 |
|
||||
| LGN-X014 | 协议页打开后登录按钮仍在栈底 | 开协议再返回 | 仍在登录;输入可保留或清空(记录) |
|
||||
| LGN-X015 | 双协议页互相切换 | 协议↔隐私 | 均可打开正文 |
|
||||
| LGN-X016 | 隐私页返回登录 | 返回 | 回登录 |
|
||||
| LGN-X017 | 协议正文可滚动到底 | 长文 | 滚到底无卡死 |
|
||||
| LGN-X018 | 登录按钮连点 | 正确账密连点 | 只成功跳转一次 |
|
||||
| LGN-X019 | loading 时再点协议勾选 | 登录中点勾选 | 不破坏请求 |
|
||||
| LGN-X020 | 成功后 Token 存在 | 登录成功 | 本地有 token(H5 storage) |
|
||||
|
||||
### 工作台 — 启用菜单逐条标题核对
|
||||
|
||||
| ID | 菜单 | 进入后标题含 |
|
||||
|----|------|--------------|
|
||||
| WB-X001 | 线索管理 | 线索 |
|
||||
| WB-X002 | 商机管理 | 商机 |
|
||||
| WB-X003 | 客户管理 | 客户 |
|
||||
| WB-X004 | 公海池 | 公海 |
|
||||
| WB-X005 | 报价管理 | 报价 |
|
||||
| WB-X006 | 合同管理 | 合同 |
|
||||
| WB-X007 | 待办任务 | 待办 |
|
||||
| WB-X008 | 报销申请 | 报销申请 |
|
||||
| WB-X009 | 报销审批 | 报销审批 |
|
||||
| WB-X010 | 借款申请 | 借款申请 |
|
||||
| WB-X011 | 借款审批 | 借款审批 |
|
||||
| WB-X012 | 我的单据 | 我的单据 |
|
||||
| WB-X013 | 仪表盘 | 仪表盘 |
|
||||
| WB-X014 | 地磅计划 | 地磅计划 |
|
||||
| WB-X015 | 地磅单管理 | 地磅单 |
|
||||
| WB-X016 | 地磅出入库 | 出入库 |
|
||||
| WB-X017 | 运输计划 | 运输计划 |
|
||||
| WB-X018 | 计划归档 | 归档 |
|
||||
| WB-X019 | 购买证上传 | 购买证或证书 |
|
||||
| WB-X020 | 运费结算 | 运费结算 |
|
||||
| WB-X021 | 运费审批 | 运费审批 |
|
||||
|
||||
### 工作台 — 灰卡连点与回归
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| WB-X030 | 灰卡连点3次 | 连点产品分类 | 每次 Toast;不跳转 |
|
||||
| WB-X031 | 灰卡后点启用菜单 | 先灰卡再线索 | 线索可正常进 |
|
||||
| WB-X032 | 全部灰卡扫一遍 | 10个灰卡 | 均「本期暂未开放」 |
|
||||
| WB-X033 | 工作台下拉刷新(若有) | 下拉 | 不丢模块 |
|
||||
| WB-X034 | 横竖屏(H5) | 旋转 | 布局可用 |
|
||||
|
||||
### 我的 — 字段空值与操作
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| MINE-X001 | 手机号空展示 | 账号无手机 | 「—」或空,不报错 |
|
||||
| MINE-X002 | 邮箱空展示 | 无邮箱 | 「—」或空 |
|
||||
| MINE-X003 | 部门空展示 | 无部门 | 「—」或空 |
|
||||
| MINE-X004 | 角色空展示 | 无角色 | 「—」或空 |
|
||||
| MINE-X005 | 保存修改连点 | 连点3次 | 每次 Toast 未开放 |
|
||||
| MINE-X006 | 退出取消(若有确认) | 若有确认框取消 | 仍登录 |
|
||||
| MINE-X007 | 退出后 storage 清空 | 退出 | token 清除 |
|
||||
| MINE-X008 | 退出后再登录 | 退出→登录 | 成功进工作台 |
|
||||
| MINE-X009 | 我的↔工作台切换 | 反复切 | 信息仍显示 |
|
||||
| MINE-X010 | onShow 刷新用户 | 他端改资料后回我的 | 尽可能刷新(记录) |
|
||||
|
||||
### Tab / 首页 / 待办加厚
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| TAB-X001 | 工作台选中态 | 在工作台时 Tab 高亮 |
|
||||
| TAB-X002 | 我的选中态 | 高亮 |
|
||||
| TAB-X003 | 首页选中态 | 高亮 |
|
||||
| TAB-X004 | 待办选中态 | 高亮 |
|
||||
| TAB-X005 | 快速连切 Tab 20次 | 不白屏不掉登录 |
|
||||
| HOME-X001 | 前往工作台后返回首页 | 可再切回首页占位 |
|
||||
| HOME-X002 | 首页无搜索 | 无 |
|
||||
| TODO-X001 | 待办无列表操作 | 无增删改 |
|
||||
| TODO-X002 | 待办说明文案完整 | 含后续开放说明 |
|
||||
@@ -0,0 +1,678 @@
|
||||
# CRM 管理 — 完整测试用例集(测试工程师版)
|
||||
|
||||
> 范围:`miniprogram/pages/crm/**`
|
||||
> 空值展示:「—」;金额:「¥」千分位
|
||||
> 能力:仅线索可**新增**;全模块**无编辑页、无删除**;写操作含跟进/转客户/无效/公海领取
|
||||
> 执行约定:缺造数标 `Blocked`;实现与预期不符记缺陷,**不删用例**
|
||||
|
||||
**本文件用例规模目标:≥400 条独立 ID(含组合展开)。**
|
||||
|
||||
---
|
||||
|
||||
# 一、线索管理 · 列表 `crm/clue/list`
|
||||
|
||||
## 1.1 进入 / 离开 / 鉴权
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-001 | 工作台入口进入 | P0 | 已登录 | 点「线索管理」 | 打开列表;标题「线索管理」 |
|
||||
| CRM-CL-002 | 页头左侧返回文案 | P0 | 在列表 | 查看 | 「← 工作台」 |
|
||||
| CRM-CL-003 | 页头右侧新增文案 | P0 | 在列表 | 查看 | 「新增」 |
|
||||
| CRM-CL-004 | 点返回回工作台 | P0 | 在列表 | 点「← 工作台」 | 工作台可见四模块 |
|
||||
| CRM-CL-005 | 未登录深链拦截 | P0 | 清 Token | 打开线索列表 URL | 跳转登录页 |
|
||||
| CRM-CL-006 | 登录后回跳或可再进 | P1 | 从拦截登录成功 | 再进线索 | 列表正常 |
|
||||
| CRM-CL-007 | 从详情返回列表定位 | P1 | 详情点返回 | — | 回到列表(Tab/滚动按实现记录) |
|
||||
| CRM-CL-008 | 从新增取消回列表 | P1 | 新增点取消 | — | 列表页,无脏数据 |
|
||||
| CRM-CL-009 | 连续进出列表稳定性 | P2 | — | 进→返工作台→再进 ×3 | 无白屏/错乱 |
|
||||
| CRM-CL-010 | 页面标题系统栏(H5) | P2 | — | 看 document.title/导航 | 不报错即可 |
|
||||
|
||||
## 1.2 空态 / 加载 / 分页
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-020 | 全空空态文案 | P0 | 库中无线索或筛空 | 打开全部 | 「暂无线索」 |
|
||||
| CRM-CL-021 | 空态时仍有搜索框 | P1 | 空列表 | 查看 | 搜索框可见可输入 |
|
||||
| CRM-CL-022 | 空态时仍有 Tab | P1 | 空列表 | 查看 | 5 个 Tab 可见 |
|
||||
| CRM-CL-023 | 空态时仍可点新增 | P0 | 空列表 | 点新增 | 进入表单 |
|
||||
| CRM-CL-024 | 首次加载 loading | P1 | 弱网 | 进入列表 | 「加载中…」后出数据或空态 |
|
||||
| CRM-CL-025 | 触底加载更多 | P0 | 数据>20 | 滑到底 | 请求下一页;条数增加 |
|
||||
| CRM-CL-026 | 已加载全部提示 | P1 | 已到最后一页 | 再触底 | 「已加载全部」 |
|
||||
| CRM-CL-027 | 加载失败 Toast | P1 | mock 接口失败 | 进入/翻页 | Toast「加载失败」或接口 message |
|
||||
| CRM-CL-028 | 失败后可再次进入重试 | P1 | 上一条后 | 返回再进 | 可恢复 |
|
||||
| CRM-CL-029 | pageSize=20 边界 | P2 | 恰好 20 条 | 触底 | 再请求或已全部(记录) |
|
||||
| CRM-CL-030 | 恰好 21 条第二页 1 条 | P2 | 21 条 | 触底 | 多 1 条 |
|
||||
|
||||
## 1.3 负向:无编辑 / 无删除
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| CRM-CL-040 | 卡片无编辑按钮 | P0 | 有数据扫所有卡片 | 无「编辑」「修改」 |
|
||||
| CRM-CL-041 | 卡片无删除按钮 | P0 | 同上 | 无「删除」「移除」 |
|
||||
| CRM-CL-042 | 页头无批量删除 | P0 | 看页头 | 无批量删 |
|
||||
| CRM-CL-043 | 长按无菜单删除(H5) | P2 | 长按卡片 | 无删除菜单(或记录实际) |
|
||||
| CRM-CL-044 | 左滑无删除(若无实现) | P2 | 左滑 | 无删除动作 |
|
||||
|
||||
## 1.4 搜索框 — 基础与边界
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-100 | placeholder 文案 | P0 | — | 查看 | 「搜索姓名 / 公司 / 手机」 |
|
||||
| CRM-CL-101 | 空关键字点搜索 | P1 | — | 不输入直接确认搜索 | 展示当前 Tab 全量或保持;不崩溃 |
|
||||
| CRM-CL-102 | 仅空格搜索 | P1 | — | 输入空格→搜索 | 空态或全量(记录);不崩溃 |
|
||||
| CRM-CL-103 | 前后空格 trim | P1 | 姓名「张三」 | 输入「 张三 」搜索 | 能命中张三 |
|
||||
| CRM-CL-104 | 超长关键字 | P2 | — | 输入 200+ 字符搜索 | 不白屏;空态或截断 |
|
||||
| CRM-CL-105 | 特殊字符 | P2 | — | 输入 `%'\"<>&` | 不脚本注入;空或正常过滤 |
|
||||
| CRM-CL-106 | emoji 搜索 | P2 | — | 输入 🙂 | 不崩溃 |
|
||||
| CRM-CL-107 | 数字串非手机 | P1 | — | 输入 123 | 按实现匹配手机/姓名 |
|
||||
| CRM-CL-108 | 清空按钮/手动清空 | P1 | 已有关键字结果 | 清空再搜索 | 恢复列表 |
|
||||
| CRM-CL-109 | 连续快速搜索 | P2 | — | 快速换关键词搜 5 次 | 最终结果与最后一次关键词一致 |
|
||||
| CRM-CL-110 | 搜索后下拉触底分页 | P1 | 命中>20 | 搜索后触底 | 仍在过滤条件下分页 |
|
||||
|
||||
## 1.5 搜索 — 按姓名(独立)
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-120 | 姓名全称命中 | P0 | 线索姓名=测试甲 | 搜「测试甲」 | 仅含该线索(或含该名) |
|
||||
| CRM-CL-121 | 姓名前缀命中 | P0 | 姓名=张三丰 | 搜「张三」 | 命中 |
|
||||
| CRM-CL-122 | 姓名后缀命中 | P1 | 同上 | 搜「三丰」 | 命中或按后端规则 |
|
||||
| CRM-CL-123 | 姓名中间字命中 | P1 | 同上 | 搜「三」 | 命中或按规则 |
|
||||
| CRM-CL-124 | 姓名大小写(英文名) | P2 | 姓名含 Alex | 搜 alex / ALEX | 按后端规则记录 |
|
||||
| CRM-CL-125 | 姓名不存在 | P0 | — | 搜「绝不存在姓名XYZ」 | 「暂无线索」 |
|
||||
| CRM-CL-126 | 多条同名 | P1 | 两人都叫王五 | 搜「王五」 | 两条都出 |
|
||||
| CRM-CL-127 | 姓名命中后核对主标题 | P0 | 命中后 | 看卡片主标题 | 等于线索姓名 |
|
||||
|
||||
## 1.6 搜索 — 按公司(独立)
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-140 | 公司全称命中 | P0 | 公司=山峡化工有限公司 | 搜全称 | 命中;副标题含公司 |
|
||||
| CRM-CL-141 | 公司关键词命中 | P0 | 同上 | 搜「山峡」 | 命中 |
|
||||
| CRM-CL-142 | 公司不存在 | P0 | — | 搜「不存在公司ZZZ」 | 空态 |
|
||||
| CRM-CL-143 | 公司与姓名同关键字 | P1 | 姓名含「华」、公司含「华」 | 搜「华」 | 两类均可出;不报错 |
|
||||
| CRM-CL-144 | 命中后核对副标题公司 | P0 | 命中 | 看副标题 | 公司字段正确 |
|
||||
|
||||
## 1.7 搜索 — 按手机(独立)
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-160 | 完整 11 位命中 | P0 | 手机=13800138000 | 搜完整号 | 命中;行「手机」正确 |
|
||||
| CRM-CL-161 | 手机后 4 位 | P1 | 同上 | 搜 8000 | 命中或按规则 |
|
||||
| CRM-CL-162 | 手机前 3 位 | P1 | 同上 | 搜 138 | 命中或按规则 |
|
||||
| CRM-CL-163 | 错误手机号 | P0 | — | 搜 10000000000 | 空态 |
|
||||
| CRM-CL-164 | 带横线/空格手机 | P2 | — | 搜 138-0013-8000 | 按规则;不崩溃 |
|
||||
| CRM-CL-165 | 命中后核对手机行 | P0 | 命中 | 看「手机」行 | 与数据一致 |
|
||||
|
||||
## 1.8 Tab — 文案与默认
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| CRM-CL-200 | Tab1 文案「全部」 | P0 | 查看 | 存在 |
|
||||
| CRM-CL-201 | Tab2 文案「我的」 | P0 | 查看 | 存在 |
|
||||
| CRM-CL-202 | Tab3 文案「新线索」 | P0 | 查看 | 存在 |
|
||||
| CRM-CL-203 | Tab4 文案「跟进中」 | P0 | 查看 | 存在 |
|
||||
| CRM-CL-204 | Tab5 文案「已转化」 | P0 | 查看 | 存在 |
|
||||
| CRM-CL-205 | 默认选中「全部」 | P0 | 刚进入 | 「全部」为选中态 |
|
||||
| CRM-CL-206 | 选中态样式可区分 | P1 | 切换 | 当前 Tab 高亮与其它不同 |
|
||||
|
||||
## 1.9 Tab — 每个 Tab 独立功能
|
||||
|
||||
| ID | 标题 | 优先级 | 预置 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|------|
|
||||
| CRM-CL-210 | 全部-多状态混合 | P0 | 各状态均有 | 点全部 | 可见多种状态 Tag |
|
||||
| CRM-CL-211 | 全部-含无效线索 | P1 | 有无效 | 点全部 | 无效 Tag「无效」可见(若全部包含) |
|
||||
| CRM-CL-212 | 我的-仅本人负责 | P0 | 本人与他人线索 | 点我的 | 负责人均为当前用户 |
|
||||
| CRM-CL-213 | 我的-无本人时为空 | P1 | 本人无线索 | 点我的 | 「暂无线索」 |
|
||||
| CRM-CL-214 | 新线索-状态全为新线索 | P0 | 有 new | 点新线索 | Tag 均为「新线索」 |
|
||||
| CRM-CL-215 | 新线索-不含跟进中 | P0 | 有 follow | 点新线索 | 无「跟进中」Tag |
|
||||
| CRM-CL-216 | 跟进中-状态全为跟进中 | P0 | 有 follow | 点跟进中 | Tag 均为「跟进中」 |
|
||||
| CRM-CL-217 | 跟进中-不含已转化 | P0 | 有 converted | 点跟进中 | 无「已转化」 |
|
||||
| CRM-CL-218 | 已转化-状态全为已转化 | P0 | 有 converted | 点已转化 | Tag 均为「已转化」 |
|
||||
| CRM-CL-219 | 已转化-不含新线索 | P0 | 有 new | 点已转化 | 无「新线索」 |
|
||||
| CRM-CL-220 | 五 Tab 依次切换无报错 | P0 | — | 全部→我的→新→跟进→转化→全部 | 每次列表刷新正常 |
|
||||
| CRM-CL-221 | 快速连点同一 Tab | P2 | — | 连点「全部」5 次 | 不重复异常请求导致错乱 |
|
||||
| CRM-CL-222 | 各 Tab 空态文案 | P1 | 对应空 | 切入 | 均为「暂无线索」 |
|
||||
|
||||
## 1.10 Tab × 搜索 组合矩阵(每格一条)
|
||||
|
||||
预置:准备可命中「组合关键字」的数据(姓名或公司含「联调」)。
|
||||
|
||||
| ID | Tab | 搜索词 | 预期 |
|
||||
|----|-----|--------|------|
|
||||
| CRM-CL-230 | 全部 | 联调 | 命中且状态不限 |
|
||||
| CRM-CL-231 | 我的 | 联调 | 命中且负责人为我 |
|
||||
| CRM-CL-232 | 新线索 | 联调 | 命中且 Tag=新线索;无则空 |
|
||||
| CRM-CL-233 | 跟进中 | 联调 | 命中且 Tag=跟进中;无则空 |
|
||||
| CRM-CL-234 | 已转化 | 联调 | 命中且 Tag=已转化;无则空 |
|
||||
| CRM-CL-235 | 全部 | 无意义串 | 空态 |
|
||||
| CRM-CL-236 | 我的 | 无意义串 | 空态 |
|
||||
| CRM-CL-237 | 新线索 | 无意义串 | 空态 |
|
||||
| CRM-CL-238 | 跟进中 | 无意义串 | 空态 |
|
||||
| CRM-CL-239 | 已转化 | 无意义串 | 空态 |
|
||||
| CRM-CL-240 | 先搜索再切 Tab | 有词时切到跟进中 | 结果=跟进中∩关键词(或清空关键词,记录实际并固化预期) |
|
||||
| CRM-CL-241 | 先切 Tab 再搜索 | 跟进中后搜联调 | 结果符合交集 |
|
||||
| CRM-CL-242 | 搜索后切回全部 | — | 按实际:保留词或清空 |
|
||||
|
||||
## 1.11 列表卡片 — 逐字段 + 状态枚举
|
||||
|
||||
对**同一条**完整线索卡片执行:
|
||||
|
||||
| ID | 核对项 | 预期 |
|
||||
|----|--------|------|
|
||||
| CRM-CL-300 | 主标题=线索姓名 | 与详情一致 |
|
||||
| CRM-CL-301 | 副标题=公司 | 与详情一致 |
|
||||
| CRM-CL-302 | 状态 Tag 文案合法 | 新线索/跟进中/无效/已转化 |
|
||||
| CRM-CL-303 | 行标签「手机」存在 | 有「手机」文案 |
|
||||
| CRM-CL-304 | 手机值正确 | 与详情一致或「—」 |
|
||||
| CRM-CL-305 | 行标签「负责人」存在 | 有 |
|
||||
| CRM-CL-306 | 负责人值正确 | 与详情一致或「—」 |
|
||||
| CRM-CL-307 | 行标签「来源」存在 | 有 |
|
||||
| CRM-CL-308 | 来源值正确 | 与详情一致或「—」 |
|
||||
|
||||
状态 Tag 抽样(各造一条):
|
||||
|
||||
| ID | 状态数据 | 预期 Tag |
|
||||
|----|----------|----------|
|
||||
| CRM-CL-310 | new | 新线索 |
|
||||
| CRM-CL-311 | follow | 跟进中 |
|
||||
| CRM-CL-312 | invalid | 无效 |
|
||||
| CRM-CL-313 | converted | 已转化 |
|
||||
| CRM-CL-314 | 未知 status | 原样显示或兜底(记录) |
|
||||
|
||||
交互:
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CL-320 | 点击卡片进详情 | 点卡片中部 | URL 含 id;详情姓名一致 |
|
||||
| CRM-CL-321 | 点击卡片边缘 | 点边角 | 仍能进详情 |
|
||||
| CRM-CL-322 | 列表多条依次点开 | 打开前 3 条详情再返回 | id/内容不错乱 |
|
||||
|
||||
---
|
||||
|
||||
# 二、线索 · 新增表单 `crm/clue/form`
|
||||
|
||||
## 2.1 进入与壳
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CF-001 | 列表点新增进入 | 点「新增」 | 标题「新增线索」 |
|
||||
| CRM-CF-002 | 有返回 | 查看 | 「← 返回」 |
|
||||
| CRM-CF-003 | 有取消 | 查看 | 「取消」 |
|
||||
| CRM-CF-004 | 有保存 | 查看 | 「保存」 |
|
||||
| CRM-CF-005 | 返回不保存 | 填一半点返回 | 回列表;无新线索 |
|
||||
| CRM-CF-006 | 取消不保存 | 填一半点取消 | 回列表;无新线索 |
|
||||
| CRM-CF-007 | 确认无「修改」模式入口 | 带 id 尝试编辑 URL | 无编辑页或不可用(记录) |
|
||||
|
||||
## 2.2 字段展示(每个字段一条)
|
||||
|
||||
| ID | 字段 | 检查项 | 预期 |
|
||||
|----|------|--------|------|
|
||||
| CRM-CF-010 | 线索姓名 | label | 「线索姓名」且必填标识 |
|
||||
| CRM-CF-011 | 线索姓名 | placeholder | 「联系人姓名」 |
|
||||
| CRM-CF-012 | 手机号 | label | 「手机号」必填 |
|
||||
| CRM-CF-013 | 手机号 | placeholder | 「手机号」 |
|
||||
| CRM-CF-014 | 手机号 | 控件类型 | number 数字键盘(H5) |
|
||||
| CRM-CF-015 | 公司名称 | label | 「公司名称」必填 |
|
||||
| CRM-CF-016 | 公司名称 | placeholder | 「公司 / 单位」 |
|
||||
| CRM-CF-017 | 职位 | label+placeholder | 「职位」 |
|
||||
| CRM-CF-018 | 来源 | label+placeholder | 「线索来源」 |
|
||||
| CRM-CF-019 | 来源 | 默认值 | 「其它」 |
|
||||
| CRM-CF-020 | 行业 | label+placeholder | 「所属行业」 |
|
||||
| CRM-CF-021 | 园区 | label+placeholder | 「所在园区」 |
|
||||
| CRM-CF-022 | 备注 | label+placeholder | 「备注」多行 |
|
||||
|
||||
## 2.3 必填校验(每个必填独立 + 组合)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 Toast |
|
||||
|----|------|------|------------|
|
||||
| CRM-CF-030 | 三空保存 | 全空保存 | 「请填写线索姓名」 |
|
||||
| CRM-CF-031 | 仅姓名空 | 手机公司有、姓名空 | 「请填写线索姓名」 |
|
||||
| CRM-CF-032 | 仅手机空 | 姓名公司有、手机空 | 「请填写手机号」 |
|
||||
| CRM-CF-033 | 仅公司空 | 姓名手机有、公司空 | 「请填写公司名称」 |
|
||||
| CRM-CF-034 | 姓名仅空格 | 姓名=空格 | 「请填写线索姓名」或按 trim 规则 |
|
||||
| CRM-CF-035 | 公司仅空格 | 公司=空格 | 「请填写公司名称」或按规则 |
|
||||
| CRM-CF-036 | 手机仅空格 | 手机空格 | 「请填写手机号」 |
|
||||
| CRM-CF-037 | 先姓名空再补全 | 触发姓名错→补姓名再存 | 进入下一校验或成功 |
|
||||
|
||||
## 2.4 字段边界与内容
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CF-050 | 姓名 1 字 | 填「甲」+必填 | 可成功(若后端允许) |
|
||||
| CRM-CF-051 | 姓名很长 | 50+ 字姓名 | 成功或后端长度错 Toast |
|
||||
| CRM-CF-052 | 姓名含符号 | 「张·三」 | 成功或明确失败 |
|
||||
| CRM-CF-053 | 手机不足 11 位 | 138001 | 前端是否拦;不拦则后端错(记缺陷若无提示) |
|
||||
| CRM-CF-054 | 手机超过 11 位 | 138001380001 | 记录截断或失败 |
|
||||
| CRM-CF-055 | 手机非号段 | 12345678901 | 成功或失败有提示 |
|
||||
| CRM-CF-056 | 公司很长 | 100+ 字 | 成功或长度错 |
|
||||
| CRM-CF-057 | 职位很长 | 长文本 | 成功或截断 |
|
||||
| CRM-CF-058 | 备注多行 | 含换行长文 | 详情备注完整或合理截断 |
|
||||
| CRM-CF-059 | 来源清空再保存 | 清空默认「其它」 | 按实现:失败或提交「-」 |
|
||||
| CRM-CF-060 | 来源改为自定义 | 填「展会」 | 详情来源=展会 |
|
||||
| CRM-CF-061 | 行业填写 | 填「化工」 | 详情行业=化工 |
|
||||
| CRM-CF-062 | 园区填写 | 填「杭州园区」 | 详情园区一致 |
|
||||
| CRM-CF-063 | 全部选填填写 | 职位来源行业园区备注都填 | 详情全部一致 |
|
||||
| CRM-CF-064 | 全部选填留空 | 仅三必填 | 详情选填「—」或默认 |
|
||||
|
||||
## 2.5 保存成功 / 失败 / 状态
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CF-080 | 最小必填成功 | 姓名手机公司合法→保存 | Toast「创建成功」;约 0.5s 回列表 |
|
||||
| CRM-CF-081 | 成功后列表可见 | 回列表搜姓名 | 新线索出现 |
|
||||
| CRM-CF-082 | 成功后详情字段 | 点进详情 | 各字段与提交一致 |
|
||||
| CRM-CF-083 | 保存中按钮文案 | 点保存瞬间 | 「提交中…」 |
|
||||
| CRM-CF-084 | 保存中防重复点 | 连点保存 | 不创建重复多条(或有防抖) |
|
||||
| CRM-CF-085 | 网络失败 | 断网保存 | Toast 失败类;停留表单;数据仍在输入框 |
|
||||
| CRM-CF-086 | 服务端 4xx/5xx | mock | Toast message;不假装成功 |
|
||||
| CRM-CF-087 | 成功后默认状态 | 打开新线索 | 一般为「新线索」 |
|
||||
|
||||
## 2.6 确认无修改 / 无删除
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-CF-090 | 表单无删除按钮 | 无 |
|
||||
| CRM-CF-091 | 保存成功后不能在本页改 | 回列表后无编辑入口 |
|
||||
|
||||
---
|
||||
|
||||
# 三、线索 · 详情 `crm/clue/detail`
|
||||
|
||||
## 3.1 打开与返回
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-001 | 列表进入详情 | 点卡片 | 标题「线索详情」 |
|
||||
| CRM-CD-002 | 返回文案 | 查看 | 「← 返回」 |
|
||||
| CRM-CD-003 | 返回列表 | 点返回 | 回线索列表 |
|
||||
| CRM-CD-004 | 非法 id | 打开不存在 id | Toast 加载失败类;可返回 |
|
||||
| CRM-CD-005 | 无 id | 打开无参详情 | 失败提示或空 |
|
||||
|
||||
## 3.2 Hero 字段(逐项)
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-CD-010 | 主标题姓名 | =线索姓名 |
|
||||
| CRM-CD-011 | 副标题公司 | =公司 |
|
||||
| CRM-CD-012 | 状态 Tag | 与列表一致 |
|
||||
| CRM-CD-013 | 级别 Chip | A级/B级/C级/D级 |
|
||||
| CRM-CD-014 | 负责人 Chip | 负责人姓名 |
|
||||
|
||||
## 3.3 基本信息(逐项)
|
||||
|
||||
| ID | label | 有数据预期 | 空数据预期 |
|
||||
|----|-------|------------|------------|
|
||||
| CRM-CD-020 | 手机 | 正确号码 | — |
|
||||
| CRM-CD-021 | 职位 | 正确 | — |
|
||||
| CRM-CD-022 | 来源 | 正确 | — |
|
||||
| CRM-CD-023 | 行业 | 正确 | — |
|
||||
| CRM-CD-024 | 园区 | 正确 | — |
|
||||
| CRM-CD-025 | 备注 | 正确(长文可展) | — |
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-026 | 基本信息区块标题 | 查看 | 有「基本信息」 |
|
||||
| CRM-CD-027 | 备注超长展示 | 很长备注 | 可完整看或展开;不撑破布局至不可用 |
|
||||
|
||||
## 3.4 跟进记录展示
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-040 | 区块标题 | 查看 | 「跟进记录」 |
|
||||
| CRM-CD-041 | 空态 | 无跟进 | 「暂无跟进」 |
|
||||
| CRM-CD-042 | 单条-跟进方式 | 有1条 | 标题=方式 |
|
||||
| CRM-CD-043 | 单条-时间 | 有1条 | 副标题=创建时间 |
|
||||
| CRM-CD-044 | 单条-内容 | 有1条 | 正文=内容 |
|
||||
| CRM-CD-045 | 单条-跟进人 | 有1条 | 行「跟进人」有值 |
|
||||
| CRM-CD-046 | 多条顺序 | ≥2 条 | 按时间倒序或正序(记录并固化) |
|
||||
| CRM-CD-047 | 新提交后列表刷新 | 提交跟进后 | 新记录出现在列表 |
|
||||
|
||||
## 3.5 跟进表单
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-060 | 底栏始终有「跟进」 | 各状态 | 「跟进」可见 |
|
||||
| CRM-CD-061 | 点跟进展开 | 点击 | 出现方式、内容、取消、提交跟进 |
|
||||
| CRM-CD-062 | 方式默认电话 | 展开 | 值为「电话」 |
|
||||
| CRM-CD-063 | 方式 placeholder | 查看 | 「如:电话 / 微信 / 拜访」 |
|
||||
| CRM-CD-064 | 内容必填标识 | 查看 | 跟进内容带必填 |
|
||||
| CRM-CD-065 | 内容 placeholder | 查看 | 「填写本次沟通要点」 |
|
||||
| CRM-CD-066 | 内容空提交 | 提交 | Toast「请填写跟进内容」 |
|
||||
| CRM-CD-067 | 内容仅空格 | 提交 | Toast 请填写… |
|
||||
| CRM-CD-068 | 取消收起 | 点取消 | 表单关;无新记录 |
|
||||
| CRM-CD-069 | 改方式为微信 | 改方式+内容提交 | 记录方式=微信 |
|
||||
| CRM-CD-070 | 改方式为拜访 | 同上 | 方式=拜访 |
|
||||
| CRM-CD-071 | 长内容提交 | 500+ 字 | 成功或长度错提示 |
|
||||
| CRM-CD-072 | 提交成功 Toast | 合法提交 | 「已保存」 |
|
||||
| CRM-CD-073 | 提交中文案 | 提交时 | 「提交中…」 |
|
||||
| CRM-CD-074 | 提交失败 | mock 失败 | 「保存失败」或 message |
|
||||
| CRM-CD-075 | 接口 422 字段问题 | 已知跟进字段 | 失败→记缺陷;用例保留 |
|
||||
| CRM-CD-076 | 连续跟进两次 | 提交两次 | 两条记录 |
|
||||
| CRM-CD-077 | 跟进中再次点跟进 | 已展开时 | 不重复叠两个表单 |
|
||||
|
||||
## 3.6 转客户 / 无效 — 显示矩阵
|
||||
|
||||
| ID | 状态 | 跟进 | 转客户 | 无效 |
|
||||
|----|------|------|--------|------|
|
||||
| CRM-CD-100 | 新线索 | 有 | 有 | 有 |
|
||||
| CRM-CD-101 | 跟进中 | 有 | 有 | 有 |
|
||||
| CRM-CD-102 | 已转化 | 有 | **无** | **无** |
|
||||
| CRM-CD-103 | 无效 | 有 | **无** | **无** |
|
||||
|
||||
## 3.7 转客户流程
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-110 | 弹窗标题 | 点转客户 | 「转化为客户」 |
|
||||
| CRM-CD-111 | 弹窗文案含姓名 | — | 「将「{姓名}」转为客户,是否继续?」 |
|
||||
| CRM-CD-112 | 取消 | 取消 | 状态不变 |
|
||||
| CRM-CD-113 | 确认成功 | 确认 | Toast「转化成功」;Tag=已转化 |
|
||||
| CRM-CD-114 | 成功后按钮隐藏 | 再看底栏 | 无转客户/无效 |
|
||||
| CRM-CD-115 | 转化失败 | mock | Toast「转化失败」或 message |
|
||||
| CRM-CD-116 | 转化后客户侧可查 | 去客户/相关 | 有对应客户(若业务生成) |
|
||||
|
||||
## 3.8 无效流程
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CD-120 | 点无效出输入 | 点击 | placeholder「无效原因(可选)」 |
|
||||
| CRM-CD-121 | 取消 | 取消 | 不变 |
|
||||
| CRM-CD-122 | 原因空确认 | 确认 | Toast「已标记」;状态无效 |
|
||||
| CRM-CD-123 | 原因有值确认 | 填原因确认 | 「已标记」;无效 |
|
||||
| CRM-CD-124 | 成功后按钮隐藏 | — | 无转客户/无效 |
|
||||
| CRM-CD-125 | 操作失败 | mock | 「操作失败」或 message |
|
||||
|
||||
## 3.9 详情负向
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-CD-130 | 无编辑按钮 | 无 |
|
||||
| CRM-CD-131 | 无删除按钮 | 无 |
|
||||
| CRM-CD-132 | 不能改姓名/手机 | 字段只读展示 |
|
||||
|
||||
---
|
||||
|
||||
# 四、商机管理
|
||||
|
||||
## 4.1 列表壳与负向
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-OL-001 | 入口标题 | →商机管理 | 「商机管理」 |
|
||||
| CRM-OL-002 | 返回工作台 | 点返回 | 回工作台 |
|
||||
| CRM-OL-003 | 无新增 | 看页头 | **无**新增 |
|
||||
| CRM-OL-004 | 无编辑删除 | 看卡片 | **无** |
|
||||
| CRM-OL-005 | 空态 | 无数据 | 「暂无商机」 |
|
||||
| CRM-OL-006 | 未登录拦截 | 清 Token | 跳登录 |
|
||||
| CRM-OL-007 | 加载失败 | mock | Toast 加载失败 |
|
||||
| CRM-OL-008 | 分页触底 | >20 | 加载更多/已全部 |
|
||||
|
||||
## 4.2 搜索(独立维度)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-OL-020 | placeholder | — | 「搜索商机 / 客户」 |
|
||||
| CRM-OL-021 | 按商机名全称 | — | 主标题命中 |
|
||||
| CRM-OL-022 | 按商机名部分 | — | 命中 |
|
||||
| CRM-OL-023 | 按客户名 | — | 副标题命中 |
|
||||
| CRM-OL-024 | 无结果 | 乱码 | 「暂无商机」 |
|
||||
| CRM-OL-025 | 空关键字 | — | 全量或保持 |
|
||||
| CRM-OL-026 | 空格/特殊字符 | — | 不崩溃 |
|
||||
| CRM-OL-027 | 清空恢复 | — | 恢复 |
|
||||
|
||||
## 4.3 Tab 逐个
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-OL-040 | Tab 文案 | 全部/跟进中/赢单/输单 |
|
||||
| CRM-OL-041 | 默认全部 | 选中全部 |
|
||||
| CRM-OL-042 | 全部混合状态 | 多种 Tag |
|
||||
| CRM-OL-043 | 跟进中纯净 | Tag=跟进中 |
|
||||
| CRM-OL-044 | 赢单纯净 | Tag=赢单 |
|
||||
| CRM-OL-045 | 输单纯净 | Tag=输单 |
|
||||
| CRM-OL-046 | 各 Tab 空态 | 「暂无商机」 |
|
||||
| CRM-OL-047 | Tab×搜索命中 | 交集正确 |
|
||||
| CRM-OL-048 | Tab×搜索无结果 | 空态 |
|
||||
|
||||
## 4.4 卡片字段
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-OL-060 | 商机名称 | 主标题 |
|
||||
| CRM-OL-061 | 客户名称 | 副标题 |
|
||||
| CRM-OL-062 | 状态 Tag | 跟进中/赢单/输单/无效 |
|
||||
| CRM-OL-063 | 阶段 | 初步接触/需求确认/方案报价/商务谈判/赢单/输单 等 |
|
||||
| CRM-OL-064 | 金额 | ¥ 格式 |
|
||||
| CRM-OL-065 | 负责人 | 有值或— |
|
||||
| CRM-OL-066 | 点进详情 | id 正确 |
|
||||
|
||||
## 4.5 详情字段与跟进
|
||||
|
||||
| ID | 项 | 预期 |
|
||||
|----|-----|------|
|
||||
| CRM-OD-001 | 标题 | 商机详情 |
|
||||
| CRM-OD-002 | Hero 商机名 | 正确 |
|
||||
| CRM-OD-003 | Hero 客户 | 正确 |
|
||||
| CRM-OD-004 | Hero 状态 | 正确 |
|
||||
| CRM-OD-005 | Hero 阶段 Chip | 正确 |
|
||||
| CRM-OD-006 | Hero 金额 Chip | ¥ |
|
||||
| CRM-OD-010 | 基本信息-商机编号 | 有或— |
|
||||
| CRM-OD-011 | 基本信息-负责人 | 有或— |
|
||||
| CRM-OD-012 | 基本信息-来源 | 有或— |
|
||||
| CRM-OD-013 | 基本信息-预计成交 | 有或— |
|
||||
| CRM-OD-014 | 基本信息-备注 | 有或— |
|
||||
| CRM-OD-020 | 跟进空态 | 暂无跟进 |
|
||||
| CRM-OD-021 | 跟进有数据字段 | 方式/时间/内容(无跟进人行) |
|
||||
| CRM-OD-030 | 写跟进展开 | 方式默认电话 |
|
||||
| CRM-OD-031 | 内容空校验 | 请填写跟进内容 |
|
||||
| CRM-OD-032 | 取消 | 收起 |
|
||||
| CRM-OD-033 | 提交成功 | 已保存 |
|
||||
| CRM-OD-034 | 提交失败 | 保存失败 |
|
||||
| CRM-OD-040 | 无编辑删除改状态 | **无** |
|
||||
| CRM-OD-041 | 返回 | 回列表 |
|
||||
|
||||
---
|
||||
|
||||
# 五、客户管理
|
||||
|
||||
## 5.1 列表
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-CUL-001 | 入口 | →客户管理 | 标题正确 |
|
||||
| CRM-CUL-002 | 无新增编辑删除 | — | **均无** |
|
||||
| CRM-CUL-003 | 空态 | — | 暂无客户 |
|
||||
| CRM-CUL-010 | 搜索 placeholder | — | 搜索客户名称 |
|
||||
| CRM-CUL-011 | 全称命中 | — | 主标题命中 |
|
||||
| CRM-CUL-012 | 部分命中 | — | 命中 |
|
||||
| CRM-CUL-013 | 无结果 | — | 空态 |
|
||||
| CRM-CUL-014 | 边界空格特殊字符 | — | 不崩溃 |
|
||||
| CRM-CUL-020 | Tab「我的」存在 | — | 有 |
|
||||
| CRM-CUL-021 | Tab「全部」存在 | — | 有 |
|
||||
| CRM-CUL-022 | **默认「我的」** | 刚进入 | 选中我的 |
|
||||
| CRM-CUL-023 | 我的范围 | — | 负责人=我 |
|
||||
| CRM-CUL-024 | 全部范围 | — | 可含他人 |
|
||||
| CRM-CUL-025 | Tab×搜索 | — | 交集 |
|
||||
| CRM-CUL-030 | 卡片-客户名 | — | 主标题 |
|
||||
| CRM-CUL-031 | 卡片-编号 | — | 副标题 |
|
||||
| CRM-CUL-032 | 卡片-状态 Tag | 潜在/意向/洽谈/已成交/流失/公海 | 合法 |
|
||||
| CRM-CUL-033 | 卡片-级别 | A/B/C/D级 | 合法 |
|
||||
| CRM-CUL-034 | 卡片-联系人/手机 | — | 有联系人示联系人否则手机 |
|
||||
| CRM-CUL-035 | 卡片-负责人 | — | 正确 |
|
||||
| CRM-CUL-036 | 进详情 | — | 详情一致 |
|
||||
| CRM-CUL-037 | 分页 | — | 正常 |
|
||||
|
||||
## 5.2 详情
|
||||
|
||||
| ID | 项 | 预期 |
|
||||
|----|-----|------|
|
||||
| CRM-CUD-001 | 标题 | 客户详情 |
|
||||
| CRM-CUD-002~006 | Hero 名/编号/状态/级别/负责人 | 正确 |
|
||||
| CRM-CUD-010 | 类型 | 有或— |
|
||||
| CRM-CUD-011 | 来源 | 有或— |
|
||||
| CRM-CUD-012 | 行业 | 有或— |
|
||||
| CRM-CUD-013 | 手机 | 有或— |
|
||||
| CRM-CUD-014 | 地址 | 有或— |
|
||||
| CRM-CUD-015 | 备注 | 有或— |
|
||||
| CRM-CUD-020 | 跟进空态 | 暂无跟进 |
|
||||
| CRM-CUD-021 | 跟进字段 | 方式/时间/内容 |
|
||||
| CRM-CUD-030 | 写跟进校验 | 空内容→请填写跟进内容 |
|
||||
| CRM-CUD-031 | 写跟进成功 | 已保存 |
|
||||
| CRM-CUD-032 | 写跟进取消 | 收起 |
|
||||
| CRM-CUD-040 | 无编辑删除 | **无** |
|
||||
| CRM-CUD-041 | 返回 | 回列表 |
|
||||
|
||||
---
|
||||
|
||||
# 六、公海池
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-PL-001 | 入口标题 | →公海池 | 公海池 |
|
||||
| CRM-PL-002 | 无 Tab | 查看 | **无**状态 Tab |
|
||||
| CRM-PL-003 | 无新增编辑删除 | — | 仅领取 |
|
||||
| CRM-PL-010 | 搜索 placeholder | — | 搜索客户名称 |
|
||||
| CRM-PL-011 | 名称命中 | — | 命中 |
|
||||
| CRM-PL-012 | 部分命中 | — | 命中 |
|
||||
| CRM-PL-013 | 无结果 | — | 公海暂无客户 |
|
||||
| CRM-PL-014 | 边界字符 | — | 不崩溃 |
|
||||
| CRM-PL-020 | 主标题客户名 | — | 正确 |
|
||||
| CRM-PL-021 | 副标题入池时间 | — | 「入池:」+时间 |
|
||||
| CRM-PL-022 | 级别 Tag | — | A/B/C/D |
|
||||
| CRM-PL-023 | 来源行 | — | 有或— |
|
||||
| CRM-PL-024 | 原负责人行 | — | 有或— |
|
||||
| CRM-PL-025 | 领取按钮文案 | — | 「领取」 |
|
||||
| CRM-PL-030 | 领取弹窗标题 | 点领取 | 「领取客户」 |
|
||||
| CRM-PL-031 | 弹窗含客户名 | — | 确认领取「姓名」? |
|
||||
| CRM-PL-032 | 领取取消 | 取消 | 仍在公海 |
|
||||
| CRM-PL-033 | 领取成功 | 确认 | 「领取成功」;列表更新 |
|
||||
| CRM-PL-034 | 领取中文案 | 请求中 | 「领取中…」 |
|
||||
| CRM-PL-035 | 领取失败 | mock | 领取失败或 message |
|
||||
| CRM-PL-036 | 领取后我的客户可见 | 去客户-我的 | 可见该客户 |
|
||||
| CRM-PL-040 | 点头进详情-成功 | 有权限 | 进客户详情 |
|
||||
| CRM-PL-041 | 点头进详情-失败 | 无权限 | 「暂无法查看详情」 |
|
||||
| CRM-PL-042 | 分页/加载 | — | 正常 |
|
||||
|
||||
---
|
||||
|
||||
# 七、报价管理
|
||||
|
||||
## 7.1 列表
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-QL-001 | 入口/返回 | 正常 |
|
||||
| CRM-QL-002 | 无新增编辑删除 | **无** |
|
||||
| CRM-QL-003 | 空态 | 暂无报价 |
|
||||
| CRM-QL-010 | 搜索 placeholder | 搜索报价单号 / 客户 |
|
||||
| CRM-QL-011 | 按单号全称 | 命中 |
|
||||
| CRM-QL-012 | 按单号部分 | 命中 |
|
||||
| CRM-QL-013 | 按客户 | 命中 |
|
||||
| CRM-QL-014 | 无结果 | 空态 |
|
||||
| CRM-QL-015 | 边界字符 | 不崩溃 |
|
||||
| CRM-QL-020 | Tab 全部 | 有 |
|
||||
| CRM-QL-021 | Tab 草稿 | 状态=草稿 |
|
||||
| CRM-QL-022 | Tab 待审批 | 状态=待审批 |
|
||||
| CRM-QL-023 | Tab 已通过 | 状态=已通过 |
|
||||
| CRM-QL-024 | 默认全部 | 是 |
|
||||
| CRM-QL-025 | 各 Tab 空态 | 暂无报价 |
|
||||
| CRM-QL-026 | Tab×搜索 | 交集 |
|
||||
| CRM-QL-030 | 主标题 | 标题或单号 |
|
||||
| CRM-QL-031 | 副标题 | 单号·客户 |
|
||||
| CRM-QL-032 | 状态 Tag 枚举 | 草稿/待审批/已通过/已驳回/已作废/已转合同 |
|
||||
| CRM-QL-033 | 金额 | ¥ |
|
||||
| CRM-QL-034 | 负责人 | 有或— |
|
||||
| CRM-QL-035 | 报价日期 | 有或创建时间 |
|
||||
| CRM-QL-036 | 进详情 | 正确 |
|
||||
| CRM-QL-037 | 分页 | 正常 |
|
||||
|
||||
## 7.2 详情只读
|
||||
|
||||
| ID | 项 | 预期 |
|
||||
|----|-----|------|
|
||||
| CRM-QD-001 | 标题 | 报价详情 |
|
||||
| CRM-QD-002~006 | Hero 字段 | 正确 |
|
||||
| CRM-QD-010 | 报价单号 | 有或— |
|
||||
| CRM-QD-011 | 报价日期 | 有或— |
|
||||
| CRM-QD-012 | 有效期至 | 有或— |
|
||||
| CRM-QD-013 | 关联商机 | 有或— |
|
||||
| CRM-QD-014 | 备注 | 有或— |
|
||||
| CRM-QD-020 | 说明文案 | 含 Web 管理端审批改价转合同 |
|
||||
| CRM-QD-021 | 无任何写操作底栏 | **无** |
|
||||
| CRM-QD-022 | 仅返回 | 回列表 |
|
||||
|
||||
---
|
||||
|
||||
# 八、合同管理
|
||||
|
||||
## 8.1 列表
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| CRM-CTL-001 | 入口/返回 | 正常 |
|
||||
| CRM-CTL-002 | 无新增编辑删除 | **无** |
|
||||
| CRM-CTL-003 | 空态 | 暂无合同 |
|
||||
| CRM-CTL-010 | 搜索 placeholder | 搜索合同 / 客户 |
|
||||
| CRM-CTL-011 | 按合同名 | 命中 |
|
||||
| CRM-CTL-012 | 按客户名 | 命中 |
|
||||
| CRM-CTL-013 | 无结果 | 空态 |
|
||||
| CRM-CTL-014 | 边界 | 不崩溃 |
|
||||
| CRM-CTL-020 | Tab 全部 | 有 |
|
||||
| CRM-CTL-021 | Tab 草稿 | 纯净 |
|
||||
| CRM-CTL-022 | Tab 待审批 | 纯净 |
|
||||
| CRM-CTL-023 | Tab 已生效 | 纯净 |
|
||||
| CRM-CTL-024 | Tab 执行中 | 纯净 |
|
||||
| CRM-CTL-025 | 默认全部 | 是 |
|
||||
| CRM-CTL-026 | Tab×搜索 | 交集 |
|
||||
| CRM-CTL-030 | 主标题合同名 | 正确 |
|
||||
| CRM-CTL-031 | 副标题编号·客户 | 正确 |
|
||||
| CRM-CTL-032 | 状态枚举 | 草稿/待审批/已生效/已驳回/执行中/已完成/已终止/已过期 |
|
||||
| CRM-CTL-033 | 金额 | ¥ |
|
||||
| CRM-CTL-034 | 负责人 | 有或— |
|
||||
| CRM-CTL-035 | 签订日期 | 有或— |
|
||||
| CRM-CTL-036 | 进详情 | 正确 |
|
||||
| CRM-CTL-037 | 分页 | 正常 |
|
||||
|
||||
## 8.2 详情只读
|
||||
|
||||
| ID | 项 | 预期 |
|
||||
|----|-----|------|
|
||||
| CRM-CTD-001 | 标题 | 合同详情 |
|
||||
| CRM-CTD-002~006 | Hero | 正确 |
|
||||
| CRM-CTD-010 | 合同编号 | 有或— |
|
||||
| CRM-CTD-011 | 合同类型 | 有或— |
|
||||
| CRM-CTD-012 | 签订日期 | 有或— |
|
||||
| CRM-CTD-013 | 开始日期 | 有或— |
|
||||
| CRM-CTD-014 | 结束日期 | 有或— |
|
||||
| CRM-CTD-015 | 审批状态 | 有或— |
|
||||
| CRM-CTD-016 | 备注 | 有或— |
|
||||
| CRM-CTD-020 | 说明文案 | 含 Web 审批签署 |
|
||||
| CRM-CTD-021 | 无写操作 | **无** |
|
||||
| CRM-CTD-022 | 返回 | 回列表 |
|
||||
|
||||
---
|
||||
|
||||
# 九、CRM 灰卡与跨页回归
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| CRM-WB-001 | 产品分类灰卡 | 点击 | 本期暂未开放 |
|
||||
| CRM-WB-002 | 产品管理灰卡 | 点击 | 同上 |
|
||||
| CRM-WB-003 | 合同审批灰卡 | 点击 | 同上 |
|
||||
| CRM-RG-001 | 新建线索后各 Tab 可见性 | 新建后查全部/新线索 | 能找到 |
|
||||
| CRM-RG-002 | 跟进后详情记录增加 | — | +1 |
|
||||
| CRM-RG-003 | 转客户后列表已转化 Tab | — | 可见 |
|
||||
| CRM-RG-004 | 无效后全部可见无效 Tag | — | 可见 |
|
||||
| CRM-RG-005 | 公海领取后公海消失 | — | 公海无;客户有 |
|
||||
| CRM-RG-006 | 模块间往返 | 线索↔商机↔客户↔报价↔合同 | 无串数据 |
|
||||
|
||||
---
|
||||
|
||||
# 执行记录
|
||||
|
||||
| 批次 | 日期 | 通过 | 失败 | 阻塞 | 备注 |
|
||||
|------|------|------|------|------|------|
|
||||
| — | — | — | — | — | 文字用例阶段 |
|
||||
|
||||
## 用例统计(本文件)
|
||||
|
||||
按 ID 前缀估算:**约 420+ 条**(含矩阵展开)。若评审仍嫌少,可再按「每状态 × 每字段空/满」继续拆分第二册。
|
||||
@@ -0,0 +1,397 @@
|
||||
# 报销管理 — 全面测试用例
|
||||
|
||||
> **增**:报销/借款新建(存草稿、提交审批)
|
||||
> **改**:无编辑页(负向确认)
|
||||
> **删**:无删除(负向确认)
|
||||
> **其它写**:审批通过/驳回、详情再提交
|
||||
|
||||
---
|
||||
|
||||
## A. 待办任务 `bx/todo/list`
|
||||
|
||||
### A1 壳与入口
|
||||
|
||||
| ID | 标题 | 优先级 | 步骤 | 预期 |
|
||||
|----|------|--------|------|------|
|
||||
| BX-TD-A01 | 标题与返回 | P0 | 工作台→待办任务 | 「待办任务」;「← 工作台」 |
|
||||
| BX-TD-A02 | 「+ 申请」入口 | P0 | 点「+ 申请」 | ActionSheet:「报销申请」「借款申请」 |
|
||||
| BX-TD-A03 | Sheet→报销新建 | P0 | 选报销申请 | 进报销表单 |
|
||||
| BX-TD-A04 | Sheet→借款新建 | P0 | 选借款申请 | 进借款表单 |
|
||||
| BX-TD-A05 | 无编辑/删除 | P0 | 看列表 | **无**编辑、删除按钮 |
|
||||
|
||||
### A2 Tab(逐个)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-TD-T01 | Tab 文案 | 查看 | 待审批 / 我的申请 / 已办 |
|
||||
| BX-TD-T02 | Tab 待审批 | 点击 | 仅待审类单据;卡片底有「驳回」「通过」 |
|
||||
| BX-TD-T03 | Tab 我的申请 | 点击 | 本人申请单;底栏无通过驳回 |
|
||||
| BX-TD-T04 | Tab 已办 | 点击 | 已审批过的单据 |
|
||||
| BX-TD-T05 | 各 Tab 空态 | 无数据时 | 「暂无单据」 |
|
||||
| BX-TD-T06 | 我的申请加载更多 | 数据>10 | 触底加载或「已加载全部」 |
|
||||
|
||||
### A3 卡片字段(逐字段)
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| BX-TD-F01 | 主标题 | 「报销单 {applyCode}」或「借款单 {applyCode}」 |
|
||||
| BX-TD-F02 | 副标题 | 报销 expenseName/title;借款 reason/purpose |
|
||||
| BX-TD-F03 | 状态 Tag | 待提交/审批中/已通过/已驳回/已发放/已付款/已处理/已冲抵 等 |
|
||||
| BX-TD-F04 | 申请人 | applicantName |
|
||||
| BX-TD-F05 | 申请时间 | applyTime |
|
||||
| BX-TD-F06 | 金额 | ¥ 格式 |
|
||||
| BX-TD-F07 | 点卡片进详情 | 按类型进报销或借款详情 |
|
||||
|
||||
### A4 审批操作(待审批 Tab)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-TD-O01 | 通过-取消 | 点通过→取消 | 状态不变 |
|
||||
| BX-TD-O02 | 通过-确认 | 确认「确认通过」 | Toast「通过成功」;列表更新 |
|
||||
| BX-TD-O03 | 驳回-取消 | 点驳回→取消 | 不变 |
|
||||
| BX-TD-O04 | 驳回-可填意见确认 | 填意见确认 | Toast「驳回成功」;placeholder「驳回意见(可选)」 |
|
||||
| BX-TD-O05 | 驳回-意见空确认 | 意见空确认 | 仍可成功(默认「驳回」) |
|
||||
|
||||
---
|
||||
|
||||
## B. 报销申请 · 列表 `bx/reimburse/list`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RL-A01 | 入口 | →报销申请 | 标题「报销申请」;「+ 申请」;「← 工作台」 |
|
||||
| BX-RL-A02 | 无检索/Tab | 查看 | 本页**无**搜索框、**无**状态 Tab(若后续加需补用例) |
|
||||
| BX-RL-A03 | 无编辑删除 | — | **无** |
|
||||
| BX-RL-A04 | 空态 | 无数据 | 「暂无报销单」 |
|
||||
| BX-RL-F01 | 主标题 applyCode | 有数据 | 单号 |
|
||||
| BX-RL-F02 | 副标题 | — | expenseName/title |
|
||||
| BX-RL-F03 | 状态 Tag | — | 枚举正确 |
|
||||
| BX-RL-F04 | 申请时间 | — | applyTime |
|
||||
| BX-RL-F05 | 金额 | — | ¥ |
|
||||
| BX-RL-F06 | 点卡片 | — | 进详情 |
|
||||
| BX-RL-N01 | +申请进新建 | 点击 | 进表单 |
|
||||
|
||||
---
|
||||
|
||||
## C. 报销 · 新建表单 `bx/reimburse/form`(新增全覆盖)
|
||||
|
||||
### C1 字段展示
|
||||
|
||||
| ID | 字段 label | 控件 | 必填 | placeholder/默认 |
|
||||
|----|------------|------|------|------------------|
|
||||
| BX-RF-D01 | 标题 | 输入 | 否 | 进页按日期自动生成,可改 |
|
||||
| BX-RF-D02 | 报销日期 | 日期选择 | 否 | 默认当天 |
|
||||
| BX-RF-D03 | 费用项目 | ActionSheet | **是** | 「点击选择」 |
|
||||
| BX-RF-D04 | 金额(元) | digit | **是** | 0.00 |
|
||||
| BX-RF-D05 | 明细说明 | 多行 | **是** | 「费用事由」 |
|
||||
| BX-RF-D06 | 备注 | 多行 | 否 | 「可选」 |
|
||||
| BX-RF-D07 | 票据照片 | 上传 | 否 | 「+ 拍照」 |
|
||||
| BX-RF-D08 | 底栏按钮 | — | — | 「存草稿」「提交审批」 |
|
||||
|
||||
### C2 校验(逐项)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RF-V01 | 未选费用项目-提交 | 其它合法不选费用→提交审批 | Toast「请选择费用项目」 |
|
||||
| BX-RF-V02 | 未选费用项目-草稿 | 同上点存草稿 | 同上校验 |
|
||||
| BX-RF-V03 | 金额空 | 金额空或0→提交 | Toast「请填写金额」 |
|
||||
| BX-RF-V04 | 金额≤0 | 填 0 →提交 | Toast「请填写金额」 |
|
||||
| BX-RF-V05 | 明细说明空 | trim 后空→提交 | Toast「请填写明细说明」 |
|
||||
| BX-RF-V06 | 无费用项配置 | Web 未配置时点选 | Toast「暂无费用项目,请先在 Web 配置」 |
|
||||
|
||||
### C3 控件与上传
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RF-C01 | 选费用项目回显 | ActionSheet 选一项 | 表单显示全路径 label |
|
||||
| BX-RF-C02 | 改报销日期 | 选其它日期 | 日期回显;标题可随日期更新 |
|
||||
| BX-RF-C03 | 改标题 | 手工改标题 | 可保存该标题 |
|
||||
| BX-RF-C04 | 拍照上传1张 | +拍照 | 出现缩略图 |
|
||||
| BX-RF-C05 | 删除已传图 | 点 × | 图移除 |
|
||||
| BX-RF-C06 | 上传满6张 | 连续至6 | 第7次 Toast「最多上传6张」 |
|
||||
| BX-RF-C07 | 上传中 | 上传时 | Loading「上传中...」 |
|
||||
| BX-RF-C08 | 备注可空 | 备注空提交 | 不因备注失败 |
|
||||
|
||||
### C4 提交结果
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RF-S01 | 存草稿成功 | 必填合法→存草稿 | Toast「已保存草稿」;回列表可见 draft |
|
||||
| BX-RF-S02 | 提交审批成功 | 必填合法→提交 | Toast「已提交审批」 |
|
||||
| BX-RF-S03 | 提交中态 | — | 按钮「…」 |
|
||||
| BX-RF-S04 | 接口失败 | 已知 details 字段问题等 | Toast 失败;**记缺陷**;用例不删 |
|
||||
| BX-RF-N01 | 确认无修改入口 | 列表/详情 | **无**「编辑报销」页 |
|
||||
|
||||
---
|
||||
|
||||
## D. 报销 · 详情 `bx/reimburse/detail`
|
||||
|
||||
### D1 字段逐项
|
||||
|
||||
| ID | 位置 | 字段 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RD-H01 | Hero | applyCode | 主标题 |
|
||||
| BX-RD-H02 | Hero | title | 副标题 |
|
||||
| BX-RD-H03 | Hero | status Tag | 正确 |
|
||||
| BX-RD-H04 | Hero | amount | ¥ chip |
|
||||
| BX-RD-H05 | Hero | applicantName | chip |
|
||||
| BX-RD-B01 | 基本信息 | 费用项目 | expenseName |
|
||||
| BX-RD-B02 | 基本信息 | 报销日期 | reimburseDate |
|
||||
| BX-RD-B03 | 基本信息 | 申请时间 | applyTime |
|
||||
| BX-RD-B04 | 基本信息 | 当前节点 | currentNode 或「—」 |
|
||||
| BX-RD-B05 | 基本信息 | 备注 | 或「—」 |
|
||||
| BX-RD-M01 | 明细 | expenseName | 有 |
|
||||
| BX-RD-M02 | 明细 | amount | ¥ |
|
||||
| BX-RD-M03 | 明细 | remark说明 | 有则显示 |
|
||||
| BX-RD-I01 | 票据 | 有图可预览 | 点图 preview |
|
||||
| BX-RD-I02 | 票据 | 无图 | 区块不强制报错 |
|
||||
| BX-RD-H06 | 审批记录 | 每条 | 审批人/节点 · 状态 · 意见 |
|
||||
|
||||
### D2 按状态操作
|
||||
|
||||
| ID | 预置状态 | 步骤 | 预期 |
|
||||
|----|----------|------|------|
|
||||
| BX-RD-O01 | draft | 见底栏「提交审批」;确认 | Toast「已提交」;弹窗「确认提交该报销单?」 |
|
||||
| BX-RD-O02 | draft | 提交取消 | 仍 draft |
|
||||
| BX-RD-O03 | pending | 「通过」确认(意见可选) | 「操作成功」 |
|
||||
| BX-RD-O04 | pending | 「驳回」填意见确认 | 「操作成功」 |
|
||||
| BX-RD-O05 | 非 draft/pending | 看底栏 | 无提交/通过/驳回 |
|
||||
| BX-RD-N01 | 任意 | — | **无编辑、无删除** |
|
||||
|
||||
---
|
||||
|
||||
## E. 报销审批列表 `bx/approve/list?type=reimburse`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RA-A01 | 标题 | →报销审批 | 「报销审批」 |
|
||||
| BX-RA-A02 | 空态 | 无数据 | 「暂无待审批」 |
|
||||
| BX-RA-A03 | 无搜索(当前) | 查看 | 无搜索则记录;有则补测 |
|
||||
| BX-RA-F01~F05 | 卡片字段 | 有数据 | 单号、副标题、状态、申请人、金额、驳回/通过 |
|
||||
| BX-RA-O01 | 列表通过 | 确认 | 「操作成功」 |
|
||||
| BX-RA-O02 | 列表驳回 | 意见框「驳回意见」确认 | 「操作成功」 |
|
||||
| BX-RA-F06 | 点卡片 | — | 进详情 |
|
||||
| BX-RA-N01 | 无编辑删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## F. 借款申请 · 列表 `bx/advance/list`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-AL-A01 | 入口 | →借款申请 | 「借款申请」;「+ 申请」 |
|
||||
| BX-AL-A02 | 空态 | — | 「暂无借款单」 |
|
||||
| BX-AL-A03 | 无编辑删除 | — | **无** |
|
||||
| BX-AL-F01 | applyCode | — | 主标题 |
|
||||
| BX-AL-F02 | reason | — | 副标题 |
|
||||
| BX-AL-F03 | status/时间/金额 | — | 正确 |
|
||||
| BX-AL-F04 | 进详情/+申请 | — | 详情或表单 |
|
||||
|
||||
---
|
||||
|
||||
## G. 借款 · 新建 `bx/advance/form`
|
||||
|
||||
| ID | 字段/用例 | 步骤 | 预期 |
|
||||
|----|-----------|------|------|
|
||||
| BX-AF-D01 | 借款事由 | 查看 | 必填多行;placeholder「用途说明」 |
|
||||
| BX-AF-D02 | 用途 | — | 选填单行「可选」 |
|
||||
| BX-AF-D03 | 金额(元) | — | 必填 digit |
|
||||
| BX-AF-D04 | 费用项目 | — | 选填 ActionSheet |
|
||||
| BX-AF-D05 | 备注 | — | 选填 |
|
||||
| BX-AF-D06 | 凭证照片 | — | +拍照,上限6,「最多上传6张」 |
|
||||
| BX-AF-V01 | 事由空 | 提交 | Toast「请填写借款事由」 |
|
||||
| BX-AF-V02 | 金额空/≤0 | 提交 | Toast「请填写金额」 |
|
||||
| BX-AF-C01 | 选费用/上传/删图 | — | 同报销规则 |
|
||||
| BX-AF-S01 | 存草稿 | 合法 | 「已保存草稿」 |
|
||||
| BX-AF-S02 | 提交审批 | 合法 | 「已提交审批」 |
|
||||
| BX-AF-N01 | 无修改页 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## H. 借款 · 详情 `bx/advance/detail`
|
||||
|
||||
| ID | 字段/操作 | 预期 |
|
||||
|----|-----------|------|
|
||||
| BX-AD-H01~H05 | Hero:单号、事由、状态、金额、申请人 | 正确 |
|
||||
| BX-AD-B01 | 用途 | purpose 或「—」 |
|
||||
| BX-AD-B02 | 申请时间 | applyTime |
|
||||
| BX-AD-B03 | 当前节点 | 或「—」 |
|
||||
| BX-AD-B04 | 发放状态 | issueStatus |
|
||||
| BX-AD-B05 | 备注 | 或「—」 |
|
||||
| BX-AD-R01 | 审批记录 | 同报销结构 |
|
||||
| BX-AD-I01 | 凭证图 | **详情可能不展示上传图**(记录实际;若无展示标产品差异) |
|
||||
| BX-AD-O01 | draft 提交 | 弹窗「确认提交该借款单?」 |
|
||||
| BX-AD-O02 | pending 通过/驳回 | 同报销 |
|
||||
| BX-AD-N01 | 无编辑删除 | **无** |
|
||||
|
||||
---
|
||||
|
||||
## I. 借款审批 `bx/approve/list?type=advance`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-AA-A01 | 标题 | →借款审批 | 「借款审批」 |
|
||||
| BX-AA-A02 | 空态 | — | 「暂无待审批」 |
|
||||
| BX-AA-F01 | 卡片字段 | — | 单号、reason、状态、申请人、金额、通过/驳回 |
|
||||
| BX-AA-O01~O02 | 通过/驳回 | — | 「操作成功」 |
|
||||
| BX-AA-N01 | 无编辑删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## J. 我的单据 `bx/mine/list`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-MN-A01 | 入口 | →我的单据 | 标题正确 |
|
||||
| BX-MN-T01 | Tab 文案 | — | 全部 / 报销 / 借款 |
|
||||
| BX-MN-T02 | Tab 全部 | — | 两类单据可同时出现 |
|
||||
| BX-MN-T03 | Tab 报销 | — | 仅报销 |
|
||||
| BX-MN-T04 | Tab 借款 | — | 仅借款 |
|
||||
| BX-MN-T05 | 空态 | — | 「暂无单据」 |
|
||||
| BX-MN-S01 | 无搜索(当前) | 查看 | 无则记录 |
|
||||
| BX-MN-F01 | 主标题 | — | 报销/借款 + applyCode |
|
||||
| BX-MN-F02 | 副标题 | — | reason 或 expenseName |
|
||||
| BX-MN-F03 | 状态/时间/金额 | — | 正确 |
|
||||
| BX-MN-F04 | 点卡片 | — | 进对应详情 |
|
||||
| BX-MN-N01 | 无编辑删除/无列表审批按钮 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
| 批次 | 日期 | 结果 | 备注 |
|
||||
|------|------|------|------|
|
||||
| — | — | — | 文字用例阶段 |
|
||||
|
||||
|
||||
## 附录:加厚用例(边界 / Tab×操作 / 上传 / 审批组合)
|
||||
|
||||
### 待办 Tab × 空态 / 加载
|
||||
|
||||
| ID | Tab | 场景 | 预期 |
|
||||
|----|-----|------|------|
|
||||
| BX-TD-X001 | 待审批 | 空 | 暂无单据 |
|
||||
| BX-TD-X002 | 我的申请 | 空 | 暂无单据 |
|
||||
| BX-TD-X003 | 已办 | 空 | 暂无单据 |
|
||||
| BX-TD-X004 | 待审批 | 加载失败 | Toast |
|
||||
| BX-TD-X005 | 我的申请 | 分页第二页 | 条数增加 |
|
||||
| BX-TD-X006 | 已办 | 有数据字段齐全 | 单号/副标题/状态/申请人/时间/金额 |
|
||||
|
||||
### 待审批 — 报销单与借款单分别审
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-TD-X010 | 报销单通过 | 待审报销点通过确认 | 通过成功 |
|
||||
| BX-TD-X011 | 报销单驳回填意见 | 填「金额不符」 | 驳回成功 |
|
||||
| BX-TD-X012 | 借款单通过 | 待审借款通过 | 通过成功 |
|
||||
| BX-TD-X013 | 借款单驳回空意见 | 意见空确认 | 成功(默认驳回) |
|
||||
| BX-TD-X014 | 通过按钮不进详情 | 点通过 | 不误进详情(阻止冒泡) |
|
||||
| BX-TD-X015 | 驳回按钮不进详情 | 点驳回 | 同上 |
|
||||
| BX-TD-X016 | 点卡片非按钮区 | 点标题 | 进详情 |
|
||||
| BX-TD-X017 | Sheet 取消 | +申请后点遮罩/取消 | 不跳转(按实现) |
|
||||
|
||||
### 报销新建 — 字段边界
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| BX-RF-X001 | 标题改超长 | 200字标题提交 | 成功或长度错 |
|
||||
| BX-RF-X002 | 金额小数 2位 | 12.34 | 成功 |
|
||||
| BX-RF-X003 | 金额很多位小数 | 1.23456 | 按实现截断/校验 |
|
||||
| BX-RF-X004 | 金额负数 | -1 | 「请填写金额」或失败 |
|
||||
| BX-RF-X005 | 金额非数字 | 粘贴 abc | 无法输入或校验失败 |
|
||||
| BX-RF-X006 | 明细说明仅空格 | 空格提交 | 「请填写明细说明」 |
|
||||
| BX-RF-X007 | 明细说明超长 | 1000字 | 成功或失败提示 |
|
||||
| BX-RF-X008 | 备注超长 | 1000字 | 同上 |
|
||||
| BX-RF-X009 | 日期选昨天 | 选昨天 | 可提交 |
|
||||
| BX-RF-X010 | 日期选未来 | 选未来 | 按业务允许/拒绝 |
|
||||
| BX-RF-X011 | 费用项目切换重选 | 选A再选B | 回显B |
|
||||
| BX-RF-X012 | 上传1张后存草稿 | — | 草稿含附件 |
|
||||
| BX-RF-X013 | 上传后删光再提交 | 删光图提交 | 允许(附件非必填) |
|
||||
| BX-RF-X014 | 上传中点提交 | 上传未完提交 | 等待或提示 |
|
||||
| BX-RF-X015 | 存草稿后再编辑入口 | 回列表 | 无编辑页(负向) |
|
||||
| BX-RF-X016 | 必填三项缺一矩阵-缺费用 | — | 请选择费用项目 |
|
||||
| BX-RF-X017 | 必填三项缺一矩阵-缺金额 | — | 请填写金额 |
|
||||
| BX-RF-X018 | 必填三项缺一矩阵-缺说明 | — | 请填写明细说明 |
|
||||
| BX-RF-X019 | 合法后断网提交 | — | 失败 Toast |
|
||||
| BX-RF-X020 | 提交成功回列表状态 | — | 审批中或对应状态 |
|
||||
|
||||
### 报销详情 — 状态×按钮矩阵
|
||||
|
||||
| ID | 状态 | 预期底栏 |
|
||||
|----|------|----------|
|
||||
| BX-RD-X001 | draft | 仅提交审批 |
|
||||
| BX-RD-X002 | pending | 驳回+通过 |
|
||||
| BX-RD-X003 | approved | 无审操作 |
|
||||
| BX-RD-X004 | rejected | 按实现(可能无) |
|
||||
| BX-RD-X005 | paid/issued 等 | 无审操作 |
|
||||
|
||||
### 报销详情 — 字段空满
|
||||
|
||||
| ID | 字段 | 空 | 满 |
|
||||
|----|------|----|----|
|
||||
| BX-RD-X010 | 当前节点 | — | 有节点名 |
|
||||
| BX-RD-X011 | 备注 | — | 全文 |
|
||||
| BX-RD-X012 | 明细多条 | 至少1 | 多条均展示 |
|
||||
| BX-RD-X013 | 票据0张 | 无区或空 | — |
|
||||
| BX-RD-X014 | 票据多张预览 | — | 每张可预览 |
|
||||
| BX-RD-X015 | 审批记录0 | 空区 | — |
|
||||
| BX-RD-X016 | 审批记录多条 | — | 人/状态/意见齐全 |
|
||||
|
||||
### 借款新建边界
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| BX-AF-X001 | 事由1字 | 可成功或按规则 |
|
||||
| BX-AF-X002 | 事由超长 | 成功或提示 |
|
||||
| BX-AF-X003 | 用途填写 | 详情可见(若展示) |
|
||||
| BX-AF-X004 | 金额0 | 请填写金额 |
|
||||
| BX-AF-X005 | 金额很大 | 成功或上限提示 |
|
||||
| BX-AF-X006 | 不选费用项目 | 可提交(选填) |
|
||||
| BX-AF-X007 | 选费用项目 | 可提交 |
|
||||
| BX-AF-X008 | 凭证0张 | 可提交 |
|
||||
| BX-AF-X009 | 凭证6张 | 可提交 |
|
||||
| BX-AF-X010 | 凭证第7张 | 最多上传6张 |
|
||||
| BX-AF-X011 | 存草稿成功回列表 | 可见草稿 |
|
||||
| BX-AF-X012 | 提交成功 | 已提交审批 |
|
||||
|
||||
### 借款详情状态矩阵
|
||||
|
||||
| ID | 状态 | 预期 |
|
||||
|----|------|------|
|
||||
| BX-AD-X001 | draft 有提交 | 有 |
|
||||
| BX-AD-X002 | pending 有通过驳回 | 有 |
|
||||
| BX-AD-X003 | 终态无审按钮 | 无 |
|
||||
| BX-AD-X004 | 发放状态字段 | 有值或— |
|
||||
| BX-AD-X005 | 用途字段 | 有值或— |
|
||||
|
||||
### 审批列表加厚
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| BX-RA-X001 | 报销审批空 | 暂无待审批 |
|
||||
| BX-RA-X002 | 借款审批空 | 暂无待审批 |
|
||||
| BX-RA-X003 | 列表通过后条目消失或变 | 更新 |
|
||||
| BX-RA-X004 | 列表驳回后更新 | 更新 |
|
||||
| BX-RA-X005 | 点卡片进详情再返回 | 列表仍可用 |
|
||||
|
||||
### 我的单据 Tab×类型
|
||||
|
||||
| ID | Tab | 预期 |
|
||||
|----|-----|------|
|
||||
| BX-MN-X001 | 全部含报销借款 | 两类标题前缀正确 |
|
||||
| BX-MN-X002 | 报销纯净 | 无借款前缀 |
|
||||
| BX-MN-X003 | 借款纯净 | 无报销前缀 |
|
||||
| BX-MN-X004 | 点报销进报销详情 | 正确 |
|
||||
| BX-MN-X005 | 点借款进借款详情 | 正确 |
|
||||
| BX-MN-X006 | 分页 | 正常 |
|
||||
| BX-MN-X007 | 无搜索时确认 | 无搜索框 |
|
||||
| BX-MN-X008 | 状态 Tag 抽样 | 各状态可展示 |
|
||||
|
||||
### 报销模块负向汇总
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| BX-N-X001 | 任意列表无删除 | 无 |
|
||||
| BX-N-X002 | 任意列表无编辑 | 无 |
|
||||
| BX-N-X003 | 灰卡借款发放 | 本期暂未开放 |
|
||||
| BX-N-X004 | 灰卡全部单据 | 本期暂未开放 |
|
||||
| BX-N-X005 | 灰卡付款记录 | 本期暂未开放 |
|
||||
@@ -0,0 +1,377 @@
|
||||
# 地磅管理 — 全面测试用例
|
||||
|
||||
> **改**:地磅单二次过磅(皮重)
|
||||
> **删/作废**:当天单据作废
|
||||
> **增**:计划详情「生成出入库关联单」
|
||||
> 列表多为前端关键词过滤,须对**每个搜索维度**分别测命中。
|
||||
|
||||
---
|
||||
|
||||
## A. 仪表盘 `db/dashboard/index`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-DS-A01 | 标题副标题 | →仪表盘 | 「地磅仪表盘」;含今日出入库与过磅概况 |
|
||||
| DB-DS-S01 | 统计「今日过磅」 | 查看 | 数字≥0,与后端一致(可对接口) |
|
||||
| DB-DS-S02 | 统计「待二次过磅」 | — | 数字≥0 |
|
||||
| DB-DS-S03 | 统计「今日入库」 | — | 数字≥0 |
|
||||
| DB-DS-S04 | 统计「今日出库」 | — | 数字≥0 |
|
||||
| DB-DS-Q01 | 快捷「地磅单管理」 | 点击 | 进地磅单列表;Tag「现场」文案可见 |
|
||||
| DB-DS-Q02 | 快捷「地磅计划」 | 点击 | 进计划列表 |
|
||||
| DB-DS-Q03 | 快捷「出入库单」 | 点击 | 进出入库列表 |
|
||||
| DB-DS-G01 | 货品节选行 | 有数据 | 每行「计划 x / 实绩 y」;最多约6条 |
|
||||
| DB-DS-G02 | 货品节选空 | 无数据 | 不报错白屏 |
|
||||
| DB-DS-L01 | 加载中 | 弱网 | 「加载中…」 |
|
||||
| DB-DS-N01 | 无检索/无增删改 | — | 仪表盘本身无搜索框、无编辑删除 |
|
||||
| DB-DS-A02 | 返回工作台 | — | 回工作台 |
|
||||
|
||||
---
|
||||
|
||||
## B. 地磅计划 · 列表 `db/plans/list`
|
||||
|
||||
### B1 检索(每个条件单独测)
|
||||
|
||||
搜索框 placeholder:「计划号 / 产品 / 客户 / 车牌」(前端过滤)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-PL-S01 | 占位符 | 查看 | 上述 placeholder |
|
||||
| DB-PL-S02 | 按计划号命中 | 输入已知 planNo | 主标题匹配 |
|
||||
| DB-PL-S03 | 按产品命中 | 输入产品名 | 副标题含产品 |
|
||||
| DB-PL-S04 | 按客户命中 | 输入客户名 | 「客户」行匹配 |
|
||||
| DB-PL-S05 | 按车牌命中 | 输入车牌 | 过滤出对应计划 |
|
||||
| DB-PL-S06 | 按执行状态关键词 | 若过滤含 executionStatus | 输入状态关键字有结果或无 |
|
||||
| DB-PL-S07 | 无命中 | 乱码 | 「暂无计划」 |
|
||||
| DB-PL-S08 | 清空恢复 | 清空关键字 | 恢复当前 Tab 列表 |
|
||||
|
||||
### B2 Tab(逐个)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-PL-T01 | Tab 文案 | — | 全部 / 入库 / 出库 |
|
||||
| DB-PL-T02 | 全部 | — | 入出库均可出现 |
|
||||
| DB-PL-T03 | 入库 | — | businessType 为入库 |
|
||||
| DB-PL-T04 | 出库 | — | businessType 为出库 |
|
||||
| DB-PL-T05 | Tab+搜索组合 | 先搜再切 Tab | 结果同时满足 |
|
||||
|
||||
### B3 卡片字段
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-PL-F01 | planNo | 主标题 |
|
||||
| DB-PL-F02 | product · businessType | 副标题 |
|
||||
| DB-PL-F03 | executionStatus Tag | 含已/中/待语义着色 |
|
||||
| DB-PL-F04 | 计划量 | plannedQuantity kg 格式 |
|
||||
| DB-PL-F05 | 已执行 | executedQuantity kg |
|
||||
| DB-PL-F06 | 客户 | customerName 或「—」 |
|
||||
| DB-PL-F07 | 点卡片 | 进计划详情 |
|
||||
| DB-PL-A01 | 无新增/编辑/删除 | **无** |
|
||||
| DB-PL-A02 | 空态 | 「暂无计划」 |
|
||||
| DB-PL-A03 | 上限约80条 | 超大数据仅前80(记录实际) |
|
||||
|
||||
---
|
||||
|
||||
## C. 地磅计划 · 详情 `db/plans/detail`
|
||||
|
||||
### C1 字段逐项
|
||||
|
||||
| ID | 位置 | 字段 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-PD-H01 | Hero | planNo | 主标题 |
|
||||
| DB-PD-H02 | Hero | product | 副标题 |
|
||||
| DB-PD-H03 | Hero | executionStatus | Tag |
|
||||
| DB-PD-H04 | Hero | businessType | chip |
|
||||
| DB-PD-H05 | Hero | 已执行/计划量 | chip |
|
||||
| DB-PD-B01 | 基本信息 | 计划日期 | planDate |
|
||||
| DB-PD-B02 | 基本信息 | 起止 | startDate ~ endDate |
|
||||
| DB-PD-B03 | 基本信息 | 规格 | 或「—」 |
|
||||
| DB-PD-B04 | 基本信息 | 车牌 | 或「—」 |
|
||||
| DB-PD-B05 | 基本信息 | 客户 | 或「—」 |
|
||||
| DB-PD-B06 | 基本信息 | 备注 | 或「—」 |
|
||||
| DB-PD-I01 | 关联出入库空 | 无 | 「暂无出入库单」 |
|
||||
| DB-PD-I02 | 关联出入库有 | 点卡片 | 进出入库详情;卡片含 batchNo、车牌·状态 |
|
||||
| DB-PD-W01 | 关联地磅空 | 无 | 「暂无地磅单」 |
|
||||
| DB-PD-W02 | 关联地磅有 | 点卡片 | 进地磅详情;含车牌、净重 |
|
||||
|
||||
### C2 生成出入库(写)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-PD-O01 | 按钮存在 | 查看底栏 | 「生成出入库关联单」 |
|
||||
| DB-PD-O02 | 取消 | 弹窗取消 | 不生成 |
|
||||
| DB-PD-O03 | 确认成功 | 确认「为该计划创建…」 | Toast「已生成」;可跳转新出入库详情 |
|
||||
| DB-PD-N01 | 无编辑计划/无删除计划 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## D. 地磅单 · 列表 `db/weighing/list`
|
||||
|
||||
### D1 统计与 Tab
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-WL-A01 | 入口 | →地磅单管理 | 标题正确 |
|
||||
| DB-WL-S01 | 统计「今日过磅」 | 顶栏 | 数字展示 |
|
||||
| DB-WL-S02 | 统计「待二次过磅」 | — | 数字展示 |
|
||||
| DB-WL-S03 | 统计「当前列表」 | — | 等于当前列表条数 |
|
||||
| DB-WL-T01 | Tab 文案 | — | 今日 / 待二次 / 全部 |
|
||||
| DB-WL-T02 | Tab 今日 | — | 仅今日单据;会重新 load |
|
||||
| DB-WL-T03 | Tab 待二次 | — | 阶段=一次过磅 |
|
||||
| DB-WL-T04 | Tab 全部 | — | 全量 |
|
||||
|
||||
### D2 检索(逐维度)
|
||||
|
||||
placeholder:「车牌 / 货物 / 计划号」
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-WL-K01 | 占位 | — | 上述 |
|
||||
| DB-WL-K02 | 按车牌 | 输入 | 副标题车牌匹配 |
|
||||
| DB-WL-K03 | 按货物 | 输入 cargoName | 匹配 |
|
||||
| DB-WL-K04 | 按计划号 | 输入 planNo | 匹配 |
|
||||
| DB-WL-K05 | 按批次号 | 若支持 batchNo | 匹配 |
|
||||
| DB-WL-K06 | 按供货/收货单位 | 若过滤含 supplier/consignee | 匹配 |
|
||||
| DB-WL-K07 | 无命中 | 乱码 | 「暂无地磅单」 |
|
||||
|
||||
### D3 卡片字段
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-WL-F01 | 主标题 | batchNo / id |
|
||||
| DB-WL-F02 | 副标题 | 车牌 · 货物 |
|
||||
| DB-WL-F03 | 阶段 Tag | 已作废/已完成/一次过磅/待过磅 |
|
||||
| DB-WL-F04 | 净重 | kg 格式 |
|
||||
| DB-WL-F05 | 过磅时间 | grossWeightTime |
|
||||
| DB-WL-F06 | 一次过磅「二次过磅」样式 | 可见;点击进详情 |
|
||||
| DB-WL-N01 | 无列表编辑删除 | **无** |
|
||||
|
||||
---
|
||||
|
||||
## E. 地磅单 · 详情 `db/weighing/detail`(含修改皮重、作废)
|
||||
|
||||
### E1 字段逐项
|
||||
|
||||
| ID | 位置 | 字段 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-WD-H01 | Hero | 车牌 | 主标题 |
|
||||
| DB-WD-H02 | Hero | 货物·业务类型 | 副标题 |
|
||||
| DB-WD-H03 | Hero | 过磅阶段 Tag | 正确 |
|
||||
| DB-WD-H04 | Hero | 净重 / 批次 | chip |
|
||||
| DB-WD-W01 | 过磅数据 | 毛重 | grossWeight |
|
||||
| DB-WD-W02 | 过磅数据 | 毛重时间 | |
|
||||
| DB-WD-W03 | 过磅数据 | 皮重 | |
|
||||
| DB-WD-W04 | 过磅数据 | 皮重时间 | |
|
||||
| DB-WD-W05 | 过磅数据 | 扣重 | |
|
||||
| DB-WD-W06 | 过磅数据 | 净重 | |
|
||||
| DB-WD-B01 | 业务信息 | 计划号 | |
|
||||
| DB-WD-B02 | 业务信息 | 规格 | |
|
||||
| DB-WD-B03 | 业务信息 | 供货单位 | |
|
||||
| DB-WD-B04 | 业务信息 | 收货单位 | |
|
||||
| DB-WD-B05 | 业务信息 | 过磅点 | |
|
||||
| DB-WD-B06 | 业务信息 | 备注 | |
|
||||
|
||||
### E2 二次过磅(修改类)
|
||||
|
||||
| ID | 标题 | 预置 | 步骤 | 预期 |
|
||||
|----|------|------|------|------|
|
||||
| DB-WD-M01 | 一次过磅显示入口 | 阶段=一次过磅 | 看底栏 | 「二次过磅」 |
|
||||
| DB-WD-M02 | 非一次过磅无入口 | 已完成等 | 看底栏 | 无二次过磅 |
|
||||
| DB-WD-M03 | 展开表单 | 点二次过磅 | 皮重(kg)输入;取消/确认保存 |
|
||||
| DB-WD-M04 | 皮重空校验 | 空确认 | Toast「请填写皮重」 |
|
||||
| DB-WD-M05 | 皮重≤0 | 0 确认 | Toast「请填写皮重」 |
|
||||
| DB-WD-M06 | 保存成功 | 合法皮重 | Toast「二次过磅已保存」;净重=max(毛-皮-扣,0) 更新 |
|
||||
| DB-WD-M07 | 取消表单 | 点取消 | 收起;数据不变 |
|
||||
|
||||
### E3 作废(删除语义)
|
||||
|
||||
| ID | 标题 | 预置 | 步骤 | 预期 |
|
||||
|----|------|------|------|------|
|
||||
| DB-WD-D01 | 当天未作废显示作废 | 当天且未作废 | 底栏 | 「作废」 |
|
||||
| DB-WD-D02 | 作废取消 | 弹窗取消 | 状态不变 |
|
||||
| DB-WD-D03 | 作废确认 | 确认「仅支持作废当天单据…」 | Toast「已作废」;阶段已作废 |
|
||||
| DB-WD-D04 | 非当天无作废或失败 | 历史单 | — | 无按钮或提示不可作废 |
|
||||
| DB-WD-N01 | 无通用编辑页 | — | **无**改毛重等其它字段入口 |
|
||||
|
||||
---
|
||||
|
||||
## F. 出入库 · 列表 `db/inventory/list`
|
||||
|
||||
### F1 检索逐维度
|
||||
|
||||
placeholder:「单号 / 车牌 / 产品 / 往来单位」
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-IL-K01 | 占位 | — | 上述 |
|
||||
| DB-IL-K02 | 按单号/批次 | batchNo | 命中 |
|
||||
| DB-IL-K03 | 按车牌 | — | 命中 |
|
||||
| DB-IL-K04 | 按产品 | — | 命中 |
|
||||
| DB-IL-K05 | 按往来单位 | partnerCompany | 命中 |
|
||||
| DB-IL-K06 | 按计划号 | 若支持 | 命中 |
|
||||
| DB-IL-K07 | 无命中 | — | 「暂无出入库单」 |
|
||||
|
||||
### F2 Tab
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-IL-T01 | 全部/入库/出库/待处理 | 文案齐全 |
|
||||
| DB-IL-T02 | 入库 | 仅入库 |
|
||||
| DB-IL-T03 | 出库 | 仅出库 |
|
||||
| DB-IL-T04 | 待处理 | status 含「待」 |
|
||||
| DB-IL-T05 | Tab+搜索 | 组合过滤正确 |
|
||||
|
||||
### F3 卡片字段
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-IL-F01 | 主标题 | batchNo/id |
|
||||
| DB-IL-F02 | 副标题 | 车牌·产品·业务类型 |
|
||||
| DB-IL-F03 | status Tag | 正确 |
|
||||
| DB-IL-F04 | 数量 | quantity |
|
||||
| DB-IL-F05 | 往来单位 | partnerCompany |
|
||||
| DB-IL-F06 | 日期 | date |
|
||||
| DB-IL-F07 | 进详情 | 点击进入 |
|
||||
| DB-IL-N01 | 无增删改按钮 | **无** |
|
||||
|
||||
---
|
||||
|
||||
## G. 出入库 · 详情 `db/inventory/detail`
|
||||
|
||||
| ID | 位置 | 字段/操作 | 预期 |
|
||||
|----|------|-----------|------|
|
||||
| DB-ID-H01~H05 | Hero | 批次、产品·类型、状态、车牌、数量 | 正确 |
|
||||
| DB-ID-B01 | 基本信息 | 日期 | |
|
||||
| DB-ID-B02 | 基本信息 | 往来单位 | |
|
||||
| DB-ID-B03 | 基本信息 | 规格 | |
|
||||
| DB-ID-B04 | 基本信息 | 计划号 | |
|
||||
| DB-ID-B05 | 基本信息 | 备注 | |
|
||||
| DB-ID-W01 | 过磅数据 | 毛重 | |
|
||||
| DB-ID-W02 | 过磅数据 | 皮重 | |
|
||||
| DB-ID-W03 | 过磅数据 | 净重 | |
|
||||
| DB-ID-F01 | 现场反馈 | 有 remark 显示文本 | 有则显示;图可能未渲染(记录实际) |
|
||||
| DB-ID-L01 | 关联地磅单-有 | 点「查看」 | 进地磅详情 |
|
||||
| DB-ID-L02 | 关联地磅单-无 | 点 | Toast「暂无关联地磅单」 |
|
||||
| DB-ID-L03 | 关联计划-有 | 点 | 进计划详情 |
|
||||
| DB-ID-L04 | 关联计划-无 | 点 | Toast「暂无关联计划」 |
|
||||
| DB-ID-N01 | 无编辑底栏/无删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
| 批次 | 日期 | 结果 | 备注 |
|
||||
|------|------|------|------|
|
||||
| — | — | — | 文字用例阶段 |
|
||||
|
||||
|
||||
## 附录:加厚用例(检索维度×Tab 矩阵 / 二次过磅边界 / 作废边界)
|
||||
|
||||
### 计划列表 — 检索×Tab 矩阵
|
||||
|
||||
关键词样例:已知 planNo=`P001`,产品=`尿素`,客户=`测试客户`,车牌=`浙A12345`
|
||||
|
||||
| ID | Tab | 关键字 | 预期 |
|
||||
|----|-----|--------|------|
|
||||
| DB-PL-X001 | 全部 | P001 | 命中 |
|
||||
| DB-PL-X002 | 入库 | P001 | 仅当该计划入库才命中 |
|
||||
| DB-PL-X003 | 出库 | P001 | 仅当出库才命中 |
|
||||
| DB-PL-X004 | 全部 | 尿素 | 命中 |
|
||||
| DB-PL-X005 | 入库 | 尿素 | 交集 |
|
||||
| DB-PL-X006 | 出库 | 尿素 | 交集 |
|
||||
| DB-PL-X007 | 全部 | 测试客户 | 命中 |
|
||||
| DB-PL-X008 | 全部 | 浙A12345 | 命中 |
|
||||
| DB-PL-X009 | 全部 | 不存在 | 暂无计划 |
|
||||
| DB-PL-X010 | 入库 | 不存在 | 暂无计划 |
|
||||
| DB-PL-X011 | 出库 | 不存在 | 暂无计划 |
|
||||
| DB-PL-X012 | 全部 | 空 | 全量 |
|
||||
| DB-PL-X013 | 全部 | 空格 | 不崩溃 |
|
||||
| DB-PL-X014 | 先搜再切 Tab | P001→出库 | 交集或空 |
|
||||
| DB-PL-X015 | 先 Tab 再搜 | 入库→尿素 | 交集 |
|
||||
|
||||
### 计划卡片 / 详情字段空满
|
||||
|
||||
| ID | 项 | 预期 |
|
||||
|----|-----|------|
|
||||
| DB-PL-X020 | 计划量0 | 显示0 kg 类 |
|
||||
| DB-PL-X021 | 已执行=计划量 | 进度满 |
|
||||
| DB-PL-X022 | 备注空 | — |
|
||||
| DB-PL-X023 | 规格空 | — |
|
||||
| DB-PL-X024 | 车牌空 | — |
|
||||
| DB-PD-X001 | 关联出入库多条 | 均可点进 |
|
||||
| DB-PD-X002 | 关联地磅多条 | 均可点进 |
|
||||
| DB-PD-X003 | 生成出入库连点 | 不重复乱生成(或有防抖) |
|
||||
| DB-PD-X004 | 生成失败 Toast | mock 失败有提示 |
|
||||
|
||||
### 地磅单列表 — 检索×Tab
|
||||
|
||||
| ID | Tab | 关键字类型 | 预期 |
|
||||
|----|-----|------------|------|
|
||||
| DB-WL-X001 | 今日 | 车牌 | 命中今日 |
|
||||
| DB-WL-X002 | 待二次 | 车牌 | 一次过磅命中 |
|
||||
| DB-WL-X003 | 全部 | 车牌 | 命中 |
|
||||
| DB-WL-X004 | 全部 | 货物名 | 命中 |
|
||||
| DB-WL-X005 | 全部 | 计划号 | 命中 |
|
||||
| DB-WL-X006 | 全部 | 批次号 | 命中 |
|
||||
| DB-WL-X007 | 全部 | 供货单位 | 命中(若支持) |
|
||||
| DB-WL-X008 | 全部 | 收货单位 | 命中(若支持) |
|
||||
| DB-WL-X009 | 今日 | 无意义 | 空 |
|
||||
| DB-WL-X010 | 待二次 | 无意义 | 空 |
|
||||
| DB-WL-X011 | 统计条与列表条数一致 | 看当前列表 | 当前列表数=统计 |
|
||||
| DB-WL-X012 | Tab 切换统计刷新 | 切待二次 | 统计变化合理 |
|
||||
|
||||
### 二次过磅边界
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| DB-WD-X001 | 皮重=毛重 | 保存 | 净重按扣重算≥0 |
|
||||
| DB-WD-X002 | 皮重>毛重 | 保存 | 净重0或失败提示 |
|
||||
| DB-WD-X003 | 皮重小数 | 1.23 | 成功 |
|
||||
| DB-WD-X004 | 皮重很大 | 999999 | 成功或上限 |
|
||||
| DB-WD-X005 | 保存后阶段变化 | 成功后 | 不再一次过磅/变已完成 |
|
||||
| DB-WD-X006 | 保存后底栏二次入口消失 | — | 无二次过磅或不可再办 |
|
||||
| DB-WD-X007 | 已完成后打开详情 | — | 无二次过磅 |
|
||||
| DB-WD-X008 | 作废后详情 | — | Tag 已作废;无二次 |
|
||||
|
||||
### 作废边界
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-WD-X010 | 当天可作废 | 成功 |
|
||||
| DB-WD-X011 | 作废后列表 Tag | 已作废 |
|
||||
| DB-WD-X012 | 作废后不可再作废 | 无按钮或失败 |
|
||||
| DB-WD-X013 | 非当天无作废 | 无或提示 |
|
||||
| DB-WD-X014 | 作废确认文案 | 含「仅支持作废当天」 |
|
||||
|
||||
### 出入库检索×Tab
|
||||
|
||||
| ID | Tab | 关键字 | 预期 |
|
||||
|----|-----|--------|------|
|
||||
| DB-IL-X001 | 全部 | 单号 | 命中 |
|
||||
| DB-IL-X002 | 入库 | 单号 | 交集 |
|
||||
| DB-IL-X003 | 出库 | 单号 | 交集 |
|
||||
| DB-IL-X004 | 待处理 | 单号 | 交集 |
|
||||
| DB-IL-X005 | 全部 | 车牌 | 命中 |
|
||||
| DB-IL-X006 | 全部 | 产品 | 命中 |
|
||||
| DB-IL-X007 | 全部 | 往来单位 | 命中 |
|
||||
| DB-IL-X008 | 全部 | 计划号 | 命中(若支持) |
|
||||
| DB-IL-X009 | 待处理 | 无数据 | 暂无出入库单 |
|
||||
| DB-IL-X010 | 各 Tab 空态 | — | 暂无出入库单 |
|
||||
|
||||
### 出入库详情关联
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-ID-X001 | 有地磅跳转往返 | 详情↔地磅↔回 |
|
||||
| DB-ID-X002 | 有计划跳转往返 | 详情↔计划↔回 |
|
||||
| DB-ID-X003 | 无关联双 Toast | 两个入口分别提示 |
|
||||
| DB-ID-X004 | 现场反馈长文 | 可展示 |
|
||||
| DB-ID-X005 | 过磅三角色空 | — 展示 |
|
||||
|
||||
### 仪表盘加厚
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| DB-DS-X001 | 四统计均为0 | 显示0不报错 |
|
||||
| DB-DS-X002 | 快捷入口×3 往返 | 都能回仪表盘 |
|
||||
| DB-DS-X003 | 货品节选>6只显示6 | 最多约6 |
|
||||
| DB-DS-X004 | 加载失败 | Toast |
|
||||
@@ -0,0 +1,424 @@
|
||||
# 运输管理 — 全面测试用例
|
||||
|
||||
> **增/写**:安排运输、生成结算单、购买证上传、标记齐备、提交审批、审批
|
||||
> **改**:无通用编辑页(负向)
|
||||
> **删**:无删除(负向);归档为业务完结
|
||||
|
||||
---
|
||||
|
||||
## A. 运输计划 · 列表 `pl/plan/list`
|
||||
|
||||
### A1 检索
|
||||
|
||||
placeholder:「计划号 / 客户 / 承运」(确认搜索 + 前端二次过滤)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-PL-K01 | 占位 | — | 上述 |
|
||||
| PL-PL-K02 | 按计划号 | 输入确认 | 主标题命中 |
|
||||
| PL-PL-K03 | 按客户 | — | 命中 |
|
||||
| PL-PL-K04 | 按承运/供应商 | — | 命中 |
|
||||
| PL-PL-K05 | 按目的地等副标题组合 | 若过滤含 | 命中 |
|
||||
| PL-PL-K06 | 无命中 | 乱码 | 「暂无运输计划」 |
|
||||
|
||||
### A2 Tab(逐个)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-PL-T01 | Tab 文案 | — | 待发运 / 在途 / 已完成 |
|
||||
| PL-PL-T02 | 待发运 | — | 阶段 Tag「待发运」;可有「安排运输」 |
|
||||
| PL-PL-T03 | 在途 | — | 「在途」;一般无安排按钮 |
|
||||
| PL-PL-T04 | 已完成 | — | 「已完成」 |
|
||||
| PL-PL-T05 | Tab+搜索 | — | 组合正确 |
|
||||
|
||||
### A3 卡片字段与操作
|
||||
|
||||
| ID | 字段/操作 | 预期 |
|
||||
|----|-----------|------|
|
||||
| PL-PL-F01 | planNo | 主标题 |
|
||||
| PL-PL-F02 | planCardSub | 副标题(品名·数量·路线) |
|
||||
| PL-PL-F03 | 阶段 Tag | 待发运/在途/已完成/已作废 |
|
||||
| PL-PL-F04 | planDate | 计划日期 |
|
||||
| PL-PL-O01 | 「详情」 | 进计划详情 |
|
||||
| PL-PL-O02 | 待发运「安排运输」 | 进安排页 |
|
||||
| PL-PL-O03 | 非待发运无安排 | 无按钮或不可点 |
|
||||
| PL-PL-O04 | 已取消无安排 | 无安排 |
|
||||
| PL-PL-P01 | 加载更多 | 多页时追加 |
|
||||
| PL-PL-N01 | 无编辑/删除 | **无** |
|
||||
| PL-PL-A01 | 空态 | 「暂无运输计划」 |
|
||||
|
||||
---
|
||||
|
||||
## B. 计划详情 `pl/plan/detail`
|
||||
|
||||
### B1 字段逐项
|
||||
|
||||
| ID | 位置 | 字段 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-PD-H01 | Hero | planNo | |
|
||||
| PL-PD-H02 | Hero | planCardSub | |
|
||||
| PL-PD-H03 | Hero | transportStatus Tag | 待安排/已安排 |
|
||||
| PL-PD-H04 | Hero | planType chip | 直销单/入库单/出库单 |
|
||||
| PL-PD-H05 | Hero | followStatus / archiveStatus | chip |
|
||||
| PL-PD-B01 | 基本信息 | 计划日期 | |
|
||||
| PL-PD-B02 | 基本信息 | 客户 | |
|
||||
| PL-PD-B03 | 基本信息 | 供应商 | |
|
||||
| PL-PD-B04 | 基本信息 | 取货地址 | |
|
||||
| PL-PD-B05 | 基本信息 | 送货地址 | |
|
||||
| PL-PD-B06 | 基本信息 | 联系人+电话 | |
|
||||
| PL-PD-B07 | 基本信息 | 跟进人 | |
|
||||
| PL-PD-B08 | 基本信息 | 备注 | |
|
||||
| PL-PD-M01 | 产品明细空 | — | 「暂无明细」 |
|
||||
| PL-PD-M02 | 产品明细有 | 每行 | productName + quantity + unit |
|
||||
| PL-PD-T01 | 已安排块 | 车牌 | 有则显示 |
|
||||
| PL-PD-T02 | 已安排块 | 司机 | |
|
||||
| PL-PD-T03 | 已安排块 | 承运商 | |
|
||||
| PL-PD-T04 | 已安排块 | 司机扫码 | 有链接显示「已生成链接」 |
|
||||
|
||||
### B2 安排 / 归档
|
||||
|
||||
| ID | 标题 | 预置 | 步骤 | 预期 |
|
||||
|----|------|------|------|------|
|
||||
| PL-PD-O01 | 可安排显示按钮 | pending 未作废 | — | 「安排运输」 |
|
||||
| PL-PD-O02 | 只读模式无按钮 | readonly=1 | — | 标题「归档详情」;无安排/归档 |
|
||||
| PL-PD-O03 | 可归档显示 | 已安排未归档 | — | 「归档」 |
|
||||
| PL-PD-O04 | 归档-无明细 | 无产品 | 点归档 | Toast「无产品明细,无法归档」 |
|
||||
| PL-PD-O05 | 归档-取消 | — | 取消 | 不变 |
|
||||
| PL-PD-O06 | 归档-确认 | 有明细 | 确认说明写入实际数量 | Toast「已归档」 |
|
||||
| PL-PD-N01 | 无编辑删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## C. 安排运输表单 `pl/plan/arrange`(新增写操作)
|
||||
|
||||
| ID | 字段/用例 | 步骤 | 预期 |
|
||||
|----|-----------|------|------|
|
||||
| PL-AR-D01 | 头副标题 | — | planNo · planCardSub |
|
||||
| PL-AR-D02 | 车辆 picker | — | 「请选择车辆」;必填 |
|
||||
| PL-AR-D03 | 司机 picker | — | 「请选择司机」;必填 |
|
||||
| PL-AR-D04 | 运费方式 | — | 默认「按重量」;可选「按趟次」 |
|
||||
| PL-AR-D05 | 运费单价 | — | 选填 digit |
|
||||
| PL-AR-D06 | 运费金额 | — | 选填 |
|
||||
| PL-AR-D07 | 公里数 | — | 选填 |
|
||||
| PL-AR-V01 | 未选车辆 | 确认 | Toast「请选择车辆」 |
|
||||
| PL-AR-V02 | 未选司机 | 已选车不选司机 | Toast「请选择司机」 |
|
||||
| PL-AR-C01 | 选车辆回显 | 选择 | 显示车牌等 |
|
||||
| PL-AR-C02 | 选司机回显 | 选择 | 显示姓名 |
|
||||
| PL-AR-C03 | 切换运费方式 | — | 回显正确 |
|
||||
| PL-AR-S01 | 确认安排成功 | 合法且业务允许 | Toast「已安排运输」;回详情见安排信息 |
|
||||
| PL-AR-S02 | 证书未齐备拒绝 | 已知业务拒绝 | 有明确错误提示;不白屏(记环境/缺陷) |
|
||||
| PL-AR-N01 | 无删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## D. 计划归档列表 `pl/archive/list`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-AC-K01 | 搜索占位 | — | 「计划号 / 客户」 |
|
||||
| PL-AC-K02 | 按计划号命中 | — | 命中 |
|
||||
| PL-AC-K03 | 按客户命中 | — | 命中 |
|
||||
| PL-AC-K04 | 无命中 | — | 「暂无已归档计划」 |
|
||||
| PL-AC-F01 | planNo / 副标题 | — | 正确 |
|
||||
| PL-AC-F02 | archiveStatus Tag | — | 已归档 |
|
||||
| PL-AC-F03 | planType / planDate | — | 正确 |
|
||||
| PL-AC-F04 | 进只读详情 | 点卡片 | readonly=1;无底栏操作 |
|
||||
| PL-AC-N01 | 无增删改 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## E. 购买证 · 列表 `pl/cert/list`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-CL-K01 | 搜索占位 | — | 「计划号 / 客户」 |
|
||||
| PL-CL-K02~K04 | 计划号/客户/无命中 | — | 命中或「暂无计划」 |
|
||||
| PL-CL-T01 | Tab 文案 | — | 待齐备 / 已齐备 / 全部 |
|
||||
| PL-CL-T02 | 待齐备 | — | hazardCertReady=false |
|
||||
| PL-CL-T03 | 已齐备 | — | true |
|
||||
| PL-CL-T04 | 全部 | — | 两种都可 |
|
||||
| PL-CL-F01 | planNo / 副标题 | — | 正确 |
|
||||
| PL-CL-F02 | 齐备 Tag | — | 待齐备/证书齐备 |
|
||||
| PL-CL-F03 | 购买证 x/y | — | 进度;无产品显示「无产品」 |
|
||||
| PL-CL-F04 | planDate | — | 正确 |
|
||||
| PL-CL-O01 | 「上传证书」 | 点击 | 进证详情 |
|
||||
| PL-CL-N01 | 无删除列表项 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## F. 购买证 · 详情 `pl/cert/detail`(上传=写)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-CD-H01 | Hero | — | planNo、副标题、齐备 Tag |
|
||||
| PL-CD-P01 | 每个产品块 | — | 产品名;已上传/未上传 |
|
||||
| PL-CD-P02 | 已传图预览 | 点图 | 可预览 |
|
||||
| PL-CD-U01 | +拍照上传 | 某产品 | Toast「已上传」;进度+1 |
|
||||
| PL-CD-U02 | 单产品满6张 | 第7次 | 「最多上传6张」 |
|
||||
| PL-CD-U03 | 上传中禁用 | acting | 按钮 disabled |
|
||||
| PL-CD-O01 | 未齐备→标记齐备 | 确认弹窗 | 「已更新」;变已齐备 |
|
||||
| PL-CD-O02 | 已齐备→取消齐备 | 确认 | 「已更新」 |
|
||||
| PL-CD-O03 | 弹窗取消 | — | 状态不变 |
|
||||
| PL-CD-N01 | 无删除单张 UI | — | 仅追加上传(记录实际) |
|
||||
| PL-CD-N02 | 无编辑计划 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## G. 运费结算 · 列表 `pl/freight/list`
|
||||
|
||||
### G1 检索与 Tab
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FL-K01 | 占位 | — | 「结算号 / 计划 / 车牌」 |
|
||||
| PL-FL-K02 | 按结算号 | — | 命中 |
|
||||
| PL-FL-K03 | 按计划 | — | 命中 |
|
||||
| PL-FL-K04 | 按车牌 | — | 命中 |
|
||||
| PL-FL-K05 | 无命中 | — | 对应空态 |
|
||||
| PL-FL-T01 | Tab 结算单/待结算 | — | 文案正确 |
|
||||
| PL-FL-T02 | 结算单空态 | — | 「暂无结算单」 |
|
||||
| PL-FL-T03 | 待结算空态 | — | 「暂无待结算计划」 |
|
||||
|
||||
### G2 结算单卡片字段
|
||||
|
||||
| ID | 字段 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-FL-F01 | settlementNo | 主标题 |
|
||||
| PL-FL-F02 | 车牌摘要 · 计划数 | 副标题 |
|
||||
| PL-FL-F03 | settlementStatus | 草稿/已驳回/审批中/待发放/已发放/已作废 |
|
||||
| PL-FL-F04 | totalFreight | 运费合计 ¥ |
|
||||
| PL-FL-F05 | 草稿/已驳回「详情」 | 进详情 |
|
||||
|
||||
### G3 待结算与生成(增)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FL-W01 | 待结算卡片字段 | — | planNo、客户/承运、运费、「生成结算单」 |
|
||||
| PL-FL-W02 | 生成-取消 | 取消 | 不生成 |
|
||||
| PL-FL-W03 | 生成-确认成功 | 确认「确认根据计划…」 | 「已创建」;可跳转详情 |
|
||||
| PL-FL-W04 | 计划无效 | 异常数据 | Toast「计划无效」 |
|
||||
| PL-FL-N01 | 无编辑删除结算单 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## H. 运费结算 · 详情 `pl/freight/detail`
|
||||
|
||||
### H1 字段逐项
|
||||
|
||||
| ID | 位置 | 字段 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FD-H01 | Hero | settlementNo | |
|
||||
| PL-FD-H02 | Hero | 车牌摘要 | |
|
||||
| PL-FD-H03 | Hero | 状态 Tag | |
|
||||
| PL-FD-H04 | Hero | 运费合计 / 审批人 chip | |
|
||||
| PL-FD-B01 | 结算信息 | 发票号 | |
|
||||
| PL-FD-B02 | 结算信息 | 发票金额 | |
|
||||
| PL-FD-B03 | 结算信息 | 提交人 | |
|
||||
| PL-FD-B04 | 结算信息 | 备注 | |
|
||||
| PL-FD-R01 | 关联计划 | 每行 planNo + 运费 | |
|
||||
| PL-FD-R02 | 关联运输 | 车牌/运单号 + 运费 | |
|
||||
| PL-FD-R03 | 审批流 | 步骤/审批人/结果 | |
|
||||
|
||||
### H2 提交审批(草稿/驳回态)
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FD-C01 | 审批类型选项 | 查看 picker | 或签 / 顺序审批 / 会签 |
|
||||
| PL-FD-C02 | 打开选人 Sheet | 点审批人 | 标题「选择审批人」;搜索「搜索姓名 / 账号」 |
|
||||
| PL-FD-C03 | 搜索审批人 | 输入姓名 | 列表过滤 |
|
||||
| PL-FD-C04 | 多选回显 | 选多人确定 | 「已选 N 人」;详情回显 |
|
||||
| PL-FD-C05 | 未选人点确定 | Sheet 内确定 | Toast「请至少选择一位审批人」 |
|
||||
| PL-FD-C06 | Sheet 取消 | — | 按实现保持原选 |
|
||||
| PL-FD-V01 | 未选审批人提交 | 点提交审批 | Toast「请选择审批人」 |
|
||||
| PL-FD-C07 | 提交说明 | 填 textarea | maxlength≤500;可选 |
|
||||
| PL-FD-S01 | 提交确认摘要 | 已选人提交 | 弹窗含审批类型+名单;成功后状态变 |
|
||||
| PL-FD-S02 | 提交取消 | — | 不变 |
|
||||
|
||||
### H3 审批通过/驳回
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FD-A01 | canApprove 显示按钮 | — | 「驳回」「通过」 |
|
||||
| PL-FD-A02 | 通过-意见可选 | 确认 | 成功;审批流更新 |
|
||||
| PL-FD-A03 | 驳回-意见可选 | 确认 | 成功 |
|
||||
| PL-FD-A04 | 无待审节点 | 异常 | Toast「无待审节点」 |
|
||||
| PL-FD-N01 | 非草稿无提交区 | — | 提交表单隐藏 |
|
||||
| PL-FD-N02 | 无编辑结算字段/无删除 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## I. 运费审批列表 `pl/freight/approval`
|
||||
|
||||
| ID | 标题 | 步骤 | 预期 |
|
||||
|----|------|------|------|
|
||||
| PL-FA-K01 | 搜索占位 | — | 「结算号 / 车牌」 |
|
||||
| PL-FA-K02 | 按结算号 | — | 命中 |
|
||||
| PL-FA-K03 | 按车牌 | — | 命中 |
|
||||
| PL-FA-K04 | 无命中 | — | 「暂无审批单据」 |
|
||||
| PL-FA-T01 | Tab 待我审批 | — | 待我审数据 |
|
||||
| PL-FA-T02 | Tab 已审批 | — | 已审数据 |
|
||||
| PL-FA-F01 | settlementNo | — | 主标题 |
|
||||
| PL-FA-F02 | 车牌·提交人 | — | 副标题 |
|
||||
| PL-FA-F03 | 状态 / 运费合计 | — | 正确 |
|
||||
| PL-FA-O01 | canApprove「去审批」 | 点击 | 进详情可审 |
|
||||
| PL-FA-O02 | 非 canApprove「详情」 | 点击 | 进详情 |
|
||||
| PL-FA-N01 | 列表无直接删改 | — | **无** |
|
||||
|
||||
---
|
||||
|
||||
## J. 运输灰卡
|
||||
|
||||
| ID | 菜单 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-WB-01 | 财务发放 | Toast「本期暂未开放」 |
|
||||
| PL-WB-02 | 车辆充值 | 同上 |
|
||||
| PL-WB-03 | 车辆维修 | 同上 |
|
||||
| PL-WB-04 | 运输地址库 | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
| 批次 | 日期 | 结果 | 备注 |
|
||||
|------|------|------|------|
|
||||
| — | — | — | 文字用例阶段 |
|
||||
|
||||
|
||||
## 附录:加厚用例(Tab×搜索矩阵 / 安排边界 / 证书 / 结算审批组合)
|
||||
|
||||
### 运输计划 — Tab×搜索
|
||||
|
||||
| ID | Tab | 关键字 | 预期 |
|
||||
|----|-----|--------|------|
|
||||
| PL-PL-X001 | 待发运 | 计划号 | 命中待发运 |
|
||||
| PL-PL-X002 | 在途 | 计划号 | 命中在途或空 |
|
||||
| PL-PL-X003 | 已完成 | 计划号 | 命中完成或空 |
|
||||
| PL-PL-X004 | 待发运 | 客户 | 命中 |
|
||||
| PL-PL-X005 | 待发运 | 承运 | 命中 |
|
||||
| PL-PL-X006 | 在途 | 客户 | 命中 |
|
||||
| PL-PL-X007 | 已完成 | 客户 | 命中 |
|
||||
| PL-PL-X008 | 待发运 | 无意义 | 空 |
|
||||
| PL-PL-X009 | 在途 | 无意义 | 空 |
|
||||
| PL-PL-X010 | 已完成 | 无意义 | 空 |
|
||||
| PL-PL-X011 | 待发运有安排按钮 | 卡片 | 有安排运输 |
|
||||
| PL-PL-X012 | 在途无安排按钮 | 卡片 | 无 |
|
||||
| PL-PL-X013 | 已完成无安排按钮 | 卡片 | 无 |
|
||||
| PL-PL-X014 | 加载更多两次 | — | 条数递增或已全部 |
|
||||
| PL-PL-X015 | 详情与安排往返 | — | 数据不串 |
|
||||
|
||||
### 安排运输边界
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-AR-X001 | 车辆列表空 | 无法选;确认提示请选择车辆 |
|
||||
| PL-AR-X002 | 司机列表空 | 请选择司机 |
|
||||
| PL-AR-X003 | 只选车不选司机 | 请选择司机 |
|
||||
| PL-AR-X004 | 运费方式切按趟次 | 回显按趟次 |
|
||||
| PL-AR-X005 | 单价金额公里都填 | 可提交 |
|
||||
| PL-AR-X006 | 单价金额公里都空 | 可提交(选填) |
|
||||
| PL-AR-X007 | 金额负数 | 按实现拒绝或仍提交(记) |
|
||||
| PL-AR-X008 | 确认安排连点 | 不重复安排 |
|
||||
| PL-AR-X009 | 成功后详情运输块 | 车牌司机可见 |
|
||||
| PL-AR-X010 | 业务拒绝文案可读 | 证书未齐等 |
|
||||
|
||||
### 详情归档边界
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-PD-X001 | 未安排无归档 | 无归档按钮 |
|
||||
| PL-PD-X002 | 已归档只读 | 归档详情无底栏 |
|
||||
| PL-PD-X003 | 无明细归档 | Toast 无产品明细 |
|
||||
| PL-PD-X004 | 有明细归档成功 | 已归档 |
|
||||
| PL-PD-X005 | 产品明细多行 | 每行名+量+单位 |
|
||||
| PL-PD-X006 | 联系人电话拼接 | 正确 |
|
||||
| PL-PD-X007 | 取送货地址长文 | 可展示 |
|
||||
|
||||
### 归档列表
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-AC-X001 | 搜索计划号 | 命中 |
|
||||
| PL-AC-X002 | 搜索客户 | 命中 |
|
||||
| PL-AC-X003 | 无结果 | 暂无已归档计划 |
|
||||
| PL-AC-X004 | 进只读再返回 | 正常 |
|
||||
| PL-AC-X005 | 只读页尝试找安排按钮 | 无 |
|
||||
|
||||
### 购买证 Tab×搜索×上传
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-CL-X001 | 待齐备过滤 | 仅未齐备 |
|
||||
| PL-CL-X002 | 已齐备过滤 | 仅齐备 |
|
||||
| PL-CL-X003 | 全部混合 | 两种都有 |
|
||||
| PL-CL-X004 | 待齐备×计划号 | 交集 |
|
||||
| PL-CL-X005 | 已齐备×客户 | 交集 |
|
||||
| PL-CL-X006 | 进度 0/n | 显示 |
|
||||
| PL-CL-X007 | 进度 n/n | 显示 |
|
||||
| PL-CL-X008 | 无产品文案 | 「无产品」 |
|
||||
| PL-CD-X001 | 产品A上传1张 | 已上传 |
|
||||
| PL-CD-X002 | 产品B未传 | 未上传 |
|
||||
| PL-CD-X003 | 产品A满6 | 第7拒绝 |
|
||||
| PL-CD-X004 | 标记齐备后列表 Tab | 已齐备可见 |
|
||||
| PL-CD-X005 | 取消齐备后回待齐备 | 可见 |
|
||||
| PL-CD-X006 | 上传失败 | Toast |
|
||||
| PL-CD-X007 | 预览多图切换 | 可用 |
|
||||
|
||||
### 运费结算 Tab×搜索×生成
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-FL-X001 | 结算单×结算号 | 命中 |
|
||||
| PL-FL-X002 | 结算单×车牌 | 命中 |
|
||||
| PL-FL-X003 | 待结算×计划号 | 命中 |
|
||||
| PL-FL-X004 | 结算单空 | 暂无结算单 |
|
||||
| PL-FL-X005 | 待结算空 | 暂无待结算计划 |
|
||||
| PL-FL-X006 | 生成成功跳详情 | 有 settlementNo |
|
||||
| PL-FL-X007 | 生成后待结算减少 | 列表更新 |
|
||||
| PL-FL-X008 | 状态 Tag 抽样 | 各状态可显示 |
|
||||
| PL-FL-X009 | 草稿显示详情按钮 | 有 |
|
||||
| PL-FL-X010 | 非草稿详情入口 | 按实现可点卡片 |
|
||||
|
||||
### 结算详情 — 选人与审批组合
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-FD-X001 | 审批类型或签提交 | 成功 |
|
||||
| PL-FD-X002 | 审批类型顺序提交 | 成功 |
|
||||
| PL-FD-X003 | 审批类型会签提交 | 成功 |
|
||||
| PL-FD-X004 | 选1人提交 | 成功 |
|
||||
| PL-FD-X005 | 选多人提交 | 成功 |
|
||||
| PL-FD-X006 | 搜索无审批人 | Sheet 空列表 |
|
||||
| PL-FD-X007 | 搜索命中审批人 | 可勾选 |
|
||||
| PL-FD-X008 | 提交说明500字 | 可提交 |
|
||||
| PL-FD-X009 | 提交说明超长 | 截断或无法输入 |
|
||||
| PL-FD-X010 | 通过意见空 | 成功 |
|
||||
| PL-FD-X011 | 通过意见有值 | 成功 |
|
||||
| PL-FD-X012 | 驳回意见空 | 成功 |
|
||||
| PL-FD-X013 | 驳回意见有值 | 成功 |
|
||||
| PL-FD-X014 | 发票号空展示 | — |
|
||||
| PL-FD-X015 | 关联计划多条 | 都显示 |
|
||||
| PL-FD-X016 | 关联运输多条 | 都显示 |
|
||||
| PL-FD-X017 | 审批流多步 | 都显示 |
|
||||
| PL-FD-X018 | 非草稿隐藏提交区 | 隐藏 |
|
||||
| PL-FD-X019 | 非审批人无通过驳回 | 无 |
|
||||
|
||||
### 运费审批列表
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-FA-X001 | 待我审批×结算号 | 命中 |
|
||||
| PL-FA-X002 | 已审批×车牌 | 命中 |
|
||||
| PL-FA-X003 | 待我审批空 | 暂无审批单据 |
|
||||
| PL-FA-X004 | 已审批空 | 暂无审批单据 |
|
||||
| PL-FA-X005 | 去审批进详情可操作 | 有通过驳回 |
|
||||
| PL-FA-X006 | 详情只读入口 | 无通过驳回或不可 |
|
||||
|
||||
### 运输灰卡与回归
|
||||
|
||||
| ID | 标题 | 预期 |
|
||||
|----|------|------|
|
||||
| PL-WB-X001 | 四灰卡 | 均暂未开放 |
|
||||
| PL-RG-X001 | 安排后待发运→在途 | Tab 迁移 |
|
||||
| PL-RG-X002 | 归档后归档列表可见 | 可见 |
|
||||
| PL-RG-X003 | 证书齐备后可安排(业务) | 按规则 |
|
||||
| PL-RG-X004 | 生成结算→提交→待我审批可见 | 链路通 |
|
||||
@@ -0,0 +1,314 @@
|
||||
# 按钮与弹窗总清单(门禁)
|
||||
|
||||
> 依据:`docs/superpowers/specs/2026-07-18-mp-button-dialog-cases-design.md`
|
||||
> 组织方式:**C**(总清单 + 模块用例文件)
|
||||
> 盘点方式:源码静态扫描 `miniprogram/pages/**` + `components/mp-approver-picker`
|
||||
> **完成标准**:本表「用例 ID」列无 `TODO`
|
||||
> `showModal` 系统确认键文案一般为「确定」;取消为「取消」(以 H5/真机为准)
|
||||
|
||||
**图例**
|
||||
|
||||
| 弹出层类型 | 含义 |
|
||||
|------------|------|
|
||||
| 无 | 点击直接导航/请求/Toast |
|
||||
| 自定义弹窗 | 页面内 v-if 弹层 |
|
||||
| uni.showModal | 系统确认框 |
|
||||
| uni.showActionSheet | 系统选项表 |
|
||||
| Sheet | 如 MpApproverPicker |
|
||||
|
||||
有弹层时,**触发行** + **层内每个按钮各一行**。最小集见 design §6。
|
||||
|
||||
---
|
||||
|
||||
## 1. 壳层
|
||||
|
||||
### 1.1 登录 `pages/login/login`
|
||||
|
||||
| INV-ID | 触发控件 | 控件类型 | 弹出层 | 弹层标题 | 层内按钮(本行) | 显示条件 | 用例 ID |
|
||||
|--------|----------|----------|--------|----------|---------------|----------|---------|
|
||||
| INV-LGN-01 | 账号密码登录 | 主按钮 | 自定义弹窗(未勾选时) | 协议确认 | —(触发行) | 始终 | TODO → 需:出现弹层 |
|
||||
| INV-LGN-02 | (协议弹窗) | 弹窗按钮 | 自定义弹窗 | 协议确认 | 否 | 弹层可见 | TODO → 需:取消 |
|
||||
| INV-LGN-03 | (协议弹窗) | 弹窗按钮 | 自定义弹窗 | 协议确认 | 是,继续登录 | 弹层可见 | TODO → 需:确认继续 |
|
||||
| INV-LGN-04 | (协议弹窗遮罩) | 遮罩 | 自定义弹窗 | 协议确认 | 点遮罩 | 弹层可见 | TODO → 需:遮罩=否 |
|
||||
| INV-LGN-05 | 协议勾选框 | 勾选 | 无 | — | — | 始终 | TODO |
|
||||
| INV-LGN-06 | 《用户服务协议》 | 链接 | 无(跳转) | — | — | 始终 | LGN-P09 类 |
|
||||
| INV-LGN-07 | 《隐私政策》 | 链接 | 无(跳转) | — | — | 始终 | LGN-P10 类 |
|
||||
| INV-LGN-08 | 账号密码登录 | 主按钮 | 无(已勾选) | — | — | 已勾选协议 | LGN-V* 类 |
|
||||
|
||||
### 1.2 工作台 `pages/workbench/workbench`
|
||||
|
||||
| INV-ID | 触发控件 | 控件类型 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-WB-01~21 | 各启用菜单卡 | 菜单卡 | 无 | — | — | enabled | WB-E / WB-X* |
|
||||
| INV-WB-G01~10 | 各灰卡菜单 | 菜单卡 | 无(Toast) | — | — | enabled=false | WB-G* / 本期暂未开放 |
|
||||
|
||||
灰卡名单:产品分类、产品管理、合同审批、借款发放、全部单据、付款记录、财务发放、车辆充值、车辆维修、运输地址库。
|
||||
|
||||
### 1.3 首页 / 待办 / 我的
|
||||
|
||||
| INV-ID | 界面 | 触发控件 | 弹出层 | 层内按钮 | 用例 ID |
|
||||
|--------|------|----------|--------|----------|---------|
|
||||
| INV-HOME-01 | 首页 | 前往工作台 | 无 | — | HOME-04 |
|
||||
| INV-MINE-01 | 我的 | 保存修改 | 无(Toast) | — | MINE-B01 |
|
||||
| INV-MINE-02 | 我的 | 退出登录 | 无 | — | MINE-B03 |
|
||||
| INV-TODO-01 | 待办 Tab | (无业务按钮) | — | — | TODO-0* |
|
||||
|
||||
---
|
||||
|
||||
## 2. CRM
|
||||
|
||||
### 2.1 线索列表 `crm/clue/list`
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|---------|
|
||||
| INV-CRM-CL-01 | ← 工作台 | 无 | — | 始终 | CRM-CL-004 |
|
||||
| INV-CRM-CL-02 | 新增 | 无 | — | 始终 | CRM-CF-001 |
|
||||
| INV-CRM-CL-03 | 整卡点击 | 无 | — | 有数据 | CRM-CL-320 |
|
||||
|
||||
### 2.2 线索新增 `crm/clue/form`
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 层内按钮 | 用例 ID |
|
||||
|--------|----------|--------|----------|---------|
|
||||
| INV-CRM-CF-01 | ← 返回 | 无 | — | CRM-CF-005 |
|
||||
| INV-CRM-CF-02 | 取消 | 无 | — | CRM-CF-006 |
|
||||
| INV-CRM-CF-03 | 保存 | 无 | — | CRM-CF-080 等 |
|
||||
|
||||
### 2.3 线索详情 `crm/clue/detail`
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮(本行) | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|---------------|----------|---------|
|
||||
| INV-CRM-CD-01 | ← 返回 | 无 | — | — | 始终 | CRM-CD-003 |
|
||||
| INV-CRM-CD-02 | 跟进 | 无(内嵌表单) | — | — | 始终 | CRM-CD-061 |
|
||||
| INV-CRM-CD-03 | (跟进区)取消 | 无 | — | — | 表单展开 | CRM-CD-068 |
|
||||
| INV-CRM-CD-04 | (跟进区)提交跟进 | 无 | — | — | 表单展开 | CRM-CD-072 |
|
||||
| INV-CRM-CD-05 | 转客户 | showModal | 转化为客户 | —触发行 | 非 converted/invalid | TODO 出现 |
|
||||
| INV-CRM-CD-06 | (转客户弹窗) | showModal | 转化为客户 | 取消 | 同上 | CRM-CD-112 |
|
||||
| INV-CRM-CD-07 | (转客户弹窗) | showModal | 转化为客户 | 确定 | 同上 | CRM-CD-113 |
|
||||
| INV-CRM-CD-08 | 无效 | showModal | 标记无效 | —触发行 | 非 converted/invalid | TODO 出现 |
|
||||
| INV-CRM-CD-09 | (无效弹窗) | showModal | 标记无效 | 取消 | 同上 | CRM-CD-121 |
|
||||
| INV-CRM-CD-10 | (无效弹窗) | showModal | 标记无效 | 确定 | 同上 | CRM-CD-122 |
|
||||
|
||||
### 2.4 商机/客户详情
|
||||
|
||||
| INV-ID | 界面 | 触发控件 | 弹出层 | 层内按钮 | 用例 ID |
|
||||
|--------|------|----------|--------|----------|---------|
|
||||
| INV-CRM-OD-01 | 商机详情 | ← 返回 | 无 | — | CRM-OD-041 |
|
||||
| INV-CRM-OD-02 | 商机详情 | 写跟进 | 无(内嵌) | — | CRM-OD-030 |
|
||||
| INV-CRM-OD-03 | 商机详情 | (表单)取消 | 无 | — | CRM-OD-032 |
|
||||
| INV-CRM-OD-04 | 商机详情 | (表单)提交 | 无 | — | CRM-OD-033 |
|
||||
| INV-CRM-CUD-01 | 客户详情 | ← 返回 | 无 | — | CRM-CUD-041 |
|
||||
| INV-CRM-CUD-02 | 客户详情 | 写跟进 | 无(内嵌) | — | CRM-CUD-030 |
|
||||
| INV-CRM-CUD-03 | 客户详情 | 取消 / 提交 | 无 | — | CRM-CUD-031/032 |
|
||||
|
||||
### 2.5 公海池 `crm/pool/list`
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|---------|
|
||||
| INV-CRM-PL-01 | ← 工作台 | 无 | — | — | TODO |
|
||||
| INV-CRM-PL-02 | 领取 | showModal | 领取客户 | —触发行 | TODO 出现 |
|
||||
| INV-CRM-PL-03 | (领取弹窗)取消 | showModal | 领取客户 | 取消 | CRM-PL-032 |
|
||||
| INV-CRM-PL-04 | (领取弹窗)确定 | showModal | 领取客户 | 确定 | CRM-PL-033 |
|
||||
| INV-CRM-PL-05 | 卡片标题区 | 无 | — | — | CRM-PL-040/041 |
|
||||
|
||||
### 2.6 报价/合同(只读)
|
||||
|
||||
| INV-ID | 界面 | 触发控件 | 弹出层 | 用例 ID |
|
||||
|--------|------|----------|--------|---------|
|
||||
| INV-CRM-QL-01 | 报价列表 | ← 工作台 / 整卡 | 无 | CRM-QL-* |
|
||||
| INV-CRM-QD-01 | 报价详情 | ← 返回 | 无 | CRM-QD-022 |
|
||||
| INV-CRM-CTL-01 | 合同列表 | ← 工作台 / 整卡 | 无 | CRM-CTL-* |
|
||||
| INV-CRM-CTD-01 | 合同详情 | ← 返回 | 无 | CRM-CTD-022 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 报销
|
||||
|
||||
### 3.1 待办 `bx/todo/list`
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-BX-TD-01 | ← 工作台 | 无 | — | — | 始终 | TODO |
|
||||
| INV-BX-TD-02 | + 申请 | ActionSheet | 系统 | —触发行 | 始终 | TODO 出现 Sheet |
|
||||
| INV-BX-TD-03 | (Sheet)报销申请 | ActionSheet | — | item:报销申请 | Sheet 开 | BX-TD-A03 |
|
||||
| INV-BX-TD-04 | (Sheet)借款申请 | ActionSheet | — | item:借款申请 | Sheet 开 | BX-TD-A04 |
|
||||
| INV-BX-TD-05 | 通过 | showModal | 确认通过 | —触发行 | Tab=待审批 | TODO 出现 |
|
||||
| INV-BX-TD-06 | (通过弹窗)取消 | showModal | 确认通过 | 取消 | 同上 | BX-TD-O01 |
|
||||
| INV-BX-TD-07 | (通过弹窗)确定 | showModal | 确认通过 | 确定 | 同上 | BX-TD-O02 |
|
||||
| INV-BX-TD-08 | 驳回 | showModal | 确认驳回 | —触发行 | Tab=待审批 | TODO 出现 |
|
||||
| INV-BX-TD-09 | (驳回弹窗)取消 | showModal | 确认驳回 | 取消 | 同上 | BX-TD-O03 |
|
||||
| INV-BX-TD-10 | (驳回弹窗)确定 | showModal | 确认驳回 | 确定(+可选意见) | 同上 | BX-TD-O04 |
|
||||
| INV-BX-TD-11 | 整卡 | 无 | — | — | 有数据 | BX-TD-F07 |
|
||||
|
||||
### 3.2 报销申请列表 / 新建
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 层内按钮/项 | 用例 ID |
|
||||
|--------|----------|--------|-------------|---------|
|
||||
| INV-BX-RL-01 | ← 工作台 | 无 | — | TODO |
|
||||
| INV-BX-RL-02 | + 申请 | 无 | — | BX-RL-N01 |
|
||||
| INV-BX-RL-03 | 整卡 | 无 | — | BX-RL-F06 |
|
||||
| INV-BX-RF-01 | ← 返回 | 无 | — | TODO |
|
||||
| INV-BX-RF-02 | 费用项目(点击选择) | ActionSheet | 动态费用项 | BX-RF-C01 / V06 |
|
||||
| INV-BX-RF-03 | + 拍照 | 无(系统相册/相机) | — | BX-RF-C04 |
|
||||
| INV-BX-RF-04 | 删附件 × | 无 | — | BX-RF-C05 |
|
||||
| INV-BX-RF-05 | 存草稿 | 无 | — | BX-RF-S01 |
|
||||
| INV-BX-RF-06 | 提交审批 | 无 | — | BX-RF-S02 |
|
||||
|
||||
### 3.3 报销详情
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-BX-RD-01 | ← 返回 | 无 | — | — | 始终 | TODO |
|
||||
| INV-BX-RD-02 | 提交审批 | showModal | 提交审批 | —触发行 | draft | TODO 出现 |
|
||||
| INV-BX-RD-03 | (提交弹窗)取消 | showModal | 提交审批 | 取消 | draft | BX-RD-O02 |
|
||||
| INV-BX-RD-04 | (提交弹窗)确定 | showModal | 提交审批 | 确定 | draft | BX-RD-O01 |
|
||||
| INV-BX-RD-05 | 通过 | showModal | 审批通过 | —触发行 | pending | TODO 出现 |
|
||||
| INV-BX-RD-06 | (通过弹窗)取消 | showModal | 审批通过 | 取消 | pending | TODO |
|
||||
| INV-BX-RD-07 | (通过弹窗)确定 | showModal | 审批通过 | 确定(+意见) | pending | BX-RD-O03 |
|
||||
| INV-BX-RD-08 | 驳回 | showModal | 审批驳回 | —触发行 | pending | TODO 出现 |
|
||||
| INV-BX-RD-09 | (驳回弹窗)取消 | showModal | 审批驳回 | 取消 | pending | TODO |
|
||||
| INV-BX-RD-10 | (驳回弹窗)确定 | showModal | 审批驳回 | 确定(+意见) | pending | BX-RD-O04 |
|
||||
| INV-BX-RD-11 | 票据预览 | 无(previewImage) | — | — | 有图 | BX-RD-I01 |
|
||||
|
||||
### 3.4 借款列表 / 新建 / 详情 / 审批列表 / 我的单据
|
||||
|
||||
| INV-ID | 界面 | 触发控件 | 弹出层 | 层内要点 | 用例 ID |
|
||||
|--------|------|----------|--------|----------|---------|
|
||||
| INV-BX-AL-01 | 借款列表 | + 申请 / 整卡 / ←工作台 | 无 | — | BX-AL-* |
|
||||
| INV-BX-AF-01 | 借款新建 | 费用项目 ActionSheet | ActionSheet | 动态项 | BX-AF-* |
|
||||
| INV-BX-AF-02 | 借款新建 | + 拍照 / 存草稿 / 提交审批 | 无 | — | BX-AF-* |
|
||||
| INV-BX-AD-01 | 借款详情 | 提交审批 | showModal「提交审批」 | 取消\|确定 | TODO 拆全 |
|
||||
| INV-BX-AD-02 | 借款详情 | 通过/驳回 | showModal 审批通过/驳回 | 取消\|确定 | TODO 拆全 |
|
||||
| INV-BX-RA-01 | 报销审批列表 | 通过 | showModal「通过」 | 取消\|确定 | BX-RA-O01 |
|
||||
| INV-BX-RA-02 | 报销审批列表 | 驳回 | showModal「驳回」 | 取消\|确定(+意见) | BX-RA-O02 |
|
||||
| INV-BX-AA-01 | 借款审批列表 | 通过/驳回 | 同左 | 同上 | BX-AA-* |
|
||||
| INV-BX-MN-01 | 我的单据 | ←工作台 / 整卡 | 无 | — | BX-MN-* |
|
||||
|
||||
---
|
||||
|
||||
## 4. 地磅
|
||||
|
||||
### 4.1 仪表盘
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 用例 ID |
|
||||
|--------|----------|--------|---------|
|
||||
| INV-DB-DS-01 | ← 工作台 | 无 | DB-DS-A02 |
|
||||
| INV-DB-DS-02 | 快捷:地磅单管理 | 无 | DB-DS-Q01 |
|
||||
| INV-DB-DS-03 | 快捷:地磅计划 | 无 | DB-DS-Q02 |
|
||||
| INV-DB-DS-04 | 快捷:出入库单 | 无 | DB-DS-Q03 |
|
||||
|
||||
### 4.2 计划列表 / 详情
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|---------|
|
||||
| INV-DB-PL-01 | ← 工作台 / 整卡 | 无 | — | — | DB-PL-* |
|
||||
| INV-DB-PD-01 | ← 返回 | 无 | — | — | DB-PD-N 返回 |
|
||||
| INV-DB-PD-02 | 关联出入库卡片 | 无 | — | — | DB-PD-I02 |
|
||||
| INV-DB-PD-03 | 关联地磅单卡片 | 无 | — | — | DB-PD-W02 |
|
||||
| INV-DB-PD-04 | 生成出入库关联单 | showModal | 生成出入库单 | —触发行 | TODO 出现 |
|
||||
| INV-DB-PD-05 | (弹窗)取消 | showModal | 生成出入库单 | 取消 | DB-PD-O02 |
|
||||
| INV-DB-PD-06 | (弹窗)确定 | showModal | 生成出入库单 | 确定 | DB-PD-O03 |
|
||||
|
||||
### 4.3 地磅单列表 / 详情
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-DB-WL-01 | ← 工作台 / 整卡 / 二次过磅样式 | 无 | — | — | — | DB-WL-* |
|
||||
| INV-DB-WD-01 | ← 返回 | 无 | — | — | 始终 | TODO |
|
||||
| INV-DB-WD-02 | 二次过磅 | 无(内嵌表单) | — | — | 一次过磅 | DB-WD-M01 |
|
||||
| INV-DB-WD-03 | (表单)取消 | 无 | — | — | 表单开 | DB-WD-M07 |
|
||||
| INV-DB-WD-04 | (表单)确认保存 | 无 | — | — | 表单开 | DB-WD-M06 |
|
||||
| INV-DB-WD-05 | 作废 | showModal | 作废地磅单 | —触发行 | 当天未作废 | TODO 出现 |
|
||||
| INV-DB-WD-06 | (作废)取消 | showModal | 作废地磅单 | 取消 | 同上 | DB-WD-D02 |
|
||||
| INV-DB-WD-07 | (作废)确定 | showModal | 作废地磅单 | 确定 | 同上 | DB-WD-D03 |
|
||||
|
||||
### 4.4 出入库
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 用例 ID |
|
||||
|--------|----------|--------|---------|
|
||||
| INV-DB-IL-01 | ← 工作台 / 整卡 | 无 | DB-IL-* |
|
||||
| INV-DB-ID-01 | ← 返回 | 无 | TODO |
|
||||
| INV-DB-ID-02 | 关联地磅单 | 无(或 Toast) | DB-ID-L01/L02 |
|
||||
| INV-DB-ID-03 | 关联计划 | 无(或 Toast) | DB-ID-L03/L04 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 运输
|
||||
|
||||
### 5.1 计划列表 / 详情 / 安排
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-PL-PL-01 | ← 工作台 | 无 | — | — | 始终 | TODO |
|
||||
| INV-PL-PL-02 | 详情 | 无 | — | — | 有数据 | PL-PL-O01 |
|
||||
| INV-PL-PL-03 | 安排运输 | 无 | — | — | 待发运未取消 | PL-PL-O02 |
|
||||
| INV-PL-PL-04 | 加载更多 | 无 | — | — | 有下一页 | PL-PL-P01 |
|
||||
| INV-PL-PD-01 | ← 返回 | 无 | — | — | 始终 | TODO |
|
||||
| INV-PL-PD-02 | 安排运输 | 无 | — | — | 可安排 | PL-PD-O01 |
|
||||
| INV-PL-PD-03 | 归档 | showModal | 确认归档 | —触发行 | 可归档 | TODO 出现 |
|
||||
| INV-PL-PD-04 | (归档)取消 | showModal | 确认归档 | 取消 | 同上 | PL-PD-O05 |
|
||||
| INV-PL-PD-05 | (归档)确定 | showModal | 确认归档 | 确定 | 同上 | PL-PD-O06 |
|
||||
| INV-PL-AR-01 | 确认安排 | 无 | — | — | 安排页 | PL-AR-S01 |
|
||||
|
||||
### 5.2 归档 / 购买证
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|---------|
|
||||
| INV-PL-AC-01 | ← 工作台 / 整卡 | 无 | — | — | PL-AC-* |
|
||||
| INV-PL-CL-01 | ← 工作台 | 无 | — | — | TODO |
|
||||
| INV-PL-CL-02 | 上传证书 | 无 | — | — | PL-CL-O01 |
|
||||
| INV-PL-CD-01 | ← 返回 | 无 | — | — | TODO |
|
||||
| INV-PL-CD-02 | + 拍照 | 无(选图上传) | — | — | PL-CD-U01 |
|
||||
| INV-PL-CD-03 | 标记证书齐备 | showModal | 标记证书齐备 | —触发行 | TODO 出现 |
|
||||
| INV-PL-CD-04 | (齐备)取消 | showModal | 标记证书齐备 | 取消 | PL-CD-O03 |
|
||||
| INV-PL-CD-05 | (齐备)确定 | showModal | 标记证书齐备 | 确定 | PL-CD-O01 |
|
||||
| INV-PL-CD-06 | 取消齐备 | showModal | 取消齐备 | —触发行 | TODO 出现 |
|
||||
| INV-PL-CD-07 | (取消齐备)取消 | showModal | 取消齐备 | 取消 | PL-CD-O03 |
|
||||
| INV-PL-CD-08 | (取消齐备)确定 | showModal | 取消齐备 | 确定 | PL-CD-O02 |
|
||||
|
||||
### 5.3 运费结算 / 详情 / 审批
|
||||
|
||||
| INV-ID | 触发控件 | 弹出层 | 弹层标题 | 层内按钮 | 显示条件 | 用例 ID |
|
||||
|--------|----------|--------|----------|----------|----------|---------|
|
||||
| INV-PL-FL-01 | ← 工作台 | 无 | — | — | 始终 | TODO |
|
||||
| INV-PL-FL-02 | 生成结算单 | showModal | 生成结算单 | —触发行 | 待结算 Tab | TODO 出现 |
|
||||
| INV-PL-FL-03 | (生成)取消 | showModal | 生成结算单 | 取消 | 同上 | PL-FL-W02 |
|
||||
| INV-PL-FL-04 | (生成)确定 | showModal | 生成结算单 | 确定 | 同上 | PL-FL-W03 |
|
||||
| INV-PL-FL-05 | 详情 | 无 | — | — | 结算单 | PL-FL-F05 |
|
||||
| INV-PL-FD-01 | ← 返回 | 无 | — | — | 始终 | TODO |
|
||||
| INV-PL-FD-02 | 审批人(点击选择) | Sheet | 选择审批人 | —触发行 | 可提交态 | PL-FD-C02 |
|
||||
| INV-PL-FD-03 | (Sheet)取消 | Sheet | 选择审批人 | 取消 | Sheet 开 | PL-FD-C06 |
|
||||
| INV-PL-FD-04 | (Sheet)确定 | Sheet | 选择审批人 | 确定 | Sheet 开 | PL-FD-C04/C05 |
|
||||
| INV-PL-FD-05 | (Sheet)遮罩 | Sheet | 选择审批人 | 点遮罩关闭 | Sheet 开 | TODO |
|
||||
| INV-PL-FD-06 | (Sheet)用户行勾选 | Sheet | 选择审批人 | 点行切换选中 | Sheet 开 | TODO |
|
||||
| INV-PL-FD-07 | 提交审批 | showModal | 提交审批 | —触发行 | 已选审批人 | TODO 出现 |
|
||||
| INV-PL-FD-08 | (提交)取消 | showModal | 提交审批 | 取消 | 同上 | PL-FD-S02 |
|
||||
| INV-PL-FD-09 | (提交)确定 | showModal | 提交审批 | 确定 | 同上 | PL-FD-S01 |
|
||||
| INV-PL-FD-10 | 通过 | showModal | (审批通过类) | 取消\|确定(+意见) | canApprove | PL-FD-A02 拆全 |
|
||||
| INV-PL-FD-11 | 驳回 | showModal | (审批驳回类) | 取消\|确定(+意见) | canApprove | PL-FD-A03 拆全 |
|
||||
| INV-PL-FA-01 | ← 工作台 | 无 | — | — | 始终 | TODO |
|
||||
| INV-PL-FA-02 | 去审批 | 无 | — | — | canApprove | PL-FA-O01 |
|
||||
| INV-PL-FA-03 | 详情 | 无 | — | — | 非 canApprove | PL-FA-O02 |
|
||||
|
||||
---
|
||||
|
||||
## 6. TODO 统计与下一步
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| 已回链 | 用例 ID 列已有模块用例编号 |
|
||||
| TODO | 尚未写成「出现/取消/确认」完整三条,或导航返回未单独编号 |
|
||||
|
||||
**下一步(补全用例,仍不执行点测)**
|
||||
|
||||
1. 按界面处理所有 `TODO`:在对应 `00–04` 文件增加「按钮与弹窗」专节。
|
||||
2. 建议优先顺序:登录协议弹窗 → 线索转客户/无效 → 公海领取 → 报销待办通过驳回 → 地磅作废/生成出入库 → 运输归档/证书/结算 Sheet。
|
||||
3. 每补完一界面,把本表对应行从 `TODO` 改为正式 ID。
|
||||
4. 本表无 `TODO` 后,再进入第二步执行。
|
||||
|
||||
---
|
||||
|
||||
## 7. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-18 | 初版:design 确认后源码盘点生成 |
|
||||
@@ -0,0 +1,56 @@
|
||||
# 小程序测试用例总册(测试工程师级)
|
||||
|
||||
## 写作标准(必须遵守)
|
||||
|
||||
写用例时按人类测试工程师做功能测试的粒度,**禁止**只写「搜索可用」「详情正常」这类笼统条目。
|
||||
|
||||
| 类型 | 要求 |
|
||||
|------|------|
|
||||
| 检索/筛选 | **每一个** Tab、**每一个**搜索框、关键词命中/不命中/清空,各写独立用例 |
|
||||
| 列表展示 | 卡片上**每一个**展示字段单独或成组可核对(有数据时逐字段对照接口/造数) |
|
||||
| 详情 | **每一个**区块、**每一个** label 字段都要核对;空值显示「—」 |
|
||||
| 新增 | **每一个**必填校验单独测;选填可填可空;提交成功/失败 |
|
||||
| 修改 | 有入口则测全字段保存;**无入口则写「确认无编辑」负向用例** |
|
||||
| 删除/作废/无效 | 有则测确认/取消/成功;**无则写「确认无删除」** |
|
||||
| 状态按钮 | 按状态矩阵测显示/隐藏 |
|
||||
| 上传 | 上限、删除缩略图、超限提示 |
|
||||
|
||||
## 两阶段流程
|
||||
|
||||
1. **第一步**:完善文字用例(含按钮/弹窗门禁)→ 你评审
|
||||
2. **第二步**:按界面逐条执行(点测或脚本)→ 记缺陷 → 修复 → 回归 → 下一界面
|
||||
|
||||
## 按钮与弹窗门禁(方式 C)
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [BUTTON-DIALOG-INVENTORY.md](./BUTTON-DIALOG-INVENTORY.md) | 总清单:每个触发按钮 + 弹层内每个按钮一行;`TODO` 清零才算写完整 |
|
||||
| [设计说明](../../docs/superpowers/specs/2026-07-18-mp-button-dialog-cases-design.md) | 字段、最小用例集、DoD |
|
||||
|
||||
有弹层时最小集:出现 → 取消 → 确认(+ 遮罩/ActionSheet 每项/Sheet 确定取消)。
|
||||
|
||||
## 文档索引与规模
|
||||
|
||||
| 文档 | 模块 | 独立用例 ID(约) |
|
||||
|------|------|------------------|
|
||||
| [00-shell.md](./00-shell.md) | 登录 / Tab / 工作台 / 我的 | ~150 |
|
||||
| [01-crm.md](./01-crm.md) | CRM(含检索×Tab 矩阵、字段、新增、跟进、状态) | ~410 |
|
||||
| [02-bx.md](./02-bx.md) | 报销(含附录加厚) | ~220 |
|
||||
| [03-db.md](./03-db.md) | 地磅(含附录加厚) | ~210 |
|
||||
| [04-pl.md](./04-pl.md) | 运输(含附录加厚) | ~250 |
|
||||
|
||||
**合计约 1200+ 条**(功能/字段级)。按钮弹窗专节仍有清单 `TODO` 待回填到各模块后才算门禁通过。缺造数执行时标 `Blocked`。
|
||||
|
||||
## 能力总览(避免误测「不存在的修改/删除」)
|
||||
|
||||
| 模块 | 新增 | 修改 | 删除 | 其它写操作 |
|
||||
|------|------|------|------|------------|
|
||||
| 线索 | ✓ 表单 | ✗ | ✗ | 跟进、转客户、无效 |
|
||||
| 客户/商机 | ✗ | ✗ | ✗ | 写跟进 |
|
||||
| 公海 | ✗ | ✗ | ✗ | 领取 |
|
||||
| 报价/合同 | ✗ | ✗ | ✗ | 仅查看 |
|
||||
| 报销/借款 | ✓ 新建 | ✗ | ✗ | 存草稿、提交、审批 |
|
||||
| 地磅 | ✗ | 二次过磅皮重 | 作废 | 生成出入库单 |
|
||||
| 运输 | 安排运输 | ✗ | ✗ | 归档、上传证、结算审批 |
|
||||
|
||||
造数不足时:用例仍要写全;执行时标「Blocked-缺数据」。
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* uni-app H5 + Playwright 公共操作(登录页起复用)
|
||||
*/
|
||||
export function env() {
|
||||
return {
|
||||
base: process.env.E2E_BASE || 'http://localhost:5173',
|
||||
user: process.env.E2E_USER || 'admin',
|
||||
pass: process.env.E2E_PASS || 'admin123'
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearSession(page) {
|
||||
await page.goto(env().base + '/', { waitUntil: 'domcontentloaded' })
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
} catch (_) {}
|
||||
})
|
||||
}
|
||||
|
||||
export async function openLoginFresh(page) {
|
||||
await clearSession(page)
|
||||
await page.goto(env().base + '/', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(800)
|
||||
// 若仍被跳到工作台,再清一次
|
||||
if (page.url().includes('workbench')) {
|
||||
await clearSession(page)
|
||||
await page.goto(env().base + '/pages/login/login', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
}
|
||||
|
||||
export function userInput(page) {
|
||||
return page.locator('input.uni-input-input[type="text"]').first()
|
||||
}
|
||||
|
||||
export function passInput(page) {
|
||||
return page.locator('input.uni-input-input[type="password"]').first()
|
||||
}
|
||||
|
||||
export async function fillLogin(page, username, password) {
|
||||
await userInput(page).fill(username ?? '')
|
||||
await passInput(page).fill(password ?? '')
|
||||
}
|
||||
|
||||
export async function clickLoginButton(page) {
|
||||
await page.locator('uni-button.btn-login, button.btn-login').first().click({ force: true })
|
||||
}
|
||||
|
||||
export async function clickAgreeCheck(page) {
|
||||
await page.locator('.agree-check').first().click({ force: true })
|
||||
}
|
||||
|
||||
export async function waitToast(page, textRe, timeout = 5000) {
|
||||
// uni-app H5 toast 常见:.uni-sample-toast / .uni-toast / uni-toast
|
||||
const toast = page.locator('.uni-sample-toast, .uni-toast, uni-toast, .uni-toast__content').filter({
|
||||
hasText: textRe
|
||||
})
|
||||
await toast.first().waitFor({ state: 'visible', timeout })
|
||||
return toast.first()
|
||||
}
|
||||
|
||||
export async function isOnLogin(page) {
|
||||
const url = page.url()
|
||||
if (url.includes('/pages/login/login') || url.endsWith('/') || url.match(/:5173\/?$/)) {
|
||||
const btn = await page.getByText('账号密码登录').first().isVisible().catch(() => false)
|
||||
const loading = await page.getByText('登录中...').first().isVisible().catch(() => false)
|
||||
return btn || loading
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function isOnWorkbench(page) {
|
||||
return (
|
||||
page.url().includes('/pages/workbench/workbench') &&
|
||||
(await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "miniprogram-e2e",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "miniprogram-e2e",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"playwright": "^1.49.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "miniprogram-e2e",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:login": "node pages/01-login.mjs",
|
||||
"test:smoke": "node smoke.mjs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"playwright": "^1.49.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 第 1 屏:登录页 — 按用例逐条执行(人类测试工程师节奏)
|
||||
* cd miniprogram-e2e && node pages/01-login.mjs
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import {
|
||||
env,
|
||||
openLoginFresh,
|
||||
fillLogin,
|
||||
clickLoginButton,
|
||||
clickAgreeCheck,
|
||||
waitToast,
|
||||
isOnLogin,
|
||||
isOnWorkbench
|
||||
} from '../lib/uni-h5.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.join(__dirname, '..')
|
||||
const outDir = path.join(root, 'artifacts', 'login')
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const { base, user, pass } = env()
|
||||
const results = []
|
||||
|
||||
function logStep(msg) {
|
||||
console.log(` → ${msg}`)
|
||||
}
|
||||
|
||||
async function shot(page, id) {
|
||||
await page.screenshot({ path: path.join(outDir, `${id}.png`), fullPage: true })
|
||||
}
|
||||
|
||||
async function runCase(page, id, title, fn) {
|
||||
console.log(`\n[${id}] ${title}`)
|
||||
try {
|
||||
await openLoginFresh(page)
|
||||
await fn()
|
||||
results.push({ id, title, ok: true, detail: 'PASS' })
|
||||
console.log(` PASS`)
|
||||
} catch (e) {
|
||||
const detail = String(e.message || e).split('\n')[0]
|
||||
results.push({ id, title, ok: false, detail })
|
||||
console.log(` FAIL: ${detail}`)
|
||||
try {
|
||||
await shot(page, `${id}-fail`)
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== 登录页界面测试(逐条)===')
|
||||
console.log(`base=${base} user=${user}`)
|
||||
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
locale: 'zh-CN'
|
||||
})
|
||||
const page = await context.newPage()
|
||||
page.setDefaultTimeout(12000)
|
||||
|
||||
// LGN-01
|
||||
await runCase(page, 'LGN-01', '登录页基础展示', async () => {
|
||||
logStep('检查品牌与表单控件')
|
||||
await page.getByText('山峡化工').first().waitFor({ state: 'visible' })
|
||||
await page.getByText('一体化业务移动端').first().waitFor({ state: 'visible' })
|
||||
await page.getByText('用户名', { exact: true }).first().waitFor({ state: 'visible' })
|
||||
await page.getByText('密码', { exact: true }).first().waitFor({ state: 'visible' })
|
||||
await page.getByText('账号密码登录').first().waitFor({ state: 'visible' })
|
||||
await page.getByText(/用户服务协议/).first().waitFor({ state: 'visible' })
|
||||
await page.getByText(/隐私政策/).first().waitFor({ state: 'visible' })
|
||||
await shot(page, 'LGN-01')
|
||||
})
|
||||
|
||||
// LGN-02
|
||||
await runCase(page, 'LGN-02', '输入框占位提示', async () => {
|
||||
logStep('检查 placeholder 独立节点文案')
|
||||
const texts = await page.locator('.uni-input-placeholder, .input-placeholder').allTextContents()
|
||||
const joined = texts.join('|')
|
||||
if (!joined.includes('请输入用户名')) throw new Error(`缺用户名占位: ${joined}`)
|
||||
if (!joined.includes('请输入密码')) throw new Error(`缺密码占位: ${joined}`)
|
||||
await shot(page, 'LGN-02')
|
||||
})
|
||||
|
||||
// LGN-03
|
||||
await runCase(page, 'LGN-03', '未勾选协议点击登录弹出确认', async () => {
|
||||
logStep('填写账号密码(不勾选)并点登录')
|
||||
await fillLogin(page, user, pass)
|
||||
await clickLoginButton(page)
|
||||
await page.getByText('协议确认').waitFor({ state: 'visible' })
|
||||
await page.getByText('是,继续登录').waitFor({ state: 'visible' })
|
||||
await page.getByText('否', { exact: true }).waitFor({ state: 'visible' })
|
||||
await shot(page, 'LGN-03')
|
||||
})
|
||||
|
||||
// LGN-04
|
||||
await runCase(page, 'LGN-04', '协议弹窗点「否」取消', async () => {
|
||||
logStep('唤起弹窗后点否')
|
||||
await fillLogin(page, user, pass)
|
||||
await clickLoginButton(page)
|
||||
await page.getByText('协议确认').waitFor({ state: 'visible' })
|
||||
await page.getByText('否', { exact: true }).click({ force: true })
|
||||
await page.waitForTimeout(500)
|
||||
const dialogGone = !(await page.getByText('协议确认').isVisible().catch(() => false))
|
||||
if (!dialogGone) throw new Error('弹窗未关闭')
|
||||
if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
|
||||
if (!(await isOnLogin(page))) throw new Error('应停留登录页')
|
||||
await shot(page, 'LGN-04')
|
||||
})
|
||||
|
||||
// LGN-05
|
||||
await runCase(page, 'LGN-05', '同意协议后用户名为空', async () => {
|
||||
logStep('勾选协议,清空用户名后提交')
|
||||
await clickAgreeCheck(page)
|
||||
await fillLogin(page, '', 'x')
|
||||
await clickLoginButton(page)
|
||||
await waitToast(page, /请输入用户名/)
|
||||
if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
|
||||
await shot(page, 'LGN-05')
|
||||
})
|
||||
|
||||
// LGN-06
|
||||
await runCase(page, 'LGN-06', '有用户名无密码', async () => {
|
||||
logStep('勾选协议,仅填用户名')
|
||||
await clickAgreeCheck(page)
|
||||
await fillLogin(page, user, '')
|
||||
await clickLoginButton(page)
|
||||
await waitToast(page, /请输入密码/)
|
||||
if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
|
||||
await shot(page, 'LGN-06')
|
||||
})
|
||||
|
||||
// LGN-07
|
||||
await runCase(page, 'LGN-07', '错误密码登录失败', async () => {
|
||||
logStep('勾选协议,错误密码提交')
|
||||
await clickAgreeCheck(page)
|
||||
await fillLogin(page, user, 'wrong-password-xxx')
|
||||
await clickLoginButton(page)
|
||||
await waitToast(page, /失败|错误|不正确|无效|密码/)
|
||||
await page.waitForTimeout(800)
|
||||
if (await isOnWorkbench(page)) throw new Error('错误密码不应进入工作台')
|
||||
if (!(await isOnLogin(page))) throw new Error('应停留登录页')
|
||||
await shot(page, 'LGN-07')
|
||||
})
|
||||
|
||||
// LGN-08
|
||||
await runCase(page, 'LGN-08', '正确账号登录成功', async () => {
|
||||
logStep('勾选协议,正确账号登录')
|
||||
await clickAgreeCheck(page)
|
||||
await fillLogin(page, user, pass)
|
||||
await clickLoginButton(page)
|
||||
await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 })
|
||||
await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible' })
|
||||
await shot(page, 'LGN-08')
|
||||
})
|
||||
|
||||
// LGN-09
|
||||
await runCase(page, 'LGN-09', '打开用户服务协议', async () => {
|
||||
logStep('点击协议链接')
|
||||
await page.getByText('《用户服务协议》').click({ force: true })
|
||||
await page.waitForURL(/user-agreement/, { timeout: 10000 })
|
||||
await page.waitForTimeout(600)
|
||||
await shot(page, 'LGN-09')
|
||||
})
|
||||
|
||||
// LGN-10
|
||||
await runCase(page, 'LGN-10', '打开隐私政策', async () => {
|
||||
logStep('点击隐私政策链接')
|
||||
await page.getByText('《隐私政策》').click({ force: true })
|
||||
await page.waitForURL(/privacy-policy/, { timeout: 10000 })
|
||||
await page.waitForTimeout(600)
|
||||
await shot(page, 'LGN-10')
|
||||
})
|
||||
|
||||
// LGN-11
|
||||
await runCase(page, 'LGN-11', '勾选框可切换同意状态', async () => {
|
||||
logStep('勾选后提交不应再弹协议窗;取消后再弹')
|
||||
await fillLogin(page, user, 'any')
|
||||
await clickAgreeCheck(page)
|
||||
await clickLoginButton(page)
|
||||
await page.waitForTimeout(600)
|
||||
const firstDialog = await page.getByText('协议确认').isVisible().catch(() => false)
|
||||
if (firstDialog) throw new Error('已勾选仍弹出协议确认')
|
||||
// 可能出现请输入…或登录失败 toast,忽略;回到干净页再测取消
|
||||
await openLoginFresh(page)
|
||||
await fillLogin(page, user, pass)
|
||||
await clickAgreeCheck(page) // on
|
||||
await clickAgreeCheck(page) // off
|
||||
await clickLoginButton(page)
|
||||
await page.getByText('协议确认').waitFor({ state: 'visible', timeout: 5000 })
|
||||
await shot(page, 'LGN-11')
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
const fail = results.filter((r) => !r.ok).length
|
||||
const summary = {
|
||||
screen: 'login',
|
||||
base,
|
||||
user,
|
||||
results,
|
||||
fail,
|
||||
ts: new Date().toISOString()
|
||||
}
|
||||
fs.writeFileSync(path.join(outDir, 'result.json'), JSON.stringify(summary, null, 2))
|
||||
|
||||
// 回写用例执行记录(追加一行到 md 表尾不便解析,写独立 result 即可)
|
||||
const mdNote = path.join(outDir, 'RESULT.md')
|
||||
const lines = [
|
||||
`# 登录页执行结果`,
|
||||
``,
|
||||
`时间:${summary.ts}`,
|
||||
`失败数:${fail} / ${results.length}`,
|
||||
``,
|
||||
`| ID | 结果 | 说明 |`,
|
||||
`|----|------|------|`,
|
||||
...results.map((r) => `| ${r.id} | ${r.ok ? 'PASS' : 'FAIL'} | ${r.detail.replace(/\|/g, '/')} |`),
|
||||
``
|
||||
]
|
||||
fs.writeFileSync(mdNote, lines.join('\n'))
|
||||
|
||||
console.log('\n=== 登录页汇总 ===')
|
||||
results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'PASS' : 'FAIL'} — ${r.detail}`))
|
||||
console.log(`fail=${fail} artifacts=${outDir}`)
|
||||
console.log(fail ? '\n本屏未通过,请先修登录页问题,再进入下一界面。' : '\n本屏全部通过。确认后可进入下一屏「工作台」。')
|
||||
process.exit(fail ? 1 : 0)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* H5 UI 冒烟(不改动 miniprogram 业务工程)
|
||||
* 用法:H5 已在 http://localhost:5173 运行时
|
||||
* cd miniprogram-e2e && node smoke.mjs
|
||||
*
|
||||
* uni-app H5 注意:
|
||||
* - placeholder 在独立 div,真实输入为 input.uni-input-input
|
||||
* - 文案常在 span/text 上,点击会被父级 uni-button/view 拦截,需 force 点容器
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const BASE = process.env.E2E_BASE || 'http://localhost:5173'
|
||||
const USER = process.env.E2E_USER || 'admin'
|
||||
const PASS = process.env.E2E_PASS || 'admin123'
|
||||
const outDir = path.join(__dirname, 'artifacts')
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const results = []
|
||||
|
||||
function record(id, ok, detail) {
|
||||
results.push({ id, ok, detail })
|
||||
console.log(`[${ok ? 'PASS' : 'FAIL'}] ${id}: ${detail}`)
|
||||
}
|
||||
|
||||
async function shot(page, name) {
|
||||
await page.screenshot({ path: path.join(outDir, `${name}.png`), fullPage: true })
|
||||
}
|
||||
|
||||
async function tapText(page, text, opts = {}) {
|
||||
const loc = opts.exact
|
||||
? page.getByText(text, { exact: true }).first()
|
||||
: page.getByText(text).first()
|
||||
await loc.click({ force: true, timeout: opts.timeout || 15000 })
|
||||
}
|
||||
|
||||
async function login(page) {
|
||||
await page.goto(BASE + '/', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(1200)
|
||||
await shot(page, '01-landing')
|
||||
|
||||
if (page.url().includes('/pages/workbench/workbench')) return
|
||||
const wbVisible = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false)
|
||||
if (wbVisible) return
|
||||
|
||||
await page.locator('input.uni-input-input[type="text"]').first().fill(USER)
|
||||
await page.locator('input.uni-input-input[type="password"]').first().fill(PASS)
|
||||
await page.locator('uni-button.btn-login, button.btn-login').first().click({ force: true })
|
||||
|
||||
// 协议弹窗:未勾选时出现
|
||||
try {
|
||||
const confirm = page.getByText('是,继续登录')
|
||||
await confirm.waitFor({ state: 'visible', timeout: 3000 })
|
||||
await confirm.click({ force: true })
|
||||
} catch (_) {
|
||||
/* 已勾选协议则无弹窗 */
|
||||
}
|
||||
|
||||
await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 })
|
||||
await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible', timeout: 10000 })
|
||||
await page.waitForTimeout(800)
|
||||
await shot(page, '02-after-login')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
locale: 'zh-CN'
|
||||
})
|
||||
const page = await context.newPage()
|
||||
page.setDefaultTimeout(20000)
|
||||
|
||||
try {
|
||||
await login(page)
|
||||
|
||||
const wb = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false)
|
||||
const crm = await page.getByText('CRM 管理').first().isVisible().catch(() => false)
|
||||
const bx = await page.getByText('报销管理').first().isVisible().catch(() => false)
|
||||
const db = await page.getByText('地磅管理').first().isVisible().catch(() => false)
|
||||
const pl = await page.getByText('运输管理').first().isVisible().catch(() => false)
|
||||
record(
|
||||
'E1',
|
||||
wb && crm && bx && db && pl,
|
||||
`工作台=${wb} CRM=${crm} 报销=${bx} 地磅=${db} 运输=${pl}`
|
||||
)
|
||||
if (!(wb && crm && bx && db && pl)) {
|
||||
await shot(page, '02-fail-workbench')
|
||||
}
|
||||
|
||||
async function openMenu(label) {
|
||||
await tapText(page, label, { exact: true })
|
||||
await page.waitForTimeout(2200)
|
||||
}
|
||||
|
||||
async function backWorkbench() {
|
||||
const back = page.getByText(/←\s*工作台|←\s*返回/).first()
|
||||
if (await back.isVisible().catch(() => false)) {
|
||||
await back.click({ force: true })
|
||||
await page.waitForTimeout(1500)
|
||||
return
|
||||
}
|
||||
await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
|
||||
if (!wb) return
|
||||
|
||||
// E2 线索
|
||||
try {
|
||||
await openMenu('线索管理')
|
||||
await shot(page, '03-crm-clue')
|
||||
const ok =
|
||||
(await page.getByText(/线索|暂无/).first().isVisible().catch(() => false)) ||
|
||||
(await page.locator('.mp-card, .mp-empty, .mp-header__title').first().isVisible().catch(() => false))
|
||||
record('E2', ok, ok ? '线索列表页已打开' : '未识别到线索列表特征')
|
||||
await backWorkbench()
|
||||
} catch (e) {
|
||||
record('E2', false, String(e.message || e))
|
||||
await shot(page, '03-crm-fail')
|
||||
await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
|
||||
}
|
||||
|
||||
// E3 报销申请
|
||||
try {
|
||||
await openMenu('报销申请')
|
||||
await shot(page, '04-bx-reimburse')
|
||||
const ok =
|
||||
(await page.getByText(/报销|暂无|新建/).first().isVisible().catch(() => false)) ||
|
||||
(await page.locator('.mp-header__title, .mp-empty, .mp-card').first().isVisible().catch(() => false))
|
||||
record('E3', ok, ok ? '报销申请页已打开' : '未识别到报销页特征')
|
||||
await backWorkbench()
|
||||
} catch (e) {
|
||||
record('E3', false, String(e.message || e))
|
||||
await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
|
||||
}
|
||||
|
||||
// E4 地磅仪表盘
|
||||
try {
|
||||
await openMenu('仪表盘')
|
||||
await shot(page, '05-db-dashboard')
|
||||
const ok =
|
||||
(await page.getByText(/仪表盘|今日|入库|出库|统计/).first().isVisible().catch(() => false)) ||
|
||||
(await page.locator('.mp-stat, .mp-header__title').first().isVisible().catch(() => false))
|
||||
record('E4', ok, ok ? '地磅仪表盘已打开' : '未识别到仪表盘特征')
|
||||
await backWorkbench()
|
||||
} catch (e) {
|
||||
record('E4', false, String(e.message || e))
|
||||
await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
|
||||
}
|
||||
|
||||
// E5 运输计划
|
||||
try {
|
||||
await openMenu('运输计划')
|
||||
await shot(page, '06-pl-plan')
|
||||
const ok =
|
||||
(await page.getByText(/待发运|在途|已完成|运输计划|暂无/).first().isVisible().catch(() => false)) ||
|
||||
(await page.locator('.mp-tab, .mp-header__title').first().isVisible().catch(() => false))
|
||||
record('E5', ok, ok ? '运输计划页已打开' : '未识别到运输计划特征')
|
||||
} catch (e) {
|
||||
record('E5', false, String(e.message || e))
|
||||
await shot(page, '06-pl-fail')
|
||||
}
|
||||
} catch (e) {
|
||||
record('EX', false, String(e.message || e))
|
||||
try {
|
||||
await shot(page, '99-exception')
|
||||
} catch (_) {}
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
const summary = {
|
||||
base: BASE,
|
||||
user: USER,
|
||||
results,
|
||||
fail: results.filter((r) => !r.ok).length,
|
||||
ts: new Date().toISOString()
|
||||
}
|
||||
fs.writeFileSync(path.join(outDir, 'smoke-result.json'), JSON.stringify(summary, null, 2))
|
||||
console.log('\n=== SUMMARY ===')
|
||||
results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'OK' : 'FAIL'} — ${r.detail}`))
|
||||
console.log(`fail=${summary.fail} artifacts=${outDir}`)
|
||||
process.exit(summary.fail ? 1 : 0)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -213,3 +213,26 @@ export async function listContract(query = {}) {
|
||||
export async function getContract(id) {
|
||||
return get(`/web/crm/contract/${String(id)}`)
|
||||
}
|
||||
|
||||
// ── 产品分类 ──
|
||||
|
||||
/** 产品分类树(不分页,嵌套 children) */
|
||||
export async function listProductCategoryTree(query = {}) {
|
||||
const body = {}
|
||||
if (query.categoryName) body.categoryName = String(query.categoryName).trim()
|
||||
if (query.status != null && query.status !== '') body.status = String(query.status)
|
||||
const data = await post('/web/crm/product-categories/tree', body)
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
export function addProductCategory(data) {
|
||||
return post('/web/crm/product-categories/add', data)
|
||||
}
|
||||
|
||||
export function updateProductCategory(data) {
|
||||
return post('/web/crm/product-categories/update', data)
|
||||
}
|
||||
|
||||
export function deleteProductCategory(id) {
|
||||
return post('/web/crm/product-categories/delete', { id: String(id) })
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 全局空态(方案 B · 插画文件夹)
|
||||
* 用法:
|
||||
* <mp-empty title="暂无产品分类" description="可先新增一个顶级分类" action-text="新增分类" @action="goAdd" />
|
||||
* <mp-empty title="暂无线索" />
|
||||
* <mp-empty compact title="暂无跟进" />
|
||||
*/
|
||||
defineProps({
|
||||
/** 主文案 */
|
||||
title: { type: String, default: '暂无数据' },
|
||||
/** 辅助说明,可选 */
|
||||
description: { type: String, default: '' },
|
||||
/** 主按钮文案;空则不展示按钮 */
|
||||
actionText: { type: String, default: '' },
|
||||
/** 次要按钮文案,可选 */
|
||||
secondaryText: { type: String, default: '' },
|
||||
/** 详情内嵌等紧凑场景 */
|
||||
compact: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action', 'secondary'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-empty" :class="{ 'mp-empty--compact': compact }">
|
||||
<view class="mp-empty__art" aria-hidden="true">
|
||||
<view class="mp-empty__shadow" />
|
||||
<view class="mp-empty__folder">
|
||||
<view class="mp-empty__folder-tab" />
|
||||
<view class="mp-empty__folder-back" />
|
||||
<view class="mp-empty__folder-front">
|
||||
<view class="mp-empty__lines">
|
||||
<view class="mp-empty__line" />
|
||||
<view class="mp-empty__line mp-empty__line--mid" />
|
||||
<view class="mp-empty__line mp-empty__line--short" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mp-empty__title">{{ title }}</text>
|
||||
<text v-if="description" class="mp-empty__desc">{{ description }}</text>
|
||||
<view v-if="actionText || secondaryText" class="mp-empty__actions">
|
||||
<view
|
||||
v-if="secondaryText"
|
||||
class="mp-btn mp-btn--ghost"
|
||||
@click="emit('secondary')"
|
||||
>
|
||||
{{ secondaryText }}
|
||||
</view>
|
||||
<view
|
||||
v-if="actionText"
|
||||
class="mp-btn mp-btn--primary"
|
||||
@click="emit('action')"
|
||||
>
|
||||
{{ actionText }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -53,6 +53,8 @@ export const PAGE_PATHS = {
|
||||
crmQuoteDetail: '/pages/crm/quote/detail',
|
||||
crmContractList: '/pages/crm/contract/list',
|
||||
crmContractDetail: '/pages/crm/contract/detail',
|
||||
crmProductCategoryList: '/pages/crm/product-category/list',
|
||||
crmProductCategoryForm: '/pages/crm/product-category/form',
|
||||
bxTodoList: '/pages/bx/todo/list',
|
||||
bxReimburseList: '/pages/bx/reimburse/list',
|
||||
bxReimburseForm: '/pages/bx/reimburse/form',
|
||||
@@ -82,6 +84,7 @@ export const PAGE_PATHS = {
|
||||
|
||||
/** 工作台菜单 code → 页面路径 */
|
||||
export const WORKBENCH_ROUTE_MAP = {
|
||||
'crm-product-category': PAGE_PATHS.crmProductCategoryList,
|
||||
'crm-clue': PAGE_PATHS.crmClueList,
|
||||
'crm-opportunity': PAGE_PATHS.crmOpportunityList,
|
||||
'crm-customer': PAGE_PATHS.crmCustomerList,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const WORKBENCH_MODULES = [
|
||||
desc: '线索 · 商机 · 客户 · 报价 · 合同',
|
||||
iconText: 'CRM',
|
||||
menus: [
|
||||
{ name: '产品分类', enabled: false },
|
||||
{ name: '产品分类', enabled: true, code: 'crm-product-category' },
|
||||
{ name: '产品管理', enabled: false },
|
||||
{ name: '线索管理', enabled: true, code: 'crm-clue' },
|
||||
{ name: '商机管理', enabled: true, code: 'crm-opportunity' },
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/crm/product-category/list",
|
||||
"style": { "navigationStyle": "custom" }
|
||||
},
|
||||
{
|
||||
"path": "pages/crm/product-category/form",
|
||||
"style": { "navigationStyle": "custom" }
|
||||
},
|
||||
{
|
||||
"path": "pages/crm/clue/list",
|
||||
"style": { "navigationStyle": "custom", "enablePullDownRefresh": false }
|
||||
|
||||
@@ -60,10 +60,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无借款单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无借款单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -103,10 +103,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无待审批</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无待审批" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -113,10 +113,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无单据</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -60,10 +60,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无报销单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无报销单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -171,10 +171,7 @@ onReachBottom(() => {
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无单据</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -151,9 +151,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">跟进记录</text>
|
||||
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
|
||||
<text class="mp-empty__text">暂无跟进</text>
|
||||
</view>
|
||||
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="f in follows" :key="f.id" class="mp-card">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -119,10 +119,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无线索</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无线索" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -107,10 +107,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无合同</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无合同" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -99,9 +99,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">跟进记录</text>
|
||||
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
|
||||
<text class="mp-empty__text">暂无跟进</text>
|
||||
</view>
|
||||
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="f in follows" :key="f.id" class="mp-card">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -104,10 +104,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无客户</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无客户" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -98,9 +98,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">跟进记录</text>
|
||||
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
|
||||
<text class="mp-empty__text">暂无跟进</text>
|
||||
</view>
|
||||
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="f in follows" :key="f.id" class="mp-card">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -106,10 +106,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无商机</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无商机" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -102,10 +102,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">公海暂无客户</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="公海暂无客户" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card">
|
||||
<view class="mp-card__head" @click="goDetail(row)">
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import {
|
||||
addProductCategory,
|
||||
listProductCategoryTree,
|
||||
updateProductCategory
|
||||
} from '@/api/crm'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
|
||||
const submitting = ref(false)
|
||||
const loading = ref(false)
|
||||
const editId = ref('')
|
||||
const tree = ref([])
|
||||
const showParentSheet = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
parentId: '0',
|
||||
categoryName: '',
|
||||
orderNum: 0,
|
||||
status: '0',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ value: '0', label: '正常' },
|
||||
{ value: '1', label: '停用' }
|
||||
]
|
||||
|
||||
const pageTitle = computed(() => (editId.value ? '修改产品分类' : '新增产品分类'))
|
||||
|
||||
/** 收集某节点及其全部子孙 id(编辑时禁止选为上级) */
|
||||
function collectDescendantIds(nodes, rootId, out = new Set()) {
|
||||
;(nodes || []).forEach((n) => {
|
||||
if (n.id === rootId) {
|
||||
out.add(n.id)
|
||||
const walk = (kids) => {
|
||||
;(kids || []).forEach((c) => {
|
||||
out.add(c.id)
|
||||
walk(c.children)
|
||||
})
|
||||
}
|
||||
walk(n.children)
|
||||
} else if (n.children?.length) {
|
||||
collectDescendantIds(n.children, rootId, out)
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function flattenParents(nodes, pathParts, excludeIds, out = []) {
|
||||
;(nodes || []).forEach((n) => {
|
||||
if (excludeIds.has(n.id)) return
|
||||
const path = [...pathParts, n.categoryName]
|
||||
out.push({
|
||||
id: n.id,
|
||||
label: path.join(' / '),
|
||||
name: n.categoryName
|
||||
})
|
||||
if (n.children?.length) {
|
||||
flattenParents(n.children, path, excludeIds, out)
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
const parentOptions = computed(() => {
|
||||
const exclude = editId.value
|
||||
? collectDescendantIds(tree.value, editId.value)
|
||||
: new Set()
|
||||
return [{ id: '0', label: '顶级分类', name: '顶级分类' }].concat(
|
||||
flattenParents(tree.value, [], exclude, [])
|
||||
)
|
||||
})
|
||||
|
||||
const parentLabel = computed(() => {
|
||||
const hit = parentOptions.value.find((o) => o.id === (form.parentId || '0'))
|
||||
return hit?.label || '顶级分类'
|
||||
})
|
||||
|
||||
const statusIndex = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
statusOptions.findIndex((o) => o.value === form.status)
|
||||
)
|
||||
)
|
||||
|
||||
const findNodeById = (nodes, id) => {
|
||||
for (const n of nodes || []) {
|
||||
if (n.id === id) return n
|
||||
const found = findNodeById(n.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.navigateTo({ url: PAGE_PATHS.crmProductCategoryList })
|
||||
})
|
||||
}
|
||||
|
||||
const loadTree = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
tree.value = await listProductCategoryTree({})
|
||||
if (editId.value) {
|
||||
const node = findNodeById(tree.value, editId.value)
|
||||
if (!node) {
|
||||
uni.showToast({ title: '分类不存在', icon: 'none' })
|
||||
return
|
||||
}
|
||||
form.parentId = node.parentId && node.parentId !== '0' ? node.parentId : '0'
|
||||
form.categoryName = node.categoryName || ''
|
||||
form.orderNum = node.orderNum ?? 0
|
||||
form.status = node.status != null ? String(node.status) : '0'
|
||||
form.remark = node.remark || ''
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openParentSheet = () => {
|
||||
showParentSheet.value = true
|
||||
}
|
||||
|
||||
const closeParentSheet = () => {
|
||||
showParentSheet.value = false
|
||||
}
|
||||
|
||||
const selectParent = (opt) => {
|
||||
form.parentId = opt.id
|
||||
closeParentSheet()
|
||||
}
|
||||
|
||||
const onStatusChange = (e) => {
|
||||
const idx = Number(e.detail.value)
|
||||
form.status = statusOptions[idx]?.value || '0'
|
||||
}
|
||||
|
||||
const onOrderInput = (e) => {
|
||||
const raw = e.detail.value
|
||||
const n = parseInt(raw, 10)
|
||||
form.orderNum = Number.isFinite(n) && n >= 0 ? n : 0
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.categoryName.trim()) {
|
||||
uni.showToast({ title: '请填写分类名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
parentId: form.parentId || '0',
|
||||
categoryName: form.categoryName.trim(),
|
||||
orderNum: form.orderNum ?? 0,
|
||||
status: form.status || '0',
|
||||
remark: form.remark || ''
|
||||
}
|
||||
if (editId.value) {
|
||||
await updateProductCategory({ id: editId.value, ...payload })
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} else {
|
||||
await addProductCategory(payload)
|
||||
uni.showToast({ title: '新增成功', icon: 'success' })
|
||||
}
|
||||
setTimeout(() => goBack(), 400)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
editId.value = query?.id ? String(query.id) : ''
|
||||
if (!editId.value && query?.parentId) {
|
||||
form.parentId = String(query.parentId)
|
||||
}
|
||||
loadTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub mp-page--with-bottom">
|
||||
<view class="mp-header">
|
||||
<text class="mp-subpage-back" @click="goBack">← 返回</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">{{ pageTitle }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
|
||||
<view v-else class="mp-form-panel">
|
||||
<view class="mp-form-row" @click="openParentSheet">
|
||||
<text class="mp-form-label">上级分类</text>
|
||||
<view class="mp-form-picker mp-form-picker--link">{{ parentLabel }}</view>
|
||||
</view>
|
||||
<view class="mp-form-row">
|
||||
<text class="mp-form-label mp-form-label--req">分类名称</text>
|
||||
<input
|
||||
v-model="form.categoryName"
|
||||
class="mp-form-input"
|
||||
placeholder="请输入分类名称"
|
||||
/>
|
||||
</view>
|
||||
<view class="mp-form-row">
|
||||
<text class="mp-form-label">排序号</text>
|
||||
<input
|
||||
class="mp-form-input"
|
||||
type="number"
|
||||
:value="String(form.orderNum)"
|
||||
placeholder="数值越小越靠前"
|
||||
@input="onOrderInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="mp-form-row">
|
||||
<text class="mp-form-label">状态</text>
|
||||
<picker :range="statusOptions" range-key="label" :value="statusIndex" @change="onStatusChange">
|
||||
<view class="mp-form-picker">
|
||||
{{ statusOptions[statusIndex]?.label || '正常' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="mp-form-row">
|
||||
<text class="mp-form-label">备注</text>
|
||||
<textarea v-model="form.remark" class="mp-form-textarea" placeholder="请输入备注" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-bottom-bar">
|
||||
<view class="mp-btn mp-btn--ghost" @click="goBack">取消</view>
|
||||
<view class="mp-btn mp-btn--primary" @click="submit">
|
||||
{{ submitting ? '提交中…' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showParentSheet" class="mp-sheet-mask" @click="closeParentSheet">
|
||||
<view class="mp-sheet" @click.stop>
|
||||
<view class="mp-sheet__head">
|
||||
<text class="mp-sheet__title">选择上级分类</text>
|
||||
<text class="mp-sheet__cancel" @click="closeParentSheet">关闭</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="parent-sheet-body">
|
||||
<view
|
||||
v-for="opt in parentOptions"
|
||||
:key="opt.id"
|
||||
class="parent-opt"
|
||||
:class="{ 'parent-opt--active': form.parentId === opt.id }"
|
||||
@click="selectParent(opt)"
|
||||
>
|
||||
<text class="parent-opt__label">{{ opt.label }}</text>
|
||||
<text v-if="form.parentId === opt.id" class="parent-opt__check">✓</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.parent-sheet-body {
|
||||
max-height: 60vh;
|
||||
padding: 8rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.parent-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--mp-border-light);
|
||||
}
|
||||
|
||||
.parent-opt--active .parent-opt__label {
|
||||
color: var(--mp-brand-green-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.parent-opt__label {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--mp-text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.parent-opt__check {
|
||||
color: var(--mp-brand-green);
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,702 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import {
|
||||
deleteProductCategory,
|
||||
listProductCategoryTree
|
||||
} from '@/api/crm'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
|
||||
const keyword = ref('')
|
||||
const statusFilter = ref('') // '' | '0' | '1'
|
||||
const tree = ref([])
|
||||
const loading = ref(false)
|
||||
const expandedIds = ref([])
|
||||
const allExpanded = ref(false)
|
||||
|
||||
let loadSeq = 0
|
||||
let searchTimer = null
|
||||
|
||||
const statusChips = [
|
||||
{ key: '', label: '全部' },
|
||||
{ key: '0', label: '正常' },
|
||||
{ key: '1', label: '停用' }
|
||||
]
|
||||
|
||||
const statusLabel = (status) =>
|
||||
String(status) === '0'
|
||||
? { text: '正常', tag: 'success' }
|
||||
: { text: '停用', tag: 'muted' }
|
||||
|
||||
const childCount = (node) => (node.children || []).length
|
||||
|
||||
function collectExpandableIds(nodes, out = []) {
|
||||
;(nodes || []).forEach((n) => {
|
||||
if (childCount(n) > 0) {
|
||||
out.push(n.id)
|
||||
collectExpandableIds(n.children, out)
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function defaultExpandLevel1(nodes) {
|
||||
return (nodes || []).filter((n) => childCount(n) > 0).map((n) => n.id)
|
||||
}
|
||||
|
||||
function flattenVisible(nodes, level, expandedSet, out = []) {
|
||||
;(nodes || []).forEach((n) => {
|
||||
const kids = childCount(n)
|
||||
out.push({
|
||||
id: n.id,
|
||||
parentId: n.parentId,
|
||||
categoryName: n.categoryName,
|
||||
status: n.status,
|
||||
orderNum: n.orderNum ?? 0,
|
||||
level,
|
||||
hasChildren: kids > 0,
|
||||
childCount: kids
|
||||
})
|
||||
if (kids > 0 && expandedSet.has(n.id)) {
|
||||
flattenVisible(n.children, level + 1, expandedSet, out)
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
const visibleRows = computed(() => {
|
||||
const set = new Set(expandedIds.value)
|
||||
return flattenVisible(tree.value, 0, set, [])
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端过滤:categoryName / status 写入 POST JSON,经加密中间件解密后由 Pydantic 绑定。
|
||||
* @param {{ categoryName?: string, status?: string }} [override]
|
||||
*/
|
||||
const loadTree = async (override = {}) => {
|
||||
const seq = ++loadSeq
|
||||
loading.value = true
|
||||
try {
|
||||
const kw = String(
|
||||
override.categoryName != null ? override.categoryName : keyword.value
|
||||
).trim()
|
||||
const status =
|
||||
override.status != null ? override.status : statusFilter.value
|
||||
const rows = await listProductCategoryTree({
|
||||
categoryName: kw || undefined,
|
||||
status: status || undefined
|
||||
})
|
||||
if (seq !== loadSeq) return
|
||||
tree.value = rows
|
||||
const expandable = collectExpandableIds(rows)
|
||||
const hasFilter = Boolean(kw) || Boolean(status)
|
||||
if (hasFilter) {
|
||||
expandedIds.value = expandable
|
||||
allExpanded.value = true
|
||||
} else if (allExpanded.value) {
|
||||
expandedIds.value = expandable
|
||||
} else {
|
||||
expandedIds.value = defaultExpandLevel1(rows)
|
||||
}
|
||||
} catch (e) {
|
||||
if (seq !== loadSeq) return
|
||||
tree.value = []
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
if (seq === loadSeq) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleSearch = () => {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
loadTree({ categoryName: keyword.value })
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
clearTimeout(searchTimer)
|
||||
loadTree({ categoryName: keyword.value })
|
||||
}
|
||||
|
||||
const clearKeyword = () => {
|
||||
if (!keyword.value) return
|
||||
keyword.value = ''
|
||||
clearTimeout(searchTimer)
|
||||
loadTree({ categoryName: '' })
|
||||
}
|
||||
|
||||
// 即时检索:防抖后走后台 tree 接口过滤(对齐 Web 300ms)
|
||||
watch(keyword, (val, oldVal) => {
|
||||
if (val === oldVal) return
|
||||
scheduleSearch()
|
||||
})
|
||||
|
||||
const switchStatus = (key) => {
|
||||
if (statusFilter.value === key) return
|
||||
statusFilter.value = key
|
||||
clearTimeout(searchTimer)
|
||||
loadTree({ status: key })
|
||||
}
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
allExpanded.value = !allExpanded.value
|
||||
if (allExpanded.value) {
|
||||
expandedIds.value = collectExpandableIds(tree.value)
|
||||
} else {
|
||||
expandedIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNode = (row) => {
|
||||
if (!row.hasChildren) return
|
||||
const set = new Set(expandedIds.value)
|
||||
if (set.has(row.id)) set.delete(row.id)
|
||||
else set.add(row.id)
|
||||
expandedIds.value = Array.from(set)
|
||||
const allIds = collectExpandableIds(tree.value)
|
||||
allExpanded.value = allIds.length > 0 && allIds.every((id) => set.has(id))
|
||||
}
|
||||
|
||||
const goForm = (query = {}) => {
|
||||
const parts = Object.keys(query)
|
||||
.filter((k) => query[k] != null && query[k] !== '')
|
||||
.map((k) => `${k}=${encodeURIComponent(query[k])}`)
|
||||
const qs = parts.length ? `?${parts.join('&')}` : ''
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.crmProductCategoryForm}${qs}` })
|
||||
}
|
||||
|
||||
const goAdd = () => goForm()
|
||||
|
||||
const onEmptyAction = () => {
|
||||
if (keyword.value.trim()) clearKeyword()
|
||||
else goAdd()
|
||||
}
|
||||
|
||||
const goAddChild = (row) => {
|
||||
goForm({ parentId: row.id, parentName: row.categoryName })
|
||||
}
|
||||
|
||||
const goEdit = (row) => {
|
||||
goForm({ id: row.id })
|
||||
}
|
||||
|
||||
const moreVisible = ref(false)
|
||||
const moreTarget = ref(null)
|
||||
|
||||
const closeMore = () => {
|
||||
moreVisible.value = false
|
||||
moreTarget.value = null
|
||||
}
|
||||
|
||||
const confirmDelete = (row) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `是否确认删除「${row.categoryName}」?`,
|
||||
confirmColor: '#d14343',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await deleteProductCategory(row.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
loadTree()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openMore = (row) => {
|
||||
moreTarget.value = row
|
||||
moreVisible.value = true
|
||||
}
|
||||
|
||||
const onMoreAction = (action) => {
|
||||
const row = moreTarget.value
|
||||
if (!row) return
|
||||
closeMore()
|
||||
// 等关闭动画后再跳转/弹窗,避免层级冲突
|
||||
setTimeout(() => {
|
||||
if (action === 'edit') goEdit(row)
|
||||
else if (action === 'addChild') goAddChild(row)
|
||||
else if (action === 'delete') confirmDelete(row)
|
||||
}, 180)
|
||||
}
|
||||
|
||||
onShow(() => loadTree())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header">
|
||||
<view class="mp-header__row">
|
||||
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
|
||||
<view class="cat-header-tools">
|
||||
<view class="mp-header__action" @click="toggleExpandAll">
|
||||
{{ allExpanded ? '全部折叠' : '全部展开' }}
|
||||
</view>
|
||||
<view class="mp-header__action" @click="goAdd">+ 新增</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">产品分类</text>
|
||||
<view class="mp-header__search">
|
||||
<view class="mp-header__search-dot" />
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="mp-header__search-input"
|
||||
type="text"
|
||||
placeholder="搜索分类名称"
|
||||
confirm-type="search"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<text v-if="keyword" class="cat-search-clear" @click="clearKeyword">清除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-x class="cat-chips" :show-scrollbar="false">
|
||||
<view
|
||||
v-for="chip in statusChips"
|
||||
:key="chip.key || 'all'"
|
||||
class="cat-chip"
|
||||
:class="{ 'cat-chip--active': statusFilter === chip.key }"
|
||||
@click="switchStatus(chip.key)"
|
||||
>
|
||||
{{ chip.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="mp-section">
|
||||
<mp-empty
|
||||
v-if="!visibleRows.length && !loading"
|
||||
:title="keyword.trim() ? '未找到相关分类' : '暂无产品分类'"
|
||||
:description="
|
||||
keyword.trim()
|
||||
? '试试更短的关键词,或清除搜索后查看全部'
|
||||
: '还没有分类数据,可先新增一个顶级分类'
|
||||
"
|
||||
:action-text="keyword.trim() ? '清除搜索' : '新增分类'"
|
||||
@action="onEmptyAction"
|
||||
/>
|
||||
<view v-else class="cat-tree">
|
||||
<view
|
||||
v-for="(row, index) in visibleRows"
|
||||
:key="row.id"
|
||||
class="cat-row"
|
||||
:class="'cat-row--l' + Math.min(row.level, 2)"
|
||||
>
|
||||
<text class="cat-index">{{ index + 1 }}</text>
|
||||
<view
|
||||
class="cat-row__body"
|
||||
:style="{ paddingLeft: row.level * 36 + 'rpx' }"
|
||||
>
|
||||
<view
|
||||
class="cat-chevron"
|
||||
:class="{
|
||||
'cat-chevron--leaf': !row.hasChildren,
|
||||
'cat-chevron--open': expandedIds.includes(row.id)
|
||||
}"
|
||||
@click.stop="toggleNode(row)"
|
||||
>
|
||||
▶
|
||||
</view>
|
||||
<view class="cat-main" @click="goEdit(row)">
|
||||
<text class="cat-name">{{ row.categoryName }}</text>
|
||||
<view class="cat-meta">
|
||||
<text class="mp-tag" :class="'mp-tag--' + statusLabel(row.status).tag">
|
||||
{{ statusLabel(row.status).text }}
|
||||
</text>
|
||||
<text class="cat-meta__text">排序 {{ row.orderNum }}</text>
|
||||
<text v-if="row.hasChildren" class="cat-meta__text">{{ row.childCount }} 个子类</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cat-actions">
|
||||
<view class="cat-icon-btn" @click.stop="goAddChild(row)">+</view>
|
||||
<view class="cat-icon-btn cat-icon-btn--more" @click.stop="openMore(row)">⋯</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多操作:自定义底部菜单(替代系统 ActionSheet) -->
|
||||
<view
|
||||
v-if="moreVisible"
|
||||
class="cat-as-mask"
|
||||
@click="closeMore"
|
||||
@touchmove.stop.prevent
|
||||
>
|
||||
<view class="cat-as" @click.stop>
|
||||
<view class="cat-as__handle" />
|
||||
<view class="cat-as__head">
|
||||
<text class="cat-as__eyebrow">操作分类</text>
|
||||
<text class="cat-as__title">{{ moreTarget?.categoryName || '' }}</text>
|
||||
</view>
|
||||
<view class="cat-as__group">
|
||||
<view class="cat-as__item" @click="onMoreAction('edit')">
|
||||
<view class="cat-as__icon cat-as__icon--edit">编</view>
|
||||
<view class="cat-as__meta">
|
||||
<text class="cat-as__name">编辑</text>
|
||||
<text class="cat-as__desc">修改名称、上级、状态等</text>
|
||||
</view>
|
||||
<text class="cat-as__arrow">›</text>
|
||||
</view>
|
||||
<view class="cat-as__item" @click="onMoreAction('addChild')">
|
||||
<view class="cat-as__icon cat-as__icon--add">子</view>
|
||||
<view class="cat-as__meta">
|
||||
<text class="cat-as__name">新增子分类</text>
|
||||
<text class="cat-as__desc">在此分类下创建下级</text>
|
||||
</view>
|
||||
<text class="cat-as__arrow">›</text>
|
||||
</view>
|
||||
<view class="cat-as__item cat-as__item--danger" @click="onMoreAction('delete')">
|
||||
<view class="cat-as__icon cat-as__icon--del">删</view>
|
||||
<view class="cat-as__meta">
|
||||
<text class="cat-as__name">删除</text>
|
||||
<text class="cat-as__desc">有子分类或关联产品时不可删</text>
|
||||
</view>
|
||||
<text class="cat-as__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cat-as__cancel" @click="closeMore">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cat-header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.cat-search-clear {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: var(--mp-brand-green-dark);
|
||||
padding: 8rpx 4rpx 8rpx 12rpx;
|
||||
}
|
||||
|
||||
.cat-chips {
|
||||
white-space: nowrap;
|
||||
padding: 20rpx 32rpx 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cat-chip {
|
||||
display: inline-block;
|
||||
margin-right: 16rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--mp-muted);
|
||||
background: var(--mp-surface);
|
||||
border: 1rpx solid var(--mp-border);
|
||||
}
|
||||
|
||||
.cat-chip--active {
|
||||
color: var(--mp-brand-green-dark);
|
||||
background: var(--mp-brand-green-soft);
|
||||
border-color: #b7e6d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cat-tree {
|
||||
background: var(--mp-surface);
|
||||
border: 1rpx solid var(--mp-border);
|
||||
border-radius: var(--mp-radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-height: 104rpx;
|
||||
padding: 20rpx 20rpx 20rpx 16rpx;
|
||||
border-bottom: 1rpx solid var(--mp-border-light);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cat-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cat-row--l1 {
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.cat-row--l2 {
|
||||
background: #fafcfb;
|
||||
}
|
||||
|
||||
.cat-index {
|
||||
flex-shrink: 0;
|
||||
width: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--mp-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cat-row__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.cat-chevron {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18rpx;
|
||||
color: var(--mp-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.cat-chevron--open {
|
||||
transform: rotate(90deg);
|
||||
color: var(--mp-brand-green);
|
||||
}
|
||||
|
||||
.cat-chevron--leaf {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.cat-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cat-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--mp-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cat-meta {
|
||||
margin-top: 6rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.cat-meta__text {
|
||||
font-size: 22rpx;
|
||||
color: var(--mp-muted);
|
||||
}
|
||||
|
||||
.cat-actions {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-icon-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--mp-brand-green-dark);
|
||||
background: var(--mp-brand-green-soft);
|
||||
}
|
||||
|
||||
.cat-icon-btn--more {
|
||||
background: #f1f5f3;
|
||||
color: var(--mp-muted);
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── 更多操作 Action Sheet ── */
|
||||
.cat-as-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(15, 30, 25, 0.48);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
animation: cat-as-fade 0.2s ease;
|
||||
}
|
||||
|
||||
.cat-as {
|
||||
width: 100%;
|
||||
padding: 0 24rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
|
||||
animation: cat-as-up 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.cat-as__handle {
|
||||
width: 72rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.cat-as__head {
|
||||
background: var(--mp-surface);
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
padding: 28rpx 32rpx 20rpx;
|
||||
border-bottom: 1rpx solid var(--mp-border-light);
|
||||
}
|
||||
|
||||
.cat-as__eyebrow {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: var(--mp-muted);
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.cat-as__title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: var(--mp-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cat-as__group {
|
||||
background: var(--mp-surface);
|
||||
border-radius: 0 0 28rpx 28rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.cat-as__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
min-height: 112rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
border-bottom: 1rpx solid var(--mp-border-light);
|
||||
}
|
||||
|
||||
.cat-as__item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cat-as__item:active {
|
||||
background: #f7fbf9;
|
||||
}
|
||||
|
||||
.cat-as__icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-as__icon--edit {
|
||||
color: var(--mp-brand-green-dark);
|
||||
background: var(--mp-brand-green-soft);
|
||||
}
|
||||
|
||||
.cat-as__icon--add {
|
||||
color: #087f57;
|
||||
background: #e9f9f2;
|
||||
}
|
||||
|
||||
.cat-as__icon--del {
|
||||
color: #d14343;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.cat-as__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cat-as__name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--mp-text);
|
||||
}
|
||||
|
||||
.cat-as__item--danger .cat-as__name {
|
||||
color: #d14343;
|
||||
}
|
||||
|
||||
.cat-as__desc {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--mp-muted);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cat-as__arrow {
|
||||
font-size: 36rpx;
|
||||
color: #c5d5cd;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cat-as__cancel {
|
||||
background: var(--mp-surface);
|
||||
border-radius: 28rpx;
|
||||
min-height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--mp-muted);
|
||||
}
|
||||
|
||||
.cat-as__cancel:active {
|
||||
background: #f3f7f5;
|
||||
}
|
||||
|
||||
@keyframes cat-as-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cat-as-up {
|
||||
from {
|
||||
transform: translateY(24rpx);
|
||||
opacity: 0.6;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -105,10 +105,7 @@ onReachBottom(() => loadList(false))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无报价</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无报价" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -107,10 +107,7 @@ onShow(() => load())
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无出入库单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无出入库单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -100,9 +100,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">关联出入库({{ related.inventories?.length || 0 }})</text>
|
||||
<view v-if="!related.inventories?.length" class="mp-empty" style="padding: 32rpx 0">
|
||||
<text class="mp-empty__text">暂无出入库单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!related.inventories?.length" compact title="暂无出入库单" />
|
||||
<view v-else class="mp-list">
|
||||
<view
|
||||
v-for="inv in related.inventories"
|
||||
@@ -118,9 +116,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">关联地磅单({{ related.weighings?.length || 0 }})</text>
|
||||
<view v-if="!related.weighings?.length" class="mp-empty" style="padding: 32rpx 0">
|
||||
<text class="mp-empty__text">暂无地磅单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!related.weighings?.length" compact title="暂无地磅单" />
|
||||
<view v-else class="mp-list">
|
||||
<view
|
||||
v-for="w in related.weighings"
|
||||
|
||||
@@ -87,10 +87,7 @@ onShow(() => load())
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无计划</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无计划" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -137,10 +137,7 @@ onShow(() => load())
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无地磅单</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无地磅单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -81,10 +81,7 @@ onShow(() => load(true))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无已归档计划</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无已归档计划" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -114,9 +114,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">产品购买证</text>
|
||||
<view v-if="!products.length" class="mp-empty" style="padding: 32rpx 0">
|
||||
<text class="mp-empty__text">暂无产品明细</text>
|
||||
</view>
|
||||
<mp-empty v-if="!products.length" compact title="暂无产品明细" />
|
||||
<view v-for="p in products" :key="p.id" class="mp-block__panel" style="margin-bottom: 24rpx">
|
||||
<view class="mp-kv">
|
||||
<text class="mp-kv__label">{{ dash(p.productName) }}</text>
|
||||
|
||||
@@ -118,10 +118,7 @@ onShow(() => load(true))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无计划</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无计划" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -106,10 +106,7 @@ onShow(() => load(true))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无审批单据</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无审批单据" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
|
||||
@@ -173,10 +173,10 @@ onShow(() => load(true))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">{{ activeTab === 'unsettled' ? '暂无待结算计划' : '暂无结算单' }}</text>
|
||||
</view>
|
||||
<mp-empty
|
||||
v-if="!list.length && !loading"
|
||||
:title="activeTab === 'unsettled' ? '暂无待结算计划' : '暂无结算单'"
|
||||
/>
|
||||
<view v-else class="mp-list">
|
||||
<!-- 结算单 -->
|
||||
<template v-if="activeTab === 'bills'">
|
||||
|
||||
@@ -147,9 +147,7 @@ onShow(() => load())
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">产品明细</text>
|
||||
<view v-if="!products.length" class="mp-empty" style="padding: 32rpx 0">
|
||||
<text class="mp-empty__text">暂无明细</text>
|
||||
</view>
|
||||
<mp-empty v-if="!products.length" compact title="暂无明细" />
|
||||
<view v-else class="mp-block__panel">
|
||||
<view v-for="p in products" :key="p.id" class="mp-kv">
|
||||
<text class="mp-kv__label">{{ dash(p.productName) }}</text>
|
||||
|
||||
@@ -131,10 +131,7 @@ onShow(() => load(true))
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view v-if="!list.length && !loading" class="mp-empty">
|
||||
<view class="mp-empty__icon" />
|
||||
<text class="mp-empty__text">暂无运输计划</text>
|
||||
</view>
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无运输计划" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card">
|
||||
<view class="mp-card__head" @click="goDetail(row)">
|
||||
|
||||
@@ -220,20 +220,167 @@
|
||||
padding: 24rpx 0 8rpx;
|
||||
}
|
||||
|
||||
/* 全局空态 · 方案 B 插画文件夹(组件 mp-empty;类名可复用) */
|
||||
.mp-empty {
|
||||
padding: 80rpx 32rpx;
|
||||
padding: 80rpx 40rpx 64rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mp-empty--compact {
|
||||
padding: 40rpx 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.mp-empty__art {
|
||||
position: relative;
|
||||
width: 296rpx;
|
||||
height: 224rpx;
|
||||
margin: 0 auto 36rpx;
|
||||
}
|
||||
|
||||
.mp-empty--compact .mp-empty__art {
|
||||
width: 220rpx;
|
||||
height: 168rpx;
|
||||
margin-bottom: 24rpx;
|
||||
transform: scale(0.92);
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.mp-empty__shadow {
|
||||
position: absolute;
|
||||
left: 48rpx;
|
||||
right: 48rpx;
|
||||
bottom: 12rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 30, 25, 0.08);
|
||||
}
|
||||
|
||||
.mp-empty__folder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 20rpx;
|
||||
transform: translateX(-50%);
|
||||
width: 216rpx;
|
||||
height: 156rpx;
|
||||
}
|
||||
|
||||
.mp-empty__folder-tab {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: -20rpx;
|
||||
width: 76rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
background: #87d4b0;
|
||||
}
|
||||
|
||||
.mp-empty__folder-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 24rpx 24rpx 28rpx 28rpx;
|
||||
background: linear-gradient(160deg, #b7e6d2, #7dcea0);
|
||||
}
|
||||
|
||||
.mp-empty__folder-front {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 116rpx;
|
||||
border-radius: 20rpx 28rpx 28rpx 28rpx;
|
||||
background: linear-gradient(180deg, #ecfdf5, #dff7ec);
|
||||
border: 2rpx solid rgba(8, 127, 87, 0.18);
|
||||
box-shadow: 0 16rpx 36rpx rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
|
||||
.mp-empty__lines {
|
||||
position: absolute;
|
||||
left: 36rpx;
|
||||
right: 36rpx;
|
||||
top: 36rpx;
|
||||
}
|
||||
|
||||
.mp-empty__line {
|
||||
display: block;
|
||||
height: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #cfe9dc;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mp-empty__line--mid {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.mp-empty__line--short {
|
||||
width: 48%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mp-empty__title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 650;
|
||||
color: var(--mp-text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mp-empty__desc {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--mp-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mp-empty__actions {
|
||||
margin-top: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.mp-empty__actions .mp-btn {
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
/* 兼容旧写法:仅圆点 icon 时也渲染为文件夹插画占位(无内部结构则显示浅绿底) */
|
||||
.mp-empty__icon {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin: 0 auto 24rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #edf5f1, #dcebe4);
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(160deg, #b7e6d2, #7dcea0);
|
||||
position: relative;
|
||||
box-shadow: 0 12rpx 28rpx rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
|
||||
.mp-empty__icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: -12rpx;
|
||||
width: 48rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
background: #87d4b0;
|
||||
}
|
||||
|
||||
.mp-empty__icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
right: 12rpx;
|
||||
bottom: 16rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 14rpx 18rpx 18rpx 18rpx;
|
||||
background: linear-gradient(180deg, #ecfdf5, #dff7ec);
|
||||
border: 2rpx solid rgba(8, 127, 87, 0.16);
|
||||
}
|
||||
|
||||
.mp-empty__text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: var(--mp-muted);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/
|
||||
let clearingAuth = false
|
||||
// TODO(config-hardcoded): 以下加密参数为临时写死,后续应迁移至构建期注入或安全配置下发。
|
||||
// 写死配置:不从环境变量读取,避免小程序端注入差异导致行为不一致。
|
||||
let API_ENCRYPT_ENABLED = true
|
||||
// 移动端(H5 + 微信小程序)统一走明文 JSON,便于联调排查入参;Web 管理端仍可单独开加密。
|
||||
const API_ENCRYPT_ENABLED = false
|
||||
const API_ENCRYPT_FAIL_OPEN = false
|
||||
const API_ENCRYPT_FIXED_SECRET = 'shanxia-web-api-crypto-fixed-secret'
|
||||
// TODO(config-hardcoded): 排除路径清单后续改为集中配置,避免多端重复维护。
|
||||
@@ -14,11 +15,6 @@ const API_ENCRYPT_EXCLUDE_PATHS = String(
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序运行时不保证 WebCrypto(getRandomValues/subtle),关闭前端加密避免全链路请求失败。
|
||||
API_ENCRYPT_ENABLED = false
|
||||
// #endif
|
||||
|
||||
function appendTimestampQuery(url) {
|
||||
const base = String(url || '')
|
||||
const ts = Date.now()
|
||||
|
||||
@@ -902,20 +902,140 @@ button {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── 空态 ── */
|
||||
/* ── 空态(方案 B · 插画文件夹,对齐 miniprogram/components/mp-empty) ── */
|
||||
.mp-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
padding: 40px 20px 32px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mp-empty__art {
|
||||
position: relative;
|
||||
width: 148px;
|
||||
height: 112px;
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
.mp-empty__shadow {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 6px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 30, 25, 0.08);
|
||||
}
|
||||
|
||||
.mp-empty__folder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
width: 108px;
|
||||
height: 78px;
|
||||
}
|
||||
|
||||
.mp-empty__folder-tab {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: -10px;
|
||||
width: 38px;
|
||||
height: 16px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: #87d4b0;
|
||||
}
|
||||
|
||||
.mp-empty__folder-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px 12px 14px 14px;
|
||||
background: linear-gradient(160deg, #b7e6d2, #7dcea0);
|
||||
}
|
||||
|
||||
.mp-empty__folder-front {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 58px;
|
||||
border-radius: 10px 14px 14px 14px;
|
||||
background: linear-gradient(180deg, #ecfdf5, #dff7ec);
|
||||
border: 1px solid rgba(8, 127, 87, 0.18);
|
||||
box-shadow: 0 8px 18px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
|
||||
.mp-empty__lines {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.mp-empty__line {
|
||||
display: block;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #cfe9dc;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mp-empty__line--mid { width: 70%; }
|
||||
.mp-empty__line--short { width: 48%; margin-bottom: 0; }
|
||||
|
||||
.mp-empty__title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mp-empty__desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mp-empty__actions {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mp-empty__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 12px;
|
||||
border-radius: 50%;
|
||||
background: #edf5f1;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(160deg, #b7e6d2, #7dcea0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mp-empty__icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: -6px;
|
||||
width: 22px;
|
||||
height: 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: #87d4b0;
|
||||
}
|
||||
|
||||
.mp-empty__icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: 8px;
|
||||
height: 34px;
|
||||
border-radius: 8px 10px 10px 10px;
|
||||
background: linear-gradient(180deg, #ecfdf5, #dff7ec);
|
||||
border: 1px solid rgba(8, 127, 87, 0.16);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>山峡化工 · 产品分类树形展示 Demo</title>
|
||||
<link rel="stylesheet" href="shanxia-mp-base.css" />
|
||||
<style>
|
||||
/* 方案切换条(Demo 专用,非业务 UI) */
|
||||
.approach-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 10px 12px 8px;
|
||||
}
|
||||
.approach-bar__title {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.approach-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.approach-tab {
|
||||
flex: 1;
|
||||
padding: 8px 4px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
background: #f3f7f5;
|
||||
border: 1px solid transparent;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
.approach-tab.active {
|
||||
color: var(--brand-green-dark);
|
||||
background: var(--brand-green-soft);
|
||||
border-color: #b7e6d2;
|
||||
}
|
||||
.approach-tab small {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
opacity: 0.85;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.approach-note {
|
||||
margin: 10px 12px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.approach-note strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.approach-note .rec {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-green-soft);
|
||||
color: var(--brand-green-dark);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
|
||||
.mp-subpage-back { color: #fff; font-size: 13px; opacity: 0.95; }
|
||||
.header-tools {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.header-tools .mp-header__action {
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── 方案 A:缩进展开树 ── */
|
||||
.tree-list {
|
||||
margin: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 52px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: #fff;
|
||||
}
|
||||
.tree-row:last-child { border-bottom: none; }
|
||||
.tree-row:active { background: #f7fbf9; }
|
||||
.tree-row.is-l2 { background: #fbfdff; }
|
||||
.tree-row.is-l3 { background: #fafcfb; }
|
||||
.tree-chevron {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.tree-chevron.is-leaf {
|
||||
visibility: hidden;
|
||||
}
|
||||
.tree-chevron.is-open {
|
||||
transform: rotate(90deg);
|
||||
color: var(--brand-green);
|
||||
}
|
||||
.tree-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tree-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.tree-meta {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tree-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: var(--brand-green-dark);
|
||||
background: var(--brand-green-soft);
|
||||
}
|
||||
.icon-btn.more {
|
||||
background: #f1f5f3;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.children-wrap { display: none; }
|
||||
.children-wrap.open { display: block; }
|
||||
|
||||
/* ── 方案 B:逐级下钻 ── */
|
||||
.crumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 12px 0;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.crumb a {
|
||||
color: var(--brand-green-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
.crumb .sep { opacity: 0.5; }
|
||||
.crumb .current { color: var(--text-primary); font-weight: 600; }
|
||||
.drill-list {
|
||||
margin: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.drill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.drill-row:last-child { border-bottom: none; }
|
||||
.drill-row:active { background: #f7fbf9; }
|
||||
.drill-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: var(--brand-green-soft);
|
||||
color: var(--brand-green-dark);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.drill-main { flex: 1; min-width: 0; }
|
||||
.drill-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.drill-sub {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.drill-arrow {
|
||||
color: var(--text-placeholder);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ── 方案 C:扁平 + 全路径 ── */
|
||||
.flat-list {
|
||||
margin: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.flat-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.flat-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.flat-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.flat-card__path {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.flat-card__foot {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
margin-top: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.chip {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.chip.active {
|
||||
background: var(--brand-green-soft);
|
||||
border-color: #b7e6d2;
|
||||
color: var(--brand-green-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 操作菜单 Action Sheet */
|
||||
.cat-as {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%) translateY(100%);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
z-index: 210;
|
||||
padding: 0 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
.cat-as.is-open {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.cat-as__handle {
|
||||
width: 36px; height: 4px; border-radius: 999px;
|
||||
background: rgba(255,255,255,0.55); margin: 0 auto 8px;
|
||||
}
|
||||
.cat-as__head {
|
||||
background: #fff; border-radius: 16px 16px 0 0;
|
||||
padding: 14px 16px 10px; border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.cat-as__eyebrow { display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
|
||||
.cat-as__title { display: block; font-size: 16px; font-weight: 700; color: var(--text-primary); }
|
||||
.cat-as__group {
|
||||
background: #fff; border-radius: 0 0 16px 16px;
|
||||
overflow: hidden; margin-bottom: 8px;
|
||||
}
|
||||
.cat-as__item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
min-height: 56px; padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
width: 100%; text-align: left;
|
||||
}
|
||||
.cat-as__item:last-child { border-bottom: none; }
|
||||
.cat-as__item:active { background: #f7fbf9; }
|
||||
.cat-as__icon {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.cat-as__icon--edit { color: var(--brand-green-dark); background: var(--brand-green-soft); }
|
||||
.cat-as__icon--add { color: #087f57; background: #e9f9f2; }
|
||||
.cat-as__icon--del { color: var(--danger); background: var(--danger-soft); }
|
||||
.cat-as__meta { flex: 1; min-width: 0; }
|
||||
.cat-as__name { display: block; font-size: 15px; font-weight: 600; color: var(--text-primary); }
|
||||
.cat-as__item--danger .cat-as__name { color: var(--danger); }
|
||||
.cat-as__desc { display: block; margin-top: 2px; font-size: 11px; color: var(--text-muted); }
|
||||
.cat-as__arrow { color: #c5d5cd; font-size: 18px; }
|
||||
.cat-as__cancel {
|
||||
width: 100%; min-height: 50px; background: #fff; border-radius: 16px;
|
||||
font-size: 15px; font-weight: 600; color: var(--text-muted);
|
||||
}
|
||||
.cat-as__cancel:active { background: #f3f7f5; }
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 40px;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
background: rgba(31, 42, 38, 0.92);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 100;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="proto-nav">
|
||||
<strong>Demo 导航</strong>
|
||||
<a href="shanxia-mp-index.html">索引</a>
|
||||
<a href="shanxia-mp-workbench-demo.html">工作台</a>
|
||||
<a href="shanxia-mp-crm-demo.html">CRM列表</a>
|
||||
<a class="active" href="shanxia-mp-crm-product-category-demo.html">产品分类树</a>
|
||||
</nav>
|
||||
|
||||
<main class="phone no-tabbar">
|
||||
<!-- 方案切换 -->
|
||||
<div class="approach-bar">
|
||||
<div class="approach-bar__title">移动端树形展示方案对比 · 已采用方案 A(点击切换预览)</div>
|
||||
<div class="approach-tabs">
|
||||
<button class="approach-tab active" type="button" data-approach="a">
|
||||
A 缩进展开
|
||||
<small>推荐</small>
|
||||
</button>
|
||||
<button class="approach-tab" type="button" data-approach="b">
|
||||
B 逐级下钻
|
||||
<small>目录式</small>
|
||||
</button>
|
||||
<button class="approach-tab" type="button" data-approach="c">
|
||||
C 扁平路径
|
||||
<small>搜索友好</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 方案 A ── -->
|
||||
<section class="panel active" id="panel-a">
|
||||
<p class="approach-note">
|
||||
<strong>方案 A · 缩进展开树</strong>
|
||||
<span class="rec">推荐</span><br />
|
||||
对齐 Web 端树形表格:一屏看全层级,点击箭头展开/折叠;右侧「+」新增子分类、「⋯」编辑/删除。适合管理端 CRUD。
|
||||
</p>
|
||||
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<div class="header-tools">
|
||||
<button class="mp-header__action" type="button" id="btn-expand-all">全部展开</button>
|
||||
<button class="mp-header__action" type="button" data-toast="打开新增分类表单">+ 新增</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="filter-chips">
|
||||
<button class="chip active" type="button">全部</button>
|
||||
<button class="chip" type="button">启用</button>
|
||||
<button class="chip" type="button">停用</button>
|
||||
</div>
|
||||
|
||||
<div class="tree-list" id="tree-a"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── 方案 B ── -->
|
||||
<section class="panel" id="panel-b">
|
||||
<p class="approach-note">
|
||||
<strong>方案 B · 逐级下钻</strong><br />
|
||||
类似文件管理器:每次只看当前层;面包屑回退。交互简单、层级深时更清晰,但无法一屏总览整棵树,新增子分类需先进入父级。
|
||||
</p>
|
||||
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button" data-toast="在当前层新增分类">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="crumb" id="drill-crumb"></nav>
|
||||
<div class="drill-list" id="drill-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── 方案 C ── -->
|
||||
<section class="panel" id="panel-c">
|
||||
<p class="approach-note">
|
||||
<strong>方案 C · 扁平列表 + 全路径</strong><br />
|
||||
把树拍平为卡片列表,用「父 / 子 / 孙」全路径展示归属(分隔符 <code> / </code>)。搜索命中直观,但丢失展开折叠的树感,深层级可读性一般。
|
||||
</p>
|
||||
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button" data-toast="打开新增分类表单">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="filter-chips">
|
||||
<button class="chip active" type="button">全部</button>
|
||||
<button class="chip" type="button">启用</button>
|
||||
<button class="chip" type="button">停用</button>
|
||||
</div>
|
||||
|
||||
<div class="flat-list" id="flat-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 遮罩 + 操作 Action Sheet -->
|
||||
<div class="mp-overlay" id="mask"></div>
|
||||
<div class="cat-as" id="action-sheet">
|
||||
<div class="cat-as__handle"></div>
|
||||
<div class="cat-as__head">
|
||||
<span class="cat-as__eyebrow">操作分类</span>
|
||||
<span class="cat-as__title" id="sheet-title">分类名称</span>
|
||||
</div>
|
||||
<div class="cat-as__group">
|
||||
<button class="cat-as__item" type="button" data-toast="编辑分类">
|
||||
<span class="cat-as__icon cat-as__icon--edit">编</span>
|
||||
<span class="cat-as__meta">
|
||||
<span class="cat-as__name">编辑</span>
|
||||
<span class="cat-as__desc">修改名称、上级、状态等</span>
|
||||
</span>
|
||||
<span class="cat-as__arrow">›</span>
|
||||
</button>
|
||||
<button class="cat-as__item" type="button" data-toast="新增子分类">
|
||||
<span class="cat-as__icon cat-as__icon--add">子</span>
|
||||
<span class="cat-as__meta">
|
||||
<span class="cat-as__name">新增子分类</span>
|
||||
<span class="cat-as__desc">在此分类下创建下级</span>
|
||||
</span>
|
||||
<span class="cat-as__arrow">›</span>
|
||||
</button>
|
||||
<button class="cat-as__item cat-as__item--danger" type="button" data-toast="删除(需二次确认)">
|
||||
<span class="cat-as__icon cat-as__icon--del">删</span>
|
||||
<span class="cat-as__meta">
|
||||
<span class="cat-as__name">删除</span>
|
||||
<span class="cat-as__desc">有子分类或关联产品时不可删</span>
|
||||
</span>
|
||||
<span class="cat-as__arrow">›</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="cat-as__cancel" type="button" id="sheet-close">取消</button>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 模拟数据:对齐 Web CRM 产品分类树结构
|
||||
* 字段:id / parentId / categoryName / status / orderNum / children
|
||||
*/
|
||||
const TREE = [
|
||||
{
|
||||
id: '1', parentId: '0', categoryName: '化工原料', status: '0', orderNum: 1,
|
||||
children: [
|
||||
{
|
||||
id: '11', parentId: '1', categoryName: '有机溶剂', status: '0', orderNum: 1,
|
||||
children: [
|
||||
{ id: '111', parentId: '11', categoryName: '醇类', status: '0', orderNum: 1, children: [] },
|
||||
{ id: '112', parentId: '11', categoryName: '酮类', status: '0', orderNum: 2, children: [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '12', parentId: '1', categoryName: '无机盐', status: '0', orderNum: 2,
|
||||
children: [
|
||||
{ id: '121', parentId: '12', categoryName: '硫酸盐', status: '1', orderNum: 1, children: [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2', parentId: '0', categoryName: '精细化学品', status: '0', orderNum: 2,
|
||||
children: [
|
||||
{
|
||||
id: '21', parentId: '2', categoryName: '催化剂', status: '0', orderNum: 1,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: '22', parentId: '2', categoryName: '助剂', status: '0', orderNum: 2,
|
||||
children: [
|
||||
{ id: '221', parentId: '22', categoryName: '分散剂', status: '0', orderNum: 1, children: [] },
|
||||
{ id: '222', parentId: '22', categoryName: '消泡剂', status: '0', orderNum: 2, children: [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3', parentId: '0', categoryName: '包装耗材', status: '1', orderNum: 3,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
|
||||
const statusLabel = (s) => (String(s) === '0' ? '正常' : '停用')
|
||||
const statusTag = (s) =>
|
||||
String(s) === '0'
|
||||
? '<span class="mp-tag mp-tag--success">正常</span>'
|
||||
: '<span class="mp-tag mp-tag--muted">停用</span>'
|
||||
|
||||
/* ── Toast / Sheet ── */
|
||||
const toastEl = document.getElementById('toast')
|
||||
function showToast(msg) {
|
||||
toastEl.textContent = msg
|
||||
toastEl.classList.add('show')
|
||||
clearTimeout(showToast._t)
|
||||
showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1600)
|
||||
}
|
||||
|
||||
const mask = document.getElementById('mask')
|
||||
const sheet = document.getElementById('action-sheet')
|
||||
function openSheet(name) {
|
||||
document.getElementById('sheet-title').textContent = name
|
||||
mask.classList.add('is-open')
|
||||
sheet.classList.add('is-open')
|
||||
}
|
||||
function closeSheet() {
|
||||
mask.classList.remove('is-open')
|
||||
sheet.classList.remove('is-open')
|
||||
}
|
||||
document.getElementById('sheet-close').onclick = closeSheet
|
||||
mask.onclick = closeSheet
|
||||
document.querySelectorAll('[data-toast]').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
showToast(el.getAttribute('data-toast'))
|
||||
if (el.closest('.cat-as')) closeSheet()
|
||||
})
|
||||
})
|
||||
|
||||
/* ── 方案切换 ── */
|
||||
document.querySelectorAll('.approach-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.approach-tab').forEach((t) => t.classList.remove('active'))
|
||||
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active'))
|
||||
tab.classList.add('active')
|
||||
document.getElementById('panel-' + tab.dataset.approach).classList.add('active')
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll('.filter-chips .chip').forEach((chip) => {
|
||||
chip.addEventListener('click', () => {
|
||||
const group = chip.parentElement
|
||||
group.querySelectorAll('.chip').forEach((c) => c.classList.remove('active'))
|
||||
chip.classList.add('active')
|
||||
showToast('筛选:' + chip.textContent.trim() + '(Demo 仅示意)')
|
||||
})
|
||||
})
|
||||
|
||||
/* ── 方案 A:缩进展开树 ── */
|
||||
const expanded = new Set(['1', '11', '2']) // 默认展开部分节点
|
||||
|
||||
function childCount(node) {
|
||||
return (node.children || []).length
|
||||
}
|
||||
|
||||
function renderTreeA() {
|
||||
const root = document.getElementById('tree-a')
|
||||
root.innerHTML = ''
|
||||
|
||||
function renderNodes(nodes, level, container) {
|
||||
nodes.forEach((node) => {
|
||||
const hasKids = childCount(node) > 0
|
||||
const isOpen = expanded.has(node.id)
|
||||
const pad = 12 + level * 18
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.className = 'tree-row' + (level === 1 ? ' is-l2' : level >= 2 ? ' is-l3' : '')
|
||||
row.style.paddingLeft = pad + 'px'
|
||||
row.innerHTML = `
|
||||
<button class="tree-chevron ${hasKids ? '' : 'is-leaf'} ${isOpen ? 'is-open' : ''}" type="button" data-toggle="${node.id}" aria-label="展开折叠">▶</button>
|
||||
<div class="tree-main">
|
||||
<div class="tree-name">${node.categoryName}</div>
|
||||
<div class="tree-meta">
|
||||
${statusTag(node.status)}
|
||||
<span>排序 ${node.orderNum}</span>
|
||||
${hasKids ? `<span>${childCount(node)} 个子类</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree-actions">
|
||||
<button class="icon-btn" type="button" data-toast="新增子分类:${node.categoryName}">+</button>
|
||||
<button class="icon-btn more" type="button" data-more="${node.categoryName}">⋯</button>
|
||||
</div>
|
||||
`
|
||||
container.appendChild(row)
|
||||
|
||||
if (hasKids) {
|
||||
const wrap = document.createElement('div')
|
||||
wrap.className = 'children-wrap' + (isOpen ? ' open' : '')
|
||||
wrap.dataset.wrap = node.id
|
||||
renderNodes(node.children, level + 1, wrap)
|
||||
container.appendChild(wrap)
|
||||
}
|
||||
})
|
||||
}
|
||||
renderNodes(TREE, 0, root)
|
||||
|
||||
root.querySelectorAll('[data-toggle]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const id = btn.getAttribute('data-toggle')
|
||||
if (expanded.has(id)) expanded.delete(id)
|
||||
else expanded.add(id)
|
||||
renderTreeA()
|
||||
})
|
||||
})
|
||||
root.querySelectorAll('[data-toast]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
showToast(btn.getAttribute('data-toast'))
|
||||
})
|
||||
})
|
||||
root.querySelectorAll('[data-more]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
openSheet(btn.getAttribute('data-more'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function collectIds(nodes, set) {
|
||||
nodes.forEach((n) => {
|
||||
if (childCount(n)) {
|
||||
set.add(n.id)
|
||||
collectIds(n.children, set)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let allExpanded = false
|
||||
document.getElementById('btn-expand-all').addEventListener('click', () => {
|
||||
allExpanded = !allExpanded
|
||||
expanded.clear()
|
||||
if (allExpanded) collectIds(TREE, expanded)
|
||||
document.getElementById('btn-expand-all').textContent = allExpanded ? '全部折叠' : '全部展开'
|
||||
renderTreeA()
|
||||
})
|
||||
|
||||
/* ── 方案 B:逐级下钻 ── */
|
||||
let drillStack = [{ id: '0', categoryName: '全部', children: TREE }]
|
||||
|
||||
function currentDrillNode() {
|
||||
return drillStack[drillStack.length - 1]
|
||||
}
|
||||
|
||||
function renderDrill() {
|
||||
const node = currentDrillNode()
|
||||
const crumb = document.getElementById('drill-crumb')
|
||||
crumb.innerHTML = drillStack
|
||||
.map((n, i) => {
|
||||
if (i === drillStack.length - 1) {
|
||||
return `<span class="current">${n.categoryName}</span>`
|
||||
}
|
||||
return `<a href="#" data-crumb="${i}">${n.categoryName}</a><span class="sep">/</span>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
crumb.querySelectorAll('[data-crumb]').forEach((a) => {
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const idx = Number(a.getAttribute('data-crumb'))
|
||||
drillStack = drillStack.slice(0, idx + 1)
|
||||
renderDrill()
|
||||
})
|
||||
})
|
||||
|
||||
const list = document.getElementById('drill-list')
|
||||
const kids = node.children || []
|
||||
if (!kids.length) {
|
||||
list.innerHTML = `<div class="drill-row"><div class="drill-main"><div class="drill-name">暂无子分类</div><div class="drill-sub">可点击右上角新增</div></div></div>`
|
||||
return
|
||||
}
|
||||
|
||||
list.innerHTML = kids
|
||||
.map((n) => {
|
||||
const hasKids = childCount(n) > 0
|
||||
return `
|
||||
<div class="drill-row" data-enter="${n.id}">
|
||||
<div class="drill-icon">${n.categoryName.slice(0, 1)}</div>
|
||||
<div class="drill-main">
|
||||
<div class="drill-name">${n.categoryName}</div>
|
||||
<div class="drill-sub">${statusLabel(n.status)} · 排序 ${n.orderNum}${hasKids ? ' · ' + childCount(n) + ' 个子类' : ' · 末级'}</div>
|
||||
</div>
|
||||
<button class="icon-btn more" type="button" data-more="${n.categoryName}">⋯</button>
|
||||
${hasKids ? '<span class="drill-arrow">›</span>' : '<span class="drill-arrow" style="opacity:0">›</span>'}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
list.querySelectorAll('[data-enter]').forEach((row) => {
|
||||
row.addEventListener('click', (e) => {
|
||||
if (e.target.closest('[data-more]')) return
|
||||
const id = row.getAttribute('data-enter')
|
||||
const next = kids.find((k) => k.id === id)
|
||||
if (next && childCount(next)) {
|
||||
drillStack.push(next)
|
||||
renderDrill()
|
||||
} else if (next) {
|
||||
openSheet(next.categoryName)
|
||||
}
|
||||
})
|
||||
})
|
||||
list.querySelectorAll('[data-more]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
openSheet(btn.getAttribute('data-more'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* ── 方案 C:扁平 + 全路径 ── */
|
||||
function flattenWithPath(nodes, pathParts, out) {
|
||||
nodes.forEach((n) => {
|
||||
const path = [...pathParts, n.categoryName]
|
||||
out.push({ ...n, pathLabel: path.join(' / ') })
|
||||
if (n.children?.length) flattenWithPath(n.children, path, out)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function renderFlat() {
|
||||
const items = flattenWithPath(TREE, [], [])
|
||||
const list = document.getElementById('flat-list')
|
||||
list.innerHTML = items
|
||||
.map(
|
||||
(n) => `
|
||||
<article class="flat-card">
|
||||
<div class="flat-card__head">
|
||||
<div>
|
||||
<div class="flat-card__title">${n.categoryName}</div>
|
||||
<div class="flat-card__path">${n.pathLabel}</div>
|
||||
</div>
|
||||
${statusTag(n.status)}
|
||||
</div>
|
||||
<div class="flat-card__foot">
|
||||
<button class="mp-btn mp-btn--ghost" type="button" data-toast="新增子分类:${n.categoryName}">子分类</button>
|
||||
<button class="mp-btn mp-btn--ghost" type="button" data-more="${n.categoryName}">更多</button>
|
||||
<button class="mp-btn mp-btn--primary" type="button" data-toast="编辑:${n.categoryName}">编辑</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
list.querySelectorAll('[data-toast]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => showToast(btn.getAttribute('data-toast')))
|
||||
})
|
||||
list.querySelectorAll('[data-more]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => openSheet(btn.getAttribute('data-more')))
|
||||
})
|
||||
}
|
||||
|
||||
renderTreeA()
|
||||
renderDrill()
|
||||
renderFlat()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,431 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>山峡化工 · 全局空态 Empty State Demo</title>
|
||||
<link rel="stylesheet" href="shanxia-mp-base.css" />
|
||||
<style>
|
||||
body { background: #e8eeeb; }
|
||||
.page-wrap {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 16px 48px;
|
||||
}
|
||||
.hero {
|
||||
background: var(--header-gradient);
|
||||
color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 22px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.hero h1 { font-size: 20px; margin-bottom: 6px; }
|
||||
.hero p { font-size: 13px; opacity: 0.92; line-height: 1.55; }
|
||||
.pick-bar {
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
z-index: 20;
|
||||
background: rgba(255,255,255,0.96);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.pick-bar__label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pick-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.pick-tabs { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.pick-tab {
|
||||
border: 1px solid var(--border);
|
||||
background: #f6faf8;
|
||||
border-radius: 12px;
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pick-tab small {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.pick-tab.active {
|
||||
color: var(--brand-green-dark);
|
||||
background: var(--brand-green-soft);
|
||||
border-color: #b7e6d2;
|
||||
box-shadow: 0 0 0 2px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
.note {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
margin: 0 0 16px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.note strong { color: var(--text-primary); }
|
||||
.stage {
|
||||
display: none;
|
||||
}
|
||||
.stage.active { display: block; }
|
||||
.phone-frame {
|
||||
max-width: 390px;
|
||||
margin: 0 auto;
|
||||
background: var(--bg-page);
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
min-height: 640px;
|
||||
}
|
||||
.mp-subpage-back { color: #fff; font-size: 13px; opacity: 0.95; }
|
||||
|
||||
/* ════════ 方案公共 ════════ */
|
||||
.empty {
|
||||
padding: 48px 28px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.empty__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.empty__desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.empty__actions {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ════════ A · 柔和圆底 + 清单图标 ════════ */
|
||||
.empty-a__badge {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #e9f9f2, #dcebe4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(10, 159, 109, 0.08);
|
||||
}
|
||||
.empty-a__badge svg { width: 42px; height: 42px; }
|
||||
|
||||
/* ════════ B · 插画卡片(文件夹) ════════ */
|
||||
.empty-b__art {
|
||||
width: 148px;
|
||||
height: 112px;
|
||||
margin: 0 auto 18px;
|
||||
position: relative;
|
||||
}
|
||||
.empty-b__shadow {
|
||||
position: absolute;
|
||||
left: 24px; right: 24px; bottom: 6px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 30, 25, 0.08);
|
||||
filter: blur(2px);
|
||||
}
|
||||
.empty-b__folder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
width: 108px;
|
||||
height: 78px;
|
||||
}
|
||||
.empty-b__folder-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px 12px 14px 14px;
|
||||
background: linear-gradient(160deg, #b7e6d2, #7dcea0);
|
||||
}
|
||||
.empty-b__folder-tab {
|
||||
position: absolute;
|
||||
left: 10px; top: -10px;
|
||||
width: 38px; height: 16px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: #87d4b0;
|
||||
}
|
||||
.empty-b__folder-front {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
height: 58px;
|
||||
border-radius: 10px 14px 14px 14px;
|
||||
background: linear-gradient(180deg, #ecfdf5, #dff7ec);
|
||||
border: 1px solid rgba(8, 127, 87, 0.18);
|
||||
box-shadow: 0 8px 18px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
.empty-b__lines {
|
||||
position: absolute;
|
||||
left: 18px; right: 18px; top: 18px;
|
||||
}
|
||||
.empty-b__lines i {
|
||||
display: block;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #cfe9dc;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.empty-b__lines i:nth-child(2) { width: 70%; }
|
||||
.empty-b__lines i:nth-child(3) { width: 48%; margin-bottom: 0; }
|
||||
|
||||
/* ════════ C · 极简线条图标 ════════ */
|
||||
.empty-c__glyph {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto 14px;
|
||||
color: #9aa8a1;
|
||||
}
|
||||
.empty-c__glyph svg { width: 100%; height: 100%; }
|
||||
.empty-c .empty__title { font-weight: 600; color: #5f6f68; }
|
||||
.empty-c .empty__desc { max-width: 220px; margin-left: auto; margin-right: auto; }
|
||||
|
||||
/* ════════ D · 虚线卡片容器 ════════ */
|
||||
.empty-d__panel {
|
||||
margin: 8px 8px 0;
|
||||
border: 1.5px dashed #b7d9c8;
|
||||
border-radius: 18px;
|
||||
background: #fbfefc;
|
||||
padding: 36px 20px 28px;
|
||||
}
|
||||
.empty-d__icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto 14px;
|
||||
border-radius: 16px;
|
||||
background: var(--brand-green-soft);
|
||||
color: var(--brand-green-dark);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 搜索无结果变体提示条 */
|
||||
.scene-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 12px 16px 0;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
background: #fff7e8;
|
||||
color: #a67a1f;
|
||||
border: 1px solid #f0e0b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="proto-nav">
|
||||
<strong>Demo 导航</strong>
|
||||
<a href="shanxia-mp-index.html">索引</a>
|
||||
<a href="shanxia-mp-pattern-index.html">Pattern</a>
|
||||
<a class="active" href="shanxia-mp-empty-state-demo.html">空态</a>
|
||||
</nav>
|
||||
|
||||
<div class="page-wrap">
|
||||
<header class="hero">
|
||||
<h1>全局空态 Empty State</h1>
|
||||
<p>用于列表无数据 / 搜索无结果。<strong>已采用方案 B · 插画文件夹</strong>,落地组件 <code>miniprogram/components/mp-empty</code>。下方可继续对比未选方案。</p>
|
||||
</header>
|
||||
|
||||
<div class="pick-bar">
|
||||
<div class="pick-bar__label">已采用 B;点击可切换对比其他方案</div>
|
||||
<div class="pick-tabs">
|
||||
<button class="pick-tab" type="button" data-stage="a">A 圆底图标<small>未采用</small></button>
|
||||
<button class="pick-tab active" type="button" data-stage="b">B 插画文件夹<small>已采用</small></button>
|
||||
<button class="pick-tab" type="button" data-stage="c">C 极简线条<small>未采用</small></button>
|
||||
<button class="pick-tab" type="button" data-stage="d">D 虚线卡片<small>未采用</small></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="note" id="note-a" style="display:none">
|
||||
<strong>方案 A · 柔和圆底 + SVG 清单图标</strong><br />
|
||||
保留「圆底」结构,补上可识别图形与标题/说明/主按钮。选型时备选,未采用。
|
||||
</p>
|
||||
<p class="note" id="note-b">
|
||||
<strong>方案 B · 插画风文件夹(已采用)</strong><br />
|
||||
用 CSS 拼出文件夹,品牌绿更明显。对应实现:<code>mp-empty</code> + <code>mp-biz.scss</code> / <code>shanxia-mp-base.css</code> 中的 <code>.mp-empty__*</code>。
|
||||
</p>
|
||||
<p class="note" id="note-c" style="display:none">
|
||||
<strong>方案 C · 极简线条</strong><br />
|
||||
单色 stroke 图标,信息密度最低。适合「搜索无结果」或希望页面更安静的场景。
|
||||
</p>
|
||||
<p class="note" id="note-d" style="display:none">
|
||||
<strong>方案 D · 虚线卡片容器</strong><br />
|
||||
把空态放进虚线面板,CTA 更突出。适合「尚未创建任何数据,引导新增」的首次空态。
|
||||
</p>
|
||||
|
||||
<!-- ── A ── -->
|
||||
<section class="stage" id="stage-a">
|
||||
<div class="phone-frame">
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||||
</div>
|
||||
</header>
|
||||
<span class="scene-tag">场景:列表无数据</span>
|
||||
<div class="empty empty-a">
|
||||
<div class="empty-a__badge" aria-hidden="true">
|
||||
<svg viewBox="0 0 48 48" fill="none">
|
||||
<rect x="10" y="8" width="28" height="34" rx="4" stroke="#0a9f6d" stroke-width="2.2"/>
|
||||
<path d="M16 18h16M16 25h16M16 32h10" stroke="#0a9f6d" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="34" cy="34" r="9" fill="#e9f9f2" stroke="#087f57" stroke-width="2"/>
|
||||
<path d="M34 30v8M30 34h8" stroke="#087f57" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="empty__title">暂无产品分类</p>
|
||||
<p class="empty__desc">还没有分类数据,可先新增一个顶级分类</p>
|
||||
<div class="empty__actions">
|
||||
<button class="mp-btn mp-btn--primary" type="button">新增分类</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── B(已采用) ── -->
|
||||
<section class="stage active" id="stage-b">
|
||||
<div class="phone-frame">
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||||
</div>
|
||||
</header>
|
||||
<span class="scene-tag">场景:列表无数据</span>
|
||||
<div class="empty empty-b">
|
||||
<div class="empty-b__art" aria-hidden="true">
|
||||
<div class="empty-b__shadow"></div>
|
||||
<div class="empty-b__folder">
|
||||
<div class="empty-b__folder-tab"></div>
|
||||
<div class="empty-b__folder-back"></div>
|
||||
<div class="empty-b__folder-front">
|
||||
<div class="empty-b__lines"><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="empty__title">暂无产品分类</p>
|
||||
<p class="empty__desc">分类树为空,创建后即可在产品中引用</p>
|
||||
<div class="empty__actions">
|
||||
<button class="mp-btn mp-btn--ghost" type="button">刷新</button>
|
||||
<button class="mp-btn mp-btn--primary" type="button">新增分类</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── C ── -->
|
||||
<section class="stage" id="stage-c">
|
||||
<div class="phone-frame">
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
<div class="mp-header__search" style="margin-top:10px">
|
||||
<span class="mp-header__search-dot"></span>有机溶剂zzz
|
||||
</div>
|
||||
</header>
|
||||
<span class="scene-tag">场景:搜索无结果</span>
|
||||
<div class="empty empty-c">
|
||||
<div class="empty-c__glyph" aria-hidden="true">
|
||||
<svg viewBox="0 0 64 64" fill="none">
|
||||
<circle cx="28" cy="28" r="14" stroke="currentColor" stroke-width="2.4"/>
|
||||
<path d="M38 38l12 12" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M22 28h12M28 22v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="empty__title">未找到相关分类</p>
|
||||
<p class="empty__desc">试试更短的关键词,或清除筛选后查看全部</p>
|
||||
<div class="empty__actions">
|
||||
<button class="mp-btn mp-btn--ghost" type="button">清除条件</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── D ── -->
|
||||
<section class="stage" id="stage-d">
|
||||
<div class="phone-frame">
|
||||
<header class="mp-header">
|
||||
<div class="mp-header__row">
|
||||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||||
<button class="mp-header__action" type="button">+ 新增</button>
|
||||
</div>
|
||||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||||
</header>
|
||||
<span class="scene-tag">场景:首次引导创建</span>
|
||||
<div class="empty empty-d">
|
||||
<div class="empty-d__panel">
|
||||
<div class="empty-d__icon" aria-hidden="true">+</div>
|
||||
<p class="empty__title">还没有产品分类</p>
|
||||
<p class="empty__desc">建议先建一级分类(如原料、助剂),再按需添加子类</p>
|
||||
<div class="empty__actions">
|
||||
<button class="mp-btn mp-btn--primary" type="button">立即新增</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const notes = {
|
||||
a: document.getElementById('note-a'),
|
||||
b: document.getElementById('note-b'),
|
||||
c: document.getElementById('note-c'),
|
||||
d: document.getElementById('note-d')
|
||||
}
|
||||
document.querySelectorAll('.pick-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
const key = tab.dataset.stage
|
||||
document.querySelectorAll('.pick-tab').forEach((t) => t.classList.remove('active'))
|
||||
document.querySelectorAll('.stage').forEach((s) => s.classList.remove('active'))
|
||||
Object.values(notes).forEach((n) => { n.style.display = 'none' })
|
||||
tab.classList.add('active')
|
||||
document.getElementById('stage-' + key).classList.add('active')
|
||||
notes[key].style.display = 'block'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,6 +41,14 @@
|
||||
<span class="index-card__tag">开发前必读</span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="index-card" href="shanxia-mp-empty-state-demo.html">
|
||||
<div class="index-card__icon" style="background:linear-gradient(145deg,#087f57,#11b37a)">空态</div>
|
||||
<div>
|
||||
<h2>全局空态 Empty State</h2>
|
||||
<p>已采用方案 B · 插画文件夹;组件 <code>mp-empty</code>,CRM/BX/DB/PL 列表已统一。</p>
|
||||
<span class="index-card__tag">已采用 B</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="index-section-title">Tab 主框架(4 个)</p>
|
||||
@@ -85,6 +93,14 @@
|
||||
<div class="index-card__icon" style="background:linear-gradient(145deg,#0a9f6d,#11b37a)">CRM</div>
|
||||
<div><h2>线索管理列表</h2><p>示例:CRM 业务办理页</p></div>
|
||||
</a>
|
||||
<a class="index-card" href="shanxia-mp-crm-product-category-demo.html">
|
||||
<div class="index-card__icon" style="background:linear-gradient(145deg,#087f57,#0a9f6d)">分类</div>
|
||||
<div>
|
||||
<h2>产品分类(树形)</h2>
|
||||
<p>已采用方案 A · 缩进展开;页面 <code>pages/crm/product-category</code>。</p>
|
||||
<span class="index-card__tag">已采用 A</span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="index-card" href="shanxia-mp-bx-demo.html">
|
||||
<div class="index-card__icon" style="background:linear-gradient(145deg,#c9922f,#d6a343)">报销</div>
|
||||
<div><h2>待办任务列表</h2><p>示例:报销审批办理页</p></div>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<h3>核心 Class 速查</h3>
|
||||
<div class="class-list">
|
||||
布局:<code>.mp-navbar</code> <code>.mp-header</code> <code>.mp-footer-bar</code><br>
|
||||
列表:<code>.mp-tabs</code> <code>.mp-card--clickable</code> <code>.mp-list-end</code> <code>.mp-empty</code><br>
|
||||
列表:<code>.mp-tabs</code> <code>.mp-card--clickable</code> <code>.mp-list-end</code> <code>.mp-empty</code>(方案 B · 组件 <code>mp-empty</code>)<br>
|
||||
详情:<code>.mp-detail-hero</code> <code>.mp-block</code> <code>.mp-kv-list</code> <code>.mp-timeline</code><br>
|
||||
表单:<code>.mp-form-group</code> <code>.mp-field</code> <code>.mp-field-row</code> <code>.mp-attach-upload</code><br>
|
||||
弹窗:<code>.mp-overlay</code> <code>.mp-sheet</code> <code>.mp-dialog</code> <code>.mp-action-sheet</code> <code>.mp-toast</code><br>
|
||||
|
||||