| .. | ||
| src | ||
| .gitignore | ||
| Dockerfile | ||
| index.html | ||
| nginx.conf | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.tsbuildinfo | ||
| vite.config.ts | ||
后台管理系统前端开发文档
基于 Vue 3 + Element Plus + TypeScript 的后台管理系统前端模板。
技术栈
- Vue 3.5 + Composition API
- TypeScript 5.6
- Element Plus 2.9
- Pinia 状态管理
- Vue Router 4
- Vite 6 构建工具
目录结构
src/
├── api/ # API 接口
│ ├── auth.ts # 认证接口
│ ├── adminUser.ts # 管理员接口
│ ├── role.ts # 角色接口
│ ├── menu.ts # 菜单接口
│ ├── department.ts # 部门接口
│ ├── permission.ts # 权限接口
│ ├── dict.ts # 字典接口
│ ├── operationLog.ts # 操作日志接口
│ └── upload.ts # 上传接口
├── components/ # 公共组件
│ ├── ImageUpload/ # 图片上传组件
│ ├── DictSelect/ # 字典下拉组件
│ ├── DictRadio/ # 字典单选组件
│ └── DictCheckbox/ # 字典多选组件
├── directives/ # 自定义指令
│ └── permission.ts # 权限指令
├── layout/ # 布局组件
├── router/ # 路由配置
├── store/ # 状态管理
│ └── modules/
│ ├── user.ts # 用户状态
│ ├── permission.ts # 权限状态
│ └── theme.ts # 主题状态
├── styles/ # 全局样式
├── utils/ # 工具函数
│ ├── request.ts # 请求封装
│ ├── auth.ts # Token 管理
│ └── format.ts # 格式化工具
└── views/ # 页面
├── dashboard/ # 首页
├── login/ # 登录
├── password/ # 修改密码
├── profile/ # 个人中心
├── error/ # 错误页
└── system/ # 系统管理
├── user/ # 管理员管理
├── role/ # 角色管理
├── menu/ # 菜单管理
├── department/ # 部门管理
├── permission/ # 权限管理
├── dict/ # 字典管理
└── log/ # 操作日志
快速开始
# 安装依赖
npm install
# 开发模式
npm run dev
# 构建生产版本
npm run build
开发指南
1. 新增页面
- 在
src/views/下创建页面目录和 Vue 文件 - 在后台「菜单管理」中配置菜单,设置
component为页面路径(不含.vue后缀)
示例:创建商品列表页面
src/views/business/goods/index.vue
菜单配置:
- 路径:
/business/goods/list - 组件:
business/goods/index - 权限标识:
goods:list
2. 新增 API
在 src/api/ 下创建接口文件:
// src/api/goods.ts
import request from '@/utils/request'
export interface Goods {
id: number
name: string
price: number
status: number
}
// 获取商品列表
export function getGoodsList(params: { page: number; pageSize: number }) {
return request.get<Goods[]>('/api/admin/goods', { params })
}
// 创建商品
export function createGoods(data: Partial<Goods>) {
return request.post<Goods>('/api/admin/goods', data)
}
// 更新商品
export function updateGoods(id: number, data: Partial<Goods>) {
return request.put(`/api/admin/goods/${id}`, data)
}
// 删除商品
export function deleteGoods(id: number) {
return request.delete(`/api/admin/goods/${id}`)
}
3. 使用字典组件
字典组件用于动态加载下拉选项,避免在代码中硬编码。
步骤:
- 在「字典管理」中创建字典类型,如
goods_status - 添加字典数据项,如
上架(1)、下架(0) - 在页面中使用字典组件
DictSelect - 下拉选择
<template>
<el-form-item label="状态">
<DictSelect v-model="form.status" type="goods_status" />
</el-form-item>
</template>
Props:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | string | - | 字典类型编码(必填) |
| placeholder | string | '请选择' | 占位文本 |
| disabled | boolean | false | 是否禁用 |
| clearable | boolean | true | 是否可清空 |
| filterable | boolean | false | 是否可搜索 |
DictRadio - 单选
<template>
<el-form-item label="状态">
<DictRadio v-model="form.status" type="goods_status" />
<!-- 按钮样式 -->
<DictRadio v-model="form.status" type="goods_status" button />
</el-form-item>
</template>
DictCheckbox - 多选
<template>
<el-form-item label="标签">
<DictCheckbox v-model="form.tags" type="goods_tags" />
<!-- 按钮样式 -->
<DictCheckbox v-model="form.tags" type="goods_tags" button />
</el-form-item>
</template>
4. 权限控制
按钮级权限指令
使用 v-permission 指令控制按钮显示:
<template>
<el-button v-permission="'goods:create'" type="primary">新增</el-button>
<el-button v-permission="'goods:edit'" type="warning">编辑</el-button>
<el-button v-permission="'goods:delete'" type="danger">删除</el-button>
<!-- 多个权限(满足其一即可) -->
<el-button v-permission="['goods:edit', 'goods:delete']">操作</el-button>
</template>
代码中判断权限
import { usePermissionStore } from '@/store/modules/permission'
const permissionStore = usePermissionStore()
// 判断是否有权限
if (permissionStore.hasPermission('goods:create')) {
// 有权限
}
// 判断是否有任一权限
if (permissionStore.hasAnyPermission(['goods:edit', 'goods:delete'])) {
// 有权限
}
5. 图片上传
<template>
<el-form-item label="商品图片">
<!-- 单图上传 -->
<ImageUpload v-model="form.image" />
<!-- 多图上传 -->
<ImageUpload v-model="form.images" :limit="5" multiple />
</el-form-item>
</template>
Props:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| limit | number | 1 | 最大上传数量 |
| multiple | boolean | false | 是否多选 |
| accept | string | 'image/*' | 接受的文件类型 |
| maxSize | number | 5 | 最大文件大小(MB) |
6. 请求封装
src/utils/request.ts 已封装:
- 自动携带 Token
- 401 自动刷新 Token
- 统一错误处理
- 响应数据解构
import request from '@/utils/request'
// GET 请求
const res = await request.get('/api/admin/goods', { params: { page: 1 } })
// POST 请求
const res = await request.post('/api/admin/goods', { name: '商品1' })
// PUT 请求
const res = await request.put('/api/admin/goods/1', { name: '商品2' })
// DELETE 请求
const res = await request.delete('/api/admin/goods/1')
7. 页面模板
标准 CRUD 页面模板:
<template>
<div class="page-container">
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :model="queryParams" inline>
<el-form-item label="名称">
<el-input v-model="queryParams.keyword" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="状态">
<DictSelect v-model="queryParams.status" type="goods_status" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card>
<template #header>
<el-button v-permission="'goods:create'" type="primary" @click="handleAdd">
新增
</el-button>
</template>
<el-table v-loading="loading" :data="tableData">
<el-table-column prop="name" label="名称" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button v-permission="'goods:edit'" type="primary" link @click="handleEdit(row)">
编辑
</el-button>
<el-button v-permission="'goods:delete'" type="danger" link @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, sizes, prev, pager, next"
@change="loadData"
/>
</el-card>
<!-- 表单弹窗 -->
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑' : '新增'" width="500px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<DictRadio v-model="form.status" type="goods_status" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getGoodsList, createGoods, updateGoods, deleteGoods, type Goods } from '@/api/goods'
// 查询参数
const queryParams = reactive({
keyword: '',
status: '',
page: 1,
pageSize: 20
})
// 表格数据
const loading = ref(false)
const tableData = ref<Goods[]>([])
const total = ref(0)
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await getGoodsList(queryParams)
tableData.value = res.data?.items || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
queryParams.page = 1
loadData()
}
function handleReset() {
Object.assign(queryParams, { keyword: '', status: '', page: 1 })
loadData()
}
// 表单
const dialogVisible = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()
const form = reactive({ id: 0, name: '', status: 1 })
const rules: FormRules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
}
function handleAdd() {
Object.assign(form, { id: 0, name: '', status: 1 })
dialogVisible.value = true
}
function handleEdit(row: Goods) {
Object.assign(form, row)
dialogVisible.value = true
}
async function handleSubmit() {
await formRef.value?.validate()
submitting.value = true
try {
if (form.id) {
await updateGoods(form.id, form)
ElMessage.success('更新成功')
} else {
await createGoods(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadData()
} finally {
submitting.value = false
}
}
async function handleDelete(row: Goods) {
await ElMessageBox.confirm('确定删除吗?', '提示', { type: 'warning' })
await deleteGoods(row.id)
ElMessage.success('删除成功')
loadData()
}
onMounted(() => loadData())
</script>
<style scoped>
.page-container {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
构建部署
# 构建
npm run build
# 输出目录:../wwwroot
# 构建产物会自动输出到后端的 wwwroot 目录
注意事项
- 菜单配置的
component路径对应src/views/下的文件路径 - 权限标识建议使用
模块:操作格式,如goods:list、goods:create - 字典类型编码建议使用小写字母和下划线,如
order_status - API 接口统一使用
/api/admin/前缀