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 @@
<AppHeader :drawer-active="drawerActive" @toggle-drawer="toggleDrawer" @logout="showLogoutDialog = true" />
<AppDrawer v-model="drawerActive" />
<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>
</v-main>
<AppFooter />
......@@ -31,16 +47,18 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import AppHeader from './AppHeader.vue'
import AppDrawer from './AppDrawer.vue'
import AppFooter from './AppFooter.vue'
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 router = useRouter()
const route = useRoute()
const drawerActive = ref(false)
const toggleDrawer = () => { drawerActive.value = !drawerActive.value }
......@@ -66,6 +84,35 @@ const handleLogout = () => {
onMounted(() => {
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>
<style scoped>
......
......@@ -8,7 +8,7 @@ export const GLOBAL_MENU_ITEMS = [
{
name: 'Profile',
path: '/profile',
mdiIcon: 'mdi-account-card',
mdiIcon: 'mdi-account',
hasRouter: true
},
{
......@@ -19,7 +19,7 @@ export const GLOBAL_MENU_ITEMS = [
children: [
{
name: 'Students',
path: '/students',
path: '/master-data/students',
mdiIcon: 'mdi-account-school',
hasRouter: true
},
......
......@@ -3,6 +3,7 @@ import { useAuthStore } from '@/stores/auth'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue'
import StudentView from '../views/StudentView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
......@@ -11,7 +12,7 @@ const router = createRouter({
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: true, layout: 'default' },
meta: { requiresAuth: true, layout: 'default', title: 'Home' },
},
{
path: '/login',
......@@ -23,7 +24,13 @@ const router = createRouter({
path: '/profile',
name: 'profile',
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(.*)*',
......
......@@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
import axios from 'axios'
import { login as authLogin, refreshAccessToken as authRefreshToken } from '@/api/authService.js'
export const useAuthStore = defineStore('auth', () => {
const router = useRouter()
......@@ -71,14 +72,9 @@ export const useAuthStore = defineStore('auth', () => {
}
isRefreshing.value = true
refreshPromise = axios
.post(
'http://192.168.1.52:8001/api/token/refresh/',
{ refresh: refreshToken.value },
{ headers: { 'Content-Type': 'application/json' } }
)
.then((resp) => {
const newAccess = resp.data?.access
refreshPromise = authRefreshToken(refreshToken.value)
.then((data) => {
const newAccess = data?.access
if (!newAccess) throw new Error('NO_ACCESS_FROM_REFRESH')
accessToken.value = newAccess
localStorage.setItem('accessToken', newAccess)
......@@ -141,21 +137,12 @@ export const useAuthStore = defineStore('auth', () => {
try {
const { username, password } = credentials || {}
// 基于后端接口进行真实登录
const response = await axios.post(
'http://192.168.1.52:8001/api/token/',
{
// 使用新的认证服务进行登录
const { access, refresh } = await authLogin({
username,
password,
},
{
headers: {
'Content-Type': 'application/json',
},
}
)
})
const { access, refresh } = response.data || {}
if (!access) {
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