hookehuyr

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

新增了用户设置页面及其子页面,支持用户修改头像、用户名和密码。添加了相关API接口,并在路由中配置了对应的路径。同时引入了@heroicons/vue库以支持页面中的图标使用。
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
8 "name": "vue-vite", 8 "name": "vue-vite",
9 "version": "0.0.0", 9 "version": "0.0.0",
10 "dependencies": { 10 "dependencies": {
11 + "@heroicons/vue": "^2.2.0",
11 "@vant/use": "^1.6.0", 12 "@vant/use": "^1.6.0",
12 "dayjs": "^1.11.13", 13 "dayjs": "^1.11.13",
13 "swiper": "^11.2.6", 14 "swiper": "^11.2.6",
...@@ -824,6 +825,14 @@ ...@@ -824,6 +825,14 @@
824 "node": ">=18" 825 "node": ">=18"
825 } 826 }
826 }, 827 },
828 + "node_modules/@heroicons/vue": {
829 + "version": "2.2.0",
830 + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
831 + "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==",
832 + "peerDependencies": {
833 + "vue": ">= 3"
834 + }
835 + },
827 "node_modules/@isaacs/cliui": { 836 "node_modules/@isaacs/cliui": {
828 "version": "8.0.2", 837 "version": "8.0.2",
829 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 838 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
15 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar" 15 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar"
16 }, 16 },
17 "dependencies": { 17 "dependencies": {
18 + "@heroicons/vue": "^2.2.0",
18 "@vant/use": "^1.6.0", 19 "@vant/use": "^1.6.0",
19 "dayjs": "^1.11.13", 20 "dayjs": "^1.11.13",
20 "swiper": "^11.2.6", 21 "swiper": "^11.2.6",
......
...@@ -11,6 +11,9 @@ const Api = { ...@@ -11,6 +11,9 @@ const Api = {
11 USER_LOGIN: '/users/login', 11 USER_LOGIN: '/users/login',
12 USER_REGISTER: '/users/register', 12 USER_REGISTER: '/users/register',
13 USER_INFO: '/users/info', 13 USER_INFO: '/users/info',
14 + USER_UPDATE: '/users/update',
15 + USER_AVATAR: '/users/avatar',
16 + USER_PASSWORD: '/users/password',
14 } 17 }
15 18
16 /** 19 /**
...@@ -32,3 +35,22 @@ export const registerAPI = (params) => fn(fetch.post(Api.USER_REGISTER, params)) ...@@ -32,3 +35,22 @@ export const registerAPI = (params) => fn(fetch.post(Api.USER_REGISTER, params))
32 * @description: 获取用户信息 35 * @description: 获取用户信息
33 */ 36 */
34 export const getUserInfoAPI = () => fn(fetch.get(Api.USER_INFO)); 37 export const getUserInfoAPI = () => fn(fetch.get(Api.USER_INFO));
38 +
39 +/**
40 + * @description: 更新用户信息
41 + * @param: name 用户名称
42 + */
43 +export const updateUserInfoAPI = (params) => fn(fetch.put(Api.USER_UPDATE, params));
44 +
45 +/**
46 + * @description: 上传用户头像
47 + * @param: avatar 头像文件
48 + */
49 +export const uploadAvatarAPI = (formData) => fn(fetch.post(Api.USER_AVATAR, formData));
50 +
51 +/**
52 + * @description: 修改用户密码
53 + * @param: oldPassword 原密码
54 + * @param: newPassword 新密码
55 + */
56 +export const updatePasswordAPI = (params) => fn(fetch.put(Api.USER_PASSWORD, params));
......
...@@ -23,14 +23,21 @@ declare module 'vue' { ...@@ -23,14 +23,21 @@ declare module 'vue' {
23 SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] 23 SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
24 SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] 24 SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
25 TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] 25 TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
26 + VanButton: typeof import('vant/es')['Button']
27 + VanCell: typeof import('vant/es')['Cell']
28 + VanCellGroup: typeof import('vant/es')['CellGroup']
26 VanCheckbox: typeof import('vant/es')['Checkbox'] 29 VanCheckbox: typeof import('vant/es')['Checkbox']
27 VanDatePicker: typeof import('vant/es')['DatePicker'] 30 VanDatePicker: typeof import('vant/es')['DatePicker']
31 + VanField: typeof import('vant/es')['Field']
32 + VanForm: typeof import('vant/es')['Form']
28 VanIcon: typeof import('vant/es')['Icon'] 33 VanIcon: typeof import('vant/es')['Icon']
34 + VanImage: typeof import('vant/es')['Image']
29 VanList: typeof import('vant/es')['List'] 35 VanList: typeof import('vant/es')['List']
30 VanPickerGroup: typeof import('vant/es')['PickerGroup'] 36 VanPickerGroup: typeof import('vant/es')['PickerGroup']
31 VanPopup: typeof import('vant/es')['Popup'] 37 VanPopup: typeof import('vant/es')['Popup']
32 VanRate: typeof import('vant/es')['Rate'] 38 VanRate: typeof import('vant/es')['Rate']
33 VanTab: typeof import('vant/es')['Tab'] 39 VanTab: typeof import('vant/es')['Tab']
34 VanTabs: typeof import('vant/es')['Tabs'] 40 VanTabs: typeof import('vant/es')['Tabs']
41 + VanUploader: typeof import('vant/es')['Uploader']
35 } 42 }
36 } 43 }
......
...@@ -140,6 +140,30 @@ const routes = [ ...@@ -140,6 +140,30 @@ const routes = [
140 meta: { title: '消息详情' }, 140 meta: { title: '消息详情' },
141 }, 141 },
142 { 142 {
143 + path: '/profile/settings',
144 + name: 'Settings',
145 + component: () => import('../views/profile/SettingsPage.vue'),
146 + meta: { title: '设置' },
147 + },
148 + {
149 + path: '/profile/settings/avatar',
150 + name: 'AvatarSetting',
151 + component: () => import('../views/profile/settings/AvatarSettingPage.vue'),
152 + meta: { title: '修改头像' },
153 + },
154 + {
155 + path: '/profile/settings/username',
156 + name: 'UsernameSetting',
157 + component: () => import('../views/profile/settings/UsernameSettingPage.vue'),
158 + meta: { title: '修改用户名' },
159 + },
160 + {
161 + path: '/profile/settings/password',
162 + name: 'PasswordSetting',
163 + component: () => import('../views/profile/settings/PasswordSettingPage.vue'),
164 + meta: { title: '修改密码' },
165 + },
166 + {
143 path: '/test', 167 path: '/test',
144 name: 'test', 168 name: 'test',
145 component: () => import('../views/test.vue'), 169 component: () => import('../views/test.vue'),
......
...@@ -328,6 +328,11 @@ const menuItems3 = [ ...@@ -328,6 +328,11 @@ const menuItems3 = [
328 badge: "3", 328 badge: "3",
329 }, 329 },
330 { 330 {
331 + icon: "settings",
332 + title: "设置",
333 + path: "/profile/settings",
334 + },
335 + {
331 icon: "question", 336 icon: "question",
332 title: "帮助中心", 337 title: "帮助中心",
333 path: "/profile/help", 338 path: "/profile/help",
......
1 +<!--
2 + * @Date: 2025-03-24 13:04:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-24 13:22:18
5 + * @FilePath: /mlaj/src/views/profile/SettingsPage.vue
6 + * @Description: 用户设置页面
7 +-->
8 +<template>
9 + <AppLayout title="设置">
10 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
11 + <div class="px-4 py-6">
12 + <FrostedGlass class="rounded-xl overflow-hidden">
13 + <!-- 设置列表 -->
14 + <div class="divide-y divide-gray-100">
15 + <!-- 头像设置 -->
16 + <div class="p-4" @click="router.push('/profile/settings/avatar')">
17 + <div class="flex items-center justify-between">
18 + <div class="flex items-center">
19 + <!-- <div class="relative">
20 + <img
21 + :src="userAvatar || '/src/assets/images/avatar1.jpg'"
22 + alt="用户头像"
23 + class="w-16 h-16 rounded-full object-cover"
24 + />
25 + </div> -->
26 + <div class="ml-4">
27 + <h3 class="text-base font-medium text-gray-900">修改头像</h3>
28 + <p class="text-sm text-gray-500">支持 jpg、png 格式,大小不超过 2MB</p>
29 + </div>
30 + </div>
31 + <ChevronRightIcon class="w-5 h-5 text-gray-400" />
32 + </div>
33 + </div>
34 +
35 + <!-- 用户名设置 -->
36 + <div class="p-4" @click="router.push('/profile/settings/username')">
37 + <div class="flex items-center justify-between">
38 + <div>
39 + <h3 class="text-base font-medium text-gray-900">修改用户名</h3>
40 + <p class="text-sm text-gray-500">{{ username }}</p>
41 + </div>
42 + <ChevronRightIcon class="w-5 h-5 text-gray-400" />
43 + </div>
44 + </div>
45 +
46 + <!-- 密码设置 -->
47 + <div class="p-4" @click="router.push('/profile/settings/password')">
48 + <div class="flex items-center justify-between">
49 + <div>
50 + <h3 class="text-base font-medium text-gray-900">修改密码</h3>
51 + <p class="text-sm text-gray-500">定期修改密码更安全</p>
52 + </div>
53 + <ChevronRightIcon class="w-5 h-5 text-gray-400" />
54 + </div>
55 + </div>
56 + </div>
57 + </FrostedGlass>
58 + </div>
59 + </div>
60 + </AppLayout>
61 +</template>
62 +
63 +<script setup>
64 +import { ref, onMounted } from 'vue';
65 +import { useRoute, useRouter } from 'vue-router';
66 +import AppLayout from '@/components/layout/AppLayout.vue';
67 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
68 +import { ChevronRightIcon } from '@heroicons/vue/24/outline';
69 +import { useTitle } from '@vueuse/core';
70 +import { getUserInfoAPI } from '@/api/users';
71 +
72 +const $route = useRoute();
73 +const router = useRouter();
74 +useTitle($route.meta.title);
75 +
76 +// 用户头像
77 +const userAvatar = ref('');
78 +
79 +// 用户名
80 +const username = ref('');
81 +
82 +// 获取用户信息
83 +onMounted(async () => {
84 + try {
85 + const response = await getUserInfoAPI();
86 + if (response.data) {
87 + userAvatar.value = response.data.avatar;
88 + username.value = response.data.name;
89 + }
90 + } catch (error) {
91 + console.error('获取用户信息失败:', error);
92 + }
93 +});
94 +</script>
1 +<!--
2 + * @Date: 2025-03-24 13:04:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-24 13:32:25
5 + * @FilePath: /mlaj/src/views/profile/settings/AvatarSettingPage.vue
6 + * @Description: 修改头像页面
7 +-->
8 +<template>
9 + <AppLayout title="">
10 + <div
11 + class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"
12 + >
13 + <div class="px-4 py-6">
14 + <FrostedGlass class="rounded-xl overflow-hidden">
15 + <div class="p-4">
16 + <div class="flex flex-col items-center">
17 + <div class="mb-6">
18 + <van-image
19 + round
20 + :src="
21 + userAvatar || 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
22 + "
23 + alt="用户头像"
24 + class="rounded-full border-4 border-white shadow-lg"
25 + width="8rem"
26 + height="8rem"
27 + fit="cover"
28 + />
29 + </div>
30 + <p class="text-sm text-gray-500 mb-4">支持 jpg、png 格式,大小不超过 2MB</p>
31 + <van-uploader
32 + :after-read="handleAvatarChange"
33 + :max-size="2 * 1024 * 1024"
34 + accept="image/jpeg,image/png"
35 + :preview-image="false"
36 + >
37 + <van-button
38 + block
39 + type="primary"
40 + class="max-w-xs"
41 + >
42 + 选择新头像
43 + </van-button>
44 + </van-uploader>
45 + </div>
46 + </div>
47 + </FrostedGlass>
48 + </div>
49 + </div>
50 + </AppLayout>
51 +</template>
52 +
53 +<script setup>
54 +import { ref, onMounted } from "vue";
55 +import AppLayout from "@/components/layout/AppLayout.vue";
56 +import FrostedGlass from "@/components/ui/FrostedGlass.vue";
57 +import { getUserInfoAPI, uploadAvatarAPI } from "@/api/users";
58 +
59 +import { useTitle } from '@vueuse/core';
60 +
61 +const $route = useRoute();
62 +useTitle($route.meta.title);
63 +
64 +
65 +// 用户头像
66 +const userAvatar = ref("");
67 +
68 +// 获取用户信息
69 +onMounted(async () => {
70 + try {
71 + const response = await getUserInfoAPI();
72 + if (response.data) {
73 + userAvatar.value = response.data.avatar;
74 + }
75 + } catch (error) {
76 + console.error("获取用户信息失败:", error);
77 + }
78 +});
79 +
80 +// 处理头像上传
81 +const handleAvatarChange = async (file) => {
82 + try {
83 + const formData = new FormData();
84 + formData.append("avatar", file);
85 +
86 + const response = await uploadAvatarAPI(formData);
87 + if (response.data) {
88 + userAvatar.value = response.data.url;
89 + alert("头像上传成功");
90 + }
91 + } catch (error) {
92 + console.error("头像上传失败:", error);
93 + alert("头像上传失败,请重试");
94 + }
95 +};
96 +</script>
1 +<!--
2 + * @Date: 2025-03-24 13:04:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-24 13:19:54
5 + * @FilePath: /mlaj/src/views/profile/settings/PasswordSettingPage.vue
6 + * @Description: 修改密码页面
7 +-->
8 +<template>
9 + <AppLayout title="">
10 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
11 + <div class="px-4 py-6">
12 + <FrostedGlass class="rounded-xl overflow-hidden">
13 + <div class="p-4">
14 + <van-form @submit="handlePasswordChange">
15 + <van-cell-group inset>
16 + <van-field
17 + v-model="oldPassword"
18 + type="password"
19 + name="oldPassword"
20 + label="原密码"
21 + placeholder="请输入原密码"
22 + :rules="[{ required: true, message: '请输入原密码' }, { min: 6, message: '密码长度至少6位' }]"
23 + />
24 + <van-field
25 + v-model="newPassword"
26 + type="password"
27 + name="newPassword"
28 + label="新密码"
29 + placeholder="请输入新密码"
30 + :rules="[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码长度至少6位' }]"
31 + />
32 + <van-field
33 + v-model="confirmPassword"
34 + type="password"
35 + name="confirmPassword"
36 + label="确认新密码"
37 + placeholder="请确认新密码"
38 + :rules="[{ required: true, message: '请确认新密码' }, { validator: validateConfirmPassword, message: '两次输入的密码不一致' }]"
39 + />
40 + </van-cell-group>
41 + <div style="margin: 16px;">
42 + <van-button
43 + native-type="submit"
44 + type="primary"
45 + block
46 + round
47 + :loading="loading"
48 + class="bg-green-500 hover:bg-green-600 transition-colors"
49 + >
50 + 保存修改
51 + </van-button>
52 + </div>
53 + </van-form>
54 + </div>
55 + </FrostedGlass>
56 + </div>
57 + </div>
58 + </AppLayout>
59 +</template>
60 +
61 +<script setup>
62 +import { ref } from 'vue';
63 +import AppLayout from '@/components/layout/AppLayout.vue';
64 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
65 +import { updatePasswordAPI } from '@/api/users';
66 +
67 +import { useTitle } from '@vueuse/core';
68 +
69 +const $route = useRoute();
70 +useTitle($route.meta.title);
71 +
72 +// 密码
73 +const oldPassword = ref('');
74 +const newPassword = ref('');
75 +const confirmPassword = ref('');
76 +const loading = ref(false);
77 +
78 +// 确认密码验证
79 +const validateConfirmPassword = (val) => val === newPassword.value;
80 +
81 +// 处理密码修改
82 +const handlePasswordChange = async () => {
83 + loading.value = true;
84 + try {
85 + const response = await updatePasswordAPI({
86 + oldPassword: oldPassword.value,
87 + newPassword: newPassword.value
88 + });
89 +
90 + if (response.data) {
91 + // 清空输入框
92 + oldPassword.value = '';
93 + newPassword.value = '';
94 + confirmPassword.value = '';
95 + showSuccessToast('密码修改成功');
96 + }
97 + } catch (error) {
98 + console.error('密码修改失败:', error);
99 + showFailToast('密码修改失败,请重试');
100 + } finally {
101 + loading.value = false;
102 + }
103 +};
104 +</script>
1 +<!--
2 + * @Date: 2025-03-24 13:04:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-24 13:33:27
5 + * @FilePath: /mlaj/src/views/profile/settings/UsernameSettingPage.vue
6 + * @Description: 修改用户名页面
7 +-->
8 +<template>
9 + <AppLayout title="">
10 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
11 + <div class="px-4 py-6">
12 + <FrostedGlass class="rounded-xl overflow-hidden">
13 + <div class="p-4">
14 + <div class="space-y-4">
15 + <div class="mb-6">
16 + <label for="username" class="block text-sm font-medium text-gray-700 mb-2">新用户名</label>
17 + <input
18 + v-model="username"
19 + type="text"
20 + id="username"
21 + placeholder="请输入新的用户名"
22 + 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"
23 + />
24 + </div>
25 + <van-button
26 + @click="handleUsernameChange"
27 + type="primary"
28 + block
29 + round
30 + class="bg-green-500 hover:bg-green-600 transition-colors"
31 + >
32 + 保存修改
33 + </van-button>
34 + </div>
35 + </div>
36 + </FrostedGlass>
37 + </div>
38 + </div>
39 + </AppLayout>
40 +</template>
41 +
42 +<script setup>
43 +import { ref, onMounted } from 'vue';
44 +import AppLayout from '@/components/layout/AppLayout.vue';
45 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
46 +import { getUserInfoAPI, updateUserInfoAPI } from '@/api/users';
47 +
48 +import { useTitle } from '@vueuse/core';
49 +
50 +const $route = useRoute();
51 +useTitle($route.meta.title);
52 +
53 +// 用户名
54 +const username = ref('');
55 +
56 +// 获取用户信息
57 +onMounted(async () => {
58 + try {
59 + const response = await getUserInfoAPI();
60 + if (response.data) {
61 + username.value = response.data.name;
62 + }
63 + } catch (error) {
64 + console.error('获取用户信息失败:', error);
65 + }
66 +});
67 +
68 +// 处理用户名修改
69 +const handleUsernameChange = async () => {
70 + if (!username.value) {
71 + alert('请输入新的用户名');
72 + return;
73 + }
74 +
75 + try {
76 + const response = await updateUserInfoAPI({ name: username.value });
77 + if (response.data) {
78 + alert('用户名修改成功');
79 + }
80 + } catch (error) {
81 + console.error('用户名修改失败:', error);
82 + alert('用户名修改失败,请重试');
83 + }
84 +};
85 +</script>