Files
medical-mall/pages/user/profile.uvue

656 lines
17 KiB
Plaintext

<template>
<view class="page-wrapper">
<view class="top-section">
<view class="language-switch">
<button class="language-btn" @click="toggleLanguage">
{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}
</button>
</view>
</view>
<view class="main-section">
<scroll-view direction="vertical" class="profile-container">
<view v-if="isLoading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="profile.email == ''" class="error-container">
<text class="error-text">加载失败</text>
<button class="retry-button" @click="loadProfile">重试</button>
</view>
<view v-else class="profile-content">
<view class="avatar-section">
<image class="avatar" :src="userAvatar" mode="aspectFill" @click="chooseAvatar"></image>
</view>
<form @submit="onSubmit">
<view class="input-group">
<text class="input-label">用户名</text>
<input class="input-field" name="username" type="text" v-model="profile.username" placeholder="请输入用户名" />
</view>
<view class="input-group">
<text class="input-label">邮箱</text>
<input class="input-field readonly" name="email" type="text" v-model="profile.email" disabled />
<text class="hint-text">邮箱不可修改</text>
</view>
<view class="input-group">
<text class="input-label">性别</text>
<view class="picker-field" @click="showGenderPickerNow">
<text>{{ getGenderText(profile.gender ?? 'other') }}</text>
<text class="picker-arrow">></text>
</view>
<view v-if="showGenderPicker" class="picker-modal">
<picker-view class="picker-view" :value="tempGenderIndex" :indicator-style="'height: 50px;'" @change="onGenderPickerViewChange">
<picker-view-column style="width:750rpx;">
<view v-for="(g, idx) in genderOptions" :key="g" class="picker-item">
{{ getGenderText(g) }}
</view>
</picker-view-column>
</picker-view>
<view class="picker-actions">
<button @click="showGenderPicker = false">取消</button>
<button @click="confirmGenderPicker" class="picker-actions-button">确定</button>
</view>
</view>
</view>
<view class="input-group">
<text class="input-label">生日</text>
<view class="picker-field" @click="showBirthdayPickernow">
<text>{{ profile.birthday != null && profile.birthday != '' ? profile.birthday : '请选择生日' }}</text>
<text class="picker-arrow">></text>
</view>
<view v-if="showBirthdayPicker" class="picker-modal">
<picker-date :startYear="1970" :endYear="new Date().getFullYear()" :value="tempBirthday" @change="onBirthdayDateChange" />
<view class="picker-actions">
<button @click="showBirthdayPicker = false">取消</button>
<button @click="confirmBirthdayPicker" class="picker-actions-button">确定</button>
</view>
</view>
</view>
<view class="input-group">
<text class="input-label">身高 (cm)</text>
<input class="input-field" name="height" type="number" :value="profile.height_cm != null && profile.height_cm > 0 ? profile.height_cm : ''" placeholder="请输入身高" @input="onHeightInput" />
</view>
<view class="input-group">
<text class="input-label">体重 (kg)</text>
<input class="input-field" name="weight" type="number" :value="profile.weight_kg != null && profile.weight_kg > 0 ? profile.weight_kg : ''" placeholder="请输入体重" @input="onWeightInput" />
</view>
<view class="input-group">
<text class="input-label">个人简介</text>
<textarea class="textarea-field" name="bio" v-model="profile.bio" placeholder="请输入个人简介"></textarea>
</view>
<button form-type="submit" class="save-button" :disabled="isSaving" :loading="isSaving">
保存
</button>
</form>
<view v-if="saveSuccess != ''" class="success-message">
<text class="success-text">{{ saveSuccess }}</text>
</view>
<view v-else-if="saveError != ''" class="error-message">
<text class="error-text">{{ saveError }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { AkSupaSelectOptions } from '@/components/supadb/aksupa.uts'
import { setUserProfile } from '@/utils/store.uts'
import type { UserProfile } from '@/types/mall-types.uts'
const isLoading = ref<boolean>(false)
const saveSuccess = ref<string>('')
const saveError = ref<string>('')
const isSaving = ref<boolean>(false)
const userAvatar = ref<string>('/static/logo.png')
const currentLocale = ref<string>('zh-CN')
const genderOptions: Array<string> = ['male', 'female', 'other']
const tempGenderIndex = ref<Array<number>>([0])
const showGenderPicker = ref<boolean>(false)
const showBirthdayPicker = ref<boolean>(false)
const tempBirthday = ref<Array<number>>([2000, 1, 1])
const profile = ref<UserProfile>({
id: '',
username: '',
email: '',
gender: 'other',
birthday: '',
height_cm: 0,
weight_kg: 0,
bio: '',
avatar_url: '/static/logo.png',
preferred_language: 'zh-CN'
} as UserProfile)
const toggleLanguage = (): void => {
if (currentLocale.value === 'zh-CN') {
currentLocale.value = 'en-US'
} else {
currentLocale.value = 'zh-CN'
}
uni.showToast({
title: '语言已切换',
icon: 'success'
})
}
const getGenderText = (genderCode: string): string => {
if (genderCode == 'male') {
return '男'
} else if (genderCode == 'female') {
return '女'
} else {
return '其他'
}
}
const loadProfile = async (): Promise<void> => {
isLoading.value = true
const user = supa.user
if (user == null) {
profile.value.email = ''
isLoading.value = false
return
}
const userEmail = user.getString('email')
if (userEmail == null || userEmail == '') {
profile.value.email = ''
isLoading.value = false
return
}
const filter = `id=eq.${user.id as string}`
const options = { single: true } as AkSupaSelectOptions
const result = await supa.select('ak_users', filter, options)
const data = result.data
const error = result.error
if (Array.isArray(data) && data.length > 0) {
const prodata = data[0] as UTSJSONObject
const p: UserProfile = {
id: user.id as string,
username: prodata.getString('username') ?? '',
email: prodata.getString('email') ?? '',
gender: prodata.getString('gender') ?? 'other',
birthday: prodata.getString('birthday') ?? '',
height_cm: prodata.getNumber('height_cm') ?? 0,
weight_kg: prodata.getNumber('weight_kg') ?? 0,
bio: prodata.getString('bio') ?? '',
avatar_url: prodata.getString('avatar_url') ?? '/static/logo.png',
preferred_language: prodata.getString('preferred_language') ?? 'zh-CN'
} as UserProfile
profile.value = p
if (p.avatar_url != null && p.avatar_url != '') {
userAvatar.value = p.avatar_url!
}
setUserProfile(p)
} else {
profile.value.id = user.getString('id') ?? ''
profile.value.username = user.getString('username') ?? ''
profile.value.email = user.getString('email') ?? ''
if (profile.value.username == '') {
const emailStr = profile.value.email
if (emailStr != null && emailStr != '') {
const parts = emailStr.split('@')
if (parts.length > 0) {
profile.value.username = parts[0]
}
}
}
const newProfile = new UTSJSONObject({
id: profile.value.id,
username: profile.value.username,
email: profile.value.email,
gender: profile.value.gender,
preferred_language: profile.value.preferred_language
})
const insertResult = await supa.from('ak_users').insert(newProfile).execute()
if (insertResult.error == null) {
const newProfileData: UserProfile = {
id: profile.value.id,
username: profile.value.username,
email: profile.value.email,
gender: profile.value.gender,
preferred_language: profile.value.preferred_language
} as UserProfile
setUserProfile(newProfileData)
}
}
isLoading.value = false
}
const saveProfile = async (): Promise<void> => {
isSaving.value = true
saveSuccess.value = ''
saveError.value = ''
try {
const userid: string = profile.value.id ?? ''
const updateData = {
username: profile.value.username,
gender: profile.value.gender,
birthday: profile.value.birthday,
height_cm: profile.value.height_cm,
weight_kg: profile.value.weight_kg,
bio: profile.value.bio,
avatar_url: profile.value.avatar_url,
preferred_language: profile.value.preferred_language
}
const result = await supa
.from('ak_users')
.update(updateData)
.eq('id', userid)
.execute()
if (result.error == null) {
saveSuccess.value = '保存成功'
} else {
saveError.value = '保存失败'
}
} catch (e) {
saveError.value = '保存失败'
}
isSaving.value = false
}
const onSubmit = (): void => {
saveProfile()
}
const getUuid = (): string => {
return `${Date.now()}_${Math.floor(Math.random() * 1e8)}`
}
const chooseAvatar = (): void => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res: ChooseImageSuccess) => {
const upfilepath = res.tempFilePaths[0]
const userId = profile.value.id ?? ''
let ext = 'png'
const tempFiles = res.tempFiles
if (Array.isArray(tempFiles) && tempFiles.length > 0) {
const fileObj: ChooseImageTempFile = tempFiles[0]
const fileName = fileObj.name
if (fileName != null && fileName != '') {
const idx = fileName.lastIndexOf('.')
if (idx >= 0) {
ext = fileName.substring(idx + 1)
}
}
}
const uuid = getUuid()
const remotePath = `profiles/${userId}_${uuid}.${ext}`
supa.storage.from('zhipao').upload(remotePath, upfilepath, {}).then((uploadResult) => {
if (uploadResult.status == 200 || uploadResult.status == 201) {
const data = uploadResult.data
if (data != null) {
const dataObj = data as UTSJSONObject
let avatarUrl = dataObj.getString('Key')
if (avatarUrl != null && avatarUrl != '') {
avatarUrl = 'https://ak3.oulog.com/storage/v1/object/public/' + avatarUrl
userAvatar.value = avatarUrl
profile.value.avatar_url = avatarUrl
saveProfile()
uni.showToast({ title: '头像已更新', icon: 'success' })
}
}
} else {
uni.showToast({ title: '上传失败', icon: 'none' })
}
})
}
})
}
const onHeightInput = (e: UniInputEvent): void => {
const val = e.detail.value
if (val == '') {
profile.value.height_cm = 0
} else {
profile.value.height_cm = parseInt(val)
}
}
const onWeightInput = (e: UniInputEvent): void => {
const val = e.detail.value
if (val == '') {
profile.value.weight_kg = 0
} else {
profile.value.weight_kg = parseInt(val)
}
}
const showGenderPickerNow = (): void => {
const genderValue = profile.value.gender
const idx = genderValue != null ? genderOptions.indexOf(genderValue) : -1
tempGenderIndex.value = [idx >= 0 ? idx : 0]
showGenderPicker.value = true
}
const onGenderPickerViewChange = (e: UniPickerViewChangeEvent): void => {
const idx = e.detail.value[0]
tempGenderIndex.value = [(idx >= 0 && idx < genderOptions.length) ? idx : 0]
}
const confirmGenderPicker = (): void => {
profile.value.gender = genderOptions[tempGenderIndex.value[0]]
showGenderPicker.value = false
}
const onBirthdayDateChange = (vals: Array<number>): void => {
tempBirthday.value = vals
}
const showBirthdayPickernow = (): void => {
const birthday = profile.value.birthday
if (birthday != null && birthday != '') {
const parts = birthday.split('-')
if (parts.length == 3) {
tempBirthday.value = [parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2])]
}
}
showBirthdayPicker.value = true
}
const confirmBirthdayPicker = (): void => {
showBirthdayPicker.value = false
const y = tempBirthday.value[0]
const m = tempBirthday.value[1]
const d = tempBirthday.value[2]
const mm = m < 10 ? '0' + m : '' + m
const dd = d < 10 ? '0' + d : '' + d
profile.value.birthday = `${y}-${mm}-${dd}`
}
onMounted(() => {
loadProfile()
})
</script>
<style>
.page-wrapper {
display: flex;
flex-direction: column;
background-color: #f8f9fa;
/* 使用 100% 替代 100vh 以增强安卓兼容性 */
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.top-section {
flex-shrink: 0;
height: 100rpx;
position: relative;
background-color: #f8f9fa;
}
.main-section {
flex: 1;
/* 显式声明高度为 0 是 flex 容器内 scroll-view 生效的关键 */
height: 0;
}
.profile-container {
width: 100%;
height: 100%;
padding: 20rpx;
background-color: #f8f9fa;
box-sizing: border-box;
}
.language-switch {
position: absolute;
top: 20rpx;
right: 30rpx;
z-index: 10;
}
.language-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
font-size: 22rpx;
background-color: rgba(33, 150, 243, 0.8);
color: #fff;
font-weight: normal;
border: 2rpx solid rgba(255, 255, 255, 0.3);
text-align: center;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80%;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80%;
}
.error-text {
font-size: 28rpx;
color: #f44336;
margin-bottom: 20rpx;
}
.retry-button {
padding: 20rpx 40rpx;
font-size: 28rpx;
background-color: #2196f3;
color: white;
border-radius: 10rpx;
}
.profile-content {
padding: 20rpx;
background-color: white;
border-radius: 20rpx;
}
.avatar-section {
display: flex;
justify-content: center;
margin-bottom: 40rpx;
}
.avatar {
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
background-color: #eee;
}
.input-group {
margin-bottom: 30rpx;
}
.input-label {
font-size: 28rpx;
margin-bottom: 10rpx;
font-weight: normal;
color: #333;
display: flex;
}
.input-field {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
border: 2rpx solid #ddd;
background-color: #fff;
box-sizing: border-box;
}
.input-field.readonly {
background-color: #f5f5f5;
color: #999;
}
.hint-text {
font-size: 24rpx;
margin-top: 6rpx;
color: #999;
}
.picker-field {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
border: 2rpx solid #ddd;
background-color: #fff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.picker-arrow {
color: #999;
font-size: 24rpx;
}
.textarea-field {
width: 100%;
height: 200rpx;
padding: 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
border: 2rpx solid #ddd;
background-color: #fff;
box-sizing: border-box;
}
.save-button {
width: 100%;
height: 90rpx;
font-size: 32rpx;
margin: 20rpx 0;
border-radius: 45rpx;
background-image: linear-gradient(to right, #2196f3, #03a9f4);
color: #fff;
font-weight: normal;
text-align: center;
}
.save-button:disabled {
background: #ccc;
}
.success-message {
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #e8f5e9;
border-radius: 10rpx;
}
.success-text {
color: #43a047;
font-size: 28rpx;
text-align: center;
}
.error-message {
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #ffebee;
border-radius: 10rpx;
}
.picker-modal {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
z-index: 1000;
padding-bottom: 30rpx;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.picker-view {
width: 750rpx;
height: 320px;
background: #fff;
}
.picker-item {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
width: 750rpx;
}
.picker-actions {
display: flex;
justify-content: space-between;
width: 750rpx;
padding: 20rpx 40rpx 0 40rpx;
box-sizing: border-box;
}
.picker-actions-button {
flex: 1;
margin: 0 10rpx;
background: #2196f3;
color: #fff;
border-radius: 10rpx;
font-size: 28rpx;
height: 80rpx;
}
</style>