feat: add dashboard page

This commit is contained in:
vben
2024-06-23 23:18:55 +08:00
parent 199d5506ac
commit c58c0797ba
100 changed files with 1908 additions and 1081 deletions

View File

@@ -46,9 +46,10 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben/chart-ui": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/types": "workspace:*",
"@vueuse/integrations": "^10.11.0",
"qrcode": "^1.5.3",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {

View File

@@ -13,9 +13,9 @@ defineOptions({
withDefaults(defineProps<Props>(), {
description:
'是一个基于Vue3.0、Vite 、TypeScript 等前沿技术的后台解决方案,目标是为服务中大型项目开发提供现成的开箱解决方案及丰富的示例。',
'是一个现代化开箱即用的中后台解决方案,采用最新的技术栈,包括 Vue 3.0、Vite、TailwindCSS 和 TypeScript 等前沿技术,代码规范严谨,提供丰富的配置选项,旨在为中大型项目开发提供现成的开箱即用解决方案及丰富的示例,同时,它也是学习和深入前端技术的一个极佳示例。',
name: 'Vben Admin Pro',
title: '关于我们',
title: '关于项目',
});
const {
@@ -29,7 +29,9 @@ const {
license,
repositoryUrl,
version,
} = window.__VBEN_ADMIN_METADATA__ || {};
// vite inject-metadata 插件注入的全局变量
// eslint-disable-next-line no-undef
} = __VBEN_ADMIN_METADATA__ || {};
const vbenDescriptionItems: DescriptionItem[] = [
{
@@ -105,7 +107,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
<template>
<div class="m-5">
<div class="bg-card rounded-md p-5">
<div class="bg-card border-border rounded-md border p-5 shadow">
<div>
<h3 class="text-foreground text-2xl font-semibold leading-7">
{{ title }}
@@ -133,7 +135,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
</div>
</div>
<div class="bg-card mt-6 rounded-md p-5">
<div class="bg-card border-border mt-6 rounded-md border p-5">
<div>
<h5 class="text-foreground text-lg">生产环境依赖</h5>
</div>
@@ -152,8 +154,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
</dl>
</div>
</div>
<div class="bg-card mt-6 rounded-md p-5">
<div class="bg-card border-border mt-6 rounded-md border p-5">
<div>
<h5 class="text-foreground text-lg">开发环境依赖</h5>
</div>

View File

@@ -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>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { TabsItem } from '@vben/types';
import { computed } from 'vue';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@vben-core/shadcn-ui';
interface Props {
tabs: TabsItem[];
}
defineOptions({
name: 'AnalysisChartsTabs',
});
const props = withDefaults(defineProps<Props>(), {
tabs: () => [],
});
const defaultValue = computed(() => {
return props.tabs?.[0].value;
});
</script>
<template>
<div
class="bg-card border-border w-full rounded-xl border px-4 pb-5 pt-3 shadow"
>
<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>

View File

@@ -0,0 +1,59 @@
<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="md:flex">
<template v-for="(item, index) in items" :key="item.title">
<Card
:class="{ 'md:mr-4': index + 1 < 4 }"
:title="item.title"
class="mt-5 w-full md:mt-0 md:w-1/4"
>
<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>

View File

@@ -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';

View File

@@ -1,45 +0,0 @@
<script lang="ts" setup>
import { VbenIcon, Badge } from '@vben-core/shadcn-ui';
defineOptions({ name: 'DashboardCard' });
import type { CardItem } from './typings';
interface Props {
item: CardItem;
}
withDefaults(defineProps<Props>(), {});
</script>
<template>
<div class="rounded-lg border-2 border-solid">
<div class="flex justify-between p-2">
<div class="">
<slot name="title">{{ item.title }}</slot>
</div>
<div class="text-xs" :class="`bg-${item.color}-500`">
<slot name="extra"
><Badge>{{ item.extra }}</Badge></slot
>
</div>
</div>
<div class="ml-2 mr-2">
<div class="m-2 flex justify-between">
<div class="text-4xl">
<slot name="leftContent">{{ item.leftContent }}</slot>
</div>
<div>
<slot name="rightContent"
><VbenIcon :icon="item.rightContent" class="size-10"
/></slot>
</div>
</div>
<div class="m-2 flex justify-between">
<div>
<slot name="leftFooter">{{ item.leftFooter }}</slot>
</div>
<div>
<slot name="rightFooter">{{ item.rightFooter }}</slot>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,24 +0,0 @@
<script lang="ts" setup>
import { chart } from '@vben/chart-ui';
defineOptions({ name: 'ChartCard' });
import type { ChartItem } from './typings';
import { onMounted, ref } from 'vue';
interface Props {
item: ChartItem;
}
const chartRef = ref();
onMounted(() => {
chartRef.value.setChart(props.item.option);
});
const props = withDefaults(defineProps<Props>(), {});
</script>
<template>
<div class="rounded-lg border-2 border-solid">
<div class="">
{{ item.title }}
</div>
<chart ref="chartRef" />
</div>
</template>

View File

@@ -1,41 +0,0 @@
<script lang="ts" setup>
import { Tabs, TabsList, TabsTrigger } from '@vben-core/shadcn-ui';
import { chart } from '@vben/chart-ui';
defineOptions({ name: 'ChartTab' });
import type { ChartItem } from './typings';
import { onMounted, ref } from 'vue';
interface Props {
items: ChartItem[];
}
const chartRef = ref();
onMounted(() => {
change(0);
});
const change = (i) => {
const item = props.items[i];
if (!item) return;
item.option && chartRef.value.setChart(item.option);
};
const props = withDefaults(defineProps<Props>(), {});
</script>
<template>
<div class="rounded-lg border-2 border-solid">
<Tabs
:defaultValue="items[0].name"
className="w-[400px]"
@update:modelValue="change"
>
<TabsList className="flex w-full ">
<TabsTrigger
:value="index"
v-for="(item, index) in items"
:key="index"
>{{ item.title }}</TabsTrigger
>
</TabsList>
</Tabs>
<chart ref="chartRef" />
</div>
</template>

View File

@@ -1,156 +0,0 @@
<script lang="ts" setup>
import type { CardItem, ChartItem } from './typings';
import { ref } from 'vue';
import Card from './card.vue';
import ChartCard from './chartCard.vue';
import ChartTab from './chartTab.vue';
interface Props {
cardList: CardItem[];
chartTabs: ChartItem[];
}
defineOptions({ name: 'Dashboard' });
withDefaults(defineProps<Props>(), {
cardList: () => [],
chartTabs: () => [],
});
const itemA = ref({
option: {
legend: {
top: 'bottom',
},
series: [
{
center: ['50%', '50%'],
data: [
{ name: 'rose 1', value: 40 },
{ name: 'rose 2', value: 38 },
{ name: 'rose 3', value: 32 },
{ name: 'rose 4', value: 30 },
{ name: 'rose 5', value: 28 },
{ name: 'rose 6', value: 26 },
{ name: 'rose 7', value: 22 },
{ name: 'rose 8', value: 18 },
],
itemStyle: {
borderRadius: 8,
},
name: 'Nightingale Chart',
radius: [50, 200],
roseType: 'area',
type: 'pie',
},
],
toolbox: {
feature: {
dataView: { readOnly: false, show: true },
mark: { show: true },
restore: { show: true },
saveAsImage: { show: true },
},
show: true,
},
},
title: '玫瑰图',
});
const itemB = ref({
option: {
legend: {
data: ['Allocated Budget', 'Actual Spending'],
},
radar: {
// shape: 'circle',
indicator: [
{ max: 6500, name: 'Sales' },
{ max: 16_000, name: 'Administration' },
{ max: 30_000, name: 'Information Technology' },
{ max: 38_000, name: 'Customer Support' },
{ max: 52_000, name: 'Development' },
{ max: 25_000, name: 'Marketing' },
],
},
series: [
{
data: [
{
name: 'Allocated Budget',
value: [4200, 3000, 20_000, 35_000, 50_000, 18_000],
},
{
name: 'Actual Spending',
value: [5000, 14_000, 28_000, 26_000, 42_000, 21_000],
},
],
name: 'Budget vs spending',
type: 'radar',
},
],
},
title: '雷达图',
});
const itemC = ref({
option: {
legend: {
left: 'center',
top: '5%',
},
series: [
{
avoidLabelOverlap: false,
data: [
{ name: 'Search Engine', value: 1048 },
{ name: 'Direct', value: 735 },
{ name: 'Email', value: 580 },
{ name: 'Union Ads', value: 484 },
{ name: 'Video Ads', value: 300 },
],
emphasis: {
label: {
fontSize: 40,
fontWeight: 'bold',
show: true,
},
},
itemStyle: {
borderColor: '#fff',
borderRadius: 10,
borderWidth: 2,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
name: 'Access From',
radius: ['40%', '70%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
},
title: '饼图',
});
</script>
<template>
<div>
<div class="grid grid-cols-4 gap-4 p-2">
<Card v-for="item in cardList" :key="item.title" :item="item" />
</div>
<div class="p-2"><ChartTab :items="chartTabs" /></div>
<div class="grid grid-cols-3 gap-2 p-2">
<ChartCard :item="itemA" />
<ChartCard :item="itemB" />
<ChartCard :item="itemC" />
</div>
</div>
</template>

View File

@@ -1,3 +1,3 @@
export { default as DashboardLayout } from './layout.vue';
export { default as Dashboard } from './dashboard.vue';
export { default as chartCard } from './chartCard.vue';
export * from './analysis';
export type * from './typing';
export * from './workbench';

View File

@@ -1,7 +0,0 @@
<script lang="ts" setup>
defineOptions({ name: 'DashboardLayout' });
</script>
<template>
<div>dashboardLayout</div>
</template>

View File

@@ -0,0 +1,29 @@
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;
}
interface WorkbenchQuickNavItem {
color?: string;
icon: Component | string;
title: string;
}
export type {
AnalysisOverviewItem,
WorkbenchProjectItem,
WorkbenchQuickNavItem,
};

View File

@@ -1,17 +0,0 @@
interface CardItem {
title: string;
extra: string;
leftContent: string;
rightContent: string;
color?: string;
leftFooter: string;
rightFooter: string;
}
interface ChartItem {
name: string;
title: string;
options: any;
}
export type { CardItem, ChartItem };

View File

@@ -0,0 +1,3 @@
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';

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { VbenAvatar } from '@vben-core/shadcn-ui';
interface Props {
avatar?: string;
}
defineOptions({
name: 'WorkbenchHeader',
});
withDefaults(defineProps<Props>(), {
avatar: '',
});
</script>
<template>
<div class="bg-card border-border rounded-xl p-4 py-6 shadow lg:flex">
<VbenAvatar :src="avatar" class="size-20" />
<div
v-if="$slots.title || $slots.description"
class="flex flex-col justify-center md:ml-6 md:mt-0"
>
<h1 v-if="$slots.title" class="text-md font-semibold md:text-xl">
<slot name="title"></slot>
</h1>
<span v-if="$slots.description" class="text-foreground/80 mt-1">
<slot name="description"></slot>
</span>
</div>
<div class="mt-4 flex flex-1 justify-end md:mt-0">
<div class="flex flex-col justify-center text-right">
<span class="text-foreground/80"> 待办 </span>
<span class="text-2xl">2/10</span>
</div>
<div class="mx-12 flex flex-col justify-center text-right md:mx-16">
<span class="text-foreground/80"> 项目 </span>
<span class="text-2xl">8</span>
</div>
<div class="mr-4 flex flex-col justify-center text-right md:mr-10">
<span class="text-foreground/80"> 团队 </span>
<span class="text-2xl">300</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,56 @@
<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: () => [],
});
</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,
}"
class="border-border w-1/3 border-b border-r border-t p-4 transition-all hover:shadow-xl"
>
<div class="flex items-center">
<VbenIcon :color="item.color" :icon="item.icon" class="size-8" />
<span class="ml-4 text-lg font-medium">{{ item.title }}</span>
</div>
<div class="text-foreground/80 mt-4 flex h-10">
{{ 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>

View File

@@ -0,0 +1,47 @@
<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: () => [],
});
</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,
'pb-4': index > 2,
'border-b-0': index < 3,
}"
class="flex-col-center border-border w-1/3 border-b border-r border-t py-5 transition-all hover:shadow-xl"
>
<VbenIcon :color="item.color" :icon="item.icon" class="size-5" />
<span class="text-md mt-2 truncate">{{ item.title }}</span>
</div>
</template>
</CardContent>
</Card>
</template>

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["@vben/types/window"]
},
"include": ["src"],
"exclude": ["node_modules"]
}