银行端口添加客户拜访功能
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Change: 添加银行端客户拜访计划功能
|
||||
|
||||
## Why
|
||||
|
||||
银行端客户经理需要记录和管理客户拜访活动,以便跟踪客户关系、记录营销产品推广情况,并保存现场拜访照片作为凭证。当前系统缺少拜访计划管理功能,无法满足客户关系管理的业务需求。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在银行端添加客户拜访计划创建页面
|
||||
- 支持设置拜访日期、选择客户、定位位置、选择营销产品、填写拜访主题和备注
|
||||
- 支持上传拜访场景照片(拍照或从相册选择)
|
||||
- 添加拜访计划保存功能
|
||||
- 添加拜访计划列表和详情查看功能
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `bank-visit-plan` (新能力)
|
||||
- Affected code:
|
||||
- `src/pagesBank/` - 新增拜访计划相关页面
|
||||
- `src/pagesBank/api/` - 新增拜访计划 API 接口
|
||||
- `src/typings/bank.ts` - 新增拜访计划类型定义
|
||||
@@ -0,0 +1,72 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建拜访计划
|
||||
银行端用户 SHALL 能够创建客户拜访计划,记录拜访的详细信息。
|
||||
|
||||
#### Scenario: 成功创建拜访计划
|
||||
- **WHEN** 用户填写完整的拜访计划信息(日期、客户、位置、拜访主题、照片)
|
||||
- **THEN** 系统 SHALL 保存拜访计划并返回成功提示
|
||||
|
||||
#### Scenario: 创建拜访计划时验证必填字段
|
||||
- **WHEN** 用户提交拜访计划时缺少必填字段(日期、客户、位置、拜访主题、照片)
|
||||
- **THEN** 系统 SHALL 显示相应的错误提示,阻止提交
|
||||
|
||||
#### Scenario: 选择营销产品
|
||||
- **WHEN** 用户点击营销产品选择器
|
||||
- **THEN** 系统 SHALL 显示可选产品列表,支持多选
|
||||
|
||||
#### Scenario: 上传拜访场景图
|
||||
- **WHEN** 用户点击上传按钮
|
||||
- **THEN** 系统 SHALL 提供拍照和从相册选择两种方式
|
||||
|
||||
### Requirement: 拜访计划列表
|
||||
银行端用户 SHALL 能够查看拜访计划列表,支持筛选和搜索。
|
||||
|
||||
#### Scenario: 查看拜访计划列表
|
||||
- **WHEN** 用户进入拜访计划列表页面
|
||||
- **THEN** 系统 SHALL 显示所有拜访计划,按日期倒序排列
|
||||
|
||||
#### Scenario: 按状态筛选拜访计划
|
||||
- **WHEN** 用户选择状态筛选条件(待拜访、已完成、已取消)
|
||||
- **THEN** 系统 SHALL 显示对应状态的拜访计划
|
||||
|
||||
#### Scenario: 搜索拜访计划
|
||||
- **WHEN** 用户输入关键词搜索
|
||||
- **THEN** 系统 SHALL 显示匹配的拜访计划(按客户名称或拜访主题)
|
||||
|
||||
### Requirement: 拜访计划详情
|
||||
银行端用户 SHALL 能够查看拜访计划的详细信息。
|
||||
|
||||
#### Scenario: 查看拜访计划详情
|
||||
- **WHEN** 用户点击拜访计划列表项
|
||||
- **THEN** 系统 SHALL 显示拜访计划的完整信息(日期、客户、位置、营销产品、拜访主题、备注、照片)
|
||||
|
||||
#### Scenario: 更新拜访状态
|
||||
- **WHEN** 用户在详情页更新拜访状态
|
||||
- **THEN** 系统 SHALL 保存状态变更并刷新页面
|
||||
|
||||
#### Scenario: 编辑拜访计划
|
||||
- **WHEN** 用户点击编辑按钮
|
||||
- **THEN** 系统 SHALL 进入编辑模式,允许修改拜访计划信息
|
||||
|
||||
### Requirement: 客户选择器
|
||||
系统 SHALL 提供客户选择器,支持从客户列表中选择目标客户。
|
||||
|
||||
#### Scenario: 打开客户选择器
|
||||
- **WHEN** 用户点击客户选择字段
|
||||
- **THEN** 系统 SHALL 弹出客户列表页面
|
||||
|
||||
#### Scenario: 选择客户
|
||||
- **WHEN** 用户从列表中选择一个客户
|
||||
- **THEN** 系统 SHALL 返回并显示选中的客户信息
|
||||
|
||||
### Requirement: 位置定位
|
||||
系统 SHALL 支持获取当前位置或手动输入地址。
|
||||
|
||||
#### Scenario: 自动定位
|
||||
- **WHEN** 用户点击定位按钮
|
||||
- **THEN** 系统 SHALL 获取当前位置并显示地址信息
|
||||
|
||||
#### Scenario: 手动输入地址
|
||||
- **WHEN** 用户手动输入地址
|
||||
- **THEN** 系统 SHALL 保存用户输入的地址信息
|
||||
@@ -0,0 +1,42 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 类型定义和 API 接口
|
||||
- [ ] 1.1 在 `src/typings/bank.ts` 中添加拜访计划相关类型定义
|
||||
- [ ] 1.2 在 `src/pagesBank/api/index.ts` 中添加拜访计划 API 接口
|
||||
- [ ] 1.3 在 `src/pagesBank/mock/index.ts` 中添加拜访计划 Mock 数据
|
||||
|
||||
## 2. 拜访计划创建页面
|
||||
- [ ] 2.1 创建 `src/pagesBank/visit/create.vue` 拜访计划创建页面
|
||||
- [ ] 2.2 实现日期选择器组件
|
||||
- [ ] 2.3 实现客户选择器(弹出客户列表)
|
||||
- [ ] 2.4 实现位置定位功能(地图定位/文本输入)
|
||||
- [ ] 2.5 实现营销产品多选功能
|
||||
- [ ] 2.6 实现拜访主题和备注输入
|
||||
- [ ] 2.7 实现图片上传功能(拍照/相册选择)
|
||||
- [ ] 2.8 实现表单验证和提交功能
|
||||
|
||||
## 3. 拜访计划列表页面
|
||||
- [ ] 3.1 创建 `src/pagesBank/visit/list.vue` 拜访计划列表页面
|
||||
- [ ] 3.2 实现拜访计划列表展示
|
||||
- [ ] 3.3 实现状态筛选(待拜访、已完成、已取消)
|
||||
- [ ] 3.4 实现搜索功能
|
||||
|
||||
## 4. 拜访计划详情页面
|
||||
- [ ] 4.1 创建 `src/pagesBank/visit/detail.vue` 拜访计划详情页面
|
||||
- [ ] 4.2 实现拜访计划详情展示
|
||||
- [ ] 4.3 实现拜访状态更新功能
|
||||
- [ ] 4.4 实现拜访记录编辑功能
|
||||
|
||||
## 5. 路由配置
|
||||
- [ ] 5.1 在 `src/pages.json` 中添加拜访计划相关路由配置
|
||||
|
||||
## 6. 导航入口
|
||||
- [ ] 6.1 在银行端首页或客户详情页添加"创建拜访计划"入口
|
||||
- [ ] 6.2 在客户详情页添加"拜访记录"入口
|
||||
|
||||
## 7. 测试和验证
|
||||
- [ ] 7.1 测试拜访计划创建流程
|
||||
- [ ] 7.2 测试拜访计划列表展示和筛选
|
||||
- [ ] 7.3 测试拜访计划详情查看和编辑
|
||||
- [ ] 7.4 测试图片上传功能
|
||||
- [ ] 7.5 测试表单验证
|
||||
@@ -0,0 +1,29 @@
|
||||
# Change: 修改银行端客户拜访计划功能
|
||||
|
||||
## Why
|
||||
|
||||
当前拜访计划功能在创建时要求填写位置和上传拜访场景图,但在实际业务场景中,这些信息应该在拜访完成时才需要填写。创建拜访计划时只需要记录基本的拜访安排(日期、客户、主题等),而在标记拜访完成时才需要补充位置信息和上传现场照片。这样的流程更符合实际业务需求。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **创建拜访计划页面** (`/pagesBank/visit/create`) 修改:
|
||||
- 移除位置输入和定位功能
|
||||
- 移除上传拜访场景图功能
|
||||
- 保留日期、客户选择、营销产品、拜访主题、备注字段
|
||||
- 更新表单验证逻辑,不再验证位置和照片
|
||||
|
||||
- **拜访计划详情页面** (`/pagesBank/visit/detail?id={}`) 修改:
|
||||
- 在标记完成时,要求填写位置信息(模拟从地图获取地址,默认使用"广东省茂名市")
|
||||
- 在标记完成时,要求上传拜访场景图
|
||||
- 添加位置和照片的必填验证
|
||||
- 更新状态更新流程,先验证位置和照片后再更新状态
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `bank-visit-plan` (修改)
|
||||
- Affected code:
|
||||
- `src/pagesBank/visit/create.vue` - 移除位置和照片相关代码
|
||||
- `src/pagesBank/visit/detail.vue` - 添加完成时的位置和照片填写功能
|
||||
- `src/pagesBank/api/index.ts` - 更新 `updateVisitStatus` API 支持位置和照片参数
|
||||
- `src/typings/bank.ts` - 更新 `CreateVisitPlanParams` 类型定义
|
||||
- `openspec/specs/bank-visit-plan/spec.md` - 更新需求规格说明
|
||||
@@ -0,0 +1,91 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建拜访计划
|
||||
银行端用户 SHALL 能够创建客户拜访计划,记录拜访的基本信息。
|
||||
|
||||
#### Scenario: 成功创建拜访计划
|
||||
- **WHEN** 用户填写完整的拜访计划信息(日期、客户、拜访主题)
|
||||
- **THEN** 系统 SHALL 保存拜访计划并返回成功提示
|
||||
|
||||
#### Scenario: 创建拜访计划时验证必填字段
|
||||
- **WHEN** 用户提交拜访计划时缺少必填字段(日期、客户、拜访主题)
|
||||
- **THEN** 系统 SHALL 显示相应的错误提示,阻止提交
|
||||
|
||||
#### Scenario: 选择营销产品
|
||||
- **WHEN** 用户点击营销产品选择器
|
||||
- **THEN** 系统 SHALL 显示可选产品列表,支持多选
|
||||
|
||||
### Requirement: 拜访计划列表
|
||||
银行端用户 SHALL 能够查看拜访计划列表,支持筛选和搜索。
|
||||
|
||||
#### Scenario: 查看拜访计划列表
|
||||
- **WHEN** 用户进入拜访计划列表页面
|
||||
- **THEN** 系统 SHALL 显示所有拜访计划,按日期倒序排列
|
||||
|
||||
#### Scenario: 按状态筛选拜访计划
|
||||
- **WHEN** 用户选择状态筛选条件(待拜访、已完成、已取消)
|
||||
- **THEN** 系统 SHALL 显示对应状态的拜访计划
|
||||
|
||||
#### Scenario: 搜索拜访计划
|
||||
- **WHEN** 用户输入关键词搜索
|
||||
- **THEN** 系统 SHALL 显示匹配的拜访计划(按客户名称或拜访主题)
|
||||
|
||||
### Requirement: 拜访计划详情
|
||||
银行端用户 SHALL 能够查看拜访计划的详细信息。
|
||||
|
||||
#### Scenario: 查看拜访计划详情
|
||||
- **WHEN** 用户点击拜访计划列表项
|
||||
- **THEN** 系统 SHALL 显示拜访计划的完整信息(日期、客户、营销产品、拜访主题、备注)
|
||||
|
||||
#### Scenario: 标记拜访完成
|
||||
- **WHEN** 用户点击"标记完成"按钮
|
||||
- **THEN** 系统 SHALL 弹出填写位置和上传照片的表单
|
||||
- **AND** 用户填写位置信息(支持自动定位或手动输入,默认地址为"广东省茂名市")
|
||||
- **AND** 用户上传至少一张拜访场景图
|
||||
- **AND** 系统 SHALL 验证位置和照片必填
|
||||
- **AND** 验证通过后更新拜访状态为"已完成"
|
||||
|
||||
#### Scenario: 标记完成时验证必填字段
|
||||
- **WHEN** 用户提交完成标记时缺少位置信息或照片
|
||||
- **THEN** 系统 SHALL 显示相应的错误提示,阻止状态更新
|
||||
|
||||
#### Scenario: 编辑拜访计划
|
||||
- **WHEN** 用户点击编辑按钮
|
||||
- **THEN** 系统 SHALL 进入编辑模式,允许修改拜访计划信息(日期、主题、备注)
|
||||
|
||||
### Requirement: 客户选择器
|
||||
系统 SHALL 提供客户选择器,支持从客户列表中选择目标客户。
|
||||
|
||||
#### Scenario: 打开客户选择器
|
||||
- **WHEN** 用户点击客户选择字段
|
||||
- **THEN** 系统 SHALL 弹出客户列表页面
|
||||
|
||||
#### Scenario: 选择客户
|
||||
- **WHEN** 用户从列表中选择一个客户
|
||||
- **THEN** 系统 SHALL 返回并显示选中的客户信息
|
||||
|
||||
### Requirement: 位置定位
|
||||
系统 SHALL 支持获取当前位置或手动输入地址。
|
||||
|
||||
#### Scenario: 自动定位
|
||||
- **WHEN** 用户点击定位按钮
|
||||
- **THEN** 系统 SHALL 获取当前位置并显示地址信息
|
||||
|
||||
#### Scenario: 手动输入地址
|
||||
- **WHEN** 用户手动输入地址
|
||||
- **THEN** 系统 SHALL 保存用户输入的地址信息
|
||||
|
||||
#### Scenario: 使用默认地址
|
||||
- **WHEN** 用户未填写位置信息
|
||||
- **THEN** 系统 SHALL 使用默认地址"广东省茂名市"
|
||||
|
||||
### Requirement: 上传拜访场景图
|
||||
系统 SHALL 支持上传拜访场景照片。
|
||||
|
||||
#### Scenario: 上传拜访场景图
|
||||
- **WHEN** 用户点击上传按钮
|
||||
- **THEN** 系统 SHALL 提供拍照和从相册选择两种方式
|
||||
|
||||
#### Scenario: 验证照片数量
|
||||
- **WHEN** 用户标记拜访完成时
|
||||
- **THEN** 系统 SHALL 要求至少上传一张拜访场景图
|
||||
@@ -0,0 +1,67 @@
|
||||
# Tasks: 修改银行端客户拜访计划功能
|
||||
|
||||
## 任务列表
|
||||
|
||||
### 1. 修改类型定义
|
||||
- [x] 更新 `src/typings/bank.ts` 中的 `CreateVisitPlanParams` 接口
|
||||
- 移除 `location`、`latitude`、`longitude`、`photos` 字段
|
||||
- 这些字段将在标记完成时通过 `updateVisitStatus` API 提供
|
||||
- 新增 `CompleteVisitPlanParams` 接口用于标记完成时的参数
|
||||
|
||||
### 2. 修改创建拜访计划页面
|
||||
- [x] 修改 `src/pagesBank/visit/create.vue`
|
||||
- 移除位置输入和定位功能
|
||||
- 移除上传拜访场景图功能
|
||||
- 更新表单验证逻辑
|
||||
- 移除位置验证
|
||||
- 移除照片验证
|
||||
- 更新提交表单逻辑
|
||||
- 移除位置和照片参数
|
||||
|
||||
### 3. 修改拜访计划详情页面
|
||||
- [x] 修改 `src/pagesBank/visit/detail.vue`
|
||||
- 添加标记完成时的表单状态管理
|
||||
- 添加位置输入和定位功能
|
||||
- 添加上传拜访场景图功能
|
||||
- 修改 `handleUpdateStatus` 函数
|
||||
- 点击"标记完成"时弹出填写位置和上传照片的表单
|
||||
- 验证位置和照片必填
|
||||
- 验证通过后调用更新状态 API
|
||||
- 更新模板部分,添加位置和照片填写表单
|
||||
|
||||
### 4. 修改 API 接口
|
||||
- [x] 修改 `src/pagesBank/api/index.ts`
|
||||
- 更新 `createVisitPlan` 函数
|
||||
- 移除位置和照片参数处理
|
||||
- 创建时位置和照片为空
|
||||
- 更新 `updateVisitStatus` 函数
|
||||
- 添加 `location`、`latitude`、`longitude`、`photos` 参数
|
||||
- 更新拜访计划时保存位置和照片信息
|
||||
- 更新 `updateVisitPlan` 函数
|
||||
- 移除位置和照片相关字段的处理
|
||||
|
||||
### 5. 更新 Mock 数据
|
||||
- [x] 修改 `src/pagesBank/mock/index.ts`
|
||||
- 更新 `mockVisitPlans` 数据结构
|
||||
- 确保待拜访的拜访计划位置和照片为空
|
||||
- 已完成的拜访计划包含位置和照片信息
|
||||
|
||||
### 6. 更新规格说明文档
|
||||
- [x] 更新 `openspec/specs/bank-visit-plan/spec.md`
|
||||
- 应用修改提案中的规格变更
|
||||
- 更新创建拜访计划的需求
|
||||
- 更新拜访计划详情的需求
|
||||
- 添加标记完成时的场景
|
||||
|
||||
### 7. 测试验证
|
||||
- [x] 测试创建拜访计划功能
|
||||
- 验证不填写位置和照片可以成功创建
|
||||
- 验证必填字段验证正常工作
|
||||
- [x] 测试标记拜访完成功能
|
||||
- 验证必须填写位置和上传照片才能标记完成
|
||||
- 验证自动定位功能正常
|
||||
- 验证默认地址"广东省茂名市"正常使用
|
||||
- 验证照片上传功能正常
|
||||
- [x] 测试拜访计划详情页面
|
||||
- 验证位置和照片信息正确显示
|
||||
- 验证编辑功能正常工作
|
||||
95
openspec/specs/bank-visit-plan/spec.md
Normal file
95
openspec/specs/bank-visit-plan/spec.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# bank-visit-plan Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-bank-visit-plan. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 创建拜访计划
|
||||
银行端用户 SHALL 能够创建客户拜访计划,记录拜访的基本信息。
|
||||
|
||||
#### Scenario: 成功创建拜访计划
|
||||
- **WHEN** 用户填写完整的拜访计划信息(日期、客户、拜访主题)
|
||||
- **THEN** 系统 SHALL 保存拜访计划并返回成功提示
|
||||
|
||||
#### Scenario: 创建拜访计划时验证必填字段
|
||||
- **WHEN** 用户提交拜访计划时缺少必填字段(日期、客户、拜访主题)
|
||||
- **THEN** 系统 SHALL 显示相应的错误提示,阻止提交
|
||||
|
||||
#### Scenario: 选择营销产品
|
||||
- **WHEN** 用户点击营销产品选择器
|
||||
- **THEN** 系统 SHALL 显示可选产品列表,支持多选
|
||||
|
||||
### Requirement: 拜访计划列表
|
||||
银行端用户 SHALL 能够查看拜访计划列表,支持筛选和搜索。
|
||||
|
||||
#### Scenario: 查看拜访计划列表
|
||||
- **WHEN** 用户进入拜访计划列表页面
|
||||
- **THEN** 系统 SHALL 显示所有拜访计划,按日期倒序排列
|
||||
|
||||
#### Scenario: 按状态筛选拜访计划
|
||||
- **WHEN** 用户选择状态筛选条件(待拜访、已完成、已取消)
|
||||
- **THEN** 系统 SHALL 显示对应状态的拜访计划
|
||||
|
||||
#### Scenario: 搜索拜访计划
|
||||
- **WHEN** 用户输入关键词搜索
|
||||
- **THEN** 系统 SHALL 显示匹配的拜访计划(按客户名称或拜访主题)
|
||||
|
||||
### Requirement: 拜访计划详情
|
||||
银行端用户 SHALL 能够查看拜访计划的详细信息。
|
||||
|
||||
#### Scenario: 查看拜访计划详情
|
||||
- **WHEN** 用户点击拜访计划列表项
|
||||
- **THEN** 系统 SHALL 显示拜访计划的完整信息(日期、客户、营销产品、拜访主题、备注)
|
||||
|
||||
#### Scenario: 标记拜访完成
|
||||
- **WHEN** 用户点击"标记完成"按钮
|
||||
- **THEN** 系统 SHALL 弹出填写位置和上传照片的表单
|
||||
- **AND** 用户填写位置信息(支持自动定位或手动输入,默认地址为"广东省茂名市")
|
||||
- **AND** 用户上传至少一张拜访场景图
|
||||
- **AND** 系统 SHALL 验证位置和照片必填
|
||||
- **AND** 验证通过后更新拜访状态为"已完成"
|
||||
|
||||
#### Scenario: 标记完成时验证必填字段
|
||||
- **WHEN** 用户提交完成标记时缺少位置信息或照片
|
||||
- **THEN** 系统 SHALL 显示相应的错误提示,阻止状态更新
|
||||
|
||||
#### Scenario: 编辑拜访计划
|
||||
- **WHEN** 用户点击编辑按钮
|
||||
- **THEN** 系统 SHALL 进入编辑模式,允许修改拜访计划信息(日期、主题、备注)
|
||||
|
||||
### Requirement: 客户选择器
|
||||
系统 SHALL 提供客户选择器,支持从客户列表中选择目标客户。
|
||||
|
||||
#### Scenario: 打开客户选择器
|
||||
- **WHEN** 用户点击客户选择字段
|
||||
- **THEN** 系统 SHALL 弹出客户列表页面
|
||||
|
||||
#### Scenario: 选择客户
|
||||
- **WHEN** 用户从列表中选择一个客户
|
||||
- **THEN** 系统 SHALL 返回并显示选中的客户信息
|
||||
|
||||
### Requirement: 位置定位
|
||||
系统 SHALL 支持获取当前位置或手动输入地址。
|
||||
|
||||
#### Scenario: 自动定位
|
||||
- **WHEN** 用户点击定位按钮
|
||||
- **THEN** 系统 SHALL 获取当前位置并显示地址信息
|
||||
|
||||
#### Scenario: 手动输入地址
|
||||
- **WHEN** 用户手动输入地址
|
||||
- **THEN** 系统 SHALL 保存用户输入的地址信息
|
||||
|
||||
#### Scenario: 使用默认地址
|
||||
- **WHEN** 用户未填写位置信息
|
||||
- **THEN** 系统 SHALL 使用默认地址"广东省茂名市"
|
||||
|
||||
### Requirement: 上传拜访场景图
|
||||
系统 SHALL 支持上传拜访场景照片。
|
||||
|
||||
#### Scenario: 上传拜访场景图
|
||||
- **WHEN** 用户点击上传按钮
|
||||
- **THEN** 系统 SHALL 提供拍照和从相册选择两种方式
|
||||
|
||||
#### Scenario: 验证照片数量
|
||||
- **WHEN** 用户标记拜访完成时
|
||||
- **THEN** 系统 SHALL 要求至少上传一张拜访场景图
|
||||
|
||||
@@ -246,6 +246,30 @@
|
||||
"navigationBarTitleText": "客户管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "customer/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "客户详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visit/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "拜访计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visit/create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "创建拜访计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visit/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "拜访计划详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "me/index",
|
||||
"style": {
|
||||
|
||||
@@ -3,13 +3,19 @@ import type {
|
||||
AuditItem,
|
||||
AuditStatus,
|
||||
BankCustomer,
|
||||
WithdrawAuditDetail
|
||||
WithdrawAuditDetail,
|
||||
VisitPlan,
|
||||
CreateVisitPlanParams,
|
||||
MarketingProduct
|
||||
} from '@/typings/bank'
|
||||
import { VisitStatus } from '@/typings/bank'
|
||||
import {
|
||||
mockBankStats,
|
||||
mockAuditList,
|
||||
getMockWithdrawDetail,
|
||||
mockCustomerList
|
||||
mockCustomerList,
|
||||
mockVisitPlans,
|
||||
mockMarketingProducts
|
||||
} from '../mock'
|
||||
|
||||
/** 获取银行端首页统计 */
|
||||
@@ -173,3 +179,150 @@ export function getCustomerWithdraws(params: {
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取营销产品列表 */
|
||||
export function getMarketingProducts(): Promise<MarketingProduct[]> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockMarketingProducts)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取拜访计划列表 */
|
||||
export function getVisitPlanList(params: {
|
||||
status?: string
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
keyword?: string
|
||||
}): Promise<{ list: VisitPlan[]; total: number }> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
let list = [...mockVisitPlans]
|
||||
|
||||
// 状态筛选
|
||||
if (params.status) {
|
||||
list = list.filter(item => item.status === params.status)
|
||||
}
|
||||
|
||||
// 关键词筛选
|
||||
if (params.keyword) {
|
||||
const keyword = params.keyword.toLowerCase()
|
||||
list = list.filter(item =>
|
||||
item.customerName.toLowerCase().includes(keyword) ||
|
||||
item.topic.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按日期倒序
|
||||
list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
|
||||
resolve({ list, total: list.length })
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取拜访计划详情 */
|
||||
export function getVisitPlanDetail(id: string): Promise<VisitPlan | null> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const plan = mockVisitPlans.find(item => item.id === id)
|
||||
resolve(plan || null)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建拜访计划 */
|
||||
export function createVisitPlan(data: CreateVisitPlanParams): Promise<VisitPlan> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const customer = mockCustomerList.find(c => c.id === data.customerId)
|
||||
const products = mockMarketingProducts.filter(p => data.productIds.includes(p.id))
|
||||
|
||||
const newPlan: VisitPlan = {
|
||||
id: `V${Date.now()}`,
|
||||
customerId: data.customerId,
|
||||
customerName: customer?.merchantName || '',
|
||||
date: data.date,
|
||||
location: '',
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
products,
|
||||
topic: data.topic,
|
||||
remark: data.remark,
|
||||
photos: [],
|
||||
status: VisitStatus.PENDING,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
mockVisitPlans.unshift(newPlan)
|
||||
resolve(newPlan)
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新拜访计划 */
|
||||
export function updateVisitPlan(id: string, data: Partial<CreateVisitPlanParams>): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const plan = mockVisitPlans.find(item => item.id === id)
|
||||
if (plan) {
|
||||
if (data.customerId) {
|
||||
const customer = mockCustomerList.find(c => c.id === data.customerId)
|
||||
plan.customerId = data.customerId
|
||||
plan.customerName = customer?.merchantName || ''
|
||||
}
|
||||
if (data.date) plan.date = data.date
|
||||
if (data.productIds) {
|
||||
plan.products = mockMarketingProducts.filter(p => data.productIds!.includes(p.id))
|
||||
}
|
||||
if (data.topic) plan.topic = data.topic
|
||||
if (data.remark !== undefined) plan.remark = data.remark
|
||||
plan.updatedAt = new Date().toISOString()
|
||||
}
|
||||
resolve(true)
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新拜访状态 */
|
||||
export function updateVisitStatus(
|
||||
id: string,
|
||||
status: VisitStatus,
|
||||
location?: string,
|
||||
latitude?: number,
|
||||
longitude?: number,
|
||||
photos?: string[]
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const plan = mockVisitPlans.find(item => item.id === id)
|
||||
if (plan) {
|
||||
plan.status = status
|
||||
plan.updatedAt = new Date().toISOString()
|
||||
// 标记完成时更新位置和照片
|
||||
if (status === VisitStatus.COMPLETED) {
|
||||
if (location) plan.location = location
|
||||
if (latitude !== undefined) plan.latitude = latitude
|
||||
if (longitude !== undefined) plan.longitude = longitude
|
||||
if (photos) plan.photos = photos
|
||||
}
|
||||
}
|
||||
resolve(true)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除拜访计划 */
|
||||
export function deleteVisitPlan(id: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const index = mockVisitPlans.findIndex(item => item.id === id)
|
||||
if (index > -1) {
|
||||
mockVisitPlans.splice(index, 1)
|
||||
}
|
||||
resolve(true)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,6 +92,18 @@ function handleFreeze() {
|
||||
})
|
||||
}
|
||||
|
||||
function handleCreateVisit() {
|
||||
uni.navigateTo({
|
||||
url: `/pagesBank/visit/create?customerId=${id.value}`
|
||||
})
|
||||
}
|
||||
|
||||
function handleVisitRecords() {
|
||||
uni.navigateTo({
|
||||
url: `/pagesBank/visit/list?customerId=${id.value}`
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.id) {
|
||||
id.value = options.id
|
||||
@@ -189,6 +201,14 @@ onLoad((options) => {
|
||||
<text class="i-carbon-wallet"></text>
|
||||
提现记录
|
||||
</view>
|
||||
<view class="action-item" @click="handleCreateVisit">
|
||||
<text class="i-carbon-calendar-add"></text>
|
||||
创建拜访
|
||||
</view>
|
||||
<view class="action-item" @click="handleVisitRecords">
|
||||
<text class="i-carbon-calendar"></text>
|
||||
拜访记录
|
||||
</view>
|
||||
<view class="action-item" :class="{ danger: detail.status !== 'frozen' }" @click="handleFreeze">
|
||||
<text :class="detail.status === 'frozen' ? 'i-carbon-locked' : 'i-carbon-unlocked'"></text>
|
||||
{{ detail.status === 'frozen' ? '解冻账户' : '冻结账户' }}
|
||||
|
||||
@@ -15,7 +15,8 @@ const bankStore = useBankStore()
|
||||
const quickActions = [
|
||||
{ icon: 'i-carbon-task-approved', label: '待审核', path: '/pagesBank/audit/list' },
|
||||
{ icon: 'i-carbon-group', label: '客户管理', path: '/pagesBank/customer/list' },
|
||||
{ icon: 'i-carbon-report', label: '数据报表', path: '' }, // 暂未实现
|
||||
{ icon: 'i-carbon-calendar', label: '拜访计划', path: '/pagesBank/visit/list' },
|
||||
{ icon: 'i-carbon-add', label: '创建拜访', path: '/pagesBank/visit/create' },
|
||||
{ icon: 'i-carbon-settings', label: '设置', path: '/pagesBank/me/index' },
|
||||
]
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import type {
|
||||
BankStats,
|
||||
AuditItem,
|
||||
BankCustomer,
|
||||
WithdrawAuditDetail
|
||||
WithdrawAuditDetail,
|
||||
VisitPlan,
|
||||
MarketingProduct
|
||||
} from '@/typings/bank'
|
||||
import { AuditStatus, AuditType } from '@/typings/bank'
|
||||
import { AuditStatus, AuditType, VisitStatus } from '@/typings/bank'
|
||||
|
||||
// 统计数据 Mock
|
||||
export const mockBankStats: BankStats = {
|
||||
@@ -109,3 +111,69 @@ export const mockWithdrawHistory = [
|
||||
{ id: 'W005', amount: 10000.00, status: AuditStatus.APPROVED, time: '2024-12-01 09:00', bank: '工商银行(8888)' },
|
||||
{ id: 'W008', amount: 2000.00, status: AuditStatus.REJECTED, time: '2024-11-20 16:20', bank: '工商银行(8888)', reason: '账户信息有误' },
|
||||
]
|
||||
|
||||
// 营销产品 Mock
|
||||
export const mockMarketingProducts: MarketingProduct[] = [
|
||||
{ id: 'P001', name: '公司理财', type: 'credit' },
|
||||
{ id: 'P002', name: '公司贷款', type: 'loan' },
|
||||
{ id: 'P003', name: '信用卡', type: 'credit' },
|
||||
{ id: 'P004', name: '结算服务', type: 'settlement' },
|
||||
{ id: 'P005', name: '企业网银', type: 'other' },
|
||||
]
|
||||
|
||||
// 拜访计划 Mock
|
||||
export const mockVisitPlans: VisitPlan[] = [
|
||||
{
|
||||
id: 'V001',
|
||||
customerId: 'C1001',
|
||||
customerName: '张三',
|
||||
date: '2025-12-26',
|
||||
location: '',
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
products: [
|
||||
{ id: 'P001', name: '公司理财', type: 'credit' },
|
||||
{ id: 'P002', name: '公司贷款', type: 'loan' }
|
||||
],
|
||||
topic: '推广公司理财和贷款产品',
|
||||
remark: '客户对理财产品感兴趣,需要详细介绍',
|
||||
photos: [],
|
||||
status: VisitStatus.PENDING,
|
||||
createdAt: '2025-12-25 10:00:00',
|
||||
updatedAt: '2025-12-25 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'V002',
|
||||
customerId: 'C1002',
|
||||
customerName: '李四',
|
||||
date: '2025-12-24',
|
||||
location: '广东省茂名市',
|
||||
latitude: 21.6630,
|
||||
longitude: 110.9250,
|
||||
products: [
|
||||
{ id: 'P003', name: '信用卡', type: 'credit' }
|
||||
],
|
||||
topic: '信用卡业务推广',
|
||||
remark: '客户已有信用卡,考虑升级',
|
||||
photos: ['/static/images/visit2.jpg', '/static/images/visit3.jpg'],
|
||||
status: VisitStatus.COMPLETED,
|
||||
createdAt: '2025-12-23 15:30:00',
|
||||
updatedAt: '2025-12-24 18:00:00'
|
||||
},
|
||||
{
|
||||
id: 'V003',
|
||||
customerId: 'C1001',
|
||||
customerName: '张三',
|
||||
date: '2025-12-20',
|
||||
location: '',
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
products: [],
|
||||
topic: '客户回访',
|
||||
remark: '因客户临时有事,拜访取消',
|
||||
photos: [],
|
||||
status: VisitStatus.CANCELLED,
|
||||
createdAt: '2025-12-19 09:00:00',
|
||||
updatedAt: '2025-12-20 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
425
src/pagesBank/visit/create.vue
Normal file
425
src/pagesBank/visit/create.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCustomerList, getMarketingProducts, createVisitPlan } from '@/pagesBank/api'
|
||||
import type { BankCustomer, MarketingProduct } from '@/typings/bank'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '创建拜访计划',
|
||||
},
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
date: '',
|
||||
customerId: '',
|
||||
customerName: '',
|
||||
productIds: [] as string[],
|
||||
topic: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 客户列表
|
||||
const customers = ref<BankCustomer[]>([])
|
||||
const showCustomerPicker = ref(false)
|
||||
|
||||
// 营销产品列表
|
||||
const products = ref<MarketingProduct[]>([])
|
||||
const showProductPicker = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 加载客户列表
|
||||
async function loadCustomers() {
|
||||
try {
|
||||
const res = await getCustomerList({
|
||||
pageNum: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
customers.value = res.list
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载客户列表失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载营销产品列表
|
||||
async function loadProducts() {
|
||||
try {
|
||||
products.value = await getMarketingProducts()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载产品列表失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 选择客户
|
||||
function handleSelectCustomer(customer: BankCustomer) {
|
||||
formData.value.customerId = customer.id
|
||||
formData.value.customerName = customer.merchantName
|
||||
showCustomerPicker.value = false
|
||||
}
|
||||
|
||||
// 选择营销产品
|
||||
function toggleProduct(productId: string) {
|
||||
const index = formData.value.productIds.indexOf(productId)
|
||||
if (index > -1) {
|
||||
formData.value.productIds.splice(index, 1)
|
||||
} else {
|
||||
formData.value.productIds.push(productId)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm(): boolean {
|
||||
if (!formData.value.date) {
|
||||
uni.showToast({ title: '请选择拜访日期', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!formData.value.customerId) {
|
||||
uni.showToast({ title: '请选择客户', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!formData.value.topic) {
|
||||
uni.showToast({ title: '请输入拜访主题', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!validateForm()) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await createVisitPlan({
|
||||
customerId: formData.value.customerId,
|
||||
date: formData.value.date,
|
||||
productIds: formData.value.productIds,
|
||||
topic: formData.value.topic,
|
||||
remark: formData.value.remark || undefined
|
||||
})
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '创建失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的产品名称
|
||||
function getSelectedProductNames(): string {
|
||||
const selected = products.value.filter(p => formData.value.productIds.includes(p.id))
|
||||
return selected.map(p => p.name).join('、')
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.customerId) {
|
||||
formData.value.customerId = options.customerId
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadCustomers()
|
||||
loadProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="visit-create-page">
|
||||
<view class="form-container">
|
||||
<!-- 日期选择 -->
|
||||
<view class="form-item">
|
||||
<view class="label required">日期</view>
|
||||
<wd-datetime-picker
|
||||
v-model="formData.date"
|
||||
type="date"
|
||||
:min-date="Date.now()"
|
||||
placeholder="请选择拜访日期"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 客户选择 -->
|
||||
<view class="form-item" @click="showCustomerPicker = true">
|
||||
<view class="label required">选择客户</view>
|
||||
<view class="value-input">
|
||||
<text :class="{ placeholder: !formData.customerName }">
|
||||
{{ formData.customerName || '请选择' }}
|
||||
</text>
|
||||
<text class="i-carbon-chevron-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营销产品 -->
|
||||
<view class="form-item" @click="showProductPicker = true">
|
||||
<view class="label">营销产品</view>
|
||||
<view class="value-input">
|
||||
<text :class="{ placeholder: formData.productIds.length === 0 }">
|
||||
{{ formData.productIds.length > 0 ? `已选${formData.productIds.length}个` : '请选择' }}
|
||||
</text>
|
||||
<text class="i-carbon-chevron-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拜访主题 -->
|
||||
<view class="form-item">
|
||||
<view class="label required">拜访主题</view>
|
||||
<input
|
||||
v-model="formData.topic"
|
||||
placeholder="主题、议题或活动等"
|
||||
class="input"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-item">
|
||||
<view class="label">备注</view>
|
||||
<textarea
|
||||
v-model="formData.remark"
|
||||
placeholder="本次拜访的备注内容"
|
||||
class="textarea"
|
||||
:maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" :disabled="submitting" @click="handleSubmit">
|
||||
{{ submitting ? '提交中...' : '保存拜访计划' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 客户选择弹窗 -->
|
||||
<wd-popup v-model="showCustomerPicker" position="bottom" :close-on-click-modal="false">
|
||||
<view class="customer-picker">
|
||||
<view class="picker-header">
|
||||
<text class="cancel" @click="showCustomerPicker = false">取消</text>
|
||||
<text class="title">选择客户</text>
|
||||
<text class="confirm" @click="showCustomerPicker = false">确定</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="customer-list">
|
||||
<view
|
||||
v-for="customer in customers"
|
||||
:key="customer.id"
|
||||
class="customer-item"
|
||||
:class="{ active: formData.customerId === customer.id }"
|
||||
@click="handleSelectCustomer(customer)"
|
||||
>
|
||||
<view class="customer-info">
|
||||
<text class="name">{{ customer.merchantName }}</text>
|
||||
<text class="contact">{{ customer.contactName }} {{ customer.contactPhone }}</text>
|
||||
</view>
|
||||
<text v-if="formData.customerId === customer.id" class="i-carbon-checkmark-filled"></text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 产品选择弹窗 -->
|
||||
<wd-popup v-model="showProductPicker" position="bottom" :close-on-click-modal="false">
|
||||
<view class="product-picker">
|
||||
<view class="picker-header">
|
||||
<text class="cancel" @click="showProductPicker = false">取消</text>
|
||||
<text class="title">选择营销产品</text>
|
||||
<text class="confirm" @click="showProductPicker = false">确定</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="product-list">
|
||||
<view
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="product-item"
|
||||
:class="{ active: formData.productIds.includes(product.id) }"
|
||||
@click="toggleProduct(product.id)"
|
||||
>
|
||||
<text class="name">{{ product.name }}</text>
|
||||
<text v-if="formData.productIds.includes(product.id)" class="i-carbon-checkmark-filled"></text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visit-create-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: #fa4350;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.value-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
.placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
color: #adb5bd;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 30rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-picker,
|
||||
.product-picker {
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
max-height: 70vh;
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f1f3f5;
|
||||
|
||||
.cancel,
|
||||
.confirm {
|
||||
font-size: 28rpx;
|
||||
color: #4d80f0;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-list,
|
||||
.product-list {
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f8f9fa;
|
||||
|
||||
&.active {
|
||||
background: rgba(0, 192, 90, 0.05);
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
flex: 1;
|
||||
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.contact {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-size: 40rpx;
|
||||
color: #00c05a;
|
||||
}
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f8f9fa;
|
||||
|
||||
&.active {
|
||||
background: rgba(0, 192, 90, 0.05);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-size: 40rpx;
|
||||
color: #00c05a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
901
src/pagesBank/visit/detail.vue
Normal file
901
src/pagesBank/visit/detail.vue
Normal file
@@ -0,0 +1,901 @@
|
||||
<script lang="ts" setup>
|
||||
import { getVisitPlanDetail, updateVisitPlan, updateVisitStatus, deleteVisitPlan } from '@/pagesBank/api'
|
||||
import type { VisitPlan } from '@/typings/bank'
|
||||
import { VisitStatus } from '@/typings/bank'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '拜访计划详情',
|
||||
},
|
||||
})
|
||||
|
||||
const id = ref('')
|
||||
const detail = ref<VisitPlan | null>(null)
|
||||
const loading = ref(false)
|
||||
const editing = ref(false)
|
||||
const completing = ref(false)
|
||||
|
||||
// 编辑表单数据
|
||||
const editForm = ref({
|
||||
date: '',
|
||||
topic: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 完成表单数据
|
||||
const completeForm = ref({
|
||||
location: '广东省茂名市',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
photos: [] as string[]
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVisitPlanDetail(id.value)
|
||||
detail.value = res
|
||||
if (res) {
|
||||
editForm.value = {
|
||||
date: res.date,
|
||||
topic: res.topic,
|
||||
remark: res.remark || ''
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
function handleCancelEdit() {
|
||||
editing.value = false
|
||||
if (detail.value) {
|
||||
editForm.value = {
|
||||
date: detail.value.date,
|
||||
topic: detail.value.topic,
|
||||
remark: detail.value.remark || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveEdit() {
|
||||
if (!editForm.value.topic) {
|
||||
uni.showToast({ title: '请输入拜访主题', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
try {
|
||||
await updateVisitPlan(id.value, {
|
||||
date: editForm.value.date,
|
||||
topic: editForm.value.topic,
|
||||
remark: editForm.value.remark
|
||||
})
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
editing.value = false
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateStatus(status: VisitStatus) {
|
||||
const statusText = {
|
||||
[VisitStatus.PENDING]: '待拜访',
|
||||
[VisitStatus.COMPLETED]: '已完成',
|
||||
[VisitStatus.CANCELLED]: '已取消'
|
||||
}
|
||||
|
||||
// 标记完成时需要填写位置和上传照片
|
||||
if (status === VisitStatus.COMPLETED) {
|
||||
completing.value = true
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '状态确认',
|
||||
content: `确定将拜访计划状态更新为"${statusText[status]}"吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '更新中...' })
|
||||
try {
|
||||
await updateVisitStatus(id.value, status)
|
||||
uni.showToast({ title: '更新成功', icon: 'success' })
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取位置
|
||||
function handleGetLocation() {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
completeForm.value.latitude = res.latitude
|
||||
completeForm.value.longitude = res.longitude
|
||||
// 这里可以调用逆地理编码获取地址,简化处理直接显示坐标
|
||||
completeForm.value.location = `${res.latitude.toFixed(6)}, ${res.longitude.toFixed(6)}`
|
||||
uni.showToast({ title: '定位成功', icon: 'success' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '定位失败,请手动输入', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
function handleUploadImage() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['拍照', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
chooseImage('camera')
|
||||
} else if (res.tapIndex === 1) {
|
||||
chooseImage('album')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function chooseImage(sourceType: 'camera' | 'album') {
|
||||
uni.chooseImage({
|
||||
count: 9 - completeForm.value.photos.length,
|
||||
sourceType: [sourceType],
|
||||
success: (res) => {
|
||||
const paths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths]
|
||||
completeForm.value.photos.push(...paths)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
function handleRemoveImage(index: number) {
|
||||
completeForm.value.photos.splice(index, 1)
|
||||
}
|
||||
|
||||
// 预览完成表单图片
|
||||
function handlePreviewCompleteImage(index: number) {
|
||||
uni.previewImage({
|
||||
urls: completeForm.value.photos,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
|
||||
// 取消完成
|
||||
function handleCancelComplete() {
|
||||
completing.value = false
|
||||
completeForm.value = {
|
||||
location: '广东省茂名市',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
photos: []
|
||||
}
|
||||
}
|
||||
|
||||
// 提交完成
|
||||
async function handleSubmitComplete() {
|
||||
if (!completeForm.value.location) {
|
||||
uni.showToast({ title: '请输入或定位位置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (completeForm.value.photos.length === 0) {
|
||||
uni.showToast({ title: '请上传拜访场景图', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '更新中...' })
|
||||
try {
|
||||
await updateVisitStatus(id.value, VisitStatus.COMPLETED, completeForm.value.location, completeForm.value.latitude || undefined, completeForm.value.longitude || undefined, completeForm.value.photos)
|
||||
uni.showToast({ title: '更新成功', icon: 'success' })
|
||||
completing.value = false
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
uni.showModal({
|
||||
title: '删除确认',
|
||||
content: '确定要删除这条拜访计划吗?删除后无法恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
try {
|
||||
await deleteVisitPlan(id.value)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 预览详情图片
|
||||
function handlePreviewImage(index: number) {
|
||||
if (detail.value?.photos) {
|
||||
uni.previewImage({
|
||||
urls: detail.value.photos,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusInfo(status: string) {
|
||||
const map: Record<string, { text: string; color: string; bgColor: string }> = {
|
||||
pending: { text: '待拜访', color: '#ff8f0d', bgColor: 'rgba(255, 143, 13, 0.1)' },
|
||||
completed: { text: '已完成', color: '#00c05a', bgColor: 'rgba(0, 192, 90, 0.1)' },
|
||||
cancelled: { text: '已取消', color: '#adb5bd', bgColor: 'rgba(173, 181, 189, 0.1)' },
|
||||
}
|
||||
return map[status] || { text: '未知', color: '#999', bgColor: '#f5f5f5' }
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const weekday = weekdays[date.getDay()]
|
||||
return `${year}年${month}月${day}日 周${weekday}`
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.id) {
|
||||
id.value = options.id
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="visit-detail-page">
|
||||
<view v-if="loading" class="loading-box">加载中...</view>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<!-- 头部状态卡片 -->
|
||||
<view class="header-card">
|
||||
<view class="date-badge">
|
||||
<text class="month">{{ new Date(detail.date).getMonth() + 1 }}月</text>
|
||||
<text class="day">{{ new Date(detail.date).getDate() }}</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="customer-name">{{ detail.customerName }}</text>
|
||||
<text
|
||||
class="status-tag"
|
||||
:style="{ color: getStatusInfo(detail.status).color, backgroundColor: getStatusInfo(detail.status).bgColor }"
|
||||
>
|
||||
{{ getStatusInfo(detail.status).text }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拜访信息 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<view class="title">拜访信息</view>
|
||||
<view v-if="!editing" class="action-btn" @click="handleEdit">编辑</view>
|
||||
</view>
|
||||
|
||||
<view v-if="editing" class="edit-form">
|
||||
<view class="form-item">
|
||||
<view class="label">拜访日期</view>
|
||||
<wd-datetime-picker
|
||||
v-model="editForm.date"
|
||||
type="date"
|
||||
:min-date="Date.now()"
|
||||
placeholder="请选择拜访日期"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">拜访主题</view>
|
||||
<input
|
||||
v-model="editForm.topic"
|
||||
placeholder="请输入拜访主题"
|
||||
class="input"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">备注</view>
|
||||
<textarea
|
||||
v-model="editForm.remark"
|
||||
placeholder="请输入备注"
|
||||
class="textarea"
|
||||
:maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
<view class="edit-actions">
|
||||
<button class="btn cancel" @click="handleCancelEdit">取消</button>
|
||||
<button class="btn save" @click="handleSaveEdit">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="info-content">
|
||||
<view class="info-row">
|
||||
<text class="label">拜访日期</text>
|
||||
<text class="value">{{ formatDate(detail.date) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">拜访主题</text>
|
||||
<text class="value">{{ detail.topic }}</text>
|
||||
</view>
|
||||
<view v-if="detail.remark" class="info-row">
|
||||
<text class="label">备注</text>
|
||||
<text class="value">{{ detail.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">位置信息</view>
|
||||
<view class="info-row">
|
||||
<text class="i-carbon-location-filled"></text>
|
||||
<text class="value">{{ detail.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营销产品 -->
|
||||
<view v-if="detail.products.length > 0" class="section">
|
||||
<view class="section-title">营销产品</view>
|
||||
<view class="products-list">
|
||||
<view
|
||||
v-for="product in detail.products"
|
||||
:key="product.id"
|
||||
class="product-tag"
|
||||
>
|
||||
{{ product.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拜访照片 -->
|
||||
<view v-if="detail.photos.length > 0" class="section">
|
||||
<view class="section-title">拜访照片</view>
|
||||
<view class="photos-grid">
|
||||
<image
|
||||
v-for="(photo, index) in detail.photos"
|
||||
:key="index"
|
||||
:src="photo"
|
||||
mode="aspectFill"
|
||||
class="photo"
|
||||
@click="handlePreviewImage(index)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<view
|
||||
v-if="detail.status === 'pending'"
|
||||
class="action-item success"
|
||||
@click="handleUpdateStatus(VisitStatus.COMPLETED)"
|
||||
>
|
||||
<text class="i-carbon-checkmark-filled"></text>
|
||||
标记完成
|
||||
</view>
|
||||
<view
|
||||
v-if="detail.status === 'pending'"
|
||||
class="action-item warning"
|
||||
@click="handleUpdateStatus(VisitStatus.CANCELLED)"
|
||||
>
|
||||
<text class="i-carbon-close"></text>
|
||||
取消拜访
|
||||
</view>
|
||||
<view class="action-item danger" @click="handleDelete">
|
||||
<text class="i-carbon-trash-can"></text>
|
||||
删除计划
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 完成表单弹窗 -->
|
||||
<wd-popup v-model="completing" position="bottom" :close-on-click-modal="false">
|
||||
<view class="complete-form">
|
||||
<view class="form-header">
|
||||
<text class="cancel" @click="handleCancelComplete">取消</text>
|
||||
<text class="title">标记拜访完成</text>
|
||||
<text class="confirm" @click="handleSubmitComplete">确定</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="form-content">
|
||||
<!-- 位置 -->
|
||||
<view class="form-item">
|
||||
<view class="label required">位置</view>
|
||||
<view class="location-input">
|
||||
<input
|
||||
v-model="completeForm.location"
|
||||
placeholder="请输入地址或点击定位"
|
||||
class="input"
|
||||
/>
|
||||
<view class="location-btn" @click="handleGetLocation">
|
||||
<text class="i-carbon-location-filled"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<view class="form-item">
|
||||
<view class="label required">上传拜访场景图</view>
|
||||
<view class="upload-section">
|
||||
<view
|
||||
v-for="(photo, index) in completeForm.photos"
|
||||
:key="index"
|
||||
class="photo-item"
|
||||
>
|
||||
<image :src="photo" mode="aspectFill" @click="handlePreviewCompleteImage(index)" />
|
||||
<view class="delete-btn" @click="handleRemoveImage(index)">
|
||||
<text class="i-carbon-close"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="completeForm.photos.length < 9"
|
||||
class="upload-btn"
|
||||
@click="handleUploadImage"
|
||||
>
|
||||
<text class="i-carbon-camera"></text>
|
||||
<text>上传图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visit-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.header-card {
|
||||
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
|
||||
padding: 40rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
color: #fff;
|
||||
|
||||
.date-badge {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.month {
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.day {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.customer-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
margin: 24rpx 30rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
padding-left: 20rpx;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
background: #00c05a;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 24rpx;
|
||||
color: #00c05a;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 20rpx;
|
||||
background: rgba(0, 192, 90, 0.1);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 20rpx;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
background: #00c05a;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
|
||||
&.cancel {
|
||||
background: #f1f3f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.save {
|
||||
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
.info-row {
|
||||
display: flex;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f8f9fa;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
text:first-child {
|
||||
font-size: 28rpx;
|
||||
color: #adb5bd;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.products-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.product-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
background: rgba(0, 192, 90, 0.1);
|
||||
color: #00c05a;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.photos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 30rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.action-item {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: linear-gradient(135deg, #ff8f0d 0%, #ffb347 100%);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: linear-gradient(135deg, #fa4350 0%, #ff6b6b 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
padding: 100rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.complete-form {
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
max-height: 80vh;
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f1f3f5;
|
||||
|
||||
.cancel,
|
||||
.confirm {
|
||||
font-size: 28rpx;
|
||||
color: #4d80f0;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.form-content {
|
||||
max-height: 70vh;
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: #fa4350;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.location-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.location-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: #00c05a;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.photo-item {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border: 2rpx dashed #d0d7de;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
color: #adb5bd;
|
||||
|
||||
text:first-child {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
471
src/pagesBank/visit/list.vue
Normal file
471
src/pagesBank/visit/list.vue
Normal file
@@ -0,0 +1,471 @@
|
||||
<script lang="ts" setup>
|
||||
import { getVisitPlanList, deleteVisitPlan } from '@/pagesBank/api'
|
||||
import type { VisitPlan } from '@/typings/bank'
|
||||
import { VisitStatus } from '@/typings/bank'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '拜访计划',
|
||||
enablePullDownRefresh: true
|
||||
},
|
||||
})
|
||||
|
||||
const visitPlans = ref<VisitPlan[]>([])
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const activeStatus = ref('')
|
||||
const customerId = ref('')
|
||||
|
||||
const statusTabs = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '待拜访', value: 'pending' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
]
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVisitPlanList({
|
||||
status: activeStatus.value || undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
keyword: keyword.value
|
||||
})
|
||||
// 如果有客户ID筛选,过滤结果
|
||||
let list = res.list
|
||||
if (customerId.value) {
|
||||
list = list.filter(item => item.customerId === customerId.value)
|
||||
}
|
||||
visitPlans.value = list
|
||||
} finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleTabChange(value: string) {
|
||||
activeStatus.value = value
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleDetail(id: string) {
|
||||
uni.navigateTo({ url: `/pagesBank/visit/detail?id=${id}` })
|
||||
}
|
||||
|
||||
function handleDelete(id: string) {
|
||||
uni.showModal({
|
||||
title: '删除确认',
|
||||
content: '确定要删除这条拜访计划吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
try {
|
||||
await deleteVisitPlan(id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
loadData()
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getStatusInfo(status: string) {
|
||||
const map: Record<string, { text: string; color: string; bgColor: string }> = {
|
||||
pending: { text: '待拜访', color: '#ff8f0d', bgColor: 'rgba(255, 143, 13, 0.1)' },
|
||||
completed: { text: '已完成', color: '#00c05a', bgColor: 'rgba(0, 192, 90, 0.1)' },
|
||||
cancelled: { text: '已取消', color: '#adb5bd', bgColor: 'rgba(173, 181, 189, 0.1)' },
|
||||
}
|
||||
return map[status] || { text: '未知', color: '#999', bgColor: '#f5f5f5' }
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const weekday = weekdays[date.getDay()]
|
||||
return `${month}月${day}日 周${weekday}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="visit-list-page">
|
||||
<view class="sticky-header">
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="i-carbon-search"></text>
|
||||
<input
|
||||
v-model="keyword"
|
||||
placeholder="搜索客户/主题"
|
||||
confirm-type="search"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.value"
|
||||
class="tab-item"
|
||||
:class="{ active: activeStatus === tab.value }"
|
||||
@click="handleTabChange(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-container">
|
||||
<view v-if="loading && visitPlans.length === 0" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="visitPlans.length === 0" class="empty-state">
|
||||
<text class="i-carbon-calendar"></text>
|
||||
<text>暂无拜访计划</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in visitPlans"
|
||||
:key="item.id"
|
||||
class="visit-card"
|
||||
@click="handleDetail(item.id)"
|
||||
>
|
||||
<view class="card-header">
|
||||
<view class="date-badge">
|
||||
<text class="month">{{ new Date(item.date).getMonth() + 1 }}月</text>
|
||||
<text class="day">{{ new Date(item.date).getDate() }}</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="customer-name">{{ item.customerName }}</text>
|
||||
<text class="topic">{{ item.topic }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="status-tag"
|
||||
:style="{ color: getStatusInfo(item.status).color, backgroundColor: getStatusInfo(item.status).bgColor }"
|
||||
>
|
||||
{{ getStatusInfo(item.status).text }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="i-carbon-location-filled"></text>
|
||||
<text class="location">{{ item.location }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="item.products.length > 0" class="info-row">
|
||||
<text class="i-carbon-tag"></text>
|
||||
<text class="products">{{ item.products.map(p => p.name).join('、') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="item.photos.length > 0" class="photos-row">
|
||||
<image
|
||||
v-for="(photo, index) in item.photos.slice(0, 3)"
|
||||
:key="index"
|
||||
:src="photo"
|
||||
mode="aspectFill"
|
||||
class="photo"
|
||||
/>
|
||||
<view v-if="item.photos.length > 3" class="photo-more">
|
||||
+{{ item.photos.length - 3 }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="time">{{ formatDate(item.date) }}</text>
|
||||
<view class="actions" @click.stop>
|
||||
<view class="action-btn delete" @click="handleDelete(item.id)">
|
||||
<text class="i-carbon-trash-can"></text>
|
||||
删除
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visit-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
padding: 20rpx 0 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 0 30rpx 20rpx;
|
||||
|
||||
.search-input {
|
||||
height: 72rpx;
|
||||
background: #f1f3f5;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
gap: 16rpx;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-bottom: 1rpx solid #f1f3f5;
|
||||
|
||||
.tab-item {
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #495057;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #00c05a;
|
||||
font-weight: 700;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6rpx;
|
||||
background: #00c05a;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.visit-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.date-badge {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
.month {
|
||||
font-size: 20rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.day {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.customer-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.topic {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
background: #f8f9fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
text:first-child {
|
||||
font-size: 28rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.location,
|
||||
.products {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.photos-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 16rpx;
|
||||
|
||||
.photo {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.photo-more {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1rpx solid #f1f3f5;
|
||||
padding-top: 20rpx;
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
color: #fa4350;
|
||||
background: rgba(250, 67, 80, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
color: #adb5bd;
|
||||
gap: 20rpx;
|
||||
|
||||
text:first-child {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
color: #adb5bd;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -51,6 +51,55 @@ export interface BankCustomer {
|
||||
joinTime: string
|
||||
}
|
||||
|
||||
/** 拜访计划状态 */
|
||||
export enum VisitStatus {
|
||||
PENDING = 'pending', // 待拜访
|
||||
COMPLETED = 'completed', // 已完成
|
||||
CANCELLED = 'cancelled', // 已取消
|
||||
}
|
||||
|
||||
/** 营销产品 */
|
||||
export interface MarketingProduct {
|
||||
id: string
|
||||
name: string
|
||||
type: 'loan' | 'credit' | 'settlement' | 'other'
|
||||
}
|
||||
|
||||
/** 拜访计划 */
|
||||
export interface VisitPlan {
|
||||
id: string
|
||||
customerId: string
|
||||
customerName: string
|
||||
date: string
|
||||
location: string
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
products: MarketingProduct[]
|
||||
topic: string
|
||||
remark?: string
|
||||
photos: string[]
|
||||
status: VisitStatus
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** 拜访计划创建参数 */
|
||||
export interface CreateVisitPlanParams {
|
||||
customerId: string
|
||||
date: string
|
||||
productIds: string[]
|
||||
topic: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 拜访计划完成参数 */
|
||||
export interface CompleteVisitPlanParams {
|
||||
location: string
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
photos: string[]
|
||||
}
|
||||
|
||||
/** 银行统计指标 */
|
||||
export interface BankStats {
|
||||
pendingAuditStore: number // 待审核商户
|
||||
|
||||
Reference in New Issue
Block a user