@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.
@@ -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
+ />
@@ -0,0 +1,5 @@
1
+ import ApiTester from './ApiTester.astro';
2
+ import ApiEndpointCard from './ApiEndpointCard.astro';
3
+ import ApiTestScript from './ApiTestScript.astro';
4
+
5
+ export { ApiTester, ApiEndpointCard, ApiTestScript };
@@ -0,0 +1,5 @@
1
+ import ApiTester from './ApiTester.astro';
2
+ import ApiEndpointCard from './ApiEndpointCard.astro';
3
+ import ApiTestScript from './ApiTestScript.astro';
4
+
5
+ export { ApiTester, ApiEndpointCard, ApiTestScript };
@@ -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={false}
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="xl"
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="cosy:absolute cosy:inset-0 cosy:w-full cosy:h-full cosy:rounded-xl">
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="cosy:w-full cosy:h-full cosy:object-cover cosy:rounded-xl"
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffic/cosy-ui",
3
- "version": "0.9.39",
3
+ "version": "0.9.40",
4
4
  "description": "An astro component library",
5
5
  "author": {
6
6
  "name": "nookery",