656 lines
17 KiB
Plaintext
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>
|