feat: add dashboard page

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

View File

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