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/MCP.md +5 -5
- package/README.ja.md +8 -8
- package/README.ko.md +8 -8
- package/README.md +27 -15
- package/README.zh.md +27 -15
- package/dist/assets/index-CV6e3HAP.js +299 -0
- package/dist/assets/index-slFOragU.css +1 -0
- package/dist/index.html +2 -2
- package/docs/cli.ja.md +1 -1
- package/docs/cli.ko.md +1 -1
- package/docs/cli.md +4 -2
- package/docs/cli.zh.md +4 -2
- package/docs/mcp.ja.md +1 -1
- package/docs/mcp.ko.md +1 -1
- package/docs/mcp.md +3 -1
- package/docs/mcp.zh.md +3 -1
- package/docs/web.ja.md +1 -1
- package/docs/web.ko.md +1 -1
- package/docs/web.md +2 -2
- package/docs/web.zh.md +3 -3
- 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 +223 -184
- package/src/styles.css +172 -6
- 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,42 @@ 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
|
-
{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
|
-
|
|
973
|
+
if (!selectedVersion) return;
|
|
974
|
+
void prepareVersionUpdate(selectedVersion);
|
|
1061
975
|
}}
|
|
1062
|
-
disabled={Boolean(syncingMode) || !
|
|
976
|
+
disabled={Boolean(syncingMode) || !selectedVersion}
|
|
977
|
+
title={selectedVersion ? '按当前文档最初的生成方式更新并覆盖原版本' : '请先新建或导入文档'}
|
|
1063
978
|
>
|
|
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 ? '更新' : '确认'}
|
|
979
|
+
<RefreshCcw size={16} />
|
|
980
|
+
更新文档
|
|
1074
981
|
</button>
|
|
1075
|
-
<button className="secondary-button" onClick={
|
|
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
|
-
{
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1021
|
+
{documentDialogOpen ? (
|
|
1022
|
+
<DocumentDialog
|
|
1023
|
+
activeTab={activeDocumentTab}
|
|
1024
|
+
updating={activeDocumentCanUpdate}
|
|
1113
1025
|
title={blankDocTitle}
|
|
1114
1026
|
version={blankDocVersion}
|
|
1115
1027
|
description={blankDocDescription}
|
|
1116
|
-
|
|
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
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
|
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="
|
|
1384
|
-
<div className="
|
|
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="
|
|
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="
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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="
|
|
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 || !
|
|
1420
|
-
<
|
|
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>
|