Element Plus 多页面后台与 ASP.NET Core API 从零手敲教程

> 本文由用户逐步手工执行。按用户要求,不编写、不运行自动化测试代码。教程使用完整文件内容和代码锚点,不使用容易漂移的绝对行号。

**目标:** 从空目录创建 Vue 3 + Element Plus 多页面后台,完成两级菜单、四个路由、多标签页、真实菜单折叠、KeepAlive 状态保留,以及 Axios 调用 ASP.NET Core 10 人员查询 API。

**架构:** 前端使用 Vue Router 管理四个独立页面,AdminLayout 统一负责菜单、面包屑、标签页和 KeepAlive。人员信息页面通过 Axios 传递 name、department 参数并显示表格;其余三个页面只保留输入框与按钮。后端使用一个 .NET 10 Minimal API 和内存数据。

**技术栈:** Vue 3.5、Vite 8、TypeScript、Element Plus 2.14、Vue Router 5、Axios 1.18、ASP.NET Core 10、Swagger。

---

## 0. 最终目录

完成后目录为:

    C:\ElementPlus多页面跟练
     ├─ .git
     ├─ .gitignore
     ├─ activity-admin-web
     └─ ActivityAdmin.Api

前端主要文件:

    activity-admin-web\src
     ├─ api
     │  ├─ http.ts
     │  └─ person.ts
     ├─ layouts
     │  └─ AdminLayout.vue
     ├─ router
     │  └─ index.ts
     ├─ views
     │  ├─ PersonPage.vue
     │  ├─ DepartmentPage.vue
     │  ├─ WorkOrderPage.vue
     │  └─ QualityPage.vue
     ├─ App.vue
     ├─ main.ts
     └─ style.css

API 主要文件:

    ActivityAdmin.Api
     ├─ Properties
     │  └─ launchSettings.json
     ├─ ActivityAdmin.Api.csproj
     └─ Program.cs

---

## 1. 检查开发环境

本阶段不修改代码。

- [ ] 检查 Node.js

    node --version

- [ ] 检查 pnpm

    pnpm --version

- [ ] 检查 .NET SDK

   dotnet --version

本教程已在以下主要版本设计:

    Vue 3.5.40
     Vite 8.1.5
     Element Plus 2.14.3
     Axios 1.18.1
     Vue Router 5.2.0
     .NET SDK 10.0.302

- [ ] 检查 Git

    git --version

---

## 2. 创建根目录和 Git 仓库

- [ ] 创建独立目录

    New-Item -ItemType Directory -Force -Path 'C:\ElementPlus多页面跟练'
     Set-Location 'C:\ElementPlus多页面跟练'

- [ ] 初始化 Git

    git init

- [ ] 新建根目录 .gitignore

文件:

    C:\ElementPlus多页面跟练\.gitignore

新建并输入:

    **/node_modules/
     **/dist/
     **/bin/
     **/obj/
     **/publish/
     .vs/
     *.user
     *.suo

保存文件。

---

## 3. 创建 Vue 3 + TypeScript 前端

- [ ] 在根目录执行

    pnpm create vite@latest activity-admin-web --template vue-ts

- [ ] 进入前端目录

   Set-Location 'C:\ElementPlus多页面跟练\activity-admin-web'

- [ ] 安装基础依赖

    pnpm install

- [ ] 安装正式依赖

    pnpm add element-plus@2.14.3 @element-plus/icons-vue vue-router@5.2.0 axios@1.18.1

- [ ] 用代码编辑器打开整个前端项目目录

      C:\ElementPlus多页面跟练\activity-admin-web

  使用 HBuilderX 时也打开 `activity-admin-web`,不要只打开 `src`。这样编辑器才能识别 package.json、TypeScript 配置和依赖。

- [ ] 第一次运行默认页面

    pnpm dev

预期:

    Local: http://localhost:5173/

浏览器打开:

    http://localhost:5173/

看到 Vite 默认页面即可。回到终端按 Ctrl+C 停止。

---

## 4. 注册 Element Plus 和 Vue Router

### 文件

整文件替换:

    C:\ElementPlus多页面跟练\activity-admin-web\src\main.ts

### 操作

打开 main.ts,按 Ctrl+A,用以下内容完整覆盖:

    import { createApp } from 'vue'
     import ElementPlus from 'element-plus'
     import 'element-plus/dist/index.css'
     import App from './App.vue'
     import router from './router'
     import './style.css'

    const app = createApp(App)

    app.use(ElementPlus)
     app.use(router)
     app.mount('#app')

