Просмотр исходного кода

feat: 添加API拦截器和请求管理系统

- 创建axios请求拦截器,支持token自动管理
- 添加代理配置,指向测试服务器
- 实现token存储和认证管理工具
- 封装博物馆、用户、预约等API接口
- 添加API使用示例组件
- 完善错误处理和统一响应格式
wangfumin 6 месяцев назад
Родитель
Сommit
fa34ec7d43
7 измененных файлов с 839 добавлено и 4 удалено
  1. 183 0
      API_README.md
  2. 260 0
      src/api/index.js
  3. 171 0
      src/components/ApiExample.vue
  4. 4 0
      src/main.js
  5. 89 0
      src/utils/auth.js
  6. 126 0
      src/utils/request.js
  7. 6 4
      vite.config.js

+ 183 - 0
API_README.md

@@ -0,0 +1,183 @@
+# API 拦截器使用说明
+
+## 概述
+
+本项目已集成完整的 API 请求拦截器系统,包括:
+
+- 统一的请求/响应拦截
+- Token 自动管理
+- 错误处理
+- 接口代理配置
+
+## 文件结构
+
+```
+src/
+├── utils/
+│   ├── request.js    # axios 拦截器配置
+│   └── auth.js       # Token 管理工具
+├── api/
+│   └── index.js      # API 接口封装
+└── components/
+    └── ApiExample.vue # API 使用示例
+```
+
+## 配置说明
+
+### 1. 代理配置 (vite.config.js)
+
+```javascript
+server: {
+  proxy: {
+    '/api': {
+      target: 'https://sit-kelamayi.4dage.com',
+      changeOrigin: true,
+      rewrite: (path) => path.replace(/^\/api/, '/api')
+    }
+  }
+}
+```
+
+### 2. 需要 Token 的接口配置
+
+在 `src/utils/request.js` 中的 `needsTokenApis` 数组中配置需要携带 token 的接口:
+
+```javascript
+const needsTokenApis = [
+  '/api/user', // 用户相关接口
+  '/api/booking', // 预约相关接口
+  '/api/activity/register', // 活动报名
+  '/api/collection', // 收藏相关接口
+]
+```
+
+## API 使用方法
+
+### 方式一:直接导入使用
+
+```javascript
+import { museumApi, userApi } from '@/api'
+
+// 获取轮播图(不需要token)
+const carouselData = await museumApi.getCarouselList({
+  pageNum: 1,
+  pageSize: 5,
+  status: 1,
+})
+
+// 获取用户信息(需要token)
+const userInfo = await userApi.getUserInfo()
+```
+
+### 方式二:使用全局 API
+
+```javascript
+// 在组件中
+const { proxy } = getCurrentInstance()
+const data = await proxy.$api.museumApi.getExhibitionList(params)
+```
+
+## Token 管理
+
+### 设置 Token
+
+```javascript
+import { setToken, setUserInfo } from '@/utils/auth'
+
+// 登录成功后
+setToken(token, remember) // remember: true=localStorage, false=sessionStorage
+setUserInfo(userInfo, remember)
+```
+
+### 检查登录状态
+
+```javascript
+import { isLoggedIn, getToken } from '@/utils/auth'
+
+if (isLoggedIn()) {
+  // 已登录
+  const token = getToken()
+}
+```
+
+### 清除认证信息
+
+```javascript
+import { clearAuth } from '@/utils/auth'
+
+// 退出登录时
+clearAuth()
+```
+
+## 接口列表
+
+### 博物馆相关接口
+
+- `getCarouselList(params)` - 获取轮播图列表
+- `getExhibitionList(params)` - 获取展览列表
+- `getExhibitionDetail(id)` - 获取展览详情
+- `getActivityList(params)` - 获取活动列表
+- `getActivityDetail(id)` - 获取活动详情
+- `getNewsList(params)` - 获取新闻列表
+- `getNewsDetail(id)` - 获取新闻详情
+
+### 预约相关接口(需要 Token)
+
+- `createBooking(data)` - 创建预约
+- `getBookingList(params)` - 获取预约列表
+- `getBookingDetail(id)` - 获取预约详情
+- `cancelBooking(id)` - 取消预约
+
+### 用户相关接口(需要 Token)
+
+- `getUserInfo()` - 获取用户信息
+- `updateUserInfo(data)` - 更新用户信息
+- `changePassword(data)` - 修改密码
+
+### 活动报名接口(需要 Token)
+
+- `registerActivity(data)` - 报名活动
+- `getRegistrationList(params)` - 获取报名列表
+- `cancelRegistration(id)` - 取消报名
+
+### 收藏相关接口(需要 Token)
+
+- `addCollection(data)` - 添加收藏
+- `removeCollection(id)` - 取消收藏
+- `getCollectionList(params)` - 获取收藏列表
+
+## 错误处理
+
+拦截器会自动处理以下错误:
+
+- **401/403**: 自动清除 token,提示重新登录
+- **404**: 接口不存在
+- **500**: 服务器错误
+- **网络错误**: 网络连接失败
+
+## 示例组件
+
+查看 `src/components/ApiExample.vue` 了解完整的使用示例。
+
+## 注意事项
+
+1. **Token 自动管理**: 拦截器会自动在需要的接口中添加 Authorization 头
+2. **错误统一处理**: 所有 API 错误都会被拦截器捕获并统一处理
+3. **请求时间戳**: 自动为所有请求添加时间戳,防止缓存
+4. **开发环境代理**: 开发时通过 Vite 代理转发到测试服务器
+5. **生产环境**: 需要配置正确的 API 基础路径
+
+## 扩展接口
+
+如需添加新接口,请在 `src/api/index.js` 中添加:
+
+```javascript
+// 添加新的接口分类
+export const newApi = {
+  // 新接口方法
+  getNewData: (params) => request.get('/api/new/data', { params }),
+  createNewData: (data) => request.post('/api/new/data', data),
+}
+
+// 如果需要 token,在 request.js 的 needsTokenApis 中添加路径
+```

