261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons'
|
|
import { Block, ConfirmModal, Img, Input, Text, Toast } from '@share/components'
|
|
import * as ImagePicker from 'expo-image-picker'
|
|
import { router, Stack } from 'expo-router'
|
|
import { observer } from 'mobx-react-lite'
|
|
import React, { useEffect, useState } from 'react'
|
|
import { ScrollView } from 'react-native'
|
|
|
|
import { imgPicker } from '@/@share/apis'
|
|
import ChangePasswordModal from '@/components/ChangePasswordModal'
|
|
import { authClient } from '@/lib/auth'
|
|
import { userStore } from '@/stores'
|
|
import { uploadFile } from '@/utils'
|
|
|
|
type InfoItem = {
|
|
id: string
|
|
label: string
|
|
value: string
|
|
valueGray?: boolean
|
|
onPress: () => void
|
|
}
|
|
|
|
type EditNicknameModalProps = {
|
|
initialName: string
|
|
onConfirm: (name: string) => void | Promise<void>
|
|
onCancel: () => void
|
|
}
|
|
|
|
function EditNicknameModal({ initialName, onConfirm, onCancel }: EditNicknameModalProps) {
|
|
const [name, setName] = useState(initialName)
|
|
|
|
useEffect(() => {
|
|
setName(initialName)
|
|
}, [initialName])
|
|
|
|
return (
|
|
<ConfirmModal
|
|
badge="用户名"
|
|
cancelText="取消"
|
|
confirmText="确定"
|
|
content={
|
|
<Block className="w-full">
|
|
<Input
|
|
className="w-full rounded-lg border-2 border-black px-[12px] py-[10px] text-[14px]"
|
|
placeholder="请输入6-12个字符"
|
|
placeholderTextColor="#9CA3AF"
|
|
value={name}
|
|
onChangeText={setName}
|
|
/>
|
|
</Block>
|
|
}
|
|
title="修改昵称"
|
|
onCancel={onCancel}
|
|
onConfirm={() => onConfirm(name)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export default observer(function ProfilePage() {
|
|
const { user } = userStore
|
|
|
|
const handleEditAvatar = async () => {
|
|
try {
|
|
const assets = await imgPicker({
|
|
maxImages: 1,
|
|
type: ImagePicker.MediaTypeOptions.Images,
|
|
resultType: 'asset',
|
|
})
|
|
const asset = assets[0] as ImagePicker.ImagePickerAsset
|
|
if (!asset) return
|
|
|
|
Toast.showLoading({ title: '上传中...', duration: 0 })
|
|
const url = await uploadFile({
|
|
uri: asset.uri,
|
|
mimeType: asset.mimeType ?? 'image/jpeg',
|
|
fileName: asset.fileName ?? `avatar_${Date.now()}.jpg`,
|
|
})
|
|
|
|
const res = await authClient.updateUser({ image: url })
|
|
const err = (res as { error?: { message?: string } }).error
|
|
if (err) {
|
|
Toast.hideLoading()
|
|
Toast.show({ title: err.message || '头像更新失败,请重试' })
|
|
return
|
|
}
|
|
if (userStore.user) {
|
|
userStore.setUser({ ...userStore.user, image: url })
|
|
}
|
|
Toast.hideLoading()
|
|
Toast.show({ title: '头像已更新' })
|
|
} catch (e) {
|
|
Toast.hideLoading()
|
|
if (e instanceof Error && e.message !== '未选择任何图片') {
|
|
Toast.show({ title: '上传失败,请重试' })
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleEditNickname = () => {
|
|
Toast.showModal(
|
|
<EditNicknameModal
|
|
initialName={user?.name || ''}
|
|
onConfirm={async (name) => {
|
|
const trimmed = name.trim()
|
|
if (trimmed.length < 6 || trimmed.length > 12) {
|
|
Toast.show({ title: '请输入6-12个字符' })
|
|
return
|
|
}
|
|
try {
|
|
const res = await authClient.updateUser({ name: trimmed })
|
|
const err = (res as { error?: { message?: string } }).error
|
|
if (err) {
|
|
Toast.show({ title: err.message || '修改失败,请重试' })
|
|
return
|
|
}
|
|
if (userStore.user) {
|
|
userStore.setUser({ ...userStore.user, name: trimmed })
|
|
}
|
|
Toast.hideModal()
|
|
Toast.show({ title: '昵称已更新' })
|
|
} catch (e) {
|
|
Toast.show({ title: '修改失败,请重试' })
|
|
}
|
|
}}
|
|
onCancel={() => Toast.hideModal()}
|
|
/>,
|
|
)
|
|
}
|
|
|
|
const handleChangePassword = () => {
|
|
Toast.showModal(
|
|
<ChangePasswordModal
|
|
onConfirm={async (currentPassword, newPassword) => {
|
|
try {
|
|
// better-auth 使用 changePassword 方法修改密码
|
|
const res = await (authClient as any).changePassword({
|
|
currentPassword,
|
|
newPassword,
|
|
})
|
|
const err = (res as { error?: { message?: string } }).error
|
|
if (err) {
|
|
Toast.show({ title: err.message || '修改失败,请重试' })
|
|
return
|
|
}
|
|
Toast.hideModal()
|
|
Toast.show({ title: '密码已修改,请重新登录' })
|
|
// 退出登录
|
|
await userStore.signOut()
|
|
// 跳转到登录页
|
|
router.replace('/auth')
|
|
} catch (e) {
|
|
Toast.show({ title: '修改失败,请重试' })
|
|
}
|
|
}}
|
|
onCancel={() => Toast.hideModal()}
|
|
/>,
|
|
)
|
|
}
|
|
|
|
const infoItems: InfoItem[] = [
|
|
{
|
|
id: 'nickname',
|
|
label: '昵称',
|
|
value: user?.name || '未设置',
|
|
onPress: handleEditNickname,
|
|
},
|
|
...(user?.emailVerified
|
|
? [
|
|
{
|
|
id: 'email',
|
|
label: '邮箱',
|
|
value: user.email,
|
|
onPress: () => {
|
|
Toast.show({ title: '邮箱不可修改' })
|
|
},
|
|
},
|
|
{
|
|
id: 'password',
|
|
label: '密码',
|
|
value: '修改密码',
|
|
valueGray: true,
|
|
onPress: handleChangePassword,
|
|
},
|
|
]
|
|
: []),
|
|
...(user?.phoneNumber
|
|
? [
|
|
{
|
|
id: 'phone',
|
|
label: '手机号',
|
|
value: user.phoneNumber,
|
|
onPress: () => {
|
|
Toast.show({ title: '手机号不可修改' })
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
]
|
|
|
|
const renderHeader = () => (
|
|
<Block className="flex-row items-center justify-between px-[16px] py-[10px]">
|
|
<Block className="ml-[-8px] size-[40px] items-center justify-center" opacity={0.7} onClick={() => router.back()}>
|
|
<Ionicons color="black" name="chevron-back" size={24} />
|
|
</Block>
|
|
<Text className="text-[16px] font-[700] text-black">个人信息</Text>
|
|
<Block className="w-[32px]" />
|
|
</Block>
|
|
)
|
|
|
|
const renderAvatarSection = () => (
|
|
<Block className="mt-[24px] items-center">
|
|
<Block className="relative size-[100px] overflow-hidden rounded-full">
|
|
<Block className="size-[100px] items-center justify-center overflow-hidden rounded-full bg-gray-200">
|
|
{user?.image ? (
|
|
<Img src={user.image} style={{ width: 100, height: 100, borderRadius: 50 }} width={100} />
|
|
) : (
|
|
<Ionicons color="#9CA3AF" name="person" size={48} />
|
|
)}
|
|
</Block>
|
|
<Block
|
|
className="absolute inset-x-0 bottom-0 items-center justify-center bg-[#00000080] py-[2px]"
|
|
style={{ borderBottomLeftRadius: 50, borderBottomRightRadius: 50 }}
|
|
onClick={handleEditAvatar}
|
|
>
|
|
<Text className="text-[12px] font-[600] text-white">编辑</Text>
|
|
</Block>
|
|
</Block>
|
|
</Block>
|
|
)
|
|
|
|
const renderInfoList = () => (
|
|
<Block className="mx-[16px] mt-[32px] rounded-xl bg-white px-[16px]">
|
|
{infoItems.map((item) => (
|
|
<Block key={item.id} className="flex-row items-center gap-[12px] py-[16px]" onClick={item.onPress}>
|
|
<Text className="w-[56px] text-[14px] text-black">{item.label}</Text>
|
|
<Text
|
|
className="flex-1 text-right text-[14px] text-[#9D9D9D]"
|
|
style={{ color: item.valueGray ? '#9CA3AF' : '#000' }}
|
|
numberOfLines={1}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{item.value}
|
|
</Text>
|
|
{item.id !== 'phone' && item.id !== 'email' && <Ionicons color="#9D9D9D" name="chevron-forward" size={18} />}
|
|
</Block>
|
|
))}
|
|
</Block>
|
|
)
|
|
|
|
return (
|
|
<Block className="h-full flex-1 bg-[#fafafa]">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
{renderHeader()}
|
|
<ScrollView contentContainerStyle={{ flexGrow: 1 }} showsVerticalScrollIndicator={false}>
|
|
{renderAvatarSection()}
|
|
{renderInfoList()}
|
|
</ScrollView>
|
|
</Block>
|
|
)
|
|
})
|