保存文件。

此时 router 文件尚未创建,编辑器会暂时报错。继续下一步。

---

## 5. 创建四个页面的路由

### 文件

新建目录:

    src\router

新建文件:

    src\router\index.ts

### 完整内容

    import { createRouter, createWebHistory } from 'vue-router'
     import type { RouteRecordRaw } from 'vue-router'

    const routes: RouteRecordRaw[] = [
       {
         path: '/',
         redirect: '/basic/person',
       },
       {
         path: '/basic/person',
         name: 'PersonPage',
         component: () => import('../views/PersonPage.vue'),
         meta: {
           title: '人员信息',
           group: '基础数据',
         },
       },
       {
         path: '/basic/department',
         name: 'DepartmentPage',
         component: () => import('../views/DepartmentPage.vue'),
         meta: {
           title: '部门信息',
           group: '基础数据',
         },
       },
       {
         path: '/business/work-order',
         name: 'WorkOrderPage',
         component: () => import('../views/WorkOrderPage.vue'),
         meta: {
           title: '工单查询',
           group: '业务功能',
         },
       },
       {
         path: '/business/quality',
         name: 'QualityPage',
         component: () => import('../views/QualityPage.vue'),
         meta: {
           title: '质检查询',
           group: '业务功能',
         },
       },
     ]

    const router = createRouter({
       history: createWebHistory(),
       routes,
     })

    export default router

保存文件。

说明:

- 每个页面使用独立路由。
- meta.title 用于标签标题。
- meta.group 用于面包屑一级名称。
- 根路径自动进入人员信息。

视图文件还没创建,编辑器暂时报错是正常的。

---

## 6. 创建 Axios 实例

### 文件

新建目录:

    src\api

新建文件:

    src\api\http.ts

### 完整内容

    import axios from 'axios'
     import type { AxiosInstance } from 'axios'

    const http: AxiosInstance = axios.create({
       baseURL: '
http://localhost:5080',
       timeout: 5000,
     })

    export default http

保存文件。

说明:

- 所有 API 请求共用该实例。
- baseURL 指向后面创建的 ASP.NET Core API。
- timeout 为 5 秒。
- 后续需要 token 时,只在该文件增加拦截器。

---

## 7. 创建人员 API 客户端

### 文件

新建:

    src\api\person.ts

### 完整内容

    import type { AxiosResponse } from 'axios'
     import http from './http'

    export interface Person {
       personNo: string
       name: string
       department: string
       position: string
       phone: string
       employmentStatus: string
     }

    export interface PersonQuery {
       name?: string
       department?: string
     }

    // 根据姓名和部门查询人员。
     export async function queryPeople(query: PersonQuery): Promise<Person[]> {
       const response: AxiosResponse<Person[]> = await http.get<Person[]>('/api/people', {
         params: query,
       })

      return response.data
     }

保存文件。

Axios 的 params 会自动生成:

    /api/people?name=张&department=研发部

不需要手工拼接和编码 URL。

---

## 8. 创建人员信息页面

### 文件

新建目录:

    src\views

新建文件:

    src\views\PersonPage.vue

