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>