feat: 完成基础配置、API层、状态管理和路由配置

This commit is contained in:
UGREEN USER
2026-01-28 14:37:31 +08:00
parent 1377b18790
commit 1c831dbe69
14 changed files with 623 additions and 0 deletions

41
src/api/appointment.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}