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/
浏览器打开:
看到 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
预期浏览器自动打开:
如果没有自动打开,手工访问该地址。
### 验证 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
打开:
预期:
- 左侧显示“基础数据”和“业务功能”。
- “基础数据”下有人员信息、部门信息。
- “业务功能”下有工单查询、质检查询。
- 默认进入人员信息。
- 人员表格自动显示 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.ts:
baseURL: 'http://localhost:5080'
确认前端使用:
不要用 127.0.0.1:5173,否则与当前 CORS 来源不一致。
### 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 阶段由学习者逐项运行和验收。

浙公网安备 33010602011771号