100 changed files with 1908 additions and 1081 deletions
@ -0,0 +1,39 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { $t } from '@vben/locales/helper'; |
|||
|
|||
import { BasicLayout } from '#/layouts'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
order: -1, |
|||
title: $t('page.dashboard.title'), |
|||
}, |
|||
name: 'Dashboard', |
|||
path: '/', |
|||
redirect: '/analytics', |
|||
children: [ |
|||
{ |
|||
name: 'Analytics', |
|||
path: '/analytics', |
|||
component: () => import('#/views/dashboard/analytics/index.vue'), |
|||
meta: { |
|||
affixTab: true, |
|||
title: $t('page.dashboard.analytics'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'Workspace', |
|||
path: '/workspace', |
|||
component: () => import('#/views/dashboard/workspace/index.vue'), |
|||
meta: { |
|||
title: $t('page.dashboard.workspace'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -1,30 +0,0 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { BasicLayout } from '#/layouts'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
hideChildrenInMenu: true, |
|||
order: -1, |
|||
title: '首页', |
|||
}, |
|||
name: 'Home', |
|||
path: '/', |
|||
redirect: '/welcome', |
|||
children: [ |
|||
{ |
|||
name: 'Welcome', |
|||
path: '/welcome', |
|||
component: () => import('#/views/dashboard/index.vue'), |
|||
meta: { |
|||
affixTab: true, |
|||
title: 'Welcome', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,80 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'AnalyticsTrends' }); |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
grid: { |
|||
bottom: 0, |
|||
containLabel: true, |
|||
left: '1%', |
|||
right: '1%', |
|||
top: '2 %', |
|||
}, |
|||
series: [ |
|||
{ |
|||
areaStyle: {}, |
|||
data: [ |
|||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000, |
|||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222, |
|||
111, |
|||
], |
|||
itemStyle: { |
|||
color: '#5ab1ef', |
|||
}, |
|||
smooth: true, |
|||
type: 'line', |
|||
}, |
|||
{ |
|||
areaStyle: {}, |
|||
data: [ |
|||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000, |
|||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11, |
|||
], |
|||
itemStyle: { |
|||
color: '#019680', |
|||
}, |
|||
smooth: true, |
|||
type: 'line', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
axisPointer: { |
|||
lineStyle: { |
|||
color: '#019680', |
|||
width: 1, |
|||
}, |
|||
}, |
|||
trigger: 'axis', |
|||
}, |
|||
xAxis: { |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
boundaryGap: false, |
|||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`), |
|||
type: 'category', |
|||
}, |
|||
yAxis: [ |
|||
{ |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
max: 80_000, |
|||
|
|||
type: 'value', |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,82 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'AnalyticsVisitsData' }); |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
legend: { |
|||
bottom: 0, |
|||
data: ['访问', '趋势'], |
|||
}, |
|||
radar: { |
|||
indicator: [ |
|||
{ |
|||
name: '网页', |
|||
}, |
|||
{ |
|||
name: '移动端', |
|||
}, |
|||
{ |
|||
name: 'Ipad', |
|||
}, |
|||
{ |
|||
name: '客户端', |
|||
}, |
|||
{ |
|||
name: '第三方', |
|||
}, |
|||
{ |
|||
name: '其它', |
|||
}, |
|||
], |
|||
radius: '60%', |
|||
splitNumber: 8, |
|||
}, |
|||
series: [ |
|||
{ |
|||
areaStyle: { |
|||
opacity: 1, |
|||
shadowBlur: 0, |
|||
shadowColor: 'rgba(0,0,0,.2)', |
|||
shadowOffsetX: 0, |
|||
shadowOffsetY: 10, |
|||
}, |
|||
data: [ |
|||
{ |
|||
itemStyle: { |
|||
color: '#b6a2de', |
|||
}, |
|||
name: '访问', |
|||
value: [90, 50, 86, 40, 50, 20], |
|||
}, |
|||
{ |
|||
itemStyle: { |
|||
color: '#5ab1ef', |
|||
}, |
|||
name: '趋势', |
|||
value: [70, 75, 70, 76, 20, 85], |
|||
}, |
|||
], |
|||
itemStyle: { |
|||
// borderColor: '#fff', |
|||
borderRadius: 10, |
|||
borderWidth: 2, |
|||
}, |
|||
symbolSize: 0, |
|||
type: 'radar', |
|||
}, |
|||
], |
|||
tooltip: {}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,46 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'AnalyticsVisitsSales' }); |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
series: [ |
|||
{ |
|||
animationDelay() { |
|||
return Math.random() * 400; |
|||
}, |
|||
animationEasing: 'exponentialInOut', |
|||
animationType: 'scale', |
|||
center: ['50%', '50%'], |
|||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
|||
data: [ |
|||
{ name: '外包', value: 500 }, |
|||
{ name: '定制', value: 310 }, |
|||
{ name: '技术支持', value: 274 }, |
|||
{ name: '远程', value: 400 }, |
|||
].sort((a, b) => { |
|||
return a.value - b.value; |
|||
}), |
|||
name: '商业占比', |
|||
radius: '80%', |
|||
roseType: 'radius', |
|||
type: 'pie', |
|||
}, |
|||
], |
|||
|
|||
tooltip: { |
|||
trigger: 'item', |
|||
}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,65 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'AnalyticsVisitsSource' }); |
|||
|
|||
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: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
|||
data: [ |
|||
{ name: '搜索引擎', value: 1048 }, |
|||
{ name: '直接访问', value: 735 }, |
|||
{ name: '邮件营销', value: 580 }, |
|||
{ name: '联盟广告', value: 484 }, |
|||
], |
|||
emphasis: { |
|||
label: { |
|||
fontSize: '12', |
|||
fontWeight: 'bold', |
|||
show: true, |
|||
}, |
|||
}, |
|||
itemStyle: { |
|||
// borderColor: '#fff', |
|||
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,55 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'AnalyticsVisits' }); |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
grid: { |
|||
bottom: 0, |
|||
containLabel: true, |
|||
left: '1%', |
|||
right: '1%', |
|||
top: '2 %', |
|||
}, |
|||
series: [ |
|||
{ |
|||
barMaxWidth: 80, |
|||
// color: '#4f69fd', |
|||
data: [ |
|||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, |
|||
3200, 4800, |
|||
], |
|||
type: 'bar', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
axisPointer: { |
|||
lineStyle: { |
|||
// color: '#4f69fd', |
|||
width: 1, |
|||
}, |
|||
}, |
|||
trigger: 'axis', |
|||
}, |
|||
xAxis: { |
|||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`), |
|||
type: 'category', |
|||
}, |
|||
yAxis: { |
|||
max: 8000, |
|||
splitNumber: 4, |
|||
type: 'value', |
|||
}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,92 @@ |
|||
<script lang="ts" setup> |
|||
import type { TabsItem } from '@vben/types'; |
|||
import type { AnalysisOverviewItem } from '@vben/universal-ui'; |
|||
|
|||
import { |
|||
SvgBellIcon, |
|||
SvgCakeIcon, |
|||
SvgCardIcon, |
|||
SvgDownloadIcon, |
|||
} from '@vben/icons'; |
|||
import { |
|||
AnalysisChartCard, |
|||
AnalysisChartsTabs, |
|||
AnalysisOverview, |
|||
} from '@vben/universal-ui'; |
|||
|
|||
import AnalyticsTrends from './analytics-trends.vue'; |
|||
import AnalyticsVisits from './analytics-visits.vue'; |
|||
import AnalyticsVisitsData from './analytics-visits-data.vue'; |
|||
import AnalyticsVisitsSales from './analytics-visits-sales.vue'; |
|||
import AnalyticsVisitsSource from './analytics-visits-source.vue'; |
|||
|
|||
defineOptions({ name: 'Analytics' }); |
|||
|
|||
const overviewItems: AnalysisOverviewItem[] = [ |
|||
{ |
|||
icon: SvgCardIcon, |
|||
title: '用户量', |
|||
totalTitle: '总用户量', |
|||
totalValue: 120_000, |
|||
value: 2000, |
|||
}, |
|||
{ |
|||
icon: SvgCakeIcon, |
|||
title: '访问量', |
|||
totalTitle: '总访问量', |
|||
totalValue: 500_000, |
|||
value: 20_000, |
|||
}, |
|||
{ |
|||
icon: SvgDownloadIcon, |
|||
title: '下载量', |
|||
totalTitle: '总下载量', |
|||
totalValue: 120_000, |
|||
value: 8000, |
|||
}, |
|||
{ |
|||
icon: SvgBellIcon, |
|||
title: '使用量', |
|||
totalTitle: '总使用量', |
|||
totalValue: 50_000, |
|||
value: 5000, |
|||
}, |
|||
]; |
|||
|
|||
const chartTabs: TabsItem[] = [ |
|||
{ |
|||
label: '流量趋势', |
|||
value: 'trends', |
|||
}, |
|||
{ |
|||
label: '月访问量', |
|||
value: 'visits', |
|||
}, |
|||
]; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<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,250 +0,0 @@ |
|||
<script lang="ts" setup> |
|||
// import { ref } from 'vue'; |
|||
|
|||
// import { echartsInstance as echarts } from '@vben/chart-ui'; |
|||
|
|||
defineOptions({ name: 'Welcome' }); |
|||
|
|||
// const cardList = ref([ |
|||
// { |
|||
// color: 'green', |
|||
// extra: '月', |
|||
// leftContent: '2000', |
|||
// leftFooter: '总访问数', |
|||
// rightContent: 'flat-color-icons:conference-call', |
|||
// rightFooter: '5000', |
|||
// title: '访问数', |
|||
// }, |
|||
// { |
|||
// color: 'red', |
|||
// extra: '日', |
|||
// leftContent: '$1350', |
|||
// leftFooter: '总销售额', |
|||
// rightContent: 'flat-color-icons:sales-performance', |
|||
// rightFooter: '$550000', |
|||
// title: '销售额', |
|||
// }, |
|||
// ]); |
|||
// const chartTabs = ref([ |
|||
// { |
|||
// name: '1', |
|||
// option: { |
|||
// color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'], |
|||
|
|||
// grid: { |
|||
// bottom: '3%', |
|||
// containLabel: true, |
|||
// left: '3%', |
|||
// right: '4%', |
|||
// }, |
|||
// legend: { |
|||
// data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'], |
|||
// }, |
|||
// series: [ |
|||
// { |
|||
// areaStyle: { |
|||
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
// { |
|||
// color: 'rgb(128, 255, 165)', |
|||
// offset: 0, |
|||
// }, |
|||
// { |
|||
// color: 'rgb(1, 191, 236)', |
|||
// offset: 1, |
|||
// }, |
|||
// ]), |
|||
// opacity: 0.8, |
|||
// }, |
|||
// data: [140, 232, 101, 264, 90, 340, 250], |
|||
// emphasis: { |
|||
// focus: 'series', |
|||
// }, |
|||
// lineStyle: { |
|||
// width: 0, |
|||
// }, |
|||
// name: 'Line 1', |
|||
// showSymbol: false, |
|||
// smooth: true, |
|||
// stack: 'Total', |
|||
// type: 'line', |
|||
// }, |
|||
// { |
|||
// areaStyle: { |
|||
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
// { |
|||
// color: 'rgb(0, 221, 255)', |
|||
// offset: 0, |
|||
// }, |
|||
// { |
|||
// color: 'rgb(77, 119, 255)', |
|||
// offset: 1, |
|||
// }, |
|||
// ]), |
|||
// opacity: 0.8, |
|||
// }, |
|||
// data: [120, 282, 111, 234, 220, 340, 310], |
|||
// emphasis: { |
|||
// focus: 'series', |
|||
// }, |
|||
// lineStyle: { |
|||
// width: 0, |
|||
// }, |
|||
// name: 'Line 2', |
|||
// showSymbol: false, |
|||
// smooth: true, |
|||
// stack: 'Total', |
|||
// type: 'line', |
|||
// }, |
|||
// { |
|||
// areaStyle: { |
|||
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
// { |
|||
// color: 'rgb(55, 162, 255)', |
|||
// offset: 0, |
|||
// }, |
|||
// { |
|||
// color: 'rgb(116, 21, 219)', |
|||
// offset: 1, |
|||
// }, |
|||
// ]), |
|||
// opacity: 0.8, |
|||
// }, |
|||
// data: [320, 132, 201, 334, 190, 130, 220], |
|||
// emphasis: { |
|||
// focus: 'series', |
|||
// }, |
|||
// lineStyle: { |
|||
// width: 0, |
|||
// }, |
|||
// name: 'Line 3', |
|||
// showSymbol: false, |
|||
// smooth: true, |
|||
// stack: 'Total', |
|||
// type: 'line', |
|||
// }, |
|||
// { |
|||
// areaStyle: { |
|||
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
// { |
|||
// color: 'rgb(255, 0, 135)', |
|||
// offset: 0, |
|||
// }, |
|||
// { |
|||
// color: 'rgb(135, 0, 157)', |
|||
// offset: 1, |
|||
// }, |
|||
// ]), |
|||
// opacity: 0.8, |
|||
// }, |
|||
// data: [220, 402, 231, 134, 190, 230, 120], |
|||
// emphasis: { |
|||
// focus: 'series', |
|||
// }, |
|||
// lineStyle: { |
|||
// width: 0, |
|||
// }, |
|||
// name: 'Line 4', |
|||
// showSymbol: false, |
|||
// smooth: true, |
|||
// stack: 'Total', |
|||
// type: 'line', |
|||
// }, |
|||
// { |
|||
// areaStyle: { |
|||
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
|||
// { |
|||
// color: 'rgb(255, 191, 0)', |
|||
// offset: 0, |
|||
// }, |
|||
// { |
|||
// color: 'rgb(224, 62, 76)', |
|||
// offset: 1, |
|||
// }, |
|||
// ]), |
|||
// opacity: 0.8, |
|||
// }, |
|||
// data: [220, 302, 181, 234, 210, 290, 150], |
|||
// emphasis: { |
|||
// focus: 'series', |
|||
// }, |
|||
// label: { |
|||
// position: 'top', |
|||
// show: true, |
|||
// }, |
|||
// lineStyle: { |
|||
// width: 0, |
|||
// }, |
|||
// name: 'Line 5', |
|||
// showSymbol: false, |
|||
// smooth: true, |
|||
// stack: 'Total', |
|||
// type: 'line', |
|||
// }, |
|||
// ], |
|||
// toolbox: { |
|||
// feature: { |
|||
// saveAsImage: {}, |
|||
// }, |
|||
// }, |
|||
// tooltip: { |
|||
// axisPointer: { |
|||
// type: 'cross', |
|||
// // label: { |
|||
// // backgroundColor: '#6a7985', |
|||
// // }, |
|||
// }, |
|||
// trigger: 'axis', |
|||
// }, |
|||
// xAxis: [ |
|||
// { |
|||
// boundaryGap: false, |
|||
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
// type: 'category', |
|||
// }, |
|||
// ], |
|||
// yAxis: [ |
|||
// { |
|||
// type: 'value', |
|||
// }, |
|||
// ], |
|||
// }, |
|||
// title: '流量趋势', |
|||
// }, |
|||
// { |
|||
// name: '2', |
|||
// option: { |
|||
// series: [ |
|||
// { |
|||
// data: [ |
|||
// 120, |
|||
// { |
|||
// itemStyle: { |
|||
// color: '#a90000', |
|||
// }, |
|||
// value: 200, |
|||
// }, |
|||
// 150, |
|||
// 80, |
|||
// 70, |
|||
// 110, |
|||
// 130, |
|||
// ], |
|||
// type: 'bar', |
|||
// }, |
|||
// ], |
|||
// xAxis: { |
|||
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
// type: 'category', |
|||
// }, |
|||
// yAxis: { |
|||
// type: 'value', |
|||
// }, |
|||
// }, |
|||
// title: '访问量', |
|||
// }, |
|||
// ]); |
|||
</script> |
|||
|
|||
<template> |
|||
<div>dashboard</div> |
|||
</template> |
|||
@ -0,0 +1,125 @@ |
|||
<script lang="ts" setup> |
|||
import type { |
|||
WorkbenchProjectItem, |
|||
WorkbenchQuickNavItem, |
|||
} from '@vben/universal-ui'; |
|||
|
|||
import { |
|||
WorkbenchHeader, |
|||
WorkbenchProject, |
|||
WorkbenchQuickNav, |
|||
} from '@vben/universal-ui'; |
|||
import { preferences } from '@vben-core/preferences'; |
|||
|
|||
import { useAccessStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'Workspace' }); |
|||
|
|||
const { userInfo } = useAccessStore(); |
|||
|
|||
const projectItems: WorkbenchProjectItem[] = [ |
|||
{ |
|||
color: '', |
|||
content: '不要等待机会,而要创造机会。', |
|||
date: '2021-04-01', |
|||
group: '开源组', |
|||
icon: 'carbon:logo-github', |
|||
title: 'Github', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
content: '现在的你决定将来的你。', |
|||
date: '2021-04-01', |
|||
group: '算法组', |
|||
icon: 'ion:logo-vue', |
|||
title: 'Vue', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
content: '没有什么才能比努力更重要。', |
|||
date: '2021-04-01', |
|||
group: '上班摸鱼', |
|||
icon: 'ion:logo-html5', |
|||
title: 'Html5', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
content: '热情和欲望可以突破一切难关。', |
|||
date: '2021-04-01', |
|||
group: 'UI', |
|||
icon: 'ion:logo-angular', |
|||
title: 'Angular', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
content: '健康的身体是实现目标的基石。', |
|||
date: '2021-04-01', |
|||
group: '技术牛', |
|||
icon: 'bx:bxl-react', |
|||
title: 'React', |
|||
}, |
|||
{ |
|||
color: '#EBD94E', |
|||
content: '路是走出来的,而不是空想出来的。', |
|||
date: '2021-04-01', |
|||
group: '架构组', |
|||
icon: 'ion:logo-javascript', |
|||
title: 'Js', |
|||
}, |
|||
]; |
|||
|
|||
const quickNavItems: WorkbenchQuickNavItem[] = [ |
|||
{ |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
title: '首页', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
title: '仪表盘', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
icon: 'ion:layers-outline', |
|||
title: '组件', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
icon: 'ion:settings-outline', |
|||
title: '系统管理', |
|||
}, |
|||
{ |
|||
color: '#4daf1bc9', |
|||
icon: 'ion:key-outline', |
|||
title: '权限管理', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
icon: 'ion:bar-chart-outline', |
|||
title: '图表', |
|||
}, |
|||
]; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<WorkbenchHeader |
|||
:avatar="userInfo?.avatar || preferences.app.defaultAvatar" |
|||
> |
|||
<template #title> |
|||
早安, {{ userInfo?.realName }}, 开始您一天的工作吧! |
|||
</template> |
|||
<template #description> 今日晴,20℃ - 32℃! </template> |
|||
</WorkbenchHeader> |
|||
|
|||
<div class="mt-5 flex"> |
|||
<div class="mr-4 w-full md:w-2/3"> |
|||
<WorkbenchProject :items="projectItems" title="项目" /> |
|||
</div> |
|||
<div class="w-full md:w-1/3"> |
|||
<WorkbenchQuickNav :items="quickNavItems" title="快捷导航" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,140 @@ |
|||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; |
|||
|
|||
import { getElementVisibleHeight } from './dom'; // 假设函数位于 utils.ts 中
|
|||
|
|||
describe('getElementVisibleHeight', () => { |
|||
// Mocking the getBoundingClientRect method
|
|||
const mockGetBoundingClientRect = vi.fn(); |
|||
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; |
|||
|
|||
beforeAll(() => { |
|||
// Mock getBoundingClientRect method
|
|||
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
// Restore original getBoundingClientRect method
|
|||
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; |
|||
}); |
|||
|
|||
it('should return 0 if the element is null or undefined', () => { |
|||
expect(getElementVisibleHeight(null)).toBe(0); |
|||
expect(getElementVisibleHeight()).toBe(0); |
|||
}); |
|||
|
|||
it('should return the visible height of the element', () => { |
|||
// Mock the getBoundingClientRect return value
|
|||
mockGetBoundingClientRect.mockReturnValue({ |
|||
bottom: 500, |
|||
height: 400, |
|||
left: 0, |
|||
right: 0, |
|||
toJSON: () => ({}), |
|||
top: 100, |
|||
width: 0, |
|||
x: 0, |
|||
y: 0, |
|||
}); |
|||
|
|||
const mockElement = document.createElement('div'); |
|||
document.body.append(mockElement); |
|||
|
|||
// Mocking window.innerHeight and document.documentElement.clientHeight
|
|||
const originalInnerHeight = window.innerHeight; |
|||
const originalClientHeight = document.documentElement.clientHeight; |
|||
|
|||
Object.defineProperty(window, 'innerHeight', { |
|||
value: 600, |
|||
writable: true, |
|||
}); |
|||
|
|||
Object.defineProperty(document.documentElement, 'clientHeight', { |
|||
value: 600, |
|||
writable: true, |
|||
}); |
|||
|
|||
expect(getElementVisibleHeight(mockElement)).toBe(400); |
|||
|
|||
// Restore original values
|
|||
Object.defineProperty(window, 'innerHeight', { |
|||
value: originalInnerHeight, |
|||
writable: true, |
|||
}); |
|||
|
|||
Object.defineProperty(document.documentElement, 'clientHeight', { |
|||
value: originalClientHeight, |
|||
writable: true, |
|||
}); |
|||
|
|||
mockElement.remove(); |
|||
}); |
|||
|
|||
it('should return the visible height when element is partially out of viewport', () => { |
|||
// Mock the getBoundingClientRect return value
|
|||
mockGetBoundingClientRect.mockReturnValue({ |
|||
bottom: 300, |
|||
height: 400, |
|||
left: 0, |
|||
right: 0, |
|||
toJSON: () => ({}), |
|||
top: -100, |
|||
width: 0, |
|||
x: 0, |
|||
y: 0, |
|||
}); |
|||
|
|||
const mockElement = document.createElement('div'); |
|||
document.body.append(mockElement); |
|||
|
|||
// Mocking window.innerHeight and document.documentElement.clientHeight
|
|||
const originalInnerHeight = window.innerHeight; |
|||
const originalClientHeight = document.documentElement.clientHeight; |
|||
|
|||
Object.defineProperty(window, 'innerHeight', { |
|||
value: 600, |
|||
writable: true, |
|||
}); |
|||
|
|||
Object.defineProperty(document.documentElement, 'clientHeight', { |
|||
value: 600, |
|||
writable: true, |
|||
}); |
|||
|
|||
expect(getElementVisibleHeight(mockElement)).toBe(300); |
|||
|
|||
// Restore original values
|
|||
Object.defineProperty(window, 'innerHeight', { |
|||
value: originalInnerHeight, |
|||
writable: true, |
|||
}); |
|||
|
|||
Object.defineProperty(document.documentElement, 'clientHeight', { |
|||
value: originalClientHeight, |
|||
writable: true, |
|||
}); |
|||
|
|||
mockElement.remove(); |
|||
}); |
|||
|
|||
it('should return 0 if the element is completely out of viewport', () => { |
|||
// Mock the getBoundingClientRect return value
|
|||
mockGetBoundingClientRect.mockReturnValue({ |
|||
bottom: -100, |
|||
height: 400, |
|||
left: 0, |
|||
right: 0, |
|||
toJSON: () => ({}), |
|||
top: -500, |
|||
width: 0, |
|||
x: 0, |
|||
y: 0, |
|||
}); |
|||
|
|||
const mockElement = document.createElement('div'); |
|||
document.body.append(mockElement); |
|||
|
|||
expect(getElementVisibleHeight(mockElement)).toBe(0); |
|||
|
|||
mockElement.remove(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* 获取元素可见高度 |
|||
* @param element |
|||
* @returns |
|||
*/ |
|||
function getElementVisibleHeight( |
|||
element?: HTMLElement | null | undefined, |
|||
): number { |
|||
if (!element) { |
|||
return 0; |
|||
} |
|||
const rect = element.getBoundingClientRect(); |
|||
const viewHeight = Math.max( |
|||
document.documentElement.clientHeight, |
|||
window.innerHeight, |
|||
); |
|||
|
|||
const top = Math.max(rect.top, 0); |
|||
const bottom = Math.min(rect.bottom, viewHeight); |
|||
|
|||
return Math.max(0, bottom - top); |
|||
} |
|||
|
|||
export { getElementVisibleHeight }; |
|||
@ -0,0 +1,61 @@ |
|||
import { describe, expect, it } from 'vitest'; |
|||
|
|||
import { uniqueByField } from './unique'; |
|||
|
|||
describe('uniqueByField', () => { |
|||
it('should return an array with unique items based on id field', () => { |
|||
const items = [ |
|||
{ id: 1, name: 'Item 1' }, |
|||
{ id: 2, name: 'Item 2' }, |
|||
{ id: 3, name: 'Item 3' }, |
|||
{ id: 1, name: 'Duplicate Item' }, |
|||
]; |
|||
|
|||
const uniqueItems = uniqueByField(items, 'id'); |
|||
|
|||
// Assert expected results
|
|||
expect(uniqueItems).toHaveLength(3); // After deduplication, there should be three objects left
|
|||
expect(uniqueItems).toEqual([ |
|||
{ id: 1, name: 'Item 1' }, |
|||
{ id: 2, name: 'Item 2' }, |
|||
{ id: 3, name: 'Item 3' }, |
|||
]); |
|||
}); |
|||
|
|||
it('should return an empty array when input array is empty', () => { |
|||
const items: any[] = []; // Empty array
|
|||
|
|||
const uniqueItems = uniqueByField(items, 'id'); |
|||
|
|||
// Assert expected results
|
|||
expect(uniqueItems).toEqual([]); |
|||
}); |
|||
|
|||
it('should handle arrays with only one item correctly', () => { |
|||
const items = [{ id: 1, name: 'Item 1' }]; |
|||
|
|||
const uniqueItems = uniqueByField(items, 'id'); |
|||
|
|||
// Assert expected results
|
|||
expect(uniqueItems).toHaveLength(1); |
|||
expect(uniqueItems).toEqual([{ id: 1, name: 'Item 1' }]); |
|||
}); |
|||
|
|||
it('should preserve the order of the first occurrence of each item', () => { |
|||
const items = [ |
|||
{ id: 2, name: 'Item 2' }, |
|||
{ id: 1, name: 'Item 1' }, |
|||
{ id: 3, name: 'Item 3' }, |
|||
{ id: 1, name: 'Duplicate Item' }, |
|||
]; |
|||
|
|||
const uniqueItems = uniqueByField(items, 'id'); |
|||
|
|||
// Assert expected results (order of first occurrences preserved)
|
|||
expect(uniqueItems).toEqual([ |
|||
{ id: 2, name: 'Item 2' }, |
|||
{ id: 1, name: 'Item 1' }, |
|||
{ id: 3, name: 'Item 3' }, |
|||
]); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,15 @@ |
|||
/** |
|||
* 根据指定字段对对象数组进行去重 |
|||
* @param arr 要去重的对象数组 |
|||
* @param key 去重依据的字段名 |
|||
* @returns 去重后的对象数组 |
|||
*/ |
|||
function uniqueByField<T>(arr: T[], key: keyof T): T[] { |
|||
const seen = new Map<any, T>(); |
|||
return arr.filter((item) => { |
|||
const value = item[key]; |
|||
return seen.has(value) ? false : (seen.set(value, item), true); |
|||
}); |
|||
} |
|||
|
|||
export { uniqueByField }; |
|||
@ -0,0 +1,111 @@ |
|||
<script lang="ts" setup> |
|||
import { computed, onMounted, ref, unref, watch, watchEffect } from 'vue'; |
|||
|
|||
import { isNumber } from '@vben-core/toolkit'; |
|||
|
|||
import { TransitionPresets, useTransition } from '@vueuse/core'; |
|||
|
|||
interface Props { |
|||
autoplay?: boolean; |
|||
color?: string; |
|||
decimal?: string; |
|||
decimals?: number; |
|||
duration?: number; |
|||
endVal?: number; |
|||
prefix?: string; |
|||
separator?: string; |
|||
startVal?: number; |
|||
suffix?: string; |
|||
transition?: keyof typeof TransitionPresets; |
|||
useEasing?: boolean; |
|||
} |
|||
|
|||
defineOptions({ name: 'CountToAnimator' }); |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
autoplay: true, |
|||
color: '', |
|||
decimal: '.', |
|||
decimals: 0, |
|||
duration: 1500, |
|||
endVal: 2021, |
|||
prefix: '', |
|||
separator: ',', |
|||
startVal: 0, |
|||
suffix: '', |
|||
transition: 'linear', |
|||
useEasing: true, |
|||
}); |
|||
|
|||
const emit = defineEmits(['onStarted', 'onFinished']); |
|||
|
|||
const source = ref(props.startVal); |
|||
const disabled = ref(false); |
|||
let outputValue = useTransition(source); |
|||
|
|||
const value = computed(() => formatNumber(unref(outputValue))); |
|||
|
|||
watchEffect(() => { |
|||
source.value = props.startVal; |
|||
}); |
|||
|
|||
watch([() => props.startVal, () => props.endVal], () => { |
|||
if (props.autoplay) { |
|||
start(); |
|||
} |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
props.autoplay && start(); |
|||
}); |
|||
|
|||
function start() { |
|||
run(); |
|||
source.value = props.endVal; |
|||
} |
|||
|
|||
function reset() { |
|||
source.value = props.startVal; |
|||
run(); |
|||
} |
|||
|
|||
function run() { |
|||
outputValue = useTransition(source, { |
|||
disabled, |
|||
duration: props.duration, |
|||
onFinished: () => emit('onFinished'), |
|||
onStarted: () => emit('onStarted'), |
|||
...(props.useEasing |
|||
? { transition: TransitionPresets[props.transition] } |
|||
: {}), |
|||
}); |
|||
} |
|||
|
|||
function formatNumber(num: number | string) { |
|||
if (!num && num !== 0) { |
|||
return ''; |
|||
} |
|||
const { decimal, decimals, prefix, separator, suffix } = props; |
|||
num = Number(num).toFixed(decimals); |
|||
num += ''; |
|||
|
|||
const x = num.split('.'); |
|||
let x1 = x[0]; |
|||
const x2 = x.length > 1 ? decimal + x[1] : ''; |
|||
|
|||
const rgx = /(\d+)(\d{3})/; |
|||
if (separator && !isNumber(separator)) { |
|||
while (rgx.test(x1)) { |
|||
x1 = x1.replace(rgx, `$1${separator}$2`); |
|||
} |
|||
} |
|||
return prefix + x1 + x2 + suffix; |
|||
} |
|||
|
|||
defineExpose({ reset }); |
|||
</script> |
|||
<template> |
|||
<span :style="{ color }"> |
|||
{{ value }} |
|||
</span> |
|||
</template> |
|||
@ -0,0 +1 @@ |
|||
export { default as VbenCountToAnimator } from './count-to-animator.vue'; |
|||
@ -1,37 +0,0 @@ |
|||
<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,15 @@ |
|||
<script setup lang="ts"> |
|||
interface Props { |
|||
height?: string; |
|||
width?: string; |
|||
} |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
height: '300px', |
|||
width: '100%', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div v-bind="$attrs" :style="{ height, width }"></div> |
|||
</template> |
|||
@ -0,0 +1,59 @@ |
|||
import type { |
|||
// 系列类型的定义后缀都为 SeriesOption
|
|||
BarSeriesOption, |
|||
LineSeriesOption, |
|||
} from 'echarts/charts'; |
|||
import type { |
|||
DatasetComponentOption, |
|||
GridComponentOption, |
|||
// 组件类型的定义后缀都为 ComponentOption
|
|||
TitleComponentOption, |
|||
TooltipComponentOption, |
|||
} from 'echarts/components'; |
|||
import type { ComposeOption } from 'echarts/core'; |
|||
|
|||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'; |
|||
import { |
|||
// 数据集组件
|
|||
DatasetComponent, |
|||
GridComponent, |
|||
LegendComponent, |
|||
TitleComponent, |
|||
ToolboxComponent, |
|||
TooltipComponent, |
|||
// 内置数据转换器组件 (filter, sort)
|
|||
TransformComponent, |
|||
} from 'echarts/components'; |
|||
import * as echarts from 'echarts/core'; |
|||
import { LabelLayout, UniversalTransition } from 'echarts/features'; |
|||
import { CanvasRenderer } from 'echarts/renderers'; |
|||
|
|||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
|||
export type ECOption = ComposeOption< |
|||
| BarSeriesOption |
|||
| DatasetComponentOption |
|||
| GridComponentOption |
|||
| LineSeriesOption |
|||
| TitleComponentOption |
|||
| TooltipComponentOption |
|||
>; |
|||
|
|||
// 注册必须的组件
|
|||
echarts.use([ |
|||
TitleComponent, |
|||
PieChart, |
|||
RadarChart, |
|||
TooltipComponent, |
|||
GridComponent, |
|||
DatasetComponent, |
|||
TransformComponent, |
|||
BarChart, |
|||
LineChart, |
|||
LabelLayout, |
|||
UniversalTransition, |
|||
CanvasRenderer, |
|||
LegendComponent, |
|||
ToolboxComponent, |
|||
]); |
|||
|
|||
export { echarts }; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './echarts'; |
|||
export { default as EchartsUI } from './echarts-ui.vue'; |
|||
export * from './use-echarts'; |
|||
@ -0,0 +1,108 @@ |
|||
import type { EChartsOption } from 'echarts'; |
|||
|
|||
import type EchartsUI from './echarts-ui.vue'; |
|||
|
|||
import type { Ref } from 'vue'; |
|||
import { computed, nextTick, watch } from 'vue'; |
|||
|
|||
import { usePreferences } from '@vben-core/preferences'; |
|||
|
|||
import { |
|||
tryOnUnmounted, |
|||
useDebounceFn, |
|||
useTimeoutFn, |
|||
useWindowSize, |
|||
} from '@vueuse/core'; |
|||
|
|||
import { echarts } from './echarts'; |
|||
|
|||
type EchartsUIType = typeof EchartsUI | undefined; |
|||
|
|||
type EchartsThemeType = 'dark' | 'light' | null; |
|||
|
|||
function useEcharts(chartRef: Ref<EchartsUIType>) { |
|||
let chartInstance: echarts.ECharts | null = null; |
|||
let cacheOptions: EChartsOption = {}; |
|||
|
|||
const { isDark } = usePreferences(); |
|||
const { height, width } = useWindowSize(); |
|||
const resizeHandler: () => void = useDebounceFn(resize, 200); |
|||
|
|||
const getOptions = computed((): EChartsOption => { |
|||
if (!isDark.value) { |
|||
return cacheOptions; |
|||
} |
|||
|
|||
return { |
|||
backgroundColor: 'transparent', |
|||
...cacheOptions, |
|||
}; |
|||
}); |
|||
|
|||
const initCharts = (t?: EchartsThemeType) => { |
|||
const el = chartRef?.value?.$el; |
|||
if (!el) { |
|||
return; |
|||
} |
|||
chartInstance = echarts.init(el, t || isDark.value ? 'dark' : null); |
|||
|
|||
return chartInstance; |
|||
}; |
|||
|
|||
const renderEcharts = (options: EChartsOption, clear = true) => { |
|||
cacheOptions = options; |
|||
return new Promise((resolve) => { |
|||
if (chartRef.value?.offsetHeight === 0) { |
|||
useTimeoutFn(() => { |
|||
renderEcharts(getOptions.value); |
|||
resolve(null); |
|||
}, 30); |
|||
return; |
|||
} |
|||
nextTick(() => { |
|||
useTimeoutFn(() => { |
|||
if (!chartInstance) { |
|||
const instance = initCharts(); |
|||
if (!instance) return; |
|||
} |
|||
clear && chartInstance?.clear(); |
|||
chartInstance?.setOption(getOptions.value); |
|||
resolve(null); |
|||
}, 30); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
function resize() { |
|||
chartInstance?.resize({ |
|||
animation: { |
|||
duration: 300, |
|||
easing: 'quadraticIn', |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
watch([width, height], () => { |
|||
resizeHandler?.(); |
|||
}); |
|||
|
|||
watch(isDark, () => { |
|||
if (chartInstance) { |
|||
chartInstance.dispose(); |
|||
initCharts(); |
|||
renderEcharts(cacheOptions); |
|||
} |
|||
}); |
|||
|
|||
tryOnUnmounted(() => { |
|||
// 销毁实例,释放资源
|
|||
chartInstance?.dispose(); |
|||
}); |
|||
return { |
|||
renderEcharts, |
|||
}; |
|||
} |
|||
|
|||
export { useEcharts }; |
|||
|
|||
export type { EchartsUIType }; |
|||
@ -1,59 +1 @@ |
|||
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'; |
|||
export * from './echarts'; |
|||
|
|||
@ -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,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> |
|||
@ -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> |
|||
@ -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'; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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'; |
|||
|
|||
@ -1,7 +0,0 @@ |
|||
<script lang="ts" setup> |
|||
defineOptions({ name: 'DashboardLayout' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<div>dashboardLayout</div> |
|||
</template> |
|||
@ -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, |
|||
}; |
|||
@ -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 }; |
|||
@ -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'; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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"] |
|||
} |
|||
|
|||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@ -1,19 +1,19 @@ |
|||
export {}; |
|||
|
|||
declare global { |
|||
interface Window { |
|||
__VBEN_ADMIN_METADATA__: { |
|||
authorEmail: string; |
|||
authorName: string; |
|||
authorUrl: string; |
|||
buildTime: string; |
|||
dependencies: Record<string, string>; |
|||
description: string; |
|||
devDependencies: Record<string, string>; |
|||
homepage: string; |
|||
license: string; |
|||
repositoryUrl: string; |
|||
version: string; |
|||
}; |
|||
} |
|||
// interface Window {
|
|||
const __VBEN_ADMIN_METADATA__: { |
|||
authorEmail: string; |
|||
authorName: string; |
|||
authorUrl: string; |
|||
buildTime: string; |
|||
dependencies: Record<string, string>; |
|||
description: string; |
|||
devDependencies: Record<string, string>; |
|||
homepage: string; |
|||
license: string; |
|||
repositoryUrl: string; |
|||
version: string; |
|||
}; |
|||
// }
|
|||
} |
|||
|
|||
File diff suppressed because it is too large
Loading…
Reference in new issue