ProChart 统计图表
基于 ECharts 的通用图表容器,支持两种使用方式:
- 透传
option:直接使用 ECharts 原生配置(最灵活) - 简化模型:通过
type / xAxis / series快速生成常见图表(更易用),并可与option合并(option优先覆盖)
基础用法(简化模型)
vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
const xAxis = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
</script>
<template>
<pro-chart
:x-axis="xAxis"
:series="[{ name: 'Visits', data: [120, 200, 150, 80, 70, 110, 130] }]"
:height="280"
/>
</template>
隐藏源代码
渐变堆叠面积图
当 type="line" 且 series.length >= 2 时,会自动启用多系列折线场景的默认值(tooltip.trigger='axis'、smooth=true 等),也可通过 tooltip/legend/grid/option 覆盖。
vue
<script setup lang="ts">
/**
* 渐变堆叠面积图:标题 / 图例与绘图区 grid 左右边严格对齐(同百分比边距)
* 堆叠面积 + 设计稿 tooltip / legend 样式
*/
import { computed, defineAsyncComponent } from 'vue'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
/** 与 grid.left / grid.right 一致,保证标题左沿、图例右沿与折线绘图区对齐 */
const GUTTER_X = '3%'
const categories = Array.from(
{ length: 25 },
(_, i) => `${String(i).padStart(2, '0')}:00`
)
const receiptsData = categories.map((_, i) =>
Math.round(3000 + Math.sin(i / 4) * 420)
)
const terminalData = categories.map((_, i) =>
Math.round(1500 + Math.cos(i / 3) * 280)
)
const palette = ['#B56BFF', '#6C7DFF'] as const
function formatNumber(v: unknown) {
const n = typeof v === 'number' ? v : Number(v)
if (!Number.isFinite(n)) return String(v ?? '')
return new Intl.NumberFormat().format(n)
}
/** 横轴仅有「HH:mm」时左侧日期占位;若 category 内含 YYYY-MM-DD 则由其解析日期 */
const TOOLTIP_DEFAULT_DATE = '2022-07-17'
function pad2(n: number) {
return String(n).padStart(2, '0')
}
/** 当前刻度对应的时间段(对齐设计稿「开始—结束」) */
function timeRangeFromClockPart(part: string): string {
const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(part.trim())
if (!m) return part
const h = Number(m[1])
const mi = Number(m[2])
const secGiven = m[3] !== undefined
const s = secGiven ? Number(m[3]) : 0
if (
!Number.isFinite(h) ||
h < 0 ||
h > 24 ||
!Number.isFinite(mi) ||
mi < 0 ||
mi > 59 ||
(secGiven && (!Number.isFinite(s) || s < 0 || s > 59))
)
return part
// 刻度为整点且无秒:视为「该整点 ~ 59:59」(小时 bucket)
if (!secGiven && mi === 0 && h === 24) return '23:59:00—23:59:59'
if (!secGiven && mi === 0 && s === 0) {
return `${pad2(h)}:00:00—${pad2(h)}:59:59`
}
// 刻度带秒或非整点:`…:秒` ~ 同分钟内 :59(贴近设计稿 14:45:00—14:59:59 样式)
if (secGiven) {
return `${pad2(h)}:${pad2(mi)}:${pad2(s)}—${pad2(h)}:${pad2(mi)}:59`
}
return `${pad2(h)}:${pad2(mi)}:00—${pad2(h)}:${pad2(mi)}:59`
}
function tooltipTitleFromAxis(axisRaw: unknown): {
date: string
timeRange: string
} {
const axisLabel =
typeof axisRaw === 'string'
? axisRaw.trim()
: axisRaw !== undefined && axisRaw !== null
? String(axisRaw).trim()
: ''
if (!axisLabel) {
return { date: TOOLTIP_DEFAULT_DATE, timeRange: '' }
}
const normalized = axisLabel.replace(/\//g, '-')
let date: string | null = null
let tail: string | undefined
const dash = /^(\d{4}-\d{2}-\d{2})(?:[ T]+(.+))?$/.exec(normalized)
if (dash) {
date = dash[1]
tail = dash[2]?.trim()
if (!tail?.length) tail = undefined
}
if (date && tail?.length)
return { date, timeRange: timeRangeFromClockPart(tail) }
if (date) return { date, timeRange: '' }
return {
date: TOOLTIP_DEFAULT_DATE,
timeRange: timeRangeFromClockPart(axisLabel),
}
}
function formatDesignTooltip(params: unknown) {
const list = Array.isArray(params) ? params : [params]
const first = list[0] as Record<string, unknown> | undefined
const axisKey = first?.axisValueLabel ?? first?.axisValue ?? first?.name
const { date, timeRange } = tooltipTitleFromAxis(axisKey)
const header =
`<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px">` +
`<span style="font-weight:600;font-size:12px;color:#303133">${date}</span>` +
`<span style="font-size:12px;color:rgba(0,0,0,0.55)">${timeRange}</span>` +
`</div>`
const rows = list
.map((p: any) => {
const raw = p.value
const val = Array.isArray(raw) ? raw[1] : raw
const color = p.color ?? '#999'
const name = p.seriesName ?? ''
return (
`<div style="background:rgba(0,0,0,0.04);border-radius:8px;padding:10px 12px;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between;gap:20px;min-width:220px">` +
`<div style="display:flex;align-items:center;min-width:0">` +
`<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-right:8px;flex-shrink:0"></span>` +
`<span style="font-size:12px;color:rgba(0,0,0,0.68)">${name}</span>` +
`</div>` +
`<span style="font-weight:600;font-size:13px;color:#303133;flex-shrink:0">${formatNumber(val)}</span>` +
`</div>`
)
})
.join('')
return `<div style="min-width:232px">${header}${rows}</div>`
}
const trendOption = computed(() => ({
color: [...palette],
title: {
text: 'Trend Chart',
left: GUTTER_X,
top: 6,
textStyle: {
fontSize: 14,
fontWeight: 600,
color: '#303133',
},
},
tooltip: {
trigger: 'axis',
appendToBody: true,
backgroundColor: 'rgba(255,255,255,0.98)',
borderColor: 'transparent',
borderWidth: 0,
borderRadius: 8,
padding: [12, 14],
extraCssText: 'box-shadow:0 6px 20px rgba(0,0,0,0.08);border-radius:8px;',
axisPointer: {
type: 'line',
lineStyle: {
color: '#dcdfe6',
width: 1,
},
},
formatter: formatDesignTooltip,
},
legend: {
show: true,
data: ['Receipts', 'Terminal'],
orient: 'horizontal',
icon: 'circle',
itemWidth: 8,
itemHeight: 8,
itemGap: 14,
top: 6,
right: GUTTER_X,
textStyle: {
fontSize: 12,
color: 'rgba(0,0,0,0.68)',
},
},
grid: {
left: GUTTER_X,
right: GUTTER_X,
top: 48,
bottom: '4%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: categories,
axisLabel: {
interval: 1,
fontSize: 11,
color: 'rgba(0,0,0,0.45)',
},
axisTick: { show: false },
axisLine: {
lineStyle: { color: '#eaeaea' },
},
},
],
yAxis: [
{
type: 'value',
interval: 1000,
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: {
color: 'rgba(0,0,0,0.06)',
},
},
axisLabel: {
fontSize: 11,
color: 'rgba(0,0,0,0.45)',
formatter: (value: number) => {
if (Math.abs(value) >= 1000) {
return `${Math.round((value / 1000) * 10) / 10}K`
}
return String(value)
},
},
},
],
series: [
{
name: 'Receipts',
type: 'line',
stack: 'Total',
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 7,
lineStyle: { width: 2 },
areaStyle: {
opacity: 0.14,
},
emphasis: {
focus: 'series',
},
data: receiptsData,
},
{
name: 'Terminal',
type: 'line',
stack: 'Total',
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 7,
lineStyle: { width: 2 },
areaStyle: {
opacity: 0.12,
},
emphasis: {
focus: 'series',
},
data: terminalData,
},
],
}))
</script>
<template>
<div class="trend-demo">
<pro-chart :option="trendOption" :height="340" />
</div>
</template>
<style scoped>
.trend-demo {
width: 100%;
}
</style>
隐藏源代码
柱状图(Bar)
vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
const xAxis = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
</script>
<template>
<pro-chart
type="bar"
:x-axis="xAxis"
:series="[{ name: 'Sales', data: [12, 20, 15, 8, 7, 11, 13] }]"
:height="260"
:option="{
series: [
{
barWidth: '40%',
showBackground: true,
backgroundStyle: { color: 'rgba(180,180,180,0.2)' },
},
],
}"
/>
</template>
隐藏源代码
饼图(Pie)
vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
</script>
<template>
<pro-chart
type="pie"
:height="280"
:series="[
{
name: 'Category',
data: [
{ name: 'A', value: 40 },
{ name: 'B', value: 32 },
{ name: 'C', value: 28 },
],
},
]"
:option="{
tooltip: { trigger: 'item' },
series: [{ radius: ['40%', '70%'] }],
}"
/>
</template>
隐藏源代码
透传 option(与简化模型合并)
vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
const xAxis = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
</script>
<template>
<pro-chart
:x-axis="xAxis"
:series="[{ name: 'Visits', data: [120, 200, 150, 80, 70, 110, 130] }]"
:height="260"
:option="{
title: {
text: 'Option 覆盖示例',
left: 0,
top: 0,
textStyle: { fontSize: 14 },
},
grid: { left: 12, right: 12, top: 48, bottom: 12, containLabel: true },
yAxis: { splitLine: { lineStyle: { type: 'dashed' } } },
series: [{ symbol: 'circle', symbolSize: 8 }],
}"
/>
</template>
隐藏源代码
Loading 与空态
vue
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
import { UButton, USpace } from 'uniboot-ui'
const ProChart = defineAsyncComponent(() =>
import('uniboot-ui').then((m: any) => m.ProChart ?? m.default)
)
const loading = ref(false)
const empty = ref(false)
const xAxis = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
</script>
<template>
<u-space class="mb-3">
<u-button @click="loading = !loading">{{
loading ? 'Stop Loading' : 'Loading'
}}</u-button>
<u-button @click="empty = !empty">{{
empty ? 'Show Data' : 'Empty'
}}</u-button>
</u-space>
<pro-chart
:loading="loading"
:empty="empty ? '暂无数据' : false"
:x-axis="xAxis"
:series="[{ name: 'Visits', data: [120, 200, 150, 80, 70, 110, 130] }]"
:height="260"
/>
</template>
隐藏源代码
ProChart API
ProChart 属性
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 图表类型 | enum | 'line' |
| xAxis | x 轴简化数据(数组)或完整 xAxis 配置 | array / object | — |
| yAxis | 完整 yAxis 配置 | object / array | — |
| series | series 简化模型 | array | — |
| option | ECharts 原生 option(会覆盖简化模型生成的同名字段) | object | — |
| legend | legend 开关或配置 | boolean / object | — |
| tooltip | tooltip 开关或配置 | boolean / object | — |
| grid | grid 配置 | object | — |
| width | 宽度(数字按 px) | string / number | '100%' |
| height | 高度(数字按 px) | string / number | 320 |
| loading | 是否加载中 | boolean | false |
| empty | 空态(true/文案) | boolean / string | false |
| theme | 主题 | enum | 'auto' |
| palette | 色板(优先于内置默认色板) | string[] | — |
| notMerge | 传给 ECharts setOption 的 notMerge | boolean | false |
| autoResize | 是否自动 resize | boolean | true |
ProChartSeriesItem
| 名称 | 说明 | 类型 |
|---|---|---|
| name | series 名称 | string |
| data | 数据 | array |
| color | 自定义颜色 | string |
| smooth | 是否平滑(line) | boolean |
| area | 是否区域(line) | boolean / object |
| stack | stack 名称(bar/line) | string |
| yAxisIndex | y 轴索引 | number |