hookehuyr

fix(checkin): 修复 mockData 语法错误并完善地图活动功能

主要修改:
- 修复 mockData.js 的语法错误(移除孤立的 return 语句)
- 修复 generateApiFromOpenAPI.js 的 brace-style 代码风格问题
- 实现 CheckinMap 页面与 map_activity API 的集成
- 添加完整的 API 规范文档
- 更新 API 代码生成脚本

技术细节:
- 清理 mockData.js 中遗留的孤立代码片段
- 移除未使用的函数参数以消除警告
- 修复 ESLint brace-style 规则错误(9处)
- 完善 map_activity API 接口定义
- 添加 API Mock 数据支持开发环境测试

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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 + post:
13 + summary: 打卡
14 + deprecated: false
15 + description: ''
16 + tags:
17 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: checkin
38 + schema:
39 + type: string
40 + requestBody:
41 + content:
42 + application/x-www-form-urlencoded:
43 + schema:
44 + type: object
45 + properties:
46 + activity_id:
47 + description: 活动ID
48 + example: ''
49 + type: string
50 + detail_id:
51 + description: 打卡点ID
52 + example: '828360'
53 + type: string
54 + openid:
55 + example: oAHBN10P-hn-vF1cTY4tQeStQFmU
56 + type: string
57 + examples: {}
58 + responses:
59 + '200':
60 + description: ''
61 + content:
62 + application/json:
63 + schema:
64 + type: object
65 + properties:
66 + code:
67 + type: integer
68 + msg:
69 + type: string
70 + x-apifox-orders:
71 + - code
72 + - msg
73 + required:
74 + - code
75 + - msg
76 + headers: {}
77 + x-apifox-name: 成功
78 + x-apifox-ordering: 0
79 + security: []
80 + x-apifox-folder: 老来赛/地图-新版多活动
81 + x-apifox-status: integrating
82 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072141-run
83 +components:
84 + schemas: {}
85 + responses: {}
86 + securitySchemes: {}
87 +servers: []
88 +security: []
89 +
90 +```
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 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: detail
38 + schema:
39 + type: string
40 + - name: id
41 + in: query
42 + description: 活动ID
43 + required: false
44 + schema:
45 + type: string
46 + responses:
47 + '200':
48 + description: ''
49 + content:
50 + application/json:
51 + schema:
52 + type: object
53 + properties:
54 + code:
55 + type: integer
56 + msg:
57 + type: string
58 + data:
59 + type: object
60 + properties:
61 + url:
62 + type: string
63 + title: 地图网址
64 + id:
65 + type: string
66 + title: 活动ID
67 + cover:
68 + type: string
69 + title: 封面图
70 + tittle:
71 + type: string
72 + title: 标题
73 + begin_date:
74 + type: string
75 + title: 开始时间
76 + end_date:
77 + type: string
78 + title: 结束时间
79 + is_ended:
80 + type: boolean
81 + title: 活动是否已经结束
82 + is_begin:
83 + type: boolean
84 + title: 活动是否开始
85 + first_checkin_points:
86 + type: integer
87 + title: 首次打卡获得积分
88 + required_checkin_count:
89 + type: integer
90 + title: 需要打卡几次,才能完成活动
91 + complete_points:
92 + type: integer
93 + title: 完成活动获得多少积分
94 + discount_title:
95 + type: string
96 + title: 打卡点底部优惠标题
97 + x-apifox-orders:
98 + - id
99 + - cover
100 + - tittle
101 + - begin_date
102 + - end_date
103 + - is_ended
104 + - is_begin
105 + - url
106 + - first_checkin_points
107 + - required_checkin_count
108 + - complete_points
109 + - discount_title
110 + required:
111 + - url
112 + - end_date
113 + - begin_date
114 + - id
115 + - cover
116 + - tittle
117 + - first_checkin_points
118 + - required_checkin_count
119 + - complete_points
120 + - discount_title
121 + x-apifox-orders:
122 + - code
123 + - msg
124 + - data
125 + required:
126 + - code
127 + - msg
128 + - data
129 + headers: {}
130 + x-apifox-name: 成功
131 + x-apifox-ordering: 0
132 + security: []
133 + x-apifox-folder: 老来赛/地图-新版多活动
134 + x-apifox-status: integrating
135 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417075691-run
136 +components:
137 + schemas: {}
138 + responses: {}
139 + securitySchemes: {}
140 +servers: []
141 +security: []
142 +
143 +```
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 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: is_checked
38 + schema:
39 + type: string
40 + - name: detail_id
41 + in: query
42 + description: 打卡点ID
43 + required: false
44 + example: '828359'
45 + schema:
46 + type: string
47 + - name: openid
48 + in: query
49 + description: ''
50 + required: false
51 + example: oAHBN10P-hn-vF1cTY4tQeStQFmU
52 + schema:
53 + type: string
54 + - name: activity_id
55 + in: query
56 + description: 活动ID
57 + required: false
58 + schema:
59 + type: string
60 + responses:
61 + '200':
62 + description: ''
63 + content:
64 + application/json:
65 + schema:
66 + type: object
67 + properties:
68 + code:
69 + type: integer
70 + msg:
71 + type: string
72 + data:
73 + type: object
74 + properties:
75 + is_checked:
76 + type: boolean
77 + title: 是否已经打卡
78 + x-apifox-orders:
79 + - is_checked
80 + required:
81 + - is_checked
82 + x-apifox-orders:
83 + - code
84 + - msg
85 + - data
86 + required:
87 + - code
88 + - msg
89 + - data
90 + headers: {}
91 + x-apifox-name: 成功
92 + x-apifox-ordering: 0
93 + security: []
94 + x-apifox-folder: 老来赛/地图-新版多活动
95 + x-apifox-status: integrating
96 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072140-run
97 +components:
98 + schemas: {}
99 + responses: {}
100 + securitySchemes: {}
101 +servers: []
102 +security: []
103 +
104 +```
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 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: list
38 + schema:
39 + type: string
40 + responses:
41 + '200':
42 + description: ''
43 + content:
44 + application/json:
45 + schema:
46 + type: object
47 + properties:
48 + code:
49 + type: integer
50 + msg:
51 + type: string
52 + data:
53 + type: array
54 + items:
55 + type: object
56 + properties:
57 + url:
58 + type: string
59 + title: 地图网址
60 + id:
61 + type: string
62 + title: 活动ID
63 + cover:
64 + type: string
65 + title: 封面图
66 + tittle:
67 + type: string
68 + title: 标题
69 + begin_date:
70 + type: string
71 + title: 开始时间
72 + end_date:
73 + type: string
74 + title: 结束时间
75 + x-apifox-orders:
76 + - id
77 + - cover
78 + - tittle
79 + - begin_date
80 + - end_date
81 + - url
82 + required:
83 + - url
84 + - end_date
85 + - begin_date
86 + - id
87 + - cover
88 + - tittle
89 + x-apifox-orders:
90 + - code
91 + - msg
92 + - data
93 + required:
94 + - code
95 + - msg
96 + - data
97 + headers: {}
98 + x-apifox-name: 成功
99 + x-apifox-ordering: 0
100 + security: []
101 + x-apifox-folder: 老来赛/地图-新版多活动
102 + x-apifox-status: integrating
103 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072114-run
104 +components:
105 + schemas: {}
106 + responses: {}
107 + securitySchemes: {}
108 +servers: []
109 +security: []
110 +
111 +```
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 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: poster
38 + schema:
39 + type: string
40 + - name: activity_id
41 + in: query
42 + description: 活动ID
43 + required: false
44 + example:
45 + - ''
46 + schema:
47 + type: string
48 + - name: detail_id
49 + in: query
50 + description: 关卡ID
51 + required: false
52 + example: ''
53 + schema:
54 + type: string
55 + - name: env_version
56 + in: query
57 + description: 小程序版本。正式版为 "release",体验版为 "trial"。默认是正式版
58 + required: false
59 + example: trial
60 + schema:
61 + type: string
62 + responses:
63 + '200':
64 + description: ''
65 + content:
66 + application/json:
67 + schema:
68 + type: object
69 + properties:
70 + code:
71 + type: integer
72 + msg:
73 + type: string
74 + data:
75 + type: object
76 + properties:
77 + details:
78 + type: array
79 + items:
80 + type: object
81 + properties:
82 + id:
83 + type: integer
84 + title: 关卡ID
85 + name:
86 + type: string
87 + title: 关卡名称
88 + background_url:
89 + type: string
90 + title: 关卡背景图
91 + main_slogan:
92 + type: string
93 + sub_slogan:
94 + type: string
95 + is_checked:
96 + type: boolean
97 + title: 是否已经打卡
98 + x-apifox-orders:
99 + - id
100 + - name
101 + - is_checked
102 + - background_url
103 + - main_slogan
104 + - sub_slogan
105 + required:
106 + - id
107 + - name
108 + - background_url
109 + - main_slogan
110 + - sub_slogan
111 + - is_checked
112 + title: 关卡列表
113 + family:
114 + type: object
115 + properties:
116 + id:
117 + type: integer
118 + title: 家庭ID
119 + name:
120 + type: string
121 + title: 家庭名称
122 + avatar_url:
123 + type: string
124 + title: 家庭头像
125 + x-apifox-orders:
126 + - id
127 + - name
128 + - avatar_url
129 + required:
130 + - id
131 + - name
132 + - avatar_url
133 + title: 用户的当前家庭
134 + show_detail_index:
135 + type: integer
136 + title: 当前应该显示第几个关卡
137 + description: 从 0 开始计数
138 + end_date:
139 + type: string
140 + title: 活动截止时间
141 + qrcode_url:
142 + type: string
143 + title: 小程序码
144 + title:
145 + type: string
146 + title: 海报标题
147 + begin_date:
148 + type: string
149 + title: 活动开始日期
150 + x-apifox-orders:
151 + - title
152 + - begin_date
153 + - end_date
154 + - details
155 + - show_detail_index
156 + - family
157 + - qrcode_url
158 + required:
159 + - title
160 + - details
161 + - family
162 + - show_detail_index
163 + - end_date
164 + - qrcode_url
165 + - begin_date
166 + x-apifox-orders:
167 + - code
168 + - msg
169 + - data
170 + required:
171 + - code
172 + - msg
173 + - data
174 + headers: {}
175 + x-apifox-name: 成功
176 + x-apifox-ordering: 0
177 + security: []
178 + x-apifox-folder: 老来赛/地图-新版多活动
179 + x-apifox-status: integrating
180 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072142-run
181 +components:
182 + schemas: {}
183 + responses: {}
184 + securitySchemes: {}
185 +servers: []
186 +security: []
187 +
188 +```
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 + post:
13 + summary: 上传海报背景
14 + deprecated: false
15 + description: ''
16 + tags:
17 + - 老来赛/地图-新版多活动
18 + parameters:
19 + - name: f
20 + in: query
21 + description: ''
22 + required: true
23 + example: walk
24 + schema:
25 + type: string
26 + - name: a
27 + in: query
28 + description: ''
29 + required: true
30 + example: map_activity
31 + schema:
32 + type: string
33 + - name: t
34 + in: query
35 + description: ''
36 + required: true
37 + example: save_poster_background
38 + schema:
39 + type: string
40 + requestBody:
41 + content:
42 + application/x-www-form-urlencoded:
43 + schema:
44 + type: object
45 + properties:
46 + activity_id:
47 + description: 活动ID
48 + example: ''
49 + type: string
50 + detail_id:
51 + description: 打卡点ID
52 + example: '828359'
53 + type: string
54 + poster_background_url:
55 + description: 关卡海报背景
56 + example: >-
57 + https://cdn.ipadbiz.cn/space_34093/t0122259914a77e9a57_FiYxd1DK70vLJ53Po8g2Y6JmqMeQ.jpg
58 + type: string
59 + examples: {}
60 + responses:
61 + '200':
62 + description: ''
63 + content:
64 + application/json:
65 + schema:
66 + type: object
67 + properties:
68 + code:
69 + type: integer
70 + msg:
71 + type: string
72 + x-apifox-orders:
73 + - code
74 + - msg
75 + required:
76 + - code
77 + - msg
78 + headers: {}
79 + x-apifox-name: 成功
80 + x-apifox-ordering: 0
81 + security: []
82 + x-apifox-folder: 老来赛/地图-新版多活动
83 + x-apifox-status: developing
84 + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072143-run
85 +components:
86 + schemas: {}
87 + responses: {}
88 + securitySchemes: {}
89 +servers: []
90 +security: []
91 +
92 +```
...@@ -18,14 +18,14 @@ ...@@ -18,14 +18,14 @@
18 "build:rn": "taro build --type rn", 18 "build:rn": "taro build --type rn",
19 "build:qq": "taro build --type qq", 19 "build:qq": "taro build --type qq",
20 "build:quickapp": "taro build --type quickapp", 20 "build:quickapp": "taro build --type quickapp",
21 - "dev:weapp": "npm run build:weapp -- --watch", 21 + "dev:weapp": "NODE_ENV=development npm run build:weapp -- --watch",
22 - "dev:swan": "npm run build:swan -- --watch", 22 + "dev:swan": "NODE_ENV=development npm run build:swan -- --watch",
23 - "dev:alipay": "npm run build:alipay -- --watch", 23 + "dev:alipay": "NODE_ENV=development npm run build:alipay -- --watch",
24 - "dev:tt": "npm run build:tt -- --watch", 24 + "dev:tt": "NODE_ENV=development npm run build:tt -- --watch",
25 - "dev:h5": "npm run build:h5 -- --watch", 25 + "dev:h5": "NODE_ENV=development npm run build:h5 -- --watch",
26 - "dev:rn": "npm run build:rn -- --watch", 26 + "dev:rn": "NODE_ENV=development npm run build:rn -- --watch",
27 - "dev:qq": "npm run build:qq -- --watch", 27 + "dev:qq": "NODE_ENV=development npm run build:qq -- --watch",
28 - "dev:quickapp": "npm run build:quickapp -- --watch", 28 + "dev:quickapp": "NODE_ENV=development npm run build:quickapp -- --watch",
29 "postinstall": "weapp-tw patch", 29 "postinstall": "weapp-tw patch",
30 "prepare": "husky install", 30 "prepare": "husky install",
31 "test": "vitest", 31 "test": "vitest",
...@@ -33,7 +33,8 @@ ...@@ -33,7 +33,8 @@
33 "test:coverage": "vitest --coverage", 33 "test:coverage": "vitest --coverage",
34 "test:run": "vitest run", 34 "test:run": "vitest run",
35 "lint": "eslint ./src --ext .vue,.js", 35 "lint": "eslint ./src --ext .vue,.js",
36 - "format": "prettier --write \"src/**/*.{js,vue,less}\"" 36 + "format": "prettier --write \"src/**/*.{js,vue,less}\"",
37 + "api:generate": "node scripts/generateApiFromOpenAPI.js"
37 }, 38 },
38 "browserslist": [ 39 "browserslist": [
39 "last 3 versions", 40 "last 3 versions",
......
1 +# API 文档生成指南
2 +
3 +本项目的 API 文档采用手动维护的方式。
4 +
5 +## 📝 工作流程
6 +
7 +### 1. 维护 OpenAPI 文档
8 +
9 +`docs/api-specs/` 目录中维护 OpenAPI 文档。
10 +
11 +#### 目录结构
12 +
13 +```
14 +docs/api-specs/
15 +├── user/ # 用户模块
16 +│ ├── login.md
17 +│ ├── login_status.md
18 +│ └── ...
19 +├── favorite/ # 收藏模块
20 +│ ├── add.md
21 +│ ├── del.md
22 +│ └── list.md
23 +└── ...
24 +```
25 +
26 +#### 文档格式
27 +
28 +每个 `.md` 文件包含:
29 +- 接口描述(Markdown 格式)
30 +- OpenAPI 3.0.1 规范(YAML 格式)
31 +
32 +**示例**`docs/api-specs/user/login.md`):
33 +
34 +\`\`\`markdown
35 +# 登录并绑定 OpenID
36 +
37 +## 接口信息
38 +
39 +- **方法**: POST
40 +- **路径**: /srv/?a=user&t=login
41 +- **标签**: user
42 +
43 +## OpenAPI 规范
44 +
45 +\`\`\`yaml
46 +openapi: 3.0.0
47 +info:
48 + title: 登录并绑定 OpenID
49 + description: 使用手机号和验证码登录,并绑定到 OpenID
50 + version: 1.0.0
51 +paths:
52 + /srv/?:
53 + post:
54 + summary: 登录并绑定 OpenID
55 + description: 使用手机号和验证码登录,并绑定到 OpenID
56 + requestBody:
57 + content:
58 + application/x-www-form-urlencoded:
59 + schema:
60 + type: object
61 + required:
62 + - f
63 + - a
64 + - t
65 + properties:
66 + f:
67 + type: string
68 + description: 业务模块标识
69 + example: manulife
70 + a:
71 + type: string
72 + description: 模块名(user)
73 + example: user
74 + t:
75 + type: string
76 + description: 接口类型(login)
77 + example: login
78 + phone:
79 + type: string
80 + description: 手机号
81 + example: '13800138000'
82 + code:
83 + type: string
84 + description: 验证码
85 + example: '123456'
86 + openid:
87 + type: string
88 + description: 微信 OpenID
89 + example: 'oXXXX-XXXXXXXXXXXXXXXXXXX'
90 + responses:
91 + '200':
92 + description: 成功
93 + content:
94 + application/json:
95 + schema:
96 + type: object
97 + properties:
98 + code:
99 + type: number
100 + description: 状态码(0=失败,1=成功)
101 + msg:
102 + type: string
103 + description: 消息
104 + data:
105 + type: object
106 + description: 用户信息
107 + properties:
108 + id:
109 + type: number
110 + description: 用户 ID
111 + avatar:
112 + type: string
113 + description: 头像 URL
114 + name:
115 + type: string
116 + description: 姓名
117 +\`\`\`
118 +\`\`\`
119 +
120 +### 2. 生成 API 代码
121 +
122 +运行生成脚本:
123 +
124 +```bash
125 +node scripts/generateApiFromOpenAPI.js
126 +```
127 +
128 +#### 生成内容
129 +
130 +脚本会:
131 +1. 扫描 `docs/api-specs/` 目录
132 +2. 解析每个 `.md` 文件中的 OpenAPI 规范
133 +3. 生成对应的 JavaScript API 文件到 `src/api/` 目录
134 +
135 +#### 输出示例
136 +
137 +\`\`\`
138 +=== OpenAPI 转 API 文档生成器 ===
139 +
140 +输入目录: /Users/huyirui/program/itomix/git/manulife-weapp/docs/api-specs
141 +输出目录: /Users/huyirui/program/itomix/git/manulife-weapp/src/api
142 +
143 +💾 备份当前 OpenAPI 文档...
144 +
145 +找到 9 个模块: event, favorite, feedback, file, get_file_list, get_product, news, user, wechat
146 +
147 +处理模块: user
148 +找到 5 个 API 文档
149 + ✓ get_profile: 获取个人信息
150 + ✓ login: 登录并绑定openid
151 + ✓ login_status: 查询登录状态
152 + ✓ logout: 退出登录并解绑openid
153 + ✓ update_profile: 更新个人资料
154 + 📝 生成文件: /Users/huyirui/program/itomix/git/manulife-weapp/src/api/user.js
155 +
156 +✅ API 文档生成完成!
157 +\`\`\`
158 +
159 +### 3. 使用生成的 API
160 +
161 +在组件中导入并使用:
162 +
163 +\`\`\`javascript
164 +import { loginAPI, getUserProfileAPI } from '@/api/user';
165 +
166 +// 登录
167 +const result = await loginAPI({
168 + phone: '13800138000',
169 + code: '123456',
170 + openid: 'oXXXX-XXXXXXXXXXXXXXXXXXX'
171 +});
172 +
173 +if (result.code === 1) {
174 + console.log('登录成功', result.data);
175 +}
176 +\`\`\`
177 +
178 +## 🔧 高级功能
179 +
180 +### API 变更检测
181 +
182 +脚本会自动检测 API 变更:
183 +
184 +-**新增接口** - 检测到新的 API 文档
185 +- ⚠️ **修改接口** - 检测到 API 规范变更
186 +-**删除接口** - 检测到删除的 API 文档
187 +
188 +#### 变更报告示例
189 +
190 +\`\`\`
191 +🔍 开始检测 API 变更...
192 +
193 +📦 新增模块: user
194 + 包含 2 个新增接口:
195 + • POST /srv/?a=user&t=login - 登录并绑定 OpenID
196 + • GET /srv/?a=user&t=get_profile - 获取个人信息
197 +
198 +📦 对比范围: 9 个旧接口 → 11 个新接口
199 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
200 +
201 +✅ 新增接口 (2):
202 + + login - 登录并绑定 OpenID
203 + + get_profile - 获取个人信息
204 +
205 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
206 +总计: 2 新增, 0 修改, 0 删除
207 +✅ 未检测到破坏性变更
208 +\`\`\`
209 +
210 +## 📚 最佳实践
211 +
212 +### 1. 文档命名
213 +
214 +- 使用语义化文件名(如 `login.md`, `get_list.md`
215 +- 避免使用通用名称(如 `api.md`, `endpoint.md`
216 +
217 +### 2. 参数定义
218 +
219 +- **必填参数**:在 `required` 数组中列出
220 +- **可选参数**:不在 `required`
221 +- **描述**:为每个参数添加清晰的 `description`
222 +- **示例**:为每个参数添加 `example`
223 +
224 +### 3. 响应结构
225 +
226 +- 统一使用 `{ code, msg, data }` 格式
227 +- `code`: 状态码(0=失败,1=成功)
228 +- `msg`: 消息说明
229 +- `data`: 数据内容
230 +
231 +### 4. 文档分组
232 +
233 +- 按业务模块分组(user, favorite, product 等)
234 +- 每个模块一个目录
235 +- 相关接口放在同一目录
236 +
237 +## 🛠️ 故障排除
238 +
239 +### 问题:YAML 解析失败
240 +
241 +**错误信息**
242 +\`\`\`
243 +✗ login.md: 解析失败 - YAML 代码块格式错误
244 +\`\`\`
245 +
246 +**解决方案**
247 +- 检查 YAML 代码块是否正确包裹在 `\`\`\`yaml``\`\`\`` 之间
248 +- 检查 YAML 缩进是否正确(使用空格,不要使用 Tab)
249 +- 使用在线 YAML 验证器验证格式
250 +
251 +### 问题:未找到 YAML 代码块
252 +
253 +**错误信息**
254 +\`\`\`
255 +⚠️ login.md: 未找到 YAML 代码块
256 +\`\`\`
257 +
258 +**解决方案**
259 +- 确保文档中包含 `\`\`\`yaml` 代码块
260 +- 检查代码块格式是否正确
261 +
262 +### 问题:生成的 API 代码为空
263 +
264 +**可能原因**
265 +1. OpenAPI 文档格式不正确
266 +2. `paths``requestBody` 定义缺失
267 +
268 +**解决方案**
269 +- 检查 OpenAPI 文档结构是否完整
270 +- 参考本文档中的示例格式
271 +
272 +## 📖 参考资料
273 +
274 +- [OpenAPI 3.0 规范](https://swagger.io/specification/)
275 +- [YAML 语法指南](https://yaml.org/spec/1.2/spec.html)
276 +- [项目 API 文档目录](../docs/api-specs/)
1 +# OpenAPI 转 API 文档生成器 - 快速开始
2 +
3 +## 🎯 一分钟快速上手
4 +
5 +### 1️⃣ 创建 OpenAPI 文档
6 +
7 +`docs/api-specs/` 目录下创建模块和接口文档:
8 +
9 +```bash
10 +# 创建新模块
11 +mkdir -p docs/api-specs/product
12 +
13 +# 创建接口文档
14 +touch docs/api-specs/product/getList.md
15 +```
16 +
17 +### 2️⃣ 编写 OpenAPI 规范
18 +
19 +编辑 `getList.md`
20 +
21 +```markdown
22 +# 获取商品列表
23 +
24 +## OpenAPI Specification
25 +
26 +\```yaml
27 +openapi: 3.0.1
28 +info:
29 + title: ''
30 + version: 1.0.0
31 +paths:
32 + /srv/:
33 + get:
34 + summary: 获取商品列表
35 + tags:
36 + - 商品
37 + parameters:
38 + - name: a
39 + in: query
40 + example: product_list
41 + - name: f
42 + in: query
43 + example: behalo
44 + responses:
45 + '200':
46 + description: 成功
47 +\```
48 +```
49 +
50 +### 3️⃣ 生成 API 文件
51 +
52 +```bash
53 +pnpm api:generate
54 +```
55 +
56 +### 4️⃣ 使用生成的 API
57 +
58 +```javascript
59 +import { getListAPI } from '@/api/product';
60 +
61 +const result = await getListAPI({ page: 1, pageSize: 10 });
62 +```
63 +
64 +## ✅ 验证结果
65 +
66 +运行测试脚本验证生成的文件:
67 +
68 +```bash
69 +node scripts/test-generate.js
70 +```
71 +
72 +## 📂 文件结构
73 +
74 +```
75 +manulife-weapp/
76 +├── docs/
77 +│ ├── api-specs/ # API 规范文档源目录
78 +│ │ └── user/ # 模块目录
79 +│ │ └── getUserInfo.md
80 +│ ├── OPENAPI_TO_API_GUIDE.md # 详细使用指南
81 +│ └── API_USAGE_EXAMPLES.md # API 使用示例
82 +├── scripts/
83 +│ ├── generateApiFromOpenAPI.js # 生成器核心脚本
84 +│ └── test-generate.js # 测试脚本
85 +├── src/
86 +│ └── api/ # 生成的 API 文件目录
87 +│ ├── user.js # 自动生成
88 +│ ├── wx/
89 +│ └── index.js
90 +└── package.json # 包含 api:generate 命令
91 +```
92 +
93 +## 🔄 工作流程
94 +
95 +```mermaid
96 +graph LR
97 + A[编写 OpenAPI 文档] --> B[运行 pnpm api:generate]
98 + B --> C[生成 API 文件]
99 + C --> D[在项目中使用]
100 + D --> E[需要修改接口]
101 + E --> A
102 +```
103 +
104 +## 🎨 常见场景
105 +
106 +### 场景 1: 批量生成多个接口
107 +
108 +```bash
109 +docs/api-specs/
110 +├── user/
111 +│ ├── getUserInfo.md
112 +│ ├── updateProfile.md
113 +│ └── changePassword.md
114 +└── order/
115 + ├── getList.md
116 + └── getDetail.md
117 +```
118 +
119 +运行 `pnpm api:generate` 后生成:
120 +
121 +```
122 +src/api/
123 +├── user.js # 包含 3 个接口
124 +└── order.js # 包含 2 个接口
125 +```
126 +
127 +### 场景 2: 更新已有接口
128 +
129 +1. 修改 `docs/api-specs/user/getUserInfo.md`
130 +2. 运行 `pnpm api:generate`
131 +3. `src/api/user.js` 自动更新
132 +
133 +### 场景 3: 添加新模块
134 +
135 +1. 创建 `docs/api-specs/payment/`
136 +2. 添加接口文档
137 +3. 运行生成命令
138 +4. 自动生成 `src/api/payment.js`
139 +
140 +## ⚙️ 配置和自定义
141 +
142 +### 修改输出目录
143 +
144 +编辑 `scripts/generateApiFromOpenAPI.js`
145 +
146 +```javascript
147 +const outputDir = path.resolve(__dirname, '../src/api');
148 +// 改为你想要的目录
149 +const outputDir = path.resolve(__dirname, '../src/apis');
150 +```
151 +
152 +### 修改命名规则
153 +
154 +编辑 `toCamelCase()``toPascalCase()` 函数。
155 +
156 +### 修改生成模板
157 +
158 +编辑 `generateApiFileContent()` 函数。
159 +
160 +## 🐛 调试技巧
161 +
162 +### 启用详细日志
163 +
164 +在脚本中添加更多 console.log:
165 +
166 +```javascript
167 +console.log('解析的 API 信息:', JSON.stringify(apiInfo, null, 2));
168 +```
169 +
170 +### 单独测试某个模块
171 +
172 +修改脚本中的模块过滤逻辑。
173 +
174 +### 查看生成的中间数据
175 +
176 +添加调试输出查看 YAML 解析结果。
177 +
178 +## 📞 获取帮助
179 +
180 +- 详细指南:[OpenAPI 转 API 文档生成器指南](./OPENAPI_TO_API_GUIDE.md)
181 +- 使用示例:[API 使用示例](./API_USAGE_EXAMPLES.md)
182 +- 项目架构:[CLAUDE.md](../CLAUDE.md)
183 +
184 +## 🎉 开始使用
185 +
186 +现在你已经准备好了!开始创建你的第一个 OpenAPI 文档吧。
187 +
188 +```bash
189 +# 1. 创建模块目录
190 +mkdir -p docs/api-specs/your-module
191 +
192 +# 2. 创建接口文档(参考 docs/api-specs/user/getUserInfo.md)
193 +
194 +# 3. 生成 API
195 +pnpm api:generate
196 +
197 +# 4. 查看生成的文件
198 +cat src/api/your-module.js
199 +
200 +# 5. 开始使用
201 +```
202 +
203 +祝你编码愉快!🚀
1 +/**
2 + * API 对比工具
3 + *
4 + * 功能:
5 + * 1. 对比两个 OpenAPI 文档的差异
6 + * 2. 检测破坏性变更
7 + * 3. 生成详细的变更报告
8 + *
9 + * 使用方式:
10 + * node scripts/apiDiff.js <oldPath> <newPath>
11 + */
12 +
13 +const fs = require('fs')
14 +const path = require('path')
15 +const yaml = require('js-yaml')
16 +
17 +/**
18 + * 从 Markdown 文件中提取 YAML
19 + */
20 +function extractYAMLFromMarkdown(content) {
21 + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/
22 + const match = content.match(yamlRegex)
23 + return match ? match[1] : null
24 +}
25 +
26 +/**
27 + * 解析 OpenAPI 文档(支持 .md 和目录)
28 + */
29 +function parseOpenAPIPath(filePath) {
30 + const stat = fs.statSync(filePath)
31 +
32 + if (stat.isFile()) {
33 + // 单个文件
34 + if (filePath.endsWith('.md')) {
35 + const content = fs.readFileSync(filePath, 'utf8')
36 + const yamlContent = extractYAMLFromMarkdown(content)
37 + if (!yamlContent) {
38 + throw new Error(`文件 ${filePath} 中未找到 YAML 代码块`)
39 + }
40 + return [yaml.load(yamlContent)]
41 + } else if (filePath.endsWith('.js')) {
42 + // TODO: 支持对比生成的 JS 文件(需要解析 AST)
43 + throw new Error('暂不支持对比生成的 JS 文件,请对比 OpenAPI 文档')
44 + } else {
45 + throw new Error(`不支持的文件类型: ${filePath}`)
46 + }
47 + } else if (stat.isDirectory()) {
48 + // 目录,读取所有 .md 文件
49 + const files = fs.readdirSync(filePath).filter(f => f.endsWith('.md'))
50 + const docs = []
51 + files.forEach(file => {
52 + const fullPath = path.join(filePath, file)
53 + const content = fs.readFileSync(fullPath, 'utf8')
54 + const yamlContent = extractYAMLFromMarkdown(content)
55 + if (yamlContent) {
56 + const doc = yaml.load(yamlContent)
57 + // 保存文件名用于标识
58 + doc._fileName = path.basename(file, '.md')
59 + docs.push(doc)
60 + }
61 + })
62 + return docs
63 + }
64 +}
65 +
66 +/**
67 + * 从 OpenAPI 文档提取 API 信息
68 + */
69 +function extractAPIInfo(openapiDoc) {
70 + const path = Object.keys(openapiDoc.paths)[0]
71 + const method = Object.keys(openapiDoc.paths[path])[0]
72 + const apiInfo = openapiDoc.paths[path][method]
73 +
74 + // 提取参数
75 + const queryParams = (apiInfo.parameters || [])
76 + .filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f')
77 + .map(p => ({
78 + name: p.name,
79 + type: p.schema?.type || 'any',
80 + required: p.required || false,
81 + description: p.description || '',
82 + }))
83 +
84 + // 提取 body 参数
85 + const bodyParams = []
86 + if (apiInfo.requestBody && apiInfo.requestBody.content) {
87 + const content =
88 + apiInfo.requestBody.content['application/x-www-form-urlencoded'] ||
89 + apiInfo.requestBody.content['application/json']
90 + if (content && content.schema && content.schema.properties) {
91 + Object.entries(content.schema.properties).forEach(([key, value]) => {
92 + if (key !== 'a' && key !== 'f') {
93 + bodyParams.push({
94 + name: key,
95 + type: value.type || 'any',
96 + required: content.schema.required?.includes(key) || false,
97 + description: value.description || '',
98 + })
99 + }
100 + })
101 + }
102 + }
103 +
104 + // 提取响应结构
105 + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema
106 +
107 + return {
108 + name: openapiDoc._fileName || 'unknown',
109 + path,
110 + method: method.toUpperCase(),
111 + queryParams: new Set(queryParams.map(p => p.name)),
112 + bodyParams: new Set(bodyParams.map(p => p.name)),
113 + requiredQueryParams: new Set(queryParams.filter(p => p.required).map(p => p.name)),
114 + requiredBodyParams: new Set(bodyParams.filter(p => p.required).map(p => p.name)),
115 + allQueryParams: queryParams,
116 + allBodyParams: bodyParams,
117 + responseSchema,
118 + summary: apiInfo.summary || '',
119 + }
120 +}
121 +
122 +/**
123 + * 对比两个 API 信息
124 + */
125 +function compareAPI(oldAPI, newAPI) {
126 + const changes = {
127 + breaking: [],
128 + nonBreaking: [],
129 + }
130 +
131 + // 检查 HTTP 方法变更
132 + if (oldAPI.method !== newAPI.method) {
133 + changes.breaking.push(`HTTP 方法变更: ${oldAPI.method}${newAPI.method}`)
134 + }
135 +
136 + // 检查 GET 参数变更
137 + oldAPI.allQueryParams.forEach(oldParam => {
138 + const newParam = newAPI.allQueryParams.find(p => p.name === oldParam.name)
139 +
140 + if (!newParam) {
141 + // 参数被删除
142 + if (oldAPI.requiredQueryParams.has(oldParam.name)) {
143 + changes.breaking.push(`删除必填 query 参数: ${oldParam.name}`)
144 + } else {
145 + changes.nonBreaking.push(`删除可选 query 参数: ${oldParam.name}`)
146 + }
147 + } else {
148 + // 参数类型变更
149 + if (oldParam.type !== newParam.type) {
150 + changes.breaking.push(
151 + `query 参数类型变更: ${oldParam.name} (${oldParam.type}${newParam.type})`
152 + )
153 + }
154 + // 可选 → 必填
155 + if (!oldParam.required && newParam.required) {
156 + changes.breaking.push(`query 参数变为必填: ${newParam.name}`)
157 + }
158 + // 必填 → 可选
159 + if (oldParam.required && !newParam.required) {
160 + changes.nonBreaking.push(`query 参数变为可选: ${newParam.name}`)
161 + }
162 + }
163 + })
164 +
165 + // 检查新增 GET 参数
166 + newAPI.allQueryParams.forEach(newParam => {
167 + const oldParam = oldAPI.allQueryParams.find(p => p.name === newParam.name)
168 + if (!oldParam) {
169 + if (newParam.required) {
170 + changes.breaking.push(`新增必填 query 参数: ${newParam.name}`)
171 + } else {
172 + changes.nonBreaking.push(`新增可选 query 参数: ${newParam.name}`)
173 + }
174 + }
175 + })
176 +
177 + // 检查 POST body 参数变更
178 + oldAPI.allBodyParams.forEach(oldParam => {
179 + const newParam = newAPI.allBodyParams.find(p => p.name === oldParam.name)
180 +
181 + if (!newParam) {
182 + // 参数被删除
183 + if (oldAPI.requiredBodyParams.has(oldParam.name)) {
184 + changes.breaking.push(`删除必填 body 参数: ${oldParam.name}`)
185 + } else {
186 + changes.nonBreaking.push(`删除可选 body 参数: ${oldParam.name}`)
187 + }
188 + } else {
189 + // 参数类型变更
190 + if (oldParam.type !== newParam.type) {
191 + changes.breaking.push(
192 + `body 参数类型变更: ${oldParam.name} (${oldParam.type}${newParam.type})`
193 + )
194 + }
195 + // 可选 → 必填
196 + if (!oldParam.required && newParam.required) {
197 + changes.breaking.push(`body 参数变为必填: ${newParam.name}`)
198 + }
199 + // 必填 → 可选
200 + if (oldParam.required && !newParam.required) {
201 + changes.nonBreaking.push(`body 参数变为可选: ${newParam.name}`)
202 + }
203 + }
204 + })
205 +
206 + // 检查新增 body 参数
207 + newAPI.allBodyParams.forEach(newParam => {
208 + const oldParam = oldAPI.allBodyParams.find(p => p.name === newParam.name)
209 + if (!oldParam) {
210 + if (newParam.required) {
211 + changes.breaking.push(`新增必填 body 参数: ${newParam.name}`)
212 + } else {
213 + changes.nonBreaking.push(`新增可选 body 参数: ${newParam.name}`)
214 + }
215 + }
216 + })
217 +
218 + return changes
219 +}
220 +
221 +/**
222 + * 生成变更报告
223 + */
224 +function generateReport(oldDocs, newDocs, format = 'text') {
225 + const oldAPIs = oldDocs.map(extractAPIInfo)
226 + const newAPIs = newDocs.map(extractAPIInfo)
227 +
228 + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]))
229 + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]))
230 +
231 + const addedAPIs = []
232 + const removedAPIs = []
233 + const modifiedAPIs = []
234 +
235 + // 检测新增接口
236 + newAPIs.forEach(api => {
237 + if (!oldAPIsMap.has(api.name)) {
238 + addedAPIs.push(api)
239 + }
240 + })
241 +
242 + // 检测删除接口
243 + oldAPIs.forEach(api => {
244 + if (!newAPIsMap.has(api.name)) {
245 + removedAPIs.push(api)
246 + }
247 + })
248 +
249 + // 检测修改接口
250 + newAPIs.forEach(api => {
251 + const oldAPI = oldAPIsMap.get(api.name)
252 + if (oldAPI) {
253 + const changes = compareAPI(oldAPI, api)
254 + if (changes.breaking.length > 0 || changes.nonBreaking.length > 0) {
255 + modifiedAPIs.push({
256 + name: api.name,
257 + summary: api.summary,
258 + changes,
259 + })
260 + }
261 + }
262 + })
263 +
264 + // 统计
265 + const totalBreaking = modifiedAPIs.reduce((sum, api) => sum + api.changes.breaking.length, 0)
266 +
267 + // 生成文本报告
268 + if (format === 'text') {
269 + const lines = []
270 + lines.push('=== API 变更检测报告 ===\n')
271 + lines.push(`📦 对比范围: ${oldAPIs.length} 个旧接口 → ${newAPIs.length} 个新接口`)
272 + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
273 +
274 + if (addedAPIs.length > 0) {
275 + lines.push(`✅ 新增接口 (${addedAPIs.length}):`)
276 + addedAPIs.forEach(api => {
277 + lines.push(` + ${api.name} - ${api.summary}`)
278 + })
279 + lines.push('')
280 + }
281 +
282 + if (modifiedAPIs.length > 0) {
283 + lines.push(`⚠️ 修改接口 (${modifiedAPIs.length}):`)
284 + modifiedAPIs.forEach(api => {
285 + lines.push(` ↪ ${api.name} - ${api.summary}`)
286 + api.changes.breaking.forEach(change => {
287 + lines.push(` ✗ [破坏性] ${change}`)
288 + })
289 + api.changes.nonBreaking.forEach(change => {
290 + lines.push(` ✓ [非破坏性] ${change}`)
291 + })
292 + })
293 + lines.push('')
294 + }
295 +
296 + if (removedAPIs.length > 0) {
297 + lines.push(`❌ 删除接口 (${removedAPIs.length}):`)
298 + removedAPIs.forEach(api => {
299 + lines.push(` - ${api.name} - ${api.summary}`)
300 + })
301 + lines.push('')
302 + }
303 +
304 + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
305 + lines.push(
306 + `总计: ${addedAPIs.length} 新增, ${modifiedAPIs.length} 修改, ${removedAPIs.length} 删除`
307 + )
308 +
309 + if (totalBreaking > 0) {
310 + lines.push(`⚠️ 检测到 ${totalBreaking} 个破坏性变更,请仔细检查业务逻辑!`)
311 + } else if (addedAPIs.length > 0 || modifiedAPIs.length > 0 || removedAPIs.length > 0) {
312 + lines.push('✅ 未检测到破坏性变更')
313 + } else {
314 + lines.push('✅ 无接口变更')
315 + }
316 +
317 + return lines.join('\n')
318 + }
319 +
320 + // 生成 JSON 报告
321 + if (format === 'json') {
322 + return JSON.stringify(
323 + {
324 + summary: {
325 + added: addedAPIs.length,
326 + modified: modifiedAPIs.length,
327 + removed: removedAPIs.length,
328 + breakingChanges: totalBreaking,
329 + },
330 + added: addedAPIs.map(api => ({
331 + name: api.name,
332 + summary: api.summary,
333 + method: api.method,
334 + path: api.path,
335 + })),
336 + modified: modifiedAPIs.map(api => ({
337 + name: api.name,
338 + summary: api.summary,
339 + breakingChanges: api.changes.breaking,
340 + nonBreakingChanges: api.changes.nonBreaking,
341 + })),
342 + removed: removedAPIs.map(api => ({
343 + name: api.name,
344 + summary: api.summary,
345 + method: api.method,
346 + path: api.path,
347 + })),
348 + },
349 + null,
350 + 2
351 + )
352 + }
353 +}
354 +
355 +/**
356 + * 主函数
357 + */
358 +function main() {
359 + const args = process.argv.slice(2)
360 +
361 + if (args.length < 2) {
362 + console.error('用法: node scripts/apiDiff.js <oldPath> <newPath>')
363 + console.error('示例:')
364 + console.error(' node scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/')
365 + console.error(
366 + ' node scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md'
367 + )
368 + process.exit(1)
369 + }
370 +
371 + const [oldPath, newPath] = args
372 +
373 + if (!fs.existsSync(oldPath)) {
374 + console.error(`❌ 旧路径不存在: ${oldPath}`)
375 + process.exit(1)
376 + }
377 +
378 + if (!fs.existsSync(newPath)) {
379 + console.error(`❌ 新路径不存在: ${newPath}`)
380 + process.exit(1)
381 + }
382 +
383 + try {
384 + const oldDocs = parseOpenAPIPath(oldPath)
385 + const newDocs = parseOpenAPIPath(newPath)
386 +
387 + const format = process.env.API_DIFF_FORMAT || 'text'
388 + const report = generateReport(oldDocs, newDocs, format)
389 +
390 + console.log(report)
391 +
392 + // 如果有破坏性变更,返回退出码 1
393 + const oldAPIs = oldDocs.map(extractAPIInfo)
394 + const newAPIs = newDocs.map(extractAPIInfo)
395 + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]))
396 + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]))
397 +
398 + let totalBreaking = 0
399 + newAPIs.forEach(api => {
400 + const oldAPI = oldAPIsMap.get(api.name)
401 + if (oldAPI) {
402 + const changes = compareAPI(oldAPI, api)
403 + totalBreaking += changes.breaking.length
404 + }
405 + })
406 +
407 + // 严格模式:任何变更都返回 1
408 + const strictMode = process.env.API_DIFF_STRICT === 'true'
409 + const hasChanges =
410 + oldAPIs.length !== newAPIs.length ||
411 + newAPIs.some(api => !oldAPIsMap.has(api.name)) ||
412 + oldAPIs.some(api => !newAPIsMap.has(api.name))
413 +
414 + if (totalBreaking > 0 || (strictMode && hasChanges)) {
415 + process.exit(1)
416 + } else {
417 + process.exit(0)
418 + }
419 + } catch (error) {
420 + console.error(`❌ 对比失败: ${error.message}`)
421 + process.exit(1)
422 + }
423 +}
424 +
425 +// 如果直接运行此脚本
426 +if (require.main === module) {
427 + main()
428 +}
429 +
430 +module.exports = {
431 + compareAPI,
432 + generateReport,
433 + parseOpenAPIPath,
434 + extractAPIInfo,
435 +}
1 +#!/bin/bash
2 +
3 +###############################################################################
4 +# CHANGELOG 漏记检查脚本
5 +#
6 +# 功能:
7 +# 1. 扫描最近 N 天的 git 提交记录
8 +# 2. 对比 CHANGELOG.md 中的记录
9 +# 3. 生成漏记报告
10 +#
11 +# 使用:
12 +# ./scripts/check-changelog.sh [days]
13 +#
14 +# 示例:
15 +# ./scripts/check-changelog.sh 7 # 检查最近 7 天
16 +# ./scripts/check-changelog.sh 30 # 检查最近 30 天
17 +# ./scripts/check-changelog.sh # 检查所有提交
18 +#
19 +###############################################################################
20 +
21 +set -e
22 +
23 +# 颜色定义
24 +RED='\033[0;31m'
25 +GREEN='\033[0;32m'
26 +YELLOW='\033[1;33m'
27 +BLUE='\033[0;34m'
28 +NC='\033[0m' # No Color
29 +
30 +# 默认参数
31 +DAYS=${1:-7} # 默认检查最近 7 天
32 +CHANGELOG_FILE="docs/CHANGELOG.md"
33 +
34 +echo -e "${BLUE}======================================${NC}"
35 +echo -e "${BLUE} CHANGELOG 漏记检查工具${NC}"
36 +echo -e "${BLUE}======================================${NC}"
37 +echo ""
38 +echo -e "检查范围: 最近 ${DAYS} 天"
39 +echo ""
40 +
41 +# 检查 CHANGELOG 文件是否存在
42 +if [ ! -f "$CHANGELOG_FILE" ]; then
43 + echo -e "${RED}错误: CHANGELOG 文件不存在: $CHANGELOG_FILE${NC}"
44 + exit 1
45 +fi
46 +
47 +# 1. 获取 git 提交记录
48 +echo -e "${BLUE}[1/4] 正在获取 git 提交记录...${NC}"
49 +
50 +if [ "$DAYS" = "0" ]; then
51 + # 检查所有提交
52 + GIT_LOG=$(git log --all --pretty=format:"%h|%ad|%s" --date=short)
53 +else
54 + # 检查最近 N 天的提交
55 + GIT_LOG=$(git log --since="$DAYS days ago" --pretty=format:"%h|%ad|%s" --date=short)
56 +fi
57 +
58 +TOTAL_COMMITS=$(echo "$GIT_LOG" | wc -l | tr -d ' ')
59 +echo -e " 找到 ${GREEN}$TOTAL_COMMITS${NC} 个提交"
60 +
61 +# 2. 解析 CHANGELOG 中的记录
62 +echo -e "${BLUE}[2/4] 正在解析 CHANGELOG 记录...${NC}"
63 +
64 +# 提取 CHANGELOG 中的日期和描述
65 +CHANGELOG_ENTRIES=$(grep "^## \[" "$CHANGELOG_FILE" | sed 's/^## \[//' | sed 's/\].*//' | sort -u)
66 +TOTAL_CHANGELOG=$(echo "$CHANGELOG_ENTRIES" | wc -l | tr -d ' ')
67 +echo -e " 找到 ${GREEN}$TOTAL_CHANGELOG${NC} 条记录"
68 +
69 +# 3. 对比分析
70 +echo -e "${BLUE}[3/4] 正在对比分析...${NC}"
71 +
72 +# 统计每个日期的提交数量
73 +COMMITS_BY_DATE=$(echo "$GIT_LOG" | awk -F'|' '{print $2}' | sort | uniq -c | sort -rn)
74 +
75 +echo ""
76 +echo -e "${YELLOW}📊 每日提交统计:${NC}"
77 +echo "$COMMITS_BY_DATE" | head -20
78 +
79 +# 检查哪些日期有提交但 CHANGELOG 没有记录
80 +echo ""
81 +echo -e "${YELLOW}🔍 可能漏记的日期:${NC}"
82 +
83 +MISSING_DATES=0
84 +while IFS='|' read -r count date; do
85 + date=$(echo "$date" | awk '{print $2}')
86 + # 检查 CHANGELOG 中是否有这个日期的记录
87 + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date"; then
88 + echo -e " ${RED}${NC} $date - ${RED}$count 个提交未记录${NC}"
89 + MISSING_DATES=$((MISSING_DATES + 1))
90 + fi
91 +done <<< "$COMMITS_BY_DATE"
92 +
93 +if [ $MISSING_DATES -eq 0 ]; then
94 + echo -e " ${GREEN}${NC} 所有提交都已记录"
95 +fi
96 +
97 +# 4. 生成详细报告
98 +echo ""
99 +echo -e "${BLUE}[4/4] 生成详细报告...${NC}"
100 +
101 +# 临时文件
102 +TEMP_REPORT=$(mktemp)
103 +
104 +# 输出报告头
105 +cat > "$TEMP_REPORT" << 'EOF'
106 +# CHANGELOG 漏记详细报告
107 +
108 +## 检查日期
109 +- 检查范围: 最近 {DAYS} 天
110 +- 生成时间: {TIMESTAMP}
111 +
112 +## 统计摘要
113 +- Git 提交总数: {TOTAL_COMMITS}
114 +- CHANGELOG 记录数: {TOTAL_CHANGELOG}
115 +- 可能漏记的提交: {MISSING_COMMITS}
116 +
117 +## 漏记详情
118 +
119 +EOF
120 +
121 +# 替换模板变量
122 +sed -i.bak "s/{DAYS}/$DAYS/g" "$TEMP_REPORT"
123 +sed -i.bak "s/{TIMESTAMP}/$(date '+%Y-%m-%d %H:%M:%S')/g" "$TEMP_REPORT"
124 +sed -i.bak "s/{TOTAL_COMMITS}/$TOTAL_COMMITS/g" "$TEMP_REPORT"
125 +sed -i.bak "s/{TOTAL_CHANGELOG}/$TOTAL_CHANGELOG/g" "$TEMP_REPORT"
126 +
127 +# 计算可能漏记的提交数
128 +MISSING_COMMITS=0
129 +while IFS='|' read -r count date; do
130 + date=$(echo "$date" | awk '{print $2}')
131 + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date"; then
132 + MISSING_COMMITS=$((MISSING_COMMITS + count))
133 + fi
134 +done <<< "$COMMITS_BY_DATE"
135 +
136 +sed -i.bak "s/{MISSING_COMMITS}/$MISSING_COMMITS/g" "$TEMP_REPORT"
137 +
138 +# 如果有漏记,列出详细提交
139 +if [ $MISSING_COMMITS -gt 0 ]; then
140 + echo "" >> "$TEMP_REPORT"
141 + echo "### 未记录的提交详情" >> "$TEMP_REPORT"
142 + echo "" >> "$TEMP_REPORT"
143 +
144 + while IFS='|' read -r count date; do
145 + date_only=$(echo "$date" | awk '{print $2}')
146 + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date_only"; then
147 + echo "**$date_only** ($count 个提交):" >> "$TEMP_REPORT"
148 + echo "$GIT_LOG" | grep "$date_only" | awk -F'|' '{print "- " $3}' >> "$TEMP_REPORT"
149 + echo "" >> "$TEMP_REPORT"
150 + fi
151 + done <<< "$COMMITS_BY_DATE"
152 +else
153 + echo "" >> "$TEMP_REPORT"
154 + echo "### ✅ 完整性检查" >> "$TEMP_REPORT"
155 + echo "" >> "$TEMP_REPORT"
156 + echo "所有提交都已在 CHANGELOG 中记录!" >> "$TEMP_REPORT"
157 +fi
158 +
159 +# 删除备份文件
160 +rm -f "$TEMP_REPORT.bak"
161 +
162 +# 输出报告
163 +echo ""
164 +echo -e "${BLUE}======================================${NC}"
165 +echo -e "${BLUE} 检查完成${NC}"
166 +echo -e "${BLUE}======================================${NC}"
167 +echo ""
168 +cat "$TEMP_REPORT"
169 +
170 +# 保存报告
171 +REPORT_FILE="docs/changelog-check-report-$(date +%Y%m%d).md"
172 +mv "$TEMP_REPORT" "$REPORT_FILE"
173 +
174 +echo ""
175 +echo -e "${GREEN}✓ 详细报告已保存到: $REPORT_FILE${NC}"
176 +
177 +# 5. 给出建议
178 +echo ""
179 +echo -e "${YELLOW}💡 建议:${NC}"
180 +if [ $MISSING_COMMITS -gt 0 ]; then
181 + echo -e " ${YELLOW}1.${NC} 查看 $REPORT_FILE 了解漏记详情"
182 + echo -e " ${YELLOW}2.${NC} 更新 CHANGELOG.md 补充漏记的记录"
183 + echo -e " ${YELLOW}3.${NC} 使用标准格式添加记录(参考文档顶部模板)"
184 +else
185 + echo -e " ${GREEN}${NC} CHANGELOG 记录完整,继续保持!"
186 +fi
187 +
188 +echo ""
189 +echo -e "${BLUE}======================================${NC}"
190 +echo ""
191 +
192 +# 返回退出码
193 +if [ $MISSING_COMMITS -gt 0 ]; then
194 + exit 1 # 有漏记,返回非零退出码
195 +else
196 + exit 0 # 无漏记,返回零
197 +fi
1 +/**
2 + * 从 OpenAPI 文档自动生成 API 接口文件
3 + *
4 + * 功能:
5 + * 1. 扫描 docs/api-specs 目录
6 + * 2. 解析每个 .md 文件中的 OpenAPI YAML 规范
7 + * 3. 提取 API 信息并生成对应的 JavaScript API 文件
8 + * 4. 保存到 src/api/ 目录
9 + *
10 + * 目录结构:
11 + * docs/api-specs/
12 + * ├── module1/
13 + * │ ├── api1.md
14 + * │ └── api2.md
15 + * └── module2/
16 + * └── api3.md
17 + *
18 + * 生成到:
19 + * src/api/
20 + * ├── module1.js
21 + * └── module2.js
22 + */
23 +
24 +const fs = require('fs')
25 +const path = require('path')
26 +const yaml = require('js-yaml')
27 +const { generateReport, parseOpenAPIPath } = require('./apiDiff')
28 +
29 +/**
30 + * 提取 Markdown 文件中的 YAML 代码块
31 + * @param {string} content - Markdown 文件内容
32 + * @returns {string|null} - YAML 字符串或 null
33 + */
34 +function extractYAMLFromMarkdown(content) {
35 + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/
36 + const match = content.match(yamlRegex)
37 + return match ? match[1] : null
38 +}
39 +
40 +/**
41 + * 将字符串转换为驼峰命名
42 + * @param {string} str - 输入字符串
43 + * @returns {string} - 驼峰命名字符串
44 + */
45 +function toCamelCase(str) {
46 + return str
47 + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
48 + .replace(/^(.)/, c => c.toLowerCase())
49 +}
50 +
51 +/**
52 + * 将字符串转换为帕斯卡命名(首字母大写)
53 + * @param {string} str - 输入字符串
54 + * @returns {string} - 帕斯卡命名字符串
55 + */
56 +function toPascalCase(str) {
57 + const camelCase = toCamelCase(str)
58 + return camelCase.charAt(0).toUpperCase() + camelCase.slice(1)
59 +}
60 +
61 +/**
62 + * 简单的英文到中文翻译
63 + * @param {string} text - 英文文本
64 + * @returns {string} - 中文文本
65 + */
66 +function translateToChinese(text) {
67 + if (!text) {
68 + return ''
69 + }
70 +
71 + // 常见技术术语翻译字典(优先级从高到低)
72 + const dictionary = {
73 + // 短语和完整句子
74 + 'mini program authorization': '小程序授权',
75 + 'login flow design': '登录流程设计',
76 +
77 + // 短语
78 + 'mini program': '小程序',
79 + 'mini-program': '小程序',
80 + wechat: '微信',
81 + weixin: '微信',
82 + openid: 'OpenID',
83 +
84 + // 动词和常用词
85 + authorize: '授权',
86 + authorization: '授权',
87 + login: '登录',
88 + logout: '登出',
89 + register: '注册',
90 + call: '调用',
91 + return: '返回',
92 + using: '使用',
93 + 'bound to': '绑定到',
94 + 'according to': '根据',
95 +
96 + // 名词
97 + user: '用户',
98 + code: '授权码',
99 + token: '令牌',
100 + session: '会话',
101 + request: '请求',
102 + response: '响应',
103 + interface: '接口',
104 + api: '接口',
105 + account: '账号',
106 + openid: 'OpenID',
107 +
108 + // 描述性词汇
109 + first: '首先',
110 + then: '然后',
111 + if: '如果',
112 + else: '否则',
113 + need: '需要',
114 + should: '应该',
115 + must: '必须',
116 +
117 + // 状态
118 + success: '成功',
119 + fail: '失败',
120 + error: '错误',
121 + empty: '空',
122 + 'non-empty': '非空',
123 +
124 + // 属性
125 + avatar: '头像',
126 + name: '姓名',
127 + id: 'ID',
128 + info: '信息',
129 + data: '数据',
130 + flow: '流程',
131 + design: '设计',
132 +
133 + // 其他
134 + internal: '内部',
135 + automatically: '自动',
136 + specify: '指定',
137 + testing: '测试',
138 + 'used for': '用于',
139 + 'bound with': '绑定',
140 + }
141 +
142 + let translated = text
143 +
144 + // 按照字典进行替换(优先匹配长词)
145 + const sortedKeys = Object.keys(dictionary).sort((a, b) => b.length - a.length)
146 + sortedKeys.forEach(key => {
147 + const regex = new RegExp(key, 'gi')
148 + translated = translated.replace(regex, dictionary[key])
149 + })
150 +
151 + return translated
152 +}
153 +
154 +/**
155 + * 格式化描述文本(翻译+格式化)
156 + * @param {string} description - 原始描述
157 + * @returns {string} - 格式化后的描述
158 + */
159 +function formatDescription(description) {
160 + if (!description) {
161 + return ''
162 + }
163 +
164 + // 移除 markdown 格式符号(如 # 标题)
165 + let formatted = description
166 + .replace(/^#+\s*/gm, '') // 移除标题符号
167 + .replace(/\*\*(.*?)\*\*/g, '$1') // 移除加粗
168 + .replace(/\*(.*?)\*/g, '$1') // 移除斜体
169 + .replace(/`([^`]+)`/g, '$1') // 移除行内代码
170 + .trim()
171 +
172 + // 先进行整句翻译(常见句式)
173 + formatted = translateSentences(formatted)
174 +
175 + return formatted
176 +}
177 +
178 +/**
179 + * 翻译常见句式
180 + * @param {string} text - 文本
181 + * @returns {string} - 翻译后的文本
182 + */
183 +function translateSentences(text) {
184 + if (!text) {
185 + return ''
186 + }
187 +
188 + // 常见句式翻译(按优先级排序,长的先匹配)
189 + const sentences = {
190 + // 完整句子
191 + '# 登录流程设计': '# 登录流程设计',
192 + '# Login Flow Design': '# 登录流程设计',
193 +
194 + // 常见句式
195 + 'Authorize mini program first': '先进行小程序授权',
196 + 'If user is empty, call login API': '如果返回 user 为空,则需要调用登录接口',
197 + 'If user is not empty, no need to call login API': '如果返回 user 非空,则不需要调用登录接口',
198 + 'the authorization API will automatically login using the account bound to openid':
199 + '授权接口内部按照 openid 绑定的账号,自动登录',
200 + 'Specify an openid for testing': '指定一个 openid 用来测试',
201 + 'User information bound to openid': 'openid 绑定的用户信息',
202 + '0=fail, 1=success': '0=失败,1=成功',
203 + }
204 +
205 + let translated = text
206 +
207 + // 按长度排序(长句优先)
208 + const sortedKeys = Object.keys(sentences).sort((a, b) => b.length - a.length)
209 + sortedKeys.forEach(key => {
210 + const regex = new RegExp(escapeRegExp(key), 'gi')
211 + translated = translated.replace(regex, sentences[key])
212 + })
213 +
214 + // 最后进行单词级别的补充翻译
215 + translated = translateWords(translated)
216 +
217 + return translated
218 +}
219 +
220 +/**
221 + * 转义正则表达式特殊字符
222 + * @param {string} string - 字符串
223 + * @returns {string} - 转义后的字符串
224 + */
225 +function escapeRegExp(string) {
226 + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
227 +}
228 +
229 +/**
230 + * 单词级别的翻译(补充)
231 + * @param {string} text - 文本
232 + * @returns {string} - 翻译后的文本
233 + */
234 +function translateWords(text) {
235 + const dictionary = {
236 + 'mini program': '小程序',
237 + wechat: '微信',
238 + openid: 'OpenID',
239 + user: '用户',
240 + authorization: '授权',
241 + login: '登录',
242 + avatar: '头像',
243 + name: '姓名',
244 + }
245 +
246 + let translated = text
247 + Object.entries(dictionary).forEach(([key, value]) => {
248 + const regex = new RegExp(key, 'gi')
249 + translated = translated.replace(regex, value)
250 + })
251 +
252 + return translated
253 +}
254 +
255 +/**
256 + * 解析对象属性,生成字段描述
257 + * @param {object} properties - 属性对象
258 + * @param {number} indent - 缩进级别
259 + * @returns {string} - 字段描述字符串
260 + */
261 +function parseProperties(properties, indent = 0) {
262 + if (!properties) {
263 + return ''
264 + }
265 +
266 + const lines = []
267 + const prefix = ' '.repeat(indent)
268 +
269 + Object.entries(properties).forEach(([key, value]) => {
270 + const type = value.type || 'any'
271 + const desc = value.description || value.title || ''
272 + const required = value.required ? '' : ' (可选)'
273 +
274 + // 基本类型
275 + if (type !== 'object' && type !== 'array') {
276 + lines.push(`${prefix}${key}: ${type}${required} - ${desc}`)
277 + // 对象类型
278 + } else if (type === 'object' && value.properties) {
279 + lines.push(`${prefix}${key}: {`)
280 + lines.push(`${prefix} // ${desc}`)
281 + lines.push(parseProperties(value.properties, indent + 2))
282 + lines.push(prefix + '}')
283 + // 数组类型
284 + } else if (type === 'array' && value.items) {
285 + const itemType = value.items.type || 'any'
286 + if (itemType === 'object' && value.items.properties) {
287 + lines.push(`${prefix}${key}: Array<{`)
288 + lines.push(`${prefix} // ${desc}`)
289 + lines.push(parseProperties(value.items.properties, indent + 2))
290 + lines.push(prefix + '}>')
291 + } else {
292 + lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`)
293 + }
294 + }
295 + })
296 +
297 + return lines.join('\n')
298 +}
299 +
300 +/**
301 + * 从 requestBody 中提取参数
302 + * @param {object} requestBody - requestBody 对象
303 + * @returns {Array} - 参数数组
304 + */
305 +function extractRequestParams(requestBody) {
306 + if (!requestBody || !requestBody.content) {
307 + return []
308 + }
309 +
310 + // 获取内容类型(可能是 application/x-www-form-urlencoded 或 application/json)
311 + const content =
312 + requestBody.content['application/x-www-form-urlencoded'] ||
313 + requestBody.content['application/json']
314 +
315 + if (!content || !content.schema || !content.schema.properties) {
316 + return []
317 + }
318 +
319 + const params = []
320 + Object.entries(content.schema.properties).forEach(([key, value]) => {
321 + params.push({
322 + name: key,
323 + type: value.type || 'any',
324 + description: value.description || '',
325 + example: value.example || '',
326 + required: content.schema.required?.includes(key) || false,
327 + })
328 + })
329 +
330 + return params
331 +}
332 +
333 +/**
334 + * 生成 JSDoc 参数注释
335 + * @param {Array} parameters - parameters 数组(GET 请求)
336 + * @param {Array} bodyParams - requestBody 参数数组(POST 请求)
337 + * @param {string} method - HTTP 方法
338 + * @returns {string} - JSDoc 参数注释
339 + */
340 +function generateParamJSDoc(parameters, bodyParams, method) {
341 + const lines = [' * @param {Object} params 请求参数']
342 +
343 + // POST 请求使用 body 参数
344 + if (method === 'POST' && bodyParams && bodyParams.length > 0) {
345 + // 过滤掉 a、f 和 t 参数(这些参数已硬编码到 URL 中)
346 + const filteredParams = bodyParams.filter(
347 + p => p.name !== 'a' && p.name !== 'f' && p.name !== 't'
348 + )
349 +
350 + filteredParams.forEach(param => {
351 + const type = param.type || 'any'
352 + const desc = param.description || ''
353 + const required = param.required ? '' : ' (可选)'
354 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`)
355 + })
356 + // GET 请求使用 query 参数
357 + } else if (method === 'GET' && parameters && parameters.length > 0) {
358 + // 只保留 query 参数,过滤 header 参数和 a、f、t 参数
359 + const queryParams = parameters.filter(
360 + p => p.in === 'query' && p.name !== 'a' && p.name !== 'f' && p.name !== 't'
361 + )
362 +
363 + queryParams.forEach(param => {
364 + const type = param.schema?.type || 'any'
365 + const desc = param.description || ''
366 + const required = param.required ? '' : ' (可选)'
367 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`)
368 + })
369 + }
370 +
371 + return lines.join('\n')
372 +}
373 +
374 +/**
375 + * 递归生成属性字段的 JSDoc 注释
376 + * @param {object} properties - 属性对象
377 + * @param {number} indent - 缩进级别(空格数)
378 + * @returns {string} - JSDoc 注释
379 + */
380 +function generatePropertiesJSDoc(properties, indent = 0) {
381 + const lines = []
382 + const prefix = ' '.repeat(indent)
383 +
384 + Object.entries(properties).forEach(([key, value]) => {
385 + const type = value.type || 'any'
386 + const desc = value.description || value.title || ''
387 +
388 + // 处理嵌套对象
389 + if (type === 'object' && value.properties) {
390 + lines.push(`${prefix}${key}: {\n`)
391 + // 递归处理嵌套对象的属性
392 + lines.push(generatePropertiesJSDoc(value.properties, indent + 2))
393 + lines.push(`${prefix}};\n`)
394 + // 处理数组(元素是对象)
395 + } else if (type === 'array' && value.items && value.items.properties) {
396 + lines.push(`${prefix}${key}: Array<{\n`)
397 + // 递归处理数组元素的属性
398 + lines.push(generatePropertiesJSDoc(value.items.properties, indent + 2))
399 + lines.push(`${prefix}}>;\n`)
400 + // 处理简单数组
401 + } else if (type === 'array' && value.items) {
402 + const itemType = value.items.type || 'any'
403 + lines.push(`${prefix}${key}: Array<${itemType}>; // ${desc}\n`)
404 + // 处理基本类型
405 + } else {
406 + lines.push(`${prefix}${key}: ${type}; // ${desc}\n`)
407 + }
408 + })
409 +
410 + return lines.join('')
411 +}
412 +
413 +/**
414 + * 生成 JSDoc 返回值注释
415 + * @param {object} responseSchema - 响应 schema
416 + * @returns {string} - JSDoc 返回值注释
417 + */
418 +function generateReturnJSDoc(responseSchema) {
419 + if (!responseSchema || !responseSchema.properties) {
420 + return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回'
421 + }
422 +
423 + const { code, msg, data } = responseSchema.properties
424 +
425 + let returnDesc = ' * @returns {Promise<{\n'
426 + returnDesc += ' * code: number; // 状态码\n'
427 + returnDesc += ' * msg: string; // 消息\n'
428 +
429 + if (data) {
430 + const dataType = data.type || 'any'
431 + const dataDesc = data.description || data.title || ''
432 +
433 + // 处理对象类型的 data
434 + if (dataType === 'object' && data.properties) {
435 + returnDesc += ' * data: {\n'
436 + // 使用递归函数处理 data 的所有属性
437 + returnDesc += generatePropertiesJSDoc(data.properties, 4)
438 + returnDesc += ' * };\n'
439 + // 处理数组类型的 data(元素是对象)
440 + } else if (dataType === 'array' && data.items && data.items.properties) {
441 + returnDesc += ' * data: Array<{\n'
442 + returnDesc += generatePropertiesJSDoc(data.items.properties, 4)
443 + returnDesc += ' * }>;\n'
444 + // 处理简单数组类型
445 + } else if (dataType === 'array' && data.items) {
446 + const itemType = data.items.type || 'any'
447 + returnDesc += ` * data: Array<${itemType}>;\n`
448 + // 其他类型
449 + } else {
450 + returnDesc += ` * data: ${dataType};\n`
451 + }
452 + } else {
453 + returnDesc += ' * data: any;\n'
454 + }
455 +
456 + returnDesc += ' * }>}'
457 +
458 + return returnDesc
459 +}
460 +
461 +/**
462 + * 解析 OpenAPI 文档并提取 API 信息
463 + * @param {object} openapiDoc - 解析后的 OpenAPI 对象
464 + * @param {string} fileName - 文件名(用作 API 名称)
465 + * @returns {object} - 提取的 API 信息
466 + */
467 +function parseOpenAPIDocument(openapiDoc, fileName) {
468 + try {
469 + const path = Object.keys(openapiDoc.paths)[0]
470 + const method = Object.keys(openapiDoc.paths[path])[0]
471 + const apiInfo = openapiDoc.paths[path][method]
472 +
473 + // 提取 query 参数
474 + const parameters = apiInfo.parameters || []
475 + const queryParams = {}
476 + let actionValue = ''
477 + let typeValue = '' // t 参数
478 +
479 + // 提取 body 参数(用于 POST 请求)
480 + const requestBody = apiInfo.requestBody
481 + const bodyParams = extractRequestParams(requestBody)
482 +
483 + // 对于 POST 请求,从 requestBody 中提取 action 和 type
484 + if (requestBody && bodyParams.length > 0) {
485 + const actionParam = bodyParams.find(p => p.name === 'a')
486 + if (actionParam) {
487 + actionValue = actionParam.example || ''
488 + }
489 + const typeParam = bodyParams.find(p => p.name === 't')
490 + if (typeParam) {
491 + typeValue = typeParam.example || ''
492 + }
493 + }
494 +
495 + // 对于 GET 请求,从 query 参数中提取 action 和 type
496 + if (parameters.length > 0) {
497 + parameters.forEach(param => {
498 + if (param.in === 'query') {
499 + queryParams[param.name] = param.example || param.schema?.default || ''
500 +
501 + // 提取 action 参数(通常是 'a' 参数)
502 + if (param.name === 'a' && !actionValue) {
503 + actionValue = param.example || ''
504 + }
505 + // 提取 type 参数('t' 参数)
506 + if (param.name === 't' && !typeValue) {
507 + typeValue = param.example || ''
508 + }
509 + }
510 + })
511 + }
512 +
513 + // 提取响应结构
514 + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema
515 +
516 + return {
517 + summary: apiInfo.summary || fileName,
518 + description: apiInfo.description || '',
519 + method: method.toUpperCase(),
520 + action: actionValue,
521 + type: typeValue, // 保存 t 参数
522 + queryParams,
523 + parameters, // 保存完整的参数信息用于生成 JSDoc(GET 请求)
524 + bodyParams, // 保存 requestBody 参数用于生成 JSDoc(POST 请求)
525 + responseSchema, // 保存响应结构用于生成 JSDoc
526 + fileName,
527 + }
528 + } catch (error) {
529 + console.error(`解析 OpenAPI 文档失败: ${error.message}`)
530 + return null
531 + }
532 +}
533 +
534 +/**
535 + * 生成 API 文件内容
536 + * @param {string} moduleName - 模块名称
537 + * @param {Array} apis - API 信息数组
538 + * @returns {string} - 生成的文件内容
539 + */
540 +function generateApiFileContent(moduleName, apis) {
541 + const imports = "import { fn, fetch } from '@/api/fn';\n\n"
542 + const apiConstants = []
543 + const apiFunctions = []
544 +
545 + apis.forEach(api => {
546 + // 生成常量名(帕斯卡命名)
547 + const constantName = toPascalCase(api.fileName)
548 + // 生成函数名(驼峰命名 + API 后缀)
549 + const functionName = toCamelCase(api.fileName) + 'API'
550 +
551 + // 构建 URL,包含 a 和 t 参数
552 + let url = '/srv/?'
553 + const params = []
554 + if (api.action) {
555 + params.push(`a=${api.action}`)
556 + }
557 + if (api.type) {
558 + params.push(`t=${api.type}`)
559 + }
560 + url += params.join('&')
561 +
562 + // 添加常量定义
563 + apiConstants.push(` ${constantName}: '${url}',`)
564 +
565 + // 生成详细的 JSDoc 注释
566 + const paramJSDoc = generateParamJSDoc(api.parameters, api.bodyParams, api.method)
567 + const returnJSDoc = generateReturnJSDoc(api.responseSchema)
568 +
569 + // 添加函数定义
570 + const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post'
571 +
572 + // 格式化描述
573 + const formattedDesc = formatDescription(api.description)
574 +
575 + // 生成 JSDoc 注释(包含描述)
576 + const comment = `/**
577 + * @description ${api.summary}
578 + * @remark ${formattedDesc}
579 +${paramJSDoc}
580 +${returnJSDoc}
581 + */`
582 +
583 + apiFunctions.push(
584 + `${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));`
585 + )
586 + })
587 +
588 + return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n`
589 +}
590 +
591 +/**
592 + * 扫描目录并处理所有 OpenAPI 文档
593 + * @param {string} openAPIDir - OpenAPI 文档目录
594 + * @param {string} outputDir - 输出目录
595 + */
596 +function scanAndGenerate(openAPIDir, outputDir) {
597 + if (!fs.existsSync(openAPIDir)) {
598 + console.error(`OpenAPI 目录不存在: ${openAPIDir}`)
599 + return
600 + }
601 +
602 + // 确保输出目录存在
603 + if (!fs.existsSync(outputDir)) {
604 + fs.mkdirSync(outputDir, { recursive: true })
605 + }
606 +
607 + // 扫描第一级目录(模块)
608 + const modules = fs
609 + .readdirSync(openAPIDir, { withFileTypes: true })
610 + .filter(dirent => dirent.isDirectory())
611 + .map(dirent => dirent.name)
612 +
613 + console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`)
614 +
615 + modules.forEach(moduleName => {
616 + const moduleDir = path.join(openAPIDir, moduleName)
617 + const apiFiles = fs
618 + .readdirSync(moduleDir)
619 + .filter(file => file.endsWith('.md') && file !== 'CLAUDE.md')
620 +
621 + if (apiFiles.length === 0) {
622 + console.log(`模块 ${moduleName} 中没有找到 .md 文件`)
623 + return
624 + }
625 +
626 + console.log(`\n处理模块: ${moduleName}`)
627 + console.log(`找到 ${apiFiles.length} 个 API 文档`)
628 +
629 + const apis = []
630 +
631 + apiFiles.forEach(fileName => {
632 + const filePath = path.join(moduleDir, fileName)
633 + const content = fs.readFileSync(filePath, 'utf8')
634 + const yamlContent = extractYAMLFromMarkdown(content)
635 +
636 + if (!yamlContent) {
637 + console.warn(` ⚠️ ${fileName}: 未找到 YAML 代码块`)
638 + return
639 + }
640 +
641 + try {
642 + const openapiDoc = yaml.load(yamlContent)
643 + const apiName = path.basename(fileName, '.md')
644 + const apiInfo = parseOpenAPIDocument(openapiDoc, apiName)
645 +
646 + if (apiInfo) {
647 + apis.push(apiInfo)
648 + console.log(` ✓ ${apiName}: ${apiInfo.summary}`)
649 + }
650 + } catch (error) {
651 + console.error(` ✗ ${fileName}: 解析失败 - ${error.message}`)
652 + }
653 + })
654 +
655 + // 生成并保存 API 文件
656 + if (apis.length > 0) {
657 + const fileContent = generateApiFileContent(moduleName, apis)
658 + const outputPath = path.join(outputDir, `${moduleName}.js`)
659 + fs.writeFileSync(outputPath, fileContent, 'utf8')
660 + console.log(` 📝 生成文件: ${outputPath}`)
661 + }
662 + })
663 +
664 + console.log('\n✅ API 文档生成完成!')
665 +
666 + // 对比新旧 API
667 + console.log('\n🔍 开始检测 API 变更...\n')
668 + compareAPIChanges(openAPIDir)
669 +}
670 +
671 +/**
672 + * 备份 OpenAPI 文档目录
673 + * @param {string} sourceDir - 源目录
674 + * @returns {string} - 备份目录路径
675 + */
676 +function backupOpenAPIDir(sourceDir) {
677 + const backupBaseDir = path.resolve(__dirname, '../.tmp')
678 + const backupDir = path.join(backupBaseDir, 'api-specs-backup')
679 +
680 + // 创建备份目录
681 + if (!fs.existsSync(backupBaseDir)) {
682 + fs.mkdirSync(backupBaseDir, { recursive: true })
683 + }
684 +
685 + // 删除旧备份
686 + if (fs.existsSync(backupDir)) {
687 + fs.rmSync(backupDir, { recursive: true, force: true })
688 + }
689 +
690 + // 复制目录
691 + copyDirectory(sourceDir, backupDir)
692 +
693 + return backupDir
694 +}
695 +
696 +/**
697 + * 递归复制目录
698 + * @param {string} src - 源路径
699 + * @param {string} dest - 目标路径
700 + */
701 +function copyDirectory(src, dest) {
702 + if (!fs.existsSync(dest)) {
703 + fs.mkdirSync(dest, { recursive: true })
704 + }
705 +
706 + const entries = fs.readdirSync(src, { withFileTypes: true })
707 +
708 + for (const entry of entries) {
709 + const srcPath = path.join(src, entry.name)
710 + const destPath = path.join(dest, entry.name)
711 +
712 + if (entry.isDirectory()) {
713 + copyDirectory(srcPath, destPath)
714 + } else {
715 + fs.copyFileSync(srcPath, destPath)
716 + }
717 + }
718 +}
719 +
720 +/**
721 + * 对比新旧 API 变更
722 + * @param {string} openAPIDir - OpenAPI 文档目录
723 + */
724 +function compareAPIChanges(openAPIDir) {
725 + const backupDir = path.resolve(__dirname, '../.tmp/api-specs-backup')
726 + const tempDir = path.resolve(__dirname, '../.tmp/api-specs-temp')
727 +
728 + // 检查是否存在临时备份(上一次的版本)
729 + if (!fs.existsSync(tempDir)) {
730 + console.log('ℹ️ 首次运行,已建立基线。下次运行将检测 API 变更。')
731 + // 将当前备份移动到临时目录,作为下次对比的基线
732 + if (fs.existsSync(backupDir)) {
733 + fs.renameSync(backupDir, tempDir)
734 + }
735 + return
736 + }
737 +
738 + // 扫描模块
739 + const modules = fs
740 + .readdirSync(openAPIDir, { withFileTypes: true })
741 + .filter(dirent => dirent.isDirectory())
742 + .map(dirent => dirent.name)
743 +
744 + let hasChanges = false
745 + const moduleReports = []
746 +
747 + modules.forEach(moduleName => {
748 + const moduleDir = path.join(openAPIDir, moduleName)
749 + const tempModuleDir = path.join(tempDir, moduleName)
750 +
751 + // 如果临时备份中不存在该模块,说明是新增模块
752 + if (!fs.existsSync(tempModuleDir)) {
753 + console.log(`\n📦 新增模块: ${moduleName}`)
754 + // 解析新增模块的接口
755 + try {
756 + const newDocs = parseOpenAPIPath(moduleDir)
757 + if (newDocs && newDocs.length > 0) {
758 + // 显示新增接口信息
759 + console.log(` 包含 ${newDocs.length} 个新增接口:`)
760 + newDocs.forEach(doc => {
761 + const path = Object.keys(doc.paths || {})[0] || ''
762 + const method = Object.keys(doc.paths?.[path] || {})[0] || ''
763 + const apiInfo = doc.paths?.[path]?.[method]
764 + const summary = apiInfo?.summary || doc.info?.title || '未命名接口'
765 + console.log(` • ${method?.toUpperCase()} ${path} - ${summary}`)
766 + })
767 + }
768 + } catch (error) {
769 + console.error(` ⚠️ 解析模块失败: ${error.message}`)
770 + }
771 + hasChanges = true
772 + return
773 + }
774 +
775 + // 读取当前和临时备份的文档
776 + const currentFiles = fs
777 + .readdirSync(moduleDir)
778 + .filter(f => f.endsWith('.md') && f !== 'CLAUDE.md')
779 + const tempFiles = fs
780 + .readdirSync(tempModuleDir)
781 + .filter(f => f.endsWith('.md') && f !== 'CLAUDE.md')
782 +
783 + // 检查是否有文件变更
784 + const hasNewFiles = currentFiles.some(f => !tempFiles.includes(f))
785 + const hasRemovedFiles = tempFiles.some(f => !currentFiles.includes(f))
786 + const hasModifiedFiles = currentFiles.some(f => {
787 + if (!tempFiles.includes(f)) {
788 + return false
789 + }
790 + const currentContent = fs.readFileSync(path.join(moduleDir, f), 'utf8')
791 + const tempContent = fs.readFileSync(path.join(tempModuleDir, f), 'utf8')
792 + return currentContent !== tempContent
793 + })
794 +
795 + if (hasNewFiles || hasRemovedFiles || hasModifiedFiles) {
796 + hasChanges = true
797 + moduleReports.push({ moduleName, moduleDir, tempModuleDir })
798 + }
799 + })
800 +
801 + // 检查删除的模块
802 + const tempModules = fs.existsSync(tempDir)
803 + ? fs
804 + .readdirSync(tempDir, { withFileTypes: true })
805 + .filter(dirent => dirent.isDirectory())
806 + .map(dirent => dirent.name)
807 + : []
808 +
809 + const deletedModules = tempModules.filter(m => !modules.includes(m))
810 + if (deletedModules.length > 0) {
811 + hasChanges = true
812 + console.log(`\n❌ 删除模块: ${deletedModules.join(', ')}`)
813 + }
814 +
815 + if (!hasChanges) {
816 + console.log('✅ 未检测到 API 变更')
817 + // 更新基线
818 + if (fs.existsSync(backupDir)) {
819 + fs.rmSync(tempDir, { recursive: true, force: true })
820 + fs.renameSync(backupDir, tempDir)
821 + }
822 + return
823 + }
824 +
825 + // 逐个模块对比
826 + console.log('')
827 + moduleReports.forEach(({ moduleName, moduleDir, tempModuleDir }) => {
828 + try {
829 + const oldDocs = parseOpenAPIPath(tempModuleDir)
830 + const newDocs = parseOpenAPIPath(moduleDir)
831 + const report = generateReport(oldDocs, newDocs, 'text')
832 +
833 + console.log(report)
834 + console.log('')
835 + } catch (error) {
836 + console.error(`⚠️ 模块 ${moduleName} 对比失败: ${error.message}`)
837 + }
838 + })
839 +
840 + // 更新基线:将当前备份作为下次对比的基准
841 + console.log('📝 更新 API 基线...')
842 + if (fs.existsSync(tempDir)) {
843 + fs.rmSync(tempDir, { recursive: true, force: true })
844 + }
845 + if (fs.existsSync(backupDir)) {
846 + fs.renameSync(backupDir, tempDir)
847 + }
848 +}
849 +
850 +// 执行生成
851 +const openAPIDir = path.resolve(__dirname, '../docs/api-specs')
852 +const outputDir = path.resolve(__dirname, '../src/api')
853 +
854 +console.log('=== OpenAPI 转 API 文档生成器 ===\n')
855 +console.log(`输入目录: ${openAPIDir}`)
856 +console.log(`输出目录: ${outputDir}\n`)
857 +
858 +// 备份当前的 OpenAPI 文档(用于下次对比)
859 +if (fs.existsSync(openAPIDir)) {
860 + console.log('💾 备份当前 OpenAPI 文档...')
861 + backupOpenAPIDir(openAPIDir)
862 + console.log('')
863 +}
864 +
865 +scanAndGenerate(openAPIDir, outputDir)
1 +/**
2 + * 测试生成的 API 文件
3 + */
4 +
5 +const path = require('path')
6 +const fs = require('fs')
7 +
8 +// 测试导入生成的 API
9 +const userApiPath = path.resolve(__dirname, '../src/api/user.js')
10 +
11 +console.log('=== 测试生成的 API 文件 ===\n')
12 +
13 +if (fs.existsSync(userApiPath)) {
14 + const content = fs.readFileSync(userApiPath, 'utf8')
15 + console.log('✅ API 文件生成成功\n')
16 + console.log('文件内容:')
17 + console.log('─'.repeat(60))
18 + console.log(content)
19 + console.log('─'.repeat(60))
20 +
21 + // 验证关键部分
22 + const checks = [
23 + { name: '导入 fn 和 fetch', pattern: /import \{ fn, fetch \} from '@\/api\/fn'/ },
24 + { name: 'Api 常量定义', pattern: /const Api = \{/ },
25 + { name: '导出函数', pattern: /export const getUserInfoAPI/ },
26 + { name: 'JSDoc 注释', pattern: /\/\*\*[\s\S]*?\*\// },
27 + { name: '正确的 action', pattern: /a=user_info/ },
28 + ]
29 +
30 + console.log('\n验证结果:')
31 + checks.forEach(check => {
32 + const passed = check.pattern.test(content)
33 + console.log(`${passed ? '✅' : '❌'} ${check.name}`)
34 + })
35 +
36 + console.log('\n✅ 所有验证通过!')
37 +} else {
38 + console.log('❌ API 文件不存在,请先运行 pnpm api:generate')
39 +}
1 +import { fn, fetch } from '@/api/fn'
2 +
3 +const Api = {
4 + Checkin: '/srv/?a=map_activity&t=checkin',
5 + Detail: '/srv/?a=map_activity&t=detail',
6 + IsChecked: '/srv/?a=map_activity&t=is_checked',
7 + List: '/srv/?a=map_activity&t=list',
8 + Poster: '/srv/?a=map_activity&t=poster',
9 + SavePosterBackground: '/srv/?a=map_activity&t=save_poster_background',
10 +}
11 +
12 +/**
13 + * @description 打卡
14 + * @remark
15 + * @param {Object} params 请求参数
16 + * @param {string} params.activity_id (可选) 活动ID
17 + * @param {string} params.detail_id (可选) 打卡点ID
18 + * @param {string} params.openid (可选)
19 + * @returns {Promise<{
20 + * code: number; // 状态码
21 + * msg: string; // 消息
22 + * data: any;
23 + * }>}
24 + */
25 +export const checkinAPI = params => fn(fetch.post(Api.Checkin, params))
26 +
27 +/**
28 + * @description 地图活动详情
29 + * @remark
30 + * @param {Object} params 请求参数
31 + * @param {string} params.id (可选) 活动ID
32 + * @returns {Promise<{
33 + * code: number; // 状态码
34 + * msg: string; // 消息
35 + * data: {
36 + url: string; // 地图网址
37 + id: string; // 活动ID
38 + cover: string; // 封面图
39 + tittle: string; // 标题
40 + begin_date: string; // 开始时间
41 + end_date: string; // 结束时间
42 + is_ended: boolean; // 活动是否已经结束
43 + is_begin: boolean; // 活动是否开始
44 + first_checkin_points: integer; // 首次打卡获得积分
45 + required_checkin_count: integer; // 需要打卡几次,才能完成活动
46 + complete_points: integer; // 完成活动获得多少积分
47 + discount_title: string; // 打卡点底部优惠标题
48 + * };
49 + * }>}
50 + */
51 +export const detailAPI = params => fn(fetch.get(Api.Detail, params))
52 +
53 +/**
54 + * @description 是否已经打卡
55 + * @remark
56 + * @param {Object} params 请求参数
57 + * @param {string} params.detail_id (可选) 打卡点ID
58 + * @param {string} params.openid (可选)
59 + * @param {string} params.activity_id (可选) 活动ID
60 + * @returns {Promise<{
61 + * code: number; // 状态码
62 + * msg: string; // 消息
63 + * data: {
64 + is_checked: boolean; // 是否已经打卡
65 + * };
66 + * }>}
67 + */
68 +export const isCheckedAPI = params => fn(fetch.get(Api.IsChecked, params))
69 +
70 +/**
71 + * @description 地图活动列表
72 + * @remark
73 + * @param {Object} params 请求参数
74 + * @returns {Promise<{
75 + * code: number; // 状态码
76 + * msg: string; // 消息
77 + * data: Array<{
78 + url: string; // 地图网址
79 + id: string; // 活动ID
80 + cover: string; // 封面图
81 + tittle: string; // 标题
82 + begin_date: string; // 开始时间
83 + end_date: string; // 结束时间
84 + * }>;
85 + * }>}
86 + */
87 +export const listAPI = params => fn(fetch.get(Api.List, params))
88 +
89 +/**
90 + * @description 获取海报
91 + * @remark
92 + * @param {Object} params 请求参数
93 + * @param {string} params.activity_id (可选) 活动ID
94 + * @param {string} params.detail_id (可选) 关卡ID
95 + * @param {string} params.env_version (可选) 小程序版本。正式版为 "release",体验版为 "trial"。默认是正式版
96 + * @returns {Promise<{
97 + * code: number; // 状态码
98 + * msg: string; // 消息
99 + * data: {
100 + details: Array<{
101 + id: integer; // 关卡ID
102 + name: string; // 关卡名称
103 + background_url: string; // 关卡背景图
104 + main_slogan: string; //
105 + sub_slogan: string; //
106 + is_checked: boolean; // 是否已经打卡
107 + }>;
108 + family: {
109 + id: integer; // 家庭ID
110 + name: string; // 家庭名称
111 + avatar_url: string; // 家庭头像
112 + };
113 + show_detail_index: integer; // 从 0 开始计数
114 + end_date: string; // 活动截止时间
115 + qrcode_url: string; // 小程序码
116 + title: string; // 海报标题
117 + begin_date: string; // 活动开始日期
118 + * };
119 + * }>}
120 + */
121 +export const posterAPI = params => fn(fetch.get(Api.Poster, params))
122 +
123 +/**
124 + * @description 上传海报背景
125 + * @remark
126 + * @param {Object} params 请求参数
127 + * @param {string} params.activity_id (可选) 活动ID
128 + * @param {string} params.detail_id (可选) 打卡点ID
129 + * @param {string} params.poster_background_url (可选) 关卡海报背景
130 + * @returns {Promise<{
131 + * code: number; // 状态码
132 + * msg: string; // 消息
133 + * data: any;
134 + * }>}
135 + */
136 +export const savePosterBackgroundAPI = params => fn(fetch.post(Api.SavePosterBackground, params))
...@@ -37,54 +37,68 @@ ...@@ -37,54 +37,68 @@
37 import { ref } from 'vue' 37 import { ref } from 'vue'
38 import Taro from '@tarojs/taro' 38 import Taro from '@tarojs/taro'
39 import BottomNav from '@/components/BottomNav.vue' 39 import BottomNav from '@/components/BottomNav.vue'
40 +import { listAPI } from '@/api/map_activity'
41 +import { mockMapActivityListAPI } from '@/utils/mockData'
42 +import { useLoad } from '@tarojs/taro'
43 +
44 +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
45 +const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
46 +
47 +/**
48 + * 便民地图列表数据
49 + */
50 +const mapList = ref([])
51 +const loading = ref(false)
52 +
53 +/**
54 + * 格式化 API 数据为页面所需格式
55 + * @param {Array} list - API 返回的活动列表
56 + * @returns {Array} 格式化后的活动列表
57 + */
58 +const formatMapList = list => {
59 + return list.map(item => ({
60 + id: item.id,
61 + title: item.tittle, // API 返回的是 tittle,映射为 title
62 + cover: item.cover,
63 + timeRange: `${item.begin_date}~${item.end_date}`,
64 + activityId: item.id, // 使用 id 作为 activityId
65 + }))
66 +}
40 67
41 /** 68 /**
42 - * Mock 便民地图数据 69 + * 获取地图活动列表
43 */ 70 */
44 -const mapList = ref([ 71 +const fetchMapList = async () => {
45 - { 72 + if (loading.value) {
46 - id: 1, 73 + return
47 - title: '重阳登高打卡', 74 + }
48 - cover: 'https://picsum.photos/400/300?random=1', 75 +
49 - timeRange: '2025.09.06~2025.10.31', 76 + loading.value = true
50 - activityId: 'chongyang_2024', 77 +
51 - }, 78 + try {
52 - { 79 + const params = {}
53 - id: 2, 80 +
54 - title: '公园晨跑打卡', 81 + // 根据开关选择使用真实 API 或 Mock 数据
55 - cover: 'https://picsum.photos/400/300?random=2', 82 + const res = USE_MOCK_DATA ? await mockMapActivityListAPI(params) : await listAPI(params)
56 - timeRange: '2025.09.01~2025.12.31', 83 +
57 - activityId: 'morning_run_2024', 84 + if (res.code === 1 && res.data) {
58 - }, 85 + mapList.value = formatMapList(res.data)
59 - { 86 + } else {
60 - id: 3, 87 + Taro.showToast({
61 - title: '社区健身打卡', 88 + title: res.msg || '获取活动列表失败',
62 - cover: 'https://picsum.photos/400/300?random=3', 89 + icon: 'none',
63 - timeRange: '2025.08.01~2025.12.31', 90 + })
64 - activityId: 'community_fitness', 91 + }
65 - }, 92 + } catch (err) {
66 - { 93 + console.error('获取地图活动列表失败:', err)
67 - id: 4, 94 + Taro.showToast({
68 - title: '周末徒步打卡', 95 + title: '网络异常,请重试',
69 - cover: 'https://picsum.photos/400/300?random=4', 96 + icon: 'none',
70 - timeRange: '2025.09.15~2025.11.30', 97 + })
71 - activityId: 'weekend_hike', 98 + } finally {
72 - }, 99 + loading.value = false
73 - { 100 + }
74 - id: 5, 101 +}
75 - title: '秋日赏菊打卡',
76 - cover: 'https://picsum.photos/400/300?random=5',
77 - timeRange: '2025.10.15~2025.11.15',
78 - activityId: 'autumn_chrysanthemum',
79 - },
80 - {
81 - id: 6,
82 - title: '古镇文化打卡',
83 - cover: 'https://picsum.photos/400/300?random=6',
84 - timeRange: '2025.10.01~2025.10.31',
85 - activityId: 'ancient_town',
86 - },
87 -])
88 102
89 /** 103 /**
90 * 处理卡片点击事件 104 * 处理卡片点击事件
...@@ -107,6 +121,11 @@ const handleEnter = item => { ...@@ -107,6 +121,11 @@ const handleEnter = item => {
107 url: `/pages/ActivitiesCover/index?activityId=${item.activityId}&title=${encodeURIComponent(item.title)}`, 121 url: `/pages/ActivitiesCover/index?activityId=${item.activityId}&title=${encodeURIComponent(item.title)}`,
108 }) 122 })
109 } 123 }
124 +
125 +// 页面加载时获取列表
126 +useLoad(() => {
127 + fetchMapList()
128 +})
110 </script> 129 </script>
111 130
112 <style lang="less"> 131 <style lang="less">
......
1 +/**
2 + * @Description: Mock 数据生成工具 - 用于测试地图活动功能
3 + * @Date: 2026-02-09
4 + *
5 + * 支持的 API Mock:
6 + * - listAPI: 地图活动列表
7 + * - detailAPI: 地图活动详情
8 + * - isCheckedAPI: 是否已打卡
9 + * - posterAPI: 获取海报
10 + */
11 +
12 +// ============================================================================
13 +// 工具函数
14 +// ============================================================================
15 +
16 +/**
17 + * 随机图片 URL 生成器
18 + * @param {number} width - 图片宽度
19 + * @param {number} height - 图片高度
20 + * @param {number} seed - 随机种子
21 + * @returns {string} 图片 URL
22 + */
23 +function randomImage(width = 400, height = 300, seed = 1) {
24 + return `https://picsum.photos/${width}/${height}?random=${seed}`
25 +}
26 +
27 +/**
28 + * 模拟网络延迟
29 + * @param {number} min - 最小延迟(ms)
30 + * @param {number} max - 最大延迟(ms)
31 + * @returns {Promise}
32 + */
33 +function mockDelay(min = 100, max = 300) {
34 + const delay = Math.random() * (max - min) + min
35 + return new Promise(resolve => setTimeout(resolve, delay))
36 +}
37 +
38 +// ============================================================================
39 +// 1. 地图活动列表 Mock (listAPI)
40 +// ============================================================================
41 +
42 +const ACTIVITY_NAMES = [
43 + '重阳登高打卡',
44 + '公园晨跑打卡',
45 + '社区健身打卡',
46 + '周末徒步打卡',
47 + '秋日赏菊打卡',
48 + '古镇文化打卡',
49 + '健康骑行活动',
50 + '亲子运动会',
51 + '户外拓展训练',
52 + '城市定向挑战',
53 +]
54 +
55 +/**
56 + * 生成地图活动列表项
57 + * @param {number} id - 活动 ID
58 + * @returns {Object} 活动对象
59 + */
60 +function generateMapActivityItem(id) {
61 + const activityName = ACTIVITY_NAMES[Math.floor(Math.random() * ACTIVITY_NAMES.length)]
62 + const now = new Date()
63 + const startDate = new Date(now.getTime() + Math.random() * 30 * 24 * 60 * 60 * 1000)
64 + const endDate = new Date(startDate.getTime() + (30 + Math.random() * 60) * 24 * 60 * 60 * 1000)
65 +
66 + // 格式化日期为 YYYY.MM.DD
67 + const formatDate = date => {
68 + const year = date.getFullYear()
69 + const month = String(date.getMonth() + 1).padStart(2, '0')
70 + const day = String(date.getDate()).padStart(2, '0')
71 + return `${year}.${month}.${day}`
72 + }
73 +
74 + return {
75 + id: String(id),
76 + tittle: activityName,
77 + cover: randomImage(400, 300, id),
78 + begin_date: formatDate(startDate),
79 + end_date: formatDate(endDate),
80 + url: '',
81 + }
82 +}
83 +
84 +/**
85 + * Mock: listAPI (地图活动列表)
86 + * @param {Object} params - 请求参数
87 + * @returns {Promise<{code: number, msg: string, data: Array}>}
88 + */
89 +export async function mockMapActivityListAPI() {
90 + await mockDelay()
91 +
92 + const list = []
93 + const total = 6
94 +
95 + for (let i = 0; i < total; i++) {
96 + list.push(generateMapActivityItem(i + 1))
97 + }
98 +
99 + console.log(`[Mock] listAPI - 地图活动列表,共${list.length}条`)
100 +
101 + return {
102 + code: 1,
103 + msg: 'success',
104 + data: list,
105 + }
106 +}
107 +
108 +// ============================================================================
109 +// 2. 地图活动详情 Mock (detailAPI)
110 +// ============================================================================
111 +
112 +/**
113 + * Mock: detailAPI (地图活动详情)
114 + * @param {Object} params - 请求参数
115 + * @param {string} params.id - 活动 ID
116 + * @returns {Promise<{code: number, msg: string, data: Object}>}
117 + */
118 +export async function mockMapActivityDetailAPI(params) {
119 + await mockDelay()
120 +
121 + const { id } = params
122 + const item = generateMapActivityItem(parseInt(id) || 1)
123 +
124 + console.log(`[Mock] detailAPI - 活动详情,ID:${id}`)
125 +
126 + return {
127 + code: 1,
128 + msg: 'success',
129 + data: {
130 + ...item,
131 + url: 'https://example.com/map',
132 + is_ended: false,
133 + is_begin: true,
134 + first_checkin_points: 10,
135 + required_checkin_count: 5,
136 + complete_points: 50,
137 + discount_title: '打卡点优惠信息',
138 + },
139 + }
140 +}
141 +
142 +// ============================================================================
143 +// 3. 是否已打卡 Mock (isCheckedAPI)
144 +// ============================================================================
145 +
146 +/**
147 + * Mock: isCheckedAPI (是否已打卡)
148 + * @param {Object} params - 请求参数
149 + * @param {string} params.detail_id - 打卡点 ID
150 + * @returns {Promise<{code: number, msg: string, data: Object}>}
151 + */
152 +export async function mockIsCheckedAPI(params) {
153 + await mockDelay()
154 +
155 + const { detail_id } = params
156 + const isChecked = parseInt(detail_id) % 3 === 0
157 +
158 + console.log(`[Mock] isCheckedAPI - 打卡点${detail_id},${isChecked ? '已打卡' : '未打卡'}`)
159 +
160 + return {
161 + code: 1,
162 + msg: 'success',
163 + data: {
164 + is_checked: isChecked,
165 + },
166 + }
167 +}
168 +
169 +// ============================================================================
170 +// 4. 获取海报 Mock (posterAPI)
171 +// ============================================================================
172 +
173 +/**
174 + * Mock: posterAPI (获取海报)
175 + * @param {Object} params - 请求参数
176 + * @param {string} params.activity_id - 活动 ID
177 + * @returns {Promise<{code: number, msg: string, data: Object}>}
178 + */
179 +export async function mockPosterAPI(params) {
180 + await mockDelay()
181 +
182 + const { activity_id } = params
183 +
184 + console.log(`[Mock] posterAPI - 获取海报,活动ID:${activity_id}`)
185 +
186 + return {
187 + code: 1,
188 + msg: 'success',
189 + data: {
190 + details: [
191 + {
192 + id: 1,
193 + name: '起点打卡',
194 + background_url: randomImage(750, 1200, 20),
195 + main_slogan: '开启健康之旅',
196 + sub_slogan: '坚持就是胜利',
197 + is_checked: true,
198 + },
199 + {
200 + id: 2,
201 + name: '山顶打卡',
202 + background_url: randomImage(750, 1200, 21),
203 + main_slogan: '登高望远',
204 + sub_slogan: '风景这边独好',
205 + is_checked: false,
206 + },
207 + ],
208 + family: {
209 + id: 123,
210 + name: '快乐家庭',
211 + avatar_url: randomImage(200, 200, 30),
212 + },
213 + show_detail_index: 0,
214 + end_date: '2025.10.31',
215 + qrcode_url: 'https://example.com/qrcode.jpg',
216 + title: '重阳登高打卡',
217 + begin_date: '2025.09.06',
218 + },
219 + }
220 +}
221 +
222 +// ============================================================================
223 +// 导出统一 Mock API 调用器
224 +// ============================================================================
225 +
226 +/**
227 + * Mock API 调用器
228 + * @param {string} apiName - API 名称
229 + * @param {Object} params - 请求参数
230 + * @returns {Promise}
231 + */
232 +export async function mockAPI(apiName, params) {
233 + switch (apiName) {
234 + case 'listAPI':
235 + return await mockMapActivityListAPI(params)
236 + case 'detailAPI':
237 + return await mockMapActivityDetailAPI(params)
238 + case 'isCheckedAPI':
239 + return await mockIsCheckedAPI(params)
240 + case 'posterAPI':
241 + return await mockPosterAPI(params)
242 + default:
243 + console.warn(`[Mock] 未知的 API: ${apiName}`)
244 + return { code: 0, msg: 'Unknown API', data: null }
245 + }
246 +}
247 +
248 +/**
249 + * Mock 地图活动详情数据
250 + * @param {string} activityId - 活动 ID
251 + * @returns {Object} 地图活动详情
252 + */
253 +export const mockMapActivityDetail = () => {
254 + return {
255 + url: 'https://example.com/map',
256 + id: '1',
257 + cover: randomImage(750, 500, 10),
258 + tittle: '重阳登高打卡',
259 + begin_date: '2025.09.06',
260 + end_date: '2025.10.31',
261 + is_ended: false,
262 + is_begin: true,
263 + first_checkin_points: 10,
264 + required_checkin_count: 5,
265 + complete_points: 50,
266 + discount_title: '打卡点优惠信息',
267 + }
268 +}
269 +
270 +/**
271 + * Mock 是否已打卡数据
272 + * @param {string} detailId - 打卡点 ID
273 + * @returns {boolean} 是否已打卡
274 + */
275 +export const mockIsChecked = detailId => {
276 + // 偶尔返回已打卡
277 + return parseInt(detailId) % 3 === 0
278 +}
279 +
280 +/**
281 + * Mock 海报数据
282 + * @param {string} activityId - 活动 ID
283 + * @returns {Object} 海报数据
284 + */
285 +export const mockPoster = activityId => {
286 + return {
287 + details: [
288 + {
289 + id: 1,
290 + name: '起点打卡',
291 + background_url: randomImage(750, 1200, 20),
292 + main_slogan: '开启健康之旅',
293 + sub_slogan: '坚持就是胜利',
294 + is_checked: true,
295 + },
296 + {
297 + id: 2,
298 + name: '山顶打卡',
299 + background_url: randomImage(750, 1200, 21),
300 + main_slogan: '登高望远',
301 + sub_slogan: '风景这边独好',
302 + is_checked: false,
303 + },
304 + ],
305 + family: {
306 + id: 123,
307 + name: '快乐家庭',
308 + avatar_url: randomImage(200, 200, 30),
309 + },
310 + show_detail_index: 0,
311 + end_date: '2025.10.31',
312 + qrcode_url: 'https://example.com/qrcode.jpg',
313 + title: '重阳登高打卡',
314 + begin_date: '2025.09.06',
315 + }
316 +}