搜索

📄 文章 📚 合集
热门搜索
🐘 PHP ⚡ Laravel 🎨 Vue.js ⚛️ React 📦 Yii 📘 JavaScript 🗄️ MySQL 🐳 Docker
返回合集

搭建 Vue 3 + Element Plus 后台

代码示例
# Vue 3 + Element Plus 后台搭建完整记录


## 1. 概述

### (1)目标

为「好站站」企业建站引擎搭建一个现代化的后台管理系统,实现以下功能:

- 独立 SPA(单页应用)架构,提升用户体验
- 美观的登录页面和仪表盘布局
- API 认证(登录/退出),保障后台安全
- 可扩展的页面管理基础,便于后续功能迭代

### (2)技术栈选型及原因

| 模块 | 技术 | 版本 | 选型原因 |
|------|------|------|---------|
| 前端框架 | Vue 3 | 3.x | Composition API 让代码更清晰,TypeScript 支持好,生态成熟 |
| UI 组件库 | Element Plus | 最新 | 国内 Vue 3 后台事实标准,组件丰富,文档完善 |
| 状态管理 | Pinia | 最新 | Vue 3 官方推荐,比 Vuex 更轻量,API 更简洁 |
| 路由 | Vue Router 4 | 4.x | Vue 官方路由,支持 SPA,与 Vue 3 完美集成 |
| HTTP 客户端 | Axios | 最新 | 支持拦截器、请求取消,API 简洁 |
| 拖拽库 | vue-draggable-next | 最新 | 基于 Sortable.js,支持触摸屏,用于可视化拖拽编辑器 |
| 认证方案 | Laravel Sanctum | 最新 | Laravel 官方 API 认证,轻量且安全,开箱即用 |
| 构建工具 | Vite | 6.x | 编译速度快,热更新秒级响应,开发体验好 |

### (3)整体架构设计

```
用户访问 /admin
    ↓
Laravel 路由返回 admin.blade.php 视图
    ↓
Vite 加载编译后的 JS/CSS
    ↓
Vue Router 根据 URL 显示对应组件
    ↓
Pinia 管理登录状态
    ↓
Axios 调用 Laravel Sanctum API
    ↓
后端验证 Token,返回数据
```

### (4)路由设计

| 路径 | 组件 | 权限 | 说明 |
|------|------|------|------|
| `/admin/login` | Login.vue | 未登录可访问 | 登录页面 |
| `/admin` | MainLayout.vue + Dashboard.vue | 需要登录 | 仪表盘 |
| `/admin/pages` | MainLayout.vue + Pages.vue | 需要登录 | 页面管理 |
| `/admin/site` | MainLayout.vue + SiteSettings.vue | 需要登录 | 站点配置 |

### (5)认证流程设计

```
用户访问 /admin
    ↓
路由守卫检查 isAuthenticated
    ↓ 未登录
重定向到 /admin/login
    ↓
用户输入邮箱密码
    ↓
调用 authStore.login()
    ↓
Axios POST /api/admin/login
    ↓
后端验证成功,返回 Token
    ↓
Token 存入 localStorage
设置 Axios 默认 Authorization Header
    ↓
跳转到 /admin 仪表盘
```

---

## 2. 安装依赖

### (1)前端依赖安装

**执行命令**:

```bash
cd D:\laragon\www\engine-api

npm install vue@^3.5 vue-router@^4.5
npm install element-plus
npm install @element-plus/icons-vue
npm install pinia
npm install axios
npm install vue-draggable-next
npm install -D @vitejs/plugin-vue
```

**原因解释**:

| 依赖 | 作用 |
|------|------|
| `vue@^3.5` | Vue 3 核心框架 |
| `vue-router@^4.5` | Vue 官方路由,实现 SPA 页面切换 |
| `element-plus` | UI 组件库,提供表格、表单、弹窗等组件 |
| `@element-plus/icons-vue` | Element Plus 官方图标库 |
| `pinia` | 状态管理,管理用户登录状态和 Token |
| `axios` | HTTP 客户端,调用后端 API |
| `vue-draggable-next` | 拖拽库,用于可视化拖拽编辑器 |
| `@vitejs/plugin-vue` | Vite 官方 Vue 3 插件,让 Vite 识别 `.vue` 文件 |

