| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | 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', |
| | |
| | | }, |
| | | }) |
| | | |
| | | 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 formData = ref({ |
| | | name: '王明', |
| | | company: '成都xxxxx有限公司', |
| | | gender: 'female', // 'male' or 'female' |
| | | 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() |
| | | } |
| | | |
| | | function handleSave() { |
| | | uni.showToast({ title: '保存成功', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | }, 1000) |
| | | 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 |
| | | } |
| | | } |
| | | |
| | | 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 handleChooseAvatar() { |
| | | if (uploadingAvatar.value) { |
| | | return |
| | | } |
| | | try { |
| | | const chooseResult = await uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ['compressed'], |
| | | sourceType: ['album', 'camera'], |
| | | }) |
| | | const filePath = chooseResult.tempFilePaths?.[0] |
| | | 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(() => { |
| | | 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="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 class="absolute left-0 w-full text-center text-[36rpx] font-500 text-[#000000] z-0"> |
| | | 个人资料 |
| | | </view> |
| | | <view class="w-[88rpx]"></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="w-[160rpx] h-[160rpx] rounded-full border-[4rpx] border-solid border-white shadow-[0_4rpx_12rpx_rgba(0,0,0,0.1)] overflow-hidden mb-[64rpx] shrink-0 relative bg-white"> |
| | | <image src="/static/mine/header.png" class="w-full h-full" mode="aspectFill" /> |
| | | <view class="mb-[64rpx] flex flex-col items-center"> |
| | | <view 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" |
| | | @click="handleChooseAvatar"> |
| | | <image :src="formData.avatarUrl || authStore.profile?.avatarUrl || '/static/mine/header.png'" class="w-full h-full" mode="aspectFill" /> |
| | | </view> |
| | | <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="text" |
| | | placeholder="请输入姓名" |
| | | <input |
| | | v-model="formData.name" |
| | | type="text" |
| | | placeholder="请输入姓名" |
| | | placeholder-style="color: #B3B3B3; font-size: 28rpx;" |
| | | class="flex-1 text-[28rpx] text-[#333333]" |
| | | 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="请输入公司名称" |
| | | <input |
| | | v-model="formData.company" |
| | | type="text" |
| | | placeholder="请输入公司名称" |
| | | placeholder-style="color: #B3B3B3; font-size: 28rpx;" |
| | | class="flex-1 text-[28rpx] text-[#333333]" |
| | | 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> |
| | | <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]" /> |
| | | </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> |
| | | <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]" /> |
| | | </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" @click="handleSave"> |
| | | <text class="text-[32rpx] text-white font-medium">保存</text> |
| | | <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-b1 { |
| | | border: 2rpx solid #F5F5F5; |
| | | } |
| | | </style> |
| | | </style> |