longz
2026-05-28 23fe797f9dded6d7ec9ecbcf760f764374dea8e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
<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>