Files
loan-pricing/docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md

663 lines
21 KiB
Markdown
Raw Normal View History

# Customer Map Selection Frontend Implementation Plan
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
**Goal:** Add the frontend customer-id query and customer-internal-code selection step before opening personal or corporate workflow creation dialogs.
**Architecture:** Keep the existing list page and personal/corporate creation dialogs. Insert a shared customer-map selector dialog between customer-type selection and workflow creation, then pass the selected underscore-field record into the existing create dialogs. Existing workflow create APIs stay unchanged and still receive `custIsn` and `custName` in camelCase.
**Tech Stack:** Vue 2, Element UI, existing RuoYi request wrapper, Node static assertion tests, nvm-managed frontend runtime, Playwright/browser verification after implementation.
---
## File Structure
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Adds personal/corporate customer-map query functions.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
- Owns customer-id input, query action, result table, loading state, and row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Opens customer-map selector after customer type selection and then opens the correct create dialog after row selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Accepts selected customer-map record, fills `custIsn` and `custName`, and makes both fields read-only.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Same selected-record behavior for corporate creation.
- Modify: `ruoyi-ui/package.json`
- Adds a focused test script for the new customer-map selection checks.
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
- Static test coverage for API paths, selector fields, parent wiring, selected underscore fields, and read-only create dialog inputs.
## Task 1: Add Frontend API Methods
**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
- Modify: `ruoyi-ui/package.json`
- [ ] **Step 1: Write failing API assertions**
Create the first version of `ruoyi-ui/tests/customer-map-selection.test.js`:
```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const workflowApi = read('src/api/loanPricing/workflow.js')
assert(
workflowApi.includes('export function queryPersonalCustomerMap') &&
workflowApi.includes("url: '/loanPricing/workflow/customer-map/personal'") &&
workflowApi.includes('params: { custId: custId }'),
'缺少个人客户号映射查询 API'
)
assert(
workflowApi.includes('export function queryCorporateCustomerMap') &&
workflowApi.includes("url: '/loanPricing/workflow/customer-map/corporate'") &&
workflowApi.includes('params: { custId: custId }'),
'缺少企业客户号映射查询 API'
)
console.log('customer map selection assertions passed')
```
Add a script in `ruoyi-ui/package.json`:
```json
"test:customer-map-selection": "node tests/customer-map-selection.test.js"
```
- [ ] **Step 2: Run the test to verify it fails**
Run with the project Node version:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because the API methods do not exist.
- [ ] **Step 3: Implement the API methods**
Append to `workflow.js`:
```js
// 查询个人客户号映射
export function queryPersonalCustomerMap(custId) {
return request({
url: '/loanPricing/workflow/customer-map/personal',
method: 'get',
params: { custId: custId }
})
}
// 查询企业客户号映射
export function queryCorporateCustomerMap(custId) {
return request({
url: '/loanPricing/workflow/customer-map/corporate',
method: 'get',
params: { custId: custId }
})
}
```
- [ ] **Step 4: Run the API test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS for API assertions.
## Task 2: Create Customer Map Selector Dialog
**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend the static test for selector requirements**
Add these assertions to `customer-map-selection.test.js`:
```js
const selector = read('src/views/loanPricing/workflow/components/CustomerMapSelector.vue')
assert(
selector.includes('title="客户号查询"') &&
selector.includes('v-model="queryForm.custId"') &&
selector.includes('handleQuery'),
'客户号查询弹窗缺少客户号输入或查询动作'
)
assert(
selector.includes("queryPersonalCustomerMap") &&
selector.includes("queryCorporateCustomerMap") &&
selector.includes("this.customerType === 'personal'"),
'客户号查询弹窗未按客户类型调用个人/企业接口'
)
;['cust_id', 'cust_isn', 'cust_name', 'faith_day', 'balance_avg', 'loan_count_his', 'last_loan_date'].forEach((field) => {
assert(selector.includes(`prop="${field}"`) || selector.includes(`row.${field}`), `查询结果表格缺少字段 ${field}`)
})
assert(
selector.includes("this.$emit('select', row)") &&
selector.includes('未查询到客户信息') &&
selector.includes('请输入客户号'),
'客户号查询弹窗缺少选择事件或关键提示'
)
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because `CustomerMapSelector.vue` does not exist.
- [ ] **Step 3: Implement `CustomerMapSelector.vue`**
Create the component:
```vue
<template>
<el-dialog title="客户号查询" :visible.sync="dialogVisible" width="900px" append-to-body
:close-on-click-modal="false" @close="handleClose">
<el-form :model="queryForm" inline size="small">
<el-form-item label="客户号">
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="customerList">
<el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="用信天数" prop="faith_day" align="center"/>
<el-table-column label="存款年日均" prop="balance_avg" align="center"/>
<el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
<el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
<el-table-column label="操作" align="center" width="90">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script>
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"
export default {
name: "CustomerMapSelector",
props: {
visible: {
type: Boolean,
default: false
},
customerType: {
type: String,
default: undefined
}
},
data() {
return {
loading: false,
queryForm: {
custId: undefined
},
customerList: []
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
handleQuery() {
if (!this.queryForm.custId) {
this.$modal.msgWarning("请输入客户号")
return
}
this.loading = true
const request = this.customerType === 'personal'
? queryPersonalCustomerMap
: queryCorporateCustomerMap
request(this.queryForm.custId).then(response => {
this.customerList = response.data || []
if (this.customerList.length === 0) {
this.$modal.msgWarning("未查询到客户信息")
}
}).finally(() => {
this.loading = false
})
},
handleSelect(row) {
this.$emit('select', row)
this.dialogVisible = false
},
handleClose() {
this.queryForm.custId = undefined
this.customerList = []
this.loading = false
}
}
}
</script>
```
- [ ] **Step 4: Run the selector test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through selector assertions.
## Task 3: Wire Selector Into Workflow List Page
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend the test for parent wiring**
Add assertions:
```js
const workflowIndex = read('src/views/loanPricing/workflow/index.vue')
assert(
workflowIndex.includes('CustomerMapSelector') &&
workflowIndex.includes('<customer-map-selector') &&
workflowIndex.includes(':customer-type="selectedCustomerType"') &&
workflowIndex.includes('@select="handleCustomerMapSelect"'),
'流程列表页未接入客户号查询选择弹窗'
)
assert(
workflowIndex.includes('selectedCustomerType') &&
workflowIndex.includes('selectedCustomerMap') &&
workflowIndex.includes('showCustomerMapSelector'),
'流程列表页缺少客户类型、客户映射选择状态'
)
assert(
workflowIndex.includes("this.selectedCustomerType = type") &&
workflowIndex.includes('this.showCustomerMapSelector = true') &&
!workflowIndex.includes("if (type === 'personal') {\\n this.showPersonalDialog = true"),
'选择客户类型后应先打开客户号查询弹窗,而不是直接打开新增弹窗'
)
assert(
workflowIndex.includes('handleCustomerMapSelect') &&
workflowIndex.includes('this.selectedCustomerMap = row') &&
workflowIndex.includes('this.showPersonalDialog = true') &&
workflowIndex.includes('this.showCorporateDialog = true'),
'选择客户内码后未打开对应新增弹窗'
)
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because the list page does not yet use the selector.
- [ ] **Step 3: Update `index.vue` template and script**
Add component usage after the customer type selector:
```vue
<customer-map-selector
:visible.sync="showCustomerMapSelector"
:customer-type="selectedCustomerType"
@select="handleCustomerMapSelect"
/>
```
Pass the selected record into both create dialogs:
```vue
<personal-create-dialog
:visible.sync="showPersonalDialog"
:customer-map="selectedCustomerMap"
@success="handleCreateSuccess"
/>
<corporate-create-dialog
:visible.sync="showCorporateDialog"
:customer-map="selectedCustomerMap"
@success="handleCreateSuccess"
/>
```
Import and register the selector:
```js
import CustomerMapSelector from "./components/CustomerMapSelector"
```
Add state:
```js
showCustomerMapSelector: false,
selectedCustomerType: undefined,
selectedCustomerMap: null,
```
Change `handleSelectType`:
```js
handleSelectType(type) {
this.selectedCustomerType = type
this.selectedCustomerMap = null
this.showCustomerMapSelector = true
}
```
Add selected-row handler:
```js
handleCustomerMapSelect(row) {
this.selectedCustomerMap = row
if (this.selectedCustomerType === 'personal') {
this.showPersonalDialog = true
} else if (this.selectedCustomerType === 'corporate') {
this.showCorporateDialog = true
}
}
```
Clear selected state when the create flow completes:
```js
handleCreateSuccess() {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
this.getList()
}
```
Add watchers for dialog close so cancellation also clears the selected record:
```js
watch: {
showPersonalDialog(val) {
if (!val && !this.showCorporateDialog) {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
}
},
showCorporateDialog(val) {
if (!val && !this.showPersonalDialog) {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
}
}
}
```
- [ ] **Step 4: Run the parent wiring test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through parent wiring assertions.
## Task 4: Make Create Dialog Customer Fields Read-Only and Auto-Filled
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
- [ ] **Step 1: Extend test for create dialog behavior**
Add assertions:
```js
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')
;[
['个人新增弹窗', personalCreateDialog],
['企业新增弹窗', corporateCreateDialog]
].forEach(([name, source]) => {
assert(source.includes('customerMap'), `${name} 缺少 customerMap 入参`)
assert(source.includes(':readonly="true"') || source.includes('readonly'), `${name} 客户内码和客户名称应只读`)
assert(source.includes('this.customerMap.cust_isn'), `${name} 未从 cust_isn 自动带入客户内码`)
assert(source.includes('this.customerMap.cust_name'), `${name} 未从 cust_name 自动带入客户名称`)
assert(source.includes('clearValidate'), `${name} 应清空校验而不是用 resetFields 覆盖已选客户`)
assert(!source.includes('this.resetForm("form")'), `${name} 不应在 reset() 中调用 resetForm("form") 覆盖只读客户字段`)
})
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: FAIL because dialogs do not yet consume `customerMap`.
- [ ] **Step 3: Update personal create dialog**
Add prop:
```js
customerMap: {
type: Object,
default: null
}
```
Make customer fields read-only:
```vue
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
```
In `reset()`, initialize from selected row and do not call `this.resetForm("form")`. The existing RuoYi helper delegates to Element UI `resetFields()`, which can restore stale initial values and overwrite the selected read-only customer fields. After assigning the new form object, only clear validation:
```js
reset() {
this.form = {
orgCode: '892000',
runType: '1',
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
custName: this.customerMap ? this.customerMap.cust_name : undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
}
```
Keep existing required validation for `custIsn` and `custName`.
- [ ] **Step 4: Update corporate create dialog**
Apply the same prop, read-only inputs, and `reset()` initialization to `CorporateCreateDialog.vue`. Do not call `this.resetForm("form")` in the corporate dialog reset path; assign the new form object with `custIsn` and `custName` from `customerMap`, then use `this.$refs.form.clearValidate()` inside `$nextTick()`.
- [ ] **Step 5: Run the create dialog test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS through create dialog assertions.
## Task 5: Frontend Verification
**Files:**
- Verify only, no new source files.
- [ ] **Step 1: Run focused customer-map test**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
```
Expected: PASS.
- [ ] **Step 2: Run affected existing frontend tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```
Expected: PASS.
- [ ] **Step 3: Build frontend**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: build succeeds.
- [ ] **Step 4: Browser verification after backend and frontend are running**
Use the real application page, not a prototype page:
1. Open the workflow list page.
2. Click “新增”.
3. Select “个人客户”.
4. Confirm the customer-id query dialog opens.
5. Query any customer number.
6. Select one result row.
7. Confirm the personal create dialog opens and `客户内码` / `客户名称` are auto-filled and read-only.
8. Fill the remaining required fields and submit.
9. Repeat the same flow for “企业客户”.
10. Close and reopen the create flow at least once with a second selected row, and confirm the new row's `cust_isn` / `cust_name` replace the previous values in the create dialog.
11. Confirm both created records appear in the list or detail page.
Expected: both personal and corporate flows pass through customer-map selection before creation.
- [ ] **Step 5: Cleanup test processes**
Stop any backend or frontend processes started for verification before ending the task.
- [ ] **Step 6: Commit frontend work**
Use a Chinese commit message and avoid unrelated dirty files:
```bash
git status --short
git add ruoyi-ui/src/api/loanPricing/workflow.js \
ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue \
ruoyi-ui/src/views/loanPricing/workflow/index.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/package.json \
ruoyi-ui/tests/customer-map-selection.test.js
git commit -m "新增客户号查询选择前端流程"
```
Do not commit screenshots, browser traces, temporary spreadsheets, or generated test data.
## Task 6: Final End-to-End Verification and Implementation Record
**Files:**
- Create: `doc/implementation-report-2026-04-29-customer-map-selection.md`
- [ ] **Step 1: Run backend and frontend verification together**
After backend and frontend implementation are both complete:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
```
Expected: all tests pass.
- [ ] **Step 2: Complete browser verification**
Run the browser flow from Task 5 for both personal and corporate customers. Confirm the backend API response wrapper is still the existing RuoYi wrapper while records inside `data` keep underscore fields.
- [ ] **Step 3: Write implementation report**
Create `doc/implementation-report-2026-04-29-customer-map-selection.md` with:
```markdown
# 2026-04-29 客户号查询选择客户内码实施记录
## 修改内容
- 后端新增个人/企业客户号映射业务接口。
- 后端新增个人/企业客户号映射 mock 接口。
- 配置文件新增 `customer-map` 个人/企业地址并指向本项目 mock。
- 前端新增客户号查询选择弹窗。
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
## 验证结果
- 后端测试:填写实际执行命令和通过/失败结果。
- 前端测试:填写实际执行命令和通过/失败结果。
- 真实页面验证:填写个人、企业两条浏览器验证流程和结果。
- 进程清理:填写本次启动的前后端进程是否已关闭。
```
- [ ] **Step 4: Commit implementation report**
```bash
git add doc/implementation-report-2026-04-29-customer-map-selection.md
git commit -m "补充客户号映射选择实施记录"
```