mirror of https://github.com/halo-dev/halo-admin
refactor: use new apis to refactor themes management (#820)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 使用新的 API 来管理主题,用于区分主题管理和配置相关的权限。适配:https://github.com/halo-dev/halo/pull/3135 #### Which issue(s) this PR fixes: Ref https://github.com/halo-dev/halo/issues/3069 #### Screenshots: #### Special notes for your reviewer: 重点测试: 1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/3135 分支。 2. Console 需要 `pnpm install`。 1. 主题设置项,需要测试保存,安装新主题之后表单是否正常。 3. 创建一个角色,测试仅有管理/查看主题的角色,登录之后检查是否符合预期。 #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端主题的设置表单逻辑。 ```pull/828/head^2
parent
a2935de6ef
commit
ab888119ef
|
@ -16,11 +16,14 @@ 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";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { activatedTheme } = storeToRefs(useThemeStore());
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -248,30 +251,17 @@ const handleBuildSearchIndex = () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// get theme settings
|
||||
apiClient.extension.configMap
|
||||
.getv1alpha1ConfigMap({
|
||||
name: "system",
|
||||
})
|
||||
.then(({ data: systemConfigMap }) => {
|
||||
if (systemConfigMap.data?.theme) {
|
||||
const themeConfig = JSON.parse(systemConfigMap.data.theme);
|
||||
|
||||
apiClient.extension.theme
|
||||
.getthemeHaloRunV1alpha1Theme({
|
||||
name: themeConfig.active,
|
||||
})
|
||||
.then(({ data: theme }) => {
|
||||
if (theme && theme.spec.settingName) {
|
||||
apiClient.extension.setting
|
||||
.getv1alpha1Setting({
|
||||
name: theme.spec.settingName,
|
||||
})
|
||||
if (currentUserHasPermission(["system:themes:view"])) {
|
||||
apiClient.theme
|
||||
.fetchThemeSetting({ name: "-" })
|
||||
.then(({ data: themeSettings }) => {
|
||||
themeSettings.spec.forms.forEach((form) => {
|
||||
fuse.add({
|
||||
title: `${theme.spec.displayName} / ${form.label}`,
|
||||
title: [activatedTheme.value?.spec.displayName, form.label].join(
|
||||
" / "
|
||||
),
|
||||
icon: {
|
||||
component: markRaw(IconPalette),
|
||||
},
|
||||
|
@ -286,10 +276,6 @@ const handleBuildSearchIndex = () => {
|
|||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// core libs
|
||||
// types
|
||||
import type { Ref } from "vue";
|
||||
import { computed, watch, type ComputedRef, type Ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { apiClient } from "../utils/api-client";
|
||||
|
||||
|
@ -179,3 +179,68 @@ export function useSettingForm(
|
|||
handleReset,
|
||||
};
|
||||
}
|
||||
|
||||
interface useSettingFormConvertReturn {
|
||||
formSchema: ComputedRef<
|
||||
(FormKitSchemaCondition | FormKitSchemaNode)[] | undefined
|
||||
>;
|
||||
configMapFormData: Ref<Record<string, Record<string, string>>>;
|
||||
convertToSave: () => ConfigMap | undefined;
|
||||
}
|
||||
|
||||
export function useSettingFormConvert(
|
||||
setting: Ref<Setting | undefined>,
|
||||
configMap: Ref<ConfigMap | undefined>,
|
||||
group: Ref<string>
|
||||
): useSettingFormConvertReturn {
|
||||
const configMapFormData = ref<Record<string, Record<string, string>>>({});
|
||||
|
||||
const formSchema = computed(() => {
|
||||
if (!setting.value) {
|
||||
return;
|
||||
}
|
||||
const { forms } = setting.value.spec;
|
||||
return forms.find((item) => item.group === group?.value)?.formSchema as (
|
||||
| FormKitSchemaCondition
|
||||
| FormKitSchemaNode
|
||||
)[];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => configMap.value,
|
||||
() => {
|
||||
const { forms } = setting.value?.spec || {};
|
||||
|
||||
forms?.forEach((form) => {
|
||||
configMapFormData.value[form.group] = JSON.parse(
|
||||
configMap.value?.data?.[form.group] || "{}"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function convertToSave() {
|
||||
const configMapToUpdate = cloneDeep(configMap.value);
|
||||
|
||||
if (!configMapToUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: {
|
||||
[key: string]: string;
|
||||
} = {};
|
||||
|
||||
setting.value?.spec.forms.forEach((item: SettingForm) => {
|
||||
data[item.group] = JSON.stringify(configMapFormData?.value?.[item.group]);
|
||||
});
|
||||
|
||||
configMapToUpdate.data = data;
|
||||
return configMapToUpdate;
|
||||
}
|
||||
|
||||
return {
|
||||
formSchema,
|
||||
configMapFormData,
|
||||
convertToSave,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -203,7 +203,11 @@ async function loadUserPermissions() {
|
|||
return;
|
||||
}
|
||||
|
||||
enable ? (el.style.backgroundColor = "red") : el.remove();
|
||||
if (enable) {
|
||||
//TODO
|
||||
return;
|
||||
}
|
||||
el?.remove?.();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ const actions: Action[] = [
|
|||
action: () => {
|
||||
themePreviewVisible.value = true;
|
||||
},
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconBookRead),
|
||||
|
|
|
@ -1,45 +1,73 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { computed, inject, watch } from "vue";
|
||||
import { inject, ref, watch } from "vue";
|
||||
|
||||
// components
|
||||
import { VButton } from "@halo-dev/components";
|
||||
|
||||
// types
|
||||
import type { Ref } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import type { ConfigMap, Setting, Theme } from "@halo-dev/api-client";
|
||||
|
||||
// hooks
|
||||
import { useSettingForm } from "@/composables/use-setting-form";
|
||||
import { useRouteParams } from "@vueuse/router";
|
||||
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useSettingFormConvert } from "@/composables/use-setting-form";
|
||||
|
||||
const group = useRouteParams<string>("group");
|
||||
|
||||
const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme");
|
||||
|
||||
const settingName = computed(() => selectedTheme?.value?.spec.settingName);
|
||||
const configMapName = computed(() => selectedTheme?.value?.spec.configMapName);
|
||||
const saving = ref(false);
|
||||
const setting = ref<Setting>();
|
||||
const configMap = ref<ConfigMap>();
|
||||
|
||||
const {
|
||||
const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
|
||||
setting,
|
||||
configMapFormData,
|
||||
saving,
|
||||
handleFetchConfigMap,
|
||||
handleFetchSettings,
|
||||
handleSaveConfigMap,
|
||||
} = useSettingForm(settingName, configMapName);
|
||||
configMap,
|
||||
group
|
||||
);
|
||||
|
||||
const formSchema = computed(() => {
|
||||
if (!setting.value) {
|
||||
const handleFetchSettings = async () => {
|
||||
if (!selectedTheme?.value) return;
|
||||
|
||||
const { data } = await apiClient.theme.fetchThemeSetting({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
});
|
||||
|
||||
setting.value = data;
|
||||
};
|
||||
|
||||
const handleFetchConfigMap = async () => {
|
||||
if (!selectedTheme?.value) return;
|
||||
|
||||
const { data } = await apiClient.theme.fetchThemeConfig({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
});
|
||||
|
||||
configMap.value = data;
|
||||
};
|
||||
|
||||
const handleSaveConfigMap = async () => {
|
||||
saving.value = true;
|
||||
|
||||
const configMapToUpdate = convertToSave();
|
||||
|
||||
if (!configMapToUpdate || !selectedTheme?.value) {
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
const { forms } = setting.value.spec;
|
||||
return forms.find((item) => item.group === group?.value)?.formSchema as (
|
||||
| FormKitSchemaCondition
|
||||
| FormKitSchemaNode
|
||||
)[];
|
||||
});
|
||||
|
||||
const { data: newConfigMap } = await apiClient.theme.updateThemeConfig({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
await handleFetchSettings();
|
||||
configMap.value = newConfigMap;
|
||||
|
||||
saving.value = false;
|
||||
};
|
||||
|
||||
await handleFetchSettings();
|
||||
await handleFetchConfigMap();
|
||||
|
@ -57,7 +85,7 @@ watch(
|
|||
<div class="bg-white p-4">
|
||||
<div>
|
||||
<FormKit
|
||||
v-if="group && formSchema && configMapFormData"
|
||||
v-if="group && formSchema && configMapFormData?.[group]"
|
||||
:id="group"
|
||||
v-model="configMapFormData[group]"
|
||||
:name="group"
|
||||
|
@ -72,7 +100,7 @@ watch(
|
|||
/>
|
||||
</FormKit>
|
||||
</div>
|
||||
<div v-permission="['system:configmaps:manage']" class="pt-5">
|
||||
<div v-permission="['system:themes:manage']" class="pt-5">
|
||||
<div class="flex justify-start">
|
||||
<VButton
|
||||
:loading="saving"
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
|
||||
import { useSettingForm } from "@/composables/use-setting-form";
|
||||
import { useSettingFormConvert } from "@/composables/use-setting-form";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
|
||||
import type { SettingForm, Theme } from "@halo-dev/api-client";
|
||||
import type {
|
||||
ConfigMap,
|
||||
Setting,
|
||||
SettingForm,
|
||||
Theme,
|
||||
} from "@halo-dev/api-client";
|
||||
import {
|
||||
VModal,
|
||||
IconLink,
|
||||
|
@ -111,21 +115,61 @@ const modalTitle = computed(() => {
|
|||
});
|
||||
|
||||
// theme settings
|
||||
const setting = ref<Setting>();
|
||||
const configMap = ref<ConfigMap>();
|
||||
const saving = ref(false);
|
||||
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
|
||||
const activeSettingTab = ref("");
|
||||
const settingsVisible = ref(false);
|
||||
|
||||
const settingName = computed(() => selectedTheme.value?.spec.settingName);
|
||||
const configMapName = computed(() => selectedTheme.value?.spec.configMapName);
|
||||
|
||||
const {
|
||||
const { formSchema, configMapFormData, convertToSave } = useSettingFormConvert(
|
||||
setting,
|
||||
configMapFormData,
|
||||
saving,
|
||||
handleFetchConfigMap,
|
||||
handleFetchSettings,
|
||||
handleSaveConfigMap,
|
||||
} = useSettingForm(settingName, configMapName);
|
||||
configMap,
|
||||
activeSettingTab
|
||||
);
|
||||
|
||||
const handleFetchSettings = async () => {
|
||||
if (!selectedTheme?.value) return;
|
||||
|
||||
const { data } = await apiClient.theme.fetchThemeSetting({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
});
|
||||
|
||||
setting.value = data;
|
||||
};
|
||||
|
||||
const handleFetchConfigMap = async () => {
|
||||
if (!selectedTheme?.value) return;
|
||||
|
||||
const { data } = await apiClient.theme.fetchThemeConfig({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
});
|
||||
|
||||
configMap.value = data;
|
||||
};
|
||||
|
||||
const handleSaveConfigMap = async () => {
|
||||
saving.value = true;
|
||||
|
||||
const configMapToUpdate = convertToSave();
|
||||
|
||||
if (!configMapToUpdate || !selectedTheme?.value) {
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: newConfigMap } = await apiClient.theme.updateThemeConfig({
|
||||
name: selectedTheme?.value?.metadata.name,
|
||||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
await handleFetchSettings();
|
||||
configMap.value = newConfigMap;
|
||||
|
||||
saving.value = false;
|
||||
|
||||
handleRefresh();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => selectedTheme.value,
|
||||
|
@ -157,20 +201,6 @@ const handleOpenSettings = (theme?: Theme) => {
|
|||
settingsVisible.value = !settingsVisible.value;
|
||||
};
|
||||
|
||||
const formSchema = computed(() => {
|
||||
if (!setting.value) {
|
||||
return;
|
||||
}
|
||||
const { forms } = setting.value.spec;
|
||||
return forms.find((item) => item.group === activeSettingTab.value)
|
||||
?.formSchema as (FormKitSchemaCondition | FormKitSchemaNode)[];
|
||||
});
|
||||
|
||||
const handleSaveThemeConfigMap = async () => {
|
||||
await handleSaveConfigMap();
|
||||
handleRefresh();
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
previewFrame.value?.contentWindow?.location.reload();
|
||||
};
|
||||
|
@ -288,14 +318,14 @@ const iframeClasses = computed(() => {
|
|||
configMapFormData &&
|
||||
formSchema
|
||||
"
|
||||
:id="tab.id"
|
||||
:id="`preview-setting-${tab.id}`"
|
||||
:key="tab.id"
|
||||
v-model="configMapFormData[tab.id]"
|
||||
:name="tab.id"
|
||||
:actions="false"
|
||||
:preserve="true"
|
||||
type="form"
|
||||
@submit="handleSaveThemeConfigMap"
|
||||
@submit="handleSaveConfigMap"
|
||||
>
|
||||
<FormKitSchema
|
||||
:schema="formSchema"
|
||||
|
@ -303,12 +333,16 @@ const iframeClasses = computed(() => {
|
|||
/>
|
||||
</FormKit>
|
||||
</div>
|
||||
<div v-permission="['system:configmaps:manage']" class="pt-5">
|
||||
<div v-permission="['system:themes:manage']" class="pt-5">
|
||||
<div class="flex justify-start">
|
||||
<VButton
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
@click="$formkit.submit(activeSettingTab || '')"
|
||||
@click="
|
||||
$formkit.submit(
|
||||
`preview-setting-${activeSettingTab}` || ''
|
||||
)
|
||||
"
|
||||
>
|
||||
保存
|
||||
</VButton>
|
||||
|
|
|
@ -45,23 +45,11 @@ export function useThemeLifeCycle(
|
|||
description: theme.value?.spec.displayName,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const { data: systemConfigMap } =
|
||||
await apiClient.extension.configMap.getv1alpha1ConfigMap({
|
||||
name: "system",
|
||||
});
|
||||
if (!theme.value) return;
|
||||
|
||||
if (systemConfigMap.data) {
|
||||
const themeConfigToUpdate = JSON.parse(
|
||||
systemConfigMap.data?.theme || "{}"
|
||||
);
|
||||
themeConfigToUpdate.active = theme.value?.metadata.name;
|
||||
systemConfigMap.data["theme"] = JSON.stringify(themeConfigToUpdate);
|
||||
|
||||
await apiClient.extension.configMap.updatev1alpha1ConfigMap({
|
||||
name: "system",
|
||||
configMap: systemConfigMap,
|
||||
await apiClient.theme.activateTheme({
|
||||
name: theme.value?.metadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
Toast.success("启用成功");
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { nextTick, onMounted, type Ref } from "vue";
|
||||
import { computed, provide, ref, watch } from "vue";
|
||||
import { provide, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
// libs
|
||||
|
@ -11,7 +11,6 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { useThemeLifeCycle } from "../composables/use-theme";
|
||||
// types
|
||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||
import { useSettingForm } from "@/composables/use-setting-form";
|
||||
|
||||
// components
|
||||
import {
|
||||
|
@ -28,10 +27,11 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import ThemeListModal from "../components/ThemeListModal.vue";
|
||||
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
|
||||
import type { SettingForm, Theme } from "@halo-dev/api-client";
|
||||
import type { Setting, SettingForm, Theme } from "@halo-dev/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -63,19 +63,23 @@ const activeTab = ref("");
|
|||
const { loading, isActivated, handleActiveTheme } =
|
||||
useThemeLifeCycle(selectedTheme);
|
||||
|
||||
const settingName = computed(() => selectedTheme.value?.spec.settingName);
|
||||
const configMapName = computed(() => selectedTheme.value?.spec.configMapName);
|
||||
|
||||
const { setting, handleFetchSettings } = useSettingForm(
|
||||
settingName,
|
||||
configMapName
|
||||
);
|
||||
|
||||
provide<Ref<Theme | undefined>>("selectedTheme", selectedTheme);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const setting = ref<Setting>();
|
||||
|
||||
const handleFetchSettings = async () => {
|
||||
if (!selectedTheme.value) return;
|
||||
|
||||
const { data } = await apiClient.theme.fetchThemeSetting({
|
||||
name: selectedTheme.value?.metadata.name,
|
||||
});
|
||||
|
||||
setting.value = data;
|
||||
};
|
||||
|
||||
const handleTabChange = (id: string) => {
|
||||
const tab = tabs.value.find((item) => item.id === id);
|
||||
if (tab) {
|
||||
|
@ -91,7 +95,7 @@ watch(
|
|||
// reset tabs
|
||||
tabs.value = cloneDeep(initialTabs);
|
||||
|
||||
if (!currentUserHasPermission(["system:settings:view"])) {
|
||||
if (!currentUserHasPermission(["system:themes:view"])) {
|
||||
handleTriggerTabChange();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default definePlugin({
|
|||
component: ThemeSetting,
|
||||
meta: {
|
||||
title: "主题设置",
|
||||
permissions: ["system:settings:view"],
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -2,36 +2,24 @@ import { apiClient } from "@/utils/api-client";
|
|||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
export const useThemeStore = defineStore("theme", () => {
|
||||
const activatedTheme = ref<Theme>();
|
||||
|
||||
async function fetchActivatedTheme() {
|
||||
try {
|
||||
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap(
|
||||
{
|
||||
name: "system",
|
||||
},
|
||||
{ mute: true }
|
||||
);
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
if (!data.data?.theme) {
|
||||
async function fetchActivatedTheme() {
|
||||
if (!currentUserHasPermission(["system:themes:view"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themeConfig = JSON.parse(data.data.theme);
|
||||
|
||||
const { data: themeData } =
|
||||
await apiClient.extension.theme.getthemeHaloRunV1alpha1Theme(
|
||||
{
|
||||
name: themeConfig.active,
|
||||
},
|
||||
{
|
||||
try {
|
||||
const { data } = await apiClient.theme.fetchActivatedTheme({
|
||||
mute: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
activatedTheme.value = themeData;
|
||||
activatedTheme.value = data;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch active theme", e);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue