feat: add dashboard page

pull/48/MERGE
vben 2024-06-23 23:18:55 +08:00
parent 199d5506ac
commit c58c0797ba
100 changed files with 1908 additions and 1081 deletions

View File

@ -5,7 +5,7 @@ const fakeUserList = [
accessToken: 'fakeAdminToken',
avatar: '',
desc: 'manager',
homePath: '/welcome',
homePath: '/',
password: '123456',
realName: 'Vben Admin',
roles: [
@ -21,7 +21,7 @@ const fakeUserList = [
accessToken: 'fakeTestToken',
avatar: '',
desc: 'tester',
homePath: '/welcome',
homePath: '/',
password: '123456',
realName: 'test user',
roles: [

View File

@ -44,7 +44,7 @@
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.11",
"pinia": "2.1.7",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {

View File

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

View File

@ -9,7 +9,7 @@ const routes: RouteRecordRaw[] = [
component: BasicLayout,
meta: {
icon: 'mdi:lightbulb-error-outline',
title: $t('page.fallback.page'),
title: $t('page.fallback.title'),
},
name: 'FallbackLayout',
path: '/fallback',

View File

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

View File

@ -11,7 +11,7 @@ const routes: RouteRecordRaw[] = [
icon: 'ic:round-menu',
keepAlive: true,
order: 1000,
title: $t('page.nested.page'),
title: $t('page.nested.title'),
},
name: 'Nested',
path: '/nested',

View File

@ -9,7 +9,7 @@ const routes: RouteRecordRaw[] = [
component: BasicLayout,
meta: {
icon: 'ic:round-settings-input-composite',
title: $t('page.outside.page'),
title: $t('page.outside.title'),
},
name: 'Outside',
path: '/outside',

View File

@ -2,7 +2,7 @@ import type { InitStoreOptions } from '@vben-core/stores';
import type { App } from 'vue';
import { initStore } from '@vben-core/stores';
import { initStore, useAccessStore, useTabsStore } from '@vben-core/stores';
/**
* @zh_CN pinia
@ -13,4 +13,4 @@ async function setupStore(app: App, options: InitStoreOptions) {
app.use(pinia);
}
export { setupStore };
export { setupStore, useAccessStore, useTabsStore };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
"words": [
"clsx",
"esno",
"unref",
"taze",
"acmr",
"antd",

View File

@ -29,7 +29,7 @@
},
"dependencies": {
"@changesets/git": "^3.0.0",
"@manypkg/get-packages": "^2.2.1",
"@manypkg/get-packages": "^2.2.2",
"consola": "^3.2.3",
"dayjs": "^1.11.11",
"find-up": "^7.0.0",

View File

@ -45,7 +45,7 @@
"tailwindcss": "^3.4.3"
},
"dependencies": {
"@iconify/json": "^2.2.222",
"@iconify/json": "^2.2.223",
"@iconify/tailwind": "^1.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",

View File

@ -20,6 +20,6 @@
],
"dependencies": {
"@vben/types": "workspace:*",
"vite": "^5.3.1"
"vite": "^5.3.2"
}
}

View File

@ -8,7 +8,7 @@
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"moduleResolution": "bundler",
"types": ["vite/client", "@vben/types/window"],
"types": ["vite/client"],
"declaration": false
}
}

View File

@ -46,8 +46,8 @@
"rollup": "^4.18.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.6",
"unplugin-turbo-console": "^1.8.8-beta.1",
"vite": "^5.3.1",
"unplugin-turbo-console": "^1.8.9",
"vite": "^5.3.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^3.9.1",
"vite-plugin-html": "^3.2.2",

View File

@ -43,7 +43,7 @@ interface CommonPluginOptions {
/** 环境变量 */
env?: Record<string, any>;
/** 是否开启注入metadata */
injectMetadata: boolean;
injectMetadata?: boolean;
/** 是否构建模式 */
isBuild?: boolean;
/** 构建模式 */

View File

@ -72,7 +72,7 @@
"turbo": "^2.0.5",
"typescript": "^5.5.2",
"unbuild": "^2.0.0",
"vite": "^5.3.1",
"vite": "^5.3.2",
"vitest": "^2.0.0-beta.10",
"vue-tsc": "^2.0.22"
},
@ -86,7 +86,7 @@
"@ant-design/colors": "^7.0.2",
"@ctrl/tinycolor": "^4.1.0",
"clsx": "^2.1.1",
"vue": "^3.4.30"
"vue": "^3.4.31"
},
"neverBuiltDependencies": [
"canvas",

View File

@ -34,6 +34,6 @@
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -27,7 +27,7 @@ const defaultPreferences: Preferences = {
styleType: 'normal',
},
footer: {
enable: true,
enable: false,
fixed: true,
},
header: {

View File

@ -36,7 +36,7 @@ describe('preferences', () => {
});
it('initPreferences should initialize preferences with overrides and namespace', async () => {
const overrides = { theme: { colorPrimary: 'hsl(211 91% 39%)' } };
const overrides = { theme: { colorPrimary: 'hsl(245 82% 67%)' } };
const namespace = 'testNamespace';
await preferenceManager.initPreferences({ namespace, overrides });

View File

@ -42,7 +42,7 @@
"@vben-core/typings": "workspace:*",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
}
}

View File

@ -1,9 +1,9 @@
import type { TabItem } from '@vben-core/typings';
import type { RouteRecordNormalized, Router } from 'vue-router';
import { toRaw } from 'vue';
import { startProgress, stopProgress } from '@vben-core/toolkit';
import { TabItem } from '@vben-core/typings';
import { acceptHMRUpdate, defineStore } from 'pinia';

View File

@ -22,6 +22,6 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -4,6 +4,7 @@ import { Icon } from '@iconify/vue';
function createIcon(name: string) {
return defineComponent({
name: `SvgIcon-${name}`,
setup(props, { attrs }) {
return () => h(Icon, { icon: name, ...props, ...attrs });
},

View File

@ -36,7 +36,7 @@
}
},
"dependencies": {
"@vue/shared": "^3.4.30",
"@vue/shared": "^3.4.31",
"clsx": "2.1.1",
"defu": "^6.1.4",
"nprogress": "^0.2.0",

View File

@ -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();
});
});

View File

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

View File

@ -1,5 +1,6 @@
export * from './cn';
export * from './diff';
export * from './dom';
export * from './hash';
export * from './inference';
export * from './letter';
@ -7,5 +8,6 @@ export * from './merge';
export * from './namespace';
export * from './nprogress';
export * from './tree';
export * from './unique';
export * from './update-css-variables';
export * from './window';

View File

@ -97,11 +97,20 @@ function isWindowsOs(): boolean {
return windowsRegex.test(navigator.userAgent);
}
/**
*
* @param value
*/
function isNumber(value: any): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
export {
isEmpty,
isFunction,
isHttpUrl,
isMacOs,
isNumber,
isObject,
isString,
isUndefined,

View File

@ -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' },
]);
});
});

View File

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

View File

@ -39,7 +39,7 @@
}
},
"dependencies": {
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
}
}

View File

@ -39,8 +39,9 @@
"dependencies": {
"@vben-core/iconify": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -4,6 +4,8 @@ import type { ContentCompactType } from '@vben-core/typings';
import type { CSSProperties } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { getElementVisibleHeight } from '@vben-core/toolkit';
import { useCssVar, useDebounceFn, useWindowSize } from '@vueuse/core';
interface Props {
@ -54,12 +56,12 @@ const props = withDefaults(defineProps<Props>(), {
paddingTop: 16,
});
const domElement = ref<HTMLDivElement | null>();
const contentElement = ref<HTMLDivElement | null>();
const { height, width } = useWindowSize();
const contentClientHeight = useCssVar('--vben-content-client-height');
const debouncedCalcHeight = useDebounceFn(() => {
contentClientHeight.value = `${domElement.value?.clientHeight ?? window.innerHeight}px`;
contentClientHeight.value = `${getElementVisibleHeight(contentElement.value)}px`;
}, 200);
const style = computed((): CSSProperties => {
@ -97,7 +99,7 @@ onMounted(() => {
</script>
<template>
<main ref="domElement" :style="style">
<main ref="contentElement" :style="style">
<slot></slot>
</main>
</template>

View File

@ -43,6 +43,6 @@
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -50,7 +50,7 @@
"@vueuse/core": "^10.11.0",
"class-variance-authority": "^0.7.0",
"radix-vue": "^1.8.5",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-sonner": "^1.1.3"
}
}

View File

@ -13,7 +13,7 @@ interface Props extends BacktopProps {}
defineOptions({ name: 'BackTop' });
const props = withDefaults(defineProps<Props>(), {
bottom: 40,
bottom: 24,
isGroup: false,
right: 40,
target: '',
@ -32,7 +32,7 @@ const { handleClick, visible } = useBackTop(props);
<VbenButton
v-if="visible"
:style="backTopStyle"
class="bg-accent data fixed bottom-10 right-5 h-10 w-10 rounded-full"
class="bg-accent hover:bg-heavy data fixed bottom-10 right-5 z-10 h-10 w-10 rounded-full"
size="icon"
variant="icon"
@click="handleClick"

View File

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

View File

@ -0,0 +1 @@
export { default as VbenCountToAnimator } from './count-to-animator.vue';

View File

@ -6,6 +6,7 @@ export * from './breadcrumb';
export * from './button';
export * from './checkbox';
export * from './context-menu';
export * from './count-to-animator';
export * from './dropdown-menu';
export * from './floating-button-group';
export * from './full-screen';

View File

@ -42,6 +42,6 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -41,7 +41,8 @@
},
"dependencies": {
"@vben-core/preferences": "workspace:*",
"echarts": "^5.5.0",
"vue": "^3.4.30"
"@vueuse/core": "^10.11.0",
"echarts": "^5.5.1",
"vue": "^3.4.31"
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export * from './echarts';
export { default as EchartsUI } from './echarts-ui.vue';
export * from './use-echarts';

View File

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

View File

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

View File

@ -46,9 +46,10 @@
"@vben-core/stores": "workspace:*",
"@vben-core/tabs-ui": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/widgets": "workspace:*",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {

View File

@ -10,8 +10,8 @@ import { useContentSpinner } from './use-content-spinner';
defineOptions({ name: 'LayoutContent' });
const { keepAlive } = usePreferences();
const tabsStore = useTabsStore();
const { keepAlive } = usePreferences();
const { spinning } = useContentSpinner();
const { getCacheTabs, getExcludeTabs, renderRouteView } =

View File

@ -64,6 +64,7 @@ const breadcrumbs = computed((): IBreadcrumb[] => {
if (props.hideWhenOnlyOne && resultBreadcrumb.length === 1) {
return [];
}
return resultBreadcrumb;
});

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"]
}

View File

@ -51,7 +51,7 @@
"@vueuse/core": "^10.11.0",
"@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

@ -56,9 +56,9 @@ onUnmounted(() => {
<style>
.coze-assistant {
position: absolute;
position: fixed;
right: 30px;
bottom: 30px;
bottom: 60px;
z-index: 1000;
img {

View File

@ -7,7 +7,7 @@ import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IcRoundClose, IcRoundSearchOff } from '@vben-core/iconify';
import { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { mapTree, traverseTreeValues } from '@vben-core/toolkit';
import { mapTree, traverseTreeValues, uniqueByField } from '@vben-core/toolkit';
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
@ -247,16 +247,17 @@ onMounted(() => {
{{ $t('search.recent') }}
</li>
<li
v-for="(item, index) in searchResults"
v-for="(item, index) in uniqueByField(searchResults, 'path')"
:key="item.path"
:class="
activeIndex === index
? 'active bg-primary text-primary-foreground text-muted-foreground'
? 'active bg-primary text-primary-foreground'
: ''
"
:data-index="index"
:data-search-item="index"
class="bg-accent flex-center group mb-3 w-full cursor-pointer rounded-lg px-4 py-4"
@click="handleEnter"
@mouseenter="handleMouseenter"
>
<VbenIcon

View File

@ -22,7 +22,7 @@ const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
const typeItems: SelectListItem[] = [
{ label: $t('preferences.normal'), value: 'normal' },
{ label: $t('preferences.breadcrumb-background'), value: 'background' },
{ label: $t('preferences.breadcrumb.background'), value: 'background' },
];
const disableItem = computed(() => {
@ -32,22 +32,22 @@ const disableItem = computed(() => {
<template>
<SwitchItem v-model="breadcrumbEnable" :disabled="disabled">
{{ $t('preferences.breadcrumb-enable') }}
{{ $t('preferences.breadcrumb.enable') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbHideOnlyOne" :disabled="disableItem">
{{ $t('preferences.breadcrumb-hide-only-one') }}
{{ $t('preferences.breadcrumb.hide-only-one') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbShowHome" :disabled="disableItem">
{{ $t('preferences.breadcrumb-home') }}
{{ $t('preferences.breadcrumb.home') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbShowIcon" :disabled="disableItem">
{{ $t('preferences.breadcrumb-icon') }}
{{ $t('preferences.breadcrumb.icon') }}
</SwitchItem>
<ToggleItem
v-model="breadcrumbStyleType"
:disabled="disableItem"
:items="typeItems"
>
{{ $t('preferences.breadcrumb-style') }}
{{ $t('preferences.breadcrumb.style') }}
</ToggleItem>
</template>

View File

@ -180,7 +180,7 @@ function handleReset() {
<VbenSheet
v-model:open="openPreferences"
:description="$t('preferences.subtitle')"
:title="$t('preferences.name')"
:title="$t('preferences.title')"
>
<template #trigger>
<Trigger />
@ -210,7 +210,7 @@ function handleReset() {
/>
</Block>
<Block :title="$t('preferences.animation.name')">
<Block :title="$t('preferences.animation.title')">
<Animation
v-model:transition-enable="transitionEnable"
v-model:transition-loading="transitionLoading"
@ -220,7 +220,7 @@ function handleReset() {
</Block>
</template>
<template #appearance>
<Block :title="$t('preferences.theme.name')">
<Block :title="$t('preferences.theme.title')">
<Theme
v-model="themeMode"
v-model:app-semi-dark-menu="appSemiDarkMenu"
@ -266,7 +266,7 @@ function handleReset() {
/>
</Block>
<Block :title="$t('preferences.header.name')">
<Block :title="$t('preferences.header.title')">
<Header
v-model:headerEnable="headerEnable"
v-model:headerMode="headerMode"
@ -284,7 +284,7 @@ function handleReset() {
/>
</Block>
<Block :title="$t('preferences.breadcrumb')">
<Block :title="$t('preferences.breadcrumb.title')">
<Breadcrumb
v-model:breadcrumb-enable="breadcrumbEnable"
v-model:breadcrumb-hide-only-one="breadcrumbHideOnlyOne"
@ -303,7 +303,7 @@ function handleReset() {
v-model:tabbar-show-icon="tabbarShowIcon"
/>
</Block>
<Block :title="$t('preferences.footer.name')">
<Block :title="$t('preferences.footer.title')">
<Footer
v-model:footer-enable="footerEnable"
v-model:footer-fixed="footerFixed"

View File

@ -11,7 +11,7 @@ defineOptions({
<template>
<VbenButton
:title="$t('preferences.name')"
:title="$t('preferences.title')"
class="bg-primary flex-col-center h-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
>
<IconSetting

View File

@ -175,7 +175,7 @@ if (enableShortcutKey.value) {
@click="handleOpenPreference"
>
<IcRoundSettingsSuggest class="mr-2 size-5" />
{{ $t('preferences.name') }}
{{ $t('preferences.title') }}
<DropdownMenuShortcut v-if="enablePreferencesShortcutKey">
{{ altView }} ,
</DropdownMenuShortcut>

View File

@ -6,6 +6,6 @@ const LOGIN_PATH = '/auth/login';
/**
* @zh_CN
*/
const DEFAULT_HOME_PATH = '/welcome';
const DEFAULT_HOME_PATH = '/analytics';
export { DEFAULT_HOME_PATH, LOGIN_PATH };

View File

@ -38,6 +38,6 @@
}
},
"dependencies": {
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 419.23 419.23"><defs><style>.svg-bell-cls-1{fill:#fbc907;}.svg-bell-cls-2{fill:#f3a70f;}.svg-bell-cls-3{fill:#426572;}.svg-bell-cls-4,.svg-bell-cls-9{fill:#fff;}.svg-bell-cls-5{fill:#e8e8e8;}.svg-bell-cls-6{fill:#dadada;}.svg-bell-cls-7{opacity:0.1;}.svg-bell-cls-8{fill:#55e0ff;}.svg-bell-cls-9{opacity:0.4;}</style></defs><title>Asset 510</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><circle class="svg-bell-cls-1" cx="210.66" cy="209.62" r="203.61"/><path class="svg-bell-cls-2" d="M27.21,209.62A203.61,203.61,0,0,1,220.72,6.26q-5-.25-10.08-.25C98.19,4.86,6.11,95.09,5,207.54S94.05,412.07,206.5,413.21q2.07,0,4.13,0,5.06,0,10.08-.25A203.61,203.61,0,0,1,27.21,209.62Z"/><path class="svg-bell-cls-3" d="M209.61,419.23C94,419.23,0,325.19,0,209.61S94,0,209.61,0,419.23,94,419.23,209.61,325.19,419.23,209.61,419.23Zm0-407.23C100.65,12,12,100.65,12,209.61s88.65,197.61,197.61,197.61,197.61-88.65,197.61-197.61S318.58,12,209.61,12Z"/><path class="svg-bell-cls-4" d="M111.69,60.1a195,195,0,0,1,41.08-21.2c3.59-1.34,2-7.14-1.6-5.79a201.47,201.47,0,0,0-42.51,21.8c-3.18,2.15-.18,7.35,3,5.18Z"/><path class="svg-bell-cls-4" d="M35.09,160.61c3.09-10.2,8-20,13.05-29.32A212.37,212.37,0,0,1,95.87,72.18c2.93-2.52-1.33-6.75-4.24-4.24A217.08,217.08,0,0,0,43,128.26C37.63,138,32.54,148.34,29.31,159c-1.12,3.7,4.67,5.29,5.79,1.6Z"/><circle class="svg-bell-cls-5" cx="211.45" cy="212.12" r="156.89"/><path class="svg-bell-cls-6" d="M67.05,232.07a156.89,156.89,0,0,1,283.33-92.82A156.91,156.91,0,1,0,85,304.92,156.19,156.19,0,0,1,67.05,232.07Z"/><path class="svg-bell-cls-5" d="M211.32,152.25h0a9.16,9.16,0,0,1,9.16,9.16V210.5a9.16,9.16,0,0,1-9.16,9.16h0a9.16,9.16,0,0,1-9.16-9.16V161.41A9.16,9.16,0,0,1,211.32,152.25Z"/><circle class="svg-bell-cls-5" cx="211.14" cy="221.32" r="15.94"/><path class="svg-bell-cls-3" d="M210.48,92.62c6.29,0,6.29-9.77,0-9.77S204.19,92.62,210.48,92.62Z"/><path class="svg-bell-cls-3" d="M210.48,343.89c6.29,0,6.29-9.77,0-9.77S204.19,343.89,210.48,343.89Z"/><path class="svg-bell-cls-3" d="M339.84,218.25c6.29,0,6.29-9.77,0-9.77S333.55,218.25,339.84,218.25Z"/><path class="svg-bell-cls-3" d="M81.13,218.25c6.29,0,6.29-9.77,0-9.77S74.84,218.25,81.13,218.25Z"/><path class="svg-bell-cls-3" d="M205.56,153.32h0a9.16,9.16,0,0,1,9.16,9.16v49.09a9.16,9.16,0,0,1-9.16,9.16h0a9.16,9.16,0,0,1-9.16-9.16V162.49A9.16,9.16,0,0,1,205.56,153.32Z"/><circle class="cls-3" cx="205.38" cy="221.15" r="15.94"/><path class="cls-3" d="M135.78,272.58l135.16-89.89L290.11,170c5.22-3.46.33-11.94-4.92-8.44L150,251.4l-19.17,12.74C125.64,267.6,130.52,276.08,135.78,272.58Z"/><g class="svg-bell-cls-7"><ellipse class="svg-bell-cls-8" cx="210.2" cy="211.21" rx="156.89" ry="154.23"/></g><path class="svg-bell-cls-9" d="M243.13,60.17,84.37,301.88a162.18,162.18,0,0,1-18.58-47.29L193.5,60.21a153.88,153.88,0,0,1,49.67,0Z"/><path class="svg-bell-cls-9" d="M289.69,72.6,115.93,325.78a155.09,155.09,0,0,1-14.77-15L270,64.76A155.38,155.38,0,0,1,289.69,72.6Z"/><path class="svg-bell-cls-9" d="M362.16,171.75h0L232.51,360.68h0a160.93,160.93,0,0,1-42.54.43L346.63,132.84A151.63,151.63,0,0,1,362.16,171.75Z"/><path class="cls-3" d="M210.12,369.75c-89.82,0-162.89-71.88-162.89-160.23S120.31,49.29,210.12,49.29,373,121.17,373,209.52,299.94,369.75,210.12,369.75Zm0-308.46c-83.2,0-150.89,66.5-150.89,148.23s67.69,148.23,150.89,148.23S361,291.25,361,209.52,293.32,61.29,210.12,61.29Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 392.49 390.69"><defs><style>.svg-cake-cls-1{fill:#fff;}.svg-cake-cls-2{fill:#f3aa9f;}.svg-cake-cls-3{fill:#e1978f;}.svg-cake-cls-4,.svg-cake-cls-6{fill:#426572;}.svg-cake-cls-5{fill:#e1d2d5;}.svg-cake-cls-6{font-size:100.43px;font-family:Dosis-ExtraBold, Dosis;font-weight:700;}</style></defs><title>Asset 480</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="svg-cake-cls-1" d="M383.9,162H199.69V2.19q4-.19,8.16-.19A176.87,176.87,0,0,1,383.9,162Z"/><path class="svg-cake-cls-2" d="M355.38,210a176.83,176.83,0,0,1-95.72,157.18l-.15.07A176.88,176.88,0,1,1,101.72,50.67l.15-.07a175.93,175.93,0,0,1,72.82-17.4V191H354.37A177.9,177.9,0,0,1,355.38,210Z"/><path class="svg-cake-cls-3" d="M357.53,212.16a176,176,0,0,1-17.44,76.66,1,1,0,0,1-.07.15A176.89,176.89,0,0,1,73.47,352.79l1.23.38q6,1.86,12.26,3.29A177,177,0,0,0,303.49,191h52.78A178.15,178.15,0,0,1,357.53,212.16Z"/><path class="svg-cake-cls-4" d="M182.85,390.69a182.87,182.87,0,0,1-84-345.31l.41-.2a180.59,180.59,0,0,1,75.13-20l6.27-.28V185H364.36l.51,5.44c.54,5.77.82,11.62.82,17.4a180.72,180.72,0,0,1-20.18,83.56c-.06.12-.12.26-.2.41a184.39,184.39,0,0,1-83,80.77l-.18.08,0,0A181.06,181.06,0,0,1,182.85,390.69ZM104.33,56.08A170.88,170.88,0,0,0,256.9,361.85l.17-.08,0,0a172.34,172.34,0,0,0,77.5-75.38l.15-.29a168.84,168.84,0,0,0,18.93-78.23c0-3.6-.11-7.23-.34-10.84H168.69V37.58a168.41,168.41,0,0,0-64.07,18.35Z"/><path class="svg-cake-cls-5" d="M382.9,158H309.11c-2.89-46.4-18.43-98.49-36.89-144.29l1.33.51a177.49,177.49,0,0,1,92.51,83.56A175.63,175.63,0,0,1,382.9,158Z"/><path class="svg-cake-cls-4" d="M392.49,172H195.69V.47L201.4.2C204.11.07,207,0,209.85,0a182.87,182.87,0,0,1,182,165.44Zm-184.8-12H379.18A170.89,170.89,0,0,0,209.85,12h-2.16Z"/><text class="svg-cake-cls-6" transform="translate(232.67 133.93)">%</text><path class="svg-cake-cls-1" d="M101.22,81.14a166.34,166.34,0,0,1,34.83-18c3.58-1.34,2-7.14-1.6-5.79A172.89,172.89,0,0,0,98.19,76c-3.18,2.15-.18,7.35,3,5.18Z"/><path class="svg-cake-cls-1" d="M36.28,166.34c2.62-8.63,6.74-16.94,11.05-24.83A180.58,180.58,0,0,1,87.86,91.34c2.93-2.52-1.33-6.75-4.24-4.24-23.3,20.06-44.07,47.84-53.12,77.65-1.12,3.7,4.67,5.29,5.79,1.6Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 445 271.8"><defs><style>.svg-card-cls-1{fill:#32caf8;}.svg-card-cls-2{fill:#00aaf8;opacity:0.5;}.svg-card-cls-3{fill:#fff;}.svg-card-cls-4{fill:#426572;}</style></defs><g><g><rect class="svg-card-cls-1" x="6" y="8.17" width="433" height="259.8" rx="12" ry="12"/><path class="svg-card-cls-2" d="M439,21.16V255a13,13,0,0,1-13,13H28.72l381-259.8H426A13,13,0,0,1,439,21.16Z"/><path class="svg-card-cls-3" d="M328,33.24h88.92c3.86,0,3.87-6,0-6H328c-3.86,0-3.87,6,0,6Z"/><path class="svg-card-cls-3" d="M283.49,33.24H312.6c3.86,0,3.87-6,0-6H283.49c-3.86,0-3.87,6,0,6Z"/><path class="svg-card-cls-4" d="M427,271.8H18a18,18,0,0,1-18-18V18A18,18,0,0,1,18,0H427a18,18,0,0,1,18,18V253.8A18,18,0,0,1,427,271.8ZM18,12a6,6,0,0,0-6,6V253.8a6,6,0,0,0,6,6H427a6,6,0,0,0,6-6V18a6,6,0,0,0-6-6Z"/><rect class="svg-card-cls-4" x="37.89" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="55.93" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="73.97" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="92.01" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="118.71" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="136.76" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="154.8" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="172.84" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="199.54" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="217.58" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="235.63" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="253.67" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="280.37" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="298.41" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="316.45" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="334.49" y="125.08" width="12" height="20.57"/><rect class="svg-card-cls-4" x="43.89" y="177.53" width="161.29" height="12"/><rect class="svg-card-cls-4" x="43.89" y="204.59" width="68.2" height="12"/><circle class="svg-card-cls-3" cx="379.46" cy="207.35" r="23.82"/><rect class="svg-card-cls-3" x="43.89" y="36.31" width="72.53" height="47.63" rx="12" ry="12"/><path class="svg-card-cls-4" d="M104.42,88.86H55.89a18,18,0,0,1-18-18V47.23a18,18,0,0,1,18-18h48.53a18,18,0,0,1,18,18V70.86A18,18,0,0,1,104.42,88.86ZM55.89,41.23a6,6,0,0,0-6,6V70.86a6,6,0,0,0,6,6h48.53a6,6,0,0,0,6-6V47.23a6,6,0,0,0-6-6Z"/><path class="svg-card-cls-4" d="M379.46,241.49a29.81,29.81,0,1,1,29.82-29.82A29.85,29.85,0,0,1,379.46,241.49Zm0-47.63a17.81,17.81,0,1,0,17.82,17.81A17.84,17.84,0,0,0,379.46,193.86Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356.99 419.8"><defs><style>.svg-download-cls-1{fill:#ffa546;}.svg-download-cls-2{fill:#ff6059;opacity:0.4;}.svg-download-cls-3{fill:#426572;}.cls-4{fill:#ffd947;}</style></defs><g><g><path class="svg-download-cls-1" d="M351,380.73v17.59a15.52,15.52,0,0,1-15.47,15.48H21.46A15.52,15.52,0,0,1,6,398.32V380.73a15.51,15.51,0,0,1,15.47-15.47H335.52A15.51,15.51,0,0,1,351,380.73Z"/><path class="svg-download-cls-2" d="M351,406.85c0,3.95-7,7.19-15.47,7.19H21.46C13,414,6,410.8,6,406.85V380.73a15.51,15.51,0,0,1,15.47-15.47H37.66l3.44,25.27c0,4,7,7.2,15.47,7.2l283.72,12.44,7.38-2.28Z"/><path class="svg-download-cls-3" d="M335.52,419.8H21.46A21.5,21.5,0,0,1,0,398.32V380.73a21.49,21.49,0,0,1,21.46-21.47H335.52A21.49,21.49,0,0,1,357,380.73v17.59a21.52,21.52,0,0,1-21.46,21.48ZM21.46,371.26A9.48,9.48,0,0,0,12,380.73v17.59a9.48,9.48,0,0,0,9.46,9.48H335.52a9.52,9.52,0,0,0,9.46-9.48V380.73a9.48,9.48,0,0,0-9.46-9.47Z"/><path class="svg-download-cls-1" d="M247.93,138H233.23V41.7A35.7,35.7,0,0,0,197.53,6H159.45a35.7,35.7,0,0,0-35.7,35.7V138H109.06C80,138,61.84,169.48,76.37,194.64l34.72,60.13,30,52c16.6,28.76,58.12,28.76,74.72,0l30-52,34.72-60.13C295.14,169.48,277,138,247.93,138Z"/><path class="svg-download-cls-2" d="M280.62,188l-34.73,60.13-30,52c-11.24,19.46-66.68,32.78-52.52,18.88,60.22-59.12,104.3-182.16,104.3-182.16A37.74,37.74,0,0,1,280.62,188Z"/><path class="cls-4" d="M192.3,6c-.22.23-.42.47-.63.72-38.92,45-18.36,116.49-42.85,170.71-10.14,22.45-29.18,41.51-52.15,49.48L78,194.64C63.52,169.48,81.67,138,110.72,138h14.7V41.7A35.7,35.7,0,0,1,161.12,6Z"/><path class="svg-download-cls-3" d="M178.49,334.39h0a48.64,48.64,0,0,1-42.56-24.57L71.17,197.64A43.75,43.75,0,0,1,109.06,132h8.69V41.7A41.74,41.74,0,0,1,159.45,0h38.09a41.75,41.75,0,0,1,41.7,41.7V132h8.69a43.75,43.75,0,0,1,37.89,65.62L221,309.82A48.64,48.64,0,0,1,178.49,334.39ZM109.06,144a31.75,31.75,0,0,0-27.49,47.62l64.76,112.17a37.14,37.14,0,0,0,64.33,0l64.76-112.17A31.75,31.75,0,0,0,247.92,144H227.23V41.7A29.73,29.73,0,0,0,197.53,12H159.45a29.73,29.73,0,0,0-29.7,29.7V144Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -9,5 +9,15 @@ if (!loaded) {
}
const SvgAvatarIcon = createIcon('svg:avatar');
const SvgDownloadIcon = createIcon('svg:download');
const SvgCardIcon = createIcon('svg:card');
const SvgBellIcon = createIcon('svg:bell');
const SvgCakeIcon = createIcon('svg:cake');
export { SvgAvatarIcon };
export {
SvgAvatarIcon,
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
};

View File

@ -37,14 +37,15 @@ async function loadSvgIcons() {
await Promise.all(
Object.entries(svgEagers).map((svg) => {
const [key, body] = svg as [string, string];
const [key, body] = svg as [string, { default: string } | string];
// ./icons/xxxx.svg => xxxxxx
const start = key.lastIndexOf('/') + 1;
const end = key.lastIndexOf('.');
const iconName = key.slice(start, end);
return addIcon(`svg-icon:${iconName}`, {
body,
return addIcon(`svg:${iconName}`, {
body: typeof body === 'object' ? body.default : body,
});
}),
);

View File

@ -48,7 +48,7 @@
"dependencies": {
"@intlify/core-base": "^9.13.1",
"@vben-core/typings": "workspace:*",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-i18n": "^9.13.1"
}
}

View File

@ -98,15 +98,19 @@ page:
code-login: Code Login
qrcode-login: Qrcode Login
forget-password: Forget Password
dashboard:
title: Dashboard
analytics: Analytics
workspace: Workspace
vben:
about: About
document: Document
outside:
page: External Page
title: External Page
embedded: embedded Page
external-link: External Link
nested:
page: Nested Menu
title: Nested Menu
menu1: Menu 1
menu2: Menu 2
menu21: Menu 2-1
@ -115,10 +119,10 @@ page:
menu32: Menu 3-2
menu321: Menu 3-2-1
fallback:
page: Exception Page
title: Exception Page
preferences:
name: Preferences
title: Preferences
subtitle: Customize Preferences & Preview in Real Time
reset-tip: The data has changed, click to reset
ai-assistant: Ai Assistant
@ -147,7 +151,6 @@ preferences:
full-content-tip: Display only the main content, no menus
weak-mode: Color Weak Mode
gray-mode: Gray Mode
language: Language
dynamic-title: Dynamic Title
normal: Normal
@ -161,14 +164,6 @@ preferences:
navigation-accordion: Sidebar Navigation Menu Accordion mode
navigation-split-tip: When enabled, the sidebar shows the top bar's submenu
interface-control: Interface Layout Control
breadcrumb: Breadcrumb
breadcrumb-home: Display the home button
breadcrumb-enable: Enable Breadcrumb
breadcrumb-icon: Display breadcrumb icon
breadcrumb-background: background
breadcrumb-style: Breadcrumb Type
breadcrumb-hide-only-one: Hidden when only one left
copy: Copy Preferences
copy-success: Copy successful. Please replace in `src/preferences.ts` of the app
reset-success: Preferences reset successfully
@ -179,13 +174,21 @@ preferences:
tabs-icon: Display Tabbar Icon
mode: Mode
logo-visible: Display Logo
breadcrumb:
title: Breadcrumb
home: Display the home button
enable: Enable Breadcrumb
icon: Display breadcrumb icon
background: background
style: Breadcrumb Type
hide-only-one: Hidden when only one left
animation:
name: Animation
title: Animation
loading: Page transition loading
transition: Page transition animation
progress: Page transition progress
theme:
name: Theme
title: Theme
builtin: Built-in
radius: Radius
default: Default
@ -204,14 +207,14 @@ preferences:
gray: Gray
custom: Custom
header:
name: Header
title: Header
visible: Display Header
mode-static: Static
mode-fixed: Fixed
mode-auto: Auto hide/display
mode-auto-scroll: Scroll hide/display
footer:
name: Footer
title: Footer
visible: Fixed at the bottom
fixed: Display Footer
shortcut-keys:

View File

@ -97,15 +97,19 @@ page:
code-login: 验证码登陆
qrcode-login: 二维码登陆
forget-password: 忘记密码
dashboard:
title: 概览
analytics: 分析页
workspace: 工作台
vben:
about: 关于
document: 文档
outside:
page: 外部页面
title: 外部页面
embedded: 内嵌
external-link: 外链
nested:
page: 嵌套菜单
title: 嵌套菜单
menu1: 菜单 1
menu2: 菜单 2
menu21: 菜单 2-1
@ -114,10 +118,10 @@ page:
menu32: 菜单 3-2
menu321: 菜单 3-2-1
fallback:
page: 异常页面
title: 异常页面
preferences:
name: 偏好设置
title: 偏好设置
subtitle: 自定义偏好设置 & 实时预览
reset-tip: 数据有变化,点击可进行重置
appearance: 外观
@ -150,7 +154,6 @@ preferences:
follow-system: 跟随系统
weak-mode: 色弱模式
gray-mode: 灰色模式
navigation-menu: 导航菜单
navigation-style: 导航菜单风格
navigation-accordion: 侧边导航菜单手风琴模式
@ -160,13 +163,6 @@ preferences:
normal: 默认
plain: 朴素
rounded: 圆润
breadcrumb: 面包屑导航
breadcrumb-enable: 开启面包屑导航
breadcrumb-icon: 显示面包屑图标
breadcrumb-home: 显示首页按钮
breadcrumb-style: 面包屑风格
breadcrumb-hide-only-one: 只有一个时隐藏
breadcrumb-background: 背景
copy: 复制偏好设置
copy-success: 拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖
reset-success: 重置偏好设置成功
@ -177,13 +173,21 @@ preferences:
tabs-icon: 显示标签栏图标
mode: 模式
logo-visible: 显示 Logo
breadcrumb:
title: 面包屑导航
enable: 开启面包屑导航
icon: 显示面包屑图标
home: 显示首页按钮
style: 面包屑风格
hide-only-one: 只有一个时隐藏
background: 背景
animation:
name: 动画
title: 动画
loading: 页面切换 Loading
transition: 页面切换动画
progress: 页面切换进度条
theme:
name: 主题
title: 主题
builtin: 内置主题
radius: 圆角
default: 默认
@ -202,14 +206,14 @@ preferences:
gray: 中灰色
custom: 自定义
header:
name: 顶栏
title: 顶栏
mode-static: 静止
mode-fixed: 固定
mode-auto: 自动隐藏和显示
mode-auto-scroll: 滚动隐藏和显示
visible: 显示顶栏
footer:
name: 底栏
title: 底栏
visible: 显示底栏
fixed: 固定在底部
shortcut-keys:

View File

@ -43,7 +43,7 @@
},
"dependencies": {
"@vben-core/typings": "workspace:*",
"vue": "^3.4.30",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
}
}

View File

@ -3,4 +3,9 @@ interface SelectListItem {
value: string;
}
export type { SelectListItem };
interface TabsItem {
label: string;
value: string;
}
export type { SelectListItem, TabsItem };

View File

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

View File

@ -9,6 +9,6 @@
},
"devDependencies": {
"vitepress": "^1.2.3",
"vue": "^3.4.30"
"vue": "^3.4.31"
}
}