perf: split light dashboard runtime
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardConfig } from './store/dashboardDesignStore';
|
||||
import type { DashboardConfig } from './store/dashboardRuntimeStore';
|
||||
|
||||
/**
|
||||
* 仪表盘渲染器
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<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>
|
||||
@@ -0,0 +1,147 @@
|
||||
<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>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {
|
||||
DashboardWidget,
|
||||
WidgetMaterial,
|
||||
} from '../store/dashboardDesignStore';
|
||||
} from '../store/dashboardRuntimeStore';
|
||||
|
||||
import {
|
||||
computed,
|
||||
@@ -18,8 +18,8 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
defaultWidgetStyle,
|
||||
useDashboardDesignStore,
|
||||
} from '../store/dashboardDesignStore';
|
||||
useDashboardRuntimeStore,
|
||||
} from '../store/dashboardRuntimeStore';
|
||||
import { createRefreshTimer, fetchWidgetData } from '../utils/dataFetcher';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -106,15 +106,13 @@ const widgetComponents: Record<
|
||||
'filter-select': defineAsyncComponent(
|
||||
() => import('./widgets/FilterSelect.vue'),
|
||||
),
|
||||
'filter-date': defineAsyncComponent(
|
||||
() => import('./widgets/FilterDate.vue'),
|
||||
),
|
||||
'filter-date': defineAsyncComponent(() => import('./widgets/FilterDate.vue')),
|
||||
'filter-date-range': defineAsyncComponent(
|
||||
() => import('./widgets/FilterDateRange.vue'),
|
||||
),
|
||||
};
|
||||
|
||||
const store = useDashboardDesignStore();
|
||||
const store = useDashboardRuntimeStore();
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
return widgetComponents[props.widget.type];
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import type { UserAnnouncement } from '#/api/core/announcement';
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
@@ -7,14 +7,14 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
|
||||
import { useDashboardDesignStore } from '../../store/dashboardDesignStore';
|
||||
import { useDashboardRuntimeStore } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const store = useDashboardDesignStore();
|
||||
const store = useDashboardRuntimeStore();
|
||||
const dateValue = ref<string>(props.widget.props.defaultValue || '');
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
|
||||
+6
-12
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
@@ -7,14 +7,14 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
|
||||
import { useDashboardDesignStore } from '../../store/dashboardDesignStore';
|
||||
import { useDashboardRuntimeStore } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const store = useDashboardDesignStore();
|
||||
const store = useDashboardRuntimeStore();
|
||||
const dateRange = ref<[string, string] | []>(
|
||||
props.widget.props.defaultValue || [],
|
||||
);
|
||||
@@ -30,12 +30,8 @@ const endPlaceholder = computed(
|
||||
props.widget.props.endPlaceholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterEndDate'),
|
||||
);
|
||||
const startParamKey = computed(
|
||||
() => props.widget.props.startParamKey || '',
|
||||
);
|
||||
const endParamKey = computed(
|
||||
() => props.widget.props.endParamKey || '',
|
||||
);
|
||||
const startParamKey = computed(() => props.widget.props.startParamKey || '');
|
||||
const endParamKey = computed(() => props.widget.props.endParamKey || '');
|
||||
const dateFormat = computed(
|
||||
() => props.widget.props.dateFormat || 'YYYY-MM-DD',
|
||||
);
|
||||
@@ -108,9 +104,7 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const hasParamKey = computed(
|
||||
() => startParamKey.value || endParamKey.value,
|
||||
);
|
||||
const hasParamKey = computed(() => startParamKey.value || endParamKey.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
@@ -8,14 +8,14 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import { ElInput } from 'element-plus';
|
||||
|
||||
import { useDashboardDesignStore } from '../../store/dashboardDesignStore';
|
||||
import { useDashboardRuntimeStore } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const store = useDashboardDesignStore();
|
||||
const store = useDashboardRuntimeStore();
|
||||
const inputValue = ref(props.widget.props.defaultValue || '');
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
@@ -9,14 +9,14 @@ import { ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
import { useDashboardDesignStore } from '../../store/dashboardDesignStore';
|
||||
import { useDashboardRuntimeStore } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const store = useDashboardDesignStore();
|
||||
const store = useDashboardRuntimeStore();
|
||||
const selectValue = ref<any>(props.widget.props.defaultValue || '');
|
||||
const dynamicOptions = ref<{ label: string; value: any }[]>([]);
|
||||
const loadingOptions = ref(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
import type { ApplicationListItem } from '#/api/core/application';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
@@ -107,20 +107,23 @@ onMounted(() => {
|
||||
</div>
|
||||
<!-- 应用网格 -->
|
||||
<ElScrollbar class="flex-1">
|
||||
<div
|
||||
v-if="displayApps.length > 0"
|
||||
class="flex flex-wrap gap-4"
|
||||
>
|
||||
<div v-if="displayApps.length > 0" class="flex flex-wrap gap-4">
|
||||
<div
|
||||
v-for="app in displayApps"
|
||||
:key="app.id"
|
||||
class="flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105 m-4"
|
||||
class="m-4 flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105"
|
||||
style="width: 72px"
|
||||
@click="handleClick(app)"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl"
|
||||
style="background: linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary))"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-3),
|
||||
var(--el-color-primary)
|
||||
);
|
||||
"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="app.icon"
|
||||
@@ -129,9 +132,10 @@ onMounted(() => {
|
||||
/>
|
||||
<AppWindow v-else class="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<span class="text-muted-foreground w-full truncate text-center text-xs">{{
|
||||
getAppName(app)
|
||||
}}</span>
|
||||
<span
|
||||
class="text-muted-foreground w-full truncate text-center text-xs"
|
||||
>{{ getAppName(app) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import type { Message } from '#/api/core/message';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import type {
|
||||
RealtimeStats,
|
||||
ServerMonitorResponse,
|
||||
} from '#/api/core/server-monitor';
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../store/dashboardDesignStore';
|
||||
import type { DashboardWidget } from '../../store/dashboardRuntimeStore';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export type DataSourceType = 'api' | 'dataSource' | 'static' | 'upload';
|
||||
|
||||
export interface FieldMapping {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface ParamBinding {
|
||||
globalKey: string;
|
||||
paramName: string;
|
||||
}
|
||||
|
||||
export interface DataSourceConfig {
|
||||
apiBody?: Record<string, any>;
|
||||
apiHeaders?: Record<string, string>;
|
||||
apiMethod?: 'GET' | 'POST';
|
||||
apiParams?: Record<string, any>;
|
||||
apiUrl?: string;
|
||||
dataPath?: string;
|
||||
dataSourceCode?: string;
|
||||
fieldMappings?: FieldMapping[];
|
||||
paramBindings?: ParamBinding[];
|
||||
refreshEnabled?: boolean;
|
||||
refreshInterval?: number;
|
||||
type: DataSourceType;
|
||||
}
|
||||
|
||||
export interface WidgetStyle {
|
||||
backgroundColor?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundSize?: 'auto' | 'contain' | 'cover';
|
||||
borderColor?: string;
|
||||
borderRadius?: number;
|
||||
borderStyle?: 'dashed' | 'dotted' | 'none' | 'solid';
|
||||
borderWidth?: number;
|
||||
padding?: number;
|
||||
shadowBlur?: number;
|
||||
shadowColor?: string;
|
||||
shadowEnabled?: boolean;
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
titleAlign?: 'center' | 'left' | 'right';
|
||||
titleColor?: string;
|
||||
titleFontSize?: number;
|
||||
titleFontWeight?: 'bold' | 'normal';
|
||||
titleShow?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardWidget {
|
||||
dataSource?: DataSourceConfig;
|
||||
h: number;
|
||||
i: string;
|
||||
id: string;
|
||||
maxH?: number;
|
||||
maxW?: number;
|
||||
minH?: number;
|
||||
minW?: number;
|
||||
props: Record<string, any>;
|
||||
style?: WidgetStyle;
|
||||
title?: string;
|
||||
type: string;
|
||||
w: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface DashboardConfig {
|
||||
backgroundColor?: string;
|
||||
columns: number;
|
||||
id: string;
|
||||
margin: [number, number];
|
||||
name: string;
|
||||
rowHeight: number;
|
||||
showOuterMargin?: boolean;
|
||||
widgets: DashboardWidget[];
|
||||
}
|
||||
|
||||
export interface WidgetMaterial {
|
||||
category: 'chart' | 'filter' | 'list' | 'widget';
|
||||
defaultH: number;
|
||||
defaultProps: Record<string, any>;
|
||||
defaultW: number;
|
||||
icon: string;
|
||||
minH?: number;
|
||||
minW?: number;
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const defaultWidgetStyle: WidgetStyle = {
|
||||
backgroundColor: '',
|
||||
borderWidth: 0,
|
||||
borderColor: '',
|
||||
borderStyle: 'solid',
|
||||
borderRadius: 8,
|
||||
shadowEnabled: true,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 4,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 1,
|
||||
padding: 16,
|
||||
titleShow: true,
|
||||
titleFontSize: 14,
|
||||
titleColor: '',
|
||||
titleAlign: 'left',
|
||||
titleFontWeight: 'normal',
|
||||
};
|
||||
|
||||
export const useDashboardRuntimeStore = defineStore('dashboard-runtime', () => {
|
||||
const globalParams = ref<Record<string, any>>({});
|
||||
const globalParamsVersion = ref(0);
|
||||
|
||||
const updateGlobalParam = (key: string, value: any) => {
|
||||
globalParams.value = { ...globalParams.value, [key]: value };
|
||||
globalParamsVersion.value++;
|
||||
};
|
||||
|
||||
const removeGlobalParam = (key: string) => {
|
||||
const next = { ...globalParams.value };
|
||||
delete next[key];
|
||||
globalParams.value = next;
|
||||
globalParamsVersion.value++;
|
||||
};
|
||||
|
||||
const clearGlobalParams = () => {
|
||||
globalParams.value = {};
|
||||
globalParamsVersion.value++;
|
||||
};
|
||||
|
||||
return {
|
||||
clearGlobalParams,
|
||||
globalParams,
|
||||
globalParamsVersion,
|
||||
removeGlobalParam,
|
||||
updateGlobalParam,
|
||||
};
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
DataSourceConfig,
|
||||
FieldMapping,
|
||||
} from '../store/dashboardDesignStore';
|
||||
} from '../store/dashboardRuntimeStore';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
@@ -87,7 +87,9 @@ export async function fetchWidgetData(
|
||||
try {
|
||||
const response = await requestClient.get(
|
||||
`/api/core/data-source/execute/${dataSource.dataSourceCode}`,
|
||||
{ params: params && Object.keys(params).length > 0 ? params : undefined },
|
||||
{
|
||||
params: params && Object.keys(params).length > 0 ? params : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
// 后端返回格式是 {data: ...},需要提取 data 字段
|
||||
|
||||
@@ -1,19 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { defineAsyncComponent, h, 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) {
|
||||
@@ -33,6 +83,19 @@ 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) {
|
||||
@@ -45,12 +108,16 @@ async function loadPageData() {
|
||||
|
||||
try {
|
||||
const page = await getPageByCodeApi(code);
|
||||
pageName.value = page.name;
|
||||
pageConfig.value =
|
||||
const rawConfig =
|
||||
page.page_config && Object.keys(page.page_config).length > 0
|
||||
? JSON.stringify(page.page_config)
|
||||
: '';
|
||||
? page.page_config
|
||||
: null;
|
||||
|
||||
pageName.value = page.name;
|
||||
useLightRenderer.value = canUseLightRenderer(code, rawConfig);
|
||||
pageConfig.value = rawConfig ? JSON.stringify(rawConfig) : '';
|
||||
} catch (error: any) {
|
||||
useLightRenderer.value = false;
|
||||
ElMessage.error(error?.message || '加载页面失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -72,7 +139,11 @@ watch(
|
||||
<template>
|
||||
<ElScrollbar class="rounded-[8px] py-3">
|
||||
<div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3">
|
||||
<DashboardRenderer v-if="pageConfig" :config="pageConfig" />
|
||||
<LightDashboardRenderer
|
||||
v-if="pageConfig && useLightRenderer"
|
||||
:config="pageConfig"
|
||||
/>
|
||||
<DashboardRenderer v-else-if="pageConfig" :config="pageConfig" />
|
||||
<ElEmpty v-else-if="!loading" description="暂无页面配置" />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
@@ -11,7 +11,14 @@ import type { ChatMessage } from '#/components/ChatBox/index';
|
||||
* Agent 对话面板组件
|
||||
* 可复用于 Agent 编辑页面和 Agent 对话页面
|
||||
*/
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
@@ -23,10 +30,16 @@ import {
|
||||
sendAgentMessageStream,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ChatBox } from '#/components/ChatBox/index';
|
||||
import { AppDesignPanel, DesignEditorPanel } from '#/components/form-editor';
|
||||
|
||||
import AiWorkingAnimation from '../../../../components/ai-loading/AiWorkingAnimation.vue';
|
||||
|
||||
const AppDesignPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/AppDesignPanel.vue'),
|
||||
);
|
||||
const DesignEditorPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/DesignEditorPanel.vue'),
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
/** Agent 详情(可选,用于显示名称和欢迎配置) */
|
||||
agent?: Agent | null;
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
SystemSummaryData,
|
||||
} from '#/components/form-editor';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { Settings2, X } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -30,15 +30,27 @@ import {
|
||||
runWorkflowStreamApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ChatBox } from '#/components/ChatBox/index';
|
||||
import {
|
||||
AppDesignPanel,
|
||||
AppSettingsPanel,
|
||||
DashboardBasicInfoConfirmPanel,
|
||||
DashboardDesignConfirmPanel,
|
||||
DashboardPublishConfirmPanel,
|
||||
DesignEditorPanel,
|
||||
SystemSummaryConfirmPanel,
|
||||
} from '#/components/form-editor';
|
||||
const AppDesignPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/AppDesignPanel.vue'),
|
||||
);
|
||||
const AppSettingsPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/AppSettingsPanel.vue'),
|
||||
);
|
||||
const DashboardBasicInfoConfirmPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/DashboardBasicInfoConfirmPanel.vue'),
|
||||
);
|
||||
const DashboardDesignConfirmPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/DashboardDesignConfirmPanel.vue'),
|
||||
);
|
||||
const DashboardPublishConfirmPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/DashboardPublishConfirmPanel.vue'),
|
||||
);
|
||||
const DesignEditorPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/DesignEditorPanel.vue'),
|
||||
);
|
||||
const SystemSummaryConfirmPanel = defineAsyncComponent(
|
||||
() => import('#/components/form-editor/SystemSummaryConfirmPanel.vue'),
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: any[];
|
||||
@@ -1250,6 +1262,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 设计编辑面板 -->
|
||||
<DesignEditorPanel
|
||||
v-if="showDesignPanel"
|
||||
:visible="showDesignPanel"
|
||||
:design="currentDesign"
|
||||
@update:visible="showDesignPanel = $event"
|
||||
@@ -1259,6 +1272,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 应用设计面板 -->
|
||||
<AppDesignPanel
|
||||
v-if="showAppDesignPanel"
|
||||
:visible="showAppDesignPanel"
|
||||
:design="currentAppDesign"
|
||||
@update:visible="showAppDesignPanel = $event"
|
||||
@@ -1268,6 +1282,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 应用设置面板 -->
|
||||
<AppSettingsPanel
|
||||
v-if="showAppSettingsPanel"
|
||||
:visible="showAppSettingsPanel"
|
||||
:settings="currentAppSettings"
|
||||
@update:visible="showAppSettingsPanel = $event"
|
||||
@@ -1277,6 +1292,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 仪表盘基础信息面板 -->
|
||||
<DashboardBasicInfoConfirmPanel
|
||||
v-if="showDashboardBasicInfoPanel"
|
||||
:visible="showDashboardBasicInfoPanel"
|
||||
:basic-info="currentDashboardBasicInfo"
|
||||
@update:visible="showDashboardBasicInfoPanel = $event"
|
||||
@@ -1286,6 +1302,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 仪表盘设计面板 -->
|
||||
<DashboardDesignConfirmPanel
|
||||
v-if="showDashboardDesignPanel"
|
||||
:visible="showDashboardDesignPanel"
|
||||
:design="currentDashboardDesign"
|
||||
@update:visible="showDashboardDesignPanel = $event"
|
||||
@@ -1295,6 +1312,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 仪表盘发布面板 -->
|
||||
<DashboardPublishConfirmPanel
|
||||
v-if="showDashboardPublishPanel"
|
||||
:visible="showDashboardPublishPanel"
|
||||
:publish-data="currentDashboardPublish"
|
||||
@update:visible="showDashboardPublishPanel = $event"
|
||||
@@ -1304,6 +1322,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 系统总结面板 -->
|
||||
<SystemSummaryConfirmPanel
|
||||
v-if="showSystemSummaryPanel"
|
||||
:visible="showSystemSummaryPanel"
|
||||
:data="currentSystemSummary"
|
||||
@update:visible="showSystemSummaryPanel = $event"
|
||||
|
||||
Reference in New Issue
Block a user