fix: restore dashboard workspace widgets
This commit is contained in:
+76
-23
@@ -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
|
||||
|
||||
+4
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user