feat: 完成基础配置、API层、状态管理和路由配置
This commit is contained in:
41
src/api/appointment.ts
Normal file
41
src/api/appointment.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import request from '@/utils/request'
|
||||
import type { AppointmentInfo } from '@/types/appointment'
|
||||
|
||||
export const appointmentApi = {
|
||||
// 创建预约
|
||||
create(data: {
|
||||
title: string
|
||||
gameId: string
|
||||
groupId: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
maxParticipants: number
|
||||
}) {
|
||||
return request.post<AppointmentInfo>('/appointments', data)
|
||||
},
|
||||
|
||||
// 获取我的预约列表
|
||||
getMyAppointments(params?: { status?: string }) {
|
||||
return request.get<AppointmentInfo[]>('/appointments/my', { params })
|
||||
},
|
||||
|
||||
// 获取预约详情
|
||||
getAppointmentDetail(id: string) {
|
||||
return request.get<AppointmentInfo>(`/appointments/${id}`)
|
||||
},
|
||||
|
||||
// 加入预约
|
||||
join(id: string) {
|
||||
return request.post(`/appointments/${id}/join`)
|
||||
},
|
||||
|
||||
// 退出预约
|
||||
leave(id: string) {
|
||||
return request.delete(`/appointments/${id}/leave`)
|
||||
},
|
||||
|
||||
// 取消预约
|
||||
cancel(id: string) {
|
||||
return request.delete(`/appointments/${id}`)
|
||||
}
|
||||
}
|
||||
38
src/api/auth.ts
Normal file
38
src/api/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface LoginParams {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterParams {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
user: import('@/types/user').UserInfo
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
// 登录
|
||||
login(params: LoginParams) {
|
||||
return request.post<LoginResponse>('/auth/login', params)
|
||||
},
|
||||
|
||||
// 注册
|
||||
register(params: RegisterParams) {
|
||||
return request.post<LoginResponse>('/auth/register', params)
|
||||
},
|
||||
|
||||
// 刷新令牌
|
||||
refreshToken(refreshToken: string) {
|
||||
return request.post<{ accessToken: string; refreshToken: string }>('/auth/refresh', {
|
||||
refreshToken
|
||||
})
|
||||
}
|
||||
}
|
||||
24
src/api/game.ts
Normal file
24
src/api/game.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/request'
|
||||
import type { GameInfo } from '@/types/game'
|
||||
|
||||
export const gameApi = {
|
||||
// 获取游戏列表
|
||||
getGames(params?: { page?: number; limit?: number }) {
|
||||
return request.get<GameInfo[]>('/games', { params })
|
||||
},
|
||||
|
||||
// 获取热门游戏
|
||||
getPopularGames(limit: number = 10) {
|
||||
return request.get<GameInfo[]>('/games/popular', { params: { limit } })
|
||||
},
|
||||
|
||||
// 获取游戏详情
|
||||
getGameDetail(id: string) {
|
||||
return request.get<GameInfo>(`/games/${id}`)
|
||||
},
|
||||
|
||||
// 搜索游戏
|
||||
searchGames(keyword: string) {
|
||||
return request.get<GameInfo[]>('/games/search', { params: { keyword } })
|
||||
}
|
||||
}
|
||||
39
src/api/group.ts
Normal file
39
src/api/group.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/request'
|
||||
import type { GroupInfo, CreateGroupParams } from '@/types/group'
|
||||
|
||||
export const groupApi = {
|
||||
// 创建小组
|
||||
create(data: CreateGroupParams) {
|
||||
return request.post<GroupInfo>('/groups', data)
|
||||
},
|
||||
|
||||
// 加入小组
|
||||
join(data: { groupId: string; nickname?: string }) {
|
||||
return request.post<GroupInfo>('/groups/join', data)
|
||||
},
|
||||
|
||||
// 获取我的小组列表
|
||||
getMyGroups() {
|
||||
return request.get<GroupInfo[]>('/groups/my')
|
||||
},
|
||||
|
||||
// 获取小组详情
|
||||
getGroupDetail(id: string) {
|
||||
return request.get<GroupInfo>(`/groups/${id}`)
|
||||
},
|
||||
|
||||
// 更新小组信息
|
||||
updateGroup(id: string, data: Partial<GroupInfo>) {
|
||||
return request.put<GroupInfo>(`/groups/${id}`, data)
|
||||
},
|
||||
|
||||
// 退出小组
|
||||
leaveGroup(id: string) {
|
||||
return request.delete(`/groups/${id}/leave`)
|
||||
},
|
||||
|
||||
// 解散小组
|
||||
dissolveGroup(id: string) {
|
||||
return request.delete(`/groups/${id}`)
|
||||
}
|
||||
}
|
||||
6
src/api/index.ts
Normal file
6
src/api/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 导出所有 API 模块
|
||||
export * from './auth'
|
||||
export * from './user'
|
||||
export * from './group'
|
||||
export * from './game'
|
||||
export * from './appointment'
|
||||
24
src/api/user.ts
Normal file
24
src/api/user.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/request'
|
||||
import type { UserInfo } from '@/types/user'
|
||||
|
||||
export const userApi = {
|
||||
// 获取当前用户信息
|
||||
getProfile() {
|
||||
return request.get<UserInfo>('/users/me')
|
||||
},
|
||||
|
||||
// 获取指定用户信息
|
||||
getUserInfo(id: string) {
|
||||
return request.get<UserInfo>(`/users/${id}`)
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateProfile(data: Partial<UserInfo>) {
|
||||
return request.put<UserInfo>('/users/me', data)
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword(data: { oldPassword: string; newPassword: string }) {
|
||||
return request.put('/users/me/password', data)
|
||||
}
|
||||
}
|
||||
15
src/main.ts
15
src/main.ts
@@ -1,9 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
// Element Plus
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
// 样式
|
||||
import './assets/styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
|
||||
106
src/router/index.ts
Normal file
106
src/router/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/auth/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('@/views/auth/Register.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/components/layout/MainLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/home/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'user/profile',
|
||||
name: 'UserProfile',
|
||||
component: () => import('@/views/user/Profile.vue')
|
||||
},
|
||||
{
|
||||
path: 'user/settings',
|
||||
name: 'UserSettings',
|
||||
component: () => import('@/views/user/Settings.vue')
|
||||
},
|
||||
{
|
||||
path: 'groups',
|
||||
name: 'GroupList',
|
||||
component: () => import('@/views/group/List.vue')
|
||||
},
|
||||
{
|
||||
path: 'groups/create',
|
||||
name: 'GroupCreate',
|
||||
component: () => import('@/views/group/Create.vue')
|
||||
},
|
||||
{
|
||||
path: 'groups/:id',
|
||||
name: 'GroupDetail',
|
||||
component: () => import('@/views/group/Detail.vue')
|
||||
},
|
||||
{
|
||||
path: 'games',
|
||||
name: 'GameList',
|
||||
component: () => import('@/views/game/List.vue')
|
||||
},
|
||||
{
|
||||
path: 'games/:id',
|
||||
name: 'GameDetail',
|
||||
component: () => import('@/views/game/Detail.vue')
|
||||
},
|
||||
{
|
||||
path: 'appointments',
|
||||
name: 'AppointmentList',
|
||||
component: () => import('@/views/appointment/List.vue')
|
||||
},
|
||||
{
|
||||
path: 'appointments/create',
|
||||
name: 'AppointmentCreate',
|
||||
component: () => import('@/views/appointment/Create.vue')
|
||||
},
|
||||
{
|
||||
path: 'points/ranking',
|
||||
name: 'PointsRanking',
|
||||
component: () => import('@/views/points/Ranking.vue')
|
||||
},
|
||||
{
|
||||
path: 'honors',
|
||||
name: 'Honors',
|
||||
component: () => import('@/views/honor/Timeline.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
|
||||
if (requiresAuth && !token) {
|
||||
next({
|
||||
name: 'Login',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
29
src/stores/app.ts
Normal file
29
src/stores/app.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const theme = ref<'light' | 'dark'>('light')
|
||||
const sidebarCollapsed = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
function toggleTheme() {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
function setLoading(value: boolean) {
|
||||
loading.value = value
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
sidebarCollapsed,
|
||||
loading,
|
||||
toggleTheme,
|
||||
toggleSidebar,
|
||||
setLoading
|
||||
}
|
||||
})
|
||||
77
src/stores/auth.ts
Normal file
77
src/stores/auth.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { storage } from '@/utils/storage'
|
||||
import type { UserInfo } from '@/types/user'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// State
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const accessToken = ref<string>('')
|
||||
const refreshToken = ref<string>('')
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!accessToken.value && !!userInfo.value)
|
||||
|
||||
// Actions
|
||||
async function login(account: string, password: string) {
|
||||
const data = await authApi.login({ account, password })
|
||||
userInfo.value = data.user
|
||||
accessToken.value = data.accessToken
|
||||
refreshToken.value = data.refreshToken
|
||||
|
||||
// 保存到 localStorage
|
||||
storage.setToken(data.accessToken, data.refreshToken)
|
||||
storage.setUserInfo(data.user)
|
||||
}
|
||||
|
||||
async function register(params: {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}) {
|
||||
const data = await authApi.register(params)
|
||||
userInfo.value = data.user
|
||||
accessToken.value = data.accessToken
|
||||
refreshToken.value = data.refreshToken
|
||||
|
||||
storage.setToken(data.accessToken, data.refreshToken)
|
||||
storage.setUserInfo(data.user)
|
||||
}
|
||||
|
||||
function logout() {
|
||||
userInfo.value = null
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
|
||||
storage.clear()
|
||||
}
|
||||
|
||||
function initAuth() {
|
||||
const token = storage.getAccessToken()
|
||||
const refresh = storage.getRefreshToken()
|
||||
const user = storage.getUserInfo()
|
||||
|
||||
if (token) {
|
||||
accessToken.value = token
|
||||
}
|
||||
if (refresh) {
|
||||
refreshToken.value = refresh
|
||||
}
|
||||
if (user) {
|
||||
userInfo.value = user
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
isLoggedIn,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
initAuth
|
||||
}
|
||||
})
|
||||
62
src/utils/format.ts
Normal file
62
src/utils/format.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
export function formatDateTime(date: string | Date): string {
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
export function formatDate(date: string | Date): string {
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
export function formatTime(date: string | Date): string {
|
||||
const d = new Date(date)
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
*/
|
||||
export function formatRelativeTime(date: string | Date): string {
|
||||
const d = new Date(date)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - d.getTime()
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) return `${days}天前`
|
||||
if (hours > 0) return `${hours}小时前`
|
||||
if (minutes > 0) return `${minutes}分钟前`
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
54
src/utils/request.ts
Normal file
54
src/utils/request.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios, { AxiosInstance, AxiosError, AxiosResponse } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { API_BASE_URL, API_TIMEOUT } from '@/constants/config'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: API_TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 从 localStorage 获取 token
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { code, message, data } = response.data
|
||||
|
||||
if (code === 0) {
|
||||
return data
|
||||
} else {
|
||||
ElMessage.error(message || '请求失败')
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
66
src/utils/storage.ts
Normal file
66
src/utils/storage.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { STORAGE_KEYS } from '@/constants/config'
|
||||
|
||||
/**
|
||||
* 本地存储工具
|
||||
*/
|
||||
export const storage = {
|
||||
/**
|
||||
* 设置 token
|
||||
*/
|
||||
setToken(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken)
|
||||
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 access token
|
||||
*/
|
||||
getAccessToken(): string | null {
|
||||
return localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 refresh token
|
||||
*/
|
||||
getRefreshToken(): string | null {
|
||||
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除 token
|
||||
*/
|
||||
clearToken() {
|
||||
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
setUserInfo(userInfo: any) {
|
||||
localStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo))
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserInfo(): any | null {
|
||||
const info = localStorage.getItem(STORAGE_KEYS.USER_INFO)
|
||||
return info ? JSON.parse(info) : null
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除用户信息
|
||||
*/
|
||||
clearUserInfo() {
|
||||
localStorage.removeItem(STORAGE_KEYS.USER_INFO)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
clear() {
|
||||
this.clearToken()
|
||||
this.clearUserInfo()
|
||||
}
|
||||
}
|
||||
42
src/utils/validate.ts
Normal file
42
src/utils/validate.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 验证邮箱
|
||||
*/
|
||||
export function validateEmail(email: string): boolean {
|
||||
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return reg.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
*/
|
||||
export function validatePhone(phone: string): boolean {
|
||||
const reg = /^1[3-9]\d{9}$/
|
||||
return reg.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名 (3-20位字母数字下划线)
|
||||
*/
|
||||
export function validateUsername(username: string): boolean {
|
||||
const reg = /^[a-zA-Z0-9_]{3,20}$/
|
||||
return reg.test(username)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码 (6-20位)
|
||||
*/
|
||||
export function validatePassword(password: string): boolean {
|
||||
return password.length >= 6 && password.length <= 20
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
*/
|
||||
export function validateURL(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user