### 完整内容

    <script lang="ts" setup>
     import { onMounted, reactive, ref } from 'vue'
     import { ElMessage } from 'element-plus'
     import { queryPeople } from '../api/person'
     import type { Person, PersonQuery } from '../api/person'

    defineOptions({
       name: 'PersonPage',
     })

    const loading = ref(false)
     const people = ref<Person[]>([])

    const query = reactive<PersonQuery>({
       name: '',
       department: '',
     })

    const departmentOptions = ['研发部', '质检部', '生产部', '行政部']

    // 从 API 加载人员数据。
     async function loadPeople() {
       loading.value = true

      try {
         people.value = await queryPeople({
           name: query.name?.trim() || undefined,
           department: query.department || undefined,
         })
       }
       catch (error: unknown) {
         people.value = []

        const message = error instanceof Error
           ? error.message
           : '人员数据加载失败'

        ElMessage.error(message)
       }
       finally {
         loading.value = false
       }
     }

    // 清空条件并重新加载全部人员。
     function resetQuery() {
       query.name = ''
       query.department = ''
       void loadPeople()
     }

    onMounted(() => {
       void loadPeople()
     })
     </script>

    <template>
       <el-space direction="vertical" fill :size="16">
         <el-page-header :icon="null" title="" content="人员信息" />

        <el-card header="人员查询">
           <el-form :inline="true" :model="query">
             <el-form-item label="姓名">
               <el-input
                 v-model="query.name"
                 clearable
                 placeholder="请输入姓名"
                 style="width: 180px"
               />
             </el-form-item>

            <el-form-item label="部门">
               <el-select
                 v-model="query.department"
                 clearable
                 placeholder="请选择部门"
                 style="width: 180px"
               >
                 <el-option
                   v-for="department in departmentOptions"
                   :key="department"
                   :label="department"
                   :value="department"
                 />
               </el-select>
             </el-form-item>

            <el-form-item>
               <el-button type="primary" @click="loadPeople">
                 查询
               </el-button>
               <el-button @click="resetQuery">
                 重置
               </el-button>
             </el-form-item>
           </el-form>
         </el-card>

        <el-card header="人员列表">
           <el-table
             v-loading="loading"
             :data="people"
             stripe
             empty-text="没有符合条件的人员"
             style="width: 100%"
           >
             <el-table-column prop="personNo" label="人员编号" min-width="120" />
             <el-table-column prop="name" label="姓名" min-width="100" />
             <el-table-column prop="department" label="部门" min-width="110" />
             <el-table-column prop="position" label="职位" min-width="140" />
             <el-table-column prop="phone" label="手机号" min-width="140" />

            <el-table-column label="在职状态" min-width="100">
               <template #default="{ row }">
                 <el-tag :type="row.employmentStatus === '在职' ? 'success' : 'info'">
                   {{ row.employmentStatus }}
                 </el-tag>
               </template>
             </el-table-column>
           </el-table>
         </el-card>
       </el-space>
     </template>

保存文件。

状态保留点:

- query 是页面组件自己的 reactive 状态。
- people 是页面组件自己的 ref 状态。
- 后面 KeepAlive 会缓存整个 PersonPage 实例。
- 切换页面再回来,条件和表格都不会重新创建。

---

## 9. 创建部门信息页面

### 文件

新建:

    src\views\DepartmentPage.vue

### 完整内容

   <script lang="ts" setup>
     import { ref } from 'vue'
     import { ElMessage } from 'element-plus'

    defineOptions({
       name: 'DepartmentPage',
     })

    const departmentName = ref('')

    // 显示当前输入的部门名称。
     function showDepartment() {
       const value = departmentName.value.trim()

      if (!value) {
         ElMessage.warning('请输入部门名称')
         return
       }

      ElMessage.success('部门名称:' + value)
     }
     </script>

    <template>
       <el-space direction="vertical" fill :size="16">
         <el-page-header :icon="null" title="" content="部门信息" />

        <el-card header="部门信息 Demo">
           <el-form :inline="true">
             <el-form-item label="部门名称">
               <el-input
                 v-model="departmentName"
                 clearable
                 placeholder="请输入部门名称"
                 style="width: 240px"
               />
             </el-form-item>

            <el-form-item>
               <el-button type="primary" @click="showDepartment">
                 显示内容
               </el-button>
             </el-form-item>
           </el-form>
         </el-card>
       </el-space>
     </template>

保存文件。

---

## 10. 创建工单查询页面

### 文件

新建:

    src\views\WorkOrderPage.vue

### 完整内容

   <script lang="ts" setup>
     import { ref } from 'vue'
     import { ElMessage } from 'element-plus'

    defineOptions({
       name: 'WorkOrderPage',
     })

    const workOrderNo = ref('')

    // 显示当前输入的工单号。
     function showWorkOrder() {
       const value = workOrderNo.value.trim()

      if (!value) {
         ElMessage.warning('请输入工单号')
         return
       }

      ElMessage.success('工单号:' + value)
     }
     </script>

    <template>
       <el-space direction="vertical" fill :size="16">
         <el-page-header :icon="null" title="" content="工单查询" />

        <el-card header="工单查询 Demo">
           <el-form :inline="true">
             <el-form-item label="工单号">
               <el-input
                 v-model="workOrderNo"
                 clearable
                 placeholder="请输入工单号"
                 style="width: 240px"
               />
             </el-form-item>

            <el-form-item>
               <el-button type="primary" @click="showWorkOrder">
                 显示内容
               </el-button>
             </el-form-item>
           </el-form>
         </el-card>
       </el-space>
     </template>

保存文件。

---

## 11. 创建质检查询页面

### 文件

新建:

    src\views\QualityPage.vue

