feat: add theme settings support (#593)

Signed-off-by: Ryan Wang <i@ryanc.cc>

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。
1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>.
2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.
3. 请确保你已经添加并运行了适当的测试。
3. Ensure you have added or ran the appropriate tests for your PR.
-->

#### What type of PR is this?

/kind feature
/milestone 2.0

<!--
添加其中一个类别:
Add one of the following kinds:

/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind optimization

适当添加其中一个或多个类别(可选):
Optionally add one or more of the following kinds if applicable:

/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

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

为主题添加设置表单支持,适配 https://github.com/halo-dev/halo/pull/2299

/hold until https://github.com/halo-dev/halo/pull/2299 merge

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

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

<!--
PR 合并时自动关闭 issue。
Automatically closes linked issue when PR is merged.

用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)`
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
#### Screenshots:

|<img width="1564" alt="image" src="https://user-images.githubusercontent.com/21301288/182583412-f14a890e-25ee-418c-b921-0924f94c0d1b.png">|<img width="1564" alt="image" src="https://user-images.githubusercontent.com/21301288/182583525-26fe165c-2dd0-4068-9d52-659dc626367a.png">|
| ---- | ---- |

<!--
如果此 PR 有 UI 的改动,最好截图说明这个 PR 的改动。
If there are UI changes to this PR, it is best to take a screenshot to illustrate the changes to this PR.

eg.

Before:

![screenshot-before](https://user-images.githubusercontent.com/screenshot.png)

After:

![screenshot-after](https://user-images.githubusercontent.com/screenshot.png)
-->

#### Special notes for your reviewer:

测试方式:

1. Halo 后端需要 checkout 到 https://github.com/halo-dev/halo/pull/2299 分支。
2. 将被测主题放置在 `~/halo-next/themes`,可以使用 https://github.com/ruibaby/theme-astro-starter
3. 使用主题内的 `theme.yaml` 和 `settings.yaml` 创建 `Theme` 和 `Setting` 的资源。
4. admin 需要 checkout 到当前 PR 的分支,启动开发服务之后在主题列表即可启用该主题。
5. 测试保存和更新主题配置。

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

<!--
如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。
否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change),
Release Note 需要以 `action required` 开头。
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note
None
```
pull/594/head
Ryan Wang 2022-08-04 09:44:13 +08:00 committed by GitHub
parent 86e86d138b
commit 7bde20a515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 527 additions and 258 deletions

View File

@ -34,7 +34,7 @@
"@formkit/vue": "1.0.0-beta.10",
"@halo-dev/admin-api": "^1.1.0",
"@halo-dev/admin-shared": "workspace:*",
"@halo-dev/api-client": "^0.0.7",
"@halo-dev/api-client": "^0.0.8",
"@halo-dev/components": "workspace:*",
"@vueuse/components": "^8.9.4",
"@vueuse/core": "^8.9.4",

View File

