Browse Source
* feat(dashboard): 增加dashboard示例 * feat(chart-ui): 增加图表UI组件库 * feat(dashboard): 完善dashboard示例pull/3993/head
committed by
GitHub
16 changed files with 690 additions and 1 deletions
@ -1,7 +1,249 @@ |
|||
<script lang="ts" setup> |
|||
import { ref } from 'vue'; |
|||
|
|||
defineOptions({ name: 'WelCome' }); |
|||
import { Dashboard } from '@vben/universal-ui'; |
|||
import { echartsInstance as echarts } from '@vben/chart-ui'; |
|||
const cardList = ref([ |
|||
{ |
|||
title: '访问数', |
|||
extra: '月', |
|||
leftContent: '2000', |
|||
rightContent: 'flat-color-icons:conference-call', |
|||
leftFooter: '总访问数', |
|||
color: 'green', |
|||
rightFooter: '5000', |
|||
}, |
|||
{ |
|||
title: '销售额', |
|||
extra: '日', |
|||
leftContent: '$1350', |
|||
rightContent: 'flat-color-icons:sales-performance', |
|||
leftFooter: '总销售额', |
|||
color: 'red', |
|||
rightFooter: '$550000', |
|||
}, |
|||
]); |
|||
const chartTabs = ref([ |
|||
{ |
|||
name: '1', |
|||
title: '流量趋势', |
|||
option: { |
|||
color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'], |
|||
|
|||
tooltip: { |
|||
trigger: 'axis', |
|||
axisPointer: { |
|||
type: 'cross', |
|||
// label: { |
|||
// backgroundColor: '#6a7985', |
|||
// }, |
|||
}, |
|||
}, |
|||
legend: { |
|||
data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'], |
|||
}, |
|||
toolbox: { |
|||
feature: { |
|||
saveAsImage: {}, |
|||
}, |
|||
}, |
|||
grid: { |
|||
left: '3%', |
|||
right: '4%', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: [ |
|||
{ |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
}, |
|||
], |
|||
yAxis: [ |
|||
{ |
|||
type: 'value', |
|||
}, |
|||
], |
|||
series: [ |
|||
{ |
|||
name: 'Line 1', |
|||
type: 'line', |
|||
stack: 'Total', |
|||
smooth: true, |
|||
lineStyle: { |
|||
width: 0, |
|||
}, |
|||
showSymbol: false, |
|||
areaStyle: { |
|||
opacity: 0.8, |
|||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
{ |
|||
offset: 0, |
|||
color: 'rgb(128, 255, 165)', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgb(1, 191, 236)', |
|||
}, |
|||
]), |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [140, 232, 101, 264, 90, 340, 250], |
|||
}, |
|||
{ |
|||
name: 'Line 2', |
|||
type: 'line', |
|||
stack: 'Total', |
|||
smooth: true, |
|||
lineStyle: { |
|||
width: 0, |
|||
}, |
|||
showSymbol: false, |
|||
areaStyle: { |
|||
opacity: 0.8, |
|||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
{ |
|||
offset: 0, |
|||
color: 'rgb(0, 221, 255)', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgb(77, 119, 255)', |
|||
}, |
|||
]), |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [120, 282, 111, 234, 220, 340, 310], |
|||
}, |
|||
{ |
|||
name: 'Line 3', |
|||
type: 'line', |
|||
stack: 'Total', |
|||
smooth: true, |
|||
lineStyle: { |
|||
width: 0, |
|||
}, |
|||
showSymbol: false, |
|||
areaStyle: { |
|||
opacity: 0.8, |
|||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
{ |
|||
offset: 0, |
|||
color: 'rgb(55, 162, 255)', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgb(116, 21, 219)', |
|||
}, |
|||
]), |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [320, 132, 201, 334, 190, 130, 220], |
|||
}, |
|||
{ |
|||
name: 'Line 4', |
|||
type: 'line', |
|||
stack: 'Total', |
|||
smooth: true, |
|||
lineStyle: { |
|||
width: 0, |
|||
}, |
|||
showSymbol: false, |
|||
areaStyle: { |
|||
opacity: 0.8, |
|||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
{ |
|||
offset: 0, |
|||
color: 'rgb(255, 0, 135)', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgb(135, 0, 157)', |
|||
}, |
|||
]), |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [220, 402, 231, 134, 190, 230, 120], |
|||
}, |
|||
{ |
|||
name: 'Line 5', |
|||
type: 'line', |
|||
stack: 'Total', |
|||
smooth: true, |
|||
lineStyle: { |
|||
width: 0, |
|||
}, |
|||
showSymbol: false, |
|||
label: { |
|||
show: true, |
|||
position: 'top', |
|||
}, |
|||
areaStyle: { |
|||
opacity: 0.8, |
|||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
{ |
|||
offset: 0, |
|||
color: 'rgb(255, 191, 0)', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgb(224, 62, 76)', |
|||
}, |
|||
]), |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [220, 302, 181, 234, 210, 290, 150], |
|||
}, |
|||
], |
|||
}, |
|||
}, |
|||
{ |
|||
name: '2', |
|||
title: '访问量', |
|||
option: { |
|||
xAxis: { |
|||
type: 'category', |
|||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
}, |
|||
series: [ |
|||
{ |
|||
data: [ |
|||
120, |
|||
{ |
|||
value: 200, |
|||
itemStyle: { |
|||
color: '#a90000', |
|||
}, |
|||
}, |
|||
150, |
|||
80, |
|||
70, |
|||
110, |
|||
130, |
|||
], |
|||
type: 'bar', |
|||
}, |
|||
], |
|||
}, |
|||
}, |
|||
]); |
|||
</script> |
|||
|
|||
<template> |
|||
<div>dashboard</div> |
|||
<Dashboard :cardList="cardList" :chartTabs="chartTabs"></Dashboard> |
|||
</template> |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
{ |
|||
"name": "@vben/chart-ui", |
|||
"version": "5.0.0", |
|||
"homepage": "https://github.com/vbenjs/vue-vben-admin", |
|||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
|||
"directory": "packages/business/chart-ui" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"scripts": { |
|||
"build": "pnpm vite build", |
|||
"prepublishOnly": "npm run build" |
|||
}, |
|||
"files": [ |
|||
"dist" |
|||
], |
|||
"sideEffects": [ |
|||
"**/*.css" |
|||
], |
|||
"main": "./dist/index.mjs", |
|||
"module": "./dist/index.mjs", |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"development": "./src/index.ts", |
|||
"default": "./dist/index.mjs" |
|||
} |
|||
}, |
|||
"publishConfig": { |
|||
"exports": { |
|||
".": { |
|||
"default": "./dist/index.mjs" |
|||
} |
|||
} |
|||
}, |
|||
"peerDependencies": { |
|||
"@vben-core/design": "workspace:*" |
|||
}, |
|||
"dependencies": { |
|||
"@vben-core/preferences": "workspace:*", |
|||
"echarts": "^5.5.0", |
|||
"vue": "^3.4.29" |
|||
}, |
|||
"devDependencies": { |
|||
"@vben/types": "workspace:*" |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
<script setup lang="ts"> |
|||
import { echartsInstance, ECOption } from './index'; |
|||
import { onMounted, ref, unref, warn } from 'vue'; |
|||
import { usePreferences } from '@vben-core/preferences'; |
|||
const { isDark } = usePreferences(); |
|||
interface Props { |
|||
height?: string; |
|||
width?: string; |
|||
} |
|||
withDefaults(defineProps<Props>(), { |
|||
height: '500px', |
|||
width: '100%', |
|||
}); |
|||
|
|||
const instance = ref(); |
|||
const instanceRef = ref(HTMLElement); |
|||
onMounted(() => { |
|||
instance.value = echartsInstance.init( |
|||
instanceRef.value, |
|||
isDark.value ? 'dark' : '', |
|||
); |
|||
}); |
|||
const setChart = (option: ECOption, clear: boolean = true) => { |
|||
const c = unref(instance); |
|||
if (!c) { |
|||
warn('instance is null'); |
|||
return; |
|||
} |
|||
if (clear) c.clear(); |
|||
c.setOption(option); |
|||
}; |
|||
defineExpose({ setChart }); |
|||
</script> |
|||
|
|||
<template> |
|||
<div ref="instanceRef" :style="{ height, width }"></div> |
|||
</template> |
|||
@ -0,0 +1,59 @@ |
|||
import * as echarts from 'echarts/core'; |
|||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'; |
|||
import { |
|||
TitleComponent, |
|||
TooltipComponent, |
|||
GridComponent, |
|||
|
|||
// 数据集组件
|
|||
DatasetComponent, |
|||
// 内置数据转换器组件 (filter, sort)
|
|||
TransformComponent, |
|||
LegendComponent, |
|||
ToolboxComponent, |
|||
} from 'echarts/components'; |
|||
import { LabelLayout, UniversalTransition } from 'echarts/features'; |
|||
import { CanvasRenderer } from 'echarts/renderers'; |
|||
import type { |
|||
// 系列类型的定义后缀都为 SeriesOption
|
|||
BarSeriesOption, |
|||
LineSeriesOption, |
|||
} from 'echarts/charts'; |
|||
import type { |
|||
// 组件类型的定义后缀都为 ComponentOption
|
|||
TitleComponentOption, |
|||
TooltipComponentOption, |
|||
GridComponentOption, |
|||
DatasetComponentOption, |
|||
} from 'echarts/components'; |
|||
import type { ComposeOption } from 'echarts/core'; |
|||
|
|||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
|||
export type ECOption = ComposeOption< |
|||
| BarSeriesOption |
|||
| LineSeriesOption |
|||
| TitleComponentOption |
|||
| TooltipComponentOption |
|||
| GridComponentOption |
|||
| DatasetComponentOption |
|||
>; |
|||
|
|||
// 注册必须的组件
|
|||
echarts.use([ |
|||
TitleComponent, |
|||
PieChart, |
|||
RadarChart, |
|||
TooltipComponent, |
|||
GridComponent, |
|||
DatasetComponent, |
|||
TransformComponent, |
|||
BarChart, |
|||
LineChart, |
|||
LabelLayout, |
|||
UniversalTransition, |
|||
CanvasRenderer, |
|||
LegendComponent, |
|||
ToolboxComponent, |
|||
]); |
|||
export const echartsInstance = echarts; |
|||
export { default as chart } from './chart.vue'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
import { defineConfig } from '@vben/vite-config'; |
|||
|
|||
export default defineConfig(); |
|||
@ -0,0 +1,45 @@ |
|||
<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> |
|||
@ -0,0 +1,24 @@ |
|||
<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> |
|||
@ -0,0 +1,41 @@ |
|||
<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> |
|||
@ -0,0 +1,152 @@ |
|||
<script lang="ts" setup> |
|||
import type { CardItem, ChartItem } from './typings'; |
|||
defineOptions({ name: 'Dashboard' }); |
|||
import Card from './card.vue'; |
|||
import ChartTab from './chartTab.vue'; |
|||
import ChartCard from './chartCard.vue'; |
|||
import { ref } from 'vue'; |
|||
|
|||
interface Props { |
|||
cardList: CardItem[]; |
|||
chartTabs: ChartItem[]; |
|||
} |
|||
const itemA = ref({ |
|||
title: '玫瑰图', |
|||
option: { |
|||
legend: { |
|||
top: 'bottom', |
|||
}, |
|||
toolbox: { |
|||
show: true, |
|||
feature: { |
|||
mark: { show: true }, |
|||
dataView: { show: true, readOnly: false }, |
|||
restore: { show: true }, |
|||
saveAsImage: { show: true }, |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: 'Nightingale Chart', |
|||
type: 'pie', |
|||
radius: [50, 200], |
|||
center: ['50%', '50%'], |
|||
roseType: 'area', |
|||
itemStyle: { |
|||
borderRadius: 8, |
|||
}, |
|||
data: [ |
|||
{ value: 40, name: 'rose 1' }, |
|||
{ value: 38, name: 'rose 2' }, |
|||
{ value: 32, name: 'rose 3' }, |
|||
{ value: 30, name: 'rose 4' }, |
|||
{ value: 28, name: 'rose 5' }, |
|||
{ value: 26, name: 'rose 6' }, |
|||
{ value: 22, name: 'rose 7' }, |
|||
{ value: 18, name: 'rose 8' }, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
const itemB = ref({ |
|||
title: '雷达图', |
|||
option: { |
|||
legend: { |
|||
data: ['Allocated Budget', 'Actual Spending'], |
|||
}, |
|||
radar: { |
|||
// shape: 'circle', |
|||
indicator: [ |
|||
{ name: 'Sales', max: 6500 }, |
|||
{ name: 'Administration', max: 16000 }, |
|||
{ name: 'Information Technology', max: 30000 }, |
|||
{ name: 'Customer Support', max: 38000 }, |
|||
{ name: 'Development', max: 52000 }, |
|||
{ name: 'Marketing', max: 25000 }, |
|||
], |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: 'Budget vs spending', |
|||
type: 'radar', |
|||
data: [ |
|||
{ |
|||
value: [4200, 3000, 20000, 35000, 50000, 18000], |
|||
name: 'Allocated Budget', |
|||
}, |
|||
{ |
|||
value: [5000, 14000, 28000, 26000, 42000, 21000], |
|||
name: 'Actual Spending', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
const itemC = ref({ |
|||
title: '饼图', |
|||
option: { |
|||
tooltip: { |
|||
trigger: 'item', |
|||
}, |
|||
legend: { |
|||
top: '5%', |
|||
left: 'center', |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: 'Access From', |
|||
type: 'pie', |
|||
radius: ['40%', '70%'], |
|||
avoidLabelOverlap: false, |
|||
itemStyle: { |
|||
borderRadius: 10, |
|||
borderColor: '#fff', |
|||
borderWidth: 2, |
|||
}, |
|||
label: { |
|||
show: false, |
|||
position: 'center', |
|||
}, |
|||
emphasis: { |
|||
label: { |
|||
show: true, |
|||
fontSize: 40, |
|||
fontWeight: 'bold', |
|||
}, |
|||
}, |
|||
labelLine: { |
|||
show: false, |
|||
}, |
|||
data: [ |
|||
{ value: 1048, name: 'Search Engine' }, |
|||
{ value: 735, name: 'Direct' }, |
|||
{ value: 580, name: 'Email' }, |
|||
{ value: 484, name: 'Union Ads' }, |
|||
{ value: 300, name: 'Video Ads' }, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
cardList: () => [], |
|||
chartTabs: () => [], |
|||
}); |
|||
</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> |
|||
@ -0,0 +1,3 @@ |
|||
export { default as DashboardLayout } from './layout.vue'; |
|||
export { default as Dashboard } from './dashboard.vue'; |
|||
export { default as chartCard } from './chartCard.vue'; |
|||
@ -0,0 +1,7 @@ |
|||
<script lang="ts" setup> |
|||
defineOptions({ name: 'DashboardLayout' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<div>dashboardLayout</div> |
|||
</template> |
|||
@ -0,0 +1,17 @@ |
|||
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 }; |
|||
Loading…
Reference in new issue