@tsingroc/tsingroc-components 3.13.4 → 3.14.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.
Files changed (39) hide show
  1. package/dist/components/TsingrocTheme.js +1 -1
  2. package/dist/components/TsingrocTheme.js.map +1 -1
  3. package/dist/deckgl/TiandituLayer.js +6 -17
  4. package/dist/deckgl/TiandituLayer.js.map +1 -1
  5. package/dist/deckgl/WeatherData.d.ts +3 -3
  6. package/dist/echarts/index.js +1 -1
  7. package/dist/echarts/index.js.map +1 -1
  8. package/dist/echarts/series.d.ts +44 -0
  9. package/dist/echarts/series.js +54 -1
  10. package/dist/echarts/series.js.map +1 -1
  11. package/package.json +3 -3
  12. package/src/components/Auth.tsx +389 -0
  13. package/src/components/Calendar.tsx +182 -0
  14. package/src/components/CircularProgress.tsx +38 -0
  15. package/src/components/Header.tsx +136 -0
  16. package/src/components/ImageBackground.tsx +58 -0
  17. package/src/components/IndicatorLight.tsx +106 -0
  18. package/src/components/LineChartEditor.tsx +558 -0
  19. package/src/components/LineChartTable.tsx +285 -0
  20. package/src/components/LinkedLineChart.tsx +223 -0
  21. package/src/components/QuickDateRangePicker.tsx +84 -0
  22. package/src/components/SegmentedButtons.tsx +46 -0
  23. package/src/components/Sidebar.tsx +250 -0
  24. package/src/components/TsingrocDatePicker.tsx +103 -0
  25. package/src/components/TsingrocTheme.tsx +47 -0
  26. package/src/components/UserButton.tsx +152 -0
  27. package/src/components/VerticalColorLegend.tsx +73 -0
  28. package/src/components/WeatherMap.tsx +521 -0
  29. package/src/deckgl/TiandituLayer.ts +56 -0
  30. package/src/deckgl/WeatherData.ts +157 -0
  31. package/src/deckgl/index.ts +4 -0
  32. package/src/echarts/coordinateSystem.ts +132 -0
  33. package/src/echarts/gl.ts +158 -0
  34. package/src/echarts/index.ts +120 -0
  35. package/src/echarts/legend.ts +36 -0
  36. package/src/echarts/radar.ts +46 -0
  37. package/src/echarts/series.ts +441 -0
  38. package/src/echarts/tooltip.ts +17 -0
  39. package/src/index.ts +79 -0