@ -7,7 +7,8 @@
],
"scripts": {
"dev": "vite build --watch",
"build": "vite build"
"build": "vite build",
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
},
"keywords": [],
"author": {
@ -35,7 +36,7 @@
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
"license": "MIT",
"dependencies": {
"@halo-dev/api-client": "^0.0.7",
"@halo-dev/api-client": "^0.0.8",
"@halo-dev/components": "workspace:*",
"axios": "^0.27.2"
},

View File

@ -1,4 +1,4 @@
import type { Plugin } from "@/types/plugin";
import type { Plugin } from "../types/plugin";
export function definePlugin(plugin: Plugin): Plugin {
return plugin;

View File

@ -8,14 +8,14 @@ import {
VRoutesMenu,
VTag,
} from "@halo-dev/components";
import type { MenuGroupType, MenuItemType } from "@/types/menus";
import type { MenuGroupType, MenuItemType } from "../types/menus";
import type { User } from "@halo-dev/api-client";
import logo from "@/assets/logo.svg";
import { RouterView, useRoute, useRouter } from "vue-router";
import { computed, inject, ref } from "vue";
const menus = inject<MenuGroupType>("menus");
const minimenus = inject<MenuItemType>("minimenus");
const menus = inject<MenuGroupType[]>("menus");
const minimenus = inject<MenuItemType[]>("minimenus");
const route = useRoute();
const router = useRouter();
@ -58,12 +58,12 @@ const currentRole = computed(() => {
</div>
<VRoutesMenu :menus="menus" />
<div class="current-profile">
<div v-if="currentUser.spec.avatar" class="profile-avatar">
<img :src="currentUser.spec.avatar" class="h-11 w-11 rounded-full" />
<div v-if="currentUser?.spec.avatar" class="profile-avatar">
<img :src="currentUser?.spec.avatar" class="h-11 w-11 rounded-full" />
</div>
<div class="profile-name">
<div class="flex text-sm font-medium">
{{ currentUser.spec.displayName }}
{{ currentUser?.spec.displayName }}
</div>
<div class="flex">
<VTag>
@ -89,14 +89,15 @@ const currentRole = computed(() => {
<!--bottom nav bar-->
<div
v-if="minimenus"
class="bottom-nav-bar fixed left-0 bottom-0 right-0 grid grid-cols-6 border-t-2 border-black drop-shadow-2xl mt-safe pb-safe md:hidden bg-secondary"
>
<div
v-for="(menu, index) in minimenus"
:key="index"
:class="{ 'bg-black': route.path === menu.path }"
:class="{ 'bg-black': route.path === menu?.path }"
class="nav-item"
@click="router.push(menu.path)"
@click="router.push(menu?.path)"
>
<div
class="flex w-full cursor-pointer items-center justify-center p-1 text-white"
@ -105,10 +106,10 @@ const currentRole = computed(() => {
class="is-active is-active0 flex h-10 w-10 flex-col items-center justify-center"
>
<div class="text-base">
<Component :is="menu.icon" />
<Component :is="menu?.icon" />
</div>
<div class="mt-0.5 text-xs">
{{ menu.name }}
{{ menu?.name }}
</div>
</div>
</div>

View File

@ -5,9 +5,9 @@ import { RouterView, useRoute, useRouter } from "vue-router";
import type { Ref } from "vue";
import { onMounted, provide, ref, watch } from "vue";
import type { Plugin } from "@halo-dev/api-client";
import type { FormKitSetting, FormKitSettingSpec } from "@/types/formkit";
import type { FormKitSetting, FormKitSettingSpec } from "../types/formkit";
import { BasicLayout } from "../layouts";
import { apiClient } from "@/utils/api-client";
import { apiClient } from "../utils/api-client";
interface PluginTab {
id: string;

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { BasicLayout } from "@/layouts";
import { BasicLayout } from "./index";
import {
IconSettings,
VButton,

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { BasicLayout } from "@/layouts";
import { BasicLayout } from "../layouts";
import { IconUpload, VButton, VTabbar } from "@halo-dev/components";
import { onMounted, provide, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { apiClient } from "@/utils/api-client";
import { apiClient } from "../utils/api-client";
import type { User } from "@halo-dev/api-client";
const tabs = [

View File

@ -1,7 +1,7 @@
import type { Component, Ref } from "vue";
import type { RouteRecordRaw } from "vue-router";
import type { MenuGroupType } from "./menus";
import type { PagesPublicState } from "@/states/pages";
import type { PagesPublicState } from "../states/pages";
export type ExtensionPointName = "PAGES" | "POSTS";

View File

@ -14,7 +14,7 @@ importers:
'@formkit/vue': 1.0.0-beta.10
'@halo-dev/admin-api': ^1.1.0
'@halo-dev/admin-shared': workspace:*
'@halo-dev/api-client': ^0.0.7
'@halo-dev/api-client': ^0.0.8
'@halo-dev/components': workspace:*
'@rushstack/eslint-patch': ^1.1.4
'@tailwindcss/aspect-ratio': ^0.4.0
@ -84,7 +84,7 @@ importers:
'@formkit/vue': 1.0.0-beta.10_h3koegrx2cxvlj7ceqfsnru344
'@halo-dev/admin-api': 1.1.0
'@halo-dev/admin-shared': link:packages/shared
'@halo-dev/api-client': 0.0.7
'@halo-dev/api-client': 0.0.8
'@halo-dev/components': link:packages/components
'@vueuse/components': 8.9.4_vue@3.2.37
'@vueuse/core': 8.9.4_vue@3.2.37
@ -174,12 +174,12 @@ importers:
packages/shared:
specifiers:
'@halo-dev/api-client': ^0.0.7
'@halo-dev/api-client': ^0.0.8
'@halo-dev/components': workspace:*
axios: ^0.27.2
vite-plugin-dts: ^1.4.0
dependencies:
'@halo-dev/api-client': 0.0.7
'@halo-dev/api-client': 0.0.8
'@halo-dev/components': link:../components
axios: 0.27.2
devDependencies:
@ -1873,8 +1873,8 @@ packages:
- debug
dev: false
/@halo-dev/api-client/0.0.7:
resolution: {integrity: sha512-UP36IYuSuDh/rRX+fwZAiPE+h/bhBLgh4XVfqVFaynHc3fLQhaUchqzqvep0HYAPHW4E7EDfQaNiFLXHUrH1bA==}
/@halo-dev/api-client/0.0.8:
resolution: {integrity: sha512-ANCJ9/O++FHyLfiREQQbNWHP0sj721VxoNqPgdG4ctDwbOdXpEyxgXKmOqGPadfThCtMolMS9XKDr6qG3q+xLQ==}
dev: false
/@halo-dev/logger/1.1.0:

View File

@ -8,13 +8,15 @@ const textClassification = {
};
const boxClassification = {
fieldset: "border border-gray-400 rounded-md px-2 pb-1",
fieldset:
"border border-gray-300 rounded-base px-2 pb-1 focus-within:border-primary",
legend: "font-bold text-sm",
wrapper: "flex items-center mb-1 cursor-pointer",
help: "mb-2",
input:
"form-check-input appearance-none h-5 w-5 mr-2 border border-gray-500 rounded-sm bg-white checked:bg-blue-500 focus:outline-none focus:ring-0 transition duration-200",
label: "text-sm text-gray-700 mt-1",
"form-check-input appearance-none h-4 w-4 mr-2 border border-gray-500 rounded-sm bg-white checked:bg-primary focus:outline-none focus:ring-0 transition duration-200",
label: "text-sm text-gray-700",
inner: "flex items-center",
};
const buttonClassification = {

View File

@ -1,239 +1,153 @@
<script lang="ts" setup>
import {
IconExchange,
IconEye,
IconPalette,
VAlert,
VButton,
VCard,
VPageHeader,
VSpace,
VTabbar,
VTag,
} from "@halo-dev/components";
import ThemeListModal from "./components/ThemeListModal.vue";
import { ref } from "vue";
import { VAlert, VSpace, VTag } from "@halo-dev/components";
import type { ComputedRef, Ref } from "vue";
import { computed, inject, ref } from "vue";
import { RouterLink } from "vue-router";
import type { Theme } from "@halo-dev/api-client";
import { useThemeLifeCycle } from "./composables/use-theme";
const selectedTheme = ref<Theme>({} as Theme);
const themesModal = ref(false);
const tabActiveId = ref("detail");
const themeListRef = ref();
const { isActivated, activatedTheme, handleActiveTheme } =
useThemeLifeCycle(selectedTheme);
const selectedTheme = inject<Ref<Theme>>(
"selectedTheme",
ref<Theme>({} as Theme)
);
const isActivated = inject<ComputedRef<boolean>>(
"isActivated",
computed(() => false)
);
</script>
<template>
<ThemeListModal
ref="themeListRef"
v-model:activated-theme="activatedTheme"
v-model:selected-theme="selectedTheme"
v-model:visible="themesModal"
/>
<VPageHeader :title="selectedTheme.spec?.displayName">
<template #icon>
<IconPalette class="mr-2 self-center" />
</template>
<template #actions>
<VSpace>
<VButton size="sm" type="default" @click="themesModal = true">
<template #icon>
<IconExchange class="h-full w-full" />
</template>
切换主题
</VButton>
<VButton
v-if="!isActivated"
size="sm"
type="primary"
@click="handleActiveTheme"
<div class="bg-white px-4 py-4 sm:px-6">
<div class="flex flex-row gap-3">
<div v-if="selectedTheme.spec?.logo">
<div
class="h-12 w-12 overflow-hidden rounded border bg-white hover:shadow-sm"
>
启用
</VButton>
<VButton :route="{ name: 'ThemeVisual' }" type="secondary">
<template #icon>
<IconEye class="h-full w-full" />
</template>
可视化编辑
</VButton>
</VSpace>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="tabActiveId"
:items="[
{ id: 'detail', label: '详情' },
{ id: 'settings', label: '基础设置' },
]"
class="w-full !rounded-none"
type="outline"
></VTabbar>
</template>
<div v-if="tabActiveId === 'detail'">
<div class="px-4 py-4 sm:px-6">
<div class="flex flex-row gap-3">
<div v-if="selectedTheme.spec?.logo">
<img
:alt="selectedTheme.spec?.displayName"
:src="selectedTheme.spec?.logo"
class="h-full w-full"
/>
</div>
</div>
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ selectedTheme.spec?.displayName }}
</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">
{{ selectedTheme.spec?.version }}
</span>
<VTag>
{{ isActivated ? "当前启用" : "未启用" }}
</VTag>
</p>
</div>
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.metadata?.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">作者</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.author?.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">网站</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme.spec?.website" target="_blank">
{{ selectedTheme.spec?.website }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">源码仓库</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme.spec?.repo" target="_blank">
{{ selectedTheme.spec?.repo }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">当前版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.require }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">存储位置</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0"></dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">插件依赖</dt>
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
<VAlert description="当前有 1 个插件还未安装" title="提示"></VAlert>
<ul class="mt-2 space-y-2">
<li>
<div
class="h-12 w-12 overflow-hidden rounded border bg-white hover:shadow-sm"
class="inline-flex w-96 cursor-pointer flex-row flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<img
:alt="selectedTheme.spec?.displayName"
:src="selectedTheme.spec?.logo"
class="h-full w-full"
/>
<RouterLink
:to="{
name: 'PluginDetail',
params: { name: 'PluginLinks' },
}"
class="font-medium text-gray-900 hover:text-blue-400"
>
run.halo.plugins.links
</RouterLink>
<div class="text-xs">
<VSpace>
<VTag> 已安装</VTag>
</VSpace>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ selectedTheme.spec?.displayName }}
</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">
{{ selectedTheme.spec?.version }}
</li>
<li>
<div
class="inline-flex w-96 cursor-pointer flex-row flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<span class="font-medium hover:text-blue-400">
run.halo.plugins.photos
</span>
<VTag>
{{ isActivated ? "当前启用" : "未启用" }}
</VTag>
</p>
</div>
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.metadata?.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">作者</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.author?.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">网站</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme.spec?.website" target="_blank">
{{ selectedTheme.spec?.website }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">源码仓库</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme.spec?.repo" target="_blank">
{{ selectedTheme.spec?.repo }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">当前版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme.spec?.require }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">存储位置</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">插件依赖</dt>
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
<VAlert
description="当前有 1 个插件还未安装"
title="提示"
></VAlert>
<ul class="mt-2 space-y-2">
<li>
<div
class="inline-flex w-96 cursor-pointer flex-row flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<RouterLink
:to="{
name: 'PluginDetail',
params: { name: 'PluginLinks' },
}"
class="font-medium text-gray-900 hover:text-blue-400"
>
run.halo.plugins.links
</RouterLink>
<div class="text-xs">
<VSpace>
<VTag> 已安装</VTag>
</VSpace>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-96 cursor-pointer flex-row flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<span class="font-medium hover:text-blue-400">
run.halo.plugins.photos
</span>
<div class="text-xs">
<VSpace>
<VTag>未安装</VTag>
</VSpace>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
</div>
<div class="text-xs">
<VSpace>
<VTag>未安装</VTag>
</VSpace>
</div>
</div>
</li>
</ul>
</dd>
</div>
<div v-if="tabActiveId === 'settings'" class="p-4 sm:px-6">
<div class="w-1/3">
<FormKit id="theme-setting-form" :actions="false" type="form">
<FormKit label="侧边栏宽度" type="text"></FormKit>
<FormKit label="侧边栏背景图" type="text"></FormKit>
<FormKit label="右上角图标" type="text"></FormKit>
<FormKit label="文章代码高亮语言" type="text"></FormKit>
</FormKit>
</div>
<div class="pt-5">
<div class="flex justify-start">
<VButton type="secondary"> 保存</VButton>
</div>
</div>
</div>
</VCard>
</dl>
</div>
</template>

View File

@ -0,0 +1,98 @@
<script lang="ts" setup>
import type { Ref } from "vue";
import { computed, inject, ref } from "vue";
import { VButton } from "@halo-dev/components";
import { apiClient } from "@halo-dev/admin-shared";
import type { ConfigMap, Theme } from "@halo-dev/api-client";
import type {
FormKitSetting,
FormKitSettingSpec,
} from "@halo-dev/admin-shared/src";
const settings = inject<Ref<FormKitSetting | undefined>>("settings");
const configmapFormData =
inject<Ref<Record<string, Record<string, string>> | undefined>>(
"configmapFormData"
);
const configmap = inject<Ref<ConfigMap>>("configmap", {} as Ref<ConfigMap>);
const selectedTheme = inject<Ref<Theme>>("selectedTheme", ref({} as Theme));
const group = inject<Ref<string | undefined>>("activeTab");
const saving = ref(false);
const formSchema = computed(() => {
if (!settings?.value?.spec) {
return;
}
return settings.value.spec.find((item) => item.group === group?.value)
?.formSchema;
});
const handleFetchSettings = inject<() => void>("handleFetchSettings");
const handleFetchConfigMap = inject<() => void>("handleFetchConfigMap");
const handleSaveConfigMap = async () => {
try {
saving.value = true;
if (
!configmap.value.metadata.name &&
selectedTheme.value.spec.configMapName
) {
configmap.value.metadata.name = selectedTheme.value.spec.configMapName;
}
settings?.value?.spec.forEach((item: FormKitSettingSpec) => {
// @ts-ignore
configmap.value.data[item.group] = JSON.stringify(
configmapFormData?.value?.[item.group]
);
});
if (!configmap.value.metadata.creationTimestamp) {
await apiClient.extension.configMap.createv1alpha1ConfigMap(
configmap.value
);
} else {
await apiClient.extension.configMap.updatev1alpha1ConfigMap(
configmap.value.metadata.name,
configmap.value
);
}
} catch (e) {
console.error(e);
} finally {
handleFetchSettings?.();
handleFetchConfigMap?.();
saving.value = false;
}
};
</script>
<template>
<div class="bg-white p-4 sm:px-6">
<div class="w-1/3">
<FormKit
v-if="group && formSchema && configmapFormData"
:id="group"
v-model="configmapFormData[group]"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
<FormKitSchema :schema="formSchema" />
</FormKit>
</div>
<div class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
>
保存
</VButton>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,246 @@
<script lang="ts" setup>
import {
IconExchange,
IconEye,
IconPalette,
VButton,
VCard,
VPageHeader,
VSpace,
VTabbar,
} from "@halo-dev/components";
import type {
FormKitSetting,
FormKitSettingSpec,
} from "@halo-dev/admin-shared";
import { apiClient, BasicLayout } from "@halo-dev/admin-shared";
import ThemeListModal from "../components/ThemeListModal.vue";
import type { ComputedRef, Ref } from "vue";
import { provide, ref, watch, watchEffect } from "vue";
import type { RouteLocationRaw } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import type { ConfigMap, Theme } from "@halo-dev/api-client";
import { useThemeLifeCycle } from "../composables/use-theme";
import cloneDeep from "lodash.clonedeep";
interface ThemeTab {
id: string;
label: string;
route: RouteLocationRaw;
}
const initialTabs: ThemeTab[] = [
{
id: "detail",
label: "详情",
route: {
name: "ThemeDetail",
},
},
];
const initialConfigMap: ConfigMap = {
data: {},
apiVersion: "v1alpha1",
kind: "ConfigMap",
metadata: {
name: "",
},
};
const tabs = ref<ThemeTab[]>(cloneDeep(initialTabs));
const selectedTheme = ref<Theme>({} as Theme);
const settings = ref<FormKitSetting | undefined>();
const configmapFormData = ref<
Record<string, Record<string, string>> | undefined
>();
const configmap = ref<ConfigMap>(cloneDeep(initialConfigMap));
const themesModal = ref(false);
const activeTab = ref("detail");
const { isActivated, activatedTheme, handleActiveTheme } =
useThemeLifeCycle(selectedTheme);
provide<Ref<Theme>>("activatedTheme", activatedTheme);
provide<Ref<Theme>>("selectedTheme", selectedTheme);
provide<Ref<FormKitSetting | undefined>>("settings", settings);
provide<Ref<Record<string, Record<string, string>> | undefined>>(
"configmapFormData",
configmapFormData
);
provide<Ref<ConfigMap>>("configmap", configmap);
provide<ComputedRef<boolean>>("isActivated", isActivated);
provide<Ref<string | undefined>>("activeTab", activeTab);
const route = useRoute();
const router = useRouter();
const handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id);
if (tab) {
router.push(tab.route);
}
};
const handleFetchSettings = async () => {
tabs.value = cloneDeep(initialTabs);
if (!selectedTheme.value?.spec?.settingName) {
return;
}
try {
const response = await apiClient.extension.setting.getv1alpha1Setting(
selectedTheme.value.spec.settingName as string
);
settings.value = response.data as FormKitSetting;
const { spec } = settings.value;
if (spec) {
tabs.value = [
...tabs.value,
...spec.map((item: FormKitSettingSpec) => {
return {
id: item.group,
label: item.label || "",
route: {
name: "ThemeSetting",
params: {
group: item.group,
},
},
};
}),
] as ThemeTab[];
onTabChange(route.name as string);
}
} catch (e) {
console.error(e);
}
};
const handleFetchConfigMap = async () => {
if (!selectedTheme.value.spec?.configMapName) {
return;
}
try {
const response = await apiClient.extension.configMap.getv1alpha1ConfigMap(
selectedTheme.value.spec?.configMapName as string
);
configmap.value = response.data;
const { data } = configmap.value;
if (data) {
configmapFormData.value = Object.keys(data).reduce((acc, key) => {
acc[key] = JSON.parse(data[key]);
return acc;
}, {});
}
} catch (e) {
console.error(e);
} finally {
if (!configmapFormData.value) {
configmapFormData.value = settings.value?.spec.reduce((acc, item) => {
acc[item.group] = {};
return acc;
}, {});
}
}
};
provide<() => void>("handleFetchSettings", handleFetchSettings);
provide<() => void>("handleFetchConfigMap", handleFetchConfigMap);
watchEffect(() => {
if (selectedTheme.value) {
handleFetchSettings();
handleFetchConfigMap();
}
});
const onTabChange = (routeName: string) => {
if (routeName === "ThemeSetting") {
const tab = tabs.value.find((tab) => {
return (
// @ts-ignore
tab.route.name === routeName &&
// @ts-ignore
tab.route.params.group === route.params.group
);
});
if (tab) {
activeTab.value = tab.id;
return;
}
router.push({ name: "ThemeDetail" });
return;
}
// @ts-ignore
const tab = tabs.value.find((tab) => tab.route.name === route.name);
activeTab.value = tab ? tab.id : tabs.value[0].id;
};
watch(
() => route.name,
async (newRouteName) => {
onTabChange(newRouteName as string);
}
);
</script>
<template>
<BasicLayout>
<ThemeListModal
v-model:activated-theme="activatedTheme"
v-model:selected-theme="selectedTheme"
v-model:visible="themesModal"
/>
<VPageHeader :title="selectedTheme.spec?.displayName">
<template #icon>
<IconPalette class="mr-2 self-center" />
</template>
<template #actions>
<VSpace>
<VButton size="sm" type="default" @click="themesModal = true">
<template #icon>
<IconExchange class="h-full w-full" />
</template>
切换主题
</VButton>
<VButton
v-if="!isActivated"
size="sm"
type="primary"
@click="handleActiveTheme"
>
启用
</VButton>
<VButton :route="{ name: 'ThemeVisual' }" type="secondary">
<template #icon>
<IconEye class="h-full w-full" />
</template>
可视化编辑
</VButton>
</VSpace>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="activeTab"
:items="tabs"
class="w-full !rounded-none"
type="outline"
@change="handleTabChange"
></VTabbar>
</template>
</VCard>
<div>
<RouterView :key="activeTab" />
</div>
</div>
</BasicLayout>
</template>

View File

@ -1,5 +1,7 @@
import { BasicLayout, BlankLayout, definePlugin } from "@halo-dev/admin-shared";
import { BlankLayout, definePlugin } from "@halo-dev/admin-shared";
import ThemeLayout from "./layouts/ThemeLayout.vue";
import ThemeDetail from "./ThemeDetail.vue";
import ThemeSetting from "./ThemeSetting.vue";
import Visual from "./Visual.vue";
import { IconPalette } from "@halo-dev/components";
@ -9,13 +11,18 @@ export default definePlugin({
routes: [
{
path: "/theme",
component: BasicLayout,
component: ThemeLayout,
children: [
{
path: "",
name: "Theme",
name: "ThemeDetail",
component: ThemeDetail,
},
{
path: "settings/:group",
name: "ThemeSetting",
component: ThemeSetting,
},
],
},
{

View File

@ -115,7 +115,7 @@ onMounted(() => {
});
watch([() => plugin.value, () => group?.value], () => {
handleFetchConfigMap();
handleFetchSettings();
handleFetchConfigMap();
});
</script>

View File

@ -1,7 +1,7 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"exclude": ["src/**/__tests__/*", "packages/**/*"],
"compilerOptions": {
"baseUrl": ".",
"composite": true,