@yuku123/z-frontend-common 0.2.9 → 1.1.0

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 (75) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +55 -42
  3. package/dist/PagedTable-DyjNokXN.js +1211 -0
  4. package/dist/index.js +310 -0
  5. package/dist/ui.js +27 -0
  6. package/package.json +52 -44
  7. package/src/components/ErrorBoundary/index.jsx +24 -77
  8. package/src/components/Layout/index.jsx +141 -38
  9. package/src/components/LoginPage/index.jsx +47 -21
  10. package/src/components/StatusTag/index.jsx +14 -12
  11. package/src/components/ui/CategoryDot.jsx +38 -0
  12. package/src/components/ui/DarkBanner.jsx +142 -0
  13. package/src/components/ui/EChartsChart.jsx +82 -0
  14. package/src/components/ui/EmptyState.jsx +94 -0
  15. package/src/components/ui/ErrorState.jsx +114 -0
  16. package/src/components/ui/FeatureCard.jsx +205 -0
  17. package/src/components/ui/GradientStatCard.jsx +196 -0
  18. package/src/components/ui/LoadingState.jsx +116 -0
  19. package/src/components/ui/PageHeader.jsx +143 -0
  20. package/src/components/ui/PagedTable.jsx +83 -0
  21. package/src/components/ui/SearchInput.jsx +111 -0
  22. package/src/components/ui/SectionHeader.jsx +76 -0
  23. package/src/components/ui/StatusBadge.jsx +66 -0
  24. package/src/components/ui/TableToolbar.jsx +58 -0
  25. package/src/components/ui/index.js +36 -0
  26. package/src/components/ui/tokens.js +224 -0
  27. package/src/index.js +33 -37
  28. package/src/services/request.js +74 -4
  29. package/dist/z-frontend-common.css +0 -1
  30. package/dist/z-frontend-common.es.js +0 -51275
  31. package/dist/z-frontend-common.umd.js +0 -459
  32. package/src/components/Ctc/Layout.jsx +0 -330
  33. package/src/components/Ctc/Layout.module.css +0 -145
  34. package/src/components/Ctc/agentTeam/index.tsx +0 -308
  35. package/src/components/Ctc/login/AuthPage.module.css +0 -26
  36. package/src/components/Ctc/login/AuthPage.tsx +0 -235
  37. package/src/components/Ctc/login/index.less +0 -49
  38. package/src/components/Ctc/login/index.tsx +0 -142
  39. package/src/components/Ctc/uc/UCLogin.tsx +0 -31
  40. package/src/components/Ctc/uc/UCPhoneLogin.tsx +0 -62
  41. package/src/components/Ctc/uc/UCRegister.tsx +0 -37
  42. package/src/components/Ctc/uc/UCResetPassword.tsx +0 -69
  43. package/src/components/Ctc/uc/index.tsx +0 -15
  44. package/src/components/Ctc/userPanel/index.tsx +0 -999
  45. package/src/components/Ctc/webide/index.tsx +0 -272
  46. package/src/components/LowCode/LowCodeModel.jsx +0 -962
  47. package/src/components/LowCode/LowCodePage.jsx +0 -31
  48. package/src/components/LowCode/LowCodeRuntime.jsx +0 -335
  49. package/src/components/LowCode/MaterializePage.jsx +0 -235
  50. package/src/components/LowCode/index.js +0 -1
  51. package/src/components/MockPlatform/CurlImportModal.jsx +0 -362
  52. package/src/components/MockPlatform/EndpointsTab.jsx +0 -509
  53. package/src/components/MockPlatform/EnvironmentsTab.jsx +0 -212
  54. package/src/components/MockPlatform/MockTemplateHelper.jsx +0 -200
  55. package/src/components/MockPlatform/OpenApiImportModal.jsx +0 -305
  56. package/src/components/MockPlatform/RecordingsTab.jsx +0 -397
  57. package/src/components/MockPlatform/RequestLogsTab.jsx +0 -239
  58. package/src/components/MockPlatform/ScenariosTab.jsx +0 -236
  59. package/src/components/MockPlatform/TestCasesTab.jsx +0 -462
  60. package/src/components/MockPlatform/index.jsx +0 -127
  61. package/src/components/Overview.jsx +0 -74
  62. package/src/dev.jsx +0 -28
  63. package/src/services/agentTeam.js +0 -7
  64. package/src/services/api.js +0 -84
  65. package/src/services/ctcAc.js +0 -7
  66. package/src/services/ctcAcDomain.js +0 -7
  67. package/src/services/ctcAuthorization.js +0 -7
  68. package/src/services/ctcSurl.js +0 -7
  69. package/src/services/ctcUser.js +0 -7
  70. package/src/services/job.js +0 -7
  71. package/src/services/metaApp.js +0 -7
  72. package/src/services/privateConfig.js +0 -7
  73. package/src/services/webide.js +0 -6
  74. package/src/services/workspace.js +0 -21
  75. package/src/utils/request.ts +0 -102
