浏览代码

feat: purchase field config UI + consignee mini-program pages (P2-P3)

User 2 月之前
父节点
当前提交
48dbafb263

+ 8 - 0
cfc-frontend/pages.json

@@ -436,6 +436,14 @@
           "path": "address-edit/address-edit",
           "style": { "navigationBarTitleText": "编辑地址" }
         },
+        {
+          "path": "consignee-list/consignee-list",
+          "style": { "navigationBarTitleText": "收货人管理" }
+        },
+        {
+          "path": "consignee-edit/consignee-edit",
+          "style": { "navigationBarTitleText": "编辑收货人" }
+        },
         {
           "path": "detail/detail",
           "style": { "navigationBarTitleText": "商品详情" }

+ 4 - 4
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -46,8 +46,8 @@
           <text>🛒 订单管理</text>
           <text class="arrow">›</text>
         </view>
-        <view class="menu-item" @click="goToAddresses">
-          <text>📍 我的地址</text>
+        <view class="menu-item" @click="goToConsigneeList">
+          <text>📍 收货人管理</text>
           <text class="arrow">›</text>
         </view>
         <view class="menu-item" @click="goToAfterSales">
@@ -169,8 +169,8 @@ export default {
     goToOrders() {
       uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
     },
-    goToAddresses() {
-      uni.navigateTo({ url: '/pages/shop/address/address' })
+    goToConsigneeList() {
+      uni.navigateTo({ url: '/pages/shop/consignee-list/consignee-list' })
     },
     goToAfterSales() {
       uni.navigateTo({ url: '/pages/shop/after-sales/after-sales' })

+ 150 - 65
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -4,26 +4,49 @@
       <text class="loading-text">加载中...</text>
     </view>
     <scroll-view v-else scroll-y class="content">
-      <!-- Address Section -->
-      <view class="section address-section" @click="goSelectAddress">
-        <view v-if="selectedAddress" class="address-content">
+      <!-- Consignee Section -->
+      <view class="section address-section" @click="openConsigneePicker">
+        <view v-if="selectedConsignee" 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>
+            <text class="receiver-name">{{ selectedConsignee.name }}</text>
+            <text class="receiver-phone">{{ formatPhone(selectedConsignee.phone) }}</text>
+            <text v-if="selectedConsignee.isDefault === 1" class="default-badge">默认</text>
           </view>
-          <text class="address-detail">
-            {{ selectedAddress.province }}{{ selectedAddress.city }}{{ selectedAddress.district }}{{ selectedAddress.street }}
-          </text>
+          <text v-if="selectedConsignee.idCard" class="address-detail">身份证: {{ maskField(selectedConsignee.idCard) }}</text>
+          <text v-if="selectedConsignee.ethnicity" class="address-detail">民族: {{ selectedConsignee.ethnicity }}</text>
+          <text v-if="selectedConsignee.bloodType" class="address-detail">血型: {{ selectedConsignee.bloodType }}</text>
           <text class="address-arrow">›</text>
         </view>
-        <view v-else class="address-empty" @click.stop="goAddAddress">
+        <view v-else class="address-empty" @click.stop="goAddConsignee">
           <text class="address-empty-icon">📍</text>
-          <text class="address-empty-text">请添加收货地址</text>
+          <text class="address-empty-text">请选择收货人</text>
           <text class="address-arrow">›</text>
         </view>
       </view>
 
+      <!-- Purchase Info Section (when product requires it) -->
+      <view class="section" v-if="requiredFields.length > 0">
+        <text class="section-title">购买信息</text>
+        <view
+          v-for="field in requiredFields"
+          :key="field.fieldKey"
+          class="purchase-field-row"
+        >
+          <text class="purchase-field-label">
+            {{ field.fieldName }}
+            <text v-if="field.isRequired" class="required-mark">*</text>
+          </text>
+          <input
+            v-model="purchaseForm[field.fieldKey]"
+            class="purchase-field-input"
+            :placeholder="'请输入' + field.fieldName + (field.isRequired ? '' : '(选填)')"
+            :disabled="!!field.prefillValue"
+            placeholder-style="color: #ccc; font-size: 26rpx;"
+          />
+          <text v-if="field.prefillValue" class="purchase-field-prefill">已填写</text>
+        </view>
+      </view>
+
       <!-- Order Items Section -->
       <view class="section">
         <text class="section-title">商品信息</text>
@@ -159,13 +182,17 @@
 
 <script>
 import config from '@/config.js'
-import { getCouponList } from '../../../utils/api.js'
+import { getCouponList, consigneeList, getProductRequiredFields } from '../../../utils/api.js'
 
 export default {
   data() {
     return {
       items: [],
-      selectedAddress: null,
+      selectedConsignee: null,
+      requiredFields: [],       // 需要的购买信息字段
+      purchaseForm: {},         // 用户填写的购买信息
+      consigneeList: [],
+      showConsigneePicker: false,
       freight: 0,
       remark: '',
       loading: false,
@@ -234,7 +261,8 @@ export default {
       this.items = [productData]
       this.calcAmount()
     }
-    this.loadDefaultAddress()
+    this.loadDefaultConsignee()
+    this.loadRequiredFields()
     this.loadCoupons()
     this.loadUserPoints()
   },
@@ -286,55 +314,76 @@ export default {
       if (!minSpend || minSpend <= 0) return '无门槛'
       return '满' + (minSpend / 100).toFixed(2) + '元可用'
     },
-    loadDefaultAddress() {
+    async loadDefaultConsignee() {
       this.loading = true
-      var that = this
-      uni.request({
-        url: config.api('/api/user/address/list'),
-        method: 'POST',
-        data: {},
-        header: {
-          'Content-Type': 'application/json',
-          'Authorization': 'Bearer ' + uni.getStorageSync('token')
-        },
-        success: function(res) {
-          that.loading = false
-          if (res.data && res.data.code === 200) {
-            var list = res.data.data || []
-            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]
+      try {
+        var res = await consigneeList()
+        if (res.code === 200) {
+          var list = res.data || []
+          this.consigneeList = list
+          for (var i = 0; i < list.length; i++) {
+            if (list[i].isDefault === 1) {
+              this.selectedConsignee = list[i]
+              this.autoFillPurchaseForm()
+              break
             }
           }
-        },
-        fail: function() {
-          that.loading = false
+          if (!this.selectedConsignee && list.length > 0) {
+            this.selectedConsignee = list[0]
+            this.autoFillPurchaseForm()
+          }
         }
-      })
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.loading = false
+      }
     },
-    goSelectAddress() {
-      var self = this
-      uni.navigateTo({
-        url: '/pages/shop/address/address?selectMode=1',
-        events: {
-          onAddressSelect: function(data) {
-            self.selectedAddress = data
-          }
-        },
+    async loadRequiredFields() {
+      if (this.items.length === 0) return
+      var productId = this.items[0].productId
+      try {
+        var res = await getProductRequiredFields(productId)
+        if (res.code === 200) {
+          this.requiredFields = res.data || []
+          this.autoFillPurchaseForm()
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    autoFillPurchaseForm() {
+      if (!this.selectedConsignee || this.requiredFields.length === 0) return
+      var form = {}
+      for (var i = 0; i < this.requiredFields.length; i++) {
+        var field = this.requiredFields[i]
+        if (field.prefillValue) {
+          form[field.fieldKey] = field.prefillValue
+        } else {
+          form[field.fieldKey] = ''
+        }
+      }
+      this.purchaseForm = form
+    },
+    openConsigneePicker() {
+      var that = this
+      if (this.consigneeList.length === 0) {
+        uni.showToast({ title: '暂无收货人,请先添加', icon: 'none' })
+        return
+      }
+      var items = this.consigneeList.map(function(c) {
+        return (c.name || '') + ' ' + (c.phone || '')
+      })
+      uni.showActionSheet({
+        itemList: items,
         success: function(res) {
-          res.eventChannel.on('onAddressSelect', function(data) {
-            self.selectedAddress = data
-          })
+          that.selectedConsignee = that.consigneeList[res.tapIndex]
+          that.autoFillPurchaseForm()
         }
       })
     },
-    goAddAddress() {
-      uni.navigateTo({ url: '/pages/shop/address-edit/address-edit' })
+    goAddConsignee() {
+      uni.navigateTo({ url: '/pages/shop/consignee-edit/consignee-edit' })
     },
     loadUserPoints() {
       var that = this
@@ -354,8 +403,8 @@ export default {
       })
     },
     onSubmit() {
-      if (!this.selectedAddress) {
-        uni.showToast({ title: '请选择收货地址', icon: 'none' })
+      if (!this.selectedConsignee) {
+        uni.showToast({ title: '请选择收货', icon: 'none' })
         return
       }
       if (this.items.length === 0) {
@@ -374,14 +423,6 @@ export default {
         }
         orderItems.push(itemData)
       }
-      var addressSnapshot = JSON.stringify({
-        receiverName: this.selectedAddress.receiverName,
-        phone: this.selectedAddress.phone,
-        province: this.selectedAddress.province,
-        city: this.selectedAddress.city,
-        district: this.selectedAddress.district,
-        street: this.selectedAddress.street
-      })
       var that = this
       uni.request({
         url: config.api('/api/product/order/create'),
@@ -389,7 +430,8 @@ export default {
         data: {
           items: orderItems,
           remark: this.remark,
-          addressSnapshot: addressSnapshot,
+          consigneeId: that.selectedConsignee.id,
+          purchaseInfo: that.purchaseForm,
           pointsUsed: that.pointsUsed
         },
         header: {
@@ -429,6 +471,13 @@ export default {
         return phone.substr(0, 3) + '****' + phone.substr(7)
       }
       return phone
+    },
+    maskField(val) {
+      if (!val) return ''
+      if (val.length >= 8) {
+        return val.substr(0, 4) + '****' + val.substr(val.length - 4)
+      }
+      return val
     }
   }
 }
@@ -854,4 +903,40 @@ export default {
   text-decoration: line-through;
   margin-left: 8rpx;
 }
+
+/* ===== Purchase Info Fields ===== */
+.purchase-field-row {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.purchase-field-row:last-child {
+  border-bottom: none;
+}
+.purchase-field-label {
+  font-size: 26rpx;
+  color: #333;
+  width: 160rpx;
+  flex-shrink: 0;
+}
+.required-mark {
+  color: #f56c6c;
+}
+.purchase-field-input {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  text-align: right;
+  padding: 8rpx 0;
+}
+.purchase-field-input[disabled] {
+  background: transparent;
+  color: #999;
+}
+.purchase-field-prefill {
+  font-size: 22rpx;
+  color: #67c23a;
+  margin-left: 8rpx;
+}
 </style>

+ 219 - 0
cfc-frontend/pages/shop/consignee-edit/consignee-edit.vue

@@ -0,0 +1,219 @@
+<template>
+  <view class="container">
+    <view class="form-wrap">
+      <!-- 姓名 -->
+      <view class="form-row">
+        <text class="form-label">收货人姓名</text>
+        <input
+          v-model="form.name"
+          class="form-input"
+          placeholder="请输入姓名"
+          maxlength="30"
+        />
+      </view>
+
+      <!-- 手机号 -->
+      <view class="form-row">
+        <text class="form-label">手机号</text>
+        <input
+          v-model="form.phone"
+          class="form-input"
+          placeholder="请输入手机号"
+          type="number"
+          maxlength="11"
+        />
+      </view>
+
+      <!-- 身份证号 -->
+      <view class="form-row">
+        <text class="form-label">身份证号</text>
+        <input
+          v-model="form.idCard"
+          class="form-input"
+          placeholder="请输入身份证号(选填)"
+          maxlength="18"
+        />
+      </view>
+
+      <!-- 民族 -->
+      <view class="form-row">
+        <text class="form-label">民族</text>
+        <input
+          v-model="form.ethnicity"
+          class="form-input"
+          placeholder="请输入民族(选填)"
+          maxlength="10"
+        />
+      </view>
+
+      <!-- 血型 -->
+      <view class="form-row">
+        <text class="form-label">血型</text>
+        <picker mode="selector" :range="bloodTypes" @change="onBloodTypeChange">
+          <view class="picker-value">
+            <text v-if="form.bloodType">{{ form.bloodType }}</text>
+            <text v-else class="placeholder">请选择血型(选填)</text>
+          </view>
+        </picker>
+      </view>
+
+      <!-- 设为默认 -->
+      <view class="form-row form-row-switch">
+        <text class="form-label">设为默认收货人</text>
+        <switch :checked="form.isDefault === 1" @change="onDefaultChange" color="#667eea" />
+      </view>
+
+      <!-- Save -->
+      <button
+        :class="['save-btn', saving ? 'disabled' : '']"
+        :disabled="saving"
+        @click="handleSave"
+      >
+        {{ saving ? '保存中...' : '保存' }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { consigneeDetail, consigneeSave } from '../../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      form: {
+        name: '',
+        phone: '',
+        idCard: '',
+        ethnicity: '',
+        bloodType: '',
+        isDefault: 0
+      },
+      bloodTypes: ['A型', 'B型', 'O型', 'AB型', '其他'],
+      saving: false,
+      isEdit: false
+    }
+  },
+  onLoad(options) {
+    if (options && options.id) {
+      this.isEdit = true
+      this.loadDetail(options.id)
+      uni.setNavigationBarTitle({ title: '编辑收货人' })
+    }
+  },
+  methods: {
+    async loadDetail(id) {
+      try {
+        const res = await consigneeDetail(id)
+        if (res.code === 200 && res.data) {
+          this.form = {
+            id: res.data.id,
+            name: res.data.name || '',
+            phone: res.data.phone || '',
+            idCard: res.data.idCard || '',
+            ethnicity: res.data.ethnicity || '',
+            bloodType: res.data.bloodType || '',
+            isDefault: res.data.isDefault || 0
+          }
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    onBloodTypeChange(e) {
+      this.form.bloodType = this.bloodTypes[e.detail.value]
+    },
+    onDefaultChange(e) {
+      this.form.isDefault = e.detail.value ? 1 : 0
+    },
+    async handleSave() {
+      if (!this.form.name || !this.form.name.trim()) {
+        uni.showToast({ title: '请输入收货人姓名', icon: 'none' })
+        return
+      }
+      if (!this.form.phone || this.form.phone.length < 11) {
+        uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
+        return
+      }
+      this.saving = true
+      try {
+        const res = await consigneeSave(this.form)
+        if (res.code === 200) {
+          uni.showToast({ title: '保存成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 1000)
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.saving = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding: 20rpx;
+}
+.form-wrap {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+}
+.form-row {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.form-row:last-child {
+  border-bottom: none;
+}
+.form-label {
+  font-size: 28rpx;
+  color: #333;
+  width: 180rpx;
+  flex-shrink: 0;
+}
+.form-input {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+  text-align: right;
+  padding: 8rpx 0;
+}
+.picker-value {
+  flex: 1;
+  text-align: right;
+  font-size: 28rpx;
+  color: #333;
+  padding: 8rpx 0;
+}
+.picker-value .placeholder {
+  color: #ccc;
+}
+.form-row-switch {
+  justify-content: space-between;
+}
+.form-row-switch .form-label {
+  width: auto;
+}
+.save-btn {
+  width: 100%;
+  margin-top: 40rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 50rpx;
+  font-size: 30rpx;
+  padding: 24rpx;
+  line-height: 1.5;
+}
+.save-btn.disabled {
+  opacity: 0.6;
+}
+</style>

+ 212 - 0
cfc-frontend/pages/shop/consignee-list/consignee-list.vue

@@ -0,0 +1,212 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text>加载中...</text>
+    </view>
+
+    <view v-else-if="list.length === 0" class="empty-wrap">
+      <text class="empty-icon">📍</text>
+      <text class="empty-text">暂无收货人信息</text>
+      <text class="empty-desc">添加收货人后可在购买时快速选择</text>
+      <button class="add-btn" @click="goAdd">添加收货人</button>
+    </view>
+
+    <view v-else class="list-wrap">
+      <view
+        v-for="item in list"
+        :key="item.id"
+        class="consignee-card"
+        @click="goEdit(item.id)"
+      >
+        <view class="card-header">
+          <text class="card-name">{{ item.name || '未填写' }}</text>
+          <text class="card-phone">{{ item.phone || '' }}</text>
+          <text v-if="item.isDefault === 1" class="default-tag">默认</text>
+        </view>
+        <view class="card-body">
+          <text v-if="item.idCard" class="card-info">身份证: {{ maskIdCard(item.idCard) }}</text>
+          <text v-if="item.ethnicity" class="card-info">民族: {{ item.ethnicity }}</text>
+          <text v-if="item.bloodType" class="card-info">血型: {{ item.bloodType }}</text>
+          <text v-if="item.handSignature" class="card-info">已设置手签名</text>
+          <text v-if="!item.idCard && !item.ethnicity && !item.bloodType && !item.handSignature" class="card-info card-info-muted">暂无详细信息</text>
+        </view>
+        <view class="card-footer">
+          <text class="delete-btn" @click.stop="handleDelete(item.id)">删除</text>
+          <text class="arrow">›</text>
+        </view>
+      </view>
+
+      <button class="add-btn add-btn-bottom" @click="goAdd">添加收货人</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { consigneeList, consigneeDelete } from '../../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      list: []
+    }
+  },
+  onShow() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await consigneeList()
+        if (res.code === 200) {
+          this.list = res.data || []
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.loading = false
+      }
+    },
+    goAdd() {
+      uni.navigateTo({ url: '/pages/shop/consignee-edit/consignee-edit' })
+    },
+    goEdit(id) {
+      uni.navigateTo({ url: '/pages/shop/consignee-edit/consignee-edit?id=' + id })
+    },
+    handleDelete(id) {
+      var that = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除该收货人吗?',
+        success: function(res) {
+          if (res.confirm) {
+            that.doDelete(id)
+          }
+        }
+      })
+    },
+    async doDelete(id) {
+      try {
+        const res = await consigneeDelete(id)
+        if (res.code === 200) {
+          uni.showToast({ title: '删除成功', icon: 'success' })
+          this.loadList()
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    maskIdCard(card) {
+      if (!card || card.length < 8) return card
+      return card.substring(0, 4) + '********' + card.substring(card.length - 4)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding: 20rpx;
+}
+.loading-wrap, .empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 200rpx 60rpx;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 60rpx;
+}
+.add-btn {
+  width: 60%;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 50rpx;
+  font-size: 30rpx;
+  padding: 24rpx;
+  line-height: 1.5;
+}
+.consignee-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.card-header {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+.card-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.card-phone {
+  font-size: 28rpx;
+  color: #666;
+}
+.default-tag {
+  font-size: 22rpx;
+  color: #F97316;
+  background: #FFF7ED;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #FED7AA;
+}
+.card-body {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8rpx;
+  margin-bottom: 16rpx;
+}
+.card-info {
+  font-size: 24rpx;
+  color: #999;
+  background: #f5f5f5;
+  padding: 6rpx 12rpx;
+  border-radius: 8rpx;
+}
+.card-info-muted {
+  color: #ccc;
+  background: transparent;
+}
+.card-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #f5f5f5;
+}
+.delete-btn {
+  font-size: 26rpx;
+  color: #f56c6c;
+  padding: 8rpx 16rpx;
+}
+.arrow {
+  font-size: 28rpx;
+  color: #ccc;
+}
+.add-btn-bottom {
+  margin-top: 20rpx;
+  width: 80%;
+}
+</style>

+ 9 - 0
cfc-frontend/utils/api.js

@@ -1619,3 +1619,12 @@ export const cartAdd = (data) => request('/api/cart/add', 'POST', data)
 export const cartList = (data) => request('/api/cart/list', 'POST', data)
 export const cartUpdate = (data) => request('/api/cart/update', 'POST', data)
 export const cartRemove = (data) => request('/api/cart/remove', 'POST', data)
+
+// ===== 收货人管理 =====
+export const consigneeList = () => request('/api/consignee/list', 'POST', {})
+export const consigneeDetail = (id) => request('/api/consignee/detail', 'POST', { id })
+export const consigneeSave = (data) => request('/api/consignee/save', 'POST', data)
+export const consigneeDelete = (id) => request('/api/consignee/delete', 'POST', { id })
+
+// ===== 商品购买字段 =====
+export const getProductRequiredFields = (productId) => request('/api/product/purchase_fields/required', 'POST', { productId })

+ 25 - 0
cfc-web/src/api/admin.js

@@ -526,6 +526,31 @@ export function deleteProductSku(id) {
   })
 }
 
+// ========== 购买信息采集 API ==========
+
+export function getPredefinedPurchaseFields() {
+  return request({
+    url: '/api/admin/product/purchase_fields/predefined',
+    method: 'post'
+  })
+}
+
+export function getProductPurchaseFields(productId) {
+  return request({
+    url: '/api/admin/product/purchase_fields/list',
+    method: 'post',
+    data: { productId }
+  })
+}
+
+export function saveProductPurchaseFields(productId, fields) {
+  return request({
+    url: '/api/admin/product/purchase_fields/save',
+    method: 'post',
+    data: { productId, fields }
+  })
+}
+
 // ========== 分类管理 API ==========
 
 export function getCategoryTree(includeDisabled) {

+ 129 - 1
cfc-web/src/views/admin/ProductEdit.vue

@@ -122,6 +122,40 @@
           <ProductSkuEditor :productId="form.id" />
         </el-form-item>
 
+        <!-- 购买信息采集 -->
+        <el-form-item label="购买信息采集">
+          <div class="purchase-field-section">
+            <el-checkbox v-model="purchaseFieldsEnabled" @change="handlePurchaseFieldsToggle">
+              启用购买信息采集(用户购买时需填写/确认以下信息)
+            </el-checkbox>
+            <div v-if="purchaseFieldsEnabled && predefinedFields.length" class="purchase-field-list">
+              <div
+                v-for="(pf, idx) in predefinedFields"
+                :key="pf.fieldKey"
+                class="purchase-field-item"
+              >
+                <el-checkbox
+                  v-model="pf.selected"
+                  @change="syncPurchaseFieldOrder"
+                >
+                  {{ pf.fieldName }}
+                </el-checkbox>
+                <el-checkbox
+                  v-if="pf.selected"
+                  v-model="pf.required"
+                  class="purchase-field-required"
+                  size="mini"
+                >
+                  必填
+                </el-checkbox>
+                <span v-if="pf.selected" class="purchase-field-drag">
+                  <i class="el-icon-rank"></i>
+                </span>
+              </div>
+            </div>
+          </div>
+        </el-form-item>
+
         <el-form-item>
           <el-button type="primary" :loading="saving" @click="handleSave">
             {{ isEdit ? '保存修改' : '创建商品' }}
@@ -134,7 +168,7 @@
 </template>
 
 <script>
-import { getProduct, createProduct, updateProduct, listAllSupplySystems } from '@/api/admin'
+import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields } from '@/api/admin'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import ProductSkuEditor from './components/ProductSkuEditor.vue'
@@ -173,6 +207,8 @@ export default {
       },
       saving: false,
       pageLoading: false,
+      purchaseFieldsEnabled: false,
+      predefinedFields: [],
       editorConfig: {
         MENU_CONF: {
           uploadImage: {
@@ -216,6 +252,7 @@ export default {
     this.editorConfig.MENU_CONF.uploadImage.server = this.uploadActionUrl
     this.editorConfig.MENU_CONF.uploadImage.headers = this.uploadHeaders
     this.loadSupplySystems()
+    this.loadPredefinedFields()
     if (this.isEdit) {
       this.loadProduct(this.$route.query.id)
     }
@@ -274,6 +311,8 @@ export default {
           }
           // Load dimension weights
           this.loadDimensionWeights(p.id)
+          // Load purchase fields config
+          this.loadPurchaseFields(p.id)
           // v-model 已绑定 form.description,editor 内容自动更新
         }
       } catch (e) {
@@ -296,6 +335,56 @@ export default {
         // ignore
       }
     },
+    async loadPredefinedFields() {
+      try {
+        const res = await getPredefinedPurchaseFields()
+        if (res.code === 200 && Array.isArray(res.data)) {
+          this.predefinedFields = res.data.map(f => ({
+            fieldKey: f.fieldKey,
+            fieldName: f.fieldName,
+            selected: false,
+            required: true
+          }))
+        }
+      } catch (e) {
+        console.warn('加载预定义字段失败', e)
+      }
+    },
+    async loadPurchaseFields(productId) {
+      try {
+        const res = await getProductPurchaseFields(productId)
+        if (res.code === 200 && Array.isArray(res.data)) {
+          const configs = res.data
+          this.purchaseFieldsEnabled = configs.length > 0
+          this.predefinedFields.forEach(pf => {
+            const match = configs.find(c => c.fieldKey === pf.fieldKey)
+            pf.selected = !!match
+            pf.required = match ? match.isRequired === 1 : false
+          })
+        }
+      } catch (e) {
+        console.warn('加载购买字段配置失败', e)
+      }
+    },
+    handlePurchaseFieldsToggle(val) {
+      if (!val) {
+        this.predefinedFields.forEach(pf => { pf.selected = false })
+      }
+    },
+    syncPurchaseFieldOrder() {
+      // sortOrder is derived from selection order
+    },
+    buildPurchaseFieldPayload() {
+      if (!this.purchaseFieldsEnabled) return []
+      return this.predefinedFields
+        .filter(pf => pf.selected)
+        .map((pf, idx) => ({
+          fieldKey: pf.fieldKey,
+          fieldName: pf.fieldName,
+          isRequired: pf.required ? 1 : 0,
+          sortOrder: idx
+        }))
+    },
     beforeUpload(file) {
       const isImage = file.type.startsWith('image/')
       const isLt5M = file.size / 1024 / 1024 < 5
@@ -367,6 +456,15 @@ export default {
                 console.warn('保存维度权重失败', e)
               }
             }
+            // Save purchase fields config
+            if (productId) {
+              try {
+                const fields = this.buildPurchaseFieldPayload()
+                await saveProductPurchaseFields(productId, fields)
+              } catch (e) {
+                console.warn('保存购买字段配置失败', e)
+              }
+            }
             this.$message.success(this.isEdit ? '保存成功' : '创建成功')
             this.$router.push('/product-manage')
           } else {
@@ -482,4 +580,34 @@ export default {
 .rich-editor /deep/ .w-e-text-container {
   min-height: 300px;
 }
+.purchase-field-section {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.purchase-field-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  padding: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+  border: 1px solid #eee;
+}
+.purchase-field-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #e8e8e8;
+  border-radius: 4px;
+}
+.purchase-field-required {
+  font-size: 12px;
+}
+.purchase-field-drag {
+  color: #ccc;
+  cursor: move;
+}
 </style>