### (2)后端依赖安装

**执行命令**:

```bash
composer require laravel/sanctum
php artisan vendor:publish --tag=sanctum-migrations
php artisan migrate
```

**原因解释**:

| 命令 | 作用 |
|------|------|
| `composer require laravel/sanctum` | 安装 Laravel Sanctum 认证包 |
| `php artisan vendor:publish --tag=sanctum-migrations` | 发布 Sanctum 的数据库迁移文件 |
| `php artisan migrate` | 执行迁移,生成 `personal_access_tokens` 表 |

**Sanctum 工作原理**:

- 用户登录成功后,后端生成唯一的 Token 返回给前端
- 前端将 Token 存储在 localStorage 中
- 后续请求在 HTTP Header 中携带 Token:`Authorization: Bearer {token}`
- 后端验证 Token 有效性,识别用户身份

---

## 3. 配置 Vite

### (1)文件路径

`D:\laragon\www\engine-api\vite.config.js`

### (2)完整配置代码

```javascript
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
                'resources/js/admin/main.js',
            ],
            refresh: true,
        }),
        vue(),
    ],
});
```

### (3)配置解释

| 配置项 | 作用 | 原因 |
|--------|------|------|
| `laravel()` 插件 | 让 Vite 与 Laravel 集成,自动处理资源版本 | Laravel 官方推荐方式 |
| `input` 数组 | 定义三个入口文件 | 分离前端样式、前端主入口、后台主入口 |
| `vue()` 插件 | 让 Vite 支持 `.vue` 单文件组件 | Vue 3 官方插件 |
| `refresh: true` | 检测到 PHP/Blade 文件变化时自动刷新浏览器 | 提升开发体验 |

**设计原因**:后台使用独立入口 `resources/js/admin/main.js`,与前台分离,便于维护。

---

## 4. 创建后台目录结构

### (1)创建目录命令

```bash
cd resources/js
mkdir admin
cd admin
mkdir api
mkdir components
mkdir layouts
mkdir router
mkdir stores
mkdir views
```

### (2)最终目录结构

```
resources/js/admin/
├── api/              # API 请求封装
├── components/       # 公共组件
├── layouts/          # 布局组件(侧边栏、顶部栏)
├── router/           # 路由配置
├── stores/           # Pinia 状态管理
├── views/            # 页面组件
├── App.vue           # 根组件
└── main.js           # 入口文件
```

**设计原因**:这种目录结构是 Vue 3 后台项目的标准组织方式,职责清晰,便于团队协作和后期维护。

---

## 5. 创建后台入口文件

### (1)`main.js`

**文件路径**:`resources/js/admin/main.js`

**完整代码**:

```javascript
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);

// 注册所有 Element Plus 图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component);
}

app.use(createPinia());
app.use(router);
app.use(ElementPlus);

app.mount('#app');
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `createApp(App)` | 创建 Vue 应用实例 | Vue 3 应用启动入口 |
| `createPinia()` | 创建状态管理实例 | Pinia 是 Vue 3 官方推荐的状态管理 |
| `router` | 注入路由 | 实现 SPA 页面切换 |
| `ElementPlus` | 注入 UI 组件库 | 使用 Element Plus 组件 |
| `app.component(key, component)` | 注册图标组件 | 可在模板中直接使用 `<el-icon><User /></el-icon>` |
| `app.mount('#app')` | 挂载到 DOM 元素 | Vue 应用接管页面中的 `#app` 容器 |

### (2)`App.vue`

**文件路径**:`resources/js/admin/App.vue`

**完整代码**:

