feat: 添加用户设置页面及相关功能,更新用户头像组件

This commit is contained in:
2025-12-14 23:20:44 +07:00
parent 3c1b2d7d0f
commit ae0cc18551
9 changed files with 229 additions and 7 deletions

2
components.d.ts vendored
View File

@@ -53,6 +53,7 @@ declare module 'vue' {
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
UiDivider: typeof import('./src/components/ui/divider/index.vue')['default']
UiInput: typeof import('./src/components/ui/input/index.vue')['default']
UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
@@ -103,6 +104,7 @@ declare global {
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
const UiDivider: typeof import('./src/components/ui/divider/index.vue')['default']
const UiInput: typeof import('./src/components/ui/input/index.vue')['default']
const UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']

View File

@@ -15,3 +15,7 @@ export type WithdrawBody = Omit<Parameters<typeof client.api.asset.withdraw.post
assetCode: AssetCodeEnum;
withdrawMethod: WithdrawMethodEnum;
};
export type UserProfileData = Treaty.Data<typeof client.api.user.profile.get>;
export type UpdateUserProfileBody = Parameters<typeof client.api.user.profile.put>[0];

View File

@@ -0,0 +1,21 @@
<script lang='ts' setup></script>
<template>
<ion-avatar v-bind="$attrs">
<img
src="https://api.iconify.design/material-icon-theme:bruno.svg"
alt="Avatar"
>
</ion-avatar>
</template>
<style lang='css' scoped>
ion-avatar {
background-color: #7e5cff;
}
@media (prefers-color-scheme: dark) {
ion-avatar {
background-color: #7e5cff;
}
}
</style>

View File

@@ -48,6 +48,10 @@ const routes: Array<RouteRecordRaw> = [
path: "/withdraw/index",
component: () => import("@/views/withdraw/index.vue"),
},
{
path: "/user/settings",
component: () => import("@/views/user/settings.vue"),
},
];
const router = createRouter({

View File

@@ -1,3 +1,4 @@
.ui-toolbar {
--background: var(--ion-color-primary-contrast);
--min-height: 50px;
}

View File

@@ -19,9 +19,7 @@ const { user } = useAuth();
<div class="container">
<div class="user-info">
<div class="avatar-wrapper">
<ion-avatar class="avatar size-16">
<img alt="User avatar" class="size-full" :src="user?.image || 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'">
</ion-avatar>
<ui-avatar class="size-18" />
</div>
<IonText class="user-email">
{{ user?.email }}

View File

@@ -7,9 +7,7 @@ const { user } = useAuth();
<template>
<div class="user-info-container">
<div class="user-info">
<ion-avatar>
<img class="size-full" alt="User avatar" :src="user?.image || 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'">
</ion-avatar>
<ui-avatar class="size-18" />
<div>
<div class="user-name">
{{ user?.email }}

View File

@@ -19,7 +19,7 @@ import WalletCard from "./components/wallet-card.vue";
<ion-button fill="clear">
<ion-icon slot="icon-only" :icon="notificationsOutline" />
</ion-button>
<ion-button fill="clear">
<ion-button fill="clear" router-link="/user/settings">
<ion-icon slot="icon-only" :icon="settingsOutline" />
</ion-button>
</div>

194
src/views/user/settings.vue Normal file
View File

@@ -0,0 +1,194 @@
<script lang='ts' setup>
import type { UpdateUserProfileBody, UserProfileData } from "@/api/types";
import { alertController, toastController } from "@ionic/vue";
import { arrowBackOutline, cameraOutline, chevronForwardOutline } from "ionicons/icons";
import { client } from "@/api";
const router = useRouter();
const userProfile = ref<UserProfileData | null>(null);
async function getUserProfile() {
const { data } = await client.api.user.profile.get();
if (data) {
userProfile.value = data;
}
}
async function updateProfile(updates: UpdateUserProfileBody) {
const { data } = await client.api.user.profile.put(updates);
if (data) {
userProfile.value = data;
const toast = await toastController.create({
message: "Profile updated successfully",
duration: 2000,
position: "bottom",
color: "success",
});
await toast.present();
}
}
function handleBack() {
router.back();
}
async function handleEditField(field: keyof UpdateUserProfileBody, label: string) {
if (!userProfile.value)
return;
const currentValue = userProfile.value[field as keyof UserProfileData];
const alert = await alertController.create({
header: `Edit ${label}`,
inputs: [
{
name: "value",
type: "text",
placeholder: `Enter your ${label.toLowerCase()}`,
value: currentValue?.toString() || "",
},
],
buttons: [
{
text: "Cancel",
role: "cancel",
},
{
text: "Save",
handler: async (data) => {
if (data.value !== undefined) {
await updateProfile({ [field]: data.value } as UpdateUserProfileBody);
}
},
},
],
});
await alert.present();
}
async function handleSignOut() {
const alert = await alertController.create({
header: "Sign Out",
message: "Are you sure you want to sign out?",
buttons: [
{
text: "Cancel",
role: "cancel",
},
{
text: "Sign Out",
role: "destructive",
handler: async () => {
// TODO: 实现登出逻辑
router.push("/");
},
},
],
});
await alert.present();
}
onMounted(() => {
getUserProfile();
});
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ui-toolbar">
<ion-buttons slot="start">
<ion-button @click="handleBack">
<ion-icon slot="icon-only" :icon="arrowBackOutline" />
</ion-button>
</ion-buttons>
<ion-title>Settings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<div class="flex flex-col items-center justify-center py-10">
<div class="relative">
<ui-avatar class="size-25" />
</div>
<div class="mt-4 text-lg font-semibold">
{{ userProfile?.fullName || 'User Name' }}
</div>
</div>
<!-- User Info List -->
<ion-list class="mt-5">
<ion-item button @click="handleEditField('fullName', 'Full Name')">
<ion-label>
<p class="text-xs text-text-400">
Full Name
</p>
<h2 class="mt-1">
{{ userProfile?.fullName || 'Not set' }}
</h2>
</ion-label>
</ion-item>
<ion-item button @click="handleEditField('gender', 'Gender')">
<ion-label>
<p class="text-xs text-text-400">
Gender
</p>
<h2 class="mt-1">
{{ userProfile?.gender || 'Not set' }}
</h2>
</ion-label>
</ion-item>
<ion-item button @click="handleEditField('birthday', 'Birthday')">
<ion-label>
<p class="text-xs text-text-400">
Birthday
</p>
<h2 class="mt-1">
{{ userProfile?.birthday || 'Not set' }}
</h2>
</ion-label>
</ion-item>
<ion-item button @click="handleEditField('country', 'Country')">
<ion-label>
<p class="text-xs text-text-400">
Country
</p>
<h2 class="mt-1">
{{ userProfile?.country || 'Not set' }}
</h2>
</ion-label>
</ion-item>
<ion-item button @click="handleEditField('city', 'City')">
<ion-label>
<p class="text-xs text-text-400">
City
</p>
<h2 class="mt-1">
{{ userProfile?.city || 'Not set' }}
</h2>
</ion-label>
</ion-item>
</ion-list>
<!-- Action Buttons -->
<div class="px-5 mt-10">
<ion-button expand="block" color="tertiary" @click="handleSignOut">
Sign Out
</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped>
ion-avatar {
--border-radius: 50%;
}
</style>