dsh-plugin-yapi 0.0.0-stage → 1.4.0

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/client.js ADDED
@@ -0,0 +1,2464 @@
1
+ /**
2
+ * dsh-plugin-yapi — client half.
3
+ *
4
+ * Registers two slots, exactly the way the shipped sidebar expects:
5
+ * sidebar.panellist (list/root) → the "YAPI" row in the left column
6
+ * main (keyed/root) → the workbench panel behind that row
7
+ *
8
+ * Everything is origin-relative: the panel calls `/api/yapi/interface`, and the
9
+ * host half owns the YAPI session. The only external module requested is React
10
+ * from the browser module table.
11
+ */
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-plugin-yapi',
14
+ factory: (require) => {
15
+ const React = require('react')
16
+ const h = React.createElement
17
+ const { useState, useEffect, useRef, useMemo } = React
18
+
19
+ const PANEL_ID = 'yapi'
20
+ const PANEL_LABEL = 'YAPI'
21
+ const PANEL_ORDER = 70
22
+ const STYLE_ID = 'dsh-plugin-yapi-style'
23
+ const LAST_KEY = 'dsh-plugin-yapi:last-id'
24
+ const RECENT_KEY = 'dsh-plugin-yapi:recent'
25
+ const COPY_FORMAT_KEY = 'dsh-plugin-yapi:copy-format'
26
+ const TABS_KEY = 'dsh-plugin-yapi:tabs'
27
+ const ACTIVE_KEY = 'dsh-plugin-yapi:active-tab'
28
+ const PROJECT_KEY = 'dsh-plugin-yapi:search-project'
29
+ const COPY_OPTIONS_KEY = 'dsh-plugin-yapi:copy-options'
30
+ const DEFAULT_COPY_OPTIONS = {
31
+ title: true,
32
+ address: true,
33
+ headers: true,
34
+ params: true,
35
+ requestBody: true,
36
+ responseBody: true,
37
+ desc: false,
38
+ }
39
+ const COPY_OPTIONS = [
40
+ { key: 'title', label: '接口标题' },
41
+ { key: 'address', label: '请求地址(含方法)' },
42
+ { key: 'headers', label: '请求头' },
43
+ { key: 'params', label: '请求参数(Query / Path / Form)' },
44
+ { key: 'requestBody', label: '请求 Body' },
45
+ { key: 'responseBody', label: '返回体' },
46
+ { key: 'desc', label: '接口说明' },
47
+ ]
48
+ const TABS_CACHE = { initialized: false, tabs: [], activeId: null }
49
+
50
+ /* ------------------------------------------------------------------ */
51
+ /* helpers */
52
+ /* ------------------------------------------------------------------ */
53
+
54
+ function readStorage(key, fallback) {
55
+ try {
56
+ const value = window.localStorage.getItem(key)
57
+ return value === null ? fallback : value
58
+ } catch {
59
+ return fallback
60
+ }
61
+ }
62
+
63
+ function writeStorage(key, value) {
64
+ try {
65
+ window.localStorage.setItem(key, value)
66
+ } catch {
67
+ /* private mode / quota — the panel still works without persistence */
68
+ }
69
+ }
70
+
71
+ function readCopyOptions() {
72
+ try {
73
+ const parsed = JSON.parse(readStorage(COPY_OPTIONS_KEY, '{}'))
74
+ const options = Object.assign({}, DEFAULT_COPY_OPTIONS)
75
+ if (parsed && typeof parsed === 'object') {
76
+ for (const key of Object.keys(DEFAULT_COPY_OPTIONS)) {
77
+ if (typeof parsed[key] === 'boolean') options[key] = parsed[key]
78
+ }
79
+ }
80
+ return options
81
+ } catch {
82
+ return Object.assign({}, DEFAULT_COPY_OPTIONS)
83
+ }
84
+ }
85
+
86
+ function readRecent() {
87
+ try {
88
+ const parsed = JSON.parse(readStorage(RECENT_KEY, '[]'))
89
+ if (!Array.isArray(parsed)) return []
90
+ return parsed.filter((id) => Number.isSafeInteger(id) && id > 0).slice(0, 6)
91
+ } catch {
92
+ return []
93
+ }
94
+ }
95
+
96
+ function readStoredTabs() {
97
+ try {
98
+ const parsed = JSON.parse(readStorage(TABS_KEY, '[]'))
99
+ if (!Array.isArray(parsed)) return []
100
+ const tabs = []
101
+ for (const entry of parsed.slice(0, 12)) {
102
+ const id = Number(entry && entry.id)
103
+ if (!Number.isSafeInteger(id) || id <= 0) continue
104
+ if (tabs.some((tab) => tab.id === id)) continue
105
+ tabs.push({
106
+ id: id,
107
+ title: String((entry && entry.title) || ''),
108
+ method: String((entry && entry.method) || ''),
109
+ path: String((entry && entry.path) || ''),
110
+ doc: null,
111
+ loading: false,
112
+ error: null,
113
+ base: '',
114
+ })
115
+ }
116
+ return tabs
117
+ } catch {
118
+ return []
119
+ }
120
+ }
121
+
122
+ function ensureTabs() {
123
+ if (TABS_CACHE.initialized) return TABS_CACHE
124
+ TABS_CACHE.initialized = true
125
+ TABS_CACHE.tabs = readStoredTabs()
126
+ const storedActive = Number(readStorage(ACTIVE_KEY, ''))
127
+ TABS_CACHE.activeId = TABS_CACHE.tabs.some((tab) => tab.id === storedActive)
128
+ ? storedActive
129
+ : TABS_CACHE.tabs.length > 0
130
+ ? TABS_CACHE.tabs[0].id
131
+ : null
132
+ return TABS_CACHE
133
+ }
134
+
135
+ function looksLikeDocRef(value) {
136
+ const text = String(value === undefined || value === null ? '' : value).trim()
137
+ if (text === '') return false
138
+ if (/^\d{1,12}$/.test(text)) return true
139
+ return /\/interface\/api\/\d+/i.test(text) || /[?&]id=\d+/i.test(text)
140
+ }
141
+
142
+ function parseDocId(raw) {
143
+ if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) return raw
144
+ const text = String(raw === undefined || raw === null ? '' : raw).trim()
145
+ if (text === '') return null
146
+ if (/^\d{1,12}$/.test(text)) {
147
+ const id = Number(text)
148
+ return id > 0 ? id : null
149
+ }
150
+ const patterns = [
151
+ /\/interface\/api\/(\d{1,12})(?:[/?#]|$)/,
152
+ /[?&]id=(\d{1,12})(?:[&#]|$)/,
153
+ /(\d{1,12})\/?(?:[?#].*)?$/,
154
+ ]
155
+ for (const pattern of patterns) {
156
+ const match = pattern.exec(text)
157
+ if (match && Number(match[1]) > 0) return Number(match[1])
158
+ }
159
+ return null
160
+ }
161
+
162
+ function joinUrl() {
163
+ let out = ''
164
+ for (let index = 0; index < arguments.length; index += 1) {
165
+ const text = String(arguments[index] === undefined || arguments[index] === null ? '' : arguments[index]).trim()
166
+ if (text === '') continue
167
+ if (out === '') out = text.replace(/\/+$/, '')
168
+ else out += '/' + text.replace(/^\/+/, '').replace(/\/+$/, '')
169
+ }
170
+ return out
171
+ }
172
+
173
+ function formatTime(timestamp) {
174
+ const date = new Date(timestamp)
175
+ if (Number.isNaN(date.getTime())) return ''
176
+ try {
177
+ return date.toLocaleString('zh-CN', {
178
+ year: 'numeric',
179
+ month: '2-digit',
180
+ day: '2-digit',
181
+ hour: '2-digit',
182
+ minute: '2-digit',
183
+ hour12: false,
184
+ })
185
+ } catch {
186
+ return date.toISOString().slice(0, 16).replace('T', ' ')
187
+ }
188
+ }
189
+
190
+ function htmlToText(html) {
191
+ const text = String(html === undefined || html === null ? '' : html)
192
+ if (text === '') return ''
193
+ if (typeof document === 'undefined') return text.replace(/<[^>]+>/g, '')
194
+ const holder = document.createElement('div')
195
+ holder.innerHTML = text
196
+ return holder.textContent || ''
197
+ }
198
+
199
+ function bodyText(body, mode, annotated) {
200
+ if (!body) return ''
201
+ if (mode === 'schema') return body.raw || ''
202
+ if (mode === 'annotated') return (annotated && annotated.text) || body.example || body.raw || ''
203
+ return body.example || body.raw || ''
204
+ }
205
+
206
+ function buildCopyText(doc, options, address, format, requestAnnotation, responseAnnotation, descText) {
207
+ const sections = []
208
+ const mode = format === 'annotated' ? 'annotated' : 'example'
209
+ if (options.title) sections.push('# ' + (doc.title || '未命名接口'))
210
+ if (options.address) {
211
+ const target = address || doc.path || ''
212
+ sections.push('## 请求地址\n' + doc.method + ' ' + target)
213
+ }
214
+ if (options.headers && doc.request.headers.length > 0) {
215
+ const lines = doc.request.headers.map(function (row) {
216
+ return row.name + ': ' + (row.value || '')
217
+ })
218
+ sections.push('## 请求头\n' + lines.join('\n'))
219
+ }
220
+ if (options.params) {
221
+ const lines = []
222
+ const addRows = function (title, rows) {
223
+ if (rows.length === 0) return
224
+ lines.push(title + ':')
225
+ for (const row of rows) {
226
+ const meta = []
227
+ if (row.type) meta.push(row.type)
228
+ meta.push(row.required ? '必填' : '可选')
229
+ if (row.desc) meta.push(row.desc)
230
+ const example = row.example ? ' = ' + row.example : ''
231
+ lines.push('- ' + row.name + example + '(' + meta.join(',') + ')')
232
+ }
233
+ }
234
+ addRows('Query', doc.request.query)
235
+ addRows('Path', doc.request.params)
236
+ addRows('Form', doc.request.form)
237
+ if (lines.length > 0) sections.push('## 请求参数\n' + lines.join('\n'))
238
+ }
239
+ if (options.requestBody) {
240
+ const text = bodyText(doc.request.body, mode, requestAnnotation)
241
+ if (text.trim() !== '') sections.push('## 请求 Body\n' + text)
242
+ }
243
+ if (options.responseBody) {
244
+ const text = bodyText(doc.response.body, mode, responseAnnotation)
245
+ if (text.trim() !== '') sections.push('## 返回体\n' + text)
246
+ }
247
+ if (options.desc && descText !== '') sections.push('## 接口说明\n' + descText)
248
+ return sections.join('\n\n')
249
+ }
250
+
251
+ async function copyText(text) {
252
+ const value = String(text === undefined || text === null ? '' : text)
253
+ if (value === '') return false
254
+ try {
255
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
256
+ await navigator.clipboard.writeText(value)
257
+ return true
258
+ }
259
+ } catch {
260
+ /* fall through to the selection-based path */
261
+ }
262
+ try {
263
+ const area = document.createElement('textarea')
264
+ area.value = value
265
+ area.setAttribute('readonly', '')
266
+ area.style.position = 'fixed'
267
+ area.style.top = '-9999px'
268
+ area.style.opacity = '0'
269
+ document.body.appendChild(area)
270
+ area.select()
271
+ area.setSelectionRange(0, value.length)
272
+ const ok = document.execCommand('copy')
273
+ document.body.removeChild(area)
274
+ return ok
275
+ } catch {
276
+ return false
277
+ }
278
+ }
279
+
280
+
281
+ /* ------------------------------------------------------------------ */
282
+ /* schema annotation (client-side) */
283
+ /* ------------------------------------------------------------------ */
284
+ /* The host already sends both the generated example and the raw YAPI
285
+ * schema. Annotating in the browser keeps the feature independent of the
286
+ * host half: a client update alone can add field remarks, and the copied
287
+ * JSONC always matches the example the panel shows. */
288
+
289
+ function parseJsonSafe(text) {
290
+ if (typeof text !== 'string' || text.trim() === '') return undefined
291
+ try {
292
+ return JSON.parse(text)
293
+ } catch {
294
+ return undefined
295
+ }
296
+ }
297
+
298
+ function oneLine(text) {
299
+ return String(text === undefined || text === null ? '' : text).replace(/\s+/g, ' ').trim()
300
+ }
301
+
302
+ function unwrapComposite(input) {
303
+ let current = input && typeof input === 'object' ? input : {}
304
+ for (let round = 0; round < 8; round += 1) {
305
+ if (!current || typeof current !== 'object') return {}
306
+ if (Array.isArray(current.allOf) && current.allOf.length > 0) {
307
+ const merged = Object.assign({}, current)
308
+ delete merged.allOf
309
+ const properties = Object.assign({}, current.properties || {})
310
+ for (const member of current.allOf) {
311
+ if (member && typeof member === 'object' && member.properties) Object.assign(properties, member.properties)
312
+ }
313
+ merged.properties = properties
314
+ merged.type = merged.type || 'object'
315
+ return merged
316
+ }
317
+ const list = Array.isArray(current.oneOf) ? current.oneOf : Array.isArray(current.anyOf) ? current.anyOf : null
318
+ if (list && list.length > 0 && list[0] && typeof list[0] === 'object') {
319
+ current = list[0]
320
+ continue
321
+ }
322
+ return current
323
+ }
324
+ return current
325
+ }
326
+
327
+ function schemaComment(node) {
328
+ return oneLine(unwrapComposite(node).description)
329
+ }
330
+
331
+ function typeLabel(node) {
332
+ const schema = unwrapComposite(node)
333
+ let type = schema.type
334
+ if (Array.isArray(type)) type = type.filter(function (item) { return item !== 'null' }).join(' | ') || 'null'
335
+ if (type === 'array') {
336
+ const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
337
+ const inner = items ? typeLabel(items) : ''
338
+ return inner ? 'array<' + inner + '>' : 'array'
339
+ }
340
+ if (!type && schema.properties) return 'object'
341
+ return type || ''
342
+ }
343
+
344
+ function renderAnnotated(node, value, depth, state, path) {
345
+ const schema = unwrapComposite(node)
346
+ const pad = ' '.repeat(depth)
347
+ if (Array.isArray(value)) {
348
+ if (value.length === 0) return '[]'
349
+ const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
350
+ const lines = ['[']
351
+ value.forEach(function (item, index) {
352
+ const last = index === value.length - 1
353
+ lines.push(pad + ' ' + renderAnnotated(items, item, depth + 1, state, path + '[]') + (last ? '' : ','))
354
+ })
355
+ lines.push(pad + ']')
356
+ return lines.join('\n')
357
+ }
358
+ if (value && typeof value === 'object') {
359
+ const keys = Object.keys(value)
360
+ if (keys.length === 0) return '{}'
361
+ const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {}
362
+ const required = Array.isArray(schema.required) ? schema.required : []
363
+ const lines = ['{']
364
+ keys.forEach(function (key, index) {
365
+ const last = index === keys.length - 1
366
+ const child = properties[key]
367
+ const childPath = path ? path + '.' + key : key
368
+ if (!state.seen.has(childPath)) {
369
+ state.seen.add(childPath)
370
+ state.fields.push({
371
+ path: childPath,
372
+ type: typeLabel(child),
373
+ required: required.indexOf(key) !== -1,
374
+ description: schemaComment(child),
375
+ })
376
+ }
377
+ const rendered = renderAnnotated(child, value[key], depth + 1, state, childPath)
378
+ const childLines = rendered.split('\n')
379
+ const comment = schemaComment(child)
380
+ if (value[key] !== null && typeof value[key] === 'object') {
381
+ lines.push(pad + ' ' + JSON.stringify(key) + ': ' + childLines[0] + (comment ? ' // ' + comment : ''))
382
+ for (let line = 1; line < childLines.length - 1; line += 1) lines.push(childLines[line])
383
+ lines.push(childLines[childLines.length - 1] + (last ? '' : ','))
384
+ } else {
385
+ lines.push(
386
+ pad + ' ' + JSON.stringify(key) + ': ' + childLines[0] + (last ? '' : ',') + (comment ? ' // ' + comment : ''),
387
+ )
388
+ }
389
+ })
390
+ lines.push(pad + '}')
391
+ return lines.join('\n')
392
+ }
393
+ return JSON.stringify(value === undefined ? null : value)
394
+ }
395
+
396
+ function annotateSchemaValue(raw, exampleText) {
397
+ const schema = parseJsonSafe(raw)
398
+ let value = parseJsonSafe(exampleText)
399
+ if (schema === undefined) return { text: exampleText || '', fields: [] }
400
+ if (value === undefined) value = null
401
+ const state = { fields: [], seen: new Set() }
402
+ return { text: renderAnnotated(schema, value, 0, state, ''), fields: state.fields }
403
+ }
404
+
405
+ function annotateFormValue(form, exampleText) {
406
+ const example = parseJsonSafe(exampleText)
407
+ const object = example && typeof example === 'object' ? example : {}
408
+ const fields = form.map(function (field) {
409
+ return {
410
+ path: field.name,
411
+ type: field.type || '',
412
+ required: field.required === true,
413
+ description: field.desc || '',
414
+ }
415
+ })
416
+ const lines = ['{']
417
+ form.forEach(function (field, index) {
418
+ const last = index === form.length - 1
419
+ const value = object[field.name]
420
+ const comment = field.desc ? ' // ' + oneLine(field.desc) : ''
421
+ lines.push(' ' + JSON.stringify(field.name) + ': ' + JSON.stringify(value === undefined ? null : value) + (last ? '' : ',') + comment)
422
+ })
423
+ lines.push('}')
424
+ return { text: lines.join('\n'), fields: fields }
425
+ }
426
+
427
+ function countTree(nodes) {
428
+ let total = 0
429
+ for (const node of nodes) total += 1 + countTree(node.children)
430
+ return total
431
+ }
432
+
433
+ function treeChildren(schema, value, basePath) {
434
+ const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {}
435
+ const required = Array.isArray(schema.required) ? schema.required : []
436
+ const object = value && typeof value === 'object' ? value : {}
437
+ return Object.keys(properties).map(function (key) {
438
+ return makeTreeNode(
439
+ key,
440
+ properties[key],
441
+ object[key],
442
+ basePath ? basePath + '.' + key : key,
443
+ required.indexOf(key) !== -1,
444
+ )
445
+ })
446
+ }
447
+
448
+ function makeTreeNode(name, nodeSchema, value, path, required) {
449
+ const schema = unwrapComposite(nodeSchema)
450
+ const rawType = schema.type
451
+ const isArray = rawType === 'array' || (Array.isArray(rawType) && rawType.indexOf('array') !== -1)
452
+ const isObject = Boolean(schema.properties) || rawType === 'object'
453
+ const kind = isArray ? 'array' : isObject ? 'object' : 'scalar'
454
+ const children = []
455
+ if (kind === 'object') {
456
+ children.push.apply(children, treeChildren(schema, value, path))
457
+ } else if (kind === 'array') {
458
+ const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
459
+ const itemSchema = unwrapComposite(items)
460
+ if (itemSchema.properties) {
461
+ const itemValue = Array.isArray(value) && value.length > 0 ? value[0] : {}
462
+ children.push.apply(children, treeChildren(itemSchema, itemValue, path + '[]'))
463
+ }
464
+ }
465
+ return {
466
+ id: path,
467
+ name: name,
468
+ path: path,
469
+ type: typeLabel(schema) || '',
470
+ kind: kind,
471
+ required: required === true,
472
+ description: schemaComment(schema),
473
+ children: children,
474
+ }
475
+ }
476
+
477
+ function buildTreeFromSchema(raw, exampleText) {
478
+ const schema = parseJsonSafe(raw)
479
+ const value = parseJsonSafe(exampleText)
480
+ if (schema === undefined) return { nodes: [], total: 0 }
481
+ const root = unwrapComposite(schema)
482
+ const nodes = treeChildren(root, value && typeof value === 'object' ? value : {}, '')
483
+ return { nodes: nodes, total: countTree(nodes) }
484
+ }
485
+
486
+ function buildTreeFromForm(form, exampleText) {
487
+ const nodes = form.map(function (field) {
488
+ return {
489
+ id: field.name,
490
+ name: field.name,
491
+ path: field.name,
492
+ type: field.type || '',
493
+ kind: 'scalar',
494
+ required: field.required === true,
495
+ description: field.desc || '',
496
+ children: [],
497
+ }
498
+ })
499
+ return { nodes: nodes, total: nodes.length }
500
+ }
501
+
502
+ function fieldTreeFor(doc, kind) {
503
+ const source = kind === 'request' ? doc.request : doc.response
504
+ const body = source.body
505
+ if (!body || body.kind === 'none') return { nodes: [], total: 0 }
506
+ if (kind === 'request' && (source.bodyType === 'form' || (doc.request.form.length > 0 && !source.bodyIsJsonSchema))) {
507
+ return buildTreeFromForm(doc.request.form, body.example)
508
+ }
509
+ if (source.bodyIsJsonSchema) return buildTreeFromSchema(body.raw, body.example)
510
+ return { nodes: [], total: 0 }
511
+ }
512
+
513
+ function annotationFor(doc, kind) {
514
+ const source = kind === 'request' ? doc.request : doc.response
515
+ const body = source.body
516
+ const tree = fieldTreeFor(doc, kind)
517
+ if (!body || body.kind === 'none') return { text: '', fields: [], tree: tree }
518
+ if (kind === 'request' && (source.bodyType === 'form' || (doc.request.form.length > 0 && !source.bodyIsJsonSchema))) {
519
+ const form = annotateFormValue(doc.request.form, body.example)
520
+ form.tree = tree
521
+ return form
522
+ }
523
+ if (source.bodyIsJsonSchema) {
524
+ const schema = annotateSchemaValue(body.raw, body.example)
525
+ schema.tree = tree
526
+ return schema
527
+ }
528
+ return { text: body.example || '', fields: [], tree: tree }
529
+ }
530
+
531
+ const STATUS_LABEL = {
532
+ done: '已完成',
533
+ undone: '开发中',
534
+ design: '设计中',
535
+ deprecated: '已废弃',
536
+ stub: '未完成',
537
+ }
538
+
539
+ /* ------------------------------------------------------------------ */
540
+ /* icons */
541
+ /* ------------------------------------------------------------------ */
542
+
543
+ function svg(size, children) {
544
+ const nodes = children.map(function (child, index) {
545
+ if (child && child.key === null) return React.cloneElement(child, { key: 'n' + index })
546
+ return child
547
+ })
548
+ return h(
549
+ 'svg',
550
+ {
551
+ width: size,
552
+ height: size,
553
+ viewBox: '0 0 24 24',
554
+ fill: 'none',
555
+ 'aria-hidden': 'true',
556
+ focusable: 'false',
557
+ },
558
+ nodes,
559
+ )
560
+ }
561
+
562
+ function strokePath(d, extra) {
563
+ return h('path', Object.assign({
564
+ d: d,
565
+ stroke: 'currentColor',
566
+ strokeWidth: 1.7,
567
+ strokeLinecap: 'round',
568
+ strokeLinejoin: 'round',
569
+ }, extra || {}))
570
+ }
571
+
572
+ function YapiIcon(props) {
573
+ const size = props && props.size ? props.size : 18
574
+ return svg(size, [
575
+ h('path', {
576
+ key: 'doc',
577
+ d: 'M5.5 3.5h8.2L18.5 8v12.5h-13z',
578
+ stroke: 'currentColor',
579
+ strokeWidth: 1.6,
580
+ strokeLinejoin: 'round',
581
+ }),
582
+ h('path', {
583
+ key: 'fold',
584
+ d: 'M13.7 3.5V8h4.8',
585
+ stroke: 'currentColor',
586
+ strokeWidth: 1.6,
587
+ strokeLinejoin: 'round',
588
+ }),
589
+ h('path', {
590
+ key: 'code',
591
+ d: 'M9.4 11.4 7.2 13.6l2.2 2.2M14.6 11.4l2.2 2.2-2.2 2.2',
592
+ stroke: 'currentColor',
593
+ strokeWidth: 1.6,
594
+ strokeLinecap: 'round',
595
+ strokeLinejoin: 'round',
596
+ }),
597
+ ])
598
+ }
599
+
600
+ function SearchIcon(props) {
601
+ const size = props && props.size ? props.size : 16
602
+ return svg(size, [
603
+ h('circle', { key: 'c', cx: 11, cy: 11, r: 6.2, stroke: 'currentColor', strokeWidth: 1.7 }),
604
+ strokePath('m16 16 4 4'),
605
+ ])
606
+ }
607
+
608
+
609
+
610
+
611
+
612
+ function CopyIcon(props) {
613
+ const size = props && props.size ? props.size : 14
614
+ return svg(size, [
615
+ h('rect', { key: 'a', x: 8.5, y: 8.5, width: 11, height: 11, rx: 2.2, stroke: 'currentColor', strokeWidth: 1.6 }),
616
+ strokePath('M6 15.5H5.5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2V6'),
617
+ ])
618
+ }
619
+
620
+ function ExternalIcon(props) {
621
+ const size = props && props.size ? props.size : 13
622
+ return svg(size, [
623
+ strokePath('M13.5 5.5H18.5V10.5'),
624
+ strokePath('M18 6 11.5 12.5'),
625
+ strokePath('M9.5 5.5h-3a1.5 1.5 0 0 0-1.5 1.5v11a1.5 1.5 0 0 0 1.5 1.5h11a1.5 1.5 0 0 0 1.5-1.5v-3'),
626
+ ])
627
+ }
628
+
629
+ function UserIcon(props) {
630
+ const size = props && props.size ? props.size : 14
631
+ return svg(size, [
632
+ h('circle', { key: 'head', cx: 12, cy: 8.2, r: 3.6, stroke: 'currentColor', strokeWidth: 1.7 }),
633
+ strokePath('M4.9 20.1c.9-3.4 3.7-5.4 7.1-5.4s6.2 2 7.1 5.4'),
634
+ ])
635
+ }
636
+
637
+ function CloseIcon(props) {
638
+ const size = props && props.size ? props.size : 16
639
+ return svg(size, [strokePath('M6.5 6.5l11 11M17.5 6.5l-11 11')])
640
+ }
641
+
642
+ /* ------------------------------------------------------------------ */
643
+ /* styles */
644
+ /* ------------------------------------------------------------------ */
645
+
646
+ const CSS = [
647
+ '.yapi-root{min-height:100%;--yapi-accent:var(--dsw-alias-brand-primary,#4d6bfe);--yapi-text:var(--dsw-alias-label-primary,#17181c);--yapi-text-2:var(--dsw-alias-label-secondary,#5b5f6b);--yapi-text-3:var(--dsw-alias-label-tertiary,#8a8f9b);--yapi-bg:var(--dsw-alias-bg-base,#fff);--yapi-surface:var(--dsw-alias-bg-layer-1,#f7f8fa);--yapi-surface-2:var(--dsw-alias-bg-layer-2,#fff);--yapi-line:var(--dsw-alias-border-l1,#eaeaef);--yapi-line-2:var(--dsw-alias-border-l2,#dedee5);height:100%;overflow:auto;background:var(--yapi-bg);color:var(--yapi-text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}',
648
+ '.yapi-inner{max-width:1040px;margin:0 auto;padding:36px 32px 96px;box-sizing:border-box}',
649
+ '.yapi-hero{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:24px}',
650
+ '.yapi-hero-main{min-width:0}',
651
+ '.yapi-hero-side{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:6px}',
652
+ '.yapi-accountstate{max-width:230px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--yapi-text-3)}',
653
+ '.yapi-accountstate[data-set="false"]{color:var(--dsw-alias-state-warn-primary,#b45309)}',
654
+ '.yapi-kicker{display:inline-flex;align-items:center;gap:6px;color:var(--yapi-accent);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}',
655
+ '.yapi-hero h1{margin:8px 0 6px;font-size:26px;line-height:1.25;font-weight:650;letter-spacing:-.02em}',
656
+ '.yapi-hero p{margin:0;color:var(--yapi-text-2);font-size:13.5px}',
657
+ '.yapi-searchwrap{position:relative}',
658
+ '.yapi-search{display:flex;align-items:center;gap:10px;padding:8px 8px 8px 14px;border:1px solid var(--yapi-line-2);border-radius:14px;background:var(--yapi-surface-2);transition:border-color .15s,box-shadow .15s}',
659
+ '.yapi-search:focus-within{border-color:var(--yapi-accent);box-shadow:0 0 0 3px rgba(77,107,254,.12)}',
660
+ '.yapi-search .yapi-search-ico{display:flex;color:var(--yapi-text-3);flex:none}',
661
+ '.yapi-search input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:inherit;font-family:inherit;font-size:14px;padding:6px 0}',
662
+ '.yapi-search input::placeholder{color:var(--yapi-text-3)}',
663
+ '.yapi-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;border:1px solid transparent;border-radius:9px;background:var(--dsw-alias-button-primary-fill,var(--yapi-accent));color:var(--dsw-alias-label-primary-foreground,#fff);font-family:inherit;font-size:13px;font-weight:600;padding:8px 16px;cursor:pointer;white-space:nowrap;transition:filter .15s,opacity .15s}',
664
+ '.yapi-btn:hover:not(:disabled){filter:brightness(1.06)}',
665
+ '.yapi-btn:disabled{opacity:.55;cursor:not-allowed}',
666
+ '.yapi-btn.ghost{background:transparent;color:var(--yapi-text-2);border-color:var(--yapi-line-2)}',
667
+ '.yapi-btn.ghost:hover:not(:disabled){background:var(--yapi-surface);border-color:var(--yapi-line-2);color:var(--yapi-text);filter:none}',
668
+ '.yapi-btn.sm{padding:5px 10px;font-size:12px;border-radius:7px}',
669
+ '.yapi-hint{margin:8px 2px 0;font-size:12px;color:var(--yapi-text-3)}',
670
+ '.yapi-recent{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:14px;font-size:12px;color:var(--yapi-text-3)}',
671
+ '.yapi-chip{appearance:none;border:1px solid var(--yapi-line);background:transparent;color:var(--yapi-text-2);border-radius:999px;padding:3px 10px;font-family:inherit;font-size:12px;cursor:pointer}',
672
+ '.yapi-chip:hover{border-color:var(--yapi-accent);color:var(--yapi-accent)}',
673
+ '.yapi-searchpanel{position:absolute;left:0;right:0;top:calc(100% + 8px);z-index:40;border:1px solid var(--yapi-line-2);border-radius:14px;background:var(--yapi-bg);box-shadow:0 18px 48px rgba(15,20,40,.18);overflow:hidden}',
674
+ '.yapi-searchpanel-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--yapi-line);background:var(--yapi-surface)}',
675
+ '.yapi-searchpanel-label{font-size:11.5px;font-weight:600;color:var(--yapi-text-3)}',
676
+ '.yapi-projectinput{width:70px;box-sizing:border-box;border:1px solid var(--yapi-line);border-radius:7px;background:var(--yapi-surface-2);color:inherit;font-family:inherit;font-size:12px;padding:4px 7px;outline:0}',
677
+ '.yapi-projectinput:focus{border-color:var(--yapi-accent)}',
678
+ '.yapi-searchpanel-meta{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;color:var(--yapi-text-3)}',
679
+ '.yapi-searchpanel-match{font-size:11.5px;font-weight:600;color:var(--yapi-accent)}',
680
+ '.yapi-searchpanel-close{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--yapi-text-3);cursor:pointer;padding:0}',
681
+ '.yapi-searchpanel-close:hover{background:var(--yapi-surface-2);color:var(--yapi-text)}',
682
+ '.yapi-searchpanel-body{max-height:380px;overflow:auto}',
683
+ '.yapi-searchpanel-state{display:flex;align-items:center;gap:8px;padding:18px 14px;font-size:12.5px;color:var(--yapi-text-3)}',
684
+ '.yapi-searchpanel-error{padding:14px;font-size:12.5px;color:var(--dsw-alias-state-error-primary,#dc2626);word-break:break-word}',
685
+ '.yapi-searchitem{display:flex;align-items:center;gap:10px;width:100%;border:0;border-bottom:1px solid var(--yapi-line);background:transparent;color:inherit;text-align:left;padding:10px 12px;cursor:pointer;font-family:inherit}',
686
+ '.yapi-searchitem:last-child{border-bottom:0}',
687
+ '.yapi-searchitem:hover{background:var(--yapi-surface)}',
688
+ '.yapi-searchitem-method{flex:none;width:42px;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:9.5px;font-weight:800;color:var(--yapi-text-3)}',
689
+ '.yapi-searchitem-method[data-method="GET"]{color:var(--dsw-alias-state-success-primary,#0b8a4b)}',
690
+ '.yapi-searchitem-method[data-method="POST"]{color:var(--dsw-alias-brand-primary,#2058d8)}',
691
+ '.yapi-searchitem-method[data-method="PUT"]{color:var(--dsw-alias-state-warn-primary,#b45309)}',
692
+ '.yapi-searchitem-method[data-method="PATCH"]{color:var(--dsw-alias-state-business-primary,#7c3aed)}',
693
+ '.yapi-searchitem-method[data-method="DELETE"]{color:var(--dsw-alias-state-error-primary,#c62828)}',
694
+ '.yapi-searchitem-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}',
695
+ '.yapi-searchitem-main code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:12px;color:var(--yapi-text)}',
696
+ '.yapi-searchitem-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;color:var(--yapi-text-3)}',
697
+ '.yapi-searchitem-id{flex:none;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:11px;color:var(--yapi-text-3)}',
698
+ '.yapi-error{margin-top:22px;border:1px solid rgba(220,38,38,.28);background:rgba(220,38,38,.06);border-radius:12px;padding:14px 16px}',
699
+ '.yapi-error-title{font-size:13px;font-weight:650;color:var(--dsw-alias-state-error-primary,#dc2626);margin-bottom:4px}',
700
+ '.yapi-error-msg{font-size:13px;color:var(--yapi-text);word-break:break-word}',
701
+ '.yapi-error-hint{margin-top:8px;font-size:12px;color:var(--yapi-text-3)}',
702
+ '.yapi-error-action{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}',
703
+ '.yapi-mask{position:fixed;inset:0;z-index:90;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(15,18,25,.45)}',
704
+ '.yapi-modal{width:100%;max-width:460px;max-height:90vh;overflow:auto;border:1px solid var(--yapi-line-2);border-radius:16px;background:var(--yapi-bg);box-shadow:0 24px 70px rgba(0,0,0,.28)}',
705
+ '.yapi-modal-head{display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--yapi-line)}',
706
+ '.yapi-modal-head h3{margin:0;font-size:15px;font-weight:650}',
707
+ '.yapi-modal-close{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:8px;background:transparent;color:var(--yapi-text-3);cursor:pointer}',
708
+ '.yapi-modal-close:hover{background:var(--yapi-surface);color:var(--yapi-text)}',
709
+ '.yapi-modal-body{display:flex;flex-direction:column;gap:12px;padding:16px 18px}',
710
+ '.yapi-modal-state{padding:8px 10px;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface);font-size:12px;color:var(--yapi-text-2)}',
711
+ '.yapi-formrow{display:flex;flex-direction:column;gap:5px}',
712
+ '.yapi-formrow label{font-size:11.5px;font-weight:600;color:var(--yapi-text-3)}',
713
+ '.yapi-formrow input{width:100%;box-sizing:border-box;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface-2);color:inherit;font-family:inherit;font-size:13px;padding:8px 10px;outline:0}',
714
+ '.yapi-formrow input:focus{border-color:var(--yapi-accent)}',
715
+ '.yapi-formhint{font-size:11.5px;line-height:1.6;color:var(--yapi-text-3)}',
716
+ '.yapi-modal-error{border:1px solid rgba(220,38,38,.22);border-radius:8px;background:rgba(220,38,38,.07);padding:8px 10px;font-size:12px;line-height:1.55;color:var(--dsw-alias-state-error-primary,#dc2626);word-break:break-word}',
717
+ '.yapi-copymodal{max-width:520px}',
718
+ '.yapi-copymodal .yapi-modal-head,.yapi-copymodal .yapi-modal-foot{user-select:none;-webkit-user-select:none}',
719
+ '.yapi-copycount{margin-left:8px;font-size:11.5px;color:var(--yapi-text-3);user-select:none;-webkit-user-select:none}',
720
+ '.yapi-copytoggleall{margin-left:8px;border:0;background:transparent;color:var(--yapi-accent);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;padding:4px 6px;white-space:nowrap;user-select:none;-webkit-user-select:none}',
721
+ '.yapi-copytoggleall:hover{text-decoration:underline}',
722
+ '.yapi-checkgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px 10px}',
723
+ '.yapi-check{display:flex;align-items:center;gap:8px;min-width:0;padding:7px 9px;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface-2);font-size:12.5px;color:var(--yapi-text-2);cursor:pointer;user-select:none;-webkit-user-select:none}',
724
+ '.yapi-check:hover{border-color:var(--yapi-line-2);color:var(--yapi-text)}',
725
+ '.yapi-check[data-checked="true"]{border-color:var(--yapi-accent);background:color-mix(in srgb,var(--yapi-accent) 7%,var(--yapi-surface-2));color:var(--yapi-text)}',
726
+ '.yapi-check input{width:14px;height:14px;margin:0;flex:none;accent-color:var(--yapi-accent)}',
727
+ '.yapi-copyformatrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--yapi-text-3);user-select:none;-webkit-user-select:none}',
728
+ '.yapi-copyformat-hint{font-size:11.5px;color:var(--yapi-text-3)}',
729
+ '.yapi-copyhint{font-size:11.5px;line-height:1.6;color:var(--yapi-text-3)}',
730
+ '.yapi-modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--yapi-line);background:var(--yapi-surface)}',
731
+ '.yapi-loading{display:flex;align-items:center;gap:10px;margin-top:26px;color:var(--yapi-text-3);font-size:13px}',
732
+ '.yapi-spinner{width:16px;height:16px;border:2px solid var(--yapi-line-2);border-top-color:var(--yapi-accent);border-radius:50%;animation:yapi-spin .8s linear infinite}',
733
+ '@keyframes yapi-spin{to{transform:rotate(360deg)}}',
734
+ '.yapi-tabbar{display:flex;align-items:center;gap:8px;margin-top:18px;min-width:0}',
735
+ '.yapi-tabbar-list{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow-x:auto;padding-bottom:2px;scrollbar-width:thin}',
736
+ '.yapi-tabbar-list::-webkit-scrollbar{height:6px}',
737
+ '.yapi-tabbar-list::-webkit-scrollbar-thumb{background:var(--yapi-line-2);border-radius:999px}',
738
+ '.yapi-doctab{display:flex;align-items:center;gap:6px;flex:none;max-width:240px;min-width:0;padding:7px 8px 7px 10px;border:1px solid var(--yapi-line);border-radius:9px;background:var(--yapi-surface);color:var(--yapi-text-2);font-size:12.5px;cursor:pointer;user-select:none}',
739
+ '.yapi-doctab:hover{border-color:var(--yapi-line-2);color:var(--yapi-text)}',
740
+ '.yapi-doctab[data-active="true"]{background:var(--yapi-surface-2);border-color:var(--yapi-line-2);color:var(--yapi-text);font-weight:600;box-shadow:0 1px 3px rgba(15,20,40,.06)}',
741
+ '.yapi-doctab-method{flex:none;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:9.5px;font-weight:800;letter-spacing:.04em;color:var(--yapi-text-3)}',
742
+ '.yapi-doctab-method[data-method="GET"]{color:var(--dsw-alias-state-success-primary,#0b8a4b)}',
743
+ '.yapi-doctab-method[data-method="POST"]{color:var(--dsw-alias-brand-primary,#2058d8)}',
744
+ '.yapi-doctab-method[data-method="PUT"]{color:var(--dsw-alias-state-warn-primary,#b45309)}',
745
+ '.yapi-doctab-method[data-method="PATCH"]{color:var(--dsw-alias-state-business-primary,#7c3aed)}',
746
+ '.yapi-doctab-method[data-method="DELETE"]{color:var(--dsw-alias-state-error-primary,#c62828)}',
747
+ '.yapi-doctab-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
748
+ '.yapi-doctab-spin{width:10px;height:10px;flex:none;border:1.5px solid var(--yapi-line-2);border-top-color:var(--yapi-accent);border-radius:50%;animation:yapi-spin .8s linear infinite}',
749
+ '.yapi-doctab-close{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex:none;border:0;border-radius:5px;background:transparent;color:var(--yapi-text-3);cursor:pointer;padding:0}',
750
+ '.yapi-doctab-close:hover{background:var(--yapi-line);color:var(--yapi-text)}',
751
+ '.yapi-doctab-clear{flex:none;border:0;background:transparent;color:var(--yapi-text-3);font-family:inherit;font-size:11.5px;cursor:pointer;padding:4px 2px;white-space:nowrap}',
752
+ '.yapi-doctab-clear:hover{color:var(--yapi-text)}',
753
+ '.yapi-doc{margin-top:24px;border:1px solid var(--yapi-line);border-radius:18px;background:var(--yapi-surface);overflow:hidden}',
754
+ '.yapi-doc-head{padding:22px 24px 18px;border-bottom:1px solid var(--yapi-line)}',
755
+ '.yapi-titlebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
756
+ '.yapi-titlebar h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.01em;line-height:1.35}',
757
+ '.yapi-method{font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:11px;font-weight:800;letter-spacing:.06em;border-radius:6px;padding:3px 8px;flex:none;background:var(--yapi-surface)}',
758
+ '.yapi-method[data-method="GET"]{color:var(--dsw-alias-state-success-primary,#0b8a4b);background:color-mix(in srgb,currentColor 15%,transparent)}',
759
+ '.yapi-method[data-method="POST"]{color:var(--dsw-alias-brand-primary,#2058d8);background:color-mix(in srgb,currentColor 15%,transparent)}',
760
+ '.yapi-method[data-method="PUT"]{color:var(--dsw-alias-state-warn-primary,#b45309);background:color-mix(in srgb,currentColor 16%,transparent)}',
761
+ '.yapi-method[data-method="PATCH"]{color:var(--dsw-alias-state-business-primary,#7c3aed);background:color-mix(in srgb,currentColor 15%,transparent)}',
762
+ '.yapi-method[data-method="DELETE"]{color:var(--dsw-alias-state-error-primary,#c62828);background:color-mix(in srgb,currentColor 15%,transparent)}',
763
+ '.yapi-method[data-method="HEAD"],.yapi-method[data-method="OPTIONS"]{color:var(--yapi-text-3);background:var(--yapi-surface)}',
764
+ '.yapi-status{font-size:11px;font-weight:600;border:1px solid currentColor;border-radius:999px;padding:2px 9px;flex:none}',
765
+ '.yapi-status[data-status="done"]{color:var(--dsw-alias-state-success-primary,#0b8a4b)}',
766
+ '.yapi-status[data-status="undone"]{color:var(--dsw-alias-state-warn-primary,#b45309)}',
767
+ '.yapi-status[data-status="deprecated"]{color:var(--dsw-alias-state-error-primary,#c62828)}',
768
+ '.yapi-status[data-status="design"]{color:var(--dsw-alias-brand-primary,#2058d8)}',
769
+ '.yapi-status[data-status="stub"]{color:var(--yapi-text-3)}',
770
+ '.yapi-openlink{display:inline-flex;align-items:center;gap:4px;margin-left:auto;color:var(--yapi-text-3);text-decoration:none;font-size:12px;border:1px solid var(--yapi-line);border-radius:7px;padding:4px 9px;white-space:nowrap}',
771
+ '.yapi-openlink:hover{color:var(--yapi-accent);border-color:var(--yapi-accent)}',
772
+ '.yapi-titlebar-actions{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
773
+ '.yapi-titlebar-actions .yapi-openlink{margin-left:0}',
774
+ '.yapi-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:9px;font-size:12.5px;color:var(--yapi-text-3)}',
775
+ '.yapi-dot{opacity:.55}',
776
+ '.yapi-addrs{display:grid;grid-template-columns:1.1fr 1.4fr;gap:12px;padding:0 24px 12px}',
777
+ '.yapi-field{display:flex;flex-direction:column;gap:6px;min-width:0}',
778
+ '.yapi-field label{font-size:11.5px;font-weight:600;color:var(--yapi-text-3)}',
779
+ '.yapi-field input{width:100%;box-sizing:border-box;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface-2);color:inherit;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:12.5px;padding:7px 10px;outline:0}',
780
+ '.yapi-field input:focus{border-color:var(--yapi-accent)}',
781
+ '.yapi-pathstatic{width:100%;box-sizing:border-box;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface-2);padding:7px 10px;min-width:0;overflow:hidden}',
782
+ '.yapi-pathstatic code{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:12.5px;color:var(--yapi-text)}',
783
+ '.yapi-addrpreview{margin:0 24px 6px;font-size:11.5px;color:var(--yapi-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
784
+ '.yapi-addrpreview code{font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);color:var(--yapi-text-2)}',
785
+ '.yapi-tabs{display:flex;gap:6px;flex-wrap:wrap;padding:10px 18px;margin-top:12px;border-top:1px solid var(--yapi-line)}',
786
+ '.yapi-tab{appearance:none;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--yapi-text-3);font-family:inherit;font-size:13px;font-weight:500;padding:7px 13px;cursor:pointer}',
787
+ '.yapi-tab:hover{color:var(--yapi-text)}',
788
+ '.yapi-tab[data-active="true"]{background:var(--yapi-surface-2);border-color:var(--yapi-line);color:var(--yapi-text);font-weight:600}',
789
+ '.yapi-tabbody{padding:18px 24px 24px;border-top:1px solid var(--yapi-line);background:var(--yapi-surface-2)}',
790
+ '.yapi-seg{display:inline-flex;border:1px solid var(--yapi-line);border-radius:8px;padding:2px;background:var(--yapi-surface)}',
791
+ '.yapi-seg button{appearance:none;border:0;background:transparent;color:var(--yapi-text-3);font-family:inherit;font-size:12px;padding:3px 9px;border-radius:6px;cursor:pointer}',
792
+ '.yapi-seg button[data-active="true"]{background:var(--yapi-surface-2);color:var(--yapi-text);font-weight:600;box-shadow:0 1px 2px rgba(15,20,40,.09)}',
793
+ '.yapi-fieldpanel{display:flex;flex-direction:column;gap:10px}',
794
+ '.yapi-fieldbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
795
+ '.yapi-fieldbar-left{display:flex;align-items:center;gap:8px}',
796
+ '.yapi-fieldbar-title{font-size:13px;font-weight:600}',
797
+ '.yapi-fieldmatch{font-size:11.5px;font-weight:600;color:var(--yapi-accent)}',
798
+ '.yapi-fieldbar-right{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}',
799
+ '.yapi-fieldsearch{width:220px;max-width:100%;box-sizing:border-box;border:1px solid var(--yapi-line);border-radius:8px;background:var(--yapi-surface-2);color:inherit;font-family:inherit;font-size:12px;padding:6px 10px;outline:0}',
800
+ '.yapi-fieldsearch:focus{border-color:var(--yapi-accent)}',
801
+ '.yapi-tree{border:1px solid var(--yapi-line);border-radius:12px;overflow:hidden;background:var(--yapi-surface-2)}',
802
+ '.yapi-treehead{display:grid;grid-template-columns:minmax(0,1.4fr) 118px 62px minmax(0,1.6fr);gap:10px;padding:8px 12px;background:var(--yapi-surface);border-bottom:1px solid var(--yapi-line);font-size:11.5px;font-weight:600;color:var(--yapi-text-3)}',
803
+ '.yapi-treehead span:first-child{padding-left:30px}',
804
+ '.yapi-treebody{max-height:540px;overflow:auto}',
805
+ '.yapi-trow{display:grid;grid-template-columns:minmax(0,1.4fr) 118px 62px minmax(0,1.6fr);gap:10px;padding:6px 12px;border-bottom:1px solid var(--yapi-line);align-items:start}',
806
+ '.yapi-trow:last-child{border-bottom:0}',
807
+ '.yapi-trow:hover{background:var(--yapi-surface)}',
808
+ '.yapi-tcell{min-width:0;font-size:12.5px;line-height:1.5}',
809
+ '.yapi-tname{display:flex;align-items:center;gap:6px}',
810
+ '.yapi-tname code{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:12px;color:var(--yapi-text)}',
811
+ '.yapi-tchev{width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;flex:none;border:0;border-radius:4px;background:transparent;color:var(--yapi-text-3);cursor:pointer;padding:0}',
812
+ '.yapi-tchev:hover{background:var(--yapi-line);color:var(--yapi-text)}',
813
+ '.yapi-tchev svg{transition:transform .15s}',
814
+ '.yapi-tchev[data-open="true"] svg{transform:rotate(90deg)}',
815
+ '.yapi-tchev-empty{pointer-events:none}',
816
+ '.yapi-tcount{flex:none;margin-left:2px;font-size:10px;font-weight:600;color:var(--yapi-text-3);border:1px solid var(--yapi-line);border-radius:999px;padding:0 5px}',
817
+ '.yapi-ttype{font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:11.5px;color:var(--yapi-text-2);word-break:break-word}',
818
+ '.yapi-treq{white-space:nowrap}',
819
+ '.yapi-tdesc{color:var(--yapi-text-2);word-break:break-word}',
820
+ '.yapi-section{margin-bottom:22px}',
821
+ '.yapi-section:last-child{margin-bottom:0}',
822
+ '.yapi-section h3{display:flex;align-items:center;gap:8px;margin:0 0 10px;font-size:13px;font-weight:600}',
823
+ '.yapi-count{font-size:11px;font-weight:600;color:var(--yapi-text-3);background:var(--yapi-surface);border:1px solid var(--yapi-line);border-radius:999px;padding:0 7px}',
824
+ '.yapi-tablewrap{border:1px solid var(--yapi-line);border-radius:10px;overflow:auto;background:var(--yapi-surface-2)}',
825
+ '.yapi-table{width:100%;min-width:560px;border-collapse:collapse;font-size:12.5px}',
826
+ '.yapi-table th{text-align:left;font-size:11.5px;font-weight:600;color:var(--yapi-text-3);background:var(--yapi-surface);padding:8px 12px;border-bottom:1px solid var(--yapi-line);white-space:nowrap}',
827
+ '.yapi-table td{padding:8px 12px;border-bottom:1px solid var(--yapi-line);vertical-align:top;color:var(--yapi-text-2);word-break:break-word}',
828
+ '.yapi-table tr:last-child td{border-bottom:0}',
829
+ '.yapi-table td.mono{font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:12px;color:var(--yapi-text)}',
830
+ '.yapi-req{font-size:11.5px;font-weight:600;color:var(--dsw-alias-state-error-primary,#dc2626)}',
831
+ '.yapi-opt{font-size:11.5px;color:var(--yapi-text-3)}',
832
+ '.yapi-empty{padding:40px 0;text-align:center;font-size:13px;color:var(--yapi-text-3)}',
833
+ '.yapi-desc{white-space:pre-wrap;font-size:13.5px;line-height:1.75;color:var(--yapi-text-2)}',
834
+ '.yapi-toast{position:fixed;left:50%;bottom:36px;z-index:80;transform:translateX(-50%);padding:8px 18px;border:1px solid var(--yapi-line-2);border-radius:999px;background:var(--dsw-alias-bg-layer-3,#2a2d34);color:var(--dsw-alias-label-primary,#fff);font-size:13px;box-shadow:0 10px 30px rgba(0,0,0,.2);animation:yapi-rise .18s ease-out;pointer-events:none}',
835
+ '@keyframes yapi-rise{from{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%,0)}}',
836
+ '@media (max-width:820px){.yapi-addrs{grid-template-columns:1fr}}',
837
+ '@media (max-width:720px){.yapi-inner{padding:24px 18px 64px}.yapi-doc-head{padding:18px 16px 14px}.yapi-addrs{padding:0 16px 12px}.yapi-addrpreview{margin:0 16px 6px}.yapi-tabbody{padding:16px 16px 22px}.yapi-tabs{padding:6px 10px 0}}',
838
+ '@media (max-width:560px){.yapi-checkgrid{grid-template-columns:1fr}}',
839
+ '@media (prefers-reduced-motion:reduce){.yapi-copy,.yapi-search,.yapi-btn{transition:none}.yapi-spinner{animation:none}.yapi-toast{animation:none}}',
840
+ ].join('\n')
841
+
842
+ function ensureStyle() {
843
+ if (typeof document === 'undefined') return
844
+ if (document.getElementById(STYLE_ID)) return
845
+ const style = document.createElement('style')
846
+ style.id = STYLE_ID
847
+ style.textContent = CSS
848
+ document.head.appendChild(style)
849
+ }
850
+
851
+ /* ------------------------------------------------------------------ */
852
+ /* small view pieces */
853
+ /* ------------------------------------------------------------------ */
854
+
855
+ function ParamSection(props) {
856
+ const rows = props.rows || []
857
+ if (rows.length === 0) return null
858
+ const header = props.kind === 'header'
859
+ const columns = header
860
+ ? [
861
+ { key: 'name', label: '名称', className: 'mono' },
862
+ { key: 'value', label: '值', className: 'mono' },
863
+ { key: 'required', label: '必填' },
864
+ { key: 'desc', label: '说明' },
865
+ ]
866
+ : [
867
+ { key: 'name', label: '名称', className: 'mono' },
868
+ { key: 'type', label: '类型' },
869
+ { key: 'required', label: '必填' },
870
+ { key: 'example', label: '示例' },
871
+ { key: 'desc', label: '说明' },
872
+ ]
873
+ return h(
874
+ 'div',
875
+ { className: 'yapi-section' },
876
+ h('h3', null, props.title, h('span', { className: 'yapi-count' }, rows.length)),
877
+ h(
878
+ 'div',
879
+ { className: 'yapi-tablewrap' },
880
+ h(
881
+ 'table',
882
+ { className: 'yapi-table' },
883
+ h('thead', null, h('tr', null, columns.map((column) => h('th', { key: column.key }, column.label)))),
884
+ h(
885
+ 'tbody',
886
+ null,
887
+ rows.map((row, index) =>
888
+ h(
889
+ 'tr',
890
+ { key: row.name + '-' + index },
891
+ columns.map((column) => {
892
+ if (column.key === 'required') {
893
+ return h(
894
+ 'td',
895
+ { key: column.key },
896
+ row.required
897
+ ? h('span', { className: 'yapi-req' }, '必填')
898
+ : h('span', { className: 'yapi-opt' }, '可选'),
899
+ )
900
+ }
901
+ const value = row[column.key]
902
+ return h(
903
+ 'td',
904
+ { key: column.key, className: column.className || '' },
905
+ value === undefined || value === null || value === '' ? '—' : String(value),
906
+ )
907
+ }),
908
+ ),
909
+ ),
910
+ ),
911
+ ),
912
+ ),
913
+ )
914
+ }
915
+
916
+ function ChevronIcon(props) {
917
+ const size = props && props.size ? props.size : 12
918
+ return svg(size, [strokePath('m9 6 6 6-6 6')])
919
+ }
920
+
921
+ function initialExpanded(nodes) {
922
+ const set = new Set()
923
+ for (const node of nodes) {
924
+ if (node.children.length > 0) set.add(node.id)
925
+ }
926
+ return set
927
+ }
928
+
929
+ function collectExpandable(nodes, out) {
930
+ for (const node of nodes) {
931
+ if (node.children.length > 0) {
932
+ out.push(node.id)
933
+ collectExpandable(node.children, out)
934
+ }
935
+ }
936
+ return out
937
+ }
938
+
939
+ function filterTree(nodes, keyword) {
940
+ const kept = []
941
+ for (const node of nodes) {
942
+ const children = filterTree(node.children, keyword)
943
+ const haystack = (node.name + ' ' + node.path + ' ' + node.type + ' ' + node.description).toLowerCase()
944
+ if (haystack.indexOf(keyword) !== -1 || children.length > 0) {
945
+ kept.push(Object.assign({}, node, { children: children }))
946
+ }
947
+ }
948
+ return kept
949
+ }
950
+
951
+ function countMatches(nodes, keyword) {
952
+ let total = 0
953
+ for (const node of nodes) {
954
+ const haystack = (node.name + ' ' + node.path + ' ' + node.type + ' ' + node.description).toLowerCase()
955
+ if (haystack.indexOf(keyword) !== -1) total += 1
956
+ total += countMatches(node.children, keyword)
957
+ }
958
+ return total
959
+ }
960
+
961
+ function flattenTree(nodes, expanded, depth, out) {
962
+ for (const node of nodes) {
963
+ out.push({ node: node, depth: depth })
964
+ if (node.children.length > 0 && expanded.has(node.id)) {
965
+ flattenTree(node.children, expanded, depth + 1, out)
966
+ }
967
+ }
968
+ return out
969
+ }
970
+
971
+ function FieldPanel(props) {
972
+ const tree = props.tree || { nodes: [], total: 0 }
973
+ const nodes = tree.nodes
974
+ const [query, setQuery] = useState('')
975
+ const [expanded, setExpanded] = useState(function () {
976
+ return initialExpanded(nodes)
977
+ })
978
+
979
+ useEffect(
980
+ function () {
981
+ setExpanded(initialExpanded(nodes))
982
+ setQuery('')
983
+ },
984
+ [nodes],
985
+ )
986
+
987
+ const keyword = query.trim().toLowerCase()
988
+ const filtered = keyword === '' ? null : filterTree(nodes, keyword)
989
+ const visible = filtered === null ? nodes : filtered
990
+ const visibleExpanded = filtered === null ? expanded : new Set(collectExpandable(visible, []))
991
+ const rows = flattenTree(visible, visibleExpanded, 0, [])
992
+ const expandable = collectExpandable(nodes, [])
993
+ const allExpanded =
994
+ expandable.length > 0 &&
995
+ expandable.every(function (id) {
996
+ return expanded.has(id)
997
+ })
998
+ const interactive = filtered === null
999
+
1000
+ function toggle(node) {
1001
+ setExpanded(function (previous) {
1002
+ const next = new Set(previous)
1003
+ if (next.has(node.id)) next.delete(node.id)
1004
+ else next.add(node.id)
1005
+ return next
1006
+ })
1007
+ }
1008
+
1009
+ function toggleAll() {
1010
+ setExpanded(allExpanded ? new Set() : new Set(expandable))
1011
+ }
1012
+
1013
+ return h(
1014
+ 'div',
1015
+ { className: 'yapi-fieldpanel' },
1016
+ h(
1017
+ 'div',
1018
+ { className: 'yapi-fieldbar' },
1019
+ h(
1020
+ 'div',
1021
+ { className: 'yapi-fieldbar-left' },
1022
+ h('span', { className: 'yapi-fieldbar-title' }, props.title),
1023
+ h('span', { className: 'yapi-count' }, tree.total),
1024
+ filtered !== null ? h('span', { className: 'yapi-fieldmatch' }, '匹配 ' + countMatches(filtered, keyword)) : null,
1025
+ ),
1026
+ h(
1027
+ 'div',
1028
+ { className: 'yapi-fieldbar-right' },
1029
+ h('input', {
1030
+ className: 'yapi-fieldsearch',
1031
+ value: query,
1032
+ placeholder: '筛选字段 / 类型 / 备注…',
1033
+ spellCheck: false,
1034
+ onChange: function (event) {
1035
+ setQuery(event.target.value)
1036
+ },
1037
+ }),
1038
+ h(
1039
+ 'button',
1040
+ {
1041
+ type: 'button',
1042
+ className: 'yapi-btn ghost sm',
1043
+ disabled: expandable.length === 0 || filtered !== null,
1044
+ title: filtered !== null ? '筛选时自动展开全部匹配项' : '展开或收起全部层级',
1045
+ onClick: toggleAll,
1046
+ },
1047
+ allExpanded ? '收起全部' : '展开全部',
1048
+ ),
1049
+ ),
1050
+ ),
1051
+ rows.length === 0
1052
+ ? h('div', { className: 'yapi-empty' }, keyword !== '' ? '没有匹配的字段' : '该接口没有提供字段说明')
1053
+ : h(
1054
+ 'div',
1055
+ { className: 'yapi-tree' },
1056
+ h(
1057
+ 'div',
1058
+ { className: 'yapi-treehead' },
1059
+ h('span', null, '字段'),
1060
+ h('span', null, '类型'),
1061
+ h('span', null, '必填'),
1062
+ h('span', null, '说明'),
1063
+ ),
1064
+ h(
1065
+ 'div',
1066
+ { className: 'yapi-treebody' },
1067
+ rows.map(function (row, index) {
1068
+ const node = row.node
1069
+ const open = visibleExpanded.has(node.id)
1070
+ const hasChildren = node.children.length > 0
1071
+ return h(
1072
+ 'div',
1073
+ { key: node.id + '-' + index, className: 'yapi-trow' },
1074
+ h(
1075
+ 'div',
1076
+ { className: 'yapi-tcell yapi-tname', style: { paddingLeft: 8 + row.depth * 16 + 'px' } },
1077
+ hasChildren
1078
+ ? interactive
1079
+ ? h(
1080
+ 'button',
1081
+ {
1082
+ type: 'button',
1083
+ className: 'yapi-tchev',
1084
+ 'data-open': open ? 'true' : 'false',
1085
+ 'aria-label': open ? '收起' : '展开',
1086
+ onClick: function () {
1087
+ toggle(node)
1088
+ },
1089
+ },
1090
+ h(ChevronIcon, { size: 11 }),
1091
+ )
1092
+ : h(
1093
+ 'span',
1094
+ { className: 'yapi-tchev', 'data-open': 'true' },
1095
+ h(ChevronIcon, { size: 11 }),
1096
+ )
1097
+ : h('span', { className: 'yapi-tchev yapi-tchev-empty' }),
1098
+ h('code', { title: node.path }, node.name),
1099
+ hasChildren ? h('span', { className: 'yapi-tcount' }, node.children.length) : null,
1100
+ ),
1101
+ h('div', { className: 'yapi-tcell yapi-ttype' }, node.type || '—'),
1102
+ h(
1103
+ 'div',
1104
+ { className: 'yapi-tcell yapi-treq' },
1105
+ node.required
1106
+ ? h('span', { className: 'yapi-req' }, '必填')
1107
+ : h('span', { className: 'yapi-opt' }, '可选'),
1108
+ ),
1109
+ h('div', { className: 'yapi-tcell yapi-tdesc' }, node.description || '—'),
1110
+ )
1111
+ }),
1112
+ ),
1113
+ ),
1114
+ )
1115
+ }
1116
+
1117
+ /* ------------------------------------------------------------------ */
1118
+ /* panel */
1119
+ /* ------------------------------------------------------------------ */
1120
+
1121
+ function YapiPanel() {
1122
+ const [input, setInput] = useState(function () {
1123
+ return readStorage(LAST_KEY, '')
1124
+ })
1125
+ const [tabs, setTabs] = useState(function () {
1126
+ return ensureTabs().tabs
1127
+ })
1128
+ const [activeId, setActiveId] = useState(function () {
1129
+ return ensureTabs().activeId
1130
+ })
1131
+ const [tab, setTab] = useState('params')
1132
+ const [copyFormat, setCopyFormat] = useState(function () {
1133
+ return readStorage(COPY_FORMAT_KEY, 'annotated') === 'json' ? 'json' : 'annotated'
1134
+ })
1135
+ const [base, setBase] = useState(function () {
1136
+ const cache = ensureTabs()
1137
+ const active = cache.tabs.find((item) => item.id === cache.activeId)
1138
+ return active && active.doc && active.doc.project.envs.length > 0 ? active.doc.project.envs[0].domain : ''
1139
+ })
1140
+ const [recent, setRecent] = useState(readRecent)
1141
+ const [toast, setToast] = useState(null)
1142
+ const [copyOpen, setCopyOpen] = useState(false)
1143
+ const [copyOptions, setCopyOptions] = useState(readCopyOptions)
1144
+ const [search, setSearch] = useState({
1145
+ open: false,
1146
+ keyword: '',
1147
+ loading: false,
1148
+ error: '',
1149
+ items: [],
1150
+ total: 0,
1151
+ projectName: '',
1152
+ })
1153
+ const [searchProject, setSearchProject] = useState(function () {
1154
+ return readStorage(PROJECT_KEY, '')
1155
+ })
1156
+ const [account, setAccount] = useState(null)
1157
+ const [accountOpen, setAccountOpen] = useState(false)
1158
+ const [accountForm, setAccountForm] = useState({ baseUrl: '', email: '', password: '' })
1159
+ const [accountSaving, setAccountSaving] = useState(false)
1160
+ const [accountError, setAccountError] = useState('')
1161
+
1162
+ const tabsRef = useRef(tabs)
1163
+ const toastTimer = useRef(0)
1164
+ const searchTimer = useRef(0)
1165
+ const searchSeq = useRef(0)
1166
+ const searchWrapRef = useRef(null)
1167
+ const autoTabbed = useRef(false)
1168
+
1169
+ const activeTab = tabs.find((item) => item.id === activeId) || null
1170
+ const doc = activeTab && activeTab.doc ? activeTab.doc : null
1171
+ const loading = activeTab ? activeTab.loading : false
1172
+ const error = activeTab ? activeTab.error : null
1173
+
1174
+ useEffect(function () {
1175
+ return function () {
1176
+ clearTimeout(toastTimer.current)
1177
+ clearTimeout(searchTimer.current)
1178
+ }
1179
+ }, [])
1180
+
1181
+ useEffect(
1182
+ function () {
1183
+ if (!doc) return
1184
+ setBase(doc.project.envs.length > 0 ? doc.project.envs[0].domain : '')
1185
+ },
1186
+ [doc],
1187
+ )
1188
+
1189
+ useEffect(
1190
+ function () {
1191
+ setCopyOpen(false)
1192
+ },
1193
+ [activeId],
1194
+ )
1195
+
1196
+ useEffect(
1197
+ function () {
1198
+ tabsRef.current = tabs
1199
+ TABS_CACHE.tabs = tabs
1200
+ TABS_CACHE.activeId = activeId
1201
+ writeStorage(
1202
+ TABS_KEY,
1203
+ JSON.stringify(
1204
+ tabs.map(function (item) {
1205
+ return { id: item.id, title: item.title, method: item.method, path: item.path }
1206
+ }),
1207
+ ),
1208
+ )
1209
+ writeStorage(ACTIVE_KEY, activeId === null ? '' : String(activeId))
1210
+ },
1211
+ [tabs, activeId],
1212
+ )
1213
+
1214
+ useEffect(
1215
+ function () {
1216
+ if (tabsRef.current.length === 0) {
1217
+ const last = readStorage(LAST_KEY, '')
1218
+ if (last) openDoc(last)
1219
+ return
1220
+ }
1221
+ const active = tabsRef.current.find((item) => item.id === activeId)
1222
+ if (active && !active.doc && !active.loading) fetchDoc(active.id)
1223
+ },
1224
+ [],
1225
+ )
1226
+
1227
+ useEffect(function () {
1228
+ fetch('/api/yapi/settings')
1229
+ .then(function (response) {
1230
+ return response.json().catch(function () {
1231
+ return null
1232
+ })
1233
+ })
1234
+ .then(function (payload) {
1235
+ if (payload && payload.ok) setAccount(payload)
1236
+ })
1237
+ .catch(function () {})
1238
+ }, [])
1239
+
1240
+ useEffect(
1241
+ function () {
1242
+ if (!accountOpen && !search.open && !copyOpen) return undefined
1243
+ function handleKey(event) {
1244
+ if (event.key !== 'Escape') return
1245
+ setAccountOpen(false)
1246
+ setCopyOpen(false)
1247
+ setSearch(function (previous) {
1248
+ return Object.assign({}, previous, { open: false })
1249
+ })
1250
+ }
1251
+ window.addEventListener('keydown', handleKey)
1252
+ return function () {
1253
+ window.removeEventListener('keydown', handleKey)
1254
+ }
1255
+ },
1256
+ [accountOpen, search.open, copyOpen],
1257
+ )
1258
+
1259
+ useEffect(
1260
+ function () {
1261
+ if (!search.open) return undefined
1262
+ function handlePointer(event) {
1263
+ if (searchWrapRef.current && !searchWrapRef.current.contains(event.target)) {
1264
+ setSearch(function (previous) {
1265
+ return Object.assign({}, previous, { open: false })
1266
+ })
1267
+ }
1268
+ }
1269
+ document.addEventListener('mousedown', handlePointer)
1270
+ return function () {
1271
+ document.removeEventListener('mousedown', handlePointer)
1272
+ }
1273
+ },
1274
+ [search.open],
1275
+ )
1276
+
1277
+ function commitTabs(next) {
1278
+ tabsRef.current = next
1279
+ setTabs(next)
1280
+ }
1281
+
1282
+ function updateTabs(updater) {
1283
+ const next = updater(tabsRef.current)
1284
+ commitTabs(next)
1285
+ return next
1286
+ }
1287
+
1288
+ function selectTab(id) {
1289
+ setActiveId(id)
1290
+ const target = tabsRef.current.find((item) => item.id === id)
1291
+ if (target && !target.doc && !target.loading && !target.error) fetchDoc(id)
1292
+ }
1293
+
1294
+ function closeTab(id) {
1295
+ const currentTabs = tabsRef.current
1296
+ const index = currentTabs.findIndex((item) => item.id === id)
1297
+ const next = currentTabs.filter((item) => item.id !== id)
1298
+ commitTabs(next)
1299
+ if (next.length === 0) writeStorage(LAST_KEY, '')
1300
+ if (activeId === id) {
1301
+ const fallback = next[Math.max(0, index - 1)] || next[0] || null
1302
+ setActiveId(fallback ? fallback.id : null)
1303
+ if (fallback && !fallback.doc && !fallback.loading && !fallback.error) fetchDoc(fallback.id)
1304
+ }
1305
+ }
1306
+
1307
+ function closeAll() {
1308
+ commitTabs([])
1309
+ setActiveId(null)
1310
+ writeStorage(LAST_KEY, '')
1311
+ setSearch(function (previous) {
1312
+ return Object.assign({}, previous, { open: false })
1313
+ })
1314
+ }
1315
+
1316
+ function openDoc(raw, meta, options) {
1317
+ const id = parseDocId(raw)
1318
+ if (id === null) return false
1319
+ const keepInput = Boolean(options && options.keepInput)
1320
+ if (!keepInput) {
1321
+ setInput(String(id))
1322
+ writeStorage(LAST_KEY, String(id))
1323
+ }
1324
+ setActiveId(id)
1325
+ const existing = tabsRef.current.find((item) => item.id === id)
1326
+ if (existing) {
1327
+ if (!existing.doc && !existing.loading) fetchDoc(id)
1328
+ return true
1329
+ }
1330
+ const label = meta && typeof meta === 'object' ? meta : {}
1331
+ updateTabs(function (previous) {
1332
+ return previous.concat([
1333
+ {
1334
+ id: id,
1335
+ title: String(label.title || ''),
1336
+ method: String(label.method || ''),
1337
+ path: String(label.path || ''),
1338
+ doc: null,
1339
+ loading: true,
1340
+ error: null,
1341
+ base: '',
1342
+ },
1343
+ ])
1344
+ })
1345
+ fetchDoc(id)
1346
+ return true
1347
+ }
1348
+
1349
+ function fetchDoc(id) {
1350
+ updateTabs(function (previous) {
1351
+ return previous.map(function (item) {
1352
+ return item.id === id ? Object.assign({}, item, { loading: true, error: null }) : item
1353
+ })
1354
+ })
1355
+ fetch('/api/yapi/interface', {
1356
+ method: 'POST',
1357
+ headers: { 'content-type': 'application/json' },
1358
+ body: JSON.stringify({ id: id }),
1359
+ })
1360
+ .then(function (response) {
1361
+ return response
1362
+ .json()
1363
+ .catch(function () {
1364
+ return null
1365
+ })
1366
+ .then(function (payload) {
1367
+ if (!payload) {
1368
+ if (response.status === 401) throw new Error('DSH 登录已失效,请刷新页面后重试。')
1369
+ throw new Error('接口返回了非 JSON 响应(HTTP ' + response.status + ')')
1370
+ }
1371
+ if (!payload.ok) {
1372
+ const failure = new Error((payload.error && payload.error.message) || '获取 YAPI 文档失败')
1373
+ failure.code = (payload.error && payload.error.code) || ''
1374
+ throw failure
1375
+ }
1376
+ const loaded = payload.data
1377
+ updateTabs(function (previous) {
1378
+ return previous.map(function (item) {
1379
+ return item.id === id
1380
+ ? Object.assign({}, item, {
1381
+ doc: loaded,
1382
+ title: loaded.title,
1383
+ method: loaded.method,
1384
+ path: loaded.path,
1385
+ base: loaded.project.envs.length > 0 ? loaded.project.envs[0].domain : '',
1386
+ loading: false,
1387
+ error: null,
1388
+ })
1389
+ : item
1390
+ })
1391
+ })
1392
+ addRecent(id)
1393
+ if (!autoTabbed.current) {
1394
+ autoTabbed.current = true
1395
+ const hasBody = loaded.request.body && loaded.request.body.kind !== 'none'
1396
+ const hasParams =
1397
+ loaded.request.query.length + loaded.request.params.length + loaded.request.form.length > 0
1398
+ setTab(hasBody ? 'request' : hasParams ? 'params' : 'response')
1399
+ }
1400
+ if (loaded.project && Number.isSafeInteger(loaded.project.id)) {
1401
+ const project = String(loaded.project.id)
1402
+ setSearchProject(project)
1403
+ writeStorage(PROJECT_KEY, project)
1404
+ }
1405
+ })
1406
+ })
1407
+ .catch(function (failure) {
1408
+ updateTabs(function (previous) {
1409
+ return previous.map(function (item) {
1410
+ return item.id === id
1411
+ ? Object.assign({}, item, {
1412
+ loading: false,
1413
+ error: {
1414
+ message: (failure && failure.message) || String(failure),
1415
+ code: (failure && failure.code) || '',
1416
+ },
1417
+ })
1418
+ : item
1419
+ })
1420
+ })
1421
+ })
1422
+ }
1423
+
1424
+ function addRecent(id) {
1425
+ setRecent(function (previous) {
1426
+ const next = [id].concat(previous.filter((item) => item !== id)).slice(0, 6)
1427
+ writeStorage(RECENT_KEY, JSON.stringify(next))
1428
+ return next
1429
+ })
1430
+ }
1431
+
1432
+ function showToast(message) {
1433
+ setToast(message)
1434
+ clearTimeout(toastTimer.current)
1435
+ toastTimer.current = window.setTimeout(function () {
1436
+ setToast(null)
1437
+ }, 1900)
1438
+ }
1439
+
1440
+ function changeCopyFormat(next) {
1441
+ setCopyFormat(next)
1442
+ writeStorage(COPY_FORMAT_KEY, next)
1443
+ }
1444
+
1445
+ function updateAccountForm(patch) {
1446
+ setAccountForm(function (previous) {
1447
+ return Object.assign({}, previous, patch)
1448
+ })
1449
+ }
1450
+
1451
+ function openAccount() {
1452
+ setAccountOpen(true)
1453
+ setAccountError('')
1454
+ fetch('/api/yapi/settings')
1455
+ .then(function (response) {
1456
+ return response.json().catch(function () {
1457
+ return null
1458
+ })
1459
+ })
1460
+ .then(function (payload) {
1461
+ if (!payload || !payload.ok) {
1462
+ throw new Error((payload && payload.error && payload.error.message) || '读取账号设置失败')
1463
+ }
1464
+ setAccount(payload)
1465
+ setAccountForm({ baseUrl: payload.baseUrl || '', email: payload.email || '', password: '' })
1466
+ })
1467
+ .catch(function (error) {
1468
+ setAccountError((error && error.message) || String(error))
1469
+ })
1470
+ }
1471
+
1472
+ function saveAccount() {
1473
+ if (accountSaving) return
1474
+ setAccountSaving(true)
1475
+ setAccountError('')
1476
+ fetch('/api/yapi/settings', {
1477
+ method: 'POST',
1478
+ headers: { 'content-type': 'application/json' },
1479
+ body: JSON.stringify(accountForm),
1480
+ })
1481
+ .then(function (response) {
1482
+ return response.json().catch(function () {
1483
+ return null
1484
+ })
1485
+ })
1486
+ .then(function (payload) {
1487
+ if (!payload) throw new Error('宿主返回了非 JSON 响应')
1488
+ if (!payload.ok) throw new Error((payload.error && payload.error.message) || '保存失败')
1489
+ setAccount(payload)
1490
+ setAccountForm({ baseUrl: payload.baseUrl || '', email: payload.email || '', password: '' })
1491
+ setAccountOpen(false)
1492
+ showToast('账号已保存并登录成功')
1493
+ if (activeId !== null) fetchDoc(activeId)
1494
+ })
1495
+ .catch(function (error) {
1496
+ setAccountError((error && error.message) || String(error))
1497
+ })
1498
+ .finally(function () {
1499
+ setAccountSaving(false)
1500
+ })
1501
+ }
1502
+
1503
+ function runSearch(keyword, projectOverride) {
1504
+ const text = String(keyword === undefined || keyword === null ? '' : keyword).trim()
1505
+ if (text === '') return
1506
+ const project = String(
1507
+ projectOverride === undefined || projectOverride === null ? searchProject : projectOverride,
1508
+ ).trim()
1509
+ if (!/^\d+$/.test(project)) {
1510
+ setSearch(function (previous) {
1511
+ return Object.assign({}, previous, {
1512
+ open: true,
1513
+ loading: false,
1514
+ error: project === '' ? '请先填写项目 ID' : '项目 ID 必须是数字',
1515
+ items: [],
1516
+ total: 0,
1517
+ })
1518
+ })
1519
+ return
1520
+ }
1521
+ const sequence = ++searchSeq.current
1522
+ setSearch(function (previous) {
1523
+ return Object.assign({}, previous, {
1524
+ open: true,
1525
+ keyword: text,
1526
+ loading: true,
1527
+ error: '',
1528
+ items: [],
1529
+ total: 0,
1530
+ projectName: '',
1531
+ })
1532
+ })
1533
+ fetch('/api/yapi/search', {
1534
+ method: 'POST',
1535
+ headers: { 'content-type': 'application/json' },
1536
+ body: JSON.stringify({ projectId: Number(project), keyword: text }),
1537
+ })
1538
+ .then(function (response) {
1539
+ return response.json().catch(function () {
1540
+ return null
1541
+ })
1542
+ })
1543
+ .then(function (payload) {
1544
+ if (sequence !== searchSeq.current) return
1545
+ if (!payload) throw new Error('宿主返回了非 JSON 响应')
1546
+ if (!payload.ok) throw new Error((payload.error && payload.error.message) || '搜索失败')
1547
+ const data = payload.data || {}
1548
+ setSearch(function (previous) {
1549
+ return Object.assign({}, previous, {
1550
+ loading: false,
1551
+ items: Array.isArray(data.items) ? data.items : [],
1552
+ total: Number(data.total) || 0,
1553
+ projectName: data.projectName || '',
1554
+ })
1555
+ })
1556
+ })
1557
+ .catch(function (failure) {
1558
+ if (sequence !== searchSeq.current) return
1559
+ setSearch(function (previous) {
1560
+ return Object.assign({}, previous, {
1561
+ loading: false,
1562
+ error: (failure && failure.message) || String(failure),
1563
+ items: [],
1564
+ total: 0,
1565
+ })
1566
+ })
1567
+ })
1568
+ }
1569
+
1570
+ function scheduleSearch(keyword, projectOverride) {
1571
+ clearTimeout(searchTimer.current)
1572
+ searchTimer.current = window.setTimeout(function () {
1573
+ runSearch(keyword, projectOverride)
1574
+ }, 280)
1575
+ }
1576
+
1577
+ function handleSearchInput(value) {
1578
+ setInput(value)
1579
+ clearTimeout(searchTimer.current)
1580
+ const text = value.trim()
1581
+ if (text === '' || looksLikeDocRef(text)) {
1582
+ setSearch(function (previous) {
1583
+ return Object.assign({}, previous, { open: false, loading: false, error: '' })
1584
+ })
1585
+ return
1586
+ }
1587
+ scheduleSearch(text)
1588
+ }
1589
+
1590
+ function changeSearchProject(value) {
1591
+ setSearchProject(value)
1592
+ writeStorage(PROJECT_KEY, value)
1593
+ if (search.open && search.keyword !== '') {
1594
+ scheduleSearch(search.keyword, value)
1595
+ }
1596
+ }
1597
+
1598
+ function openSearchResult(item) {
1599
+ openDoc(item.id, { title: item.title, method: item.method, path: item.path }, { keepInput: true })
1600
+ setSearch(function (previous) {
1601
+ return Object.assign({}, previous, { open: false })
1602
+ })
1603
+ }
1604
+
1605
+ function submit(event) {
1606
+ event.preventDefault()
1607
+ const text = input.trim()
1608
+ if (text === '') return
1609
+ if (looksLikeDocRef(text)) {
1610
+ setSearch(function (previous) {
1611
+ return Object.assign({}, previous, { open: false })
1612
+ })
1613
+ openDoc(text)
1614
+ return
1615
+ }
1616
+ runSearch(text)
1617
+ }
1618
+
1619
+ function copySelected() {
1620
+ if (!doc) return
1621
+ const text = buildCopyText(
1622
+ doc,
1623
+ copyOptions,
1624
+ address,
1625
+ copyFormat,
1626
+ requestAnnotation,
1627
+ responseAnnotation,
1628
+ descText,
1629
+ )
1630
+ if (text.trim() === '') {
1631
+ showToast('请至少勾选一项内容')
1632
+ return
1633
+ }
1634
+ copyToClipboard('所选内容', text).then(function (ok) {
1635
+ if (ok) setCopyOpen(false)
1636
+ })
1637
+ }
1638
+
1639
+ function copyToClipboard(label, text) {
1640
+ if (!text) {
1641
+ showToast('没有可复制的内容')
1642
+ return Promise.resolve(false)
1643
+ }
1644
+ return copyText(text).then(function (ok) {
1645
+ if (!ok) {
1646
+ showToast('复制失败,请手动选择文本')
1647
+ return false
1648
+ }
1649
+ showToast('已复制' + label)
1650
+ return true
1651
+ })
1652
+ }
1653
+
1654
+ function updateCopyOption(key, value) {
1655
+ setCopyOptions(function (previous) {
1656
+ const next = Object.assign({}, previous)
1657
+ next[key] = value
1658
+ writeStorage(COPY_OPTIONS_KEY, JSON.stringify(next))
1659
+ return next
1660
+ })
1661
+ }
1662
+
1663
+ function toggleAllCopyOptions() {
1664
+ const all = Object.keys(DEFAULT_COPY_OPTIONS).every(function (key) {
1665
+ return copyOptions[key]
1666
+ })
1667
+ const next = {}
1668
+ for (const key of Object.keys(DEFAULT_COPY_OPTIONS)) next[key] = !all
1669
+ setCopyOptions(next)
1670
+ writeStorage(COPY_OPTIONS_KEY, JSON.stringify(next))
1671
+ }
1672
+
1673
+ const address = useMemo(
1674
+ function () {
1675
+ if (!doc) return ''
1676
+ const domain = base || (doc.project.envs.length > 0 ? doc.project.envs[0].domain : '')
1677
+ return joinUrl(domain, doc.project.basepath, doc.path)
1678
+ },
1679
+ [doc, base],
1680
+ )
1681
+
1682
+ const requestBody = doc ? doc.request.body : null
1683
+ const responseBody = doc ? doc.response.body : null
1684
+ const requestAnnotation = useMemo(
1685
+ function () {
1686
+ return doc ? annotationFor(doc, 'request') : null
1687
+ },
1688
+ [doc],
1689
+ )
1690
+ const responseAnnotation = useMemo(
1691
+ function () {
1692
+ return doc ? annotationFor(doc, 'response') : null
1693
+ },
1694
+ [doc],
1695
+ )
1696
+ const copyCount = Object.keys(DEFAULT_COPY_OPTIONS).filter(function (key) {
1697
+ return copyOptions[key]
1698
+ }).length
1699
+
1700
+ const paramSections = doc
1701
+ ? [
1702
+ { key: 'headers', title: 'Headers', kind: 'header', rows: doc.request.headers },
1703
+ { key: 'query', title: 'Query 参数', kind: 'param', rows: doc.request.query },
1704
+ { key: 'params', title: 'Path 参数', kind: 'param', rows: doc.request.params },
1705
+ { key: 'form', title: 'Form 参数', kind: 'param', rows: doc.request.form },
1706
+ ].filter(function (section) {
1707
+ return section.rows.length > 0
1708
+ })
1709
+ : []
1710
+
1711
+ const descText = doc ? doc.markdown || htmlToText(doc.desc) : ''
1712
+
1713
+ return h(
1714
+ 'div',
1715
+ { className: 'yapi-root' },
1716
+ h(
1717
+ 'div',
1718
+ { className: 'yapi-inner' },
1719
+ h(
1720
+ 'header',
1721
+ { className: 'yapi-hero' },
1722
+ h(
1723
+ 'div',
1724
+ { className: 'yapi-hero-main' },
1725
+ h('div', { className: 'yapi-kicker' }, h(YapiIcon, { size: 14 }), 'YAPI · 接口工作台'),
1726
+ h('h1', null, 'YAPI 接口文档'),
1727
+ h('p', null, '输入文档 ID 或链接,获取接口信息,并一键复制请求地址、方法类型、请求 Body 与返回体。'),
1728
+ ),
1729
+ h(
1730
+ 'div',
1731
+ { className: 'yapi-hero-side' },
1732
+ h(
1733
+ 'span',
1734
+ {
1735
+ className: 'yapi-accountstate',
1736
+ 'data-set': account && account.hasPassword ? 'true' : 'false',
1737
+ title: account && account.email ? account.email : '点击「账号设置」填写 YAPI 账号',
1738
+ },
1739
+ account && account.hasPassword
1740
+ ? (account.user && account.user.email) || account.email || '已配置账号'
1741
+ : '未配置账号',
1742
+ ),
1743
+ h(
1744
+ 'button',
1745
+ { type: 'button', className: 'yapi-btn ghost sm', onClick: openAccount },
1746
+ h(UserIcon, { size: 13 }),
1747
+ '账号设置',
1748
+ ),
1749
+ ),
1750
+ ),
1751
+ h(
1752
+ 'div',
1753
+ { className: 'yapi-searchwrap', ref: searchWrapRef },
1754
+ h(
1755
+ 'form',
1756
+ { className: 'yapi-search', onSubmit: submit },
1757
+ h('span', { className: 'yapi-search-ico' }, h(SearchIcon, { size: 17 })),
1758
+ h('input', {
1759
+ value: input,
1760
+ placeholder: '输入文档 ID / 链接直接打开,或输入关键字搜索接口',
1761
+ spellCheck: false,
1762
+ autoComplete: 'off',
1763
+ onChange: function (event) {
1764
+ handleSearchInput(event.target.value)
1765
+ },
1766
+ onFocus: function () {
1767
+ if (search.keyword !== '' && input.trim() !== '' && !looksLikeDocRef(input)) {
1768
+ setSearch(function (previous) {
1769
+ return Object.assign({}, previous, { open: true })
1770
+ })
1771
+ }
1772
+ },
1773
+ }),
1774
+ h(
1775
+ 'button',
1776
+ {
1777
+ type: 'submit',
1778
+ className: 'yapi-btn',
1779
+ disabled: loading || (!looksLikeDocRef(input) && input.trim() === ''),
1780
+ },
1781
+ looksLikeDocRef(input) ? (loading ? '打开中…' : '打开文档') : '搜索接口',
1782
+ ),
1783
+ ),
1784
+ search.open && input.trim() !== '' && !looksLikeDocRef(input)
1785
+ ? h(
1786
+ 'div',
1787
+ { className: 'yapi-searchpanel', role: 'listbox' },
1788
+ h(
1789
+ 'div',
1790
+ { className: 'yapi-searchpanel-head' },
1791
+ h('span', { className: 'yapi-searchpanel-label' }, '项目'),
1792
+ h('input', {
1793
+ className: 'yapi-projectinput',
1794
+ value: searchProject,
1795
+ inputMode: 'numeric',
1796
+ spellCheck: false,
1797
+ 'aria-label': '项目 ID',
1798
+ placeholder: '项目 ID',
1799
+ onChange: function (event) {
1800
+ changeSearchProject(event.target.value)
1801
+ },
1802
+ onKeyDown: function (event) {
1803
+ if (event.key === 'Enter') {
1804
+ event.preventDefault()
1805
+ runSearch(search.keyword || input)
1806
+ }
1807
+ },
1808
+ }),
1809
+ h(
1810
+ 'span',
1811
+ { className: 'yapi-searchpanel-meta' },
1812
+ (search.projectName ? search.projectName + ' · ' : '') +
1813
+ (search.total > 0 ? '共 ' + search.total + ' 个接口' : ''),
1814
+ ),
1815
+ search.items.length > 0
1816
+ ? h('span', { className: 'yapi-searchpanel-match' }, '匹配 ' + search.items.length)
1817
+ : null,
1818
+ h(
1819
+ 'button',
1820
+ {
1821
+ type: 'button',
1822
+ className: 'yapi-searchpanel-close',
1823
+ 'aria-label': '关闭搜索',
1824
+ onClick: function () {
1825
+ setSearch(function (previous) {
1826
+ return Object.assign({}, previous, { open: false })
1827
+ })
1828
+ },
1829
+ },
1830
+ h(CloseIcon, { size: 14 }),
1831
+ ),
1832
+ ),
1833
+ h(
1834
+ 'div',
1835
+ { className: 'yapi-searchpanel-body' },
1836
+ search.loading
1837
+ ? h(
1838
+ 'div',
1839
+ { className: 'yapi-searchpanel-state' },
1840
+ h('span', { className: 'yapi-spinner' }),
1841
+ '正在搜索接口…',
1842
+ )
1843
+ : search.error !== ''
1844
+ ? h('div', { className: 'yapi-searchpanel-error' }, search.error)
1845
+ : search.items.length === 0
1846
+ ? h(
1847
+ 'div',
1848
+ { className: 'yapi-searchpanel-state' },
1849
+ search.keyword === '' ? '输入关键字搜索接口路径或标题' : '没有匹配的接口',
1850
+ )
1851
+ : search.items.map(function (item) {
1852
+ return h(
1853
+ 'button',
1854
+ {
1855
+ key: item.id,
1856
+ type: 'button',
1857
+ className: 'yapi-searchitem',
1858
+ onClick: function () {
1859
+ openSearchResult(item)
1860
+ },
1861
+ },
1862
+ h(
1863
+ 'span',
1864
+ { className: 'yapi-searchitem-method', 'data-method': item.method },
1865
+ item.method,
1866
+ ),
1867
+ h(
1868
+ 'span',
1869
+ { className: 'yapi-searchitem-main' },
1870
+ h('code', null, item.path || '/'),
1871
+ h(
1872
+ 'span',
1873
+ { className: 'yapi-searchitem-title' },
1874
+ item.title || '未命名接口',
1875
+ ),
1876
+ ),
1877
+ h('span', { className: 'yapi-searchitem-id' }, '#' + item.id),
1878
+ )
1879
+ }),
1880
+ ),
1881
+ )
1882
+ : null,
1883
+ ),
1884
+ h(
1885
+ 'div',
1886
+ { className: 'yapi-hint' },
1887
+ '输入文档 ID 或完整链接直接打开;输入路径 / 标题关键字搜索项目接口,点击结果会新建标签页。',
1888
+ ),
1889
+ recent.length > 0
1890
+ ? h(
1891
+ 'div',
1892
+ { className: 'yapi-recent' },
1893
+ h('span', null, '最近'),
1894
+ recent.map(function (id) {
1895
+ return h(
1896
+ 'button',
1897
+ {
1898
+ key: id,
1899
+ type: 'button',
1900
+ className: 'yapi-chip',
1901
+ onClick: function () {
1902
+ openDoc(id)
1903
+ },
1904
+ },
1905
+ '#' + id,
1906
+ )
1907
+ }),
1908
+ )
1909
+ : null,
1910
+ tabs.length > 0
1911
+ ? h(
1912
+ 'div',
1913
+ { className: 'yapi-tabbar' },
1914
+ h(
1915
+ 'div',
1916
+ { className: 'yapi-tabbar-list' },
1917
+ tabs.map(function (item) {
1918
+ const active = item.id === activeId
1919
+ return h(
1920
+ 'div',
1921
+ {
1922
+ key: item.id,
1923
+ className: 'yapi-doctab',
1924
+ 'data-active': active ? 'true' : 'false',
1925
+ role: 'button',
1926
+ tabIndex: 0,
1927
+ title: item.title || '#' + item.id,
1928
+ onClick: function () {
1929
+ selectTab(item.id)
1930
+ },
1931
+ onKeyDown: function (event) {
1932
+ if (event.key === 'Enter' || event.key === ' ') {
1933
+ event.preventDefault()
1934
+ selectTab(item.id)
1935
+ }
1936
+ },
1937
+ },
1938
+ item.method
1939
+ ? h(
1940
+ 'span',
1941
+ { className: 'yapi-doctab-method', 'data-method': item.method },
1942
+ item.method,
1943
+ )
1944
+ : null,
1945
+ h('span', { className: 'yapi-doctab-title' }, item.title || '#' + item.id),
1946
+ item.loading ? h('span', { className: 'yapi-doctab-spin' }) : null,
1947
+ h(
1948
+ 'button',
1949
+ {
1950
+ type: 'button',
1951
+ className: 'yapi-doctab-close',
1952
+ 'aria-label': '关闭标签页',
1953
+ onClick: function (event) {
1954
+ event.stopPropagation()
1955
+ closeTab(item.id)
1956
+ },
1957
+ },
1958
+ h(CloseIcon, { size: 12 }),
1959
+ ),
1960
+ )
1961
+ }),
1962
+ ),
1963
+ tabs.length > 1
1964
+ ? h(
1965
+ 'button',
1966
+ { type: 'button', className: 'yapi-doctab-clear', onClick: closeAll },
1967
+ '全部关闭',
1968
+ )
1969
+ : null,
1970
+ )
1971
+ : null,
1972
+ error
1973
+ ? h(
1974
+ 'div',
1975
+ { className: 'yapi-error', role: 'alert' },
1976
+ h('div', { className: 'yapi-error-title' }, '获取失败'),
1977
+ h('div', { className: 'yapi-error-msg' }, error.message),
1978
+ h(
1979
+ 'div',
1980
+ { className: 'yapi-error-hint' },
1981
+ '请检查文档 ID 与账号权限;若提示未配置账号,点下方按钮直接填写。',
1982
+ ),
1983
+ h(
1984
+ 'div',
1985
+ { className: 'yapi-error-action' },
1986
+ h(
1987
+ 'button',
1988
+ {
1989
+ type: 'button',
1990
+ className: 'yapi-btn ghost sm',
1991
+ onClick: function () {
1992
+ if (activeId !== null) fetchDoc(activeId)
1993
+ },
1994
+ },
1995
+ '重试',
1996
+ ),
1997
+ error.code === 'NO_CONFIG' ||
1998
+ error.code === 'NO_CREDENTIALS' ||
1999
+ error.code === 'LOGIN_FAILED' ||
2000
+ error.code === 'SESSION_LOST' ||
2001
+ error.code === 'SAVE_FAILED'
2002
+ ? h(
2003
+ 'button',
2004
+ { type: 'button', className: 'yapi-btn sm', onClick: openAccount },
2005
+ '配置账号',
2006
+ )
2007
+ : null,
2008
+ ),
2009
+ )
2010
+ : null,
2011
+ loading && !doc
2012
+ ? h('div', { className: 'yapi-loading' }, h('span', { className: 'yapi-spinner' }), '正在读取 YAPI 文档…')
2013
+ : null,
2014
+ doc
2015
+ ? h(
2016
+ 'section',
2017
+ { className: 'yapi-doc' },
2018
+ h(
2019
+ 'div',
2020
+ { className: 'yapi-doc-head' },
2021
+ h(
2022
+ 'div',
2023
+ { className: 'yapi-titlebar' },
2024
+ h('span', { className: 'yapi-method', 'data-method': doc.method }, doc.method),
2025
+ h('h2', null, doc.title || '未命名接口'),
2026
+ doc.status
2027
+ ? h(
2028
+ 'span',
2029
+ { className: 'yapi-status', 'data-status': doc.status },
2030
+ STATUS_LABEL[doc.status] || doc.status,
2031
+ )
2032
+ : null,
2033
+ h(
2034
+ 'div',
2035
+ { className: 'yapi-titlebar-actions' },
2036
+ h(
2037
+ 'button',
2038
+ {
2039
+ type: 'button',
2040
+ className: 'yapi-btn sm',
2041
+ onClick: function () {
2042
+ setCopyOpen(true)
2043
+ },
2044
+ },
2045
+ h(CopyIcon, { size: 13 }),
2046
+ '复制',
2047
+ ),
2048
+ h(
2049
+ 'a',
2050
+ {
2051
+ className: 'yapi-openlink',
2052
+ href: doc.pageUrl,
2053
+ target: '_blank',
2054
+ rel: 'noreferrer noopener',
2055
+ },
2056
+ '在 YAPI 打开',
2057
+ h(ExternalIcon, { size: 12 }),
2058
+ ),
2059
+ ),
2060
+ ),
2061
+ h(
2062
+ 'div',
2063
+ { className: 'yapi-meta' },
2064
+ h('span', null, doc.project.name || '未命名项目'),
2065
+ h('span', { className: 'yapi-dot' }, '·'),
2066
+ h('span', null, doc.tags.length > 0 ? doc.tags.join(' / ') : '未分组'),
2067
+ doc.author
2068
+ ? h('span', { className: 'yapi-dot' }, '·')
2069
+ : null,
2070
+ doc.author ? h('span', null, doc.author) : null,
2071
+ doc.updatedAt
2072
+ ? h('span', { className: 'yapi-dot' }, '·')
2073
+ : null,
2074
+ doc.updatedAt ? h('span', null, '更新于 ' + formatTime(doc.updatedAt)) : null,
2075
+ ),
2076
+ ),
2077
+ h(
2078
+ 'div',
2079
+ { className: 'yapi-addrs' },
2080
+ h(
2081
+ 'div',
2082
+ { className: 'yapi-field' },
2083
+ h('label', null, '服务地址'),
2084
+ h('input', {
2085
+ value: base,
2086
+ placeholder: doc.project.envs.length > 0 ? doc.project.envs[0].domain : 'https://api.example.com',
2087
+ spellCheck: false,
2088
+ onChange: function (event) {
2089
+ setBase(event.target.value)
2090
+ },
2091
+ }),
2092
+ ),
2093
+ h(
2094
+ 'div',
2095
+ { className: 'yapi-field' },
2096
+ h('label', null, '接口路径'),
2097
+ h('div', { className: 'yapi-pathstatic' }, h('code', null, doc.path || '/')),
2098
+ ),
2099
+ ),
2100
+ h(
2101
+ 'div',
2102
+ { className: 'yapi-addrpreview' },
2103
+ '完整请求地址:',
2104
+ h('code', null, address || '—'),
2105
+ ),
2106
+ h(
2107
+ 'div',
2108
+ { className: 'yapi-tabs' },
2109
+ [
2110
+ { id: 'params', label: '请求参数' },
2111
+ { id: 'request', label: '请求字段' },
2112
+ { id: 'response', label: '返回字段' },
2113
+ { id: 'desc', label: '说明' },
2114
+ ].map(function (item) {
2115
+ return h(
2116
+ 'button',
2117
+ {
2118
+ key: item.id,
2119
+ type: 'button',
2120
+ className: 'yapi-tab',
2121
+ 'data-tab': item.id,
2122
+ 'data-active': tab === item.id ? 'true' : 'false',
2123
+ onClick: function () {
2124
+ setTab(item.id)
2125
+ },
2126
+ },
2127
+ item.label,
2128
+ )
2129
+ }),
2130
+ ),
2131
+ h(
2132
+ 'div',
2133
+ { className: 'yapi-tabbody' },
2134
+ tab === 'params'
2135
+ ? paramSections.length > 0
2136
+ ? paramSections.map(function (section) {
2137
+ return h(ParamSection, {
2138
+ key: section.key,
2139
+ title: section.title,
2140
+ kind: section.kind,
2141
+ rows: section.rows,
2142
+ })
2143
+ })
2144
+ : h('div', { className: 'yapi-empty' }, '该接口没有 Query、Path、Header 或 Form 参数')
2145
+ : null,
2146
+ tab === 'request'
2147
+ ? requestBody.kind === 'none'
2148
+ ? h('div', { className: 'yapi-empty' }, '该接口没有请求 Body')
2149
+ : h(FieldPanel, {
2150
+ title: '请求字段',
2151
+ tree: requestAnnotation.tree,
2152
+ })
2153
+ : null,
2154
+ tab === 'response'
2155
+ ? responseBody.kind === 'none'
2156
+ ? h('div', { className: 'yapi-empty' }, '该接口没有返回体')
2157
+ : h(FieldPanel, {
2158
+ title: '返回字段',
2159
+ tree: responseAnnotation.tree,
2160
+ })
2161
+ : null,
2162
+ tab === 'desc'
2163
+ ? descText === ''
2164
+ ? h('div', { className: 'yapi-empty' }, '该接口没有说明')
2165
+ : h('div', { className: 'yapi-desc' }, descText)
2166
+ : null,
2167
+ ),
2168
+ )
2169
+ : null,
2170
+ copyOpen && doc
2171
+ ? h(
2172
+ 'div',
2173
+ {
2174
+ className: 'yapi-mask',
2175
+ role: 'presentation',
2176
+ onClick: function () {
2177
+ setCopyOpen(false)
2178
+ },
2179
+ },
2180
+ h(
2181
+ 'div',
2182
+ {
2183
+ className: 'yapi-modal yapi-copymodal',
2184
+ role: 'dialog',
2185
+ 'aria-modal': 'true',
2186
+ 'aria-label': '复制接口内容',
2187
+ onClick: function (event) {
2188
+ event.stopPropagation()
2189
+ },
2190
+ },
2191
+ h(
2192
+ 'div',
2193
+ { className: 'yapi-modal-head' },
2194
+ h('h3', null, '复制接口内容'),
2195
+ h('span', { className: 'yapi-copycount' }, '已选 ' + copyCount + ' 项'),
2196
+ h(
2197
+ 'button',
2198
+ {
2199
+ type: 'button',
2200
+ className: 'yapi-copytoggleall',
2201
+ onClick: toggleAllCopyOptions,
2202
+ },
2203
+ copyCount === COPY_OPTIONS.length ? '清空' : '全选',
2204
+ ),
2205
+ h(
2206
+ 'button',
2207
+ {
2208
+ type: 'button',
2209
+ className: 'yapi-modal-close',
2210
+ 'aria-label': '关闭',
2211
+ onClick: function () {
2212
+ setCopyOpen(false)
2213
+ },
2214
+ },
2215
+ h(CloseIcon, { size: 16 }),
2216
+ ),
2217
+ ),
2218
+ h(
2219
+ 'div',
2220
+ { className: 'yapi-modal-body' },
2221
+ h(
2222
+ 'div',
2223
+ { className: 'yapi-checkgrid' },
2224
+ COPY_OPTIONS.map(function (option) {
2225
+ const checked = copyOptions[option.key] === true
2226
+ return h(
2227
+ 'label',
2228
+ {
2229
+ key: option.key,
2230
+ className: 'yapi-check',
2231
+ 'data-checked': checked ? 'true' : 'false',
2232
+ },
2233
+ h('input', {
2234
+ type: 'checkbox',
2235
+ checked: checked,
2236
+ onChange: function (event) {
2237
+ updateCopyOption(option.key, event.target.checked)
2238
+ },
2239
+ }),
2240
+ h('span', null, option.label),
2241
+ )
2242
+ }),
2243
+ ),
2244
+ h(
2245
+ 'div',
2246
+ { className: 'yapi-copyformatrow' },
2247
+ h('span', null, 'Body 格式'),
2248
+ h(
2249
+ 'div',
2250
+ { className: 'yapi-seg' },
2251
+ h(
2252
+ 'button',
2253
+ {
2254
+ type: 'button',
2255
+ 'data-active': copyFormat === 'json' ? 'true' : 'false',
2256
+ onClick: function () {
2257
+ changeCopyFormat('json')
2258
+ },
2259
+ },
2260
+ '纯 JSON',
2261
+ ),
2262
+ h(
2263
+ 'button',
2264
+ {
2265
+ type: 'button',
2266
+ 'data-active': copyFormat === 'annotated' ? 'true' : 'false',
2267
+ onClick: function () {
2268
+ changeCopyFormat('annotated')
2269
+ },
2270
+ },
2271
+ '带备注',
2272
+ ),
2273
+ ),
2274
+ h('span', { className: 'yapi-copyformat-hint' }, '只影响请求 / 返回 Body'),
2275
+ ),
2276
+ h(
2277
+ 'div',
2278
+ { className: 'yapi-copyhint' },
2279
+ '复制出的是一段可直接粘贴的文本:按勾选项组合标题、地址、请求头、参数、Body、返回体和说明。',
2280
+ ),
2281
+ ),
2282
+ h(
2283
+ 'div',
2284
+ { className: 'yapi-modal-foot' },
2285
+ h(
2286
+ 'button',
2287
+ {
2288
+ type: 'button',
2289
+ className: 'yapi-btn ghost',
2290
+ onClick: function () {
2291
+ setCopyOpen(false)
2292
+ },
2293
+ },
2294
+ '取消',
2295
+ ),
2296
+ h('button', { type: 'button', className: 'yapi-btn', onClick: copySelected }, '复制'),
2297
+ ),
2298
+ ),
2299
+ )
2300
+ : null,
2301
+ accountOpen
2302
+ ? h(
2303
+ 'div',
2304
+ {
2305
+ className: 'yapi-mask',
2306
+ role: 'presentation',
2307
+ onClick: function () {
2308
+ setAccountOpen(false)
2309
+ },
2310
+ },
2311
+ h(
2312
+ 'div',
2313
+ {
2314
+ className: 'yapi-modal',
2315
+ role: 'dialog',
2316
+ 'aria-modal': 'true',
2317
+ 'aria-label': 'YAPI 账号设置',
2318
+ onClick: function (event) {
2319
+ event.stopPropagation()
2320
+ },
2321
+ },
2322
+ h(
2323
+ 'div',
2324
+ { className: 'yapi-modal-head' },
2325
+ h('h3', null, 'YAPI 账号设置'),
2326
+ h(
2327
+ 'button',
2328
+ {
2329
+ type: 'button',
2330
+ className: 'yapi-modal-close',
2331
+ 'aria-label': '关闭',
2332
+ onClick: function () {
2333
+ setAccountOpen(false)
2334
+ },
2335
+ },
2336
+ h(CloseIcon, { size: 16 }),
2337
+ ),
2338
+ ),
2339
+ h(
2340
+ 'div',
2341
+ { className: 'yapi-modal-body' },
2342
+ h(
2343
+ 'div',
2344
+ { className: 'yapi-modal-state' },
2345
+ account && account.loggedIn
2346
+ ? '当前已登录:' +
2347
+ ((account.user && (account.user.email || account.user.username)) || account.email || '')
2348
+ : account && account.hasPassword
2349
+ ? '账号已保存,尚未登录'
2350
+ : '尚未配置 YAPI 账号',
2351
+ ),
2352
+ h(
2353
+ 'div',
2354
+ { className: 'yapi-formrow' },
2355
+ h('label', null, 'YAPI 地址'),
2356
+ h('input', {
2357
+ value: accountForm.baseUrl,
2358
+ placeholder: 'https://your-yapi.example.com',
2359
+ spellCheck: false,
2360
+ onChange: function (event) {
2361
+ updateAccountForm({ baseUrl: event.target.value })
2362
+ },
2363
+ }),
2364
+ ),
2365
+ h(
2366
+ 'div',
2367
+ { className: 'yapi-formrow' },
2368
+ h('label', null, '账号(邮箱)'),
2369
+ h('input', {
2370
+ value: accountForm.email,
2371
+ placeholder: 'you@example.com',
2372
+ spellCheck: false,
2373
+ autoComplete: 'username',
2374
+ onChange: function (event) {
2375
+ updateAccountForm({ email: event.target.value })
2376
+ },
2377
+ }),
2378
+ ),
2379
+ h(
2380
+ 'div',
2381
+ { className: 'yapi-formrow' },
2382
+ h('label', null, '密码'),
2383
+ h('input', {
2384
+ type: 'password',
2385
+ value: accountForm.password,
2386
+ placeholder: account && account.hasPassword ? '已保存,留空表示不修改' : '输入 YAPI 密码',
2387
+ autoComplete: 'current-password',
2388
+ onChange: function (event) {
2389
+ updateAccountForm({ password: event.target.value })
2390
+ },
2391
+ }),
2392
+ ),
2393
+ h(
2394
+ 'div',
2395
+ { className: 'yapi-formhint' },
2396
+ '点击「测试并保存」后,插件会先用这组账号真实登录 YAPI;成功后才写入 ' +
2397
+ ((account && account.configFile) || '~/.dsh/dsh-plugin-yapi.json') +
2398
+ '(权限 0600)。密码只保存在 Host 端,不会回传页面。',
2399
+ ),
2400
+ accountError ? h('div', { className: 'yapi-modal-error' }, accountError) : null,
2401
+ ),
2402
+ h(
2403
+ 'div',
2404
+ { className: 'yapi-modal-foot' },
2405
+ h(
2406
+ 'button',
2407
+ {
2408
+ type: 'button',
2409
+ className: 'yapi-btn ghost',
2410
+ onClick: function () {
2411
+ setAccountOpen(false)
2412
+ },
2413
+ },
2414
+ '取消',
2415
+ ),
2416
+ h(
2417
+ 'button',
2418
+ { type: 'button', className: 'yapi-btn', disabled: accountSaving, onClick: saveAccount },
2419
+ accountSaving ? '验证中…' : '测试并保存',
2420
+ ),
2421
+ ),
2422
+ ),
2423
+ )
2424
+ : null,
2425
+ toast ? h('div', { className: 'yapi-toast', role: 'status' }, toast) : null,
2426
+ ),
2427
+ )
2428
+ }
2429
+
2430
+ if (typeof globalThis !== 'undefined') {
2431
+ globalThis.__YAPI_PANEL_TEST__ = {
2432
+ parseDocId: parseDocId,
2433
+ joinUrl: joinUrl,
2434
+ annotateSchemaValue: annotateSchemaValue,
2435
+ annotateFormValue: annotateFormValue,
2436
+ annotationFor: annotationFor,
2437
+ buildTreeFromSchema: buildTreeFromSchema,
2438
+ fieldTreeFor: fieldTreeFor,
2439
+ buildCopyText: buildCopyText,
2440
+ filterTree: filterTree,
2441
+ countMatches: countMatches,
2442
+ YapiPanel: YapiPanel,
2443
+ YapiIcon: YapiIcon,
2444
+ }
2445
+ }
2446
+
2447
+ return {
2448
+ name: PANEL_ID,
2449
+ inject: ['slots'],
2450
+ apply(ctx) {
2451
+ ensureStyle()
2452
+ ctx.slots.inject('sidebar.panellist', function () {
2453
+ return ctx.slots.register(
2454
+ { name: 'sidebar.panellist', id: PANEL_ID, order: PANEL_ORDER, label: PANEL_LABEL },
2455
+ YapiIcon,
2456
+ )
2457
+ })
2458
+ ctx.slots.inject('main', function () {
2459
+ return ctx.slots.register({ name: 'main', key: PANEL_ID }, YapiPanel)
2460
+ })
2461
+ },
2462
+ }
2463
+ },
2464
+ })