@@ -0,0 +1,558 @@
1
+ import {
2
+ Button,
3
+ ConfigProvider,
4
+ Flex,
5
+ type FlexProps,
6
+ Input,
7
+ message,
8
+ Modal,
9
+ Table,
10
+ theme,
11
+ } from "antd";
12
+ import type { ColumnsType, ColumnType, TableRef } from "antd/es/table";
13
+ import {
14
+ type KeyboardEvent,
15
+ type TdHTMLAttributes,
16
+ useCallback,
17
+ useContext,
18
+ useEffect,
19
+ useMemo,
20
+ useRef,
21
+ useState,
22
+ } from "react";
23
+ import { createStyles } from "antd-style";
24
+ import EChartsReact from "echarts-for-react";
25
+ import type { ElementEvent } from "echarts";
26
+
27
+ import * as echarts from "../echarts";
28
+
29
+ export interface LineChartEditorProps extends Omit<FlexProps, "children"> {
30
+ /** X 轴的数据。*/
31
+ xData: (string | number)[];
32
+ /** Y 轴的原始数据。*/
33
+ origData: number[];
34
+ /**
35
+ * X 轴的名称。
36
+ * @default "时间"
37
+ */
38
+ xName?: string;
39
+ /**
40
+ * Y 轴的名称。
41
+ * @default "数值"
42
+ */
43
+ yName?: string;
44
+ /** Y 轴的单位,默认不显示。*/
45
+ yUnit?: string;
46
+ /**
47
+ * 原始曲线的名称。
48
+ * @default "原始曲线"
49
+ */
50
+ origName?: string;
51
+ /**
52
+ * 修改后曲线的名称。
53
+ * @default "修正曲线"
54
+ */
55
+ newName?: string;
56
+ /** 原始曲线的颜色,默认使用 ECharts 的默认值。*/
57
+ origColor?: string;
58
+ /** 修改后曲线的颜色,默认使用 ECharts 的默认值。*/
59
+ newColor?: string;
60
+ /**
61
+ * 是否平滑曲线显示。
62
+ *
63
+ * 如果是 `boolean` 类型,则表示是否开启平滑处理。如果是 `number` 类型(取值范围 0 到 1),
64
+ * 表示平滑程度,越小表示越接近折线段,反之亦然。设为 `true` 时相当于设为 0.5。
65
+ *
66
+ * @default true
67
+ */
68
+ smooth?: boolean | number;
69
+ /**
70
+ * 折线图的高度。表格会自动占满剩余的高度。
71
+ * @default 175
72
+ */
73
+ chartHeight?: string | number;
74
+ /**
75
+ * 控制按钮的位置。
76
+ * @default "bottom"
77
+ */
78
+ controlsLocation?: "top" | "bottom";
79
+ /**
80
+ * 保存时是否弹窗要求确认。
81
+ * @default true
82
+ */
83
+ confirmPopup?: boolean;
84
+ /** 确认提交修改时的回调。*/
85
+ onConfirm?: (newData: number[], xData: (string | number)[]) => void;
86
+ /**
87
+ * 上下调整数值的步长的默认值。用户总是可以在组件内自行调整步长。
88
+ * @default 0.1
89
+ */
90
+ adjustStep?: number;
91
+ }
92
+
93
+ interface DataItem {
94
+ key: number;
95
+ x: string | number;
96
+ orig: number;
97
+ new: number;
98
+ }
99
+
100
+ /**
101
+ * 一个折线图编辑器,由折线图、数据表格和确认/撤销按钮三部分组成。
102
+ * 可以在曲线上使用键盘上下/WS 键,或者在表格中编辑数据。
103
+ *
104
+ * 除了文档列出的属性外,多余的属性会被传递给最外层的 [Flex][1] 组件。
105
+ *
106
+ * [1]: https://ant-design.antgroup.com/components/flex-cn#api
107
+ */
108
+ function LineChartEditor(props: LineChartEditorProps) {
109
+ const {
110
+ xData,
111
+ origData,
112
+ xName = "时间",
113
+ yName = "数值",
114
+ yUnit,
115
+ origName = "原始曲线",
116
+ newName = "修正曲线",
117
+ origColor,
118
+ newColor,
119
+ smooth = true,
120
+ chartHeight = 175,
121
+ controlsLocation = "bottom",
122
+ confirmPopup = true,
123
+ onConfirm,
124
+ adjustStep: defaultAdjustStep = 0.1,
125
+ ...rest
126
+ } = props;
127
+ // TODO i18n
128
+ const unitString = yUnit !== undefined ? `(${yUnit})` : "";
129
+ const { cx, styles, theme: token } = useStyles();
130
+ const isDark =
131
+ useContext(ConfigProvider.ConfigContext).theme?.algorithm ===
132
+ theme.darkAlgorithm;
133
+ const [modal, modalContextHolder] = Modal.useModal();
134
+ const [messageApi, messageContextHolder] = message.useMessage();
135
+ const [newData, setNewData] = useState(origData);
136
+ const echartsRef = useRef<EChartsReact>(null);
137
+ const tableRef = useRef<TableRef>(null);
138
+ const [chartFocused, setChartFocused] = useState<boolean>(false);
139
+ const [hoverIndex, setHoverIndex] = useState<number>();
140
+ const [selectedIndex, setSelectedIndex] = useState<number>();
141
+ const highlightIndex = selectedIndex ?? hoverIndex;
142
+ const [adjustStep, setAdjustStep] = useState(defaultAdjustStep);
143
+ const option = echarts.buildEChartsOption(
144
+ {
145
+ backgroundColor: "transparent",
146
+ },
147
+ echarts.useGrid({
148
+ option: {
149
+ id: "grid",
150
+ left: 12,
151
+ right: 42,
152
+ bottom: 40,
153
+ },
154
+ xAxis: {
155
+ name: xName,
156
+ data: xData,
157
+ axisPointer: { value: xData[highlightIndex ?? 0] },
158
+ },
159
+ yAxis: {
160
+ name: yName + unitString,
161
+ axisPointer: { value: newData[highlightIndex ?? 0] },
162
+ },
163
+ series: [
164
+ echarts.lineSeries({
165
+ name: origName,
166
+ data: origData,
167
+ option: {
168
+ color: origColor,
169
+ lineStyle: { type: "dashed" },
170
+ smooth,
171
+ },
172
+ }),
173
+ echarts.lineSeries({
174
+ name: newName,
175
+ data: newData,
176
+ option: {
177
+ color: newColor,
178
+ smooth,
179
+ },
180
+ }),
181
+ ],
182
+ }),
183
+ echarts.legend({
184
+ data: [origName, newName],
185
+ option: { right: 42 },
186
+ }),
187
+ {
188
+ axisPointer: [
189
+ {
190
+ show: true,
191
+ status: highlightIndex !== undefined ? "show" : "hide",
192
+ triggerOn: "none",
193
+ },
194
+ ],
195
+ dataZoom: [
196
+ {
197
+ orient: "horizontal",
198
+ bottom: 8,
199
+ filterMode: "empty",
200
+ },
201
+ ],
202
+ graphic: [
203
+ {
204
+ type: "text",
205
+ ignore: highlightIndex === undefined,
206
+ left: "center",
207
+ top: 0,
208
+ style: {
209
+ // TODO i18n
210
+ text: !chartFocused
211
+ ? ""
212
+ : selectedIndex !== undefined
213
+ ? "可使用键盘 ↑/↓/W/S 键调节选中数据,使用 +/- 键修改调节步长"
214
+ : hoverIndex !== undefined
215
+ ? "点击可选中当前数据点"
216
+ : "",
217
+ fill: token.colorTextLabel,
218
+ },
219
+ },
220
+ ],
221
+ },
222
+ );
223
+ useEffect(() => {
224
+ const inst = echartsRef.current?.getEchartsInstance()?.getZr();
225
+ if (!inst) return () => {};
226
+ const onMousemove = (event: ElementEvent) => {
227
+ const inst = echartsRef.current?.getEchartsInstance();
228
+ if (!inst) return;
229
+ const coord = [event.offsetX, event.offsetY];
230
+ if (inst.containPixel("grid", coord)) {
231
+ const [index] = inst.convertFromPixel("grid", coord);
232
+ setChartFocused(true);
233
+ setHoverIndex(index);
234
+ } else {
235
+ setHoverIndex(undefined);
236
+ }
237
+ };
238
+ const onGlobalout = () => setHoverIndex(undefined);
239
+ const onClick = () =>
240
+ setHoverIndex((hoverIndex) => {
241
+ setSelectedIndex(hoverIndex);
242
+ return hoverIndex;
243
+ });
244
+ inst.on("mousemove", onMousemove);
245
+ inst.on("globalout", onGlobalout);
246
+ inst.on("click", onClick);
247
+ return () => {
248
+ inst.off("mousemove", onMousemove);
249
+ inst.off("globalout", onGlobalout);
250
+ inst.off("click", onClick);
251
+ };
252
+ }, []);
253
+ useEffect(() => {
254
+ const inst = echartsRef.current?.getEchartsInstance();
255
+ if (!inst) return;
256
+ if (highlightIndex !== undefined) {
257
+ inst.dispatchAction({
258
+ type: "highlight",
259
+ seriesIndex: 1,
260
+ dataIndex: highlightIndex,
261
+ });
262
+ } else {
263
+ inst.dispatchAction({
264
+ type: "downplay",
265
+ seriesIndex: 1,
266
+ });
267
+ }
268
+ if (selectedIndex !== undefined) {
269
+ tableScrollTo(selectedIndex);
270
+ }
271
+ }, [highlightIndex, selectedIndex]);
272
+ const onKeyDown = (event: KeyboardEvent) => {
273
+ if (selectedIndex === undefined) return;
274
+ switch (event.code) {
275
+ case "ArrowUp":
276
+ case "KeyW":
277
+ setNewData((prev) => {
278
+ const next = [...prev];
279
+ next[selectedIndex] += adjustStep;
280
+ return next;
281
+ });
282
+ break;
283
+ case "ArrowDown":
284
+ case "KeyS":
285
+ setNewData((prev) => {
286
+ const next = [...prev];
287
+ next[selectedIndex] -= adjustStep;
288
+ return next;
289
+ });
290
+ break;
291
+ case "Equal":
292
+ setAdjustStep((prev) => prev + defaultAdjustStep / 10);
293
+ break;
294
+ case "Minus":
295
+ setAdjustStep((prev) => prev - defaultAdjustStep / 10);
296
+ break;
297
+ case "Escape":
298
+ case "Enter":
299
+ setSelectedIndex(undefined);
300
+ break;
301
+ default:
302
+ return;
303
+ }
304
+ event.preventDefault();
305
+ };
306
+ const tableData: DataItem[] = useMemo(
307
+ () =>
308
+ xData.map((x, i) => ({
309
+ key: i,
310
+ x,
311
+ orig: origData[i],
312
+ new: newData[i],
313
+ })),
314
+ [xData, origData, newData],
315
+ );
316
+ const tableColumns = useMemo(
317
+ (): ColumnsType<DataItem> => [
318
+ {
319
+ key: "x",
320
+ dataIndex: "x",
321
+ title: xName,
322
+ },
323
+ {
324
+ key: "orig",
325
+ dataIndex: "orig",
326
+ title: origName + unitString,
327
+ width: "50%",
328
+ },
329
+ {
330
+ key: "new",
331
+ dataIndex: "new",
332
+ title: newName + unitString,
333
+ width: "50%",
334
+ onCell: ((data: DataItem, index: number): EditorCellProps => ({
335
+ value: data.new,
336
+ onChange: (value) => {
337
+ setNewData((prev) => {
338
+ const next = [...prev];
339
+ next[index] = value;
340
+ return next;
341
+ });
342
+ },
343
+ step: adjustStep,
344
+ })) as unknown as ColumnType<DataItem>["onCell"],
345
+ onHeaderCell: () => ({
346
+ style: {
347
+ paddingInlineStart: token.paddingXS + token.paddingSM,
348
+ },
349
+ }),
350
+ },
351
+ ],
352
+ [xName, origName, newName, adjustStep, unitString, token],
353
+ );
354
+ const onRow = useCallback(
355
+ (data: DataItem, index?: number): TdHTMLAttributes<HTMLTableRowElement> => {
356
+ if (index === undefined) return {};
357
+ const modified = data.orig != data.new;
358
+ const selected = index === selectedIndex;
359
+ return {
360
+ className: cx(styles.editableRow, {
361
+ [styles.modified]: modified,
362
+ [styles.selected]: selected,
363
+ }),
364
+ onMouseEnter: () => {
365
+ setChartFocused(false);
366
+ setHoverIndex(index);
367
+ },
368
+ onMouseLeave: () => setHoverIndex(undefined),
369
+ };
370
+ },
371
+ [cx, styles, selectedIndex],
372
+ );
373
+ const tableScrollTo = (index: number) => {
374
+ const table = tableRef.current?.nativeElement;
375
+ if (table) {
376
+ const header = table.getElementsByTagName("thead")[0];
377
+ const row = table.getElementsByTagName("tr")[index];
378
+ table.scrollTo({
379
+ top:
380
+ row.offsetTop +
381
+ row.offsetHeight / 2 -
382
+ (table.offsetHeight - header.offsetHeight) / 2,
383
+ behavior: "smooth",
384
+ });
385
+ }
386
+ };
387
+ const onSave = async () => {
388
+ const modifiedData = tableData.filter((item) => item.new !== item.orig);
389
+ if (modifiedData.length === 0) {
390
+ messageApi.warning("您尚未做任何修改!");
391
+ return;
392
+ }
393
+ let confirmed = true;
394
+ if (confirmPopup) {
395
+ // TODO i18n
396
+ confirmed = await modal.confirm({
397
+ title: "确定要保存修改吗?",
398
+ width: "fit-content",
399
+ content: (
400
+ <>
401
+ <p>
402
+ 您修改了以下{xName}的{yName}:
403
+ </p>
404
+ <Table<DataItem>
405
+ ref={tableRef}
406
+ dataSource={modifiedData}
407
+ columns={tableColumns.map((col) => ({
408
+ ...col,
409
+ onCell: undefined,
410
+ onHeaderCell: undefined,
411
+ }))}
412
+ size="small"
413
+ pagination={false}
414
+ style={{
415
+ marginTop: token.marginSM,
416
+ maxHeight: "50vh",
417
+ overflowY: "auto",
418
+ }}
419
+ />
420
+ </>
421
+ ),
422
+ });
423
+ }
424
+ if (confirmed) {
425
+ onConfirm?.(newData, xData);
426
+ }
427
+ };
428
+ const controls = (
429
+ <Flex gap={token.marginXXS} align="baseline">
430
+ {/* TODO i18n */}
431
+ <div style={{ flexShrink: 0 }}>调节步长:</div>
432
+ <Input
433
+ type="number"
434
+ step={defaultAdjustStep / 10}
435
+ value={adjustStep}
436
+ onChange={(event) => setAdjustStep(event.currentTarget.valueAsNumber)}
437
+ style={{ width: 120 }}
438
+ />
439
+ <div style={{ flexBasis: "100%" }}></div>
440
+ <Button type="primary" onClick={onSave}>
441
+ 保存
442
+ </Button>
443
+ <Button danger onClick={() => setNewData(origData)}>
444
+ 重置
445
+ </Button>
446
+ </Flex>
447
+ );
448
+ return (
449
+ <Flex vertical gap={token.marginXS} {...rest}>
450
+ {modalContextHolder}
451
+ {messageContextHolder}
452
+ {controlsLocation === "top" ? controls : ""}
453
+ <div
454
+ tabIndex={0}
455
+ style={{ flexShrink: 0, outline: "none" }}
456
+ onKeyDown={onKeyDown}
457
+ onBlur={() => {
458
+ setChartFocused(false);
459
+ setSelectedIndex(undefined);
460
+ }}
461
+ >
462
+ <EChartsReact
463
+ ref={echartsRef}
464
+ option={option}
465
+ theme={isDark ? "dark" : undefined}
466
+ style={{ height: chartHeight }}
467
+ />
468
+ </div>
469
+ {useMemo(
470
+ () => (
471
+ <Table<DataItem>
472
+ ref={tableRef}
473
+ dataSource={tableData}
474
+ columns={tableColumns}
475
+ size="small"
476
+ pagination={false}
477
+ style={{ flexBasis: "100%", overflowY: "auto" }}
478
+ components={{ body: { cell: EditorCell } }}
479
+ onHeaderRow={() => ({
480
+ style: { position: "sticky", top: 0, zIndex: 1 },
481
+ })}
482
+ onRow={onRow}
483
+ />
484
+ ),
485
+ [tableData, tableColumns, onRow],
486
+ )}
487
+ {controlsLocation === "bottom" ? controls : ""}
488
+ </Flex>
489
+ );
490
+ }
491
+
492
+ export default LineChartEditor;
493
+
494
+ interface EditorCellProps {
495
+ value: number;
496
+ onChange: (value: number) => void;
497
+ step?: number;
498
+ }
499
+
500
+ function EditorCell({ value, onChange, step, ...props }: EditorCellProps) {
501
+ if (value === undefined) {
502
+ return <td {...props} />;
503
+ }
504
+ /* eslint-disable react-hooks/rules-of-hooks */
505
+ const { styles } = useStyles();
506
+ const [editing, setEditing] = useState(false);
507
+ /* eslint-enable react-hooks/rules-of-hooks */
508
+ return (
509
+ <td {...props} style={{ paddingBlock: 0 }}>
510
+ {editing ? (
511
+ <Input
512
+ ref={(input) => input?.focus()}
513
+ type="number"
514
+ step={step}
515
+ style={{ margin: 0 }}
516
+ defaultValue={value}
517
+ onChange={(event) => onChange(event.currentTarget.valueAsNumber)}
518
+ onPressEnter={() => setEditing(false)}
519
+ onBlur={() => setEditing(false)}
520
+ />
521
+ ) : (
522
+ <div
523
+ className={styles.editableWrapper}
524
+ onClick={() => setEditing(true)}
525
+ >
526
+ {value}
527
+ </div>
528
+ )}
529
+ </td>
530
+ );
531
+ }
532
+
533
+ const useStyles = createStyles(({ css, cx, token }) => {
534
+ const editableWrapper = css`
535
+ padding: ${token.paddingXXS}px ${token.paddingSM - 1 + 24}px
536
+ ${token.paddingXXS}px ${token.paddingSM - 1}px;
537
+ cursor: text;
538
+ border-radius: ${token.borderRadius}px;
539
+ border: 1px solid transparent;
540
+ transition: border ${token.motionDurationFast};
541
+ `;
542
+ const modified = css`
543
+ color: ${token.colorSuccess};
544
+ background: ${token.colorSuccessBg};
545
+ `;
546
+ const selected = css`
547
+ background: ${token.colorFillTertiary};
548
+ `;
549
+ const editableRow = css`
550
+ &:hover:not(.${cx(selected)}) {
551
+ background: ${token.colorFillQuaternary};
552
+ }
553
+ &:hover .${cx(editableWrapper)} {
554
+ border-color: ${token.colorBorder};
555
+ }
556
+ `;
557
+ return { editableRow, editableWrapper, modified, selected };
558
+ });