rack:字体打包进前端(Noto Sans SC 可变 + Barlow),各端统一

图表 init 前先 document.fonts.load 字形:zrender 按字体缓存文字宽度且不清空,
字体未到就量会让时间轴标签叠成一团。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 16:54:47 +08:00
共同撰写人 Claude Opus 5.5
父节点 82d10ffefc
当前提交 afc5e81299
共修改 8 个文件,包含 65 行新增和 24 行删除
+2
查看文件
@@ -10,6 +10,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@fontsource-variable/noto-sans-sc": "^5.3.0",
"@fontsource/barlow": "^5.3.0",
"echarts": "^5.6.0",
"element-plus": "^2.9.0",
"vue": "^3.5.0",
+16 -14
查看文件
@@ -11,6 +11,12 @@ importers:
'@element-plus/icons-vue':
specifier: ^2.3.1
version: 2.3.2([email protected])
'@fontsource-variable/noto-sans-sc':
specifier: ^5.3.0
version: 5.3.0
'@fontsource/barlow':
specifier: ^5.3.0
version: 5.3.0
echarts:
specifier: ^5.6.0
version: 5.6.0
@@ -224,6 +230,12 @@ packages:
'@floating-ui/[email protected]':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource-variable/[email protected]':
resolution: {integrity: sha512-lNar1dF7Ik/lHNPo/7JWG0TolXY29LtsqYgMvEysooZ5bsO9uH4shJmRrwyJ3PjyTPljhpMJEK0jDuLSU4vJ1w==}
'@fontsource/[email protected]':
resolution: {integrity: sha512-tGR0y6QObRtKv71MOth44222+mdNFM/ZWFWDV1VLzNbG4oK9PNRlyJzmkck4vHAi9ibHBXgt4SlY51amFP55CA==}
'@jridgewell/[email protected]':
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
@@ -232,7 +244,6 @@ packages:
engines: {node: ^22.20 || ^24.12 || >=25}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==}
@@ -268,79 +279,66 @@ packages:
resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==}
@@ -706,6 +704,10 @@ snapshots:
'@floating-ui/[email protected]': {}
'@fontsource-variable/[email protected]': {}
'@fontsource/[email protected]': {}
'@jridgewell/[email protected]': {}
'@napi-rs/[email protected]':
+11
查看文件
@@ -14,6 +14,17 @@ import { cssVar, dark } from './theme'
echarts.use([LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, AxisPointerComponent, CanvasRenderer])
export { echarts }
// 字体是打包进来异步下的,而 zrender 按「字体 → 文字」缓存宽度、从不清空:
// 字形没到就量,会一直按回退字体的宽度排版(时间轴标签叠成一团,重画也救不回来)。
// 所以每张图 init 之前先把它 canvas 上要用的字形下完:全部 ASCII + 轴名里的汉字。
// 图例和 tooltip 是 DOM,字体到了自己会换,不用管。
const ASCII = Array.from({ length: 95 }, (_, i) => String.fromCharCode(32 + i)).join('')
export function chartFonts(...texts) {
if (!document.fonts) return Promise.resolve()
const family = getComputedStyle(document.body).fontFamily
return document.fonts.load(`12px ${family}`, ASCII + '°' + texts.join('')).catch(() => {})
}
const SERIES = { light: ['#2a78d6', '#eb6834'], dark: ['#3987e5', '#d95926'] }
export function palette() {
+11 -3
查看文件
@@ -22,7 +22,7 @@ import RangePicker from './RangePicker.vue'
import { api } from '../api'
import { onMessage } from '../store'
import { dark } from '../theme'
import { echarts, palette, band, stacked } from '../chart'
import { echarts, palette, band, stacked, chartFonts } from '../chart'
const props = defineProps({ board: String })
const range = ref(3600e3)
@@ -36,6 +36,7 @@ let live = false
const degC = v => (v == null ? null : v / 1000)
function render() {
if (!chart) return // 还在等字体
const p = palette()
c.value = p
const hasBand = pts.some(x => x[2] != null && x[2] !== x[3])
@@ -91,8 +92,15 @@ const off = onMessage(m => {
})
const onResize = () => chart && chart.resize()
onMounted(() => { chart = echarts.init(el.value); load(); window.addEventListener('resize', onResize) })
onBeforeUnmount(() => { off(); window.removeEventListener('resize', onResize); chart && chart.dispose() })
let gone = false
onMounted(async () => {
window.addEventListener('resize', onResize)
await chartFonts('温度 °C', '风扇 %')
if (gone) return
chart = echarts.init(el.value)
load()
})
onBeforeUnmount(() => { gone = true; off(); window.removeEventListener('resize', onResize); chart && chart.dispose() })
watch(() => [props.board, range.value], load)
watch(dark, () => chart && render())
</script>
+11 -3
查看文件
@@ -17,7 +17,7 @@ import RangePicker from './RangePicker.vue'
import { api } from '../api'
import { onMessage } from '../store'
import { dark } from '../theme'
import { echarts, palette, band, stacked } from '../chart'
import { echarts, palette, band, stacked, chartFonts } from '../chart'
const props = defineProps({ board: String, rail: Number })
const range = ref(600e3)
@@ -28,6 +28,7 @@ let pts = [] // [t, mv, ua, ua_min, ua_max]
let live = false
function render() {
if (!chart) return // 还在等字体
const p = palette()
const hasBand = pts.some(x => x[3] != null && x[3] !== x[4])
const mA = v => (v == null ? null : v / 1000)
@@ -80,8 +81,15 @@ const off = onMessage(m => {
})
const onResize = () => chart && chart.resize()
onMounted(() => { chart = echarts.init(el.value); load(); window.addEventListener('resize', onResize) })
onBeforeUnmount(() => { off(); window.removeEventListener('resize', onResize); chart && chart.dispose() })
let gone = false
onMounted(async () => {
window.addEventListener('resize', onResize)
await chartFonts('电流 mA', '电压 V')
if (gone) return
chart = echarts.init(el.value)
load()
})
onBeforeUnmount(() => { gone = true; off(); window.removeEventListener('resize', onResize); chart && chart.dispose() })
watch(() => [props.board, props.rail, range.value], load)
watch(dark, () => chart && render())
</script>
+8
查看文件
@@ -4,6 +4,14 @@ import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
// 字体随前端打包,不靠系统字体 —— Windows / Mac / 安卓 / iPhone 看到的一样。
// 中文 Noto Sans SC(可变字重,按 unicode-range 分成约 100 片,浏览器只下用到的那几片);
// 读数 Barlow(DIN 风格,带 tnum 等宽数字),只要拉丁子集。
import '@fontsource-variable/noto-sans-sc'
import '@fontsource/barlow/latin-400.css'
import '@fontsource/barlow/latin-500.css'
import '@fontsource/barlow/latin-600.css'
import '@fontsource/barlow/latin-700.css'
import './theme.css'
import './theme'
+3 -3
查看文件
@@ -32,9 +32,9 @@
--led-warn: #fab219;
--led-bad: #e04343;
--readout: "DIN Alternate", "DIN 2014", Bahnschrift, "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
--ui: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei UI", "Microsoft YaHei",
"Noto Sans CJK SC", system-ui, sans-serif;
/* 两套字体都随前端打包(main.js 引入),系统字体只在字体文件还没下完时顶一下 */
--readout: "Barlow", "Noto Sans SC Variable", sans-serif;
--ui: "Noto Sans SC Variable", sans-serif;
--r-panel: 10px;
--r-bay: 6px;