```vue
<template>
    <router-view />
</template>

<script setup>
import { onMounted } from 'vue';
import { useAuthStore } from './stores/auth';

const authStore = useAuthStore();

onMounted(() => {
    authStore.checkAuth();
});
</script>
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `<router-view />` | 路由出口,根据 URL 显示不同页面 | Vue Router 的核心组件 |
| `onMounted()` | 组件挂载后执行的生命周期钩子 | 确保 DOM 已渲染后再执行逻辑 |
| `authStore.checkAuth()` | 检查用户登录状态 | 页面刷新时验证 Token 是否有效 |

---

## 6. 配置路由

### (1)路由文件

**文件路径**:`resources/js/admin/router/index.js`

**完整代码**:

```javascript
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '../stores/auth';

const routes = [
    {
        path: '/admin/login',
        name: 'login',
        component: () => import('../views/Login.vue'),
        meta: { guest: true },
    },
    {
        path: '/admin',
        component: () => import('../layouts/MainLayout.vue'),
        meta: { requiresAuth: true },
        children: [
            {
                path: '',
                name: 'dashboard',
                component: () => import('../views/Dashboard.vue'),
            },
            {
                path: 'pages',
                name: 'pages',
                component: () => import('../views/Pages.vue'),
            },
            {
                path: 'site',
                name: 'site-settings',
                component: () => import('../views/SiteSettings.vue'),
            },
        ],
    },
];

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

// 路由守卫:在每次路由跳转前执行权限检查
router.beforeEach((to, from, next) => {
    const authStore = useAuthStore();
    const isAuthenticated = authStore.isAuthenticated;

    if (to.meta.requiresAuth && !isAuthenticated) {
        next('/admin/login');
    } else if (to.meta.guest && isAuthenticated) {
        next('/admin');
    } else {
        next();
    }
});

export default router;
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `createWebHistory()` | 使用 HTML5 历史模式 | URL 不带 `#` 号,更美观 |
| `meta: { guest: true }` | 标记为「游客页面」 | 只有未登录用户才能访问 |
| `meta: { requiresAuth: true }` | 标记为「需认证页面」 | 必须登录才能访问 |
| `children` 数组 | 嵌套路由 | 共用同一个布局组件(MainLayout) |
| `() => import(...)` | 动态导入组件 | 实现代码分割,减少首屏加载体积 |
| `router.beforeEach` | 全局前置守卫 | 在跳转前检查权限,决定是否允许访问 |

**路由守卫逻辑**:

```
访问 /admin/dashboard
    ↓
检查 requiresAuth: true
    ↓
isAuthenticated 是否 true?
    ↓ 否
重定向到 /admin/login
    ↓ 是
正常访问页面
```

---

## 7. 创建认证 Store

### (1)`auth.js`

**文件路径**:`resources/js/admin/stores/auth.js`

**完整代码**:

```javascript
import { defineStore } from 'pinia';
import axios from 'axios';

export const useAuthStore = defineStore('auth', {
    state: () => ({
        user: null,
        token: localStorage.getItem('token'),
    }),
    getters: {
        isAuthenticated: (state) => !!state.token,
    },
    actions: {
        async login(email, password) {
            try {
                const response = await axios.post('/api/admin/login', {
                    email,
                    password,
                });
                this.token = response.data.token;
                this.user = response.data.user;
                localStorage.setItem('token', this.token);
                axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
                return true;
            } catch (error) {
                return false;
            }
        },
        async logout() {
            try {
                await axios.post('/api/admin/logout');
            } catch (error) {
                // 忽略退出请求失败,仍然清除本地状态
            }
            this.token = null;
            this.user = null;
            localStorage.removeItem('token');
            delete axios.defaults.headers.common['Authorization'];
        },
        async checkAuth() {
            if (!this.token) return;
            axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
            try {
                const response = await axios.get('/api/admin/user');
                this.user = response.data;
            } catch {
                this.logout();
            }
        },
    },
});
```

**代码解释**:

| 模块 | 代码 | 作用 | 原因 |
|------|------|------|------|
| state | `user: null` | 存储用户信息 | 用于页面显示用户名称 |
| state | `token: localStorage.getItem('token')` | 从 localStorage 读取 token | 页面刷新后恢复登录状态 |
| getter | `isAuthenticated: (state) => !!state.token` | 计算是否已登录 | 用于路由守卫判断 |
| action | `login()` | 发送登录请求,保存 token | 核心认证逻辑 |
| action | `logout()` | 清除 token 和用户信息 | 退出登录 |
| action | `checkAuth()` | 验证 token 有效性 | 页面刷新时自动验证 |

