feat(miniprogram): 微信小程序个人资料页面功能优化

This commit is contained in:
pikachu1995@126.com
2026-08-26 11:01:59 +08:00
parent 4a194ee0a1
commit e026b4e0d4
2 changed files with 247 additions and 23 deletions

View File

@@ -1,9 +1,32 @@
<template> <template>
<!-- #ifdef MP-WEIXIN -->
<form class="person-msg" @submit="onWxSubmit">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="person-msg"> <view class="person-msg">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<button
v-if="useWxChooseAvatar"
class="head avatar-btn"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
@error="onChooseAvatarError"
>
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
<view>点击修改头像</view>
</button>
<view v-else class="head" @click="changeFace">
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
<view>点击修改头像</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="head" @click="changeFace"> <view class="head" @click="changeFace">
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image> <image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
<view>点击修改头像</view> <view>点击修改头像</view>
</view> </view>
<!-- #endif -->
<up-form <up-form
:model="form" :model="form"
@@ -13,12 +36,26 @@
label-width="180rpx" label-width="180rpx"
> >
<up-form-item label="昵称" label-width="180rpx" :border-bottom="true"> <up-form-item label="昵称" label-width="180rpx" :border-bottom="true">
<!-- #ifdef MP-WEIXIN -->
<input
class="wx-nickname"
type="nickname"
name="nickName"
:value="form.nickName"
placeholder="请输入昵称"
placeholder-class="wx-nickname-placeholder"
@blur="onNicknameBlur"
@nicknamereview="onNicknameReview"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<u-input <u-input
v-model="form.nickName" v-model="form.nickName"
border="none" border="none"
input-align="right" input-align="right"
placeholder="请输入昵称" placeholder="请输入昵称"
/> />
<!-- #endif -->
</up-form-item> </up-form-item>
<up-form-item label="性别" label-width="180rpx" :border-bottom="true"> <up-form-item label="性别" label-width="180rpx" :border-bottom="true">
@@ -55,8 +92,13 @@
</up-form> </up-form>
<view class="bottom"> <view class="bottom">
<!-- #ifdef MP-WEIXIN -->
<button class="submit" form-type="submit">保存</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="submit" @click="submit">保存</view> <view class="submit" @click="submit">保存</view>
<view class="submit light" @click="quiteLoginOut">退出登录</view> <!-- #endif -->
<view class="submit light" @click="goBack">返回</view>
</view> </view>
<u-datetime-picker <u-datetime-picker
@@ -77,19 +119,27 @@
@funcValue="getPickerParentValue" @funcValue="getPickerParentValue"
pickerSize="4" pickerSize="4"
></m-city> ></m-city>
<!-- #ifdef MP-WEIXIN -->
</form>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
</view> </view>
<!-- #endif -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { useStore } from '@/store' import { useStore } from '@/store'
import { quiteLoginOut } from '@/utils/filters.js'
import { saveUserInfo } from '@/api/members.js' import { saveUserInfo } from '@/api/members.js'
import { upload } from '@/api/common.js' import { upload } from '@/api/common.js'
import storage from '@/utils/storage.js' import storage from '@/utils/storage.js'
import MCity from '@/components/m-city/m-city.vue' import MCity from '@/components/m-city/m-city.vue'
const DEFAULT_FACE = '/static/missing-face.png'
const NICKNAME_MIN = 2
const NICKNAME_MAX = 20
function parseBirthdayTimestamp(str?: string) { function parseBirthdayTimestamp(str?: string) {
if (!str) return Date.now() if (!str) return Date.now()
const timestamp = Date.parse(String(str).replace(/-/g, '/')) const timestamp = Date.parse(String(str).replace(/-/g, '/'))
@@ -111,7 +161,7 @@ const userInfo = storage.getUserInfo() || {}
const form = reactive<Record<string, any>>({ const form = reactive<Record<string, any>>({
nickName: userInfo.nickName || '', nickName: userInfo.nickName || '',
birthday: userInfo.birthday || '', birthday: userInfo.birthday || '',
face: userInfo.face || '/static/missing-face.png', face: userInfo.face || DEFAULT_FACE,
regionId: [], regionId: [],
region: userInfo.region || [], region: userInfo.region || [],
sex: userInfo.sex != null ? String(userInfo.sex) : '1', sex: userInfo.sex != null ? String(userInfo.sex) : '1',
@@ -126,6 +176,31 @@ const maxBirthdayDate = Date.now()
const region = ref([{ id: '', localName: '请选择', children: [] }]) const region = ref([{ id: '', localName: '请选择', children: [] }])
const showBirthday = ref(false) const showBirthday = ref(false)
const cityPicker = ref<any>(null) const cityPicker = ref<any>(null)
const useWxChooseAvatar = ref(false)
function isWeixinDevtools() {
try {
const sys = uni.getSystemInfoSync() as Record<string, any>
return sys?.platform === 'devtools'
} catch {
return false
}
}
// #ifdef MP-WEIXIN
useWxChooseAvatar.value = !isWeixinDevtools()
// #endif
function ensurePrivacyAuthorize() {
// #ifdef MP-WEIXIN
const wxApi = (globalThis as any).wx
if (!wxApi?.requirePrivacyAuthorize) return
wxApi.requirePrivacyAuthorize({
success: () => {},
fail: () => {},
})
// #endif
}
function getPickerParentValue(selected: any[]) { function getPickerParentValue(selected: any[]) {
form.region = [] form.region = []
@@ -145,7 +220,33 @@ function clickRegion() {
cityPicker.value?.show() cityPicker.value?.show()
} }
function isTempFacePath(filePath: string) {
return /^(wxfile:|http:\/\/tmp\/|https:\/\/tmp\/)/i.test(filePath)
}
function validateNickName(nickName: string) {
if (nickName.length < NICKNAME_MIN || nickName.length > NICKNAME_MAX) {
uni.showToast({
title: `昵称需为${NICKNAME_MIN}-${NICKNAME_MAX}个字符`,
icon: 'none',
})
return false
}
return true
}
function submit() { function submit() {
const nickName = String(form.nickName || '').trim()
form.nickName = nickName
if (!validateNickName(nickName)) return
if (isTempFacePath(form.face)) {
uni.showToast({
title: '头像上传中,请稍后再保存',
icon: 'none',
})
return
}
const params = JSON.parse(JSON.stringify(form)) const params = JSON.parse(JSON.stringify(form))
delete params.regionPath delete params.regionPath
saveUserInfo(params).then((res) => { saveUserInfo(params).then((res) => {
@@ -156,23 +257,74 @@ function submit() {
}) })
} }
function changeFace() { function onWxSubmit(e: any) {
uni.chooseImage({ const nickName = String(e?.detail?.value?.nickName ?? form.nickName ?? '').trim()
success: (chooseImageRes) => { form.nickName = nickName
submit()
}
function onNicknameBlur(e: any) {
form.nickName = String(e?.detail?.value ?? '').trim()
}
function onNicknameReview(e: any) {
if (e?.detail?.pass === false) {
form.nickName = ''
uni.showToast({
title: '昵称未通过安全检测',
icon: 'none',
})
}
}
function uploadFace(filePath: string) {
if (!filePath) return
uni.showLoading({ title: '上传中', mask: true })
uni.uploadFile({ uni.uploadFile({
url: upload, url: upload,
filePath: chooseImageRes.tempFilePaths[0], filePath,
name: 'file', name: 'file',
header: { accessToken: storage.getAccessToken() }, header: { accessToken: storage.getAccessToken() },
success: (uploadFileRes) => { success: (uploadFileRes) => {
try {
const data = JSON.parse(uploadFileRes.data) const data = JSON.parse(uploadFileRes.data)
if (data.result) {
form.face = data.result form.face = data.result
} else {
uni.showToast({ title: '头像上传失败', icon: 'none' })
}
} catch {
uni.showToast({ title: '头像上传失败', icon: 'none' })
}
},
fail: () => {
uni.showToast({ title: '头像上传失败', icon: 'none' })
},
complete: () => {
uni.hideLoading()
}, },
}) })
}
function changeFace() {
uni.chooseImage({
count: 1,
success: (chooseImageRes) => {
uploadFace(chooseImageRes.tempFilePaths[0])
}, },
}) })
} }
function onChooseAvatar(e: any) {
const avatarUrl = e?.detail?.avatarUrl
if (!avatarUrl) return
uploadFace(avatarUrl)
}
function onChooseAvatarError() {
changeFace()
}
function selectTime(e: any) { function selectTime(e: any) {
const timestamp = typeof e?.value === 'number' ? e.value : birthdayValue.value const timestamp = typeof e?.value === 'number' ? e.value : birthdayValue.value
const formatted = formatBirthday(timestamp) const formatted = formatBirthday(timestamp)
@@ -192,11 +344,15 @@ function navigateToBindMobile(username: string) {
}) })
} }
function goBack() {
uni.navigateBack()
}
function syncUserInfo() { function syncUserInfo() {
const latest = storage.getUserInfo() || {} const latest = storage.getUserInfo() || {}
form.nickName = latest.nickName || '' form.nickName = latest.nickName || ''
form.birthday = latest.birthday || '' form.birthday = latest.birthday || ''
form.face = latest.face || '/static/missing-face.png' form.face = latest.face || DEFAULT_FACE
form.region = latest.region || [] form.region = latest.region || []
form.sex = latest.sex != null ? String(latest.sex) : '1' form.sex = latest.sex != null ? String(latest.sex) : '1'
form.regionPath = latest.region form.regionPath = latest.region
@@ -208,6 +364,7 @@ function syncUserInfo() {
onShow(() => { onShow(() => {
syncUserInfo() syncUserInfo()
ensurePrivacyAuthorize()
}) })
</script> </script>
@@ -239,6 +396,22 @@ page {
} }
} }
.avatar-btn {
width: 100%;
padding: 40rpx 0 20rpx;
margin: 0;
background: transparent;
border: none;
border-radius: 0;
line-height: 2em;
font-size: $font-sm;
color: $font-color-light;
&::after {
border: none;
}
}
.form { .form {
background: #fff; background: #fff;
} }
@@ -274,6 +447,22 @@ page {
color: #666 !important; color: #666 !important;
} }
.wx-nickname {
width: 100%;
height: 100%;
min-height: 44rpx;
text-align: right;
font-size: 28rpx;
color: #666;
background: transparent;
}
.wx-nickname-placeholder {
color: #c0c4cc;
font-size: 28rpx;
text-align: right;
}
.sex-row { .sex-row {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -322,6 +511,13 @@ page {
font-size: 30rpx; font-size: 30rpx;
background: $light-color; background: $light-color;
color: #fff; color: #fff;
border: none;
padding: 0;
margin: 0;
&::after {
border: none;
}
} }
.submit + .submit { .submit + .submit {

View File

@@ -234,7 +234,7 @@ import {
getStoreIsCollect, getStoreIsCollect,
} from '@/api/members.js' } from '@/api/members.js'
import config from '@/config/config' import config from '@/config/config'
import { getGoodsList } from '@/api/goods.js' import { getGoodsList, getMpScene } from '@/api/goods.js'
import { getAllCoupons } from '@/api/promotions.js' import { getAllCoupons } from '@/api/promotions.js'
import { getFloorStoreData } from '@/api/home' import { getFloorStoreData } from '@/api/home'
import { import {
@@ -504,12 +504,41 @@ function getCoupon(item: any) {
}) })
} }
onLoad((options: any) => { function parseShopIdFromScene(raw: string) {
storeId.value = options.id const scene = decodeURIComponent(String(raw || ''))
console.log(storeId.value, 'this.storeId') if (!scene) return ''
if (scene.includes('id=')) {
return scene.split('id=')[1].split('&')[0]
}
return scene
}
goodsParams.storeId = options.id async function resolveShopId(options: any) {
couponParams.storeId = options.id if (options?.id) return String(options.id)
if (!options?.scene) return ''
const scene = decodeURIComponent(String(options.scene))
if (scene.includes('id=')) {
return parseShopIdFromScene(scene)
}
try {
const res = await getMpScene(scene)
const payload = res?.data != null ? res.data : res
if (payload?.success && payload.result) {
return parseShopIdFromScene(String(payload.result))
}
} catch (e) {
// 短链解析失败时,把 scene 当店铺ID用
}
return scene
}
onLoad(async (options: any) => {
storeId.value = await resolveShopId(options)
goodsParams.storeId = storeId.value
couponParams.storeId = storeId.value
if (storeId.value) {
init()
}
}) })
onPageScroll((e) => { onPageScroll((e) => {
@@ -531,7 +560,6 @@ onMounted(() => {
withShareTicket: true, withShareTicket: true,
}) })
// #endif // #endif
init()
}) })
</script> </script>