apiskill 0.1.1 → 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/README.md +2 -2
- package/README.zh.md +2 -2
- package/dist/assets/index-BeAJ1G-n.css +1 -0
- package/dist/assets/index-CPxVZ06Y.js +299 -0
- package/dist/index.html +2 -2
- package/docs/cli.md +1 -1
- package/docs/cli.zh.md +1 -1
- package/docs/web.md +1 -1
- package/docs/web.zh.md +2 -2
- package/package.json +1 -1
- package/scripts/apiskill-cli.mjs +11 -4
- package/scripts/lib/cache-paths.mjs +8 -0
- package/scripts/lib/openapi-store.mjs +2 -3
- package/scripts/mcp-server.mjs +2 -1
- package/src/App.tsx +213 -186
- package/src/styles.css +160 -3
- package/vite.config.ts +2 -1
- package/dist/assets/index-DH0wsJCI.js +0 -299
- package/dist/assets/index-vocUDpcf.css +0 -1
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 [
|
|
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 [
|
|
208
|
-
const [
|
|
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
|
-
|
|
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
|
|
417
|
-
|
|
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 (
|
|
428
|
-
if (
|
|
429
|
-
if (
|
|
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 (
|
|
441
|
-
else if (
|
|
442
|
-
else if (
|
|
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
|
|
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 =
|
|
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
|
-
|
|
647
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
|
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-
|
|
960
|
-
<
|
|
961
|
-
|
|
962
|
-
|
|
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-
|
|
1017
|
-
|
|
1018
|
-
<
|
|
1019
|
-
|
|
1020
|
-
|
|
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="
|
|
975
|
+
className="secondary-button"
|
|
1059
976
|
onClick={() => {
|
|
1060
|
-
|
|
977
|
+
setDocumentUpdateVersionId('');
|
|
978
|
+
setActiveDocumentTab('blank');
|
|
979
|
+
setDocumentDialogOpen(true);
|
|
1061
980
|
}}
|
|
1062
|
-
disabled={Boolean(syncingMode)
|
|
981
|
+
disabled={Boolean(syncingMode)}
|
|
1063
982
|
>
|
|
1064
|
-
{
|
|
1065
|
-
|
|
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
|
-
{
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1009
|
+
{documentDialogOpen ? (
|
|
1010
|
+
<DocumentDialog
|
|
1011
|
+
activeTab={activeDocumentTab}
|
|
1012
|
+
updating={activeDocumentCanUpdate}
|
|
1113
1013
|
title={blankDocTitle}
|
|
1114
1014
|
version={blankDocVersion}
|
|
1115
1015
|
description={blankDocDescription}
|
|
1116
|
-
|
|
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
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
|
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="
|
|
1384
|
-
<div className="
|
|
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="
|
|
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="
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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="
|
|
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 || !
|
|
1420
|
-
<
|
|
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>
|