当前位置: 首页 > news >正文

九、【前后端联调篇】Vue3 + Axios 异步通信实战

九、【前后端联调篇】Vue3 + Axios 异步通信实战

    • 前言
      • 准备工作
      • 第一步:安装 Axios
      • 第二步:封装 Axios 实例
      • 第三步:创建 API 服务模块
      • 第四步:在组件中调用 API
      • 第五步:测试前后端联调
    • 总结

前言

在 Web 开发中,前后端分离架构已成为主流。前端负责用户界面和交互,后端负责业务逻辑和数据处理。它们之间通过 API(通常是 RESTful API)进行通信。

  • 前端需要向后端发送请求来:
    • 获取数据(GET请求),如获取项目列表、用例详情等。
    • 创建新数据(POST请求),如新建项目、提交表单等。
    • 更新数据(PUT/PATCH请求),如修改用例信息等。
    • 删除数据(DELETE请求),如删除模块等。
  • 后端接收请求,处理后返回响应给前端:
    • 响应通常是 JSON 格式的数据。
    • 响应中还包含状态码,用于指示请求的处理结果。

为什么选择 Axios?

Axios 是一个基于 Promise 的 HTTP 客户端,可以用在浏览器和 Node.js 中。它拥有许多优秀的特性:

  • API 简洁易用: 发送各种类型的 HTTP 请求非常方便。
  • 支持 Promise API: 天然支持 async/await,使得异步代码更易读写。
  • 请求和响应拦截器: 可以在请求发送前或响应处理前进行全局的预处理,如添加 Token、统一错误处理等。
  • 自动转换 JSON 数据: 默认情况下,它会自动将请求数据序列化为 JSON 字符串,并将响应数据解析为 JavaScript 对象。
  • 客户端支持防御 XSRF: 增强安全性。
  • 浏览器兼容性好。

