@coffic/cosy-ui 0.9.39 → 0.9.40
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/dist/index-astro.ts +2 -0
- package/dist/src-astro/api-test/ApiEndpointCard.astro +254 -0
- package/dist/src-astro/api-test/ApiTestScript.astro +357 -0
- package/dist/src-astro/api-test/ApiTester.astro +111 -0
- package/dist/src-astro/api-test/index.ts +5 -0
- package/dist/src-astro/api-test/index_astro.ts +5 -0
- package/dist/src-astro/layout-app/AppLayout.astro +3 -1
- package/dist/src-astro/mac-window/MacWindow.astro +2 -2
- package/dist/src-astro/picture-book/PictureBookPage.astro +6 -3
- package/dist/src-astro/types/api-test.ts +138 -0
- package/package.json +1 -1
package/dist/index-astro.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 基础组件和功能模块 (按字母顺序)
|
|
2
2
|
export * from './src-astro/alert';
|
|
3
3
|
export * from './src-astro/alert-dialog';
|
|
4
|
+
export * from './src-astro/api-test';
|
|
4
5
|
export * from './src-astro/article';
|
|
5
6
|
export * from './src-astro/banner';
|
|
6
7
|
export * from './src-astro/badge';
|
|
@@ -57,6 +58,7 @@ export * from './src-astro/toc';
|
|
|
57
58
|
export * from './src-astro/toast';
|
|
58
59
|
|
|
59
60
|
// 类型定义 (按字母顺序)
|
|
61
|
+
export * from './src-astro/types/api-test';
|
|
60
62
|
export * from './src-astro/types/article';
|
|
61
63
|
export * from './src-astro/types/footer';
|
|
62
64
|
export * from './src-astro/types/header';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component ApiEndpointCard
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* ApiEndpointCard 组件用于显示单个API端点的测试界面,包括参数输入、快速测试和测试结果展示。
|
|
7
|
+
* 支持多种参数类型,提供直观的测试体验。
|
|
8
|
+
*
|
|
9
|
+
* @design
|
|
10
|
+
* 设计理念:
|
|
11
|
+
* 1. 清晰的信息层次 - 通过视觉分组和间距清晰展示API信息
|
|
12
|
+
* 2. 直观的参数输入 - 支持多种输入类型,提供验证和默认值
|
|
13
|
+
* 3. 便捷的快速测试 - 预设常用参数组合,提高测试效率
|
|
14
|
+
* 4. 详细的结果展示 - 清晰展示请求和响应信息
|
|
15
|
+
*
|
|
16
|
+
* @usage
|
|
17
|
+
* 基本用法:
|
|
18
|
+
* ```astro
|
|
19
|
+
* <ApiEndpointCard endpoint={endpoint} />
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @props
|
|
23
|
+
* @prop {IApiEndpoint} endpoint - API端点配置信息
|
|
24
|
+
* @prop {string} [class] - 自定义CSS类名
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import Card from '../card/Card.astro';
|
|
28
|
+
import Button from '../button/Button.astro';
|
|
29
|
+
import Badge from '../badge/Badge.astro';
|
|
30
|
+
import type { IApiEndpoint } from '../types/api-test';
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
endpoint: IApiEndpoint;
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { endpoint, class: className } = Astro.props;
|
|
38
|
+
|
|
39
|
+
// 预处理数据
|
|
40
|
+
const hasParams = endpoint.params && endpoint.params.length > 0;
|
|
41
|
+
const hasQuickTests = endpoint.quickTests && endpoint.quickTests.length > 0;
|
|
42
|
+
const hasDescription = !!endpoint.description;
|
|
43
|
+
|
|
44
|
+
// 获取HTTP方法对应的颜色
|
|
45
|
+
const getMethodColor = (
|
|
46
|
+
method: string
|
|
47
|
+
):
|
|
48
|
+
| 'success'
|
|
49
|
+
| 'primary'
|
|
50
|
+
| 'warning'
|
|
51
|
+
| 'error'
|
|
52
|
+
| 'info'
|
|
53
|
+
| 'secondary'
|
|
54
|
+
| 'accent'
|
|
55
|
+
| 'ghost' => {
|
|
56
|
+
const colors: Record<
|
|
57
|
+
string,
|
|
58
|
+
| 'success'
|
|
59
|
+
| 'primary'
|
|
60
|
+
| 'warning'
|
|
61
|
+
| 'error'
|
|
62
|
+
| 'info'
|
|
63
|
+
| 'secondary'
|
|
64
|
+
| 'accent'
|
|
65
|
+
| 'ghost'
|
|
66
|
+
> = {
|
|
67
|
+
GET: 'success',
|
|
68
|
+
POST: 'primary',
|
|
69
|
+
PUT: 'warning',
|
|
70
|
+
DELETE: 'error',
|
|
71
|
+
PATCH: 'info',
|
|
72
|
+
};
|
|
73
|
+
return colors[method] || 'neutral';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 获取参数类型对应的输入元素
|
|
77
|
+
const getInputElement = (param: any) => {
|
|
78
|
+
const baseAttrs = {
|
|
79
|
+
'data-param': param.name,
|
|
80
|
+
'data-endpoint': endpoint.path,
|
|
81
|
+
'data-required': param.required || false,
|
|
82
|
+
'data-validation': JSON.stringify(param.validation || {}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
type: param.type || 'text',
|
|
87
|
+
attrs: baseAttrs,
|
|
88
|
+
options: param.options || [],
|
|
89
|
+
defaultValue: param.defaultValue,
|
|
90
|
+
placeholder: param.placeholder,
|
|
91
|
+
required: param.required,
|
|
92
|
+
validation: param.validation,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
<Card title={endpoint.name} class={className}>
|
|
98
|
+
<div class="space-y-4">
|
|
99
|
+
{/* HTTP方法和路径 */}
|
|
100
|
+
<div class="bg-base-200 p-3 rounded-lg flex items-center gap-2 flex-wrap">
|
|
101
|
+
<Badge variant={getMethodColor(endpoint.method)}>
|
|
102
|
+
{endpoint.method}
|
|
103
|
+
</Badge>
|
|
104
|
+
<code class="text-primary font-mono text-sm break-all">
|
|
105
|
+
{endpoint.path}
|
|
106
|
+
</code>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* 描述 */}
|
|
110
|
+
{
|
|
111
|
+
hasDescription && (
|
|
112
|
+
<p class="text-base-content/70">{endpoint.description}</p>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
{/* 参数输入 */}
|
|
117
|
+
{
|
|
118
|
+
hasParams && (
|
|
119
|
+
<div class="space-y-3">
|
|
120
|
+
<p class="font-medium text-base-content">请求参数:</p>
|
|
121
|
+
{endpoint.params!.map((param) => {
|
|
122
|
+
const inputConfig = getInputElement(param);
|
|
123
|
+
return (
|
|
124
|
+
<div class="form-control">
|
|
125
|
+
<label class="label">
|
|
126
|
+
<span class="label-text">
|
|
127
|
+
{param.name}
|
|
128
|
+
{param.required && <span class="text-error ml-1">*</span>}
|
|
129
|
+
</span>
|
|
130
|
+
{param.validation?.message && (
|
|
131
|
+
<span class="label-text-alt text-warning">
|
|
132
|
+
{param.validation.message}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</label>
|
|
136
|
+
|
|
137
|
+
{inputConfig.type === 'select' ? (
|
|
138
|
+
<select
|
|
139
|
+
class="select select-bordered w-full"
|
|
140
|
+
data-param={inputConfig.attrs['data-param']}
|
|
141
|
+
data-endpoint={inputConfig.attrs['data-endpoint']}
|
|
142
|
+
data-required={inputConfig.attrs['data-required']}
|
|
143
|
+
data-validation={inputConfig.attrs['data-validation']}
|
|
144
|
+
required={inputConfig.required}>
|
|
145
|
+
<option value="">请选择</option>
|
|
146
|
+
{inputConfig.options.map((option: string) => (
|
|
147
|
+
<option
|
|
148
|
+
value={option}
|
|
149
|
+
selected={option === inputConfig.defaultValue}>
|
|
150
|
+
{option}
|
|
151
|
+
</option>
|
|
152
|
+
))}
|
|
153
|
+
</select>
|
|
154
|
+
) : inputConfig.type === 'textarea' ? (
|
|
155
|
+
<textarea
|
|
156
|
+
class="textarea textarea-bordered w-full"
|
|
157
|
+
placeholder={inputConfig.placeholder}
|
|
158
|
+
rows={3}
|
|
159
|
+
data-param={inputConfig.attrs['data-param']}
|
|
160
|
+
data-endpoint={inputConfig.attrs['data-endpoint']}
|
|
161
|
+
data-required={inputConfig.attrs['data-required']}
|
|
162
|
+
data-validation={inputConfig.attrs['data-validation']}
|
|
163
|
+
required={inputConfig.required}>
|
|
164
|
+
{inputConfig.defaultValue || ''}
|
|
165
|
+
</textarea>
|
|
166
|
+
) : inputConfig.type === 'checkbox' ? (
|
|
167
|
+
<input
|
|
168
|
+
type="checkbox"
|
|
169
|
+
class="checkbox checkbox-primary"
|
|
170
|
+
data-param={inputConfig.attrs['data-param']}
|
|
171
|
+
data-endpoint={inputConfig.attrs['data-endpoint']}
|
|
172
|
+
data-required={inputConfig.attrs['data-required']}
|
|
173
|
+
data-validation={inputConfig.attrs['data-validation']}
|
|
174
|
+
checked={inputConfig.defaultValue || false}
|
|
175
|
+
/>
|
|
176
|
+
) : inputConfig.type === 'radio' ? (
|
|
177
|
+
<div class="flex flex-wrap gap-3">
|
|
178
|
+
{inputConfig.options.map((option: string) => (
|
|
179
|
+
<label class="label cursor-pointer gap-2">
|
|
180
|
+
<input
|
|
181
|
+
type="radio"
|
|
182
|
+
name={`${endpoint.path}-${param.name}`}
|
|
183
|
+
class="radio radio-primary"
|
|
184
|
+
value={option}
|
|
185
|
+
data-param={inputConfig.attrs['data-param']}
|
|
186
|
+
data-endpoint={inputConfig.attrs['data-endpoint']}
|
|
187
|
+
data-required={inputConfig.attrs['data-required']}
|
|
188
|
+
data-validation={inputConfig.attrs['data-validation']}
|
|
189
|
+
checked={option === inputConfig.defaultValue}
|
|
190
|
+
/>
|
|
191
|
+
<span class="label-text">{option}</span>
|
|
192
|
+
</label>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
) : (
|
|
196
|
+
<input
|
|
197
|
+
type={inputConfig.type}
|
|
198
|
+
placeholder={inputConfig.placeholder}
|
|
199
|
+
class="input input-bordered w-full"
|
|
200
|
+
data-param={inputConfig.attrs['data-param']}
|
|
201
|
+
data-endpoint={inputConfig.attrs['data-endpoint']}
|
|
202
|
+
data-required={inputConfig.attrs['data-required']}
|
|
203
|
+
data-validation={inputConfig.attrs['data-validation']}
|
|
204
|
+
required={inputConfig.required}
|
|
205
|
+
value={inputConfig.defaultValue || ''}
|
|
206
|
+
min={inputConfig.validation?.min}
|
|
207
|
+
max={inputConfig.validation?.max}
|
|
208
|
+
pattern={inputConfig.validation?.pattern}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
{/* 快速测试 */}
|
|
219
|
+
{
|
|
220
|
+
hasQuickTests && (
|
|
221
|
+
<div>
|
|
222
|
+
<p class="text-sm text-base-content/70 mb-3">快速填充参数:</p>
|
|
223
|
+
<div class="flex flex-wrap gap-2">
|
|
224
|
+
{endpoint.quickTests!.map((quickTest, index) => (
|
|
225
|
+
<Button
|
|
226
|
+
variant="outline"
|
|
227
|
+
size="sm"
|
|
228
|
+
data-quick-test={JSON.stringify(quickTest.values)}
|
|
229
|
+
data-endpoint={endpoint.path}
|
|
230
|
+
title={quickTest.description}>
|
|
231
|
+
{quickTest.label}
|
|
232
|
+
</Button>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
{/* 测试按钮 */}
|
|
240
|
+
<Button
|
|
241
|
+
variant="primary"
|
|
242
|
+
block
|
|
243
|
+
data-endpoint={endpoint.path}
|
|
244
|
+
data-method={endpoint.method}>
|
|
245
|
+
测试接口
|
|
246
|
+
</Button>
|
|
247
|
+
|
|
248
|
+
{/* 测试结果 */}
|
|
249
|
+
<div
|
|
250
|
+
class="hidden mt-4 p-4 bg-base-200 rounded-lg border-l-4 border-l-primary"
|
|
251
|
+
data-result={endpoint.path}>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</Card>
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component ApiTestScript
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* ApiTestScript 组件提供API测试的核心JavaScript逻辑,包括请求发送、响应处理和结果展示。
|
|
7
|
+
* 支持多种HTTP方法、参数验证、请求头配置和详细的响应信息展示。
|
|
8
|
+
*
|
|
9
|
+
* @design
|
|
10
|
+
* 设计理念:
|
|
11
|
+
* 1. 健壮性 - 完善的错误处理和边界情况处理
|
|
12
|
+
* 2. 可观测性 - 详细的日志记录和状态反馈
|
|
13
|
+
* 3. 灵活性 - 支持多种请求配置和自定义选项
|
|
14
|
+
* 4. 用户体验 - 清晰的加载状态和结果展示
|
|
15
|
+
*
|
|
16
|
+
* @usage
|
|
17
|
+
* 基本用法:
|
|
18
|
+
* ```astro
|
|
19
|
+
* <ApiTestScript />
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 带配置的用法:
|
|
23
|
+
* ```astro
|
|
24
|
+
* <ApiTestScript
|
|
25
|
+
* showResponseTime={true}
|
|
26
|
+
* showRequestDetails={true}
|
|
27
|
+
* defaultHeaders={{ 'Authorization': 'Bearer token' }}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @props
|
|
32
|
+
* @prop {boolean} [showResponseTime=true] - 是否显示响应时间
|
|
33
|
+
* @prop {boolean} [showRequestDetails=false] - 是否显示请求详情
|
|
34
|
+
* @prop {Record<string, string>} [defaultHeaders] - 默认请求头
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
showResponseTime?: boolean;
|
|
39
|
+
showRequestDetails?: boolean;
|
|
40
|
+
defaultHeaders?: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
showResponseTime = true,
|
|
45
|
+
showRequestDetails = false,
|
|
46
|
+
defaultHeaders = {},
|
|
47
|
+
} = Astro.props;
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
<script define:vars={{ showResponseTime, showRequestDetails, defaultHeaders }}>
|
|
51
|
+
// 格式化响应显示
|
|
52
|
+
function formatResponse(response, data, responseTime) {
|
|
53
|
+
const statusColor = response.ok ? 'success' : 'error';
|
|
54
|
+
const statusText = response.ok ? '成功' : '失败';
|
|
55
|
+
|
|
56
|
+
let result = `
|
|
57
|
+
<div class="space-y-3">
|
|
58
|
+
<div class="flex items-center gap-2">
|
|
59
|
+
<span class="font-semibold text-base-content">HTTP状态:</span>
|
|
60
|
+
<span class="badge badge-${statusColor}">${response.status} ${response.statusText}</span>
|
|
61
|
+
<span class="text-sm text-base-content/70">(${statusText})</span>
|
|
62
|
+
</div>
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
if (showResponseTime && responseTime) {
|
|
66
|
+
result += `
|
|
67
|
+
<div>
|
|
68
|
+
<span class="font-semibold text-base-content">响应时间:</span>
|
|
69
|
+
<span class="text-success">${responseTime}ms</span>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (showRequestDetails) {
|
|
75
|
+
result += `
|
|
76
|
+
<div>
|
|
77
|
+
<span class="font-semibold text-base-content">请求URL:</span>
|
|
78
|
+
<code class="text-primary font-mono text-sm break-all">${response.url}</code>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
result += `
|
|
84
|
+
<div>
|
|
85
|
+
<span class="font-semibold text-base-content">响应内容:</span>
|
|
86
|
+
<pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto text-sm">${JSON.stringify(data, null, 2)}</pre>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 收集请求头
|
|
95
|
+
function collectHeaders() {
|
|
96
|
+
const headers = { ...defaultHeaders };
|
|
97
|
+
|
|
98
|
+
// 收集自定义请求头
|
|
99
|
+
document
|
|
100
|
+
.querySelectorAll('[data-header-key][data-header-value]')
|
|
101
|
+
.forEach((input) => {
|
|
102
|
+
const key = input.dataset.headerKey;
|
|
103
|
+
const value = input.dataset.headerValue;
|
|
104
|
+
if (key && value) {
|
|
105
|
+
headers[key] = value;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 设置默认Content-Type
|
|
110
|
+
if (!headers['Content-Type']) {
|
|
111
|
+
headers['Content-Type'] = 'application/json';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return headers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 验证参数
|
|
118
|
+
function validateParams(params) {
|
|
119
|
+
const errors = [];
|
|
120
|
+
|
|
121
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
122
|
+
const input = document.querySelector(
|
|
123
|
+
`[data-param="${key}"][data-endpoint]`
|
|
124
|
+
);
|
|
125
|
+
if (input) {
|
|
126
|
+
const validation = JSON.parse(input.dataset.validation || '{}');
|
|
127
|
+
const required = input.dataset.required === 'true';
|
|
128
|
+
|
|
129
|
+
if (required && (!value || value.toString().trim() === '')) {
|
|
130
|
+
errors.push(`${key} 是必填参数`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (validation.min && Number(value) < validation.min) {
|
|
134
|
+
errors.push(`${key} 不能小于 ${validation.min}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (validation.max && Number(value) > validation.max) {
|
|
138
|
+
errors.push(`${key} 不能大于 ${validation.max}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (validation.pattern && !new RegExp(validation.pattern).test(value)) {
|
|
142
|
+
errors.push(`${key} 格式不正确`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return { isValid: errors.length === 0, errors };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 测试API端点
|
|
151
|
+
async function testEndpoint(endpoint, method) {
|
|
152
|
+
console.log('🚀 开始测试端点:', endpoint, method);
|
|
153
|
+
|
|
154
|
+
// 找到所有匹配的结果区域和按钮
|
|
155
|
+
const resultDivs = document.querySelectorAll(`[data-result="${endpoint}"]`);
|
|
156
|
+
const testBtns = document.querySelectorAll(
|
|
157
|
+
`[data-endpoint="${endpoint}"][data-method="${method}"]`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
console.log('找到结果区域数量:', resultDivs.length);
|
|
161
|
+
console.log('找到按钮数量:', testBtns.length);
|
|
162
|
+
|
|
163
|
+
// 使用第一个匹配的元素
|
|
164
|
+
const resultDiv = resultDivs[0];
|
|
165
|
+
const testBtn = testBtns[0];
|
|
166
|
+
|
|
167
|
+
if (!resultDiv || !testBtn) {
|
|
168
|
+
console.error('❌ 找不到结果区域或按钮:', { endpoint, method });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 显示结果区域
|
|
173
|
+
resultDiv.classList.remove('hidden');
|
|
174
|
+
resultDiv.innerHTML =
|
|
175
|
+
'<span class="text-primary italic">正在测试接口...</span>';
|
|
176
|
+
|
|
177
|
+
// 禁用按钮并显示加载状态
|
|
178
|
+
testBtn.disabled = true;
|
|
179
|
+
testBtn.innerHTML =
|
|
180
|
+
'<span class="loading loading-spinner loading-sm"></span> 测试中...';
|
|
181
|
+
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// 构建URL和参数
|
|
186
|
+
let url = endpoint;
|
|
187
|
+
const params = {};
|
|
188
|
+
|
|
189
|
+
// 收集参数值
|
|
190
|
+
document
|
|
191
|
+
.querySelectorAll(`[data-endpoint="${endpoint}"][data-param]`)
|
|
192
|
+
.forEach((input) => {
|
|
193
|
+
const paramName = input.dataset.param;
|
|
194
|
+
if (paramName) {
|
|
195
|
+
if (input.type === 'checkbox') {
|
|
196
|
+
params[paramName] = input.checked;
|
|
197
|
+
} else if (input.type === 'radio') {
|
|
198
|
+
if (input.checked) {
|
|
199
|
+
params[paramName] = input.value;
|
|
200
|
+
}
|
|
201
|
+
} else if (input.value.trim()) {
|
|
202
|
+
params[paramName] = input.value.trim();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 验证参数
|
|
208
|
+
const validation = validateParams(params);
|
|
209
|
+
if (!validation.isValid) {
|
|
210
|
+
resultDiv.innerHTML = `
|
|
211
|
+
<div class="text-error">
|
|
212
|
+
<span class="font-semibold">参数验证失败:</span>
|
|
213
|
+
<ul class="mt-2 list-disc list-inside">
|
|
214
|
+
${validation.errors.map((error) => `<li>${error}</li>`).join('')}
|
|
215
|
+
</ul>
|
|
216
|
+
</div>
|
|
217
|
+
`;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 为GET请求添加查询参数
|
|
222
|
+
if (method === 'GET' && Object.keys(params).length > 0) {
|
|
223
|
+
const searchParams = new URLSearchParams();
|
|
224
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
225
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
226
|
+
searchParams.append(key, value.toString());
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
url += `?${searchParams.toString()}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 收集请求头
|
|
233
|
+
const headers = collectHeaders();
|
|
234
|
+
|
|
235
|
+
console.log('📤 发送请求:', { url, method, params, headers });
|
|
236
|
+
|
|
237
|
+
// 发送请求
|
|
238
|
+
const response = await fetch(url, {
|
|
239
|
+
method: method,
|
|
240
|
+
headers,
|
|
241
|
+
body: method !== 'GET' ? JSON.stringify(params) : undefined,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const responseTime = Date.now() - startTime;
|
|
245
|
+
|
|
246
|
+
// 处理响应
|
|
247
|
+
let responseText = '';
|
|
248
|
+
try {
|
|
249
|
+
responseText = await response.text();
|
|
250
|
+
let data;
|
|
251
|
+
try {
|
|
252
|
+
data = JSON.parse(responseText);
|
|
253
|
+
} catch {
|
|
254
|
+
data = responseText;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
resultDiv.innerHTML = formatResponse(response, data, responseTime);
|
|
258
|
+
console.log('✅ 请求成功:', { response, data, responseTime });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
resultDiv.innerHTML = formatResponse(
|
|
261
|
+
response,
|
|
262
|
+
'[无法读取响应内容]',
|
|
263
|
+
responseTime
|
|
264
|
+
);
|
|
265
|
+
console.error('❌ 响应解析失败:', error);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const errorMessage =
|
|
269
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
270
|
+
resultDiv.innerHTML = `
|
|
271
|
+
<div class="text-error">
|
|
272
|
+
<span class="font-semibold">请求失败:</span> ${errorMessage}
|
|
273
|
+
</div>
|
|
274
|
+
`;
|
|
275
|
+
console.error('❌ 请求失败:', error);
|
|
276
|
+
} finally {
|
|
277
|
+
// 恢复按钮状态
|
|
278
|
+
testBtn.disabled = false;
|
|
279
|
+
testBtn.innerHTML = '测试接口';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 快速填充参数
|
|
284
|
+
function quickFill(endpoint, values) {
|
|
285
|
+
console.log('⚡ 快速填充参数:', { endpoint, values });
|
|
286
|
+
|
|
287
|
+
Object.entries(values).forEach(([key, value]) => {
|
|
288
|
+
const inputs = document.querySelectorAll(
|
|
289
|
+
`[data-endpoint="${endpoint}"][data-param="${key}"]`
|
|
290
|
+
);
|
|
291
|
+
console.log(`找到参数 ${key} 的输入框数量:`, inputs.length);
|
|
292
|
+
|
|
293
|
+
// 使用第一个匹配的输入框
|
|
294
|
+
const input = inputs[0];
|
|
295
|
+
if (input) {
|
|
296
|
+
if (input.type === 'checkbox') {
|
|
297
|
+
input.checked = Boolean(value);
|
|
298
|
+
} else if (input.type === 'radio') {
|
|
299
|
+
const radioInput = document.querySelector(
|
|
300
|
+
`[data-endpoint="${endpoint}"][data-param="${key}"][value="${value}"]`
|
|
301
|
+
);
|
|
302
|
+
if (radioInput) {
|
|
303
|
+
radioInput.checked = true;
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
input.value = value.toString();
|
|
307
|
+
}
|
|
308
|
+
console.log(`✅ 已填充参数 ${key}:`, value);
|
|
309
|
+
} else {
|
|
310
|
+
console.warn(`⚠️ 未找到参数 ${key} 的输入框`);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 页面加载完成后绑定事件
|
|
316
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
317
|
+
console.log('🚀 API测试组件已加载,可以开始测试了!');
|
|
318
|
+
|
|
319
|
+
// 绑定测试按钮事件
|
|
320
|
+
document.querySelectorAll('[data-endpoint][data-method]').forEach((btn) => {
|
|
321
|
+
btn.addEventListener('click', (event) => {
|
|
322
|
+
const clickedBtn = event.currentTarget;
|
|
323
|
+
const endpoint = clickedBtn.dataset.endpoint;
|
|
324
|
+
const method = clickedBtn.dataset.method;
|
|
325
|
+
console.log('🖱️ 点击了按钮:', { endpoint, method });
|
|
326
|
+
if (endpoint && method) {
|
|
327
|
+
testEndpoint(endpoint, method);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// 绑定快速测试按钮事件
|
|
333
|
+
document.querySelectorAll('[data-quick-test]').forEach((btn) => {
|
|
334
|
+
btn.addEventListener('click', (event) => {
|
|
335
|
+
const clickedBtn = event.currentTarget;
|
|
336
|
+
const endpoint = clickedBtn.dataset.endpoint;
|
|
337
|
+
const values = JSON.parse(clickedBtn.dataset.quickTest || '{}');
|
|
338
|
+
console.log('⚡ 快速填充:', { endpoint, values });
|
|
339
|
+
if (endpoint && values) {
|
|
340
|
+
quickFill(endpoint, values);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// 绑定请求头输入框事件
|
|
346
|
+
document.querySelectorAll('[data-header-key]').forEach((input) => {
|
|
347
|
+
input.addEventListener('input', (event) => {
|
|
348
|
+
const target = event.target;
|
|
349
|
+
const key = target.dataset.headerKey;
|
|
350
|
+
const value = target.value;
|
|
351
|
+
if (key) {
|
|
352
|
+
target.dataset.headerValue = value;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
</script>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component ApiTester
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* ApiTester 组件是一个完整的API测试工具,提供多个API端点的测试界面。
|
|
7
|
+
* 支持参数配置、快速测试、请求发送和结果展示等功能。
|
|
8
|
+
*
|
|
9
|
+
* @design
|
|
10
|
+
* 设计理念:
|
|
11
|
+
* 1. 统一管理 - 在一个界面中管理多个API端点的测试
|
|
12
|
+
* 2. 灵活配置 - 支持多种参数类型和验证规则
|
|
13
|
+
* 3. 快速测试 - 提供预设参数组合,提高测试效率
|
|
14
|
+
* 4. 清晰展示 - 直观展示测试结果和请求详情
|
|
15
|
+
*
|
|
16
|
+
* @usage
|
|
17
|
+
* 基本用法:
|
|
18
|
+
* ```astro
|
|
19
|
+
* <ApiTester endpoints={endpoints} title="用户API测试" />
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 带描述的用法:
|
|
23
|
+
* ```astro
|
|
24
|
+
* <ApiTester
|
|
25
|
+
* endpoints={endpoints}
|
|
26
|
+
* title="用户API测试"
|
|
27
|
+
* description="测试用户相关的所有API接口"
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @props
|
|
32
|
+
* @prop {IApiEndpoint[]} endpoints - API端点配置列表
|
|
33
|
+
* @prop {string} [title="API 测试"] - 组件标题
|
|
34
|
+
* @prop {string} [description] - 组件描述
|
|
35
|
+
* @prop {boolean} [showHeaders=false] - 是否显示请求头配置
|
|
36
|
+
* @prop {Record<string, string>} [defaultHeaders] - 默认请求头
|
|
37
|
+
* @prop {boolean} [showResponseTime=true] - 是否显示响应时间
|
|
38
|
+
* @prop {boolean} [showRequestDetails=false] - 是否显示请求详情
|
|
39
|
+
* @prop {string} [class] - 自定义CSS类名
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import Heading from '../heading/Heading.astro';
|
|
43
|
+
import Grid from '../grid/Grid.astro';
|
|
44
|
+
import ApiEndpointCard from './ApiEndpointCard.astro';
|
|
45
|
+
import ApiTestScript from './ApiTestScript.astro';
|
|
46
|
+
import type { IApiTesterProps, IApiEndpoint } from '../types/api-test';
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
endpoints,
|
|
50
|
+
title = 'API 测试',
|
|
51
|
+
description,
|
|
52
|
+
showHeaders = false,
|
|
53
|
+
defaultHeaders,
|
|
54
|
+
showResponseTime = true,
|
|
55
|
+
showRequestDetails = false,
|
|
56
|
+
class: className,
|
|
57
|
+
} = Astro.props;
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<div class={`space-y-8 ${className || ''}`}>
|
|
61
|
+
<div class="text-center">
|
|
62
|
+
<Heading level={1}>
|
|
63
|
+
{title}
|
|
64
|
+
</Heading>
|
|
65
|
+
{
|
|
66
|
+
description && (
|
|
67
|
+
<p class="mt-2 text-base-content/70 max-w-2xl mx-auto">{description}</p>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* 请求头配置 */}
|
|
73
|
+
{
|
|
74
|
+
showHeaders && (
|
|
75
|
+
<div class="bg-base-200 p-4 rounded-lg">
|
|
76
|
+
<h3 class="text-lg font-medium mb-3">请求头配置</h3>
|
|
77
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
78
|
+
{defaultHeaders &&
|
|
79
|
+
Object.entries(defaultHeaders).map(([key, value]) => (
|
|
80
|
+
<div class="form-control">
|
|
81
|
+
<label class="label">
|
|
82
|
+
<span class="label-text">{key}</span>
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
class="input input-bordered w-full"
|
|
87
|
+
value={value as string}
|
|
88
|
+
data-header-key={key}
|
|
89
|
+
data-header-value={value as string}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
<Grid cols={{ base: 1, lg: Math.min(endpoints.length, 2) }}>
|
|
99
|
+
{
|
|
100
|
+
endpoints.map((endpoint: IApiEndpoint) => (
|
|
101
|
+
<ApiEndpointCard endpoint={endpoint} />
|
|
102
|
+
))
|
|
103
|
+
}
|
|
104
|
+
</Grid>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<ApiTestScript
|
|
108
|
+
showResponseTime={showResponseTime}
|
|
109
|
+
showRequestDetails={showRequestDetails}
|
|
110
|
+
defaultHeaders={defaultHeaders}
|
|
111
|
+
/>
|
|
@@ -342,8 +342,10 @@ const sidebarHeightClass = getSidebarHeightClass(headerConfig?.height);
|
|
|
342
342
|
<Container
|
|
343
343
|
flex="row"
|
|
344
344
|
gap="none"
|
|
345
|
-
width="full"
|
|
346
345
|
padding="none"
|
|
346
|
+
width="full"
|
|
347
|
+
centered={true}
|
|
348
|
+
aria-label="AppLayout-Container"
|
|
347
349
|
class={containerMinHeightClass}>
|
|
348
350
|
<!-- 侧边栏容器 -->
|
|
349
351
|
{
|
|
@@ -118,9 +118,9 @@ const windowId = id || `mac-window-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
118
118
|
|
|
119
119
|
<Container
|
|
120
120
|
background={bgType}
|
|
121
|
-
size="full"
|
|
122
121
|
padding="none"
|
|
123
|
-
centered={
|
|
122
|
+
centered={true}
|
|
123
|
+
aria-label="MacWindow-Container"
|
|
124
124
|
class={`cosy:flex cosy:relative cosy:rounded-2xl cosy:overflow-hidden ${height} ${withShadow ? 'cosy:shadow-lg' : ''}`}
|
|
125
125
|
data-window-id={windowId}>
|
|
126
126
|
<!-- 窗口控制按钮 -->
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* - background?: BackgroundColor | ImageMetadata - 背景配置,支持背景色或图片。背景色如:base-100、primary/20、secondary/30 等;图片直接传递 ImageMetadata 对象
|
|
42
42
|
* - pageAspectRatio?: number - 页面宽高比(宽/高),默认 3/4
|
|
43
43
|
* - border?: boolean - 是否在比例内容区域显示边框(由容器控制),默认 true
|
|
44
|
+
* - rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full' - 页面圆角大小,默认 'xl'
|
|
44
45
|
*
|
|
45
46
|
* @slots
|
|
46
47
|
* - overlay: 覆盖层插槽,相对安全区域进行绝对定位放置文本框/装饰
|
|
@@ -58,12 +59,14 @@ export interface IPictureBookPageProps {
|
|
|
58
59
|
background?: BackgroundColor | ImageMetadata;
|
|
59
60
|
pageAspectRatio?: number;
|
|
60
61
|
border?: boolean | ContentBorderColor;
|
|
62
|
+
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const {
|
|
64
66
|
background,
|
|
65
67
|
pageAspectRatio = 3 / 4,
|
|
66
68
|
border = true,
|
|
69
|
+
rounded = 'xl',
|
|
67
70
|
} = Astro.props as IPictureBookPageProps;
|
|
68
71
|
|
|
69
72
|
// 判断是否为图片类型
|
|
@@ -79,20 +82,20 @@ const isImageBackground =
|
|
|
79
82
|
background={isImageBackground ? undefined : (background as BackgroundColor)}
|
|
80
83
|
padding="none"
|
|
81
84
|
margin="none"
|
|
82
|
-
rounded=
|
|
85
|
+
rounded={rounded}
|
|
83
86
|
border={false}
|
|
84
87
|
centered={true}
|
|
85
88
|
aria-label="绘本页"
|
|
86
89
|
class="cosy:relative cosy:overflow-hidden">
|
|
87
90
|
<!-- 背景层:自动显示背景色或图片,填充比例内容区域 -->
|
|
88
91
|
<div
|
|
89
|
-
class=
|
|
92
|
+
class={`cosy:absolute cosy:inset-0 cosy:w-full cosy:h-full ${rounded !== 'none' ? `cosy:rounded-${rounded}` : ''}`}>
|
|
90
93
|
{
|
|
91
94
|
isImageBackground && (
|
|
92
95
|
<Image
|
|
93
96
|
src={background as ImageMetadata}
|
|
94
97
|
alt="页面背景"
|
|
95
|
-
class=
|
|
98
|
+
class={`cosy:w-full cosy:h-full cosy:object-cover ${rounded !== 'none' ? `cosy:rounded-${rounded}` : ''}`}
|
|
96
99
|
/>
|
|
97
100
|
)
|
|
98
101
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export interface IApiEndpoint {
|
|
2
|
+
/**
|
|
3
|
+
* API端点名称
|
|
4
|
+
*/
|
|
5
|
+
name: string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HTTP请求方法
|
|
9
|
+
*/
|
|
10
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* API端点路径
|
|
14
|
+
*/
|
|
15
|
+
path: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* API端点描述
|
|
19
|
+
*/
|
|
20
|
+
description?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 请求参数配置
|
|
24
|
+
*/
|
|
25
|
+
params?: IApiParam[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 快速测试配置
|
|
29
|
+
*/
|
|
30
|
+
quickTests?: IQuickTest[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IApiParam {
|
|
34
|
+
/**
|
|
35
|
+
* 参数名称
|
|
36
|
+
*/
|
|
37
|
+
name: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 参数占位符文本
|
|
41
|
+
*/
|
|
42
|
+
placeholder: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 参数类型
|
|
46
|
+
* @default "text"
|
|
47
|
+
*/
|
|
48
|
+
type?: 'text' | 'number' | 'select' | 'textarea' | 'checkbox' | 'radio';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 选择器选项(当type为select或radio时使用)
|
|
52
|
+
*/
|
|
53
|
+
options?: string[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 是否为必填参数
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
required?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 参数默认值
|
|
63
|
+
*/
|
|
64
|
+
defaultValue?: string | number | boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 参数验证规则
|
|
68
|
+
*/
|
|
69
|
+
validation?: {
|
|
70
|
+
min?: number;
|
|
71
|
+
max?: number;
|
|
72
|
+
pattern?: string;
|
|
73
|
+
message?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IQuickTest {
|
|
78
|
+
/**
|
|
79
|
+
* 快速测试标签
|
|
80
|
+
*/
|
|
81
|
+
label: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 快速测试参数值
|
|
85
|
+
*/
|
|
86
|
+
values: Record<string, string | number | boolean>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 快速测试描述
|
|
90
|
+
*/
|
|
91
|
+
description?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IApiTesterProps {
|
|
95
|
+
/**
|
|
96
|
+
* API端点配置列表
|
|
97
|
+
*/
|
|
98
|
+
endpoints: IApiEndpoint[];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 组件标题
|
|
102
|
+
* @default "API 测试"
|
|
103
|
+
*/
|
|
104
|
+
title?: string;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 组件描述
|
|
108
|
+
*/
|
|
109
|
+
description?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 是否显示请求头配置
|
|
113
|
+
* @default false
|
|
114
|
+
*/
|
|
115
|
+
showHeaders?: boolean;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 默认请求头
|
|
119
|
+
*/
|
|
120
|
+
defaultHeaders?: Record<string, string>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 是否显示响应时间
|
|
124
|
+
* @default true
|
|
125
|
+
*/
|
|
126
|
+
showResponseTime?: boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 是否显示请求详情
|
|
130
|
+
* @default false
|
|
131
|
+
*/
|
|
132
|
+
showRequestDetails?: boolean;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 自定义CSS类名
|
|
136
|
+
*/
|
|
137
|
+
class?: string;
|
|
138
|
+
}
|