• 简体中文
  • dashboard

    retail-operations-dashboard

    参考运营监控大屏,组合 10 张卡片:真实查询生成的经营摘要、品类与客群占比、销售利润趋势、省份排行、配送分布、客户明细和行动建议。宽屏首行四卡,文档内容区双列,手机单列。

    标签: dashboard multi-column responsive operations

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            // 所有图表与洞察注册在同一个 VBI 实例中,数据来自 supermarket 演示连接器。
            const createChart = (chartType: string) => {
              const chart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
              chart.chartType.changeChartType(chartType)
              return chart
            }
    
            const totals = createChart('table')
            totals.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
            totals.measures.add('profit', node => node.setAlias('利润').setAggregate({ func: 'sum' }))
            totals.measures.add('order_id', node => node.setAlias('订单数').setAggregate({ func: 'countDistinct' }))
            totals.measures.add('customer_id', node => node.setAlias('客户数').setAggregate({ func: 'countDistinct' }))
            totals.measures.add('amount', node => node.setAlias('销售件数').setAggregate({ func: 'sum' }))
    
            const salesSummary = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            const customerSummary = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            const actions = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            actions.setContent('01  区域经营:结合销售排行与利润表现,优先跟进高贡献区域。\n\n02  客户维护:对高销售客户检查利润质量,制定分层回访计划。\n\n03  履约复盘:按配送方式观察订单分布,结合服务成本调整资源。')
    
            const categoryShare = createChart('donut')
            categoryShare.dimensions.add('product_type', node => node.setAlias('品类'))
            categoryShare.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const customerShare = createChart('donut')
            customerShare.dimensions.add('customer_type', node => node.setAlias('客群'))
            customerShare.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const monthlyTrend = createChart('dualAxis')
            monthlyTrend.dimensions.add('order_date', node => node.setAlias('月份').setAggregate({ func: 'toMonth' }).setSort({ order: 'asc' }))
            monthlyTrend.measures.add('sales', node => node.setAlias('销售额').setEncoding('primaryYAxis').setAggregate({ func: 'sum' }))
            monthlyTrend.measures.add('profit', node => node.setAlias('利润').setEncoding('secondaryYAxis').setAggregate({ func: 'sum' }))
            monthlyTrend.limit.setLimit(12)
    
            const provinceRanking = createChart('bar')
            provinceRanking.dimensions.add('province', node => node.setAlias('省份'))
            provinceRanking.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }).setSort({ order: 'desc' }))
            provinceRanking.limit.setLimit(6)
    
            const categoryProfit = createChart('column')
            categoryProfit.dimensions.add('product_type', node => node.setAlias('品类'))
            categoryProfit.measures.add('profit', node => node.setAlias('利润').setAggregate({ func: 'sum' }))
    
            const deliveryOrders = createChart('column')
            deliveryOrders.dimensions.add('delivery_method', node => node.setAlias('配送方式'))
            deliveryOrders.measures.add('order_id', node => node.setAlias('订单数').setAggregate({ func: 'countDistinct' }))
    
            const topCustomers = createChart('table')
            topCustomers.dimensions.add('customer_name', node => node.setAlias('客户'))
            topCustomers.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }).setSort({ order: 'desc' }).setFormat({ autoFormat: true }))
            topCustomers.measures.add('profit', node => node.setAlias('利润').setAggregate({ func: 'sum' }).setFormat({ autoFormat: true }))
            topCustomers.limit.setLimit(6)
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              // 断点基于仪表盘内容宽度;400px 起让文档预览也能并排展示卡片。
              breakpoints: { xxl: 1600, xl: 1200, lg: 960, md: 640, sm: 400, xs: 0 },
              meta: {
                title: '零售运营全景',
                description: 'RETAIL OPERATIONS / 演示数据全量汇总 · 趋势展示最早 12 个月',
              },
            })
    
            // 摘要也来自真实查询,通过 Insight Builder 保存显示文本。
            const summary = await totals.buildVSeed()
            const readTotal = (field: string) => {
              const id = totals.measures.find(node => node.getField() === field)?.getId()
              return id ? Number(summary.dataset[0]?.[id] ?? 0) : 0
            }
            const sales = readTotal('sales')
            const profit = readTotal('profit')
            salesSummary.setContent([
              `销售额  ¥${(sales / 10000).toFixed(2)} 万`,
              `利润  ¥${(profit / 10000).toFixed(2)} 万`,
              `利润率  ${sales ? (profit / sales * 100).toFixed(1) : '0.0'}%`,
            ].join('\n\n'))
            customerSummary.setContent([
              `订单  ${readTotal('order_id').toFixed(0)} 单`,
              `客户  ${readTotal('customer_id').toFixed(0)} 位`,
              `销量  ${readTotal('amount').toFixed(0)} 件`,
            ].join('\n\n'))
    
            dashboardBuilder.insight.add(widget => {
              widget.setInsightId(salesSummary)
                .setTitle('经营快照')
                .setDescription('全量销售与利润')
                .setLayouts({
                  lg: { x: 0, y: 0, w: 3, h: 4 },
                  md: { x: 0, y: 0, w: 3, h: 3 },
                  sm: { x: 0, y: 0, w: 2, h: 3 },
                  xs: { x: 0, y: 0, w: 2, h: 3 },
                })
            })
    
            dashboardBuilder.insight.add(widget => {
              widget.setInsightId(customerSummary)
                .setTitle('客户与订单')
                .setDescription('去重客户与订单统计')
                .setLayouts({
                  lg: { x: 3, y: 0, w: 3, h: 4 },
                  md: { x: 3, y: 0, w: 3, h: 3 },
                  sm: { x: 2, y: 0, w: 2, h: 3 },
                  xs: { x: 0, y: 3, w: 2, h: 3 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(categoryShare)
                .setTitle('品类销售占比')
                .setLayouts({
                  lg: { x: 6, y: 0, w: 3, h: 4 },
                  md: { x: 0, y: 3, w: 3, h: 4 },
                  sm: { x: 0, y: 3, w: 4, h: 4 },
                  xs: { x: 0, y: 6, w: 2, h: 4 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(customerShare)
                .setTitle('客群销售占比')
                .setLayouts({
                  lg: { x: 9, y: 0, w: 3, h: 4 },
                  md: { x: 3, y: 3, w: 3, h: 4 },
                  sm: { x: 0, y: 7, w: 4, h: 4 },
                  xs: { x: 0, y: 10, w: 2, h: 4 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(monthlyTrend)
                .setTitle('销售与利润趋势')
                .setDescription('月度双轴对比 · 最早 12 个月')
                .setLayouts({
                  lg: { x: 0, y: 4, w: 8, h: 5 },
                  md: { x: 0, y: 7, w: 6, h: 5 },
                  sm: { x: 0, y: 11, w: 4, h: 5 },
                  xs: { x: 0, y: 14, w: 2, h: 5 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(provinceRanking)
                .setTitle('省份销售 TOP 6')
                .setDescription('按销售额降序排列')
                .setLayouts({
                  lg: { x: 8, y: 4, w: 4, h: 5 },
                  md: { x: 0, y: 12, w: 3, h: 5 },
                  sm: { x: 0, y: 16, w: 4, h: 5 },
                  xs: { x: 0, y: 19, w: 2, h: 5 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(categoryProfit)
                .setTitle('品类利润')
                .setLayouts({
                  lg: { x: 0, y: 9, w: 4, h: 5 },
                  md: { x: 3, y: 12, w: 3, h: 5 },
                  sm: { x: 0, y: 21, w: 4, h: 5 },
                  xs: { x: 0, y: 24, w: 2, h: 5 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(deliveryOrders)
                .setTitle('配送订单分布')
                .setLayouts({
                  lg: { x: 4, y: 9, w: 4, h: 5 },
                  md: { x: 0, y: 17, w: 3, h: 5 },
                  sm: { x: 0, y: 26, w: 4, h: 5 },
                  xs: { x: 0, y: 29, w: 2, h: 5 },
                })
            })
    
            dashboardBuilder.chart.add(widget => {
              widget.setChart(topCustomers)
                .setTitle('重点客户 TOP 6')
                .setDescription('销售额与利润明细')
                .setLayouts({
                  lg: { x: 8, y: 9, w: 4, h: 5 },
                  md: { x: 3, y: 17, w: 3, h: 5 },
                  sm: { x: 0, y: 31, w: 4, h: 5 },
                  xs: { x: 0, y: 34, w: 2, h: 5 },
                })
            })
    
            dashboardBuilder.insight.add(widget => {
              widget.setInsightId(actions)
                .setTitle('经营观察与行动')
                .setDescription('结合趋势、结构与明细制定下一步计划')
                .setLayouts({
                  lg: { x: 0, y: 14, w: 12, h: 3 },
                  md: { x: 0, y: 22, w: 6, h: 4 },
                  sm: { x: 0, y: 36, w: 4, h: 4 },
                  xs: { x: 0, y: 39, w: 2, h: 4 },
                })
            })
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    basic-dashboard

    通过 dashboard builder 组合 chart 与 insight 资源,生成基础仪表盘

    标签: basic

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const salesChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            salesChart.chartType.changeChartType('bar')
            salesChart.dimensions.add('province', dimension => dimension.setAlias('省份'))
            salesChart.measures.add('sales', measure => measure.setAlias('销售额').setEncoding('xAxis').setAggregate({ func: 'sum' }))
            salesChart.limit.setLimit(8)
    
            const salesInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            salesInsight.setContent("华东区域销售额领先,建议继续跟进重点客户。")
    
            const resources = {
              charts: { salesChart },
              insights: { salesInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '销售仪表盘' },
            })
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.salesChart)
                .setTitle('销售趋势')
                .setDescription('按省份汇总销售额,展示前 8 项')
                .setLayouts({
                  lg: { x: 0, y: 0, w: 8, h: 6 },
                  md: { x: 0, y: 0, w: 6, h: 5 }
                })
            })
    
            dashboardBuilder.insight.add(insight => {
              insight
                .setInsightId(resources.insights.salesInsight)
                .setTitle('关键洞察')
                .setLayouts({
                  lg: { x: 8, y: 0, w: 4, h: 6 },
                  md: { x: 0, y: 5, w: 6, h: 3 }
                })
            })
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    responsive-dashboard-layout

    组合多个 chart 与 insight 资源,并为不同断点设置响应式 dashboard 布局

    标签: layout responsive resources

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const salesChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            salesChart.chartType.changeChartType('bar')
            salesChart.dimensions.add('province', dimension => dimension.setAlias('省份'))
            salesChart.measures.add('sales', measure => measure.setAlias('销售额').setEncoding('xAxis').setAggregate({ func: 'sum' }))
            salesChart.limit.setLimit(8)
    
            const profitChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            profitChart.chartType.changeChartType('line')
            profitChart.dimensions.add('order_date', dimension => dimension.setAlias('订单月份').setAggregate({ func: 'toMonth' }))
            profitChart.measures.add('profit', measure => measure.setAlias('利润').setEncoding('yAxis').setAggregate({ func: 'sum' }))
            profitChart.limit.setLimit(8)
    
            const opsInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            opsInsight.setContent("销售额与利润趋势需要联合观察,低利润月份应进一步拆解品类结构。")
    
            const resources = {
              charts: { salesChart, profitChart },
              insights: { opsInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '经营看板' },
            })
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.salesChart)
                .setTitle('区域销售')
                .setDescription('按省份汇总销售额,展示前 8 项')
                .setLayouts({
                  lg: { x: 0, y: 0, w: 7, h: 5 },
                  md: { x: 0, y: 0, w: 6, h: 4 },
                  sm: { x: 0, y: 0, w: 4, h: 4 }
                })
            })
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.profitChart)
                .setTitle('利润趋势')
                .setDescription('按月份汇总利润,展示前 8 个月')
                .setLayouts({
                  lg: { x: 7, y: 0, w: 5, h: 5 },
                  md: { x: 0, y: 4, w: 6, h: 4 },
                  sm: { x: 0, y: 4, w: 4, h: 4 }
                })
            })
    
            dashboardBuilder.insight.add(insight => {
              insight
                .setInsightId(resources.insights.opsInsight)
                .setTitle('经营洞察')
                .setDescription('销售与利润联动说明')
                .setLayouts({
                  lg: { x: 0, y: 5, w: 12, h: 3 },
                  md: { x: 0, y: 8, w: 6, h: 3 },
                  sm: { x: 0, y: 8, w: 4, h: 3 }
                })
            })
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    update-and-remove-dashboard-widgets

    新增多个 dashboard widget 后更新布局并移除临时图表

    标签: layout mutation

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const retainedInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            retainedInsight.setContent("临时图表已移除,保留洞察用于记录仪表盘调整结果。")
    
            const resources = {
              charts: {  },
              insights: { retainedInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '组件更新与移除' },
            })
    
            dashboardBuilder.chart.add(chart => {
              chart.setTitle('临时图表').setLayouts({ lg: { x: 0, y: 0, w: 6, h: 4 } })
            })
            dashboardBuilder.insight.add(insight => {
              insight.setInsightId(resources.insights.retainedInsight).setTitle('保留洞察').setLayouts({ lg: { x: 6, y: 0, w: 6, h: 4 } })
            })
    
            const [chartWidget] = dashboardBuilder.chart.toJSON()
            const [insightWidget] = dashboardBuilder.insight.toJSON()
    
            dashboardBuilder.chart.update(chartWidget.id, chart => {
              chart
                .setDescription('更新后移除')
                .setLayouts({ lg: { x: 0, y: 1, w: 5, h: 3 } })
            })
            dashboardBuilder.insight.update(insightWidget.id, insight => {
              insight.setDescription('仪表盘保留的洞察说明')
            })
            dashboardBuilder.chart.remove(chartWidget.id)
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    light-dashboard-theme

    将浅色主题保存在 Dashboard 中,预览使用文档主题,不受网站外观影响。

    标签: theme

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const chartBuilder = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            chartBuilder.chartType.changeChartType('column')
            chartBuilder.dimensions.add('product_type', node => node.setAlias('商品品类'))
            chartBuilder.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const insightBuilder = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            insightBuilder.setContent('按品类比较销售贡献,结合利润质量制定下一步经营计划。')
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '浅色经营看板', description: '品类表现 / 销售贡献与经营观察' },
            })
            dashboardBuilder.theme.setTheme('light-default')
    
            dashboardBuilder.chart.add(widget => widget
              .setChart(chartBuilder)
              .setTitle('品类销售额')
              .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 5 }, md: { x: 0, y: 0, w: 6, h: 5 } }))
            dashboardBuilder.insight.add(widget => widget
              .setInsightId(insightBuilder)
              .setTitle('经营观察')
              .setLayouts({ lg: { x: 8, y: 0, w: 4, h: 5 }, md: { x: 0, y: 5, w: 6, h: 3 } }))
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    dark-dashboard-theme

    通过 dashboardBuilder.theme.setTheme 切换深色主题,卡片、图表和编辑器保持一致。

    标签: theme

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const chartBuilder = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            chartBuilder.chartType.changeChartType('column')
            chartBuilder.dimensions.add('product_type', node => node.setAlias('商品品类'))
            chartBuilder.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const insightBuilder = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            insightBuilder.setContent('按品类比较销售贡献,结合利润质量制定下一步经营计划。')
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '深色经营看板', description: '品类表现 / 销售贡献与经营观察' },
            })
            dashboardBuilder.theme.setTheme('dark-default')
    
            dashboardBuilder.chart.add(widget => widget
              .setChart(chartBuilder)
              .setTitle('品类销售额')
              .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 5 }, md: { x: 0, y: 0, w: 6, h: 5 } }))
            dashboardBuilder.insight.add(widget => widget
              .setInsightId(insightBuilder)
              .setTitle('经营观察')
              .setLayouts({ lg: { x: 8, y: 0, w: 4, h: 5 }, md: { x: 0, y: 5, w: 6, h: 3 } }))
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    brand-dashboard-theme

    通过 Dashboard Builder 直接配置品牌主题,统一图表色板、文字、背景和卡片样式;配置随文档保存、撤销和同步,无需组件注册。

    标签: theme

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const chartBuilder = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            chartBuilder.chartType.changeChartType('column')
            chartBuilder.dimensions.add('product_type', node => node.setAlias('商品品类'))
            chartBuilder.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const insightBuilder = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            insightBuilder.setContent('按品类比较销售贡献,结合利润质量制定下一步经营计划。')
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '翡翠绿经营看板', description: '品类表现 / 销售贡献与经营观察' },
            })
            dashboardBuilder.theme.setTheme('dark-retail-emerald', {
              label: '零售翡翠',
              tokens: {
                baseTheme: 'dark',
                fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
                colorScheme: ['#34d399', '#38bdf8', '#fbbf24', '#a78bfa'],
                linearColorScheme: ['#123b32', '#34d399'],
                textPrimary: '#edfdf5',
                textSecondary: '#a4c7ba',
                borderColor: '#315448',
                surfaceColor: '#102b22',
                surfaceBackgroundColor: '#091a14',
                accentColor: '#34d399',
                tooltipBackgroundColor: '#15382c',
              },
              dashboard: { widgetBorderRadius: 12, toolbarBackground: '#102b22', padding: 20, gap: 16 },
            })
    
            dashboardBuilder.chart.add(widget => widget
              .setChart(chartBuilder)
              .setTitle('品类销售额')
              .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 5 }, md: { x: 0, y: 0, w: 6, h: 5 } }))
            dashboardBuilder.insight.add(widget => widget
              .setInsightId(insightBuilder)
              .setTitle('经营观察')
              .setLayouts({ lg: { x: 8, y: 0, w: 4, h: 5 }, md: { x: 0, y: 5, w: 6, h: 3 } }))
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    pastel-dashboard-theme

    使用雾玫粉配色,并在主题菜单的浅色分组中切换海盐蓝、森雾青、薰衣紫、杏桃橘、竹月青、晴空蓝和雪松玫瑰,统一图表、卡片和编辑器的配色。

    标签: theme

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const chartBuilder = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            chartBuilder.chartType.changeChartType('column')
            chartBuilder.dimensions.add('product_type', node => node.setAlias('商品品类'))
            chartBuilder.measures.add('sales', node => node.setAlias('销售额').setAggregate({ func: 'sum' }))
    
            const insightBuilder = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            insightBuilder.setContent('按品类比较销售贡献,结合利润质量制定下一步经营计划。')
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '柔彩经营看板', description: '品类表现 / 销售贡献与经营观察' },
            })
            dashboardBuilder.theme.setTheme('light-misty-rose')
    
            dashboardBuilder.chart.add(widget => widget
              .setChart(chartBuilder)
              .setTitle('品类销售额')
              .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 5 }, md: { x: 0, y: 0, w: 6, h: 5 } }))
            dashboardBuilder.insight.add(widget => widget
              .setInsightId(insightBuilder)
              .setTitle('经营观察')
              .setLayouts({ lg: { x: 8, y: 0, w: 4, h: 5 }, md: { x: 0, y: 5, w: 6, h: 3 } }))
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    undo-redo-dashboard

    仪表盘组件、响应式布局与主题按事务一起撤销和重做,引用的洞察内容独立保存。

    标签: undoManager yjs

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const insightBuilder = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            insightBuilder.setContent('撤销删除操作后,组件和所有断点布局一起恢复。')
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '仪表盘撤销与重做' },
            })
            dashboardBuilder.transact(() => {
              dashboardBuilder.theme.setTheme('dark-default')
              dashboardBuilder.insight.add(widget => widget
                .setInsightId(insightBuilder)
                .setTitle('可恢复的经营洞察')
                .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 4 }, md: { x: 0, y: 0, w: 6, h: 4 } }))
            })
            if (!dashboardBuilder.undoManager.undo() || !dashboardBuilder.isEmpty()) {
              throw new Error('撤销必须同时还原组件与布局')
            }
            if (!dashboardBuilder.undoManager.redo()) throw new Error('重做失败')
            const [widget] = dashboardBuilder.insight.toJSON()
            dashboardBuilder.insight.remove(widget.id)
            dashboardBuilder.undoManager.undo()
    
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    executive-dashboard-widget-lifecycle

    经营看板先校验组件布局,再组合销售图表与经营洞察,并在调整布局后同步到协作副本

    标签: workflow dashboard collaboration

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const salesChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            salesChart.chartType.changeChartType('bar')
            salesChart.dimensions.add('province', dimension => dimension.setAlias('省份'))
            salesChart.measures.add('sales', measure => measure.setAlias('销售额').setEncoding('xAxis').setAggregate({ func: 'sum' }))
            salesChart.limit.setLimit(8)
    
            const salesInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            salesInsight.setContent("华东区域销售额领先,管理层需要关注复购与利润结构。")
    
            const resources = {
              charts: { salesChart },
              insights: { salesInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '经营驾驶舱' },
            })
    
            if (!dashboardBuilder.isEmpty()) {
              throw new Error('new dashboard should start without widgets')
            }
    
            let missingLayoutRejected = false
            try {
              dashboardBuilder.chart.add(chart => {
                chart.setTitle('缺少布局的草稿图表').setChart(resources.charts.salesChart)
              })
            } catch {
              missingLayoutRejected = true
            }
            if (!missingLayoutRejected || !dashboardBuilder.isEmpty()) {
              throw new Error('dashboard should reject chart widgets without lg layout')
            }
    
            dashboardBuilder
              .chart.add(chart => {
                chart
                  .setChart(resources.charts.salesChart)
                  .setTitle('省份销售额')
                  .setDescription('按省份汇总销售额,展示前 8 项')
                  .setLayouts({
                    lg: { x: 0, y: 0, w: 8, h: 6 },
                    md: { x: 0, y: 0, w: 6, h: 5 }
                  })
              })
              .insight.add(insight => {
                insight
                  .setInsightId(resources.insights.salesInsight)
                  .setTitle('经营洞察')
                  .setDescription('解释销售额集中区域')
                  .setLayouts({
                    lg: { x: 8, y: 0, w: 4, h: 6 },
                    md: { x: 0, y: 5, w: 6, h: 3 }
                  })
              })
    
            const chartWidget = dashboardBuilder.chart.findAll()[0]
            const insightWidget = dashboardBuilder.insight.findAll()[0]
            if (!chartWidget || !insightWidget) {
              throw new Error('dashboard should contain chart and insight widgets')
            }
            if (!chartWidget.getBuilder() || !insightWidget.getBuilder()) {
              throw new Error('dashboard widgets should resolve registered resources')
            }
            if (dashboardBuilder.chart.get(chartWidget.getId())?.toJSON().title !== '省份销售额') {
              throw new Error('chart widget should be findable by widget id')
            }
            if (!dashboardBuilder.chart.find(resources.charts.salesChart.getUUID())) {
              throw new Error('chart widget should be findable by chart resource id')
            }
            if (!dashboardBuilder.insight.find(resources.insights.salesInsight.getUUID())) {
              throw new Error('insight widget should be findable by insight resource id')
            }
    
            dashboardBuilder.chart.update(chartWidget.getId(), chart => {
              chart
                .setTitle('重点省份销售额')
                .setLayouts({ lg: { x: 0, y: 0, w: 7, h: 6 } })
            })
            dashboardBuilder.insight.update(insightWidget.getId(), insight => {
              insight.setDescription('更新为管理层复盘口径')
            })
    
            if (dashboardBuilder.chart.toJSON()[0].title !== '重点省份销售额') {
              throw new Error('chart widget title should update')
            }
            if (dashboardBuilder.insight.toJSON()[0].description !== '更新为管理层复盘口径') {
              throw new Error('insight widget description should update')
            }
    
            let missingInsightRejected = false
            try {
              dashboardBuilder.insight.update('missing-widget', insight => insight.setTitle('missing'))
            } catch {
              missingInsightRejected = true
            }
            if (!missingInsightRejected) {
              throw new Error('updating missing insight widget should fail')
            }
    
            dashboardBuilder.insight.remove(insightWidget.getId())
            dashboardBuilder.chart.remove('missing-widget')
            if (dashboardBuilder.insight.findAll().length !== 0 || dashboardBuilder.chart.findAll().length !== 1) {
              throw new Error('only the chart widget should remain')
            }
    
            const ReplicaVBI = createVBI()
            const replica = ReplicaVBI.dashboard.create(ReplicaVBI.dashboard.createEmpty('dashboard-replica'))
            replica.applyUpdate(dashboardBuilder.encodeStateAsUpdate(), 'dashboard-sync')
            replica.build()
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    merchandising-dashboard-widget-lifecycle

    商品运营仪表盘中新增、更新、移除并清空 chart/insight widget,随后重建最终布局以验证布局合并

    标签: dashboard workflow layout mutation

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const categorySalesChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            categorySalesChart.chartType.changeChartType('bar')
            categorySalesChart.dimensions.add('product_type', dimension => dimension.setAlias('商品品类'))
            categorySalesChart.measures.add('sales', measure => measure.setAlias('销售额').setEncoding('xAxis').setAggregate({ func: 'sum' }))
            categorySalesChart.limit.setLimit(8)
    
            const discountProfitChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            discountProfitChart.chartType.changeChartType('scatter')
            discountProfitChart.dimensions.add('discount', dimension => dimension.setAlias('折扣'))
            discountProfitChart.measures.add('profit', measure => measure.setAlias('利润').setEncoding('yAxis').setAggregate({ func: 'sum' }))
    
            const promotionInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            promotionInsight.setContent("高折扣品类带来的销售增长未完全转化为利润,需要收紧促销门槛。")
    
            const resources = {
              charts: { categorySalesChart, discountProfitChart },
              insights: { promotionInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '商品运营看板' },
            })
    
            if (!dashboardBuilder.isEmpty()) throw new Error('dashboard should be empty')
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.categorySalesChart)
                .setTitle('品类销售')
                .setDescription('初版品类销售布局')
                .setLayouts({ lg: { x: 0, y: 0, w: 6, h: 4 }, md: { x: 0, y: 0, w: 6, h: 4 } })
            })
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.discountProfitChart)
                .setTitle('折扣利润散点')
                .setLayouts({ lg: { x: 6, y: 0, w: 6, h: 4 }, md: { x: 0, y: 4, w: 6, h: 4 } })
            })
            dashboardBuilder.insight.add(insight => {
              insight
                .setInsightId(resources.insights.promotionInsight)
                .setTitle('促销洞察')
                .setLayouts({ lg: { x: 0, y: 4, w: 12, h: 3 }, md: { x: 0, y: 8, w: 6, h: 3 } })
            })
    
            const [categoryWidget, discountWidget] = dashboardBuilder.chart.toJSON()
            const [promotionWidget] = dashboardBuilder.insight.toJSON()
            if (dashboardBuilder.chart.find(resources.charts.categorySalesChart.getUUID())?.getId() !== categoryWidget.id) throw new Error('chart reference should resolve')
            if (dashboardBuilder.insight.find(resources.insights.promotionInsight.getUUID())?.getId() !== promotionWidget.id) throw new Error('insight reference should resolve')
    
            dashboardBuilder.chart.update(categoryWidget.id, chart => {
              chart
                .setTitle('重点品类销售')
                .setDescription('更新后保留 md 布局,并合并新的 lg 布局')
                .setLayouts({ lg: { x: 0, y: 0, w: 7, h: 5 } })
            })
            dashboardBuilder.insight.update(promotionWidget.id, insight => {
              insight
                .setDescription('更新后的促销策略说明')
                .setLayouts({ lg: { x: 7, y: 0, w: 5, h: 5 } })
            })
            dashboardBuilder.chart.remove(discountWidget.id)
            if (dashboardBuilder.chart.get(discountWidget.id)) throw new Error('removed chart should be absent')
    
            for (const widget of dashboardBuilder.chart.toJSON()) {
              dashboardBuilder.chart.remove(widget.id)
            }
            for (const widget of dashboardBuilder.insight.toJSON()) {
              dashboardBuilder.insight.remove(widget.id)
            }
            if (!dashboardBuilder.isEmpty()) throw new Error('dashboard should be empty')
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(resources.charts.categorySalesChart)
                .setTitle('最终品类销售')
                .setDescription('清空草稿后重建的主图')
                .setLayouts({ lg: { x: 0, y: 0, w: 7, h: 5 }, md: { x: 0, y: 0, w: 6, h: 4 } })
            })
            dashboardBuilder.insight.add(insight => {
              insight
                .setInsightId(resources.insights.promotionInsight)
                .setTitle('最终促销洞察')
                .setDescription('清空草稿后重建的洞察')
                .setLayouts({ lg: { x: 7, y: 0, w: 5, h: 5 }, md: { x: 0, y: 4, w: 6, h: 3 } })
            })
            if (dashboardBuilder.isEmpty()) throw new Error('dashboard should contain widgets')
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }

    register-and-reference-quarterly-resources

    经营复盘场景中通过 VBI resources namespace 注册、查询、列出、移除和快照化可复用 chart/insight 资源,再在 dashboard 中引用

    标签: resources workflow dashboard

    Loading...
    import { createVBI, type VBIDashboardBuilder } from '@visactor/vbi'
    import { DashboardRenderer, type DashboardRendererProps } from 'dashboard'
    import { useLang } from '@rspress/core/runtime'
    import { useEffect, useState } from 'react'
    
    export default () => {
      const [dashboardBuilder, setDashboardBuilder] = useState<VBIDashboardBuilder | null>(null)
      const [error, setError] = useState<string | null>(null)
      const locale = useLang() as DashboardRendererProps['locale']
    
      useEffect(() => {
        let cancelled = false
        const run = async () => {
          try {
            const LocalVBI = createVBI()
            const quarterlyRevenueChart = LocalVBI.chart.create(LocalVBI.chart.createEmpty('demoSupermarket'))
            quarterlyRevenueChart.chartType.changeChartType('line')
            quarterlyRevenueChart.dimensions.add('order_date', dimension => dimension.setAlias('订单季度').setAggregate({ func: 'toQuarter' }))
            quarterlyRevenueChart.measures.add('sales', measure => measure.setAlias('季度销售额').setEncoding('yAxis').setAggregate({ func: 'sum' }))
            quarterlyRevenueChart.limit.setLimit(8)
    
            const quarterlyInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            quarterlyInsight.setContent("Q4 销售额继续增长,但利润贡献需要结合品类折扣进一步拆解。")
    
            const temporaryInsight = LocalVBI.insight.create(LocalVBI.insight.createEmpty())
            temporaryInsight.setContent("临时备注用于验证资源移除,不进入最终仪表盘。")
    
            const resources = {
              charts: { quarterlyRevenueChart },
              insights: { quarterlyInsight, temporaryInsight },
            }
    
            const dashboardBuilder = LocalVBI.dashboard.create({
              ...LocalVBI.dashboard.createEmpty(),
              meta: { title: '季度经营复盘' },
            })
    
            const chartId = resources.charts.quarterlyRevenueChart.getUUID()
            const insightId = resources.insights.quarterlyInsight.getUUID()
            const temporaryInsightId = resources.insights.temporaryInsight.getUUID()
    
            if (!LocalVBI.resources.chart.has(chartId)) throw new Error('chart should be registered')
            if (LocalVBI.resources.chart.get(chartId)?.chartType !== 'line') throw new Error('registered chart should be a line chart')
            if (!LocalVBI.resources.chart.list().some(chart => chart.uuid === chartId)) throw new Error('chart should be listed')
            if (!LocalVBI.resources.snapshot().insights[insightId]?.content.includes('Q4 销售额')) throw new Error('snapshot should include insight content')
    
            if (!LocalVBI.resources.insight.unregister(temporaryInsightId)) throw new Error('temporary insight should be removed')
            if (LocalVBI.resources.insight.get(temporaryInsightId)) throw new Error('removed insight should be absent')
            const restoredTemporaryInsight = LocalVBI.resources.insight.register(resources.insights.temporaryInsight.build())
            if (restoredTemporaryInsight.uuid !== temporaryInsightId) throw new Error('restored insight should retain its id')
    
            const registered = LocalVBI.resources.register({
              charts: [resources.charts.quarterlyRevenueChart.build()],
              insights: [resources.insights.quarterlyInsight.build()]
            })
            if (registered.charts[0].uuid !== chartId) throw new Error('registered chart id should match')
            if (registered.insights[0].uuid !== insightId) throw new Error('registered insight id should match')
    
            dashboardBuilder.chart.add(chart => {
              chart
                .setChart(chartId)
                .setTitle('季度销售走势')
                .setDescription('共享资源中的季度销售图,展示前 8 个季度')
                .setLayouts({ lg: { x: 0, y: 0, w: 8, h: 5 } })
            })
    
            dashboardBuilder.insight.add(insight => {
              insight
                .setInsightId(resources.insights.quarterlyInsight)
                .setTitle('季度经营解读')
                .setDescription('引用已注册 insight builder')
                .setLayouts({ lg: { x: 8, y: 0, w: 4, h: 5 } })
            })
            if (!cancelled) setDashboardBuilder(dashboardBuilder)
          } catch (err) {
            if (!cancelled) setError(err instanceof Error ? err.message : String(err))
          }
        }
        void run()
        return () => { cancelled = true }
      }, [])
    
      if (error) return <div role='alert'>{error}</div>
      if (!dashboardBuilder) return <div role='status'>Loading...</div>
    
      return <DashboardRenderer builder={dashboardBuilder} mode='edit' locale={locale} />
    }