Răsfoiți Sursa

feat: 个人信息所在地改为省市区三级联动并与订单联系人共享地区数据

- 前端 user-edit.vue: 移除四级 AddressPicker, 改为与收货人地址一致的省市区三级联动弹窗, 提交 province/city/district/street
- 前端 profile.vue: loadUserInfo 按14个字段计算个人信息完整度 profileProgress, 修复一直显示0/14的问题
- 后端: users/consignees 表新增 province/city/district/street 字段(迁移211 + schema.sql同步)
- UserService.updateUserInfo: 保存地区后自动创建/更新同名同手机号联系人, 共享地区数据
- ConsigneeService.save: 联系人姓名手机号与当前用户一致时, 反向同步地区回用户
asus 1 lună în urmă
părinte
comite
37fef220b7

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8586,5 +8586,15 @@ private void runMigration100() {
 		} catch (Exception e) {
 			// 表已存在,忽略错误
 		}
+
+		// 迁移212: users/consignees表添加省份/城市/区县/详细地址字段(个人信息所在地与订单联系人共享数据)
+		ensureColumn("users", "province", "VARCHAR(50) DEFAULT NULL COMMENT '省份'");
+		ensureColumn("users", "city", "VARCHAR(50) DEFAULT NULL COMMENT '城市'");
+		ensureColumn("users", "district", "VARCHAR(50) DEFAULT NULL COMMENT '区县'");
+		ensureColumn("users", "street", "VARCHAR(100) DEFAULT NULL COMMENT '详细地址'");
+		ensureColumn("consignees", "province", "VARCHAR(50) DEFAULT NULL COMMENT '省份'");
+		ensureColumn("consignees", "city", "VARCHAR(50) DEFAULT NULL COMMENT '城市'");
+		ensureColumn("consignees", "district", "VARCHAR(50) DEFAULT NULL COMMENT '区县'");
+		ensureColumn("consignees", "street", "VARCHAR(100) DEFAULT NULL COMMENT '详细地址'");
 	}
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java

