@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.
- package/LICENSE +25 -0
- package/README.md +55 -42
- package/dist/PagedTable-DyjNokXN.js +1211 -0
- package/dist/index.js +310 -0
- package/dist/ui.js +27 -0
- package/package.json +52 -44
- package/src/components/ErrorBoundary/index.jsx +24 -77
- package/src/components/Layout/index.jsx +141 -38
- package/src/components/LoginPage/index.jsx +47 -21
- package/src/components/StatusTag/index.jsx +14 -12
- package/src/components/ui/CategoryDot.jsx +38 -0
- package/src/components/ui/DarkBanner.jsx +142 -0
- package/src/components/ui/EChartsChart.jsx +82 -0
- package/src/components/ui/EmptyState.jsx +94 -0
- package/src/components/ui/ErrorState.jsx +114 -0
- package/src/components/ui/FeatureCard.jsx +205 -0
- package/src/components/ui/GradientStatCard.jsx +196 -0
- package/src/components/ui/LoadingState.jsx +116 -0
- package/src/components/ui/PageHeader.jsx +143 -0
- package/src/components/ui/PagedTable.jsx +83 -0
- package/src/components/ui/SearchInput.jsx +111 -0
- package/src/components/ui/SectionHeader.jsx +76 -0
- package/src/components/ui/StatusBadge.jsx +66 -0
- package/src/components/ui/TableToolbar.jsx +58 -0
- package/src/components/ui/index.js +36 -0
- package/src/components/ui/tokens.js +224 -0
- package/src/index.js +33 -37
- package/src/services/request.js +74 -4
- package/dist/z-frontend-common.css +0 -1
- package/dist/z-frontend-common.es.js +0 -51275
- package/dist/z-frontend-common.umd.js +0 -459
- package/src/components/Ctc/Layout.jsx +0 -330
- package/src/components/Ctc/Layout.module.css +0 -145
- package/src/components/Ctc/agentTeam/index.tsx +0 -308
- package/src/components/Ctc/login/AuthPage.module.css +0 -26
- package/src/components/Ctc/login/AuthPage.tsx +0 -235
- package/src/components/Ctc/login/index.less +0 -49
- package/src/components/Ctc/login/index.tsx +0 -142
- package/src/components/Ctc/uc/UCLogin.tsx +0 -31
- package/src/components/Ctc/uc/UCPhoneLogin.tsx +0 -62
- package/src/components/Ctc/uc/UCRegister.tsx +0 -37
- package/src/components/Ctc/uc/UCResetPassword.tsx +0 -69
- package/src/components/Ctc/uc/index.tsx +0 -15
- package/src/components/Ctc/userPanel/index.tsx +0 -999
- package/src/components/Ctc/webide/index.tsx +0 -272
- package/src/components/LowCode/LowCodeModel.jsx +0 -962
- package/src/components/LowCode/LowCodePage.jsx +0 -31
- package/src/components/LowCode/LowCodeRuntime.jsx +0 -335
- package/src/components/LowCode/MaterializePage.jsx +0 -235
- package/src/components/LowCode/index.js +0 -1
- package/src/components/MockPlatform/CurlImportModal.jsx +0 -362
- package/src/components/MockPlatform/EndpointsTab.jsx +0 -509
- package/src/components/MockPlatform/EnvironmentsTab.jsx +0 -212
- package/src/components/MockPlatform/MockTemplateHelper.jsx +0 -200
- package/src/components/MockPlatform/OpenApiImportModal.jsx +0 -305
- package/src/components/MockPlatform/RecordingsTab.jsx +0 -397
- package/src/components/MockPlatform/RequestLogsTab.jsx +0 -239
- package/src/components/MockPlatform/ScenariosTab.jsx +0 -236
- package/src/components/MockPlatform/TestCasesTab.jsx +0 -462
- package/src/components/MockPlatform/index.jsx +0 -127
- package/src/components/Overview.jsx +0 -74
- package/src/dev.jsx +0 -28
- package/src/services/agentTeam.js +0 -7
- package/src/services/api.js +0 -84
- package/src/services/ctcAc.js +0 -7
- package/src/services/ctcAcDomain.js +0 -7
- package/src/services/ctcAuthorization.js +0 -7
- package/src/services/ctcSurl.js +0 -7
- package/src/services/ctcUser.js +0 -7
- package/src/services/job.js +0 -7
- package/src/services/metaApp.js +0 -7
- package/src/services/privateConfig.js +0 -7
- package/src/services/webide.js +0 -6
- package/src/services/workspace.js +0 -21
- package/src/utils/request.ts +0 -102
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {Skeleton} from 'antd'
|
|
2
|
+
import {radius} from './tokens'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LoadingState - 统一骨架屏 - 借鉴 ui-ux-pro-max P3: Reserve space (CLS < 0.1).
|
|
6
|
+
*
|
|
7
|
+
* 原则:
|
|
8
|
+
* - 提前占位避免内容跳动 (CLS)
|
|
9
|
+
* - 不用 spinner (除全屏初次加载), 用骨架屏更专业
|
|
10
|
+
* - 提供常见布局预设
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} props
|
|
13
|
+
* @param {'cards'|'table'|'list'|'detail'} [props.variant] - 预设布局
|
|
14
|
+
* @param {number} [props.rows] - 行数
|
|
15
|
+
* @param {boolean} [props.active] - 是否显示流光效果
|
|
16
|
+
* @param {Object} [props.style]
|
|
17
|
+
*/
|
|
18
|
+
export default function LoadingState({
|
|
19
|
+
variant = 'cards',
|
|
20
|
+
rows = 4,
|
|
21
|
+
active = true,
|
|
22
|
+
style,
|
|
23
|
+
}) {
|
|
24
|
+
if (variant === 'cards') {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
role="status"
|
|
28
|
+
aria-busy="true"
|
|
29
|
+
aria-label="加载中"
|
|
30
|
+
style={{
|
|
31
|
+
display: 'grid',
|
|
32
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
|
33
|
+
gap: 16,
|
|
34
|
+
...style,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{Array.from({length: rows}).map((_, i) => (
|
|
38
|
+
<div
|
|
39
|
+
key={i}
|
|
40
|
+
style={{
|
|
41
|
+
background: '#ffffff',
|
|
42
|
+
borderRadius: radius.lg,
|
|
43
|
+
overflow: 'hidden',
|
|
44
|
+
border: '1px solid #f1f5f9',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<Skeleton.Image
|
|
48
|
+
active={active}
|
|
49
|
+
style={{width: '100%', height: 140, borderRadius: 0}}
|
|
50
|
+
/>
|
|
51
|
+
<div style={{padding: 16}}>
|
|
52
|
+
<Skeleton active={active} paragraph={{rows: 2}} title={{width: '60%'}}/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (variant === 'table') {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
role="status"
|
|
64
|
+
aria-busy="true"
|
|
65
|
+
aria-label="加载中"
|
|
66
|
+
style={{
|
|
67
|
+
background: '#ffffff',
|
|
68
|
+
padding: 24,
|
|
69
|
+
borderRadius: radius.md,
|
|
70
|
+
...style,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<Skeleton active={active} paragraph={{rows}}/>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (variant === 'list') {
|
|
79
|
+
return (
|
|
80
|
+
<div role="status" aria-busy="true" aria-label="加载中" style={style}>
|
|
81
|
+
{Array.from({length: rows}).map((_, i) => (
|
|
82
|
+
<div
|
|
83
|
+
key={i}
|
|
84
|
+
style={{
|
|
85
|
+
display: 'flex',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
gap: 12,
|
|
88
|
+
padding: '12px 0',
|
|
89
|
+
borderBottom: i < rows - 1 ? '1px solid #f1f5f9' : 'none',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<Skeleton.Avatar active={active} size="default"/>
|
|
93
|
+
<Skeleton active={active} paragraph={{rows: 1}} title={{width: 120}} style={{flex: 1}}/>
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// detail: 用于详情页
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
role="status"
|
|
104
|
+
aria-busy="true"
|
|
105
|
+
aria-label="加载中"
|
|
106
|
+
style={{
|
|
107
|
+
background: '#ffffff',
|
|
108
|
+
padding: 32,
|
|
109
|
+
borderRadius: radius.md,
|
|
110
|
+
...style,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<Skeleton active={active} title={{width: '40%'}} paragraph={{rows}}/>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {Breadcrumb} from 'antd'
|
|
2
|
+
import {useNavigate} from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PageHeader - 通用页面标题(紧凑单行版).
|
|
6
|
+
*
|
|
7
|
+
* 解决 z-opc 后台每个页面都重复写 Title + Description + 返回按钮的问题.
|
|
8
|
+
*
|
|
9
|
+
* 原则 (来自 anthropics/frontend-design):
|
|
10
|
+
* - Structure is information. 面包屑末级即当前页名, 与操作按钮同行, 信息密度优先.
|
|
11
|
+
* - 不用 emoji/装饰元素, 用 typography 表达层级.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} props
|
|
14
|
+
* @param {string} props.title - 主标题(有 breadcrumb 时以面包屑末级为准, 不再单独渲染)
|
|
15
|
+
* @param {string} [props.subtitle] - 副标题/描述(同行小灰字, 超长截断, hover 可见全文)
|
|
16
|
+
* @param {Array<{label: string, href?: string}>} [props.breadcrumb] - 面包屑
|
|
17
|
+
* @param {ReactNode} [props.extra] - 右上角操作按钮区
|
|
18
|
+
* @param {ReactNode} [props.back] - 是否显示返回按钮 + onBack 回调
|
|
19
|
+
* @param {Function} [props.onBack] - 返回回调
|
|
20
|
+
*/
|
|
21
|
+
export default function PageHeader({
|
|
22
|
+
title,
|
|
23
|
+
subtitle,
|
|
24
|
+
breadcrumb,
|
|
25
|
+
extra,
|
|
26
|
+
back,
|
|
27
|
+
onBack,
|
|
28
|
+
}) {
|
|
29
|
+
const navigate = useNavigate()
|
|
30
|
+
const handleBack = () => {
|
|
31
|
+
if (onBack) onBack()
|
|
32
|
+
else navigate(-1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div style={{
|
|
37
|
+
marginBottom: 12,
|
|
38
|
+
paddingBottom: 10,
|
|
39
|
+
borderBottom: '1px solid #f1f5f9',
|
|
40
|
+
}}>
|
|
41
|
+
{/* 单行: 面包屑/标题 + 副标题 | 操作区 */}
|
|
42
|
+
<div style={{
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'space-between',
|
|
46
|
+
gap: 12,
|
|
47
|
+
flexWrap: 'wrap',
|
|
48
|
+
}}>
|
|
49
|
+
<div style={{
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
gap: 10,
|
|
53
|
+
minWidth: 0,
|
|
54
|
+
flexWrap: 'wrap',
|
|
55
|
+
}}>
|
|
56
|
+
{back && (
|
|
57
|
+
<button
|
|
58
|
+
onClick={handleBack}
|
|
59
|
+
aria-label="返回上一页"
|
|
60
|
+
style={{
|
|
61
|
+
width: 28, height: 28,
|
|
62
|
+
borderRadius: 6,
|
|
63
|
+
border: '1px solid #e5e7eb',
|
|
64
|
+
background: '#ffffff',
|
|
65
|
+
color: '#6b7280',
|
|
66
|
+
cursor: 'pointer',
|
|
67
|
+
display: 'inline-flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
fontSize: 13,
|
|
71
|
+
transition: 'all 150ms ease',
|
|
72
|
+
flexShrink: 0,
|
|
73
|
+
}}
|
|
74
|
+
onMouseEnter={(e) => {
|
|
75
|
+
e.currentTarget.style.background = '#f8fafc'
|
|
76
|
+
e.currentTarget.style.color = '#111827'
|
|
77
|
+
e.currentTarget.style.borderColor = '#7c3aed'
|
|
78
|
+
}}
|
|
79
|
+
onMouseLeave={(e) => {
|
|
80
|
+
e.currentTarget.style.background = '#ffffff'
|
|
81
|
+
e.currentTarget.style.color = '#6b7280'
|
|
82
|
+
e.currentTarget.style.borderColor = '#e5e7eb'
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
←
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
{breadcrumb && breadcrumb.length > 0 ? (
|
|
89
|
+
<Breadcrumb
|
|
90
|
+
items={breadcrumb.map((b, i) => {
|
|
91
|
+
const last = i === breadcrumb.length - 1
|
|
92
|
+
return {
|
|
93
|
+
title: b.href && !last
|
|
94
|
+
? <a onClick={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
navigate(b.href)
|
|
97
|
+
}}>{b.label}</a>
|
|
98
|
+
: <span style={last ? {
|
|
99
|
+
fontWeight: 600,
|
|
100
|
+
color: '#111827'
|
|
101
|
+
} : undefined}>{b.label}</span>,
|
|
102
|
+
}
|
|
103
|
+
})}
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
title && (
|
|
107
|
+
<span role="heading" aria-level={1} style={{
|
|
108
|
+
fontSize: 15,
|
|
109
|
+
fontWeight: 600,
|
|
110
|
+
color: '#111827',
|
|
111
|
+
lineHeight: 1.4,
|
|
112
|
+
}}>
|
|
113
|
+
{title}
|
|
114
|
+
</span>
|
|
115
|
+
)
|
|
116
|
+
)}
|
|
117
|
+
{subtitle && (
|
|
118
|
+
<span title={subtitle} style={{
|
|
119
|
+
fontSize: 12,
|
|
120
|
+
color: '#94a3b8',
|
|
121
|
+
lineHeight: 1.4,
|
|
122
|
+
whiteSpace: 'nowrap',
|
|
123
|
+
overflow: 'hidden',
|
|
124
|
+
textOverflow: 'ellipsis',
|
|
125
|
+
maxWidth: 360,
|
|
126
|
+
}}>
|
|
127
|
+
{subtitle}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
{extra && (
|
|
132
|
+
<div style={{
|
|
133
|
+
display: 'flex',
|
|
134
|
+
gap: 8,
|
|
135
|
+
flexShrink: 0,
|
|
136
|
+
}}>
|
|
137
|
+
{extra}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import {Button, Card, Table} from 'antd'
|
|
3
|
+
import {ReloadOutlined} from '@ant-design/icons'
|
|
4
|
+
import {EmptyState, ErrorState, PageHeader} from './index'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 中间件管理台共用的分页表格(z-opc 内孵化的 Tier A 页面用)。
|
|
8
|
+
*
|
|
9
|
+
* load(current, size) 既可以返回 MyBatis-Plus 的 {records,total,...},也可以返回裸数组。
|
|
10
|
+
* 后端 z-meta 的分页 count 有缺陷(records 有值但 total 返回 "0"),这里不把 0 当"共 0 条"显示,
|
|
11
|
+
* 而是显式标出接口给的总数,避免页面上出现"有数据却说没有数据"的假象。
|
|
12
|
+
*/
|
|
13
|
+
export default function PagedTable({title, description, load, columns, rowKey = 'id', extra, expandable}) {
|
|
14
|
+
const [rows, setRows] = useState([])
|
|
15
|
+
const [page, setPage] = useState({current: 1, size: 10, total: null})
|
|
16
|
+
const [loading, setLoading] = useState(false)
|
|
17
|
+
const [error, setError] = useState(null)
|
|
18
|
+
|
|
19
|
+
const fetch = useCallback(async (current = page.current, size = page.size) => {
|
|
20
|
+
setLoading(true)
|
|
21
|
+
setError(null)
|
|
22
|
+
try {
|
|
23
|
+
const res = await load(current, size)
|
|
24
|
+
if (Array.isArray(res)) {
|
|
25
|
+
setRows(res || [])
|
|
26
|
+
setPage({current, size, total: res.length})
|
|
27
|
+
} else {
|
|
28
|
+
const records = res?.records || []
|
|
29
|
+
setRows(records)
|
|
30
|
+
setPage({current, size, total: res?.total == null ? null : Number(res.total)})
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
setRows([])
|
|
34
|
+
setError(e)
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}, [load, page.current, page.size])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetch(1)
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
const totalKnown = page.total != null && page.total > 0
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<PageHeader title={title} subtitle={description}/>
|
|
49
|
+
<Card
|
|
50
|
+
extra={
|
|
51
|
+
<Button icon={<ReloadOutlined/>} loading={loading}
|
|
52
|
+
onClick={() => fetch(page.current, page.size)}>刷新</Button>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
{extra}
|
|
56
|
+
{error ? (
|
|
57
|
+
<ErrorState error={error} onRetry={() => fetch(page.current, page.size)} type="inline"/>
|
|
58
|
+
) : (
|
|
59
|
+
<Table
|
|
60
|
+
size="small"
|
|
61
|
+
columns={columns}
|
|
62
|
+
dataSource={rows}
|
|
63
|
+
loading={loading}
|
|
64
|
+
rowKey={(r) => r?.[rowKey] ?? r?.key ?? JSON.stringify(r).slice(0, 32)}
|
|
65
|
+
pagination={{
|
|
66
|
+
current: page.current,
|
|
67
|
+
pageSize: page.size,
|
|
68
|
+
total: totalKnown ? page.total : rows.length,
|
|
69
|
+
showSizeChanger: true,
|
|
70
|
+
showTotal: () =>
|
|
71
|
+
totalKnown
|
|
72
|
+
? `共 ${page.total} 条`
|
|
73
|
+
: `本页 ${rows.length} 条(接口 total 未返回有效值)`,
|
|
74
|
+
}}
|
|
75
|
+
onChange={(p) => fetch(p.current, p.pageSize)}
|
|
76
|
+
expandable={expandable}
|
|
77
|
+
locale={{emptyText: <EmptyState title="暂无数据" description="接口返回空列表"/>}}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</Card>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {Input} from 'antd'
|
|
3
|
+
import {SearchOutlined} from '@ant-design/icons'
|
|
4
|
+
import {radius} from './tokens'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Debounced SearchInput - 修复 z-opc 后台"搜索必须点按钮"的交互不合理.
|
|
8
|
+
*
|
|
9
|
+
* 原则 (来自 ui-ux-pro-max P8 / frontend-ui-engineering Forms):
|
|
10
|
+
* - 输入即时反馈, 不要让用户多一次点击
|
|
11
|
+
* - debounce 300ms 防止每个键击都发请求
|
|
12
|
+
* - 受控 value + onChange, 避免组件内部状态混乱
|
|
13
|
+
* - Enter 立即触发 (不等待 debounce)
|
|
14
|
+
* - Escape 清空
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {string} [props.value] - 受控值
|
|
18
|
+
* @param {Function} props.onChange - 输入变化回调 (debounced)
|
|
19
|
+
* @param {Function} [props.onSearch] - 显式搜索回调 (Enter / 点按钮)
|
|
20
|
+
* @param {string} [props.placeholder]
|
|
21
|
+
* @param {number} [props.debounceMs] - 防抖延迟 (默认 300)
|
|
22
|
+
* @param {number} [props.width] - 宽度 (默认 280)
|
|
23
|
+
*/
|
|
24
|
+
export default function SearchInput({
|
|
25
|
+
value: controlledValue,
|
|
26
|
+
onChange,
|
|
27
|
+
onSearch,
|
|
28
|
+
placeholder = '搜索...',
|
|
29
|
+
debounceMs = 300,
|
|
30
|
+
width = 280,
|
|
31
|
+
allowClear = true,
|
|
32
|
+
// antd v6: enterButton 已被废弃,SearchInput 已通过 onKeyDown 自行处理 Enter 键,
|
|
33
|
+
// 所以从 rest 里过滤掉,避免透传到原生 <input> 报 React 警告。
|
|
34
|
+
enterButton: _ignored,
|
|
35
|
+
...rest
|
|
36
|
+
}) {
|
|
37
|
+
const [inner, setInner] = useState(controlledValue || '')
|
|
38
|
+
const timerRef = useRef(null)
|
|
39
|
+
const lastEmittedRef = useRef(controlledValue || '')
|
|
40
|
+
|
|
41
|
+
// 受控/非受控兼容
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (controlledValue !== undefined && controlledValue !== inner) {
|
|
44
|
+
setInner(controlledValue || '')
|
|
45
|
+
lastEmittedRef.current = controlledValue || ''
|
|
46
|
+
}
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, [controlledValue])
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
return () => {
|
|
52
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
53
|
+
}
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const emit = (v) => {
|
|
57
|
+
lastEmittedRef.current = v
|
|
58
|
+
if (onChange) onChange(v)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleChange = (e) => {
|
|
62
|
+
const v = e.target.value
|
|
63
|
+
setInner(v)
|
|
64
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
65
|
+
// 清空时立即触发 (用户期望立即看到结果)
|
|
66
|
+
if (v === '') {
|
|
67
|
+
emit('')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
timerRef.current = setTimeout(() => emit(v), debounceMs)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleSearch = (v) => {
|
|
74
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
75
|
+
emit(v)
|
|
76
|
+
if (onSearch) onSearch(v)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = (e) => {
|
|
80
|
+
if (e.key === 'Enter') {
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
handleSearch(inner)
|
|
83
|
+
} else if (e.key === 'Escape') {
|
|
84
|
+
setInner('')
|
|
85
|
+
handleSearch('')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleClear = () => {
|
|
90
|
+
setInner('')
|
|
91
|
+
handleSearch('')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Input
|
|
96
|
+
{...rest}
|
|
97
|
+
value={inner}
|
|
98
|
+
onChange={handleChange}
|
|
99
|
+
onPressEnter={handleSearch}
|
|
100
|
+
onKeyDown={handleKeyDown}
|
|
101
|
+
placeholder={placeholder}
|
|
102
|
+
allowClear={allowClear ? {clearIcon: <span onClick={handleClear}>×</span>} : false}
|
|
103
|
+
prefix={<SearchOutlined style={{color: '#9ca3af'}}/>}
|
|
104
|
+
style={{
|
|
105
|
+
width,
|
|
106
|
+
borderRadius: radius.md,
|
|
107
|
+
...(rest.style || {}),
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import CategoryDot from './CategoryDot'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 段落标题 - 借鉴 wenming7/marketplace 分类段头
|
|
5
|
+
*
|
|
6
|
+
* 视觉:
|
|
7
|
+
* � 热门爆款 全部 >
|
|
8
|
+
* 圆点 + 标题 右侧跳转链接
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {string} props.color - 圆点配色
|
|
12
|
+
* @param {string} props.title - 标题
|
|
13
|
+
* @param {string} [props.subtitle] - 副标题
|
|
14
|
+
* @param {string} [props.moreText] - "全部 >" 文案
|
|
15
|
+
* @param {string} [props.moreHref] - 跳转路径
|
|
16
|
+
*/
|
|
17
|
+
export default function SectionHeader({
|
|
18
|
+
color = 'blue',
|
|
19
|
+
title,
|
|
20
|
+
subtitle,
|
|
21
|
+
moreText = '全部',
|
|
22
|
+
moreHref,
|
|
23
|
+
onMore,
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div style={{
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'space-between',
|
|
30
|
+
marginBottom: 16,
|
|
31
|
+
}}>
|
|
32
|
+
<div style={{display: 'flex', alignItems: 'baseline', gap: 12}}>
|
|
33
|
+
<CategoryDot color={color} label={title} size={10}/>
|
|
34
|
+
{subtitle && (
|
|
35
|
+
<span style={{
|
|
36
|
+
fontSize: 13,
|
|
37
|
+
color: '#6b7280',
|
|
38
|
+
fontWeight: 400,
|
|
39
|
+
}}>
|
|
40
|
+
{subtitle}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
{(moreHref || onMore) && (
|
|
45
|
+
<a
|
|
46
|
+
href={moreHref}
|
|
47
|
+
onClick={(e) => {
|
|
48
|
+
if (onMore) {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
onMore()
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
style={{
|
|
54
|
+
fontSize: 13,
|
|
55
|
+
color: '#6b7280',
|
|
56
|
+
textDecoration: 'none',
|
|
57
|
+
display: 'inline-flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
gap: 4,
|
|
60
|
+
cursor: 'pointer',
|
|
61
|
+
transition: 'color 150ms ease',
|
|
62
|
+
}}
|
|
63
|
+
onMouseEnter={(e) => {
|
|
64
|
+
e.currentTarget.style.color = '#7c3aed'
|
|
65
|
+
}}
|
|
66
|
+
onMouseLeave={(e) => {
|
|
67
|
+
e.currentTarget.style.color = '#6b7280'
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{moreText}
|
|
71
|
+
<span style={{fontSize: 12}}>›</span>
|
|
72
|
+
</a>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {Tag} from 'antd'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 通用状态 Badge — 替代散落在各页面的 Tag 颜色定义
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* <StatusBadge status={1} type="status"/> // 任务状态
|
|
8
|
+
* <StatusBadge status="ACTIVE" type="model"/> // 模型状态
|
|
9
|
+
* <StatusBadge level="high" type="priority"/> // 优先级
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TASK_STATUS = {
|
|
13
|
+
0: {label: '待办', color: 'default'},
|
|
14
|
+
1: {label: '进行中', color: 'blue'},
|
|
15
|
+
2: {label: '已完成', color: 'green'},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MODEL_STATUS = {
|
|
19
|
+
ACTIVE: {label: '正常', color: 'green'},
|
|
20
|
+
INACTIVE: {label: '禁用', color: 'red'},
|
|
21
|
+
ENABLED: {label: '启用', color: 'green'},
|
|
22
|
+
DISABLED: {label: '禁用', color: 'red'},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PRIORITY = {
|
|
26
|
+
low: {label: '低', color: 'default'},
|
|
27
|
+
medium: {label: '中', color: 'orange'},
|
|
28
|
+
high: {label: '高', color: 'red'},
|
|
29
|
+
1: {label: '低', color: 'default'},
|
|
30
|
+
2: {label: '中', color: 'orange'},
|
|
31
|
+
3: {label: '高', color: 'red'},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const APPROVAL = {
|
|
35
|
+
PENDING: {label: '待审批', color: 'orange'},
|
|
36
|
+
APPROVED: {label: '已通过', color: 'green'},
|
|
37
|
+
REJECTED: {label: '已拒绝', color: 'red'},
|
|
38
|
+
CANCELLED: {label: '已撤回', color: 'default'},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SCHEDULE = {
|
|
42
|
+
RUNNING: {label: '运行中', color: 'green'},
|
|
43
|
+
PAUSED: {label: '已暂停', color: 'orange'},
|
|
44
|
+
STOPPED: {label: '已停止', color: 'default'},
|
|
45
|
+
FAILED: {label: '失败', color: 'red'},
|
|
46
|
+
SUCCESS: {label: '成功', color: 'green'},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MAP = {
|
|
50
|
+
task: TASK_STATUS,
|
|
51
|
+
model: MODEL_STATUS,
|
|
52
|
+
priority: PRIORITY,
|
|
53
|
+
approval: APPROVAL,
|
|
54
|
+
schedule: SCHEDULE,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function StatusBadge({status, type = 'task', mapping, ...rest}) {
|
|
58
|
+
const m = mapping || MAP[type] || {}
|
|
59
|
+
const cfg = m[status]
|
|
60
|
+
if (!cfg) return <Tag {...rest}>{String(status)}</Tag>
|
|
61
|
+
return (
|
|
62
|
+
<Tag color={cfg.color} {...rest}>
|
|
63
|
+
{cfg.label}
|
|
64
|
+
</Tag>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {Button, Space} from 'antd'
|
|
2
|
+
import {ReloadOutlined} from '@ant-design/icons'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 表格上方工具栏 — 统一搜索 + 过滤 + 操作按钮布局
|
|
6
|
+
*
|
|
7
|
+
* Props:
|
|
8
|
+
* - left: ReactNode 左侧过滤区 (Select, Radio, Checkbox 等)
|
|
9
|
+
* - right: ReactNode 右侧操作区 (按钮组)
|
|
10
|
+
* - extra: ReactNode 最右侧插入
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* <TableToolbar
|
|
14
|
+
* left={
|
|
15
|
+
* <>
|
|
16
|
+
* <SearchInput placeholder="搜索..."/>
|
|
17
|
+
* <Select placeholder="状态"/>
|
|
18
|
+
* </>
|
|
19
|
+
* }
|
|
20
|
+
* right={
|
|
21
|
+
* <>
|
|
22
|
+
* <Button icon={<ReloadOutlined/>}>刷新</Button>
|
|
23
|
+
* <Button type="primary" icon={<PlusOutlined/>}>新建</Button>
|
|
24
|
+
* </>
|
|
25
|
+
* }
|
|
26
|
+
* />
|
|
27
|
+
*/
|
|
28
|
+
export default function TableToolbar({left, right, extra}) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'space-between',
|
|
35
|
+
gap: 12,
|
|
36
|
+
marginBottom: 12,
|
|
37
|
+
flexWrap: 'wrap',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Space size={8} wrap>{left}</Space>
|
|
41
|
+
<Space size={8} wrap>
|
|
42
|
+
{right}
|
|
43
|
+
{extra}
|
|
44
|
+
</Space>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 紧凑版刷新按钮 — 表格内常用
|
|
51
|
+
*/
|
|
52
|
+
export function RefreshButton({onClick, loading, title = '刷新'}) {
|
|
53
|
+
return (
|
|
54
|
+
<Button icon={<ReloadOutlined/>} onClick={onClick} loading={loading} title={title}>
|
|
55
|
+
{title}
|
|
56
|
+
</Button>
|
|
57
|
+
)
|
|
58
|
+
}
|