Ver código fonte

dashboard (#6)

* feat(dashboard): 增加dashboard示例

* feat(chart-ui): 增加图表UI组件库

* feat(dashboard): 完善dashboard示例
jinmao88 9 meses atrás
pai
commit
6f0c05dd50

+ 1 - 0
apps/web-antd/package.json

@@ -37,6 +37,7 @@
     "@vben/styles": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/universal-ui": "workspace:*",
+    "@vben/chart-ui": "workspace:*",
     "@vben/utils": "workspace:*",
     "@vueuse/core": "^10.11.0",
     "ant-design-vue": "^4.2.3",

+ 243 - 1
apps/web-antd/src/views/dashboard/index.vue

@@ -1,7 +1,249 @@
 <script lang="ts" setup>
+import { ref } from 'vue';
+
 defineOptions({ name: 'WelCome' });
+import { Dashboard } from '@vben/universal-ui';
+import { echartsInstance as echarts } from '@vben/chart-ui';
+const cardList = ref([
+  {
+    title: '访问数',
+    extra: '月',
+    leftContent: '2000',
+    rightContent: 'flat-color-icons:conference-call',
+    leftFooter: '总访问数',
+    color: 'green',
+    rightFooter: '5000',
+  },
+  {
+    title: '销售额',
+    extra: '日',
+    leftContent: '$1350',
+    rightContent: 'flat-color-icons:sales-performance',
+    leftFooter: '总销售额',
+    color: 'red',
+    rightFooter: '$550000',
+  },
+]);
+const chartTabs = ref([
+  {
+    name: '1',
+    title: '流量趋势',
+    option: {
+      color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
+
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          // label: {
+          //   backgroundColor: '#6a7985',
+          // },
+        },
+      },
+      legend: {
+        data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'],
+      },
+      toolbox: {
+        feature: {
+          saveAsImage: {},
+        },
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true,
+      },
+      xAxis: [
+        {
+          type: 'category',
+          boundaryGap: false,
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+        },
+      ],
+      yAxis: [
+        {
+          type: 'value',
+        },
+      ],
+      series: [
+        {
+          name: 'Line 1',
+          type: 'line',
+          stack: 'Total',
+          smooth: true,
+          lineStyle: {
+            width: 0,
+          },
+          showSymbol: false,
+          areaStyle: {
+            opacity: 0.8,
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: 'rgb(128, 255, 165)',
+              },
+              {
+                offset: 1,
+                color: 'rgb(1, 191, 236)',
+              },
+            ]),
+          },
+          emphasis: {
+            focus: 'series',
+          },
+          data: [140, 232, 101, 264, 90, 340, 250],
+        },
+        {
+          name: 'Line 2',
+          type: 'line',
+          stack: 'Total',
+          smooth: true,
+          lineStyle: {
+            width: 0,
+          },
+          showSymbol: false,
+          areaStyle: {
+            opacity: 0.8,
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: 'rgb(0, 221, 255)',
+              },
+              {
+                offset: 1,
+                color: 'rgb(77, 119, 255)',
+              },
+            ]),
+          },
+          emphasis: {
+            focus: 'series',
+          },
+          data: [120, 282, 111, 234, 220, 340, 310],
+        },
+        {
+          name: 'Line 3',
+          type: 'line',
+          stack: 'Total',
+          smooth: true,
+          lineStyle: {
+            width: 0,
+          },
+          showSymbol: false,
+          areaStyle: {
+            opacity: 0.8,
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: 'rgb(55, 162, 255)',
+              },
+              {
+                offset: 1,
+                color: 'rgb(116, 21, 219)',
+              },
+            ]),
+          },
+          emphasis: {
+            focus: 'series',
+          },
+          data: [320, 132, 201, 334, 190, 130, 220],
+        },
+        {
+          name: 'Line 4',
+          type: 'line',
+          stack: 'Total',
+          smooth: true,
+          lineStyle: {
+            width: 0,
+          },
+          showSymbol: false,
+          areaStyle: {
+            opacity: 0.8,
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: 'rgb(255, 0, 135)',
+              },
+              {
+                offset: 1,
+                color: 'rgb(135, 0, 157)',
+              },
+            ]),
+          },
+          emphasis: {
+            focus: 'series',
+          },
+          data: [220, 402, 231, 134, 190, 230, 120],
+        },
+        {
+          name: 'Line 5',
+          type: 'line',
+          stack: 'Total',
+          smooth: true,
+          lineStyle: {
+            width: 0,
+          },
+          showSymbol: false,
+          label: {
+            show: true,
+            position: 'top',
+          },
+          areaStyle: {
+            opacity: 0.8,
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: 'rgb(255, 191, 0)',
+              },
+              {
+                offset: 1,
+                color: 'rgb(224, 62, 76)',
+              },
+            ]),
+          },
+          emphasis: {
+            focus: 'series',
+          },
+          data: [220, 302, 181, 234, 210, 290, 150],
+        },
+      ],
+    },
+  },
+  {
+    name: '2',
+    title: '访问量',
+    option: {
+      xAxis: {
+        type: 'category',
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: [
+        {
+          data: [
+            120,
+            {
+              value: 200,
+              itemStyle: {
+                color: '#a90000',
+              },
+            },
+            150,
+            80,
+            70,
+            110,
+            130,
+          ],
+          type: 'bar',
+        },
+      ],
+    },
+  },
+]);
 </script>
 
 <template>