**Token 存储位置选择原因**:

| 存储位置 | 优点 | 缺点 | 选择 |
|----------|------|------|------|
| localStorage | 持久化存储,页面刷新不丢失 | 可能受 XSS 攻击 | ✅ 采用(配合 CSP) |
| sessionStorage | 标签页关闭即清除 | 用户体验差 | ❌ 不选 |
| Cookie | 自动携带 | CSRF 风险 | ❌ 不选 |

---

## 8. 创建登录页面

### (1)`Login.vue`

**文件路径**:`resources/js/admin/views/Login.vue`

**完整代码**:

```vue
<template>
    <div>
        <el-card>
            <template #header>
                <div>
                    <h2>好站站后台管理</h2>
                    <p>登录开始建站</p>
                </div>
            </template>

            <el-form :model="form" :rules="rules" ref="formRef">
                <el-form-item prop="email">
                    <el-input
                        v-model="form.email"
                        placeholder="邮箱"
                        :prefix-icon="User"
                        size="large"
                    />
                </el-form-item>

                <el-form-item prop="password">
                    <el-input
                        v-model="form.password"
                        type="password"
                        placeholder="密码"
                        :prefix-icon="Lock"
                        size="large"
                        show-password
                    />
                </el-form-item>

                <el-form-item>
                    <el-button
                        type="primary"
                        size="large"
                        :loading="loading"
                        @click="handleLogin"
                        style="width: 100%"
                    >
                        登录
                    </el-button>
                </el-form-item>
            </el-form>
        </el-card>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { User, Lock } from '@element-plus/icons-vue';
import { useAuthStore } from '../stores/auth';

const router = useRouter();
const authStore = useAuthStore();
const formRef = ref();
const loading = ref(false);

const form = reactive({
    email: '',
    password: '',
});

const rules = {
    email: [
        { required: true, message: '请输入邮箱', trigger: 'blur' },
        { type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 6, message: '密码至少6位', trigger: 'blur' },
    ],
};

const handleLogin = async () => {
    if (!formRef.value) return;

    await formRef.value.validate(async (valid) => {
        if (!valid) return;

        loading.value = true;
        const success = await authStore.login(form.email, form.password);
        loading.value = false;

        if (success) {
            ElMessage.success('登录成功');
            router.push('/admin');
        } else {
            ElMessage.error('邮箱或密码错误');
        }
    });
};
</script>

<style scoped>
.login-container {
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-card {
    width: 400px;
}

.login-header {
    text-align: center;
}
</style>
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `el-card` | 卡片容器 | 让登录表单有层次感 |
| `el-form` + `rules` | 表单验证 | 前端校验,减少无效请求 |
| `:loading="loading"` | 按钮加载状态 | 防止重复提交 |
| `ref="formRef"` | 表单引用 | 调用表单验证方法 |
| `show-password` | 密码显示切换 | 用户体验优化 |
| `linear-gradient` 渐变背景 | 视觉美化 | 提升品牌感 |

---

## 9. 创建后台布局

### (1)`MainLayout.vue`

**文件路径**:`resources/js/admin/layouts/MainLayout.vue`

**完整代码**:

```vue
<template>
    <el-container>
        <!-- 侧边栏 -->
        <el-aside :width="isCollapse ? '64px' : '220px'">
            <div>
                <span v-if="!isCollapse">好站站</span>
                <span v-else>好</span>
            </div>
            <el-menu
                :collapse="isCollapse"
                router
                :default-active="$route.path"
                background-color="#001529"
                text-color="#bfbfbf"
                active-text-color="#fff"
            >
                <el-menu-item index="/admin">
                    <el-icon><Odometer /></el-icon>
                    <span>仪表盘</span>
                </el-menu-item>
                <el-menu-item index="/admin/pages">
                    <el-icon><Document /></el-icon>
                    <span>页面管理</span>
                </el-menu-item>
                <el-menu-item index="/admin/site">
                    <el-icon><Setting /></el-icon>
                    <span>站点配置</span>
                </el-menu-item>
            </el-menu>
        </el-aside>

        <el-container>
            <!-- 顶部栏 -->
            <el-header>
                <div>
                    <el-icon @click="toggleCollapse">
                        <Fold v-if="!isCollapse" />
                        <Expand v-else />
                    </el-icon>
                </div>
                <div>
                    <el-dropdown @command="handleCommand">
                        <span>
                            {{ authStore.user?.name }}
                            <el-icon><CaretBottom /></el-icon>
                        </span>
                        <template #dropdown>
                            <el-dropdown-menu>
                                <el-dropdown-item command="logout">退出登录</el-dropdown-item>
                            </el-dropdown-menu>
                        </template>
                    </el-dropdown>
                </div>
            </el-header>

            <!-- 主内容区 -->
            <el-main>
                <router-view />
            </el-main>
        </el-container>
    </el-container>
