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
Takagi 2024-06-19 18:55:00 +08:00 committed by GitHub
parent 0f87ed8370
commit b445b505be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 213 additions and 15 deletions

View File

@ -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") }}

View File

@ -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,
};
}

View File

@ -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

View File

@ -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: 已安装

View File

@ -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: 已安裝