+ 260 - 0
src/api/index.js

@@ -0,0 +1,260 @@
+import request from '@/utils/request'
+
+// 博物馆相关接口
+export const museumApi = {
+  // 获取轮播图列表
+  getCarouselList(params = {}) {
+    return request({
+      url: '/museum/carousel/page',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        status: 1,
+        ...params,
+      },
+    })
+  },
+
+  // 获取展览列表
+  getExhibitionList(params = {}) {
+    return request({
+      url: '/museum/exhibition/page',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        status: 1,
+        ...params,
+      },
+    })
+  },
+
+  // 获取展览详情
+  getExhibitionDetail(id) {
+    return request({
+      url: `/museum/exhibition/${id}`,
+      method: 'get',
+    })
+  },
+
+  // 获取典藏列表
+  getCollectionList(params = {}) {
+    return request({
+      url: '/museum/collection/page',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        status: 1,
+        ...params,
+      },
+    })
+  },
+
+  // 获取典藏详情
+  getCollectionDetail(id) {
+    return request({
+      url: `/museum/collection/${id}`,
+      method: 'get',
+    })
+  },
+
+  // 获取新闻资讯列表
+  getNewsList(params = {}) {
+    return request({
+      url: '/museum/news/page',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        status: 1,
+        ...params,
+      },
+    })
+  },
+
+  // 获取新闻详情
+  getNewsDetail(id) {
+    return request({
+      url: `/museum/news/${id}`,
+      method: 'get',
+    })
+  },
+
+  // 获取活动列表
+  getActivityList(params = {}) {
+    return request({
+      url: '/museum/activity/page',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        status: 1,
+        ...params,
+      },
+    })
+  },
+
+  // 获取活动详情
+  getActivityDetail(id) {
+    return request({
+      url: `/museum/activity/${id}`,
+      method: 'get',
+    })
+  },
+}
+
+// 预约相关接口(需要token)
+export const reservationApi = {
+  // 创建预约
+  createReservation(data) {
+    return request({
+      url: '/reservation/create',
+      method: 'post',
+      data,
+    })
+  },
+
+  // 获取预约列表
+  getReservationList(params = {}) {
+    return request({
+      url: '/reservation/list',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        ...params,
+      },
+    })
+  },
+
+  // 取消预约
+  cancelReservation(id) {
+    return request({
+      url: `/reservation/cancel/${id}`,
+      method: 'post',
+    })
+  },
+
+  // 获取可预约时间段
+  getAvailableTimeSlots(date) {
+    return request({
+      url: '/reservation/time-slots',
+      method: 'get',
+      params: { date },
+    })
+  },
+}
+
+// 用户相关接口(需要token)
+export const userApi = {
+  // 获取用户信息
+  getUserInfo() {
+    return request({
+      url: '/user/info',
+      method: 'get',
+    })
+  },
+
+  // 更新用户信息
+  updateUserInfo(data) {
+    return request({
+      url: '/user/update',
+      method: 'post',
+      data,
+    })
+  },
+
+  // 用户登录
+  login(data) {
+    return request({
+      url: '/user/login',
+      method: 'post',
+      data,
+    })
+  },
+
+  // 微信登录
+  weixinLogin(data) {
+    return request({
+      url: '/user/weixin/login',
+      method: 'post',
+      data,
+    })
+  },
+}
+
+// 活动报名相关接口(需要token)
+export const activityApi = {
+  // 报名活动
+  registerActivity(activityId, data) {
+    return request({
+      url: `/activity/register/${activityId}`,
+      method: 'post',
+      data,
+    })
+  },
+
+  // 取消报名
+  cancelActivity(activityId) {
+    return request({
+      url: `/activity/cancel/${activityId}`,
+      method: 'post',
+    })
+  },
+
+  // 获取我的报名列表
+  getMyActivityList(params = {}) {
+    return request({
+      url: '/activity/my-list',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        ...params,
+      },
+    })
+  },
+}
+
+// 收藏相关接口(需要token)
+export const favoriteApi = {
+  // 收藏典藏
+  favoriteCollection(collectionId) {
+    return request({
+      url: `/collection/favorite/${collectionId}`,
+      method: 'post',
+    })
+  },
+
+  // 取消收藏
+  unfavoriteCollection(collectionId) {
+    return request({
+      url: `/collection/unfavorite/${collectionId}`,
+      method: 'post',
+    })
+  },
+
+  // 获取收藏列表
+  getFavoriteList(params = {}) {
+    return request({
+      url: '/collection/favorite/list',
+      method: 'get',
+      params: {
+        pageNum: 1,
+        pageSize: 10,
+        ...params,
+      },
+    })
+  },
+}
+
+// 导出所有API
+export default {
+  museumApi,
+  reservationApi,
+  userApi,
+  activityApi,
+  favoriteApi,
+}

