hookehuyr

feat(mock): 新增消息模块及本地 mock 环境支持

- 新增消息详情页面及路由配置
- 重构环境配置,支持通过 API_RUNTIME_ENV 统一切换正式/mock 环境
- 添加完整的 mock 架构,包含模块化 handler、状态管理 store 和静态 fixture
- 集成 mock 到现有请求层,实现 axios 和 Taro 请求的无缝拦截
- 更新项目文档,说明 mock 目录约定和环境切换方式
...@@ -12,6 +12,11 @@ ...@@ -12,6 +12,11 @@
12 ## 测试要求 12 ## 测试要求
13 当前 `package.json` 中没有独立的单元测试框架,因此默认验证方式为 `pnpm lint` 加手工联调。修改页面、路由、认证或请求逻辑后,请至少在微信开发者工具中走通相关流程。若变更涉及 `src/utils/authRedirect.js``src/utils/request.js``src/api/`,需要重点检查登录态、页面回跳和接口请求是否正常。后续如新增自动化测试,建议贴近功能放置,并使用 `*.spec.js` 命名。 13 当前 `package.json` 中没有独立的单元测试框架,因此默认验证方式为 `pnpm lint` 加手工联调。修改页面、路由、认证或请求逻辑后,请至少在微信开发者工具中走通相关流程。若变更涉及 `src/utils/authRedirect.js``src/utils/request.js``src/api/`,需要重点检查登录态、页面回跳和接口请求是否正常。后续如新增自动化测试,建议贴近功能放置,并使用 `*.spec.js` 命名。
14 14
15 +## Mock 环境与接口联调约定
16 +仓库当前支持“正式环境 / 本地 Mock 环境”两套 API 运行模式,且环境切换只允许通过配置文件统一控制,不要在页面里单独写开关。开发环境默认读取 `config/dev.js` 里的 `env.API_RUNTIME_ENV`,生产构建默认读取 `config/prod.js`;当 `API_RUNTIME_ENV``mock` 时,本地所有接口统一走 `src/mock/`,当其为 `production` 时,统一走真实接口。若只是临时联调某个页面,也不要在页面组件里直接写 `USE_MOCK_DATA` 之类的局部布尔开关,优先保持“一个地方切整个环境”的规则稳定。
17 +
18 +Mock 目录也有明确分工:`src/mock/index.js` 只做统一入口和分发;`src/mock/modules/*.mock.js` 按业务模块注册 handler;`src/mock/shared/` 放请求解析、响应包装、handler 匹配等公共能力;`src/mock/stores/*.store.js` 只处理需要状态变化的 mock 数据;`src/mock/fixtures/*.fixture.js` 只放静态样本和数据工厂。新增 mock 时,优先按“fixture -> store(如有状态)-> module handler -> modules/index.js 注册”的顺序扩展,不要再回到一个超大 `mockData.js` 式文件里堆条件分支。页面层不应该知道 mock 细节,只应该继续调用正常的 `xxxAPI`
19 +
15 ## 授权与支付链路约定 20 ## 授权与支付链路约定
16 授权逻辑的核心在 `src/app.js``src/utils/authRedirect.js``src/utils/request.js``src/pages/auth/index`。应用启动时会优先尝试静默授权,`sessionid` 统一写入 Taro 本地缓存,并由请求拦截器动态注入到请求头;接口返回 `401` 时,会先尝试 `refreshSession` 静默续期并重放原请求,失败后再降级跳转授权页。因此除非明确重构整条链路,否则不要随意改动 `sessionid` 的存取方式、`saveCurrentPagePath` / `returnToOriginalPage` 的回跳机制、`navigateToAuth` 的防重逻辑,也不要跳过 `src/pages/auth/index` 直接在业务页硬编码授权流程。若修改分享进入、启动授权、401 重试或来源页回填逻辑,需至少手工验证一次“未授权进入页面 -> 自动或手动授权 -> 成功回跳原页面”的完整闭环。 21 授权逻辑的核心在 `src/app.js``src/utils/authRedirect.js``src/utils/request.js``src/pages/auth/index`。应用启动时会优先尝试静默授权,`sessionid` 统一写入 Taro 本地缓存,并由请求拦截器动态注入到请求头;接口返回 `401` 时,会先尝试 `refreshSession` 静默续期并重放原请求,失败后再降级跳转授权页。因此除非明确重构整条链路,否则不要随意改动 `sessionid` 的存取方式、`saveCurrentPagePath` / `returnToOriginalPage` 的回跳机制、`navigateToAuth` 的防重逻辑,也不要跳过 `src/pages/auth/index` 直接在业务页硬编码授权流程。若修改分享进入、启动授权、401 重试或来源页回填逻辑,需至少手工验证一次“未授权进入页面 -> 自动或手动授权 -> 成功回跳原页面”的完整闭环。
17 22
...@@ -26,4 +31,4 @@ ...@@ -26,4 +31,4 @@
26 本仓库默认使用中文沟通与书写。今后新增或修改的说明性文字请统一使用中文,包括但不限于文档、注释说明、提交说明、PR 描述、变更摘要和协作备注;如必须保留英文术语,请同时提供中文语义,避免只写英文描述。 31 本仓库默认使用中文沟通与书写。今后新增或修改的说明性文字请统一使用中文,包括但不限于文档、注释说明、提交说明、PR 描述、变更摘要和协作备注;如必须保留英文术语,请同时提供中文语义,避免只写英文描述。
27 32
28 ## 安全与配置提示 33 ## 安全与配置提示
29 -不要提交真实的 AppID、令牌或生产环境域名。首次接手项目时,请优先检查 `src/utils/config.js``project.config.json`。授权与支付测试环境当前会复用既有后端配置,调整域名、`client_id`、支付接口地址或 WebView 跳转地址前,必须先确认不会影响 `openid`、会话续期和微信支付参数生成。除非明确要重构认证链路,否则不要随意删除或绕过 `src/pages/auth/index` 认证页 34 +不要提交真实的 AppID、令牌或生产环境域名。首次接手项目时,请优先检查 `src/utils/config.js``config/dev.js``config/prod.js``project.config.json`。授权与支付测试环境当前会复用既有后端配置,调整域名、`client_id`、支付接口地址或 WebView 跳转地址前,必须先确认不会影响 `openid`、会话续期和微信支付参数生成。除非明确要重构认证链路,否则不要随意删除或绕过 `src/pages/auth/index` 认证页;同理,除非明确要废弃本地 mock 联调能力,否则不要把 `API_RUNTIME_ENV``src/mock/index.js``src/mock/modules/` 里的统一分发能力改回页面内局部开关
......
...@@ -76,19 +76,104 @@ src/ ...@@ -76,19 +76,104 @@ src/
76 76
77 ### 1. 修改服务器配置 77 ### 1. 修改服务器配置
78 78
79 -编辑 `src/utils/config.js` 79 +环境读取逻辑集中在 `src/utils/config.js`,实际运行模式由构建配置决定
80 80
81 ```javascript 81 ```javascript
82 -const BASE_URL = process.env.NODE_ENV === 'production' 82 +export const API_ENVIRONMENTS = {
83 - ? 'https://your-production-domain.com' // 生产环境域名 83 + production: {
84 - : 'https://your-dev-domain.com' // 开发环境域名 84 + baseURL: 'https://your-production-domain.com',
85 + requestDefaultParams: {
86 + f: 'YOUR_MODULE',
87 + client_id: 'YOUR_CLIENT_ID',
88 + },
89 + },
90 + mock: {
91 + baseURL: 'https://your-production-domain.com',
92 + requestDefaultParams: {
93 + f: 'YOUR_MODULE',
94 + client_id: 'YOUR_CLIENT_ID',
95 + },
96 + useMock: true,
97 + },
98 +}
99 +```
100 +
101 +### 1.1 正式环境 / 本地 Mock 环境
102 +
103 +仓库现在内置两套 API 运行模式:
104 +
105 +- `production`:正式环境,走真实接口
106 +- `mock`:本地 Mock 环境,开发态默认启用
107 +
108 +核心文件:
85 109
86 -export const REQUEST_DEFAULT_PARAMS = { 110 +- `config/dev.js`:本地开发环境开关,默认 `API_RUNTIME_ENV = "mock"`
87 - f: 'YOUR_MODULE', // 业务模块标识 111 +- `config/prod.js`:生产构建环境开关,默认 `API_RUNTIME_ENV = "production"`
88 - client_name: 'YOUR_APP', // 应用名称 112 +- `src/utils/config.js`:环境定义与当前配置读取
113 +- `src/mock/index.js`:统一 Mock 入口
114 +- `src/mock/modules/`:按模块拆分的 Mock 处理器
115 +- `src/mock/shared/`:请求解析、响应包装、路由匹配等公共能力
116 +- `src/mock/stores/`:需要状态的 Mock 数据存储
117 +- `src/mock/fixtures/`:纯静态样本和数据工厂
118 +- `src/mock/README.md`:新增模块时的目录约定与 handler 示例
119 +- `src/api/message.js`:消息列表 / 详情接口示例
120 +
121 +当前已经预置了以下 mock:
122 +
123 +- 启动授权:`/srv/?a=openid`
124 +- 支付参数:`/srv/?a=pay``/srv/?a=icbc_pay_wxamp`
125 +- 分享配置:`/srv/?a=wx_share`
126 +- 短信与上传:`/srv/?a=sms``/srv/?a=upload`
127 +- 消息示例:`/srv/?a=message&t=list``/srv/?a=message&t=detail`
128 +
129 +如果你想让本地所有接口都走 Mock,只改一个地方就行:
130 +
131 +```js
132 +// config/dev.js
133 +env: {
134 + NODE_ENV: '"development"',
135 + API_RUNTIME_ENV: '"mock"',
89 } 136 }
90 ``` 137 ```
91 138
139 +要切回本地真实接口,把它改成:
140 +
141 +```js
142 +API_RUNTIME_ENV: '"production"'
143 +```
144 +
145 +这套开关是“全局生效”的:
146 +
147 +- `mock`:当前构建下所有接口统一走 `src/mock/`
148 +- `production`:当前构建下所有接口统一走真实接口
149 +
150 +页面层不要自己再写局部 `USE_MOCK_DATA` 开关,避免同一个页面和别的页面跑在不同环境里。
151 +
152 +建议的接入顺序:
153 +
154 +1. 先在 `src/mock/fixtures/` 里按真实字段准备样本数据。
155 +2. 如果该接口存在状态变化,再在 `src/mock/stores/` 里补状态读写。
156 +3.`src/mock/modules/` 里新增对应 handler,并在 `src/mock/modules/index.js` 注册。
157 +4. 页面继续只调用正常的 `xxxAPI`,先对着 mock 跑通交互。
158 +5. 拿到真实接口后,把 `config/dev.js` 切回 `production`,再开始联调真实 API。
159 +
160 +### 1.2 Mock 目录约定
161 +
162 +`src/mock/` 当前按这几个层次组织:
163 +
164 +- `index.js`
165 + 统一入口,只负责创建请求上下文、匹配 handler、返回 axios / Taro 兼容结果。
166 +- `modules/*.mock.js`
167 + 按模块定义接口级 handler,一个 handler 对应一个 `method + action + type` 组合。
168 +- `stores/*.store.js`
169 + 放有状态的 mock 数据,比如消息已读、列表新增删除、详情回写等。
170 +- `fixtures/*.fixture.js`
171 + 放纯静态样本和数据工厂,不直接处理状态变化。
172 +- `shared/*.js`
173 + 放请求解析、响应包装、匹配规则等可复用能力。
174 +
175 +详细约定见 [src/mock/README.md](/Users/huyirui/program/itomix/jls_weapp/src/mock/README.md)
176 +
92 ### 2. 定义 API 接口 177 ### 2. 定义 API 接口
93 178
94 编辑 `src/api/index.js`,添加您的业务接口: 179 编辑 `src/api/index.js`,添加您的业务接口:
...@@ -136,7 +221,7 @@ export default { ...@@ -136,7 +221,7 @@ export default {
136 - `src/utils/authRedirect.js` - 认证流程管理 221 - `src/utils/authRedirect.js` - 认证流程管理
137 - `src/utils/request.js` - HTTP 请求拦截器 222 - `src/utils/request.js` - HTTP 请求拦截器
138 223
139 -**重要**后端需提供 `/srv/?a=openid_wxapp` 接口用于微信登录 224 +**重要**当前授权链路使用的是 `/srv/?a=openid`。如果后端动作名、cookie 返回方式或公共参数发生变化,需要同时检查真实授权链路和本地 mock 授权链路是否保持一致
140 225
141 ## 🌐 弱网/离线支持 226 ## 🌐 弱网/离线支持
142 227
...@@ -183,9 +268,10 @@ export default { ...@@ -183,9 +268,10 @@ export default {
183 - Props 定义清晰,注释详细 268 - Props 定义清晰,注释详细
184 269
185 ### API 调用 270 ### API 调用
186 -- 接口统一`src/api/index.js` 定义 271 +- 接口统一放在 `src/api/` 下按模块定义
187 - 使用 `xxxAPI(params)` 命名格式 272 - 使用 `xxxAPI(params)` 命名格式
188 - 请求方法统一使用 `src/api/fn.js` 中的封装 273 - 请求方法统一使用 `src/api/fn.js` 中的封装
274 +- 页面不要直接判断当前是否 Mock,统一让 `src/api/fn.js``src/mock/` 兜底
189 275
190 ### 状态管理 276 ### 状态管理
191 - 使用 Pinia 进行状态管理 277 - 使用 Pinia 进行状态管理
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
7 */ 7 */
8 export default { 8 export default {
9 env: { 9 env: {
10 - NODE_ENV: '"development"' 10 + NODE_ENV: '"development"',
11 + API_RUNTIME_ENV: '"mock"',
11 }, 12 },
12 logger: { 13 logger: {
13 quiet: false, 14 quiet: false,
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
7 */ 7 */
8 export default { 8 export default {
9 env: { 9 env: {
10 - NODE_ENV: '"production"' 10 + NODE_ENV: '"production"',
11 + API_RUNTIME_ENV: '"production"',
11 }, 12 },
12 mini: {}, 13 mini: {},
13 h5: { 14 h5: {
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
8 import axios from '@/utils/request'; 8 import axios from '@/utils/request';
9 import Taro from '@tarojs/taro' 9 import Taro from '@tarojs/taro'
10 import qs from 'qs' 10 import qs from 'qs'
11 +import { createAxiosMockResponse, shouldUseMock } from '../mock'
11 12
12 /** 13 /**
13 * @description 统一后端返回格式(强制 { code, data, msg }) 14 * @description 统一后端返回格式(强制 { code, data, msg })
...@@ -83,6 +84,14 @@ export const fetch = { ...@@ -83,6 +84,14 @@ export const fetch = {
83 * @returns {Promise<any>} axios Promise 84 * @returns {Promise<any>} axios Promise
84 */ 85 */
85 get: function (api, params) { 86 get: function (api, params) {
87 + if (shouldUseMock()) {
88 + return createAxiosMockResponse({
89 + url: api,
90 + method: 'GET',
91 + params: params?.params || params || {},
92 + config: { url: api, method: 'get' },
93 + })
94 + }
86 return axios.get(api, params) 95 return axios.get(api, params)
87 }, 96 },
88 /** 97 /**
...@@ -92,6 +101,14 @@ export const fetch = { ...@@ -92,6 +101,14 @@ export const fetch = {
92 * @returns {Promise<any>} axios Promise 101 * @returns {Promise<any>} axios Promise
93 */ 102 */
94 post: function (api, params) { 103 post: function (api, params) {
104 + if (shouldUseMock()) {
105 + return createAxiosMockResponse({
106 + url: api,
107 + method: 'POST',
108 + data: params,
109 + config: { url: api, method: 'post' },
110 + })
111 + }
95 return axios.post(api, params) 112 return axios.post(api, params)
96 }, 113 },
97 /** 114 /**
...@@ -101,6 +118,17 @@ export const fetch = { ...@@ -101,6 +118,17 @@ export const fetch = {
101 * @returns {Promise<any>} axios Promise 118 * @returns {Promise<any>} axios Promise
102 */ 119 */
103 stringifyPost: function (api, params) { 120 stringifyPost: function (api, params) {
121 + if (shouldUseMock()) {
122 + return createAxiosMockResponse({
123 + url: api,
124 + method: 'POST',
125 + data: qs.stringify(params),
126 + headers: {
127 + 'content-type': 'application/x-www-form-urlencoded'
128 + },
129 + config: { url: api, method: 'post' },
130 + })
131 + }
104 return axios.post(api, qs.stringify(params), { 132 return axios.post(api, qs.stringify(params), {
105 headers: { 133 headers: {
106 'content-type': 'application/x-www-form-urlencoded' 134 'content-type': 'application/x-www-form-urlencoded'
...@@ -115,6 +143,19 @@ export const fetch = { ...@@ -115,6 +143,19 @@ export const fetch = {
115 * @returns {Promise<any>} axios Promise 143 * @returns {Promise<any>} axios Promise
116 */ 144 */
117 basePost: function (url, data, config) { 145 basePost: function (url, data, config) {
146 + if (shouldUseMock()) {
147 + return createAxiosMockResponse({
148 + url,
149 + method: 'POST',
150 + data,
151 + headers: config?.headers || {},
152 + config: {
153 + url,
154 + method: 'post',
155 + ...(config || {}),
156 + },
157 + })
158 + }
118 return axios.post(url, data, config) 159 return axios.post(url, data, config)
119 } 160 }
120 } 161 }
......
1 +import { fn, fetch } from './fn'
2 +
3 +const Api = {
4 + MESSAGE_LIST: '/srv/?a=message&t=list',
5 + MESSAGE_DETAIL: '/srv/?a=message&t=detail',
6 +}
7 +
8 +export const messageListAPI = (params) => fn(fetch.get(Api.MESSAGE_LIST, { params }))
9 +
10 +export const messageDetailAPI = (params) => fn(fetch.get(Api.MESSAGE_DETAIL, { params }))
...@@ -3,6 +3,7 @@ export default { ...@@ -3,6 +3,7 @@ export default {
3 'pages/index/index', 3 'pages/index/index',
4 'pages/map-guide/index', 4 'pages/map-guide/index',
5 'pages/message/index', 5 'pages/message/index',
6 + 'pages/message-detail/index',
6 'pages/mine/index', 7 'pages/mine/index',
7 'pages/pay-test/index', 8 'pages/pay-test/index',
8 'pages/pay-bridge/index', 9 'pages/pay-bridge/index',
......
1 +# Mock 目录约定
2 +
3 +## 目标
4 +
5 +这一层的目标不是“写一个很大的假接口文件”,而是让本地在没有真实 API 时,也能按模块稳定联调,并且后续容易替换成真实接口。
6 +
7 +## 环境切换
8 +
9 +全局环境由配置文件控制,不在页面里单独切换:
10 +
11 +- 本地开发:`config/dev.js`
12 +- 生产构建:`config/prod.js`
13 +
14 +当前使用 `API_RUNTIME_ENV` 控制:
15 +
16 +- `mock`:当前构建下所有接口统一走本地 Mock
17 +- `production`:当前构建下所有接口统一走真实接口
18 +
19 +## 目录职责
20 +
21 +- `index.js`
22 + 统一入口,只做请求上下文创建、路由分发和响应包装。
23 +- `modules/*.mock.js`
24 + 按业务模块定义 handler。一个 handler 对应一个接口组合:`method + action + type`
25 +- `shared/`
26 + 放公共能力,例如请求解析、响应包装、handler 匹配。
27 +- `stores/*.store.js`
28 + 放有状态的 mock 数据,例如“已读/未读”“新增后列表变化”这类场景。
29 +- `fixtures/*.fixture.js`
30 + 放静态样本和数据工厂,不做业务状态修改。
31 +
32 +## 推荐写法
33 +
34 +### 1. 先放静态样本
35 +
36 +如果只是字段模板、列表假数据,先放到 `fixtures/xxx.fixture.js`
37 +
38 +### 2. 需要状态变化时再加 store
39 +
40 +如果接口会互相影响,例如:
41 +
42 +- 详情读取后列表变已读
43 +- 新增后列表里能看到新增项
44 +- 删除后再次查询不再返回
45 +
46 +这类场景再在 `stores/xxx.store.js` 里维护状态。
47 +
48 +### 3. handler 只做路由和调用
49 +
50 +`modules/*.mock.js` 里尽量保持轻量:
51 +
52 +- 判断命中哪个接口
53 +- 调用 store / fixture
54 +-`buildMockSuccess``buildMockError` 返回
55 +
56 +不要把大量假数据、复杂状态、通用工具都堆到 handler 里。
57 +
58 +## 新增一个模块时的顺序
59 +
60 +1. 新建 `fixtures/xxx.fixture.js`
61 +2. 如果需要状态,再新建 `stores/xxx.store.js`
62 +3. 新建 `modules/xxx.mock.js`
63 +4.`modules/index.js` 注册
64 +5.`src/api/` 下补对应 API 方法
65 +6. 在页面里直接按真实接口调用方式联调
66 +
67 +## handler 示例
68 +
69 +```js
70 +export const exampleMockHandlers = [
71 + {
72 + action: 'example',
73 + type: 'list',
74 + method: 'GET',
75 + handle: ({ requestParams }) => buildMockSuccess({
76 + list: [],
77 + page: Number(requestParams.page || 0),
78 + }),
79 + },
80 +]
81 +```
82 +
83 +## 维护原则
84 +
85 +- 模块边界优先,不要再回到“大一统 mock 文件”
86 +- 页面不关心 mock 细节,只调用正常的 `xxxAPI`
87 +- 能放 fixture 的,不要直接写进 handler
88 +- 能放 store 的状态,不要散在多个 handler 里各自维护
89 +- mock 字段名尽量贴近未来真实接口,避免后面页面再改一轮
1 +export const MESSAGE_STATUS = {
2 + unread: 'send',
3 + read: 'read',
4 +}
5 +
6 +export const MESSAGE_TITLE_POOL = [
7 + '法会安排更新通知',
8 + '预约审核结果提醒',
9 + '地图导览功能已开放',
10 + '支付状态同步通知',
11 + '活动报名成功提醒',
12 + '系统维护时间说明',
13 +]
14 +
15 +export const MESSAGE_CONTENT_POOL = [
16 + '这是一条用于前端联调的 Mock 消息。后续拿到真实接口后,可以直接对照字段结构替换。',
17 + '如果页面已经能完整跑通列表、详情和已读状态,后面只需要把接口 action 与返回字段切到真实后端即可。',
18 + '当前数据由本地 Mock 生成,适合在 API 尚未确定时提前联调页面交互和状态切换。',
19 +]
20 +
21 +export const createMessageFixtureList = (count = 12) => (
22 + Array.from({ length: count }).map((_, index) => {
23 + const id = `msg_${index + 1}`
24 + const createdAt = new Date(Date.now() - index * 1000 * 60 * 60 * 6)
25 +
26 + return {
27 + id,
28 + title: MESSAGE_TITLE_POOL[index % MESSAGE_TITLE_POOL.length],
29 + summary: `第 ${index + 1} 条消息摘要,可用于测试列表样式与详情跳转。`,
30 + content: `${MESSAGE_CONTENT_POOL[index % MESSAGE_CONTENT_POOL.length]}\n\n消息编号:${id}\n建议后续把这里替换成真实富文本或业务说明。`,
31 + status: index < 4 ? MESSAGE_STATUS.unread : MESSAGE_STATUS.read,
32 + category: index % 2 === 0 ? '系统通知' : '业务提醒',
33 + created_time: createdAt.toISOString().slice(0, 16).replace('T', ' '),
34 + }
35 + })
36 +)
1 +import { isMockEnabled } from '../utils/config'
2 +import { mockHandlers } from './modules'
3 +import { createMockRequestContext } from './shared/request'
4 +import { buildMockFallbackPayload, buildMockSuccess } from './shared/response'
5 +import { findMockHandler } from './shared/router'
6 +
7 +const MOCK_DELAY_MS = 160
8 +
9 +const mockDelay = (delay = MOCK_DELAY_MS) =>
10 + new Promise((resolve) => {
11 + setTimeout(resolve, delay)
12 + })
13 +
14 +const createMockBody = (context) => {
15 + const matchedHandler = findMockHandler(mockHandlers, context)
16 + if (matchedHandler) {
17 + return matchedHandler.handle(context)
18 + }
19 +
20 + return buildMockSuccess(buildMockFallbackPayload(context))
21 +}
22 +
23 +export const shouldUseMock = () => isMockEnabled()
24 +
25 +export const createAxiosMockResponse = async (options) => {
26 + const context = createMockRequestContext(options)
27 + await mockDelay()
28 +
29 + return {
30 + data: createMockBody(context),
31 + status: 200,
32 + statusText: 'OK',
33 + headers: {},
34 + config: options?.config || {},
35 + }
36 +}
37 +
38 +export const createTaroMockResponse = async (options) => {
39 + const context = createMockRequestContext(options)
40 + await mockDelay()
41 +
42 + const body = createMockBody(context)
43 + const mockCookie = `mock_sessionid=mock-${Date.now()}; Path=/; HttpOnly`
44 +
45 + return {
46 + data: body,
47 + statusCode: 200,
48 + header: {
49 + 'Set-Cookie': mockCookie,
50 + },
51 + cookies: [mockCookie],
52 + }
53 +}
1 +import { buildMockSuccess } from '../shared/response'
2 +
3 +export const authMockHandlers = [
4 + {
5 + action: 'openid',
6 + method: 'POST',
7 + handle: () => buildMockSuccess({
8 + session_status: 'ok',
9 + user_id: 'mock_user_001',
10 + nickname: '测试账号',
11 + }, '授权成功 (mock)'),
12 + },
13 +]
1 +import { buildMockSuccess } from '../shared/response'
2 +
3 +export const commonMockHandlers = [
4 + {
5 + action: 'wx_share',
6 + method: 'GET',
7 + handle: ({ requestParams }) => buildMockSuccess({
8 + appId: 'mock-app-id',
9 + timestamp: `${Math.floor(Date.now() / 1000)}`,
10 + nonceStr: `mock_share_${Date.now()}`,
11 + signature: 'mock-signature',
12 + jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'],
13 + url: requestParams?.url || '',
14 + }, '分享配置获取成功 (mock)'),
15 + },
16 + {
17 + action: 'sms',
18 + method: 'POST',
19 + handle: ({ requestData }) => buildMockSuccess({
20 + sent: true,
21 + phone: String(requestData?.phone || ''),
22 + sms_id: `mock_sms_${Date.now()}`,
23 + }, '验证码发送成功 (mock)'),
24 + },
25 + {
26 + action: 'upload',
27 + type: 'save_file',
28 + method: 'POST',
29 + handle: ({ requestData }) => buildMockSuccess({
30 + filekey: requestData?.filekey || `mock_file_${Date.now()}`,
31 + format: requestData?.format || 'jpg',
32 + url: `https://mock-assets.local/files/${requestData?.filekey || 'mock_file'}`,
33 + }, '保存成功 (mock)'),
34 + },
35 + {
36 + action: 'upload',
37 + method: 'POST',
38 + handle: () => buildMockSuccess({
39 + token: `mock_qiniu_token_${Date.now()}`,
40 + domain: 'https://mock-assets.local',
41 + upload_url: 'https://mock-assets.local/upload',
42 + }, '上传 token 获取成功 (mock)'),
43 + },
44 +]
1 +import { authMockHandlers } from './auth.mock'
2 +import { paymentMockHandlers } from './payment.mock'
3 +import { commonMockHandlers } from './common.mock'
4 +import { messageMockHandlers } from './message.mock'
5 +
6 +export const mockHandlers = [
7 + ...authMockHandlers,
8 + ...paymentMockHandlers,
9 + ...commonMockHandlers,
10 + ...messageMockHandlers,
11 +]
1 +import { getMessageDetail, getMessagePage } from '../stores/message.store'
2 +import { buildMockError, buildMockSuccess } from '../shared/response'
3 +
4 +export const messageMockHandlers = [
5 + {
6 + action: 'message',
7 + type: 'list',
8 + method: 'GET',
9 + handle: ({ requestParams }) => buildMockSuccess(getMessagePage({
10 + page: requestParams.page || 0,
11 + limit: requestParams.limit || 6,
12 + }), '消息列表获取成功 (mock)'),
13 + },
14 + {
15 + action: 'message',
16 + type: 'detail',
17 + method: 'GET',
18 + handle: ({ requestParams, requestData }) => {
19 + const detail = getMessageDetail(requestParams.id || requestData.id)
20 + if (!detail) {
21 + return buildMockError('消息不存在 (mock)')
22 + }
23 +
24 + return buildMockSuccess(detail, '消息详情获取成功 (mock)')
25 + },
26 + },
27 +]
1 +import { buildMockSuccess } from '../shared/response'
2 +
3 +const buildPaymentParams = (prefix, id) => {
4 + const normalized = String(id || 'mock-id')
5 + const seed = `${prefix}_${normalized}_${Date.now()}`
6 +
7 + return {
8 + timeStamp: `${Math.floor(Date.now() / 1000)}`,
9 + nonceStr: `mock_nonce_${seed}`,
10 + package: `prepay_id=${seed}`,
11 + signType: 'MD5',
12 + paySign: `mock_sign_${seed}`,
13 + }
14 +}
15 +
16 +export const paymentMockHandlers = [
17 + {
18 + action: 'pay',
19 + method: 'POST',
20 + handle: ({ requestData }) => buildMockSuccess({
21 + ...buildPaymentParams('order', requestData?.order_id),
22 + order_id: String(requestData?.order_id || 'mock_order_id'),
23 + }, '支付参数获取成功 (mock)'),
24 + },
25 + {
26 + action: 'icbc_pay_wxamp',
27 + method: 'POST',
28 + handle: ({ requestData }) => buildMockSuccess({
29 + ...buildPaymentParams('pay', requestData?.pay_id),
30 + pay_id: String(requestData?.pay_id || 'mock_pay_id'),
31 + }, '支付参数获取成功 (mock)'),
32 + },
33 +]
1 +import qs from 'qs'
2 +
3 +export const parseMockUrl = (input) => {
4 + const raw = String(input || '')
5 + const queryString = raw.includes('?') ? raw.slice(raw.indexOf('?') + 1) : ''
6 + const query = qs.parse(queryString)
7 +
8 + return {
9 + raw,
10 + action: String(query.a || ''),
11 + type: String(query.t || ''),
12 + query,
13 + }
14 +}
15 +
16 +export const normalizeMockData = (data, headers = {}) => {
17 + if (!data) return {}
18 +
19 + if (typeof data === 'string') {
20 + const contentType = String(headers?.['content-type'] || headers?.['Content-Type'] || '')
21 + if (contentType.includes('application/x-www-form-urlencoded')) {
22 + return qs.parse(data)
23 + }
24 +
25 + try {
26 + return JSON.parse(data)
27 + } catch (error) {
28 + return { raw: data }
29 + }
30 + }
31 +
32 + if (typeof data === 'object') {
33 + return data
34 + }
35 +
36 + return { raw: data }
37 +}
38 +
39 +export const createMockRequestContext = ({ url, method = 'GET', data, params, headers }) => {
40 + const urlParts = parseMockUrl(url)
41 +
42 + return {
43 + url: String(url || ''),
44 + method: String(method || 'GET').toUpperCase(),
45 + action: urlParts.action,
46 + type: urlParts.type,
47 + requestData: normalizeMockData(data, headers),
48 + requestParams: {
49 + ...(urlParts.query || {}),
50 + ...(params || {}),
51 + },
52 + }
53 +}
1 +export const buildMockSuccess = (data, msg = 'success (mock)') => ({
2 + code: 1,
3 + msg,
4 + data,
5 +})
6 +
7 +export const buildMockError = (msg = 'mock error', data = null, code = 0) => ({
8 + code,
9 + msg,
10 + data,
11 +})
12 +
13 +export const buildMockFallbackPayload = ({ action, type, method, requestData, requestParams }) => ({
14 + action,
15 + type,
16 + method,
17 + echo: {
18 + params: requestParams,
19 + data: requestData,
20 + },
21 + list: [],
22 + detail: {
23 + title: '请按真实接口结构替换这里的 mock 数据',
24 + updated_at: new Date().toISOString(),
25 + },
26 +})
1 +export const matchMockHandler = (handler, context) => {
2 + const matchedMethod = !handler.method || String(handler.method).toUpperCase() === context.method
3 + const matchedAction = !handler.action || String(handler.action) === context.action
4 + const matchedType = !handler.type || String(handler.type) === context.type
5 +
6 + return matchedMethod && matchedAction && matchedType
7 +}
8 +
9 +export const findMockHandler = (handlers, context) => (
10 + handlers.find((handler) => matchMockHandler(handler, context)) || null
11 +)
1 +import { createMessageFixtureList } from '../fixtures/message.fixture'
2 +
3 +let messageList = null
4 +
5 +export const ensureMessageStore = () => {
6 + if (!messageList) {
7 + messageList = createMessageFixtureList()
8 + }
9 +
10 + return messageList
11 +}
12 +
13 +export const getMessagePage = ({ page = 0, limit = 6 } = {}) => {
14 + const list = ensureMessageStore()
15 + const start = Number(page) * Number(limit)
16 + const pageList = list.slice(start, start + Number(limit)).map((item) => ({
17 + id: item.id,
18 + title: item.title,
19 + summary: item.summary,
20 + status: item.status,
21 + category: item.category,
22 + created_time: item.created_time,
23 + }))
24 +
25 + return {
26 + list: pageList,
27 + total: list.length,
28 + page: Number(page),
29 + limit: Number(limit),
30 + has_more: start + Number(limit) < list.length,
31 + }
32 +}
33 +
34 +export const getMessageDetail = (id) => {
35 + const list = ensureMessageStore()
36 + const current = list.find((item) => item.id === String(id || ''))
37 +
38 + if (!current) {
39 + return null
40 + }
41 +
42 + current.status = MESSAGE_STATUS.read
43 + return { ...current }
44 +}
1 +export default {
2 + navigationBarTitleText: '消息详情',
3 +}
1 +<template>
2 + <view class="message-detail-page">
3 + <view class="page-content">
4 + <view class="hero-card">
5 + <text class="hero-title">消息详情</text>
6 + <text class="hero-desc">
7 + 进入详情页后,当前消息会在 mock 中自动标记为已读,返回列表即可看到状态变化。
8 + </text>
9 + </view>
10 +
11 + <view v-if="loading" class="placeholder-card">
12 + <text class="section-title">加载中</text>
13 + <text class="section-desc">正在获取消息详情...</text>
14 + </view>
15 +
16 + <view v-else-if="detail" class="detail-card">
17 + <view class="detail-meta">
18 + <text class="detail-category">{{ detail.category }}</text>
19 + <text class="detail-time">{{ detail.created_time }}</text>
20 + </view>
21 + <text class="detail-title">{{ detail.title }}</text>
22 + <text class="detail-content">{{ detail.content }}</text>
23 + </view>
24 +
25 + <view v-else class="placeholder-card">
26 + <text class="section-title">未找到消息</text>
27 + <text class="section-desc">
28 + 当前消息不存在,或者真实接口还没有返回这条详情数据。
29 + </text>
30 + </view>
31 +
32 + <button class="back-btn" @tap="goBack">返回消息列表</button>
33 + </view>
34 + </view>
35 +</template>
36 +
37 +<script setup>
38 +import { ref } from 'vue'
39 +import Taro, { useLoad } from '@tarojs/taro'
40 +import { messageDetailAPI } from '@/api/message'
41 +
42 +const loading = ref(true)
43 +const detail = ref(null)
44 +
45 +const fetchMessageDetail = async (id) => {
46 + loading.value = true
47 +
48 + try {
49 + const response = await messageDetailAPI({ id })
50 + if (response?.code === 1) {
51 + detail.value = response.data
52 + return
53 + }
54 +
55 + detail.value = null
56 + Taro.showToast({
57 + title: response?.msg || '获取详情失败',
58 + icon: 'none',
59 + })
60 + } catch (error) {
61 + console.error('获取消息详情失败:', error)
62 + detail.value = null
63 + } finally {
64 + loading.value = false
65 + }
66 +}
67 +
68 +const goBack = () => {
69 + Taro.navigateBack()
70 +}
71 +
72 +useLoad((options) => {
73 + const id = String(options?.id || '').trim()
74 + if (!id) {
75 + loading.value = false
76 + return
77 + }
78 +
79 + fetchMessageDetail(id)
80 +})
81 +</script>
82 +
83 +<style lang="less">
84 +.message-detail-page {
85 + min-height: 100vh;
86 + background:
87 + radial-gradient(circle at top right, rgba(166, 121, 57, 0.16), transparent 30%),
88 + linear-gradient(180deg, #fffaf4 0%, #f4f6fb 100%);
89 +
90 + .page-content {
91 + padding: 32rpx 24rpx 48rpx;
92 + box-sizing: border-box;
93 + }
94 +
95 + .hero-card,
96 + .detail-card,
97 + .placeholder-card {
98 + padding: 32rpx;
99 + border-radius: 28rpx;
100 + background: rgba(255, 255, 255, 0.94);
101 + border: 2rpx solid rgba(166, 121, 57, 0.08);
102 + box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
103 + box-sizing: border-box;
104 + }
105 +
106 + .hero-title,
107 + .section-title,
108 + .detail-title {
109 + display: block;
110 + color: #111827;
111 + }
112 +
113 + .hero-title,
114 + .detail-title {
115 + font-size: 40rpx;
116 + font-weight: 700;
117 + }
118 +
119 + .hero-desc,
120 + .section-desc,
121 + .detail-content {
122 + display: block;
123 + margin-top: 16rpx;
124 + font-size: 26rpx;
125 + line-height: 1.8;
126 + color: #6b7280;
127 + white-space: pre-wrap;
128 + }
129 +
130 + .detail-card,
131 + .placeholder-card,
132 + .back-btn {
133 + margin-top: 24rpx;
134 + }
135 +
136 + .detail-meta {
137 + display: flex;
138 + align-items: center;
139 + gap: 16rpx;
140 + margin-bottom: 18rpx;
141 + }
142 +
143 + .detail-category {
144 + padding: 8rpx 14rpx;
145 + border-radius: 999rpx;
146 + font-size: 22rpx;
147 + color: #92400e;
148 + background: #fef3c7;
149 + }
150 +
151 + .detail-time {
152 + font-size: 22rpx;
153 + color: #9ca3af;
154 + }
155 +
156 + .back-btn {
157 + border-radius: 999rpx;
158 + font-size: 28rpx;
159 + line-height: 84rpx;
160 + color: #0f172a;
161 + background: #ffffff;
162 + border: 2rpx solid #d1d5db;
163 + }
164 +}
165 +</style>
...@@ -2,18 +2,70 @@ ...@@ -2,18 +2,70 @@
2 <view class="message-page"> 2 <view class="message-page">
3 <view class="page-content"> 3 <view class="page-content">
4 <view class="hero-card"> 4 <view class="hero-card">
5 - <text class="hero-title">消息</text> 5 + <view class="hero-head">
6 + <text class="hero-title">消息</text>
7 + <text class="env-tag" :class="{ mock: current_env_use_mock }">
8 + {{ current_env_label }}
9 + </text>
10 + </view>
6 <text class="hero-desc"> 11 <text class="hero-desc">
7 - 这里预留给系统通知、预约提醒和支付结果消息。当前先完成 Tab 栏结构,后续再接真实消息数据 12 + 当前环境由配置文件统一控制。本地如果要让所有接口都走 mock,只需要改 `config/dev.js` 里的 `API_RUNTIME_ENV`
8 </text> 13 </text>
9 </view> 14 </view>
10 15
11 - <view class="placeholder-card"> 16 + <view class="toolbar-card">
12 - <text class="section-title">当前状态</text> 17 + <view>
18 + <text class="section-title">列表状态</text>
19 + <text class="section-desc">
20 + 共 {{ total }} 条消息,未读 {{ unread_count }} 条。进入详情页后,当前消息会在 mock 中自动变成已读。
21 + </text>
22 + </view>
23 + <button class="refresh-btn" :loading="loading" @tap="handleRefresh">
24 + 刷新
25 + </button>
26 + </view>
27 +
28 + <view v-if="loading && !message_list.length" class="placeholder-card">
29 + <text class="section-title">加载中</text>
13 <text class="section-desc"> 30 <text class="section-desc">
14 - 暂无消息内容,后续可在这里接接口列表、未读计数和消息详情跳转。 31 + 正在拉取消息列表...
15 </text> 32 </text>
16 </view> 33 </view>
34 +
35 + <view v-else-if="!message_list.length" class="placeholder-card">
36 + <text class="section-title">暂无消息</text>
37 + <text class="section-desc">
38 + 当前接口还没有返回消息内容,可以先在 mock 里补结构,再回到这个页面验证展示效果。
39 + </text>
40 + </view>
41 +
42 + <view
43 + v-for="item in message_list"
44 + :key="item.id"
45 + class="message-card"
46 + @tap="goToDetail(item.id)"
47 + >
48 + <view class="message-top">
49 + <view class="message-meta">
50 + <text class="message-category">{{ item.category }}</text>
51 + <text class="message-time">{{ item.created_time }}</text>
52 + </view>
53 + <text class="message-status" :class="{ unread: item.status === 'send' }">
54 + {{ item.status === 'send' ? '未读' : '已读' }}
55 + </text>
56 + </view>
57 + <text class="message-title">{{ item.title }}</text>
58 + <text class="message-summary">{{ item.summary }}</text>
59 + </view>
60 +
61 + <button
62 + v-if="has_more && message_list.length"
63 + class="load-more-btn"
64 + :loading="loading_more"
65 + @tap="handleLoadMore"
66 + >
67 + 加载更多
68 + </button>
17 </view> 69 </view>
18 70
19 <AppTabbar current="message" /> 71 <AppTabbar current="message" />
...@@ -21,7 +73,86 @@ ...@@ -21,7 +73,86 @@
21 </template> 73 </template>
22 74
23 <script setup> 75 <script setup>
76 +import { computed, ref } from 'vue'
77 +import Taro, { useDidShow, useLoad } from '@tarojs/taro'
24 import AppTabbar from '@/components/AppTabbar.vue' 78 import AppTabbar from '@/components/AppTabbar.vue'
79 +import { messageListAPI } from '@/api/message'
80 +import { getCurrentApiConfig } from '@/utils/config'
81 +
82 +const api_config = getCurrentApiConfig()
83 +const current_env_label = api_config.label
84 +const current_env_use_mock = api_config.useMock
85 +const message_list = ref([])
86 +const page = ref(0)
87 +const page_size = 6
88 +const total = ref(0)
89 +const has_more = ref(true)
90 +const loading = ref(false)
91 +const loading_more = ref(false)
92 +const has_loaded_once = ref(false)
93 +
94 +const unread_count = computed(() => (
95 + message_list.value.filter((item) => item.status === 'send').length
96 +))
97 +
98 +const fetchMessageList = async (nextPage = 0, append = false) => {
99 + if (append) {
100 + loading_more.value = true
101 + } else {
102 + loading.value = true
103 + }
104 +
105 + try {
106 + const response = await messageListAPI({
107 + page: nextPage,
108 + limit: page_size,
109 + })
110 +
111 + if (response?.code !== 1) {
112 + Taro.showToast({
113 + title: response?.msg || '获取消息失败',
114 + icon: 'none',
115 + })
116 + return
117 + }
118 +
119 + const list = response?.data?.list || []
120 + message_list.value = append ? [...message_list.value, ...list] : list
121 + total.value = Number(response?.data?.total || list.length)
122 + has_more.value = !!response?.data?.has_more
123 + page.value = nextPage
124 + } catch (error) {
125 + console.error('获取消息列表失败:', error)
126 + } finally {
127 + loading.value = false
128 + loading_more.value = false
129 + }
130 +}
131 +
132 +const handleRefresh = async () => {
133 + await fetchMessageList(0, false)
134 +}
135 +
136 +const handleLoadMore = async () => {
137 + if (!has_more.value || loading_more.value) return
138 + await fetchMessageList(page.value + 1, true)
139 +}
140 +
141 +const goToDetail = (id) => {
142 + Taro.navigateTo({
143 + url: `/pages/message-detail/index?id=${encodeURIComponent(id)}`,
144 + })
145 +}
146 +
147 +useLoad(async () => {
148 + await fetchMessageList(0, false)
149 + has_loaded_once.value = true
150 +})
151 +
152 +useDidShow(async () => {
153 + if (!has_loaded_once.value) return
154 + await fetchMessageList(0, false)
155 +})
25 </script> 156 </script>
26 157
27 <style lang="less"> 158 <style lang="less">
...@@ -37,6 +168,8 @@ import AppTabbar from '@/components/AppTabbar.vue' ...@@ -37,6 +168,8 @@ import AppTabbar from '@/components/AppTabbar.vue'
37 } 168 }
38 169
39 .hero-card, 170 .hero-card,
171 + .toolbar-card,
172 + .message-card,
40 .placeholder-card { 173 .placeholder-card {
41 padding: 32rpx; 174 padding: 32rpx;
42 border-radius: 28rpx; 175 border-radius: 28rpx;
...@@ -46,6 +179,20 @@ import AppTabbar from '@/components/AppTabbar.vue' ...@@ -46,6 +179,20 @@ import AppTabbar from '@/components/AppTabbar.vue'
46 box-sizing: border-box; 179 box-sizing: border-box;
47 } 180 }
48 181
182 + .hero-head,
183 + .toolbar-card,
184 + .message-top,
185 + .message-meta {
186 + display: flex;
187 + align-items: center;
188 + }
189 +
190 + .hero-head,
191 + .toolbar-card,
192 + .message-top {
193 + justify-content: space-between;
194 + }
195 +
49 .hero-title, 196 .hero-title,
50 .section-title { 197 .section-title {
51 display: block; 198 display: block;
...@@ -67,8 +214,94 @@ import AppTabbar from '@/components/AppTabbar.vue' ...@@ -67,8 +214,94 @@ import AppTabbar from '@/components/AppTabbar.vue'
67 margin-top: 24rpx; 214 margin-top: 24rpx;
68 } 215 }
69 216
217 + .toolbar-card,
218 + .message-card {
219 + margin-top: 24rpx;
220 + }
221 +
70 .section-title { 222 .section-title {
71 font-size: 30rpx; 223 font-size: 30rpx;
72 } 224 }
225 +
226 + .env-tag {
227 + padding: 10rpx 18rpx;
228 + border-radius: 999rpx;
229 + font-size: 22rpx;
230 + color: #1d4ed8;
231 + background: #dbeafe;
232 + }
233 +
234 + .env-tag.mock {
235 + color: #166534;
236 + background: #dcfce7;
237 + }
238 +
239 + .refresh-btn,
240 + .load-more-btn {
241 + border-radius: 999rpx;
242 + font-size: 26rpx;
243 + line-height: 80rpx;
244 + }
245 +
246 + .refresh-btn {
247 + min-width: 180rpx;
248 + color: #0f172a;
249 + background: #fff;
250 + border: 2rpx solid #d1d5db;
251 + }
252 +
253 + .message-card {
254 + display: flex;
255 + flex-direction: column;
256 + gap: 18rpx;
257 + }
258 +
259 + .message-meta {
260 + gap: 14rpx;
261 + }
262 +
263 + .message-category,
264 + .message-time,
265 + .message-status {
266 + font-size: 22rpx;
267 + }
268 +
269 + .message-category {
270 + padding: 8rpx 14rpx;
271 + border-radius: 999rpx;
272 + color: #92400e;
273 + background: #fef3c7;
274 + }
275 +
276 + .message-time {
277 + color: #9ca3af;
278 + }
279 +
280 + .message-status {
281 + color: #94a3b8;
282 + }
283 +
284 + .message-status.unread {
285 + color: #dc2626;
286 + }
287 +
288 + .message-title {
289 + font-size: 32rpx;
290 + font-weight: 700;
291 + color: #111827;
292 + }
293 +
294 + .message-summary {
295 + font-size: 25rpx;
296 + line-height: 1.7;
297 + color: #6b7280;
298 + }
299 +
300 + .load-more-btn {
301 + margin-top: 24rpx;
302 + color: #0f172a;
303 + background: #ffffff;
304 + border: 2rpx solid #d1d5db;
305 + }
73 } 306 }
74 </style> 307 </style>
......
1 import Taro from '@tarojs/taro' 1 import Taro from '@tarojs/taro'
2 import { routerStore } from '@/stores/router' 2 import { routerStore } from '@/stores/router'
3 import { buildApiUrl } from './tools' 3 import { buildApiUrl } from './tools'
4 +import { createTaroMockResponse, shouldUseMock } from '../mock'
4 5
5 // 改进:添加全局状态变量注释 6 // 改进:添加全局状态变量注释
6 /** 7 /**
...@@ -124,11 +125,18 @@ export const refreshSession = async (options) => { ...@@ -124,11 +125,18 @@ export const refreshSession = async (options) => {
124 } 125 }
125 126
126 // 换取后端会话(服务端通过 Set-Cookie 返回会话信息) 127 // 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
127 - const response = await Taro.request({ 128 + const auth_url = buildApiUrl('openid')
128 - url: buildApiUrl('openid'), 129 + const response = shouldUseMock()
129 - method: 'POST', 130 + ? await createTaroMockResponse({
130 - data: request_data, 131 + url: auth_url,
131 - }) 132 + method: 'POST',
133 + data: request_data,
134 + })
135 + : await Taro.request({
136 + url: auth_url,
137 + method: 'POST',
138 + data: request_data,
139 + })
132 140
133 if (!response?.data || response.data.code !== 1) { 141 if (!response?.data || response.data.code !== 1) {
134 throw new Error(response?.data?.msg || '授权失败') 142 throw new Error(response?.data?.msg || '授权失败')
......
1 /* 1 /*
2 - * @Description: 服务器环境配置 2 + * @Description: API 环境配置
3 - * @Note: 对齐 meihuaApp 的最小授权/支付测试环境 3 + * @Note: 当前环境只由构建配置控制;本地开发默认 mock,生产构建默认正式环境
4 */ 4 */
5 5
6 -/** 6 +export const API_ENVIRONMENTS = {
7 - * @description 接口基础域名 7 + production: {
8 - * - 授权与支付测试均复用 meihuaApp 所在后端 8 + key: 'production',
9 - * @type {string} 9 + label: '正式环境',
10 - */ 10 + baseURL: 'https://oa.onwall.cn',
11 -const BASE_URL = 'https://oa.onwall.cn' 11 + requestDefaultParams: {
12 + f: 'room',
13 + client_id: '772428',
14 + },
15 + useMock: false,
16 + },
17 + mock: {
18 + key: 'mock',
19 + label: '本地 Mock 环境',
20 + baseURL: 'https://oa.onwall.cn',
21 + requestDefaultParams: {
22 + f: 'room',
23 + client_id: '772428',
24 + },
25 + useMock: true,
26 + },
27 +}
12 28
13 -/** 29 +export const DEFAULT_API_ENV = process.env.NODE_ENV === 'production' ? 'production' : 'mock'
14 - * @description 接口默认公共参数 30 +
15 - * - f/client_id 与 meihuaApp 保持一致,确保 openid 与 pay 接口可用 31 +export const getApiEnv = () => {
16 - */ 32 + const runtimeEnv = process.env.API_RUNTIME_ENV
17 -export const REQUEST_DEFAULT_PARAMS = { 33 + if (runtimeEnv && API_ENVIRONMENTS[runtimeEnv]) {
18 - f: 'room', 34 + return runtimeEnv
19 - client_id: '772428', 35 + }
36 +
37 + return DEFAULT_API_ENV
38 +}
39 +
40 +export const getCurrentApiConfig = () => {
41 + const env = getApiEnv()
42 + return API_ENVIRONMENTS[env] || API_ENVIRONMENTS[DEFAULT_API_ENV]
20 } 43 }
21 44
45 +export const getBaseUrl = () => getCurrentApiConfig().baseURL
46 +
47 +export const getRequestDefaultParams = () => ({ ...getCurrentApiConfig().requestDefaultParams })
48 +
49 +export const isMockEnabled = () => !!getCurrentApiConfig().useMock
50 +
51 +export const getApiEnvLabel = () => getCurrentApiConfig().label
52 +
53 +const BASE_URL = getBaseUrl()
54 +
55 +export const REQUEST_DEFAULT_PARAMS = getRequestDefaultParams()
56 +
22 export default BASE_URL 57 export default BASE_URL
......
...@@ -17,7 +17,7 @@ import { parseQueryString } from './tools' ...@@ -17,7 +17,7 @@ import { parseQueryString } from './tools'
17 // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress'; 17 // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
18 // import store from '@/store' 18 // import store from '@/store'
19 // import { getToken } from '@/utils/auth' 19 // import { getToken } from '@/utils/auth'
20 -import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'; 20 +import { getBaseUrl, getRequestDefaultParams } from './config';
21 21
22 /** 22 /**
23 * @description 获取 sessionid 的工具函数 23 * @description 获取 sessionid 的工具函数
...@@ -73,7 +73,7 @@ export const clearSessionId = () => { ...@@ -73,7 +73,7 @@ export const clearSessionId = () => {
73 * - 通过拦截器处理:默认参数、cookie 注入、401 自动续期、弱网降级 73 * - 通过拦截器处理:默认参数、cookie 注入、401 自动续期、弱网降级
74 */ 74 */
75 const service = axios.create({ 75 const service = axios.create({
76 - baseURL: BASE_URL, // url = base url + request url 76 + baseURL: getBaseUrl(), // url = base url + request url
77 // withCredentials: true, // send cookies when cross-domain requests 77 // withCredentials: true, // send cookies when cross-domain requests
78 timeout: 5000, // request timeout 78 timeout: 5000, // request timeout
79 }) 79 })
...@@ -153,6 +153,8 @@ const handle_request_timeout = async () => { ...@@ -153,6 +153,8 @@ const handle_request_timeout = async () => {
153 // 请求拦截器:合并默认参数 / 注入 cookie 153 // 请求拦截器:合并默认参数 / 注入 cookie
154 service.interceptors.request.use( 154 service.interceptors.request.use(
155 config => { 155 config => {
156 + config.baseURL = getBaseUrl()
157 +
156 // console.warn(config) 158 // console.warn(config)
157 // console.warn(store) 159 // console.warn(store)
158 160
...@@ -166,7 +168,7 @@ service.interceptors.request.use( ...@@ -166,7 +168,7 @@ service.interceptors.request.use(
166 168
167 // 优先级:调用传参 > URL参数 > 默认参数 169 // 优先级:调用传参 > URL参数 > 默认参数
168 config.params = { 170 config.params = {
169 - ...REQUEST_DEFAULT_PARAMS, 171 + ...getRequestDefaultParams(),
170 ...url_params, 172 ...url_params,
171 ...(config.params || {}) 173 ...(config.params || {})
172 } 174 }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 */ 7 */
8 import dayjs from 'dayjs'; 8 import dayjs from 'dayjs';
9 import Taro from '@tarojs/taro'; 9 import Taro from '@tarojs/taro';
10 -import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config' 10 +import { getBaseUrl, getRequestDefaultParams } from './config'
11 11
12 /** 12 /**
13 * @description 格式化时间 13 * @description 格式化时间
...@@ -191,11 +191,11 @@ const get_bill_status_text = (status) => { ...@@ -191,11 +191,11 @@ const get_bill_status_text = (status) => {
191 const buildApiUrl = (action, params = {}) => { 191 const buildApiUrl = (action, params = {}) => {
192 const base_params = { 192 const base_params = {
193 a: action, 193 a: action,
194 - ...REQUEST_DEFAULT_PARAMS, 194 + ...getRequestDefaultParams(),
195 ...params, 195 ...params,
196 } 196 }
197 const queryParams = new URLSearchParams(base_params) 197 const queryParams = new URLSearchParams(base_params)
198 - return `${BASE_URL}/srv/?${queryParams.toString()}` 198 + return `${getBaseUrl()}/srv/?${queryParams.toString()}`
199 } 199 }
200 200
201 export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, mask_id_number, get_qrcode_status_text, get_bill_status_text, buildApiUrl }; 201 export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, mask_id_number, get_qrcode_status_text, get_bill_status_text, buildApiUrl };
......