feat: Regular monitoring page update [deploy]

This commit is contained in:
vben
2024-07-29 22:11:22 +08:00
parent 66fd052709
commit cd10eb9471
37 changed files with 491 additions and 261 deletions

View File

@@ -4,6 +4,7 @@ const defaultPreferences: Preferences = {
app: {
accessMode: 'frontend',
authPageLayout: 'panel-right',
checkUpdatesPollingTime: 1,
colorGrayMode: false,
colorWeakMode: false,
compact: false,
@@ -11,6 +12,7 @@ const defaultPreferences: Preferences = {
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.5/source/avatar-v1.webp',
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,
isMobile: false,
layout: 'sidebar-nav',
@@ -27,7 +29,7 @@ const defaultPreferences: Preferences = {
styleType: 'normal',
},
copyright: {
companyName: 'Vben Admin',
companyName: 'Vben',
companySiteLink: 'https://www.vben.pro',
date: '2024',
enable: true,

View File

@@ -21,6 +21,8 @@ interface AppPreferences {
accessMode: AccessModeType;
/** 登录注册页面布局 */
authPageLayout: AuthPageLayoutType;
/** 检查更新轮询时间 */
checkUpdatesPollingTime: number;
/** 是否开启灰色模式 */
colorGrayMode: boolean;
/** 是否开启色弱模式 */
@@ -33,6 +35,8 @@ interface AppPreferences {
defaultAvatar: string;
// /** 开启动态标题 */
dynamicTitle: boolean;
/** 是否开启检查更新 */
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/** 是否移动端 */

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
import { Toaster } from '@vben-core/shadcn-ui';
defineOptions({ name: 'GlobalProvider' });
</script>
<template>
<Toaster />
<slot></slot>
</template>

View File

@@ -1 +0,0 @@
export { default as GlobalProvider } from './global-provider.vue';

View File

@@ -2,5 +2,4 @@ export * from './about';
export * from './authentication';
export * from './dashboard';
export * from './fallback';
export * from './global-provider';
export { useToast } from '@vben-core/shadcn-ui';

View File

@@ -1,23 +1,25 @@
import { computed, ref, watch } from 'vue';
import { reactive, watch } from 'vue';
import { preferences } from '@vben/preferences';
export function useDesignTokens() {
const rootStyles = getComputedStyle(document.documentElement);
const colorPrimary = ref('');
const colorError = ref('');
const colorSuccess = ref('');
const colorWarning = ref('');
const colorInfo = ref('');
const colorBgBase = ref('');
const colorTextBase = ref('');
const colorBgContainer = ref('');
const colorBgElevated = ref('');
const colorBgLayout = ref('');
const colorBgMask = ref('');
const colorBorder = ref('');
const borderRadius = ref<any>('');
const antDesignTokens = reactive({
borderRadius: '' as any,
colorBgBase: '',
colorBgContainer: '',
colorBgElevated: '',
colorBgLayout: '',
colorBgMask: '',
colorBorder: '',
colorError: '',
colorInfo: '',
colorPrimary: '',
colorSuccess: '',
colorTextBase: '',
colorWarning: '',
});
const getCssVariableValue = (variable: string, isColor: boolean = true) => {
const value = rootStyles.getPropertyValue(variable);
@@ -27,52 +29,23 @@ export function useDesignTokens() {
watch(
() => preferences.theme,
() => {
colorInfo.value = colorPrimary.value = getCssVariableValue('--primary');
colorError.value = getCssVariableValue('--destructive');
colorWarning.value = getCssVariableValue('--warning');
colorSuccess.value = getCssVariableValue('--success');
colorBgBase.value = getCssVariableValue('--background');
colorBgLayout.value = getCssVariableValue('--background-deep');
colorBgMask.value = getCssVariableValue('--overlay');
colorBorder.value = getCssVariableValue('--border');
colorTextBase.value = getCssVariableValue('--foreground');
colorBgElevated.value = getCssVariableValue('--popover');
colorBgContainer.value = getCssVariableValue('--card');
borderRadius.value = getCssVariableValue('--radius', false);
antDesignTokens.colorPrimary = getCssVariableValue('--primary');
antDesignTokens.colorError = getCssVariableValue('--destructive');
antDesignTokens.colorWarning = getCssVariableValue('--warning');
antDesignTokens.colorSuccess = getCssVariableValue('--success');
antDesignTokens.colorBgBase = getCssVariableValue('--background');
antDesignTokens.colorBgLayout = getCssVariableValue('--background-deep');
antDesignTokens.colorBgMask = getCssVariableValue('--overlay');
antDesignTokens.colorBorder = getCssVariableValue('--border');
antDesignTokens.colorTextBase = getCssVariableValue('--foreground');
antDesignTokens.colorBgElevated = getCssVariableValue('--popover');
antDesignTokens.colorBgContainer = getCssVariableValue('--card');
antDesignTokens.borderRadius = getCssVariableValue('--radius', false);
},
{ immediate: true },
);
const antDesignTokens = computed(() => {
return {
borderRadius: borderRadius.value,
colorBgBase: colorBgBase.value,
colorBgContainer: colorBgContainer.value,
colorBgElevated: colorBgElevated.value,
colorBgLayout: colorBgLayout.value,
colorBgMask: colorBgMask.value,
colorBorder: colorBorder.value,
colorError: colorError.value,
colorInfo: colorInfo.value,
colorPrimary: colorPrimary.value,
colorSuccess: colorSuccess.value,
colorTextBase: colorTextBase.value,
colorWarning: colorWarning.value,
};
});
return {
antDesignTokens,
borderRadius,
colorBgBase,
colorBgContainer,
colorBgElevated,
colorBorder,
colorError,
colorInfo,
colorPrimary,
colorSuccess,
colorTextBase,
colorWarning,
};
}

View File

@@ -12,9 +12,9 @@ import { useCoreAccessStore, useCoreLockStore } from '@vben/stores';
import { MenuRecordRaw } from '@vben/types';
import { mapTree } from '@vben/utils';
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { Toaster, VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { Breadcrumb, Preferences } from '../widgets';
import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
import { LayoutContent } from './content';
import { Copyright } from './copyright';
import { LayoutFooter } from './footer';
@@ -310,6 +310,12 @@ watch(
<template #extra>
<slot name="extra"></slot>
<Toaster />
<CheckUpdates
v-if="preferences.app.enableCheckUpdates"
:polling-time="preferences.app.checkUpdatesPollingTime"
/>
<Transition v-if="preferences.widget.lockScreen" name="slide-up">
<slot v-if="coreLockStore.isLockScreen" name="lock-screen"></slot>
</Transition>

View File

@@ -0,0 +1,126 @@
<script setup lang="ts">
import { h, onMounted, onUnmounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { ToastAction, useToast } from '@vben-core/shadcn-ui';
interface Props {
// 轮训时间,分钟
pollingTime?: number;
}
defineOptions({ name: 'CheckUpdates' });
const props = withDefaults(defineProps<Props>(), {
pollingTime: 1,
});
const lastVersionTag = ref('');
let isCheckingUpdates = false;
const timer = ref<ReturnType<typeof setInterval>>();
const { toast } = useToast();
async function getVersionTag() {
try {
const response = await fetch('/', {
cache: 'no-cache',
method: 'HEAD',
});
return (
response.headers.get('etag') || response.headers.get('last-modified')
);
} catch {
console.error('Failed to fetch version tag');
return null;
}
}
async function checkForUpdates() {
const versionTag = await getVersionTag();
if (!versionTag) {
return;
}
// 首次运行时不提示更新
if (!lastVersionTag.value) {
lastVersionTag.value = versionTag;
return;
}
if (lastVersionTag.value !== versionTag) {
lastVersionTag.value = versionTag;
clearInterval(timer.value);
handleNotice();
}
}
function handleNotice() {
const { dismiss } = toast({
action: h('div', [
h(
ToastAction,
{
altText: $t('common.cancel'),
onClick: () => dismiss(),
},
{
default: () => $t('common.cancel'),
},
),
h(
ToastAction,
{
altText: $t('common.refresh'),
class: 'bg-primary hover:bg-primary-hover mx-1',
onClick: () => {
window.location.reload();
},
},
{
default: () => $t('common.refresh'),
},
),
]),
description: $t('widgets.checkUpdatesDescription'),
duration: 0,
title: $t('widgets.checkUpdatesTitle'),
});
}
function start() {
// 每5分钟检查一次
timer.value = setInterval(checkForUpdates, props.pollingTime * 60 * 1000);
}
function handleVisibilitychange() {
if (document.hidden) {
stop();
} else {
if (!isCheckingUpdates) {
isCheckingUpdates = true;
checkForUpdates().finally(() => {
isCheckingUpdates = false;
start();
});
}
}
}
function stop() {
clearInterval(timer.value);
}
onMounted(() => {
start();
document.addEventListener('visibilitychange', handleVisibilitychange);
});
onUnmounted(() => {
stop();
document.removeEventListener('visibilitychange', handleVisibilitychange);
});
</script>
<template>
<slot></slot>
</template>

View File

@@ -0,0 +1 @@
export { default as CheckUpdates } from './check-updates.vue';

View File

@@ -1,4 +1,5 @@
export { default as Breadcrumb } from './breadcrumb.vue';
export * from './check-updates';
export { default as AuthenticationColorToggle } from './color-toggle.vue';
export * from './global-search';
export { default as LanguageToggle } from './language-toggle.vue';

View File

@@ -12,6 +12,7 @@ defineOptions({
const appLocale = defineModel<string>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appWatermark = defineModel<boolean>('appWatermark');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
</script>
<template>
@@ -24,4 +25,7 @@ const appWatermark = defineModel<boolean>('appWatermark');
<SwitchItem v-model="appWatermark">
{{ $t('preferences.watermark') }}
</SwitchItem>
<SwitchItem v-model="appEnableCheckUpdates">
{{ $t('preferences.checkUpdates') }}
</SwitchItem>
</template>

View File

@@ -62,6 +62,7 @@ const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
const appWatermark = defineModel<boolean>('appWatermark');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
const transitionProgress = defineModel<boolean>('transitionProgress');
const transitionName = defineModel<string>('transitionName');
@@ -254,6 +255,7 @@ async function handleReset() {
<Block :title="$t('preferences.general')">
<General
v-model:app-dynamic-title="appDynamicTitle"
v-model:app-enable-check-updates="appEnableCheckUpdates"
v-model:app-locale="appLocale"
v-model:app-watermark="appWatermark"
/>

View File

@@ -60,6 +60,8 @@
"notifications": "Notifications",
"markAllAsRead": "Make All as Read",
"clearNotifications": "Clear",
"checkUpdatesTitle": "New Version Available",
"checkUpdatesDescription": "Click to refresh and get the latest version",
"search": {
"title": "Search",
"searchNavigate": "Search Navigation",
@@ -166,6 +168,7 @@
"language": "Language",
"dynamicTitle": "Dynamic Title",
"watermark": "Watermark",
"checkUpdates": "Periodic update check",
"sidebar": {
"title": "Sidebar",
"width": "Width",

View File

@@ -60,6 +60,8 @@
"notifications": "通知",
"markAllAsRead": "全部标记为已读",
"clearNotifications": "清空",
"checkUpdatesTitle": "新版本可用",
"checkUpdatesDescription": "点击刷新以获取最新版本",
"search": {
"title": "搜索",
"searchNavigate": "搜索导航菜单",
@@ -166,6 +168,7 @@
"language": "语言",
"dynamicTitle": "动态标题",
"watermark": "水印",
"checkUpdates": "定时检查更新",
"sidebar": {
"title": "侧边栏",
"width": "宽度",