mirror of https://github.com/halo-dev/halo
fix: refactor the theme and post preview feature to solve the session expiration issue (#5518)
#### What type of PR is this? /area ui /kind bug /milestone 2.14.x #### What this PR does / why we need it: 重构文章预览和主题预览的页面加载方式,尝试解决在某些情况下打开预览导致登录 session 失效的问题。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/4047 #### Special notes for your reviewer: 需要测试: 1. 主题预览中的各个功能是否正常。 2. 文章预览功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note 修复在某些情况下,打开文章预览或主题预览之后导致登录失效的问题。 ```pull/5531/head
parent
fa26516ecc
commit
47d837952b
|
@ -433,10 +433,10 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UrlPreviewModal
|
<UrlPreviewModal
|
||||||
v-if="isUpdateMode"
|
v-if="previewModal"
|
||||||
v-model:visible="previewModal"
|
|
||||||
:title="formState.page.spec.title"
|
:title="formState.page.spec.title"
|
||||||
:url="`/preview/singlepages/${formState.page.metadata.name}`"
|
:url="`/preview/singlepages/${formState.page.metadata.name}`"
|
||||||
|
@close="previewModal = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VPageHeader :title="$t('core.page.title')">
|
<VPageHeader :title="$t('core.page.title')">
|
||||||
|
|
|
@ -461,10 +461,10 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UrlPreviewModal
|
<UrlPreviewModal
|
||||||
v-if="isUpdateMode"
|
v-if="previewModal"
|
||||||
v-model:visible="previewModal"
|
|
||||||
:title="formState.post.spec.title"
|
:title="formState.post.spec.title"
|
||||||
:url="`/preview/posts/${formState.post.metadata.name}`"
|
:url="`/preview/posts/${formState.post.metadata.name}`"
|
||||||
|
@close="previewModal = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VPageHeader :title="$t('core.post.title')">
|
<VPageHeader :title="$t('core.post.title')">
|
||||||
|
|
|
@ -229,9 +229,10 @@ const actions: Action[] = [
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</VCard>
|
</VCard>
|
||||||
<ThemePreviewModal
|
<ThemePreviewModal
|
||||||
v-model:visible="themePreviewVisible"
|
v-if="themePreviewVisible"
|
||||||
:title="
|
:title="
|
||||||
$t('core.dashboard.widgets.presets.quicklink.actions.view_site.title')
|
$t('core.dashboard.widgets.presets.quicklink.actions.view_site.title')
|
||||||
"
|
"
|
||||||
|
@close="themePreviewVisible = false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -106,8 +106,9 @@ const handleOpenPreview = (theme: Theme) => {
|
||||||
</ul>
|
</ul>
|
||||||
</Transition>
|
</Transition>
|
||||||
<ThemePreviewModal
|
<ThemePreviewModal
|
||||||
v-model:visible="previewVisible"
|
v-if="previewVisible"
|
||||||
:theme="selectedPreviewTheme"
|
:theme="selectedPreviewTheme"
|
||||||
|
@close="previewVisible = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
|
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
|
||||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||||
import { useThemeStore } from "@console/stores/theme";
|
import { useThemeStore } from "@console/stores/theme";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient, axiosInstance } from "@/utils/api-client";
|
||||||
import type {
|
import type {
|
||||||
ConfigMap,
|
ConfigMap,
|
||||||
Setting,
|
Setting,
|
||||||
|
@ -22,28 +22,27 @@ import {
|
||||||
IconTablet,
|
IconTablet,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
Toast,
|
Toast,
|
||||||
|
VLoading,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { computed, markRaw, ref, toRaw, watch } from "vue";
|
import { computed, markRaw, ref, toRaw } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
theme: undefined,
|
theme: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -56,7 +55,6 @@ interface SettingTab {
|
||||||
|
|
||||||
const { activatedTheme } = storeToRefs(useThemeStore());
|
const { activatedTheme } = storeToRefs(useThemeStore());
|
||||||
|
|
||||||
const previewFrame = ref<HTMLIFrameElement | null>(null);
|
|
||||||
const themesVisible = ref(false);
|
const themesVisible = ref(false);
|
||||||
const switching = ref(false);
|
const switching = ref(false);
|
||||||
const selectedTheme = ref<Theme>();
|
const selectedTheme = ref<Theme>();
|
||||||
|
@ -71,29 +69,11 @@ const { data: themes } = useQuery<Theme[]>({
|
||||||
});
|
});
|
||||||
return data.items;
|
return data.items;
|
||||||
},
|
},
|
||||||
enabled: computed(() => props.visible),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
onMounted(() => {
|
||||||
() => props.visible,
|
selectedTheme.value = toRaw(props.theme) || toRaw(activatedTheme?.value);
|
||||||
(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 = () => {
|
const handleOpenThemes = () => {
|
||||||
settingsVisible.value = false;
|
settingsVisible.value = false;
|
||||||
|
@ -122,6 +102,26 @@ const modalTitle = computed(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: previewHTML,
|
||||||
|
isLoading,
|
||||||
|
refetch: refetchPreviewHTML,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["site-preview", previewUrl],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await axiosInstance.get(previewUrl.value, {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/html",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache",
|
||||||
|
Expires: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!previewUrl.value),
|
||||||
|
});
|
||||||
|
|
||||||
// theme settings
|
// theme settings
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
|
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
|
||||||
|
@ -150,9 +150,7 @@ const { data: setting } = useQuery<Setting>({
|
||||||
|
|
||||||
activeSettingTab.value = settingTabs.value[0].id;
|
activeSettingTab.value = settingTabs.value[0].id;
|
||||||
},
|
},
|
||||||
enabled: computed(
|
enabled: computed(() => !!selectedTheme.value?.spec.settingName),
|
||||||
() => props.visible && !!selectedTheme.value?.spec.settingName
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: configMap, refetch: handleFetchConfigMap } = useQuery<ConfigMap>({
|
const { data: configMap, refetch: handleFetchConfigMap } = useQuery<ConfigMap>({
|
||||||
|
@ -195,7 +193,7 @@ const handleSaveConfigMap = async () => {
|
||||||
|
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
|
|
||||||
handleRefresh();
|
refetchPreviewHTML();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSettings = (theme?: Theme) => {
|
const handleOpenSettings = (theme?: Theme) => {
|
||||||
|
@ -206,10 +204,6 @@ const handleOpenSettings = (theme?: Theme) => {
|
||||||
settingsVisible.value = !settingsVisible.value;
|
settingsVisible.value = !settingsVisible.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
previewFrame.value?.contentWindow?.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
// mock devices
|
// mock devices
|
||||||
const mockDevices = [
|
const mockDevices = [
|
||||||
{
|
{
|
||||||
|
@ -241,11 +235,10 @@ const iframeClasses = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
:body-class="['!p-0']"
|
:body-class="['!p-0']"
|
||||||
:visible="visible"
|
|
||||||
fullscreen
|
fullscreen
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
:mount-to-body="true"
|
:mount-to-body="true"
|
||||||
@update:visible="onVisibleChange"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<template #center>
|
<template #center>
|
||||||
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
||||||
|
@ -281,7 +274,7 @@ const iframeClasses = computed(() => {
|
||||||
content: $t('core.common.buttons.refresh'),
|
content: $t('core.common.buttons.refresh'),
|
||||||
delay: 300,
|
delay: 300,
|
||||||
}"
|
}"
|
||||||
@click="handleRefresh"
|
@click="refetchPreviewHTML()"
|
||||||
>
|
>
|
||||||
<IconRefreshLine />
|
<IconRefreshLine />
|
||||||
</span>
|
</span>
|
||||||
|
@ -432,12 +425,12 @@ const iframeClasses = computed(() => {
|
||||||
<div
|
<div
|
||||||
class="flex h-full flex-1 items-center justify-center transition-all duration-300"
|
class="flex h-full flex-1 items-center justify-center transition-all duration-300"
|
||||||
>
|
>
|
||||||
|
<VLoading v-if="isLoading" />
|
||||||
<iframe
|
<iframe
|
||||||
v-if="visible"
|
v-else
|
||||||
ref="previewFrame"
|
|
||||||
class="border-none transition-all duration-500"
|
class="border-none transition-all duration-500"
|
||||||
:class="iframeClasses"
|
:class="iframeClasses"
|
||||||
:src="previewUrl"
|
:srcdoc="previewHTML"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -269,6 +269,10 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeListModal v-model:visible="themesModal" @select="onSelectTheme" />
|
<ThemeListModal v-model:visible="themesModal" @select="onSelectTheme" />
|
||||||
<ThemePreviewModal v-model:visible="previewModal" :theme="selectedTheme" />
|
<ThemePreviewModal
|
||||||
|
v-if="previewModal"
|
||||||
|
:theme="selectedTheme"
|
||||||
|
@close="previewModal = false"
|
||||||
|
/>
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { axiosInstance } from "@/utils/api-client";
|
||||||
import {
|
import {
|
||||||
VModal,
|
VModal,
|
||||||
IconLink,
|
IconLink,
|
||||||
|
@ -6,34 +7,29 @@ import {
|
||||||
IconComputer,
|
IconComputer,
|
||||||
IconTablet,
|
IconTablet,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
|
VLoading,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { toRefs } from "vue";
|
||||||
import { computed, markRaw, ref } from "vue";
|
import { computed, markRaw, ref } from "vue";
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
url: "",
|
url: "",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { url } = toRefs(props);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
|
||||||
emit("update:visible", visible);
|
|
||||||
if (!visible) {
|
|
||||||
emit("close");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockDevices = [
|
const mockDevices = [
|
||||||
{
|
{
|
||||||
id: "desktop",
|
id: "desktop",
|
||||||
|
@ -59,15 +55,30 @@ const iframeClasses = computed(() => {
|
||||||
}
|
}
|
||||||
return "w-96 h-[50rem] ring-2 rounded ring-gray-300";
|
return "w-96 h-[50rem] ring-2 rounded ring-gray-300";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: html, isLoading } = useQuery({
|
||||||
|
queryKey: ["url-preview", url],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await axiosInstance.get(url.value, {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/html",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache",
|
||||||
|
Expires: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!url.value),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
:body-class="['!p-0']"
|
:body-class="['!p-0']"
|
||||||
:visible="visible"
|
|
||||||
fullscreen
|
fullscreen
|
||||||
:title="title"
|
:title="title"
|
||||||
:layer-closable="true"
|
:layer-closable="true"
|
||||||
@update:visible="onVisibleChange"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<template #center>
|
<template #center>
|
||||||
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
||||||
|
@ -86,11 +97,12 @@ const iframeClasses = computed(() => {
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<VLoading v-if="isLoading" />
|
||||||
<iframe
|
<iframe
|
||||||
v-if="visible"
|
v-else
|
||||||
class="border-none transition-all duration-500"
|
class="border-none transition-all duration-500"
|
||||||
:class="iframeClasses"
|
:class="iframeClasses"
|
||||||
:src="url"
|
:srcdoc="html"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</VModal>
|
</VModal>
|
||||||
|
|
|
@ -273,4 +273,4 @@ function setupApiClient(axios: AxiosInstance) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { apiClient };
|
export { apiClient, axiosInstance };
|
||||||
|
|
Loading…
Reference in New Issue