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
Ryan Wang 2024-03-19 14:58:22 +08:00 committed by GitHub
parent fa26516ecc
commit 47d837952b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 75 additions and 64 deletions

View File

@ -433,10 +433,10 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
/>
<UrlPreviewModal
v-if="isUpdateMode"
v-model:visible="previewModal"
v-if="previewModal"
:title="formState.page.spec.title"
:url="`/preview/singlepages/${formState.page.metadata.name}`"
@close="previewModal = false"
/>
<VPageHeader :title="$t('core.page.title')">

View File

@ -461,10 +461,10 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
/>
<UrlPreviewModal
v-if="isUpdateMode"
v-model:visible="previewModal"
v-if="previewModal"
:title="formState.post.spec.title"
:url="`/preview/posts/${formState.post.metadata.name}`"
@close="previewModal = false"
/>
<VPageHeader :title="$t('core.post.title')">

View File

@ -229,9 +229,10 @@ const actions: Action[] = [
</OverlayScrollbarsComponent>
</VCard>
<ThemePreviewModal
v-model:visible="themePreviewVisible"
v-if="themePreviewVisible"
:title="
$t('core.dashboard.widgets.presets.quicklink.actions.view_site.title')
"
@close="themePreviewVisible = false"
/>
</template>

View File

@ -106,8 +106,9 @@ const handleOpenPreview = (theme: Theme) => {
</ul>
</Transition>
<ThemePreviewModal
v-model:visible="previewVisible"
v-if="previewVisible"
:theme="selectedPreviewTheme"
@close="previewVisible = false"
/>
</div>
</template>

View File

@ -2,7 +2,7 @@
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 { apiClient, axiosInstance } from "@/utils/api-client";
import type {
ConfigMap,
Setting,
@ -22,28 +22,27 @@ import {
IconTablet,
IconRefreshLine,
Toast,
VLoading,
} from "@halo-dev/components";
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 { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import { useQuery } from "@tanstack/vue-query";
import { onMounted } 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;
}>();
@ -56,7 +55,6 @@ interface SettingTab {
const { activatedTheme } = storeToRefs(useThemeStore());
const previewFrame = ref<HTMLIFrameElement | null>(null);
const themesVisible = ref(false);
const switching = ref(false);
const selectedTheme = ref<Theme>();
@ -71,29 +69,11 @@ const { data: themes } = useQuery<Theme[]>({
});
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");
}
};
onMounted(() => {
selectedTheme.value = toRaw(props.theme) || toRaw(activatedTheme?.value);
});
const handleOpenThemes = () => {
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
const saving = ref(false);
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
@ -150,9 +150,7 @@ const { data: setting } = useQuery<Setting>({
activeSettingTab.value = settingTabs.value[0].id;
},
enabled: computed(
() => props.visible && !!selectedTheme.value?.spec.settingName
),
enabled: computed(() => !!selectedTheme.value?.spec.settingName),
});
const { data: configMap, refetch: handleFetchConfigMap } = useQuery<ConfigMap>({
@ -195,7 +193,7 @@ const handleSaveConfigMap = async () => {
saving.value = false;
handleRefresh();
refetchPreviewHTML();
};
const handleOpenSettings = (theme?: Theme) => {
@ -206,10 +204,6 @@ const handleOpenSettings = (theme?: Theme) => {
settingsVisible.value = !settingsVisible.value;
};
const handleRefresh = () => {
previewFrame.value?.contentWindow?.location.reload();
};
// mock devices
const mockDevices = [
{
@ -241,11 +235,10 @@ const iframeClasses = computed(() => {
<template>
<VModal
:body-class="['!p-0']"
:visible="visible"
fullscreen
:title="modalTitle"
:mount-to-body="true"
@update:visible="onVisibleChange"
@close="emit('close')"
>
<template #center>
<!-- TODO: Reactor VTabbar component to support icon prop -->
@ -281,7 +274,7 @@ const iframeClasses = computed(() => {
content: $t('core.common.buttons.refresh'),
delay: 300,
}"
@click="handleRefresh"
@click="refetchPreviewHTML()"
>
<IconRefreshLine />
</span>
@ -432,12 +425,12 @@ const iframeClasses = computed(() => {
<div
class="flex h-full flex-1 items-center justify-center transition-all duration-300"
>
<VLoading v-if="isLoading" />
<iframe
v-if="visible"
ref="previewFrame"
v-else
class="border-none transition-all duration-500"
:class="iframeClasses"
:src="previewUrl"
:srcdoc="previewHTML"
></iframe>
</div>
</div>

View File

@ -269,6 +269,10 @@ onMounted(() => {
</div>
<ThemeListModal v-model:visible="themesModal" @select="onSelectTheme" />
<ThemePreviewModal v-model:visible="previewModal" :theme="selectedTheme" />
<ThemePreviewModal
v-if="previewModal"
:theme="selectedTheme"
@close="previewModal = false"
/>
</BasicLayout>
</template>

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { axiosInstance } from "@/utils/api-client";
import {
VModal,
IconLink,
@ -6,34 +7,29 @@ import {
IconComputer,
IconTablet,
IconPhone,
VLoading,
} from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import { toRefs } from "vue";
import { computed, markRaw, ref } from "vue";
withDefaults(
const props = withDefaults(
defineProps<{
visible: boolean;
title?: string;
url?: string;
}>(),
{
visible: false,
title: undefined,
url: "",
}
);
const { url } = toRefs(props);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const mockDevices = [
{
id: "desktop",
@ -59,15 +55,30 @@ const iframeClasses = computed(() => {
}
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>
<template>
<VModal
:body-class="['!p-0']"
:visible="visible"
fullscreen
:title="title"
:layer-closable="true"
@update:visible="onVisibleChange"
@close="emit('close')"
>
<template #center>
<!-- TODO: Reactor VTabbar component to support icon prop -->
@ -86,11 +97,12 @@ const iframeClasses = computed(() => {
</span>
</template>
<div class="flex h-full items-center justify-center">
<VLoading v-if="isLoading" />
<iframe
v-if="visible"
v-else
class="border-none transition-all duration-500"
:class="iframeClasses"
:src="url"
:srcdoc="html"
></iframe>
</div>
</VModal>

View File

@ -273,4 +273,4 @@ function setupApiClient(axios: AxiosInstance) {
};
}
export { apiClient };
export { apiClient, axiosInstance };