|
...
|
...
|
@@ -2,14 +2,9 @@ |
|
|
|
<view class="profile-page">
|
|
|
|
<view class="avatar-section">
|
|
|
|
<button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar">
|
|
|
|
<image
|
|
|
|
:src="
|
|
|
|
formData.avatar ||
|
|
|
|
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
|
|
|
|
"
|
|
|
|
mode="aspectFill"
|
|
|
|
class="avatar-image"
|
|
|
|
></image>
|
|
|
|
<image :src="formData.avatar ||
|
|
|
|
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
|
|
|
|
" mode="aspectFill" class="avatar-image"></image>
|
|
|
|
</button>
|
|
|
|
</view>
|
|
|
|
|
|
...
|
...
|
@@ -23,25 +18,13 @@ |
|
|
|
<view class="list-item" @click="gotoSelectGender">
|
|
|
|
<text class="item-title">性别</text>
|
|
|
|
<view class="item-value">{{ formatGender(formData.sex) }}</view>
|
|
|
|
<uni-icons
|
|
|
|
v-if="formData.isAllowUpdSex"
|
|
|
|
type="right"
|
|
|
|
color="#999"
|
|
|
|
size="20"
|
|
|
|
class="arrow-icon"
|
|
|
|
></uni-icons>
|
|
|
|
<uni-icons v-if="formData.isAllowUpdSex" type="right" color="#999" size="20" class="arrow-icon"></uni-icons>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="list-item">
|
|
|
|
<text class="item-title">地区</text>
|
|
|
|
<picker
|
|
|
|
mode="multiSelector"
|
|
|
|
:range="multiArray"
|
|
|
|
range-key="name"
|
|
|
|
:value="multiIndex"
|
|
|
|
@change="pickerChange"
|
|
|
|
@columnchange="pickerColumnChange"
|
|
|
|
>
|
|
|
|
<picker mode="multiSelector" :range="multiArray" range-key="name" :value="multiIndex" @change="pickerChange"
|
|
|
|
@columnchange="pickerColumnChange">
|
|
|
|
<view class="picker" :class="{ 'placeholder-txt': !selectedAddress }">
|
|
|
|
{{ selectedAddress || '请选择省市区' }}
|
|
|
|
</view>
|
|
...
|
...
|
@@ -51,13 +34,8 @@ |
|
|
|
<view class="list-item signature-item">
|
|
|
|
<text class="item-title">个性签名</text>
|
|
|
|
<view class="signature-input-wrapper">
|
|
|
|
<textarea
|
|
|
|
v-model="formData.signature"
|
|
|
|
placeholder="请输入个性签名..."
|
|
|
|
:maxlength="50"
|
|
|
|
class="signature-input"
|
|
|
|
auto-height
|
|
|
|
/>
|
|
|
|
<textarea v-model="formData.signature" placeholder="请输入个性签名..." :maxlength="50" class="signature-input"
|
|
|
|
auto-height />
|
|
|
|
<text class="char-count">{{ formData.signature?.length || 0 }}/50</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
...
|
...
|
@@ -80,13 +58,8 @@ |
|
|
|
</view>
|
|
|
|
<view class="popup-body">
|
|
|
|
<view class="input-wrap">
|
|
|
|
<input
|
|
|
|
class="nickname-input"
|
|
|
|
placeholder="请输入新昵称"
|
|
|
|
type="nickname"
|
|
|
|
v-model="editCacheData.nickname"
|
|
|
|
maxlength="20"
|
|
|
|
/>
|
|
|
|
<input class="nickname-input" placeholder="请输入新昵称" type="nickname" v-model="editCacheData.nickname"
|
|
|
|
maxlength="20" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="popup-footer">
|
|
...
|
...
|
@@ -105,18 +78,10 @@ |
|
|
|
</view>
|
|
|
|
<view class="popup-body">
|
|
|
|
<view class="gender-options">
|
|
|
|
<view
|
|
|
|
class="gender-item"
|
|
|
|
:class="{ selected: editCacheData.sex === 1 }"
|
|
|
|
@click="selectGender(1)"
|
|
|
|
>
|
|
|
|
<view class="gender-item" :class="{ selected: editCacheData.sex === 1 }" @click="selectGender(1)">
|
|
|
|
<text class="gender-text">男</text>
|
|
|
|
</view>
|
|
|
|
<view
|
|
|
|
class="gender-item"
|
|
|
|
:class="{ selected: editCacheData.sex === 2 }"
|
|
|
|
@click="selectGender(2)"
|
|
|
|
>
|
|
|
|
<view class="gender-item" :class="{ selected: editCacheData.sex === 2 }" @click="selectGender(2)">
|
|
|
|
<text class="gender-text">女</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
...
|
...
|
@@ -128,594 +93,580 @@ |
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
import { ref, reactive, nextTick } from 'vue';
|
|
|
|
import { onShow, onBackPress } from '@dcloudio/uni-app';
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
import UserApi from '@/sheep/api/member/user';
|
|
|
|
import AreaApi from '@/sheep/api/system/area';
|
|
|
|
import FileApi from '@/sheep/api/infra/file';
|
|
|
|
import useUserStore from '@/sheep/store/user';
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
// 弹窗 DOM 引用声明
|
|
|
|
const nicknamePopupRef = ref(null);
|
|
|
|
const genderPopupRef = ref(null);
|
|
|
|
|
|
|
|
// 状态与防抖
|
|
|
|
const saveLoading = ref(false);
|
|
|
|
const hasModified = ref(false); // 侦听变更状态
|
|
|
|
|
|
|
|
// 核心业务隔离表单
|
|
|
|
const formData = ref({
|
|
|
|
avatar: '',
|
|
|
|
nickname: '',
|
|
|
|
sex: 0,
|
|
|
|
province: '',
|
|
|
|
city: '',
|
|
|
|
region: '',
|
|
|
|
signature: '',
|
|
|
|
createTime: '',
|
|
|
|
isAllowUpdSex: false,
|
|
|
|
});
|
|
|
|
import { ref, reactive, nextTick } from 'vue';
|
|
|
|
import { onShow, onBackPress } from '@dcloudio/uni-app';
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
import UserApi from '@/sheep/api/member/user';
|
|
|
|
import AreaApi from '@/sheep/api/system/area';
|
|
|
|
import FileApi from '@/sheep/api/infra/file';
|
|
|
|
import useUserStore from '@/sheep/store/user';
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
// 弹窗 DOM 引用声明
|
|
|
|
const nicknamePopupRef = ref(null);
|
|
|
|
const genderPopupRef = ref(null);
|
|
|
|
|
|
|
|
// 状态与防抖
|
|
|
|
const saveLoading = ref(false);
|
|
|
|
const hasModified = ref(false); // 侦听变更状态
|
|
|
|
|
|
|
|
// 核心业务隔离表单
|
|
|
|
const formData = ref({
|
|
|
|
avatar: '',
|
|
|
|
nickname: '',
|
|
|
|
sex: 0,
|
|
|
|
province: '',
|
|
|
|
city: '',
|
|
|
|
region: '',
|
|
|
|
signature: '',
|
|
|
|
createTime: '',
|
|
|
|
isAllowUpdSex: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
// 深度解耦的弹窗缓存数据
|
|
|
|
const editCacheData = reactive({
|
|
|
|
nickname: '',
|
|
|
|
sex: 0,
|
|
|
|
});
|
|
|
|
|
|
|
|
// 省市区级联选择器状态流
|
|
|
|
const addressTree = ref([]);
|
|
|
|
const multiArray = ref([[], [], []]);
|
|
|
|
const multiIndex = ref([0, 0, 0]);
|
|
|
|
const selectedAddress = ref('');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 宿主生命周期钩子流
|
|
|
|
*/
|
|
|
|
onShow(async () => {
|
|
|
|
try {
|
|
|
|
// 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)
|
|
|
|
await fetchAreaTree();
|
|
|
|
|
|
|
|
// 2. 深度克隆 Store 数据,隔离表单与 Store 的直接联动,保持单向数据流
|
|
|
|
if (userStore.userInfo) {
|
|
|
|
formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
|
|
|
|
initEchoAddress();
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[Init Error] 初始化个人信息数据流失败:', error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失
|
|
|
|
*/
|
|
|
|
onBackPress((options) => {
|
|
|
|
// 监听表单内容变动(仅在未保存且已有修改时提示)
|
|
|
|
const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo);
|
|
|
|
if (isUnsaved && !saveLoading.value) {
|
|
|
|
uni.showModal({
|
|
|
|
title: '提示',
|
|
|
|
content: '您有尚未保存的修改,确定要离开吗?',
|
|
|
|
success: (res) => {
|
|
|
|
if (res.confirm) {
|
|
|
|
hasModified.value = false;
|
|
|
|
uni.navigateBack({ delta: 1 });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return true; // 拦截返回键
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试
|
|
|
|
*/
|
|
|
|
const handleChooseAvatar = async (e) => {
|
|
|
|
const { avatarUrl } = e.detail;
|
|
|
|
|
|
|
|
|
|
|
|
if (!avatarUrl) return;
|
|
|
|
|
|
|
|
// 深度解耦的弹窗缓存数据
|
|
|
|
const editCacheData = reactive({
|
|
|
|
nickname: '',
|
|
|
|
sex: 0,
|
|
|
|
});
|
|
|
|
|
|
|
|
// 省市区级联选择器状态流
|
|
|
|
const addressTree = ref([]);
|
|
|
|
const multiArray = ref([[], [], []]);
|
|
|
|
const multiIndex = ref([0, 0, 0]);
|
|
|
|
const selectedAddress = ref('');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 宿主生命周期钩子流
|
|
|
|
*/
|
|
|
|
onShow(async () => {
|
|
|
|
try {
|
|
|
|
// 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)
|
|
|
|
await fetchAreaTree();
|
|
|
|
|
|
|
|
// 2. 深度克隆 Store 数据,隔离表单与 Store 的直接联动,保持单向数据流
|
|
|
|
if (userStore.userInfo) {
|
|
|
|
formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
|
|
|
|
initEchoAddress();
|
|
|
|
// 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守)
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
uni.getFileInfo({
|
|
|
|
filePath: avatarUrl,
|
|
|
|
success: (fileInfo) => {
|
|
|
|
const sizeMb = fileInfo.size / 1024 / 1024;
|
|
|
|
if (sizeMb > 5) {
|
|
|
|
uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[Init Error] 初始化个人信息数据流失败:', error);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失
|
|
|
|
*/
|
|
|
|
onBackPress((options) => {
|
|
|
|
// 监听表单内容变动(仅在未保存且已有修改时提示)
|
|
|
|
const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo);
|
|
|
|
if (isUnsaved && !saveLoading.value) {
|
|
|
|
uni.showModal({
|
|
|
|
title: '提示',
|
|
|
|
content: '您有尚未保存的修改,确定要离开吗?',
|
|
|
|
success: (res) => {
|
|
|
|
if (res.confirm) {
|
|
|
|
hasModified.value = false;
|
|
|
|
uni.navigateBack({ delta: 1 });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return true; // 拦截返回键
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试
|
|
|
|
*/
|
|
|
|
const handleChooseAvatar = async (e) => {
|
|
|
|
const { avatarUrl } = e.detail;
|
|
|
|
if (!avatarUrl) return;
|
|
|
|
|
|
|
|
// 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守)
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
uni.getFileInfo({
|
|
|
|
filePath: avatarUrl,
|
|
|
|
success: (fileInfo) => {
|
|
|
|
const sizeMb = fileInfo.size / 1024 / 1024;
|
|
|
|
if (sizeMb > 5) {
|
|
|
|
uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
uni.showLoading({ title: '头像上传中...', mask: true });
|
|
|
|
try {
|
|
|
|
const res = await FileApi.uploadFile(avatarUrl);
|
|
|
|
if (res?.data) {
|
|
|
|
formData.value.avatar = res.data;
|
|
|
|
uni.showToast({ title: '头像上传成功', icon: 'success' });
|
|
|
|
} else {
|
|
|
|
throw new Error('服务器未返回有效的图片URL路径');
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error('[Upload Error] 临时路径转存服务器失败:', err);
|
|
|
|
uni.showToast({ title: '头像保存失败,请稍后重试', icon: 'none' });
|
|
|
|
} finally {
|
|
|
|
uni.hideLoading();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 唤起并初始化昵称弹窗缓存
|
|
|
|
*/
|
|
|
|
const gotoEditNickname = () => {
|
|
|
|
editCacheData.nickname = formData.value.nickname || '';
|
|
|
|
nicknamePopupRef.value.open();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 确认编辑昵称
|
|
|
|
*/
|
|
|
|
const confirmEditNickname = () => {
|
|
|
|
const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';
|
|
|
|
if (!targetNickname) {
|
|
|
|
uni.showToast({ title: '昵称不能为空', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
formData.value.nickname = targetNickname;
|
|
|
|
nicknamePopupRef.value.close();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 唤起并初始化性别弹窗
|
|
|
|
*/
|
|
|
|
const gotoSelectGender = () => {
|
|
|
|
if (!formData.value.isAllowUpdSex) {
|
|
|
|
uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
editCacheData.sex = formData.value.sex || 0;
|
|
|
|
genderPopupRef.value.open();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 优化:解耦的性别变更逻辑
|
|
|
|
*/
|
|
|
|
const selectGender = (sex) => {
|
|
|
|
// 1. 临时更改缓存态而非直接写回 formData
|
|
|
|
editCacheData.sex = sex;
|
|
|
|
formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈
|
|
|
|
setTimeout(() => {
|
|
|
|
genderPopupRef.value.close();
|
|
|
|
}, 200);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 弹窗关闭时彻底释放和清理临时的输入脏状态
|
|
|
|
*/
|
|
|
|
const onNicknamePopupChange = (e) => {
|
|
|
|
if (!e.show) {
|
|
|
|
editCacheData.nickname = '';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费
|
|
|
|
*/
|
|
|
|
const fetchAreaTree = async () => {
|
|
|
|
if (addressTree.value && addressTree.value.length > 0) return;
|
|
|
|
|
|
|
|
// 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API
|
|
|
|
const cachedTree = uni.getStorageSync('sys_area_tree');
|
|
|
|
if (cachedTree) {
|
|
|
|
addressTree.value = cachedTree;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const res = await FileApi.uploadFile(avatarUrl);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const res = await AreaApi.getAreaTree();
|
|
|
|
if (res?.data) {
|
|
|
|
addressTree.value = res.data;
|
|
|
|
uni.setStorageSync('sys_area_tree', res.data); // 写入临时磁盘缓存,生命周期内有效
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[API Error] 获取省市区行政结构树失败:', error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 安全地同步极简Picker的三列静态节点关联数据
|
|
|
|
*/
|
|
|
|
const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {
|
|
|
|
if (!addressTree.value || addressTree.value.length === 0) return;
|
|
|
|
|
|
|
|
const provinces = addressTree.value;
|
|
|
|
const pSelected = provinces[pIdx] || provinces[0]; // 极端越界安全垫付
|
|
|
|
const cities = pSelected?.children || [];
|
|
|
|
const cSelected = cities[cIdx] || cities[0];
|
|
|
|
const regions = cSelected?.children || [];
|
|
|
|
|
|
|
|
multiArray.value = [provinces, cities, regions];
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退
|
|
|
|
*/
|
|
|
|
const pickerColumnChange = (e) => {
|
|
|
|
const { column, value } = e.detail;
|
|
|
|
|
|
|
|
// 1. 原子化本地临时深拷贝索引指针
|
|
|
|
const nextIndex = [...multiIndex.value];
|
|
|
|
nextIndex[column] = value;
|
|
|
|
|
|
|
|
if (column === 0) {
|
|
|
|
// 2. 变动第一列(省):联动重置市、区索引指针归零
|
|
|
|
nextIndex[1] = 0;
|
|
|
|
nextIndex[2] = 0;
|
|
|
|
|
|
|
|
// 3. 拦截防御:确保选择的省份在当前最新行政数组内
|
|
|
|
const safeProvinceIdx = Math.min(value, addressTree.value.length - 1);
|
|
|
|
syncPickerColumnData(safeProvinceIdx, 0);
|
|
|
|
} else if (column === 1) {
|
|
|
|
// 4. 变动第二列(市):联动重置区索引指针归零
|
|
|
|
nextIndex[2] = 0;
|
|
|
|
|
|
|
|
const safeProvinceIdx = Math.min(nextIndex[0], addressTree.value.length - 1);
|
|
|
|
const safeProvince = addressTree.value[safeProvinceIdx];
|
|
|
|
const cities = safeProvince?.children || [];
|
|
|
|
const safeCityIdx = Math.min(value, cities.length - 1);
|
|
|
|
|
|
|
|
syncPickerColumnData(safeProvinceIdx, safeCityIdx);
|
|
|
|
}
|
|
|
|
console.log('res----------', res);
|
|
|
|
|
|
|
|
// 5. 在同一视图更新 tick 下完成覆盖更新,避免多路联动逻辑竞态导致的重叠渲染
|
|
|
|
nextTick(() => {
|
|
|
|
multiIndex.value = nextIndex;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 确认选择省市区,绑定数据映射
|
|
|
|
*/
|
|
|
|
const pickerChange = (e) => {
|
|
|
|
const indices = e.detail.value;
|
|
|
|
multiIndex.value = [...indices];
|
|
|
|
|
|
|
|
const pObj = multiArray.value[0][indices[0]];
|
|
|
|
const cObj = multiArray.value[1][indices[1]];
|
|
|
|
const rObj = multiArray.value[2][indices[2]];
|
|
|
|
|
|
|
|
formData.value.province = pObj?.id || '';
|
|
|
|
formData.value.city = cObj?.id || '';
|
|
|
|
formData.value.region = rObj?.id || '';
|
|
|
|
|
|
|
|
selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' ');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 数据回显定位器:精准利用底层树定位当前用户已有地区信息
|
|
|
|
*/
|
|
|
|
const initEchoAddress = () => {
|
|
|
|
const { province, city, region } = formData.value;
|
|
|
|
if (!province || !addressTree.value || addressTree.value.length === 0) {
|
|
|
|
syncPickerColumnData(0, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 1. 查找省份物理位置
|
|
|
|
const pIdx = addressTree.value.findIndex((p) => String(p.id) === String(province));
|
|
|
|
if (pIdx === -1) {
|
|
|
|
syncPickerColumnData(0, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
formData.value.avatar = res.data;
|
|
|
|
|
|
|
|
// 2. 查找地级市物理位置
|
|
|
|
const cities = addressTree.value[pIdx]?.children || [];
|
|
|
|
const cIdx = cities.findIndex((c) => String(c.id) === String(city));
|
|
|
|
|
|
|
|
// 3. 查找区县物理位置
|
|
|
|
const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : [];
|
|
|
|
const rIdx = regions.findIndex((r) => String(r.id) === String(region));
|
|
|
|
|
|
|
|
// 4. 组装安全的映射矩阵索引
|
|
|
|
const safePIdx = pIdx;
|
|
|
|
const safeCIdx = cIdx !== -1 ? cIdx : 0;
|
|
|
|
const safeRIdx = rIdx !== -1 ? rIdx : 0;
|
|
|
|
|
|
|
|
multiIndex.value = [safePIdx, safeCIdx, safeRIdx];
|
|
|
|
|
|
|
|
// 5. 更新数据池并刷新回显文字描述
|
|
|
|
syncPickerColumnData(safePIdx, safeCIdx);
|
|
|
|
|
|
|
|
selectedAddress.value = [
|
|
|
|
addressTree.value[safePIdx]?.name,
|
|
|
|
cities[safeCIdx]?.name,
|
|
|
|
regions[safeRIdx]?.name,
|
|
|
|
]
|
|
|
|
.filter(Boolean)
|
|
|
|
.join(' ');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 保存修改逻辑:进行脏字段过滤净化后,更新用户信息
|
|
|
|
*/
|
|
|
|
const saveProfile = async () => {
|
|
|
|
if (saveLoading.value) return;
|
|
|
|
|
|
|
|
// 前置轻校验:去除尾部空隙并校验合法性
|
|
|
|
if (!formData.value.nickname || !formData.value.nickname.trim()) {
|
|
|
|
uni.showToast({ title: '昵称不能为空', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
|
|
|
|
saveLoading.value = true;
|
|
|
|
uni.showLoading({ title: '正在保存资料...', mask: true });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 唤起并初始化昵称弹窗缓存
|
|
|
|
*/
|
|
|
|
const gotoEditNickname = () => {
|
|
|
|
editCacheData.nickname = formData.value.nickname || '';
|
|
|
|
nicknamePopupRef.value.open();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 确认编辑昵称
|
|
|
|
*/
|
|
|
|
const confirmEditNickname = () => {
|
|
|
|
const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';
|
|
|
|
if (!targetNickname) {
|
|
|
|
uni.showToast({ title: '昵称不能为空', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
formData.value.nickname = targetNickname;
|
|
|
|
nicknamePopupRef.value.close();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 唤起并初始化性别弹窗
|
|
|
|
*/
|
|
|
|
const gotoSelectGender = () => {
|
|
|
|
if (!formData.value.isAllowUpdSex) {
|
|
|
|
uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
editCacheData.sex = formData.value.sex || 0;
|
|
|
|
genderPopupRef.value.open();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 优化:解耦的性别变更逻辑
|
|
|
|
*/
|
|
|
|
const selectGender = (sex) => {
|
|
|
|
// 1. 临时更改缓存态而非直接写回 formData
|
|
|
|
editCacheData.sex = sex;
|
|
|
|
formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈
|
|
|
|
setTimeout(() => {
|
|
|
|
genderPopupRef.value.close();
|
|
|
|
}, 200);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 弹窗关闭时彻底释放和清理临时的输入脏状态
|
|
|
|
*/
|
|
|
|
const onNicknamePopupChange = (e) => {
|
|
|
|
if (!e.show) {
|
|
|
|
editCacheData.nickname = '';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费
|
|
|
|
*/
|
|
|
|
const fetchAreaTree = async () => {
|
|
|
|
if (addressTree.value && addressTree.value.length > 0) return;
|
|
|
|
|
|
|
|
// 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API
|
|
|
|
const cachedTree = uni.getStorageSync('sys_area_tree');
|
|
|
|
if (cachedTree) {
|
|
|
|
addressTree.value = cachedTree;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// 脏数据深度过滤净化:只保留后端允许更新的可写属性,剥离统计及只读系统字段
|
|
|
|
const { createTime, isAllowUpdSex, registerIp, id, userId, mobile, ...payload } =
|
|
|
|
formData.value;
|
|
|
|
try {
|
|
|
|
const res = await AreaApi.getAreaTree();
|
|
|
|
if (res?.data) {
|
|
|
|
addressTree.value = res.data;
|
|
|
|
uni.setStorageSync('sys_area_tree', res.data); // 写入临时磁盘缓存,生命周期内有效
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[API Error] 获取省市区行政结构树失败:', error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 安全地同步极简Picker的三列静态节点关联数据
|
|
|
|
*/
|
|
|
|
const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {
|
|
|
|
if (!addressTree.value || addressTree.value.length === 0) return;
|
|
|
|
|
|
|
|
const provinces = addressTree.value;
|
|
|
|
const pSelected = provinces[pIdx] || provinces[0]; // 极端越界安全垫付
|
|
|
|
const cities = pSelected?.children || [];
|
|
|
|
const cSelected = cities[cIdx] || cities[0];
|
|
|
|
const regions = cSelected?.children || [];
|
|
|
|
|
|
|
|
multiArray.value = [provinces, cities, regions];
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退
|
|
|
|
*/
|
|
|
|
const pickerColumnChange = (e) => {
|
|
|
|
const { column, value } = e.detail;
|
|
|
|
|
|
|
|
// 1. 原子化本地临时深拷贝索引指针
|
|
|
|
const nextIndex = [...multiIndex.value];
|
|
|
|
nextIndex[column] = value;
|
|
|
|
|
|
|
|
if (column === 0) {
|
|
|
|
// 2. 变动第一列(省):联动重置市、区索引指针归零
|
|
|
|
nextIndex[1] = 0;
|
|
|
|
nextIndex[2] = 0;
|
|
|
|
|
|
|
|
// 3. 拦截防御:确保选择的省份在当前最新行政数组内
|
|
|
|
const safeProvinceIdx = Math.min(value, addressTree.value.length - 1);
|
|
|
|
syncPickerColumnData(safeProvinceIdx, 0);
|
|
|
|
} else if (column === 1) {
|
|
|
|
// 4. 变动第二列(市):联动重置区索引指针归零
|
|
|
|
nextIndex[2] = 0;
|
|
|
|
|
|
|
|
const safeProvinceIdx = Math.min(nextIndex[0], addressTree.value.length - 1);
|
|
|
|
const safeProvince = addressTree.value[safeProvinceIdx];
|
|
|
|
const cities = safeProvince?.children || [];
|
|
|
|
const safeCityIdx = Math.min(value, cities.length - 1);
|
|
|
|
|
|
|
|
syncPickerColumnData(safeProvinceIdx, safeCityIdx);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 保证提交数据的值是修剪过左右空格的
|
|
|
|
payload.nickname = payload.nickname.trim();
|
|
|
|
if (payload.signature) {
|
|
|
|
payload.signature = payload.signature.trim();
|
|
|
|
}
|
|
|
|
// 5. 在同一视图更新 tick 下完成覆盖更新,避免多路联动逻辑竞态导致的重叠渲染
|
|
|
|
nextTick(() => {
|
|
|
|
multiIndex.value = nextIndex;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 确认选择省市区,绑定数据映射
|
|
|
|
*/
|
|
|
|
const pickerChange = (e) => {
|
|
|
|
const indices = e.detail.value;
|
|
|
|
multiIndex.value = [...indices];
|
|
|
|
|
|
|
|
const pObj = multiArray.value[0][indices[0]];
|
|
|
|
const cObj = multiArray.value[1][indices[1]];
|
|
|
|
const rObj = multiArray.value[2][indices[2]];
|
|
|
|
|
|
|
|
formData.value.province = pObj?.id || '';
|
|
|
|
formData.value.city = cObj?.id || '';
|
|
|
|
formData.value.region = rObj?.id || '';
|
|
|
|
|
|
|
|
selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' ');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 数据回显定位器:精准利用底层树定位当前用户已有地区信息
|
|
|
|
*/
|
|
|
|
const initEchoAddress = () => {
|
|
|
|
const { province, city, region } = formData.value;
|
|
|
|
if (!province || !addressTree.value || addressTree.value.length === 0) {
|
|
|
|
syncPickerColumnData(0, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await UserApi.updateUser(payload);
|
|
|
|
// 1. 查找省份物理位置
|
|
|
|
const pIdx = addressTree.value.findIndex((p) => String(p.id) === String(province));
|
|
|
|
if (pIdx === -1) {
|
|
|
|
syncPickerColumnData(0, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步
|
|
|
|
if (userStore.getUserInfo) {
|
|
|
|
await userStore.getUserInfo();
|
|
|
|
}
|
|
|
|
// 2. 查找地级市物理位置
|
|
|
|
const cities = addressTree.value[pIdx]?.children || [];
|
|
|
|
const cIdx = cities.findIndex((c) => String(c.id) === String(city));
|
|
|
|
|
|
|
|
// 3. 查找区县物理位置
|
|
|
|
const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : [];
|
|
|
|
const rIdx = regions.findIndex((r) => String(r.id) === String(region));
|
|
|
|
|
|
|
|
// 4. 组装安全的映射矩阵索引
|
|
|
|
const safePIdx = pIdx;
|
|
|
|
const safeCIdx = cIdx !== -1 ? cIdx : 0;
|
|
|
|
const safeRIdx = rIdx !== -1 ? rIdx : 0;
|
|
|
|
|
|
|
|
multiIndex.value = [safePIdx, safeCIdx, safeRIdx];
|
|
|
|
|
|
|
|
// 5. 更新数据池并刷新回显文字描述
|
|
|
|
syncPickerColumnData(safePIdx, safeCIdx);
|
|
|
|
|
|
|
|
selectedAddress.value = [
|
|
|
|
addressTree.value[safePIdx]?.name,
|
|
|
|
cities[safeCIdx]?.name,
|
|
|
|
regions[safeRIdx]?.name,
|
|
|
|
]
|
|
|
|
.filter(Boolean)
|
|
|
|
.join(' ');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 保存修改逻辑:进行脏字段过滤净化后,更新用户信息
|
|
|
|
*/
|
|
|
|
const saveProfile = async () => {
|
|
|
|
if (saveLoading.value) return;
|
|
|
|
|
|
|
|
// 前置轻校验:去除尾部空隙并校验合法性
|
|
|
|
if (!formData.value.nickname || !formData.value.nickname.trim()) {
|
|
|
|
uni.showToast({ title: '昵称不能为空', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
uni.navigateBack({ delta: 1 });
|
|
|
|
}, 1200);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('[API Error] 保存个人信息更新请求失败:', err);
|
|
|
|
uni.showToast({ title: err.msg || '保存失败,请稍后重试', icon: 'none' });
|
|
|
|
} finally {
|
|
|
|
uni.hideLoading();
|
|
|
|
saveLoading.value = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 格式化展示原子转换工具
|
|
|
|
const formatGender = (sex) => {
|
|
|
|
if (sex === 1) return '男';
|
|
|
|
if (sex === 2) return '女';
|
|
|
|
return '保密';
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatRegisterTime = (time) => {
|
|
|
|
if (!time) return '--';
|
|
|
|
return dayjs(time).format('YYYY-MM-DD');
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
saveLoading.value = true;
|
|
|
|
uni.showLoading({ title: '正在保存资料...', mask: true });
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
$color-bg: #f5f5f5;
|
|
|
|
$color-white: #ffffff;
|
|
|
|
$color-text-dark: #333;
|
|
|
|
$color-text-middle: #666;
|
|
|
|
$color-text-light: #999;
|
|
|
|
$color-primary: #fdd511;
|
|
|
|
$color-border: #eee;
|
|
|
|
$radius: 12rpx;
|
|
|
|
$radius-lg: 24rpx;
|
|
|
|
$shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|
|
|
$spacing-sm: 20rpx;
|
|
|
|
$spacing-md: 24rpx;
|
|
|
|
$spacing-lg: 32rpx;
|
|
|
|
$spacing-xl: 60rpx;
|
|
|
|
|
|
|
|
.profile-page {
|
|
|
|
background-color: $color-bg;
|
|
|
|
height: 100vh;
|
|
|
|
// #ifdef H5
|
|
|
|
height: calc(100vh - 44px);
|
|
|
|
// #endif
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
|
|
|
|
.avatar-section {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
padding: $spacing-xl 0 $spacing-lg;
|
|
|
|
|
|
|
|
.avatar-edit-btn {
|
|
|
|
background-color: transparent;
|
|
|
|
border: none;
|
|
|
|
padding: 0;
|
|
|
|
line-height: 0;
|
|
|
|
border-radius: 50%;
|
|
|
|
&::after {
|
|
|
|
content: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
// 脏数据深度过滤净化:只保留后端允许更新的可写属性,剥离统计及只读系统字段
|
|
|
|
const { createTime, isAllowUpdSex, registerIp, id, userId, mobile, ...payload } =
|
|
|
|
formData.value;
|
|
|
|
|
|
|
|
.avatar-image {
|
|
|
|
width: 160rpx;
|
|
|
|
height: 160rpx;
|
|
|
|
border-radius: 50%;
|
|
|
|
background-color: #e1e1e1;
|
|
|
|
}
|
|
|
|
// 保证提交数据的值是修剪过左右空格的
|
|
|
|
payload.nickname = payload.nickname.trim();
|
|
|
|
if (payload.signature) {
|
|
|
|
payload.signature = payload.signature.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
.info-list {
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
await UserApi.updateUser(payload);
|
|
|
|
|
|
|
|
// 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步
|
|
|
|
if (userStore.getUserInfo) {
|
|
|
|
await userStore.getUserInfo();
|
|
|
|
}
|
|
|
|
|
|
|
|
.list-item {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
background-color: $color-white;
|
|
|
|
border-radius: $radius;
|
|
|
|
padding: $spacing-md $spacing-sm;
|
|
|
|
margin-bottom: $spacing-sm;
|
|
|
|
box-shadow: $shadow;
|
|
|
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
|
|
|
|
|
|
|
.item-title {
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
flex-shrink: 0;
|
|
|
|
width: 140rpx;
|
|
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
uni.navigateBack({ delta: 1 });
|
|
|
|
}, 1200);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('[API Error] 保存个人信息更新请求失败:', err);
|
|
|
|
uni.showToast({ title: err.msg || '保存失败,请稍后重试', icon: 'none' });
|
|
|
|
} finally {
|
|
|
|
uni.hideLoading();
|
|
|
|
saveLoading.value = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 格式化展示原子转换工具
|
|
|
|
const formatGender = (sex) => {
|
|
|
|
if (sex === 1) return '男';
|
|
|
|
if (sex === 2) return '女';
|
|
|
|
return '保密';
|
|
|
|
};
|
|
|
|
|
|
|
|
const formatRegisterTime = (time) => {
|
|
|
|
if (!time) return '--';
|
|
|
|
return dayjs(time).format('YYYY-MM-DD');
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
.item-value {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
margin-right: $spacing-sm;
|
|
|
|
text-align: right;
|
|
|
|
flex: 1;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
<style scoped lang="scss">
|
|
|
|
$color-bg: #f5f5f5;
|
|
|
|
$color-white: #ffffff;
|
|
|
|
$color-text-dark: #333;
|
|
|
|
$color-text-middle: #666;
|
|
|
|
$color-text-light: #999;
|
|
|
|
$color-primary: #fdd511;
|
|
|
|
$color-border: #eee;
|
|
|
|
$radius: 12rpx;
|
|
|
|
$radius-lg: 24rpx;
|
|
|
|
$shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|
|
|
$spacing-sm: 20rpx;
|
|
|
|
$spacing-md: 24rpx;
|
|
|
|
$spacing-lg: 32rpx;
|
|
|
|
$spacing-xl: 60rpx;
|
|
|
|
|
|
|
|
.profile-page {
|
|
|
|
background-color: $color-bg;
|
|
|
|
height: 100vh;
|
|
|
|
// #ifdef H5
|
|
|
|
height: calc(100vh - 44px);
|
|
|
|
// #endif
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
|
|
|
|
.avatar-section {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
padding: $spacing-xl 0 $spacing-lg;
|
|
|
|
|
|
|
|
.avatar-edit-btn {
|
|
|
|
background-color: transparent;
|
|
|
|
border: none;
|
|
|
|
padding: 0;
|
|
|
|
line-height: 0;
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
|
|
&::after {
|
|
|
|
content: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.picker {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
text-align: right;
|
|
|
|
flex: 1;
|
|
|
|
padding-right: 10rpx;
|
|
|
|
.avatar-image {
|
|
|
|
width: 160rpx;
|
|
|
|
height: 160rpx;
|
|
|
|
border-radius: 50%;
|
|
|
|
background-color: #e1e1e1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.placeholder-txt {
|
|
|
|
color: $color-text-light;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.info-list {
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
}
|
|
|
|
|
|
|
|
.arrow-icon {
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
.list-item {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
background-color: $color-white;
|
|
|
|
border-radius: $radius;
|
|
|
|
padding: $spacing-md $spacing-sm;
|
|
|
|
margin-bottom: $spacing-sm;
|
|
|
|
box-shadow: $shadow;
|
|
|
|
|
|
|
|
&.signature-item {
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: flex-start;
|
|
|
|
gap: 16rpx;
|
|
|
|
.item-title {
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
flex-shrink: 0;
|
|
|
|
width: 140rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.signature-input-wrapper {
|
|
|
|
width: 100%;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
.signature-input {
|
|
|
|
width: 100%;
|
|
|
|
min-height: 120rpx;
|
|
|
|
max-height: 200rpx;
|
|
|
|
padding: 16rpx 20rpx 40rpx;
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
border-radius: $radius;
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
box-sizing: border-box;
|
|
|
|
line-height: 1.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
.char-count {
|
|
|
|
position: absolute;
|
|
|
|
right: 20rpx;
|
|
|
|
bottom: 12rpx;
|
|
|
|
font-size: 22rpx;
|
|
|
|
color: $color-text-light;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.item-value {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
margin-right: $spacing-sm;
|
|
|
|
text-align: right;
|
|
|
|
flex: 1;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 弹出层统一样式
|
|
|
|
.popup-container {
|
|
|
|
background-color: $color-white;
|
|
|
|
border-radius: $radius-lg $radius-lg 0 0;
|
|
|
|
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
|
|
|
.picker {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
text-align: right;
|
|
|
|
flex: 1;
|
|
|
|
padding-right: 10rpx;
|
|
|
|
|
|
|
|
.popup-header {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
padding: $spacing-md $spacing-sm;
|
|
|
|
border-bottom: 1rpx solid #fcfcfc;
|
|
|
|
.popup-title {
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
font-weight: 600;
|
|
|
|
&.placeholder-txt {
|
|
|
|
color: $color-text-light;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.popup-body {
|
|
|
|
padding: 30rpx $spacing-sm;
|
|
|
|
.arrow-icon {
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.signature-item {
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: flex-start;
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
|
|
.signature-input-wrapper {
|
|
|
|
width: 100%;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
.input-wrap {
|
|
|
|
.nickname-input {
|
|
|
|
.signature-input {
|
|
|
|
width: 100%;
|
|
|
|
height: 90rpx;
|
|
|
|
min-height: 120rpx;
|
|
|
|
max-height: 200rpx;
|
|
|
|
padding: 16rpx 20rpx 40rpx;
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
border-radius: $radius;
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
font-size: 28rpx;
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
box-sizing: border-box;
|
|
|
|
line-height: 1.5;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.gender-options {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: $spacing-md;
|
|
|
|
|
|
|
|
.gender-item {
|
|
|
|
position: relative;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
height: 96rpx;
|
|
|
|
font-size: 30rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
border-radius: $radius;
|
|
|
|
background-color: #fafafa;
|
|
|
|
border: 2rpx solid $color-border;
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
&.selected {
|
|
|
|
background-color: rgba($color-primary, 0.15);
|
|
|
|
color: #333;
|
|
|
|
border-color: $color-primary;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
.char-count {
|
|
|
|
position: absolute;
|
|
|
|
right: 20rpx;
|
|
|
|
bottom: 12rpx;
|
|
|
|
font-size: 22rpx;
|
|
|
|
color: $color-text-light;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 弹出层统一样式
|
|
|
|
.popup-container {
|
|
|
|
background-color: $color-white;
|
|
|
|
border-radius: $radius-lg $radius-lg 0 0;
|
|
|
|
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
|
|
.popup-header {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
padding: $spacing-md $spacing-sm;
|
|
|
|
border-bottom: 1rpx solid #fcfcfc;
|
|
|
|
|
|
|
|
.popup-title {
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: $color-text-dark;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.popup-footer {
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
.confirm-btn {
|
|
|
|
.popup-body {
|
|
|
|
padding: 30rpx $spacing-sm;
|
|
|
|
|
|
|
|
.input-wrap {
|
|
|
|
.nickname-input {
|
|
|
|
width: 100%;
|
|
|
|
height: 88rpx;
|
|
|
|
line-height: 88rpx;
|
|
|
|
background-color: $color-primary;
|
|
|
|
color: #333;
|
|
|
|
height: 90rpx;
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
border-radius: $radius;
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
font-size: 28rpx;
|
|
|
|
font-weight: 500;
|
|
|
|
box-sizing: border-box;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.gender-options {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: $spacing-md;
|
|
|
|
|
|
|
|
.gender-item {
|
|
|
|
position: relative;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
height: 96rpx;
|
|
|
|
font-size: 30rpx;
|
|
|
|
color: $color-text-middle;
|
|
|
|
border-radius: $radius;
|
|
|
|
&:after {
|
|
|
|
content: none;
|
|
|
|
background-color: #fafafa;
|
|
|
|
border: 2rpx solid $color-border;
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
&.selected {
|
|
|
|
background-color: rgba($color-primary, 0.15);
|
|
|
|
color: #333;
|
|
|
|
border-color: $color-primary;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.button-section {
|
|
|
|
padding: $spacing-lg $spacing-sm;
|
|
|
|
.save-btn {
|
|
|
|
.popup-footer {
|
|
|
|
padding: 0 $spacing-sm;
|
|
|
|
|
|
|
|
.confirm-btn {
|
|
|
|
width: 100%;
|
|
|
|
height: 88rpx;
|
|
|
|
line-height: 88rpx;
|
|
...
|
...
|
@@ -724,9 +675,30 @@ |
|
|
|
font-size: 28rpx;
|
|
|
|
font-weight: 500;
|
|
|
|
border-radius: $radius;
|
|
|
|
|
|
|
|
&:after {
|
|
|
|
content: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.button-section {
|
|
|
|
padding: $spacing-lg $spacing-sm;
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
width: 100%;
|
|
|
|
height: 88rpx;
|
|
|
|
line-height: 88rpx;
|
|
|
|
background-color: $color-primary;
|
|
|
|
color: #333;
|
|
|
|
font-size: 28rpx;
|
|
|
|
font-weight: 500;
|
|
|
|
border-radius: $radius;
|
|
|
|
|
|
|
|
&:after {
|
|
|
|
content: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style> |
...
|
...
|
|