Commit 38326bc5 authored by Administrator's avatar Administrator
Browse files

moved endpoints to /src/api; moved menu items to...

moved endpoints to /src/api; moved menu items to /src/components/constants/menuItems.js; added breadcrumbs; added Student page
parent 944fdeba
Pipeline #29052 failed with stages
in 0 seconds
# API 管理
本目录用于统一管理项目的API端点和服务。
## 目录结构
```
api/
├── index.js # API基础配置和端点定义
├── authService.js # 认证相关API服务
├── studentService.js # 学生相关API服务
└── README.md # API管理说明文档
```
## 使用方法
### 1. 引入API服务
```javascript
import { login } from '@/api/authService.js'
import { getStudents } from '@/api/studentService.js'
```
### 2. 调用API服务
```javascript
// 登录
const { access, refresh } = await login({ username, password })
// 获取学生列表
const students = await getStudents()
```
## 添加新的API服务
1.`index.js` 中定义新的端点
2. 创建新的服务文件(参考 `authService.js``studentService.js`
3. 在需要的地方引入并使用新服务
\ No newline at end of file
import apiClient, { apiEndpoints } from './index.js'
// 用户登录
export const login = async (credentials) => {
try {
const response = await apiClient.post(apiEndpoints.AUTH.LOGIN, credentials)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.detail || error?.response?.data?.message || error.message || 'Login failed')
}
}
// 刷新访问令牌
export const refreshAccessToken = async (refreshToken) => {
try {
const response = await apiClient.post(apiEndpoints.AUTH.REFRESH, { refresh: refreshToken })
return response.data
} catch (error) {
throw new Error(error?.response?.data?.detail || error?.message || 'Failed to refresh token')
}
}
\ No newline at end of file
import axios from 'axios'
// API基础配置
const API_BASE_URL = 'http://192.168.1.52:8001'
// 创建axios实例
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// API端点定义
export const apiEndpoints = {
// 认证相关
AUTH: {
LOGIN: '/api/token/',
REFRESH: '/api/token/refresh/',
},
// 学生相关
STUDENTS: {
LIST: '/api/student/',
},
}
export default apiClient
\ No newline at end of file
import apiClient, { apiEndpoints } from './index.js'
// 获取学生列表
export const getStudents = async () => {
try {
const response = await apiClient.get(apiEndpoints.STUDENTS.LIST)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.detail || error?.message || 'Failed to fetch students')
}
}
\ No newline at end of file
...@@ -10,6 +10,22 @@ ...@@ -10,6 +10,22 @@
<AppHeader :drawer-active="drawerActive" @toggle-drawer="toggleDrawer" @logout="showLogoutDialog = true" /> <AppHeader :drawer-active="drawerActive" @toggle-drawer="toggleDrawer" @logout="showLogoutDialog = true" />
<AppDrawer v-model="drawerActive" /> <AppDrawer v-model="drawerActive" />
<v-main> <v-main>
<v-container fluid class="pt-4 pb-0">
<v-breadcrumbs :items="breadcrumbs">
<template #prepend>
<v-icon :icon="currentBreadcrumbIcon" size="small" class="me-2" />
</template>
<template #item="{ item }">
<v-breadcrumbs-item
:disabled="item.disabled"
:to="item.to"
:exact="true"
>
{{ item.title }}
</v-breadcrumbs-item>
</template>
</v-breadcrumbs>
</v-container>
<slot></slot> <slot></slot>
</v-main> </v-main>
<AppFooter /> <AppFooter />
...@@ -31,16 +47,18 @@ ...@@ -31,16 +47,18 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import AppHeader from './AppHeader.vue' import AppHeader from './AppHeader.vue'
import AppDrawer from './AppDrawer.vue' import AppDrawer from './AppDrawer.vue'
import AppFooter from './AppFooter.vue' import AppFooter from './AppFooter.vue'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { GLOBAL_MENU_ITEMS } from './constants/menuItems'
const auth = useAuthStore() const auth = useAuthStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const drawerActive = ref(false) const drawerActive = ref(false)
const toggleDrawer = () => { drawerActive.value = !drawerActive.value } const toggleDrawer = () => { drawerActive.value = !drawerActive.value }
...@@ -66,6 +84,35 @@ const handleLogout = () => { ...@@ -66,6 +84,35 @@ const handleLogout = () => {
onMounted(() => { onMounted(() => {
drawerActive.value = true drawerActive.value = true
}) })
const breadcrumbs = computed(() => {
const matched = route.matched.filter(r => r.meta && r.meta.title)
const items = matched.map((r, index) => ({
title: r.meta.title,
to: r.path,
disabled: index === matched.length - 1,
}))
// 如果没有任何匹配标题,至少返回 Home
if (items.length === 0) {
return [{ title: 'Home', to: '/', disabled: route.path === '/' }]
}
return items
})
const currentBreadcrumbIcon = computed(() => {
const targetPath = route.path
const findIconByPath = (items, path) => {
for (const item of items) {
if (item.path === path && item.mdiIcon) return item.mdiIcon
if (item.children) {
const childIcon = findIconByPath(item.children, path)
if (childIcon) return childIcon
}
}
return null
}
return findIconByPath(GLOBAL_MENU_ITEMS, targetPath) || 'mdi-home'
})
</script> </script>
<style scoped> <style scoped>
......
...@@ -8,7 +8,7 @@ export const GLOBAL_MENU_ITEMS = [ ...@@ -8,7 +8,7 @@ export const GLOBAL_MENU_ITEMS = [
{ {
name: 'Profile', name: 'Profile',
path: '/profile', path: '/profile',
mdiIcon: 'mdi-account-card', mdiIcon: 'mdi-account',
hasRouter: true hasRouter: true
}, },
{ {
...@@ -19,7 +19,7 @@ export const GLOBAL_MENU_ITEMS = [ ...@@ -19,7 +19,7 @@ export const GLOBAL_MENU_ITEMS = [
children: [ children: [
{ {
name: 'Students', name: 'Students',
path: '/students', path: '/master-data/students',
mdiIcon: 'mdi-account-school', mdiIcon: 'mdi-account-school',
hasRouter: true hasRouter: true
}, },
......
...@@ -3,6 +3,7 @@ import { useAuthStore } from '@/stores/auth' ...@@ -3,6 +3,7 @@ import { useAuthStore } from '@/stores/auth'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue' import ProfileView from '../views/ProfileView.vue'
import StudentView from '../views/StudentView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
...@@ -11,7 +12,7 @@ const router = createRouter({ ...@@ -11,7 +12,7 @@ const router = createRouter({
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView, component: HomeView,
meta: { requiresAuth: true, layout: 'default' }, meta: { requiresAuth: true, layout: 'default', title: 'Home' },
}, },
{ {
path: '/login', path: '/login',
...@@ -23,7 +24,13 @@ const router = createRouter({ ...@@ -23,7 +24,13 @@ const router = createRouter({
path: '/profile', path: '/profile',
name: 'profile', name: 'profile',
component: ProfileView, component: ProfileView,
meta: { requiresAuth: true, layout: 'default' }, meta: { requiresAuth: true, layout: 'default', title: 'Profile' },
},
{
path: '/master-data/students',
name: 'students',
component: StudentView,
meta: { requiresAuth: true, layout: 'default', title: 'Students' },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
......
...@@ -2,6 +2,7 @@ import { ref, computed } from 'vue' ...@@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import { login as authLogin, refreshAccessToken as authRefreshToken } from '@/api/authService.js'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const router = useRouter() const router = useRouter()
...@@ -71,14 +72,9 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -71,14 +72,9 @@ export const useAuthStore = defineStore('auth', () => {
} }
isRefreshing.value = true isRefreshing.value = true
refreshPromise = axios refreshPromise = authRefreshToken(refreshToken.value)
.post( .then((data) => {
'http://192.168.1.52:8001/api/token/refresh/', const newAccess = data?.access
{ refresh: refreshToken.value },
{ headers: { 'Content-Type': 'application/json' } }
)
.then((resp) => {
const newAccess = resp.data?.access
if (!newAccess) throw new Error('NO_ACCESS_FROM_REFRESH') if (!newAccess) throw new Error('NO_ACCESS_FROM_REFRESH')
accessToken.value = newAccess accessToken.value = newAccess
localStorage.setItem('accessToken', newAccess) localStorage.setItem('accessToken', newAccess)
...@@ -141,21 +137,12 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -141,21 +137,12 @@ export const useAuthStore = defineStore('auth', () => {
try { try {
const { username, password } = credentials || {} const { username, password } = credentials || {}
// 基于后端接口进行真实登录 // 使用新的认证服务进行登录
const response = await axios.post( const { access, refresh } = await authLogin({
'http://192.168.1.52:8001/api/token/',
{
username, username,
password, password,
}, })
{
headers: {
'Content-Type': 'application/json',
},
}
)
const { access, refresh } = response.data || {}
if (!access) { if (!access) {
return { success: false, error: '未获取到访问令牌' } return { success: false, error: '未获取到访问令牌' }
} }
......
<script setup>
import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { getStudents } from '@/api/studentService.js'
const authStore = useAuthStore()
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const students = ref([])
const headers = [
{ title: 'Avatar', value: 'avatar', align: 'start' },
{ title: 'Name', value: 'student_name' },
{ title: 'Enabled', value: 'enabled_flag' },
{ title: 'Age', value: 'age' },
{ title: 'Grade', value: 'grade' },
]
const fetchStudents = async () => {
isLoading.value = true
isError.value = false
errorMessage.value = ''
try {
const data = await getStudents()
students.value = Array.isArray(data) ? data : (data?.items || [])
} catch (e) {
isError.value = true
errorMessage.value = e?.response?.data?.detail || e?.message || 'Failed to load students'
} finally {
isLoading.value = false
}
}
onMounted(() => {
// 确保 axios 鉴权拦截器与头已设置
if (authStore && authStore.initializeAuth) {
authStore.initializeAuth()
}
fetchStudents()
})
const getAvatarFallback = (name) => {
if (!name || typeof name !== 'string') return '?'
const t = name.trim()
return t ? t.charAt(0).toUpperCase() : '?'
}
</script>
<template>
<v-container fluid>
<v-row class="mb-4" align="center" justify="space-between">
<v-col cols="12" sm="6">
<h2 class="text-h5">Students</h2>
</v-col>
<v-col cols="12" sm="6" class="text-sm-right text-right">
<v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchStudents">
Refresh
</v-btn>
</v-col>
</v-row>
<v-alert v-if="isError" type="error" class="mb-4" closable>
{{ errorMessage }}
</v-alert>
<v-progress-linear v-if="isLoading" indeterminate color="primary" class="mb-4"></v-progress-linear>
<v-data-table :headers="headers" :items="students" item-key="id" :items-per-page="-1" class="elevation-1">
<template v-slot:[`item.avatar`]="{ item }">
<v-avatar size="36">
<template v-if="item.avatar">
<v-img :src="item.avatar" alt="avatar" cover></v-img>
</template>
<template v-else>
<span>{{ getAvatarFallback(item.student_name) }}</span>
</template>
</v-avatar>
</template>
<template v-slot:[`item.enabled_flag`]="{ item }">
<v-chip :color="item.enabled_flag ? 'success' : 'error'" size="small" variant="flat">
{{ item.enabled_flag ? 'Enabled' : 'Disabled' }}
</v-chip>
</template>
</v-data-table>
</v-container>
</template>
<style scoped>
</style>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment