mirror of https://github.com/halo-dev/halo
446 lines
12 KiB
Vue
446 lines
12 KiB
Vue
<script lang="ts" setup>
|
|
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
|
|
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
|
import { useThemeStore } from "@console/stores/theme";
|
|
import { apiClient } from "@/utils/api-client";
|
|
import type {
|
|
ConfigMap,
|
|
Setting,
|
|
SettingForm,
|
|
Theme,
|
|
} from "@halo-dev/api-client";
|
|
import {
|
|
VModal,
|
|
IconLink,
|
|
IconPalette,
|
|
IconSettings,
|
|
IconArrowLeft,
|
|
VTabbar,
|
|
VButton,
|
|
IconComputer,
|
|
IconPhone,
|
|
IconTablet,
|
|
IconRefreshLine,
|
|
Toast,
|
|
} from "@halo-dev/components";
|
|
import { storeToRefs } from "pinia";
|
|
import { computed, markRaw, ref, toRaw, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
|
import { useQuery } from "@tanstack/vue-query";
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
visible: boolean;
|
|
title?: string;
|
|
theme?: Theme;
|
|
}>(),
|
|
{
|
|
visible: false,
|
|
title: undefined,
|
|
theme: undefined,
|
|
}
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
(event: "update:visible", visible: boolean): void;
|
|
(event: "close"): void;
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
|
|
interface SettingTab {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
const { activatedTheme } = storeToRefs(useThemeStore());
|
|
|
|
const previewFrame = ref<HTMLIFrameElement | null>(null);
|
|
const themesVisible = ref(false);
|
|
const switching = ref(false);
|
|
const selectedTheme = ref<Theme>();
|
|
|
|
const { data: themes } = useQuery<Theme[]>({
|
|
queryKey: ["themes"],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.theme.listThemes({
|
|
page: 0,
|
|
size: 0,
|
|
uninstalled: false,
|
|
});
|
|
return data.items;
|
|
},
|
|
enabled: computed(() => props.visible),
|
|
});
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(visible) => {
|
|
if (visible) {
|
|
selectedTheme.value = props.theme || activatedTheme?.value;
|
|
} else {
|
|
setTimeout(() => {
|
|
themesVisible.value = false;
|
|
settingsVisible.value = false;
|
|
}, 200);
|
|
}
|
|
}
|
|
);
|
|
|
|
const onVisibleChange = (visible: boolean) => {
|
|
emit("update:visible", visible);
|
|
if (!visible) {
|
|
emit("close");
|
|
}
|
|
};
|
|
|
|
const handleOpenThemes = () => {
|
|
settingsVisible.value = false;
|
|
themesVisible.value = !themesVisible.value;
|
|
};
|
|
|
|
const handleSelect = (theme: Theme) => {
|
|
selectedTheme.value = theme;
|
|
};
|
|
|
|
const previewUrl = computed(() => {
|
|
if (!selectedTheme.value) {
|
|
return "#";
|
|
}
|
|
return `${import.meta.env.VITE_API_URL}/?preview-theme=${
|
|
selectedTheme.value.metadata.name
|
|
}`;
|
|
});
|
|
|
|
const modalTitle = computed(() => {
|
|
if (props.title) {
|
|
return props.title;
|
|
}
|
|
return t("core.theme.preview_model.title", {
|
|
display_name: selectedTheme.value?.spec.displayName,
|
|
});
|
|
});
|
|
|
|
// theme settings
|
|
const saving = ref(false);
|
|
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
|
|
const activeSettingTab = ref("");
|
|
const settingsVisible = ref(false);
|
|
|
|
const { data: setting } = useQuery<Setting>({
|
|
queryKey: ["theme-setting", selectedTheme],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.theme.fetchThemeSetting({
|
|
name: selectedTheme?.value?.metadata.name as string,
|
|
});
|
|
|
|
return data;
|
|
},
|
|
onSuccess(data) {
|
|
if (data) {
|
|
const { forms } = data.spec;
|
|
settingTabs.value = forms.map((item: SettingForm) => {
|
|
return {
|
|
id: item.group,
|
|
label: item.label || "",
|
|
};
|
|
});
|
|
}
|
|
|
|
activeSettingTab.value = settingTabs.value[0].id;
|
|
},
|
|
enabled: computed(
|
|
() => props.visible && !!selectedTheme.value?.spec.settingName
|
|
),
|
|
});
|
|
|
|
const { data: configMap, refetch: handleFetchConfigMap } = useQuery<ConfigMap>({
|
|
queryKey: ["theme-configMap", selectedTheme],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.theme.fetchThemeConfig({
|
|
name: selectedTheme?.value?.metadata.name as string,
|
|
});
|
|
return data;
|
|
},
|
|
enabled: computed(
|
|
() => !!setting.value && !!selectedTheme.value?.spec.configMapName
|
|
),
|
|
});
|
|
|
|
const { formSchema, configMapFormData, convertToSave } = useSettingFormConvert(
|
|
setting,
|
|
configMap,
|
|
activeSettingTab
|
|
);
|
|
|
|
const handleSaveConfigMap = async () => {
|
|
saving.value = true;
|
|
|
|
const configMapToUpdate = convertToSave();
|
|
|
|
if (!configMapToUpdate || !selectedTheme?.value) {
|
|
saving.value = false;
|
|
return;
|
|
}
|
|
|
|
await apiClient.theme.updateThemeConfig({
|
|
name: selectedTheme?.value?.metadata.name,
|
|
configMap: configMapToUpdate,
|
|
});
|
|
|
|
Toast.success(t("core.common.toast.save_success"));
|
|
|
|
await handleFetchConfigMap();
|
|
|
|
saving.value = false;
|
|
|
|
handleRefresh();
|
|
};
|
|
|
|
const handleOpenSettings = (theme?: Theme) => {
|
|
if (theme) {
|
|
selectedTheme.value = theme;
|
|
}
|
|
themesVisible.value = false;
|
|
settingsVisible.value = !settingsVisible.value;
|
|
};
|
|
|
|
const handleRefresh = () => {
|
|
previewFrame.value?.contentWindow?.location.reload();
|
|
};
|
|
|
|
// mock devices
|
|
const mockDevices = [
|
|
{
|
|
id: "desktop",
|
|
icon: markRaw(IconComputer),
|
|
},
|
|
{
|
|
id: "tablet",
|
|
icon: markRaw(IconTablet),
|
|
},
|
|
{
|
|
id: "phone",
|
|
icon: markRaw(IconPhone),
|
|
},
|
|
];
|
|
|
|
const deviceActiveId = ref(mockDevices[0].id);
|
|
|
|
const iframeClasses = computed(() => {
|
|
if (deviceActiveId.value === "desktop") {
|
|
return "w-full h-full";
|
|
}
|
|
if (deviceActiveId.value === "tablet") {
|
|
return "w-2/3 h-2/3 ring-2 rounded ring-gray-300";
|
|
}
|
|
return "w-96 h-[50rem] ring-2 rounded ring-gray-300";
|
|
});
|
|
</script>
|
|
<template>
|
|
<VModal
|
|
:body-class="['!p-0']"
|
|
:visible="visible"
|
|
fullscreen
|
|
:title="modalTitle"
|
|
:mount-to-body="true"
|
|
@update:visible="onVisibleChange"
|
|
>
|
|
<template #center>
|
|
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
|
<VTabbar
|
|
v-model:active-id="deviceActiveId"
|
|
:items="mockDevices as any"
|
|
type="outline"
|
|
></VTabbar>
|
|
</template>
|
|
<template #actions>
|
|
<span
|
|
v-tooltip="{
|
|
content: $t('core.theme.empty.actions.switch'),
|
|
delay: 300,
|
|
}"
|
|
:class="{ 'bg-gray-200': themesVisible }"
|
|
@click="handleOpenThemes"
|
|
>
|
|
<IconPalette />
|
|
</span>
|
|
<span
|
|
v-tooltip="{
|
|
content: $t('core.theme.preview_model.actions.setting'),
|
|
delay: 300,
|
|
}"
|
|
:class="{ 'bg-gray-200': settingsVisible }"
|
|
@click="handleOpenSettings(undefined)"
|
|
>
|
|
<IconSettings />
|
|
</span>
|
|
<span
|
|
v-tooltip="{
|
|
content: $t('core.common.buttons.refresh'),
|
|
delay: 300,
|
|
}"
|
|
@click="handleRefresh"
|
|
>
|
|
<IconRefreshLine />
|
|
</span>
|
|
<span
|
|
v-tooltip="{
|
|
content: $t('core.theme.preview_model.actions.open'),
|
|
delay: 300,
|
|
}"
|
|
>
|
|
<a :href="previewUrl" target="_blank">
|
|
<IconLink />
|
|
</a>
|
|
</span>
|
|
</template>
|
|
<div
|
|
class="flex h-full items-center justify-center divide-x divide-gray-100 transition-all"
|
|
>
|
|
<transition
|
|
enter-active-class="transform transition ease-in-out duration-300"
|
|
enter-from-class="-translate-x-full"
|
|
enter-to-class="translate-x-0"
|
|
leave-active-class="transform transition ease-in-out duration-300"
|
|
leave-from-class="translate-x-0"
|
|
leave-to-class="-translate-x-full"
|
|
appear
|
|
>
|
|
<OverlayScrollbarsComponent
|
|
v-if="themesVisible || settingsVisible"
|
|
element="div"
|
|
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
|
class="relative h-full w-96"
|
|
:class="{ '!overflow-hidden': switching }"
|
|
defer
|
|
>
|
|
<transition
|
|
enter-active-class="transform transition ease-in-out duration-300 delay-150"
|
|
enter-from-class="translate-x-full"
|
|
enter-to-class="-translate-x-0"
|
|
leave-active-class="transform transition ease-in-out duration-300"
|
|
leave-from-class="-translate-x-0"
|
|
leave-to-class="translate-x-full"
|
|
@before-enter="switching = true"
|
|
@after-enter="switching = false"
|
|
@before-leave="switching = true"
|
|
@after-leave="switching = false"
|
|
>
|
|
<div v-show="settingsVisible" class="mb-20">
|
|
<VTabbar
|
|
v-model:active-id="activeSettingTab"
|
|
:items="settingTabs"
|
|
class="w-full !rounded-none"
|
|
type="outline"
|
|
></VTabbar>
|
|
<div class="bg-white p-3">
|
|
<div v-for="(tab, index) in settingTabs" :key="index">
|
|
<FormKit
|
|
v-if="
|
|
tab.id === activeSettingTab &&
|
|
configMapFormData[tab.id] &&
|
|
formSchema
|
|
"
|
|
:id="`preview-setting-${tab.id}`"
|
|
:key="tab.id"
|
|
v-model="configMapFormData[tab.id]"
|
|
:name="tab.id"
|
|
:actions="false"
|
|
:preserve="true"
|
|
type="form"
|
|
@submit="handleSaveConfigMap"
|
|
>
|
|
<FormKitSchema
|
|
:schema="toRaw(formSchema)"
|
|
:data="configMapFormData[tab.id]"
|
|
/>
|
|
</FormKit>
|
|
</div>
|
|
<div v-permission="['system:themes:manage']" class="pt-5">
|
|
<div class="flex justify-start">
|
|
<VButton
|
|
:loading="saving"
|
|
type="secondary"
|
|
@click="
|
|
$formkit.submit(
|
|
`preview-setting-${activeSettingTab}` || ''
|
|
)
|
|
"
|
|
>
|
|
{{ $t("core.common.buttons.save") }}
|
|
</VButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
<transition
|
|
enter-active-class="transform transition ease-in-out duration-300 delay-150"
|
|
enter-from-class="-translate-x-full"
|
|
enter-to-class="translate-x-0"
|
|
leave-active-class="transform transition ease-in-out duration-300"
|
|
leave-from-class="translate-x-0"
|
|
leave-to-class="-translate-x-full"
|
|
@before-enter="switching = true"
|
|
@after-enter="switching = false"
|
|
@before-leave="switching = true"
|
|
@after-leave="switching = false"
|
|
>
|
|
<ul
|
|
v-show="themesVisible"
|
|
class="box-border h-full w-full divide-y divide-gray-100"
|
|
role="list"
|
|
>
|
|
<li
|
|
v-for="(item, index) in themes"
|
|
:key="index"
|
|
@click="handleSelect(item)"
|
|
>
|
|
<ThemePreviewListItem
|
|
:theme="item"
|
|
:is-selected="
|
|
selectedTheme?.metadata.name === item.metadata.name
|
|
"
|
|
@open-settings="handleOpenSettings(item)"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
</transition>
|
|
<transition
|
|
enter-active-class="transform transition ease-in-out duration-300"
|
|
enter-from-class="translate-y-full"
|
|
enter-to-class="translate-y-0"
|
|
leave-active-class="transform transition ease-in-out duration-300"
|
|
leave-from-class="translate-y-0"
|
|
leave-to-class="translate-y-full"
|
|
>
|
|
<div v-if="settingsVisible" class="fixed bottom-2 left-2">
|
|
<VButton
|
|
size="md"
|
|
circle
|
|
type="primary"
|
|
@click="handleOpenThemes"
|
|
>
|
|
<IconArrowLeft />
|
|
</VButton>
|
|
</div>
|
|
</transition>
|
|
</OverlayScrollbarsComponent>
|
|
</transition>
|
|
<div
|
|
class="flex h-full flex-1 items-center justify-center transition-all duration-300"
|
|
>
|
|
<iframe
|
|
v-if="visible"
|
|
ref="previewFrame"
|
|
class="border-none transition-all duration-500"
|
|
:class="iframeClasses"
|
|
:src="previewUrl"
|
|
></iframe>
|
|
</div>
|
|
</div>
|
|
</VModal>
|
|
</template>
|