mirror of https://github.com/halo-dev/halo-admin
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
parent
bb9124231e
commit
fdf964b18d
|
@ -103,6 +103,9 @@ watch(
|
||||||
<div v-if="$slots.header || title" class="modal-header group">
|
<div v-if="$slots.header || title" class="modal-header group">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<div class="modal-header-title">{{ title }}</div>
|
<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">
|
<div class="modal-header-actions">
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
<span class="bg-gray-50" @click="handleClose()">
|
<span class="bg-gray-50" @click="handleClose()">
|
||||||
|
|
|
@ -3,11 +3,14 @@ import { VTabbar } from "./index";
|
||||||
|
|
||||||
function initState() {
|
function initState() {
|
||||||
return {
|
return {
|
||||||
activeId: "johnniang",
|
activeId: "general",
|
||||||
items: [
|
items: [
|
||||||
{ label: "Ryan Wang", id: "ryanwang" },
|
{ label: "基本设置", id: "general" },
|
||||||
{ label: "JohnNiang", id: "johnniang" },
|
{ label: "文章设置", id: "post" },
|
||||||
{ label: "guqing", id: "guqing" },
|
{ label: "SEO 设置", id: "seo" },
|
||||||
|
{ label: "评论设置", id: "comment" },
|
||||||
|
{ label: "主题路由设置", id: "theme-route" },
|
||||||
|
{ label: "代码注入", id: "code-inject" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,18 +60,45 @@ const handleChange = (id: number | string) => {
|
||||||
.tabbar-items {
|
.tabbar-items {
|
||||||
@apply flex
|
@apply flex
|
||||||
items-center
|
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 {
|
.tabbar-item {
|
||||||
@apply flex
|
@apply inline-flex
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
self-center
|
self-center
|
||||||
transition-all
|
transition-all
|
||||||
text-sm
|
text-sm
|
||||||
justify-center
|
justify-center
|
||||||
gap-2
|
gap-2
|
||||||
h-9;
|
h-9
|
||||||
|
whitespace-nowrap;
|
||||||
|
|
||||||
.tabbar-item-label,
|
.tabbar-item-label,
|
||||||
.tabbar-item-icon {
|
.tabbar-item-icon {
|
||||||
|
@ -126,7 +153,8 @@ const handleChange = (id: number | string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.tabbar-outline {
|
&.tabbar-outline {
|
||||||
@apply p-1
|
@apply px-1
|
||||||
|
py-0.5
|
||||||
bg-gray-100
|
bg-gray-100
|
||||||
rounded-base;
|
rounded-base;
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import IconMotionLine from "~icons/ri/emotion-line";
|
||||||
import IconReplyLine from "~icons/ri/reply-line";
|
import IconReplyLine from "~icons/ri/reply-line";
|
||||||
import IconExternalLinkLine from "~icons/ri/external-link-line";
|
import IconExternalLinkLine from "~icons/ri/external-link-line";
|
||||||
import IconRefreshLine from "~icons/ri/refresh-line";
|
import IconRefreshLine from "~icons/ri/refresh-line";
|
||||||
|
import IconWindowLine from "~icons/ri/window-line";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
|
@ -106,4 +107,5 @@ export {
|
||||||
IconReplyLine,
|
IconReplyLine,
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
|
IconWindowLine,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,86 +7,113 @@ import {
|
||||||
IconPlug,
|
IconPlug,
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
IconPalette,
|
IconPalette,
|
||||||
|
IconWindowLine,
|
||||||
VCard,
|
VCard,
|
||||||
IconUserLine,
|
IconUserLine,
|
||||||
} from "@halo-dev/components";
|
} 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 { useRouter } from "vue-router";
|
||||||
import type { RouteLocationRaw } from "vue-router";
|
|
||||||
import type { User } from "@halo-dev/api-client";
|
import type { User } from "@halo-dev/api-client";
|
||||||
|
import ThemePreviewModal from "@/modules/interface/themes/components/preview/ThemePreviewModal.vue";
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
icon: Component;
|
icon: Component;
|
||||||
title: string;
|
title: string;
|
||||||
route: RouteLocationRaw;
|
action: () => void;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUser = inject<User>("currentUser");
|
const currentUser = inject<User>("currentUser");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const themePreviewVisible = ref(false);
|
||||||
|
|
||||||
const actions: Action[] = [
|
const actions: Action[] = [
|
||||||
{
|
{
|
||||||
icon: markRaw(IconUserLine),
|
icon: markRaw(IconUserLine),
|
||||||
title: "个人资料",
|
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),
|
icon: markRaw(IconBookRead),
|
||||||
title: "创建文章",
|
title: "创建文章",
|
||||||
route: {
|
action: () => {
|
||||||
name: "PostEditor",
|
router.push({
|
||||||
|
name: "PostEditor",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:posts:manage"],
|
permissions: ["system:posts:manage"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: markRaw(IconPages),
|
icon: markRaw(IconPages),
|
||||||
title: "创建页面",
|
title: "创建页面",
|
||||||
route: {
|
action: () => {
|
||||||
name: "SinglePageEditor",
|
router.push({
|
||||||
|
name: "SinglePageEditor",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:singlepages:manage"],
|
permissions: ["system:singlepages:manage"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: markRaw(IconFolder),
|
icon: markRaw(IconFolder),
|
||||||
title: "附件上传",
|
title: "附件上传",
|
||||||
route: {
|
action: () => {
|
||||||
name: "Attachments",
|
router.push({
|
||||||
query: {
|
name: "Attachments",
|
||||||
action: "upload",
|
query: {
|
||||||
},
|
action: "upload",
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:attachments:manage"],
|
permissions: ["system:attachments:manage"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: markRaw(IconPalette),
|
icon: markRaw(IconPalette),
|
||||||
title: "主题管理",
|
title: "主题管理",
|
||||||
route: {
|
action: () => {
|
||||||
name: "ThemeDetail",
|
router.push({
|
||||||
|
name: "ThemeDetail",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:themes:view"],
|
permissions: ["system:themes:view"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: markRaw(IconPlug),
|
icon: markRaw(IconPlug),
|
||||||
title: "插件管理",
|
title: "插件管理",
|
||||||
route: {
|
action: () => {
|
||||||
name: "Plugins",
|
router.push({
|
||||||
|
name: "Plugins",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:plugins:view"],
|
permissions: ["system:plugins:view"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: markRaw(IconUserSettings),
|
icon: markRaw(IconUserSettings),
|
||||||
title: "新建用户",
|
title: "新建用户",
|
||||||
route: {
|
action: () => {
|
||||||
name: "Users",
|
router.push({
|
||||||
query: {
|
name: "Users",
|
||||||
action: "create",
|
query: {
|
||||||
},
|
action: "create",
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
permissions: ["system:users:manage"],
|
permissions: ["system:users:manage"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VCard
|
<VCard
|
||||||
|
@ -100,7 +127,7 @@ const router = useRouter();
|
||||||
:key="index"
|
:key="index"
|
||||||
v-permission="action.permissions"
|
v-permission="action.permissions"
|
||||||
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
|
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
|
||||||
@click="router.push(action.route)"
|
@click="action.action"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
|
@ -124,4 +151,5 @@ const router = useRouter();
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
<ThemePreviewModal v-model:visible="themePreviewVisible" title="查看站点" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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>
|
|
|
@ -2,8 +2,6 @@
|
||||||
import {
|
import {
|
||||||
IconAddCircle,
|
IconAddCircle,
|
||||||
IconGitHub,
|
IconGitHub,
|
||||||
IconArrowLeft,
|
|
||||||
IconArrowRight,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
VButton,
|
VButton,
|
||||||
VEmpty,
|
VEmpty,
|
||||||
|
@ -17,7 +15,7 @@ import {
|
||||||
VTabs,
|
VTabs,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import LazyImage from "@/components/image/LazyImage.vue";
|
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 ThemeUploadModal from "./ThemeUploadModal.vue";
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import type { Theme } from "@halo-dev/api-client";
|
import type { Theme } from "@halo-dev/api-client";
|
||||||
|
@ -32,18 +30,18 @@ const { currentUserHasPermission } = usePermission();
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
selectedTheme: Theme | null;
|
selectedTheme?: Theme;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
selectedTheme: null,
|
selectedTheme: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
(event: "update:visible", visible: boolean): void;
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
(event: "update:selectedTheme", theme: Theme | null): void;
|
(event: "update:selectedTheme", theme?: Theme): void;
|
||||||
(event: "select", theme: Theme | null): void;
|
(event: "select", theme: Theme | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -193,48 +191,12 @@ defineExpose({
|
||||||
handleFetchThemes,
|
handleFetchThemes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const preview = ref(false);
|
const previewVisible = ref(false);
|
||||||
const selectedPreviewTheme = ref<Theme>();
|
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) => {
|
const handleOpenPreview = (theme: Theme) => {
|
||||||
selectedPreviewTheme.value = theme;
|
selectedPreviewTheme.value = theme;
|
||||||
preview.value = true;
|
previewVisible.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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
@ -526,18 +488,9 @@ const handleSelectNextPreviewTheme = () => {
|
||||||
@close="handleFetchThemes"
|
@close="handleFetchThemes"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UrlPreviewModal
|
<ThemePreviewModal
|
||||||
v-model:visible="preview"
|
v-if="visible"
|
||||||
:title="previewModalTitle"
|
v-model:visible="previewVisible"
|
||||||
:url="previewUrl"
|
:theme="selectedPreviewTheme"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<span @click="handleSelectPreviousPreviewTheme">
|
|
||||||
<IconArrowLeft />
|
|
||||||
</span>
|
|
||||||
<span @click="handleSelectNextPreviewTheme">
|
|
||||||
<IconArrowRight />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</UrlPreviewModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -26,6 +26,7 @@ import {
|
||||||
VTabbar,
|
VTabbar,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import ThemeListModal from "../components/ThemeListModal.vue";
|
import ThemeListModal from "../components/ThemeListModal.vue";
|
||||||
|
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
|
||||||
import type { SettingForm, Theme } from "@halo-dev/api-client";
|
import type { SettingForm, Theme } from "@halo-dev/api-client";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useThemeStore } from "@/stores/theme";
|
import { useThemeStore } from "@/stores/theme";
|
||||||
|
@ -53,8 +54,9 @@ const initialTabs: ThemeTab[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabs = ref<ThemeTab[]>(cloneDeep(initialTabs));
|
const tabs = ref<ThemeTab[]>(cloneDeep(initialTabs));
|
||||||
const selectedTheme = ref<Theme | undefined>();
|
const selectedTheme = ref<Theme>();
|
||||||
const themesModal = ref(false);
|
const themesModal = ref(false);
|
||||||
|
const previewModal = ref(false);
|
||||||
const activeTab = ref("");
|
const activeTab = ref("");
|
||||||
|
|
||||||
const { loading, isActivated, handleActiveTheme } =
|
const { loading, isActivated, handleActiveTheme } =
|
||||||
|
@ -181,15 +183,11 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
启用
|
启用
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton
|
<VButton type="secondary" size="sm" @click="previewModal = true">
|
||||||
v-if="false"
|
|
||||||
:route="{ name: 'ThemeVisual' }"
|
|
||||||
type="secondary"
|
|
||||||
>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconEye class="h-full w-full" />
|
<IconEye class="h-full w-full" />
|
||||||
</template>
|
</template>
|
||||||
可视化编辑
|
预览
|
||||||
</VButton>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</template>
|
</template>
|
||||||
|
@ -242,5 +240,7 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ThemePreviewModal v-model:visible="previewModal" :theme="selectedTheme" />
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { definePlugin } from "@halo-dev/console-shared";
|
import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import BlankLayout from "@/layouts/BlankLayout.vue";
|
|
||||||
import ThemeLayout from "./layouts/ThemeLayout.vue";
|
import ThemeLayout from "./layouts/ThemeLayout.vue";
|
||||||
import ThemeDetail from "./ThemeDetail.vue";
|
import ThemeDetail from "./ThemeDetail.vue";
|
||||||
import ThemeSetting from "./ThemeSetting.vue";
|
import ThemeSetting from "./ThemeSetting.vue";
|
||||||
import Visual from "./Visual.vue";
|
|
||||||
import { IconPalette } from "@halo-dev/components";
|
import { IconPalette } from "@halo-dev/components";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
|
@ -42,19 +40,5 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/theme/visual",
|
|
||||||
component: BlankLayout,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
name: "ThemeVisual",
|
|
||||||
component: Visual,
|
|
||||||
meta: {
|
|
||||||
title: "可视化编辑",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue