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. 你将学到


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:


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>
逻辑代码 124 行(超过 40 行限制,仅展示)

(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>
逻辑代码 104 行(超过 40 行限制,仅展示)

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>
逻辑代码 116 行(超过 40 行限制,仅展示)

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>
逻辑代码 104 行(超过 40 行限制,仅展示)

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>
逻辑代码 87 行(超过 40 行限制,仅展示)

(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>
逻辑代码 46 行(超过 40 行限制,仅展示)

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 项表格怎么优化?
Ael-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 可以。代码全部开源,可直接用作为企业项目起点。

📖 小节


13. 🎉 30 课毕业!

恭喜完成 Vue 3 完整 30 课! 从入门到企业级项目,你已经具备:

下一步建议


📝 作业

  1. 基础题(难度⭐) 完成模块 1(项目初始化)+ 模块 2(登录鉴权):

    1. Vite + Vue 3 + Pinia + Element Plus
    2. 登录页 + 仪表盘(占位)
    3. 5 角色权限基础
  2. 进阶题(难度⭐⭐) 完成 4 大模块:初始化 / 登录 / 仪表盘 / 商品:

    1. 完整 Vite + TypeScript 配置
    2. 5 张 ECharts 图表
    3. 商品 CRUD(10 个字段)
  3. 挑战题(难度⭐⭐⭐) 完成完整的 8 大模块 + 部署:

    1. 所有 8 模块(10-12h)
    2. TypeScript 强类型
    3. 性能优化(虚拟滚动 / 手动 chunks)
    4. 测试覆盖(5 个组件 + 3 个 E2E)
    5. Vercel 部署 + Sentry 集成
    6. 完成 30 课 Vue 教程毕业! 🎉
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