mirror of https://github.com/halo-dev/halo-admin
feat: add setup post/singlePage/category custom templates support (#671)
#### What type of PR is this? /kind feature /milestone 2.0 #### What this PR does / why we need it: 支持为文章/自定义页面/分类设置自定义模板。适配 https://github.com/halo-dev/halo/pull/2638 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2569 #### Screenshots: <img width="625" alt="image" src="https://user-images.githubusercontent.com/21301288/198823380-991a702d-aae7-4587-b0f8-81fcb018a1f6.png"> #### Special notes for your reviewer: /cc @halo-dev/sig-halo-console 测试方式: 1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/2638 PR 的分支。 2. 根据 https://github.com/halo-dev/halo/pull/2638 PR 中的描述修改主题配置 `theme.yaml`,添加所需测试的模板配置。 3. 检查 Console 对应的设置项(分类编辑、文章设置、自定义页面)中的自定义模板选择框是否包含配置的模板。 4. 选择配置的模板后保存。检查主题端对应页面是否一致。 #### Does this PR introduce a user-facing change? ```release-note 支持为文章/自定义页面/分类设置自定义模板。 ```pull/678/head
parent
3d638fde37
commit
ff26058fc0
|
@ -33,7 +33,7 @@
|
|||
"@formkit/inputs": "^1.0.0-beta.11",
|
||||
"@formkit/themes": "^1.0.0-beta.11",
|
||||
"@formkit/vue": "^1.0.0-beta.11",
|
||||
"@halo-dev/api-client": "^0.0.39",
|
||||
"@halo-dev/api-client": "^0.0.40",
|
||||
"@halo-dev/components": "workspace:*",
|
||||
"@halo-dev/console-shared": "workspace:*",
|
||||
"@halo-dev/richtext-editor": "^0.0.0-alpha.8",
|
||||
|
|
|
@ -13,7 +13,7 @@ importers:
|
|||
'@formkit/inputs': ^1.0.0-beta.11
|
||||
'@formkit/themes': ^1.0.0-beta.11
|
||||
'@formkit/vue': ^1.0.0-beta.11
|
||||
'@halo-dev/api-client': ^0.0.39
|
||||
'@halo-dev/api-client': ^0.0.40
|
||||
'@halo-dev/components': workspace:*
|
||||
'@halo-dev/console-shared': workspace:*
|
||||
'@halo-dev/richtext-editor': ^0.0.0-alpha.8
|
||||
|
@ -108,7 +108,7 @@ importers:
|
|||
'@formkit/inputs': 1.0.0-beta.11
|
||||
'@formkit/themes': 1.0.0-beta.11_tailwindcss@3.2.1
|
||||
'@formkit/vue': 1.0.0-beta.11_vjnbgdptsk6bkj7ab5a6mk2cwm
|
||||
'@halo-dev/api-client': 0.0.39
|
||||
'@halo-dev/api-client': 0.0.40
|
||||
'@halo-dev/components': link:packages/components
|
||||
'@halo-dev/console-shared': link:packages/shared
|
||||
'@halo-dev/richtext-editor': 0.0.0-alpha.8_vue@3.2.41
|
||||
|
@ -1953,8 +1953,8 @@ packages:
|
|||
- windicss
|
||||
dev: false
|
||||
|
||||
/@halo-dev/api-client/0.0.39:
|
||||
resolution: {integrity: sha512-GuTTJDOj0PPMXo3KTiNYGACRUXqJKnjnApK303eNPWqVodgR3mJVLFTXwa+euAJOkcSG3KkB5OtUFAkZeHRbPA==}
|
||||
/@halo-dev/api-client/0.0.40:
|
||||
resolution: {integrity: sha512-SsB+PZkoCwp0cGKGQY+x2oDWiZDwHYH9nytN28b+kY+HnWmNIWgMT64ZnHoHxOnk44m9S9byxwkkvlOamby0AQ==}
|
||||
dev: false
|
||||
|
||||
/@halo-dev/richtext-editor/0.0.0-alpha.8_vue@3.2.41:
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
IconPages,
|
||||
IconUserSettings,
|
||||
} from "@halo-dev/components";
|
||||
import { computed, markRaw, onMounted, ref, watch, type Component } from "vue";
|
||||
import { computed, markRaw, ref, watch, type Component } from "vue";
|
||||
import Fuse from "fuse.js";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
@ -356,6 +356,8 @@ watch(
|
|||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
handleBuildSearchIndex();
|
||||
|
||||
setTimeout(() => {
|
||||
globalSearchInput.value?.focus();
|
||||
}, 100);
|
||||
|
@ -369,10 +371,6 @@ watch(
|
|||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
handleBuildSearchIndex();
|
||||
});
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
};
|
||||
|
|
21
src/main.ts
21
src/main.ts
|
@ -1,5 +1,5 @@
|
|||
import type { DirectiveBinding } from "vue";
|
||||
import { createApp } from "vue";
|
||||
import type { DirectiveBinding } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
@ -17,6 +17,7 @@ import type { User } from "@halo-dev/api-client";
|
|||
import { hasPermission } from "@/utils/permission";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { useThemeStore } from "./stores/theme";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
@ -175,9 +176,12 @@ async function loadPluginModules() {
|
|||
}
|
||||
}
|
||||
|
||||
let currentUser: User | undefined = undefined;
|
||||
|
||||
async function loadCurrentUser() {
|
||||
const { data: user } = await apiClient.user.getCurrentUserDetail();
|
||||
app.provide<User>("currentUser", user);
|
||||
currentUser = user;
|
||||
|
||||
const { data: currentPermissions } = await apiClient.user.getPermissions({
|
||||
name: "-",
|
||||
|
@ -204,6 +208,11 @@ async function loadCurrentUser() {
|
|||
);
|
||||
}
|
||||
|
||||
async function loadActivatedTheme() {
|
||||
const themeStore = useThemeStore();
|
||||
await themeStore.fetchActivatedTheme();
|
||||
}
|
||||
|
||||
(async function () {
|
||||
await initApp();
|
||||
})();
|
||||
|
@ -218,13 +227,19 @@ async function initApp() {
|
|||
try {
|
||||
loadCoreModules();
|
||||
|
||||
await loadCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
await loadActivatedTheme();
|
||||
|
||||
try {
|
||||
await loadPluginModules();
|
||||
} catch (e) {
|
||||
console.error("Failed to load plugins", e);
|
||||
}
|
||||
|
||||
await loadCurrentUser();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { SinglePageRequest } from "@halo-dev/api-client";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||
|
||||
const initialFormState: SinglePageRequest = {
|
||||
page: {
|
||||
|
@ -160,6 +161,9 @@ watchEffect(() => {
|
|||
formState.value = cloneDeep(props.singlePage);
|
||||
}
|
||||
});
|
||||
|
||||
// custom templates
|
||||
const { templates } = useThemeCustomTemplates("page");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -265,8 +269,9 @@ watchEffect(() => {
|
|||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.page.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
type="text"
|
||||
type="select"
|
||||
name="template"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
|
|
|
@ -15,6 +15,7 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { reset } from "@formkit/core";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -116,6 +117,9 @@ watch(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
// custom templates
|
||||
const { templates } = useThemeCustomTemplates("category");
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
|
@ -147,6 +151,13 @@ watch(
|
|||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
type="select"
|
||||
name="template"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.cover"
|
||||
help="需要主题适配以支持"
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import CategoryEditingModal from "../CategoryEditingModal.vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
describe("CategoryEditingModal", function () {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("should render", function () {
|
||||
expect(mount(CategoryEditingModal)).toBeDefined();
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { PostRequest } from "@halo-dev/api-client";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||
|
||||
const initialFormState: PostRequest = {
|
||||
post: {
|
||||
|
@ -163,6 +164,9 @@ watchEffect(() => {
|
|||
formState.value = cloneDeep(props.post);
|
||||
}
|
||||
});
|
||||
|
||||
// custom templates
|
||||
const { templates } = useThemeCustomTemplates("post");
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
|
@ -277,9 +281,10 @@ watchEffect(() => {
|
|||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.post.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
name="template"
|
||||
type="text"
|
||||
type="select"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.post.spec.cover"
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import PostSettingModal from "../PostSettingModal.vue";
|
||||
import { VDialogProvider } from "@halo-dev/components";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
describe("PostSettingModal", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
VDialogProvider,
|
||||
PostSettingModal,
|
||||
},
|
||||
template: `
|
||||
<VDialogProvider>
|
||||
<PostSettingModal></PostSettingModal>
|
||||
</VDialogProvider>`,
|
||||
template: `<PostSettingModal></PostSettingModal>`,
|
||||
});
|
||||
expect(wrapper).toBeDefined();
|
||||
});
|
||||
|
|
|
@ -24,6 +24,8 @@ import type { Theme } from "@halo-dev/api-client";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -31,12 +33,10 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
visible: boolean;
|
||||
selectedTheme: Theme | null;
|
||||
activatedTheme: Theme | null;
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
selectedTheme: null,
|
||||
activatedTheme: null,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -58,6 +58,8 @@ const modalTitle = computed(() => {
|
|||
return activeTab.value === "installed" ? "已安装的主题" : "未安装的主题";
|
||||
});
|
||||
|
||||
const { activatedTheme } = storeToRefs(useThemeStore());
|
||||
|
||||
const handleFetchThemes = async () => {
|
||||
try {
|
||||
clearInterval(refreshInterval.value);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { Dialog } from "@halo-dev/components";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
interface useThemeLifeCycleReturn {
|
||||
loading: Ref<boolean>;
|
||||
activatedTheme: Ref<Theme | undefined>;
|
||||
isActivated: ComputedRef<boolean>;
|
||||
handleActiveTheme: () => void;
|
||||
}
|
||||
|
@ -14,43 +15,16 @@ interface useThemeLifeCycleReturn {
|
|||
export function useThemeLifeCycle(
|
||||
theme: Ref<Theme | undefined>
|
||||
): useThemeLifeCycleReturn {
|
||||
const activatedTheme = ref<Theme | undefined>();
|
||||
const loading = ref(false);
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const { activatedTheme } = storeToRefs(themeStore);
|
||||
|
||||
const isActivated = computed(() => {
|
||||
return activatedTheme.value?.metadata.name === theme.value?.metadata.name;
|
||||
return activatedTheme?.value?.metadata.name === theme.value?.metadata.name;
|
||||
});
|
||||
|
||||
const handleFetchActivatedTheme = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap(
|
||||
{
|
||||
name: "system",
|
||||
}
|
||||
);
|
||||
|
||||
if (!data.data?.theme) {
|
||||
// Todo: show error
|
||||
return;
|
||||
}
|
||||
const themeConfig = JSON.parse(data.data.theme);
|
||||
|
||||
const { data: themeData } =
|
||||
await apiClient.extension.theme.getthemeHaloRunV1alpha1Theme({
|
||||
name: themeConfig.active,
|
||||
});
|
||||
|
||||
theme.value = themeData;
|
||||
activatedTheme.value = themeData;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch active theme", e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTheme = async () => {
|
||||
Dialog.info({
|
||||
title: "是否确认启用当前主题",
|
||||
|
@ -77,18 +51,48 @@ export function useThemeLifeCycle(
|
|||
} catch (e) {
|
||||
console.error("Failed to active theme", e);
|
||||
} finally {
|
||||
await handleFetchActivatedTheme();
|
||||
themeStore.fetchActivatedTheme();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(handleFetchActivatedTheme);
|
||||
|
||||
return {
|
||||
loading,
|
||||
activatedTheme,
|
||||
isActivated,
|
||||
handleActiveTheme,
|
||||
};
|
||||
}
|
||||
|
||||
export function useThemeCustomTemplates(type: "post" | "page" | "category") {
|
||||
const themeStore = useThemeStore();
|
||||
const templates = computed(() => {
|
||||
const defaultTemplate = [
|
||||
{
|
||||
label: "默认模板",
|
||||
value: "",
|
||||
},
|
||||
];
|
||||
|
||||
if (!themeStore.activatedTheme) {
|
||||
return defaultTemplate;
|
||||
}
|
||||
const { customTemplates } = themeStore.activatedTheme.spec;
|
||||
if (!customTemplates?.[type]) {
|
||||
return defaultTemplate;
|
||||
}
|
||||
return [
|
||||
...defaultTemplate,
|
||||
...(customTemplates[type]?.map((template) => {
|
||||
return {
|
||||
value: template.file,
|
||||
label: template.name || template.file,
|
||||
};
|
||||
}) || []),
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
templates,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { nextTick, type ComputedRef, type Ref } from "vue";
|
||||
import { nextTick, onMounted, type ComputedRef, type Ref } from "vue";
|
||||
import { computed, provide, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
|
@ -28,6 +28,8 @@ import {
|
|||
import ThemeListModal from "../components/ThemeListModal.vue";
|
||||
import type { SettingForm, Theme } from "@halo-dev/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -55,7 +57,7 @@ const selectedTheme = ref<Theme | undefined>();
|
|||
const themesModal = ref(false);
|
||||
const activeTab = ref("");
|
||||
|
||||
const { loading, isActivated, activatedTheme, handleActiveTheme } =
|
||||
const { loading, isActivated, handleActiveTheme } =
|
||||
useThemeLifeCycle(selectedTheme);
|
||||
|
||||
const settingName = computed(() => selectedTheme.value?.spec.settingName);
|
||||
|
@ -143,11 +145,18 @@ const handleTriggerTabChange = () => {
|
|||
watch([() => route.name, () => route.params], async () => {
|
||||
handleTriggerTabChange();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const { activatedTheme } = storeToRefs(themeStore);
|
||||
|
||||
selectedTheme.value = activatedTheme?.value;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<ThemeListModal
|
||||
v-model:activated-theme="activatedTheme"
|
||||
v-model:selected-theme="selectedTheme"
|
||||
v-model:visible="themesModal"
|
||||
/>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCounterStore = defineStore({
|
||||
id: "counter",
|
||||
state: () => ({
|
||||
counter: 0,
|
||||
}),
|
||||
getters: {
|
||||
doubleCount: (state) => state.counter * 2,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
interface ThemeStoreState {
|
||||
activatedTheme?: Theme;
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore("theme", {
|
||||
state: (): ThemeStoreState => ({
|
||||
activatedTheme: undefined,
|
||||
}),
|
||||
actions: {
|
||||
async fetchActivatedTheme() {
|
||||
try {
|
||||
const { data } =
|
||||
await apiClient.extension.configMap.getv1alpha1ConfigMap({
|
||||
name: "system",
|
||||
});
|
||||
|
||||
if (!data.data?.theme) {
|
||||
// Todo: show error
|
||||
return;
|
||||
}
|
||||
|
||||
const themeConfig = JSON.parse(data.data.theme);
|
||||
|
||||
const { data: themeData } =
|
||||
await apiClient.extension.theme.getthemeHaloRunV1alpha1Theme({
|
||||
name: themeConfig.active,
|
||||
});
|
||||
|
||||
this.activatedTheme = themeData;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch active theme", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue