@vela-studio/ui 1.0.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 (68) hide show
  1. package/README.md +152 -0
  2. package/dist/index.d.ts +696 -0
  3. package/dist/index.js +10 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +11786 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/index.umd.js +10 -0
  8. package/dist/index.umd.js.map +1 -0
  9. package/dist/style.css +1 -0
  10. package/index.ts +150 -0
  11. package/package.json +73 -0
  12. package/src/components/advanced/scripting/Scripting.vue +189 -0
  13. package/src/components/advanced/state/State.vue +231 -0
  14. package/src/components/advanced/trigger/Trigger.vue +256 -0
  15. package/src/components/basic/button/Button.vue +120 -0
  16. package/src/components/basic/container/Container.vue +22 -0
  17. package/src/components/chart/barChart/barChart.vue +176 -0
  18. package/src/components/chart/doughnutChart/doughnutChart.vue +128 -0
  19. package/src/components/chart/funnelChart/funnelChart.vue +128 -0
  20. package/src/components/chart/gaugeChart/gaugeChart.vue +144 -0
  21. package/src/components/chart/lineChart/lineChart.vue +188 -0
  22. package/src/components/chart/pieChart/pieChart.vue +114 -0
  23. package/src/components/chart/radarChart/radarChart.vue +115 -0
  24. package/src/components/chart/sankeyChart/sankeyChart.vue +144 -0
  25. package/src/components/chart/scatterChart/scatterChart.vue +162 -0
  26. package/src/components/chart/stackedBarChart/stackedBarChart.vue +184 -0
  27. package/src/components/content/html/Html.vue +104 -0
  28. package/src/components/content/iframe/Iframe.vue +111 -0
  29. package/src/components/content/markdown/Markdown.vue +174 -0
  30. package/src/components/controls/breadcrumb/Breadcrumb.vue +79 -0
  31. package/src/components/controls/buttonGroup/ButtonGroup.vue +93 -0
  32. package/src/components/controls/checkboxGroup/CheckboxGroup.vue +147 -0
  33. package/src/components/controls/dateRange/DateRange.vue +174 -0
  34. package/src/components/controls/multiSelect/MultiSelect.vue +155 -0
  35. package/src/components/controls/navButton/NavButton.vue +97 -0
  36. package/src/components/controls/pagination/Pagination.vue +94 -0
  37. package/src/components/controls/searchBox/SearchBox.vue +170 -0
  38. package/src/components/controls/select/Select.vue +134 -0
  39. package/src/components/controls/slider/Slider.vue +167 -0
  40. package/src/components/controls/switch/Switch.vue +107 -0
  41. package/src/components/data/cardGrid/CardGrid.vue +318 -0
  42. package/src/components/data/list/List.vue +282 -0
  43. package/src/components/data/pivot/Pivot.vue +270 -0
  44. package/src/components/data/table/Table.vue +150 -0
  45. package/src/components/data/timeline/Timeline.vue +315 -0
  46. package/src/components/group/Group.vue +75 -0
  47. package/src/components/kpi/box/Box.vue +98 -0
  48. package/src/components/kpi/countUp/CountUp.vue +193 -0
  49. package/src/components/kpi/progress/Progress.vue +159 -0
  50. package/src/components/kpi/stat/Stat.vue +205 -0
  51. package/src/components/kpi/text/Text.vue +74 -0
  52. package/src/components/layout/badge/Badge.vue +105 -0
  53. package/src/components/layout/col/Col.vue +114 -0
  54. package/src/components/layout/flex/Flex.vue +105 -0
  55. package/src/components/layout/grid/Grid.vue +89 -0
  56. package/src/components/layout/modal/Modal.vue +118 -0
  57. package/src/components/layout/panel/Panel.vue +162 -0
  58. package/src/components/layout/row/Row.vue +99 -0
  59. package/src/components/layout/tabs/Tabs.vue +117 -0
  60. package/src/components/media/image/Image.vue +132 -0
  61. package/src/components/media/video/Video.vue +115 -0
  62. package/src/components/v2/basic/BaseButton.vue +179 -0
  63. package/src/components/v2/kpi/KpiCard.vue +215 -0
  64. package/src/components/v2/layout/GridBox.vue +55 -0
  65. package/src/hooks/useDataSource.ts +123 -0
  66. package/src/types/gis.ts +251 -0
  67. package/src/utils/chartUtils.ts +349 -0
  68. package/src/utils/dataUtils.ts +403 -0