@@ -0,0 +1,94 @@
1
+ import {Button} from 'antd'
2
+ import {InboxOutlined} from '@ant-design/icons'
3
+ import {neutral, radius} from './tokens'
4
+
5
+ /**
6
+ * 统一空状态 - 借鉴 ui-ux-pro-max P8: Treat failure and emptiness as moments for direction.
7
+ *
8
+ * 原则 (来自 anthropics/frontend-design):
9
+ * - An empty screen is an invitation to act.
10
+ * - 不要只画一个插画加一行字, 要有明确的"下一步动作".
11
+ * - 默认尺寸 44×44 满足 ui-ux-pro-max P2 (touch target).
12
+ *
13
+ * @param {Object} props
14
+ * @param {ReactNode} [props.icon] - 自定义图标
15
+ * @param {string} [props.title] - 主标题 (默认 "暂无数据")
16
+ * @param {string} [props.description] - 描述 (解释空的原因 + 下一步)
17
+ * @param {string} [props.actionText] - 行动按钮文案
18
+ * @param {Function} [props.onAction] - 行动回调
19
+ * @param {string} [props.size] - sm | md | lg (默认 md)
20
+ * @param {Object} [props.style] - 容器样式
21
+ */
22
+ export default function EmptyState({
23
+ icon,
24
+ title = '暂无数据',
25
+ description,
26
+ actionText,
27
+ onAction,
28
+ size = 'md',
29
+ style,
30
+ }) {
31
+ const padMap = {sm: 24, md: 40, lg: 64}
32
+ const titleSize = {sm: 14, md: 16, lg: 20}
33
+
34
+ return (
35
+ <div
36
+ role="status"
37
+ aria-live="polite"
38
+ style={{
39
+ padding: padMap[size] || padMap.md,
40
+ textAlign: 'center',
41
+ background: '#ffffff',
42
+ borderRadius: radius.md,
43
+ border: '1px dashed #e5e7eb',
44
+ ...style,
45
+ }}
46
+ >
47
+ <div style={{
48
+ display: 'inline-flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ width: 56, height: 56,
52
+ borderRadius: '50%',
53
+ background: neutral.surfaceAlt,
54
+ marginBottom: 16,
55
+ fontSize: 28,
56
+ color: '#9ca3af',
57
+ }}>
58
+ {icon || <InboxOutlined/>}
59
+ </div>
60
+ <div style={{
61
+ fontSize: titleSize[size],
62
+ fontWeight: 600,
63
+ color: neutral.text,
64
+ marginBottom: description ? 6 : 16,
65
+ }}>
66
+ {title}
67
+ </div>
68
+ {description && (
69
+ <div style={{
70
+ fontSize: 13,
71
+ color: neutral.textMuted,
72
+ lineHeight: 1.6,
73
+ maxWidth: 360,
74
+ margin: '0 auto 16px',
75
+ }}>
76
+ {description}
77
+ </div>
78
+ )}
79
+ {actionText && onAction && (
80
+ <Button
81
+ type="primary"
82
+ onClick={onAction}
83
+ style={{
84
+ background: 'linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%)',
85
+ border: 'none',
86
+ minHeight: 36,
87
+ }}
88
+ >
89
+ {actionText}
90
+ </Button>
91
+ )}
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,114 @@
1
+ import {Button} from 'antd'
2
+ import {ExclamationCircleOutlined, ReloadOutlined} from '@ant-design/icons'
3
+ import {neutral, radius} from './tokens'
4
+
5
+ /**
6
+ * 统一错误状态 - 借鉴 ui-ux-pro-max P1/P8 + frontend-design "Treat failure as moments for direction".
7
+ *
8
+ * 原则:
9
+ * - 错误就近显示 (不要只发一个顶部 message)
10
+ * - 明确"发生了什么" + "下一步怎么做"
11
+ * - 提供重试入口
12
+ * - 不用道歉 ("对不起..." 是 AI 默认)
13
+ *
14
+ * @param {Object} props
15
+ * @param {Error|string} props.error - 错误对象或文案
16
+ * @param {string} [props.title] - 主标题
17
+ * @param {string} [props.description] - 解释发生了什么 + 下一步
18
+ * @param {Function} [props.onRetry] - 重试回调
19
+ * @param {string} [props.retryText] - 重试按钮文案
20
+ * @param {'inline'|'full'} [props.variant] - inline 紧凑嵌入页面, full 占满卡片
21
+ * @param {Object} [props.style]
22
+ */
23
+ export default function ErrorState({
24
+ error,
25
+ title = '加载失败',
26
+ description,
27
+ onRetry,
28
+ retryText = '重新加载',
29
+ variant = 'inline',
30
+ style,
31
+ }) {
32
+ const isFull = variant === 'full'
33
+
34
+ // 提取错误消息。**先取后端响应体里的业务 message**,再退回 axios/字符串:
35
+ // HTTP 500 时 axios 只给 "Request failed with status code 500",而真因在同一个响应的 `message` 里
36
+ // (实测 `/api/config/pageConfig` ⇒ "Unknown column 'encrypted_data_key' in 'field list'")。
37
+ // 原来无论拿到什么都在末尾追加"请检查网络或稍后重试" —— 那是把**服务端缺陷**说成**用户网络问题**。
38
+ const body = error?.response?.data
39
+ const backendMsg = (body && typeof body === 'object')
40
+ ? (typeof body.message === 'string' && body.message) || (typeof body.msg === 'string' && body.msg) || null
41
+ : null
42
+ const errMsg = backendMsg
43
+ || (typeof error === 'string'
44
+ ? error
45
+ : (error?.message || error?.msg || (error ? String(error) : null)))
46
+
47
+ const finalDescription = description
48
+ || (backendMsg
49
+ ? `原因: ${backendMsg}`
50
+ : (errMsg ? `原因: ${errMsg}。请检查网络或稍后重试。` : '请检查网络或稍后重试。'))
51
+
52
+ return (
53
+ <div
54
+ role="alert"
55
+ aria-live="assertive"
56
+ style={{
57
+ padding: isFull ? '48px 24px' : '20px 24px',
58
+ background: isFull ? '#ffffff' : '#fef2f2',
59
+ border: isFull ? `1px dashed ${neutral.border}` : '1px solid #fecaca',
60
+ borderRadius: radius.md,
61
+ textAlign: isFull ? 'center' : 'left',
62
+ display: 'flex',
63
+ flexDirection: isFull ? 'column' : 'row',
64
+ alignItems: isFull ? 'center' : 'flex-start',
65
+ gap: isFull ? 12 : 16,
66
+ ...style,
67
+ }}
68
+ >
69
+ <div style={{
70
+ display: 'inline-flex',
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ width: isFull ? 56 : 36, height: isFull ? 56 : 36,
74
+ borderRadius: '50%',
75
+ background: isFull ? '#fef2f2' : '#ffffff',
76
+ color: '#dc2626',
77
+ fontSize: isFull ? 28 : 18,
78
+ flexShrink: 0,
79
+ }}>
80
+ <ExclamationCircleOutlined/>
81
+ </div>
82
+ <div style={{flex: 1, minWidth: 0}}>
83
+ <div style={{
84
+ fontSize: isFull ? 16 : 14,
85
+ fontWeight: 600,
86
+ color: '#991b1b',
87
+ marginBottom: 4,
88
+ }}>
89
+ {title}
90
+ </div>
91
+ <div style={{
92
+ fontSize: 13,
93
+ color: '#7f1d1d',
94
+ lineHeight: 1.6,
95
+ wordBreak: 'break-word',
96
+ }}>
97
+ {finalDescription}
98
+ </div>
99
+ {onRetry && (
100
+ <Button
101
+ type="primary"
102
+ danger
103
+ icon={<ReloadOutlined/>}
104
+ onClick={onRetry}
105
+ size={isFull ? 'middle' : 'small'}
106
+ style={{marginTop: 12}}
107
+ >
108
+ {retryText}
109
+ </Button>
110
+ )}
111
+ </div>
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,205 @@
1
+ import {useState} from 'react'
2
+ import {palette, radius, shadow, space} from './tokens'
3
+
4
+ /**
5
+ * 功能卡 - 借鉴 wenming7/marketplace 应用卡设计
6
+ *
7
+ * 视觉:
8
+ * ┌────────────────────────────┐
9
+ * │ [封面/图标区 16:9 / 4:3] │ ← 可选渐变色背景 + 大图标
10
+ * ├────────────────────────────┤
11
+ * │ [chip] [chip] │ ← 标签 chips
12
+ * │ 模块标题 │
13
+ * │ 副描述 · 1234 次使用 │ ← 元信息
14
+ * └────────────────────────────�
15
+ *
16
+ * 单位面积信息密度高于 antd Card, 视觉更现代化.
17
+ *
18
+ * @param {Object} props
19
+ * @param {string} props.title - 标题
20
+ * @param {string} [props.description] - 描述
21
+ * @param {ReactNode} [props.icon] - 封面图标 (如不传 cover 则渲染渐变封面)
22
+ * @param {string} [props.cover] - 封面图片 URL
23
+ * @param {string} [props.color] - 配色 (用于渐变封面)
24
+ * @param {Array<{label: string, color?: string}>} [props.tags] - 标签 chips
25
+ * @param {string} [props.meta] - 元信息 (如 "1234 次使用")
26
+ * @param {string} [props.href] - 跳转路径
27
+ * @param {ReactNode} [props.extra] - 右上角额外内容 (如 NEW 角标)
28
+ */
29
+ export default function FeatureCard({
30
+ title,
31
+ description,
32
+ icon,
33
+ cover,
34
+ color = 'violet',
35
+ tags = [],
36
+ meta,
37
+ href,
38
+ extra,
39
+ }) {
40
+ const c = palette[color] || palette.violet
41
+ const [hover, setHover] = useState(false)
42
+
43
+ const handleClick = () => {
44
+ if (href) {
45
+ if (typeof window !== 'undefined') {
46
+ window.location.href = href
47
+ }
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div
53
+ onClick={handleClick}
54
+ onMouseEnter={() => setHover(true)}
55
+ onMouseLeave={() => setHover(false)}
56
+ style={{
57
+ background: '#ffffff',
58
+ borderRadius: radius.lg,
59
+ overflow: 'hidden',
60
+ cursor: href ? 'pointer' : 'default',
61
+ boxShadow: hover ? shadow.lg : shadow.sm,
62
+ transform: hover ? 'translateY(-2px)' : 'translateY(0)',
63
+ transition: 'transform 200ms ease, box-shadow 200ms ease',
64
+ border: '1px solid #f1f5f9',
65
+ display: 'flex',
66
+ flexDirection: 'column',
67
+ }}
68
+ >
69
+ {/* 封面区 */}
70
+ <div style={{
71
+ position: 'relative',
72
+ aspectRatio: '16 / 9',
73
+ background: cover ? '#f1f5f9' : c.gradient,
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ justifyContent: 'center',
77
+ overflow: 'hidden',
78
+ }}>
79
+ {cover ? (
80
+ <img
81
+ src={cover}
82
+ alt={title}
83
+ style={{
84
+ width: '100%',
85
+ height: '100%',
86
+ objectFit: 'cover',
87
+ }}
88
+ />
89
+ ) : (
90
+ <div style={{
91
+ fontSize: 48,
92
+ color: 'rgba(255, 255, 255, 0.9)',
93
+ display: 'flex',
94
+ }}>
95
+ {icon}
96
+ </div>
97
+ )}
98
+ {/* 装饰圆 */}
99
+ {!cover && (
100
+ <div style={{
101
+ position: 'absolute',
102
+ top: -24,
103
+ right: -24,
104
+ width: 80,
105
+ height: 80,
106
+ borderRadius: '50%',
107
+ background: 'rgba(255, 255, 255, 0.15)',
108
+ }}/>
109
+ )}
110
+ {/* 右上角额外内容 (如 NEW 角标) */}
111
+ {extra && (
112
+ <div style={{
113
+ position: 'absolute',
114
+ top: 8,
115
+ right: 8,
116
+ }}>
117
+ {extra}
118
+ </div>
119
+ )}
120
+ </div>
121
+
122
+ {/* 内容区 */}
123
+ <div style={{
124
+ padding: space[4],
125
+ display: 'flex',
126
+ flexDirection: 'column',
127
+ gap: space[2],
128
+ flex: 1,
129
+ }}>
130
+ {/* chips */}
131
+ {tags.length > 0 && (
132
+ <div style={{
133
+ display: 'flex',
134
+ flexWrap: 'wrap',
135
+ gap: 6,
136
+ }}>
137
+ {tags.slice(0, 3).map((t, i) => {
138
+ const tc = palette[t.color] || palette.blue
139
+ return (
140
+ <span
141
+ key={i}
142
+ style={{
143
+ fontSize: 11,
144
+ fontWeight: 500,
145
+ padding: '2px 8px',
146
+ borderRadius: 999,
147
+ background: tc.soft,
148
+ color: tc.bg,
149
+ }}
150
+ >
151
+ {t.label}
152
+ </span>
153
+ )
154
+ })}
155
+ </div>
156
+ )}
157
+
158
+ {/* 标题 */}
159
+ <div style={{
160
+ fontSize: 15,
161
+ fontWeight: 600,
162
+ color: '#111827',
163
+ lineHeight: 1.3,
164
+ overflow: 'hidden',
165
+ textOverflow: 'ellipsis',
166
+ display: '-webkit-box',
167
+ WebkitLineClamp: 1,
168
+ WebkitBoxOrient: 'vertical',
169
+ }}>
170
+ {title}
171
+ </div>
172
+
173
+ {/* 描述 */}
174
+ {description && (
175
+ <div style={{
176
+ fontSize: 12,
177
+ color: '#6b7280',
178
+ lineHeight: 1.5,
179
+ overflow: 'hidden',
180
+ textOverflow: 'ellipsis',
181
+ display: '-webkit-box',
182
+ WebkitLineClamp: 2,
183
+ WebkitBoxOrient: 'vertical',
184
+ minHeight: 36,
185
+ }}>
186
+ {description}
187
+ </div>
188
+ )}
189
+
190
+ {/* 元信息 */}
191
+ {meta && (
192
+ <div style={{
193
+ fontSize: 11,
194
+ color: '#9ca3af',
195
+ marginTop: 'auto',
196
+ paddingTop: space[2],
197
+ borderTop: '1px dashed #f1f5f9',
198
+ }}>
199
+ {meta}
200
+ </div>
201
+ )}
202
+ </div>
203
+ </div>
204
+ )
205
+ }
@@ -0,0 +1,196 @@
1
+ import {useEffect, useRef, useState} from 'react'
2
+ import {palette, radius, shadow, space} from './tokens'
3
+
4
+ /**
5
+ * 渐变数据卡 - 借鉴 wenming7/marketplace 数据卡设计
6
+ *
7
+ * 视觉特征 (相对 antd Statistic 优势):
8
+ * - 渐变彩色背景 (pink/blue/green/orange/violet/cyan)
9
+ * - 圆角图标 + 大数字 + 小标签 (单位面积信息密度高)
10
+ * - 数字滚动动画 (现代化微交互)
11
+ * - 悬浮微抬升 + 阴影变化
12
+ * - 可选 trend (同比/环比箭头)
13
+ *
14
+ * @param {Object} props
15
+ * @param {ReactNode} props.icon - 图标 (React 节点, 通常是 antd icon)
16
+ * @param {string} props.label - 标签 (如 "上架应用")
17
+ * @param {number} props.value - 数值
18
+ * @param {string} [props.suffix] - 单位 (如 "万", "%", "+")
19
+ * @param {string} [props.color] - 配色 key (pink|blue|green|orange|violet|cyan)
20
+ * @param {number|string} [props.trend] - 同比变化 (正数绿箭头上升, 负数红箭头下降)
21
+ * @param {string} [props.href] - 点击跳转路径
22
+ * @param {boolean} [props.loading] - 加载态
23
+ * @param {string} [props.precision] - 小数位
24
+ */
25
+ export default function GradientStatCard({
26
+ icon,
27
+ label,
28
+ value,
29
+ suffix,
30
+ color = 'blue',
31
+ trend,
32
+ href,
33
+ loading = false,
34
+ precision = 0,
35
+ }) {
36
+ const c = palette[color] || palette.blue
37
+ const [display, setDisplay] = useState(0)
38
+ const prevRef = useRef(0)
39
+
40
+ // 数字滚动动画
41
+ useEffect(() => {
42
+ if (loading || value == null) return
43
+ const from = prevRef.current
44
+ const to = Number(value) || 0
45
+ if (from === to) {
46
+ setDisplay(to)
47
+ return
48
+ }
49
+ const duration = 600
50
+ const start = performance.now()
51
+ let raf
52
+ const tick = (now) => {
53
+ const p = Math.min(1, (now - start) / duration)
54
+ // easeOutCubic
55
+ const eased = 1 - Math.pow(1 - p, 3)
56
+ const cur = from + (to - from) * eased
57
+ setDisplay(cur)
58
+ if (p < 1) raf = requestAnimationFrame(tick)
59
+ else prevRef.current = to
60
+ }
61
+ raf = requestAnimationFrame(tick)
62
+ return () => raf && cancelAnimationFrame(raf)
63
+ }, [value, loading])
64
+
65
+ const handleClick = () => {
66
+ if (href && typeof window !== 'undefined') {
67
+ window.location.href = href
68
+ }
69
+ }
70
+
71
+ const formatted = loading
72
+ ? '--'
73
+ : Number(display).toLocaleString('zh-CN', {
74
+ minimumFractionDigits: precision,
75
+ maximumFractionDigits: precision,
76
+ })
77
+
78
+ const trendNum = trend != null ? Number(trend) : null
79
+ const trendUp = trendNum != null && trendNum >= 0
80
+
81
+ return (
82
+ <div
83
+ onClick={handleClick}
84
+ style={{
85
+ position: 'relative',
86
+ background: c.gradient,
87
+ borderRadius: radius.lg,
88
+ padding: space[5],
89
+ color: c.fg,
90
+ cursor: href ? 'pointer' : 'default',
91
+ boxShadow: shadow.md,
92
+ overflow: 'hidden',
93
+ transition: 'transform 200ms ease, box-shadow 200ms ease',
94
+ minHeight: 110,
95
+ display: 'flex',
96
+ flexDirection: 'column',
97
+ justifyContent: 'space-between',
98
+ }}
99
+ onMouseEnter={(e) => {
100
+ e.currentTarget.style.transform = 'translateY(-2px)'
101
+ e.currentTarget.style.boxShadow = shadow.lg
102
+ }}
103
+ onMouseLeave={(e) => {
104
+ e.currentTarget.style.transform = 'translateY(0)'
105
+ e.currentTarget.style.boxShadow = shadow.md
106
+ }}
107
+ >
108
+ {/* 右上角装饰圆 - 借鉴 wenming7 数据卡的光感 */}
109
+ <div
110
+ style={{
111
+ position: 'absolute',
112
+ top: -32,
113
+ right: -32,
114
+ width: 96,
115
+ height: 96,
116
+ borderRadius: '50%',
117
+ background: 'rgba(255, 255, 255, 0.15)',
118
+ pointerEvents: 'none',
119
+ }}
120
+ />
121
+
122
+ {/* 图标 + 标签 */}
123
+ <div style={{
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ gap: space[2],
127
+ position: 'relative',
128
+ }}>
129
+ <div style={{
130
+ width: 36,
131
+ height: 36,
132
+ borderRadius: radius.sm,
133
+ background: 'rgba(255, 255, 255, 0.20)',
134
+ backdropFilter: 'blur(8px)',
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ fontSize: 18,
139
+ }}>
140
+ {icon}
141
+ </div>
142
+ <div style={{
143
+ fontSize: 13,
144
+ fontWeight: 500,
145
+ opacity: 0.92,
146
+ }}>
147
+ {label}
148
+ </div>
149
+ </div>
150
+
151
+ {/* 数值 + trend */}
152
+ <div style={{
153
+ display: 'flex',
154
+ alignItems: 'baseline',
155
+ gap: space[2],
156
+ position: 'relative',
157
+ marginTop: space[3],
158
+ }}>
159
+ <div style={{
160
+ fontSize: 30,
161
+ fontWeight: 700,
162
+ lineHeight: 1.1,
163
+ letterSpacing: '-0.02em',
164
+ fontVariantNumeric: 'tabular-nums',
165
+ }}>
166
+ {formatted}
167
+ </div>
168
+ {suffix && (
169
+ <div style={{fontSize: 14, opacity: 0.85, fontWeight: 500}}>
170
+ {suffix}
171
+ </div>
172
+ )}
173
+ {trendNum != null && (
174
+ <div style={{
175
+ marginLeft: 'auto',
176
+ fontSize: 12,
177
+ fontWeight: 600,
178
+ padding: '2px 8px',
179
+ borderRadius: 999,
180
+ background: trendUp
181
+ ? 'rgba(255, 255, 255, 0.22)'
182
+ : 'rgba(0, 0, 0, 0.18)',
183
+ display: 'flex',
184
+ alignItems: 'center',
185
+ gap: 2,
186
+ }}>
187
+ <span style={{fontSize: 10}}>
188
+ {trendUp ? '▲' : '▼'}
189
+ </span>
190
+ {Math.abs(trendNum).toFixed(1)}%
191
+ </div>
192
+ )}
193
+ </div>
194
+ </div>
195
+ )
196
+ }