mirror of https://github.com/halo-dev/halo
feat: add import and export theme configuration (#6071)
#### What type of PR is this? /kind feature /area ui /milestone 2.17.x #### What this PR does / why we need it: 为主题增加导入及导出配置的功能。 <img width="1666" alt="image" src="https://github.com/halo-dev/halo/assets/31335418/3574b33e-0293-427c-9036-5d82948aa7f2"> #### How to test it? 测试主题导入及导出功能是否正常可用。 测试当前主题下的不同版本导入导出后数据是否正常。 测试在当前主题下使用其他主题导出的配置是否会报错。 #### Which issue(s) this PR fixes: Fixes #1073 #### Does this PR introduce a user-facing change? ```release-note 为单个主题配置增加导入与导出的功能。 ```pull/6143/head
parent
0f87ed8370
commit
b445b505be
|
@ -1,11 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// types
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Ref } from "vue";
|
import type { Theme } from "@halo-dev/api-client";
|
||||||
// core libs
|
|
||||||
import { inject, ref } from "vue";
|
|
||||||
import { useThemeLifeCycle } from "./composables/use-theme";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
IconMore,
|
IconMore,
|
||||||
|
@ -19,10 +14,10 @@ import {
|
||||||
VStatusDot,
|
VStatusDot,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { Theme } from "@halo-dev/api-client";
|
import type { Ref } from "vue";
|
||||||
|
import { inject, ref } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useThemeConfigFile, useThemeLifeCycle } from "./composables/use-theme";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -78,6 +73,9 @@ const handleReloadTheme = async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { handleExportThemeConfiguration, openSelectImportFileDialog } =
|
||||||
|
useThemeConfigFile(selectedTheme);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -126,6 +124,12 @@ const handleReloadTheme = async () => {
|
||||||
<VDropdownItem @click="themesModal = true">
|
<VDropdownItem @click="themesModal = true">
|
||||||
{{ $t("core.common.buttons.upgrade") }}
|
{{ $t("core.common.buttons.upgrade") }}
|
||||||
</VDropdownItem>
|
</VDropdownItem>
|
||||||
|
<VDropdownItem @click="handleExportThemeConfiguration">
|
||||||
|
{{ $t("core.theme.operations.export_configuration.button") }}
|
||||||
|
</VDropdownItem>
|
||||||
|
<VDropdownItem @click="openSelectImportFileDialog()">
|
||||||
|
{{ $t("core.theme.operations.import_configuration.button") }}
|
||||||
|
</VDropdownItem>
|
||||||
<VDropdownDivider />
|
<VDropdownDivider />
|
||||||
<VDropdownItem type="danger" @click="handleReloadTheme">
|
<VDropdownItem type="danger" @click="handleReloadTheme">
|
||||||
{{ $t("core.theme.operations.reload.button") }}
|
{{ $t("core.theme.operations.reload.button") }}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { useThemeStore } from "@console/stores/theme";
|
||||||
|
import type { Theme } from "@halo-dev/api-client";
|
||||||
|
import { Dialog, Toast } from "@halo-dev/components";
|
||||||
|
import { useFileDialog } from "@vueuse/core";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
import type { ComputedRef, Ref } from "vue";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import type { Theme } from "@halo-dev/api-client";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
|
||||||
import { useThemeStore } from "@console/stores/theme";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
interface useThemeLifeCycleReturn {
|
interface useThemeLifeCycleReturn {
|
||||||
|
@ -138,3 +139,166 @@ export function useThemeCustomTemplates(type: "post" | "page" | "category") {
|
||||||
templates,
|
templates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExportData {
|
||||||
|
themeName: string;
|
||||||
|
version: string;
|
||||||
|
settingName: string;
|
||||||
|
configMapName: string;
|
||||||
|
configs: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useThemeConfigFile(theme: Ref<Theme | undefined>) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const handleExportThemeConfiguration = async () => {
|
||||||
|
if (!theme.value) {
|
||||||
|
console.error("No selected or activated theme");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await apiClient.theme.fetchThemeConfig({
|
||||||
|
name: theme?.value?.metadata.name as string,
|
||||||
|
});
|
||||||
|
if (!data) {
|
||||||
|
console.error("Failed to fetch theme config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeName = theme.value.metadata.name;
|
||||||
|
const exportData = {
|
||||||
|
themeName: themeName,
|
||||||
|
version: theme.value.spec.version,
|
||||||
|
settingName: theme.value.spec.settingName,
|
||||||
|
configMapName: theme.value.spec.configMapName,
|
||||||
|
configs: data.data,
|
||||||
|
} as ExportData;
|
||||||
|
const exportStr = JSON.stringify(exportData, null, 2);
|
||||||
|
const blob = new Blob([exportStr], { type: "application/json" });
|
||||||
|
const temporaryExportUrl = URL.createObjectURL(blob);
|
||||||
|
const temporaryLinkTag = document.createElement("a");
|
||||||
|
|
||||||
|
temporaryLinkTag.href = temporaryExportUrl;
|
||||||
|
temporaryLinkTag.download = `export-${themeName}-config-${Date.now().toString()}.json`;
|
||||||
|
|
||||||
|
document.body.appendChild(temporaryLinkTag);
|
||||||
|
temporaryLinkTag.click();
|
||||||
|
|
||||||
|
document.body.removeChild(temporaryLinkTag);
|
||||||
|
URL.revokeObjectURL(temporaryExportUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
open: openSelectImportFileDialog,
|
||||||
|
onChange: handleImportThemeConfiguration,
|
||||||
|
} = useFileDialog({
|
||||||
|
accept: "application/json",
|
||||||
|
multiple: false,
|
||||||
|
directory: false,
|
||||||
|
reset: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
handleImportThemeConfiguration(async (files) => {
|
||||||
|
if (files === null || files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const configText = await files[0].text();
|
||||||
|
const configJson = JSON.parse(configText || "{}");
|
||||||
|
if (!configJson.configs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!configJson.themeName || !configJson.version) {
|
||||||
|
Toast.error(
|
||||||
|
t("core.theme.operations.import_configuration.invalid_format")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!theme.value) {
|
||||||
|
console.error("No selected or activated theme");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (configJson.themeName !== theme.value.metadata.name) {
|
||||||
|
Toast.error(
|
||||||
|
t("core.theme.operations.import_configuration.mismatched_theme")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configJson.version !== theme.value.spec.version) {
|
||||||
|
Dialog.warning({
|
||||||
|
title: t(
|
||||||
|
"core.theme.operations.import_configuration.version_mismatch.title"
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
"core.theme.operations.import_configuration.version_mismatch.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: () => {
|
||||||
|
handleSaveConfigMap(configJson.configs);
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSaveConfigMap(configJson.configs);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSaveConfigMap = async (importData: Record<string, string>) => {
|
||||||
|
if (!theme.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await apiClient.theme.fetchThemeConfig({
|
||||||
|
name: theme.value.metadata.name as string,
|
||||||
|
});
|
||||||
|
if (!data || !data.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const combinedConfigData = combinedConfigMap(data.data, importData);
|
||||||
|
await apiClient.theme.updateThemeConfig({
|
||||||
|
name: theme.value.metadata.name,
|
||||||
|
configMap: {
|
||||||
|
...data,
|
||||||
|
data: combinedConfigData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* combined benchmark configuration and import configuration
|
||||||
|
*
|
||||||
|
* benchmark: { a: "{\"a\": 1}", b: "{\"b\": 2}" }
|
||||||
|
* expand: { a: "{\"c\": 3}", b: "{\"d\": 4}" }
|
||||||
|
* => { a: "{\"a\": 1, \"c\": 3}", b: "{\"b\": 2, \"d\": 4}" }
|
||||||
|
*
|
||||||
|
* benchmark: { a: "{\"a\": 1}", b: "{\"b\": 2}", d: "{\"d\": 4}"
|
||||||
|
* expand: { a: "{\"a\": 2}", b: "{\"b\": 3, \"d\": 4}", c: "{\"c\": 5}" }
|
||||||
|
* => { a: "{\"a\": 2}", b: "{\"b\": 3, \"d\": 4}", d: "{\"d\": 4}" }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const combinedConfigMap = (
|
||||||
|
benchmarkConfigMap: { [key: string]: string },
|
||||||
|
importConfigMap: { [key: string]: string }
|
||||||
|
): { [key: string]: string } => {
|
||||||
|
const result = benchmarkConfigMap;
|
||||||
|
|
||||||
|
for (const key in result) {
|
||||||
|
const benchmarkValueJson = JSON.parse(benchmarkConfigMap[key] || "{}");
|
||||||
|
const expandValueJson = JSON.parse(importConfigMap[key] || "{}");
|
||||||
|
const combinedValue = {
|
||||||
|
...benchmarkValueJson,
|
||||||
|
...expandValueJson,
|
||||||
|
};
|
||||||
|
result[key] = JSON.stringify(combinedValue);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleExportThemeConfiguration,
|
||||||
|
openSelectImportFileDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -721,6 +721,18 @@ core:
|
||||||
description: >-
|
description: >-
|
||||||
This feature allows you to refresh the cache to view the latest web
|
This feature allows you to refresh the cache to view the latest web
|
||||||
results after modifying template files at runtime.
|
results after modifying template files at runtime.
|
||||||
|
export_configuration:
|
||||||
|
button: Export theme configuration
|
||||||
|
import_configuration:
|
||||||
|
button: Import theme configuration
|
||||||
|
version_mismatch:
|
||||||
|
title: Version mismatch
|
||||||
|
description: >-
|
||||||
|
The imported configuration file version does not match the
|
||||||
|
current theme version, which may lead to compatibility issues.
|
||||||
|
Do you want to continue importing?
|
||||||
|
invalid_format: Invalid theme configuration file
|
||||||
|
mismatched_theme: Configuration file does not match the selected theme
|
||||||
list_modal:
|
list_modal:
|
||||||
tabs:
|
tabs:
|
||||||
installed: Installed
|
installed: Installed
|
||||||
|
|
|
@ -691,6 +691,15 @@ core:
|
||||||
button: 清理模板缓存
|
button: 清理模板缓存
|
||||||
title: 清除模板缓存
|
title: 清除模板缓存
|
||||||
description: 此功能适用于在运行时修改模板文件后,刷新缓存以查看最新网页结果。
|
description: 此功能适用于在运行时修改模板文件后,刷新缓存以查看最新网页结果。
|
||||||
|
export_configuration:
|
||||||
|
button: 导出主题配置
|
||||||
|
import_configuration:
|
||||||
|
button: 导入主题配置
|
||||||
|
version_mismatch:
|
||||||
|
title: 版本不匹配
|
||||||
|
description: 导入的配置文件版本与当前主题版本不匹配,这可能会导致兼容性问题。是否继续导入?
|
||||||
|
invalid_format: 无效的主题配置文件
|
||||||
|
mismatched_theme: 配置文件与所选主题不匹配
|
||||||
list_modal:
|
list_modal:
|
||||||
tabs:
|
tabs:
|
||||||
installed: 已安装
|
installed: 已安装
|
||||||
|
|
|
@ -671,6 +671,15 @@ core:
|
||||||
button: 清除模板快取
|
button: 清除模板快取
|
||||||
title: 清除模板快取
|
title: 清除模板快取
|
||||||
description: 此功能適用於在運行時修改模板檔案後,刷新快取以查看最新網頁結果。
|
description: 此功能適用於在運行時修改模板檔案後,刷新快取以查看最新網頁結果。
|
||||||
|
export_configuration:
|
||||||
|
button: 匯出主題配置
|
||||||
|
import_configuration:
|
||||||
|
button: 匯入主題配置
|
||||||
|
version_mismatch:
|
||||||
|
title: 版本不匹配
|
||||||
|
description: 匯入的配置檔版本與目前主題版本不匹配,這可能會導致相容性問題。是否繼續匯入?
|
||||||
|
invalid_format: 無效的主題配置文件
|
||||||
|
mismatched_theme: 配置文件與所選主題不匹配
|
||||||
list_modal:
|
list_modal:
|
||||||
tabs:
|
tabs:
|
||||||
installed: 已安裝
|
installed: 已安裝
|
||||||
|
|
Loading…
Reference in New Issue