hookehuyr

fix(build): 修复 API 文档生成器嵌套对象解析问题

- 添加递归函数支持任意深度嵌套
- 修复 list 字段无法展开的问题
- 支持四层嵌套结构
- 重新生成所有 API 文档
- 修复 AmountInput ESLint 错误
1 +# 搜索
2 +
3 +## OpenAPI Specification
4 +
5 +```yaml
6 +openapi: 3.0.1
7 +info:
8 + title: ''
9 + version: 1.0.0
10 +paths:
11 + /srv/:
12 + get:
13 + summary: 搜索
14 + deprecated: false
15 + description: ''
16 + tags: []
17 + parameters:
18 + - name: f
19 + in: query
20 + description: ''
21 + required: true
22 + example: manulife
23 + schema:
24 + type: string
25 + - name: a
26 + in: query
27 + description: ''
28 + required: true
29 + example: search
30 + schema:
31 + type: string
32 + - name: t
33 + in: query
34 + description: ''
35 + required: false
36 + example: icon
37 + schema:
38 + type: string
39 + - name: keyword
40 + in: query
41 + description: ''
42 + required: false
43 + schema:
44 + type: string
45 + - name: type
46 + in: query
47 + description: product=产品,file=文档
48 + required: false
49 + schema:
50 + type: string
51 + responses:
52 + '200':
53 + description: ''
54 + content:
55 + application/json:
56 + schema:
57 + type: object
58 + properties:
59 + code:
60 + type: integer
61 + msg:
62 + type: integer
63 + data:
64 + type: object
65 + properties:
66 + products:
67 + type: object
68 + properties:
69 + list:
70 + type: array
71 + items:
72 + type: object
73 + properties:
74 + id:
75 + type: integer
76 + title: 产品id
77 + product_name:
78 + type: string
79 + title: 产品名
80 + product_description:
81 + type: string
82 + title: 产品描述
83 + nullable: true
84 + recommend:
85 + type: string
86 + title: 推荐位
87 + description: normal-普通, hot-热卖
88 + created_time:
89 + type: string
90 + title: 创建时间
91 + cover_image:
92 + type: string
93 + title: 封面图
94 + nullable: true
95 + tags:
96 + type: array
97 + items:
98 + type: object
99 + properties:
100 + id:
101 + type: string
102 + title: 标签id
103 + name:
104 + type: string
105 + title: 标签名
106 + bg_color:
107 + type: string
108 + title: 标签背景色
109 + text_color:
110 + type: string
111 + title: 标签文字色
112 + required:
113 + - id
114 + - name
115 + - bg_color
116 + - text_color
117 + x-apifox-orders:
118 + - id
119 + - name
120 + - bg_color
121 + - text_color
122 + title: 产品标签
123 + type:
124 + type: string
125 + form_sn:
126 + type: string
127 + title: 表单类型
128 + required:
129 + - id
130 + - product_name
131 + - product_description
132 + - recommend
133 + - created_time
134 + - cover_image
135 + - tags
136 + - type
137 + - form_sn
138 + x-apifox-orders:
139 + - id
140 + - product_name
141 + - product_description
142 + - recommend
143 + - created_time
144 + - cover_image
145 + - form_sn
146 + - tags
147 + - type
148 + total:
149 + type: integer
150 + title: 产品总数
151 + required:
152 + - list
153 + - total
154 + x-apifox-orders:
155 + - list
156 + - total
157 + title: 产品列表
158 + files:
159 + type: object
160 + properties:
161 + list:
162 + type: array
163 + items:
164 + type: object
165 + properties:
166 + id:
167 + type: integer
168 + title: 附件id
169 + name:
170 + type: string
171 + title: 附件名
172 + value:
173 + type: string
174 + title: 附件地址
175 + extension:
176 + type: string
177 + title: 附件类型
178 + post_date:
179 + type: string
180 + title: 发布时间
181 + size:
182 + type: string
183 + title: 附件大小
184 + is_favorite:
185 + type: integer
186 + title: 是否收藏
187 + type:
188 + type: string
189 + required:
190 + - id
191 + - name
192 + - value
193 + - extension
194 + - post_date
195 + - size
196 + - is_favorite
197 + - type
198 + x-apifox-orders:
199 + - id
200 + - name
201 + - value
202 + - extension
203 + - post_date
204 + - size
205 + - is_favorite
206 + - type
207 + total:
208 + type: integer
209 + title: 附件数
210 + required:
211 + - list
212 + - total
213 + x-apifox-orders:
214 + - list
215 + - total
216 + title: 文件列表
217 + required:
218 + - products
219 + - files
220 + x-apifox-orders:
221 + - products
222 + - files
223 + required:
224 + - code
225 + - msg
226 + - data
227 + x-apifox-orders:
228 + - code
229 + - msg
230 + - data
231 + example: |-
232 + {
233 + "code": 1,
234 + "msg": 0,
235 + "data": {
236 + "products": {
237 + "list": [
238 + {
239 + "id": 2769848,
240 + "product_name": "1111",
241 + "product_description": "<p>5564</p>\r\n<p>hdye</p>",
242 + "recommend": "normal",
243 + "created_time": "2026-02-02 16:33:01",
244 + "cover_image": "https://cdn.ipadbiz.cn/space_30901/申请提交-生成计划书_FvdRVOS0K-Wmp05ZKHpx64sEXcKQ.png",
245 + "tags": [
246 + {
247 + "id": "2769846",
248 + "name": "测试1",
249 + "bg_color": "#1e9fff",
250 + "text_color": "#ffffff"
251 + },
252 + {
253 + "id": "2769847",
254 + "name": "111",
255 + "bg_color": "#3e5160",
256 + "text_color": "#ffffff"
257 + }
258 + ],
259 + "type": "product"
260 + },
261 + {
262 + "id": 2769845,
263 + "product_name": "1",
264 + "product_description": null,
265 + "recommend": "normal",
266 + "created_time": "2026-02-02 16:22:26",
267 + "cover_image": null,
268 + "tags": [],
269 + "type": "product"
270 + }
271 + ],
272 + "total": 2,
273 + },
274 + "files": {
275 + "list": [
276 + {
277 + "id": 2769815,
278 + "name": "9751dd823b7542d78fa7bde615a43ba1",
279 + "value": "https://cdn.ipadbiz.cn/space_30901/9751dd823b7542d78fa7bde615a43ba1_Fsm43FPaZOwKc5pwbxhk-h2zUUsu.png",
280 + "extension": "png",
281 + "post_date": "2026-01-13 11:26:07",
282 + "size": "0 B",
283 + "is_favorite": 0,
284 + "type": "file"
285 + },
286 + {
287 + "id": 2769814,
288 + "name": "微信图片_2026-01-12_173653_750",
289 + "value": "https://cdn.ipadbiz.cn/space_30901/微信图片_2026-01-12_173653_750_Fuu11vu9Zp5B626kEbWE62vQLeA7.png",
290 + "extension": "png",
291 + "post_date": "2026-01-13 11:26:07",
292 + "size": "0 B",
293 + "is_favorite": 0,
294 + "type": "file"
295 + },
296 + {
297 + "id": 2769811,
298 + "name": "微信图片_2026-01-12_173913_253",
299 + "value": "https://cdn.ipadbiz.cn/space_30901/微信图片_2026-01-12_173913_253_FoQoABNxtf4dB2vKLD-Eke8Tsgu5.png",
300 + "extension": "png",
301 + "post_date": "2026-01-13 11:25:47",
302 + "size": "0 B",
303 + "is_favorite": 0,
304 + "type": "file"
305 + },
306 + {
307 + "id": 2768696,
308 + "name": "123222",
309 + "value": "https://cdn.ipadbiz.cn/space/30901/4c2ab735c6996a89d7c423bf428a1748.png",
310 + "extension": "png",
311 + "post_date": "2025-10-30 18:30:28",
312 + "size": "0 B",
313 + "is_favorite": 0,
314 + "type": "file"
315 + }
316 + ],
317 + "total": 4,
318 + }
319 + }
320 + }
321 + headers: {}
322 + x-apifox-name: 成功
323 + x-apifox-ordering: 0
324 + security: []
325 + x-apifox-folder: ''
326 + x-apifox-status: released
327 + x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-416069509-run
328 +components:
329 + schemas: {}
330 + responses: {}
331 + securitySchemes: {}
332 +servers:
333 + - url: https://manulife.onwall.cn
334 + description: 正式环境
335 +security: []
336 +
337 +```
...@@ -366,6 +366,48 @@ function generateParamJSDoc(parameters, bodyParams, method) { ...@@ -366,6 +366,48 @@ function generateParamJSDoc(parameters, bodyParams, method) {
366 } 366 }
367 367
368 /** 368 /**
369 + * 递归生成属性字段的 JSDoc 注释
370 + * @param {object} properties - 属性对象
371 + * @param {number} indent - 缩进级别(空格数)
372 + * @returns {string} - JSDoc 注释
373 + */
374 +function generatePropertiesJSDoc(properties, indent = 0) {
375 + const lines = [];
376 + const prefix = ' '.repeat(indent);
377 +
378 + Object.entries(properties).forEach(([key, value]) => {
379 + const type = value.type || 'any';
380 + const desc = value.description || value.title || '';
381 +
382 + // 处理嵌套对象
383 + if (type === 'object' && value.properties) {
384 + lines.push(`${prefix}${key}: {\n`);
385 + // 递归处理嵌套对象的属性
386 + lines.push(generatePropertiesJSDoc(value.properties, indent + 2));
387 + lines.push(`${prefix}};\n`);
388 + }
389 + // 处理数组(元素是对象)
390 + else if (type === 'array' && value.items && value.items.properties) {
391 + lines.push(`${prefix}${key}: Array<{\n`);
392 + // 递归处理数组元素的属性
393 + lines.push(generatePropertiesJSDoc(value.items.properties, indent + 2));
394 + lines.push(`${prefix}}>;\n`);
395 + }
396 + // 处理简单数组
397 + else if (type === 'array' && value.items) {
398 + const itemType = value.items.type || 'any';
399 + lines.push(`${prefix}${key}: Array<${itemType}>; // ${desc}\n`);
400 + }
401 + // 处理基本类型
402 + else {
403 + lines.push(`${prefix}${key}: ${type}; // ${desc}\n`);
404 + }
405 + });
406 +
407 + return lines.join('');
408 +}
409 +
410 +/**
369 * 生成 JSDoc 返回值注释 411 * 生成 JSDoc 返回值注释
370 * @param {object} responseSchema - 响应 schema 412 * @param {object} responseSchema - 响应 schema
371 * @returns {string} - JSDoc 返回值注释 413 * @returns {string} - JSDoc 返回值注释
...@@ -388,44 +430,14 @@ function generateReturnJSDoc(responseSchema) { ...@@ -388,44 +430,14 @@ function generateReturnJSDoc(responseSchema) {
388 // 处理对象类型的 data 430 // 处理对象类型的 data
389 if (dataType === 'object' && data.properties) { 431 if (dataType === 'object' && data.properties) {
390 returnDesc += ' * data: {\n'; 432 returnDesc += ' * data: {\n';
391 - 433 + // 使用递归函数处理 data 的所有属性
392 - Object.entries(data.properties).forEach(([key, value]) => { 434 + returnDesc += generatePropertiesJSDoc(data.properties, 4);
393 - const type = value.type || 'any';
394 - const desc = value.description || value.title || '';
395 -
396 - if (type === 'object' && value.properties) {
397 - returnDesc += ` * ${key}: {\n`;
398 - Object.entries(value.properties).forEach(([subKey, subValue]) => {
399 - const subType = subValue.type || 'any';
400 - const subDesc = subValue.description || subValue.title || '';
401 - returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
402 - });
403 - returnDesc += ` * };\n`;
404 - } else if (type === 'array' && value.items && value.items.properties) {
405 - returnDesc += ` * ${key}: Array<{\n`;
406 - Object.entries(value.items.properties).forEach(([subKey, subValue]) => {
407 - const subType = subValue.type || 'any';
408 - const subDesc = subValue.description || subValue.title || '';
409 - returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
410 - });
411 - returnDesc += ` * }>;\n`;
412 - } else {
413 - returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
414 - }
415 - });
416 -
417 returnDesc += ' * };\n'; 435 returnDesc += ' * };\n';
418 } 436 }
419 - // 处理数组类型的 data(你的情况 437 + // 处理数组类型的 data(元素是对象
420 else if (dataType === 'array' && data.items && data.items.properties) { 438 else if (dataType === 'array' && data.items && data.items.properties) {
421 returnDesc += ' * data: Array<{\n'; 439 returnDesc += ' * data: Array<{\n';
422 - 440 + returnDesc += generatePropertiesJSDoc(data.items.properties, 4);
423 - Object.entries(data.items.properties).forEach(([key, value]) => {
424 - const type = value.type || 'any';
425 - const desc = value.description || value.title || '';
426 - returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
427 - });
428 -
429 returnDesc += ' * }>;\n'; 441 returnDesc += ' * }>;\n';
430 } 442 }
431 // 处理简单数组类型 443 // 处理简单数组类型
......
...@@ -43,13 +43,13 @@ export const delAPI = (params) => fn(fetch.post(Api.Del, params)); ...@@ -43,13 +43,13 @@ export const delAPI = (params) => fn(fetch.post(Api.Del, params));
43 * code: number; // 状态码 43 * code: number; // 状态码
44 * msg: string; // 消息 44 * msg: string; // 消息
45 * data: { 45 * data: {
46 - * list: Array<{ 46 + list: Array<{
47 - * meta_id: integer; // 文件ID 47 + meta_id: integer; // 文件ID
48 - * name: string; // 文件名称 48 + name: string; // 文件名称
49 - * src: string; // 文件URL 49 + src: string; // 文件URL
50 - * created_time: string; // 收藏时间 50 + created_time: string; // 收藏时间
51 - * size: string; // 文件大小 51 + size: string; // 文件大小
52 - * }>; 52 + }>;
53 * }; 53 * };
54 * }>} 54 * }>}
55 */ 55 */
......
...@@ -30,16 +30,16 @@ export const addAPI = (params) => fn(fetch.post(Api.Add, params)); ...@@ -30,16 +30,16 @@ export const addAPI = (params) => fn(fetch.post(Api.Add, params));
30 * code: number; // 状态码 30 * code: number; // 状态码
31 * msg: string; // 消息 31 * msg: string; // 消息
32 * data: { 32 * data: {
33 - * list: Array<{ 33 + list: Array<{
34 - * id: integer; // 订单ID 34 + id: integer; // 订单ID
35 - * status: integer; // 3=待处理, 5=已处理 35 + status: integer; // 3=待处理, 5=已处理
36 - * category: string; // 1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题 36 + category: string; // 1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题
37 - * images: string; // 图片 37 + images: string; // 图片
38 - * contact: string; // 联系方式 38 + contact: string; // 联系方式
39 - * note: string; // 反馈内容 39 + note: string; // 反馈内容
40 - * reply: string; // 回复 40 + reply: string; // 回复
41 - * reply_time: string; // 回复时间 41 + reply_time: string; // 回复时间
42 - * }>; 42 + }>;
43 * }; 43 * };
44 * }>} 44 * }>}
45 */ 45 */
......
...@@ -19,32 +19,46 @@ const Api = { ...@@ -19,32 +19,46 @@ const Api = {
19 * code: number; // 状态码 19 * code: number; // 状态码
20 * msg: string; // 消息 20 * msg: string; // 消息
21 * data: { 21 * data: {
22 - * cate: { 22 + cate: {
23 - * id: integer; // 分类id 23 + id: integer; // 分类id
24 - * category_name: string; // 分类名称 24 + category_name: string; // 分类名称
25 - * category_parent: integer; // 分类父级 25 + category_parent: integer; // 分类父级
26 - * category_description: null; // 分类描述 26 + category_description: null; // 分类描述
27 - * }; 27 + };
28 - * children: Array<{ 28 + children: Array<{
29 - * id: integer; // 二级分类id 29 + id: integer; // 二级分类id
30 - * category_name: string; // 二级分类名 30 + category_name: string; // 二级分类名
31 - * category_parent: integer; // 二级分类名父级id 31 + category_parent: integer; // 二级分类名父级id
32 - * category_description: null; // 二级分类描述 32 + category_description: null; // 二级分类描述
33 - * icon: string; // 二级分类图标 33 + icon: string; // 二级分类图标
34 - * list: array; // 二级分类的附件列表 34 + list: Array<{
35 - * children: array; // 三级分类 35 + name: string; // 附件名称
36 - * }>; 36 + value: string; // 附件地址
37 - * list: Array<{ 37 + extension: string; // 后缀名
38 - * id: integer; // 38 + post_date: string; // 发布时间
39 - * name: string; // 附件名称 39 + size: string; // 附件大小
40 - * value: string; // 附件地址 40 + is_favorite: integer; // 是否收藏
41 - * extension: string; // 后缀名 41 + id: string; // 附件id
42 - * post_date: string; // 发布时间 42 + }>;
43 - * size: string; // 附件大小 43 + children: Array<{
44 - * is_favorite: integer; // 是否收藏 44 + id: integer; // 三级分类id
45 - * }>; 45 + category_name: string; // 三级分类名
46 - * total: integer; // 主分类附件数量 46 + category_parent: integer; // 三级分类名父级id
47 - * max_level: integer; // 页面需要层级 47 + category_description: null; // 三级分类描述
48 + icon: string; // 二级分类图标
49 + }>;
50 + }>;
51 + list: Array<{
52 + id: integer; //
53 + name: string; // 附件名称
54 + value: string; // 附件地址
55 + extension: string; // 后缀名
56 + post_date: string; // 发布时间
57 + size: string; // 附件大小
58 + is_favorite: integer; // 是否收藏
59 + }>;
60 + total: integer; // 主分类附件数量
61 + max_level: integer; // 页面需要层级
48 * }; 62 * };
49 * }>} 63 * }>}
50 */ 64 */
...@@ -60,15 +74,15 @@ export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params)); ...@@ -60,15 +74,15 @@ export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params));
60 * code: number; // 状态码 74 * code: number; // 状态码
61 * msg: string; // 消息 75 * msg: string; // 消息
62 * data: { 76 * data: {
63 - * list: Array<{ 77 + list: Array<{
64 - * meta_id: integer; // 文件ID 78 + meta_id: integer; // 文件ID
65 - * name: string; // 文件名称 79 + name: string; // 文件名称
66 - * src: string; // 文件URL 80 + src: string; // 文件URL
67 - * size: string; // 文件大小 81 + size: string; // 文件大小
68 - * read_people_count: integer; // 学习人数 82 + read_people_count: integer; // 学习人数
69 - * read_people_percent: number; // 学习人数比例 83 + read_people_percent: number; // 学习人数比例
70 - * is_favorite: string; // 84 + is_favorite: string; //
71 - * }>; 85 + }>;
72 * }; 86 * };
73 * }>} 87 * }>}
74 */ 88 */
......
...@@ -15,33 +15,33 @@ const Api = { ...@@ -15,33 +15,33 @@ const Api = {
15 * code: number; // 状态码 15 * code: number; // 状态码
16 * msg: string; // 消息 16 * msg: string; // 消息
17 * data: { 17 * data: {
18 - * id: integer; // 产品id 18 + id: integer; // 产品id
19 - * product_name: string; // 产品名 19 + product_name: string; // 产品名
20 - * recommend: string; // 推荐位: normal-普通, hot-热卖 20 + recommend: string; // 推荐位: normal-普通, hot-热卖
21 - * status: string; // 21 + status: string; //
22 - * created_by: integer; // 22 + created_by: integer; //
23 - * created_time: string; // 23 + created_time: string; //
24 - * updated_by: integer; // 24 + updated_by: integer; //
25 - * updated_time: string; // 25 + updated_time: string; //
26 - * form_sn: string; // 关联表单sn 26 + form_sn: string; // 关联表单sn
27 - * product_description: string; // 产品描述 27 + product_description: string; // 产品描述
28 - * categories: Array<{ 28 + categories: Array<{
29 - * id: string; // 分类id 29 + id: string; // 分类id
30 - * name: string; // 分类名称 30 + name: string; // 分类名称
31 - * }>; 31 + }>;
32 - * tags: Array<{ 32 + tags: Array<{
33 - * id: string; // 标签id 33 + id: string; // 标签id
34 - * name: string; // 标签名 34 + name: string; // 标签名
35 - * bg_color: string; // 标签背景色 35 + bg_color: string; // 标签背景色
36 - * text_color: string; // 标签文字色 36 + text_color: string; // 标签文字色
37 - * }>; 37 + }>;
38 - * documents: Array<{ 38 + documents: Array<{
39 - * file_url: string; // 附件地址 39 + file_url: string; // 附件地址
40 - * file_name: string; // 附件名 40 + file_name: string; // 附件名
41 - * file_size: string; // 附件大小 41 + file_size: string; // 附件大小
42 - * file_size_formatted: string; // 附件大小(转换过显示) 42 + file_size_formatted: string; // 附件大小(转换过显示)
43 - * }>; 43 + }>;
44 - * cover_image: string; // 产品封面图 44 + cover_image: string; // 产品封面图
45 * }; 45 * };
46 * }>} 46 * }>}
47 */ 47 */
...@@ -61,21 +61,29 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params)); ...@@ -61,21 +61,29 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
61 * code: number; // 状态码 61 * code: number; // 状态码
62 * msg: string; // 消息 62 * msg: string; // 消息
63 * data: { 63 * data: {
64 - * categories: Array<{ 64 + categories: Array<{
65 - * id: integer; // 分类id 65 + id: integer; // 分类id
66 - * name: string; // 分类名 66 + name: string; // 分类名
67 - * }>; 67 + }>;
68 - * list: Array<{ 68 + list: Array<{
69 - * id: integer; // 产品id 69 + id: integer; // 产品id
70 - * product_name: string; // 产品名 70 + product_name: string; // 产品名
71 - * recommend: string; // 推荐位: normal-普通, hot-热卖 71 + recommend: string; // 推荐位: normal-普通, hot-热卖
72 - * form_sn: string; // 72 + form_sn: string; //
73 - * created_time: string; // 创建时间 73 + created_time: string; // 创建时间
74 - * categories: array; // 产品所属分类 74 + categories: Array<{
75 - * tags: array; // 产品标签 75 + id: string; // 分类id
76 - * cover_image: string; // 产品封面图 76 + name: string; // 分类名
77 - * }>; 77 + }>;
78 - * total: integer; // 产品总数 78 + tags: Array<{
79 + id: string; // 标签id
80 + name: string; // 标签名
81 + bg_color: string; // 标签背景色
82 + text_color: string; // 标签文字色
83 + }>;
84 + cover_image: string; // 产品封面图
85 + }>;
86 + total: integer; // 产品总数
79 * }; 87 * };
80 * }>} 88 * }>}
81 */ 89 */
......
...@@ -12,11 +12,11 @@ const Api = { ...@@ -12,11 +12,11 @@ const Api = {
12 * code: number; // 状态码 12 * code: number; // 状态码
13 * msg: string; // 消息 13 * msg: string; // 消息
14 * data: Array<{ 14 * data: Array<{
15 - * id: integer; // 15 + id: integer; //
16 - * name: string; // 16 + name: string; //
17 - * seq: integer; // 17 + seq: integer; //
18 - * link: string; // 18 + link: string; //
19 - * icon: string; // 19 + icon: string; //
20 * }>; 20 * }>;
21 * }>} 21 * }>}
22 */ 22 */
......
...@@ -14,10 +14,10 @@ const Api = { ...@@ -14,10 +14,10 @@ const Api = {
14 * code: number; // 状态码 14 * code: number; // 状态码
15 * msg: string; // 消息 15 * msg: string; // 消息
16 * data: Array<{ 16 * data: Array<{
17 - * id: integer; // 消息id 17 + id: integer; // 消息id
18 - * note: string; // 消息内容 18 + note: string; // 消息内容
19 - * created_time: string; // 发消息的时间 19 + created_time: string; // 发消息的时间
20 - * status: string; // send=以发送未读取,read=已读取 20 + status: string; // send=以发送未读取,read=已读取
21 * }>; 21 * }>;
22 * }>} 22 * }>}
23 */ 23 */
...@@ -33,10 +33,10 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params)); ...@@ -33,10 +33,10 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
33 * code: number; // 状态码 33 * code: number; // 状态码
34 * msg: string; // 消息 34 * msg: string; // 消息
35 * data: Array<{ 35 * data: Array<{
36 - * id: integer; // 消息id 36 + id: integer; // 消息id
37 - * note: string; // 消息内容 37 + note: string; // 消息内容
38 - * created_time: string; // 发消息的时间 38 + created_time: string; // 发消息的时间
39 - * status: string; // send=以发送未读取,read=已读取 39 + status: string; // send=以发送未读取,read=已读取
40 * }>; 40 * }>;
41 * }>} 41 * }>}
42 */ 42 */
......
1 +import { fn, fetch } from '@/api/fn';
2 +
3 +const Api = {
4 + Search: '/srv/?a=search&t=icon',
5 +}
6 +
7 +/**
8 + * @description 搜索
9 + * @remark
10 + * @param {Object} params 请求参数
11 + * @param {string} params.keyword (可选)
12 + * @param {string} params.type (可选) product=产品,file=文档
13 + * @returns {Promise<{
14 + * code: number; // 状态码
15 + * msg: string; // 消息
16 + * data: {
17 + products: {
18 + list: Array<{
19 + id: integer; // 产品id
20 + product_name: string; // 产品名
21 + product_description: string; // 产品描述
22 + recommend: string; // normal-普通, hot-热卖
23 + created_time: string; // 创建时间
24 + cover_image: string; // 封面图
25 + tags: Array<{
26 + id: string; // 标签id
27 + name: string; // 标签名
28 + bg_color: string; // 标签背景色
29 + text_color: string; // 标签文字色
30 + }>;
31 + type: string; //
32 + form_sn: string; // 表单类型
33 + }>;
34 + total: integer; // 产品总数
35 + };
36 + files: {
37 + list: Array<{
38 + id: integer; // 附件id
39 + name: string; // 附件名
40 + value: string; // 附件地址
41 + extension: string; // 附件类型
42 + post_date: string; // 发布时间
43 + size: string; // 附件大小
44 + is_favorite: integer; // 是否收藏
45 + type: string; //
46 + }>;
47 + total: integer; // 附件数
48 + };
49 + * };
50 + * }>}
51 + */
52 +export const searchAPI = (params) => fn(fetch.get(Api.Search, params));
...@@ -16,12 +16,19 @@ const Api = { ...@@ -16,12 +16,19 @@ const Api = {
16 * code: number; // 状态码 16 * code: number; // 状态码
17 * msg: string; // 消息 17 * msg: string; // 消息
18 * data: { 18 * data: {
19 - * user: { 19 + user: {
20 - * id: integer; // 用户ID 20 + id: integer; // 用户ID
21 - * name: string; // 姓名 21 + name: string; // 姓名
22 - * employee_no: string; // 工号 22 + employee_no: string; // 工号
23 - * avatar: object; // 头像 23 + avatar: {
24 - * }; 24 + name: string; // 文件名
25 + hash: string; // 文件hash
26 + src: string; // 文件地址
27 + height: string; // 文件高度
28 + width: string; // 文件宽度
29 + size: integer; // 文件大小
30 + };
31 + };
25 * }; 32 * };
26 * }>} 33 * }>}
27 */ 34 */
...@@ -49,8 +56,8 @@ export const loginAPI = (params) => fn(fetch.post(Api.Login, params)); ...@@ -49,8 +56,8 @@ export const loginAPI = (params) => fn(fetch.post(Api.Login, params));
49 * code: number; // 状态码 56 * code: number; // 状态码
50 * msg: string; // 消息 57 * msg: string; // 消息
51 * data: { 58 * data: {
52 - * is_login: boolean; // true=登录,false=未登录 59 + is_login: boolean; // true=登录,false=未登录
53 - * is_openid: boolean; // true=已授权,false=未授权 60 + is_openid: boolean; // true=已授权,false=未授权
54 * }; 61 * };
55 * }>} 62 * }>}
56 */ 63 */
......
...@@ -15,11 +15,11 @@ const Api = { ...@@ -15,11 +15,11 @@ const Api = {
15 * code: number; // 状态码 15 * code: number; // 状态码
16 * msg: string; // 消息 16 * msg: string; // 消息
17 * data: { 17 * data: {
18 - * user: { 18 + user: {
19 - * id: integer; // 用户ID 19 + id: integer; // 用户ID
20 - * avatar_url: string; // 头像 20 + avatar_url: string; // 头像
21 - * name: string; // 姓名 21 + name: string; // 姓名
22 - * }; 22 + };
23 * }; 23 * };
24 * }>} 24 * }>}
25 */ 25 */
......
1 +<template>
2 + <div>
3 + <!-- 标签 -->
4 + <div v-if="label" class="text-sm text-gray-600 mb-2">
5 + {{ label }}
6 + <span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span>
7 + </div>
8 +
9 + <!-- 多币种模式(方案 2 - 未来扩展) -->
10 + <div v-if="multiCurrencyEnabled" class="mb-2">
11 + <div class="text-sm text-gray-600 mb-2">币种</div>
12 + <div class="flex gap-2">
13 + <button
14 + v-for="curr in supportedCurrencies"
15 + :key="curr.value"
16 + :class="[
17 + 'px-4 py-2 rounded-lg text-sm border transition-colors',
18 + selectedCurrency === curr.value
19 + ? 'bg-blue-600 text-white border-blue-600'
20 + : 'bg-white text-gray-600 border-gray-200'
21 + ]"
22 + @tap="selectCurrency(curr.value)"
23 + >
24 + {{ curr.label }}
25 + </button>
26 + </div>
27 + </div>
28 +
29 + <!-- 保额输入 -->
30 + <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden">
31 + <nut-input
32 + :model-value="formattedValue"
33 + @input="onInput"
34 + type="digit"
35 + :placeholder="placeholder"
36 + class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900"
37 + :border="false"
38 + />
39 + <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span>
40 + </div>
41 + </div>
42 +</template>
43 +
44 +<script setup>
45 +/**
46 + * 保额输入组件
47 + *
48 + * @description 支持多币种的保额输入组件
49 + * - 单位转换:内部存储为分(整数),显示为元(带2位小数)
50 + * - 币种支持:CNY、USD、HKD、EUR
51 + * - 多币种模式:通过 FEATURE_FLAGS.MULTI_CURRENCY_ENABLED 控制
52 + * @author Claude Code
53 + * @example
54 + * <!-- 固定币种模式 -->
55 + * <AmountInput
56 + * v-model="coverage"
57 + * label="保额"
58 + * currency="USD"
59 + * placeholder="请输入保额"
60 + * />
61 + *
62 + * @example
63 + * <!-- 多币种模式 -->
64 + * <AmountInput
65 + * v-model="coverage"
66 + * label="保额"
67 + * :config="{ supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }"
68 + * placeholder="请输入保额"
69 + * />
70 + */
71 +import { ref, computed } from 'vue'
72 +import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates'
73 +
74 +/**
75 + * 组件属性
76 + */
77 +const props = defineProps({
78 + /**
79 + * 标签文本
80 + * @type {string}
81 + */
82 + label: {
83 + type: String,
84 + default: ''
85 + },
86 +
87 + /**
88 + * 占位符文本
89 + * @type {string}
90 + */
91 + placeholder: {
92 + type: String,
93 + default: '请输入保额'
94 + },
95 +
96 + /**
97 + * 绑定的值(单位:分)
98 + * @type {number}
99 + * @example 100000 表示 1000.00 元
100 + */
101 + modelValue: {
102 + type: Number,
103 + default: null
104 + },
105 +
106 + /**
107 + * 币种代码(固定币种模式)
108 + * @type {string}
109 + * @default 'CNY'
110 + */
111 + currency: {
112 + type: String,
113 + default: 'CNY'
114 + },
115 +
116 + /**
117 + * 模版配置(多币种模式)
118 + * @type {Object}
119 + * @property {Array<string>} supported_currencies - 支持的币种代码数组
120 + * @property {string} default_currency - 默认币种代码
121 + * @example { supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }
122 + */
123 + config: {
124 + type: Object,
125 + default: () => ({})
126 + }
127 +})
128 +
129 +/**
130 + * 组件事件
131 + */
132 +const emit = defineEmits([
133 + /**
134 + * 更新值事件
135 + * @event update:modelValue
136 + * @param {number} value - 保额值(单位:分)
137 + */
138 + 'update:modelValue'
139 +])
140 +
141 +/**
142 + * 判断是否启用多币种
143 + * @type {ComputedRef<boolean>}
144 + */
145 +const multiCurrencyEnabled = computed(() => FEATURE_FLAGS.MULTI_CURRENCY_ENABLED)
146 +
147 +/**
148 + * 当前选中的币种
149 + * @type {Ref<string>}
150 + */
151 +const selectedCurrency = ref(props.config.default_currency || props.currency || 'CNY')
152 +
153 +/**
154 + * 支持的币种列表(多币种模式)
155 + * @type {ComputedRef<Array<{label: string, symbol: string, value: string}>>}
156 + */
157 +const supportedCurrencies = computed(() => {
158 + if (!multiCurrencyEnabled.value) return []
159 +
160 + return (props.config.supported_currencies || ['CNY'])
161 + .map(code => CURRENCY_MAP[code])
162 + .filter(Boolean)
163 +})
164 +
165 +/**
166 + * 当前币种符号
167 + * @type {ComputedRef<string>}
168 + * @example
169 + * // CNY -> '¥'
170 + * // USD -> '$'
171 + */
172 +const currencySymbol = computed(() => {
173 + if (multiCurrencyEnabled.value) {
174 + // 多币种模式:使用用户选择的币种
175 + const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
176 + return curr?.symbol || '¥'
177 + }
178 +
179 + // 固定币种模式:使用 props.currency
180 + return CURRENCY_SYMBOLS[props.currency] || '¥'
181 +})
182 +
183 +/**
184 + * 币种文本(用于标签显示)
185 + * @type {ComputedRef<string>}
186 + */
187 +const currencyText = computed(() => {
188 + if (multiCurrencyEnabled.value) {
189 + const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
190 + return curr?.label || ''
191 + }
192 +
193 + const CURRENCY_NAMES = {
194 + CNY: '人民币',
195 + USD: '美元',
196 + HKD: '港币',
197 + EUR: '欧元'
198 + }
199 +
200 + return CURRENCY_NAMES[props.currency] || ''
201 +})
202 +
203 +/**
204 + * 格式化显示值(元,带2位小数)
205 + * @description 将分转换为元进行显示
206 + * @type {ComputedRef<string>}
207 + * @example
208 + * // modelValue = 100000 (分)
209 + * // formattedValue() // 返回: '1000.00'
210 + */
211 +const formattedValue = computed(() => {
212 + if (props.modelValue === null || props.modelValue === undefined) {
213 + return ''
214 + }
215 + // 分 -> 元,保留2位小数
216 + return (props.modelValue / 100).toFixed(2)
217 +})
218 +
219 +/**
220 + * 用户输入处理
221 + * @description 将用户输入的元转换为分存储
222 + * @param {string} value - 输入值
223 + *
224 + * @example
225 + * // 用户输入: '1000.50'
226 + * // onInput('1000.50')
227 + * // -> emit('update:modelValue', 100050) // 分
228 + */
229 +const onInput = (value) => {
230 + // 移除非数字和小数点
231 + const cleanValue = value.replace(/[^\d.]/g, '')
232 +
233 + // 转换为分(整数)
234 + const yuan = parseFloat(cleanValue)
235 + if (!Number.isNaN(yuan)) {
236 + emit('update:modelValue', Math.round(yuan * 100))
237 + } else {
238 + emit('update:modelValue', 0)
239 + }
240 +}
241 +
242 +/**
243 + * 选择币种(多币种模式)
244 + * @param {string} value - 币种代码
245 + */
246 +const selectCurrency = (value) => {
247 + selectedCurrency.value = value
248 +}
249 +</script>
250 +
251 +<style lang="less" scoped>
252 +/* 组件样式 */
253 +</style>