### 完整内容

    <script lang="ts" setup>
     import { ref } from 'vue'
     import { ElMessage } from 'element-plus'

    defineOptions({
       name: 'QualityPage',
     })

    const inspectionNo = ref('')

    // 显示当前输入的质检单号。
     function showInspection() {
       const value = inspectionNo.value.trim()

      if (!value) {
         ElMessage.warning('请输入质检单号')
         return
       }

      ElMessage.success('质检单号:' + value)
     }
     </script>

    <template>
       <el-space direction="vertical" fill :size="16">
         <el-page-header :icon="null" title="" content="质检查询" />

        <el-card header="质检查询 Demo">
           <el-form :inline="true">
             <el-form-item label="质检单号">
               <el-input
                 v-model="inspectionNo"
                 clearable
                 placeholder="请输入质检单号"
                 style="width: 240px"
               />
             </el-form-item>

            <el-form-item>
               <el-button type="primary" @click="showInspection">
                 显示内容
               </el-button>
             </el-form-item>
           </el-form>
         </el-card>
       </el-space>
     </template>

保存文件。

---

## 12. 创建后台布局

### 文件

新建目录:

    src\layouts

新建文件:

    src\layouts\AdminLayout.vue

### 完整内容

   <script lang="ts" setup>
     import { computed, ref, watch } from 'vue'
     import { useRoute, useRouter } from 'vue-router'
     import type { TabPaneName, TabsPaneContext } from 'element-plus'
     import {
       Avatar,
       Briefcase,
       CircleCheck,
       Document,
       Expand,
       Fold,
       OfficeBuilding,
       User,
     } from '@element-plus/icons-vue'

    interface OpenedTab {
       path: string
       title: string
     }

    const route = useRoute()
     const router = useRouter()

    const isCollapse = ref(false)
     const activeTab = ref('')
     const openedTabs = ref<OpenedTab[]>([])

    const asideWidth = computed(() => {
       return isCollapse.value ? '64px' : '240px'
     })

    // 根据当前路由新增或激活标签。
     function syncCurrentRouteToTabs() {
       const path = route.path
       const title = String(route.meta.title || '未命名页面')

      const exists = openedTabs.value.some((tab) => {
         return tab.path === path
       })

      if (!exists) {
         openedTabs.value.push({
           path,
           title,
         })
       }

      activeTab.value = path
     }

    // 点击标签后切换路由。
     function handleTabClick(tab: TabsPaneContext) {
       const path = String(tab.paneName || '')

      if (path) {
         void router.push(path)
       }
     }

    // 关闭标签;关闭当前标签时跳到相邻标签。
     function handleTabRemove(targetName: TabPaneName) {
       if (openedTabs.value.length === 1) {
         return
       }

      const targetPath = String(targetName)
       const targetIndex = openedTabs.value.findIndex((tab) => {
         return tab.path === targetPath
       })

      if (targetIndex < 0) {
         return
       }

      const closingActiveTab = activeTab.value === targetPath

      openedTabs.value.splice(targetIndex, 1)

      if (closingActiveTab) {
         const nextTab = openedTabs.value[targetIndex]
           || openedTabs.value[targetIndex - 1]

        if (nextTab) {
           void router.push(nextTab.path)
         }
       }
     }

    watch(
       () => route.path,
       () => {
         syncCurrentRouteToTabs()
       },
       {
         immediate: true,
       },
     )
     </script>

    <template>
       <el-container class="admin-shell">
         <el-aside :width="asideWidth" class="sidebar">
           <div class="brand">
             <el-avatar shape="square" :size="40">
               管
             </el-avatar>

            <div v-show="!isCollapse" class="brand-text">
               <el-text tag="strong" size="large">
                 业务管理
               </el-text>
               <el-text size="small">
                 Admin Demo
               </el-text>
             </div>
           </div>

          <el-scrollbar>
             <el-menu
               :default-active="route.path"
               :collapse="isCollapse"
               :collapse-transition="false"
               router
               background-color="#0f2f2b"
               text-color="#d8e4e2"
               active-text-color="#409eff"
             >
               <el-sub-menu index="basic">
                 <template #title>
                   <el-icon>
                     <OfficeBuilding />
                   </el-icon>
                   <span>基础数据</span>
                 </template>

                <el-menu-item index="/basic/person">
                   <el-icon>
                     <User />
                   </el-icon>
                   <span>人员信息</span>
                 </el-menu-item>

                <el-menu-item index="/basic/department">
                   <el-icon>
                     <OfficeBuilding />
                   </el-icon>
                   <span>部门信息</span>
                 </el-menu-item>
               </el-sub-menu>

              <el-sub-menu index="business">
                 <template #title>
                   <el-icon>
                     <Briefcase />
                   </el-icon>
                   <span>业务功能</span>
                 </template>

                <el-menu-item index="/business/work-order">
                   <el-icon>
                     <Document />
                   </el-icon>
                   <span>工单查询</span>
                 </el-menu-item>

                <el-menu-item index="/business/quality">
                   <el-icon>
                     <CircleCheck />
                   </el-icon>
                   <span>质检查询</span>
                 </el-menu-item>
               </el-sub-menu>
             </el-menu>
           </el-scrollbar>
         </el-aside>

        <el-container class="main-container">
           <el-header class="topbar">
             <el-space>
               <el-button
                 :icon="isCollapse ? Expand : Fold"
                 circle
                 @click="isCollapse = !isCollapse"
               />

              <el-breadcrumb separator="/">
                 <el-breadcrumb-item>
                   后台管理
                 </el-breadcrumb-item>
                 <el-breadcrumb-item>
                   {{ route.meta.group }}
                 </el-breadcrumb-item>
                 <el-breadcrumb-item>
                   {{ route.meta.title }}
                 </el-breadcrumb-item>
               </el-breadcrumb>
             </el-space>

            <el-space>
               <el-tag type="success" effect="plain">
                 本地开发
               </el-tag>
               <el-icon>
                 <Avatar />
               </el-icon>
               <el-text>管理员</el-text>
             </el-space>
           </el-header>

          <el-tabs
             v-model="activeTab"
             type="card"
             class="page-tabs"
             @tab-click="handleTabClick"
             @tab-remove="handleTabRemove"
           >
             <el-tab-pane
               v-for="tab in openedTabs"
               :key="tab.path"
               :label="tab.title"
               :name="tab.path"
               :closable="openedTabs.length > 1"
             />
           </el-tabs>

          <el-main class="main-content">
             <router-view v-slot="{ Component, route: currentRoute }">
               <keep-alive>
                 <component
                   :is="Component"
                   :key="currentRoute.name"
                 />
               </keep-alive>
             </router-view>
           </el-main>
         </el-container>
       </el-container>
     </template>

    <style scoped>
     .admin-shell {
       height: 100vh;
     }

    .sidebar {
       overflow: hidden;
       background-color: #0f2f2b;
       transition: width 0.2s;
     }

    .brand {
       height: var(--el-header-height);
       display: flex;
       align-items: center;
       gap: 12px;
       padding: 0 12px;
       overflow: hidden;
       white-space: nowrap;
     }

    .brand-text {
       display: flex;
       flex-direction: column;
     }

    .brand-text .el-text {
       color: #ffffff;
     }

    .sidebar .el-menu {
       border-right: 0;
     }

    .main-container {
       min-width: 0;
     }

    .topbar {
       display: flex;
       align-items: center;
       justify-content: space-between;
       border-bottom: 1px solid var(--el-border-color-light);
     }

    .page-tabs {
       padding: 0 16px;
       background-color: var(--el-bg-color);
     }

    .page-tabs :deep(.el-tabs__header) {
       margin: 0;
     }

    .main-content {
       overflow: auto;
       background-color: var(--el-bg-color-page);
     }
     </style>

