fix: restore dashboard renderer quality
This commit is contained in:
@@ -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>
|
||||
@@ -9,6 +9,7 @@ import { preferences } from '@vben/preferences';
|
||||
|
||||
import { getAllMenusApi } from '#/api/core/menu';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { ensureAdminRuntime } from '#/runtime/admin';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
import { createAdminRuntimePage } from './admin-runtime-page';
|
||||
@@ -93,6 +94,7 @@ function withAdminRuntimePages(pageMap: ComponentRecordType) {
|
||||
|
||||
const PREFETCH_LIGHT_PAGE_KEYS = [
|
||||
'../views/_core/menu/index.vue',
|
||||
'../views/_core/permission/index.vue',
|
||||
'../views/_core/role/index.vue',
|
||||
'../views/_core/user/index.vue',
|
||||
];
|
||||
@@ -114,6 +116,7 @@ function prefetchLightPages(pageMap: ComponentRecordType) {
|
||||
lightPagesPrefetched = true;
|
||||
window.setTimeout(() => {
|
||||
scheduleIdleTask(() => {
|
||||
void ensureAdminRuntime().catch(() => undefined);
|
||||
for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
|
||||
const loader = pageMap[key];
|
||||
if (typeof loader === 'function') {
|
||||
@@ -121,7 +124,7 @@ function prefetchLightPages(pageMap: ComponentRecordType) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 12_000);
|
||||
}, 3_000);
|
||||
}
|
||||
|
||||
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
||||
@@ -289,7 +292,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
prefetchLightPages(pageMap);
|
||||
prefetchLightPages(rawPageMap);
|
||||
return accessible;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ function createAdminRuntimePage(loader: RouteComponentLoader) {
|
||||
|
||||
async function loadPage() {
|
||||
try {
|
||||
if (!isAdminRuntimeReady()) {
|
||||
await ensureAdminRuntime();
|
||||
}
|
||||
const module = await loader();
|
||||
const runtimeReady = isAdminRuntimeReady()
|
||||
? Promise.resolve()
|
||||
: ensureAdminRuntime();
|
||||
const [module] = await Promise.all([loader(), runtimeReady]);
|
||||
loadedComponent.value = module.default;
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
|
||||
@@ -1,69 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus';
|
||||
|
||||
import { getPageByCodeApi } from '#/api/core/page-manager';
|
||||
import DashboardRenderer from '#/components/dashboard-design/DashboardRenderer.vue';
|
||||
|
||||
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 loading = ref(false);
|
||||
const pageConfig = ref('');
|
||||
const pageName = ref('');
|
||||
const pageCode = ref('');
|
||||
const useLightRenderer = ref(false);
|
||||
|
||||
function getPageCode(): string {
|
||||
if (route.query.pageCode) {
|
||||
@@ -83,19 +33,6 @@ function getPageCode(): string {
|
||||
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() {
|
||||
const code = getPageCode();
|
||||
if (!code) {
|
||||
@@ -108,16 +45,12 @@ async function loadPageData() {
|
||||
|
||||
try {
|
||||
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;
|
||||
useLightRenderer.value = canUseLightRenderer(code, rawConfig);
|
||||
pageConfig.value = rawConfig ? JSON.stringify(rawConfig) : '';
|
||||
pageConfig.value =
|
||||
page.page_config && Object.keys(page.page_config).length > 0
|
||||
? JSON.stringify(page.page_config)
|
||||
: '';
|
||||
} catch (error: any) {
|
||||
useLightRenderer.value = false;
|
||||
ElMessage.error(error?.message || '加载页面失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -139,11 +72,7 @@ watch(
|
||||
<template>
|
||||
<ElScrollbar class="rounded-[8px] py-3">
|
||||
<div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3">
|
||||
<LightDashboardRenderer
|
||||
v-if="pageConfig && useLightRenderer"
|
||||
:config="pageConfig"
|
||||
/>
|
||||
<DashboardRenderer v-else-if="pageConfig" :config="pageConfig" />
|
||||
<DashboardRenderer v-if="pageConfig" :config="pageConfig" />
|
||||
<ElEmpty v-else-if="!loading" description="暂无页面配置" />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
Reference in New Issue
Block a user