@zerodep-dev/vue-tsx-pro-ui 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/AreaChart.d.ts +1 -1
- package/dist/charts/AreaChart.js +6 -48
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.js +6 -49
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/ChordChart.d.ts +7 -2
- package/dist/charts/ChordChart.js +151 -60
- package/dist/charts/ChordChart.js.map +1 -1
- package/dist/charts/ComboChart.d.ts +2 -1
- package/dist/charts/ComboChart.js +17 -93
- package/dist/charts/ComboChart.js.map +1 -1
- package/dist/charts/DonutChart.d.ts +6 -1
- package/dist/charts/DonutChart.js +134 -51
- package/dist/charts/DonutChart.js.map +1 -1
- package/dist/charts/FunnelChart.js.map +1 -1
- package/dist/charts/GraphChart.d.ts +22 -10
- package/dist/charts/GraphChart.js +415 -175
- package/dist/charts/GraphChart.js.map +1 -1
- package/dist/charts/LineChart.d.ts +1 -29
- package/dist/charts/LineChart.js +6 -54
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/MapChart.d.ts +21 -48
- package/dist/charts/MapChart.js +426 -353
- package/dist/charts/MapChart.js.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/SankeyChart.d.ts +7 -2
- package/dist/charts/SankeyChart.js +164 -76
- package/dist/charts/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart.d.ts +7 -1
- package/dist/charts/ScatterChart.js +45 -30
- package/dist/charts/ScatterChart.js.map +1 -1
- package/dist/charts/TimelineChart.d.ts +8 -1
- package/dist/charts/TimelineChart.js +141 -89
- package/dist/charts/TimelineChart.js.map +1 -1
- package/dist/charts/TreemapChart.d.ts +6 -0
- package/dist/charts/TreemapChart.js +174 -58
- package/dist/charts/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart.d.ts +6 -7
- package/dist/charts/WaterfallChart.js +46 -51
- package/dist/charts/WaterfallChart.js.map +1 -1
- package/dist/charts/XYChart.d.ts +44 -35
- package/dist/charts/XYChart.js +493 -281
- package/dist/charts/XYChart.js.map +1 -1
- package/dist/charts/axis-ticks.d.ts +1 -1
- package/dist/charts/axis-ticks.js +4 -5
- package/dist/charts/axis-ticks.js.map +1 -1
- package/dist/charts/flow.d.ts +4 -11
- package/dist/charts/flow.js +4 -4
- package/dist/charts/flow.js.map +1 -1
- package/dist/charts/frame.d.ts +6 -30
- package/dist/charts/frame.js +6 -99
- package/dist/charts/frame.js.map +1 -1
- package/dist/charts/geo/projection.d.ts +45 -0
- package/dist/charts/geo/projection.js +201 -0
- package/dist/charts/geo/projection.js.map +1 -0
- package/dist/charts/geo/topojson.d.ts +50 -0
- package/dist/charts/geo/topojson.js +119 -0
- package/dist/charts/geo/topojson.js.map +1 -0
- package/dist/charts/index.d.ts +21 -9
- package/dist/charts/index.js +19 -8
- package/dist/charts/label-layout.js.map +1 -1
- package/dist/charts/layout/chord.d.ts +44 -0
- package/dist/charts/layout/chord.js +45 -0
- package/dist/charts/layout/chord.js.map +1 -0
- package/dist/charts/layout/graph.d.ts +49 -0
- package/dist/charts/layout/graph.js +268 -0
- package/dist/charts/layout/graph.js.map +1 -0
- package/dist/charts/layout/sankey.d.ts +79 -0
- package/dist/charts/layout/sankey.js +200 -0
- package/dist/charts/layout/sankey.js.map +1 -0
- package/dist/charts/layout/treemap.d.ts +46 -0
- package/dist/charts/layout/treemap.js +152 -0
- package/dist/charts/layout/treemap.js.map +1 -0
- package/dist/charts/marks.d.ts +79 -0
- package/dist/charts/marks.js +231 -0
- package/dist/charts/marks.js.map +1 -0
- package/dist/charts/plot.d.ts +107 -0
- package/dist/charts/plot.js +231 -0
- package/dist/charts/plot.js.map +1 -0
- package/dist/charts/scale.d.ts +29 -0
- package/dist/charts/scale.js +244 -0
- package/dist/charts/scale.js.map +1 -0
- package/dist/charts/shape.d.ts +46 -0
- package/dist/charts/shape.js +131 -0
- package/dist/charts/shape.js.map +1 -0
- package/dist/charts/stack.d.ts +17 -0
- package/dist/charts/stack.js +35 -0
- package/dist/charts/stack.js.map +1 -0
- package/dist/charts/text-size.d.ts +12 -0
- package/dist/charts/text-size.js +32 -0
- package/dist/charts/text-size.js.map +1 -0
- package/dist/config/createUi.d.ts +21 -2
- package/dist/config/createUi.js +18 -1
- package/dist/config/createUi.js.map +1 -1
- package/dist/config/theme/preference.js +2 -1
- package/dist/config/theme/preference.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/maps/china-provinces.js +35680 -0
- package/dist/maps/china-provinces.js.map +1 -0
- package/dist/maps/china.d.ts +13 -0
- package/dist/maps/china.js +8 -0
- package/dist/maps/china.js.map +1 -0
- package/dist/maps/index.d.ts +9 -0
- package/dist/maps/index.js +3 -0
- package/dist/maps/world-countries.js +22273 -0
- package/dist/maps/world-countries.js.map +1 -0
- package/dist/maps/world.d.ts +13 -0
- package/dist/maps/world.js +8 -0
- package/dist/maps/world.js.map +1 -0
- package/dist/overlay/detached.js.map +1 -1
- package/dist/utils/request/use-request.d.ts +25 -1
- package/dist/utils/request/use-request.js +35 -3
- package/dist/utils/request/use-request.js.map +1 -1
- package/package.json +7 -8
- package/dist/charts/single.d.ts +0 -41
- package/dist/charts/single.js +0 -66
- package/dist/charts/single.js.map +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** 一个点 [x, y] */
|
|
2
|
+
export type ChartPoint = readonly [number, number];
|
|
3
|
+
/** 线的形状 */
|
|
4
|
+
export interface LineCurves {
|
|
5
|
+
/** 折线:相邻的点直线相连(默认) */
|
|
6
|
+
linear: unknown;
|
|
7
|
+
/** 平滑曲线:不会越过数据点(单调插值),趋势更柔和 */
|
|
8
|
+
monotone: unknown;
|
|
9
|
+
/** 阶梯:值保持到下一个点才跳变,适合离散的状态、价格档位 */
|
|
10
|
+
step: unknown;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 一串点连成的路径;start 是开头的命令(M 新起一段,L 接着上一段画:面积的下沿)。
|
|
14
|
+
* 少于三个点时平滑曲线退化成直线
|
|
15
|
+
*/
|
|
16
|
+
export declare function linePath(points: readonly ChartPoint[], curve?: keyof LineCurves, start?: 'M' | 'L'): string;
|
|
17
|
+
/** 面积:上沿 top 从左到右,下沿 bottom(与 top 一一对应)从右到左,同一种曲线,闭合 */
|
|
18
|
+
export declare function areaPath(top: readonly ChartPoint[], bottom: readonly ChartPoint[], curve?: keyof LineCurves): string;
|
|
19
|
+
/** 柱子的哪一端是圆角:柱顶朝哪个方向 */
|
|
20
|
+
export type BarEnd = 'top' | 'bottom' | 'left' | 'right' | 'none';
|
|
21
|
+
/** 柱子:矩形(左上角 x、y,宽高),end 那一端的两个角是半径 radius 的圆角(不超过宽或高的一半) */
|
|
22
|
+
export declare function barPath(x: number, y: number, width: number, height: number, radius: number, end: BarEnd): string;
|
|
23
|
+
/** 环形的一段:角度从 12 点钟方向顺时针(弧度),圆心在原点 */
|
|
24
|
+
export interface ArcShape {
|
|
25
|
+
/** 内半径;0 是饼图的扇形 */
|
|
26
|
+
inner: number;
|
|
27
|
+
/** 外半径 */
|
|
28
|
+
outer: number;
|
|
29
|
+
/** 起始角 */
|
|
30
|
+
start: number;
|
|
31
|
+
/** 结束角 */
|
|
32
|
+
end: number;
|
|
33
|
+
/** 四个角的圆角半径,默认 0 */
|
|
34
|
+
corner?: number;
|
|
35
|
+
}
|
|
36
|
+
/** 半径 r、角度 angle(12 点钟方向顺时针)处的点 */
|
|
37
|
+
export declare function polarPoint(r: number, angle: number): [number, number];
|
|
38
|
+
/**
|
|
39
|
+
* 环形一段的路径:两条径向的边、内外两段弧;corner 时四个角各是一段与边、弧都相切的小圆弧
|
|
40
|
+
* (边是径向的,与圆正交,切点可以直接算出)。整圈时画成两段弧(没有边)
|
|
41
|
+
*/
|
|
42
|
+
export declare function arcPath({ inner, outer, start, end, corner }: ArcShape): string;
|
|
43
|
+
/** 弦图的一根弦:两端是圆上的两段弧(半径 radius,角度同 arcPath),中间经圆心的二次曲线相连(同 d3 的 ribbon) */
|
|
44
|
+
export declare function ribbonPath(radius: number, source: readonly [number, number], target: readonly [number, number]): string;
|
|
45
|
+
/** 横向的流向带(桑基图的连线):从 (x0, y0) 到 (x1, y1) 的 S 形三次曲线,控制点在两端的中间 */
|
|
46
|
+
export declare function horizontalLinkPath(x0: number, y0: number, x1: number, y1: number): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { svgNumber } from "../utils/data/number.js";
|
|
2
|
+
//#region src/charts/shape.ts
|
|
3
|
+
/**
|
|
4
|
+
* 图形的 SVG 路径:折线与面积(直线、单调平滑、阶梯)、一端圆角的柱子、环形的扇区、弦图的弦、桑基图的带子。
|
|
5
|
+
* 曲线的算法同 d3-shape(curveMonotoneX 是 Steffen 的单调三次插值,不会越过数据点),写成只产出路径字符串的精简版。
|
|
6
|
+
* 坐标保留三位小数(svgNumber):服务端与浏览器算出的路径一致。
|
|
7
|
+
*
|
|
8
|
+
* linePath([[0, 10], [40, 30], [80, 20]], 'monotone') // 'M0,10C13.333,…'
|
|
9
|
+
* areaPath(top, bottom, 'linear') // 上沿从左到右、下沿从右到左,闭合
|
|
10
|
+
* arcPath({ inner: 60, outer: 88, start: 0, end: Math.PI / 2, corner: 3 })
|
|
11
|
+
*/
|
|
12
|
+
/** 两个数字写成“x,y” */
|
|
13
|
+
var pair = (x, y) => `${svgNumber(x)},${svgNumber(y)}`;
|
|
14
|
+
var sign = (value) => value < 0 ? -1 : 1;
|
|
15
|
+
/** 中间一点的切线斜率:两侧的斜率异号时为 0(极值点不越过),否则取不超过两侧的调和值(同 d3 的 slope3) */
|
|
16
|
+
function interiorSlope(a, b, c) {
|
|
17
|
+
const h0 = b[0] - a[0];
|
|
18
|
+
const h1 = c[0] - b[0];
|
|
19
|
+
const s0 = (b[1] - a[1]) / (h0 || (h1 < 0 ? -0 : 0));
|
|
20
|
+
const s1 = (c[1] - b[1]) / (h1 || (h0 < 0 ? -0 : 0));
|
|
21
|
+
const p = (s0 * h1 + s1 * h0) / (h0 + h1);
|
|
22
|
+
return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), .5 * Math.abs(p)) || 0;
|
|
23
|
+
}
|
|
24
|
+
/** 端点的切线斜率:由相邻一段的斜率与相邻点的切线推出(同 d3 的 slope2) */
|
|
25
|
+
function endSlope(a, b, tangent) {
|
|
26
|
+
const h = b[0] - a[0];
|
|
27
|
+
return h ? (3 * (b[1] - a[1]) / h - tangent) / 2 : tangent;
|
|
28
|
+
}
|
|
29
|
+
/** 单调三次插值:各点的切线,再按 Hermite 写成三次贝塞尔 */
|
|
30
|
+
function monotoneCommands(points) {
|
|
31
|
+
const n = points.length;
|
|
32
|
+
const tangents = points.map((point, i) => i > 0 && i < n - 1 ? interiorSlope(points[i - 1], point, points[i + 1]) : 0);
|
|
33
|
+
tangents[0] = endSlope(points[0], points[1], tangents[1]);
|
|
34
|
+
tangents[n - 1] = endSlope(points[n - 2], points[n - 1], tangents[n - 2]);
|
|
35
|
+
let path = "";
|
|
36
|
+
for (let i = 1; i < n; i += 1) {
|
|
37
|
+
const [x0, y0] = points[i - 1];
|
|
38
|
+
const [x1, y1] = points[i];
|
|
39
|
+
const dx = (x1 - x0) / 3;
|
|
40
|
+
path += `C${pair(x0 + dx, y0 + dx * tangents[i - 1])},${pair(x1 - dx, y1 - dx * tangents[i])},${pair(x1, y1)}`;
|
|
41
|
+
}
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 一串点连成的路径;start 是开头的命令(M 新起一段,L 接着上一段画:面积的下沿)。
|
|
46
|
+
* 少于三个点时平滑曲线退化成直线
|
|
47
|
+
*/
|
|
48
|
+
function linePath(points, curve = "linear", start = "M") {
|
|
49
|
+
if (points.length === 0) return "";
|
|
50
|
+
const [first, ...rest] = points;
|
|
51
|
+
let path = `${start}${pair(first[0], first[1])}`;
|
|
52
|
+
if (curve === "monotone" && points.length > 2) return path + monotoneCommands(points);
|
|
53
|
+
let previous = first;
|
|
54
|
+
for (const point of rest) {
|
|
55
|
+
if (curve === "step") path += `L${pair(point[0], previous[1])}`;
|
|
56
|
+
path += `L${pair(point[0], point[1])}`;
|
|
57
|
+
previous = point;
|
|
58
|
+
}
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
/** 面积:上沿 top 从左到右,下沿 bottom(与 top 一一对应)从右到左,同一种曲线,闭合 */
|
|
62
|
+
function areaPath(top, bottom, curve = "linear") {
|
|
63
|
+
if (top.length === 0) return "";
|
|
64
|
+
const back = curve === "step" ? bottom.flatMap((point, i) => i === 0 ? [point] : [[point[0], bottom[i - 1][1]], point]) : bottom;
|
|
65
|
+
return `${linePath(top, curve)}${linePath([...back].reverse(), curve === "step" ? "linear" : curve, "L")}Z`;
|
|
66
|
+
}
|
|
67
|
+
/** 柱子:矩形(左上角 x、y,宽高),end 那一端的两个角是半径 radius 的圆角(不超过宽或高的一半) */
|
|
68
|
+
function barPath(x, y, width, height, radius, end) {
|
|
69
|
+
const w = Math.max(width, 0);
|
|
70
|
+
const h = Math.max(height, 0);
|
|
71
|
+
const vertical = end === "top" || end === "bottom";
|
|
72
|
+
const r = end === "none" ? 0 : Math.max(0, Math.min(radius, vertical ? w / 2 : h / 2, vertical ? h : w));
|
|
73
|
+
const [x1, y1] = [x + w, y + h];
|
|
74
|
+
const corner = (cx, cy) => `A${pair(r, r)},0,0,1,${pair(cx, cy)}`;
|
|
75
|
+
if (!r) return `M${pair(x, y)}H${svgNumber(x1)}V${svgNumber(y1)}H${svgNumber(x)}Z`;
|
|
76
|
+
if (end === "top") return `M${pair(x, y1)}V${svgNumber(y + r)}${corner(x + r, y)}H${svgNumber(x1 - r)}${corner(x1, y + r)}V${svgNumber(y1)}Z`;
|
|
77
|
+
if (end === "bottom") return `M${pair(x1, y)}V${svgNumber(y1 - r)}${corner(x1 - r, y1)}H${svgNumber(x + r)}${corner(x, y1 - r)}V${svgNumber(y)}Z`;
|
|
78
|
+
if (end === "right") return `M${pair(x, y)}H${svgNumber(x1 - r)}${corner(x1, y + r)}V${svgNumber(y1 - r)}${corner(x1 - r, y1)}H${svgNumber(x)}Z`;
|
|
79
|
+
return `M${pair(x1, y1)}H${svgNumber(x + r)}${corner(x, y1 - r)}V${svgNumber(y + r)}${corner(x + r, y)}H${svgNumber(x1)}Z`;
|
|
80
|
+
}
|
|
81
|
+
/** 半径 r、角度 angle(12 点钟方向顺时针)处的点 */
|
|
82
|
+
function polarPoint(r, angle) {
|
|
83
|
+
return [r * Math.sin(angle), -r * Math.cos(angle)];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 环形一段的路径:两条径向的边、内外两段弧;corner 时四个角各是一段与边、弧都相切的小圆弧
|
|
87
|
+
* (边是径向的,与圆正交,切点可以直接算出)。整圈时画成两段弧(没有边)
|
|
88
|
+
*/
|
|
89
|
+
function arcPath({ inner, outer, start, end, corner = 0 }) {
|
|
90
|
+
const sweep = end - start;
|
|
91
|
+
if (!(sweep > 0) || !(outer > 0)) return "";
|
|
92
|
+
const at = (r, angle) => pair(...polarPoint(r, angle));
|
|
93
|
+
const arc = (r, to, clockwise, span) => `A${pair(r, r)},0,${span > Math.PI ? 1 : 0},${clockwise ? 1 : 0},${at(r, to)}`;
|
|
94
|
+
if (sweep >= Math.PI * 2 - 1e-9) {
|
|
95
|
+
const outerRing = `M${at(outer, 0)}${arc(outer, Math.PI, true, Math.PI)}${arc(outer, 0, true, Math.PI)}Z`;
|
|
96
|
+
if (inner <= 0) return outerRing;
|
|
97
|
+
return `${outerRing}M${at(inner, 0)}${arc(inner, Math.PI, false, Math.PI)}${arc(inner, 0, false, Math.PI)}Z`;
|
|
98
|
+
}
|
|
99
|
+
const r = Math.max(0, Math.min(corner, (outer - inner) / 2, (inner > 0 ? inner : outer) * sweep / 2.2));
|
|
100
|
+
if (r < .5) {
|
|
101
|
+
const outerPart = `M${at(outer, start)}${arc(outer, end, true, sweep)}`;
|
|
102
|
+
if (inner <= 0) return `${outerPart}L0,0Z`;
|
|
103
|
+
return `${outerPart}L${at(inner, end)}${arc(inner, start, false, sweep)}Z`;
|
|
104
|
+
}
|
|
105
|
+
const outerShift = Math.asin(r / (outer - r));
|
|
106
|
+
const outerEdge = Math.sqrt((outer - r) ** 2 - r * r);
|
|
107
|
+
const fillet = (to) => `A${pair(r, r)},0,0,1,${to}`;
|
|
108
|
+
let path = `M${at(outerEdge, start)}${fillet(at(outer, start + outerShift))}${arc(outer, end - outerShift, true, sweep - outerShift * 2)}${fillet(at(outerEdge, end))}`;
|
|
109
|
+
if (inner <= 0) return `${path}L0,0Z`;
|
|
110
|
+
const innerShift = Math.asin(r / (inner + r));
|
|
111
|
+
const innerEdge = Math.sqrt((inner + r) ** 2 - r * r);
|
|
112
|
+
path += `L${at(innerEdge, end)}${fillet(at(inner, end - innerShift))}${arc(inner, start + innerShift, false, sweep - innerShift * 2)}${fillet(at(innerEdge, start))}Z`;
|
|
113
|
+
return path;
|
|
114
|
+
}
|
|
115
|
+
/** 弦图的一根弦:两端是圆上的两段弧(半径 radius,角度同 arcPath),中间经圆心的二次曲线相连(同 d3 的 ribbon) */
|
|
116
|
+
function ribbonPath(radius, source, target) {
|
|
117
|
+
const at = (angle) => pair(...polarPoint(radius, angle));
|
|
118
|
+
const arc = ([from, to]) => `A${pair(radius, radius)},0,${to - from > Math.PI ? 1 : 0},1,${at(to)}`;
|
|
119
|
+
let path = `M${at(source[0])}${arc(source)}`;
|
|
120
|
+
if (source[0] !== target[0] || source[1] !== target[1]) path += `Q0,0,${at(target[0])}${arc(target)}`;
|
|
121
|
+
return `${path}Q0,0,${at(source[0])}Z`;
|
|
122
|
+
}
|
|
123
|
+
/** 横向的流向带(桑基图的连线):从 (x0, y0) 到 (x1, y1) 的 S 形三次曲线,控制点在两端的中间 */
|
|
124
|
+
function horizontalLinkPath(x0, y0, x1, y1) {
|
|
125
|
+
const middle = (x0 + x1) / 2;
|
|
126
|
+
return `M${pair(x0, y0)}C${pair(middle, y0)},${pair(middle, y1)},${pair(x1, y1)}`;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { arcPath, areaPath, barPath, horizontalLinkPath, linePath, polarPoint, ribbonPath };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.js","names":[],"sources":["../../src/charts/shape.ts"],"sourcesContent":["/**\n * 图形的 SVG 路径:折线与面积(直线、单调平滑、阶梯)、一端圆角的柱子、环形的扇区、弦图的弦、桑基图的带子。\n * 曲线的算法同 d3-shape(curveMonotoneX 是 Steffen 的单调三次插值,不会越过数据点),写成只产出路径字符串的精简版。\n * 坐标保留三位小数(svgNumber):服务端与浏览器算出的路径一致。\n *\n * linePath([[0, 10], [40, 30], [80, 20]], 'monotone') // 'M0,10C13.333,…'\n * areaPath(top, bottom, 'linear') // 上沿从左到右、下沿从右到左,闭合\n * arcPath({ inner: 60, outer: 88, start: 0, end: Math.PI / 2, corner: 3 })\n */\nimport { svgNumber } from '../utils/data/number';\n\n/** 一个点 [x, y] */\nexport type ChartPoint = readonly [number, number];\n\n/** 线的形状 */\nexport interface LineCurves {\n /** 折线:相邻的点直线相连(默认) */\n linear: unknown;\n /** 平滑曲线:不会越过数据点(单调插值),趋势更柔和 */\n monotone: unknown;\n /** 阶梯:值保持到下一个点才跳变,适合离散的状态、价格档位 */\n step: unknown;\n}\n\n/** 两个数字写成“x,y” */\nconst pair = (x: number, y: number) => `${svgNumber(x)},${svgNumber(y)}`;\n\nconst sign = (value: number) => (value < 0 ? -1 : 1);\n\n/** 中间一点的切线斜率:两侧的斜率异号时为 0(极值点不越过),否则取不超过两侧的调和值(同 d3 的 slope3) */\nfunction interiorSlope(a: ChartPoint, b: ChartPoint, c: ChartPoint): number {\n const h0 = b[0] - a[0];\n const h1 = c[0] - b[0];\n // 横坐标相同时除以带符号的 0:得到同向的无穷大,下面取 min 时自然被别的值替代\n const s0 = (b[1] - a[1]) / (h0 || (h1 < 0 ? -0 : 0));\n const s1 = (c[1] - b[1]) / (h1 || (h0 < 0 ? -0 : 0));\n const p = (s0 * h1 + s1 * h0) / (h0 + h1);\n return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;\n}\n\n/** 端点的切线斜率:由相邻一段的斜率与相邻点的切线推出(同 d3 的 slope2) */\nfunction endSlope(a: ChartPoint, b: ChartPoint, tangent: number): number {\n const h = b[0] - a[0];\n return h ? ((3 * (b[1] - a[1])) / h - tangent) / 2 : tangent;\n}\n\n/** 单调三次插值:各点的切线,再按 Hermite 写成三次贝塞尔 */\nfunction monotoneCommands(points: readonly ChartPoint[]): string {\n const n = points.length;\n const tangents = points.map((point, i) =>\n i > 0 && i < n - 1 ? interiorSlope(points[i - 1]!, point, points[i + 1]!) : 0,\n );\n tangents[0] = endSlope(points[0]!, points[1]!, tangents[1]!);\n tangents[n - 1] = endSlope(points[n - 2]!, points[n - 1]!, tangents[n - 2]!);\n let path = '';\n for (let i = 1; i < n; i += 1) {\n const [x0, y0] = points[i - 1]!;\n const [x1, y1] = points[i]!;\n const dx = (x1 - x0) / 3;\n path += `C${pair(x0 + dx, y0 + dx * tangents[i - 1]!)},${pair(x1 - dx, y1 - dx * tangents[i]!)},${pair(x1, y1)}`;\n }\n return path;\n}\n\n/**\n * 一串点连成的路径;start 是开头的命令(M 新起一段,L 接着上一段画:面积的下沿)。\n * 少于三个点时平滑曲线退化成直线\n */\nexport function linePath(\n points: readonly ChartPoint[],\n curve: keyof LineCurves = 'linear',\n start: 'M' | 'L' = 'M',\n): string {\n if (points.length === 0) return '';\n const [first, ...rest] = points;\n let path = `${start}${pair(first![0], first![1])}`;\n if (curve === 'monotone' && points.length > 2) return path + monotoneCommands(points);\n let previous = first!;\n for (const point of rest) {\n // 阶梯:横着走到下一个点的横坐标,再竖着跳到它的值\n if (curve === 'step') path += `L${pair(point[0], previous[1])}`;\n path += `L${pair(point[0], point[1])}`;\n previous = point;\n }\n return path;\n}\n\n/** 面积:上沿 top 从左到右,下沿 bottom(与 top 一一对应)从右到左,同一种曲线,闭合 */\nexport function areaPath(\n top: readonly ChartPoint[],\n bottom: readonly ChartPoint[],\n curve: keyof LineCurves = 'linear',\n): string {\n if (top.length === 0) return '';\n // 阶梯的下沿倒着画时要先竖后横,才与上沿的拐角对齐:倒着画的阶梯把每一步的值挪到前一个点\n const back =\n curve === 'step'\n ? bottom.flatMap((point, i) =>\n i === 0 ? [point] : [[point[0], bottom[i - 1]![1]] as const, point],\n )\n : bottom;\n return `${linePath(top, curve)}${linePath([...back].reverse(), curve === 'step' ? 'linear' : curve, 'L')}Z`;\n}\n\n/** 柱子的哪一端是圆角:柱顶朝哪个方向 */\nexport type BarEnd = 'top' | 'bottom' | 'left' | 'right' | 'none';\n\n/** 柱子:矩形(左上角 x、y,宽高),end 那一端的两个角是半径 radius 的圆角(不超过宽或高的一半) */\nexport function barPath(\n x: number,\n y: number,\n width: number,\n height: number,\n radius: number,\n end: BarEnd,\n): string {\n const w = Math.max(width, 0);\n const h = Math.max(height, 0);\n const vertical = end === 'top' || end === 'bottom';\n const r =\n end === 'none' ? 0 : Math.max(0, Math.min(radius, vertical ? w / 2 : h / 2, vertical ? h : w));\n const [x1, y1] = [x + w, y + h];\n const corner = (cx: number, cy: number) => `A${pair(r, r)},0,0,1,${pair(cx, cy)}`;\n if (!r) return `M${pair(x, y)}H${svgNumber(x1)}V${svgNumber(y1)}H${svgNumber(x)}Z`;\n if (end === 'top')\n return `M${pair(x, y1)}V${svgNumber(y + r)}${corner(x + r, y)}H${svgNumber(x1 - r)}${corner(x1, y + r)}V${svgNumber(y1)}Z`;\n if (end === 'bottom')\n return `M${pair(x1, y)}V${svgNumber(y1 - r)}${corner(x1 - r, y1)}H${svgNumber(x + r)}${corner(x, y1 - r)}V${svgNumber(y)}Z`;\n if (end === 'right')\n return `M${pair(x, y)}H${svgNumber(x1 - r)}${corner(x1, y + r)}V${svgNumber(y1 - r)}${corner(x1 - r, y1)}H${svgNumber(x)}Z`;\n return `M${pair(x1, y1)}H${svgNumber(x + r)}${corner(x, y1 - r)}V${svgNumber(y + r)}${corner(x + r, y)}H${svgNumber(x1)}Z`;\n}\n\n/** 环形的一段:角度从 12 点钟方向顺时针(弧度),圆心在原点 */\nexport interface ArcShape {\n /** 内半径;0 是饼图的扇形 */\n inner: number;\n /** 外半径 */\n outer: number;\n /** 起始角 */\n start: number;\n /** 结束角 */\n end: number;\n /** 四个角的圆角半径,默认 0 */\n corner?: number;\n}\n\n/** 半径 r、角度 angle(12 点钟方向顺时针)处的点 */\nexport function polarPoint(r: number, angle: number): [number, number] {\n return [r * Math.sin(angle), -r * Math.cos(angle)];\n}\n\n/**\n * 环形一段的路径:两条径向的边、内外两段弧;corner 时四个角各是一段与边、弧都相切的小圆弧\n * (边是径向的,与圆正交,切点可以直接算出)。整圈时画成两段弧(没有边)\n */\nexport function arcPath({ inner, outer, start, end, corner = 0 }: ArcShape): string {\n const sweep = end - start;\n if (!(sweep > 0) || !(outer > 0)) return '';\n const at = (r: number, angle: number) => pair(...polarPoint(r, angle));\n const arc = (r: number, to: number, clockwise: boolean, span: number) =>\n `A${pair(r, r)},0,${span > Math.PI ? 1 : 0},${clockwise ? 1 : 0},${at(r, to)}`;\n if (sweep >= Math.PI * 2 - 1e-9) {\n const outerRing = `M${at(outer, 0)}${arc(outer, Math.PI, true, Math.PI)}${arc(outer, 0, true, Math.PI)}Z`;\n if (inner <= 0) return outerRing;\n return `${outerRing}M${at(inner, 0)}${arc(inner, Math.PI, false, Math.PI)}${arc(inner, 0, false, Math.PI)}Z`;\n }\n // 圆角不超过环宽的一半,也不超过这一段在内(饼图是外)弧上长度的一半\n const r = Math.max(\n 0,\n Math.min(corner, (outer - inner) / 2, ((inner > 0 ? inner : outer) * sweep) / 2.2),\n );\n if (r < 0.5) {\n const outerPart = `M${at(outer, start)}${arc(outer, end, true, sweep)}`;\n if (inner <= 0) return `${outerPart}L0,0Z`;\n return `${outerPart}L${at(inner, end)}${arc(inner, start, false, sweep)}Z`;\n }\n // 外角:小圆心在半径 outer - r 上,切边的点离圆心 √((outer-r)² - r²),切弧的点偏开 asin(r / (outer - r))\n const outerShift = Math.asin(r / (outer - r));\n const outerEdge = Math.sqrt((outer - r) ** 2 - r * r);\n const fillet = (to: string) => `A${pair(r, r)},0,0,1,${to}`;\n let path =\n `M${at(outerEdge, start)}${fillet(at(outer, start + outerShift))}` +\n `${arc(outer, end - outerShift, true, sweep - outerShift * 2)}${fillet(at(outerEdge, end))}`;\n if (inner <= 0) return `${path}L0,0Z`;\n // 内角:小圆心在半径 inner + r 上\n const innerShift = Math.asin(r / (inner + r));\n const innerEdge = Math.sqrt((inner + r) ** 2 - r * r);\n path +=\n `L${at(innerEdge, end)}${fillet(at(inner, end - innerShift))}` +\n `${arc(inner, start + innerShift, false, sweep - innerShift * 2)}${fillet(at(innerEdge, start))}Z`;\n return path;\n}\n\n/** 弦图的一根弦:两端是圆上的两段弧(半径 radius,角度同 arcPath),中间经圆心的二次曲线相连(同 d3 的 ribbon) */\nexport function ribbonPath(\n radius: number,\n source: readonly [number, number],\n target: readonly [number, number],\n): string {\n const at = (angle: number) => pair(...polarPoint(radius, angle));\n const arc = ([from, to]: readonly [number, number]) =>\n `A${pair(radius, radius)},0,${to - from > Math.PI ? 1 : 0},1,${at(to)}`;\n let path = `M${at(source[0])}${arc(source)}`;\n if (source[0] !== target[0] || source[1] !== target[1])\n path += `Q0,0,${at(target[0])}${arc(target)}`;\n return `${path}Q0,0,${at(source[0])}Z`;\n}\n\n/** 横向的流向带(桑基图的连线):从 (x0, y0) 到 (x1, y1) 的 S 形三次曲线,控制点在两端的中间 */\nexport function horizontalLinkPath(x0: number, y0: number, x1: number, y1: number): string {\n const middle = (x0 + x1) / 2;\n return `M${pair(x0, y0)}C${pair(middle, y0)},${pair(middle, y1)},${pair(x1, y1)}`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 堆叠:各系列在同一类目上依次叠起,正值从 0 往上叠、负值从 0 往下叠(同 d3 的 stackOffsetDiverging)。
|
|
3
|
+
* 柱状图、面积图、组合图的堆叠与十字线圆点、标注圈的位置共用。
|
|
4
|
+
*
|
|
5
|
+
* stackValues([[3, 5], [2, -1]]) // 系列 0:[[0,3], [0,5]];系列 1:[[3,5], [-1,0]]
|
|
6
|
+
*/
|
|
7
|
+
/** 一个系列在一行上叠出的一段 */
|
|
8
|
+
export interface StackSegment {
|
|
9
|
+
/** 下沿(负值是靠近 0 的一端以外的那端) */
|
|
10
|
+
low: number;
|
|
11
|
+
/** 上沿 */
|
|
12
|
+
high: number;
|
|
13
|
+
/** 原值;空值是 undefined,这时 low、high 都在当前的正向累计处(高度为 0,面积连续) */
|
|
14
|
+
value: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** values[系列][行] → 每个系列在每一行的一段 */
|
|
17
|
+
export declare function stackValues(values: readonly (readonly (number | undefined)[])[]): StackSegment[][];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/charts/stack.ts
|
|
2
|
+
/** values[系列][行] → 每个系列在每一行的一段 */
|
|
3
|
+
function stackValues(values) {
|
|
4
|
+
const rows = Math.max(0, ...values.map((list) => list.length));
|
|
5
|
+
const positive = Array.from({ length: rows }, () => 0);
|
|
6
|
+
const negative = Array.from({ length: rows }, () => 0);
|
|
7
|
+
return values.map((list) => Array.from({ length: rows }, (_, row) => {
|
|
8
|
+
const value = list[row];
|
|
9
|
+
if (value === void 0 || !Number.isFinite(value)) return {
|
|
10
|
+
low: positive[row],
|
|
11
|
+
high: positive[row],
|
|
12
|
+
value: void 0
|
|
13
|
+
};
|
|
14
|
+
if (value >= 0) {
|
|
15
|
+
const low = positive[row];
|
|
16
|
+
positive[row] = low + value;
|
|
17
|
+
return {
|
|
18
|
+
low,
|
|
19
|
+
high: low + value,
|
|
20
|
+
value
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const high = negative[row];
|
|
24
|
+
negative[row] = high + value;
|
|
25
|
+
return {
|
|
26
|
+
low: high + value,
|
|
27
|
+
high,
|
|
28
|
+
value
|
|
29
|
+
};
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { stackValues };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack.js","names":[],"sources":["../../src/charts/stack.ts"],"sourcesContent":["/**\n * 堆叠:各系列在同一类目上依次叠起,正值从 0 往上叠、负值从 0 往下叠(同 d3 的 stackOffsetDiverging)。\n * 柱状图、面积图、组合图的堆叠与十字线圆点、标注圈的位置共用。\n *\n * stackValues([[3, 5], [2, -1]]) // 系列 0:[[0,3], [0,5]];系列 1:[[3,5], [-1,0]]\n */\n\n/** 一个系列在一行上叠出的一段 */\nexport interface StackSegment {\n /** 下沿(负值是靠近 0 的一端以外的那端) */\n low: number;\n /** 上沿 */\n high: number;\n /** 原值;空值是 undefined,这时 low、high 都在当前的正向累计处(高度为 0,面积连续) */\n value: number | undefined;\n}\n\n/** values[系列][行] → 每个系列在每一行的一段 */\nexport function stackValues(\n values: readonly (readonly (number | undefined)[])[],\n): StackSegment[][] {\n const rows = Math.max(0, ...values.map((list) => list.length));\n const positive = Array.from({ length: rows }, () => 0);\n const negative = Array.from({ length: rows }, () => 0);\n return values.map((list) =>\n Array.from({ length: rows }, (_, row) => {\n const value = list[row];\n if (value === undefined || !Number.isFinite(value))\n return { low: positive[row]!, high: positive[row]!, value: undefined };\n if (value >= 0) {\n const low = positive[row]!;\n positive[row] = low + value;\n return { low, high: low + value, value };\n }\n const high = negative[row]!;\n negative[row] = high + value;\n return { low: high + value, high, value };\n }),\n );\n}\n"],"mappings":";;AAkBA,SAAgB,YACd,QACkB;CAClB,MAAM,OAAO,KAAK,IAAI,GAAG,GAAG,OAAO,KAAK,SAAS,KAAK,MAAM,CAAC;CAC7D,MAAM,WAAW,MAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,CAAC;CACrD,MAAM,WAAW,MAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,CAAC;CACrD,OAAO,OAAO,KAAK,SACjB,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,QAAQ;EACvC,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,KAAa,CAAC,OAAO,SAAS,KAAK,GAC/C,OAAO;GAAE,KAAK,SAAS;GAAO,MAAM,SAAS;GAAO,OAAO,KAAA;EAAU;EACvE,IAAI,SAAS,GAAG;GACd,MAAM,MAAM,SAAS;GACrB,SAAS,OAAO,MAAM;GACtB,OAAO;IAAE;IAAK,MAAM,MAAM;IAAO;GAAM;EACzC;EACA,MAAM,OAAO,SAAS;EACtB,SAAS,OAAO,OAAO;EACvB,OAAO;GAAE,KAAK,OAAO;GAAO;GAAM;EAAM;CAC1C,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表里文字的宽度估算:留坐标轴的边距、决定类目刻度隔几个写一个、地点名字的避让都要在画之前知道文字多宽。
|
|
3
|
+
* 不去量 DOM(服务端渲染、还没挂上时也要算):中日韩文字、全角符号按一个字号宽,其余按 0.6 个字号宽,
|
|
4
|
+
* 宁宽勿窄(宽了只是多留一点空,窄了会叠在一起)。
|
|
5
|
+
*
|
|
6
|
+
* estimateTextWidth('华东区 2026', 12) // 3 × 12 + 5 × 7.2 = 72
|
|
7
|
+
* truncateText('很长很长的类目名', 60, 12) // '很长很长…'
|
|
8
|
+
*/
|
|
9
|
+
/** 估算文字的宽度(px);fontSize 是字号(px),默认 12(主题的 xs) */
|
|
10
|
+
export declare function estimateTextWidth(text: string, fontSize?: number): number;
|
|
11
|
+
/** 文字超过 maxWidth 时截短并加省略号;放得下时原样返回 */
|
|
12
|
+
export declare function truncateText(text: string, maxWidth: number, fontSize?: number): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/charts/text-size.ts
|
|
2
|
+
/**
|
|
3
|
+
* 图表里文字的宽度估算:留坐标轴的边距、决定类目刻度隔几个写一个、地点名字的避让都要在画之前知道文字多宽。
|
|
4
|
+
* 不去量 DOM(服务端渲染、还没挂上时也要算):中日韩文字、全角符号按一个字号宽,其余按 0.6 个字号宽,
|
|
5
|
+
* 宁宽勿窄(宽了只是多留一点空,窄了会叠在一起)。
|
|
6
|
+
*
|
|
7
|
+
* estimateTextWidth('华东区 2026', 12) // 3 × 12 + 5 × 7.2 = 72
|
|
8
|
+
* truncateText('很长很长的类目名', 60, 12) // '很长很长…'
|
|
9
|
+
*/
|
|
10
|
+
/** 按一个字号宽算的字:中日韩文字、全角标点、假名、谚文 */
|
|
11
|
+
var WIDE = /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]/u;
|
|
12
|
+
/** 估算文字的宽度(px);fontSize 是字号(px),默认 12(主题的 xs) */
|
|
13
|
+
function estimateTextWidth(text, fontSize = 12) {
|
|
14
|
+
let width = 0;
|
|
15
|
+
for (const char of text) width += WIDE.test(char) ? fontSize : fontSize * .6;
|
|
16
|
+
return width;
|
|
17
|
+
}
|
|
18
|
+
/** 文字超过 maxWidth 时截短并加省略号;放得下时原样返回 */
|
|
19
|
+
function truncateText(text, maxWidth, fontSize = 12) {
|
|
20
|
+
if (estimateTextWidth(text, fontSize) <= maxWidth) return text;
|
|
21
|
+
const room = maxWidth - fontSize;
|
|
22
|
+
let result = "";
|
|
23
|
+
for (const char of text) {
|
|
24
|
+
if (estimateTextWidth(result + char, fontSize) > room) break;
|
|
25
|
+
result += char;
|
|
26
|
+
}
|
|
27
|
+
return `${result}…`;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { estimateTextWidth, truncateText };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=text-size.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-size.js","names":[],"sources":["../../src/charts/text-size.ts"],"sourcesContent":["/**\n * 图表里文字的宽度估算:留坐标轴的边距、决定类目刻度隔几个写一个、地点名字的避让都要在画之前知道文字多宽。\n * 不去量 DOM(服务端渲染、还没挂上时也要算):中日韩文字、全角符号按一个字号宽,其余按 0.6 个字号宽,\n * 宁宽勿窄(宽了只是多留一点空,窄了会叠在一起)。\n *\n * estimateTextWidth('华东区 2026', 12) // 3 × 12 + 5 × 7.2 = 72\n * truncateText('很长很长的类目名', 60, 12) // '很长很长…'\n */\n\n/** 按一个字号宽算的字:中日韩文字、全角标点、假名、谚文 */\nconst WIDE = /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]/u;\n\n/** 估算文字的宽度(px);fontSize 是字号(px),默认 12(主题的 xs) */\nexport function estimateTextWidth(text: string, fontSize = 12): number {\n let width = 0;\n for (const char of text) width += WIDE.test(char) ? fontSize : fontSize * 0.6;\n return width;\n}\n\n/** 文字超过 maxWidth 时截短并加省略号;放得下时原样返回 */\nexport function truncateText(text: string, maxWidth: number, fontSize = 12): string {\n if (estimateTextWidth(text, fontSize) <= maxWidth) return text;\n const room = maxWidth - fontSize;\n let result = '';\n for (const char of text) {\n if (estimateTextWidth(result + char, fontSize) > room) break;\n result += char;\n }\n return `${result}…`;\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAM,OAAO;;AAGb,SAAgB,kBAAkB,MAAc,WAAW,IAAY;CACrE,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,MAAM,SAAS,KAAK,KAAK,IAAI,IAAI,WAAW,WAAW;CAC1E,OAAO;AACT;;AAGA,SAAgB,aAAa,MAAc,UAAkB,WAAW,IAAY;CAClF,IAAI,kBAAkB,MAAM,QAAQ,KAAK,UAAU,OAAO;CAC1D,MAAM,OAAO,WAAW;CACxB,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,MAAM;EACvB,IAAI,kBAAkB,SAAS,MAAM,QAAQ,IAAI,MAAM;EACvD,UAAU;CACZ;CACA,OAAO,GAAG,OAAO;AACnB"}
|
|
@@ -10,7 +10,7 @@ import { type ComponentStyles } from './styles';
|
|
|
10
10
|
import { type Lang } from './lang/lang';
|
|
11
11
|
import type { WorkCalendar } from '../utils/data/workdays';
|
|
12
12
|
import type { HttpClient } from '../utils/request/http';
|
|
13
|
-
import { type ThemeInput } from './theme/theme';
|
|
13
|
+
import { type Theme, type ThemeInput } from './theme/theme';
|
|
14
14
|
/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */
|
|
15
15
|
export interface UiOptions {
|
|
16
16
|
/** 主题;不写时用亮色主题,也不改动页面根元素 */
|
|
@@ -49,11 +49,21 @@ export declare const documentStyles: Readonly<{
|
|
|
49
49
|
}>;
|
|
50
50
|
/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */
|
|
51
51
|
export interface DocumentAttrs {
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir。
|
|
54
|
+
* data-theme-classes 记下主题的类,防闪烁脚本和浏览器接管时据此换掉,不留下旧主题的类
|
|
55
|
+
*/
|
|
53
56
|
html: Record<string, string>;
|
|
54
57
|
/** `<body>` 的属性:paintDocument 时去掉外边距的类 */
|
|
55
58
|
body: Record<string, string>;
|
|
56
59
|
}
|
|
60
|
+
/** themeClassBoot 的结果 */
|
|
61
|
+
export interface ThemeClassBoot {
|
|
62
|
+
/** 内联到 <head> 的脚本,放在 themeBootScript 之后 */
|
|
63
|
+
script: string;
|
|
64
|
+
/** 候选主题的类:交给 renderStyleTag 的 include,它们的规则才在首屏样式里 */
|
|
65
|
+
classes: readonly string[];
|
|
66
|
+
}
|
|
57
67
|
/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */
|
|
58
68
|
export interface UiPlugin extends ObjectPlugin {
|
|
59
69
|
/**
|
|
@@ -61,6 +71,15 @@ export interface UiPlugin extends ObjectPlugin {
|
|
|
61
71
|
* 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来
|
|
62
72
|
*/
|
|
63
73
|
documentAttrs(): DocumentAttrs;
|
|
74
|
+
/**
|
|
75
|
+
* 服务端渲染的防闪烁:`themeBootScript` 只设底色和文字色,正文里的组件读的是主题变量,要把 <html> 上的主题类也换掉,
|
|
76
|
+
* 否则暗色系统上会先看到暗底配亮色主题的深色字,要等水合才对。主题按这里给的候选表选,规则随首屏样式一起输出:
|
|
77
|
+
*
|
|
78
|
+
* const boot = ui.themeClassBoot(builtinThemes);
|
|
79
|
+
* const styles = renderStyleTag(page, { include: boot.classes });
|
|
80
|
+
* // <head>:<script>themeBootScript(…)</script> <script>boot.script</script> styles
|
|
81
|
+
*/
|
|
82
|
+
themeClassBoot(themes: Readonly<Record<string, Theme>>): ThemeClassBoot;
|
|
64
83
|
}
|
|
65
84
|
/**
|
|
66
85
|
* 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。
|
package/dist/config/createUi.js
CHANGED
|
@@ -39,7 +39,7 @@ function createUi(options = {}) {
|
|
|
39
39
|
if (options.theme || options.paintDocument) {
|
|
40
40
|
const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);
|
|
41
41
|
const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);
|
|
42
|
-
if (classes.length) html.class = classes.join(" ");
|
|
42
|
+
if (classes.length) html.class = html["data-theme-classes"] = classes.join(" ");
|
|
43
43
|
const style = Object.entries(variables).map(([name, value]) => `${name}:${value}`).join(";");
|
|
44
44
|
if (style) html.style = style;
|
|
45
45
|
html["data-theme"] = theme.name;
|
|
@@ -50,6 +50,13 @@ function createUi(options = {}) {
|
|
|
50
50
|
body
|
|
51
51
|
};
|
|
52
52
|
},
|
|
53
|
+
themeClassBoot(themes) {
|
|
54
|
+
const table = Object.fromEntries(Object.entries(themes).map(([name, theme]) => [name, rootThemeAttrs(theme, options.paintDocument ?? false)]));
|
|
55
|
+
return {
|
|
56
|
+
script: themeClassBootScript(table),
|
|
57
|
+
classes: [...new Set(Object.values(table).flatMap(({ classes }) => classes))]
|
|
58
|
+
};
|
|
59
|
+
},
|
|
53
60
|
install(app) {
|
|
54
61
|
const scope = effectScope();
|
|
55
62
|
app.onUnmount(() => scope.stop());
|
|
@@ -90,6 +97,9 @@ function createUi(options = {}) {
|
|
|
90
97
|
const root = document.documentElement;
|
|
91
98
|
if (options.paintDocument) root.style.removeProperty("background-color");
|
|
92
99
|
if (options.paintDocument) root.style.removeProperty("color");
|
|
100
|
+
const rendered = root.dataset.themeClasses;
|
|
101
|
+
if (rendered) root.classList.remove(...rendered.split(" "));
|
|
102
|
+
delete root.dataset.themeClasses;
|
|
93
103
|
watchEffect((onCleanup) => onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)));
|
|
94
104
|
if (options.paintDocument && document.body) {
|
|
95
105
|
const body = document.body;
|
|
@@ -109,6 +119,13 @@ function rootThemeAttrs(theme, paint) {
|
|
|
109
119
|
variables: themeVariables(theme)
|
|
110
120
|
};
|
|
111
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* 防闪烁脚本的第二段:themeBootScript 已按用户的偏好写好 data-theme,这里把服务端写的主题类换成那套主题的。
|
|
124
|
+
* data-theme 不在候选表里时不动(用户自己定制的主题,水合后才有)
|
|
125
|
+
*/
|
|
126
|
+
function themeClassBootScript(themes) {
|
|
127
|
+
return `(()=>{const c=${JSON.stringify(themes).replaceAll("<", "\\u003c")};const r=document.documentElement;const t=c[r.dataset.theme];if(!t)return;const o=r.dataset.themeClasses;if(o)r.classList.remove(...o.split(' '));r.classList.add(...t.classes);r.dataset.themeClasses=t.classes.join(' ');for(const n in t.variables)r.style.setProperty(n,t.variables[n])})()`;
|
|
128
|
+
}
|
|
112
129
|
/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */
|
|
113
130
|
function applyTheme(element, theme, paint) {
|
|
114
131
|
const { classes, variables } = rootThemeAttrs(theme, paint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUi.js","names":[],"sources":["../../src/config/createUi.ts"],"sourcesContent":["/**\n * 应用级配置:在宿主入口里 `app.use(createUi({ theme, lang }))`,所有 TSX 页面共享,不必逐页包裹 Provider。\n * 传入 ref 或 getter 即可在运行时切换主题与语言。\n */\nimport {\n computed,\n effectScope,\n toValue,\n watchEffect,\n type MaybeRefOrGetter,\n type ObjectPlugin,\n} from 'vue';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport {\n ConfigContext,\n defaultConfig,\n type Highlighter,\n type TextMatcher,\n type UiPermissions,\n type UiRouter,\n} from './context';\nimport type { ComponentDefaults } from './defaults';\nimport { mergeStyles, type ComponentStyles } from './styles';\nimport { defineLang, type Lang } from './lang/lang';\nimport { usePrefersDark } from '../utils/dom/media-query';\nimport type { WorkCalendar } from '../utils/data/workdays';\nimport type { HttpClient } from '../utils/request/http';\nimport { resolveTheme, themeVariables, type Theme, type ThemeInput } from './theme/theme';\n\n/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */\nexport interface UiOptions {\n /** 主题;不写时用亮色主题,也不改动页面根元素 */\n theme?: MaybeRefOrGetter<ThemeInput>;\n /** 只写要改的字段,其余用简体中文的默认值 */\n lang?: MaybeRefOrGetter<Partial<Lang>>;\n /** 代码高亮器(CodeBlock 用),如 `@zerodep-dev/vue-tsx-pro-ui/shiki` 的 createShikiHighlighter();不写时显示纯文本 */\n highlighter?: Highlighter;\n /** 额外的文字匹配,如 `@zerodep-dev/vue-tsx-pro-ui/pinyin` 的 createPinyinMatcher():选项过滤、命令面板认拼音;不写时只按字面匹配 */\n textMatcher?: TextMatcher;\n /** 组件默认值,如 `{ Button: { variant: 'outline' } }`(见 defaults.ts) */\n defaults?: ComponentDefaults;\n /** 组件的主题定制:追加样式、新增档位,如 `{ Button: { base: { root: { letterSpacing: '0.02em' } } } }`(见 styles.ts) */\n styles?: ComponentStyles;\n /** 路由适配:站内链接不刷新页面地跳转、导航组件判断当前页(见 UiRouter) */\n router?: UiRouter;\n /** 权限判断:`(权限点) => 用户有没有`,Can、useCan 据此控制按钮等界面;不写时一律放行(见 UiPermissions) */\n permissions?: UiPermissions;\n /** 请求客户端:createHttpClient({ baseURL, onRequest, onResponse }),或包一层 axios、ky(见 HttpClient) */\n http?: HttpClient;\n /** 工作日历:createWorkCalendar({ holidays, workdays }) 带上法定节假日与调休;不写时只按周末算 */\n workCalendar?: WorkCalendar;\n /** 按下时的波纹效果,默认 true;关掉后组件的 ripple prop 仍可单独打开 */\n ripple?: boolean;\n /**\n * 页面根元素也用主题的背景色、文字色与字体,并去掉 body 的默认外边距,默认 false。\n * 整页都由组件库主题管理的站点(如文档站)打开:页面边缘、回弹滚动露出的地方不再是浏览器的默认色,\n * AppShell 贴满窗口、四周不留 8px 的白边;配合 themeBootScript 时接手它写的行内颜色\n */\n paintDocument?: boolean;\n}\n\n/** paintDocument 时页面根元素的样式 */\nexport const documentStyles = css.create({\n // 字号不设:根元素的字号是 rem 的基准,留给浏览器(用户的字号设置)\n page: {\n backgroundColor: '_background',\n color: '_text',\n fontFamily: '_sans',\n lineHeight: '_normal',\n // macOS 上用灰度抗锯齿,深色底上的浅色文字不发虚;其他系统忽略\n WebkitFontSmoothing: 'antialiased',\n MozOsxFontSmoothing: 'grayscale',\n },\n // 浏览器给 body 的 8px 外边距:整页的外壳(AppShell)要贴满窗口\n body: { margin: 0 },\n});\n\n/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */\nexport interface DocumentAttrs {\n /** `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir */\n html: Record<string, string>;\n /** `<body>` 的属性:paintDocument 时去掉外边距的类 */\n body: Record<string, string>;\n}\n\n/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */\nexport interface UiPlugin extends ObjectPlugin {\n /**\n * 服务端渲染:`<html>`、`<body>` 要带的属性,首屏就是这套主题与语言,不等客户端接管后再变。\n * 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来\n */\n documentAttrs(): DocumentAttrs;\n}\n\n/**\n * 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。\n * 主题的 CSS 变量写在页面根元素上,所有 TSX 页面共享;应用卸载时撤销。\n */\nexport function createUi(options: UiOptions = {}): UiPlugin {\n return {\n documentAttrs() {\n const html: Record<string, string> = {};\n const body: Record<string, string> = {};\n if (options.lang) {\n const lang = defineLang(defaultConfig.lang, toValue(options.lang));\n html.lang = lang.code;\n html.dir = lang.direction;\n }\n if (options.theme || options.paintDocument) {\n const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);\n const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);\n if (classes.length) html.class = classes.join(' ');\n const style = Object.entries(variables)\n .map(([name, value]) => `${name}:${value}`)\n .join(';');\n if (style) html.style = style;\n html['data-theme'] = theme.name;\n if (options.paintDocument) body.class = css.attrs(documentStyles.body).class ?? '';\n }\n return { html, body };\n },\n install(app) {\n const scope = effectScope();\n app.onUnmount(() => scope.stop());\n scope.run(() => {\n const prefersDark = usePrefersDark();\n const theme = computed(() =>\n resolveTheme(toValue(options.theme) ?? defaultConfig.theme, prefersDark.value),\n );\n const lang = computed(() => defineLang(defaultConfig.lang, toValue(options.lang) ?? {}));\n app.provide(ConfigContext.key, {\n get theme() {\n return theme.value;\n },\n get lang() {\n return lang.value;\n },\n highlighter: options.highlighter,\n textMatcher: options.textMatcher,\n defaults: options.defaults ?? {},\n styles: mergeStyles({}, options.styles),\n router: options.router,\n permissions: options.permissions,\n http: options.http,\n workCalendar: options.workCalendar,\n ripple: options.ripple ?? true,\n });\n // 应用级的语言也写在页面根元素上(Provider 写在自己的元素上):读屏按 lang 选发音,\n // 从右到左的语言靠 dir 镜像排版与滚动条;没传 lang 时不动,宿主自己管\n if (options.lang && typeof document !== 'undefined') {\n const root = document.documentElement;\n const previous = { lang: root.lang, dir: root.dir };\n watchEffect(() => {\n root.lang = lang.value.code;\n root.dir = lang.value.direction;\n });\n app.onUnmount(() => Object.assign(root, previous));\n }\n // 应用级没有容器元素,主题变量挂在页面根元素上;变量名带 vtp- 前缀,不会与宿主冲突。\n // 只开 paintDocument、不传主题时用默认主题(整页也要有底色、字体,body 去掉外边距)\n if ((options.theme || options.paintDocument) && typeof document !== 'undefined') {\n const root = document.documentElement;\n // themeBootScript 在应用加载前写的行内颜色:交给主题的 class\n if (options.paintDocument) root.style.removeProperty('background-color');\n if (options.paintDocument) root.style.removeProperty('color');\n watchEffect((onCleanup) =>\n onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)),\n );\n if (options.paintDocument && document.body) {\n const body = document.body;\n const classes = css.attrs(documentStyles.body).class?.split(' ') ?? [];\n body.classList.add(...classes);\n app.onUnmount(() => body.classList.remove(...classes));\n }\n }\n });\n },\n };\n}\n\n/** 主题挂到页面根元素上的类与 CSS 变量;paint 时加上整页的底色与字体(浏览器里写到元素上,服务端写进 HTML) */\nfunction rootThemeAttrs(theme: Theme, paint: boolean) {\n const classes =\n css\n .attrs(...theme.styles, paint && documentStyles.page)\n .class?.split(' ')\n .filter(Boolean) ?? [];\n return { classes, variables: themeVariables(theme) };\n}\n\n/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */\nfunction applyTheme(element: HTMLElement, theme: Theme, paint: boolean) {\n const { classes, variables } = rootThemeAttrs(theme, paint);\n element.classList.add(...classes);\n for (const [name, value] of Object.entries(variables)) element.style.setProperty(name, value);\n element.dataset.theme = theme.name;\n return () => {\n element.classList.remove(...classes);\n for (const name of Object.keys(variables)) element.style.removeProperty(name);\n delete element.dataset.theme;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createUi.js","names":[],"sources":["../../src/config/createUi.ts"],"sourcesContent":["/**\n * 应用级配置:在宿主入口里 `app.use(createUi({ theme, lang }))`,所有 TSX 页面共享,不必逐页包裹 Provider。\n * 传入 ref 或 getter 即可在运行时切换主题与语言。\n */\nimport {\n computed,\n effectScope,\n toValue,\n watchEffect,\n type MaybeRefOrGetter,\n type ObjectPlugin,\n} from 'vue';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport {\n ConfigContext,\n defaultConfig,\n type Highlighter,\n type TextMatcher,\n type UiPermissions,\n type UiRouter,\n} from './context';\nimport type { ComponentDefaults } from './defaults';\nimport { mergeStyles, type ComponentStyles } from './styles';\nimport { defineLang, type Lang } from './lang/lang';\nimport { usePrefersDark } from '../utils/dom/media-query';\nimport type { WorkCalendar } from '../utils/data/workdays';\nimport type { HttpClient } from '../utils/request/http';\nimport { resolveTheme, themeVariables, type Theme, type ThemeInput } from './theme/theme';\n\n/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */\nexport interface UiOptions {\n /** 主题;不写时用亮色主题,也不改动页面根元素 */\n theme?: MaybeRefOrGetter<ThemeInput>;\n /** 只写要改的字段,其余用简体中文的默认值 */\n lang?: MaybeRefOrGetter<Partial<Lang>>;\n /** 代码高亮器(CodeBlock 用),如 `@zerodep-dev/vue-tsx-pro-ui/shiki` 的 createShikiHighlighter();不写时显示纯文本 */\n highlighter?: Highlighter;\n /** 额外的文字匹配,如 `@zerodep-dev/vue-tsx-pro-ui/pinyin` 的 createPinyinMatcher():选项过滤、命令面板认拼音;不写时只按字面匹配 */\n textMatcher?: TextMatcher;\n /** 组件默认值,如 `{ Button: { variant: 'outline' } }`(见 defaults.ts) */\n defaults?: ComponentDefaults;\n /** 组件的主题定制:追加样式、新增档位,如 `{ Button: { base: { root: { letterSpacing: '0.02em' } } } }`(见 styles.ts) */\n styles?: ComponentStyles;\n /** 路由适配:站内链接不刷新页面地跳转、导航组件判断当前页(见 UiRouter) */\n router?: UiRouter;\n /** 权限判断:`(权限点) => 用户有没有`,Can、useCan 据此控制按钮等界面;不写时一律放行(见 UiPermissions) */\n permissions?: UiPermissions;\n /** 请求客户端:createHttpClient({ baseURL, onRequest, onResponse }),或包一层 axios、ky(见 HttpClient) */\n http?: HttpClient;\n /** 工作日历:createWorkCalendar({ holidays, workdays }) 带上法定节假日与调休;不写时只按周末算 */\n workCalendar?: WorkCalendar;\n /** 按下时的波纹效果,默认 true;关掉后组件的 ripple prop 仍可单独打开 */\n ripple?: boolean;\n /**\n * 页面根元素也用主题的背景色、文字色与字体,并去掉 body 的默认外边距,默认 false。\n * 整页都由组件库主题管理的站点(如文档站)打开:页面边缘、回弹滚动露出的地方不再是浏览器的默认色,\n * AppShell 贴满窗口、四周不留 8px 的白边;配合 themeBootScript 时接手它写的行内颜色\n */\n paintDocument?: boolean;\n}\n\n/** paintDocument 时页面根元素的样式 */\nexport const documentStyles = css.create({\n // 字号不设:根元素的字号是 rem 的基准,留给浏览器(用户的字号设置)\n page: {\n backgroundColor: '_background',\n color: '_text',\n fontFamily: '_sans',\n lineHeight: '_normal',\n // macOS 上用灰度抗锯齿,深色底上的浅色文字不发虚;其他系统忽略\n WebkitFontSmoothing: 'antialiased',\n MozOsxFontSmoothing: 'grayscale',\n },\n // 浏览器给 body 的 8px 外边距:整页的外壳(AppShell)要贴满窗口\n body: { margin: 0 },\n});\n\n/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */\nexport interface DocumentAttrs {\n /**\n * `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir。\n * data-theme-classes 记下主题的类,防闪烁脚本和浏览器接管时据此换掉,不留下旧主题的类\n */\n html: Record<string, string>;\n /** `<body>` 的属性:paintDocument 时去掉外边距的类 */\n body: Record<string, string>;\n}\n\n/** themeClassBoot 的结果 */\nexport interface ThemeClassBoot {\n /** 内联到 <head> 的脚本,放在 themeBootScript 之后 */\n script: string;\n /** 候选主题的类:交给 renderStyleTag 的 include,它们的规则才在首屏样式里 */\n classes: readonly string[];\n}\n\n/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */\nexport interface UiPlugin extends ObjectPlugin {\n /**\n * 服务端渲染:`<html>`、`<body>` 要带的属性,首屏就是这套主题与语言,不等客户端接管后再变。\n * 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来\n */\n documentAttrs(): DocumentAttrs;\n /**\n * 服务端渲染的防闪烁:`themeBootScript` 只设底色和文字色,正文里的组件读的是主题变量,要把 <html> 上的主题类也换掉,\n * 否则暗色系统上会先看到暗底配亮色主题的深色字,要等水合才对。主题按这里给的候选表选,规则随首屏样式一起输出:\n *\n * const boot = ui.themeClassBoot(builtinThemes);\n * const styles = renderStyleTag(page, { include: boot.classes });\n * // <head>:<script>themeBootScript(…)</script> <script>boot.script</script> styles\n */\n themeClassBoot(themes: Readonly<Record<string, Theme>>): ThemeClassBoot;\n}\n\n/**\n * 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。\n * 主题的 CSS 变量写在页面根元素上,所有 TSX 页面共享;应用卸载时撤销。\n */\nexport function createUi(options: UiOptions = {}): UiPlugin {\n return {\n documentAttrs() {\n const html: Record<string, string> = {};\n const body: Record<string, string> = {};\n if (options.lang) {\n const lang = defineLang(defaultConfig.lang, toValue(options.lang));\n html.lang = lang.code;\n html.dir = lang.direction;\n }\n if (options.theme || options.paintDocument) {\n const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);\n const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);\n if (classes.length) html.class = html['data-theme-classes'] = classes.join(' ');\n const style = Object.entries(variables)\n .map(([name, value]) => `${name}:${value}`)\n .join(';');\n if (style) html.style = style;\n html['data-theme'] = theme.name;\n if (options.paintDocument) body.class = css.attrs(documentStyles.body).class ?? '';\n }\n return { html, body };\n },\n themeClassBoot(themes) {\n const table = Object.fromEntries(\n Object.entries(themes).map(([name, theme]) => [\n name,\n rootThemeAttrs(theme, options.paintDocument ?? false),\n ]),\n );\n return {\n script: themeClassBootScript(table),\n classes: [...new Set(Object.values(table).flatMap(({ classes }) => classes))],\n };\n },\n install(app) {\n const scope = effectScope();\n app.onUnmount(() => scope.stop());\n scope.run(() => {\n const prefersDark = usePrefersDark();\n const theme = computed(() =>\n resolveTheme(toValue(options.theme) ?? defaultConfig.theme, prefersDark.value),\n );\n const lang = computed(() => defineLang(defaultConfig.lang, toValue(options.lang) ?? {}));\n app.provide(ConfigContext.key, {\n get theme() {\n return theme.value;\n },\n get lang() {\n return lang.value;\n },\n highlighter: options.highlighter,\n textMatcher: options.textMatcher,\n defaults: options.defaults ?? {},\n styles: mergeStyles({}, options.styles),\n router: options.router,\n permissions: options.permissions,\n http: options.http,\n workCalendar: options.workCalendar,\n ripple: options.ripple ?? true,\n });\n // 应用级的语言也写在页面根元素上(Provider 写在自己的元素上):读屏按 lang 选发音,\n // 从右到左的语言靠 dir 镜像排版与滚动条;没传 lang 时不动,宿主自己管\n if (options.lang && typeof document !== 'undefined') {\n const root = document.documentElement;\n const previous = { lang: root.lang, dir: root.dir };\n watchEffect(() => {\n root.lang = lang.value.code;\n root.dir = lang.value.direction;\n });\n app.onUnmount(() => Object.assign(root, previous));\n }\n // 应用级没有容器元素,主题变量挂在页面根元素上;变量名带 vtp- 前缀,不会与宿主冲突。\n // 只开 paintDocument、不传主题时用默认主题(整页也要有底色、字体,body 去掉外边距)\n if ((options.theme || options.paintDocument) && typeof document !== 'undefined') {\n const root = document.documentElement;\n // themeBootScript 在应用加载前写的行内颜色:交给主题的 class\n if (options.paintDocument) root.style.removeProperty('background-color');\n if (options.paintDocument) root.style.removeProperty('color');\n // 服务端或防闪烁脚本写的主题类交给 applyTheme:留着的话,旧主题的类和新主题的类同时生效,谁赢全看规则的先后\n const rendered = root.dataset.themeClasses;\n if (rendered) root.classList.remove(...rendered.split(' '));\n delete root.dataset.themeClasses;\n watchEffect((onCleanup) =>\n onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)),\n );\n if (options.paintDocument && document.body) {\n const body = document.body;\n const classes = css.attrs(documentStyles.body).class?.split(' ') ?? [];\n body.classList.add(...classes);\n app.onUnmount(() => body.classList.remove(...classes));\n }\n }\n });\n },\n };\n}\n\n/** 主题挂到页面根元素上的类与 CSS 变量;paint 时加上整页的底色与字体(浏览器里写到元素上,服务端写进 HTML) */\nfunction rootThemeAttrs(theme: Theme, paint: boolean) {\n const classes =\n css\n .attrs(...theme.styles, paint && documentStyles.page)\n .class?.split(' ')\n .filter(Boolean) ?? [];\n return { classes, variables: themeVariables(theme) };\n}\n\n/**\n * 防闪烁脚本的第二段:themeBootScript 已按用户的偏好写好 data-theme,这里把服务端写的主题类换成那套主题的。\n * data-theme 不在候选表里时不动(用户自己定制的主题,水合后才有)\n */\nfunction themeClassBootScript(themes: Record<string, ReturnType<typeof rootThemeAttrs>>): string {\n // 主题名来自调用方,写进内联脚本要防 </script>\n const table = JSON.stringify(themes).replaceAll('<', '\\\\u003c');\n return `(()=>{const c=${table};const r=document.documentElement;const t=c[r.dataset.theme];if(!t)return;const o=r.dataset.themeClasses;if(o)r.classList.remove(...o.split(' '));r.classList.add(...t.classes);r.dataset.themeClasses=t.classes.join(' ');for(const n in t.variables)r.style.setProperty(n,t.variables[n])})()`;\n}\n\n/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */\nfunction applyTheme(element: HTMLElement, theme: Theme, paint: boolean) {\n const { classes, variables } = rootThemeAttrs(theme, paint);\n element.classList.add(...classes);\n for (const [name, value] of Object.entries(variables)) element.style.setProperty(name, value);\n element.dataset.theme = theme.name;\n return () => {\n element.classList.remove(...classes);\n for (const name of Object.keys(variables)) element.style.removeProperty(name);\n delete element.dataset.theme;\n };\n}\n"],"mappings":"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"}
|
|
@@ -11,7 +11,8 @@ import { computed, effectScope, shallowRef } from "vue";
|
|
|
11
11
|
* app.use(createUi({ theme: () => preference.theme, paintDocument: true }));
|
|
12
12
|
* preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开
|
|
13
13
|
*
|
|
14
|
-
* 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts
|
|
14
|
+
* 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts);
|
|
15
|
+
* 服务端渲染的页面还要加上 ui.themeClassBoot(…) 的脚本(见 createUi.ts)。
|
|
15
16
|
*/
|
|
16
17
|
/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */
|
|
17
18
|
function createThemePreference(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preference.js","names":[],"sources":["../../../src/config/theme/preference.ts"],"sourcesContent":["/**\n * 主题偏好:用户选“跟随系统”或某一套主题,记在本地存储里,下次打开、其他标签页都沿用。\n * 在创建应用前调用,把 theme 交给 createUi:\n *\n * const preference = createThemePreference({ themes: { light: lightTheme, dark: darkTheme, paper: paperTheme } });\n * app.use(createUi({ theme: () => preference.theme, paintDocument: true }));\n * preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开\n *\n * 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts)。\n */\nimport { computed, effectScope, shallowRef } from 'vue';\nimport { revealTransition, type RevealOrigin } from '../../utils/motion/reveal';\nimport { usePrefersDark } from '../../utils/dom/media-query';\nimport { DEFAULT_THEME_STORAGE_KEY, SYSTEM_THEME } from './boot';\nimport type { Theme } from './theme';\n\n/** createThemePreference 的选项 */\nexport interface ThemePreferenceOptions<Name extends string> {\n /** 可选的主题:名字 → 主题 */\n themes: Record<Name, Theme>;\n /** 跟随系统时,亮色与暗色各用哪一套;默认 light、dark */\n system?: { light: NoInfer<Name>; dark: NoInfer<Name> };\n /** 记住选择的 localStorage 键,默认 'vue-tsx-pro-theme';false 不记 */\n storageKey?: string | false;\n /** 没记过(或记的主题已经没有了)时的偏好,默认跟随系统 */\n fallback?: NoInfer<Name> | typeof SYSTEM_THEME;\n}\n\n/** 主题偏好:当前的选择、解析出的主题,以及切换 */\nexport interface ThemePreference<Name extends string> {\n /** 可选的主题名,按 themes 里的顺序(ThemeSwitcher 据此列出选项) */\n readonly names: readonly Name[];\n /** 当前的选择:某一套主题,或 'system' */\n readonly preference: Name | typeof SYSTEM_THEME;\n /** 解析出的主题名(跟随系统时按系统明暗) */\n readonly name: Name;\n /** 解析出的主题,交给 createUi 或 Provider */\n readonly theme: Theme;\n /** 切换;给了 origin 时从那里圆形展开新主题(见 revealTransition) */\n set(preference: Name | typeof SYSTEM_THEME, options?: { origin?: RevealOrigin }): Promise<void>;\n}\n\n/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */\nexport function createThemePreference<Name extends string>(\n options: ThemePreferenceOptions<Name>,\n): ThemePreference<Name> {\n const names = Object.keys(options.themes) as Name[];\n const system =\n options.system ?? ({ light: 'light', dark: 'dark' } as { light: Name; dark: Name });\n const storageKey = options.storageKey ?? DEFAULT_THEME_STORAGE_KEY;\n const fallback = options.fallback ?? SYSTEM_THEME;\n const isChoice = (value: unknown): value is Name | typeof SYSTEM_THEME =>\n value === SYSTEM_THEME || names.includes(value as Name);\n\n const read = () => {\n const stored = storageKey ? safeStorage()?.getItem(storageKey) : null;\n return isChoice(stored) ? stored : fallback;\n };\n const preference = shallowRef<Name | typeof SYSTEM_THEME>(read());\n\n // 跟随系统的监听不属于哪个组件,放在自己的作用域里\n const scope = effectScope(true);\n const name = scope.run(() => {\n const prefersDark = usePrefersDark();\n return computed<Name>(() =>\n preference.value === SYSTEM_THEME\n ? prefersDark.value\n ? system.dark\n : system.light\n : preference.value,\n );\n })!;\n\n // 其他标签页改了偏好:跟着变\n if (storageKey && typeof window !== 'undefined') {\n window.addEventListener('storage', (event) => {\n if (event.key === storageKey) preference.value = read();\n });\n }\n\n return {\n names,\n get preference() {\n return preference.value;\n },\n get name() {\n return name.value;\n },\n get theme() {\n return options.themes[name.value];\n },\n set(next, setOptions) {\n if (!isChoice(next)) throw new Error(`没有名为 ${String(next)} 的主题`);\n if (storageKey) safeStorage()?.setItem(storageKey, next);\n return revealTransition(() => (preference.value = next), setOptions?.origin);\n },\n };\n}\n\n/** 隐私模式、禁用存储时 localStorage 会抛错:当作没有存储 */\nfunction safeStorage(): Storage | undefined {\n try {\n return globalThis.localStorage;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"preference.js","names":[],"sources":["../../../src/config/theme/preference.ts"],"sourcesContent":["/**\n * 主题偏好:用户选“跟随系统”或某一套主题,记在本地存储里,下次打开、其他标签页都沿用。\n * 在创建应用前调用,把 theme 交给 createUi:\n *\n * const preference = createThemePreference({ themes: { light: lightTheme, dark: darkTheme, paper: paperTheme } });\n * app.use(createUi({ theme: () => preference.theme, paintDocument: true }));\n * preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开\n *\n * 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts);\n * 服务端渲染的页面还要加上 ui.themeClassBoot(…) 的脚本(见 createUi.ts)。\n */\nimport { computed, effectScope, shallowRef } from 'vue';\nimport { revealTransition, type RevealOrigin } from '../../utils/motion/reveal';\nimport { usePrefersDark } from '../../utils/dom/media-query';\nimport { DEFAULT_THEME_STORAGE_KEY, SYSTEM_THEME } from './boot';\nimport type { Theme } from './theme';\n\n/** createThemePreference 的选项 */\nexport interface ThemePreferenceOptions<Name extends string> {\n /** 可选的主题:名字 → 主题 */\n themes: Record<Name, Theme>;\n /** 跟随系统时,亮色与暗色各用哪一套;默认 light、dark */\n system?: { light: NoInfer<Name>; dark: NoInfer<Name> };\n /** 记住选择的 localStorage 键,默认 'vue-tsx-pro-theme';false 不记 */\n storageKey?: string | false;\n /** 没记过(或记的主题已经没有了)时的偏好,默认跟随系统 */\n fallback?: NoInfer<Name> | typeof SYSTEM_THEME;\n}\n\n/** 主题偏好:当前的选择、解析出的主题,以及切换 */\nexport interface ThemePreference<Name extends string> {\n /** 可选的主题名,按 themes 里的顺序(ThemeSwitcher 据此列出选项) */\n readonly names: readonly Name[];\n /** 当前的选择:某一套主题,或 'system' */\n readonly preference: Name | typeof SYSTEM_THEME;\n /** 解析出的主题名(跟随系统时按系统明暗) */\n readonly name: Name;\n /** 解析出的主题,交给 createUi 或 Provider */\n readonly theme: Theme;\n /** 切换;给了 origin 时从那里圆形展开新主题(见 revealTransition) */\n set(preference: Name | typeof SYSTEM_THEME, options?: { origin?: RevealOrigin }): Promise<void>;\n}\n\n/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */\nexport function createThemePreference<Name extends string>(\n options: ThemePreferenceOptions<Name>,\n): ThemePreference<Name> {\n const names = Object.keys(options.themes) as Name[];\n const system =\n options.system ?? ({ light: 'light', dark: 'dark' } as { light: Name; dark: Name });\n const storageKey = options.storageKey ?? DEFAULT_THEME_STORAGE_KEY;\n const fallback = options.fallback ?? SYSTEM_THEME;\n const isChoice = (value: unknown): value is Name | typeof SYSTEM_THEME =>\n value === SYSTEM_THEME || names.includes(value as Name);\n\n const read = () => {\n const stored = storageKey ? safeStorage()?.getItem(storageKey) : null;\n return isChoice(stored) ? stored : fallback;\n };\n const preference = shallowRef<Name | typeof SYSTEM_THEME>(read());\n\n // 跟随系统的监听不属于哪个组件,放在自己的作用域里\n const scope = effectScope(true);\n const name = scope.run(() => {\n const prefersDark = usePrefersDark();\n return computed<Name>(() =>\n preference.value === SYSTEM_THEME\n ? prefersDark.value\n ? system.dark\n : system.light\n : preference.value,\n );\n })!;\n\n // 其他标签页改了偏好:跟着变\n if (storageKey && typeof window !== 'undefined') {\n window.addEventListener('storage', (event) => {\n if (event.key === storageKey) preference.value = read();\n });\n }\n\n return {\n names,\n get preference() {\n return preference.value;\n },\n get name() {\n return name.value;\n },\n get theme() {\n return options.themes[name.value];\n },\n set(next, setOptions) {\n if (!isChoice(next)) throw new Error(`没有名为 ${String(next)} 的主题`);\n if (storageKey) safeStorage()?.setItem(storageKey, next);\n return revealTransition(() => (preference.value = next), setOptions?.origin);\n },\n };\n}\n\n/** 隐私模式、禁用存储时 localStorage 会抛错:当作没有存储 */\nfunction safeStorage(): Storage | undefined {\n try {\n return globalThis.localStorage;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4CA,SAAgB,sBACd,SACuB;CACvB,MAAM,QAAQ,OAAO,KAAK,QAAQ,MAAM;CACxC,MAAM,SACJ,QAAQ,UAAW;EAAE,OAAO;EAAS,MAAM;CAAO;CACpD,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,WAAW,QAAQ,YAAA;CACzB,MAAM,YAAY,UAChB,UAAA,YAA0B,MAAM,SAAS,KAAa;CAExD,MAAM,aAAa;EACjB,MAAM,SAAS,aAAa,YAAY,CAAC,EAAE,QAAQ,UAAU,IAAI;EACjE,OAAO,SAAS,MAAM,IAAI,SAAS;CACrC;CACA,MAAM,aAAa,WAAuC,KAAK,CAAC;CAIhE,MAAM,OADQ,YAAY,IACb,CAAA,CAAM,UAAU;EAC3B,MAAM,cAAc,eAAe;EACnC,OAAO,eACL,WAAW,UAAA,WACP,YAAY,QACV,OAAO,OACP,OAAO,QACT,WAAW,KACjB;CACF,CAAC;CAGD,IAAI,cAAc,OAAO,WAAW,aAClC,OAAO,iBAAiB,YAAY,UAAU;EAC5C,IAAI,MAAM,QAAQ,YAAY,WAAW,QAAQ,KAAK;CACxD,CAAC;CAGH,OAAO;EACL;EACA,IAAI,aAAa;GACf,OAAO,WAAW;EACpB;EACA,IAAI,OAAO;GACT,OAAO,KAAK;EACd;EACA,IAAI,QAAQ;GACV,OAAO,QAAQ,OAAO,KAAK;EAC7B;EACA,IAAI,MAAM,YAAY;GACpB,IAAI,CAAC,SAAS,IAAI,GAAG,MAAM,IAAI,MAAM,QAAQ,OAAO,IAAI,EAAE,KAAK;GAC/D,IAAI,YAAY,YAAY,CAAC,EAAE,QAAQ,YAAY,IAAI;GACvD,OAAO,uBAAwB,WAAW,QAAQ,MAAO,YAAY,MAAM;EAC7E;CACF;AACF;;AAGA,SAAS,cAAmC;CAC1C,IAAI;EACF,OAAO,WAAW;CACpB,QAAQ;EACN;CACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -186,7 +186,7 @@ export { dataTableRecipe, dataTableStateStyles, type DataTableDensities, type Da
|
|
|
186
186
|
export { tableParams, useTableQuery, type TablePage, type TableQuery, type TableQueryOptions, type TableQueryState, } from './table/use-table-query';
|
|
187
187
|
export { ConfigScope, Provider, providerStyles, type ConfigScopeProps, type ProviderProps, } from './config/Provider';
|
|
188
188
|
export { ConfigContext, defaultConfig, useCan, useConfig, useLang, useTextMatcher, type Highlighter, type QueryPatch, type QueryValues, type TextMatcher, type UiConfig, type UiPermissions, type UiRouter, } from './config/context';
|
|
189
|
-
export { createUi, documentStyles, type DocumentAttrs, type UiOptions, type UiPlugin, } from './config/createUi';
|
|
189
|
+
export { createUi, documentStyles, type DocumentAttrs, type ThemeClassBoot, type UiOptions, type UiPlugin, } from './config/createUi';
|
|
190
190
|
export { mergeDefaults, useDefaults, type ComponentDefaults, type ResolvedDefaults, } from './config/defaults';
|
|
191
191
|
export { queryCodecs, useQueryState, useQueryStates, type QueryCodec, type QueryCodecs, } from './config/query-state';
|
|
192
192
|
export { mergeStyles, partAttrs, useRecipe, type ComponentStyles, type PartAttrs, type PartLayer, type StyleLayers, } from './config/styles';
|
|
@@ -278,7 +278,7 @@ export { computeMeasurements, defaultRangeExtractor, findVisibleRange, isWindow,
|
|
|
278
278
|
export { codeText, errorCodeOf, errorCodeText, errorMessage, useErrorMessage, } from './utils/request/error-message';
|
|
279
279
|
export { createHttpClient, HTTP_ERROR_CODES, HttpError, isAbortError, OK_CODE, scopeHttp, useHttp, type ApiEnvelope, type ApiErrorDetail, type FetchClient, type HttpClient, type HttpClientOptions, type HttpErrorCode, type HttpErrorContext, type HttpMethod, type HttpRequest, type HttpWire, type QueryValue, } from './utils/request/http';
|
|
280
280
|
export { useAutosave, type Autosave, type AutosaveOptions, type AutosaveStatus, } from './utils/request/use-autosave';
|
|
281
|
-
export { useRequest, type RequestOptions, type RequestState } from './utils/request/use-request';
|
|
281
|
+
export { useRequest, type RequestCache, type RequestOptions, type RequestState, } from './utils/request/use-request';
|
|
282
282
|
export { fromWire, toWire, wireText, type WireReviveOptions, type WireScalar, type WireType, type WireTypes, } from './utils/request/wire';
|
|
283
283
|
export { useActiveIndicator, type ActiveIndicatorOptions, type IndicatorAxes, } from './utils/motion/active-indicator';
|
|
284
284
|
export { parseDuration, themeTransition, themeTransitionOrInstant, useAnimate, type ThemeAnimate, type ThemeAnimateOptions, type ThemeTiming, type ThemeTransitionOptions, } from './utils/motion/animation';
|