修改昵称

This commit is contained in:
郭文文 2026-01-26 12:01:32 +08:00
parent 14c84de1ce
commit 9b34b51caf
5 changed files with 358 additions and 2 deletions

View File

@ -71,7 +71,9 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
onClick={handleConfirm} onClick={handleConfirm}
> >
<Text className="text-black">{confirmText}</Text> <Text className="text-black">{confirmText}</Text>
<Ionicons color="#000" name="flash" size={16} style={{ marginLeft: 4 }} /> {title==='确认支付?' && (
<Ionicons color="#000" name="flash" size={16} style={{ marginLeft: 4 }} />
)}
</Block> </Block>
</Block> </Block>
</Block> </Block>

View File

@ -575,7 +575,9 @@ const GooActions = observer<GooActionsProps>(function GooActions({ gooPoints, on
{isDev && isPolling && <Block className="ml-[4px] size-[6px] rounded-full bg-green-500" />} {isDev && isPolling && <Block className="ml-[4px] size-[6px] rounded-full bg-green-500" />}
</Block> </Block>
)} )}
<Block onClick={() => router.push('/settings')}>
<Ionicons color="white" name="settings-outline" size={22} />
</Block>
<Block <Block
className="size-[48px] items-center justify-center rounded-full border-[3px] border-black bg-white shadow-[4px_4px_0px_#000]" className="size-[48px] items-center justify-center rounded-full border-[3px] border-black bg-white shadow-[4px_4px_0px_#000]"
onClick={onOpenSearch} onClick={onOpenSearch}

View File

@ -95,6 +95,7 @@ function RootLayout() {
<Stack.Screen name="pointList" options={{ headerShown: false }} /> <Stack.Screen name="pointList" options={{ headerShown: false }} />
<Stack.Screen name="webview" options={{ headerShown: false }} /> <Stack.Screen name="webview" options={{ headerShown: false }} />
<Stack.Screen name="scan" options={{ headerShown: false }} /> <Stack.Screen name="scan" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
</Stack> </Stack>
</Providers> </Providers>
) )

186
app/profile.tsx Normal file
View File

@ -0,0 +1,186 @@
import { Ionicons } from '@expo/vector-icons'
import { Block, ConfirmModal, Img, Input, Text, Toast } from '@share/components'
import { router, Stack } from 'expo-router'
import { observer } from 'mobx-react-lite'
import React, { useEffect, useState } from 'react'
import { ScrollView } from 'react-native'
import { authClient } from '@/lib/auth'
import { userStore } from '@/stores'
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 = () => {
// TODO: 打开相册/拍照更换头像
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 infoItems: InfoItem[] = [
{
id: 'nickname',
label: '昵称',
value: user?.name || '未设置',
onPress: handleEditNickname,
},
{
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 bottom-0 left-0 right-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="mt-[32px] 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-[14px] text-right"
style={{ color: item.valueGray ? '#9CA3AF' : '#000' }}
numberOfLines={1}
ellipsizeMode="tail"
>
{item.value}
</Text>
{/*手机号 显示隐藏按钮 */}
{item.id !== 'phone' && (
<Ionicons color="#9CA3AF" name="chevron-forward" size={18} />
)}
</Block>
))}
</Block>
)
return (
<Block className="h-full flex-1 bg-white">
<Stack.Screen options={{ headerShown: false }} />
{renderHeader()}
<ScrollView contentContainerStyle={{ flexGrow: 1 }} showsVerticalScrollIndicator={false}>
{renderAvatarSection()}
{renderInfoList()}
</ScrollView>
</Block>
)
})

165
app/settings.tsx Normal file
View File

