# 实现一键发布功能
## 1。目标
在后台页面管理中添加「一键发布」按钮,点击后调用后端 API 执行静态文件生成命令,将所有已发布页面生成为 HTML 文件。
## 2。修改文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `routes/api.php` | 修改 | 添加发布接口路由 |
| `app/Http/Controllers/Admin/PageController.php` | 修改 | 添加 publish 方法 |
| `resources/js/admin/views/Pages.vue` | 修改 | 添加发布按钮和调用逻辑 |
---
## 3。修改详情
### 3.1 添加 API 路由
**文件路径**:`routes/api.php`
**修改位置**:在 `auth:sanctum` 中间件组内,页面管理路由之后
**新增代码**:
```php
// 一键发布
Route::post('/publish', [PageController::class, 'publish']);
```
**修改原因**:提供后端发布接口,供前端调用。
### 3.2 添加 publish 方法
**文件路径**:`app/Http/Controllers/Admin/PageController.php`
**修改位置**:在类中添加新方法
**新增代码**:
```php
use Illuminate\Support\Facades\Artisan;
/**
* 一键发布 - 生成静态页面
*/
public function publish()
{
try {
Artisan::call('generate:static');
$output = Artisan::output();
return response()->json([
'success' => true,
'message' => '静态页面生成成功',
'output' => $output
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '生成失败:' . $e->getMessage()
], 500);
}
}
```
**修改原因**:
- `Artisan::call('generate:static')`:调用 5-5 创建的静态文件生成命令
- 返回 JSON 格式的成功或失败信息
### 3.3 修改前端页面
**文件路径**:`resources/js/admin/views/Pages.vue`
**修改位置1:导入图标和 axios**
```typescript
import { Plus, Upload } from '@element-plus/icons-vue';
import axios from 'axios';
```
**修改位置2:添加发布状态变量**
```typescript
const publishing = ref(false);
```
**修改位置3:添加发布方法**
```typescript
const handlePublish = async () => {
publishing.value = true;
try {
const response = await axios.post('/api/admin/publish');
if (response.data.success) {
ElMessage.success(response.data.message);
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('发布失败,请检查服务器日志');
} finally {
publishing.value = false;
}
};
```
**修改位置4:修改模板头部区域**
**修改前**:
```vue
<template #header>
<div>
<span>页面管理</span>
<el-button type="primary" size="small" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建页面
</el-button>
</div>
</template>
```
**修改后**:
```vue
<template #header>
<div>
<span>页面管理</span>
<div>
<el-button type="success" size="small" @click="handlePublish" :loading="publishing">
<el-icon><Upload /></el-icon>
一键发布
</el-button>
<el-button type="primary" size="small" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建页面
</el-button>
</div>
</div>
</template>
```
**修改原因**:
- `handlePublish`:调用后端发布接口
- `:loading="publishing"`:防止重复提交,显示加载动画
- `ElMessage`:给用户明确的操作反馈
### 3.4 完整代码(Pages.vue 最终版)
**文件路径**:`resources/js/admin/views/Pages.vue`
```vue
<template>
<div>
<el-card>
<template #header>
<div>
<span>页面管理</span>
<div>
<el-button type="success" size="small" @click="handlePublish" :loading="publishing">
<el-icon><Upload /></el-icon>
一键发布
</el-button>
<el-button type="primary" size="small" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建页面
</el-button>
</div>
</div>
</template>
<el-table :data="pageStore.pages" v-loading="pageStore.loading" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="页面标题" />
<el-table-column prop="slug" label="访问别名" />
<el-table-column prop="is_home" label="是否首页" width="100">
<template #default="{ row }">
<el-tag :type="row.is_home ? 'success' : 'info'">
{{ row.is_home ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'danger'">
{{ row.status ? '已发布' : '草稿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="handleDialogClose"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="页面标题" prop="title">
<el-input v-model="form.title" placeholder="请输入页面标题" />
</el-form-item>
<el-form-item label="访问别名" prop="slug">
<el-input v-model="form.slug" placeholder="例如: about" />
</el-form-item>
<el-form-item label="是否首页" prop="is_home">
<el-switch v-model="form.is_home" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
active-value="1"
inactive-value="0"
active-text="发布"
inactive-text="草稿"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Upload } from '@element-plus/icons-vue';
import { usePageStore } from '../stores/page';
import { useRouter } from 'vue-router';
import axios from 'axios';
const pageStore = usePageStore();
const router = useRouter();
const dialogVisible = ref(false);
const dialogTitle = ref('');
const submitting = ref(false);
const formRef = ref();
const isEdit = ref(false);
const editId = ref<number | null>(null);
const publishing = ref(false);
const form = reactive({
title: '',
slug: '',
is_home: false,
status: false,
});
const rules = {
title: [{ required: true, message: '请输入页面标题', trigger: 'blur' }],
slug: [{ required: true, message: '请输入访问别名', trigger: 'blur' }],
};
const handleCreate = () => {
isEdit.value = false;
editId.value = null;
dialogTitle.value = '新建页面';
resetForm();
dialogVisible.value = true;
};
const handleEdit = (row: any) => {
router.push(`/admin/pages/${row.id}/edit`);
};
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确定要删除页面「${row.title}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
await pageStore.deletePage(row.id);
await pageStore.fetchPages();
}).catch(() => {});
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
submitting.value = true;
try {
if (isEdit.value && editId.value) {
await pageStore.updatePage(editId.value, form);
} else {
await pageStore.createPage(form);
}
dialogVisible.value = false;
await pageStore.fetchPages();
} finally {
submitting.value = false;
}
});
};
const handleDialogClose = () => {
resetForm();
formRef.value?.clearValidate();
};
const resetForm = () => {
form.title = '';
form.slug = '';
form.is_home = false;
form.status = false;
};
const handlePublish = async () => {
publishing.value = true;
try {
const response = await axios.post('/api/admin/publish');
if (response.data.success) {
ElMessage.success(response.data.message);
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('发布失败,请检查服务器日志');
} finally {
publishing.value = false;
}
};
onMounted(() => {
pageStore.fetchPages();
});
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
---
## 4。验证结果
| 验证项 | 预期结果 | 实际结果 |
|--------|---------|---------|
| 发布按钮显示 | 页面管理头部显示「一键发布」按钮 | ✅ |
| 点击发布 | 提示「静态页面生成成功」 | ✅ |
| 静态文件生成 | `public/*.html` 文件更新 | ✅ |
---