准备工作

  1. 前端项目已就绪: test-platform/frontend 项目可以正常运行 (npm run dev)。
    在这里插入图片描述

  2. 后端 API 运行: 确保你的 Django 后端开发服务器 (python manage.py runserver) 可运行,并且 API (http://127.0.0.1:8000/api/...) 可以正常访问。
    在这里插入图片描述
    在这里插入图片描述

  3. Pinia 用户状态管理已配置: 我们将使用 userStore 中的 Token 来演示如何在请求头中添加认证信息。

第一步:安装 Axios

如果你的项目中还没有 Axios,首先需要安装它。

在前端项目根目录 (test-platform/frontend) 下打开终端,运行:

npm install axios --save

在这里插入图片描述

这会将 Axios 添加到你的项目依赖中。

第二步:封装 Axios 实例

为了更好地管理 API 请求,通常我们会创建一个 Axios 实例,并对其进行一些全局配置,而不是在每个组件中都直接使用 axios.get(...)

  1. 创建 utils/request.ts 文件:
    src 目录下创建一个 utils 文件夹,并在其中创建一个 request.ts 文件。
    在这里插入图片描述

  2. 编写 request.ts
    在这里插入图片描述

    // test-platform/frontend/src/utils/request.ts
    import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
    import { ElMessage, ElMessageBox } from 'element-plus'
    import { useUserStore } from '@/stores/user' // 引入 user store// 创建 Axios 实例
    const service: AxiosInstance = axios.create({// 1. 基础配置baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // API 的 base_url, 从 .env 文件读取timeout: 10000, // 请求超时时间 (毫秒)headers: {'Content-Type': 'application/json;charset=utf-8'}
    })// 2. 请求拦截器 (Request Interceptor)
    service.interceptors.request.use((config: InternalAxiosRequestConfig) => {// 在发送请求之前做些什么const userStore = useUserStore()if (userStore.token) {// 让每个请求携带自定义 token// 请根据实际情况修改这里的 Token 格式,例如 'Bearer ' + tokenconfig.headers.Authorization = `Bearer ${userStore.token}`}console.log('Request config:', config) // 调试用return config},(error) => {// 对请求错误做些什么console.error('Request Error:', error) // for debugreturn Promise.reject(error)}
    )// 3. 响应拦截器 (Response Interceptor)
    service.interceptors.response.use((response: AxiosResponse) => {// 对响应数据做点什么// HTTP 状态码为 2xx 时会进入这里const res = response.dataconsole.log('Response data:', res) // 调试用// 这里可以根据后端返回的自定义 code/status 来判断业务成功或失败// 例如,如果后端约定 code === 0 表示成功// if (res.code !== 0) {//   ElMessage({//     message: res.message || 'Error',//     type: 'error',//     duration: 5 * 1000//   })//   // 可以根据不同的业务错误码进行特定处理//   return Promise.reject(new Error(res.message || 'Error'))// } else {//   return res // 只返回 data 部分// }// 对于我们的 DRF 后端,通常 2xx 状态码就表示业务成功,直接返回响应数据return response // 或者 return res 如果你只想取 data},(error) => {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么console.error('Response Error:', error.response || error.message) // for debugconst userStore = useUserStore() // 在错误处理中也可能需要访问 storeif (error.response) {const { status, data } = error.responselet message = `请求错误 ${status}: `if (data && typeof data === 'object' && data.detail) {message += data.detail; // DRF 认证失败等通常在 detail 中} else if (data && typeof data === 'string') {message += data;} else if (error.message) {message = error.message;} else {message += '未知错误';}if (status === 401) {// 例如:Token 过期或无效ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示',{confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning'}).then(() => {userStore.logout() // 调用 store 的 logout action 清除 token 并跳转登录页}).catch(() => {// 用户选择取消,可以什么都不做,或者提示用户某些功能可能不可用});} else if (status === 403) {message = '您没有权限执行此操作!'ElMessage({ message, type: 'error', duration: 5 * 1000 })} else if (status === 404) {message = '请求的资源未找到!'ElMessage({ message, type: 'error', duration: 5 * 1000 })} else if (status >= 500) {message = '服务器内部错误,请稍后再试!'ElMessage({ message, type: 'error', duration: 5 * 1000 })} else {ElMessage({ message, type: 'error', duration: 5 * 1000 })}} else if (error.message.includes('timeout')) {ElMessage({ message: '请求超时,请检查网络连接!', type: 'error', duration: 5 * 1000 })} else {ElMessage({ message: '请求失败,请检查网络或联系管理员!', type: 'error', duration: 5 * 1000 })}return Promise.reject(error)}
    )// 4. 导出封装好的 Axios 实例
    export default service
    

    代码解释:

    • import axios, { ... } from 'axios': 导入 Axios 及其相关的类型定义,这对于 TypeScript 项目非常重要。
    • axios.create({ ... }): 创建一个 Axios 实例 service
      • baseURL: 设置 API 请求的基础 URL。
        • import.meta.env.VITE_API_BASE_URL: 我们尝试从 Vite 的环境变量中读取 VITE_API_BASE_URL

        • || '/api': 如果环境变量未设置,则默认为 /api

        • 配置 VITE_API_BASE_URL:
          在前端项目根目录 (test-platform/frontend) 下创建 .env.development 文件 (用于开发环境) 和 .env.production 文件 (用于生产环境)。
          在这里插入图片描述

          # .env.development
          VITE_APP_TITLE=测试平台 (开发环境)
          # API 基础路径 (用于开发时直接请求后端服务)
          VITE_API_BASE_URL=http://127.0.0.1:8000/api 
          
          # .env.production
          VITE_APP_TITLE=测试平台
          # API 基础路径 (用于生产环境,通常通过 Nginx 代理到 /api)
          VITE_API_BASE_URL=/api 
          

          这样,在开发时,baseURL 会是 http://127.0.0.1:8000/api,可以直接跨域请求本地 Django 服务。在生产打包时,baseURL 会是 /api,通常会配置 Nginx 将 /api 路径代理到后端服务。

      • timeout: 设置请求超时时间。
      • headers: 设置默认的请求头。
    • 请求拦截器 (service.interceptors.request.use):
      • 在每个请求被发送之前执行。
      • const userStore = useUserStore(): 获取 Pinia Store 实例。
      • if (userStore.token): 如果用户已登录 (Store 中有 Token),则在请求头中添加 Authorization 字段。
        • config.headers.Authorization = \Bearer ${userStore.token}`: **重要!** 这里的 Token 格式 (Bearer 前缀) 需要与你后端 Django REST Framework 配置的认证方式一致。如果你的 DRF 使用的是rest_framework_simplejwt,那么默认的 JWTAuthentication就期望Bearer 格式。如果你的后端需要不同的格式 (例如直接是 Token 值,或者Token `),请相应修改。
      • return config: 必须返回修改后的 config 对象,否则请求不会被发送。
    • 响应拦截器 (service.interceptors.response.use):
      • 第一个函数处理 HTTP 状态码为 2xx 的成功响应。
        • 我们暂时直接 return response。在实际项目中,你可能需要根据后端返回的特定业务状态码 (如 res.coderes.status) 来进一步判断成功或失败,并可能只返回 response.data
      • 第二个函数处理 HTTP 状态码超出 2xx 范围的错误响应。
        • console.error(...): 打印错误信息到控制台,方便调试。
        • if (error.response): 判断是 HTTP 错误 (有响应对象但状态码非 2xx)。
          • const { status, data } = error.response: 获取错误状态码和响应数据。
          • 根据不同的 status (如 401, 403, 404, 500) 给出不同的用户提示 (使用 ElMessageElMessageBox)。
          • 特别地,对于 status === 401 (未授权),我们弹出一个确认框,如果用户选择“重新登录”,则调用 userStore.logout() 来清除本地状态并跳转到登录页。
        • else if (error.message.includes('timeout')): 处理请求超时错误。
        • else: 处理其他网络错误 (如 DNS 解析失败、网络中断等)。
        • return Promise.reject(error): 必须返回一个 rejected Promise,这样调用 API 的地方的 .catch() 块才能捕获到错误。
    • export default service: 导出配置好的 Axios 实例,以便在其他地方使用。

    关于跨域问题 (CORS) 在开发环境:
    由于我们的前端开发服务器 (如 http://localhost:5173) 和后端 API 服务器 (http://127.0.0.1:8000) 在不同的源 (协议、域名、端口有一个不同即为不同源),直接在前端 JS 中请求后端 API 会遇到浏览器的同源策略限制,导致跨域错误。

    有两种常见的解决方法,你只需要选择其中一种解决方法就可以了:

    1. 后端配置 CORS (Cross-Origin Resource Sharing): 在 Django 后端安装并配置 django-cors-headers 库,允许来自前端开发服务器源的请求。这是更规范的做法。

      • pip install django-cors-headers -i https://pypi.tuna.tsinghua.edu.cn/simple
        在这里插入图片描述

      • settings.py 中,拷贝以下内容进行替换:

         """
        Django settings for backend project.Generated by 'django-admin startproject' using Django 5.2.1.For more information on this file, see
        https://docs.djangoproject.com/en/5.2/topics/settings/For the full list of settings and their values, see
        https://docs.djangoproject.com/en/5.2/ref/settings/
        """from pathlib import Path# Build paths inside the project like this: BASE_DIR / 'subdir'.
        BASE_DIR = Path(__file__).resolve().parent.parent# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/# SECURITY WARNING: keep the secret key used in production secret!
        SECRET_KEY = "django-insecure-n797j*a(g^*i_u^ibiwu+ia)oj7bd&t=$es(j1!h1hg71s&_q)"# SECURITY WARNING: don't run with debug turned on in production!
        DEBUG = TrueALLOWED_HOSTS = ['*']  # 允许所有主机# Application definitionINSTALLED_APPS = ["corsheaders","django.contrib.admin","django.contrib.auth","django.contrib.contenttypes","django.contrib.sessions","django.contrib.messages","django.contrib.staticfiles",'rest_framework',  # 我们之前安装了DRF,在这里也注册上'api',  # 添加我们新建的 app
        ]MIDDLEWARE = ["backend.cors_middleware.CustomCorsMiddleware",  # 添加自定义中间件在最前面"corsheaders.middleware.CorsMiddleware",  # django-cors-headers中间件"django.middleware.security.SecurityMiddleware","django.contrib.sessions.middleware.SessionMiddleware","django.middleware.common.CommonMiddleware","django.middleware.csrf.CsrfViewMiddleware","django.contrib.auth.middleware.AuthenticationMiddleware","django.contrib.messages.middleware.MessageMiddleware","django.middleware.clickjacking.XFrameOptionsMiddleware",
        ]# CORS 配置 - 完全放开
        CORS_ORIGIN_ALLOW_ALL = True  # 允许所有源
        CORS_ALLOW_CREDENTIALS = True  # 允许携带Cookie
        CORS_ALLOW_ALL_HEADERS = True  # 允许所有头
        CORS_ALLOW_METHODS = ["DELETE","GET","OPTIONS","PATCH","POST","PUT",
        ]ROOT_URLCONF = "backend.urls"TEMPLATES = [{"BACKEND": "django.template.backends.django.DjangoTemplates","DIRS": [],"APP_DIRS": True,"OPTIONS": {"context_processors": ["django.template.context_processors.request","django.contrib.auth.context_processors.auth","django.contrib.messages.context_processors.messages",],},},
        ]WSGI_APPLICATION = "backend.wsgi.application"# https://docs.djangoproject.com/en/5.2/ref/settings/#databasesDATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3","NAME": BASE_DIR / "db.sqlite3",}
        }# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validatorsAUTH_PASSWORD_VALIDATORS = [{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",},{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
        ]# Internationalization
        # https://docs.djangoproject.com/en/5.2/topics/i18n/LANGUAGE_CODE = "en-us"TIME_ZONE = "UTC"USE_I18N = TrueUSE_TZ = True# Static files (CSS, JavaScript, Images)
        # https://docs.djangoproject.com/en/5.2/howto/static-files/STATIC_URL = "static/"# Default primary key field type
        # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-fieldDEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
      • 新增cors_middleware.py文件,拷贝以下内容:

        class CustomCorsMiddleware:def __init__(self, get_response):self.get_response = get_responsedef __call__(self, request):response = self.get_response(request)response["Access-Control-Allow-Origin"] = "http://127.0.0.1:5173"response["Access-Control-Allow-Headers"] = "*"response["Access-Control-Allow-Methods"] = "*"response["Access-Control-Allow-Credentials"] = "true"# 处理预检请求if request.method == "OPTIONS":response["Access-Control-Max-Age"] = "1000"response.status_code = 200return response
        
    2. 前端配置代理 (Vite Proxy): 在 Vite 的配置文件 (vite.config.ts) 中设置一个代理,将前端特定路径的 API 请求转发到后端服务器。这样浏览器看到的请求是发往同源的 (前端服务器),然后由前端服务器代为请求后端。

      // test-platform/frontend/vite.config.ts
      import { fileURLToPath, URL } from 'node:url'
      import { defineConfig, loadEnv } from 'vite'
      import vue from '@vitejs/plugin-vue'export default defineConfig(({ mode }) => {// 根据当前工作目录中的 `mode` 加载 .env 文件// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。const env = loadEnv(mode, process.cwd(), '')return {plugins: [vue()],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},server: {host: '0.0.0.0', // 允许通过 IP 访问port: parseInt(env.VITE_PORT) || 5173, // 从 .env 读取端口号proxy: {// 字符串简写写法// '/foo': 'http://localhost:4567',// 带选项写法'/api': { // 当请求路径以 /api 开头时,会走这个代理target: 'http://127.0.0.1:8000', // 后端 API 服务器地址changeOrigin: true, // 需要虚拟主机站点rewrite: (path) => path.replace(/^\/api/, '/api') // 如果后端 API 本身就带 /api 前缀,这里可以不重写或重写为空 ''// 如果 VITE_API_BASE_URL 设置为 /api, 后端接口如 /api/projects// 我们的 baseURL 已设置为 'http://127.0.0.1:8000/api' (开发时) 或 '/api' (生产时)// 所以如果 baseURL 是 /api, 实际请求会是 /api/projects,// 而如果 target 是 http://127.0.0.1:8000, 那么我们可能需要将 /api/projects 重写为 /api/projects (如果后端本身路径是 /api/projects)// 或者如果后端本身路径是 /projects (不带 /api),则 rewrite: (path) => path.replace(/^\/api/, '')}}}}
      })
      

      如果你的 VITE_API_BASE_URL 在开发时已经设置为了 http://127.0.0.1:8000/api,那么请求会直接发往后端,此时你必须在后端配置 CORS。
      如果你的 VITE_API_BASE_URL 在开发时设置为了 /api (与生产环境一致),那么你就需要在 Vite 中配置代理,将 /api 的请求代理到 http://127.0.0.1:8000,并且 rewrite 规则可能需要调整,确保最终到达后端的路径是正确的。

      推荐方案:开发时 VITE_API_BASE_URL 指向完整后端地址,并在后端配置 CORS。这样更接近真实部署情况。