@@ -0,0 +1,123 @@
1
+ import { ref, watch, onUnmounted, type Ref } from 'vue'
2
+ import axios, { type AxiosRequestConfig } from 'axios'
3
+
4
+ export interface DataSource {
5
+ enabled?: boolean
6
+ url?: string
7
+ method?: string
8
+ headers?: Record<string, string>
9
+ body?: string
10
+ interval?: number
11
+ }
12
+
13
+ /**
14
+ * 数据源 Hook
15
+ * 支持 HTTP 请求和自动刷新
16
+ *
17
+ * 职责:仅负责网络请求,返回完整响应数据
18
+ * 数据提取:由各组件使用 dataUtils 工具函数自行提取所需字段
19
+ * 优势:组件可灵活提取多个字段,互不影响
20
+ */
21
+ export function useDataSource(dataSource: Ref<DataSource | undefined>) {
22
+ const data = ref<unknown>(null)
23
+ const rawData = ref<unknown>(null) // 完整响应数据
24
+ const loading = ref(false)
25
+ const error = ref<string | null>(null)
26
+ let intervalId: number | null = null
27
+
28
+ /**
29
+ * 执行数据请求
30
+ */
31
+ const fetchData = async () => {
32
+ const ds = dataSource.value
33
+ if (!ds || !ds.enabled || !ds.url) {
34
+ return
35
+ }
36
+
37
+ loading.value = true
38
+ error.value = null
39
+
40
+ try {
41
+ // 构建请求配置
42
+ const config: AxiosRequestConfig = {
43
+ method: ds.method || 'GET',
44
+ url: ds.url,
45
+ headers: ds.headers || {},
46
+ }
47
+
48
+ // 处理请求体(仅 POST/PUT/DELETE)
49
+ if (ds.method && ['POST', 'PUT', 'DELETE'].includes(ds.method) && ds.body) {
50
+ try {
51
+ config.data = JSON.parse(ds.body)
52
+ } catch {
53
+ error.value = '请求体 JSON 格式错误'
54
+ loading.value = false
55
+ return
56
+ }
57
+ }
58
+
59
+ // 发送请求
60
+ const response = await axios(config)
61
+
62
+ // 保存完整响应数据
63
+ rawData.value = response.data
64
+ data.value = response.data
65
+ error.value = null
66
+ } catch (err: unknown) {
67
+ error.value = (err as Error).message || '请求失败'
68
+ console.error('Data source fetch error:', err)
69
+ } finally {
70
+ loading.value = false
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 启动定时刷新
76
+ */
77
+ const startPolling = () => {
78
+ stopPolling()
79
+ const ds = dataSource.value
80
+ if (ds && ds.enabled && ds.interval && ds.interval > 0) {
81
+ // interval 单位为秒,转换为毫秒
82
+ intervalId = window.setInterval(fetchData, ds.interval * 1000)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 停止定时刷新
88
+ */
89
+ const stopPolling = () => {
90
+ if (intervalId !== null) {
91
+ clearInterval(intervalId)
92
+ intervalId = null
93
+ }
94
+ }
95
+
96
+ // 监听 dataSource 变化
97
+ watch(
98
+ () => dataSource.value,
99
+ (newDs) => {
100
+ stopPolling()
101
+ if (newDs && newDs.enabled && newDs.url) {
102
+ fetchData()
103
+ startPolling()
104
+ } else {
105
+ data.value = null
106
+ }
107
+ },
108
+ { immediate: true, deep: true },
109
+ )
110
+
111
+ // 组件卸载时清理定时器
112
+ onUnmounted(() => {
113
+ stopPolling()
114
+ })
115
+
116
+ return {
117
+ data,
118
+ rawData, // 完整响应数据
119
+ loading,
120
+ error,
121
+ fetchData, // 手动刷新
122
+ }
123
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * GIS 通用类型定义
3
+ * Smart 和 Dumb 组件之间的数据契约
4
+ */
5
+
6
+ // ==================== 基础类型 ====================
7
+
8
+ /** 基础坐标点 */
9
+ export interface GISPoint {
10
+ lat: number
11
+ lng: number
12
+ }
13
+
14
+ /** 带强度的热力点 */
15
+ export interface HeatPoint extends GISPoint {
16
+ /** 热力强度 0-1 */
17
+ intensity?: number
18
+ }
19
+
20
+ /** 聚合/标记点数据 */
21
+ export interface MarkerPoint extends GISPoint {
22
+ /** 唯一标识 */
23
+ id?: string | number
24
+ /** 标签文本 */
25
+ label?: string
26
+ /** 弹窗内容 */
27
+ popup?: string
28
+ /** 图标 URL */
29
+ icon?: string
30
+ /** 图标大小 [width, height] */
31
+ iconSize?: [number, number]
32
+ /** 图标锚点 [x, y] */
33
+ iconAnchor?: [number, number]
34
+ /** 自定义数据 */
35
+ data?: Record<string, unknown>
36
+ }
37
+
38
+ // ==================== GeoJSON 类型 ====================
39
+
40
+ /** GeoJSON 几何类型 */
41
+ export type GeoJSONGeometryType =
42
+ | 'Point'
43
+ | 'MultiPoint'
44
+ | 'LineString'
45
+ | 'MultiLineString'
46
+ | 'Polygon'
47
+ | 'MultiPolygon'
48
+ | 'GeometryCollection'
49
+
50
+ /** GeoJSON 几何对象 */
51
+ export interface GeoJSONGeometry {
52
+ type: GeoJSONGeometryType
53
+ coordinates: number[] | number[][] | number[][][] | number[][][][]
54
+ }
55
+
56
+ /** GeoJSON Feature */
57
+ export interface GeoJSONFeature<P = Record<string, unknown>> {
58
+ type: 'Feature'
59
+ geometry: GeoJSONGeometry
60
+ properties: P
61
+ id?: string | number
62
+ }
63
+
64
+ /** GeoJSON FeatureCollection */
65
+ export interface GeoJSONFeatureCollection<P = Record<string, unknown>> {
66
+ type: 'FeatureCollection'
67
+ features: GeoJSONFeature<P>[]
68
+ }
69
+
70
+ /** GeoJSON 数据类型联合 */
71
+ export type GeoJSONData<P = Record<string, unknown>> =
72
+ | GeoJSONFeature<P>
73
+ | GeoJSONFeatureCollection<P>
74
+
75
+ // ==================== 地图配置类型 ====================
76
+
77
+ /** 边界框 */
78
+ export interface MapBounds {
79
+ northEast: GISPoint
80
+ southWest: GISPoint
81
+ }
82
+
83
+ /** 地图视口配置 */
84
+ export interface MapViewport {
85
+ center: GISPoint
86
+ zoom: number
87
+ bounds?: MapBounds
88
+ }
89
+
90
+ /** 瓦片图层配置 */
91
+ export interface TileLayerConfig {
92
+ url: string
93
+ attribution?: string
94
+ minZoom?: number
95
+ maxZoom?: number
96
+ subdomains?: string[]
97
+ opacity?: number
98
+ }
99
+
100
+ /** 图层可见性配置 */
101
+ export interface LayerVisibility {
102
+ id: string
103
+ name: string
104
+ visible: boolean
105
+ type: 'tile' | 'heat' | 'cluster' | 'geojson' | 'vector'
106
+ }
107
+
108
+ // ==================== 热力图配置类型 ====================
109
+
110
+ /** 热力图渐变色配置 */
111
+ export interface HeatGradient {
112
+ [stop: number]: string
113
+ }
114
+
115
+ /** 热力图配置 */
116
+ export interface HeatLayerConfig {
117
+ /** 热力点半径(像素) */
118
+ radius?: number
119
+ /** 模糊程度(像素) */
120
+ blur?: number
121
+ /** 最大缩放级别 */
122
+ maxZoom?: number
123
+ /** 最大强度值 */
124
+ max?: number
125
+ /** 最小透明度 */
126
+ minOpacity?: number
127
+ /** 渐变色配置 */
128
+ gradient?: HeatGradient
129
+ }
130
+
131
+ // ==================== 聚合图配置类型 ====================
132
+
133
+ /** 聚合图标配置 */
134
+ export interface ClusterIconConfig {
135
+ /** 默认图标 URL */
136
+ iconUrl?: string
137
+ /** 图标大小 */
138
+ iconSize?: [number, number]
139
+ /** 图标锚点 */
140
+ iconAnchor?: [number, number]
141
+ /** 弹窗锚点 */
142
+ popupAnchor?: [number, number]
143
+ }
144
+
145
+ /** 聚合簇样式配置 */
146
+ export interface ClusterStyleConfig {
147
+ /** 小聚合簇阈值 */
148
+ smallThreshold?: number
149
+ /** 中聚合簇阈值 */
150
+ mediumThreshold?: number
151
+ /** 小聚合簇样式 */
152
+ smallStyle?: {
153
+ backgroundColor?: string
154
+ color?: string
155
+ size?: number
156
+ }
157
+ /** 中聚合簇样式 */
158
+ mediumStyle?: {
159
+ backgroundColor?: string
160
+ color?: string
161
+ size?: number
162
+ }
163
+ /** 大聚合簇样式 */
164
+ largeStyle?: {
165
+ backgroundColor?: string
166
+ color?: string
167
+ size?: number
168
+ }
169
+ }
170
+
171
+ /** 聚合图层配置 */
172
+ export interface ClusterLayerConfig {
173
+ /** 最大聚合半径 */
174
+ maxClusterRadius?: number
175
+ /** 禁用聚合的缩放级别 */
176
+ disableClusteringAtZoom?: number
177
+ /** 最大缩放时展开为蜘蛛网形式 */
178
+ spiderfyOnMaxZoom?: boolean
179
+ /** 鼠标悬停时显示聚合范围 */
180
+ showCoverageOnHover?: boolean
181
+ /** 点击聚合簇时缩放到边界 */
182
+ zoomToBoundsOnClick?: boolean
183
+ /** 是否启用分块加载 */
184
+ chunkedLoading?: boolean
185
+ /** 图标配置 */
186
+ iconConfig?: ClusterIconConfig
187
+ /** 聚合簇样式 */
188
+ clusterStyle?: ClusterStyleConfig
189
+ }
190
+
191
+ // ==================== 事件类型 ====================
192
+
193
+ /** 地图点击事件数据 */
194
+ export interface MapClickEvent {
195
+ latlng: GISPoint
196
+ layerPoint: { x: number; y: number }
197
+ containerPoint: { x: number; y: number }
198
+ }
199
+
200
+ /** 标记点击事件数据 */
201
+ export interface MarkerClickEvent {
202
+ marker: MarkerPoint
203
+ index: number
204
+ originalEvent?: MouseEvent
205
+ }
206
+
207
+ /** 聚合簇点击事件数据 */
208
+ export interface ClusterClickEvent {
209
+ /** 聚合簇中心点 */
210
+ center: GISPoint
211
+ /** 聚合簇包含的标记数量 */
212
+ count: number
213
+ /** 聚合簇边界 */
214
+ bounds: MapBounds
215
+ /** 聚合簇内的所有标记 */
216
+ markers: MarkerPoint[]
217
+ }
218
+
219
+ // ==================== 数据状态类型 ====================
220
+
221
+ /** 数据加载状态 */
222
+ export type DataLoadingState = 'idle' | 'loading' | 'success' | 'error'
223
+
224
+ /** 带状态的数据包装器 */
225
+ export interface DataWithState<T> {
226
+ data: T
227
+ state: DataLoadingState
228
+ error?: string
229
+ /** 数据版本号,用于增量更新检测 */
230
+ version?: number
231
+ }
232
+
233
+ // ==================== 工具类型 ====================
234
+
235
+ /** 将普通数组转换为 HeatPoint 数组的映射配置 */
236
+ export interface HeatDataMapping {
237
+ latField: string
238
+ lngField: string
239
+ intensityField?: string
240
+ }
241
+
242
+ /** 将普通数组转换为 MarkerPoint 数组的映射配置 */
243
+ export interface MarkerDataMapping {
244
+ idField?: string
245
+ latField: string
246
+ lngField: string
247
+ labelField?: string
248
+ popupField?: string
249
+ iconField?: string
250
+ dataFields?: string[]
251
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * 数据提取工具函数(通用)
3
+ * 用于所有组件的数据源、路径解析、数据格式化等
4
+ * 图表、KPI、Text 等组件都使用这些工具函数提取数据
5
+ */
6
+
7
+ /**
8
+ * 从对象中根据路径提取值
9
+ * 支持点号路径和数组索引,如: "data.chart.values" 或 "items[0].name"
10
+ *
11
+ * @param obj - 源数据对象
12
+ * @param path - 提取路径,可选(为空则返回 undefined)
13
+ * @returns 提取的值,或 undefined
14
+ */
15
+ export function getValueByPath(obj: unknown, path: string | undefined): unknown {
16
+ if (!path || !obj) return undefined
17
+ try {
18
+ const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.')
19
+ let result: unknown = obj
20
+ for (const key of keys) {
21
+ if (result === null || result === undefined) return undefined
22
+ result = (result as Record<string, unknown>)[key]
23
+ }
24
+ return result
25
+ } catch {
26
+ return undefined
27
+ }
28
+ }
29
+
30
+ /**
31
+ * 从数据源提取单个数值
32
+ * 用于 KPI 组件(countUp, progress, badge 等)
33
+ *
34
+ * @param remoteData - 远程数据对象
35
+ * @param valuePath - 数值路径,可选
36
+ * @param defaultValue - 默认值
37
+ * @returns 提取的数值
38
+ */
39
+ export function extractNumber(
40
+ remoteData: unknown,
41
+ valuePath: string | undefined,
42
+ defaultValue: number = 0,
43
+ ): number {
44
+ if (!valuePath) return defaultValue
45
+
46
+ const value = getValueByPath(remoteData, valuePath)
47
+ if (typeof value === 'number') return value
48
+ if (value !== undefined && value !== null) {
49
+ const parsed = parseFloat(String(value))
50
+ return isNaN(parsed) ? defaultValue : parsed
51
+ }
52
+
53
+ return defaultValue
54
+ }
55
+
56
+ /**
57
+ * 解析逗号分隔的数字输入
58
+ * 支持 JSON 数组格式和逗号分隔格式
59
+ * @param input - 输入字符串,如 "150, 230, 224" 或 "[150, 230, 224]"
60
+ * @param defaultValue - 默认值数组
61
+ */
62
+ export function parseNumberInput(input: string | undefined, defaultValue: number[] = []): number[] {
63
+ if (!input) return defaultValue
64
+
65
+ // 尝试 JSON 解析
66
+ try {
67
+ const parsed = JSON.parse(input)
68
+ if (Array.isArray(parsed)) {
69
+ return parsed
70
+ .map((v) => (typeof v === 'number' ? v : parseFloat(String(v))))
71
+ .filter((v) => !isNaN(v))
72
+ }
73
+ } catch {
74
+ // JSON 解析失败,尝试逗号分隔
75
+ }
76
+
77
+ // 逗号分隔解析
78
+ return input
79
+ .split(',')
80
+ .map((v) => parseFloat(v.trim()))
81
+ .filter((v) => !isNaN(v))
82
+ }
83
+
84
+ /**
85
+ * 解析逗号分隔的字符串输入
86
+ * 支持 JSON 数组格式和逗号分隔格式
87
+ * @param input - 输入字符串,如 "Mon, Tue, Wed" 或 '["Mon", "Tue", "Wed"]'
88
+ * @param defaultValue - 默认值数组
89
+ */
90
+ export function parseStringInput(input: string | undefined, defaultValue: string[] = []): string[] {
91
+ if (!input) return defaultValue
92
+
93
+ // 尝试 JSON 解析
94
+ try {
95
+ const parsed = JSON.parse(input)
96
+ if (Array.isArray(parsed)) {
97
+ return parsed.map((v) => String(v))
98
+ }
99
+ } catch {
100
+ // JSON 解析失败,尝试逗号分隔
101
+ }
102
+
103
+ // 逗号分隔解析
104
+ return input
105
+ .split(',')
106
+ .map((v) => v.trim())
107
+ .filter((v) => v.length > 0)
108
+ }
109
+
110
+ /**
111
+ * 解析二维数组输入(用于散点图等)
112
+ * @param input - 输入字符串,如 "[[10, 8], [8, 7]]"
113
+ * @param defaultValue - 默认值数组
114
+ */
115
+ export function parse2DArrayInput(
116
+ input: string | undefined,
117
+ defaultValue: Array<[number, number]> = [],
118
+ ): Array<[number, number]> {
119
+ if (!input) return defaultValue
120
+
121
+ try {
122
+ const parsed = JSON.parse(input)
123
+ if (Array.isArray(parsed) && parsed.length > 0 && Array.isArray(parsed[0])) {
124
+ return parsed.map((item) => {
125
+ if (Array.isArray(item) && item.length >= 2) {
126
+ return [
127
+ typeof item[0] === 'number' ? item[0] : parseFloat(String(item[0])),
128
+ typeof item[1] === 'number' ? item[1] : parseFloat(String(item[1])),
129
+ ] as [number, number]
130
+ }
131
+ return [0, 0] as [number, number]
132
+ })
133
+ }
134
+ } catch {
135
+ // JSON 解析失败
136
+ }
137
+
138
+ return defaultValue
139
+ }
140
+
141
+ /**
142
+ * 从数据源提取数字数组
143
+ * @param remoteData - 远程数据对象
144
+ * @param dataPath - 数据路径
145
+ */
146
+ export function extractNumberArray(
147
+ remoteData: unknown,
148
+ dataPath: string | undefined,
149
+ ): number[] | undefined {
150
+ if (!dataPath) return undefined
151
+
152
+ const extractedData = getValueByPath(remoteData, dataPath)
153
+ if (Array.isArray(extractedData)) {
154
+ return extractedData.map((v) => (typeof v === 'number' ? v : parseFloat(String(v))))
155
+ }
156
+
157
+ return undefined
158
+ }
159
+
160
+ /**
161
+ * 从数据源提取字符串数组
162
+ * @param remoteData - 远程数据对象
163
+ * @param dataPath - 数据路径
164
+ */
165
+ export function extractStringArray(
166
+ remoteData: unknown,
167
+ dataPath: string | undefined,
168
+ ): string[] | undefined {
169
+ if (!dataPath) return undefined
170
+
171
+ const extractedData = getValueByPath(remoteData, dataPath)
172
+ if (Array.isArray(extractedData)) {
173
+ return extractedData.map((v) => String(v))
174
+ }
175
+
176
+ return undefined
177
+ }
178
+
179
+ /**
180
+ * 从数据源提取二维数组(用于散点图等)
181
+ * @param remoteData - 远程数据对象
182
+ * @param dataPath - 数据路径
183
+ */
184
+ export function extract2DArray(
185
+ remoteData: unknown,
186
+ dataPath: string | undefined,
187
+ ): Array<[number, number]> | undefined {
188
+ if (!dataPath) return undefined
189
+
190
+ const extractedData = getValueByPath(remoteData, dataPath)
191
+ if (Array.isArray(extractedData) && extractedData.length > 0 && Array.isArray(extractedData[0])) {
192
+ return extractedData.map((item) => {
193
+ if (Array.isArray(item) && item.length >= 2) {
194
+ return [
195
+ typeof item[0] === 'number' ? item[0] : parseFloat(String(item[0])),
196
+ typeof item[1] === 'number' ? item[1] : parseFloat(String(item[1])),
197
+ ] as [number, number]
198
+ }
199
+ return [0, 0] as [number, number]
200
+ })
201
+ }
202
+
203
+ return undefined
204
+ }
205
+
206
+ /**
207
+ * 从数据源提取字符串值
208
+ * @param remoteData - 远程数据对象
209
+ * @param dataPath - 数据路径
210
+ */
211
+ export function extractString(
212
+ remoteData: unknown,
213
+ dataPath: string | undefined,
214
+ ): string | undefined {
215
+ if (!dataPath) return undefined
216
+
217
+ const extractedData = getValueByPath(remoteData, dataPath)
218
+ return extractedData ? String(extractedData) : undefined
219
+ }
220
+
221
+ /**
222
+ * 解析 JSON 数组输入(通用)
223
+ * @param input - JSON 字符串输入
224
+ * @param defaultValue - 默认值
225
+ */
226
+ export function parseJSONInput<T = unknown>(input: string | undefined, defaultValue: T): T {
227
+ if (!input) return defaultValue
228
+
229
+ try {
230
+ const parsed = JSON.parse(input)
231
+ return parsed as T
232
+ } catch (e) {
233
+ console.error('Failed to parse JSON input:', e)
234
+ return defaultValue
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 提取并规范化 Sankey 图节点数据
240
+ * @param remoteData - 远程数据对象
241
+ * @param dataPath - 数据路径
242
+ */
243
+ export function extractSankeyNodes(
244
+ remoteData: unknown,
245
+ dataPath: string | undefined,
246
+ ): Array<{ name: string; value?: number; depth?: number; itemStyle?: unknown }> | undefined {
247
+ if (!dataPath) return undefined
248
+
249
+ const nodesData = getValueByPath(remoteData, dataPath)
250
+ if (!Array.isArray(nodesData) || nodesData.length === 0) return undefined
251
+
252
+ return nodesData.map((node: unknown) => {
253
+ const obj =
254
+ typeof node === 'object' && node !== null ? (node as Record<string, unknown>) : undefined
255
+ const name = typeof node === 'string' ? node : String(obj?.name ?? obj?.id ?? '')
256
+
257
+ return {
258
+ name,
259
+ value:
260
+ obj && typeof obj.value === 'number'
261
+ ? (obj.value as number)
262
+ : obj && obj.value != null
263
+ ? parseFloat(String(obj.value))
264
+ : undefined,
265
+ depth:
266
+ obj && typeof obj.depth === 'number'
267
+ ? (obj.depth as number)
268
+ : obj && obj.depth != null
269
+ ? parseFloat(String(obj.depth))
270
+ : undefined,
271
+ itemStyle: obj ? obj.itemStyle : undefined,
272
+ }
273
+ })
274
+ }
275
+
276
+ /**
277
+ * 提取并规范化 Sankey 图连接数据
278
+ * @param remoteData - 远程数据对象
279
+ * @param dataPath - 数据路径
280
+ */
281
+ export function extractSankeyLinks(
282
+ remoteData: unknown,
283
+ dataPath: string | undefined,
284
+ ): Array<{ source: string; target: string; value: number }> | undefined {
285
+ if (!dataPath) return undefined
286
+
287
+ const linksData = getValueByPath(remoteData, dataPath)
288
+ if (!Array.isArray(linksData) || linksData.length === 0) return undefined
289
+
290
+ return linksData.map((link: unknown) => {
291
+ const l = link as Record<string, unknown>
292
+ return {
293
+ source: (l.source || l.from || '') as string,
294
+ target: (l.target || l.to || '') as string,
295
+ value: (l.value || 0) as number,
296
+ }
297
+ })
298
+ }
299
+
300
+ /**
301
+ * KPI 组件专用:提取多个字段的数据
302
+ * 用于 stat 组件等需要同时提取多个字段的场景
303
+ *
304
+ * @param remoteData - 远程数据对象
305
+ * @param paths - 字段路径映射 { title: 'data.title', value: 'data.value', ... }
306
+ * @returns 提取的数据对象
307
+ *
308
+ * @example
309
+ * const data = extractMultipleFields(remoteData, {
310
+ * title: 'data.kpi.title',
311
+ * value: 'data.kpi.value',
312
+ * change: 'data.kpi.change'
313
+ * })
314
+ * // 返回: { title: '销售额', value: 12345, change: 5.2 }
315
+ */
316
+ export function extractMultipleFields<T extends Record<string, string | undefined>>(
317
+ remoteData: unknown,
318
+ paths: T,
319
+ ): Record<keyof T, unknown> {
320
+ const result: Record<string, unknown> = {}
321
+
322
+ for (const [key, path] of Object.entries(paths)) {
323
+ if (path && typeof path === 'string') {
324
+ result[key] = getValueByPath(remoteData, path)
325
+ }
326
+ }
327
+
328
+ return result as Record<keyof T, unknown>
329
+ }
330
+
331
+ /**
332
+ * 智能提取数据:优先使用路径提取,否则使用默认值
333
+ * 用于所有简单组件(text, countUp, progress, badge 等)
334
+ *
335
+ * @param remoteData - 远程数据对象
336
+ * @param path - 数据路径
337
+ * @param fallbackValue - 回退值(当路径未配置或提取失败时使用)
338
+ * @returns 提取的数据或回退值
339
+ */
340
+ export function extractWithFallback<T>(
341
+ remoteData: unknown,
342
+ path: string | undefined,
343
+ fallbackValue: T,
344
+ ): T {
345
+ if (!path) return fallbackValue
346
+
347
+ const extracted = getValueByPath(remoteData, path)
348
+ return (extracted ?? fallbackValue) as T
349
+ }