#dashboard
#retail-operations-dashboard
Inspired by an operations monitoring dashboard, this example combines 10 cards: query-based business summaries, category and customer sales shares, sales and profit trends, province rankings, delivery distribution, customer details, and suggested actions. The first row has four cards on wide screens, two columns in the documentation area, and one on mobile.
Tags:
dashboardmulti-columnresponsiveoperations
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
Combine chart and insight resources with the dashboard builder to generate a basic dashboard
Tags:
basic
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
Combine multiple chart and insight resources, and configure responsive dashboard layouts for different breakpoints
Tags:
layoutresponsiveresources
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
Add multiple dashboard widgets, then update their layouts and remove a temporary chart
Tags:
layoutmutation
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
Save the light theme in the Dashboard. The preview uses the saved theme independently of the website appearance.
Tags:
theme
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
Switch to dark mode with dashboardBuilder.theme.setTheme, keeping cards, charts and the editor consistent.
Tags:
theme
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
Configure a brand theme directly through Dashboard Builder. Colors, text, backgrounds and cards share one definition, saved and synchronized with the document without component registration.
Tags:
theme
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
Start with Misty rose and switch among all eight reference palettes in the Light theme group: Misty rose, Sea salt blue, Forest mist, Lavender, Apricot orange, Bamboo moon, Clear sky blue and Cedar rose. Charts, cards and the editor follow the selected theme.
标签:
theme
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} />
}#executive-dashboard-widget-lifecycle
Validate widget layouts, compose sales charts and business insights, then adjust the dashboard and synchronize a collaborative replica.
Tags:
workflowdashboardcollaboration
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
Add, update, remove and clear chart/insight widgets in a merchandising dashboard, then rebuild the final layout to verify layout merging.
Tags:
dashboardworkflowlayoutmutation
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
Register, inspect, list, remove and snapshot reusable chart/insight resources through the VBI resources namespace, then reference them in a quarterly dashboard.
Tags:
resourcesworkflowdashboard
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} />
}