fix: restore dashboard workspace widgets

This commit is contained in:
2026-06-22 00:27:19 +08:00
parent 328884d637
commit 8407232dc6
4 changed files with 206 additions and 73 deletions
@@ -1,88 +1,136 @@
<script setup lang="ts">
import type { Component } from 'vue';
import type { DashboardWidget } from '#/components/dashboard-design';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import {
ChevronRight,
ClipboardCheck,
ClipboardList,
FilePen,
IconifyIcon,
Play,
Send,
UserCheck,
} from '@vben/icons';
import { $t } from '@vben/locales';
import { useAppContextStore } from '#/store/app-context';
interface ApprovalMenuItem {
color: string;
icon: Component | string;
key: number | string;
path: string;
title: string;
}
const props = defineProps<{
widget: DashboardWidget;
}>();
// 审批中心菜单项
const menuItems = computed(() => [
const router = useRouter();
const appContextStore = useAppContextStore();
const defaultColors = [
'rgba(59, 130, 246, 0.85)',
'rgba(245, 158, 11, 0.85)',
'rgba(139, 92, 246, 0.85)',
'rgba(14, 165, 233, 0.85)',
'rgba(249, 115, 22, 0.85)',
'rgba(20, 184, 166, 0.85)',
];
const defaultMenuItems = computed<ApprovalMenuItem[]>(() => [
{
key: 'initiated',
title: $t('dashboard-design.widgets.approvalCenter.initiated'),
icon: Send,
color: 'rgba(59, 130, 246, 0.85)',
color: defaultColors[0]!,
path: '/workflow/initiated',
},
{
key: 'pending',
title: $t('dashboard-design.widgets.approvalCenter.pending'),
icon: ClipboardList,
color: 'rgba(245, 158, 11, 0.85)',
color: defaultColors[1]!,
path: '/workflow/pending',
},
{
key: 'handling',
title: $t('dashboard-design.widgets.approvalCenter.handling'),
icon: FilePen,
color: 'rgba(139, 92, 246, 0.85)',
color: defaultColors[2]!,
path: '/workflow/pending',
},
{
key: 'handled',
title: $t('dashboard-design.widgets.approvalCenter.handled'),
icon: ClipboardCheck,
color: 'rgba(14, 165, 233, 0.85)',
color: defaultColors[3]!,
path: '/workflow/handled',
},
{
key: 'copy',
title: $t('dashboard-design.widgets.approvalCenter.copy'),
icon: UserCheck,
color: 'rgba(249, 115, 22, 0.85)',
color: defaultColors[4]!,
path: '/workflow/copy',
},
{
key: 'start',
title: $t('dashboard-design.widgets.approvalCenter.start'),
icon: Play,
color: 'rgba(20, 184, 166, 0.85)',
color: defaultColors[5]!,
path: '/workflow/start',
},
]);
// 路由前缀
const displayMenuItems = computed<ApprovalMenuItem[]>(() => {
const customMenus = props.widget.props.menus || [];
if (!customMenus.length) return defaultMenuItems.value;
return customMenus.map((item: any, index: number) => ({
key: item.id || item.key || item.title || index,
title: item.title,
icon: item.icon || 'lucide:circle',
color:
item.color || item.bgColor || defaultColors[index % defaultColors.length],
path: item.path || '',
}));
});
const routePrefix = computed(
() => props.widget.props.routePrefix || '/app/workflow_center',
);
// 点击菜单项
const handleClick = (item: { path: string }) => {
window.open(`${routePrefix.value}${item.path}`, '_blank');
};
function handleClick(item: ApprovalMenuItem) {
if (!item.path) return;
// 点击更多
const handleMore = () => {
if (item.path.startsWith('http://') || item.path.startsWith('https://')) {
window.open(item.path, '_blank');
return;
}
if (props.widget.props.menus?.length) {
const code = appContextStore.appCode;
const targetPath = code ? `/app/${code}${item.path}` : item.path;
router.push(targetPath);
return;
}
window.open(`${routePrefix.value}${item.path}`, '_blank');
}
function handleMore() {
window.open(`${routePrefix.value}/workflow/pending`, '_blank');
};
}
</script>
<template>
<div class="approval-center flex h-full flex-col p-4">
<!-- 头部 -->
<div class="mb-4 flex items-center justify-between">
<span class="text-sm font-medium">{{ widget.props.title }}</span>
<button
@@ -95,10 +143,10 @@ const handleMore = () => {
<ChevronRight class="h-3.5 w-3.5" />
</button>
</div>
<!-- 菜单网格 -->
<div class="flex flex-1 items-center justify-around gap-2">
<div
v-for="item in menuItems"
v-for="item in displayMenuItems"
:key="item.key"
class="flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105"
@click="handleClick(item)"
@@ -107,11 +155,16 @@ const handleMore = () => {
class="flex h-11 w-11 items-center justify-center rounded-full"
:style="{ backgroundColor: item.color }"
>
<component :is="item.icon" class="h-5 w-5 text-white" />
<IconifyIcon
v-if="typeof item.icon === 'string'"
:icon="item.icon"
class="h-5 w-5 text-white"
/>
<component :is="item.icon" v-else class="h-5 w-5 text-white" />
</div>
<span class="text-muted-foreground whitespace-nowrap text-xs">{{
item.title
}}</span>
<span class="text-muted-foreground whitespace-nowrap text-xs">
{{ item.title }}
</span>
</div>
</div>
</div>
@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { DashboardWidget } from '#/components/dashboard-design';
import type { ApplicationListItem } from '#/api/core/application';
import type { DashboardWidget } from '#/components/dashboard-design';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AppWindow, ChevronRight, IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -10,23 +11,42 @@ import { $t } from '@vben/locales';
import { ElEmpty, ElScrollbar } from 'element-plus';
import { getApplicationListApi } from '#/api/core/application';
import { useAppContextStore } from '#/store/app-context';
interface StaticAppItem {
bgColor?: string;
color?: string;
icon?: string;
id?: string;
name?: string;
path?: string;
title?: string;
}
const props = defineProps<{
widget: DashboardWidget;
}>();
// 应用列表
const router = useRouter();
const appContextStore = useAppContextStore();
const appList = ref<ApplicationListItem[]>([]);
const loading = ref(false);
// 最大显示数量
const staticMenus = computed<StaticAppItem[]>(
() => props.widget.props.menus || [],
);
const maxCount = computed(() => props.widget.props.maxCount || 8);
// 显示的应用列表
const displayApps = computed(() => appList.value.slice(0, maxCount.value));
const displayApps = computed(() => {
if (staticMenus.value.length) {
return staticMenus.value.slice(0, maxCount.value);
}
return appList.value.slice(0, maxCount.value);
});
async function loadApps() {
if (staticMenus.value.length) return;
// 加载已发布的应用列表
const loadApps = async () => {
loading.value = true;
try {
const res = await getApplicationListApi({
@@ -40,17 +60,50 @@ const loadApps = async () => {
} finally {
loading.value = false;
}
};
}
// 点击应用
const handleClick = (app: ApplicationListItem) => {
function handleClick(item: ApplicationListItem | StaticAppItem) {
const staticPath = (item as StaticAppItem).path;
if (staticPath) {
if (staticPath.startsWith('http://') || staticPath.startsWith('https://')) {
window.open(staticPath, '_blank');
return;
}
const code = appContextStore.appCode;
const targetPath = code ? `/app/${code}${staticPath}` : staticPath;
router.push(targetPath);
return;
}
const app = item as ApplicationListItem;
window.open(`${window.location.origin}/app/${app.code}`, '_blank');
};
}
// 点击更多
const handleMore = () => {
function handleMore() {
const morePath = props.widget.props.morePath;
if (morePath) {
router.push(morePath);
return;
}
window.open(`${window.location.origin}/application`, '_blank');
};
}
function getItemTitle(item: ApplicationListItem | StaticAppItem) {
return (item as StaticAppItem).title || (item as StaticAppItem).name || '';
}
function getItemIcon(item: ApplicationListItem | StaticAppItem) {
return (item as StaticAppItem).icon || (item as ApplicationListItem).icon || '';
}
function getItemBg(item: ApplicationListItem | StaticAppItem) {
return (
(item as StaticAppItem).bgColor ||
(item as StaticAppItem).color ||
'linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary))'
);
}
onMounted(() => {
loadApps();
@@ -59,7 +112,6 @@ onMounted(() => {
<template>
<div class="my-apps flex h-full flex-col p-4" v-loading="loading">
<!-- 头部 -->
<div class="mb-4 flex items-center justify-between">
<span class="text-sm font-medium">{{ widget.props.title }}</span>
<button
@@ -72,31 +124,30 @@ onMounted(() => {
<ChevronRight class="h-3.5 w-3.5" />
</button>
</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"
v-for="item in displayApps"
:key="(item as any).id || getItemTitle(item)"
class="m-4 flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105"
style="width: 72px"
@click="handleClick(app)"
@click="handleClick(item)"
>
<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: getItemBg(item) }"
>
<IconifyIcon
v-if="app.icon"
:icon="app.icon"
v-if="getItemIcon(item)"
:icon="getItemIcon(item)"
class="h-6 w-6 text-white"
/>
<AppWindow v-else class="h-6 w-6 text-white" />
</div>
<span class="text-muted-foreground w-full truncate text-center text-xs">{{ app.name }}</span>
<span class="text-muted-foreground w-full truncate text-center text-xs">
{{ getItemTitle(item) }}
</span>
</div>
</div>
<ElEmpty
@@ -131,7 +131,10 @@ onUnmounted(() => {
</script>
<template>
<div class="server-monitor flex h-full flex-col p-4" v-loading="loading">
<div
class="server-monitor flex h-full flex-col p-4"
v-loading="loading && widget.props.blockingLoading"
>
<!-- 头部 -->
<div class="mb-6 flex items-center justify-between">
<span class="text-sm font-medium">{{ widget.props.title }}</span>
@@ -12,43 +12,48 @@ const route = useRoute();
const DashboardRenderer = defineAsyncComponent(
() => import('#/components/dashboard-design/DashboardRenderer.vue'),
);
const loading = ref(false);
const pageConfig = ref<string>('');
const pageName = ref('');
const pageCode = ref('');
const pageConfig = ref('');
const aiHomeMenus = [
{
color: 'rgba(59, 130, 246, 0.85)',
icon: 'lucide:bot',
id: 'ai-agent',
path: '/ai-platform/agent',
title: '智能体',
},
{
color: 'rgba(22, 163, 74, 0.85)',
icon: 'lucide:git-branch',
id: 'ai-workflow',
path: '/ai-platform/workflow',
title: '流程编排',
},
{
color: 'rgba(124, 58, 237, 0.85)',
icon: 'lucide:history',
id: 'ai-workflow-runs',
path: '/ai-platform/workflow-runs',
title: '执行历史',
},
{
color: 'rgba(249, 115, 22, 0.85)',
icon: 'lucide:message-square-code',
id: 'codex-chat',
path: '/agent-chat/codex',
title: 'Codex',
},
{
color: 'rgba(15, 118, 110, 0.85)',
icon: 'lucide:database',
id: 'knowledge-base',
path: '/ai-platform/knowledge-base',
title: '知识库',
},
{
color: 'rgba(71, 85, 105, 0.85)',
icon: 'lucide:settings',
id: 'model-config',
path: '/ai-platform/model',
@@ -58,36 +63,42 @@ const aiHomeMenus = [
const adminHomeMenus = [
{
bgColor: 'linear-gradient(135deg, #60a5fa, #2563eb)',
icon: 'lucide:users',
id: 'system-user',
path: '/system/user',
title: '用户管理',
},
{
bgColor: 'linear-gradient(135deg, #a78bfa, #7c3aed)',
icon: 'lucide:shield-check',
id: 'system-role',
path: '/system/role',
title: '角色权限',
},
{
bgColor: 'linear-gradient(135deg, #fb923c, #f97316)',
icon: 'lucide:menu',
id: 'system-menu',
path: '/system/menu',
title: '菜单管理',
},
{
bgColor: 'linear-gradient(135deg, #2dd4bf, #0f766e)',
icon: 'lucide:key-round',
id: 'system-permission',
path: '/system/new-permission',
title: 'API 权限',
},
{
bgColor: 'linear-gradient(135deg, #4ade80, #16a34a)',
icon: 'lucide:megaphone',
id: 'announcement',
path: '/message/announcement',
title: '公告管理',
},
{
bgColor: 'linear-gradient(135deg, #94a3b8, #475569)',
icon: 'lucide:inbox',
id: 'message-list',
path: '/message/list',
@@ -137,6 +148,35 @@ function toQuickLinksWidget(widget: any, title: string, menus: any[]) {
};
}
function toApprovalCenterWidget(widget: any, title: string, menus: any[]) {
return {
...widget,
props: {
...widget.props,
menus,
showMore: false,
title,
},
title,
type: 'approval-center',
};
}
function toStaticAppsWidget(widget: any, title: string, menus: any[]) {
return {
...widget,
props: {
...widget.props,
maxCount: menus.length,
menus,
showMore: false,
title,
},
title,
type: 'my-apps',
};
}
function getLightMainHomeConfig(config: Record<string, any>) {
const next = JSON.parse(JSON.stringify(config));
if (!Array.isArray(next.widgets)) return next;
@@ -154,11 +194,11 @@ function getLightMainHomeConfig(config: Record<string, any>) {
}
if (widget.type === 'approval-center') {
return toQuickLinksWidget(widget, 'AI 协作中心', aiHomeMenus);
return toApprovalCenterWidget(widget, 'AI 协作中心', aiHomeMenus);
}
if (widget.type === 'my-apps') {
return toQuickLinksWidget(widget, '基础管理', adminHomeMenus);
return toStaticAppsWidget(widget, '基础管理', adminHomeMenus);
}
if (widget.type === 'quick-links') {
@@ -171,19 +211,10 @@ function getLightMainHomeConfig(config: Record<string, any>) {
return next;
}
// 获取页面编码
function getPageCode(): string {
// 优先从 query 获取
if (route.query.pageCode) {
return route.query.pageCode as string;
}
if (route.query.pageCode) return route.query.pageCode as string;
if (route.params.code) return route.params.code as string;
// 其次从 params 获取
if (route.params.code) {
return route.params.code as string;
}
// 最后从路径中提取(路径格式:/page-render/xxx
const pathParts = route.path.split('/').filter(Boolean);
const length = pathParts.length;
if (length >= 2 && pathParts[length - 2] === 'page-render') {
@@ -193,7 +224,6 @@ function getPageCode(): string {
return '';
}
// 加载页面数据
async function loadPageData() {
const code = getPageCode();
if (!code) {
@@ -201,12 +231,9 @@ async function loadPageData() {
return;
}
pageCode.value = code;
loading.value = true;
try {
const page = await getPageByCodeApi(code);
pageName.value = page.name;
const config =
page.page_config && Object.keys(page.page_config).length > 0
? code === 'main_home'
@@ -225,7 +252,6 @@ onMounted(() => {
loadPageData();
});
// 监听路由变化
watch(
() => route.fullPath,
() => {