minitest2.0 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.editorconfig +8 -0
  2. package/.env.development +3 -0
  3. package/.env.production +3 -0
  4. package/.env.test +3 -0
  5. package/.gitattributes +1 -0
  6. package/.oxlintrc.json +10 -0
  7. package/.prettierrc.json +6 -0
  8. package/.vscode/extensions.json +9 -0
  9. package/.vscode/settings.json +13 -0
  10. package/README.md +179 -0
  11. package/auto-imports.d.ts +629 -0
  12. package/components.d.ts +21 -0
  13. package/design-qa.md +36 -0
  14. package/docs/MX_API.md +244 -0
  15. package/docs/REQUEST.md +217 -0
  16. package/docs/jsapi.js +515 -0
  17. package/docs/package-json-guide.md +132 -0
  18. package/env.d.ts +15 -0
  19. package/eslint.config.ts +26 -0
  20. package/index.html +16 -0
  21. package/package.json +83 -0
  22. package/plugins/bump-version.ts +61 -0
  23. package/postcss.config.ts +15 -0
  24. package/public/favicon.ico +0 -0
  25. package/public/images/12a73787-86a9-4891-a65f-66104746f6a8.png +0 -0
  26. package/public/images/5798d7aa-ba8b-4605-8079-58b35495ac55.png +0 -0
  27. package/public/images/73fef1e4-0fd0-4a1a-9b8b-a70a5b6acbbc.png +0 -0
  28. package/public/images/bc685b4c-0cca-4a79-924c-a8ee10e6f8eb.png +0 -0
  29. package/public/images/c3dbbd9d-be56-490e-b9f4-6ee17ebefffc.png +0 -0
  30. package/public/images/ea745a10-42aa-4f44-8d7f-3ab02cc0adcd.png +0 -0
  31. package/public/images/f5876785-b927-4347-ba19-999114240649.png +0 -0
  32. package/public/images/img.png +0 -0
  33. package/public/images/opening-reserve-estimate.png +0 -0
  34. package/public/images/position-estimate-report.png +0 -0
  35. package/src/App.vue +131 -0
  36. package/src/api/announcement.ts +405 -0
  37. package/src/api/health.ts +13 -0
  38. package/src/api/pbc-position.ts +178 -0
  39. package/src/api/rmb-position.ts +233 -0
  40. package/src/api/tam.ts +173 -0
  41. package/src/api/user.ts +92 -0
  42. package/src/auto-imports.d.ts +633 -0
  43. package/src/components/AppTitleBar.vue +376 -0
  44. package/src/components.d.ts +33 -0
  45. package/src/config/config.properties +3 -0
  46. package/src/config/env.ts +6 -0
  47. package/src/config/plugin.properties.pro +6 -0
  48. package/src/config/plugin.properties.test +6 -0
  49. package/src/core/mxApi/index.ts +451 -0
  50. package/src/core/request/index.ts +135 -0
  51. package/src/main.ts +40 -0
  52. package/src/router/index.ts +144 -0
  53. package/src/stores/app.ts +103 -0
  54. package/src/stores/counter.ts +12 -0
  55. package/src/stores/user.ts +137 -0
  56. package/src/styles/nprogress.css +22 -0
  57. package/src/styles/vant-overrides.css +42 -0
  58. package/src/types/api.d.ts +14 -0
  59. package/src/types/nprogress.d.ts +8 -0
  60. package/src/utils/auth-token.ts +241 -0
  61. package/src/utils/code-highlight.ts +165 -0
  62. package/src/utils/copy.ts +36 -0
  63. package/src/utils/query.ts +27 -0
  64. package/src/utils/request.ts +238 -0
  65. package/src/utils/rmb-forecast.ts +84 -0
  66. package/src/utils/toast.ts +61 -0
  67. package/src/views/article-detail/index.vue +289 -0
  68. package/src/views/article-edit/index.vue +600 -0
  69. package/src/views/articles/index.vue +293 -0
  70. package/src/views/clearing-detail/index.vue +26 -0
  71. package/src/views/dashboard/index.vue +18 -0
  72. package/src/views/foreign-position/index.vue +26 -0
  73. package/src/views/home/index.vue +213 -0
  74. package/src/views/login/index.vue +275 -0
  75. package/src/views/mine/index.vue +147 -0
  76. package/src/views/net-debit/index.vue +26 -0
  77. package/src/views/opening-reserve-estimate/index.vue +28 -0
  78. package/src/views/pbc-position/index.vue +357 -0
  79. package/src/views/position-estimate/index.vue +67 -0
  80. package/src/views/position-estimate-report/index.vue +28 -0
  81. package/src/views/rmb-position/index.vue +1013 -0
  82. package/src/views/rmb-position-create/index.vue +221 -0
  83. package/src/views/rmb-position-detail/index.vue +355 -0
  84. package/src/views/settings/index.vue +67 -0
  85. package/src/views/warning/index.vue +26 -0
  86. package/tsconfig.app.json +18 -0
  87. package/tsconfig.json +11 -0
  88. package/tsconfig.node.json +28 -0
  89. package/vite.config.ts +100 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * 敏行 AJAX 支持的 HTTP 请求类型。
