@yuku123/z-subscribe-frontend-component 0.1.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.
@@ -0,0 +1,296 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {useNavigate, useParams} from 'react-router-dom'
3
+ import {
4
+ Alert,
5
+ Button,
6
+ Card,
7
+ Form,
8
+ Input,
9
+ InputNumber,
10
+ message,
11
+ Radio,
12
+ Select,
13
+ Space,
14
+ Spin
15
+ } from 'antd'
16
+ import {ArrowLeftOutlined, ApiOutlined, SaveOutlined} from '@ant-design/icons'
17
+ import request from '../utils/request'
18
+
19
+ const EXTRACTOR_OPTIONS = [
20
+ {value: 'STANDARD_JSON', label: '标准 JSON (按统一契约返回的 JSON)'},
21
+ {value: 'RSS_XML', label: 'RSS 2.0 (XML feed)'},
22
+ {value: 'ATOM_XML', label: 'Atom 1.0 (XML feed)'},
23
+ {value: 'CUSTOM_SCRIPT', label: '自定义脚本 (z-script 平台)'},
24
+ {value: 'INTERNAL_BEAN', label: '内部 Bean (我们自己的模块, 暂未启用)'},
25
+ ]
26
+
27
+ const AUTH_TYPE_OPTIONS = [
28
+ {value: 'NONE', label: '无鉴权'},
29
+ {value: 'BEARER', label: 'Bearer (Authorization: Bearer xxx)'},
30
+ {value: 'API_KEY', label: 'API Key (Authorization: xxx)'},
31
+ {value: 'BASIC', label: 'Basic (user:password base64)'},
32
+ ]
33
+
34
+ const CRON_PRESETS = [
35
+ {label: '每 5 分钟', value: '0 */5 * * * ?'},
36
+ {label: '每 15 分钟', value: '0 */15 * * * ?'},
37
+ {label: '每小时', value: '0 0 * * * ?'},
38
+ {label: '每 6 小时', value: '0 0 */6 * * ?'},
39
+ {label: '每天 8 点', value: '0 0 8 * * ?'},
40
+ ]
41
+
42
+ function SubscriptionEdit() {
43
+ const {id} = useParams()
44
+ const navigate = useNavigate()
45
+ const isEdit = !!id
46
+ const [form] = Form.useForm()
47
+ const [loading, setLoading] = useState(false)
48
+ const [saving, setSaving] = useState(false)
49
+ const [secrets, setSecrets] = useState([])
50
+ const [scripts, setScripts] = useState([])
51
+
52
+ // 动态值
53
+ const sourceType = Form.useWatch('sourceType', form)
54
+ const extractorType = Form.useWatch('extractorType', form)
55
+ const authType = Form.useWatch('authType', form)
56
+
57
+ useEffect(() => {
58
+ loadSecrets()
59
+ loadScripts()
60
+ if (isEdit) loadSubscription()
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ }, [id])
63
+
64
+ const loadSecrets = async () => {
65
+ try {
66
+ const res = await request.get('/secret/list')
67
+ const data = res?.data ?? res ?? []
68
+ setSecrets(Array.isArray(data) ? data : [])
69
+ } catch (e) {
70
+ // z-mist 不可用时静默
71
+ }
72
+ }
73
+
74
+ const loadScripts = async () => {
75
+ try {
76
+ const res = await request.get('/script/list')
77
+ const data = res?.data ?? res ?? []
78
+ setScripts(Array.isArray(data) ? data.filter(s => s.status === 1) : [])
79
+ } catch (e) {
80
+ // z-script 不可用时静默
81
+ }
82
+ }
83
+
84
+ const loadSubscription = async () => {
85
+ setLoading(true)
86
+ try {
87
+ const res = await request.get(`/subscribe/subscription/${id}`)
88
+ const data = res?.data ?? res
89
+ if (data) {
90
+ form.setFieldsValue({
91
+ ...data,
92
+ enabled: data.enabled === 1 || data.enabled === true,
93
+ })
94
+ }
95
+ } catch (e) {
96
+ message.error('加载订阅失败: ' + (e?.message || ''))
97
+ } finally {
98
+ setLoading(false)
99
+ }
100
+ }
101
+
102
+ const handleSave = async () => {
103
+ try {
104
+ const values = await form.validateFields()
105
+ setSaving(true)
106
+ const payload = {
107
+ ...values,
108
+ enabled: values.enabled ? 1 : 0,
109
+ }
110
+ let res
111
+ if (isEdit) {
112
+ res = await request.put('/subscribe/subscription', payload)
113
+ } else {
114
+ res = await request.post('/subscribe/subscription', payload)
115
+ }
116
+ if (res && (res.success === false)) {
117
+ message.error(res.message || '保存失败')
118
+ return
119
+ }
120
+ message.success(isEdit ? '已更新 (注意: 修改 cron / url / 抽取器后会自动重置为 PENDING_VERIFY)' : '已创建, 已自动验证')
121
+ navigate('/subscribe/list')
122
+ } catch (e) {
123
+ if (e?.errorFields) {
124
+ message.warning('请检查表单')
125
+ } else {
126
+ message.error('保存失败: ' + (e?.message || ''))
127
+ }
128
+ } finally {
129
+ setSaving(false)
130
+ }
131
+ }
132
+
133
+ return (
134
+ <div style={{padding: 24}}>
135
+ <Card
136
+ title={
137
+ <Space>
138
+ <Button type="text" icon={<ArrowLeftOutlined/>} onClick={() => navigate('/subscribe/list')}/>
139
+ <ApiOutlined/>
140
+ {isEdit ? `编辑订阅 #${id}` : '新建订阅'}
141
+ </Space>
142
+ }
143
+ extra={
144
+ <Space>
145
+ <Button onClick={() => navigate('/subscribe/list')}>取消</Button>
146
+ <Button type="primary" icon={<SaveOutlined/>} loading={saving} onClick={handleSave}>
147
+ 保存 (自动验证)
148
+ </Button>
149
+ </Space>
150
+ }
151
+ >
152
+ <Spin spinning={loading}>
153
+ <Form
154
+ form={form}
155
+ layout="vertical"
156
+ initialValues={{
157
+ sourceType: 'HTTP',
158
+ httpMethod: 'GET',
159
+ authType: 'NONE',
160
+ extractorType: 'STANDARD_JSON',
161
+ enabled: true,
162
+ minIntervalSec: 60,
163
+ maxItemsPerRun: 1000,
164
+ timeoutMs: 15000,
165
+ }}
166
+ >
167
+ <Card type="inner" title="基本信息" style={{marginBottom: 16}}>
168
+ <Form.Item label="名称" name="name" rules={[{required: true, message: '请输入名称'}]}>
169
+ <Input placeholder="例如: GitHub PR 通知"/>
170
+ </Form.Item>
171
+ <Form.Item label="说明" name="description">
172
+ <Input.TextArea rows={2} placeholder="(可选)"/>
173
+ </Form.Item>
174
+ </Card>
175
+
176
+ <Card type="inner" title="数据源" style={{marginBottom: 16}}>
177
+ <Form.Item label="数据源形态" name="sourceType" rules={[{required: true}]}>
178
+ <Radio.Group>
179
+ <Radio.Button value="HTTP">HTTP / HTTPS</Radio.Button>
180
+ <Radio.Button value="INTERNAL" disabled>内部 Bean (TODO)</Radio.Button>
181
+ </Radio.Group>
182
+ </Form.Item>
183
+
184
+ {sourceType === 'HTTP' && (
185
+ <>
186
+ <Form.Item label="目标 URL" name="targetUrl"
187
+ rules={[{required: true, message: '请输入 URL'}]}>
188
+ <Input placeholder="https://api.github.com/repos/.../pulls?state=open"/>
189
+ </Form.Item>
190
+ <Space size="large" style={{width: '100%'}}>
191
+ <Form.Item label="HTTP 方法" name="httpMethod">
192
+ <Select style={{width: 120}} options={[
193
+ {value: 'GET', label: 'GET'},
194
+ {value: 'POST', label: 'POST'},
195
+ {value: 'PUT', label: 'PUT'},
196
+ ]}/>
197
+ </Form.Item>
198
+ <Form.Item label="超时 (ms)" name="timeoutMs">
199
+ <InputNumber min={1000} max={120000} step={1000}/>
200
+ </Form.Item>
201
+ </Space>
202
+ <Form.Item label="自定义 HTTP 头 (JSON)" name="httpHeaders">
203
+ <Input.TextArea rows={2} placeholder='{"X-Trace-Id":"abc"}'/>
204
+ </Form.Item>
205
+ <Form.Item label="请求体 (POST/PUT)" name="requestBody">
206
+ <Input.TextArea rows={3} placeholder="(可选)"/>
207
+ </Form.Item>
208
+ </>
209
+ )}
210
+ </Card>
211
+
212
+ <Card type="inner" title="鉴权 (从 z-mist 拉取 secret)" style={{marginBottom: 16}}>
213
+ <Form.Item label="鉴权方式" name="authType">
214
+ <Select options={AUTH_TYPE_OPTIONS}/>
215
+ </Form.Item>
216
+ {authType && authType !== 'NONE' && (
217
+ <Form.Item label="z-mist 凭据引用" name="authSecretKey"
218
+ extra="从密钥中心 (z-mist) 选一个 secret; secret 的解密值会作为 secret 注入">
219
+ <Select
220
+ showSearch
221
+ allowClear
222
+ placeholder="选择 secret (key)"
223
+ options={secrets.map(s => ({
224
+ value: s.secretKey,
225
+ label: `${s.secretKey}${s.secretName ? ' (' + s.secretName + ')' : ''}`,
226
+ }))}
227
+ />
228
+ </Form.Item>
229
+ )}
230
+ </Card>
231
+
232
+ <Card type="inner" title="抽取器" style={{marginBottom: 16}}>
233
+ <Form.Item label="抽取器类型" name="extractorType" rules={[{required: true}]}
234
+ extra="STANDARD_JSON/RSS_XML/ATOM_XML 直接解析; CUSTOM_SCRIPT 调 z-script 平台">
235
+ <Select options={EXTRACTOR_OPTIONS}/>
236
+ </Form.Item>
237
+ {extractorType === 'CUSTOM_SCRIPT' && (
238
+ <Form.Item label="z-script 脚本" name="extractorScriptCode"
239
+ rules={[{required: true, message: '请选择脚本'}]}
240
+ extra="在 智能中心 > 脚本中心 维护脚本. 脚本返回 { items: [...], nextCursor: '...' }">
241
+ <Select
242
+ showSearch
243
+ placeholder="选择已启用的脚本"
244
+ options={scripts.map(s => ({
245
+ value: s.scriptCode,
246
+ label: `${s.scriptCode} (${s.dslType})${s.scriptName ? ' - ' + s.scriptName : ''}`,
247
+ }))}
248
+ />
249
+ </Form.Item>
250
+ )}
251
+ </Card>
252
+
253
+ <Card type="inner" title="调度" style={{marginBottom: 16}}>
254
+ <Form.Item label="Cron 表达式" name="cronExpr"
255
+ rules={[{required: true, message: '请输入 Cron'}]}
256
+ extra="Quartz 兼容, 例如 0 */5 * * * ? = 每 5 分钟">
257
+ <Input addonAfter={
258
+ <Select
259
+ style={{width: 140}}
260
+ placeholder="常用"
261
+ options={CRON_PRESETS}
262
+ onChange={(v) => form.setFieldValue('cronExpr', v)}
263
+ value={null}
264
+ />
265
+ }/>
266
+ </Form.Item>
267
+ <Space size="large">
268
+ <Form.Item label="启用" name="enabled" valuePropName="checked">
269
+ <Radio.Group>
270
+ <Radio.Button value={true}>启用</Radio.Button>
271
+ <Radio.Button value={false}>停用</Radio.Button>
272
+ </Radio.Group>
273
+ </Form.Item>
274
+ <Form.Item label="最小间隔 (秒)" name="minIntervalSec"
275
+ extra="距上次 run 至少 N 秒">
276
+ <InputNumber min={0} max={3600}/>
277
+ </Form.Item>
278
+ <Form.Item label="单次最多条数" name="maxItemsPerRun">
279
+ <InputNumber min={1} max={10000}/>
280
+ </Form.Item>
281
+ </Space>
282
+ </Card>
283
+
284
+ <Alert
285
+ type="success"
286
+ showIcon
287
+ message="保存后会自动 dry-run: 真实 HTTP 一次, 把响应喂给抽取器, 失败时订阅进入 INVALID_AUTH 状态, 不会自动调度."
288
+ />
289
+ </Form>
290
+ </Spin>
291
+ </Card>
292
+ </div>
293
+ )
294
+ }
295
+
296
+ export default SubscriptionEdit