apiskill 0.1.0 → 0.1.2

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/src/App.tsx CHANGED
@@ -192,7 +192,7 @@ export function App() {
192
192
  const [openedEndpointTabs, setOpenedEndpointTabs] = useState<OpenEndpointTab[]>([]);
193
193
  const [tabsCacheReady, setTabsCacheReady] = useState(false);
194
194
  const [syncingMode, setSyncingMode] = useState<ImportDialogMode | ''>('');
195
- const [activeSourceTab, setActiveSourceTab] = useState<ImportMode>('file');
195
+ const [activeDocumentTab, setActiveDocumentTab] = useState<ImportDialogMode>('blank');
196
196
  const [versions, setVersions] = useState<ImportMeta[]>([]);
197
197
  const [selectedVersionId, setSelectedVersionId] = useState('');
198
198
  const [selectedVersionMeta, setSelectedVersionMeta] = useState<ImportMeta | undefined>(undefined);
@@ -204,8 +204,8 @@ export function App() {
204
204
  const [blankDocTitle, setBlankDocTitle] = useState('API Skill Document');
205
205
  const [blankDocVersion, setBlankDocVersion] = useState('1.0.0');
206
206
  const [blankDocDescription, setBlankDocDescription] = useState('Created from API Skill blank document.');
207
- const [blankDocDialogOpen, setBlankDocDialogOpen] = useState(false);
208
- const [blankUpdateVersionId, setBlankUpdateVersionId] = useState('');
207
+ const [documentDialogOpen, setDocumentDialogOpen] = useState(false);
208
+ const [documentUpdateVersionId, setDocumentUpdateVersionId] = useState('');
209
209
  const [resolvedSourceUrl, setResolvedSourceUrl] = useState('');
210
210
  const [docAuthUsername, setDocAuthUsername] = useState('');
211
211
  const [docAuthPassword, setDocAuthPassword] = useState('');
@@ -401,20 +401,28 @@ export function App() {
401
401
  function applySourceDefaults(meta?: DocumentVersionMeta, document?: SwaggerDocument | null) {
402
402
  const sourceTab = sourceTabFromCacheMode(meta?.mode);
403
403
  if (sourceTab) {
404
- setActiveSourceTab(sourceTab);
404
+ setActiveDocumentTab(sourceTab);
405
405
  }
406
406
  if (isImportMode(meta?.mode)) {
407
407
  setSourceInput(meta.mode, meta.inputUrl || '');
408
408
  }
409
409
  if ((meta?.mode === 'document' || meta?.mode === 'manual') && document?.info) {
410
+ setActiveDocumentTab('blank');
410
411
  setBlankDocTitle(document.info.title || 'API Skill Document');
411
412
  setBlankDocVersion(document.info.version || '1.0.0');
412
413
  setBlankDocDescription(document.info.description || '');
413
414
  }
414
415
  }
415
416
 
416
- function getImportUpdateVersionId(mode: ImportMode) {
417
- return selectedVersionId && selectedVersion?.mode === mode ? selectedVersionId : '';
417
+ function getDocumentUpdateVersionId(mode: ImportDialogMode) {
418
+ if (!documentUpdateVersionId) return '';
419
+ const targetVersion =
420
+ versions.find((version) => version.versionId === documentUpdateVersionId) ??
421
+ (selectedVersionId === documentUpdateVersionId ? selectedVersionMeta : undefined);
422
+ if (mode === 'blank') {
423
+ return targetVersion?.mode === 'document' || targetVersion?.mode === 'manual' ? documentUpdateVersionId : '';
424
+ }
425
+ return targetVersion?.mode === mode ? documentUpdateVersionId : '';
418
426
  }
419
427
 
420
428
  function setSourceInput(mode: ImportMode, value: string) {
@@ -423,10 +431,11 @@ export function App() {
423
431
  else if (mode === 'file') setFileSourceUrl(value);
424
432
  }
425
433
 
426
- function getActiveSourceValue() {
427
- if (activeSourceTab === 'crawl') return crawlSourceUrl.trim();
428
- if (activeSourceTab === 'curl') return curlSourceText.trim();
429
- if (activeSourceTab === 'upload') return uploadFile?.name || '';
434
+ function getActiveSourceValue(mode: ImportDialogMode = activeDocumentTab) {
435
+ if (mode === 'blank') return blankDocTitle.trim();
436
+ if (mode === 'crawl') return crawlSourceUrl.trim();
437
+ if (mode === 'curl') return curlSourceText.trim();
438
+ if (mode === 'upload') return uploadFile?.name || '';
430
439
  return fileSourceUrl.trim();
431
440
  }
432
441
 
@@ -437,9 +446,9 @@ export function App() {
437
446
  }
438
447
 
439
448
  function clearActiveSource() {
440
- if (activeSourceTab === 'crawl') setCrawlSourceUrl('');
441
- else if (activeSourceTab === 'curl') setCurlSourceText('');
442
- else if (activeSourceTab === 'upload') {
449
+ if (activeDocumentTab === 'crawl') setCrawlSourceUrl('');
450
+ else if (activeDocumentTab === 'curl') setCurlSourceText('');
451
+ else if (activeDocumentTab === 'upload') {
443
452
  setUploadFile(null);
444
453
  if (uploadInputRef.current) uploadInputRef.current.value = '';
445
454
  } else {
@@ -483,10 +492,10 @@ export function App() {
483
492
  return;
484
493
  }
485
494
  const nextSourceUrl = importInput.input;
486
- const matchingVersionId = getImportUpdateVersionId(mode);
487
- const targetVersionId = updateVersionId !== undefined ? updateVersionId || undefined : matchingVersionId || undefined;
495
+ const targetVersionId = updateVersionId || undefined;
488
496
  const updating = Boolean(targetVersionId);
489
497
 
498
+ setDocumentDialogOpen(false);
490
499
  setSyncingMode(mode);
491
500
  setError('');
492
501
  setImportDialog({
@@ -576,6 +585,7 @@ export function App() {
576
585
  message: updating ? '文档已更新并覆盖原版本' : '文档已解析并保存为独立版本',
577
586
  meta: payload.meta,
578
587
  });
588
+ setDocumentUpdateVersionId('');
579
589
  } catch (err) {
580
590
  const message = err instanceof Error ? err.message : '导入失败,请检查地址或文档格式';
581
591
  setError(message);
@@ -600,7 +610,7 @@ export function App() {
600
610
  return;
601
611
  }
602
612
 
603
- const targetVersionId = blankUpdateVersionId || undefined;
613
+ const targetVersionId = getDocumentUpdateVersionId('blank') || undefined;
604
614
  const updating = Boolean(targetVersionId);
605
615
  setSyncingMode('blank');
606
616
  setError('');
@@ -643,8 +653,8 @@ export function App() {
643
653
  message: updating ? '文档已更新并覆盖原版本' : '空白文档已创建,可以继续新增 API 配置',
644
654
  meta: payload.meta,
645
655
  });
646
- setBlankDocDialogOpen(false);
647
- setBlankUpdateVersionId('');
656
+ setDocumentDialogOpen(false);
657
+ setDocumentUpdateVersionId('');
648
658
  } catch (err) {
649
659
  const message = err instanceof Error ? err.message : '创建文档失败';
650
660
  setError(message);
@@ -791,17 +801,12 @@ export function App() {
791
801
  const versionMode = version.mode || '';
792
802
  if (versionId !== selectedVersionId) {
793
803
  await loadCache(versionId);
794
- if (versionMode === 'document' || versionMode === 'manual') {
795
- setBlankUpdateVersionId(versionId);
796
- setBlankDocDialogOpen(true);
797
- }
798
- return;
799
- }
800
- applySourceDefaults(version, doc);
801
- if (versionMode === 'document' || versionMode === 'manual') {
802
- setBlankUpdateVersionId(versionId);
803
- setBlankDocDialogOpen(true);
804
+ } else {
805
+ applySourceDefaults(version, doc);
804
806
  }
807
+ setActiveDocumentTab(isImportMode(versionMode) ? versionMode : 'blank');
808
+ setDocumentUpdateVersionId(versionId);
809
+ setDocumentDialogOpen(true);
805
810
  setError('');
806
811
  }
807
812
 
@@ -893,7 +898,7 @@ export function App() {
893
898
  const schemas = Object.keys(schemaSource).length;
894
899
  return { paths, schemas, endpoints: endpoints.length, tags: tags.length };
895
900
  }, [doc, endpoints.length, tags.length]);
896
- const activeSourceCanUpdate = Boolean(getImportUpdateVersionId(activeSourceTab));
901
+ const activeDocumentCanUpdate = Boolean(getDocumentUpdateVersionId(activeDocumentTab));
897
902
 
898
903
  if (loadState === 'loading') {
899
904
  return (
@@ -955,136 +960,30 @@ export function App() {
955
960
  </div>
956
961
  </header>
957
962
 
958
- <section className="source-panel">
959
- <div className="source-panel-header">
960
- <div className="source-tabs" role="tablist" aria-label="文档同步方式">
961
- <button
962
- className={activeSourceTab === 'file' ? 'active' : ''}
963
- onClick={() => setActiveSourceTab('file')}
964
- role="tab"
965
- aria-selected={activeSourceTab === 'file'}
966
- >
967
- <RefreshCcw size={15} />
968
- 在线文档
969
- </button>
970
- <button
971
- className={activeSourceTab === 'crawl' ? 'active' : ''}
972
- onClick={() => setActiveSourceTab('crawl')}
973
- role="tab"
974
- aria-selected={activeSourceTab === 'crawl'}
975
- >
976
- <Globe2 size={15} />
977
- 爬取网页
978
- </button>
979
- <button
980
- className={activeSourceTab === 'curl' ? 'active' : ''}
981
- onClick={() => setActiveSourceTab('curl')}
982
- role="tab"
983
- aria-selected={activeSourceTab === 'curl'}
984
- >
985
- <Send size={15} />
986
- 执行CURL
987
- </button>
988
- <button
989
- className={activeSourceTab === 'upload' ? 'active' : ''}
990
- onClick={() => setActiveSourceTab('upload')}
991
- role="tab"
992
- aria-selected={activeSourceTab === 'upload'}
993
- >
994
- <FileCode2 size={15} />
995
- 导入文件
996
- </button>
997
- </div>
998
- <div className="source-panel-actions">
999
- <button className="secondary-button" onClick={startMockService} disabled={startingMock}>
1000
- <Server size={16} className={startingMock ? 'spin' : ''} />
1001
- {startingMock ? '启动中' : '启动MOCK服务'}
1002
- </button>
1003
- <button
1004
- className="secondary-button"
1005
- onClick={() => {
1006
- setBlankUpdateVersionId('');
1007
- setBlankDocDialogOpen(true);
1008
- }}
1009
- disabled={Boolean(syncingMode)}
1010
- >
1011
- <Braces size={16} />
1012
- 新建文档
1013
- </button>
1014
- </div>
963
+ <section className="source-panel document-toolbar">
964
+ <div className="document-source-summary">
965
+ <span>当前文档源</span>
966
+ <strong>{resolvedSourceUrl || '未加载'}</strong>
967
+ {mockStatus?.running && mockStatus.mock ? <small>MOCK {mockStatus.mock.url} · {mockStatus.mock.routesCount} routes</small> : null}
1015
968
  </div>
1016
- <div className="source-form">
1017
- {activeSourceTab === 'upload' ? (
1018
- <label key="source-upload-field" className="source-file-picker">
1019
- <FileCode2 size={16} />
1020
- <input
1021
- key="source-upload-input"
1022
- ref={uploadInputRef}
1023
- type="file"
1024
- accept=".json,.yaml,.yml,application/json,application/yaml,text/yaml,text/x-yaml"
1025
- onChange={(event) => setUploadFile(event.target.files?.[0] ?? null)}
1026
- />
1027
- <span>{uploadFile ? uploadFile.name : '选择 JSON / YAML 文件'}</span>
1028
- </label>
1029
- ) : (
1030
- <label key="source-text-field" className={`source-input ${activeSourceTab === 'curl' ? 'source-input-multiline' : ''}`}>
1031
- <Link2 size={16} />
1032
- {activeSourceTab === 'curl' ? (
1033
- <textarea
1034
- key="source-curl-input"
1035
- value={curlSourceText}
1036
- onChange={(event) => setCurlSourceText(event.target.value)}
1037
- placeholder="粘贴 curl 命令,例如 curl 'https://example.com/openapi.yaml' -H 'accept: application/yaml'"
1038
- spellCheck={false}
1039
- />
1040
- ) : (
1041
- <input
1042
- key="source-url-input"
1043
- value={activeSourceTab === 'file' ? fileSourceUrl : crawlSourceUrl}
1044
- onChange={(event) => {
1045
- if (activeSourceTab === 'file') setFileSourceUrl(event.target.value);
1046
- else setCrawlSourceUrl(event.target.value);
1047
- }}
1048
- placeholder={
1049
- activeSourceTab === 'file'
1050
- ? '输入 Swagger/OpenAPI JSON 或 YAML 文件地址'
1051
- : '输入 Swagger UI / Knife4j / Redoc 在线文档页地址'
1052
- }
1053
- />
1054
- )}
1055
- </label>
1056
- )}
969
+ <div className="source-panel-actions">
970
+ <button className="secondary-button" onClick={startMockService} disabled={startingMock}>
971
+ <Server size={16} className={startingMock ? 'spin' : ''} />
972
+ {startingMock ? '启动中' : '启动MOCK服务'}
973
+ </button>
1057
974
  <button
1058
- className="primary-button"
975
+ className="secondary-button"
1059
976
  onClick={() => {
1060
- void importSpec(activeSourceTab);
977
+ setDocumentUpdateVersionId('');
978
+ setActiveDocumentTab('blank');
979
+ setDocumentDialogOpen(true);
1061
980
  }}
1062
- disabled={Boolean(syncingMode) || !getActiveSourceValue()}
981
+ disabled={Boolean(syncingMode)}
1063
982
  >
1064
- {activeSourceTab === 'crawl' ? (
1065
- <Globe2 size={16} className={syncingMode === 'crawl' ? 'spin' : ''} />
1066
- ) : activeSourceTab === 'curl' ? (
1067
- <Send size={16} className={syncingMode === 'curl' ? 'spin' : ''} />
1068
- ) : activeSourceTab === 'upload' ? (
1069
- <FileCode2 size={16} className={syncingMode === 'upload' ? 'spin' : ''} />
1070
- ) : (
1071
- <RefreshCcw size={16} className={syncingMode === 'file' ? 'spin' : ''} />
1072
- )}
1073
- {syncingMode === activeSourceTab ? '处理中' : activeSourceCanUpdate ? '更新' : '确认'}
1074
- </button>
1075
- <button className="secondary-button" onClick={clearActiveSource} disabled={Boolean(syncingMode) || !getActiveSourceValue()}>
1076
- 清空
983
+ <Braces size={16} />
984
+ 新建文档
1077
985
  </button>
1078
986
  </div>
1079
- <div className="source-meta">
1080
- 当前文档源:<span>{resolvedSourceUrl || '未加载'}</span>
1081
- </div>
1082
- {mockStatus?.running && mockStatus.mock ? (
1083
- <div className="source-meta">
1084
- MOCK服务:<span>{mockStatus.mock.url}</span>
1085
- <small>{mockStatus.mock.routesCount} routes</small>
1086
- </div>
1087
- ) : null}
1088
987
  {mockMessage ? <div className={`version-message ${mockMessage.includes('失败') || mockMessage.includes('请先') ? 'error' : ''}`}>{mockMessage}</div> : null}
1089
988
  </section>
1090
989
 
@@ -1107,22 +1006,40 @@ export function App() {
1107
1006
  onSubmit={submitDocumentAuth}
1108
1007
  />
1109
1008
  ) : null}
1110
- {blankDocDialogOpen ? (
1111
- <BlankDocumentDialog
1112
- updating={Boolean(blankUpdateVersionId)}
1009
+ {documentDialogOpen ? (
1010
+ <DocumentDialog
1011
+ activeTab={activeDocumentTab}
1012
+ updating={activeDocumentCanUpdate}
1113
1013
  title={blankDocTitle}
1114
1014
  version={blankDocVersion}
1115
1015
  description={blankDocDescription}
1116
- saving={syncingMode === 'blank'}
1016
+ fileSourceUrl={fileSourceUrl}
1017
+ crawlSourceUrl={crawlSourceUrl}
1018
+ curlSourceText={curlSourceText}
1019
+ uploadFile={uploadFile}
1020
+ uploadInputRef={uploadInputRef}
1021
+ saving={syncingMode === activeDocumentTab}
1022
+ onTabChange={setActiveDocumentTab}
1117
1023
  onTitleChange={setBlankDocTitle}
1118
1024
  onVersionChange={setBlankDocVersion}
1119
1025
  onDescriptionChange={setBlankDocDescription}
1026
+ onFileSourceUrlChange={setFileSourceUrl}
1027
+ onCrawlSourceUrlChange={setCrawlSourceUrl}
1028
+ onCurlSourceTextChange={setCurlSourceText}
1029
+ onUploadFileChange={setUploadFile}
1030
+ onClear={clearActiveSource}
1120
1031
  onClose={() => {
1121
- if (syncingMode === 'blank') return;
1122
- setBlankDocDialogOpen(false);
1123
- setBlankUpdateVersionId('');
1032
+ if (syncingMode) return;
1033
+ setDocumentDialogOpen(false);
1034
+ setDocumentUpdateVersionId('');
1035
+ }}
1036
+ onSubmit={() => {
1037
+ if (activeDocumentTab === 'blank') {
1038
+ void createBlankApiDocument();
1039
+ return;
1040
+ }
1041
+ void importSpec(activeDocumentTab, { type: 'none' }, false, getDocumentUpdateVersionId(activeDocumentTab));
1124
1042
  }}
1125
- onSubmit={createBlankApiDocument}
1126
1043
  />
1127
1044
  ) : null}
1128
1045
  {addApiOpen ? (
@@ -1356,69 +1273,179 @@ function DocumentAuthDialog({
1356
1273
  );
1357
1274
  }
1358
1275
 
1359
- function BlankDocumentDialog({
1276
+ function DocumentDialog({
1277
+ activeTab,
1360
1278
  updating,
1361
1279
  title,
1362
1280
  version,
1363
1281
  description,
1282
+ fileSourceUrl,
1283
+ crawlSourceUrl,
1284
+ curlSourceText,
1285
+ uploadFile,
1286
+ uploadInputRef,
1364
1287
  saving,
1288
+ onTabChange,
1365
1289
  onTitleChange,
1366
1290
  onVersionChange,
1367
1291
  onDescriptionChange,
1292
+ onFileSourceUrlChange,
1293
+ onCrawlSourceUrlChange,
1294
+ onCurlSourceTextChange,
1295
+ onUploadFileChange,
1296
+ onClear,
1368
1297
  onClose,
1369
1298
  onSubmit,
1370
1299
  }: {
1300
+ activeTab: ImportDialogMode;
1371
1301
  updating: boolean;
1372
1302
  title: string;
1373
1303
  version: string;
1374
1304
  description: string;
1305
+ fileSourceUrl: string;
1306
+ crawlSourceUrl: string;
1307
+ curlSourceText: string;
1308
+ uploadFile: File | null;
1309
+ uploadInputRef: { current: HTMLInputElement | null };
1375
1310
  saving: boolean;
1311
+ onTabChange: (value: ImportDialogMode) => void;
1376
1312
  onTitleChange: (value: string) => void;
1377
1313
  onVersionChange: (value: string) => void;
1378
1314
  onDescriptionChange: (value: string) => void;
1315
+ onFileSourceUrlChange: (value: string) => void;
1316
+ onCrawlSourceUrlChange: (value: string) => void;
1317
+ onCurlSourceTextChange: (value: string) => void;
1318
+ onUploadFileChange: (value: File | null) => void;
1319
+ onClear: () => void;
1379
1320
  onClose: () => void;
1380
1321
  onSubmit: () => void;
1381
1322
  }) {
1323
+ const tabs: Array<{ id: ImportDialogMode; label: string; icon: typeof Braces }> = [
1324
+ { id: 'blank', label: '空白文档', icon: Braces },
1325
+ { id: 'file', label: '在线文档', icon: RefreshCcw },
1326
+ { id: 'crawl', label: '爬取网页', icon: Globe2 },
1327
+ { id: 'curl', label: '执行 CURL', icon: Send },
1328
+ { id: 'upload', label: '导入文件', icon: FileCode2 },
1329
+ ];
1330
+ const hasValue =
1331
+ activeTab === 'blank'
1332
+ ? Boolean(title.trim())
1333
+ : activeTab === 'file'
1334
+ ? Boolean(fileSourceUrl.trim())
1335
+ : activeTab === 'crawl'
1336
+ ? Boolean(crawlSourceUrl.trim())
1337
+ : activeTab === 'curl'
1338
+ ? Boolean(curlSourceText.trim())
1339
+ : Boolean(uploadFile);
1340
+ const ActiveIcon = tabs.find((tab) => tab.id === activeTab)?.icon ?? Braces;
1341
+
1382
1342
  return (
1383
- <div className="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="blank-document-title">
1384
- <div className="auth-dialog">
1343
+ <div className="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="document-dialog-title">
1344
+ <div className="document-dialog">
1385
1345
  <div className="import-dialog-header">
1386
1346
  <div>
1387
1347
  <p className="eyebrow">OpenAPI Document</p>
1388
- <h2 id="blank-document-title">{updating ? '更新文档' : '新建文档'}</h2>
1348
+ <h2 id="document-dialog-title">{updating ? '更新文档' : '新建文档'}</h2>
1389
1349
  </div>
1390
1350
  <button className="icon-secondary-button" onClick={onClose} aria-label="关闭" disabled={saving}>
1391
1351
  <X size={17} />
1392
1352
  </button>
1393
1353
  </div>
1394
1354
 
1395
- <div className="blank-document-dialog-body">
1396
- <label className="source-input">
1397
- <FileCode2 size={16} />
1398
- <input
1399
- value={title}
1400
- onChange={(event) => onTitleChange(event.target.value)}
1401
- placeholder="文档名称,例如 Customer Service API"
1402
- autoFocus
1403
- />
1404
- </label>
1405
- <label className="source-input">
1406
- <Braces size={16} />
1407
- <input value={version} onChange={(event) => onVersionChange(event.target.value)} placeholder="文档版本,例如 1.0.0" />
1408
- </label>
1409
- <label className="source-input">
1410
- <Pencil size={16} />
1411
- <input value={description} onChange={(event) => onDescriptionChange(event.target.value)} placeholder="文档描述,可选" />
1412
- </label>
1355
+ <div className="document-dialog-tabs" role="tablist" aria-label="新建文档方式">
1356
+ {tabs.map((tab) => {
1357
+ const Icon = tab.icon;
1358
+ return (
1359
+ <button
1360
+ key={tab.id}
1361
+ className={activeTab === tab.id ? 'active' : ''}
1362
+ onClick={() => onTabChange(tab.id)}
1363
+ role="tab"
1364
+ aria-selected={activeTab === tab.id}
1365
+ disabled={saving}
1366
+ >
1367
+ <Icon size={15} />
1368
+ {tab.label}
1369
+ </button>
1370
+ );
1371
+ })}
1413
1372
  </div>
1414
1373
 
1415
- <div className="import-dialog-actions">
1374
+ <div className="document-dialog-body" role="tabpanel">
1375
+ {activeTab === 'blank' ? (
1376
+ <div className="blank-document-fields">
1377
+ <label className="source-input">
1378
+ <FileCode2 size={16} />
1379
+ <input
1380
+ value={title}
1381
+ onChange={(event) => onTitleChange(event.target.value)}
1382
+ placeholder="文档名称,例如 Customer Service API"
1383
+ autoFocus
1384
+ />
1385
+ </label>
1386
+ <label className="source-input">
1387
+ <Braces size={16} />
1388
+ <input value={version} onChange={(event) => onVersionChange(event.target.value)} placeholder="文档版本,例如 1.0.0" />
1389
+ </label>
1390
+ <label className="source-input">
1391
+ <Pencil size={16} />
1392
+ <input value={description} onChange={(event) => onDescriptionChange(event.target.value)} placeholder="文档描述,可选" />
1393
+ </label>
1394
+ </div>
1395
+ ) : activeTab === 'upload' ? (
1396
+ <label className="source-file-picker document-file-picker">
1397
+ <FileCode2 size={18} />
1398
+ <input
1399
+ ref={uploadInputRef}
1400
+ type="file"
1401
+ accept=".json,.yaml,.yml,application/json,application/yaml,text/yaml,text/x-yaml"
1402
+ onChange={(event) => onUploadFileChange(event.target.files?.[0] ?? null)}
1403
+ />
1404
+ <span>{uploadFile ? uploadFile.name : '选择 JSON / YAML 文件'}</span>
1405
+ <small>{uploadFile ? '点击可重新选择文件' : '支持 OpenAPI 3.x 与 Swagger 2.0'}</small>
1406
+ </label>
1407
+ ) : (
1408
+ <label className={`source-input document-source-input ${activeTab === 'curl' ? 'source-input-multiline' : ''}`}>
1409
+ <Link2 size={16} />
1410
+ {activeTab === 'curl' ? (
1411
+ <textarea
1412
+ value={curlSourceText}
1413
+ onChange={(event) => onCurlSourceTextChange(event.target.value)}
1414
+ placeholder="粘贴 curl 命令,例如 curl 'https://example.com/openapi.yaml' -H 'accept: application/yaml'"
1415
+ spellCheck={false}
1416
+ autoFocus
1417
+ />
1418
+ ) : (
1419
+ <input
1420
+ value={activeTab === 'file' ? fileSourceUrl : crawlSourceUrl}
1421
+ onChange={(event) => {
1422
+ if (activeTab === 'file') onFileSourceUrlChange(event.target.value);
1423
+ else onCrawlSourceUrlChange(event.target.value);
1424
+ }}
1425
+ placeholder={
1426
+ activeTab === 'file'
1427
+ ? '输入 Swagger/OpenAPI JSON 或 YAML 文件地址'
1428
+ : '输入 Swagger UI / Knife4j / Redoc 在线文档页地址'
1429
+ }
1430
+ autoFocus
1431
+ />
1432
+ )}
1433
+ </label>
1434
+ )}
1435
+ </div>
1436
+
1437
+ <div className="import-dialog-actions document-dialog-actions">
1438
+ {activeTab !== 'blank' && hasValue ? (
1439
+ <button className="secondary-button clear-document-source" onClick={onClear} disabled={saving}>
1440
+ 清空
1441
+ </button>
1442
+ ) : null}
1416
1443
  <button className="secondary-button" onClick={onClose} disabled={saving}>
1417
1444
  取消
1418
1445
  </button>
1419
- <button className="primary-button" onClick={onSubmit} disabled={saving || !title.trim()}>
1420
- <Braces size={16} className={saving ? 'spin' : ''} />
1421
- {saving ? '处理中' : updating ? '更新' : '创建'}
1446
+ <button className="primary-button" onClick={onSubmit} disabled={saving || !hasValue}>
1447
+ <ActiveIcon size={16} className={saving ? 'spin' : ''} />
1448
+ {saving ? '处理中' : updating ? '更新' : activeTab === 'blank' ? '创建' : '确认'}
1422
1449
  </button>
1423
1450
  </div>
1424
1451
  </div>