</template>

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '../stores/auth';
import {
    Odometer,
    Document,
    Setting,
    Fold,
    Expand,
    CaretBottom,
} from '@element-plus/icons-vue';

const router = useRouter();
const authStore = useAuthStore();
const isCollapse = ref(false);

const toggleCollapse = () => {
    isCollapse.value = !isCollapse.value;
};

const handleCommand = async (command) => {
    if (command === 'logout') {
        await authStore.logout();
        ElMessage.success('已退出登录');
        router.push('/admin/login');
    }
};
</script>

<style scoped>
.layout-container {
    height: 100vh;
}

.aside {
    background-color: #001529;
    transition: width 0.3s;
}

.logo {
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 20px;
    font-weight: bold;
    background-color: #002140;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
    border-bottom: 1px solid #e8e8e8;
    padding: 0 20px;
}

.collapse-btn {
    font-size: 20px;
    cursor: pointer;
}

.user-info {
    display: flex;
    align-items: center;
    gap: 5px;
    cursor: pointer;
}

.main {
    background-color: #f0f2f5;
    padding: 20px;
}
</style>
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `el-container` + `el-aside` + `el-header` + `el-main` | 经典后台布局 | Element Plus 提供的布局组件 |
| `:collapse="isCollapse"` | 侧边栏折叠状态 | 节省屏幕空间 |
| `router` 属性 | 启用路由模式 | 点击菜单自动跳转 |
| `:default-active="$route.path"` | 高亮当前菜单 | 用户知道自己在哪个页面 |
| `el-dropdown` | 用户下拉菜单 | 放置退出登录等操作 |
| `authStore.user?.name` | 可选链操作符 | 防止 user 为 null 时报错 |
| `toggleCollapse` | 切换折叠状态 | 用户体验优化 |

**布局结构**:

```
┌─────────────────────────────────────────────────────────┐
│  ┌──────┐  ┌─────────────────────────────────────────┐  │
│  │ 好站 │  │  折叠按钮                    用户 ▼    │  │
│  │ 站   │  ├─────────────────────────────────────────┤  │
│  ├──────┤  │                                         │  │
│  │ 仪表 │  │                                         │  │
│  │ 盘   │  │              主内容区                    │  │
│  ├──────┤  │          (router-view)                │  │
│  │ 页面 │  │                                         │  │
│  │ 管理 │  │                                         │  │
│  ├──────┤  │                                         │  │
│  │ 站点 │  │                                         │  │
│  │ 配置 │  │                                         │  │
│  └──────┘  └─────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
```

---

## 10. 创建仪表盘页面

### (1)`Dashboard.vue`

**文件路径**:`resources/js/admin/views/Dashboard.vue`

**完整代码**:

```vue
<template>
    <div>
        <el-row :gutter="20">
            <el-col :span="6">
                <el-card>
                    <div>{{ pageCount }}</div>
                    <div>页面总数</div>
                </el-card>
            </el-col>
            <el-col :span="6">
                <el-card>
                    <div>0</div>
                    <div>组件总数</div>
                </el-card>
            </el-col>
            <el-col :span="6">
                <el-card>
                    <div>0</div>
                    <div>今日访问</div>
                </el-card>
            </el-col>
            <el-col :span="6">
                <el-card>
                    <div>v1.0</div>
                    <div>系统版本</div>
                </el-card>
            </el-col>
        </el-row>

        <el-card style="margin-top: 20px">
            <h3>欢迎使用好站站</h3>
            <p>点击左侧菜单开始建站,或查看使用文档了解更多功能。</p>
        </el-card>
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';

const pageCount = ref(0);

onMounted(async () => {
    try {
        const response = await axios.get('/api/admin/pages');
        pageCount.value = response.data.length || 0;
    } catch (error) {
        console.error('获取页面统计失败', error);
    }
});
</script>

<style scoped>
.stat-card {
    text-align: center;
}

.stat-value {
    font-size: 32px;
    font-weight: bold;
    color: #1890ff;
}

.stat-label {
    color: #666;
    margin-top: 10px;
}

.welcome-card h3 {
    margin-bottom: 10px;
}

.welcome-card p {
    color: #666;
}
</style>
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `el-row` + `el-col` | 栅格布局 | 响应式,4 列等宽 |
| `:gutter="20"` | 列间距 | 卡片之间有间隔 |
| `:span="6"` | 每列占 6/24 | 一行 4 列(24 ÷ 4 = 6) |
| `onMounted` | 组件挂载后请求数据 | 页面加载时获取统计数据 |
| `pageCount.value` | 响应式数据 | 数据变化时自动更新视图 |

### (2)创建占位页面

**文件路径**:`resources/js/admin/views/Pages.vue`

```vue
<template>
    <el-card>
        <h3>页面管理</h3>
        <p>页面管理功能开发中...</p>
    </el-card>
</template>
```

**文件路径**:`resources/js/admin/views/SiteSettings.vue`

```vue
<template>
    <el-card>
        <h3>站点配置</h3>
        <p>站点配置功能开发中...</p>
    </el-card>
</template>
```

**设计原因**:先创建占位页面,确保路由跳转不会 404,后续再填充实际功能。

---

## 11. 配置后端 API

### (1)配置 Sanctum

**文件路径**:`config/sanctum.php`

找到 `stateful` 字段,修改为:

```php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort()
))),
```

**设计原因**:

| 设计点 | 原因 |
|--------|------|
| 不写死域名 | 使用 `Sanctum::currentApplicationUrlWithPort()` 动态获取 `APP_URL` |
| 用户安装适配 | 安装向导写入 `APP_URL` 到 `.env`,Sanctum 自动读取 |
| 本地开发兼容 | 保留 localhost、127.0.0.1 等本地域名 |

### (2)修改 `bootstrap/app.php`

**文件路径**:`bootstrap/app.php`

```php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->api(prepend: [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
```

**代码解释**:

| 代码 | 作用 | 原因 |
|------|------|------|
| `api: __DIR__.'/../routes/api.php'` | 注册 API 路由 | 确保 API 路由被加载 |
| `$middleware->api(prepend: [...])` | 在 API 中间件组最前面添加 Sanctum 中间件 | 确保 API 请求先经过 Sanctum 认证 |

### (3)修改 User 模型

**文件路径**:`app/Models/User.php`

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }
}
```

**设计原因**:`HasApiTokens` trait 提供了 `createToken()` 方法,用于生成 API Token。如果不添加,调用 `$user->createToken()` 会报 `Call to undefined method` 错误。

### (4)创建 AuthController

**执行命令**:

```bash
php artisan make:controller Admin/AuthController
```

**文件路径**:`app/Http/Controllers/Admin/AuthController.php`

**完整代码**:

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        // 验证请求参数
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        // 尝试登录
        if (!Auth::attempt($credentials)) {
            return response()->json([
                'message' => '邮箱或密码错误'
            ], 401);
        }

        // 获取用户并生成 Token
        $user = Auth::user();
        $token = $user->createToken('admin-token')->plainTextToken;

        return response()->json([
            'token' => $token,
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
        ]);
    }

    public function user(Request $request)
    {
        return response()->json($request->user());
    }

    public function logout(Request $request)
    {
        // 删除当前访问 Token
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => '已退出']);
    }
}
```

**代码解释**:

| 方法 | 路由 | 作用 | 认证要求 |
|------|------|------|---------|
| `login` | POST /api/admin/login | 验证邮箱密码,返回 Token | 无 |
| `user` | GET /api/admin/user | 获取当前登录用户信息 | 需要 Token |
| `logout` | POST /api/admin/logout | 删除 Token,退出登录 | 需要 Token |

### (5)配置 API 路由

**文件路径**:`routes/api.php`

```php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\AuthController;
use App\Http\Controllers\Admin\PageController;
use App\Http\Controllers\Admin\SiteController;

