hookehuyr

feat(profile): 新增用户设置功能,包括头像、用户名和密码修改

新增了用户设置页面及其子页面,支持用户修改头像、用户名和密码。添加了相关API接口,并在路由中配置了对应的路径。同时引入了@heroicons/vue库以支持页面中的图标使用。
......@@ -8,6 +8,7 @@
"name": "vue-vite",
"version": "0.0.0",
"dependencies": {
"@heroicons/vue": "^2.2.0",
"@vant/use": "^1.6.0",
"dayjs": "^1.11.13",
"swiper": "^11.2.6",
......@@ -824,6 +825,14 @@
"node": ">=18"
}
},
"node_modules/@heroicons/vue": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
"integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==",
"peerDependencies": {
"vue": ">= 3"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
......
......@@ -15,6 +15,7 @@
"dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar"
},
"dependencies": {
"@heroicons/vue": "^2.2.0",
"@vant/use": "^1.6.0",
"dayjs": "^1.11.13",
"swiper": "^11.2.6",
......
......@@ -11,6 +11,9 @@ const Api = {
USER_LOGIN: '/users/login',
USER_REGISTER: '/users/register',
USER_INFO: '/users/info',
USER_UPDATE: '/users/update',
USER_AVATAR: '/users/avatar',
USER_PASSWORD: '/users/password',
}
/**
......@@ -32,3 +35,22 @@ export const registerAPI = (params) => fn(fetch.post(Api.USER_REGISTER, params))
* @description: 获取用户信息
*/
export const getUserInfoAPI = () => fn(fetch.get(Api.USER_INFO));
/**
* @description: 更新用户信息
* @param: name 用户名称
*/
export const updateUserInfoAPI = (params) => fn(fetch.put(Api.USER_UPDATE, params));
/**
* @description: 上传用户头像
* @param: avatar 头像文件
*/
export const uploadAvatarAPI = (formData) => fn(fetch.post(Api.USER_AVATAR, formData));
/**
* @description: 修改用户密码
* @param: oldPassword 原密码
* @param: newPassword 新密码
*/
export const updatePasswordAPI = (params) => fn(fetch.put(Api.USER_PASSWORD, params));
......
......@@ -23,14 +23,21 @@ declare module 'vue' {
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanDatePicker: typeof import('vant/es')['DatePicker']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanList: typeof import('vant/es')['List']
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
VanRate: typeof import('vant/es')['Rate']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
VanUploader: typeof import('vant/es')['Uploader']
}
}
......
......@@ -140,6 +140,30 @@ const routes = [
meta: { title: '消息详情' },
},
{
path: '/profile/settings',
name: 'Settings',
component: () => import('../views/profile/SettingsPage.vue'),
meta: { title: '设置' },
},
{
path: '/profile/settings/avatar',
name: 'AvatarSetting',
component: () => import('../views/profile/settings/AvatarSettingPage.vue'),
meta: { title: '修改头像' },
},
{
path: '/profile/settings/username',
name: 'UsernameSetting',
component: () => import('../views/profile/settings/UsernameSettingPage.vue'),
meta: { title: '修改用户名' },
},
{
path: '/profile/settings/password',
name: 'PasswordSetting',
component: () => import('../views/profile/settings/PasswordSettingPage.vue'),
meta: { title: '修改密码' },
},
{
path: '/test',
name: 'test',
component: () => import('../views/test.vue'),
......
......@@ -328,6 +328,11 @@ const menuItems3 = [
badge: "3",
},
{
icon: "settings",
title: "设置",
path: "/profile/settings",
},
{
icon: "question",
title: "帮助中心",
path: "/profile/help",
......
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 13:22:18
* @FilePath: /mlaj/src/views/profile/SettingsPage.vue
* @Description: 用户设置页面
-->
<template>
<AppLayout title="设置">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<!-- 设置列表 -->
<div class="divide-y divide-gray-100">
<!-- 头像设置 -->
<div class="p-4" @click="router.push('/profile/settings/avatar')">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- <div class="relative">
<img
:src="userAvatar || '/src/assets/images/avatar1.jpg'"
alt="用户头像"
class="w-16 h-16 rounded-full object-cover"
/>
</div> -->
<div class="ml-4">
<h3 class="text-base font-medium text-gray-900">修改头像</h3>
<p class="text-sm text-gray-500">支持 jpg、png 格式,大小不超过 2MB</p>
</div>
</div>
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
<!-- 用户名设置 -->
<div class="p-4" @click="router.push('/profile/settings/username')">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-medium text-gray-900">修改用户名</h3>
<p class="text-sm text-gray-500">{{ username }}</p>
</div>
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
<!-- 密码设置 -->
<div class="p-4" @click="router.push('/profile/settings/password')">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-medium text-gray-900">修改密码</h3>
<p class="text-sm text-gray-500">定期修改密码更安全</p>
</div>
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</FrostedGlass>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import AppLayout from '@/components/layout/AppLayout.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { ChevronRightIcon } from '@heroicons/vue/24/outline';
import { useTitle } from '@vueuse/core';
import { getUserInfoAPI } from '@/api/users';
const $route = useRoute();
const router = useRouter();
useTitle($route.meta.title);
// 用户头像
const userAvatar = ref('');
// 用户名
const username = ref('');
// 获取用户信息
onMounted(async () => {
try {
const response = await getUserInfoAPI();
if (response.data) {
userAvatar.value = response.data.avatar;
username.value = response.data.name;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
});
</script>
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 13:32:25
* @FilePath: /mlaj/src/views/profile/settings/AvatarSettingPage.vue
* @Description: 修改头像页面
-->
<template>
<AppLayout title="">
<div
class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"
>
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<div class="p-4">
<div class="flex flex-col items-center">
<div class="mb-6">
<van-image
round
:src="
userAvatar || 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
"
alt="用户头像"
class="rounded-full border-4 border-white shadow-lg"
width="8rem"
height="8rem"
fit="cover"
/>
</div>
<p class="text-sm text-gray-500 mb-4">支持 jpg、png 格式,大小不超过 2MB</p>
<van-uploader
:after-read="handleAvatarChange"
:max-size="2 * 1024 * 1024"
accept="image/jpeg,image/png"
:preview-image="false"
>
<van-button
block
type="primary"
class="max-w-xs"
>
选择新头像
</van-button>
</van-uploader>
</div>
</div>
</FrostedGlass>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, onMounted } from "vue";
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import { getUserInfoAPI, uploadAvatarAPI } from "@/api/users";
import { useTitle } from '@vueuse/core';
const $route = useRoute();
useTitle($route.meta.title);
// 用户头像
const userAvatar = ref("");
// 获取用户信息
onMounted(async () => {
try {
const response = await getUserInfoAPI();
if (response.data) {
userAvatar.value = response.data.avatar;
}
} catch (error) {
console.error("获取用户信息失败:", error);
}
});
// 处理头像上传
const handleAvatarChange = async (file) => {
try {
const formData = new FormData();
formData.append("avatar", file);
const response = await uploadAvatarAPI(formData);
if (response.data) {
userAvatar.value = response.data.url;
alert("头像上传成功");
}
} catch (error) {
console.error("头像上传失败:", error);
alert("头像上传失败,请重试");
}
};
</script>
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 13:19:54
* @FilePath: /mlaj/src/views/profile/settings/PasswordSettingPage.vue
* @Description: 修改密码页面
-->
<template>
<AppLayout title="">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<div class="p-4">
<van-form @submit="handlePasswordChange">
<van-cell-group inset>
<van-field
v-model="oldPassword"
type="password"
name="oldPassword"
label="原密码"
placeholder="请输入原密码"
:rules="[{ required: true, message: '请输入原密码' }, { min: 6, message: '密码长度至少6位' }]"
/>
<van-field
v-model="newPassword"
type="password"
name="newPassword"
label="新密码"
placeholder="请输入新密码"
:rules="[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码长度至少6位' }]"
/>
<van-field
v-model="confirmPassword"
type="password"
name="confirmPassword"
label="确认新密码"
placeholder="请确认新密码"
:rules="[{ required: true, message: '请确认新密码' }, { validator: validateConfirmPassword, message: '两次输入的密码不一致' }]"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button
native-type="submit"
type="primary"
block
round
:loading="loading"
class="bg-green-500 hover:bg-green-600 transition-colors"
>
保存修改
</van-button>
</div>
</van-form>
</div>
</FrostedGlass>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref } from 'vue';
import AppLayout from '@/components/layout/AppLayout.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { updatePasswordAPI } from '@/api/users';
import { useTitle } from '@vueuse/core';
const $route = useRoute();
useTitle($route.meta.title);
// 密码
const oldPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const loading = ref(false);
// 确认密码验证
const validateConfirmPassword = (val) => val === newPassword.value;
// 处理密码修改
const handlePasswordChange = async () => {
loading.value = true;
try {
const response = await updatePasswordAPI({
oldPassword: oldPassword.value,
newPassword: newPassword.value
});
if (response.data) {
// 清空输入框
oldPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
showSuccessToast('密码修改成功');
}
} catch (error) {
console.error('密码修改失败:', error);
showFailToast('密码修改失败,请重试');
} finally {
loading.value = false;
}
};
</script>
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 13:33:27
* @FilePath: /mlaj/src/views/profile/settings/UsernameSettingPage.vue
* @Description: 修改用户名页面
-->
<template>
<AppLayout title="">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<div class="p-4">
<div class="space-y-4">
<div class="mb-6">
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">新用户名</label>
<input
v-model="username"
type="text"
id="username"
placeholder="请输入新的用户名"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<van-button
@click="handleUsernameChange"
type="primary"
block
round
class="bg-green-500 hover:bg-green-600 transition-colors"
>
保存修改
</van-button>
</div>
</div>
</FrostedGlass>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import AppLayout from '@/components/layout/AppLayout.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { getUserInfoAPI, updateUserInfoAPI } from '@/api/users';
import { useTitle } from '@vueuse/core';
const $route = useRoute();
useTitle($route.meta.title);
// 用户名
const username = ref('');
// 获取用户信息
onMounted(async () => {
try {
const response = await getUserInfoAPI();
if (response.data) {
username.value = response.data.name;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
});
// 处理用户名修改
const handleUsernameChange = async () => {
if (!username.value) {
alert('请输入新的用户名');
return;
}
try {
const response = await updateUserInfoAPI({ name: username.value });
if (response.data) {
alert('用户名修改成功');
}
} catch (error) {
console.error('用户名修改失败:', error);
alert('用户名修改失败,请重试');
}
};
</script>