perf: improve theme preview modal (#683)

#### What type of PR is this?

/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

重构主题预览弹框,支持选择主题以及针对主题进行设置。

todolist:

- [x] 支持保存之后自动刷新预览区域。
- [x] 优化 Tabs 组件,支持横向滚动以解决设置项过多时,选项卡的样式问题。

#### Screenshots:

https://user-images.githubusercontent.com/21301288/200233823-fe317efe-536a-47a9-9495-efdde39be7ca.mp4


#### Special notes for your reviewer:

测试方式:

1. 需要先执行 `pnpm build:packages`
2. 进入主题管理,点击右上角的预览即可打开主题预览窗口

#### Does this PR introduce a user-facing change?

```release-note
重构主题预览弹框,支持选择主题以及针对主题进行设置。
```
pull/684/head
Ryan Wang 2022-11-07 15:12:15 +08:00 committed by GitHub
parent bb9124231e
commit fdf964b18d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 583 additions and 289 deletions

View File

@ -103,6 +103,9 @@ watch(
<div v-if="$slots.header || title" class="modal-header group">
<slot name="header">
<div class="modal-header-title">{{ title }}</div>
<div v-if="$slots.center" class="modal-header-center">
<slot name="center"></slot>
</div>
<div class="modal-header-actions">
<slot name="actions"></slot>
<span class="bg-gray-50" @click="handleClose()">

View File

@ -3,11 +3,14 @@ import { VTabbar } from "./index";
function initState() {
return {
activeId: "johnniang",
activeId: "general",
items: [
{ label: "Ryan Wang", id: "ryanwang" },
{ label: "JohnNiang", id: "johnniang" },
{ label: "guqing", id: "guqing" },
{ label: "基本设置", id: "general" },
{ label: "文章设置", id: "post" },
{ label: "SEO 设置", id: "seo" },
{ label: "评论设置", id: "comment" },
{ label: "主题路由设置", id: "theme-route" },
{ label: "代码注入", id: "code-inject" },
],
};
}

View File

@ -60,18 +60,45 @@ const handleChange = (id: number | string) => {
.tabbar-items {
@apply flex
items-center
flex-row;
flex-row
overflow-x-auto
py-0.5;
&::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ddd;
background-clip: padding-box;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}
}
.tabbar-item {
@apply flex
@apply inline-flex
cursor-pointer
self-center
transition-all
text-sm
justify-center
gap-2
h-9;
h-9
whitespace-nowrap;
.tabbar-item-label,
.tabbar-item-icon {
@ -126,7 +153,8 @@ const handleChange = (id: number | string) => {
}
&.tabbar-outline {
@apply p-1
@apply px-1
py-0.5
bg-gray-100
rounded-base;

View File

@ -51,6 +51,7 @@ import IconMotionLine from "~icons/ri/emotion-line";
import IconReplyLine from "~icons/ri/reply-line";
import IconExternalLinkLine from "~icons/ri/external-link-line";
import IconRefreshLine from "~icons/ri/refresh-line";
import IconWindowLine from "~icons/ri/window-line";
export {
IconDashboard,
@ -106,4 +107,5 @@ export {
IconReplyLine,
IconExternalLinkLine,
IconRefreshLine,
IconWindowLine,
};

View File

@ -7,86 +7,113 @@ import {
IconPlug,
IconUserSettings,
IconPalette,
IconWindowLine,
VCard,
IconUserLine,
} from "@halo-dev/components";
import { inject, markRaw, type Component } from "vue";
import { inject, markRaw, ref, type Component } from "vue";
import { useRouter } from "vue-router";
import type { RouteLocationRaw } from "vue-router";
import type { User } from "@halo-dev/api-client";
import ThemePreviewModal from "@/modules/interface/themes/components/preview/ThemePreviewModal.vue";
interface Action {
icon: Component;
title: string;
route: RouteLocationRaw;
action: () => void;
permissions?: string[];
}
const currentUser = inject<User>("currentUser");
const router = useRouter();
const themePreviewVisible = ref(false);
const actions: Action[] = [
{
icon: markRaw(IconUserLine),
title: "个人资料",
route: { name: "UserDetail", params: { name: currentUser?.metadata.name } },
action: () => {
router.push({
name: "UserDetail",
params: { name: currentUser?.metadata.name },
});
},
},
{
icon: markRaw(IconWindowLine),
title: "查看站点",
action: () => {
themePreviewVisible.value = true;
},
},
{
icon: markRaw(IconBookRead),
title: "创建文章",
route: {
name: "PostEditor",
action: () => {
router.push({
name: "PostEditor",
});
},
permissions: ["system:posts:manage"],
},
{
icon: markRaw(IconPages),
title: "创建页面",
route: {
name: "SinglePageEditor",
action: () => {
router.push({
name: "SinglePageEditor",
});
},
permissions: ["system:singlepages:manage"],
},
{
icon: markRaw(IconFolder),
title: "附件上传",
route: {
name: "Attachments",
query: {
action: "upload",
},
action: () => {
router.push({
name: "Attachments",
query: {
action: "upload",
},
});
},
permissions: ["system:attachments:manage"],
},
{
icon: markRaw(IconPalette),
title: "主题管理",
route: {
name: "ThemeDetail",
action: () => {
router.push({
name: "ThemeDetail",
});
},
permissions: ["system:themes:view"],
},
{
icon: markRaw(IconPlug),
title: "插件管理",
route: {
name: "Plugins",
action: () => {
router.push({
name: "Plugins",
});
},
permissions: ["system:plugins:view"],
},
{
icon: markRaw(IconUserSettings),
title: "新建用户",
route: {
name: "Users",
query: {
action: "create",
},
action: () => {
router.push({
name: "Users",
query: {
action: "create",
},
});
},
permissions: ["system:users:manage"],
},
];
const router = useRouter();
</script>
<template>
<VCard
@ -100,7 +127,7 @@ const router = useRouter();
:key="index"
v-permission="action.permissions"
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
@click="router.push(action.route)"
@click="action.action"
>
<div>
<span
@ -124,4 +151,5 @@ const router = useRouter();
</div>
</div>
</VCard>
<ThemePreviewModal v-model:visible="themePreviewVisible" title="查看站点" />
</template>

View File

@ -1,175 +0,0 @@
<script lang="ts" setup>
import {
IconComputer,
IconPhone,
IconSave,
IconSettings,
IconTablet,
VButton,
VSpace,
VTabbar,
VTabItem,
VTabs,
} from "@halo-dev/components";
import AttachmentSelectorModal from "@/modules/contents/attachments/components/AttachmentSelectorModal.vue";
import { computed, onMounted, ref, shallowRef } from "vue";
const activeId = ref("general");
const deviceActiveId = ref("desktop");
const attachmentSelectVisible = ref(false);
const settingRootVisible = ref(false);
const settingVisible = ref(false);
const devices = shallowRef([
{
id: "desktop",
icon: IconComputer,
},
{
id: "tablet",
icon: IconTablet,
},
{
id: "phone",
icon: IconPhone,
},
]);
const iframeClasses = computed(() => {
if (deviceActiveId.value === "desktop") {
return "w-full h-full";
}
if (deviceActiveId.value === "tablet") {
return "w-2/3 h-2/3";
}
// phone
return "w-96 h-[50rem]";
});
onMounted(() => {
window.addEventListener(
"message",
function receiveMessageFromIframePage(event) {
if (event.data === "select_image") {
attachmentSelectVisible.value = true;
}
},
false
);
});
</script>
<template>
<AttachmentSelectorModal
v-model:visible="attachmentSelectVisible"
></AttachmentSelectorModal>
<div class="flex h-screen">
<div class="flex-1">
<div
class="grid h-14 grid-cols-2 items-center bg-white px-4 drop-shadow-sm sm:grid-cols-3"
>
<div>
<h2 class="truncate text-xl font-bold text-gray-800">
<span>Anatole</span>
</h2>
</div>
<div class="hidden justify-center sm:flex">
<VTabbar
v-model:active-id="deviceActiveId"
:items="devices"
type="outline"
></VTabbar>
</div>
<div class="flex justify-end">
<VSpace>
<VButton size="sm" type="default" @click="settingVisible = true">
<template #icon>
<IconSettings class="h-full w-full" />
</template>
设置
</VButton>
<VButton type="secondary">
<template #icon>
<IconSave class="h-full w-full" />
</template>
保存
</VButton>
</VSpace>
</div>
</div>
<div
class="flex h-full items-center justify-center"
style="height: calc(100vh - 4rem)"
>
<iframe
:class="iframeClasses"
class="border-none transition-all duration-300"
src="http://localhost:8090"
></iframe>
</div>
</div>
</div>
<Teleport to="body">
<div
v-show="settingRootVisible"
class="drawer-wrapper fixed top-0 left-0 z-[99999] flex h-full w-full flex-row items-end justify-center"
>
<transition
enter-active-class="ease-out duration-200"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
@before-enter="settingRootVisible = true"
@after-leave="settingRootVisible = false"
>
<div
v-show="settingVisible"
class="drawer-layer absolute top-0 left-0 h-full w-full flex-none bg-gray-500 bg-opacity-75 transition-opacity"
@click="settingVisible = false"
></div>
</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-show="settingVisible"
class="drawer-content relative flex h-3/4 w-screen flex-col items-stretch overflow-y-auto rounded-t-md bg-white shadow-xl"
>
<div class="drawer-body">
<div class="h-full w-full overflow-y-auto bg-white drop-shadow-sm">
<VTabs v-model:active-id="activeId" type="outline">
<VTabItem id="general" class="p-3" label="基础设置">
<FormKit :actions="false" type="form">
<FormKit label="Halo 当前版本" type="text"></FormKit>
<FormKit label="首页图片" type="text"></FormKit>
</FormKit>
</VTabItem>
<VTabItem id="style" class="p-3" label="样式设置">
<FormKit :actions="false" type="form">
<FormKit label="文章代码高亮语言" type="text"></FormKit>
<FormKit
:options="[
{ label: 'Java', value: 'java' },
{ label: 'C', value: 'c' },
{ label: 'Go', value: 'go' },
{ label: 'JavaScript', value: 'javascript' },
]"
label="文章代码高亮主题"
type="select"
></FormKit>
</FormKit>
</VTabItem>
</VTabs>
</div>
</div>
</div>
</transition>
</div>
</Teleport>
</template>

View File

@ -2,8 +2,6 @@
import {
IconAddCircle,
IconGitHub,
IconArrowLeft,
IconArrowRight,
Dialog,
VButton,
VEmpty,
@ -17,7 +15,7 @@ import {
VTabs,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
import ThemeUploadModal from "./ThemeUploadModal.vue";
import { computed, ref, watch } from "vue";
import type { Theme } from "@halo-dev/api-client";
@ -32,18 +30,18 @@ const { currentUserHasPermission } = usePermission();
const props = withDefaults(
defineProps<{
visible: boolean;
selectedTheme: Theme | null;
selectedTheme?: Theme;
}>(),
{
visible: false,
selectedTheme: null,
selectedTheme: undefined,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
(event: "update:selectedTheme", theme: Theme | null): void;
(event: "update:selectedTheme", theme?: Theme): void;
(event: "select", theme: Theme | null): void;
}>();
@ -193,48 +191,12 @@ defineExpose({
handleFetchThemes,
});
const preview = ref(false);
const previewVisible = ref(false);
const selectedPreviewTheme = ref<Theme>();
const previewUrl = computed(() => {
if (!selectedPreviewTheme.value) {
return "";
}
return `${import.meta.env.VITE_API_URL}/?preview-theme=${
selectedPreviewTheme.value.metadata.name
}`;
});
const previewModalTitle = computed(() => {
if (!selectedPreviewTheme.value) {
return "";
}
return `预览主题:${selectedPreviewTheme.value.spec.displayName}`;
});
const handleOpenPreview = (theme: Theme) => {
selectedPreviewTheme.value = theme;
preview.value = true;
};
const handleSelectPreviousPreviewTheme = async () => {
const index = themes.value.findIndex(
(theme) => theme.metadata.name === selectedPreviewTheme.value?.metadata.name
);
if (index > 0) {
selectedPreviewTheme.value = themes.value[index - 1];
return;
}
};
const handleSelectNextPreviewTheme = () => {
const index = themes.value.findIndex(
(theme) => theme.metadata.name === selectedPreviewTheme.value?.metadata.name
);
if (index < themes.value.length - 1) {
selectedPreviewTheme.value = themes.value[index + 1];
return;
}
previewVisible.value = true;
};
</script>
<template>
@ -526,18 +488,9 @@ const handleSelectNextPreviewTheme = () => {
@close="handleFetchThemes"
/>
<UrlPreviewModal
v-model:visible="preview"
:title="previewModalTitle"
:url="previewUrl"
>
<template #actions>
<span @click="handleSelectPreviousPreviewTheme">
<IconArrowLeft />
</span>
<span @click="handleSelectNextPreviewTheme">
<IconArrowRight />
</span>
</template>
</UrlPreviewModal>
<ThemePreviewModal
v-if="visible"
v-model:visible="previewVisible"
:theme="selectedPreviewTheme"
/>
</template>

View File

@ -0,0 +1,85 @@
<script lang="ts" setup>
import LazyImage from "@/components/image/LazyImage.vue";
import type { Theme } from "@halo-dev/api-client";
import { VEntity, VEntityField, VTag, VButton } from "@halo-dev/components";
import { toRefs } from "vue";
import { useThemeLifeCycle } from "../../composables/use-theme";
const props = withDefaults(
defineProps<{
theme: Theme;
isSelected?: boolean;
}>(),
{
isSelected: false,
}
);
const emit = defineEmits<{
(event: "open-settings"): void;
}>();
const { theme } = toRefs(props);
const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
</script>
<template>
<VEntity :is-selected="isSelected">
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400">加载中...</span>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400">加载异常</span>
</div>
</template>
</LazyImage>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
<template #extra>
<VTag v-if="isActivated"> </VTag>
</template>
</VEntityField>
</template>
<template #dropdownItems>
<VButton v-close-popper block type="secondary" @click="handleActiveTheme">
启用
</VButton>
<VButton
v-close-popper
block
type="default"
@click="emit('open-settings')"
>
设置
</VButton>
</template>
</VEntity>
</template>

View File

@ -0,0 +1,383 @@
<script lang="ts" setup>
import ThemeListItem from "./ThemeListItem.vue";
import { useSettingForm } 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 {
VModal,
IconLink,
IconPalette,
IconSettings,
IconArrowLeft,
VTabbar,
VButton,
IconComputer,
IconPhone,
IconTablet,
IconRefreshLine,
} from "@halo-dev/components";
import { storeToRefs } from "pinia";
import { computed, markRaw, ref, watch } from "vue";
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;
}>();
interface SettingTab {
id: string;
label: string;
}
const { activatedTheme } = storeToRefs(useThemeStore());
const previewFrame = ref<HTMLIFrameElement | null>(null);
const themes = ref<Theme[]>([] as Theme[]);
const themesVisible = ref(false);
const switching = ref(false);
const selectedTheme = ref<Theme>();
const handleFetchThemes = async () => {
try {
const { data } = await apiClient.theme.listThemes({
page: 0,
size: 0,
uninstalled: false,
});
themes.value = data.items;
} catch (e) {
console.error("Failed to fetch themes", e);
}
};
watch(
() => props.visible,
(visible) => {
if (visible) {
handleFetchThemes();
selectedTheme.value = props.theme || activatedTheme?.value;
} else {
themesVisible.value = false;
settingsVisible.value = false;
}
}
);
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 `预览主题:${selectedTheme.value?.spec.displayName}`;
});
// theme settings
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 {
setting,
configMapFormData,
saving,
handleFetchConfigMap,
handleFetchSettings,
handleSaveConfigMap,
} = useSettingForm(settingName, configMapName);
watch(
() => selectedTheme.value,
async () => {
if (selectedTheme.value) {
await handleFetchSettings();
await handleFetchConfigMap();
if (setting.value) {
const { forms } = setting.value.spec;
settingTabs.value = forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
};
});
}
activeSettingTab.value = settingTabs.value[0].id;
}
}
);
const handleOpenSettings = (theme?: Theme) => {
if (theme) {
selectedTheme.value = theme;
}
themesVisible.value = false;
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();
};
// 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>
<VTabbar
v-model:active-id="deviceActiveId"
:items="mockDevices"
type="outline"
></VTabbar>
</template>
<template #actions>
<span
v-tooltip="{ content: '切换主题', delay: 300 }"
:class="{ 'bg-gray-200': themesVisible }"
@click="handleOpenThemes"
>
<IconPalette />
</span>
<span
v-tooltip="{ content: '主题设置', delay: 300 }"
:class="{ 'bg-gray-200': settingsVisible }"
@click="handleOpenSettings(undefined)"
>
<IconSettings />
</span>
<span v-tooltip="{ content: '刷新', delay: 300 }" @click="handleRefresh">
<IconRefreshLine />
</span>
<span v-tooltip="{ content: '新窗口打开', 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
>
<div
v-if="themesVisible || settingsVisible"
class="relative h-full w-96 overflow-y-auto"
:class="{ '!overflow-hidden': switching }"
style="overflow-y: overlay"
>
<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 &&
formSchema
"
:id="tab.id"
:key="tab.id"
v-model="configMapFormData[tab.id]"
:name="tab.id"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveThemeConfigMap"
>
<FormKitSchema :schema="formSchema" />
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(activeSettingTab || '')"
>
保存
</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)"
>
<ThemeListItem
: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>
</div>
</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>

View File

@ -26,6 +26,7 @@ import {
VTabbar,
} 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 { usePermission } from "@/utils/permission";
import { useThemeStore } from "@/stores/theme";
@ -53,8 +54,9 @@ const initialTabs: ThemeTab[] = [
];
const tabs = ref<ThemeTab[]>(cloneDeep(initialTabs));
const selectedTheme = ref<Theme | undefined>();
const selectedTheme = ref<Theme>();
const themesModal = ref(false);
const previewModal = ref(false);
const activeTab = ref("");
const { loading, isActivated, handleActiveTheme } =
@ -181,15 +183,11 @@ onMounted(() => {
>
启用
</VButton>
<VButton
v-if="false"
:route="{ name: 'ThemeVisual' }"
type="secondary"
>
<VButton type="secondary" size="sm" @click="previewModal = true">
<template #icon>
<IconEye class="h-full w-full" />
</template>
可视化编辑
预览
</VButton>
</VSpace>
</template>
@ -242,5 +240,7 @@ onMounted(() => {
</div>
</div>
</div>
<ThemePreviewModal v-model:visible="previewModal" :theme="selectedTheme" />
</BasicLayout>
</template>

View File

@ -1,9 +1,7 @@
import { definePlugin } from "@halo-dev/console-shared";
import BlankLayout from "@/layouts/BlankLayout.vue";
import ThemeLayout from "./layouts/ThemeLayout.vue";
import ThemeDetail from "./ThemeDetail.vue";
import ThemeSetting from "./ThemeSetting.vue";
import Visual from "./Visual.vue";
import { IconPalette } from "@halo-dev/components";
import { markRaw } from "vue";
@ -42,19 +40,5 @@ export default definePlugin({
},
],
},
{
path: "/theme/visual",
component: BlankLayout,
children: [
{
path: "",
name: "ThemeVisual",
component: Visual,
meta: {
title: "可视化编辑",
},
},
],
},
],
});