+ 171 - 0
src/components/ApiExample.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="api-example">
+    <h3>API使用示例</h3>
+
+    <!-- 轮播图数据 -->
+    <div class="section">
+      <h4>轮播图数据</h4>
+      <button @click="loadCarousel" :loading="loading.carousel">加载轮播图</button>
+      <div v-if="carouselData.length" class="data-list">
+        <div v-for="item in carouselData" :key="item.id" class="data-item">
+          {{ item.title || item.name }}
+        </div>
+      </div>
+    </div>
+
+    <!-- 展览数据 -->
+    <div class="section">
+      <h4>展览数据</h4>
+      <button @click="loadExhibitions" :loading="loading.exhibitions">加载展览</button>
+      <div v-if="exhibitionData.length" class="data-list">
+        <div v-for="item in exhibitionData" :key="item.id" class="data-item">
+          {{ item.title || item.name }}
+        </div>
+      </div>
+    </div>
+
+    <!-- 用户信息(需要token) -->
+    <div class="section">
+      <h4>用户信息(需要登录)</h4>
+      <button @click="loadUserInfo" :loading="loading.userInfo">获取用户信息</button>
+      <div v-if="userInfo" class="data-item">
+        用户名: {{ userInfo.username || userInfo.name }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, getCurrentInstance } from 'vue'
+import { museumApi, userApi } from '@/api'
+
+// 获取全局API实例
+const { proxy } = getCurrentInstance()
+
+// 响应式数据
+const carouselData = ref([])
+const exhibitionData = ref([])
+const userInfo = ref(null)
+const loading = ref({
+  carousel: false,
+  exhibitions: false,
+  userInfo: false
+})
+
+// 加载轮播图数据
+const loadCarousel = async () => {
+  try {
+    loading.value.carousel = true
+
+    // 方式1: 直接使用导入的API
+    const data = await museumApi.getCarouselList({
+      pageNum: 1,
+      pageSize: 5,
+      status: 1
+    })
+
+    carouselData.value = data.records || data.list || data || []
+    console.log('轮播图数据:', data)
+  } catch (error) {
+    console.error('加载轮播图失败:', error.message)
+  } finally {
+    loading.value.carousel = false
+  }
+}
+
+// 加载展览数据
+const loadExhibitions = async () => {
+  try {
+    loading.value.exhibitions = true
+
+    // 方式2: 使用全局API
+    const data = await proxy.$api.museumApi.getExhibitionList({
+      pageNum: 1,
+      pageSize: 5,
+      status: 1
+    })
+
+    exhibitionData.value = data.records || data.list || data || []
+    console.log('展览数据:', data)
+  } catch (error) {
+    console.error('加载展览失败:', error.message)
+  } finally {
+    loading.value.exhibitions = false
+  }
+}
+
+// 获取用户信息(需要token)
+const loadUserInfo = async () => {
+  try {
+    loading.value.userInfo = true
+
+    const data = await userApi.getUserInfo()
+    userInfo.value = data
+    console.log('用户信息:', data)
+  } catch (error) {
+    console.error('获取用户信息失败:', error.message)
+    // 如果是未登录,可以提示用户登录
+    if (error.message.includes('未授权') || error.message.includes('登录')) {
+      alert('请先登录')
+    }
+  } finally {
+    loading.value.userInfo = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.api-example {
+  padding: 20px;
+  max-width: 800px;
+  margin: 0 auto;
+
+  h3 {
+    color: #333;
+    margin-bottom: 20px;
+  }
+
+  .section {
+    margin-bottom: 30px;
+    padding: 15px;
+    border: 1px solid #eee;
+    border-radius: 8px;
+
+    h4 {
+      color: #666;
+      margin-bottom: 10px;
+    }
+
+    button {
+      background: #409eff;
+      color: white;
+      border: none;
+      padding: 8px 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-bottom: 10px;
+
+      &:hover {
+        background: #66b1ff;
+      }
+
+      &:disabled {
+        background: #c0c4cc;
+        cursor: not-allowed;
+      }
+    }
+
+    .data-list {
+      margin-top: 10px;
+    }
+
+    .data-item {
+      padding: 8px;
+      background: #f5f7fa;
+      margin: 5px 0;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 4 - 0
src/main.js

@@ -3,6 +3,7 @@ import { createApp } from 'vue'
 import App from './App.vue'
 import router from './router'
 import store from './store'
+import api from './api'
 
 const app = createApp(App)
 import ElementPlus from 'element-plus'
@@ -16,4 +17,7 @@ app.use(ElementPlus)
 app.use(router)
 app.use(store)
 
+// 全局注册API
+app.config.globalProperties.$api = api
+
 app.mount('#app')

+ 89 - 0
src/utils/auth.js

@@ -0,0 +1,89 @@
+// Token管理工具
+
+const TOKEN_KEY = 'museum_token'
+const USER_INFO_KEY = 'museum_user_info'
+
+// 获取token
+export function getToken() {
+  return localStorage.getItem(TOKEN_KEY) || sessionStorage.getItem(TOKEN_KEY)
+}
+
+// 设置token
+export function setToken(token, remember = false) {
+  if (remember) {
+    localStorage.setItem(TOKEN_KEY, token)
+    sessionStorage.removeItem(TOKEN_KEY)
+  } else {
+    sessionStorage.setItem(TOKEN_KEY, token)
+    localStorage.removeItem(TOKEN_KEY)
+  }
+}
+
+// 移除token
+export function removeToken() {
+  localStorage.removeItem(TOKEN_KEY)
+  sessionStorage.removeItem(TOKEN_KEY)
+}
+
+// 检查是否已登录
+export function isLoggedIn() {
+  return !!getToken()
+}
+
+// 获取用户信息
+export function getUserInfo() {
+  const userInfo = localStorage.getItem(USER_INFO_KEY) || sessionStorage.getItem(USER_INFO_KEY)
+  return userInfo ? JSON.parse(userInfo) : null
+}
+
+// 设置用户信息
+export function setUserInfo(userInfo, remember = false) {
+  const userInfoStr = JSON.stringify(userInfo)
+  if (remember) {
+    localStorage.setItem(USER_INFO_KEY, userInfoStr)
+    sessionStorage.removeItem(USER_INFO_KEY)
+  } else {
+    sessionStorage.setItem(USER_INFO_KEY, userInfoStr)
+    localStorage.removeItem(USER_INFO_KEY)
+  }
+}
+
+// 移除用户信息
+export function removeUserInfo() {
+  localStorage.removeItem(USER_INFO_KEY)
+  sessionStorage.removeItem(USER_INFO_KEY)
+}
+
+// 清除所有认证信息
+export function clearAuth() {
+  removeToken()
+  removeUserInfo()
+}
+
+// 检查token是否过期(如果后端返回了过期时间)
+export function isTokenExpired() {
+  const token = getToken()
+  if (!token) return true
+
+  try {
+    // 如果token是JWT格式,可以解析过期时间
+    const payload = JSON.parse(atob(token.split('.')[1]))
+    const currentTime = Math.floor(Date.now() / 1000)
+    return payload.exp < currentTime
+  } catch (error) {
+    // 如果不是JWT格式或解析失败,认为token有效
+    return false
+  }
+}
+
+export default {
+  getToken,
+  setToken,
+  removeToken,
+  isLoggedIn,
+  getUserInfo,
+  setUserInfo,
+  removeUserInfo,
+  clearAuth,
+  isTokenExpired,
+}

+ 126 - 0
src/utils/request.js

@@ -0,0 +1,126 @@
+import axios from 'axios'
+import { getToken, clearAuth } from './auth'
+
+// 创建axios实例
+const request = axios.create({
+  baseURL: '/api', // 使用代理前缀
+  timeout: 10000, // 请求超时时间
+  headers: {
+    'Content-Type': 'application/json;charset=UTF-8',
+  },
+})
+
+// 需要token的接口列表
+const TOKEN_REQUIRED_APIS = [
+  '/user/info',
+  '/user/update',
+  '/reservation/create',
+  '/reservation/list',
+  '/reservation/cancel',
+  '/activity/register',
+  '/activity/cancel',
+  '/collection/favorite',
+  '/collection/unfavorite',
+]
+
+// 检查是否需要token
+function needsToken(url) {
+  return TOKEN_REQUIRED_APIS.some((api) => url.includes(api))
+}
+
+// 请求拦截器
+request.interceptors.request.use(
+  (config) => {
+    // 判断是否需要添加token
+    if (needsToken(config.url)) {
+      const token = getToken()
+      if (token) {
+        config.headers.Authorization = `Bearer ${token}`
+      }
+    }
+
+    // 添加时间戳防止缓存
+    if (config.method === 'get') {
+      config.params = {
+        ...config.params,
+        _t: Date.now(),
+      }
+    }
+
+    console.log('请求发送:', config)
+    return config
+  },
+  (error) => {
+    console.error('请求错误:', error)
+    return Promise.reject(error)
+  },
+)
+
+// 响应拦截器
+request.interceptors.response.use(
+  (response) => {
+    console.log('响应接收:', response)
+
+    // 统一处理响应数据
+    const { data } = response
+
+    // 根据后端返回的数据结构调整
+    if (data.code === 200 || data.success === true) {
+      return data.data || data
+    } else {
+      // 处理业务错误
+      const errorMsg = data.message || data.msg || '请求失败'
+      console.error('业务错误:', errorMsg)
+
+      // 如果是token过期或无效
+      if (data.code === 401 || data.code === 403) {
+        // 清除认证信息
+        clearAuth()
+
+        // 跳转到登录页面或显示登录提示
+        // 这里可以根据实际需求处理
+        console.warn('登录已过期,请重新登录')
+      }
+
+      return Promise.reject(new Error(errorMsg))
+    }
+  },
+  (error) => {
+    console.error('响应错误:', error)
+
+    let errorMsg = '网络错误'
+
+    if (error.response) {
+      // 服务器返回错误状态码
+      const { status, data } = error.response
+
+      switch (status) {
+        case 400:
+          errorMsg = '请求参数错误'
+          break
+        case 401:
+          errorMsg = '未授权,请登录'
+          clearAuth()
+          break
+        case 403:
+          errorMsg = '拒绝访问'
+          break
+        case 404:
+          errorMsg = '请求地址不存在'
+          break
+        case 500:
+          errorMsg = '服务器内部错误'
+          break
+        default:
+          errorMsg = data?.message || data?.msg || `请求失败(${status})`
+      }
+    } else if (error.request) {
+      // 请求发送但没有收到响应
+      errorMsg = '网络连接超时'
+    }
+
+    return Promise.reject(new Error(errorMsg))
+  },
+)
+
+export default request

+ 6 - 4
vite.config.js

@@ -4,6 +4,7 @@ import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import vueJsx from '@vitejs/plugin-vue-jsx'
 import vueDevTools from 'vite-plugin-vue-devtools'
+import { fileURLToPath, URL } from 'node:url'
 
 // https://vite.dev/config/
 export default defineConfig({
@@ -20,15 +21,16 @@ export default defineConfig({
       '@': fileURLToPath(new URL('./src', import.meta.url)),
     },
   },
-  devServer: {
+  server: {
     host: '0.0.0.0',
-    port: 5173, // 监听的端口号
+    port: 5173,
     open: true,
     proxy: {
       '/api': {
-        target: 'URL_ADDRESS:8080',
+        target: 'https://sit-kelamayi.4dage.com',
         changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/api/, ''),
+        secure: true,
+        rewrite: (path) => path.replace(/^\/api/, '/api'),
       },
     },
   },