3
+ *
4
+ * 该枚举范围来自 MXCommon.ajax 文档;未包含 PATCH,
5
+ * 调用方如需 PATCH 应在业务请求中提前处理。
6
+ */
7
+ export type MXAjaxType = 'GET' | 'POST' | 'PUT' | 'DELETE'
8
+
9
+ /**
10
+ * MXCommon.ajax 入参。
11
+ *
12
+ * 这里只保留敏行 AJAX 文档中明确支持的公共字段:
13
+ * - type: 请求方式,默认 GET
14
+ * - url: 请求地址
15
+ * - dataType: 返回数据格式
16
+ * - async: 是否异步请求,默认 true
17
+ * - data: 请求数据,兼容实际宿主能力透传
18
+ * - headers: 请求头,兼容实际宿主能力透传
19
+ * - success/error/complete: 宿主回调
20
+ */
21
+ export interface MXAjaxParams {
22
+ /** 请求方式,未传时按敏行文档默认 GET。 */
23
+ type?: MXAjaxType
24
+ /** 请求地址,可以是完整 URL,也可以由调用方传入业务接口路径。 */
25
+ url: string
26
+ /** 返回数据格式,例如 text/json,具体支持范围以敏行宿主为准。 */
27
+ dataType?: string
28
+ /** 是否异步请求,未传时默认 true。 */
29
+ async?: boolean
30
+ /** 请求数据,原样透传给敏行宿主。 */
31
+ data?: unknown
32
+ /** 请求头,原样透传给敏行宿主。 */
33
+ headers?: Record<string, string>
34
+ /** 请求成功回调,收到的是宿主原始返回值。 */
35
+ success?: (data: unknown) => void
36
+ /** 请求失败回调,收到的是宿主原始错误值。 */
37
+ error?: (data: unknown) => void
38
+ /** 请求完成回调,无论成功或失败都由宿主触发。 */
39
+ complete?: () => void
40
+ }
41
+
42
+ /**
43
+ * Promise 化后的 AJAX 返回结构。
44
+ *
45
+ * 敏行宿主以回调返回原始数据;封装层会尝试 JSON.parse,
46
+ * 成功则返回解析后的对象,失败则保留原始数据。
47
+ */
48
+ export interface MXAjaxResponse<T = unknown> {
49
+ data: T
50
+ }
51
+
52
+ /**
53
+ * 获取 APP token 成功回调。
54
+ */
55
+ export type MXAppTokenSuccess = (token: string) => void
56
+
57
+ /**
58
+ * 获取 APP token 失败回调。
59
+ */
60
+ export type MXAppTokenError = (error: unknown) => void
61
+
62
+ /**
63
+ * MXCommon.getEncryptString 入参。
64
+ */
65
+ interface MXGetEncryptStringOptions {
66
+ onSuccess: MXAppTokenSuccess
67
+ onError?: MXAppTokenError
68
+ }
69
+
70
+ /**
71
+ * MXCommon 宿主能力。
72
+ */
73
+ interface MXCommonHost {
74
+ ajax?: (options: MXAjaxParams) => void
75
+ getEncryptString?: (options: MXGetEncryptStringOptions) => void
76
+ }
77
+
78
+ /**
79
+ * NXCommon 宿主能力。
80
+ *
81
+ * 当前只用于 AJAX 兜底,不用于获取 APP token。
82
+ */
83
+ interface NXCommonHost {
84
+ ajax?: (options: MXAjaxParams) => void
85
+ }
86
+
87
+ export type MXWebuiSideSlot = {
88
+ default?: boolean
89
+ hidden?: boolean
90
+ disable?: boolean
91
+ content?: string | 'default'
92
+ action?: (() => void) | 'default' | 'custom' | 'native' | 'both'
93
+ preventDefault?: boolean
94
+ }
95
+
96
+ export type MXWebuiTitleSlot = {
97
+ default?: boolean
98
+ hidden?: boolean
99
+ title?: string
100
+ subtitle?: string
101
+ }
102
+
103
+ export type MXWebuiInteractiveSlot = {
104
+ default?: boolean
105
+ hidden?: boolean
106
+ title: string[]
107
+ action?: (idx: number) => void
108
+ active?: number
109
+ }
110
+
111
+ export type MXWebuiHeaderSlot = {
112
+ left?: MXWebuiSideSlot
113
+ left_1?: MXWebuiSideSlot
114
+ left_2?: MXWebuiSideSlot
115
+ middle?: MXWebuiTitleSlot | MXWebuiInteractiveSlot
116
+ right_1?: MXWebuiSideSlot
117
+ right_2?: MXWebuiSideSlot
118
+ }
119
+
120
+ export type MXWebuiCustomHeaderOptions = {
121
+ slot: MXWebuiHeaderSlot
122
+ onSuccess?: () => void
123
+ onFail?: (error: unknown) => void
124
+ }
125
+
126
+ interface MXWebuiHost {
127
+ hideWebViewTitle?: () => void
128
+ showWebViewTitle?: (title?: string) => void
129
+ setWebViewTitle?: (title: string) => void
130
+ setNavBgColor?: (bgColor: string) => void
131
+ closeWindow?: () => void
132
+ setCustomHeader?: (options: MXWebuiCustomHeaderOptions) => void
133
+ updateCustomHeader?: (options: MXWebuiCustomHeaderOptions) => void
134
+ }
135
+
136
+ const DEFAULT_NATIVE_TIMEOUT = 5000
137
+ const NATIVE_POLL_INTERVAL = 100
138
+
139
+ declare global {
140
+ /** 敏行旧宿主命名空间。 */
141
+ const MXCommon: MXCommonHost | undefined
142
+ /** 敏行新宿主命名空间,部分客户端使用 NXCommon。 */
143
+ const NXCommon: NXCommonHost | undefined
144
+ /** 敏行原生 UI 宿主命名空间。 */
145
+ const MXWebui: MXWebuiHost | undefined
146
+
147
+ interface Window {
148
+ /** 敏行旧宿主命名空间。 */
149
+ MXCommon?: MXCommonHost
150
+ /** 敏行新宿主命名空间,部分客户端使用 NXCommon。 */
151
+ NXCommon?: NXCommonHost
152
+ /** 敏行原生 UI 宿主命名空间。 */
153
+ MXWebui?: MXWebuiHost
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 获取当前可用的敏行公共宿主对象。
159
+ *
160
+ * 优先使用 MXCommon;如果客户端只注入 NXCommon,则回退到 NXCommon。
161
+ * 使用 typeof 判断是为了避免全局变量未注入时产生运行时错误。
162
+ */
163
+ const getHostCommon = () => {
164
+ return getMXCommon() || getNXCommon()
165
+ }
166
+
167
+ const getMXCommon = () => (typeof MXCommon !== 'undefined' ? MXCommon : undefined)
168
+
169
+ const getNXCommon = () => (typeof NXCommon !== 'undefined' ? NXCommon : undefined)
170
+
171
+ const getMXWebui = () => (typeof MXWebui !== 'undefined' ? MXWebui : undefined)
172
+
173
+ const getNativeSnapshot = () => ({
174
+ 是否存在MXCommon: typeof MXCommon !== 'undefined',
175
+ 是否存在NXCommon: typeof NXCommon !== 'undefined',
176
+ 是否存在MXWebui: typeof MXWebui !== 'undefined',
177
+ 是否存在MXCommonAjax: typeof MXCommon !== 'undefined' && typeof MXCommon.ajax === 'function',
178
+ 是否存在NXCommonAjax: typeof NXCommon !== 'undefined' && typeof NXCommon.ajax === 'function',
179
+ 是否存在隐藏原生标题栏方法:
180
+ typeof MXWebui !== 'undefined' && typeof MXWebui.hideWebViewTitle === 'function',
181
+ 是否存在获取Token方法:
182
+ typeof MXCommon !== 'undefined' && typeof MXCommon.getEncryptString === 'function',
183
+ })
184
+
185
+ /**
186
+ * 安全解析宿主返回的数据。
187
+ *
188
+ * 敏行 AJAX 示例中 dataType 常使用 text,业务响应可能是 JSON 字符串。
189
+ * 这里仅在字符串非空且 JSON.parse 成功时转换;否则保持原值,避免吞掉
190
+ * 文本响应或宿主返回的非 JSON 数据。
191
+ */
192
+ const safeJsonParse = (data: unknown) => {
193
+ if (typeof data !== 'string') return data
194
+
195
+ const text = data.trim()
196
+ if (!text) return data
197
+
198
+ try {
199
+ return JSON.parse(text)
200
+ } catch {
201
+ return data
202
+ }
203
+ }
204
+
205
+ /**
206
+ * 发起敏行 AJAX 请求。
207
+ *
208
+ * 该方法做三件事:
209
+ * 1. 判断当前环境是否存在 MXCommon.ajax / NXCommon.ajax。
210
+ * 2. 补齐敏行文档默认值,例如 type=GET、async=true。
211
+ * 3. 将宿主回调包装成 Promise,同时保留调用方传入的回调。
212
+ *
213
+ * @param options 敏行 AJAX 请求参数
214
+ * @returns Promise 化后的请求结果
215
+ */
216
+ export function ajax<T = unknown>(options: MXAjaxParams): Promise<MXAjaxResponse<T>> {
217
+ return new Promise((resolve, reject) => {
218
+ const hostCommon = getHostCommon()
219
+
220
+ // 非敏行宿主环境无法调用 MXCommon.ajax,直接进入失败链路。
221
+ if (!hostCommon?.ajax) {
222
+ const error = new Error('当前环境不支持 MXCommon.ajax')
223
+ options.error?.(error)
224
+ options.complete?.()
225
+ reject(error)
226
+ return
227
+ }
228
+
229
+ // 透传调用方参数,并补齐敏行文档中的默认请求配置。
230
+ const requestOptions: MXAjaxParams = {
231
+ ...options,
232
+ type: options.type || 'GET',
233
+ async: options.async ?? true,
234
+ success(data) {
235
+ console.log('[敏行AJAX] 原生请求成功返回', {
236
+ url: requestOptions.url,
237
+ data: safeJsonParse(data),
238
+ })
239
+ // 先保留原始回调行为,再 resolve Promise,避免破坏旧调用方。
240
+ options.success?.(data)
241
+ resolve({ data: safeJsonParse(data) as T })
242
+ },
243
+ error(data) {
244
+ const errorData = safeJsonParse(data)
245
+ console.error('[敏行AJAX] 原生请求失败返回', {
246
+ url: requestOptions.url,
247
+ data: errorData,
248
+ })
249
+ // error 同样保留调用方回调,再 reject 给 await/catch 使用。
250
+ options.error?.(errorData)
251
+ reject(errorData)
252
+ },
253
+ complete() {
254
+ options.complete?.()
255
+ },
256
+ }
257
+
258
+ // 每次调用敏行 AJAX 前打印完整关键参数,方便原生联调定位问题。
259
+ console.log('[敏行AJAX] 发起原生请求', {
260
+ type: requestOptions.type,
261
+ url: requestOptions.url,
262
+ dataType: requestOptions.dataType,
263
+ async: requestOptions.async,
264
+ data: requestOptions.data,
265
+ headers: requestOptions.headers,
266
+ hasSuccessCallback: Boolean(options.success),
267
+ hasErrorCallback: Boolean(options.error),
268
+ hasCompleteCallback: Boolean(options.complete),
269
+ })
270
+
271
+ hostCommon.ajax(requestOptions)
272
+ })
273
+ }
274
+
275
+ /**
276
+ * 获取敏行宿主提供的 APP token。
277
+ *
278
+ * 宿主文档方法名为 MXCommon.getEncryptString({ onSuccess, onError }),
279
+ * 这里只做 Promise 化薄封装。
280
+ *
281
+ * @returns Promise 化后的 APP token 字符串
282
+ */
283
+ export function getAppToken(): Promise<string> {
284
+ return new Promise((resolve, reject) => {
285
+ if (typeof MXCommon === 'undefined' || typeof MXCommon.getEncryptString !== 'function') {
286
+ const error = new Error('当前环境不支持 MXCommon.getEncryptString')
287
+ console.error('[敏行Token] 获取失败:方法不存在', error)
288
+ reject(error)
289
+ return
290
+ }
291
+
292
+ const onSuccess: MXAppTokenSuccess = (token) => {
293
+ console.log('[敏行Token] 获取成功', {
294
+ token,
295
+ 是否返回Token: Boolean(token),
296
+ Token长度: token?.length || 0,
297
+ })
298
+ resolve(token)
299
+ }
300
+
301
+ const onError: MXAppTokenError = (error) => {
302
+ console.error('[敏行Token] 获取失败', error)
303
+ reject(error)
304
+ }
305
+
306
+ MXCommon.getEncryptString({
307
+ onSuccess,
308
+ onError,
309
+ })
310
+ })
311
+ }
312
+
313
+ /**
314
+ * MXCommon.getEncryptString 的同名封装。
315
+ *
316
+ * 保留宿主方法名,方便调用方按敏行文档搜索和使用。
317
+ */
318
+ export const getEncryptString = getAppToken
319
+
320
+ function callMXWebui(method: keyof MXWebuiHost, ...args: unknown[]) {
321
+ const host = getMXWebui()
322
+ const api = host?.[method]
323
+
324
+ if (typeof api !== 'function') {
325
+ return false
326
+ }
327
+
328
+ try {
329
+ ;(api as (...params: unknown[]) => unknown)(...args)
330
+ return true
331
+ } catch (error) {
332
+ console.error(`[敏行UI] ${String(method)} 调用失败`, error)
333
+ return false
334
+ }
335
+ }
336
+
337
+ export function hideWebViewTitle() {
338
+ return callMXWebui('hideWebViewTitle')
339
+ }
340
+
341
+ export function showWebViewTitle(title?: string) {
342
+ return typeof title === 'string'
343
+ ? callMXWebui('showWebViewTitle', title)
344
+ : callMXWebui('showWebViewTitle')
345
+ }
346
+
347
+ export function setWebViewTitle(title: string) {
348
+ return callMXWebui('setWebViewTitle', title)
349
+ }
350
+
351
+ export function setNavBgColor(bgColor: string) {
352
+ return callMXWebui('setNavBgColor', bgColor)
353
+ }
354
+
355
+ export function closeWindow() {
356
+ return callMXWebui('closeWindow')
357
+ }
358
+
359
+ export function setCustomHeader(options: MXWebuiCustomHeaderOptions) {
360
+ return callMXWebui('setCustomHeader', options)
361
+ }
362
+
363
+ export function updateCustomHeader(options: MXWebuiCustomHeaderOptions) {
364
+ return callMXWebui('updateCustomHeader', options)
365
+ }
366
+
367
+ /**
368
+ * 判断当前页面是否运行在敏行宿主环境中。
369
+ *
370
+ * 只要宿主注入了 MXCommon 或 NXCommon,就认为是原生环境。
371
+ */
372
+ export const isNativeApp = (): boolean =>
373
+ typeof MXCommon !== 'undefined' ||
374
+ typeof NXCommon !== 'undefined' ||
375
+ typeof MXWebui !== 'undefined'
376
+
377
+ /**
378
+ * 等待敏行宿主完成注入后再判断是否为 APP 环境。
379
+ *
380
+ * 部分客户端会在 deviceready 后才注入 MXCommon/NXCommon。如果页面启动时
381
+ * 立即同步判断,容易误判为浏览器环境。这里先做一次即时判断;如果还没有
382
+ * 宿主对象,则等待 deviceready,超过 timeout 后按浏览器环境处理。
383
+ */
384
+ export function waitNativeReady(timeout = DEFAULT_NATIVE_TIMEOUT): Promise<boolean> {
385
+ return waitForNativeCapability('原生环境检测', isNativeApp, timeout)
386
+ }
387
+
388
+ function waitForNativeCapability(
389
+ label: string,
390
+ hasCapability: () => boolean,
391
+ timeout: number,
392
+ ): Promise<boolean> {
393
+ const initialReady = hasCapability()
394
+
395
+ console.log('[敏行环境] 开始检测', {
396
+ 检测目标: label,
397
+ 超时时间: timeout,
398
+ 初始是否就绪: initialReady,
399
+ ...getNativeSnapshot(),
400
+ })
401
+
402
+ if (initialReady) {
403
+ console.log('[敏行环境] 启动时已就绪', {
404
+ 检测目标: label,
405
+ ...getNativeSnapshot(),
406
+ })
407
+ return Promise.resolve(true)
408
+ }
409
+
410
+ return new Promise((resolve) => {
411
+ let settled = false
412
+
413
+ const finish = (value: boolean, reason: string) => {
414
+ if (settled) return
415
+
416
+ settled = true
417
+ document.removeEventListener('deviceready', onDeviceReady, false)
418
+ clearTimeout(timer)
419
+ clearInterval(interval)
420
+ console.log('[敏行环境] 检测完成', {
421
+ 检测目标: label,
422
+ 是否就绪: value,
423
+ 完成原因: reason,
424
+ ...getNativeSnapshot(),
425
+ 结果说明: value ? '敏行能力已就绪' : '敏行能力未就绪',
426
+ })
427
+ resolve(value)
428
+ }
429
+
430
+ const onDeviceReady = () => {
431
+ console.log('[敏行环境] 收到 deviceready 事件', {
432
+ 检测目标: label,
433
+ ...getNativeSnapshot(),
434
+ })
435
+
436
+ if (hasCapability()) {
437
+ finish(true, 'deviceready')
438
+ }
439
+ }
440
+
441
+ const interval = setInterval(() => {
442
+ if (hasCapability()) {
443
+ finish(true, 'polling')
444
+ }
445
+ }, NATIVE_POLL_INTERVAL)
446
+
447
+ const timer = setTimeout(() => finish(false, 'timeout'), timeout)
448
+
449
+ document.addEventListener('deviceready', onDeviceReady, false)
450
+ })
451
+ }
@@ -0,0 +1,135 @@
1
+ import { ajax, isNativeApp } from '@/core/mxApi'
2
+ import type { MXAjaxType } from '@/core/mxApi'
3
+ import { request as axiosRequest } from '@/utils/request'
4
+ import type { RequestConfig } from '@/utils/request'
5
+ import { appendQuery } from '@/utils/query'
6
+ import { apiConfig } from '@/config/env'
7
+ import type { ApiResponse } from '@/types/api'
8
+ import router from '@/router'
9
+ import {
10
+ clearAuthTokens,
11
+ getAuthHeaders,
12
+ getStoredRefreshToken,
13
+ REFRESH_TOKEN_URL,
14
+ refreshAccessToken,
15
+ type AuthTokenResponse,
16
+ } from '@/utils/auth-token'
17
+
18
+ export type { RequestConfig } from '@/utils/request'
19
+
20
+ export function request<R = unknown, T = unknown>(
21
+ config: RequestConfig<T>,
22
+ ): Promise<ApiResponse<R>> {
23
+ if (isNativeApp()) {
24
+ return nativeRequest<R, T>(config)
25
+ }
26
+
27
+ return axiosRequest<R, T>(config)
28
+ }
29
+
30
+ async function nativeRequest<R = unknown, T = unknown>(
31
+ config: RequestConfig<T>,
32
+ retried = false,
33
+ ): Promise<ApiResponse<R>> {
34
+ const method = (config.method || 'GET').toUpperCase()
35
+
36
+ if (!isMXAjaxType(method)) {
37
+ throw new Error(`敏行 AJAX 不支持 ${method} 请求`)
38
+ }
39
+
40
+ try {
41
+ const response = await ajax<ApiResponse<R>>({
42
+ type: method,
43
+ url: appendQuery(resolveNativeUrl(config.url), config.params),
44
+ data: config.data,
45
+ dataType: 'text',
46
+ async: true,
47
+ headers: getCommonHeaders(config.headers, config.skipAuth),
48
+ })
49
+
50
+ return response.data
51
+ } catch (error) {
52
+ if (isUnauthorized(error) && !config.skipAuth && !retried) {
53
+ try {
54
+ await refreshAccessToken(requestRefreshTokenByNative)
55
+ return nativeRequest(config, true)
56
+ } catch (refreshError) {
57
+ handleRefreshTokenFailed()
58
+ throw refreshError
59
+ }
60
+ }
61
+
62
+ throw error
63
+ }
64
+ }
65
+
66
+ function getCommonHeaders(headers?: Record<string, string>, skipAuth = false) {
67
+ return getAuthHeaders(headers, skipAuth)
68
+ }
69
+
70
+ function isUnauthorized(error: unknown) {
71
+ const result = error as {
72
+ status?: number | string
73
+ statusCode?: number | string
74
+ code?: number | string
75
+ response?: {
76
+ status?: number | string
77
+ statusCode?: number | string
78
+ }
79
+ }
80
+
81
+ return (
82
+ isUnauthorizedCode(result.status) ||
83
+ isUnauthorizedCode(result.statusCode) ||
84
+ isUnauthorizedCode(result.code) ||
85
+ isUnauthorizedCode(result.response?.status) ||
86
+ isUnauthorizedCode(result.response?.statusCode)
87
+ )
88
+ }
89
+
90
+ function isUnauthorizedCode(code: number | string | undefined) {
91
+ return code === 401 || code === '401' || code === '0401'
92
+ }
93
+
94
+ async function requestRefreshTokenByNative() {
95
+ const refreshToken = getStoredRefreshToken()
96
+
97
+ if (!refreshToken) {
98
+ throw new Error('本地不存在 refresh token')
99
+ }
100
+
101
+ const response = await ajax<ApiResponse<AuthTokenResponse> & AuthTokenResponse>({
102
+ type: 'POST',
103
+ url: resolveNativeUrl(REFRESH_TOKEN_URL),
104
+ data: {
105
+ refreshToken,
106
+ },
107
+ dataType: 'text',
108
+ async: true,
109
+ headers: getCommonHeaders(undefined, true),
110
+ })
111
+
112
+ if (typeof response.data.code === 'number' && response.data.code !== 0) {
113
+ throw new Error(response.data.message || '刷新 token 失败')
114
+ }
115
+
116
+ return response.data.data || response.data
117
+ }
118
+
119
+ function handleRefreshTokenFailed() {
120
+ clearAuthTokens()
121
+ router.push('/login')
122
+ }
123
+
124
+ function isMXAjaxType(method: string): method is MXAjaxType {
125
+ return method === 'GET' || method === 'POST' || method === 'PUT' || method === 'DELETE'
126
+ }
127
+
128
+ function resolveNativeUrl(url: string) {
129
+ if (/^[a-z][a-z\d+\-.]*:\/\//i.test(url)) return url
130
+
131
+ const baseURL = apiConfig.baseURL || ''
132
+ if (!baseURL) return url
133
+
134
+ return `${baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`
135
+ }
package/src/main.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
4
+ import 'vant/lib/index.css'
5
+ import './styles/vant-overrides.css'
6
+
7
+ import App from './App.vue'
8
+ import router from './router'
9
+ import { useAppStore } from '@/stores/app'
10
+ import { apiConfig, appEnv } from '@/config/env'
11
+
12
+ const app = createApp(App)
13
+ const pinia = createPinia()
14
+
15
+ pinia.use(piniaPluginPersistedstate)
16
+
17
+ app.use(pinia)
18
+ app.use(router)
19
+
20
+ // 等待原生环境检测完成后再挂载,确保所有组件读到的 isNative 都是正确值
21
+ const appStore = useAppStore()
22
+ await appStore.initNativeDetection()
23
+
24
+ console.log('[应用启动] 当前运行环境', {
25
+ 当前环境: appEnv,
26
+ 是否原生环境: appStore.isNative,
27
+ 环境说明: appStore.isNative ? '当前是敏行原生环境' : '当前不是敏行原生环境',
28
+ 运行模式: import.meta.env.MODE,
29
+ 接口BaseUrl: apiConfig.baseURL,
30
+ 请求超时时间: apiConfig.timeout,
31
+ 应用版本: __APP_VERSION__,
32
+ })
33
+
34
+ // 测试环境 + 原生设备:加载 vConsole 调试面板
35
+ if (appStore.isNative) {
36
+ const { default: VConsole } = await import('vconsole')
37
+ new VConsole()
38
+ }
39
+
40
+ app.mount('#app')