feat: add system setup support (halo-dev/console#632)

#### What type of PR is this?

/kind feature
/milestone 2.0

#### What this PR does / why we need it:

支持初始化默认数据。

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/2428

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试方式:

1. 清空数据库。
2. 启动之后登录即可跳转到初始化界面。
3. 测试初始化的功能是否正常。

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/3445/head
Ryan Wang 2022-09-30 17:46:19 +08:00 committed by GitHub
parent 4b6096ce20
commit 0e81366754
11 changed files with 385 additions and 0 deletions

View File

@ -0,0 +1,21 @@
import { useSystemStatesStore } from "@/stores/system-states";
import type { Router } from "vue-router";
export function setupCheckStatesGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
if (to.name === "Setup" || to.name === "Login") {
next();
}
const systemStateStore = useSystemStatesStore();
await systemStateStore.fetchSystemStates();
if (!systemStateStore.states.isSetup) {
next({ name: "Setup" });
return;
}
next();
});
}

View File

@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from "vue-router";
import routesConfig from "@/router/routes.config";
import { setupPermissionGuard } from "./guards/permission";
import { setupCheckStatesGuard } from "./guards/check-states";
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
@ -9,5 +10,6 @@ const router = createRouter({
});
setupPermissionGuard(router);
setupCheckStatesGuard(router);
export default router;

View File

@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router";
import NotFound from "@/views/exceptions/NotFound.vue";
import Forbidden from "@/views/exceptions/Forbidden.vue";
import { BasicLayout } from "@halo-dev/admin-shared";
import Setup from "@/views/system/Setup.vue";
export const routes: Array<RouteRecordRaw> = [
{
@ -20,6 +21,14 @@ export const routes: Array<RouteRecordRaw> = [
},
],
},
{
path: "/setup",
name: "Setup",
component: Setup,
meta: {
title: "系统初始化",
},
},
];
export default routes;

View File

@ -0,0 +1,37 @@
import { apiClient } from "@/utils/api-client";
import { defineStore } from "pinia";
interface SystemState {
isSetup: boolean;
}
interface SystemStatesState {
states: SystemState;
}
export const useSystemStatesStore = defineStore({
id: "system-states",
state: (): SystemStatesState => ({
states: {
isSetup: false,
},
}),
actions: {
async fetchSystemStates() {
try {
const { data } =
await apiClient.extension.configMap.getv1alpha1ConfigMap({
name: "system-states",
});
if (data.data) {
this.states = JSON.parse(data.data["states"]);
return;
}
this.states.isSetup = false;
} catch (error) {
this.states.isSetup = false;
}
},
},
});

148
src/views/system/Setup.vue Normal file
View File