-  <div>dashboard</div>
+  <Dashboard :cardList="cardList" :chartTabs="chartTabs"></Dashboard>
 </template>

+ 50 - 0
packages/business/chart-ui/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "@vben/chart-ui",
+  "version": "5.0.0",
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "packages/business/chart-ui"
+  },
+  "license": "MIT",
+  "type": "module",
+  "scripts": {
+    "build": "pnpm vite build",
+    "prepublishOnly": "npm run build"
+  },
+  "files": [
+    "dist"
+  ],
+  "sideEffects": [
+    "**/*.css"
+  ],
+  "main": "./dist/index.mjs",
+  "module": "./dist/index.mjs",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "development": "./src/index.ts",
+      "default": "./dist/index.mjs"
+    }
+  },
+  "publishConfig": {
+    "exports": {
+      ".": {
+        "default": "./dist/index.mjs"
+      }
+    }
+  },
+  "peerDependencies": {
+    "@vben-core/design": "workspace:*"
+  },
+  "dependencies": {
+    "@vben-core/preferences": "workspace:*",
+    "echarts": "^5.5.0",
+    "vue": "^3.4.29"
+  },
+  "devDependencies": {
+    "@vben/types": "workspace:*"
+  }
+}

+ 37 - 0
packages/business/chart-ui/src/chart.vue

@@ -0,0 +1,37 @@
+<script setup lang="ts">
+import { echartsInstance, ECOption } from './index';
+import { onMounted, ref, unref, warn } from 'vue';
+import { usePreferences } from '@vben-core/preferences';
+const { isDark } = usePreferences();
+interface Props {
+  height?: string;
+  width?: string;
+}
+withDefaults(defineProps<Props>(), {
+  height: '500px',
+  width: '100%',
+});
+
+const instance = ref();
+const instanceRef = ref(HTMLElement);
+onMounted(() => {
+  instance.value = echartsInstance.init(
+    instanceRef.value,
+    isDark.value ? 'dark' : '',
+  );
+});
+const setChart = (option: ECOption, clear: boolean = true) => {
+  const c = unref(instance);
+  if (!c) {
+    warn('instance is null');
+    return;
+  }
+  if (clear) c.clear();
+  c.setOption(option);
+};
+defineExpose({ setChart });
+</script>
+
+<template>
+  <div ref="instanceRef" :style="{ height, width }"></div>
+</template>

+ 59 - 0
packages/business/chart-ui/src/index.ts

@@ -0,0 +1,59 @@
+import * as echarts from 'echarts/core';
+import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+
+  // 数据集组件
+  DatasetComponent,
+  // 内置数据转换器组件 (filter, sort)
+  TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+} from 'echarts/components';
+import { LabelLayout, UniversalTransition } from 'echarts/features';
+import { CanvasRenderer } from 'echarts/renderers';
+import type {
+  // 系列类型的定义后缀都为 SeriesOption
+  BarSeriesOption,
+  LineSeriesOption,
+} from 'echarts/charts';
+import type {
+  // 组件类型的定义后缀都为 ComponentOption
+  TitleComponentOption,
+  TooltipComponentOption,
+  GridComponentOption,
+  DatasetComponentOption,
+} from 'echarts/components';
+import type { ComposeOption } from 'echarts/core';
+
+// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
+export type ECOption = ComposeOption<
+  | BarSeriesOption
+  | LineSeriesOption
+  | TitleComponentOption
+  | TooltipComponentOption
+  | GridComponentOption
+  | DatasetComponentOption
+>;
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  PieChart,
+  RadarChart,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+  BarChart,
+  LineChart,
+  LabelLayout,
+  UniversalTransition,
+  CanvasRenderer,
+  LegendComponent,
+  ToolboxComponent,
+]);
+export const echartsInstance = echarts;
+export { default as chart } from './chart.vue';

+ 6 - 0
packages/business/chart-ui/tsconfig.json

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

+ 3 - 0
packages/business/chart-ui/vite.config.mts

@@ -0,0 +1,3 @@
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig();

+ 1 - 0
packages/business/universal-ui/package.json

@@ -48,6 +48,7 @@
     "@vben/locales": "workspace:*",
     "@vueuse/core": "^10.11.0",
     "@vueuse/integrations": "^10.11.0",
+    "@vben/chart-ui": "workspace:*",
     "qrcode": "^1.5.3",
     "vue": "^3.4.30",
     "vue-router": "^4.4.0"

+ 45 - 0
packages/business/universal-ui/src/dashboard/card.vue

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

+ 24 - 0
packages/business/universal-ui/src/dashboard/chartCard.vue

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

+ 41 - 0
packages/business/universal-ui/src/dashboard/chartTab.vue

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

+ 152 - 0
packages/business/universal-ui/src/dashboard/dashboard.vue

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

+ 3 - 0
packages/business/universal-ui/src/dashboard/index.ts

@@ -0,0 +1,3 @@
+export { default as DashboardLayout } from './layout.vue';
+export { default as Dashboard } from './dashboard.vue';
+export { default as chartCard } from './chartCard.vue';

+ 7 - 0
packages/business/universal-ui/src/dashboard/layout.vue

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

+ 17 - 0
packages/business/universal-ui/src/dashboard/typings.ts

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

+ 1 - 0
packages/business/universal-ui/src/index.ts

@@ -8,3 +8,4 @@ export * from './notification';
 export * from './preferences';
 export * from './theme-toggle';
 export * from './user-dropdown';
+export * from './dashboard';