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.
- package/.editorconfig +8 -0
- package/.env.development +3 -0
- package/.env.production +3 -0
- package/.env.test +3 -0
- package/.gitattributes +1 -0
- package/.oxlintrc.json +10 -0
- package/.prettierrc.json +6 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +13 -0
- package/README.md +179 -0
- package/auto-imports.d.ts +629 -0
- package/components.d.ts +21 -0
- package/design-qa.md +36 -0
- package/docs/MX_API.md +244 -0
- package/docs/REQUEST.md +217 -0
- package/docs/jsapi.js +515 -0
- package/docs/package-json-guide.md +132 -0
- package/env.d.ts +15 -0
- package/eslint.config.ts +26 -0
- package/index.html +16 -0
- package/package.json +83 -0
- package/plugins/bump-version.ts +61 -0
- package/postcss.config.ts +15 -0
- package/public/favicon.ico +0 -0
- package/public/images/12a73787-86a9-4891-a65f-66104746f6a8.png +0 -0
- package/public/images/5798d7aa-ba8b-4605-8079-58b35495ac55.png +0 -0
- package/public/images/73fef1e4-0fd0-4a1a-9b8b-a70a5b6acbbc.png +0 -0
- package/public/images/bc685b4c-0cca-4a79-924c-a8ee10e6f8eb.png +0 -0
- package/public/images/c3dbbd9d-be56-490e-b9f4-6ee17ebefffc.png +0 -0
- package/public/images/ea745a10-42aa-4f44-8d7f-3ab02cc0adcd.png +0 -0
- package/public/images/f5876785-b927-4347-ba19-999114240649.png +0 -0
- package/public/images/img.png +0 -0
- package/public/images/opening-reserve-estimate.png +0 -0
- package/public/images/position-estimate-report.png +0 -0
- package/src/App.vue +131 -0
- package/src/api/announcement.ts +405 -0
- package/src/api/health.ts +13 -0
- package/src/api/pbc-position.ts +178 -0
- package/src/api/rmb-position.ts +233 -0
- package/src/api/tam.ts +173 -0
- package/src/api/user.ts +92 -0
- package/src/auto-imports.d.ts +633 -0
- package/src/components/AppTitleBar.vue +376 -0
- package/src/components.d.ts +33 -0
- package/src/config/config.properties +3 -0
- package/src/config/env.ts +6 -0
- package/src/config/plugin.properties.pro +6 -0
- package/src/config/plugin.properties.test +6 -0
- package/src/core/mxApi/index.ts +451 -0
- package/src/core/request/index.ts +135 -0
- package/src/main.ts +40 -0
- package/src/router/index.ts +144 -0
- package/src/stores/app.ts +103 -0
- package/src/stores/counter.ts +12 -0
- package/src/stores/user.ts +137 -0
- package/src/styles/nprogress.css +22 -0
- package/src/styles/vant-overrides.css +42 -0
- package/src/types/api.d.ts +14 -0
- package/src/types/nprogress.d.ts +8 -0
- package/src/utils/auth-token.ts +241 -0
- package/src/utils/code-highlight.ts +165 -0
- package/src/utils/copy.ts +36 -0
- package/src/utils/query.ts +27 -0
- package/src/utils/request.ts +238 -0
- package/src/utils/rmb-forecast.ts +84 -0
- package/src/utils/toast.ts +61 -0
- package/src/views/article-detail/index.vue +289 -0
- package/src/views/article-edit/index.vue +600 -0
- package/src/views/articles/index.vue +293 -0
- package/src/views/clearing-detail/index.vue +26 -0
- package/src/views/dashboard/index.vue +18 -0
- package/src/views/foreign-position/index.vue +26 -0
- package/src/views/home/index.vue +213 -0
- package/src/views/login/index.vue +275 -0
- package/src/views/mine/index.vue +147 -0
- package/src/views/net-debit/index.vue +26 -0
- package/src/views/opening-reserve-estimate/index.vue +28 -0
- package/src/views/pbc-position/index.vue +357 -0
- package/src/views/position-estimate/index.vue +67 -0
- package/src/views/position-estimate-report/index.vue +28 -0
- package/src/views/rmb-position/index.vue +1013 -0
- package/src/views/rmb-position-create/index.vue +221 -0
- package/src/views/rmb-position-detail/index.vue +355 -0
- package/src/views/settings/index.vue +67 -0
- package/src/views/warning/index.vue +26 -0
- package/tsconfig.app.json +18 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +28 -0
- 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')
|