From 6a5cf75b72af2595b0d3d1eccf0b8ccca7cbd9e9 Mon Sep 17 00:00:00 2001 From: 2991692032 Date: Mon, 5 May 2025 15:39:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Front/.idea/.gitignore | 8 + Front/.idea/Front.iml | 12 + Front/.idea/modules.xml | 8 + Front/.idea/vcs.xml | 6 + Front/vue-unilife/index.html | 8 +- Front/vue-unilife/package-lock.json | 22 + Front/vue-unilife/package.json | 1 + Front/vue-unilife/src/App.vue | 29 +- Front/vue-unilife/src/api/index.ts | 5 + Front/vue-unilife/src/api/request.ts | 90 ++ Front/vue-unilife/src/api/user.ts | 119 +++ .../src/components/GlobalLoading.vue | 76 ++ Front/vue-unilife/src/components/LogPage.vue | 553 ---------- .../src/components/PersonLayout.vue | 24 - .../src/components/Personal/AcountManager.vue | 943 ------------------ .../src/components/Personal/Home.vue | 21 - .../src/components/Personal/Personal.vue | 235 ----- .../src/components/useEmailCode.ts | 27 - Front/vue-unilife/src/hooks/useEmailCode.ts | 79 ++ Front/vue-unilife/src/layouts/BaseLayout.vue | 27 + .../src/layouts/PersonalLayout.vue | 263 +++++ Front/vue-unilife/src/main.ts | 36 +- Front/vue-unilife/src/router/index.ts | 129 +++ Front/vue-unilife/src/routers.ts | 44 - Front/vue-unilife/src/stores/index.ts | 7 + Front/vue-unilife/src/stores/ui.ts | 66 ++ Front/vue-unilife/src/stores/user.ts | 187 ++++ Front/vue-unilife/src/style.css | 79 -- Front/vue-unilife/src/styles/global.css | 151 +++ Front/vue-unilife/src/styles/reset.css | 44 + Front/vue-unilife/src/styles/variables.css | 57 ++ Front/vue-unilife/src/utils/request.ts | 37 - .../vue-unilife/src/views/AccountManager.vue | 733 ++++++++++++++ Front/vue-unilife/src/views/Home.vue | 352 +++++++ Front/vue-unilife/src/views/Login.vue | 502 ++++++++++ Front/vue-unilife/src/views/NotFound.vue | 101 ++ .../unilife/controller/UserController.java | 2 +- .../unilife/interceptor/JwtInterceptor.java | 4 + .../com/unilife/model/dto/RegisterDTO.java | 1 + .../unilife/service/impl/UserServiceImpl.java | 88 +- 40 files changed, 3177 insertions(+), 1999 deletions(-) create mode 100644 Front/.idea/.gitignore create mode 100644 Front/.idea/Front.iml create mode 100644 Front/.idea/modules.xml create mode 100644 Front/.idea/vcs.xml create mode 100644 Front/vue-unilife/src/api/index.ts create mode 100644 Front/vue-unilife/src/api/request.ts create mode 100644 Front/vue-unilife/src/api/user.ts create mode 100644 Front/vue-unilife/src/components/GlobalLoading.vue delete mode 100644 Front/vue-unilife/src/components/LogPage.vue delete mode 100644 Front/vue-unilife/src/components/PersonLayout.vue delete mode 100644 Front/vue-unilife/src/components/Personal/AcountManager.vue delete mode 100644 Front/vue-unilife/src/components/Personal/Home.vue delete mode 100644 Front/vue-unilife/src/components/Personal/Personal.vue delete mode 100644 Front/vue-unilife/src/components/useEmailCode.ts create mode 100644 Front/vue-unilife/src/hooks/useEmailCode.ts create mode 100644 Front/vue-unilife/src/layouts/BaseLayout.vue create mode 100644 Front/vue-unilife/src/layouts/PersonalLayout.vue create mode 100644 Front/vue-unilife/src/router/index.ts delete mode 100644 Front/vue-unilife/src/routers.ts create mode 100644 Front/vue-unilife/src/stores/index.ts create mode 100644 Front/vue-unilife/src/stores/ui.ts create mode 100644 Front/vue-unilife/src/stores/user.ts delete mode 100644 Front/vue-unilife/src/style.css create mode 100644 Front/vue-unilife/src/styles/global.css create mode 100644 Front/vue-unilife/src/styles/reset.css create mode 100644 Front/vue-unilife/src/styles/variables.css delete mode 100644 Front/vue-unilife/src/utils/request.ts create mode 100644 Front/vue-unilife/src/views/AccountManager.vue create mode 100644 Front/vue-unilife/src/views/Home.vue create mode 100644 Front/vue-unilife/src/views/Login.vue create mode 100644 Front/vue-unilife/src/views/NotFound.vue diff --git a/Front/.idea/.gitignore b/Front/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/Front/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Front/.idea/Front.iml b/Front/.idea/Front.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/Front/.idea/Front.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Front/.idea/modules.xml b/Front/.idea/modules.xml new file mode 100644 index 0000000..213ab34 --- /dev/null +++ b/Front/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Front/.idea/vcs.xml b/Front/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Front/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Front/vue-unilife/index.html b/Front/vue-unilife/index.html index dde16aa..3e88473 100644 --- a/Front/vue-unilife/index.html +++ b/Front/vue-unilife/index.html @@ -1,10 +1,12 @@ - + - + - Vite + Vue + TS + + + UniLife学生论坛
diff --git a/Front/vue-unilife/package-lock.json b/Front/vue-unilife/package-lock.json index 0dcd290..7b77466 100644 --- a/Front/vue-unilife/package-lock.json +++ b/Front/vue-unilife/package-lock.json @@ -12,6 +12,7 @@ "@vue/shared": "^3.5.13", "axios": "^1.8.3", "element-plus": "^2.9.7", + "pinia": "^3.0.2", "vee-validate": "^4.15.0", "vue": "^3.5.13", "vue-router": "^4.5.0", @@ -964,6 +965,27 @@ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "license": "BSD-3-Clause" }, + "node_modules/pinia": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.2.tgz", + "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/property-expr/-/property-expr-2.0.6.tgz", diff --git a/Front/vue-unilife/package.json b/Front/vue-unilife/package.json index b53d3a7..bd9b04f 100644 --- a/Front/vue-unilife/package.json +++ b/Front/vue-unilife/package.json @@ -13,6 +13,7 @@ "@vue/shared": "^3.5.13", "axios": "^1.8.3", "element-plus": "^2.9.7", + "pinia": "^3.0.2", "vee-validate": "^4.15.0", "vue": "^3.5.13", "vue-router": "^4.5.0", diff --git a/Front/vue-unilife/src/App.vue b/Front/vue-unilife/src/App.vue index 4d17c70..dae3499 100644 --- a/Front/vue-unilife/src/App.vue +++ b/Front/vue-unilife/src/App.vue @@ -1,11 +1,34 @@ - diff --git a/Front/vue-unilife/src/api/index.ts b/Front/vue-unilife/src/api/index.ts new file mode 100644 index 0000000..82f5fa6 --- /dev/null +++ b/Front/vue-unilife/src/api/index.ts @@ -0,0 +1,5 @@ +import userApi from './user'; + +export { + userApi +}; diff --git a/Front/vue-unilife/src/api/request.ts b/Front/vue-unilife/src/api/request.ts new file mode 100644 index 0000000..5be10b1 --- /dev/null +++ b/Front/vue-unilife/src/api/request.ts @@ -0,0 +1,90 @@ +import axios from 'axios'; +import type { AxiosResponse } from 'axios'; +import { ElMessage } from 'element-plus'; + +// 创建axios实例 +const service = axios.create({ + baseURL: 'http://localhost:8087', + timeout: 10000 +}); + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + // 从localStorage获取token + const token = localStorage.getItem('token'); + + // 如果存在token,则添加到请求头 + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + console.log("请求已附加token"); + } + + return config; + }, + (error) => { + console.error('请求错误:', error); + return Promise.reject(error); + } +); + +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + const res = response.data; + + // 如果接口返回的状态码不是200,则判断为错误 + if (res.code !== 200) { + ElMessage({ + message: res.message || '请求失败', + type: 'error', + duration: 5 * 1000 + }); + + // 处理特定错误码 + if (res.code === 401) { + // 未授权,清除token并重定向到登录页 + localStorage.removeItem('token'); + window.location.href = '/login'; + } + + return Promise.reject(new Error(res.message || '请求失败')); + } else { + return res; + } + }, + (error) => { + console.error('响应错误:', error); + + // 显示错误信息 + ElMessage({ + message: error.message || '请求失败', + type: 'error', + duration: 5 * 1000 + }); + + return Promise.reject(error); + } +); + +// 封装GET请求 +export function get(url: string, params?: any): Promise { + return service.get(url, { params }); +} + +// 封装POST请求 +export function post(url: string, data?: any): Promise { + return service.post(url, data); +} + +// 封装PUT请求 +export function put(url: string, data?: any): Promise { + return service.put(url, data); +} + +// 封装DELETE请求 +export function del(url: string, params?: any): Promise { + return service.delete(url, { params }); +} + +export default service; diff --git a/Front/vue-unilife/src/api/user.ts b/Front/vue-unilife/src/api/user.ts new file mode 100644 index 0000000..e2f0ec0 --- /dev/null +++ b/Front/vue-unilife/src/api/user.ts @@ -0,0 +1,119 @@ +import { get, post, put } from './request'; + +// 用户接口类型定义 +export interface UserInfo { + username: string; + email: string; + avatar?: string; + gender?: number; + bio?: string; + birthday?: string; + studentId?: string; + department?: string; + major?: string; + grade?: string; +} + +export interface LoginParams { + email: string; + password: string; +} + +export interface RegisterParams { + email: string; + password: string; + username?: string; + nickname?: string; + studentId?: string; + department?: string; + major?: string; + grade?: string; + code: string; +} + +export interface EmailCodeParams { + email: string; +} + +export interface VerifyCodeParams { + email: string; + code: string; +} + +export interface UpdateProfileParams { + username?: string; + bio?: string; + gender?: number; + birthday?: string; +} + +export interface UpdatePasswordParams { + code: string; + newPassword: string; +} + +// 用户API +export default { + // 登录 + login(data: LoginParams) { + return post<{code: number; data: {token: string}}>( + '/users/login', + data + ); + }, + + // 注册 + register(data: RegisterParams) { + return post<{code: number; data: {token: string}}>( + '/users/register', + data + ); + }, + + // 获取邮箱验证码 + getEmailCode(data: EmailCodeParams) { + return post<{code: number; message: string}>( + '/users/code', + data + ); + }, + + // 验证邮箱验证码 + verifyEmailCode(data: VerifyCodeParams) { + return post<{code: number; data: {token: string}}>( + '/users/login/code', + data + ); + }, + + // 获取用户信息 + getUserInfo() { + return get<{code: number; data: UserInfo}>( + '/users/info' + ); + }, + + // 更新用户资料 + updateProfile(data: UpdateProfileParams) { + return put<{code: number; message: string}>( + '/users/profile', + data + ); + }, + + // 更新用户密码 + updatePassword(data: UpdatePasswordParams) { + return put<{code: number; message: string}>( + '/users/password', + data + ); + }, + + // 上传头像 + uploadAvatar(formData: FormData) { + return post<{code: number; data: {avatarUrl: string}}>( + '/users/avatar', + formData + ); + } +}; diff --git a/Front/vue-unilife/src/components/GlobalLoading.vue b/Front/vue-unilife/src/components/GlobalLoading.vue new file mode 100644 index 0000000..6529a18 --- /dev/null +++ b/Front/vue-unilife/src/components/GlobalLoading.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/Front/vue-unilife/src/components/LogPage.vue b/Front/vue-unilife/src/components/LogPage.vue deleted file mode 100644 index 3d68dce..0000000 --- a/Front/vue-unilife/src/components/LogPage.vue +++ /dev/null @@ -1,553 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Front/vue-unilife/src/components/PersonLayout.vue b/Front/vue-unilife/src/components/PersonLayout.vue deleted file mode 100644 index 5e6c053..0000000 --- a/Front/vue-unilife/src/components/PersonLayout.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Front/vue-unilife/src/components/Personal/AcountManager.vue b/Front/vue-unilife/src/components/Personal/AcountManager.vue deleted file mode 100644 index 4af8e1b..0000000 --- a/Front/vue-unilife/src/components/Personal/AcountManager.vue +++ /dev/null @@ -1,943 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Front/vue-unilife/src/components/Personal/Home.vue b/Front/vue-unilife/src/components/Personal/Home.vue deleted file mode 100644 index 170b5e6..0000000 --- a/Front/vue-unilife/src/components/Personal/Home.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Front/vue-unilife/src/components/Personal/Personal.vue b/Front/vue-unilife/src/components/Personal/Personal.vue deleted file mode 100644 index 2769359..0000000 --- a/Front/vue-unilife/src/components/Personal/Personal.vue +++ /dev/null @@ -1,235 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Front/vue-unilife/src/components/useEmailCode.ts b/Front/vue-unilife/src/components/useEmailCode.ts deleted file mode 100644 index d2aef94..0000000 --- a/Front/vue-unilife/src/components/useEmailCode.ts +++ /dev/null @@ -1,27 +0,0 @@ -import request from "../../src/utils/request" - - -export function useEmailCode(){ - const sendEmailCode = async(email:string) => - { - return await request.post('/user/code', - { - params:{email:email} - } - ) - } - - const verifyEmailCode = async(email:string,code:string) => - { - return await request.post('users/login/code', - { - params:{email:email,code:code} - } - ) - } - - return{ - sendEmailCode, - verifyEmailCode - } -} diff --git a/Front/vue-unilife/src/hooks/useEmailCode.ts b/Front/vue-unilife/src/hooks/useEmailCode.ts new file mode 100644 index 0000000..7eddca8 --- /dev/null +++ b/Front/vue-unilife/src/hooks/useEmailCode.ts @@ -0,0 +1,79 @@ +import { ref } from 'vue'; +import { userApi } from '../api'; +import { ElMessage } from 'element-plus'; + +export function useEmailCode() { + const isSending = ref(false); + const countdown = ref(0); + let timer: number | null = null; + + // 发送邮箱验证码 + const sendEmailCode = async (email: string) => { + if (isSending.value) return; + + if (!email) { + ElMessage.warning('请输入邮箱地址'); + return; + } + + try { + isSending.value = true; + const res = await userApi.getEmailCode({ email }); + + if (res.code === 200) { + ElMessage.success('验证码已发送,请查收邮件'); + startCountdown(); + } + + return res; + } catch (error) { + console.error('发送验证码失败:', error); + ElMessage.error('发送验证码失败,请稍后重试'); + } finally { + isSending.value = false; + } + }; + + // 验证邮箱验证码 + const verifyEmailCode = async (email: string, code: string) => { + if (!email || !code) { + ElMessage.warning('请输入邮箱和验证码'); + return; + } + + try { + const res = await userApi.verifyEmailCode({ email, code }); + return res; + } catch (error) { + console.error('验证码验证失败:', error); + throw error; + } + }; + + // 开始倒计时 + const startCountdown = () => { + countdown.value = 60; + + if (timer) { + clearInterval(timer); + } + + timer = window.setInterval(() => { + if (countdown.value > 0) { + countdown.value--; + } else { + if (timer) { + clearInterval(timer); + timer = null; + } + } + }, 1000); + }; + + return { + isSending, + countdown, + sendEmailCode, + verifyEmailCode + }; +} diff --git a/Front/vue-unilife/src/layouts/BaseLayout.vue b/Front/vue-unilife/src/layouts/BaseLayout.vue new file mode 100644 index 0000000..37ecbfb --- /dev/null +++ b/Front/vue-unilife/src/layouts/BaseLayout.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/Front/vue-unilife/src/layouts/PersonalLayout.vue b/Front/vue-unilife/src/layouts/PersonalLayout.vue new file mode 100644 index 0000000..de2e9ec --- /dev/null +++ b/Front/vue-unilife/src/layouts/PersonalLayout.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/Front/vue-unilife/src/main.ts b/Front/vue-unilife/src/main.ts index f030057..830a08a 100644 --- a/Front/vue-unilife/src/main.ts +++ b/Front/vue-unilife/src/main.ts @@ -1,16 +1,26 @@ -import { createApp } from 'vue' -import './style.css' -import App from './App.vue' -import ElementPlus from 'element-plus' -import 'element-plus/dist/index.css' -import router from './routers' -import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import * as ElementPlusIconsVue from '@element-plus/icons-vue'; +import App from './App.vue'; +import router from './router'; -const app = createApp(App) +// 样式 +import 'element-plus/dist/index.css'; +import './styles/global.css'; -app.use(ElementPlus) -app.use(router) -for(const [key, component] of Object.entries(ElementPlusIconsVue)) { - app.component(key, component) +// 创建应用实例 +const app = createApp(App); + +// 使用插件 +app.use(createPinia()); +app.use(ElementPlus); +app.use(router); + +// 注册所有Element Plus图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); } -app.mount('#app') + +// 挂载应用 +app.mount('#app'); diff --git a/Front/vue-unilife/src/router/index.ts b/Front/vue-unilife/src/router/index.ts new file mode 100644 index 0000000..d64bed5 --- /dev/null +++ b/Front/vue-unilife/src/router/index.ts @@ -0,0 +1,129 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import type { RouteRecordRaw } from 'vue-router'; +import { useUserStore } from '../stores'; + +// 布局 +import BaseLayout from '../layouts/BaseLayout.vue'; +import PersonalLayout from '../layouts/PersonalLayout.vue'; + +// 页面 +import Login from '../views/Login.vue'; +import Home from '../views/Home.vue'; +import AccountManager from '../views/AccountManager.vue'; +import NotFound from '../views/NotFound.vue'; + +// 路由配置 +const routes: Array = [ + { + path: '/', + component: BaseLayout, + children: [ + { + path: '', + redirect: '/login' + }, + { + path: 'login', + name: 'Login', + component: Login, + meta: { + title: '登录 - UniLife学生论坛', + requiresAuth: false + } + } + ] + }, + { + path: '/personal', + component: PersonalLayout, + meta: { + requiresAuth: true + }, + children: [ + { + path: '', + name: 'Home', + component: Home, + meta: { + title: '个人主页 - UniLife学生论坛', + requiresAuth: true + } + }, + { + path: 'account', + name: 'AccountManager', + component: AccountManager, + meta: { + title: '账号管理 - UniLife学生论坛', + requiresAuth: true + } + }, + // 其他个人中心页面可以在这里添加 + { + path: 'posts', + name: 'Posts', + component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面 + meta: { + title: '我的帖子 - UniLife学生论坛', + requiresAuth: true + } + }, + { + path: 'messages', + name: 'Messages', + component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面 + meta: { + title: '消息中心 - UniLife学生论坛', + requiresAuth: true + } + }, + { + path: 'settings', + name: 'Settings', + component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面 + meta: { + title: '设置 - UniLife学生论坛', + requiresAuth: true + } + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFound, + meta: { + title: '页面不存在 - UniLife学生论坛' + } + } +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}); + +// 全局前置守卫 +router.beforeEach((to, from, next) => { + // 设置页面标题 + document.title = to.meta.title as string || 'UniLife学生论坛'; + + // 检查是否需要登录权限 + if (to.matched.some(record => record.meta.requiresAuth)) { + const userStore = useUserStore(); + + // 如果需要登录但用户未登录,重定向到登录页 + if (!userStore.isLoggedIn) { + next({ + path: '/login', + query: { redirect: to.fullPath } + }); + } else { + next(); + } + } else { + next(); + } +}); + +export default router; diff --git a/Front/vue-unilife/src/routers.ts b/Front/vue-unilife/src/routers.ts deleted file mode 100644 index f2ba186..0000000 --- a/Front/vue-unilife/src/routers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { RouteRecord, RouteRecordRaw } from 'vue-router'; -import { createWebHashHistory, createRouter,createWebHistory } from 'vue-router'; -import LogPage from './components/LogPage.vue'; -import Personal from './components/Personal/Personal.vue' -import Manager from './components/Personal/AcountManager.vue'; -import PersonalLayout from './components/PersonLayout.vue' -import PersonalHome from './components/Personal/Home.vue' - -const routes:Array = [ - { - path:'/log', - name: 'LogPage', - component: LogPage - }, - { - path:'/personal', - name: 'Personal', - component: Personal, - children: [ - { - path:'', - name:'Home', - component:PersonalHome, - }, - { - path:'manager', - name: 'Manager', - component:Manager, - }, - ] - }, - { - path:"/personalLayout", - name:'Personallayout', - component:PersonalLayout, - } -]; - -const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes -}); - -export default router; diff --git a/Front/vue-unilife/src/stores/index.ts b/Front/vue-unilife/src/stores/index.ts new file mode 100644 index 0000000..0686601 --- /dev/null +++ b/Front/vue-unilife/src/stores/index.ts @@ -0,0 +1,7 @@ +import { useUserStore } from './user'; +import { useUIStore } from './ui'; + +export { + useUserStore, + useUIStore +}; diff --git a/Front/vue-unilife/src/stores/ui.ts b/Front/vue-unilife/src/stores/ui.ts new file mode 100644 index 0000000..b8b1c9a --- /dev/null +++ b/Front/vue-unilife/src/stores/ui.ts @@ -0,0 +1,66 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const useUIStore = defineStore('ui', () => { + // 状态 + const isMobileView = ref(false); + const isSidebarCollapsed = ref(false); + const isDarkMode = ref(false); + const isLoading = ref(false); + const loadingText = ref('加载中...'); + + // 检测是否为移动视图 + const checkMobileView = () => { + isMobileView.value = window.innerWidth < 768; + }; + + // 切换侧边栏状态 + const toggleSidebar = () => { + isSidebarCollapsed.value = !isSidebarCollapsed.value; + }; + + // 切换暗黑模式 + const toggleDarkMode = () => { + isDarkMode.value = !isDarkMode.value; + + // 应用暗黑模式到文档 + if (isDarkMode.value) { + document.documentElement.classList.add('dark-mode'); + } else { + document.documentElement.classList.remove('dark-mode'); + } + }; + + // 设置加载状态 + const setLoading = (loading: boolean, text?: string) => { + isLoading.value = loading; + if (text) { + loadingText.value = text; + } + }; + + // 初始化 + const initialize = () => { + // 检测移动视图 + checkMobileView(); + window.addEventListener('resize', checkMobileView); + + // 检测系统暗黑模式偏好 + const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDarkMode) { + toggleDarkMode(); + } + }; + + return { + isMobileView, + isSidebarCollapsed, + isDarkMode, + isLoading, + loadingText, + toggleSidebar, + toggleDarkMode, + setLoading, + initialize + }; +}); diff --git a/Front/vue-unilife/src/stores/user.ts b/Front/vue-unilife/src/stores/user.ts new file mode 100644 index 0000000..283b22e --- /dev/null +++ b/Front/vue-unilife/src/stores/user.ts @@ -0,0 +1,187 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import userApi from '../api/user'; +import type { UserInfo } from '../api/user'; +import { ElMessage } from 'element-plus'; + +export const useUserStore = defineStore('user', () => { + // 状态 + const token = ref(localStorage.getItem('token') || ''); + const userInfo = ref(null); + const isLoggedIn = ref(!!token.value); + const loading = ref(false); + + // 设置token + const setToken = (newToken: string) => { + token.value = newToken; + localStorage.setItem('token', newToken); + isLoggedIn.value = true; + }; + + // 清除token + const clearToken = () => { + token.value = ''; + localStorage.removeItem('token'); + isLoggedIn.value = false; + }; + + // 登录 + const login = async (email: string, password: string) => { + try { + loading.value = true; + const res = await userApi.login({ email, password }); + + if (res.code === 200 && res.data.token) { + setToken(res.data.token); + ElMessage.success('登录成功'); + return true; + } + + return false; + } catch (error) { + console.error('登录失败:', error); + return false; + } finally { + loading.value = false; + } + }; + + // 通过验证码登录 + const loginWithCode = async (email: string, code: string) => { + try { + loading.value = true; + const res = await userApi.verifyEmailCode({ email, code }); + + if (res.code === 200 && res.data.token) { + setToken(res.data.token); + ElMessage.success('登录成功'); + return true; + } + + return false; + } catch (error) { + console.error('登录失败:', error); + return false; + } finally { + loading.value = false; + } + }; + + // 注册 + const register = async (email: string, password: string, code: string) => { + try { + loading.value = true; + + const res = await userApi.register({ email, password, code }); + + if (res.code === 200 && res.data.token) { + setToken(res.data.token); + ElMessage.success('注册成功'); + return true; + } + + return false; + } catch (error) { + console.error('注册失败:', error); + return false; + } finally { + loading.value = false; + } + }; + + // 登出 + const logout = () => { + clearToken(); + userInfo.value = null; + ElMessage.success('已退出登录'); + }; + + // 获取用户信息 + const fetchUserInfo = async () => { + if (!token.value) return null; + + try { + loading.value = true; + const res = await userApi.getUserInfo(); + + if (res.code === 200) { + userInfo.value = res.data; + return res.data; + } + + return null; + } catch (error) { + console.error('获取用户信息失败:', error); + return null; + } finally { + loading.value = false; + } + }; + + // 更新用户资料 + const updateProfile = async (data: { + username?: string; + bio?: string; + gender?: number; + birthday?: string; + }) => { + try { + loading.value = true; + const res = await userApi.updateProfile(data); + + if (res.code === 200) { + // 更新本地用户信息 + if (userInfo.value) { + userInfo.value = { + ...userInfo.value, + ...data + }; + } + + ElMessage.success('个人资料更新成功'); + return true; + } + + return false; + } catch (error) { + console.error('更新个人资料失败:', error); + return false; + } finally { + loading.value = false; + } + }; + + // 更新密码 + const updatePassword = async (code: string, newPassword: string) => { + try { + loading.value = true; + const res = await userApi.updatePassword({ code, newPassword }); + + if (res.code === 200) { + ElMessage.success('密码修改成功'); + return true; + } + + return false; + } catch (error) { + console.error('修改密码失败:', error); + return false; + } finally { + loading.value = false; + } + }; + + return { + token, + userInfo, + isLoggedIn, + loading, + login, + loginWithCode, + register, + logout, + fetchUserInfo, + updateProfile, + updatePassword + }; +}); diff --git a/Front/vue-unilife/src/style.css b/Front/vue-unilife/src/style.css deleted file mode 100644 index f691315..0000000 --- a/Front/vue-unilife/src/style.css +++ /dev/null @@ -1,79 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/Front/vue-unilife/src/styles/global.css b/Front/vue-unilife/src/styles/global.css new file mode 100644 index 0000000..2c9eb7e --- /dev/null +++ b/Front/vue-unilife/src/styles/global.css @@ -0,0 +1,151 @@ +@import './variables.css'; +@import './reset.css'; + +/* 全局样式 */ +body { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + color: var(--text-primary); + background-color: var(--bg-primary); + min-height: 100vh; + margin: 0; + padding: 0; +} + +#app { + width: 100%; + height: 100vh; + margin: 0; + padding: 0; +} + +/* 通用容器 */ +.container { + width: 100%; + max-width: var(--content-max-width); + margin: 0 auto; + padding: var(--spacing-lg); +} + +/* 卡片样式 */ +.card { + background-color: var(--bg-primary); + border-radius: var(--border-radius-lg); + padding: var(--spacing-xl); + box-shadow: var(--shadow-md); + transition: transform var(--transition-normal), box-shadow var(--transition-normal); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +/* 按钮样式 */ +.btn { + border: none; + border-radius: var(--border-radius-md); + padding: var(--spacing-sm) var(--spacing-lg); + font-size: var(--font-size-md); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-normal); +} + +.btn-primary { + background-color: var(--primary-color); + color: white; + box-shadow: 0 4px 10px rgba(147, 112, 219, 0.3); +} + +.btn-primary:hover { + background-color: var(--primary-dark); + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(147, 112, 219, 0.4); +} + +.btn-secondary { + background-color: var(--secondary-color); + color: var(--text-secondary); + box-shadow: 0 4px 10px rgba(230, 230, 250, 0.3); +} + +.btn-secondary:hover { + background-color: #dcdcdc; + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(230, 230, 250, 0.4); +} + +/* 表单样式 */ +.form-group { + margin-bottom: var(--spacing-md); + display: flex; + align-items: center; +} + +.form-label { + width: 100px; + font-size: var(--font-size-lg); + color: var(--text-secondary); +} + +.form-input { + flex: 1; + padding: var(--spacing-sm) var(--spacing-md); + border: 2px solid var(--border-color); + border-radius: var(--border-radius-md); + outline: none; + transition: border-color var(--transition-normal); + font-size: var(--font-size-md); +} + +.form-input:focus { + border-color: var(--primary-light); +} + +/* 响应式设计 */ +@media (max-width: 1024px) { + .container { + padding: var(--spacing-md); + } +} + +@media (max-width: 768px) { + .form-group { + flex-direction: column; + align-items: flex-start; + } + + .form-label { + width: 100%; + margin-bottom: var(--spacing-xs); + } +} + +/* 动画 */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.fade-in { + animation: fadeIn var(--transition-normal); +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +.float { + animation: float 5s ease-in-out infinite; +} diff --git a/Front/vue-unilife/src/styles/reset.css b/Front/vue-unilife/src/styles/reset.css new file mode 100644 index 0000000..f5b87d4 --- /dev/null +++ b/Front/vue-unilife/src/styles/reset.css @@ -0,0 +1,44 @@ +/* CSS Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; +} + +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +input, button, textarea, select { + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +a { + text-decoration: none; + color: inherit; +} + +ul, ol { + list-style: none; +} + +button { + background: none; + border: none; + cursor: pointer; +} diff --git a/Front/vue-unilife/src/styles/variables.css b/Front/vue-unilife/src/styles/variables.css new file mode 100644 index 0000000..551d803 --- /dev/null +++ b/Front/vue-unilife/src/styles/variables.css @@ -0,0 +1,57 @@ +:root { + /* 主题颜色 */ + --primary-color: #9370DB; + --primary-light: #b19cd9; + --primary-dark: #8a63d2; + --secondary-color: #e6e6fa; + + /* 文本颜色 */ + --text-primary: #333333; + --text-secondary: #666666; + --text-light: #999999; + + /* 背景颜色 */ + --bg-primary: #ffffff; + --bg-secondary: #f9f7ff; + --bg-gradient: linear-gradient(200deg, #f3e7e9, #e3eeff); + + /* 边框颜色 */ + --border-color: #e6e6fa; + + /* 阴影 */ + --shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.05); + --shadow-md: 0 5px 15px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.1); + + /* 圆角 */ + --border-radius-sm: 5px; + --border-radius-md: 10px; + --border-radius-lg: 20px; + --border-radius-full: 50%; + + /* 间距 */ + --spacing-xs: 5px; + --spacing-sm: 10px; + --spacing-md: 15px; + --spacing-lg: 20px; + --spacing-xl: 30px; + + /* 字体大小 */ + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.25rem; + --font-size-xl: 1.5rem; + --font-size-xxl: 2rem; + + /* 过渡 */ + --transition-fast: 0.2s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; + + /* 布局 */ + --sidebar-width: 84px; + --sidebar-width-expanded: 300px; + --header-height: 60px; + --content-max-width: 1280px; +} diff --git a/Front/vue-unilife/src/utils/request.ts b/Front/vue-unilife/src/utils/request.ts deleted file mode 100644 index 1e16f8a..0000000 --- a/Front/vue-unilife/src/utils/request.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from 'axios'; - -const service = axios.create({ - baseURL: 'http://localhost:8080', - timeout: 5000 -}); - -service.interceptors.request.use( - config => { - const token = localStorage.getItem('token'); - if (token) { - console.log("前端发送信息"); - return config; - } - else - { console.log("没有token"); - return config; - } - }, - error => { - // 对请求错误做些什么 - return Promise.reject(error); - } -); - -service.interceptors.response.use( - response => { - console.log("后端返回信息"); - return response.data; - }, - error => { - // 对响应错误做些什么 - return Promise.reject(error); - } -); - -export default service; diff --git a/Front/vue-unilife/src/views/AccountManager.vue b/Front/vue-unilife/src/views/AccountManager.vue new file mode 100644 index 0000000..0eb8694 --- /dev/null +++ b/Front/vue-unilife/src/views/AccountManager.vue @@ -0,0 +1,733 @@ + + + + + diff --git a/Front/vue-unilife/src/views/Home.vue b/Front/vue-unilife/src/views/Home.vue new file mode 100644 index 0000000..c731056 --- /dev/null +++ b/Front/vue-unilife/src/views/Home.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/Front/vue-unilife/src/views/Login.vue b/Front/vue-unilife/src/views/Login.vue new file mode 100644 index 0000000..496c9cd --- /dev/null +++ b/Front/vue-unilife/src/views/Login.vue @@ -0,0 +1,502 @@ + + + + + \ No newline at end of file diff --git a/Front/vue-unilife/src/views/NotFound.vue b/Front/vue-unilife/src/views/NotFound.vue new file mode 100644 index 0000000..d45d9ba --- /dev/null +++ b/Front/vue-unilife/src/views/NotFound.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/unilife-server/src/main/java/com/unilife/controller/UserController.java b/unilife-server/src/main/java/com/unilife/controller/UserController.java index 78a3bcd..4608806 100644 --- a/unilife-server/src/main/java/com/unilife/controller/UserController.java +++ b/unilife-server/src/main/java/com/unilife/controller/UserController.java @@ -86,7 +86,7 @@ public class UserController { // 用户信息管理相关API @Operation(summary = "获取用户个人信息") - @GetMapping("profile") + @GetMapping("info") public Result getUserProfile() { // 从当前上下文获取用户ID Long userId = BaseContext.getId(); diff --git a/unilife-server/src/main/java/com/unilife/interceptor/JwtInterceptor.java b/unilife-server/src/main/java/com/unilife/interceptor/JwtInterceptor.java index 168f9c8..5c1ac93 100644 --- a/unilife-server/src/main/java/com/unilife/interceptor/JwtInterceptor.java +++ b/unilife-server/src/main/java/com/unilife/interceptor/JwtInterceptor.java @@ -27,6 +27,10 @@ public class JwtInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("JwtInterceptor preHandle"); + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; // 直接允许通过,不检查 token + } + String authHeader = request.getHeader("Authorization"); if(StrUtil.isBlank(authHeader)){ diff --git a/unilife-server/src/main/java/com/unilife/model/dto/RegisterDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/RegisterDTO.java index dc85e7a..20061e1 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/RegisterDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/RegisterDTO.java @@ -16,4 +16,5 @@ public class RegisterDTO { private String department; private String major; private String grade; + private String code; } diff --git a/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java index b2f103c..e4701d7 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/UserServiceImpl.java @@ -60,36 +60,92 @@ public class UserServiceImpl implements UserService { @Autowired private StringRedisTemplate stringRedisTemplate; + @Autowired + private JwtUtil jwtUtil; + @Value("${spring.mail.username}") private String from; final int CODE_EXPIRE_MINUTES = 10; final int LIMIT_SECONDS=60; - @Autowired - private JwtUtil jwtUtil; @Override public Result register(RegisterDTO registerDTO, HttpServletRequest request) { - if(registerDTO.getEmail().isEmpty() || registerDTO.getPassword().isEmpty()) { - return Result.error(400,"邮箱或密码不能为空"); + String email=registerDTO.getEmail(); + String password=registerDTO.getPassword(); + String code=registerDTO.getCode(); + + if (StringUtils.isAnyEmpty(email, password, code)) { + return Result.error(400, "邮箱、密码和验证码不能为空"); + } + if (password.length() < 6) { + return Result.error(400, "密码长度至少为6位"); } - if(registerDTO.getPassword().length() < 6) { - return Result.error(400,"密码长度过短!"); + if (RegexUtils.isEmailInvalid(email)) { // 假设你有这个正则工具类 + return Result.error(400, "邮箱格式不正确"); + } + + String cacheCodeKey=RedisConstant.LOGIN_EMAIL_KEY + email; + String cacheCode=stringRedisTemplate.opsForValue().get(cacheCodeKey); + + if(cacheCode==null){ + return Result.error(400,"验证码已过期或未发送,请重新获取"); + } + if(!cacheCode.equals(code)){ + return Result.error(400,"验证码错误"); } User getuser = userMapper.findByEmail(registerDTO.getEmail()); if(getuser != null) { - return Result.error(400,"用户已存在!"); + return Result.error(400,"该邮箱已被注册"); } - User user = new User(); - BeanUtil.copyProperties(registerDTO,user); - String IPAddress = ipLocationService.getClientIP(request); - String Location = ipLocationService.getIPLocation(IPAddress); - user.setLoginIp(Location); - userMapper.insert(user); - RegisterVO registerVO = new RegisterVO(user.getId(),user.getUsername() - ,user.getNickname(),user.getLoginIp()); - return Result.success(registerVO); + + stringRedisTemplate.delete(cacheCodeKey); + + + User user=new User(); + user.setEmail(email); + user.setPassword(registerDTO.getPassword()); + // 设置昵称、用户名 (可以提供默认值) + String nickname = StringUtils.isNotEmpty(registerDTO.getNickname()) ? registerDTO.getNickname() : "用户" + RandomUtil.randomString(6); + user.setNickname(nickname); + // 避免用户名太长,或者你可以让用户在 DTO 中提供 + String username = StringUtils.isNotEmpty(registerDTO.getUsername()) ? registerDTO.getUsername() : email.substring(0, Math.min(email.indexOf('@'), 10)) + "_" + RandomUtil.randomString(4); + user.setUsername(username); + + // 设置其他默认值 + user.setRole((byte) 0); // 普通用户 + user.setStatus((byte) 1); // 启用状态 + user.setIsVerified((byte) 1); // 邮箱已通过验证码验证 + user.setPoints(0); + user.setGender((byte) 0); // 默认性别,或从 DTO 获取 + + // 记录注册IP + String currentIp = ipLocationService.getClientIP(request); + String ipLocation = ipLocationService.getIPLocation(currentIp); + user.setLoginIp(ipLocation); // 首次登录IP设为注册IP + user.setLoginTime(LocalDateTime.now()); // 记录首次登录时间 + // 插入数据库 + try { + userMapper.insert(user); // user 对象现在应该有 id 了 (如果配置了主键返回) + log.info("新用户注册成功: {}", user.getEmail()); + } catch (Exception e) { + log.error("数据库插入用户失败: {}", email, e); + return Result.error(500, "注册失败,服务器内部错误"); + } + // 注册成功后直接登录:生成 Token 并返回 LoginVO + LoginVO loginVO = new LoginVO(); + BeanUtil.copyProperties(user, loginVO); // 复制基本信息 + // 确保 user.getId() 能获取到刚插入的 ID + if (user.getId() == null) { + log.error("无法获取新注册用户的 ID: {}", email); + return Result.error(500, "注册成功但登录失败,请稍后重试"); + } + String token = jwtUtil.generateToken(user.getId()); + loginVO.setToken(token); + loginVO.setId(user.getId()); + + return Result.success(loginVO, "注册成功并已登录"); } @Override