@ -0,0 +1,148 @@
<script lang="ts" setup>
import logo from "@/assets/logo.svg";
import { useSettingForm } from "@/composables/use-setting-form";
import { useSystemStatesStore } from "@/stores/system-states";
import { apiClient } from "@/utils/api-client";
import { VButton } from "@halo-dev/components";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import category from "./setup-data/category.json";
import tag from "./setup-data/tag.json";
import post from "./setup-data/post.json";
import singlePage from "./setup-data/singlePage.json";
import menu from "./setup-data/menu.json";
import menuItems from "./setup-data/menu-items.json";
import type {
Category,
PostRequest,
SinglePageRequest,
Tag,
} from "@halo-dev/api-client";
const router = useRouter();
const {
configMapFormData,
handleSaveConfigMap,
handleFetchSettings,
handleFetchConfigMap,
} = useSettingForm(ref("system"), ref("system"));
const siteTitle = ref("");
const loading = ref(false);
const handleSubmit = async () => {
try {
loading.value = true;
// Set site title
if (configMapFormData.value) {
configMapFormData.value["basic"].title = siteTitle.value;
await handleSaveConfigMap();
}
// Create category / tag / post
await apiClient.extension.category.createcontentHaloRunV1alpha1Category({
category: category as Category,
});
await apiClient.extension.tag.createcontentHaloRunV1alpha1Tag({
tag: tag as Tag,
});
await apiClient.post.draftPost({ postRequest: post as PostRequest });
try {
await apiClient.post.publishPost({ name: post.post.metadata.name });
} catch (error) {
console.error("Failed to publish post", error);
}
// Create singlePage
await apiClient.singlePage.draftSinglePage({
singlePageRequest: singlePage as SinglePageRequest,
});
try {
await apiClient.singlePage.publishSinglePage({
name: singlePage.page.metadata.name,
});
} catch (error) {
console.error("Failed to publish singlePage", error);
}
// Create menu and menu items
const menuItemPromises = menuItems.map((item) => {
return apiClient.extension.menuItem.createv1alpha1MenuItem({
menuItem: item,
});
});
await Promise.all(menuItemPromises);
await apiClient.extension.menu.createv1alpha1Menu({ menu: menu });
// Create system-states ConfigMap
await apiClient.extension.configMap.createv1alpha1ConfigMap({
configMap: {
metadata: {
name: "system-states",
},
kind: "ConfigMap",
apiVersion: "v1alpha1",
data: {
states: JSON.stringify({ isSetup: true }),
},
},
});
router.push({ name: "Dashboard" });
} catch (error) {
console.error("Failed to setup", error);
} finally {
loading.value = false;
}
};
onMounted(async () => {
const systemStatesStore = useSystemStatesStore();
if (systemStatesStore.states.isSetup) {
router.push({ name: "Dashboard" });
return;
}
handleFetchSettings();
handleFetchConfigMap();
});
</script>
<template>
<div class="flex h-screen flex-col items-center justify-center">
<img :src="logo" alt="Logo" class="mb-8 w-20" />
<div class="flex w-72 flex-col gap-4">
<FormKit
id="setup-form"
name="setup-form"
:actions="false"
type="form"
@submit="handleSubmit"
@keyup.enter="$formkit.submit('setup-form')"
>
<FormKit
v-model="siteTitle"
:validation-messages="{
required: '请输入站点名称',
}"
type="text"
placeholder="站点名称"
validation="required"
></FormKit>
</FormKit>
<VButton
block
type="secondary"
:loading="loading"
@click="$formkit.submit('setup-form')"
>
初始化
</VButton>
</div>
</div>
</template>

View File

@ -0,0 +1,16 @@
{
"spec": {
"displayName": "默认分类",
"slug": "default",
"description": "这是你的默认分类,如不需要,删除即可。",
"cover": "",
"template": "",
"priority": 0,
"children": []
},
"apiVersion": "content.halo.run/v1alpha1",
"kind": "Category",
"metadata": {
"name": "76514a40-6ef1-4ed9-b58a-e26945bde3ca"
}
}

View File

@ -0,0 +1,59 @@
[
{
"spec": {
"displayName": "首页",
"href": "/",
"children": [],
"priority": 0
},
"apiVersion": "v1alpha1",
"kind": "MenuItem",
"metadata": { "name": "88c3f10b-321c-4092-86a8-70db00251b74" }
},
{
"spec": {
"children": [],
"priority": 1,
"categoryRef": {
"version": "content.halo.run/v1alpha1",
"kind": "Post",
"name": "76514a40-6ef1-4ed9-b58a-e26945bde3ca"
}
},
"apiVersion": "v1alpha1",
"kind": "MenuItem",
"metadata": { "name": "c4c814d1-0c2c-456b-8c96-4864965fee94" }
},
{
"spec": {
"displayName": "",
"href": "",
"children": [],
"priority": 2,
"tagRef": {
"version": "content.halo.run/v1alpha1",
"kind": "Tag",
"name": "c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"
}
},
"apiVersion": "v1alpha1",
"kind": "MenuItem",
"metadata": { "name": "35869bd3-33b5-448b-91ee-cf6517a59644" }
},
{
"spec": {
"displayName": "",
"href": "",
"children": [],
"priority": 3,
"singlePageRef": {
"version": "content.halo.run/v1alpha1",
"kind": "SinglePage",
"name": "373a5f79-f44f-441a-9df1-85a4f553ece8"
}
},
"apiVersion": "v1alpha1",
"kind": "MenuItem",
"metadata": { "name": "b0d041fa-dc99-48f6-a193-8604003379cf" }
}
]

