@tuoyuan/gateway-api-select-config 1.2.1
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/LICENSE +21 -0
- package/README.md +605 -0
- package/dist/gateway-api-select-config.css +3 -0
- package/dist/gateway-api-select-config.es.js +1767 -0
- package/dist/gateway-api-select-config.umd.js +28 -0
- package/package.json +60 -0
- package/src/components/ApiConfig.vue +2264 -0
- package/src/components/SchemaFieldEditor.vue +572 -0
- package/src/index.js +70 -0
- package/src/utils/dataExtractor.js +359 -0
- package/src/utils/dataFilters.js +300 -0
- package/src/utils/reqMappingUtils.js +147 -0
- package/src/utils/schemaUtils.js +197 -0
- package/src/utils/treeUtils.js +134 -0
|
@@ -0,0 +1,2264 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="api-config-component">
|
|
3
|
+
<!-- 左侧:API 树选择器 -->
|
|
4
|
+
<div class="left-panel">
|
|
5
|
+
<div class="panel-header">
|
|
6
|
+
<h3>网关接口配置</h3>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- API 搜索框 -->
|
|
10
|
+
<div class="search-box">
|
|
11
|
+
<input
|
|
12
|
+
v-model="searchKeyword"
|
|
13
|
+
type="text"
|
|
14
|
+
placeholder="搜索接口名称或路径..."
|
|
15
|
+
class="search-input"
|
|
16
|
+
@input="handleSearch"
|
|
17
|
+
/>
|
|
18
|
+
<span class="search-icon">🔍</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- API 树形菜单 -->
|
|
22
|
+
<div class="section-title">选择 API 接口</div>
|
|
23
|
+
<div class="api-tree-container">
|
|
24
|
+
<div v-if="loading" class="loading">加载中...</div>
|
|
25
|
+
<div v-else-if="error" class="error">{{ error }}</div>
|
|
26
|
+
<div v-else-if="filteredApiTree.length === 0" class="empty">
|
|
27
|
+
{{ searchKeyword ? '未找到匹配的接口' : '暂无可用接口' }}
|
|
28
|
+
</div>
|
|
29
|
+
<a-tree
|
|
30
|
+
v-else
|
|
31
|
+
:data="treeData"
|
|
32
|
+
:default-expand-all="false"
|
|
33
|
+
:expanded-keys="expandedKeys"
|
|
34
|
+
:selected-keys="selectedKeys"
|
|
35
|
+
:show-line="false"
|
|
36
|
+
@select="handleTreeSelect"
|
|
37
|
+
@expand="handleTreeExpand"
|
|
38
|
+
>
|
|
39
|
+
<template #title="nodeData">
|
|
40
|
+
<div class="tree-node-content">
|
|
41
|
+
<span class="node-name">{{ nodeData.name }}</span>
|
|
42
|
+
<span v-if="nodeData.apiPath" class="node-path">{{ nodeData.apiPath }}</span>
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
</a-tree>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- 右侧:配置区域 -->
|
|
52
|
+
<div class="right-panel">
|
|
53
|
+
<div v-if="!selectedApi" class="empty-state">
|
|
54
|
+
<div class="empty-icon">📋</div>
|
|
55
|
+
<div class="empty-text">请先选择一个 API 接口</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div v-else class="config-container">
|
|
59
|
+
<!-- 当前选中的 API 信息 -->
|
|
60
|
+
<div class="selected-api-info">
|
|
61
|
+
<div class="section-title">当前选中接口</div>
|
|
62
|
+
<div class="api-info-card">
|
|
63
|
+
<div class="info-row">
|
|
64
|
+
<span class="label">接口名称:</span>
|
|
65
|
+
<span class="value">{{ selectedApi.name }}</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="info-row">
|
|
68
|
+
<span class="label">接口路径:</span>
|
|
69
|
+
<span class="value code">{{ selectedApi.apiPath }}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- 入参配置区 -->
|
|
75
|
+
<div class="req-mapping-section">
|
|
76
|
+
<div class="section-title">
|
|
77
|
+
入参配置
|
|
78
|
+
<span class="section-subtitle">(可选)</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- 显示 API 的请求参数结构 -->
|
|
82
|
+
<div v-if="currentReqSchema" class="req-schema-hint">
|
|
83
|
+
<div class="hint-title">📋 接口请求参数:</div>
|
|
84
|
+
<div class="req-params-list">
|
|
85
|
+
<div v-for="param in reqSchemaFields" :key="param.key" class="param-item">
|
|
86
|
+
<span class="param-name">{{ param.name }}</span>
|
|
87
|
+
<span class="param-type">{{ param.type }}</span>
|
|
88
|
+
<span v-if="param.required" class="param-required">必填</span>
|
|
89
|
+
<span v-else class="param-optional">可选</span>
|
|
90
|
+
<span v-if="param.description" class="param-desc">{{ param.description }}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div v-if="missingRequiredParams.length > 0" class="missing-params-warning">
|
|
94
|
+
<div class="warning-icon">⚠️</div>
|
|
95
|
+
<div class="warning-content">
|
|
96
|
+
<div class="warning-title">缺少必填参数</div>
|
|
97
|
+
<div class="warning-text">
|
|
98
|
+
以下参数是必填的,但未在入参配置中提供:
|
|
99
|
+
<strong>{{ missingRequiredParams.join('、') }}</strong>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="req-mapping-container">
|
|
106
|
+
<div class="req-mode-selector">
|
|
107
|
+
<label title="直接填写固定的参数值,适合不需要动态变化的场景">
|
|
108
|
+
<input type="radio" v-model="reqMappingMode" value="fixed" />
|
|
109
|
+
<span>固定值</span>
|
|
110
|
+
<span class="mode-hint">直接填写固定参数</span>
|
|
111
|
+
</label>
|
|
112
|
+
<label title="使用 ${变量名} 语法引用外部变量,适合需要动态传参的场景">
|
|
113
|
+
<input type="radio" v-model="reqMappingMode" value="template" />
|
|
114
|
+
<span>模板字符串</span>
|
|
115
|
+
<span class="mode-hint">支持 ${变量} 语法</span>
|
|
116
|
+
</label>
|
|
117
|
+
<label title="使用函数处理参数,可以进行复杂的数据转换和逻辑处理">
|
|
118
|
+
<input type="radio" v-model="reqMappingMode" value="function" />
|
|
119
|
+
<span>函数</span>
|
|
120
|
+
<span class="mode-hint">支持复杂逻辑处理</span>
|
|
121
|
+
</label>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="req-mapping-editor">
|
|
125
|
+
<textarea
|
|
126
|
+
v-model="reqMappingText"
|
|
127
|
+
:placeholder="reqMappingPlaceholder"
|
|
128
|
+
class="json-editor"
|
|
129
|
+
rows="8"
|
|
130
|
+
></textarea>
|
|
131
|
+
<div v-if="reqMappingError" class="error-message">
|
|
132
|
+
{{ reqMappingError }}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- 数据提取配置 -->
|
|
139
|
+
<div class="filter-mode-section">
|
|
140
|
+
<div class="section-title">
|
|
141
|
+
数据提取配置
|
|
142
|
+
<span class="section-subtitle">(配置如何从接口数据中提取和映射字段)</span>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- 测试接口按钮 -->
|
|
146
|
+
<div class="test-api-section">
|
|
147
|
+
<a-button
|
|
148
|
+
type="outline"
|
|
149
|
+
@click="handleTestApi"
|
|
150
|
+
:loading="testingApi"
|
|
151
|
+
status="success"
|
|
152
|
+
>
|
|
153
|
+
🔬 测试接口(查看返回数据)
|
|
154
|
+
</a-button>
|
|
155
|
+
<span class="test-hint">点击测试接口,查看实际返回的字段结构</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- 显示测试结果 -->
|
|
159
|
+
<div v-if="testApiResult" class="test-result-panel">
|
|
160
|
+
<div class="result-header">
|
|
161
|
+
<span class="result-title">📋 接口返回数据预览</span>
|
|
162
|
+
<a-button size="mini" type="text" @click="testApiResult = null">关闭</a-button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- 检测到的数组路径 -->
|
|
166
|
+
<div v-if="detectedArrayPath" class="detected-path">
|
|
167
|
+
<div class="detected-label">✓ 检测到数据数组路径:</div>
|
|
168
|
+
<div class="detected-value">{{ detectedArrayPath }}</div>
|
|
169
|
+
<a-button size="mini" @click="applyDetectedPath">应用此路径</a-button>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- 检测到的字段列表 -->
|
|
173
|
+
<div v-if="detectedFields.length > 0" class="detected-fields">
|
|
174
|
+
<div class="detected-label">✓ 检测到以下字段(点击快速添加):</div>
|
|
175
|
+
<div class="fields-grid">
|
|
176
|
+
<div
|
|
177
|
+
v-for="field in detectedFields"
|
|
178
|
+
:key="field"
|
|
179
|
+
class="field-tag"
|
|
180
|
+
@click="quickAddField(field)"
|
|
181
|
+
>
|
|
182
|
+
{{ field }}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<!-- 显示 Schema 字段(如果有) -->
|
|
189
|
+
<div v-if="resSchemaFields.length > 0" class="schema-fields-hint">
|
|
190
|
+
<div class="hint-title">📝 接口定义的字段:</div>
|
|
191
|
+
<div class="fields-grid">
|
|
192
|
+
<div
|
|
193
|
+
v-for="field in resSchemaFields"
|
|
194
|
+
:key="field"
|
|
195
|
+
class="field-tag clickable"
|
|
196
|
+
@click="quickAddField(field)"
|
|
197
|
+
:title="`点击添加 ${field} 字段映射`"
|
|
198
|
+
>
|
|
199
|
+
{{ field }}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="hint-desc">点击字段名快速添加到映射配置</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- 数组路径配置 -->
|
|
206
|
+
<div class="filter-config-item">
|
|
207
|
+
<label class="config-label">
|
|
208
|
+
数据路径
|
|
209
|
+
<span class="label-hint">(可选,留空则自动推断)</span>
|
|
210
|
+
</label>
|
|
211
|
+
<input
|
|
212
|
+
v-model="filterConfig.arrayPath"
|
|
213
|
+
type="text"
|
|
214
|
+
class="config-input"
|
|
215
|
+
placeholder="例如: data.list 或 @.data.list(@表示根对象)"
|
|
216
|
+
/>
|
|
217
|
+
<div class="config-hint">
|
|
218
|
+
💡 提示: 如果接口返回的数据结构是 { data: { list: [...] } }, 可以填写 "data.list" 或 "@.data.list"
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- 字段映射配置 -->
|
|
223
|
+
<div class="filter-config-item">
|
|
224
|
+
<div class="config-label-row">
|
|
225
|
+
<label class="config-label">字段映射规则</label>
|
|
226
|
+
<a-button size="mini" type="primary" @click="addFieldMapping">
|
|
227
|
+
+ 添加字段
|
|
228
|
+
</a-button>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div v-if="filterFieldMappings.length === 0" class="empty-hint">
|
|
232
|
+
暂无字段映射,点击"添加字段"按钮开始配置,或先测试接口查看可用字段
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div v-for="(mapping, index) in filterFieldMappings" :key="index" class="field-mapping-row">
|
|
236
|
+
<div class="mapping-col mapping-col-target">
|
|
237
|
+
<label class="col-label">目标字段名</label>
|
|
238
|
+
<input
|
|
239
|
+
v-model="mapping.target"
|
|
240
|
+
type="text"
|
|
241
|
+
class="mapping-input"
|
|
242
|
+
placeholder="例如: id, title, content"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="mapping-arrow">→</div>
|
|
246
|
+
<div class="mapping-col mapping-col-source">
|
|
247
|
+
<label class="col-label">来源字段(支持多个,逗号分隔)</label>
|
|
248
|
+
<input
|
|
249
|
+
v-model="mapping.source"
|
|
250
|
+
type="text"
|
|
251
|
+
class="mapping-input"
|
|
252
|
+
placeholder="例如: content,title,message"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="mapping-col mapping-col-type">
|
|
256
|
+
<label class="col-label">类型</label>
|
|
257
|
+
<a-select
|
|
258
|
+
v-model="mapping.type"
|
|
259
|
+
placeholder="类型"
|
|
260
|
+
size="small"
|
|
261
|
+
class="mapping-select"
|
|
262
|
+
>
|
|
263
|
+
<a-option value="string">字符串</a-option>
|
|
264
|
+
<a-option value="number">数字</a-option>
|
|
265
|
+
<a-option value="integer">整数</a-option>
|
|
266
|
+
<a-option value="boolean">布尔</a-option>
|
|
267
|
+
<a-option value="date">日期</a-option>
|
|
268
|
+
</a-select>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="mapping-col mapping-col-format">
|
|
271
|
+
<label class="col-label">格式化</label>
|
|
272
|
+
<a-select
|
|
273
|
+
v-model="mapping.format"
|
|
274
|
+
placeholder="选择格式化方式"
|
|
275
|
+
size="small"
|
|
276
|
+
class="mapping-select"
|
|
277
|
+
allow-clear
|
|
278
|
+
>
|
|
279
|
+
<a-option value="">无</a-option>
|
|
280
|
+
<a-optgroup v-if="mapping.type === 'date'" label="日期格式">
|
|
281
|
+
<a-option value="YYYY-MM-DD">YYYY-MM-DD</a-option>
|
|
282
|
+
<a-option value="YYYY-MM-DD HH:mm:ss">YYYY-MM-DD HH:mm:ss</a-option>
|
|
283
|
+
<a-option value="YYYY/MM/DD">YYYY/MM/DD</a-option>
|
|
284
|
+
<a-option value="timestamp">时间戳(毫秒)</a-option>
|
|
285
|
+
<a-option value="timestamp_s">时间戳(秒)</a-option>
|
|
286
|
+
</a-optgroup>
|
|
287
|
+
<a-optgroup v-if="mapping.type === 'number' || mapping.type === 'integer'" label="数字格式">
|
|
288
|
+
<a-option value="currency">货币(¥)</a-option>
|
|
289
|
+
<a-option value="percent">百分比(%)</a-option>
|
|
290
|
+
</a-optgroup>
|
|
291
|
+
<a-optgroup v-if="mapping.type === 'string'" label="字符串格式">
|
|
292
|
+
<a-option value="uppercase">大写</a-option>
|
|
293
|
+
<a-option value="lowercase">小写</a-option>
|
|
294
|
+
<a-option value="trim">去除空格</a-option>
|
|
295
|
+
</a-optgroup>
|
|
296
|
+
<a-optgroup label="通用格式">
|
|
297
|
+
<a-option value="json">JSON字符串</a-option>
|
|
298
|
+
</a-optgroup>
|
|
299
|
+
</a-select>
|
|
300
|
+
</div>
|
|
301
|
+
<a-button
|
|
302
|
+
size="mini"
|
|
303
|
+
type="text"
|
|
304
|
+
status="danger"
|
|
305
|
+
@click="removeFieldMapping(index)"
|
|
306
|
+
class="mapping-delete-btn"
|
|
307
|
+
>
|
|
308
|
+
删除
|
|
309
|
+
</a-button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div class="config-hint">
|
|
313
|
+
💡 提示: 来源字段支持多个备选(逗号分隔),系统会自动选择第一个有值的字段。例如: "content,title,message"
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- 保存按钮 -->
|
|
319
|
+
<div class="action-section">
|
|
320
|
+
<button @click="handleReset" class="btn-secondary">
|
|
321
|
+
重置
|
|
322
|
+
</button>
|
|
323
|
+
<button @click="handleSave" class="btn-primary" :disabled="!canSave">
|
|
324
|
+
保存配置
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</template>
|
|
331
|
+
|
|
332
|
+
<script>
|
|
333
|
+
import { ref, computed, watch, onMounted } from 'vue';
|
|
334
|
+
import { Tree as ATree, Select as ASelect, Option as AOption, Optgroup as AOptgroup } from '@arco-design/web-vue';
|
|
335
|
+
import '@arco-design/web-vue/es/tree/style/css.js';
|
|
336
|
+
import '@arco-design/web-vue/es/select/style/css.js';
|
|
337
|
+
import SchemaFieldEditor from './SchemaFieldEditor.vue';
|
|
338
|
+
import { filterApiTree, matchesPattern } from '../utils/treeUtils';
|
|
339
|
+
import { parseReqMapping, validateReqMapping } from '../utils/reqMappingUtils';
|
|
340
|
+
import { extractSchemaFields, buildResSchema } from '../utils/schemaUtils';
|
|
341
|
+
import { extractArray } from '../utils/dataFilters';
|
|
342
|
+
|
|
343
|
+
// 默认的 API 加载函数
|
|
344
|
+
const defaultApiLoader = async () => {
|
|
345
|
+
try {
|
|
346
|
+
// 尝试动态导入 @tuoyuan/gateway-request-lib
|
|
347
|
+
const { request } = await import('@tuoyuan/gateway-request-lib');
|
|
348
|
+
|
|
349
|
+
const response = await request({
|
|
350
|
+
api: '/api/system.domain.get_func_tree',
|
|
351
|
+
params: {},
|
|
352
|
+
config: {
|
|
353
|
+
access_token: true
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// gateway-request-lib 会自动解包响应,直接返回 data 部分
|
|
358
|
+
if (Array.isArray(response)) {
|
|
359
|
+
return response;
|
|
360
|
+
} else if (response?.data) {
|
|
361
|
+
if (Array.isArray(response.data)) {
|
|
362
|
+
return response.data;
|
|
363
|
+
} else if (response.data?.list) {
|
|
364
|
+
return response.data.list;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return [];
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('默认 API 加载器失败:', error);
|
|
371
|
+
// 返回 null 表示默认加载器不可用,让组件显示错误提示
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export default {
|
|
377
|
+
name: 'ApiConfig',
|
|
378
|
+
components: {
|
|
379
|
+
ATree,
|
|
380
|
+
ASelect,
|
|
381
|
+
AOption,
|
|
382
|
+
AOptgroup,
|
|
383
|
+
SchemaFieldEditor
|
|
384
|
+
},
|
|
385
|
+
props: {
|
|
386
|
+
// 输出数据结构模板(可选,如果 API 有 res_schema 则使用 API 的)
|
|
387
|
+
resSchema: {
|
|
388
|
+
type: Object,
|
|
389
|
+
required: false,
|
|
390
|
+
default: null
|
|
391
|
+
},
|
|
392
|
+
// 只显示匹配规则的API
|
|
393
|
+
includePattern: {
|
|
394
|
+
type: Array,
|
|
395
|
+
default: () => []
|
|
396
|
+
},
|
|
397
|
+
// 排除匹配规则的API
|
|
398
|
+
excludePattern: {
|
|
399
|
+
type: Array,
|
|
400
|
+
default: () => []
|
|
401
|
+
},
|
|
402
|
+
// 网关 API 列表接口地址
|
|
403
|
+
apiListUrl: {
|
|
404
|
+
type: String,
|
|
405
|
+
default: ''
|
|
406
|
+
},
|
|
407
|
+
// 自定义 API 列表加载函数(可选,如果不提供则使用默认加载器)
|
|
408
|
+
apiLoader: {
|
|
409
|
+
type: Function,
|
|
410
|
+
default: defaultApiLoader
|
|
411
|
+
},
|
|
412
|
+
// 初始配置(编辑模式)
|
|
413
|
+
initialConfig: {
|
|
414
|
+
type: Object,
|
|
415
|
+
default: null
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
emits: ['save', 'change'],
|
|
419
|
+
setup(props, { emit }) {
|
|
420
|
+
// 状态
|
|
421
|
+
const loading = ref(false);
|
|
422
|
+
const error = ref('');
|
|
423
|
+
const apiTree = ref([]);
|
|
424
|
+
const searchKeyword = ref('');
|
|
425
|
+
const filteredApiTree = ref([]);
|
|
426
|
+
const selectedApi = ref(null);
|
|
427
|
+
const selectedKeys = ref([]);
|
|
428
|
+
const expandedKeys = ref([]);
|
|
429
|
+
|
|
430
|
+
// 入参配置
|
|
431
|
+
const reqMappingMode = ref('fixed');
|
|
432
|
+
const reqMappingText = ref('{\n "page_no": 1,\n "page_size": 100\n}');
|
|
433
|
+
const reqMappingError = ref('');
|
|
434
|
+
|
|
435
|
+
// 输出字段路径
|
|
436
|
+
const fieldPaths = ref({});
|
|
437
|
+
|
|
438
|
+
// 输出字段格式化
|
|
439
|
+
const fieldFormats = ref({});
|
|
440
|
+
|
|
441
|
+
// 过滤器配置
|
|
442
|
+
const filterConfig = ref({
|
|
443
|
+
arrayPath: '', // 数组提取路径,如 'data.list'
|
|
444
|
+
fieldMapping: {} // 字段映射规则
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// 字段映射的数组形式(用于界面展示和编辑)
|
|
448
|
+
const filterFieldMappings = ref([
|
|
449
|
+
// { target: 'id', source: 'id', type: 'string', format: '' }
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
// 可用的字段类型
|
|
453
|
+
const fieldTypes = [
|
|
454
|
+
{ value: 'string', label: '字符串' },
|
|
455
|
+
{ value: 'number', label: '数字' },
|
|
456
|
+
{ value: 'integer', label: '整数' },
|
|
457
|
+
{ value: 'boolean', label: '布尔值' },
|
|
458
|
+
{ value: 'date', label: '日期' }
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
// 根据类型获取格式化选项
|
|
462
|
+
const getFormatOptions = (type) => {
|
|
463
|
+
switch (type) {
|
|
464
|
+
case 'date':
|
|
465
|
+
return [
|
|
466
|
+
{ value: '', label: '无' },
|
|
467
|
+
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
|
|
468
|
+
{ value: 'YYYY-MM-DD HH:mm:ss', label: 'YYYY-MM-DD HH:mm:ss' },
|
|
469
|
+
{ value: 'YYYY/MM/DD', label: 'YYYY/MM/DD' },
|
|
470
|
+
{ value: 'timestamp', label: '时间戳(毫秒)' },
|
|
471
|
+
{ value: 'timestamp_s', label: '时间戳(秒)' }
|
|
472
|
+
];
|
|
473
|
+
case 'number':
|
|
474
|
+
case 'integer':
|
|
475
|
+
return [
|
|
476
|
+
{ value: '', label: '无' },
|
|
477
|
+
{ value: 'currency', label: '货币' },
|
|
478
|
+
{ value: 'percent', label: '百分比' }
|
|
479
|
+
];
|
|
480
|
+
default:
|
|
481
|
+
return [{ value: '', label: '无' }];
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// 测试接口相关
|
|
486
|
+
const testingApi = ref(false);
|
|
487
|
+
const testApiResult = ref(null);
|
|
488
|
+
const detectedArrayPath = ref('');
|
|
489
|
+
const detectedFields = ref([]);
|
|
490
|
+
|
|
491
|
+
// 从 res_schema 提取字段列表
|
|
492
|
+
const resSchemaFields = computed(() => {
|
|
493
|
+
if (!currentResSchema.value) return [];
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
// 如果是数组类型,提取 items.properties 的字段
|
|
497
|
+
if (currentResSchema.value.type === 'array' && currentResSchema.value.items) {
|
|
498
|
+
const props = currentResSchema.value.items.properties;
|
|
499
|
+
if (props && typeof props === 'object') {
|
|
500
|
+
return Object.keys(props);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 如果是对象类型,提取 properties 的字段
|
|
505
|
+
if (currentResSchema.value.properties && typeof currentResSchema.value.properties === 'object') {
|
|
506
|
+
return Object.keys(currentResSchema.value.properties);
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.warn('提取 Schema 字段失败:', err);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return [];
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// 提取后的数组预览 (用于测试接口结果显示)
|
|
516
|
+
const extractedArrayPreview = computed(() => {
|
|
517
|
+
if (!testApiResult.value || !filterConfig.value.arrayPath) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
return extractArray(testApiResult.value, filterConfig.value.arrayPath);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.error('提取数组失败:', err);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// 测试接口
|
|
530
|
+
const handleTestApi = async () => {
|
|
531
|
+
if (!selectedApi.value) {
|
|
532
|
+
alert('请先选择一个 API');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
testingApi.value = true;
|
|
537
|
+
testApiResult.value = null;
|
|
538
|
+
detectedArrayPath.value = '';
|
|
539
|
+
detectedFields.value = [];
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const { request } = await import('@tuoyuan/gateway-request-lib');
|
|
543
|
+
|
|
544
|
+
// 解析入参
|
|
545
|
+
let params = {};
|
|
546
|
+
try {
|
|
547
|
+
if (reqMappingText.value && reqMappingText.value.trim() !== '{}') {
|
|
548
|
+
const parsed = JSON.parse(reqMappingText.value);
|
|
549
|
+
// 只取固定值类型的参数用于测试
|
|
550
|
+
params = Object.fromEntries(
|
|
551
|
+
Object.entries(parsed).filter(([_, value]) =>
|
|
552
|
+
typeof value !== 'string' || !value.includes('=>')
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.warn('解析入参失败,使用空参数测试:', err);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 调用接口并提取业务数据
|
|
561
|
+
const response = await request({
|
|
562
|
+
api: selectedApi.value.apiPath,
|
|
563
|
+
params: params,
|
|
564
|
+
config: { access_token: true }
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const businessData = response.response || response.data || response;
|
|
568
|
+
testApiResult.value = businessData;
|
|
569
|
+
|
|
570
|
+
// 自动检测数组路径和字段
|
|
571
|
+
detectArrayPathAndFields(businessData);
|
|
572
|
+
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error('测试接口失败:', error);
|
|
575
|
+
alert('测试接口失败: ' + error.message);
|
|
576
|
+
} finally {
|
|
577
|
+
testingApi.value = false;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// 检测数组路径和字段(合并两个函数)
|
|
582
|
+
const detectArrayPathAndFields = (data) => {
|
|
583
|
+
const possiblePaths = [];
|
|
584
|
+
let targetArray = [];
|
|
585
|
+
|
|
586
|
+
// 递归查找所有数组
|
|
587
|
+
const findArrays = (obj, path = '') => {
|
|
588
|
+
if (Array.isArray(obj)) {
|
|
589
|
+
possiblePaths.push({ path: path || '@', array: obj });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (obj && typeof obj === 'object') {
|
|
593
|
+
Object.keys(obj).forEach(key => {
|
|
594
|
+
findArrays(obj[key], path ? `${path}.${key}` : key);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
findArrays(data);
|
|
600
|
+
|
|
601
|
+
// 优先选择包含 list, data, items 的路径
|
|
602
|
+
const bestMatch = possiblePaths.find(p =>
|
|
603
|
+
p.path.includes('list') || p.path.includes('data') || p.path.includes('items')
|
|
604
|
+
) || possiblePaths[0];
|
|
605
|
+
|
|
606
|
+
if (bestMatch) {
|
|
607
|
+
detectedArrayPath.value = bestMatch.path;
|
|
608
|
+
targetArray = bestMatch.array;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 从数组第一个元素提取字段
|
|
612
|
+
if (targetArray.length > 0 && typeof targetArray[0] === 'object') {
|
|
613
|
+
detectedFields.value = Object.keys(targetArray[0]);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// 快速添加字段
|
|
618
|
+
const quickAddField = (fieldName) => {
|
|
619
|
+
if (filterFieldMappings.value.some(m => m.target === fieldName)) {
|
|
620
|
+
alert(`字段 "${fieldName}" 已存在`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
filterFieldMappings.value.push({
|
|
624
|
+
target: fieldName,
|
|
625
|
+
source: fieldName,
|
|
626
|
+
type: 'string',
|
|
627
|
+
format: ''
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// 应用检测到的路径
|
|
632
|
+
const applyDetectedPath = () => {
|
|
633
|
+
filterConfig.value.arrayPath = detectedArrayPath.value;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// 格式化 JSON
|
|
637
|
+
const formatJson = (data) => {
|
|
638
|
+
try {
|
|
639
|
+
return JSON.stringify(data, null, 2);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
return String(data);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// 添加/移除字段映射
|
|
646
|
+
const addFieldMapping = () => {
|
|
647
|
+
filterFieldMappings.value.push({
|
|
648
|
+
target: '',
|
|
649
|
+
source: '',
|
|
650
|
+
type: 'string',
|
|
651
|
+
format: ''
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const removeFieldMapping = (index) => {
|
|
656
|
+
filterFieldMappings.value.splice(index, 1);
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// 监听入参模式切换,自动更新示例
|
|
660
|
+
watch(reqMappingMode, (newMode) => {
|
|
661
|
+
// 使用默认示例(不依赖 req_schema,避免复杂性)
|
|
662
|
+
const examples = {
|
|
663
|
+
fixed: '{\n "page_no": 1,\n "page_size": 100\n}',
|
|
664
|
+
template: '{\n "page_no": "{{pageNum}}",\n "page_size": "{{pageSize}}"\n}',
|
|
665
|
+
function: '{\n "page_no": "(ctx) => Number(ctx.pageNum) || 1",\n "page_size": "(ctx) => Number(ctx.pageSize) || 10"\n}'
|
|
666
|
+
};
|
|
667
|
+
reqMappingText.value = examples[newMode];
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// 将 filterFieldMappings 转换为 filterConfig.fieldMapping
|
|
671
|
+
watch(filterFieldMappings, (mappings) => {
|
|
672
|
+
const fieldMapping = {};
|
|
673
|
+
const fieldTypes = {};
|
|
674
|
+
const fieldFormats = {};
|
|
675
|
+
|
|
676
|
+
mappings.forEach(mapping => {
|
|
677
|
+
if (mapping.target && mapping.source) {
|
|
678
|
+
// 将逗号分隔的源字段转换为数组
|
|
679
|
+
const sources = mapping.source.split(',').map(s => s.trim()).filter(s => s);
|
|
680
|
+
if (sources.length === 1) {
|
|
681
|
+
fieldMapping[mapping.target] = sources[0];
|
|
682
|
+
} else if (sources.length > 1) {
|
|
683
|
+
fieldMapping[mapping.target] = sources;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 保存类型和格式化配置
|
|
687
|
+
if (mapping.type) {
|
|
688
|
+
fieldTypes[mapping.target] = mapping.type;
|
|
689
|
+
}
|
|
690
|
+
if (mapping.format) {
|
|
691
|
+
fieldFormats[mapping.target] = mapping.format;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
filterConfig.value.fieldMapping = fieldMapping;
|
|
697
|
+
filterConfig.value.fieldTypes = fieldTypes;
|
|
698
|
+
filterConfig.value.fieldFormats = fieldFormats;
|
|
699
|
+
}, { deep: true });
|
|
700
|
+
|
|
701
|
+
// 转换为 Tree 组件数据格式
|
|
702
|
+
const treeData = computed(() => {
|
|
703
|
+
return convertToTreeData(filteredApiTree.value);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// 转换树数据为 Arco Tree 格式
|
|
707
|
+
const convertToTreeData = (nodes, parentPath = '') => {
|
|
708
|
+
return nodes.map((node, index) => {
|
|
709
|
+
// 生成唯一的 key: 使用路径 + 索引 + id 确保唯一性
|
|
710
|
+
const uniqueKey = parentPath ? `${parentPath}-${index}-${node.id}` : `${index}-${node.id}`;
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
key: uniqueKey,
|
|
714
|
+
title: node.name,
|
|
715
|
+
name: node.name,
|
|
716
|
+
apiPath: node.apiPath,
|
|
717
|
+
isLeaf: node.isLeaf,
|
|
718
|
+
_raw: node._raw,
|
|
719
|
+
selectable: !!node.isLeaf,
|
|
720
|
+
// 不要禁用非叶子节点,否则无法展开
|
|
721
|
+
children: node.children && node.children.length > 0
|
|
722
|
+
? convertToTreeData(node.children, uniqueKey)
|
|
723
|
+
: undefined
|
|
724
|
+
};
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// 收集所有节点的 key (用于搜索时全部展开)
|
|
729
|
+
const getAllNodeKeys = (nodes, parentPath = '', result = []) => {
|
|
730
|
+
nodes.forEach((node, index) => {
|
|
731
|
+
const uniqueKey = parentPath ? `${parentPath}-${index}-${node.id}` : `${index}-${node.id}`;
|
|
732
|
+
|
|
733
|
+
if (!node.isLeaf) {
|
|
734
|
+
result.push(uniqueKey);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (node.children && node.children.length > 0) {
|
|
738
|
+
getAllNodeKeys(node.children, uniqueKey, result);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
return result;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// 默认展开的节点:搜索时展开所有,否则只展开第一级
|
|
746
|
+
const defaultExpandedKeys = computed(() => {
|
|
747
|
+
if (searchKeyword.value) {
|
|
748
|
+
// 搜索时展开所有父节点
|
|
749
|
+
return getAllNodeKeys(filteredApiTree.value);
|
|
750
|
+
} else {
|
|
751
|
+
// 没有搜索时只展开第一级
|
|
752
|
+
return filteredApiTree.value.map(item => item.id);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// 监听 filteredApiTree 和 searchKeyword 的变化,更新展开的节点
|
|
757
|
+
watch([filteredApiTree, searchKeyword], () => {
|
|
758
|
+
expandedKeys.value = defaultExpandedKeys.value;
|
|
759
|
+
}, { immediate: true });
|
|
760
|
+
|
|
761
|
+
// 处理树节点选择
|
|
762
|
+
const handleTreeSelect = (selectedKeysList, data) => {
|
|
763
|
+
if (selectedKeysList.length === 0) return;
|
|
764
|
+
|
|
765
|
+
const selectedKey = selectedKeysList[0];
|
|
766
|
+
const node = data.node;
|
|
767
|
+
|
|
768
|
+
// 只处理叶子节点
|
|
769
|
+
if (node.isLeaf) {
|
|
770
|
+
selectedKeys.value = [selectedKey];
|
|
771
|
+
handleSelectApi(node);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// 处理树节点展开/折叠
|
|
776
|
+
const handleTreeExpand = (expandedKeysList) => {
|
|
777
|
+
console.log('Tree expand event:', expandedKeysList);
|
|
778
|
+
expandedKeys.value = expandedKeysList;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// 检测 schema 是否不完整
|
|
782
|
+
const isSchemaIncomplete = computed(() => {
|
|
783
|
+
if (!currentResSchema.value) return false;
|
|
784
|
+
|
|
785
|
+
// 检查是否有 items 但 properties 为空
|
|
786
|
+
if (currentResSchema.value.items) {
|
|
787
|
+
const props = currentResSchema.value.items.properties;
|
|
788
|
+
return !props || Object.keys(props).length === 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 检查顶层 properties 是否为空
|
|
792
|
+
const props = currentResSchema.value.properties;
|
|
793
|
+
return !props || Object.keys(props).length === 0;
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// 当前使用的 resSchema(优先使用 API 的 res_schema,否则使用 props.resSchema)
|
|
797
|
+
const currentResSchema = ref(props.resSchema);
|
|
798
|
+
|
|
799
|
+
// 当前使用的 reqSchema(从 API 的 req_schema 提取)
|
|
800
|
+
const currentReqSchema = ref(null);
|
|
801
|
+
|
|
802
|
+
// 请求参数字段列表
|
|
803
|
+
const reqSchemaFields = computed(() => {
|
|
804
|
+
if (!currentReqSchema.value) return [];
|
|
805
|
+
return extractSchemaFields(currentReqSchema.value);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const reqMappingPlaceholder = computed(() => {
|
|
809
|
+
// 默认示例
|
|
810
|
+
const placeholders = {
|
|
811
|
+
fixed: '{\n "page_no": 1,\n "page_size": 100\n}',
|
|
812
|
+
template: '{\n "page_no": "{{pageNum}}",\n "page_size": "{{pageSize}}"\n}',
|
|
813
|
+
function: '{\n "page_no": "(ctx) => Number(ctx.pageNum) || 1",\n "page_size": "(ctx) => Number(ctx.pageSize) || 10"\n}'
|
|
814
|
+
};
|
|
815
|
+
return placeholders[reqMappingMode.value];
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const canSave = computed(() => {
|
|
819
|
+
if (!selectedApi.value) return false;
|
|
820
|
+
|
|
821
|
+
// 需要至少配置一个字段映射
|
|
822
|
+
return filterFieldMappings.value.some(m => m.target && m.source) && !reqMappingError.value;
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// 检测缺少的必填参数
|
|
826
|
+
const missingRequiredParams = computed(() => {
|
|
827
|
+
if (!currentReqSchema.value || !reqSchemaFields.value.length) return [];
|
|
828
|
+
|
|
829
|
+
// 解析当前的 reqMapping
|
|
830
|
+
let currentParams = {};
|
|
831
|
+
try {
|
|
832
|
+
if (reqMappingText.value && reqMappingText.value.trim() !== '{}') {
|
|
833
|
+
currentParams = JSON.parse(reqMappingText.value);
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
return [];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 找出缺少的必填参数
|
|
840
|
+
const missing = reqSchemaFields.value
|
|
841
|
+
.filter(field => field.required)
|
|
842
|
+
.filter(field => {
|
|
843
|
+
const value = currentParams[field.name];
|
|
844
|
+
return value === undefined || value === null || value === '';
|
|
845
|
+
})
|
|
846
|
+
.map(field => field.name);
|
|
847
|
+
|
|
848
|
+
return missing;
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// 加载 API 列表
|
|
852
|
+
const loadApiList = async () => {
|
|
853
|
+
loading.value = true;
|
|
854
|
+
error.value = '';
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
let data;
|
|
858
|
+
|
|
859
|
+
// 优先使用 apiLoader(包括默认的)
|
|
860
|
+
if (props.apiLoader) {
|
|
861
|
+
data = await props.apiLoader();
|
|
862
|
+
|
|
863
|
+
// 如果默认加载器返回 null,说明不可用
|
|
864
|
+
if (data === null) {
|
|
865
|
+
error.value = '请配置 apiListUrl 或提供 apiLoader 函数';
|
|
866
|
+
loading.value = false;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// 其次使用配置的 URL
|
|
871
|
+
else if (props.apiListUrl) {
|
|
872
|
+
const response = await fetch(props.apiListUrl);
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
875
|
+
}
|
|
876
|
+
const result = await response.json();
|
|
877
|
+
|
|
878
|
+
// 适配网关接口返回格式:{ code, msg, data: { list } }
|
|
879
|
+
if (result.code === 10000 && result.data && result.data.list) {
|
|
880
|
+
data = result.data.list;
|
|
881
|
+
} else if (Array.isArray(result)) {
|
|
882
|
+
// 如果直接返回数组
|
|
883
|
+
data = result;
|
|
884
|
+
} else if (result.data && Array.isArray(result.data)) {
|
|
885
|
+
// 如果 data 是数组
|
|
886
|
+
data = result.data;
|
|
887
|
+
} else {
|
|
888
|
+
throw new Error(result.msg || '接口返回数据格式错误');
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// 转换数据结构:将 funcs 数组转换为 children
|
|
893
|
+
const transformTree = (nodes) => {
|
|
894
|
+
return nodes.map(node => {
|
|
895
|
+
const transformed = {
|
|
896
|
+
id: String(node.id || node.func_id),
|
|
897
|
+
name: node.name || node.func_name,
|
|
898
|
+
children: [],
|
|
899
|
+
expanded: false
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// 处理子分类
|
|
903
|
+
if (node.children && node.children.length > 0) {
|
|
904
|
+
transformed.children.push(...transformTree(node.children));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// 处理 funcs 数组 - 将其转换为子节点
|
|
908
|
+
if (node.funcs && node.funcs.length > 0) {
|
|
909
|
+
const funcNodes = node.funcs.map(func => {
|
|
910
|
+
// 构建显示用的路径(用于过滤和显示)
|
|
911
|
+
let displayPath = func.api || func.path || func.func_path || func.api_path;
|
|
912
|
+
|
|
913
|
+
// 如果没有现成的路径,尝试构建
|
|
914
|
+
if (!displayPath && func.meta_domain && func.tag) {
|
|
915
|
+
// 查找 meta_domain 对应的域名
|
|
916
|
+
const findDomainName = (nodes, domainId) => {
|
|
917
|
+
for (const n of nodes) {
|
|
918
|
+
if (n.id === domainId) return n.name;
|
|
919
|
+
if (n.children) {
|
|
920
|
+
const found = findDomainName(n.children, domainId);
|
|
921
|
+
if (found) return found;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const domainName = findDomainName(data, func.meta_domain);
|
|
928
|
+
displayPath = `/api/${func.tag}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// 如果还是没有,使用名称
|
|
932
|
+
if (!displayPath) {
|
|
933
|
+
displayPath = func.name || '(未知路径)';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
id: String(func.id || func.func_id),
|
|
938
|
+
name: func.name || func.func_name || '(未命名)',
|
|
939
|
+
apiPath: displayPath, // 显示用路径
|
|
940
|
+
isLeaf: true,
|
|
941
|
+
_raw: func
|
|
942
|
+
};
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
transformed.children.push(...funcNodes);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// 如果没有子节点,也不是 func,标记为叶子
|
|
949
|
+
if (transformed.children.length === 0 && !node.funcs && !node.children) {
|
|
950
|
+
transformed.isLeaf = true;
|
|
951
|
+
transformed.apiPath = node.path || node.name;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return transformed;
|
|
955
|
+
});
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
apiTree.value = transformTree(data);
|
|
959
|
+
applyFilters();
|
|
960
|
+
} catch (err) {
|
|
961
|
+
error.value = '加载 API 列表失败:' + err.message;
|
|
962
|
+
console.error('加载 API 列表失败:', err);
|
|
963
|
+
} finally {
|
|
964
|
+
loading.value = false;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// 应用过滤规则
|
|
969
|
+
const applyFilters = () => {
|
|
970
|
+
let filtered = [...apiTree.value];
|
|
971
|
+
|
|
972
|
+
// 应用 include 和 exclude 规则
|
|
973
|
+
if (props.includePattern.length > 0 || props.excludePattern.length > 0) {
|
|
974
|
+
filtered = filterByPatterns(filtered, props.includePattern, props.excludePattern);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// 应用搜索关键词
|
|
978
|
+
if (searchKeyword.value) {
|
|
979
|
+
filtered = filterApiTree(filtered, searchKeyword.value);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
filteredApiTree.value = filtered;
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// 根据 pattern 过滤(简化版)
|
|
986
|
+
const filterByPatterns = (tree, includePatterns, excludePatterns) => {
|
|
987
|
+
const filterNode = (node) => {
|
|
988
|
+
const isLeaf = node.isLeaf || node.apiPath;
|
|
989
|
+
|
|
990
|
+
// 叶子节点:应用过滤规则
|
|
991
|
+
if (isLeaf && node.apiPath) {
|
|
992
|
+
// 先排除,再包含
|
|
993
|
+
if (excludePatterns.some(p => matchesPattern(node.apiPath, p))) return null;
|
|
994
|
+
if (includePatterns.length > 0 && !includePatterns.some(p => matchesPattern(node.apiPath, p))) return null;
|
|
995
|
+
return node;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// 分类节点:递归过滤子节点
|
|
999
|
+
if (node.children?.length > 0) {
|
|
1000
|
+
const filteredChildren = node.children.map(filterNode).filter(Boolean);
|
|
1001
|
+
return filteredChildren.length > 0 ? { ...node, children: filteredChildren } : null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return null;
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return tree.map(filterNode).filter(Boolean);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// 搜索处理
|
|
1011
|
+
const handleSearch = () => {
|
|
1012
|
+
applyFilters();
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// 提取 res_schema(简化复杂的嵌套逻辑)
|
|
1016
|
+
const extractResSchema = (apiResSchema, apiPath) => {
|
|
1017
|
+
if (!apiResSchema) return null;
|
|
1018
|
+
|
|
1019
|
+
let targetSchema = apiResSchema;
|
|
1020
|
+
let listPath = null;
|
|
1021
|
+
|
|
1022
|
+
// 按优先级查找 list 路径
|
|
1023
|
+
const paths = [
|
|
1024
|
+
{ check: () => apiResSchema.properties?.data?.properties?.list, path: '@.data.list', schema: () => apiResSchema.properties.data.properties.list },
|
|
1025
|
+
{ check: () => apiResSchema.properties?.list, path: '@.list', schema: () => apiResSchema.properties.list },
|
|
1026
|
+
{ check: () => apiResSchema.type === 'array', path: '@', schema: () => apiResSchema },
|
|
1027
|
+
];
|
|
1028
|
+
|
|
1029
|
+
for (const { check, path, schema } of paths) {
|
|
1030
|
+
if (check()) {
|
|
1031
|
+
listPath = path;
|
|
1032
|
+
targetSchema = schema();
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// 如果没找到但是列表接口,推测结构
|
|
1038
|
+
if (!listPath && apiPath?.includes('list') && apiResSchema.properties?.data) {
|
|
1039
|
+
listPath = '@.data.list';
|
|
1040
|
+
targetSchema = { type: 'array', items: { type: 'object', properties: {} } };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// 构建最终 schema
|
|
1044
|
+
if (listPath && targetSchema?.type === 'array') {
|
|
1045
|
+
return {
|
|
1046
|
+
type: 'array',
|
|
1047
|
+
x_path: listPath,
|
|
1048
|
+
items: targetSchema.items
|
|
1049
|
+
};
|
|
1050
|
+
} else if (listPath) {
|
|
1051
|
+
return { ...targetSchema, x_path: listPath };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return targetSchema;
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// 选择 API
|
|
1058
|
+
const handleSelectApi = (api) => {
|
|
1059
|
+
selectedApi.value = api;
|
|
1060
|
+
|
|
1061
|
+
// 重置所有配置
|
|
1062
|
+
reqMappingText.value = '{\n "page_no": 1,\n "page_size": 100\n}';
|
|
1063
|
+
reqMappingError.value = '';
|
|
1064
|
+
fieldPaths.value = {};
|
|
1065
|
+
fieldFormats.value = {};
|
|
1066
|
+
filterConfig.value = { arrayPath: '', fieldMapping: {} };
|
|
1067
|
+
filterFieldMappings.value = [];
|
|
1068
|
+
testApiResult.value = null;
|
|
1069
|
+
detectedArrayPath.value = '';
|
|
1070
|
+
detectedFields.value = [];
|
|
1071
|
+
|
|
1072
|
+
// 提取 schema
|
|
1073
|
+
const apiResSchema = api._raw?.res_schema || api._raw?.schema?.res_schema || api.res_schema;
|
|
1074
|
+
const apiReqSchema = api._raw?.req_schema || api._raw?.schema?.req_schema || api.req_schema;
|
|
1075
|
+
|
|
1076
|
+
currentResSchema.value = extractResSchema(apiResSchema, api.apiPath) || props.resSchema;
|
|
1077
|
+
currentReqSchema.value = apiReqSchema;
|
|
1078
|
+
|
|
1079
|
+
// 自动设置 arrayPath
|
|
1080
|
+
if (currentResSchema.value?.x_path) {
|
|
1081
|
+
filterConfig.value.arrayPath = currentResSchema.value.x_path.replace(/^@\./, '');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 自动填充默认参数
|
|
1085
|
+
if (apiReqSchema && reqSchemaFields.value.length > 0) {
|
|
1086
|
+
const defaultParams = Object.fromEntries(
|
|
1087
|
+
reqSchemaFields.value.map(f => [
|
|
1088
|
+
f.name,
|
|
1089
|
+
(f.type === 'number' || f.type === 'integer') ? (f.name.includes('page') ? 1 : 0) :
|
|
1090
|
+
f.type === 'boolean' ? true : ''
|
|
1091
|
+
])
|
|
1092
|
+
);
|
|
1093
|
+
reqMappingText.value = JSON.stringify(defaultParams, null, 2);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
emit('change', { api, config: null, resSchema: currentResSchema.value });
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// 更新字段路径
|
|
1100
|
+
const handleUpdateFieldPath = ({ key, path }) => {
|
|
1101
|
+
fieldPaths.value[key] = path;
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// 更新字段格式化
|
|
1105
|
+
const handleUpdateFieldFormat = ({ key, format }) => {
|
|
1106
|
+
fieldFormats.value[key] = format;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// 验证入参配置
|
|
1110
|
+
watch([reqMappingText, reqMappingMode], () => {
|
|
1111
|
+
try {
|
|
1112
|
+
const parsed = parseReqMapping(reqMappingText.value, reqMappingMode.value);
|
|
1113
|
+
const validation = validateReqMapping(parsed, reqMappingMode.value);
|
|
1114
|
+
reqMappingError.value = validation.error || '';
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
reqMappingError.value = err.message;
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// 保存配置
|
|
1121
|
+
const handleSave = () => {
|
|
1122
|
+
if (!canSave.value) return;
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
// 解析入参配置
|
|
1126
|
+
let reqMapping = null;
|
|
1127
|
+
if (reqMappingText.value.trim() !== '{}' && reqMappingText.value.trim() !== '') {
|
|
1128
|
+
reqMapping = parseReqMapping(reqMappingText.value, reqMappingMode.value);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// 直接使用 apiPath,不做任何转换
|
|
1132
|
+
const realApiPath = selectedApi.value.apiPath;
|
|
1133
|
+
|
|
1134
|
+
// 构建过滤器配置
|
|
1135
|
+
const config = {
|
|
1136
|
+
apiPath: realApiPath,
|
|
1137
|
+
reqMapping: reqMapping,
|
|
1138
|
+
mode: 'filter',
|
|
1139
|
+
filterConfig: {
|
|
1140
|
+
arrayPath: filterConfig.value.arrayPath || null,
|
|
1141
|
+
fieldMapping: filterConfig.value.fieldMapping,
|
|
1142
|
+
fieldTypes: filterConfig.value.fieldTypes || {},
|
|
1143
|
+
fieldFormats: filterConfig.value.fieldFormats || {}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
emit('save', config);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
alert('保存失败:' + err.message);
|
|
1150
|
+
console.error('保存配置失败:', err);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// 重置
|
|
1155
|
+
const handleReset = () => {
|
|
1156
|
+
if (confirm('确定要重置所有配置吗?')) {
|
|
1157
|
+
selectedApi.value = null;
|
|
1158
|
+
reqMappingText.value = '{\n "page_no": 1,\n "page_size": 100\n}';
|
|
1159
|
+
reqMappingError.value = '';
|
|
1160
|
+
fieldPaths.value = {};
|
|
1161
|
+
fieldFormats.value = {};
|
|
1162
|
+
filterConfig.value = {
|
|
1163
|
+
arrayPath: '',
|
|
1164
|
+
fieldMapping: {},
|
|
1165
|
+
fieldTypes: {},
|
|
1166
|
+
fieldFormats: {}
|
|
1167
|
+
};
|
|
1168
|
+
filterFieldMappings.value = [];
|
|
1169
|
+
searchKeyword.value = '';
|
|
1170
|
+
applyFilters();
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// 加载初始配置
|
|
1175
|
+
const loadInitialConfig = () => {
|
|
1176
|
+
if (!props.initialConfig) return;
|
|
1177
|
+
|
|
1178
|
+
const config = props.initialConfig;
|
|
1179
|
+
|
|
1180
|
+
// TODO: 根据 apiPath 查找并选中对应的 API
|
|
1181
|
+
// 这需要在 API 列表加载完成后执行
|
|
1182
|
+
|
|
1183
|
+
if (config.reqMapping) {
|
|
1184
|
+
// 检测入参配置类型
|
|
1185
|
+
const firstValue = Object.values(config.reqMapping)[0];
|
|
1186
|
+
if (typeof firstValue === 'string' && firstValue.includes('${')) {
|
|
1187
|
+
reqMappingMode.value = 'template';
|
|
1188
|
+
} else if (typeof firstValue === 'string' && firstValue.includes('=>')) {
|
|
1189
|
+
reqMappingMode.value = 'function';
|
|
1190
|
+
} else {
|
|
1191
|
+
reqMappingMode.value = 'fixed';
|
|
1192
|
+
}
|
|
1193
|
+
reqMappingText.value = JSON.stringify(config.reqMapping, null, 2);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// 加载 Filter 配置
|
|
1197
|
+
if (config.filterConfig) {
|
|
1198
|
+
const fc = config.filterConfig;
|
|
1199
|
+
filterConfig.value.arrayPath = fc.arrayPath || '';
|
|
1200
|
+
filterConfig.value.fieldMapping = fc.fieldMapping || {};
|
|
1201
|
+
filterConfig.value.fieldTypes = fc.fieldTypes || {};
|
|
1202
|
+
filterConfig.value.fieldFormats = fc.fieldFormats || {};
|
|
1203
|
+
|
|
1204
|
+
// 转换为数组形式用于编辑
|
|
1205
|
+
filterFieldMappings.value = [];
|
|
1206
|
+
Object.entries(fc.fieldMapping || {}).forEach(([target, source]) => {
|
|
1207
|
+
const sourceStr = Array.isArray(source) ? source.join(',') : source;
|
|
1208
|
+
filterFieldMappings.value.push({
|
|
1209
|
+
target,
|
|
1210
|
+
source: sourceStr,
|
|
1211
|
+
type: fc.fieldTypes?.[target] || 'string',
|
|
1212
|
+
format: fc.fieldFormats?.[target] || ''
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// 生命周期
|
|
1219
|
+
onMounted(() => {
|
|
1220
|
+
loadApiList();
|
|
1221
|
+
loadInitialConfig();
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// 监听 props 变化
|
|
1225
|
+
watch([() => props.includePattern, () => props.excludePattern], () => {
|
|
1226
|
+
applyFilters();
|
|
1227
|
+
}, { deep: true });
|
|
1228
|
+
|
|
1229
|
+
// 暴露给模板使用的状态和方法
|
|
1230
|
+
return {
|
|
1231
|
+
loading, // API 列表加载状态
|
|
1232
|
+
error, // 错误信息
|
|
1233
|
+
searchKeyword, // 搜索关键词(双向绑定)
|
|
1234
|
+
filteredApiTree, // 过滤后的 API 树结构
|
|
1235
|
+
treeData, // Tree 组件数据
|
|
1236
|
+
defaultExpandedKeys, // 默认展开的 keys
|
|
1237
|
+
expandedKeys, // 当前展开的 keys (受控)
|
|
1238
|
+
selectedApi, // 当前选中的 API
|
|
1239
|
+
selectedKeys, // 选中的 keys
|
|
1240
|
+
filterConfig, // 过滤器配置对象
|
|
1241
|
+
filterFieldMappings, // 字段映射数组(用于界面展示)
|
|
1242
|
+
testingApi, // 测试接口中
|
|
1243
|
+
testApiResult, // 测试接口结果
|
|
1244
|
+
extractedArrayPreview, // 提取后的数组预览
|
|
1245
|
+
detectedArrayPath, // 检测到的数组路径
|
|
1246
|
+
detectedFields, // 检测到的字段列表
|
|
1247
|
+
resSchemaFields, // 从 res_schema 提取的字段
|
|
1248
|
+
reqMappingMode, // 入参配置模式:fixed/template/function
|
|
1249
|
+
reqMappingText, // 入参配置文本内容(双向绑定)
|
|
1250
|
+
reqMappingError, // 入参配置验证错误信息
|
|
1251
|
+
reqMappingPlaceholder, // 入参配置输入框占位符
|
|
1252
|
+
fieldPaths, // 字段路径映射 { fieldKey: path }
|
|
1253
|
+
fieldFormats, // 字段格式化配置 { fieldKey: format }
|
|
1254
|
+
currentResSchema, // 当前使用的响应结构 schema
|
|
1255
|
+
currentReqSchema, // 当前使用的请求参数 schema
|
|
1256
|
+
reqSchemaFields, // 请求参数字段列表
|
|
1257
|
+
isSchemaIncomplete, // schema 是否不完整(用于显示警告)
|
|
1258
|
+
canSave, // 是否可以保存(所有必填字段已配置)
|
|
1259
|
+
missingRequiredParams, // 缺少的必填参数列表
|
|
1260
|
+
handleSearch, // 搜索处理方法
|
|
1261
|
+
handleTreeSelect, // Tree 选择处理方法
|
|
1262
|
+
handleTreeExpand, // Tree 展开/折叠处理方法
|
|
1263
|
+
handleUpdateFieldPath, // 更新字段路径方法
|
|
1264
|
+
handleUpdateFieldFormat, // 更新字段格式化方法
|
|
1265
|
+
handleTestApi, // 测试接口方法
|
|
1266
|
+
applyDetectedPath, // 应用检测到的路径
|
|
1267
|
+
quickAddField, // 快速添加字段
|
|
1268
|
+
formatJson, // 格式化 JSON
|
|
1269
|
+
addFieldMapping, // 添加字段映射
|
|
1270
|
+
removeFieldMapping, // 移除字段映射
|
|
1271
|
+
handleSave, // 保存方法
|
|
1272
|
+
handleReset // 重置方法
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
</script>
|
|
1277
|
+
|
|
1278
|
+
<style scoped>
|
|
1279
|
+
.api-config-component {
|
|
1280
|
+
display: flex;
|
|
1281
|
+
gap: 24px;
|
|
1282
|
+
height: 700px;
|
|
1283
|
+
background: #f5f7fa;
|
|
1284
|
+
border-radius: 12px;
|
|
1285
|
+
padding: 24px;
|
|
1286
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* 左侧面板 */
|
|
1290
|
+
.left-panel {
|
|
1291
|
+
flex: 0 0 380px;
|
|
1292
|
+
display: flex;
|
|
1293
|
+
flex-direction: column;
|
|
1294
|
+
background: #ffffff;
|
|
1295
|
+
border-radius: 12px;
|
|
1296
|
+
padding: 20px;
|
|
1297
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
1298
|
+
height: 100%;
|
|
1299
|
+
overflow: hidden;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.panel-header h3 {
|
|
1303
|
+
margin: 0 0 20px 0;
|
|
1304
|
+
font-size: 20px;
|
|
1305
|
+
font-weight: 600;
|
|
1306
|
+
color: #1f2937;
|
|
1307
|
+
letter-spacing: -0.3px;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/* 右侧面板 */
|
|
1311
|
+
.right-panel {
|
|
1312
|
+
flex: 1;
|
|
1313
|
+
display: flex;
|
|
1314
|
+
flex-direction: column;
|
|
1315
|
+
background: #ffffff;
|
|
1316
|
+
border-radius: 12px;
|
|
1317
|
+
padding: 20px;
|
|
1318
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
1319
|
+
height: 100%;
|
|
1320
|
+
overflow-y: auto;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.config-container {
|
|
1324
|
+
display: flex;
|
|
1325
|
+
flex-direction: column;
|
|
1326
|
+
gap: 24px;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/* 空状态 */
|
|
1330
|
+
.empty-state {
|
|
1331
|
+
display: flex;
|
|
1332
|
+
flex-direction: column;
|
|
1333
|
+
align-items: center;
|
|
1334
|
+
justify-content: center;
|
|
1335
|
+
height: 100%;
|
|
1336
|
+
color: #9ca3af;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.empty-icon {
|
|
1340
|
+
font-size: 64px;
|
|
1341
|
+
margin-bottom: 16px;
|
|
1342
|
+
opacity: 0.5;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.empty-text {
|
|
1346
|
+
font-size: 16px;
|
|
1347
|
+
font-weight: 500;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/* 标题样式 */
|
|
1351
|
+
.section-title {
|
|
1352
|
+
font-size: 14px;
|
|
1353
|
+
font-weight: 600;
|
|
1354
|
+
color: #374151;
|
|
1355
|
+
margin-bottom: 12px;
|
|
1356
|
+
display: flex;
|
|
1357
|
+
align-items: center;
|
|
1358
|
+
gap: 8px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.section-subtitle {
|
|
1362
|
+
font-size: 12px;
|
|
1363
|
+
font-weight: normal;
|
|
1364
|
+
color: #9ca3af;
|
|
1365
|
+
margin-left: 4px;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/* 搜索框 */
|
|
1369
|
+
.search-box {
|
|
1370
|
+
position: relative;
|
|
1371
|
+
margin-bottom: 20px;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.search-input {
|
|
1375
|
+
width: 100%;
|
|
1376
|
+
padding: 10px 36px 10px 12px;
|
|
1377
|
+
border: 1px solid #e5e7eb;
|
|
1378
|
+
border-radius: 8px;
|
|
1379
|
+
font-size: 14px;
|
|
1380
|
+
box-sizing: border-box;
|
|
1381
|
+
transition: all 0.2s ease;
|
|
1382
|
+
background: #f9fafb;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
.search-input:hover {
|
|
1386
|
+
border-color: #d1d5db;
|
|
1387
|
+
background: #ffffff;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.search-input:focus {
|
|
1391
|
+
outline: none;
|
|
1392
|
+
border-color: #667eea;
|
|
1393
|
+
background: #ffffff;
|
|
1394
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.search-icon {
|
|
1398
|
+
position: absolute;
|
|
1399
|
+
right: 12px;
|
|
1400
|
+
top: 50%;
|
|
1401
|
+
transform: translateY(-50%);
|
|
1402
|
+
font-size: 16px;
|
|
1403
|
+
opacity: 0.4;
|
|
1404
|
+
pointer-events: none;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/* API 树容器 */
|
|
1408
|
+
.api-tree-container {
|
|
1409
|
+
flex: 1;
|
|
1410
|
+
border: 1px solid #e5e7eb;
|
|
1411
|
+
border-radius: 8px;
|
|
1412
|
+
overflow: hidden;
|
|
1413
|
+
background: #ffffff;
|
|
1414
|
+
min-height: 400px;
|
|
1415
|
+
display: flex;
|
|
1416
|
+
flex-direction: column;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
.loading,
|
|
1420
|
+
.error,
|
|
1421
|
+
.empty {
|
|
1422
|
+
text-align: center;
|
|
1423
|
+
padding: 40px 20px;
|
|
1424
|
+
color: #6b7280;
|
|
1425
|
+
font-size: 14px;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
.error {
|
|
1429
|
+
color: #ef4444;
|
|
1430
|
+
background: #fef2f2;
|
|
1431
|
+
border-radius: 6px;
|
|
1432
|
+
padding: 12px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.empty {
|
|
1436
|
+
color: #9ca3af;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/* 选中的 API 信息 */
|
|
1440
|
+
.selected-api-info {
|
|
1441
|
+
margin-bottom: 0;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.api-info-card {
|
|
1445
|
+
background: #f8fafc;
|
|
1446
|
+
border: 1px solid #e2e8f0;
|
|
1447
|
+
border-radius: 8px;
|
|
1448
|
+
padding: 16px;
|
|
1449
|
+
color: #334155;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.info-row {
|
|
1453
|
+
margin-bottom: 10px;
|
|
1454
|
+
font-size: 13px;
|
|
1455
|
+
display: flex;
|
|
1456
|
+
align-items: flex-start;
|
|
1457
|
+
gap: 8px;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.info-row:last-child {
|
|
1461
|
+
margin-bottom: 0;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.info-row .label {
|
|
1465
|
+
font-weight: 600;
|
|
1466
|
+
color: #64748b;
|
|
1467
|
+
min-width: 70px;
|
|
1468
|
+
flex-shrink: 0;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.info-row .value {
|
|
1472
|
+
font-weight: 400;
|
|
1473
|
+
flex: 1;
|
|
1474
|
+
word-break: break-all;
|
|
1475
|
+
color: #334155;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.info-row .value.code {
|
|
1479
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
1480
|
+
background: #ffffff;
|
|
1481
|
+
padding: 6px 10px;
|
|
1482
|
+
border-radius: 4px;
|
|
1483
|
+
font-size: 12px;
|
|
1484
|
+
border: 1px solid #e2e8f0;
|
|
1485
|
+
color: #0f172a;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/* 入参配置 */
|
|
1489
|
+
.req-mapping-section {
|
|
1490
|
+
margin-bottom: 0;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.req-schema-hint {
|
|
1494
|
+
margin-bottom: 16px;
|
|
1495
|
+
padding: 16px;
|
|
1496
|
+
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
1497
|
+
border-radius: 8px;
|
|
1498
|
+
border: 2px solid #bae6fd;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.hint-title {
|
|
1502
|
+
font-size: 13px;
|
|
1503
|
+
font-weight: 600;
|
|
1504
|
+
color: #0369a1;
|
|
1505
|
+
margin-bottom: 12px;
|
|
1506
|
+
display: flex;
|
|
1507
|
+
align-items: center;
|
|
1508
|
+
gap: 6px;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.req-params-list {
|
|
1512
|
+
display: flex;
|
|
1513
|
+
flex-direction: column;
|
|
1514
|
+
gap: 8px;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.param-item {
|
|
1518
|
+
display: flex;
|
|
1519
|
+
align-items: center;
|
|
1520
|
+
gap: 10px;
|
|
1521
|
+
padding: 10px 12px;
|
|
1522
|
+
background: #ffffff;
|
|
1523
|
+
border-radius: 6px;
|
|
1524
|
+
border: 1px solid #e0f2fe;
|
|
1525
|
+
font-size: 13px;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.param-name {
|
|
1529
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
1530
|
+
font-weight: 600;
|
|
1531
|
+
color: #0c4a6e;
|
|
1532
|
+
min-width: 100px;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.param-type {
|
|
1536
|
+
font-size: 11px;
|
|
1537
|
+
color: #0284c7;
|
|
1538
|
+
background: #e0f2fe;
|
|
1539
|
+
padding: 3px 8px;
|
|
1540
|
+
border-radius: 4px;
|
|
1541
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
1542
|
+
font-weight: 600;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.param-required {
|
|
1546
|
+
font-size: 11px;
|
|
1547
|
+
color: #dc2626;
|
|
1548
|
+
background: #fee2e2;
|
|
1549
|
+
padding: 3px 8px;
|
|
1550
|
+
border-radius: 4px;
|
|
1551
|
+
font-weight: 600;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.param-optional {
|
|
1555
|
+
font-size: 11px;
|
|
1556
|
+
color: #6b7280;
|
|
1557
|
+
background: #f3f4f6;
|
|
1558
|
+
padding: 3px 8px;
|
|
1559
|
+
border-radius: 4px;
|
|
1560
|
+
font-weight: 600;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
.param-desc {
|
|
1564
|
+
color: #64748b;
|
|
1565
|
+
font-size: 12px;
|
|
1566
|
+
flex: 1;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/* 缺少必填参数警告 */
|
|
1570
|
+
.missing-params-warning {
|
|
1571
|
+
margin-top: 12px;
|
|
1572
|
+
padding: 14px;
|
|
1573
|
+
background: #fef3c7;
|
|
1574
|
+
border-radius: 8px;
|
|
1575
|
+
border: 2px solid #f59e0b;
|
|
1576
|
+
display: flex;
|
|
1577
|
+
gap: 12px;
|
|
1578
|
+
align-items: flex-start;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
.warning-icon {
|
|
1582
|
+
font-size: 24px;
|
|
1583
|
+
flex-shrink: 0;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.warning-content {
|
|
1587
|
+
flex: 1;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.warning-title {
|
|
1591
|
+
font-size: 14px;
|
|
1592
|
+
font-weight: 700;
|
|
1593
|
+
color: #92400e;
|
|
1594
|
+
margin-bottom: 6px;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.warning-text {
|
|
1598
|
+
font-size: 13px;
|
|
1599
|
+
color: #78350f;
|
|
1600
|
+
line-height: 1.6;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.warning-text strong {
|
|
1604
|
+
color: #dc2626;
|
|
1605
|
+
font-weight: 700;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
.req-mode-selector {
|
|
1609
|
+
margin-bottom: 12px;
|
|
1610
|
+
display: flex;
|
|
1611
|
+
gap: 8px;
|
|
1612
|
+
background: #f9fafb;
|
|
1613
|
+
padding: 6px;
|
|
1614
|
+
border-radius: 8px;
|
|
1615
|
+
border: 1px solid #e5e7eb;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.req-mode-selector label {
|
|
1619
|
+
flex: 1;
|
|
1620
|
+
display: flex;
|
|
1621
|
+
flex-direction: column;
|
|
1622
|
+
align-items: center;
|
|
1623
|
+
justify-content: center;
|
|
1624
|
+
gap: 4px;
|
|
1625
|
+
font-size: 13px;
|
|
1626
|
+
cursor: pointer;
|
|
1627
|
+
padding: 10px 12px;
|
|
1628
|
+
border-radius: 6px;
|
|
1629
|
+
transition: all 0.2s ease;
|
|
1630
|
+
color: #6b7280;
|
|
1631
|
+
font-weight: 500;
|
|
1632
|
+
position: relative;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.req-mode-selector label:hover {
|
|
1636
|
+
background: #ffffff;
|
|
1637
|
+
color: #374151;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.req-mode-selector label:has(input[type="radio"]:checked) {
|
|
1641
|
+
background: #ffffff;
|
|
1642
|
+
color: #667eea;
|
|
1643
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.req-mode-selector input[type="radio"] {
|
|
1647
|
+
position: absolute;
|
|
1648
|
+
opacity: 0;
|
|
1649
|
+
width: 0;
|
|
1650
|
+
height: 0;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.req-mode-selector .mode-hint {
|
|
1654
|
+
font-size: 11px;
|
|
1655
|
+
color: #9ca3af;
|
|
1656
|
+
font-weight: 400;
|
|
1657
|
+
text-align: center;
|
|
1658
|
+
line-height: 1.3;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
.req-mode-selector label:has(input[type="radio"]:checked) .mode-hint {
|
|
1662
|
+
color: #818cf8;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.json-editor {
|
|
1666
|
+
width: 100%;
|
|
1667
|
+
padding: 12px;
|
|
1668
|
+
border: 1px solid #e5e7eb;
|
|
1669
|
+
border-radius: 8px;
|
|
1670
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
1671
|
+
font-size: 12px;
|
|
1672
|
+
line-height: 1.6;
|
|
1673
|
+
resize: vertical;
|
|
1674
|
+
box-sizing: border-box;
|
|
1675
|
+
background: #f9fafb;
|
|
1676
|
+
transition: all 0.2s ease;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.json-editor:hover {
|
|
1680
|
+
border-color: #d1d5db;
|
|
1681
|
+
background: #ffffff;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.json-editor:focus {
|
|
1685
|
+
outline: none;
|
|
1686
|
+
border-color: #667eea;
|
|
1687
|
+
background: #ffffff;
|
|
1688
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.error-message {
|
|
1692
|
+
margin-top: 8px;
|
|
1693
|
+
color: #ef4444;
|
|
1694
|
+
font-size: 12px;
|
|
1695
|
+
padding: 10px;
|
|
1696
|
+
background: #fef2f2;
|
|
1697
|
+
border-radius: 6px;
|
|
1698
|
+
border-left: 3px solid #ef4444;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/* 输出结构配置 */
|
|
1702
|
+
.res-schema-section {
|
|
1703
|
+
margin-bottom: 0;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.res-schema-container {
|
|
1707
|
+
border: 1px solid #e5e7eb;
|
|
1708
|
+
border-radius: 8px;
|
|
1709
|
+
padding: 16px;
|
|
1710
|
+
background: #f9fafb;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/* 过滤器配置 */
|
|
1714
|
+
.filter-mode-section {
|
|
1715
|
+
margin-bottom: 24px;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/* 操作按钮 */
|
|
1719
|
+
.action-section {
|
|
1720
|
+
display: flex;
|
|
1721
|
+
gap: 12px;
|
|
1722
|
+
justify-content: flex-end;
|
|
1723
|
+
padding-top: 8px;
|
|
1724
|
+
margin-top: auto;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
.btn-primary,
|
|
1728
|
+
.btn-secondary {
|
|
1729
|
+
padding: 10px 24px;
|
|
1730
|
+
border: none;
|
|
1731
|
+
border-radius: 8px;
|
|
1732
|
+
font-size: 14px;
|
|
1733
|
+
font-weight: 600;
|
|
1734
|
+
cursor: pointer;
|
|
1735
|
+
transition: all 0.2s ease;
|
|
1736
|
+
letter-spacing: 0.2px;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.btn-primary {
|
|
1740
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1741
|
+
color: #ffffff;
|
|
1742
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
.btn-primary:hover:not(:disabled) {
|
|
1746
|
+
transform: translateY(-1px);
|
|
1747
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
.btn-primary:active:not(:disabled) {
|
|
1751
|
+
transform: translateY(0);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
.btn-primary:disabled {
|
|
1755
|
+
background: #d1d5db;
|
|
1756
|
+
cursor: not-allowed;
|
|
1757
|
+
box-shadow: none;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
.btn-secondary {
|
|
1761
|
+
background: #ffffff;
|
|
1762
|
+
color: #6b7280;
|
|
1763
|
+
border: 1px solid #e5e7eb;
|
|
1764
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
.btn-secondary:hover {
|
|
1768
|
+
color: #374151;
|
|
1769
|
+
border-color: #d1d5db;
|
|
1770
|
+
transform: translateY(-1px);
|
|
1771
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
.btn-secondary:active {
|
|
1775
|
+
transform: translateY(0);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/* 滚动条美化 */
|
|
1779
|
+
.api-tree-container::-webkit-scrollbar,
|
|
1780
|
+
.right-panel::-webkit-scrollbar {
|
|
1781
|
+
width: 6px;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.api-tree-container::-webkit-scrollbar-track,
|
|
1785
|
+
.right-panel::-webkit-scrollbar-track {
|
|
1786
|
+
background: #f3f4f6;
|
|
1787
|
+
border-radius: 3px;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
.api-tree-container::-webkit-scrollbar-thumb,
|
|
1791
|
+
.right-panel::-webkit-scrollbar-thumb {
|
|
1792
|
+
background: #d1d5db;
|
|
1793
|
+
border-radius: 3px;
|
|
1794
|
+
transition: background 0.2s ease;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
.api-tree-container::-webkit-scrollbar-thumb:hover,
|
|
1798
|
+
.right-panel::-webkit-scrollbar-thumb:hover {
|
|
1799
|
+
background: #9ca3af;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/* 响应式 */
|
|
1803
|
+
@media (max-width: 1024px) {
|
|
1804
|
+
.api-config-component {
|
|
1805
|
+
flex-direction: column;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
.left-panel {
|
|
1809
|
+
flex: 0 0 auto;
|
|
1810
|
+
max-height: 400px;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
.right-panel {
|
|
1814
|
+
flex: 1;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
@media (max-width: 768px) {
|
|
1819
|
+
.api-config-component {
|
|
1820
|
+
padding: 16px;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
.left-panel,
|
|
1824
|
+
.right-panel {
|
|
1825
|
+
padding: 16px;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.action-section {
|
|
1829
|
+
flex-direction: column-reverse;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.btn-primary,
|
|
1833
|
+
.btn-secondary {
|
|
1834
|
+
width: 100%;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/* Arco Tree 自定义样式 */
|
|
1839
|
+
.api-tree-container :deep(.arco-tree) {
|
|
1840
|
+
background: #ffffff;
|
|
1841
|
+
padding: 8px;
|
|
1842
|
+
overflow-y: auto;
|
|
1843
|
+
flex: 1;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
.api-tree-container :deep(.arco-tree-node) {
|
|
1847
|
+
margin: 0;
|
|
1848
|
+
padding: 0;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/* 默认节点样式 */
|
|
1852
|
+
.api-tree-container :deep(.arco-tree-node-title) {
|
|
1853
|
+
padding: 8px 12px !important;
|
|
1854
|
+
border-radius: 4px !important;
|
|
1855
|
+
transition: all 0.2s ease !important;
|
|
1856
|
+
background: #ffffff !important;
|
|
1857
|
+
color: #6b7280 !important;
|
|
1858
|
+
font-size: 13px !important;
|
|
1859
|
+
min-height: 36px !important;
|
|
1860
|
+
margin-bottom: 2px !important;
|
|
1861
|
+
border: 1px solid #f3f4f6 !important;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/* 一级父节点 */
|
|
1865
|
+
.api-tree-container :deep(.arco-tree > .arco-tree-node > .arco-tree-node-title) {
|
|
1866
|
+
background: #f9fafb !important;
|
|
1867
|
+
font-weight: 700 !important;
|
|
1868
|
+
font-size: 15px !important;
|
|
1869
|
+
padding: 14px 16px !important;
|
|
1870
|
+
margin-bottom: 6px !important;
|
|
1871
|
+
border-radius: 8px !important;
|
|
1872
|
+
color: #111827 !important;
|
|
1873
|
+
border: 1px solid #e5e7eb !important;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/* hover 效果 */
|
|
1877
|
+
.api-tree-container :deep(.arco-tree-node-title:hover) {
|
|
1878
|
+
background: #f3f4f6 !important;
|
|
1879
|
+
border-color: #d1d5db !important;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
.api-tree-container :deep(.arco-tree > .arco-tree-node > .arco-tree-node-title:hover) {
|
|
1883
|
+
background: #f3f4f6 !important;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/* 选中状态 */
|
|
1887
|
+
.api-tree-container :deep(.arco-tree-node-selected > .arco-tree-node-title) {
|
|
1888
|
+
background: #dbeafe !important;
|
|
1889
|
+
color: #1e40af !important;
|
|
1890
|
+
font-weight: 600 !important;
|
|
1891
|
+
border-color: #93c5fd !important;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/* 展开/收起图标 */
|
|
1895
|
+
.api-tree-container :deep(.arco-tree-node-switcher) {
|
|
1896
|
+
width: 18px;
|
|
1897
|
+
height: 18px;
|
|
1898
|
+
margin-right: 8px;
|
|
1899
|
+
color: #9ca3af;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
.api-tree-container :deep(.arco-tree > .arco-tree-node .arco-tree-node-switcher) {
|
|
1903
|
+
width: 20px !important;
|
|
1904
|
+
height: 20px !important;
|
|
1905
|
+
color: #374151 !important;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/* 一级节点的子节点容器 */
|
|
1909
|
+
.api-tree-container :deep(.arco-tree > .arco-tree-node > .arco-tree-node-children) {
|
|
1910
|
+
padding: 12px !important;
|
|
1911
|
+
margin: 8px 0 12px 24px !important;
|
|
1912
|
+
background: #f8f9fa !important;
|
|
1913
|
+
border-radius: 8px !important;
|
|
1914
|
+
border: 2px solid #e9ecef !important;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/* 移除默认的选择框和缩进 */
|
|
1918
|
+
.api-tree-container :deep(.arco-tree-node-checkbox) {
|
|
1919
|
+
display: none !important;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
.api-tree-container :deep(.arco-tree-node-indent) {
|
|
1923
|
+
width: 0 !important;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/* 树节点自定义内容 */
|
|
1927
|
+
.tree-node-content {
|
|
1928
|
+
display: flex;
|
|
1929
|
+
align-items: center;
|
|
1930
|
+
justify-content: space-between;
|
|
1931
|
+
width: 100%;
|
|
1932
|
+
gap: 12px;
|
|
1933
|
+
flex: 1;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.tree-node-content .node-name {
|
|
1937
|
+
flex: 1;
|
|
1938
|
+
font-size: inherit;
|
|
1939
|
+
color: inherit;
|
|
1940
|
+
overflow: hidden;
|
|
1941
|
+
text-overflow: ellipsis;
|
|
1942
|
+
white-space: nowrap;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
.tree-node-content .node-path {
|
|
1946
|
+
flex-shrink: 0;
|
|
1947
|
+
font-size: 11px;
|
|
1948
|
+
color: #6b7280;
|
|
1949
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
1950
|
+
background: #f3f4f6;
|
|
1951
|
+
padding: 3px 8px;
|
|
1952
|
+
border-radius: 4px;
|
|
1953
|
+
max-width: 180px;
|
|
1954
|
+
overflow: hidden;
|
|
1955
|
+
text-overflow: ellipsis;
|
|
1956
|
+
white-space: nowrap;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
.api-tree-container :deep(.arco-tree-node-selected .node-path) {
|
|
1960
|
+
background: #60a5fa;
|
|
1961
|
+
color: #ffffff;
|
|
1962
|
+
font-weight: 600;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/* 子节点容器 */
|
|
1966
|
+
.api-tree-container :deep(.arco-tree-node-children) {
|
|
1967
|
+
padding-left: 0;
|
|
1968
|
+
margin-top: 4px;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/* 过滤器配置样式 */
|
|
1972
|
+
.filter-config-item {
|
|
1973
|
+
margin-bottom: 20px;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
.config-label {
|
|
1977
|
+
display: inline-block;
|
|
1978
|
+
font-size: 13px;
|
|
1979
|
+
font-weight: 600;
|
|
1980
|
+
color: #374151;
|
|
1981
|
+
margin-bottom: 8px;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.config-label-row {
|
|
1985
|
+
display: flex;
|
|
1986
|
+
justify-content: space-between;
|
|
1987
|
+
align-items: center;
|
|
1988
|
+
margin-bottom: 12px;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.label-hint {
|
|
1992
|
+
font-weight: normal;
|
|
1993
|
+
color: #9ca3af;
|
|
1994
|
+
font-size: 12px;
|
|
1995
|
+
margin-left: 4px;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
.config-input {
|
|
1999
|
+
width: 100%;
|
|
2000
|
+
padding: 10px 12px;
|
|
2001
|
+
border: 1px solid #e5e7eb;
|
|
2002
|
+
border-radius: 6px;
|
|
2003
|
+
font-size: 13px;
|
|
2004
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
2005
|
+
box-sizing: border-box;
|
|
2006
|
+
transition: all 0.2s ease;
|
|
2007
|
+
background: #f9fafb;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
.config-input:hover {
|
|
2011
|
+
border-color: #d1d5db;
|
|
2012
|
+
background: #ffffff;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
.config-input:focus {
|
|
2016
|
+
outline: none;
|
|
2017
|
+
border-color: #667eea;
|
|
2018
|
+
background: #ffffff;
|
|
2019
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
.config-hint {
|
|
2023
|
+
margin-top: 8px;
|
|
2024
|
+
padding: 10px 12px;
|
|
2025
|
+
background: #f0f9ff;
|
|
2026
|
+
border-radius: 6px;
|
|
2027
|
+
font-size: 12px;
|
|
2028
|
+
color: #0369a1;
|
|
2029
|
+
line-height: 1.5;
|
|
2030
|
+
border-left: 3px solid #38bdf8;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
.empty-hint {
|
|
2034
|
+
padding: 24px;
|
|
2035
|
+
text-align: center;
|
|
2036
|
+
color: #9ca3af;
|
|
2037
|
+
font-size: 13px;
|
|
2038
|
+
background: #f9fafb;
|
|
2039
|
+
border-radius: 6px;
|
|
2040
|
+
border: 1px dashed #d1d5db;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
.field-mapping-row {
|
|
2044
|
+
display: flex;
|
|
2045
|
+
align-items: flex-start;
|
|
2046
|
+
gap: 12px;
|
|
2047
|
+
margin-bottom: 12px;
|
|
2048
|
+
padding: 14px;
|
|
2049
|
+
background: #ffffff;
|
|
2050
|
+
border: 1px solid #e5e7eb;
|
|
2051
|
+
border-radius: 8px;
|
|
2052
|
+
transition: all 0.2s ease;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.field-mapping-row:hover {
|
|
2056
|
+
border-color: #93c5fd;
|
|
2057
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
.mapping-col {
|
|
2061
|
+
flex: 1;
|
|
2062
|
+
display: flex;
|
|
2063
|
+
flex-direction: column;
|
|
2064
|
+
gap: 6px;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
.mapping-col-target {
|
|
2068
|
+
flex: 1.5;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
.mapping-col-source {
|
|
2072
|
+
flex: 2;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
.mapping-col-type {
|
|
2076
|
+
flex: 1;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
.mapping-col-format {
|
|
2080
|
+
flex: 1.2;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
.mapping-select {
|
|
2084
|
+
width: 100%;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
.mapping-delete-btn {
|
|
2088
|
+
padding-top: 24px !important;
|
|
2089
|
+
flex-shrink: 0;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
.col-label {
|
|
2093
|
+
font-size: 11px;
|
|
2094
|
+
font-weight: 600;
|
|
2095
|
+
color: #6b7280;
|
|
2096
|
+
text-transform: uppercase;
|
|
2097
|
+
letter-spacing: 0.5px;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
.mapping-input {
|
|
2101
|
+
padding: 8px 10px;
|
|
2102
|
+
border: 1px solid #e5e7eb;
|
|
2103
|
+
border-radius: 6px;
|
|
2104
|
+
font-size: 13px;
|
|
2105
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
2106
|
+
box-sizing: border-box;
|
|
2107
|
+
transition: all 0.2s ease;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.mapping-input:hover {
|
|
2111
|
+
border-color: #d1d5db;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
.mapping-input:focus {
|
|
2115
|
+
outline: none;
|
|
2116
|
+
border-color: #667eea;
|
|
2117
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
.mapping-arrow {
|
|
2121
|
+
display: flex;
|
|
2122
|
+
align-items: center;
|
|
2123
|
+
font-size: 18px;
|
|
2124
|
+
color: #9ca3af;
|
|
2125
|
+
font-weight: bold;
|
|
2126
|
+
padding-top: 24px;
|
|
2127
|
+
flex-shrink: 0;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.field-mapping-row >>> .arco-btn,
|
|
2131
|
+
.field-mapping-row ::v-deep .arco-btn,
|
|
2132
|
+
.field-mapping-row :deep(.arco-btn) {
|
|
2133
|
+
padding-top: 24px;
|
|
2134
|
+
flex-shrink: 0;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/* 测试接口相关样式 */
|
|
2138
|
+
.test-api-section {
|
|
2139
|
+
display: flex;
|
|
2140
|
+
align-items: center;
|
|
2141
|
+
gap: 12px;
|
|
2142
|
+
margin-bottom: 20px;
|
|
2143
|
+
padding: 16px;
|
|
2144
|
+
background: #f0fdf4;
|
|
2145
|
+
border-radius: 8px;
|
|
2146
|
+
border: 2px solid #86efac;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
.test-hint {
|
|
2150
|
+
font-size: 12px;
|
|
2151
|
+
color: #15803d;
|
|
2152
|
+
font-weight: 500;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
.test-result-panel {
|
|
2156
|
+
margin-bottom: 24px;
|
|
2157
|
+
background: #ffffff;
|
|
2158
|
+
border: 2px solid #93c5fd;
|
|
2159
|
+
border-radius: 12px;
|
|
2160
|
+
overflow: hidden;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
.result-header {
|
|
2164
|
+
display: flex;
|
|
2165
|
+
justify-content: space-between;
|
|
2166
|
+
align-items: center;
|
|
2167
|
+
padding: 14px 18px;
|
|
2168
|
+
background: #dbeafe;
|
|
2169
|
+
border-bottom: 1px solid #93c5fd;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
.result-title {
|
|
2173
|
+
font-size: 14px;
|
|
2174
|
+
font-weight: 600;
|
|
2175
|
+
color: #1e40af;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
.detected-path {
|
|
2179
|
+
padding: 14px 18px;
|
|
2180
|
+
background: #fef3c7;
|
|
2181
|
+
border-bottom: 1px solid #fbbf24;
|
|
2182
|
+
display: flex;
|
|
2183
|
+
align-items: center;
|
|
2184
|
+
gap: 12px;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
.detected-label {
|
|
2188
|
+
font-size: 13px;
|
|
2189
|
+
font-weight: 600;
|
|
2190
|
+
color: #92400e;
|
|
2191
|
+
min-width: 140px;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
.detected-value {
|
|
2195
|
+
flex: 1;
|
|
2196
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
2197
|
+
font-size: 13px;
|
|
2198
|
+
color: #78350f;
|
|
2199
|
+
background: #ffffff;
|
|
2200
|
+
padding: 6px 12px;
|
|
2201
|
+
border-radius: 6px;
|
|
2202
|
+
border: 1px solid #fbbf24;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
.detected-fields {
|
|
2206
|
+
padding: 14px 18px;
|
|
2207
|
+
background: #f0f9ff;
|
|
2208
|
+
border-bottom: 1px solid #bae6fd;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
.detected-fields .detected-label {
|
|
2212
|
+
margin-bottom: 10px;
|
|
2213
|
+
color: #0369a1;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
.fields-grid {
|
|
2217
|
+
display: flex;
|
|
2218
|
+
flex-wrap: wrap;
|
|
2219
|
+
gap: 8px;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
.field-tag {
|
|
2223
|
+
padding: 6px 12px;
|
|
2224
|
+
background: #ffffff;
|
|
2225
|
+
border: 1px solid #93c5fd;
|
|
2226
|
+
border-radius: 6px;
|
|
2227
|
+
font-size: 12px;
|
|
2228
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
2229
|
+
color: #1e40af;
|
|
2230
|
+
font-weight: 600;
|
|
2231
|
+
transition: all 0.2s ease;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
.field-tag.clickable {
|
|
2235
|
+
cursor: pointer;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
.field-tag.clickable:hover {
|
|
2239
|
+
background: #dbeafe;
|
|
2240
|
+
border-color: #3b82f6;
|
|
2241
|
+
transform: translateY(-2px);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
.schema-fields-hint {
|
|
2245
|
+
margin-bottom: 20px;
|
|
2246
|
+
padding: 14px;
|
|
2247
|
+
background: #f0f9ff;
|
|
2248
|
+
border-radius: 8px;
|
|
2249
|
+
border: 2px solid #bae6fd;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.schema-fields-hint .hint-title {
|
|
2253
|
+
font-size: 13px;
|
|
2254
|
+
font-weight: 600;
|
|
2255
|
+
color: #0369a1;
|
|
2256
|
+
margin-bottom: 10px;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
.schema-fields-hint .hint-desc {
|
|
2260
|
+
font-size: 12px;
|
|
2261
|
+
color: #0c4a6e;
|
|
2262
|
+
margin-top: 8px;
|
|
2263
|
+
}
|
|
2264
|
+
</style>
|