fix: restore dashboard renderer quality

This commit is contained in:
2026-06-22 13:20:30 +08:00
parent 01b801bebc
commit 1dc4abf063
5 changed files with 16 additions and 366 deletions
@@ -1,135 +0,0 @@
<script setup lang="ts">
import type { DashboardConfig } from './store/dashboardRuntimeStore';
import { computed, onMounted, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElEmpty } from 'element-plus';
import LightWidgetRenderer from './components/LightWidgetRenderer.vue';
const props = defineProps<{
config: DashboardConfig | string;
}>();
const dashboardConfig = ref<DashboardConfig | null>(null);
function parseConfig() {
if (!props.config) {
dashboardConfig.value = null;
return;
}
if (typeof props.config === 'string') {
try {
dashboardConfig.value = JSON.parse(props.config);
} catch {
console.error('Invalid light dashboard config JSON');
dashboardConfig.value = null;
}
} else {
dashboardConfig.value = props.config;
}
}
onMounted(parseConfig);
watch(() => props.config, parseConfig);
const layout = computed(() => {
if (!dashboardConfig.value) return [];
return dashboardConfig.value.widgets.map((w) => ({
i: w.i,
x: w.x,
y: w.y,
w: w.w,
h: w.h,
}));
});
const gridStyle = computed(() => {
if (!dashboardConfig.value) return {};
const [gapX, gapY] = dashboardConfig.value.margin;
const style: Record<string, string> = {
display: 'grid',
gap: `${gapY}px ${gapX}px`,
gridAutoRows: `${dashboardConfig.value.rowHeight}px`,
gridTemplateColumns: `repeat(${dashboardConfig.value.columns}, minmax(0, 1fr))`,
};
if (!dashboardConfig.value.showOuterMargin) {
style.marginLeft = `-${gapX}px`;
style.marginRight = `-${gapX}px`;
style.marginTop = `-${gapY}px`;
style.width = `calc(100% + ${gapX * 2}px)`;
}
return style;
});
function getWidget(i: string) {
if (!dashboardConfig.value) return null;
return dashboardConfig.value.widgets.find((w) => w.i === i);
}
function getAnimationDelay(i: string) {
if (!dashboardConfig.value) return 0;
const index = dashboardConfig.value.widgets.findIndex((w) => w.i === i);
return index * 60;
}
function getItemStyle(item: { h: number; w: number; x: number; y: number }) {
return {
gridColumn: `${item.x + 1} / span ${item.w}`,
gridRow: `${item.y + 1} / span ${item.h}`,
};
}
</script>
<template>
<div
class="dashboard-renderer h-full"
:style="
dashboardConfig?.backgroundColor?.includes('gradient')
? { background: dashboardConfig.backgroundColor }
: { backgroundColor: dashboardConfig?.backgroundColor || '' }
"
>
<div v-if="dashboardConfig && layout.length > 0">
<div :style="gridStyle">
<div
v-for="item in layout"
:key="item.i"
class="dashboard-widget"
:style="getItemStyle(item)"
>
<LightWidgetRenderer
v-if="getWidget(item.i)"
:widget="getWidget(item.i)!"
:is-design-mode="false"
:animation-delay="getAnimationDelay(item.i)"
/>
</div>
</div>
</div>
<ElEmpty
v-else
:description="$t('dashboard-design.noConfigTip')"
class="py-20"
/>
</div>
</template>
<style scoped>
.dashboard-renderer {
background-color: var(--el-bg-color-page);
}
.dashboard-widget {
overflow: hidden;
background: var(--el-bg-color);
border-radius: 8px;
box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
}
</style>
@@ -1,147 +0,0 @@
<script setup lang="ts">
import type {
DashboardWidget,
WidgetMaterial,
} from '../store/dashboardRuntimeStore';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { defaultWidgetStyle } from '../store/dashboardRuntimeStore';
const props = defineProps<{
animationDelay?: number;
isDesignMode?: boolean;
material?: WidgetMaterial;
widget: DashboardWidget;
}>();
const isEntered = ref(false);
const isUpdating = ref(false);
const widgetComponents: Record<
string,
ReturnType<typeof defineAsyncComponent>
> = {
'announcement-list': defineAsyncComponent(
() => import('./widgets/AnnouncementList.vue'),
),
'approval-center': defineAsyncComponent(
() => import('./widgets/ApprovalCenter.vue'),
),
'my-apps': defineAsyncComponent(() => import('./widgets/MyApps.vue')),
'notice-list': defineAsyncComponent(() => import('./widgets/NoticeList.vue')),
'quick-links': defineAsyncComponent(() => import('./widgets/QuickLinks.vue')),
'server-monitor': defineAsyncComponent(
() => import('./widgets/ServerMonitor.vue'),
),
weather: defineAsyncComponent(() => import('./widgets/WeatherWidget.vue')),
'welcome-card': defineAsyncComponent(
() => import('./widgets/WelcomeCard.vue'),
),
};
const currentComponent = computed(() => widgetComponents[props.widget.type]);
const mergedWidget = computed(() => props.widget);
const widgetStyle = computed(() => {
const style = { ...defaultWidgetStyle, ...props.widget.style };
const css: Record<string, string> = {};
const bgValue = style.backgroundColor || 'var(--el-bg-color)';
if (bgValue.includes('gradient')) {
css.background = bgValue;
} else {
css.backgroundColor = bgValue;
}
if (style.borderWidth && style.borderWidth > 0) {
css.borderWidth = `${style.borderWidth}px`;
css.borderStyle = style.borderStyle || 'solid';
css.borderColor = style.borderColor || 'var(--el-border-color)';
}
if (style.borderRadius !== undefined) {
css.borderRadius = `${style.borderRadius}px`;
}
if (style.shadowEnabled) {
const color = style.shadowColor || 'rgba(0, 0, 0, 0.1)';
const blur = style.shadowBlur || 4;
const x = style.shadowOffsetX || 0;
const y = style.shadowOffsetY || 1;
css.boxShadow = `${x}px ${y}px ${blur}px ${color}`;
} else {
css.boxShadow = 'none';
}
return css;
});
onMounted(() => {
setTimeout(() => {
isEntered.value = true;
}, props.animationDelay || 0);
});
const animationClass = computed(() => ({
'widget-enter': true,
'widget-entered': isEntered.value,
'widget-updating': isUpdating.value,
}));
</script>
<template>
<div
class="widget-renderer relative h-full w-full overflow-hidden"
:class="animationClass"
:style="widgetStyle"
>
<component
:is="currentComponent"
v-if="currentComponent"
:widget="mergedWidget"
:is-design-mode="isDesignMode"
/>
<div
v-else
class="flex h-full w-full items-center justify-center text-gray-400"
>
{{ $t('dashboard-design.unknownWidget') }}: {{ widget.type }}
</div>
</div>
</template>
<style scoped>
@keyframes pulse-update {
0% {
transform: scale(1);
}
50% {
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
transform: scale(1.02);
}
100% {
transform: scale(1);
}
}
.widget-enter {
opacity: 0;
transform: translateY(20px) scale(0.95);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.widget-entered {
opacity: 1;
transform: translateY(0) scale(1);
}
.widget-updating {
animation: pulse-update 0.3s ease-in-out;
}
</style>
+5 -2
View File
@@ -9,6 +9,7 @@ import { preferences } from '@vben/preferences';
import { getAllMenusApi } from '#/api/core/menu'; import { getAllMenusApi } from '#/api/core/menu';
import { BasicLayout, IFrameView } from '#/layouts'; import { BasicLayout, IFrameView } from '#/layouts';
import { ensureAdminRuntime } from '#/runtime/admin';
import { useAppContextStore } from '#/store/app-context'; import { useAppContextStore } from '#/store/app-context';
import { createAdminRuntimePage } from './admin-runtime-page'; import { createAdminRuntimePage } from './admin-runtime-page';
@@ -93,6 +94,7 @@ function withAdminRuntimePages(pageMap: ComponentRecordType) {
const PREFETCH_LIGHT_PAGE_KEYS = [ const PREFETCH_LIGHT_PAGE_KEYS = [
'../views/_core/menu/index.vue', '../views/_core/menu/index.vue',
'../views/_core/permission/index.vue',
'../views/_core/role/index.vue', '../views/_core/role/index.vue',
'../views/_core/user/index.vue', '../views/_core/user/index.vue',
]; ];
@@ -114,6 +116,7 @@ function prefetchLightPages(pageMap: ComponentRecordType) {
lightPagesPrefetched = true; lightPagesPrefetched = true;
window.setTimeout(() => { window.setTimeout(() => {
scheduleIdleTask(() => { scheduleIdleTask(() => {
void ensureAdminRuntime().catch(() => undefined);
for (const key of PREFETCH_LIGHT_PAGE_KEYS) { for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
const loader = pageMap[key]; const loader = pageMap[key];
if (typeof loader === 'function') { if (typeof loader === 'function') {
@@ -121,7 +124,7 @@ function prefetchLightPages(pageMap: ComponentRecordType) {
} }
} }
}); });
}, 12_000); }, 3_000);
} }
const LIGHT_AI_PLATFORM_MENU_ROUTES = [ const LIGHT_AI_PLATFORM_MENU_ROUTES = [
@@ -289,7 +292,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
layoutMap, layoutMap,
pageMap, pageMap,
}); });
prefetchLightPages(pageMap); prefetchLightPages(rawPageMap);
return accessible; return accessible;
} }
@@ -77,10 +77,10 @@ function createAdminRuntimePage(loader: RouteComponentLoader) {
async function loadPage() { async function loadPage() {
try { try {
if (!isAdminRuntimeReady()) { const runtimeReady = isAdminRuntimeReady()
await ensureAdminRuntime(); ? Promise.resolve()
} : ensureAdminRuntime();
const module = await loader(); const [module] = await Promise.all([loader(), runtimeReady]);
loadedComponent.value = module.default; loadedComponent.value = module.default;
} catch (error) { } catch (error) {
errorMessage.value = errorMessage.value =
@@ -1,69 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus'; import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus';
import { getPageByCodeApi } from '#/api/core/page-manager'; import { getPageByCodeApi } from '#/api/core/page-manager';
import DashboardRenderer from '#/components/dashboard-design/DashboardRenderer.vue';
defineOptions({ name: 'PageRender' }); defineOptions({ name: 'PageRender' });
const lightHomeWidgetTypes = new Set([
'announcement-list',
'approval-center',
'my-apps',
'notice-list',
'quick-links',
'server-monitor',
'weather',
'welcome-card',
]);
const LightDashboardRenderer = defineAsyncComponent({
loader: () =>
import('#/components/dashboard-design/LightDashboardRenderer.vue'),
delay: 120,
loadingComponent: {
render: () =>
h(
'div',
{
class:
'flex h-full min-h-[240px] items-center justify-center text-sm text-gray-500',
},
'正在加载控制中心...',
),
},
errorComponent: {
render: () => h(ElEmpty, { description: '控制中心加载失败' }),
},
});
const DashboardRenderer = defineAsyncComponent({
loader: () => import('#/components/dashboard-design/DashboardRenderer.vue'),
delay: 120,
loadingComponent: {
render: () =>
h(
'div',
{
class:
'flex h-full min-h-[240px] items-center justify-center text-sm text-gray-500',
},
'正在加载页面组件...',
),
},
errorComponent: {
render: () => h(ElEmpty, { description: '页面组件加载失败' }),
},
});
const route = useRoute(); const route = useRoute();
const loading = ref(false); const loading = ref(false);
const pageConfig = ref(''); const pageConfig = ref('');
const pageName = ref(''); const pageName = ref('');
const pageCode = ref(''); const pageCode = ref('');
const useLightRenderer = ref(false);
function getPageCode(): string { function getPageCode(): string {
if (route.query.pageCode) { if (route.query.pageCode) {
@@ -83,19 +33,6 @@ function getPageCode(): string {
return ''; return '';
} }
function canUseLightRenderer(code: string, config: any): boolean {
if (code !== 'main_home') return false;
const widgets = config?.widgets;
return (
Array.isArray(widgets) &&
widgets.every(
(widget) =>
lightHomeWidgetTypes.has(widget?.type) &&
(!widget?.dataSource || widget.dataSource.type === 'static'),
)
);
}
async function loadPageData() { async function loadPageData() {
const code = getPageCode(); const code = getPageCode();
if (!code) { if (!code) {
@@ -108,16 +45,12 @@ async function loadPageData() {
try { try {
const page = await getPageByCodeApi(code); const page = await getPageByCodeApi(code);
const rawConfig =
page.page_config && Object.keys(page.page_config).length > 0
? page.page_config
: null;
pageName.value = page.name; pageName.value = page.name;
useLightRenderer.value = canUseLightRenderer(code, rawConfig); pageConfig.value =
pageConfig.value = rawConfig ? JSON.stringify(rawConfig) : ''; page.page_config && Object.keys(page.page_config).length > 0
? JSON.stringify(page.page_config)
: '';
} catch (error: any) { } catch (error: any) {
useLightRenderer.value = false;
ElMessage.error(error?.message || '加载页面失败'); ElMessage.error(error?.message || '加载页面失败');
} finally { } finally {
loading.value = false; loading.value = false;
@@ -139,11 +72,7 @@ watch(
<template> <template>
<ElScrollbar class="rounded-[8px] py-3"> <ElScrollbar class="rounded-[8px] py-3">
<div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3"> <div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3">
<LightDashboardRenderer <DashboardRenderer v-if="pageConfig" :config="pageConfig" />
v-if="pageConfig && useLightRenderer"
:config="pageConfig"
/>
<DashboardRenderer v-else-if="pageConfig" :config="pageConfig" />
<ElEmpty v-else-if="!loading" description="暂无页面配置" /> <ElEmpty v-else-if="!loading" description="暂无页面配置" />
</div> </div>
</ElScrollbar> </ElScrollbar>