#dashboard
#retail-operations-dashboard
参考运营监控大屏,组合 10 张卡片:真实查询生成的经营摘要、品类与客群占比、销售利润趋势、省份排行、配送分布、客户明细和行动建议。宽屏首行四卡,文档内容区双列,手机单列。
标签:
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
通过 dashboard builder 组合 chart 与 insight 资源,生成基础仪表盘
标签:
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
组合多个 chart 与 insight 资源,并为不同断点设置响应式 dashboard 布局
标签:
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
新增多个 dashboard widget 后更新布局并移除临时图表
标签:
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
将浅色主题保存在 Dashboard 中,预览使用文档主题,不受网站外观影响。
标签:
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
通过 dashboardBuilder.theme.setTheme 切换深色主题,卡片、图表和编辑器保持一致。
标签:
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
通过 Dashboard Builder 直接配置品牌主题,统一图表色板、文字、背景和卡片样式;配置随文档保存、撤销和同步,无需组件注册。
标签:
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
使用雾玫粉配色,并在主题菜单的浅色分组中切换海盐蓝、森雾青、薰衣紫、杏桃橘、竹月青、晴空蓝和雪松玫瑰,统一图表、卡片和编辑器的配色。
标签:
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} />
}#undo-redo-dashboard
仪表盘组件、响应式布局与主题按事务一起撤销和重做,引用的洞察内容独立保存。
标签:
undoManageryjs
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
经营看板先校验组件布局,再组合销售图表与经营洞察,并在调整布局后同步到协作副本
标签:
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
商品运营仪表盘中新增、更新、移除并清空 chart/insight widget,随后重建最终布局以验证布局合并
标签:
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
经营复盘场景中通过 VBI resources namespace 注册、查询、列出、移除和快照化可复用 chart/insight 资源,再在 dashboard 中引用
标签:
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} />
}