// 无需认证的路由
Route::post('/admin/login', [AuthController::class, 'login']);

// 需要认证的路由(使用 auth:sanctum 中间件)
Route::middleware('auth:sanctum')->prefix('admin')->group(function () {
    Route::get('/user', [AuthController::class, 'user']);
    Route::post('/logout', [AuthController::class, 'logout']);
    
    // 站点配置
    Route::get('/site', [SiteController::class, 'index']);
    Route::put('/site', [SiteController::class, 'update']);
    
    // 页面管理
    Route::get('/pages', [PageController::class, 'index']);
    Route::post('/pages', [PageController::class, 'store']);
    Route::get('/pages/{id}', [PageController::class, 'show']);
    Route::put('/pages/{id}', [PageController::class, 'update']);
    Route::delete('/pages/{id}', [PageController::class, 'destroy']);
});
```

**设计原因**:

- `auth:sanctum` 中间件会自动验证请求 Header 中的 `Authorization: Bearer {token}`
- `prefix('admin')` 将所有路由前缀加上 `/admin`,方便管理
- 无需认证的路由只有登录接口,其他都需要 Token

### (6)创建 PageController 和 SiteController

**执行命令**:

```bash
php artisan make:controller Admin/PageController
php artisan make:controller Admin/SiteController
```

**文件路径**:`app/Http/Controllers/Admin/PageController.php`

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Page;
use Illuminate\Http\Request;

class PageController extends Controller
{
    public function index()
    {
        $pages = Page::with('site')->get();
        return response()->json($pages);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'slug' => 'required|string|max:255|unique:pages',
            'is_home' => 'boolean',
            'status' => 'boolean',
        ]);

        $page = Page::create($validated);
        return response()->json($page, 201);
    }

    public function show($id)
    {
        $page = Page::with('components')->findOrFail($id);
        return response()->json($page);
    }

    public function update(Request $request, $id)
    {
        $page = Page::findOrFail($id);
        
        $validated = $request->validate([
            'title' => 'string|max:255',
            'slug' => 'string|max:255|unique:pages,slug,' . $id,
            'is_home' => 'boolean',
            'status' => 'boolean',
        ]);

        $page->update($validated);
        return response()->json($page);
    }

    public function destroy($id)
    {
        $page = Page::findOrFail($id);
        $page->delete();
        return response()->json(null, 204);
    }
}
```

**文件路径**:`app/Http/Controllers/Admin/SiteController.php`

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Site;
use Illuminate\Http\Request;

class SiteController extends Controller
{
    public function index()
    {
        $site = Site::first();
        return response()->json($site);
    }

    public function update(Request $request)
    {
        $site = Site::first();
        
        $validated = $request->validate([
            'site_name' => 'string|max:255',
            'site_logo' => 'string|max:500',
            'site_keywords' => 'string|max:500',
            'site_description' => 'string|nullable',
        ]);

        if ($site) {
            $site->update($validated);
        } else {
            $site = Site::create($validated);
        }
        
        return response()->json($site);
    }
}
```

**设计原因**:

- `PageController` 提供页面的 CRUD 操作
- `SiteController` 提供站点配置的获取和更新
- `with('site')` 和 `with('components')` 是 Eloquent 关联查询,减少数据库查询次数
- `findOrFail` 找不到记录时自动返回 404

---

## 12. 创建后台视图和路由

### (1)后台视图

**文件路径**:`resources/views/admin.blade.php`

```blade
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>好站站后台管理</title>
    @vite('resources/js/admin/main.js')