View File

@ -0,0 +1,14 @@
{
"spec": {
"displayName": "主菜单",
"menuItems": [
"88c3f10b-321c-4092-86a8-70db00251b74",
"c4c814d1-0c2c-456b-8c96-4864965fee94",
"35869bd3-33b5-448b-91ee-cf6517a59644",
"b0d041fa-dc99-48f6-a193-8604003379cf"
]
},
"apiVersion": "v1alpha1",
"kind": "Menu",
"metadata": { "name": "2f0ef354-6f8f-40b9-b41d-2163f3e51b4f" }
}

View File

@ -0,0 +1,35 @@
{
"post": {
"spec": {
"title": "Hello Halo",
"slug": "hello-halo",
"template": "",
"cover": "",
"deleted": false,
"published": false,
"publishTime": "",
"pinned": false,
"allowComment": true,
"visible": "PUBLIC",
"version": 1,
"priority": 0,
"excerpt": {
"autoGenerate": false,
"raw": "如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 Halo 进行创作,希望能够使用愉快。"
},
"categories": ["76514a40-6ef1-4ed9-b58a-e26945bde3ca"],
"tags": ["c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"],
"htmlMetas": []
},
"apiVersion": "content.halo.run/v1alpha1",
"kind": "Post",
"metadata": {
"name": "5152aea5-c2e8-4717-8bba-2263d46e19d5"
}
},
"content": {
"raw": "<h2 id=\"hello-halo\"><strong>Hello Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run/\">Halo</a> 进行创作,希望能够使用愉快。</p><h2 id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run\">https://halo.run</a></p></li><li><p>文档:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>主题仓库:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run/themes.html\">https://halo.run/themes.html</a></p></li><li><p>开源地址:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>",
"content": "<h2 id=\"hello-halo\"><strong>Hello Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run/\">Halo</a> 进行创作,希望能够使用愉快。</p><h2 id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run\">https://halo.run</a></p></li><li><p>文档:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>主题仓库:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://halo.run/themes.html\">https://halo.run/themes.html</a></p></li><li><p>开源地址:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>",
"rawType": "HTML"
}
}

View File

@ -0,0 +1,31 @@
{
"page": {
"spec": {
"title": "关于",
"slug": "about",
"template": "",
"cover": "",
"deleted": false,
"published": false,
"publishTime": "",
"pinned": false,
"allowComment": true,
"visible": "PUBLIC",
"version": 1,
"priority": 0,
"excerpt": {
"autoGenerate": false,
"raw": "这是一个自定义页面,你可以在后台的 页面 -> 自定义页面 找到它,你可以用于新建关于页面、联系我们页面等等。"
},
"htmlMetas": []
},
"apiVersion": "content.halo.run/v1alpha1",
"kind": "SinglePage",
"metadata": { "name": "373a5f79-f44f-441a-9df1-85a4f553ece8" }
},
"content": {
"raw": "<h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code> -&gt; <code>自定义页面</code> 找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>",
"content": "<h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code> -&gt; <code>自定义页面</code> 找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>",
"rawType": "HTML"
}
}

View File

@ -0,0 +1,13 @@
{
"spec": {
"displayName": "Halo",
"slug": "halo",
"color": "#ad95b2",
"cover": ""
},
"apiVersion": "content.halo.run/v1alpha1",
"kind": "Tag",
"metadata": {
"name": "c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"
}
}