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>
|
||||
// types
|
||||
import type { Ref } from "vue";
|
||||
// core libs
|
||||
import { inject, ref } from "vue";
|
||||
import { useThemeLifeCycle } from "./composables/use-theme";
|
||||
|
||||
// components
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconMore,
|
||||
|
@ -19,10 +14,10 @@ import {
|
|||
VStatusDot,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Ref } from "vue";
|
||||
import { inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useThemeConfigFile, useThemeLifeCycle } from "./composables/use-theme";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -78,6 +73,9 @@ const handleReloadTheme = async () => {
|
|||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { handleExportThemeConfiguration, openSelectImportFileDialog } =
|
||||
useThemeConfigFile(selectedTheme);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -126,6 +124,12 @@ const handleReloadTheme = async () => {
|
|||
<VDropdownItem @click="themesModal = true">
|
||||
{{ $t("core.common.buttons.upgrade") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="handleExportThemeConfiguration">
|
||||
{{ $t("core.theme.operations.export_configuration.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="openSelectImportFileDialog()">
|
||||
{{ $t("core.theme.operations.import_configuration.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownDivider />
|
||||
<VDropdownItem type="danger" @click="handleReloadTheme">
|
||||
{{ $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 { 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";
|
||||
|
||||
interface useThemeLifeCycleReturn {
|
||||
|
@ -138,3 +139,166 @@ export function useThemeCustomTemplates(type: "post" | "page" | "category") {
|
|||
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: >-
|
||||
This feature allows you to refresh the cache to view the latest web
|
||||
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:
|
||||
tabs:
|
||||
installed: Installed
|
||||
|
|
|
@ -691,6 +691,15 @@ core:
|
|||
button: 清理模板缓存
|
||||
title: 清除模板缓存
|
||||
description: 此功能适用于在运行时修改模板文件后,刷新缓存以查看最新网页结果。
|
||||
export_configuration:
|
||||
button: 导出主题配置
|
||||
import_configuration:
|
||||
button: 导入主题配置
|
||||
version_mismatch:
|
||||
title: 版本不匹配
|
||||
description: 导入的配置文件版本与当前主题版本不匹配,这可能会导致兼容性问题。是否继续导入?
|
||||
invalid_format: 无效的主题配置文件
|
||||
mismatched_theme: 配置文件与所选主题不匹配
|
||||
list_modal:
|
||||
tabs:
|
||||
installed: 已安装
|
||||
|
|
|
@ -671,6 +671,15 @@ core:
|
|||
button: 清除模板快取
|
||||
title: 清除模板快取
|
||||
description: 此功能適用於在運行時修改模板檔案後,刷新快取以查看最新網頁結果。
|
||||
export_configuration:
|
||||
button: 匯出主題配置
|
||||
import_configuration:
|
||||
button: 匯入主題配置
|
||||
version_mismatch:
|
||||
title: 版本不匹配
|
||||
description: 匯入的配置檔版本與目前主題版本不匹配,這可能會導致相容性問題。是否繼續匯入?
|
||||
invalid_format: 無效的主題配置文件
|
||||
mismatched_theme: 配置文件與所選主題不匹配
|
||||
list_modal:
|
||||
tabs:
|
||||
installed: 已安裝
|
||||
|
|
Loading…
Reference in New Issue