</head>
<body>
    <div id="app"></div>
</body>
</html>
```

**设计原因**:

- `@vite('resources/js/admin/main.js')` 是 Laravel Vite 指令,会自动引入编译后的 JS 和 CSS
- `<div id="app"></div>` 是 Vue 应用的挂载点
- 所有后台页面共用同一个视图,由 Vue Router 控制具体显示内容

### (2)添加后台路由

**文件路径**:`routes/web.php`

在文件末尾添加:

```php
// 后台 SPA 路由(必须放在最后,避免拦截其他路由)
Route::get('/admin/{any?}', function () {
    return view('admin');
})->where('any', '.*');
```

**设计原因**:

- `where('any', '.*')` 匹配所有 `/admin/*` 路径
- 必须放在路由文件最后,避免拦截其他具体路由(如 `/admin/login` 会被前端 Vue Router 处理)
- 所有后台请求都返回同一个视图,由 Vue Router 接管路由

---

## 13. 编译和访问

### (1)编译前端资源

**执行命令**:

```bash
npm run build
```

**预期输出**:

```
vite v6.4.2 building for production...
✓ 1672 modules transformed.
public/build/manifest.json
public/build/assets/main-CxRXhp-G.js
public/build/assets/main-Cahm-8S9.css
...
```

**设计原因**:Vite 会将 `resources/js/admin/main.js` 及其所有依赖编译成浏览器可识别的 JS/CSS 文件,输出到 `public/build` 目录。编译后的文件需要提交到 Git,用户下载后直接使用,不需要安装 Node.js。

### (2)访问后台

浏览器打开:`http://engine-api.test/admin`

**预期结果**:

- 自动跳转到 `http://engine-api.test/admin/login`
- 显示登录页面(紫色渐变背景)
- 输入管理员账号密码可登录
- 登录成功后跳转到仪表盘

---

## 14. 对一键安装的影响

### (1)影响分析

| 影响项 | 说明 | 状态 |
|--------|------|------|
| Sanctum 域名配置 | 使用动态获取,不写死 | ✅ 无影响 |
| 编译文件 | 提交到 Git,用户无需编译 | ✅ 无影响 |
| 后台路由 | 独立于安装路由 | ✅ 无影响 |
| 用户安装 | 安装完成后直接访问 `/admin` | ✅ 正常 |

### (2)确保事项

| 事项 | 处理方式 |
|------|---------|
| `.env` 中的 `APP_URL` 必须正确 | 安装向导已处理 |
| 编译后的前端文件必须存在 | 提交 `public/build/` 到 Git |
| Sanctum 配置动态读取域名 | 已修改为 `Sanctum::currentApplicationUrlWithPort()` |

---

## 15. 文件清单

```
resources/js/admin/
├── api/                      # API 请求封装(待完善)
├── components/               # 公共组件(待完善)
├── layouts/
│   └── MainLayout.vue        # 后台主布局
├── router/
│   └── index.js              # 路由配置
├── stores/
│   └── auth.js               # 认证状态管理
├── views/
│   ├── Login.vue             # 登录页面
│   ├── Dashboard.vue         # 仪表盘
│   ├── Pages.vue             # 页面管理(占位)
│   └── SiteSettings.vue      # 站点配置(占位)
├── App.vue                   # 根组件
└── main.js                   # 入口文件

app/Http/Controllers/Admin/
├── AuthController.php        # 认证控制器
├── PageController.php        # 页面管理控制器
└── SiteController.php        # 站点配置控制器

routes/
├── api.php                   # API 路由
└── web.php                   # Web 路由(添加了 /admin/{any?})

resources/views/
└── admin.blade.php           # 后台视图

config/
└── sanctum.php               # Sanctum 配置(修改了 stateful)
```

---

🧸 adorable code

专注 PHP、JavaScript、Laravel、Vue.js、React、Yii 全栈开发。记录技术探索过程中的灵感与经验,分享工程实践洞见。

hello@adorablecode.com

7