apiskill 0.1.1 → 0.1.3

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,42 @@ 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">
1057
970
  <button
1058
971
  className="primary-button"
1059
972
  onClick={() => {
1060
- void importSpec(activeSourceTab);
973
+ if (!selectedVersion) return;
974
+ void prepareVersionUpdate(selectedVersion);
1061
975
  }}
1062
- disabled={Boolean(syncingMode) || !getActiveSourceValue()}
976
+ disabled={Boolean(syncingMode) || !selectedVersion}
977
+ title={selectedVersion ? '按当前文档最初的生成方式更新并覆盖原版本' : '请先新建或导入文档'}
1063
978
  >
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 ? '更新' : '确认'}
979
+ <RefreshCcw size={16} />
980
+ 更新文档
1074
981
  </button>
1075
- <button className="secondary-button" onClick={clearActiveSource} disabled={Boolean(syncingMode) || !getActiveSourceValue()}>
1076
- 清空
982
+ <button className="secondary-button" onClick={startMockService} disabled={startingMock}>
983
+ <Server size={16} className={startingMock ? 'spin' : ''} />
984
+ {startingMock ? '启动中' : '启动MOCK服务'}
985
+ </button>
986
+ <button
987
+ className="secondary-button"
988
+ onClick={() => {
989
+ setDocumentUpdateVersionId('');
990
+ setActiveDocumentTab('blank');
991
+ setDocumentDialogOpen(true);
992
+ }}
993
+ disabled={Boolean(syncingMode)}
994
+ >
995
+ <Braces size={16} />
996
+ 新建文档
1077
997
  </button>
1078
998
  </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
999
  {mockMessage ? <div className={`version-message ${mockMessage.includes('失败') || mockMessage.includes('请先') ? 'error' : ''}`}>{mockMessage}</div> : null}
1089
1000
  </section>
1090
1001
 
@@ -1107,22 +1018,40 @@ export function App() {
1107
1018
  onSubmit={submitDocumentAuth}
1108
1019
  />
1109
1020
  ) : null}