第三步:创建 API 服务模块

为了让 API 调用更有组织性,我们可以为每个后端资源 (如项目、模块、用例) 创建一个对应的 API 服务文件。

  1. 创建 api 目录和文件:
    src 目录下创建一个 api 文件夹,并在其中为 project 创建一个 project.ts 文件。
    在这里插入图片描述

  2. 编写 project.ts API 服务:
    在这里插入图片描述

    // test-platform/frontend/src/api/project.ts
    import request from '@/utils/request' // 导入我们封装的 Axios 实例
    import type { AxiosPromise } from 'axios' // 导入 AxiosPromise 类型// 定义项目相关的类型 (可以从后端 API 文档或实际响应中获取)
    // 这些类型最好与后端 DRF Serializer 的输出字段对应
    export interface Project {id: number;name: string;description: string | null;owner: string | null;status: number; // 0:规划中, 1:进行中, 2:已完成, 3:搁置// modules: any[]; // 如果需要嵌套显示模块create_time: string;update_time: string;
    }// 定义项目列表的响应类型 (如果后端有分页,结构会更复杂)
    export type ProjectListResponse = Project[] // 假设直接返回项目数组// 定义创建项目时发送的数据类型
    export interface CreateProjectData {name: string;description?: string;owner?: string;status?: number;
    }// 1. 获取项目列表的 API
    export function getProjectList(params?: any): AxiosPromise<ProjectListResponse> {// params 可以用来传递查询参数,例如分页、筛选等return request({url: '/projects/', // 完整的 URL 会是 baseURL + /projects/method: 'get',params // GET 请求的参数放在 params 中})
    }// 2. 创建项目的 API
    export function createProject(data: CreateProjectData): AxiosPromise<Project> {return request({url: '/projects/',method: 'post',data // POST/PUT/PATCH 请求的数据放在 data 中})
    }// 3. 获取单个项目详情的 API
    export function getProjectDetail(projectId: number): AxiosPromise<Project> {return request({url: `/projects/${projectId}/`,method: 'get'})
    }// 4. 更新项目的 API
    export function updateProject(projectId: number, data: Partial<CreateProjectData>): AxiosPromise<Project> {// Partial<CreateProjectData> 表示 data 对象中的属性都是可选的 (用于 PATCH)// 如果是 PUT (全量更新),类型可以是 CreateProjectDatareturn request({url: `/projects/${projectId}/`,method: 'put', // 或者 'patch'data})
    }// 5. 删除项目的 API
    export function deleteProject(projectId: number): AxiosPromise<void> { // 删除通常没有响应体内容,所以 Promise<void>return request({url: `/projects/${projectId}/`,method: 'delete'})
    }
    

    代码解释:

    • import request from '@/utils/request': 导入我们之前封装的 Axios 实例。
    • export interface Project { ... }: 定义了 Project 对象的 TypeScript 接口,这有助于类型检查和代码提示。这些字段应该与你后端 DRF ProjectSerializer 输出的字段一致。
    • export function getProjectList(...) { ... }: 每个函数对应一个 API 端点。
      • url: API 的相对路径 (相对于 baseURL)。
      • method: HTTP 请求方法。
      • params: 用于 GET 请求的 URL 查询参数。
      • data: 用于 POST, PUT, PATCH 请求的请求体数据。
      • AxiosPromise<ProjectListResponse>: 指定了函数返回的是一个 AxiosPromise,并且其 data 部分的类型是 ProjectListResponse

第四步:在组件中调用 API

现在我们可以在组件中使用这些封装好的 API 函数了。以 ProjectListView.vue 为例,获取并显示项目列表。

<!-- test-platform/frontend/src/views/project/ProjectListView.vue -->
<template><div class="project-list-view"><div class="page-header"><h2>项目列表</h2><el-button type="primary" @click="handleCreateProject"><el-icon><Plus /></el-icon> 新建项目</el-button></div><el-table :data="projects" v-loading="loading" style="width: 100%" empty-text="暂无项目数据"><el-table-column prop="id" label="ID" width="80" /><el-table-column prop="name" label="项目名称" min-width="180" /><el-table-column prop="description" label="描述" min-width="250" show-overflow-tooltip /><el-table-column prop="owner" label="负责人" width="120" /><el-table-column prop="status" label="状态" width="120"><template #default="scope"><el-tag :type="getStatusTagType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag></template></el-table-column><el-table-column prop="create_time" label="创建时间" width="180"><template #default="scope">{{ formatDateTime(scope.row.create_time) }}</template></el-table-column><el-table-column label="操作" width="200" fixed="right"><template #default="scope"><el-button size="small" type="primary" @click="handleViewDetail(scope.row.id)">查看</el-button><el-button size="small" type="warning" @click="handleEditProject(scope.row)">编辑</el-button><el-popconfirmtitle="确定要删除这个项目吗?"confirm-button-text="确定"cancel-button-text="取消"@confirm="handleDeleteProject(scope.row.id)"><template #reference><el-button size="small" type="danger">删除</el-button></template></el-popconfirm></template></el-table-column></el-table><!-- 新建/编辑项目对话框 (后续添加) --><!-- <project-form-dialog v-model="dialogVisible" :project-id="editingProjectId" @success="fetchProjectList" /> --></div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' // 导入图标
import { getProjectList, deleteProject, type Project } from '@/api/project' // 1. 导入 API 函数和类型// import ProjectFormDialog from './components/ProjectFormDialog.vue'; // 假设有表单对话框组件const router = useRouter()
const projects = ref<Project[]>([]) // 2. 定义响应式数据来存储项目列表
const loading = ref(false)
// const dialogVisible = ref(false)
// const editingProjectId = ref<number | null>(null)// 3. 获取项目列表的函数
const fetchProjectList = async () => {loading.value = truetry {const response = await getProjectList() // 调用 API// 注意:如果你的 request.ts 中的响应拦截器直接返回 response.data (例如 return res)// 那么这里直接用 response (例如 projects.value = response)// 如果响应拦截器返回的是整个 AxiosResponse (例如 return response),那么需要取 response.dataprojects.value = response.data // 假设 getProjectList 返回 { data: Project[] } 结构console.log('Fetched projects:', projects.value)} catch (error) {console.error('获取项目列表失败:', error)// ElMessage.error('获取项目列表失败,请稍后再试') // 错误提示已在 request.ts 中统一处理} finally {loading.value = false}
}// 4. 在组件挂载时调用获取列表函数
onMounted(() => {fetchProjectList()
})// 辅助函数:格式化日期时间
const formatDateTime = (dateTimeStr: string) => {if (!dateTimeStr) return ''const date = new Date(dateTimeStr)return date.toLocaleString() // 或者使用更专业的日期格式化库如 dayjs
}// 辅助函数:获取状态文本
const getStatusText = (status: number) => {const statusMap: { [key: number]: string } = {0: '规划中',1: '进行中',2: '已完成',3: '搁置',}return statusMap[status] || '未知状态'
}// 辅助函数:获取状态标签类型
const getStatusTagType = (status: number) => {const typeMap: { [key: number]: '' | 'success' | 'warning' | 'info' | 'danger' } = {0: 'info',1: '', // 默认 (primary)2: 'success',3: 'warning',}return typeMap[status] || 'info'
}const handleCreateProject = () => {// editingProjectId.value = null;// dialogVisible.value = true;router.push('/project/create') // 跳转到创建页面ElMessage.info('跳转到新建项目页面 (功能待实现)')
}const handleViewDetail = (projectId: number) => {router.push(`/project/detail/${projectId}`)
}const handleEditProject = (project: Project) => {// editingProjectId.value = project.id;// dialogVisible.value = true;ElMessage.info(`编辑项目 ID: ${project.id} (功能待实现)`)
}const handleDeleteProject = async (projectId: number) => {loading.value = true // 可以加一个局部的 loading 状态或使用表格的 loadingtry {await deleteProject(projectId) // 调用删除 APIElMessage.success('项目删除成功!')fetchProjectList() // 重新加载列表} catch (error) {console.error('删除项目失败:', error)// ElMessage.error('删除项目失败') // 错误提示已在 request.ts 中统一处理} finally {loading.value = false}
}
</script><style scoped lang="scss">
.project-list-view {padding: 20px;.page-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;}
}
</style>

代码解释:

  • import { getProjectList, deleteProject, type Project } from '@/api/project': 导入我们定义的 API 函数和 Project 类型。
  • const projects = ref<Project[]>([]): 定义一个响应式引用来存储从后端获取的项目列表数据。
  • const loading = ref(false): 用于控制表格的加载状态。
  • fetchProjectList:
    • 这是一个异步函数,用于调用 getProjectList() API。
    • loading.value = true 开始加载。
    • const response = await getProjectList(): 使用 await 等待 API 请求完成。
    • projects.value = response.data: 将 API 返回的数据 (假设在 response.data 中) 赋值给 projects这里需要注意,如果你的 request.ts 的响应拦截器修改了返回结构 (例如直接返回 res.data 而不是整个 AxiosResponse),那么这里的赋值方式需要相应调整。 我们的 request.ts 示例是 return response,所以这里用 response.data 是正确的。
    • 错误处理:try...catch 块用于捕获 API 请求可能发生的错误。我们的 request.ts 中的响应拦截器已经做了全局的错误提示,所以组件层面可以只 console.error,或者根据需要做更细致的局部处理。
    • loading.value = false 结束加载。
  • onMounted(() => { fetchProjectList() }): 在组件挂载后(即 DOM 渲染完成后)立即调用 fetchProjectList 来获取初始数据。
  • 表格中使用了 #default="scope" 的作用域插槽来定制单元格内容,例如状态的显示和操作按钮。
  • handleDeleteProject: 演示了如何调用删除 API,并在成功后重新获取列表。

第五步:测试前后端联调

  1. 确保后端 Django 服务运行在 http://127.0.0.1:8000
  2. 确保后端 API /api/projects/ 可以正常返回项目列表数据 (可以通过 Postman 或浏览器直接访问 http://127.0.0.1:8000/api/projects/ 来测试)。
  3. 确保你的前端开发环境 VITE_API_BASE_URL 设置正确 (例如 http://127.0.0.1:8000/api) 并且后端 CORS 配置允许来自前端源的请求 (例如 http://localhost:5173)。
  4. 启动前端开发服务器:
    npm run dev
    
  5. 登录并访问项目列表页 (http://localhost:5173/project/list):
    • 你应该能看到表格显示了从后端 API 获取到的真实项目数据。
    • 表格应该有加载状态的指示。
    • 打开浏览器开发者工具的 “Network” (网络) 标签页,你应该能看到向 /projects/ 端点发起的 GET 请求,以及成功的响应 (状态码 200)。
    • 检查请求头,确保 Authorization 头部被正确添加(如果已登录)。
    • 尝试删除一个项目,观察网络请求和界面的变化。
      在这里插入图片描述

常见问题与调试:

  • CORS 错误: 如果在 Network 标签页看到 CORS 相关的错误,请检查你的 Django 后端 django-cors-headers 配置是否正确,CORS_ALLOWED_ORIGINS 是否包含了你的前端开发服务器地址。
  • 401 未授权错误:
    • 确保你已登录,并且 userStore 中的 token 被正确设置。
    • 检查 request.tsAuthorization 请求头的格式是否与后端期望的一致 (例如 Bearer <token>)。
    • 如果 Token 过期,响应拦截器中的 401 处理逻辑应该会被触发。
  • 404 Not Found 错误: 检查 baseURL 和 API 的 url 拼接后的完整路径是否正确,与后端 API 端点是否匹配。
  • 500 服务器内部错误: 这通常是后端代码的问题,需要查看 Django 后端的日志来定位。
  • 数据未显示或格式不正确:
    • fetchProjectListconsole.log(response) 打印完整的响应对象,查看数据结构是否与预期一致。
    • 检查 projects.value = response.data 是否正确地取到了数据数组。
    • 检查 Project 类型定义是否与后端返回的字段匹配。

总结

在这篇文章中,我们成功地打通了 Vue3 前端和 Django 后端之间的数据交互通道:

  • 安装了 Axios HTTP 客户端库。
  • 创建并封装了一个 Axios 实例 (utils/request.ts),配置了:
    • 基础 URL (baseURL),并利用 Vite 环境变量使其在开发和生产环境可配置。
    • 请求超时时间。
    • 请求拦截器: 统一为需要认证的请求添加 Authorization Token 头部。
    • 响应拦截器: 统一处理 HTTP 成功响应和错误响应,包括对常见错误状态码 (如 401, 403, 404, 500) 的全局提示和处理逻辑 (如 401 时自动登出)。
  • 讨论了开发环境中的跨域问题 (CORS) 及其解决方案 (后端配置 CORS 或前端配置代理),并推荐了后端 CORS 方案。
  • 创建了模块化的 API 服务文件 (api/project.ts),用于集中管理与特定资源相关的 API 调用函数,并定义了相关的 TypeScript 类型。
  • 在 Vue 组件 (ProjectListView.vue) 中调用了封装好的 API 函数来获取项目列表数据,并将其展示在 Element Plus 表格中。
  • 演示了如何在组件中处理加载状态和调用删除 API。
  • 指导了如何测试前后端联调的效果并分析常见问题。

现在,你的前端应用已经具备了与后端 API 进行真实数据交互的能力!这是构建一个功能完整的全栈测试平台的关键一步。

在接下来的文章中,我们将基于这个联调基础,逐步实现项目中其他核心功能模块的前端页面和逻辑,例如创建/编辑项目表单、模块管理、测试用例管理等,让我们的测试平台越来越完善。

相关文章:

  • 探索C++标准模板库(STL):String接口实践+底层的模拟实现(中篇)
  • GitHub 趋势日报 (2025年05月27日)
  • Linux中基础IO(下)
  • Flink CEP实践总结:使用方法、常见报错、优化与难点应对
  • 【Redis】基本架构
  • java导入excel
  • Android-Room + WorkManager学习总结
  • Python生成ppt(python-pptx)N问N答(如何绘制一个没有背景的矩形框;如何绘制一个没有背景的矩形框)
  • Pytest 是什么
  • Function calling和mcp区别
  • 【pycharm】如何连接远程仓库进行版本管理(应用版本)
  • OpenWrt 插件安装失败的常见问题和解决方法
  • LeetCode 169:多数元素 - 摩尔投票法的精妙解法
  • 8.5 Q1|中山大学CHARLS发文 | 甘油三酯葡萄糖-腰身高比指数与中国中老年人心血管疾病的关系
  • 删除队列中整数
  • nova14 ultra,是如何防住80°C热水和10000KPa水压冲击的?
  • Windows下的Qtxlsx下载和编译打包成库
  • 办公效率王Word批量转PDF 50 +文档一键转换保留原格式零错乱
  • Dockerfile正确写法之现代容器化构建的最佳实践
  • LeetCode hot100-6
  • 域名注册以后怎样做网站/怎么在百度上面打广告
  • 网站群系统/360地图怎么添加商户
  • 品牌网站建设多少钱/宁波网站推广公司有哪些
  • c语言网站建设/沈阳seo团队
  • 网站建设平面要多少分辨率/网站排名在线优化工具
  • 国外大神的平面设计网站有哪些/互联网推广的方式