@ -0,0 +1,165 @@
import { Ionicons } from '@expo/vector-icons'
import { Block, Img, Text } from '@share/components'
import { router, Stack } from 'expo-router'
import { observer } from 'mobx-react-lite'
import React from 'react'
import { ScrollView } from 'react-native'
import { ConfirmModal, Toast } from '@share/components'
import { userStore } from '@/stores'
type MenuItem = {
id: string
label: string
icon: keyof typeof Ionicons.glyphMap
onPress: () => void
}
export default observer(function SettingsPage() {
const { user, isAuthenticated, signOut } = userStore
const handleLogout = () => {
Toast.showModal(
<ConfirmModal
title="退出登录"
content="确定要退出登录吗?"
onCancel={Toast.hideModal}
onConfirm={async () => {
await signOut()
Toast.show({ title: '已退出登录' })
router.replace('/(tabs)')
// 关闭modal
Toast.hideModal()
}}
/>,
)
}
const menuItems: MenuItem[] = [
{
id: 'service',
label: '服务条款',
icon: 'document-text-outline',
onPress: () => {
router.push({
pathname: '/webview',
params: { url: 'https://mixvideo.bowong.cc/terms', title: '服务条款' },
})
},
},
{
id: 'privacy',
label: '隐私协议',
icon: 'shield-outline',
onPress: () => {
router.push({
pathname: '/webview',
params: { url: 'https://mixvideo.bowong.cc/privacy', title: '隐私协议' },
})
},
},
// {
// id: 'about',
// label: '关于我们',
// icon: 'information-circle-outline',
// onPress: () => {
// // TODO: 替换为实际的关于我们URL或创建关于页面
// router.push({
// pathname: '/webview',
// params: { url: 'https://example.com/about', title: '关于我们' },
// })
// },
// },
]
const handleUserProfileClick = () => {
router.push('/profile')
}
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 renderUserSection = () => (
<Block
className="flex-row items-center gap-[12px] px-[16px] py-[10px]"
onClick={handleUserProfileClick}
>
<Block className="size-[60px] items-center justify-center overflow-hidden rounded-full bg-gray-200">
{user?.image ? (
<Img
src={user.image}
style={{ width: 60, height: 60, borderRadius: 30 }}
width={60}
/>
) : (
<Ionicons color="#9CA3AF" name="person" size={32} />
)}
</Block>
<Block className="flex-1">
<Text className="text-[16px] font-[700] text-black">{user?.name || user?.email || '未登录'}</Text>
{user?.email && user?.name && (
<Text className="mt-[4px] text-[12px] text-gray-500">{user.email}</Text>
)}
</Block>
<Ionicons color="#9CA3AF" name="chevron-forward" size={20} />
</Block>
)
const renderMenuSection = () => (
<Block className="mt-[8px]">
<Text className="px-[16px] py-[8px] text-[12px] text-gray-400">使</Text>
<Block className="bg-white">
{menuItems.map((item, index) => (
<Block key={item.id}>
<Block
className="flex-row items-center gap-[12px] px-[16px] py-[16px]"
onClick={item.onPress}
>
<Ionicons color="#666" name={item.icon} size={22} />
<Text className="flex-1 text-[14px] text-black">{item.label}</Text>
<Ionicons color="#9CA3AF" name="chevron-forward" size={20} />
</Block>
{index < menuItems.length - 1 && (
<Block className="ml-[50px] h-[1px] bg-gray-100" />
)}
</Block>
))}
</Block>
</Block>
)
const renderLogoutButton = () => (
<Block className="mt-[32px] items-center px-[16px]">
{isAuthenticated && (
<Block
className="w-full max-w-[200px] items-center justify-center rounded-lg bg-gray-100 py-[14px]"
onClick={handleLogout}
>
<Text className="text-[14px] font-[600] text-black">退</Text>
</Block>
)}
</Block>
)
return (
<Block className="flex-1 bg-white h-full">
<Stack.Screen options={{ headerShown: false }} />
{renderHeader()}
<ScrollView contentContainerStyle={{ flexGrow: 1 }} showsVerticalScrollIndicator={false}>
{renderUserSection()}
{renderMenuSection()}
{renderLogoutButton()}
</ScrollView>
</Block>
)
})