保存文件。

---

## 13. 用后台布局替换 App.vue

### 文件

整文件替换:

    src\App.vue

### 完整内容

    <script lang="ts" setup>
     import AdminLayout from './layouts/AdminLayout.vue'
     </script>

    <template>
       <AdminLayout />
     </template>

保存文件。

---

## 14. 替换全局样式

### 文件

整文件替换:

    src\style.css

### 完整内容

    * {
       box-sizing: border-box;
     }

    html,
     body,
     #app {
       height: 100%;
       margin: 0;
     }

    body {
       min-width: 960px;
       background-color: var(--el-bg-color-page);
     }

保存文件。

这里没有重新定义 Element Plus 的按钮、表格、输入框和卡片样式。

---

## 15. 清理 Vite 默认演示文件

这些文件已经不再使用,可以删除:

    Remove-Item .\src\components\HelloWorld.vue -ErrorAction SilentlyContinue
     Remove-Item .\src\assets\vue.svg -ErrorAction SilentlyContinue
     Remove-Item .\public\vite.svg -ErrorAction SilentlyContinue

如果某个文件不存在,可以忽略。

---

## 16. 创建 ASP.NET Core 10 API

回到根目录:

    Set-Location 'C:\ElementPlus多页面跟练'

创建 Minimal API:

    dotnet new webapi -n ActivityAdmin.Api -f net10.0 --use-controllers false --no-https

