feat(mock): 新增消息模块及本地 mock 环境支持
- 新增消息详情页面及路由配置 - 重构环境配置,支持通过 API_RUNTIME_ENV 统一切换正式/mock 环境 - 添加完整的 mock 架构,包含模块化 handler、状态管理 store 和静态 fixture - 集成 mock 到现有请求层,实现 axios 和 Taro 请求的无缝拦截 - 更新项目文档,说明 mock 目录约定和环境切换方式
Showing
26 changed files
with
1074 additions
and
43 deletions
| ... | @@ -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 进行状态管理 | ... | ... |
| ... | @@ -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 | } | ... | ... |
src/api/message.js
0 → 100644
| 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', | ... | ... |
src/mock/README.md
0 → 100644
| 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 字段名尽量贴近未来真实接口,避免后面页面再改一轮 |
src/mock/fixtures/message.fixture.js
0 → 100644
| 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 | +) |
src/mock/index.js
0 → 100644
| 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 | +} |
src/mock/modules/auth.mock.js
0 → 100644
src/mock/modules/common.mock.js
0 → 100644
| 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 | +] |
src/mock/modules/index.js
0 → 100644
| 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 | +] |
src/mock/modules/message.mock.js
0 → 100644
| 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 | +] |
src/mock/modules/payment.mock.js
0 → 100644
| 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 | +] |
src/mock/shared/request.js
0 → 100644
| 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 | +} |
src/mock/shared/response.js
0 → 100644
| 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 | +}) |
src/mock/shared/router.js
0 → 100644
| 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 | +) |
src/mock/stores/message.store.js
0 → 100644
| 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 | +} |
src/pages/message-detail/index.config.js
0 → 100644
src/pages/message-detail/index.vue
0 → 100644
| 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 }; | ... | ... |
-
Please register or login to post a comment