feat: 添加用户设置页面及相关功能,更新用户头像组件
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -53,6 +53,7 @@ declare module 'vue' {
|
|||||||
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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']
|
UiDivider: typeof import('./src/components/ui/divider/index.vue')['default']
|
||||||
UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
||||||
UiInputLabel: typeof import('./src/components/ui/input-label/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 LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
const RouterView: typeof import('vue-router')['RouterView']
|
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 UiDivider: typeof import('./src/components/ui/divider/index.vue')['default']
|
||||||
const UiInput: typeof import('./src/components/ui/input/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']
|
const UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export type WithdrawBody = Omit<Parameters<typeof client.api.asset.withdraw.post
|
|||||||
assetCode: AssetCodeEnum;
|
assetCode: AssetCodeEnum;
|
||||||
withdrawMethod: WithdrawMethodEnum;
|
withdrawMethod: WithdrawMethodEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserProfileData = Treaty.Data<typeof client.api.user.profile.get>;
|
||||||
|
|
||||||
|
export type UpdateUserProfileBody = Parameters<typeof client.api.user.profile.put>[0];
|
||||||
|
|||||||
21
src/components/ui/avatar/index.vue
Normal file
21
src/components/ui/avatar/index.vue
Normal 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>
|
||||||
@@ -48,6 +48,10 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: "/withdraw/index",
|
path: "/withdraw/index",
|
||||||
component: () => import("@/views/withdraw/index.vue"),
|
component: () => import("@/views/withdraw/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/user/settings",
|
||||||
|
component: () => import("@/views/user/settings.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
.ui-toolbar {
|
.ui-toolbar {
|
||||||
--background: var(--ion-color-primary-contrast);
|
--background: var(--ion-color-primary-contrast);
|
||||||
|
--min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const { user } = useAuth();
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
<ion-avatar class="avatar size-16">
|
<ui-avatar class="size-18" />
|
||||||
<img alt="User avatar" class="size-full" :src="user?.image || 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'">
|
|
||||||
</ion-avatar>
|
|
||||||
</div>
|
</div>
|
||||||
<IonText class="user-email">
|
<IonText class="user-email">
|
||||||
{{ user?.email }}
|
{{ user?.email }}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ const { user } = useAuth();
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-info-container">
|
<div class="user-info-container">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<ion-avatar>
|
<ui-avatar class="size-18" />
|
||||||
<img class="size-full" alt="User avatar" :src="user?.image || 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'">
|
|
||||||
</ion-avatar>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
{{ user?.email }}
|
{{ user?.email }}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import WalletCard from "./components/wallet-card.vue";
|
|||||||
<ion-button fill="clear">
|
<ion-button fill="clear">
|
||||||
<ion-icon slot="icon-only" :icon="notificationsOutline" />
|
<ion-icon slot="icon-only" :icon="notificationsOutline" />
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear">
|
<ion-button fill="clear" router-link="/user/settings">
|
||||||
<ion-icon slot="icon-only" :icon="settingsOutline" />
|
<ion-icon slot="icon-only" :icon="settingsOutline" />
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
194
src/views/user/settings.vue
Normal file
194
src/views/user/settings.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user