@@ -100,6 +100,10 @@ public class UserController {
         if (params.containsKey("hobbies")) dto.setHobbies((String) params.get("hobbies"));
         if (params.containsKey("dietPreferences")) dto.setDietPreferences((String) params.get("dietPreferences"));
         if (params.containsKey("address")) dto.setAddress((String) params.get("address"));
+        if (params.containsKey("province")) dto.setProvince((String) params.get("province"));
+        if (params.containsKey("city")) dto.setCity((String) params.get("city"));
+        if (params.containsKey("district")) dto.setDistrict((String) params.get("district"));
+        if (params.containsKey("street")) dto.setStreet((String) params.get("street"));
 
         boolean success = userService.updateUserInfo(userId, dto);
         if (success) {

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateUserDTO.java

@@ -35,4 +35,10 @@ public class UpdateUserDTO {
 
     // 地址
     private String address;
+
+    // 所在地区(省市区街道拆分,与订单收货人地址结构统一)
+    private String province; // 省份
+    private String city; // 城市
+    private String district; // 区县
+    private String street; // 详细地址
 }

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Consignee.java

@@ -31,6 +31,12 @@ public class Consignee implements Serializable {
 
     private String address;
 
+    // 所在地区(省市区街道拆分,与用户个人信息地区共享数据)
+    private String province; // 省份
+    private String city; // 城市
+    private String district; // 区县
+    private String street; // 详细地址
+
     private Integer isDefault;
 
     private Date createdAt;

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -78,6 +78,12 @@ public class User implements Serializable {
     // 地址
     private String address;
 
+    // 所在地区(与订单收货人地址结构统一,按省市区街道拆分)
+    private String province; // 省份
+    private String city; // 城市
+    private String district; // 区县
+    private String street; // 详细地址
+
     // 个人信息补充
     private String ethnicity; // 民族
     private String bloodType; // 血型

+ 52 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ConsigneeService.java

@@ -3,7 +3,9 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Consignee;
+import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.ConsigneeMapper;
+import com.etotem.cfc.mapper.UserMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -16,6 +18,9 @@ public class ConsigneeService {
     @Resource
     private ConsigneeMapper consigneeMapper;
 
+    @Resource
+    private UserMapper userMapper;
+
     public Result<List<Consignee>> list(Long userId) {
         List<Consignee> list = consigneeMapper.selectList(
                 new LambdaQueryWrapper<Consignee>()
@@ -52,6 +57,8 @@ public class ConsigneeService {
                 clearDefault(existing.getUserId());
             }
             consigneeMapper.updateById(consignee);
+            // 若联系人与当前用户姓名/手机号一致,同步地区信息到用户
+            syncUserRegion(existing.getUserId(), consignee);
             return Result.success(consignee.getId());
         } else {
             // create
@@ -62,10 +69,55 @@ public class ConsigneeService {
                 clearDefault(consignee.getUserId());
             }
             consigneeMapper.insert(consignee);
+            // 若联系人与当前用户姓名/手机号一致,同步地区信息到用户
+            syncUserRegion(consignee.getUserId(), consignee);
             return Result.success(consignee.getId());
         }
     }
 
+    /**
+     * 用户创建/编辑联系人时,若联系人姓名+手机号与当前用户一致,则将联系人的地区信息同步回用户。
+     * (个人信息所在地与订单联系人共享数据的反向同步)
+     */
+    private void syncUserRegion(Long userId, Consignee consignee) {
+        try {
+            if (userId == null) return;
+            User user = userMapper.selectById(userId);
+            if (user == null) return;
+            // 比对姓名:用户的真实姓名优先,其次昵称
+            String userName = user.getRealName();
+            if (userName == null || userName.trim().isEmpty()) {
+                userName = user.getNickname();
+            }
+            if (userName == null || userName.trim().isEmpty()) return;
+            if (!userName.equals(consignee.getName())) return;
+            // 比对手机号:联系人有手机号且用户有手机号时必须一致;联系人无手机号视为匹配
+            String consigneePhone = consignee.getPhone();
+            String userPhone = user.getPhone();
+            if (consigneePhone != null && !consigneePhone.isEmpty()
+                    && userPhone != null && !userPhone.isEmpty()
+                    && !consigneePhone.equals(userPhone)) {
+                return;
+            }
+            boolean hasRegion = consignee.getProvince() != null && !consignee.getProvince().isEmpty();
+            if (!hasRegion) return;
+            user.setProvince(consignee.getProvince());
+            user.setCity(consignee.getCity());
+            user.setDistrict(consignee.getDistrict());
+            user.setStreet(consignee.getStreet());
+            // 同步拼接后的完整地址文本
+            StringBuilder fullAddress = new StringBuilder();
+            if (consignee.getProvince() != null) fullAddress.append(consignee.getProvince());
+            if (consignee.getCity() != null) fullAddress.append(consignee.getCity());
+            if (consignee.getDistrict() != null) fullAddress.append(consignee.getDistrict());
+            if (consignee.getStreet() != null) fullAddress.append(consignee.getStreet());
+            user.setAddress(fullAddress.toString());
+            userMapper.updateById(user);
+        } catch (Exception e) {
+            // 反向同步失败不应阻塞联系人保存
+        }
+    }
+
     @Transactional
     public Result<String> delete(Long id, Long userId) {
         Consignee existing = consigneeMapper.selectById(id);

+ 72 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -28,6 +28,8 @@ import java.util.*;
 import com.etotem.cfc.entity.UserProfileHistory;
 import com.etotem.cfc.mapper.UserProfileHistoryMapper;
 import com.etotem.cfc.service.FamilyJoinRequestService;
+import com.etotem.cfc.entity.Consignee;
+import com.etotem.cfc.mapper.ConsigneeMapper;
 
 
 @Slf4j
@@ -70,6 +72,9 @@ private FamilyInvitationService familyInvitationService;
     @Resource
     private FamilyJoinRequestService familyJoinRequestService;
 
+    @Resource
+    private ConsigneeMapper consigneeMapper;
+
     public LoginResultDTO wechatLogin(WechatLoginDTO dto) {
         // 1. 通过code获取openid
         Map<String, String> sessionData = wechatService.code2Session(dto.getCode());
@@ -1170,15 +1175,81 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
             user.setDietPreferences(dto.getDietPreferences());
             recordProfileChange(userId, "dietPreferences", oldDiet, dto.getDietPreferences(), "user_edit");
         }
-        // 更新地址
+        // 更新地址(省市区街道拆分存储,与订单收货人地址结构统一)
+        if (dto.getProvince() != null) {
+            user.setProvince(dto.getProvince());
+        }
+        if (dto.getCity() != null) {
+            user.setCity(dto.getCity());
+        }
+        if (dto.getDistrict() != null) {
+            user.setDistrict(dto.getDistrict());
+        }
+        if (dto.getStreet() != null) {
+            user.setStreet(dto.getStreet());
+        }
         if (dto.getAddress() != null) {
             user.setAddress(dto.getAddress());
         }
 
         userMapper.updateById(user);
+
+        // 用户填写地区后,自动创建/更新一条联系人(Consignee),与订单联系人共享数据
+        syncUserConsignee(userId, user);
+
         return true;
     }
 
+    /**
+     * 用户保存个人信息(含所在地区)时,自动创建/更新一条联系人记录。
+     * 联系人姓名/手机号取用户真实姓名/手机号,地区信息与用户保持一致;
+     * 若已存在同名同手机号的联系人则更新其地区,否则新建。
+     */
+    private void syncUserConsignee(Long userId, User user) {
+        try {
+            String consigneeName = user.getRealName();
+            if (consigneeName == null || consigneeName.trim().isEmpty()) {
+                consigneeName = user.getNickname();
+            }
+            String phone = user.getPhone();
+            boolean hasRegion = user.getProvince() != null && !user.getProvince().isEmpty();
+            // 没有姓名/手机号或没有填写地区时不做自动同步
+            if (consigneeName == null || consigneeName.trim().isEmpty()
+                    || phone == null || phone.trim().isEmpty() || !hasRegion) {
+                return;
+            }
+            Consignee existing = consigneeMapper.selectOne(
+                    new LambdaQueryWrapper<Consignee>()
+                            .eq(Consignee::getUserId, userId)
+                            .eq(Consignee::getName, consigneeName)
+                            .eq(Consignee::getPhone, phone)
+                            .last("LIMIT 1"));
+            if (existing != null) {
+                existing.setProvince(user.getProvince());
+                existing.setCity(user.getCity());
+                existing.setDistrict(user.getDistrict());
+                existing.setStreet(user.getStreet());
+                existing.setAddress(user.getAddress());
+                consigneeMapper.updateById(existing);
+            } else {
+                Consignee consignee = new Consignee();
+                consignee.setUserId(userId);
+                consignee.setName(consigneeName);
+                consignee.setPhone(phone);
+                consignee.setProvince(user.getProvince());
+                consignee.setCity(user.getCity());
+                consignee.setDistrict(user.getDistrict());
+                consignee.setStreet(user.getStreet());
+                consignee.setAddress(user.getAddress());
+                consignee.setIsDefault(0);
+                consigneeMapper.insert(consignee);
+            }
+        } catch (Exception e) {
+            // 联系人自动同步失败不应阻塞用户信息保存
+            log.warn("自动同步联系人失败: {}", e.getMessage());
+        }
+    }
+
     /**
      * 记录用户信息变更历史(仅当新旧值不同时写入)
      */

+ 8 - 0
cfc-backend/src/main/resources/schema.sql

@@ -29,6 +29,10 @@ CREATE TABLE IF NOT EXISTS users (
     gender ENUM('male','female'),
     birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥',
     mascot VARCHAR(20) DEFAULT NULL COMMENT 'AI助手形象: xibao(浠宝)/fubao(福宝)',
+    province VARCHAR(50) DEFAULT NULL COMMENT '省份',
+    city VARCHAR(50) DEFAULT NULL COMMENT '城市',
+    district VARCHAR(50) DEFAULT NULL COMMENT '区县',
+    street VARCHAR(100) DEFAULT NULL COMMENT '详细地址',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_openid (openid),
@@ -2531,6 +2535,10 @@ CREATE TABLE IF NOT EXISTS consignees (
     ethnicity VARCHAR(20) COMMENT '民族',
     blood_type VARCHAR(20) COMMENT '血型',
     address VARCHAR(500) COMMENT '收货地址',
+    province VARCHAR(50) DEFAULT NULL COMMENT '省份',
+    city VARCHAR(50) DEFAULT NULL COMMENT '城市',
+    district VARCHAR(50) DEFAULT NULL COMMENT '区县',
+    street VARCHAR(100) DEFAULT NULL COMMENT '详细地址',
     is_default TINYINT(1) DEFAULT 0 COMMENT '是否默认收货人',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

+ 15 - 0
cfc-frontend/pages/profile-main/profile.vue

@@ -345,6 +345,21 @@ export default {
         if (res && res.code === 200 && res.data) {
           self.userInfo = res.data
           if (!self.nickname) self.nickname = res.data.nickname || ''
+          // 个人信息完整度:统计 14 个字段(与 user-edit.vue 表单一致)
+          var d = res.data
+          var fields = [
+            d.avatar, d.nickname, d.realName, d.gender, d.idCard,
+            d.birthday, d.birthHour, d.ethnicity, d.bloodType,
+            d.highestEducation, d.maritalStatus, d.hobbies, d.dietPreferences,
+            d.address
+          ]
+          var count = 0
+          fields.forEach(function(v) {
+            if (v !== undefined && v !== null && String(v).trim() !== '') {
+              count += 1
+            }
+          })
+          self.profileProgress = count
         }
       }).catch(function() {})
     },

+ 373 - 39
cfc-frontend/pages/user-edit/user-edit.vue

@@ -140,11 +140,21 @@
 				</picker>
 			</view>
 
-			<!-- 地址选择 -->
-			<view class="form-item">
-				<text class="label">所在地区 <text class="required" v-if="isNewUserFlow">*</text></text>
-				<AddressPicker ref="addressPicker" />
+		<!-- 所在地区(省市区三级联动,与订单收货人地址一致) -->
+		<view class="form-item address-row" @click="openRegionPicker">
+			<text class="label">所在地区 <text class="required" v-if="isNewUserFlow">*</text></text>
+			<view class="address-value-wrap">
+				<text v-if="form.address" class="address-value">{{ form.address }}</text>
+				<text v-else class="address-placeholder">请选择省市区</text>
+				<text class="address-arrow">›</text>
 			</view>
+		</view>
+
+		<!-- 详细地址 -->
+		<view class="form-item">
+			<text class="label">详细地址</text>
+			<input type="text" v-model="form.street" placeholder="请输入详细地址(选填)" class="input" maxlength="100" @input="updateFullAddress" />
+		</view>
 
 			<!-- 兴趣爱好 -->
 			<view class="form-item">
@@ -247,17 +257,48 @@
 		<view v-else-if="isEdit">
 			<FamilyEmptyState type="card" />
 		</view>
+
+		<!-- 区域选择弹窗(省市区三级联动) -->
+		<view class="region-mask" v-if="showRegionPicker" @click="cancelRegion">
+			<view class="region-picker" @click.stop>
+				<view class="region-header">
+					<text class="region-header-btn" @click="cancelRegion">取消</text>
+					<text class="region-header-title">选择地区</text>
+					<text class="region-header-btn region-confirm" @click="confirmRegion">确定</text>
+				</view>
+				<view class="region-breadcrumb">
+					<text
+						v-for="(name, i) in regionBreadcrumb"
+						:key="i"
+						:class="['crumb-item', i === currentRegionLevel ? 'crumb-active' : '']"
+						@click="jumpRegionLevel(i)"
+					>{{ name || '请选择' }}</text>
+					<text class="crumb-arrow" v-if="currentRegionLevel < 2">›</text>
+				</view>
+				<scroll-view class="region-list" scroll-y>
+					<view
+						v-for="(item, idx) in currentRegionItems"
+						:key="idx"
+						:class="['region-item', isRegionSelected(item) ? 'region-item-selected' : '']"
+						@click="selectRegionItem(item)"
+					>
+						<text class="region-item-text">{{ getRegionName(item) }}</text>
+						<text class="region-item-check" v-if="isRegionSelected(item)">✓</text>
+					</view>
+					<view v-if="regionLoading" class="region-loading">
+						<text>加载中...</text>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
 	</view>
 </template>
 
 <script>
 import { updateUserInfo, createFamily, directRegister, getUserInfo, requestJoinByCode } from '../../utils/api.js'
-import AddressPicker from '../../components/address-picker.vue'
+import config from '@/config.js'
 
 export default {
-	components: {
-		AddressPicker
-	},
 	data() {
 		return {
 			isEdit: false,
@@ -285,8 +326,21 @@ export default {
 				hobbies: '',
 				dietPreferences: '',
 				address: '',
-				addressId: null
+				province: '',
+				city: '',
+				district: '',
+				street: ''
 			},
+			// 区域选择弹窗(省市区三级联动,与订单收货人地址一致)
+			showRegionPicker: false,
+			currentRegionLevel: 0, // 0=省份, 1=城市, 2=区县
+			regionLoading: false,
+			provinceList: [],
+			cityList: [],
+			districtList: [],
+			tempProvince: null,
+			tempCity: null,
+			tempDistrict: null,
 			ethnicityOptions: ['汉族','蒙古族','回族','藏族','维吾尔族','苗族','彝族','壮族','布依族','朝鲜族','满族','侗族','瑶族','白族','土家族','哈尼族','哈萨克族','傣族','黎族','其他'],
 			bloodTypeOptions: ['A','B','AB','O','未知'],
 			birthHourOptions: ['子时(23-01)','丑时(01-03)','寅时(03-05)','卯时(05-07)','辰时(07-09)','巳时(09-11)','午时(11-13)','未时(13-15)','申时(15-17)','酉时(17-19)','戌时(19-21)','亥时(21-23)'],
@@ -318,6 +372,19 @@ export default {
       if (this.token) return true
       return this.$store.getters.hasFamily
     },
+    regionBreadcrumb: function() {
+      var names = []
+      names.push(this.tempProvince ? this.getRegionName(this.tempProvince) : '省份')
+      names.push(this.tempCity ? this.getRegionName(this.tempCity) : '城市')
+      names.push(this.tempDistrict ? this.getRegionName(this.tempDistrict) : '区县')
+      return names
+    },
+    currentRegionItems: function() {
+      if (this.currentRegionLevel === 0) return this.provinceList
+      if (this.currentRegionLevel === 1) return this.cityList
+      if (this.currentRegionLevel === 2) return this.districtList
+      return []
+    },
   },
 	onLoad(options) {
 		if (options.token && options.userId) {
@@ -430,10 +497,143 @@ export default {
 		onMaritalChange(e) {
 			this.form.maritalStatus = this.maritalOptions[e.detail.value]
 		},
-		selectMascot(code) {
-			this.form.mascot = code
-		},
-		// 加载已有用户信息
+	selectMascot(code) {
+		this.form.mascot = code
+	},
+	// ===== 省市区三级联动弹窗(与订单收货人地址一致) =====
+	openRegionPicker() {
+		this.tempProvince = this.form.province ? { name: this.form.province, id: null } : null
+		this.tempCity = this.form.city ? { name: this.form.city, id: null } : null
+		this.tempDistrict = this.form.district ? { name: this.form.district, id: null } : null
+		this.currentRegionLevel = 0
+		this.showRegionPicker = true
+		this.loadRegionProvinces()
+	},
+	loadRegionProvinces() {
+		var self = this
+		self.regionLoading = true
+		uni.request({
+			url: config.api('/api/region/provinces'),
+			method: 'POST',
+			header: {
+				'Content-Type': 'application/json',
+				'Authorization': 'Bearer ' + uni.getStorageSync('token')
+			},
+			success: function(res) {
+				self.regionLoading = false
+				if (res.data && res.data.code === 200) {
+					self.provinceList = res.data.data || []
+				}
+			},
+			fail: function() {
+				self.regionLoading = false
+			}
+		})
+	},
+	loadRegionCities(parentId) {
+		if (!parentId) return
+		var self = this
+		self.regionLoading = true
+		uni.request({
+			url: config.api('/api/region/cities'),
+			method: 'POST',
+			data: { parentId: parentId },
+			header: {
+				'Content-Type': 'application/json',
+				'Authorization': 'Bearer ' + uni.getStorageSync('token')
+			},
+			success: function(res) {
+				self.regionLoading = false
+				if (res.data && res.data.code === 200) {
+					self.cityList = res.data.data || []
+				}
+			},
+			fail: function() {
+				self.regionLoading = false
+			}
+		})
+	},
+	loadRegionDistricts(parentId) {
+		if (!parentId) return
+		var self = this
+		self.regionLoading = true
+		uni.request({
+			url: config.api('/api/region/districts'),
+			method: 'POST',
+			data: { parentId: parentId },
+			header: {
+				'Content-Type': 'application/json',
+				'Authorization': 'Bearer ' + uni.getStorageSync('token')
+			},
+			success: function(res) {
+				self.regionLoading = false
+				if (res.data && res.data.code === 200) {
+					self.districtList = res.data.data || []
+				}
+			},
+			fail: function() {
+				self.regionLoading = false
+			}
+		})
+	},
+	selectRegionItem(item) {
+		if (this.currentRegionLevel === 0) {
+			this.tempProvince = item
+			this.tempCity = null
+			this.tempDistrict = null
+			this.cityList = []
+			this.districtList = []
+			this.currentRegionLevel = 1
+			this.loadRegionCities(this.getRegionId(item))
+		} else if (this.currentRegionLevel === 1) {
+			this.tempCity = item
+			this.tempDistrict = null
+			this.districtList = []
+			this.currentRegionLevel = 2
+			this.loadRegionDistricts(this.getRegionId(item))
+		} else if (this.currentRegionLevel === 2) {
+			this.tempDistrict = item
+			this.confirmRegion()
+		}
+	},
+	confirmRegion() {
+		if (this.tempProvince) {
+			this.form.province = this.getRegionName(this.tempProvince)
+		}
+		if (this.tempCity) {
+			this.form.city = this.getRegionName(this.tempCity)
+		}
+		if (this.tempDistrict) {
+			this.form.district = this.getRegionName(this.tempDistrict)
+		}
+		this.updateFullAddress()
+		this.showRegionPicker = false
+	},
+	cancelRegion() {
+		this.showRegionPicker = false
+	},
+	jumpRegionLevel(level) {
+		if (level < this.currentRegionLevel) {
+			this.currentRegionLevel = level
+		}
+	},
+	getRegionName(item) {
+		return item && (item.name || item.province || item.city || item.district || '')
+	},
+	getRegionId(item) {
+		return item && (item.id || item.code)
+	},
+	isRegionSelected(item) {
+		var name = this.getRegionName(item)
+		if (this.currentRegionLevel === 0) return this.tempProvince && this.getRegionName(this.tempProvince) === name
+		if (this.currentRegionLevel === 1) return this.tempCity && this.getRegionName(this.tempCity) === name
+		if (this.currentRegionLevel === 2) return this.tempDistrict && this.getRegionName(this.tempDistrict) === name
+		return false
+	},
+	updateFullAddress() {
+		this.form.address = (this.form.province || '') + (this.form.city || '') + (this.form.district || '') + (this.form.street || '')
+	},
+	// 加载已有用户信息
 		async loadUserInfo() {
 			try {
 				const res = await getUserInfo()
@@ -454,9 +654,17 @@ export default {
 					this.form.maritalStatus = userData.maritalStatus || ''
 					this.form.hobbies = userData.hobbies || ''
 					this.form.dietPreferences = userData.dietPreferences || ''
-					this.form.mascot = userData.mascot || ''
-				this.form.address = userData.address || ''
-				}
+				this.form.mascot = userData.mascot || ''
+			this.form.address = userData.address || ''
+			this.form.province = userData.province || ''
+			this.form.city = userData.city || ''
+			this.form.district = userData.district || ''
+			this.form.street = userData.street || ''
+			// 若无完整地址文本但有省市区,则拼接
+			if (!this.form.address && (this.form.province || this.form.city || this.form.district)) {
+				this.updateFullAddress()
+			}
+			}
 			} catch (e) {
 				console.error('加载用户信息失败', e)
 			}
@@ -473,29 +681,31 @@ export default {
 				this.form.nickname = phone ? '用户' + phone.substring(phone.length - 4) : '微信用户'
 			}
 
-			// 从 AddressPicker 读取地址文本
-			if (this.$refs.addressPicker && this.$refs.addressPicker.fullAddress) {
-				this.form.address = this.$refs.addressPicker.fullAddress
-			}
-
-			this.loading = true
-			try {
-				await updateUserInfo({
-					nickname: this.form.nickname,
-					realName: this.form.realName,
-					gender: this.form.gender,
-					idCard: this.form.idCard,
-					birthday: this.form.birthday,
-					avatar: this.form.avatar,
-					mascot: this.form.mascot,
-					ethnicity: this.form.ethnicity,
-					bloodType: this.form.bloodType,
-					highestEducation: this.form.highestEducation,
-					maritalStatus: this.form.maritalStatus,
-					hobbies: this.form.hobbies,
-					dietPreferences: this.form.dietPreferences,
-					address: this.form.address
-				})
+		// 从省市区街道字段同步完整地址文本
+		this.updateFullAddress()
+
+		this.loading = true
+		try {
+			await updateUserInfo({
+				nickname: this.form.nickname,
+				realName: this.form.realName,
+				gender: this.form.gender,
+				idCard: this.form.idCard,
+				birthday: this.form.birthday,
+				avatar: this.form.avatar,
+				mascot: this.form.mascot,
+				ethnicity: this.form.ethnicity,
+				bloodType: this.form.bloodType,
+				highestEducation: this.form.highestEducation,
+				maritalStatus: this.form.maritalStatus,
+				hobbies: this.form.hobbies,
+				dietPreferences: this.form.dietPreferences,
+				address: this.form.address,
+				province: this.form.province,
+				city: this.form.city,
+				district: this.form.district,
+				street: this.form.street
+			})
 				uni.showToast({ title: '保存成功', icon: 'success' })
 				setTimeout(() => {
 					if (this.isNewUserFlow) {
@@ -935,4 +1145,128 @@ export default {
 	background: #f5f5f5;
 	color: #999;
 }
+
+/* ===== 区域选择弹窗 ===== */
+.region-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 999;
+	display: flex;
+	align-items: flex-end;
+}
+.region-picker {
+	background: #fff;
+	border-radius: 20rpx 20rpx 0 0;
+	width: 100%;
+	max-height: 70vh;
+	display: flex;
+	flex-direction: column;
+}
+.region-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 24rpx 30rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+.region-header-btn {
+	font-size: 28rpx;
+	color: #999;
+}
+.region-confirm {
+	color: #F97316;
+	font-weight: bold;
+}
+.region-header-title {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #333;
+}
+.region-breadcrumb {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 30rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+.crumb-item {
+	font-size: 26rpx;
+	color: #999;
+	padding: 8rpx 12rpx;
+	border-radius: 8rpx;
+}
+.crumb-item.crumb-active {
+	color: #F97316;
+	font-weight: bold;
+}
+.crumb-arrow {
+	font-size: 24rpx;
+	color: #ccc;
+	margin: 0 8rpx;
+}
+.region-list {
+	max-height: 50vh;
+	overflow-y: auto;
+}
+.region-item {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 28rpx 30rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+.region-item:active {
+	background: #FFF7ED;
+}
+.region-item-text {
+	font-size: 28rpx;
+	color: #333;
+}
+.region-item-check {
+	font-size: 28rpx;
+	color: #F97316;
+	font-weight: bold;
+}
+.region-item-selected {
+	background: #FFF7ED;
+}
+.region-item-selected .region-item-text {
+	color: #F97316;
+	font-weight: bold;
+}
+.region-loading {
+	padding: 40rpx;
+	text-align: center;
+	font-size: 26rpx;
+	color: #999;
+}
+
+/* ===== 地址行(所在地区) ===== */
+.address-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+.address-value-wrap {
+	display: flex;
+	align-items: center;
+	flex: 1;
+	justify-content: flex-end;
+}
+.address-value {
+	font-size: 28rpx;
+	color: #333;
+}
+.address-placeholder {
+	font-size: 28rpx;
+	color: #999;
+}
+.address-arrow {
+	font-size: 28rpx;
+	color: #ccc;
+	margin-left: 8rpx;
+}
 </style>