<script setup lang="ts">
|
import { reactive, ref } from 'vue'
|
import { onShow } from '@dcloudio/uni-app'
|
import { fetchMiniMe, updateMiniProfile, uploadMiniProfileImage } from '@/api/auth'
|
import { useMiniAuthStore } from '@/store/auth'
|
import { normalizeMeProfile } from '@/types/auth'
|
|
definePage({
|
name: 'profile',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
type GenderValue = 'male' | 'female' | ''
|
|
const router = useRouter()
|
const authStore = useMiniAuthStore()
|
const sysInfo = uni.getSystemInfoSync()
|
const statusBarHeight = sysInfo.statusBarHeight || 0
|
const navBarHeight = 44
|
const saving = ref(false)
|
const loading = ref(false)
|
const uploadingAvatar = ref(false)
|
const profileInitialized = ref(false)
|
|
const formData = reactive({
|
name: '',
|
company: '',
|
gender: '' as GenderValue,
|
avatarUrl: '',
|
})
|
|
function mapProfileGender(value?: string | null): GenderValue {
|
if (value === '男') {
|
return 'male'
|
}
|
if (value === '女') {
|
return 'female'
|
}
|
return ''
|
}
|
|
function mapSubmitGender(value: GenderValue): string | null {
|
if (value === 'male') {
|
return '男'
|
}
|
if (value === 'female') {
|
return '女'
|
}
|
return null
|
}
|
|
function applyProfileToForm() {
|
const profile = authStore.profile
|
formData.name = profile?.displayName || ''
|
formData.company = profile?.companyName || ''
|
formData.gender = mapProfileGender(profile?.gender)
|
formData.avatarUrl = profile?.avatarUrl || ''
|
}
|
|
function handleBack() {
|
router.back()
|
}
|
|
async function loadProfile() {
|
applyProfileToForm()
|
loading.value = true
|
try {
|
const profile = await fetchMiniMe()
|
authStore.setProfile(normalizeMeProfile(profile))
|
applyProfileToForm()
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载个人资料失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
loading.value = false
|
profileInitialized.value = true
|
}
|
}
|
|
async function handleSave() {
|
const nickName = formData.name.trim()
|
if (!nickName) {
|
return uni.showToast({ title: '请输入姓名', icon: 'none' })
|
}
|
saving.value = true
|
try {
|
const profile = await updateMiniProfile({
|
nickName,
|
companyName: formData.company.trim() || null,
|
gender: mapSubmitGender(formData.gender),
|
avatarUrl: formData.avatarUrl || null,
|
})
|
authStore.setProfile(normalizeMeProfile(profile))
|
applyProfileToForm()
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
setTimeout(() => {
|
router.back()
|
}, 600)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '保存失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
saving.value = false
|
}
|
}
|
|
async function handleWxChooseAvatar(event: any) {
|
if (uploadingAvatar.value) {
|
return
|
}
|
try {
|
const filePath = event?.detail?.avatarUrl
|
if (!filePath) {
|
return
|
}
|
uploadingAvatar.value = true
|
uni.showLoading({ title: '上传中...' })
|
const avatarUrl = await uploadMiniProfileImage(filePath)
|
formData.avatarUrl = avatarUrl
|
authStore.setProfile({
|
...(authStore.profile || normalizeMeProfile(await fetchMiniMe())),
|
avatarUrl,
|
})
|
uni.showToast({ title: '头像上传成功', icon: 'success' })
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '头像上传失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
uploadingAvatar.value = false
|
uni.hideLoading()
|
}
|
}
|
|
onShow(() => {
|
if (!profileInitialized.value) {
|
void loadProfile()
|
}
|
})
|
</script>
|
|
<template>
|
<view class="h-screen w-full relative box-border flex flex-col overflow-hidden m-0 p-0 bg-[#F6FBFF]">
|
<view class="fixed left-0 top-0 z-0 h-[500rpx] w-full pointer-events-none"
|
style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
|
|
<view class="relative z-50 w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
|
<view class="fixed left-0 top-0 w-full z-50" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
|
<view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
|
<view class="w-full h-[500rpx] absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
|
</view>
|
<view class="flex items-center justify-between w-full absolute left-0 z-10"
|
:style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
|
<view class="flex h-full w-[88rpx] items-center justify-center relative z-10" @click="handleBack">
|
<wd-icon name="arrow-left" size="40rpx" color="#000000" />
|
</view>
|
<view class="absolute left-0 w-full text-center text-[36rpx] font-500 text-[#000000] z-0">
|
个人资料
|
</view>
|
<view class="w-[88rpx]" />
|
</view>
|
</view>
|
</view>
|
|
<view class="relative z-10 flex-1 flex flex-col items-center pt-[48rpx] px-[32rpx]">
|
<view class="mb-[64rpx] flex flex-col items-center">
|
<button
|
class="w-[160rpx] h-[160rpx] rounded-full border-[4rpx] border-solid border-white shadow-[0_4rpx_12rpx_rgba(0,0,0,0.1)] overflow-hidden shrink-0 relative bg-white p-0 m-0"
|
open-type="chooseAvatar"
|
@chooseavatar="handleWxChooseAvatar"
|
>
|
<image :src="formData.avatarUrl || authStore.profile?.avatarUrl || '/static/mine/header.png'" class="w-full h-full" mode="aspectFill" />
|
</button>
|
<text class="mt-[20rpx] text-[24rpx] text-[#666666]">
|
{{ uploadingAvatar ? '头像上传中...' : '点击更换头像' }}
|
</text>
|
</view>
|
|
<view class="w-full bg-white rounded-[16rpx] py-[36rpx] shadow-[0_4rpx_16rpx_rgba(0,0,0,0.05)] px-[24rpx] box-border mb-[120rpx]">
|
<view class="h-[96rpx] pl-[40rpx] border-b1 mb-[24rpx] flex items-center">
|
<input
|
v-model="formData.name"
|
type="nickname"
|
placeholder="请输入姓名"
|
placeholder-style="color: #B3B3B3; font-size: 28rpx;"
|
class="flex-1 text-[28rpx] text-[#333333]"
|
/>
|
</view>
|
<view class="h-[96rpx] pl-[40rpx] border-b1 mb-[24rpx] flex items-center">
|
<input
|
v-model="formData.company"
|
type="text"
|
placeholder="请输入公司名称"
|
placeholder-style="color: #B3B3B3; font-size: 28rpx;"
|
class="flex-1 text-[28rpx] text-[#333333]"
|
/>
|
</view>
|
<view class="h-[96rpx] pl-[40rpx] pr-[40rpx] border-b1 flex items-center justify-between">
|
<text class="text-[28rpx] text-[#333333]">性别:</text>
|
<view class="flex items-center gap-[40rpx]">
|
<view class="flex items-center gap-[12rpx]" @click="formData.gender = 'female'">
|
<view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center box-border" :class="formData.gender === 'female' ? 'border-[#0068FF]' : 'border-[#CCCCCC]'">
|
<view v-if="formData.gender === 'female'" class="w-[16rpx] h-[16rpx] rounded-full bg-[#0068FF]" />
|
</view>
|
<image src="/static/appointment/woman.png" class="w-[32rpx] h-[32rpx]" />
|
<text class="text-[26rpx] text-[#666666]">女</text>
|
</view>
|
<view class="flex items-center gap-[12rpx]" @click="formData.gender = 'male'">
|
<view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center box-border" :class="formData.gender === 'male' ? 'border-[#0068FF]' : 'border-[#CCCCCC]'">
|
<view v-if="formData.gender === 'male'" class="w-[16rpx] h-[16rpx] rounded-full bg-[#0068FF]" />
|
</view>
|
<image src="/static/appointment/man.png" class="w-[32rpx] h-[32rpx]" />
|
<text class="text-[26rpx] text-[#666666]">男</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="w-[520rpx] h-[96rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)] transition-opacity active:opacity-70"
|
:class="saving || loading ? 'opacity-70' : ''"
|
@click="handleSave">
|
<text class="text-[32rpx] text-white font-medium">{{ saving ? '保存中...' : '保存' }}</text>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<style scoped>
|
.border-b1 {
|
border: 2rpx solid #F5F5F5;
|
}
|
</style>
|