feat: restore dashboard workspace and analytics
This commit is contained in:
+2
-1
@@ -197,6 +197,8 @@ web/apps/web-ele/src/views/online-dev/*
|
||||
web/apps/web-ele/src/views/dashboard/
|
||||
!web/apps/web-ele/src/views/dashboard/
|
||||
web/apps/web-ele/src/views/dashboard/*
|
||||
!web/apps/web-ele/src/views/dashboard/analytics/
|
||||
!web/apps/web-ele/src/views/dashboard/analytics/**
|
||||
!web/apps/web-ele/src/views/dashboard/workspace/
|
||||
!web/apps/web-ele/src/views/dashboard/workspace/**
|
||||
web/apps/web-ele/src/views/demos/
|
||||
@@ -246,7 +248,6 @@ web/apps/web-ele/src/components/dashboard-design/utils/*
|
||||
!web/apps/web-ele/src/components/dashboard-design/utils/dataFetcher.ts
|
||||
web/apps/web-ele/src/components/document-designer/
|
||||
web/apps/web-ele/src/components/workflow/designer/
|
||||
web/packages/effects/common-ui/src/ui/dashboard/
|
||||
web/docs/src/public/guide/
|
||||
web/packages/zq-univer/
|
||||
web/apps/web-ele/src/locales/langs/*/dashboard-design.json*
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"home": "首页",
|
||||
"controlCenter": "控制中心",
|
||||
"workspace": "工作台",
|
||||
"analytics": "分析页",
|
||||
"aiPlatform": "AI 平台",
|
||||
"aiAgent": "智能体",
|
||||
"workflowOrchestration": "流程编排",
|
||||
|
||||
@@ -194,6 +194,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
'../views/ai-platform/workflow/editor/index.vue',
|
||||
'../views/ai-platform/workflow/index.vue',
|
||||
'../views/ai-platform/workflow-runs/index.vue',
|
||||
'../views/dashboard/analytics/index.vue',
|
||||
'../views/dashboard/workspace/index.vue',
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const LIGHT_MENU_NAMES = new Set([
|
||||
'ControlCenter',
|
||||
'Dashboard',
|
||||
'DashboardAnalytics',
|
||||
'DashboardWorkspace',
|
||||
'AIAgent',
|
||||
'AIKnowledgeDetail',
|
||||
|
||||
@@ -12,6 +12,15 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/dashboard',
|
||||
redirect: '/dashboard/workspace',
|
||||
children: [
|
||||
{
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
meta: {
|
||||
hideInMenu: true,
|
||||
title: 'menu-title.analytics',
|
||||
},
|
||||
name: 'DashboardAnalytics',
|
||||
path: '/dashboard/analytics',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||
meta: {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
legend: {
|
||||
data: ['智能体会话', '工作流运行'],
|
||||
top: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
120, 280, 560, 1100, 1680, 2360, 3100, 2860, 3520, 4180, 4950,
|
||||
4620, 5280, 6100, 6800, 7200, 7600, 8300,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#2563eb',
|
||||
},
|
||||
name: '智能体会话',
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
60, 140, 320, 680, 920, 1380, 1820, 1700, 2260, 2600, 3120, 3480,
|
||||
3860, 4260, 4700, 5120, 5600, 6100,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#16a34a',
|
||||
},
|
||||
name: '工作流运行',
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#2563eb',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 9000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['节点事件', 'LLM 调用'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '开始' },
|
||||
{ name: '节点完成' },
|
||||
{ name: '并行分支' },
|
||||
{ name: '子流程' },
|
||||
{ name: '等待输入' },
|
||||
{ name: '错误' },
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 0.9,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#2563eb',
|
||||
},
|
||||
name: '节点事件',
|
||||
value: [92, 86, 68, 56, 42, 18],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#16a34a',
|
||||
},
|
||||
name: 'LLM 调用',
|
||||
value: [78, 72, 40, 36, 24, 12],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#2563eb', '#16a34a', '#f97316', '#7c3aed'],
|
||||
data: [
|
||||
{ name: '自主执行', value: 520 },
|
||||
{ name: '对话流', value: 430 },
|
||||
{ name: '子流程', value: 310 },
|
||||
{ name: '并行协作', value: 390 },
|
||||
].sort((a, b) => a.value - b.value),
|
||||
name: '协作类型',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#2563eb', '#16a34a', '#f97316', '#7c3aed'],
|
||||
data: [
|
||||
{ name: '智能体聊天', value: 1048 },
|
||||
{ name: '工作流运行', value: 735 },
|
||||
{ name: '模型配置', value: 420 },
|
||||
{ name: '知识库检索', value: 360 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '调用来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
data: [
|
||||
1200, 1800, 2600, 3200, 4100, 5200, 6100, 6800, 7600, 8400, 9300,
|
||||
10_200,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#2563eb',
|
||||
},
|
||||
name: '运行次数',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#2563eb',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 12_000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import { Activity, Bot, Cpu, GitBranch } from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
defineOptions({ name: 'DashboardAnalytics' });
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: Bot,
|
||||
title: '智能体',
|
||||
totalTitle: '已发布智能体',
|
||||
totalValue: 18,
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: '工作流',
|
||||
totalTitle: '启用工作流',
|
||||
totalValue: 12,
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: '模型调用',
|
||||
totalTitle: '近 30 天调用',
|
||||
totalValue: 86_400,
|
||||
value: 3200,
|
||||
},
|
||||
{
|
||||
icon: Activity,
|
||||
title: '运行事件',
|
||||
totalTitle: '已记录事件',
|
||||
totalValue: 128_000,
|
||||
value: 9800,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '协作趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月度运行',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-1/3"
|
||||
title="事件分布"
|
||||
>
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-1/3"
|
||||
title="调用来源"
|
||||
>
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="协作类型">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,534 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui/es/page';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import {
|
||||
Bell,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
Bot,
|
||||
Brain,
|
||||
GitFork,
|
||||
Database,
|
||||
GitBranch,
|
||||
History,
|
||||
Inbox,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Megaphone,
|
||||
MessageSquare,
|
||||
Menu,
|
||||
Settings,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
Workflow,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { ElButton, ElCard, ElTag } from 'element-plus';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
defineOptions({ name: 'DashboardWorkspace' });
|
||||
|
||||
type Accent = 'blue' | 'cyan' | 'green' | 'orange' | 'violet';
|
||||
|
||||
interface MetricItem {
|
||||
accent: Accent;
|
||||
label: string;
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
accent: Accent;
|
||||
description: string;
|
||||
icon: any;
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const displayName = computed(
|
||||
() => userStore.userInfo?.realName || userStore.userInfo?.username || '管理员',
|
||||
);
|
||||
|
||||
const metrics: MetricItem[] = [
|
||||
{
|
||||
accent: 'blue',
|
||||
label: '协作入口',
|
||||
title: '智能体',
|
||||
value: 'Agent',
|
||||
},
|
||||
{
|
||||
accent: 'cyan',
|
||||
label: '运行闭环',
|
||||
title: '流程编排',
|
||||
value: 'Flow',
|
||||
},
|
||||
{
|
||||
accent: 'green',
|
||||
label: '基础治理',
|
||||
title: '权限与角色',
|
||||
value: 'Auth',
|
||||
},
|
||||
{
|
||||
accent: 'orange',
|
||||
label: '运营触达',
|
||||
title: '公告消息',
|
||||
value: 'Notice',
|
||||
},
|
||||
const headerStats = [
|
||||
{ label: '智能体', value: '18' },
|
||||
{ label: '工作流', value: '12' },
|
||||
{ label: '运行事件', value: '12.8w' },
|
||||
];
|
||||
|
||||
const aiNavItems: NavItem[] = [
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
accent: 'blue',
|
||||
description: '创建、发布和调试业务智能体',
|
||||
color: '#2563eb',
|
||||
content: '管理已发布智能体,维护角色、提示词、工具能力和对话入口。',
|
||||
date: 'AI 平台',
|
||||
group: '智能体',
|
||||
icon: Bot,
|
||||
path: '/ai-platform/agent',
|
||||
title: '智能体管理',
|
||||
url: '/ai-platform/agent',
|
||||
},
|
||||
{
|
||||
accent: 'cyan',
|
||||
description: '编排多智能体、LLM 和工具节点',
|
||||
icon: Workflow,
|
||||
path: '/ai-platform/workflow',
|
||||
color: '#16a34a',
|
||||
content: '编排自主、对话流、子流程和并行节点,构建协作式任务链路。',
|
||||
date: 'AI 平台',
|
||||
group: '流程',
|
||||
icon: GitBranch,
|
||||
title: '流程编排',
|
||||
url: '/ai-platform/workflow',
|
||||
},
|
||||
{
|
||||
accent: 'violet',
|
||||
description: '查看运行事件、节点状态和失败原因',
|
||||
icon: GitFork,
|
||||
path: '/ai-platform/workflow-runs',
|
||||
color: '#f97316',
|
||||
content: '查看节点状态、LLM 调用、等待输入、错误和最终输出。',
|
||||
date: '可观测',
|
||||
group: '运行',
|
||||
icon: History,
|
||||
title: '执行历史',
|
||||
url: '/ai-platform/workflow-runs',
|
||||
},
|
||||
{
|
||||
accent: 'green',
|
||||
description: '维护可用模型和默认 Chat 模型',
|
||||
icon: Brain,
|
||||
path: '/ai-platform/model',
|
||||
title: '模型配置',
|
||||
},
|
||||
];
|
||||
|
||||
const adminNavItems: NavItem[] = [
|
||||
{
|
||||
accent: 'green',
|
||||
description: '维护后台账号、状态和基础资料',
|
||||
icon: Users,
|
||||
path: '/system/user',
|
||||
title: '用户管理',
|
||||
},
|
||||
{
|
||||
accent: 'violet',
|
||||
description: '管理角色权限和资源范围',
|
||||
icon: Shield,
|
||||
path: '/system/role',
|
||||
title: '角色权限',
|
||||
},
|
||||
{
|
||||
accent: 'blue',
|
||||
description: '维护菜单、路由和页面入口',
|
||||
icon: KeyRound,
|
||||
path: '/system/menu',
|
||||
title: '菜单管理',
|
||||
},
|
||||
{
|
||||
accent: 'orange',
|
||||
description: '发布公告并查看阅读反馈',
|
||||
icon: Megaphone,
|
||||
path: '/message/announcement',
|
||||
title: '公告管理',
|
||||
},
|
||||
];
|
||||
|
||||
const runtimeItems = [
|
||||
{
|
||||
description: '聊天、流程运行和系统通知会集中进入消息中心。',
|
||||
icon: MessageSquare,
|
||||
path: '/message/list',
|
||||
title: '消息中心',
|
||||
},
|
||||
{
|
||||
description: '检查最新公告和未读提醒。',
|
||||
icon: Bell,
|
||||
path: '/message/announcement-list',
|
||||
title: '公告列表',
|
||||
},
|
||||
{
|
||||
description: '调整系统配置、模型参数和界面偏好。',
|
||||
color: '#7c3aed',
|
||||
content: '维护默认 chat 模型和供应商,保障智能体运行时可解析模型。',
|
||||
date: '配置',
|
||||
group: '模型',
|
||||
icon: Settings,
|
||||
path: '/system/config',
|
||||
title: '系统配置',
|
||||
title: '模型配置',
|
||||
url: '/ai-platform/model',
|
||||
},
|
||||
{
|
||||
color: '#0f766e',
|
||||
content: '沉淀业务知识、提示词资料和协作过程中的可复用上下文。',
|
||||
date: '知识',
|
||||
group: '资料',
|
||||
icon: Database,
|
||||
title: '知识库',
|
||||
url: '/ai-platform/knowledge-base',
|
||||
},
|
||||
{
|
||||
color: '#475569',
|
||||
content: '查看智能体、流程、模型调用和基础管理的整体运行态势。',
|
||||
date: '看板',
|
||||
group: '分析',
|
||||
icon: LayoutDashboard,
|
||||
title: '运行分析',
|
||||
url: '/dashboard/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
function go(path: string) {
|
||||
router.push(path);
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#2563eb',
|
||||
icon: Users,
|
||||
title: '用户管理',
|
||||
url: '/system/user',
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
icon: ShieldCheck,
|
||||
title: '角色权限',
|
||||
url: '/system/role',
|
||||
},
|
||||
{
|
||||
color: '#0f766e',
|
||||
icon: KeyRound,
|
||||
title: 'API 权限',
|
||||
url: '/system/new-permission',
|
||||
},
|
||||
{
|
||||
color: '#f97316',
|
||||
icon: Menu,
|
||||
title: '菜单管理',
|
||||
url: '/system/menu',
|
||||
},
|
||||
{
|
||||
color: '#16a34a',
|
||||
icon: Megaphone,
|
||||
title: '公告管理',
|
||||
url: '/message/announcement',
|
||||
},
|
||||
{
|
||||
color: '#475569',
|
||||
icon: Inbox,
|
||||
title: '公告列表',
|
||||
url: '/message/announcement-list',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: '用真实业务需求触发业务需求分析师,确认能输出可测试需求、边界和下一步。',
|
||||
date: '今日',
|
||||
title: '验证业务需求分析师',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: '检查 start、node_start、llm_chunk、error、complete 等事件在前端是否可读。',
|
||||
date: '今日',
|
||||
title: '检查 SSE 运行事件',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: '发布智能体允许 model_id 为空,运行时回退到启用的默认 chat 模型。',
|
||||
date: '已完成',
|
||||
title: '模型兜底策略',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: '继续观察 AI 平台、智能体聊天和运行记录页面的首屏加载与预取效果。',
|
||||
date: '本周',
|
||||
title: '页面切换性能',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: '保留 systemd 非 Docker 部署路径,Docker 入口仅作为可选方式保留。',
|
||||
date: '已完成',
|
||||
title: '部署路径收敛',
|
||||
},
|
||||
]);
|
||||
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: '智能体运行时已增加默认 chat 模型解析,避免 <a>model_id 为空</a> 直接失败。',
|
||||
date: '刚刚',
|
||||
title: '运行时',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: '工作流运行记录聚焦节点状态、错误原因、子流程和并行分支结果。',
|
||||
date: '1 小时前',
|
||||
title: '可观测性',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: '用户、角色、权限、菜单和公告作为轻量后台的基础模块保留。',
|
||||
date: '今天',
|
||||
title: '基础后台',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: 'AI 平台保留智能体、流程编排、模型配置、知识库和执行历史。',
|
||||
date: '今天',
|
||||
title: 'AI 平台',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: '线上部署路径固定为 <a>/ai-agent-admin</a>,后端由 systemd 托管。',
|
||||
date: '本周',
|
||||
title: '部署',
|
||||
},
|
||||
];
|
||||
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="workspace-page">
|
||||
<section class="workspace-hero">
|
||||
<div class="min-w-0">
|
||||
<div class="hero-kicker">AI Agent Admin</div>
|
||||
<h1>你好,{{ displayName }}</h1>
|
||||
<p>
|
||||
当前后台保留基础治理和 AI 协作主链路,优先处理智能体、流程编排、运行观测和权限管理。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<ElButton type="primary" @click="go('/ai-platform/agent')">
|
||||
<Bot class="mr-1 size-4" />
|
||||
智能体
|
||||
</ElButton>
|
||||
<ElButton @click="go('/ai-platform/workflow')">
|
||||
<Workflow class="mr-1 size-4" />
|
||||
流程编排
|
||||
</ElButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid">
|
||||
<ElCard
|
||||
v-for="item in metrics"
|
||||
:key="item.title"
|
||||
class="metric-card"
|
||||
shadow="never"
|
||||
<div class="p-3">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
:stats="headerStats"
|
||||
>
|
||||
<div class="metric-top">
|
||||
<span :class="['accent-dot', `accent-${item.accent}`]"></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="metric-value">{{ item.value }}</div>
|
||||
<div class="metric-title">{{ item.title }}</div>
|
||||
</ElCard>
|
||||
</section>
|
||||
<template #title>
|
||||
早安,{{ userStore.userInfo?.realName || userStore.userInfo?.username || '管理员' }},这里是 AI Agent Admin 工作台
|
||||
</template>
|
||||
<template #description>
|
||||
保留 zq-ai-admin 成熟后台体验,聚焦智能体协作、流程编排、模型配置、知识库和基础权限治理。
|
||||
</template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<section class="content-grid">
|
||||
<ElCard class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="panel-header">
|
||||
<span>AI 协作</span>
|
||||
<ElTag effect="plain" size="small">核心链路</ElTag>
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject
|
||||
:items="projectItems"
|
||||
title="核心入口"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTrends
|
||||
:items="trendItems"
|
||||
class="mt-5"
|
||||
title="运行动态"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="基础管理"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo
|
||||
:items="todoItems"
|
||||
class="mt-5"
|
||||
title="协作闭环待办"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="nav-grid">
|
||||
<button
|
||||
v-for="item in aiNavItems"
|
||||
:key="item.path"
|
||||
class="nav-tile"
|
||||
type="button"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<span :class="['tile-icon', `tile-${item.accent}`]">
|
||||
<component :is="item.icon" class="size-5" />
|
||||
</span>
|
||||
<span class="tile-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.description }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElCard class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="panel-header">
|
||||
<span>基础后台</span>
|
||||
<ElTag effect="plain" size="small" type="success">已保留</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="nav-grid">
|
||||
<button
|
||||
v-for="item in adminNavItems"
|
||||
:key="item.path"
|
||||
class="nav-tile"
|
||||
type="button"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<span :class="['tile-icon', `tile-${item.accent}`]">
|
||||
<component :is="item.icon" class="size-5" />
|
||||
</span>
|
||||
<span class="tile-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.description }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ElCard>
|
||||
</section>
|
||||
|
||||
<ElCard class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="panel-header">
|
||||
<span>运维与消息</span>
|
||||
<ElTag effect="plain" size="small" type="warning">轻量入口</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="runtime-list">
|
||||
<button
|
||||
v-for="item in runtimeItems"
|
||||
:key="item.path"
|
||||
class="runtime-row"
|
||||
type="button"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<span class="runtime-icon">
|
||||
<component :is="item.icon" class="size-4" />
|
||||
</span>
|
||||
<span class="runtime-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.description }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workspace-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.workspace-hero {
|
||||
align-items: center;
|
||||
background:
|
||||
linear-gradient(135deg, rgb(36 99 235 / 8%), transparent 42%),
|
||||
var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workspace-hero h1 {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.workspace-hero p {
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.7;
|
||||
margin: 8px 0 0;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.panel-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metric-card :deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.metric-top {
|
||||
align-items: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.accent-dot {
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.accent-blue {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.accent-cyan {
|
||||
background: var(--el-color-info);
|
||||
}
|
||||
|
||||
.accent-green {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.accent-orange {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.accent-violet {
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
align-items: center;
|
||||
color: var(--el-text-color-primary);
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.nav-tile,
|
||||
.runtime-row {
|
||||
align-items: center;
|
||||
background: var(--el-bg-color-page);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.nav-tile:hover,
|
||||
.runtime-row:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
box-shadow: 0 8px 24px rgb(15 23 42 / 8%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tile-icon,
|
||||
.runtime-icon {
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
height: 38px;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.tile-blue {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tile-cyan {
|
||||
background: var(--el-color-info-light-9);
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
.tile-green {
|
||||
background: var(--el-color-success-light-9);
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.tile-orange {
|
||||
background: var(--el-color-warning-light-9);
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.tile-violet {
|
||||
background: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.tile-copy,
|
||||
.runtime-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tile-copy strong,
|
||||
.runtime-copy strong {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.tile-copy span,
|
||||
.runtime-copy span {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.runtime-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.runtime-icon {
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.metric-grid,
|
||||
.runtime-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.workspace-hero,
|
||||
.hero-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-grid,
|
||||
.metric-grid,
|
||||
.nav-grid,
|
||||
.runtime-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AnalysisChartCard',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<slot></slot>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
tabs?: TabOption[];
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AnalysisChartsTabs',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const defaultValue = computed(() => {
|
||||
return props.tabs?.[0]?.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-box w-full px-4 pb-5 pt-3">
|
||||
<Tabs :default-value="defaultValue">
|
||||
<TabsList>
|
||||
<template v-for="tab in tabs" :key="tab.label">
|
||||
<TabsTrigger :value="tab.value"> {{ tab.label }} </TabsTrigger>
|
||||
</template>
|
||||
</TabsList>
|
||||
<template v-for="tab in tabs" :key="tab.label">
|
||||
<TabsContent :value="tab.value" class="pt-4">
|
||||
<slot :name="tab.value"></slot>
|
||||
</TabsContent>
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnalysisOverviewItem } from '../typing';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
VbenCountToAnimator,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: AnalysisOverviewItem[];
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AnalysisOverview',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<template v-for="item in items" :key="item.title">
|
||||
<Card :title="item.title" class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex items-center justify-between">
|
||||
<VbenCountToAnimator
|
||||
:end-val="item.value"
|
||||
:start-val="1"
|
||||
class="text-xl"
|
||||
prefix=""
|
||||
/>
|
||||
<VbenIcon :icon="item.icon" class="size-8 flex-shrink-0" />
|
||||
</CardContent>
|
||||
<CardFooter class="justify-between">
|
||||
<span>{{ item.totalTitle }}</span>
|
||||
<VbenCountToAnimator
|
||||
:end-val="item.totalValue"
|
||||
:start-val="1"
|
||||
prefix=""
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as AnalysisChartCard } from './analysis-chart-card.vue';
|
||||
export { default as AnalysisChartsTabs } from './analysis-charts-tabs.vue';
|
||||
export { default as AnalysisOverview } from './analysis-overview.vue';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './analysis';
|
||||
export type * from './typing';
|
||||
export * from './workbench';
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
interface AnalysisOverviewItem {
|
||||
icon: Component | string;
|
||||
title: string;
|
||||
totalTitle: string;
|
||||
totalValue: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface WorkbenchProjectItem {
|
||||
color?: string;
|
||||
content: string;
|
||||
date: string;
|
||||
group: string;
|
||||
icon: Component | string;
|
||||
title: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface WorkbenchTrendItem {
|
||||
avatar: string;
|
||||
content: string;
|
||||
date: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface WorkbenchTodoItem {
|
||||
completed: boolean;
|
||||
content: string;
|
||||
date: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface WorkbenchQuickNavItem {
|
||||
color?: string;
|
||||
icon: Component | string;
|
||||
title: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
AnalysisOverviewItem,
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as WorkbenchHeader } from './workbench-header.vue';
|
||||
export { default as WorkbenchProject } from './workbench-project.vue';
|
||||
export { default as WorkbenchQuickNav } from './workbench-quick-nav.vue';
|
||||
export { default as WorkbenchTodo } from './workbench-todo.vue';
|
||||
export { default as WorkbenchTrends } from './workbench-trends.vue';
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenAvatar } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
avatar?: string;
|
||||
stats?: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchHeader',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
avatar: '',
|
||||
stats: () => [
|
||||
{ label: '待办', value: '2/10' },
|
||||
{ label: '项目', value: '8' },
|
||||
{ label: '团队', value: '300' },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-box p-4 py-6">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-4 md:flex-row md:items-center">
|
||||
<VbenAvatar :src="avatar" class="size-20 flex-shrink-0" />
|
||||
<div
|
||||
v-if="$slots.title || $slots.description"
|
||||
class="flex min-w-0 flex-col justify-center"
|
||||
>
|
||||
<h1
|
||||
v-if="$slots.title"
|
||||
class="text-md text-foreground font-semibold leading-7 md:text-xl"
|
||||
>
|
||||
<slot name="title"></slot>
|
||||
</h1>
|
||||
<span
|
||||
v-if="$slots.description"
|
||||
class="text-foreground/80 mt-1 leading-6"
|
||||
>
|
||||
<slot name="description"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid w-full grid-cols-3 gap-3 text-right lg:w-auto lg:min-w-80"
|
||||
>
|
||||
<div
|
||||
v-for="item in stats"
|
||||
:key="item.label"
|
||||
class="min-w-0 rounded-md px-2 py-1"
|
||||
>
|
||||
<span class="text-foreground/80 block truncate text-sm">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span class="text-foreground block truncate text-2xl">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkbenchProjectItem } from '../typing';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: WorkbenchProjectItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchProject',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-lg">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="grid grid-cols-1 p-0 md:grid-cols-2">
|
||||
<template v-for="(item, index) in items" :key="item.title">
|
||||
<div
|
||||
:class="{
|
||||
'md:border-r': index % 2 === 0,
|
||||
'rounded-bl-xl': index === items.length - 2,
|
||||
'rounded-br-xl': index === items.length - 1,
|
||||
}"
|
||||
class="border-border group min-w-0 cursor-pointer border-t p-4 transition-all hover:shadow-xl"
|
||||
@click="$emit('click', item)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<VbenIcon
|
||||
:color="item.color"
|
||||
:icon="item.icon"
|
||||
class="size-8 transition-all duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<span class="ml-4 text-lg font-medium">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="text-foreground/80 mt-4 flex min-h-12 leading-6">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="text-foreground/80 flex justify-between">
|
||||
<span>{{ item.group }}</span>
|
||||
<span>{{ item.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkbenchQuickNavItem } from '../typing';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: WorkbenchQuickNavItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchQuickNav',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-lg">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-wrap p-0">
|
||||
<template v-for="(item, index) in items" :key="item.title">
|
||||
<div
|
||||
:class="{
|
||||
'border-r-0': index % 3 === 2,
|
||||
'border-b-0': index < 3,
|
||||
'pb-4': index > 2,
|
||||
'rounded-bl-xl': index === items.length - 3,
|
||||
'rounded-br-xl': index === items.length - 1,
|
||||
}"
|
||||
class="flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl"
|
||||
@click="$emit('click', item)"
|
||||
>
|
||||
<VbenIcon
|
||||
:color="item.color"
|
||||
:icon="item.icon"
|
||||
class="size-7 transition-all duration-300 group-hover:scale-125"
|
||||
/>
|
||||
<span class="text-md mt-2 truncate">{{ item.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkbenchTodoItem } from '../typing';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
VbenCheckbox,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: WorkbenchTodoItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchTodo',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-lg">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-wrap p-5 pt-0">
|
||||
<ul class="divide-border w-full divide-y" role="list">
|
||||
<li
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
:class="{
|
||||
'select-none line-through opacity-60': item.completed,
|
||||
}"
|
||||
class="flex cursor-pointer justify-between gap-x-6 py-5"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-x-4">
|
||||
<VbenCheckbox v-model:checked="item.completed" name="completed" />
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-foreground text-sm font-semibold leading-6">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p
|
||||
class="text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5"
|
||||
v-html="item.content"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end">
|
||||
<span class="text-foreground/80 mt-6 text-xs leading-6">
|
||||
{{ item.date }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkbenchTrendItem } from '../typing';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: WorkbenchTrendItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchTrends',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-lg">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-wrap p-5 pt-0">
|
||||
<ul class="divide-border w-full divide-y" role="list">
|
||||
<li
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
class="flex justify-between gap-x-6 py-5"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-x-4">
|
||||
<VbenIcon
|
||||
:icon="item.avatar"
|
||||
alt=""
|
||||
class="size-10 flex-none rounded-full"
|
||||
/>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-foreground text-sm font-semibold leading-6">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p
|
||||
class="text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5"
|
||||
v-html="item.content"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end">
|
||||
<span class="text-foreground/80 mt-6 text-xs leading-6">
|
||||
{{ item.date }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './about';
|
||||
export * from './authentication';
|
||||
export * from './dashboard';
|
||||
export * from './fallback';
|
||||
|
||||
Reference in New Issue
Block a user