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 { 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user