进入 API:

    Set-Location 'C:\ElementPlus多页面跟练\ActivityAdmin.Api'

安装 Swagger:

    dotnet add package Swashbuckle.AspNetCore --version 10.2.3

---

## 17. 编写 Program.cs

### 文件

整文件替换:

    C:\ElementPlus多页面跟练\ActivityAdmin.Api\Program.cs

### 完整内容

    using System.Diagnostics;
     using Microsoft.AspNetCore.Mvc;

    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

    builder.Services.AddEndpointsApiExplorer(); // 注册 API 端点描述
     builder.Services.AddSwaggerGen(); // 注册 Swagger 文档生成

    builder.Services.AddCors(options =>
     {
         options.AddPolicy("Frontend", policy =>
         {
             policy
                 .WithOrigins("
http://localhost:5173")
                 .AllowAnyHeader()
                 .AllowAnyMethod();
         });
     });

    WebApplication app = builder.Build();

    if (app.Environment.IsDevelopment())
     {
         app.UseSwagger(); // 生成 Swagger JSON
         app.UseSwaggerUI(); // 提供 Swagger 页面

        app.Lifetime.ApplicationStarted.Register(() =>
         {
             ProcessStartInfo startInfo = new ProcessStartInfo
             {
                 FileName = "
http://localhost:5080/swagger",
                 UseShellExecute = true,
             };

            Process.Start(startInfo); // 服务启动后用默认浏览器打开 Swagger
         });
     }

    app.UseCors("Frontend"); // 允许本机 Vue 开发地址访问

    List<PersonDto> people = new List<PersonDto>
     {
         new PersonDto
         {
             PersonNo = "P001",
             Name = "张明",
             Department = "研发部",
             Position = "开发工程师",
             Phone = "13800000001",
             EmploymentStatus = "在职",
         },
         new PersonDto
         {
             PersonNo = "P002",
             Name = "李芳",
             Department = "质检部",
             Position = "质量工程师",
             Phone = "13800000002",
             EmploymentStatus = "在职",
         },
         new PersonDto
         {
             PersonNo = "P003",
             Name = "王强",
             Department = "生产部",
             Position = "生产主管",
             Phone = "13800000003",
             EmploymentStatus = "在职",
         },
         new PersonDto
         {
             PersonNo = "P004",
             Name = "赵敏",
             Department = "行政部",
             Position = "行政专员",
             Phone = "13800000004",
             EmploymentStatus = "离职",
         },
         new PersonDto
         {
             PersonNo = "P005",
             Name = "张华",
             Department = "研发部",
             Position = "测试工程师",
             Phone = "13800000005",
             EmploymentStatus = "在职",
         },
     };

    // 根据姓名和部门查询人员。
     app.MapGet(
         "/api/people",
         ([FromQuery] string? name, [FromQuery] string? department) =>
         {
             IEnumerable<PersonDto> query = people;

            if (!string.IsNullOrWhiteSpace(name))
             {
                 query = query.Where(person =>
                     person.Name.Contains(
                         name.Trim(),
                         StringComparison.OrdinalIgnoreCase));
             }

            if (!string.IsNullOrWhiteSpace(department))
             {
                 query = query.Where(person =>
                     string.Equals(
                         person.Department,
                         department.Trim(),
                         StringComparison.OrdinalIgnoreCase));
             }

            List<PersonDto> result = query.ToList();

            return Results.Ok(result);
         })
         .WithName("QueryPeople")
         .Produces<List<PersonDto>>(StatusCodes.Status200OK);

    app.Run();

    /// <summary>
     /// 人员查询接口返回的数据结构。
     /// </summary>
     public sealed class PersonDto
     {
         public string PersonNo { get; set; } = string.Empty;

        public string Name { get; set; } = string.Empty;

        public string Department { get; set; } = string.Empty;

        public string Position { get; set; } = string.Empty;

        public string Phone { get; set; } = string.Empty;

        public string EmploymentStatus { get; set; } = string.Empty;
     }

保存文件。

---

## 18. 固定 API 地址并自动打开 Swagger

### 文件

整文件替换:

    ActivityAdmin.Api\Properties\launchSettings.json

### 完整内容

    {
       "$schema": "
http://json.schemastore.org/launchsettings.json",
       "profiles": {
         "http": {
           "commandName": "Project",
           "dotnetRunMessages": true,
           "launchBrowser": false,
           "launchUrl": "swagger",
           "applicationUrl": "
http://localhost:5080",
           "environmentVariables": {
             "ASPNETCORE_ENVIRONMENT": "Development"
           }
         }
       }
     }

保存文件。

---

## 19. 启动 API 并用 Swagger 验证

在 API 目录执行:

    dotnet run

预期浏览器自动打开:

    http://localhost:5080/swagger

如果没有自动打开,手工访问该地址。

### 验证 1:查询全部人员

1. 展开 GET /api/people。
2. 点击 Try it out。
3. name、department 都不填写。
4. 点击 Execute。
5. 应返回 5 条数据。

### 验证 2:按姓名查询

填写:

    name = 张

department 留空。

预期返回:

- 张明
- 张华

### 验证 3:按部门查询

name 留空。

填写:

    department = 研发部

预期返回研发部人员。

### 验证 4:组合查询

填写:

    name = 张
     department = 研发部

预期返回张明和张华。

保持 API 终端继续运行,不要关闭。

---

## 20. 启动前端

打开第二个 PowerShell 窗口:

   Set-Location 'C:\ElementPlus多页面跟练\activity-admin-web'
     pnpm dev

打开:

    http://localhost:5173/

预期:

- 左侧显示“基础数据”和“业务功能”。
- “基础数据”下有人员信息、部门信息。
- “业务功能”下有工单查询、质检查询。
- 默认进入人员信息。
- 人员表格自动显示 API 返回数据。
- 顶部显示面包屑。
- 人员信息标签自动打开。

---

## 21. 验证 Axios 参数查询

### 姓名查询

1. 人员信息页面输入“张”。
2. 点击查询。
3. 表格只显示姓名包含“张”的人员。
4. 浏览器开发者工具 Network 中应看到:

       /api/people?name=%E5%BC%A0

### 部门查询

1. 清空姓名。
2. 选择“研发部”。
3. 点击查询。
4. 表格只显示研发部人员。

### 组合查询

1. 姓名输入“张”。
2. 部门选择“研发部”。
3. 点击查询。
4. 请求同时包含 name 和 department。
5. 表格显示符合两个条件的人员。

### 重置

点击重置:

- 姓名清空。
- 部门清空。
- 表格恢复全部人员。

---

## 22. 验证四个独立路由

依次点击菜单:

    /basic/person
     /basic/department
     /business/work-order
     /business/quality

浏览器地址应随页面变化。

每个页面标题、面包屑和标签标题应同步变化。

---

## 23. 验证页面输入不会丢失

### 部门信息

1. 打开“部门信息”。
2. 输入:

       技术支持部

3. 不点击清空。

### 工单查询

1. 切换“工单查询”。
2. 输入:

       WO-20260725-001

### 质检查询

1. 切换“质检查询”。
2. 输入:

       QC-20260725-001

### 返回检查

1. 返回“部门信息”,应仍显示“技术支持部”。
2. 返回“工单查询”,应仍显示工单号。
3. 返回“质检查询”,应仍显示质检单号。
4. 返回“人员信息”,查询条件和上次表格结果应保留。

原因是 RouterView 内使用了 KeepAlive,页面组件没有被销毁。

刷新浏览器后内容会重置,这是本教程的预期行为。

---

## 24. 验证真实菜单折叠

点击顶部左侧圆形折叠按钮:

- 侧栏从 240px 缩到 64px。
- 菜单文字隐藏。
- 菜单图标保留。
- 内容区自动扩大。

再次点击:

- 侧栏恢复 240px。
- 一级、二级菜单文字恢复。

---

## 25. 验证多标签页

1. 依次打开四个菜单页面。
2. 顶部应出现四个标签。
3. 点击标签可切换路由。
4. 关闭当前标签,应自动进入相邻标签。
5. 只有一个标签时,不显示关闭操作。
6. 关闭标签后,从菜单重新打开同一页面,KeepAlive 缓存的输入仍然存在。

---

## 26. 验证 API 错误提示

1. 回到运行 API 的终端。
2. 按 Ctrl+C 停止 API。
3. 在人员信息页面点击查询。
4. 页面应显示 Element Plus 错误消息。
5. 表格变为空数据。
6. 重新执行 dotnet run。
7. 再次点击查询,数据恢复。

---

## 27. 构建前端

在前端目录执行:

    Set-Location 'C:\ElementPlus多页面跟练\activity-admin-web'
     pnpm build

预期:

- TypeScript 检查通过。
- Vite 构建成功。
- 输出目录为 activity-admin-web\dist。

---

## 28. 构建 API

在 API 目录执行:

    Set-Location 'C:\ElementPlus多页面跟练\ActivityAdmin.Api'
     dotnet build

预期:

    Build succeeded.

---

## 29. Self-contained 发布 API

执行:

    dotnet publish -c Release -r win-x64 --self-contained true -o .\publish\win-x64

预期生成:

    C:\ElementPlus多页面跟练\ActivityAdmin.Api\publish\win-x64\ActivityAdmin.Api.exe

手工启动发布产物:

    .\publish\win-x64\ActivityAdmin.Api.exe --urls http://localhost:5080

发布模式默认为 Production,不自动显示 Swagger,但可以访问:

    http://localhost:5080/api/people

看到 JSON 数据即表示 self-contained 发布成功。

按 Ctrl+C 停止。

---

## 30. 查看 Git 状态并提交

回到根目录:

    Set-Location 'C:\ElementPlus多页面跟练'
     git status --short

确认没有 node_modules、bin、obj、dist、publish 被纳入 Git。

添加源码:

    git add .

提交:

    git commit -m "feat: 添加 Element Plus 多页面后台与人员 API"

查看提交:

    git log -1 --oneline

---

## 31. 常见问题

### 前端启动后页面空白

检查浏览器控制台。

重点确认:

- main.ts 已 app.use(router)。
- App.vue 已引用 AdminLayout。
- router/index.ts 的四个组件路径正确。

### 人员页面一直报 Network Error

确认 API 终端正在运行:

    http://localhost:5080/swagger

确认 http.ts:

    baseURL: 'http://localhost:5080'

确认前端使用:

    http://localhost:5173

不要用 127.0.0.1:5173,否则与当前 CORS 来源不一致。

### Swagger 没有自动打开

手工访问:

    http://localhost:5080/swagger

并确认 ASPNETCORE_ENVIRONMENT 为 Development。

如果浏览器完全没有启动,检查 Program.cs 中是否保留了
`app.Lifetime.ApplicationStarted.Register(...)` 代码。

### 页面切换后输入丢失

检查 AdminLayout.vue 必须是:

    <router-view v-slot="{ Component, route: currentRoute }">
       <keep-alive>
         <component
           :is="Component"
           :key="currentRoute.name"
         />
       </keep-alive>
     </router-view>

不要把 keep-alive 写在 router-view 外面。

同时检查四个页面是否有稳定的 defineOptions name。

### 菜单点击后地址不变化

检查 el-menu 是否包含 router:

    <el-menu router>

每个 el-menu-item 的 index 必须与路由 path 完全一致。

### 标签关闭后页面不切换

检查 handleTabRemove:

- 先判断是否只剩一个标签。
- 删除目标标签。
- 如果关闭当前标签,router.push 到相邻标签。

### Element Plus 组件没有样式

检查 main.ts 是否同时包含:

    import ElementPlus from 'element-plus'
     import 'element-plus/dist/index.css'

### dotnet new 不识别 net10.0

执行:

    dotnet --list-sdks

需要安装 .NET 10 SDK。

### API 提示 AddSwaggerGen 不存在

确认已执行:

    dotnet add package Swashbuckle.AspNetCore --version 10.2.3

然后执行:

    dotnet restore
     dotnet build

---

## 32. 教程编制与验证记录

编制日期:2026-07-25。

已完成以下静态复核:

- 使用 `dotnet new webapi --help` 核对 `net10.0`、Minimal API 和 `--no-https` 参数。
- 核对 Element Plus 2.14.3 导出的 `TabPaneName`、`TabsPaneContext` 类型。
- 核对 Vue Router 的 `RouterView` 插槽与 `KeepAlive` 组合方式。
- 核对 Axios `params` 查询参数用法。
- 核对 Swashbuckle.AspNetCore 10.2.3 可从 NuGet 获取。
- 检查四个路由名、组件名、菜单地址、API 地址和 CORS 地址前后一致。
- 执行 Markdown 差异检查,未发现空白错误或待补内容。

按跟练要求,本教程编制阶段没有创建测试项目、没有编写或运行自动化测试,也没有代替学习者启动页面验证效果。第 19 至 29 阶段由学习者逐项运行和验收。

posted @ 2026-08-11 22:46  网络来者  阅读(24)  评论(0)    收藏  举报