1110
- {blankDocDialogOpen ? (
1111
- <BlankDocumentDialog
1112
- updating={Boolean(blankUpdateVersionId)}
1021
+ {documentDialogOpen ? (
1022
+ <DocumentDialog
1023
+ activeTab={activeDocumentTab}
1024
+ updating={activeDocumentCanUpdate}
1113
1025
  title={blankDocTitle}
1114
1026
  version={blankDocVersion}
1115
1027
  description={blankDocDescription}
1116
- saving={syncingMode === 'blank'}
1028
+ fileSourceUrl={fileSourceUrl}
1029
+ crawlSourceUrl={crawlSourceUrl}
1030
+ curlSourceText={curlSourceText}
1031
+ uploadFile={uploadFile}
1032
+ uploadInputRef={uploadInputRef}
1033
+ saving={syncingMode === activeDocumentTab}
1034
+ onTabChange={setActiveDocumentTab}
1117
1035
  onTitleChange={setBlankDocTitle}
1118
1036
  onVersionChange={setBlankDocVersion}
1119
1037
  onDescriptionChange={setBlankDocDescription}
1038
+ onFileSourceUrlChange={setFileSourceUrl}
1039
+ onCrawlSourceUrlChange={setCrawlSourceUrl}
1040
+ onCurlSourceTextChange={setCurlSourceText}
1041
+ onUploadFileChange={setUploadFile}
1042
+ onClear={clearActiveSource}
1120
1043
  onClose={() => {
1121
- if (syncingMode === 'blank') return;
1122
- setBlankDocDialogOpen(false);
1123
- setBlankUpdateVersionId('');
1044
+ if (syncingMode) return;
1045
+ setDocumentDialogOpen(false);
1046
+ setDocumentUpdateVersionId('');
1047
+ }}
1048
+ onSubmit={() => {
1049
+ if (activeDocumentTab === 'blank') {
1050
+ void createBlankApiDocument();
1051
+ return;
1052
+ }
1053
+ void importSpec(activeDocumentTab, { type: 'none' }, false, getDocumentUpdateVersionId(activeDocumentTab));
1124
1054
  }}
1125
- onSubmit={createBlankApiDocument}
1126
1055
  />
1127
1056
  ) : null}
1128
1057
  {addApiOpen ? (
@@ -1356,69 +1285,179 @@ function DocumentAuthDialog({
1356
1285
  );
1357
1286
  }
1358
1287
 
1359
- function BlankDocumentDialog({
1288
+ function DocumentDialog({
1289
+ activeTab,
1360
1290
  updating,
1361
1291
  title,
1362
1292
  version,
1363
1293
  description,
1294
+ fileSourceUrl,
1295
+ crawlSourceUrl,
1296
+ curlSourceText,
1297
+ uploadFile,
1298
+ uploadInputRef,
1364
1299
  saving,
1300
+ onTabChange,
1365
1301
  onTitleChange,
1366
1302
  onVersionChange,
1367
1303
  onDescriptionChange,
1304
+ onFileSourceUrlChange,
1305
+ onCrawlSourceUrlChange,
1306
+ onCurlSourceTextChange,
1307
+ onUploadFileChange,
1308
+ onClear,
1368
1309
  onClose,
1369
1310
  onSubmit,
1370
1311
  }: {
1312
+ activeTab: ImportDialogMode;
1371
1313
  updating: boolean;
1372
1314
  title: string;
1373
1315
  version: string;
1374
1316
  description: string;
1317
+ fileSourceUrl: string;
1318
+ crawlSourceUrl: string;
1319
+ curlSourceText: string;
1320
+ uploadFile: File | null;
1321
+ uploadInputRef: { current: HTMLInputElement | null };
1375
1322
  saving: boolean;
1323
+ onTabChange: (value: ImportDialogMode) => void;
1376
1324
  onTitleChange: (value: string) => void;
1377
1325
  onVersionChange: (value: string) => void;
1378
1326
  onDescriptionChange: (value: string) => void;
1327
+ onFileSourceUrlChange: (value: string) => void;
1328
+ onCrawlSourceUrlChange: (value: string) => void;
1329
+ onCurlSourceTextChange: (value: string) => void;
1330
+ onUploadFileChange: (value: File | null) => void;
1331
+ onClear: () => void;
1379
1332
  onClose: () => void;
1380
1333
  onSubmit: () => void;
1381
1334
  }) {
1335
+ const tabs: Array<{ id: ImportDialogMode; label: string; icon: typeof Braces }> = [
1336
+ { id: 'blank', label: '空白文档', icon: Braces },
1337
+ { id: 'file', label: '在线文档', icon: RefreshCcw },
1338
+ { id: 'crawl', label: '爬取网页', icon: Globe2 },
1339
+ { id: 'curl', label: '执行 CURL', icon: Send },
1340
+ { id: 'upload', label: '导入文件', icon: FileCode2 },
1341
+ ];
1342
+ const hasValue =
1343
+ activeTab === 'blank'
1344
+ ? Boolean(title.trim())
1345
+ : activeTab === 'file'
1346
+ ? Boolean(fileSourceUrl.trim())
1347
+ : activeTab === 'crawl'
1348
+ ? Boolean(crawlSourceUrl.trim())
1349
+ : activeTab === 'curl'
1350
+ ? Boolean(curlSourceText.trim())
1351
+ : Boolean(uploadFile);
1352
+ const ActiveIcon = tabs.find((tab) => tab.id === activeTab)?.icon ?? Braces;
1353
+
1382
1354
  return (
1383
- <div className="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="blank-document-title">
1384
- <div className="auth-dialog">
1355
+ <div className="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="document-dialog-title">
1356
+ <div className="document-dialog">
1385
1357
  <div className="import-dialog-header">
1386
1358
  <div>
1387
1359
  <p className="eyebrow">OpenAPI Document</p>
1388
- <h2 id="blank-document-title">{updating ? '更新文档' : '新建文档'}</h2>
1360
+ <h2 id="document-dialog-title">{updating ? '更新文档' : '新建文档'}</h2>
1389
1361
  </div>
1390
1362
  <button className="icon-secondary-button" onClick={onClose} aria-label="关闭" disabled={saving}>
1391
1363
  <X size={17} />
1392
1364
  </button>
1393
1365
  </div>
1394
1366
 
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>
1367
+ <div className="document-dialog-tabs" role="tablist" aria-label="新建文档方式">
1368
+ {tabs.map((tab) => {
1369
+ const Icon = tab.icon;
1370
+ return (
1371
+ <button
1372
+ key={tab.id}
1373
+ className={activeTab === tab.id ? 'active' : ''}
1374
+ onClick={() => onTabChange(tab.id)}
1375
+ role="tab"
1376
+ aria-selected={activeTab === tab.id}
1377
+ disabled={saving}
1378
+ >
1379
+ <Icon size={15} />
1380
+ {tab.label}
1381
+ </button>
1382
+ );
1383
+ })}
1413
1384
  </div>
1414
1385
 
1415
- <div className="import-dialog-actions">
1386
+ <div className="document-dialog-body" role="tabpanel">
1387
+ {activeTab === 'blank' ? (
1388
+ <div className="blank-document-fields">
1389
+ <label className="source-input">
1390
+ <FileCode2 size={16} />
1391
+ <input
1392
+ value={title}
1393
+ onChange={(event) => onTitleChange(event.target.value)}
1394
+ placeholder="文档名称,例如 Customer Service API"
1395
+ autoFocus
1396
+ />
1397
+ </label>
1398
+ <label className="source-input">
1399
+ <Braces size={16} />
1400
+ <input value={version} onChange={(event) => onVersionChange(event.target.value)} placeholder="文档版本,例如 1.0.0" />
1401
+ </label>
1402
+ <label className="source-input">
1403
+ <Pencil size={16} />
1404
+ <input value={description} onChange={(event) => onDescriptionChange(event.target.value)} placeholder="文档描述,可选" />
1405
+ </label>
1406
+ </div>
1407
+ ) : activeTab === 'upload' ? (
1408
+ <label className="source-file-picker document-file-picker">
1409
+ <FileCode2 size={18} />
1410
+ <input
1411
+ ref={uploadInputRef}
1412
+ type="file"
1413
+ accept=".json,.yaml,.yml,application/json,application/yaml,text/yaml,text/x-yaml"
1414
+ onChange={(event) => onUploadFileChange(event.target.files?.[0] ?? null)}
1415
+ />
1416
+ <span>{uploadFile ? uploadFile.name : '选择 JSON / YAML 文件'}</span>
1417
+ <small>{uploadFile ? '点击可重新选择文件' : '支持 OpenAPI 3.x 与 Swagger 2.0'}</small>
1418
+ </label>
1419
+ ) : (
1420
+ <label className={`source-input document-source-input ${activeTab === 'curl' ? 'source-input-multiline' : ''}`}>
1421
+ <Link2 size={16} />
1422
+ {activeTab === 'curl' ? (
1423
+ <textarea
1424
+ value={curlSourceText}
1425
+ onChange={(event) => onCurlSourceTextChange(event.target.value)}
1426
+ placeholder="粘贴 curl 命令,例如 curl 'https://example.com/openapi.yaml' -H 'accept: application/yaml'"
1427
+ spellCheck={false}
1428
+ autoFocus
1429
+ />
1430
+ ) : (
1431
+ <input
1432
+ value={activeTab === 'file' ? fileSourceUrl : crawlSourceUrl}
1433
+ onChange={(event) => {
1434
+ if (activeTab === 'file') onFileSourceUrlChange(event.target.value);
1435
+ else onCrawlSourceUrlChange(event.target.value);
1436
+ }}
1437
+ placeholder={
1438
+ activeTab === 'file'
1439
+ ? '输入 Swagger/OpenAPI JSON 或 YAML 文件地址'
1440
+ : '输入 Swagger UI / Knife4j / Redoc 在线文档页地址'
1441
+ }
1442
+ autoFocus
1443
+ />
1444
+ )}
1445
+ </label>
1446
+ )}
1447
+ </div>
1448
+
1449
+ <div className="import-dialog-actions document-dialog-actions">
1450
+ {activeTab !== 'blank' && hasValue ? (
1451
+ <button className="secondary-button clear-document-source" onClick={onClear} disabled={saving}>
1452
+ 清空
1453
+ </button>
1454
+ ) : null}
1416
1455
  <button className="secondary-button" onClick={onClose} disabled={saving}>
1417
1456
  取消
1418
1457
  </button>
1419
- <button className="primary-button" onClick={onSubmit} disabled={saving || !title.trim()}>
1420
- <Braces size={16} className={saving ? 'spin' : ''} />
1421
- {saving ? '处理中' : updating ? '更新' : '创建'}
1458
+ <button className="primary-button" onClick={onSubmit} disabled={saving || !hasValue}>
1459
+ <ActiveIcon size={16} className={saving ? 'spin' : ''} />
1460
+ {saving ? '处理中' : updating ? '更新' : activeTab === 'blank' ? '创建' : '确认'}
1422
1461
  </button>
1423
1462
  </div>
1424
1463
  </div>