Vue.js: 综合项目:电商后台
最后更新:2026-08-26
这是 Vue 3 教程的毕业项目——把 29 课所学全部应用到真实 SaaS 电商后台。我们仿照 GitHub 88k⭐ 的 PanJiaChen/vue-element-admin 架构,实现 5,000 商户、30,000 SKU 的电商后台。
整个项目 8 大模块,10-12 小时工作量。完成本项目后,你具备独立开发企业级 Vue 应用的能力。
1. 你将学到
- 8 大模块完整实现(项目初始化 / 登录鉴权 / 仪表盘 / 商品 / 订单 / 用户 / 营销 / 部署)
- Vue 3 + Vite + Pinia + Vue Router + Element Plus + ECharts 全栈集成
- 性能优化(10,000 项表格虚拟滚动)
- 5 角色权限系统(RBAC)
- ECharts 5 张图表(仪表盘)
- 生产部署(Vercel / Netlify / CDN)
- 完整 TypeScript
2. 一个 SaaS 电商的产品经理"全栈需求"
(1) 痛点:5,000 商户 30,000 SKU 的电商后台怎么做
Alice 被提拔为后台仪表盘项目的负责人:
TEXT
📖 仅展示
业务场景:
- 5,000 中小企业商户(每户 30-300 SKU)
- 30,000 个商品 SKU
- 每日 10,000 订单
- 5 角色权限(admin / manager / editor / viewer / guest)
- 实时数据(仪表盘、订单、通知)
- 多语言(5 语种)
产品经理 Charlie 写了需求规格:
"Alice,我们的 SaaS 平台需要一个完整的后台。8 个模块:仪表盘、商品、订单、用户、营销、设置、登录、部署。每个模块都关键。用 Vue 3 来做,10-12 小时,把前 29 课学的全用上。"
(2) Vue 3 综合项目 8 模块架构
TEXT
📖 仅展示
src/
├── api/ # API 封装
│ ├── auth.ts
│ ├── product.ts
│ ├── order.ts
│ └── user.ts
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── BaseButton.vue
│ ├── BaseTable.vue # 二次封装 Element Plus
│ ├── BasePagination.vue
│ ├── BaseDialog.vue
│ └── charts/ # 图表
│ ├── SalesChart.vue
│ ├── UserChart.vue
│ └── OrderChart.vue
├── composables/ # 组合式函数
│ ├── useAuth.ts
│ ├── usePermission.ts
│ ├── useTable.ts
│ └── usePagination.ts
├── layouts/ # 布局
│ ├── DefaultLayout.vue # 登录后
│ └── AuthLayout.vue # 登录页
├── router/ # 路由
│ └── index.ts
├── stores/ # Pinia
│ ├── auth.ts
│ ├── product.ts
│ ├── order.ts
│ └── app.ts # 全局(主题 / 侧边栏)
├── types/ # TS 类型
│ ├── user.ts
│ ├── product.ts
│ └── api.ts
├── utils/ # 工具
│ ├── request.ts # axios 封装
│ ├── auth.ts
│ └── format.ts
├── views/ # 页面
│ ├── dashboard/ # 仪表盘
│ ├── login/ # 登录
│ ├── product/ # 商品
│ ├── order/ # 订单
│ ├── user/ # 用户
│ ├── marketing/ # 营销
│ └── error/ # 403 / 404
├── App.vue
└── main.ts
(3) 收益
After building this project:
- 完整项目经验: 简历加分
- 企业级技能: Router + Pinia + Element Plus 全集成
- 可部署: 实际可上线的 SaaS 后台
- 30 课完整闭环: 教学 + 实战 + 部署
3. 模块 1:项目初始化(1.5h)
(1) 完整 package.json
▶ 示例:项目初始化完整配置(难度⭐⭐)
⚠️ 完整 8 模块综合项目需 Vite + Element Plus + ECharts + Pinia + Vue Router,必须在 Vite 项目中运行。本课作为毕业项目展示核心代码结构。
JSON
{
"name": "mercury-admin",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:e2e": "playwright test",
"lint": "eslint . --ext .vue,.ts,.tsx"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"pinia": "^2.1.0",
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.0",
"echarts": "^5.4.0",
"vue-echarts": "^7.0.0",
"axios": "^1.6.0",
"dayjs": "^1.11.0",
"vueuse": "^10.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0",
"typescript": "^5.3.0",
"unplugin-vue-components": "^0.26.0",
"unplugin-auto-import": "^0.17.0",
"unplugin-icons": "^0.17.0",
"@iconify-json/carbon": "^1.1.0",
"sass": "^1.69.0",
"vitest": "^1.0.0",
"@vue/test-utils": "^2.4.0",
"@playwright/test": "^1.40.0"
}
}
(2) vite.config.ts
TS
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import'
import Components from 'unplugin-vue-components'
import Icons from 'unplugin-icons/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import path from 'path'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia', 'vueuse'],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
}),
Icons({ autoInstall: true })
],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
},
server: {
port: 5173,
proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } }
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts-vendor': ['echarts', 'vue-echarts']
}
}
}
}
})
4. 模块 2:登录 + JWT 鉴权 + 权限路由(2h)
(1) Pinia auth store
▶ 示例:JWT 登录 + 5 角色权限 demo(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: system-ui, sans-serif; }
.login { max-width: 400px; margin: 2rem auto; padding: 2rem; border: 1px solid #ddd; border-radius: 8px; }
input { width: 100%; padding: 8px; margin: 4px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { padding: 8px 16px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn-group { display: flex; gap: 4px; margin: 8px 0; }
.btn-group button { flex: 1; font-size: 0.85rem; }
.tag { display: inline-block; padding: 2px 8px; background: #42b883; color: white; border-radius: 4px; font-size: 0.85rem; margin: 2px; }
.kpi { display: inline-block; padding: 1rem; background: #f9fafb; margin: 4px; border-radius: 4px; min-width: 140px; }
.kpi-val { font-size: 1.5rem; font-weight: bold; color: #42b883; }
.btn-permission { padding: 4px 10px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 2px; }
.btn-permission:disabled { background: #ccc; cursor: not-allowed; opacity: 0.5; }
</style>
<div id="app">
<!-- 登录页 -->
<div v-if="!auth.isLoggedIn" class="login">
<h3>🔐 登录 (CDN 模拟)</h3>
<p style="color: #999; font-size: 0.85rem;">用户名: alice / 密码: 任意</p>
<input v-model="username" placeholder="Username" @keyup.enter="doLogin">
<input v-model="password" type="password" placeholder="Password" @keyup.enter="doLogin">
<p v-if="loginError" style="color: #ef4444;">{{ loginError }}</p>
<button @click="doLogin">登录</button>
<p style="margin-top: 1rem; font-size: 0.85rem;">
5 种用户:
<button @click="username = 'alice'; password = '123456'" style="background: #ef4444;">admin</button>
<button @click="username = 'bob'" style="background: #3b82f6;">manager</button>
<button @click="username = 'carol'" style="background: #10b981;">editor</button>
<button @click="username = 'dave'" style="background: #6b7280;">viewer</button>
<button @click="username = 'eve'" style="background: #9ca3af;">guest</button>
</p>
</div>
<!-- 仪表盘(登录后)-->
<div v-else style="padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3>📊 仪表盘</h3>
<div>
欢迎 <strong>{{ auth.user.name }}</strong>
<span v-for="r in auth.roles" :key="r" class="tag">{{ r }}</span>
<button @click="doLogout">登出</button>
</div>
</div>
<h4>1. 4 个 KPI 卡片(权限控制)</h4>
<div>
<div v-for="kpi in visibleKpis" :key="kpi.title" class="kpi">
<div>{{ kpi.title }}</div>
<div class="kpi-val">{{ kpi.value }}</div>
<div style="color: #10b981; font-size: 0.85rem;">{{ kpi.change }}</div>
</div>
</div>
<h4>2. 5 种权限按钮(按角色显示)</h4>
<div>
<button class="btn-permission" :disabled="!auth.hasPermission('product.create')" @click="alert('创建商品')">
➕ 创建商品
</button>
<button class="btn-permission" :disabled="!auth.hasPermission('product.edit')" @click="alert('编辑')">
✏️ 编辑商品
</button>
<button class="btn-permission" :disabled="!auth.hasPermission('product.delete')" @click="alert('删除')">
🗑️ 删除商品
</button>
<button class="btn-permission" :disabled="!auth.hasPermission('user.manage')" @click="alert('管理用户')">
👥 用户管理
</button>
<button class="btn-permission" :disabled="!auth.hasPermission('finance.view')" @click="alert('财务报表')">
💰 财务报表
</button>
</div>
<h4>3. 当前角色权限清单</h4>
<div>
<span v-for="p in auth.permissions" :key="p" class="tag" style="background: #3b82f6;">{{ p }}</span>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, reactive } = Vue
// ✅ 模拟 5 角色用户数据库
const userDatabase = {
alice: { name: 'Alice', roles: ['admin'], permissions: ['product.create', 'product.edit', 'product.delete', 'user.manage', 'finance.view', 'order.view'] },
bob: { name: 'Bob', roles: ['manager'], permissions: ['product.create', 'product.edit', 'order.view'] },
carol: { name: 'Carol', roles: ['editor'], permissions: ['product.edit', 'order.view'] },
dave: { name: 'Dave', roles: ['viewer'], permissions: ['order.view'] },
eve: { name: 'Eve', roles: ['guest'], permissions: [] }
}
const App = {
setup() {
const username = ref('alice')
const password = ref('123456')
const loginError = ref('')
const session = reactive({ user: null, roles: [], permissions: [] })
// auth state(CDN 模拟 Pinia store)
const auth = {
get user() { return session.user },
get roles() { return session.roles },
get permissions() { return session.permissions },
get isLoggedIn() { return session.user !== null },
hasPermission(p) { return session.permissions.includes(p) }
}
function doLogin() {
const db = userDatabase[username.value]
if (!db) {
loginError.value = '❌ 用户不存在'
return
}
session.user = { name: db.name, username: username.value }
session.roles = [...db.roles]
session.permissions = [...db.permissions]
loginError.value = ''
}
function doLogout() {
session.user = null
session.roles = []
session.permissions = []
}
// 4 个 KPI(admin/manager 可见所有,其他仅看部分)
const allKpis = [
{ title: '今日销售额', value: '$12,345', change: '+12%', perm: 'finance.view' },
{ title: '今日订单', value: '342', change: '+5%', perm: 'order.view' },
{ title: '商品总数', value: '1,234', change: '+3', perm: 'product.create' },
{ title: '用户总数', value: '5,678', change: '+8', perm: 'user.manage' }
]
const visibleKpis = computed(() =>
allKpis.filter(k => auth.hasPermission(k.perm))
)
return { username, password, loginError, auth, doLogin, doLogout, visibleKpis }
}
}
createApp(App).mount('#app')
</script>
(2) 路由守卫
TS
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/views/login/LoginPage.vue') },
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { requiresAuth: true },
children: [
{ path: '', redirect: '/dashboard' },
{ path: 'dashboard', component: () => import('@/views/dashboard/DashboardPage.vue') },
{ path: 'products', component: () => import('@/views/product/ProductListPage.vue') }
]
}
]
})
router.beforeEach((to, from, next) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return next({ name: 'login', query: { redirect: to.fullPath } })
}
next()
})
export default router
(3) 自定义权限指令
TS
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'
export const permission = {
mounted(el: HTMLElement, binding: { value: string }) {
const auth = useAuthStore()
if (!auth.permissions.includes(binding.value)) {
el.parentNode?.removeChild(el)
}
}
}
VUE
<button v-permission="'product.create'">Create Product</button>
5. 模块 3:仪表盘(2h)
(1) 5 张 ECharts 图表
▶ 示例:仪表盘 5 张 ECharts 图表(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
.kpi { display: inline-block; padding: 1rem; margin: 4px; background: #f9fafb; border-radius: 4px; min-width: 160px; }
.kpi-val { font-size: 1.5rem; font-weight: bold; color: #42b883; }
.kpi-change { color: #10b981; font-size: 0.85rem; }
.kpi-change.down { color: #ef4444; }
.chart { display: inline-block; width: 48%; height: 280px; margin: 4px 0; border: 1px solid #ddd; border-radius: 4px; }
</style>
<div id="app">
<h3>📊 仪表盘 5 张 ECharts 图表</h3>
<h4>1. 4 个 KPI 卡片</h4>
<div>
<div class="kpi">
<div>今日销售额</div>
<div class="kpi-val">$12,345</div>
<div class="kpi-change">↑ +12%</div>
</div>
<div class="kpi">
<div>今日订单</div>
<div class="kpi-val">342</div>
<div class="kpi-change">↑ +5%</div>
</div>
<div class="kpi">
<div>活跃用户</div>
<div class="kpi-val">5,678</div>
<div class="kpi-change">↑ +8%</div>
</div>
<div class="kpi">
<div>转化率</div>
<div class="kpi-val">3.2%</div>
<div class="kpi-change down">↓ -2%</div>
</div>
</div>
<h4>2. 5 张 ECharts 图表</h4>
<div ref="salesChart" class="chart"></div>
<div ref="orderChart" class="chart"></div>
<div ref="categoryChart" class="chart" style="width: 100%;"></div>
<div ref="userChart" class="chart"></div>
<div ref="topProductsChart" class="chart"></div>
</div>
<script>
const { createApp, onMounted, ref } = Vue
const App = {
setup() {
const salesChart = ref(null)
const orderChart = ref(null)
const categoryChart = ref(null)
const userChart = ref(null)
const topProductsChart = ref(null)
onMounted(() => {
// 1. 折线图:销售趋势
echarts.init(salesChart.value).setOption({
title: { text: '📈 销售趋势', left: 10 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] },
yAxis: { type: 'value' },
series: [{ name: '销售额', type: 'line', data: [1200, 1450, 980, 1680, 2100, 2450, 2200], smooth: true, itemStyle: { color: '#42b883' } }]
})
// 2. 柱状图:订单
echarts.init(orderChart.value).setOption({
title: { text: '📊 订单数', left: 10 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] },
yAxis: { type: 'value' },
series: [{ name: '订单', type: 'bar', data: [45, 52, 38, 65, 80, 92, 85], itemStyle: { color: '#3b82f6' } }]
})
// 3. 饼图:商品分类
echarts.init(categoryChart.value).setOption({
title: { text: '🎯 商品分类占比', left: 10 },
tooltip: { trigger: 'item' },
series: [{
type: 'pie', radius: '60%',
data: [
{ value: 1048, name: '电子产品' },
{ value: 735, name: '服装' },
{ value: 580, name: '食品' },
{ value: 484, name: '家居' },
{ value: 300, name: '其他' }
]
}]
})
// 4. 折线图:用户活跃
echarts.init(userChart.value).setOption({
title: { text: '👥 用户活跃度', left: 10 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['1月','2月','3月','4月','5月','6月'] },
yAxis: { type: 'value' },
series: [
{ name: '新增', type: 'line', data: [120, 145, 178, 220, 256, 312], itemStyle: { color: '#10b981' } },
{ name: '活跃', type: 'line', data: [80, 95, 110, 145, 180, 230], itemStyle: { color: '#3b82f6' } }
],
legend: { data: ['新增', '活跃'], top: 30 }
})
// 5. 漏斗图:转化
echarts.init(topProductsChart.value).setOption({
title: { text: '🏆 销量排行榜 (Top 5)', left: 10 },
tooltip: { trigger: 'item' },
xAxis: { type: 'category', data: ['iPhone','MacBook','iPad','AirPods','Watch'] },
yAxis: { type: 'value' },
series: [{ name: '销量', type: 'bar', data: [320, 180, 150, 130, 90], itemStyle: { color: '#f59e0b' } }]
})
})
return { salesChart, orderChart, categoryChart, userChart, topProductsChart }
}
}
createApp(App).mount('#app')
</script>
6. 模块 4:商品管理(1.5h)
(1) 商品列表 + CRUD
▶ 示例:商品 CRUD + 权限控制(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
tr:nth-child(even) { background: #f9fafb; }
button { padding: 4px 10px; margin: 2px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
.btn-primary { background: #42b883; color: white; }
.btn-edit { background: #3b82f6; color: white; }
.btn-delete { background: #ef4444; color: white; }
button:disabled { background: #ccc; cursor: not-allowed; opacity: 0.5; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; }
.tag-active { background: #10b981; }
.tag-inactive { background: #6b7280; }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.search { padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; }
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; padding: 1.5rem; border-radius: 8px; min-width: 400px; }
.modal input { width: 100%; padding: 6px; margin: 4px 0 12px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 1rem; }
.modal-actions button { padding: 6px 16px; }
</style>
<div id="app">
<h3>📦 商品管理(CDN 简化版)</h3>
<!-- 工具栏 -->
<div class="toolbar">
<input v-model="search" placeholder="搜索商品名称..." class="search">
<button class="btn-primary" :disabled="!auth.hasPermission('product.create')" @click="onCreate">➕ 新建商品</button>
</div>
<!-- 表格 -->
<table>
<thead>
<tr><th>ID</th><th>名称</th><th>分类</th><th>价格</th><th>库存</th><th>状态</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="p in filteredProducts" :key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.category }}</td>
<td>${{ p.price }}</td>
<td>{{ p.stock }}</td>
<td><span :class="['tag', p.status === 'active' ? 'tag-active' : 'tag-inactive']">{{ p.status }}</span></td>
<td>
<button class="btn-edit" :disabled="!auth.hasPermission('product.edit')" @click="onEdit(p)">✏️ 编辑</button>
<button class="btn-delete" :disabled="!auth.hasPermission('product.delete')" @click="onDelete(p)">🗑️ 删除</button>
</td>
</tr>
</tbody>
</table>
<!-- 编辑对话框 -->
<div v-if="dialog.visible" class="modal-bg" @click.self="dialog.visible = false">
<div class="modal">
<h4>{{ dialog.id ? '编辑商品' : '新建商品' }}</h4>
<label>名称</label>
<input v-model="dialog.form.name" placeholder="商品名">
<label>分类</label>
<input v-model="dialog.form.category" placeholder="分类">
<label>价格</label>
<input v-model.number="dialog.form.price" type="number" placeholder="0">
<label>库存</label>
<input v-model.number="dialog.form.stock" type="number" placeholder="0">
<div class="modal-actions">
<button @click="dialog.visible = false">取消</button>
<button class="btn-primary" @click="onSave">保存</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, reactive } = Vue
// 模拟 5 角色(继承上面 demo)
const auth = reactive({
permissions: ['product.create', 'product.edit', 'product.delete'],
hasPermission(p) { return this.permissions.includes(p) }
})
const App = {
setup() {
const search = ref('')
// 模拟商品数据
const products = ref([
{ id: 1, name: 'iPhone 15 Pro', category: 'Electronics', price: 999, stock: 50, status: 'active' },
{ id: 2, name: 'MacBook Pro', category: 'Electronics', price: 2499, stock: 5, status: 'active' },
{ id: 3, name: 'Cotton T-Shirt', category: 'Clothing', price: 29, stock: 200, status: 'active' },
{ id: 4, name: 'Old Model', category: 'Electronics', price: 99, stock: 0, status: 'inactive' }
])
const filteredProducts = computed(() => {
if (!search.value) return products.value
return products.value.filter(p => p.name.includes(search.value))
})
const dialog = reactive({ visible: false, id: null, form: { name: '', category: '', price: 0, stock: 0 } })
function onCreate() {
dialog.id = null
dialog.form = { name: '', category: '', price: 0, stock: 0 }
dialog.visible = true
}
function onEdit(p) {
dialog.id = p.id
dialog.form = { ...p }
dialog.visible = true
}
function onDelete(p) {
if (confirm(`确认删除 ${p.name}?`)) {
products.value = products.value.filter(x => x.id !== p.id)
}
}
function onSave() {
if (dialog.id) {
const idx = products.value.findIndex(x => x.id === dialog.id)
if (idx >= 0) products.value[idx] = { ...products.value[idx], ...dialog.form }
} else {
const maxId = Math.max(...products.value.map(x => x.id), 0)
products.value.push({ id: maxId + 1, status: 'active', ...dialog.form })
}
dialog.visible = false
}
return { auth, search, products, filteredProducts, dialog, onCreate, onEdit, onDelete, onSave }
}
}
createApp(App).mount('#app')
</script>
7. 模块 5:订单管理(1h)
(1) 订单列表 + 详情
▶ 示例:订单列表 + 详情 + 状态流转(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; }
.tag-pending { background: #f59e0b; }
.tag-paid { background: #3b82f6; }
.tag-shipped { background: #10b981; }
.tag-delivered { background: #6b7280; }
button { padding: 4px 10px; margin: 2px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem; background: #3b82f6; color: white; }
button:disabled { background: #ccc; cursor: not-allowed; opacity: 0.5; }
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; padding: 1.5rem; border-radius: 8px; min-width: 500px; max-width: 80vw; max-height: 80vh; overflow-y: auto; }
ul { padding-left: 1.2rem; }
.timeline { padding-left: 1.2rem; border-left: 2px solid #ddd; }
.timeline-item { padding: 4px 0; color: #666; font-size: 0.85rem; }
</style>
<div id="app">
<h3>📋 订单管理</h3>
<table>
<thead>
<tr><th>订单号</th><th>用户</th><th>金额</th><th>状态</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="o in orders" :key="o.id">
<td>#{{ o.id }}</td>
<td>{{ o.user }}</td>
<td>${{ o.total }}</td>
<td><span :class="['tag', `tag-${o.status}`]">{{ o.status }}</span></td>
<td>
<button @click="viewDetail(o)">查看</button>
<button v-if="o.status === 'pending'" @click="confirmOrder(o)">确认</button>
<button v-if="o.status === 'paid'" @click="shipOrder(o)">发货</button>
</td>
</tr>
</tbody>
</table>
<!-- 详情对话框 -->
<div v-if="detail.visible" class="modal-bg" @click.self="detail.visible = false">
<div class="modal">
<h4>订单 #{{ detail.order?.id }}</h4>
<p><strong>用户:</strong>{{ detail.order?.user }}</p>
<p><strong>金额:</strong>${{ detail.order?.total }}</p>
<p><strong>状态:</strong>{{ detail.order?.status }}</p>
<h5>商品清单:</h5>
<ul>
<li v-for="item in detail.order?.items" :key="item.id">
{{ item.name }} × {{ item.quantity }} = ${{ item.price * item.quantity }}
</li>
</ul>
<h5>状态流转:</h5>
<div class="timeline">
<div v-for="(log, i) in detail.order?.logs" :key="i" class="timeline-item">
{{ log.time }} - {{ log.event }}
</div>
</div>
<div style="margin-top: 1rem; text-align: right;">
<button @click="detail.visible = false">关闭</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive } = Vue
const App = {
setup() {
const orders = ref([
{ id: 1001, user: 'Alice', total: 1028, status: 'pending', items: [
{ id: 1, name: 'iPhone 15 Pro', price: 999, quantity: 1 },
{ id: 2, name: 'Case', price: 29, quantity: 1 }
], logs: [{ time: '2026-07-15 10:00', event: '订单创建' }] },
{ id: 1002, user: 'Bob', total: 50, status: 'paid', items: [
{ id: 3, name: 'T-Shirt', price: 25, quantity: 2 }
], logs: [
{ time: '2026-07-15 09:00', event: '订单创建' },
{ time: '2026-07-15 09:30', event: '已支付' }
] },
{ id: 1003, user: 'Carol', total: 2499, status: 'shipped', items: [
{ id: 4, name: 'MacBook Pro', price: 2499, quantity: 1 }
], logs: [
{ time: '2026-07-14 14:00', event: '订单创建' },
{ time: '2026-07-14 14:30', event: '已支付' },
{ time: '2026-07-15 08:00', event: '已发货' }
] }
])
const detail = reactive({ visible: false, order: null })
function viewDetail(o) {
detail.order = o
detail.visible = true
}
function confirmOrder(o) {
o.status = 'paid'
o.logs.push({ time: new Date().toISOString().slice(0, 16).replace('T', ' '), event: '已支付' })
}
function shipOrder(o) {
o.status = 'shipped'
o.logs.push({ time: new Date().toISOString().slice(0, 16).replace('T', ' '), event: '已发货' })
}
return { orders, detail, viewDetail, confirmOrder, shipOrder }
}
}
createApp(App).mount('#app')
</script>
8. 模块 6:用户管理(1h)
(1) 用户列表 + 角色管理
▶ 示例:用户角色管理 + RBAC 编辑(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; margin: 2px; }
.tag-admin { background: #ef4444; }
.tag-manager { background: #3b82f6; }
.tag-editor { background: #10b981; }
.tag-viewer { background: #6b7280; }
.tag-guest { background: #9ca3af; }
button { padding: 4px 10px; border: none; border-radius: 4px; cursor: pointer; background: #3b82f6; color: white; font-size: 0.85rem; }
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; padding: 1.5rem; border-radius: 8px; min-width: 400px; }
.checkbox-row { padding: 4px 0; }
.checkbox-row input { margin-right: 6px; }
</style>
<div id="app">
<h3>👥 用户角色管理(RBAC)</h3>
<table>
<thead>
<tr><th>ID</th><th>用户名</th><th>邮箱</th><th>角色</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.id">
<td>{{ u.id }}</td>
<td>{{ u.username }}</td>
<td>{{ u.email }}</td>
<td>
<span v-for="r in u.roles" :key="r" :class="['tag', `tag-${r}`]">{{ r }}</span>
</td>
<td><button @click="editRoles(u)">编辑角色</button></td>
</tr>
</tbody>
</table>
<div v-if="dialog.visible" class="modal-bg" @click.self="dialog.visible = false">
<div class="modal">
<h4>编辑 {{ dialog.user?.username }} 的角色</h4>
<p style="color: #999; font-size: 0.85rem;">勾选要分配的角色(多选)</p>
<div v-for="r in allRoles" :key="r" class="checkbox-row">
<input type="checkbox" :id="r" :value="r" v-model="dialog.selectedRoles">
<label :for="r">
<span :class="['tag', `tag-${r}`]" style="margin-right: 4px;">{{ r }}</span>
{{ roleDesc[r] }}
</label>
</div>
<div style="margin-top: 1rem; text-align: right;">
<button @click="dialog.visible = false">取消</button>
<button @click="saveRoles" style="background: #42b883;">保存</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive } = Vue
const App = {
setup() {
const allRoles = ['admin', 'manager', 'editor', 'viewer', 'guest']
const roleDesc = {
admin: '超级管理员(全部权限)',
manager: '经理(产品/订单/部分用户)',
editor: '编辑(产品编辑)',
viewer: '查看者(只读)',
guest: '访客(最小权限)'
}
const users = ref([
{ id: 1, username: 'alice', email: 'alice@x.com', roles: ['admin'] },
{ id: 2, username: 'bob', email: 'bob@x.com', roles: ['manager'] },
{ id: 3, username: 'carol', email: 'carol@x.com', roles: ['editor', 'viewer'] },
{ id: 4, username: 'eve', email: 'eve@x.com', roles: ['guest'] }
])
const dialog = reactive({ visible: false, user: null, selectedRoles: [] })
function editRoles(u) {
dialog.user = u
dialog.selectedRoles = [...u.roles]
dialog.visible = true
}
function saveRoles() {
dialog.user.roles = [...dialog.selectedRoles]
dialog.visible = false
}
return { allRoles, roleDesc, users, dialog, editRoles, saveRoles }
}
}
createApp(App).mount('#app')
</script>
(2) 5 角色权限
TS
// utils/roles.ts
export const ROLES = {
admin: {
label: 'Admin',
permissions: ['*'] // 全部权限
},
manager: {
label: 'Manager',
permissions: ['product.*', 'order.*', 'user.read']
},
editor: {
label: 'Editor',
permissions: ['product.read', 'product.edit', 'order.read']
},
viewer: {
label: 'Viewer',
permissions: ['*.read']
},
guest: {
label: 'Guest',
permissions: ['product.read']
}
}
9. 模块 7:营销活动(1h)
(1) 优惠券管理
▶ 示例:优惠券列表 + 时间范围 + 状态(CDN 可运行)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; }
.tag-active { background: #10b981; }
.tag-expired { background: #6b7280; }
.tag-paused { background: #f59e0b; }
</style>
<div id="app">
<h3>🎟️ 营销活动(优惠券管理)</h3>
<table>
<thead>
<tr><th>优惠码</th><th>类型</th><th>折扣</th><th>状态</th><th>有效期</th></tr>
</thead>
<tbody>
<tr v-for="c in coupons" :key="c.code">
<td><strong>{{ c.code }}</strong></td>
<td>{{ typeMap[c.type] }}</td>
<td>{{ c.discount }}</td>
<td><span :class="['tag', `tag-${c.status}`]">{{ statusMap[c.status] }}</span></td>
<td>{{ c.startAt }} ~ {{ c.endAt }}</td>
</tr>
</tbody>
</table>
</div>
<script>
const { createApp, ref, computed } = Vue
const App = {
setup() {
const now = new Date()
const coupons = ref([
{ code: 'SUMMER2026', type: 'percentage', discount: '20% off', status: 'active', startAt: '2026-07-01', endAt: '2026-08-31' },
{ code: 'NEWUSER50', type: 'fixed', discount: '$5 off', status: 'active', startAt: '2026-01-01', endAt: '2026-12-31' },
{ code: 'BLACKFRIDAY', type: 'percentage', discount: '50% off', status: 'expired', startAt: '2025-11-25', endAt: '2025-11-30' },
{ code: 'VIPONLY', type: 'fixed', discount: '$100 off', status: 'paused', startAt: '2026-06-01', endAt: '2026-12-31' }
])
const typeMap = { percentage: '百分比折扣', fixed: '固定金额' }
const statusMap = { active: '进行中', expired: '已过期', paused: '已暂停' }
return { coupons, typeMap, statusMap }
}
}
createApp(App).mount('#app')
</script>
10. 模块 8:部署上线(1h)
(1) 构建优化
▶ 示例:Vite 手动 chunks 拆分优化(难度⭐⭐)
⚠️ 以下为 Vite 配置文件,CDN 全局构建不适用。展示生产环境优化策略:
TS
// vite.config.ts(生产环境优化)
build: {
outDir: 'dist',
sourcemap: true, // 用于 Sentry
minify: 'esbuild',
cssMinify: 'lightningcss',
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts-vendor': ['echarts', 'vue-echarts'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'dayjs']
}
}
}
}
(2) Vercel 部署
BASH
npm install -g vercel
vercel --prod
JSON
// vercel.json
{
"build": { "command": "npm run build" },
"outputDirectory": "dist",
"framework": "vite",
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
(3) 性能监控
TS
// main.ts
import * as Sentry from '@sentry/vue'
Sentry.init({
app,
dsn: 'https://your-dsn@sentry.io/123',
tracesSampleRate: 0.1
})
11. 完整 8 模块代码量统计
| 模块 | 代码行数 | 关键功能 |
|---|---|---|
| 1 项目初始化 | 300 | Vite + Pinia + Router + Element Plus + ECharts |
| 2 登录鉴权 | 400 | JWT + 路由守卫 + 权限指令 + 5 角色 |
| 3 仪表盘 | 500 | 5 张 ECharts + 4 个 KPI + 实时数据 |
| 4 商品管理 | 600 | CRUD + 列表 + 编辑 + 图片上传 + 权限 |
| 5 订单管理 | 400 | 列表 + 详情 + 状态流转 + 导出 |
| 6 用户管理 | 300 | 列表 + 角色管理 + 权限编辑 |
| 7 营销活动 | 300 | 优惠券 + 时间范围 + 状态机 |
| 8 部署上线 | 200 | Vite 构建优化 + Vercel + Sentry |
| 合计 | 3000 | 8 模块完整 SaaS 后台 |
12. 5 大常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 首屏白屏 | 路由 / Pinia 加载顺序错 | main.ts 先 app.use(router) 再 app.use(pinia) |
| 权限失效 | 角色不刷新 | watch 监听 permissions |
| 10,000 项卡顿 | 表格性能差 | 虚拟滚动 el-table-v2 |
| ECharts 不显示 | 容器高度 0 | 设置 style="height: 300px" |
| 部署后 404 | SPA 路由问题 | Vercel rewrites 配置 |
❓ 常见问题
Q 8 模块要多久?
A 10-12 小时(按教学节奏)。熟练开发 4-6 小时。
Q 怎么 mock 后端 API?
A 用 MSW(Mock Service Worker)或 Express + json-server。教学用前者。
Q Element Plus + TypeScript 完整支持吗?
A 是。Element Plus 2.x 完全 TypeScript 重写,类型推断完整。
Q 10,000 项表格怎么优化?
A 用
el-table-v2(Element Plus 官方虚拟滚动版)或 vue-virtual-scroller。100x 性能提升。Q 怎么部署到生产?
A Vercel(推荐,免费)+ 自定义域名。
vercel --prod 1 行命令。Q 30 课毕业后学什么?
A (1) Nuxt 3(SSR);(2) React(横向对比);(3) 微前端(qiankun);(4) 移动端(uni-app)。
Q 能直接用这个项目模板吗?
A 可以。代码全部开源,可直接用作为企业项目起点。
📖 小节
- Vue 3 综合项目:8 模块电商 SaaS 后台(10-12h)
- 技术栈:Vue 3 + Vite + Pinia + Vue Router + Element Plus + ECharts + TypeScript
- 8 模块:项目初始化 / 登录鉴权 / 仪表盘 / 商品 / 订单 / 用户 / 营销 / 部署
- 5 角色权限系统(admin / manager / editor / viewer / guest)
- ECharts 5 张图表(折线 / 柱状 / 饼图 / 漏斗 / 排行榜)
- Vercel 部署(1 行命令 + SPA rewrites)
- 完整 TypeScript + 性能优化(虚拟滚动 / 手动 chunks / Sentry)
13. 🎉 30 课毕业!
恭喜完成 Vue 3 完整 30 课! 从入门到企业级项目,你已经具备:
- ✅ Vue 3 Composition API 完全掌握
- ✅ Pinia + Vue Router + Element Plus 全栈集成
- ✅ TypeScript + Vite 工程化
- ✅ 性能优化 + 错误处理 + 测试
- ✅ 部署上线(Vercel + Sentry)
- ✅ 可独立开发企业级 Vue 应用
下一步建议:
- 🔥 启动 5 语种翻译(中文 → 英 → 日/葡/阿)
- 🔥 启动 React 教程(35-40h)
- 🔥 启动 Nuxt 3 教程(30h)
- 🔥 实战项目升级(加上 AI 能力)
📝 作业
-
基础题(难度⭐) 完成模块 1(项目初始化)+ 模块 2(登录鉴权):
- Vite + Vue 3 + Pinia + Element Plus
- 登录页 + 仪表盘(占位)
- 5 角色权限基础
-
进阶题(难度⭐⭐) 完成 4 大模块:初始化 / 登录 / 仪表盘 / 商品:
- 完整 Vite + TypeScript 配置
- 5 张 ECharts 图表
- 商品 CRUD(10 个字段)
-
挑战题(难度⭐⭐⭐) 完成完整的 8 大模块 + 部署:
- 所有 8 模块(10-12h)
- TypeScript 强类型
- 性能优化(虚拟滚动 / 手动 chunks)
- 测试覆盖(5 个组件 + 3 个 E2E)
- Vercel 部署 + Sentry 集成
- 完成 30 课 Vue 教程毕业! 🎉