|
@@ -0,0 +1,262 @@
|
|
|
|
|
+# Checkout 地址迁移实施计划
|
|
|
|
|
+
|
|
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** 将 checkout 页面从 `consignees` 系统切换到 `user_address` 系统,身份证等购买信息字段保持独立。
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** checkout.vue 调用 `/api/user/address/list` 替代 `/api/consignee/list`,地址选择跳转到 `address/address.vue`,新增/编辑走 `address-edit/address-edit.vue`。后端无改动。
|
|
|
|
|
+
|
|
|
|
|
+**Tech Stack:** uni-app Vue 2, WeChat mini-program, Spring Boot 2.7.18
|
|
|
|
|
+
|
|
|
|
|
+## Global Constraints
|
|
|
|
|
+
|
|
|
|
|
+- 小程序禁止可选链 `?.`(用 `&&` 替代)
|
|
|
|
|
+- 小程序禁止 CSS Grid(用 flexbox)
|
|
|
|
|
+- Vue 2 Options API,禁止 Composition API
|
|
|
|
|
+- 所有接口统一 `@PostMapping`,前端 `method: 'POST'`
|
|
|
|
|
+- 响应格式:`uni.request` + `Authorization: Bearer {token}`
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: 添加地址 API 函数到 api.js
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `cfc-frontend/utils/api.js`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Consumes: 无
|
|
|
|
|
+- Produces: `addressList()`, `addressDetail(id)`, `addressSave(data)`, `addressDelete(id)`, `addressSetDefault(id)`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 在 api.js 末尾添加地址 API 函数**
|
|
|
|
|
+
|
|
|
|
|
+找到 `consigneeDelete` 函数附近,在 `getProductRequiredFields` 之前添加以下函数:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// ===== 地址管理 =====
|
|
|
|
|
+export const addressList = () => request('/api/user/address/list', 'POST', {})
|
|
|
|
|
+export const addressDetail = (id) => request('/api/user/address/get', 'POST', { id })
|
|
|
|
|
+export const addressSave = (data) => {
|
|
|
|
|
+ var url = data.id ? '/api/user/address/update' : '/api/user/address/create'
|
|
|
|
|
+ return request(url, 'POST', data)
|
|
|
|
|
+}
|
|
|
|
|
+export const addressDelete = (id) => request('/api/user/address/delete', 'POST', { id })
|
|
|
|
|
+export const addressSetDefault = (id) => request('/api/user/address/setDefault', 'POST', { id })
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 验证文件修改**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+grep -n 'addressList\|addressDetail\|addressSave\|addressDelete\|addressSetDefault' cfc-frontend/utils/api.js
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: 修改 checkout.vue — 切换为地址系统
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `cfc-frontend/pages/shop/checkout/checkout.vue`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Consumes: `addressList()` from api.js
|
|
|
|
|
+- Produces: 提交订单时传 `addressId` 代替 `consigneeId`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 修改 import,替换 consigneeList 为 addressList**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// 替换这一行:
|
|
|
|
|
+import { getCouponList, consigneeList, getProductRequiredFields, getMyMembership } from '../../../utils/api.js'
|
|
|
|
|
+// 改为:
|
|
|
|
|
+import { getCouponList, addressList, getProductRequiredFields, getMyMembership } from '../../../utils/api.js'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 修改 data(),替换 consignee 相关字段为 address 字段**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// 在 data() 中,替换:
|
|
|
|
|
+selectedConsignee: null,
|
|
|
|
|
+consigneeList: [],
|
|
|
|
|
+// 改为:
|
|
|
|
|
+selectedAddress: null,
|
|
|
|
|
+addressList: [],
|
|
|
|
|
+
|
|
|
|
|
+// 同时移除:
|
|
|
|
|
+showConsigneePicker: false,
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 修改模板 — 地址卡片区域**
|
|
|
|
|
+
|
|
|
|
|
+找到地址卡片区域(约第 8-25 行),替换为:
|
|
|
|
|
+
|
|
|
|
|
+```html
|
|
|
|
|
+<!-- Address Section -->
|
|
|
|
|
+<view class="section address-section" @click="openAddressPicker">
|
|
|
|
|
+ <view v-if="selectedAddress" class="address-content">
|
|
|
|
|
+ <view class="address-top">
|
|
|
|
|
+ <text class="receiver-name">{{ selectedAddress.receiverName }}</text>
|
|
|
|
|
+ <text class="receiver-phone">{{ formatPhone(selectedAddress.phone) }}</text>
|
|
|
|
|
+ <text v-if="selectedAddress.isDefault === 1" class="default-badge">默认</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="address-detail">
|
|
|
|
|
+ {{ selectedAddress.province }}{{ selectedAddress.city }}{{ selectedAddress.district }}{{ selectedAddress.street }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text class="address-arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="address-empty" @click.stop="goAddAddress">
|
|
|
|
|
+ <text class="address-empty-icon">📍</text>
|
|
|
|
|
+ <text class="address-empty-text">请选择收货地址</text>
|
|
|
|
|
+ <text class="address-arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</view>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: 替换 `loadDefaultConsignee` 方法为 `loadDefaultAddress`**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+loadDefaultAddress() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ var that = this
|
|
|
|
|
+ addressList().then(function(res) {
|
|
|
|
|
+ that.loading = false
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ var list = res.data || []
|
|
|
|
|
+ that.addressList = list
|
|
|
|
|
+ for (var i = 0; i < list.length; i++) {
|
|
|
|
|
+ if (list[i].isDefault === 1) {
|
|
|
|
|
+ that.selectedAddress = list[i]
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!that.selectedAddress && list.length > 0) {
|
|
|
|
|
+ that.selectedAddress = list[0]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(function(e) {
|
|
|
|
|
+ that.loading = false
|
|
|
|
|
+ console.error(e)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: 替换 `openConsigneePicker` 方法为 `openAddressPicker`**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+openAddressPicker() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/shop/address/address?selectMode=1' })
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: 替换 `goAddConsignee` 方法为 `goAddAddress`**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+goAddAddress() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/shop/address-edit/address-edit' })
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: 替换 `autoFillPurchaseForm` 中的 `selectedConsignee` 引用**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+autoFillPurchaseForm() {
|
|
|
|
|
+ if (!this.selectedAddress || this.requiredFields.length === 0) return
|
|
|
|
|
+ // ... 其余逻辑不变,只是 selectedConsignee → selectedAddress
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 8: 修改 `onLoad` 中的调用**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// 替换:
|
|
|
|
|
+this.loadDefaultConsignee()
|
|
|
|
|
+// 改为:
|
|
|
|
|
+this.loadDefaultAddress()
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 9: 添加 `onShow` 生命周期 — 返回时重新加载地址列表**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+onShow() {
|
|
|
|
|
+ if (this.addressList.length > 0) {
|
|
|
|
|
+ // 从 address-edit 或 address 页面返回后重新加载
|
|
|
|
|
+ this.loadDefaultAddress()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 10: 修改 `onSubmit` — 验证和提交参数**
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// 验证部分:
|
|
|
|
|
+if (!this.selectedAddress) {
|
|
|
|
|
+ uni.showToast({ title: '请选择收货地址', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 提交参数部分:
|
|
|
|
|
+data: {
|
|
|
|
|
+ items: orderItems,
|
|
|
|
|
+ remark: this.remark,
|
|
|
|
|
+ addressId: that.selectedAddress.id,
|
|
|
|
|
+ purchaseInfo: that.purchaseForm,
|
|
|
|
|
+ pointsUsed: that.pointsUsed
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 11: 移除不再使用的 `consigneeList` 相关代码**
|
|
|
|
|
+
|
|
|
|
|
+检查并移除:
|
|
|
|
|
+- `showConsigneePicker` 相关状态
|
|
|
|
|
+- 任何 `consigneeList` 引用
|
|
|
|
|
+- 任何 `consignee` 相关的 picker 弹窗
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: 完善 address/address.vue 选择模式回传
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `cfc-frontend/pages/shop/address/address.vue`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Consumes: 无
|
|
|
|
|
+- Produces: 通过 `prevPage.selectedAddress` 回传地址数据
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 确认 `onSelect` 方法回传完整数据**
|
|
|
|
|
+
|
|
|
|
|
+当前 `onSelect` 方法已存在(约第 105-113 行),数据来源于 `addressList` 加载的 `UserAddress` 对象,已有 `id`, `receiverName`, `phone`, `province`, `city`, `district`, `street`, `isDefault` 字段。确认 `onSelect` 直接回传 `item` 对象即可:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+onSelect(item) {
|
|
|
|
|
+ if (this.selectMode) {
|
|
|
|
|
+ var pages = getCurrentPages()
|
|
|
|
|
+ var prevPage = pages[pages.length - 2]
|
|
|
|
|
+ if (prevPage) {
|
|
|
|
|
+ prevPage.selectedAddress = item
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.navigateBack()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+无需修改。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 4: 验证构建
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- 无
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 运行构建验证**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd cfc-frontend && npm run build:mp-weixin
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+预期:构建成功,无报错。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 检查 checkout.vue 中所有 `selectedConsignee` 和 `consigneeList` 引用是否已清理**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+grep -n 'selectedConsignee\|consigneeList\|consignee' cfc-frontend/pages/shop/checkout/checkout.vue | head -20
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+预期:仅保留 `consignee` 相关的行不超过 2 行(如注释或无关引用)。
|