mirror of https://github.com/halo-dev/halo-admin
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
parent
86e86d138b
commit
7bde20a515
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Plugin } from "@/types/plugin";
|
||||
import type { Plugin } from "../types/plugin";
|
||||
|
||||
export function definePlugin(plugin: Plugin): Plugin {
|
||||
return plugin;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { BasicLayout } from "@/layouts";
|
||||
import { BasicLayout } from "./index";
|
||||
import {
|
||||
IconSettings,
|
||||
VButton,
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -115,7 +115,7 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
watch([() => plugin.value, () => group?.value], () => {
|
||||
handleFetchConfigMap();
|
||||
handleFetchSettings();
|
||||
handleFetchConfigMap();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue