verce-vue-test 0.0.31 → 0.0.33

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.
@@ -5,24 +5,30 @@
5
5
  * 提供原生能力调用:UI 控制、用户信息、选人组件、原生 HTTP 请求
6
6
  */
7
7
 
8
+ import { appendQuery } from '@/utils/query'
9
+
8
10
  // ============================================================
9
11
  // 类型定义
10
12
  // ============================================================
11
13
 
12
14
  /** 原生 AJAX 请求参数 */
13
- interface MXAjaxParams {
15
+ export interface MXAjaxParams {
14
16
  type: 'GET' | 'POST' | 'PUT' | 'DELETE'
15
17
  url: string
16
- data?: unknown
18
+ dataType?: string
17
19
  async?: boolean
18
- success?: (data: string, status: number, xhr: unknown) => void
19
- error?: (data: unknown, status: number, xhr: unknown) => void
20
+ data?: unknown
21
+ headers?: Record<string, string>
22
+ complete?: () => void
23
+ success?: (data: unknown, status?: number, xhr?: unknown) => void
24
+ error?: (data: unknown, status?: number, xhr?: unknown) => void
20
25
  }
21
26
 
22
27
  /** 原生 AJAX 响应 */
23
28
  export interface MXAjaxResponse<T = unknown> {
24
29
  data: T
25
- status: number
30
+ status?: number
31
+ xhr?: unknown
26
32
  }
27
33
 
28
34
  /** 用户信息 */
@@ -47,19 +53,27 @@ interface MXApiCallbacks {
47
53
  }
48
54
 
49
55
  /** 原生接口命名空间 */
50
- type MXNamespace = 'MXCommon' | 'MXWebui' | 'MXContacts'
56
+ type MXNamespace = 'MXCommon' | 'NXCommon' | 'MXWebui' | 'MXContacts'
51
57
 
52
58
  // ============================================================
53
59
  // 声明全局原生接口
54
60
  // ============================================================
55
61
 
56
62
  declare global {
63
+ const MXCommon: Window['MXCommon']
64
+ const NXCommon: Window['NXCommon']
65
+ const MXWebui: Window['MXWebui']
66
+ const MXContacts: Window['MXContacts']
67
+
57
68
  interface Window {
58
69
  MXCommon?: {
59
70
  getCurrentUser: (callback: (user: MXUser) => void) => void
60
71
  getEncryptString: (callback: (secret: string) => void) => void
61
72
  ajax: (params: MXAjaxParams) => void
62
73
  }
74
+ NXCommon?: {
75
+ ajax: (params: MXAjaxParams) => void
76
+ }
63
77
  MXWebui?: {
64
78
  hideWebViewTitle: () => void
65
79
  showOptionMenu: () => void
@@ -143,64 +157,33 @@ const makeApi =
143
157
  (api: string, ...args: unknown[]) =>
144
158
  applyApi(namespace, api, args)
145
159
 
146
- const MXCommon = makeApi('MXCommon')
147
- const MXWebui = makeApi('MXWebui')
148
- const MXContacts = makeApi('MXContacts')
160
+ const callMXCommon = makeApi('MXCommon')
161
+ const callNXCommon = makeApi('NXCommon')
162
+ const callMXWebui = makeApi('MXWebui')
163
+ const callMXContacts = makeApi('MXContacts')
149
164
 
150
165
  // ============================================================
151
166
  // 导出的业务 API
152
167
  // ============================================================
153
168
 
154
169
  /** 隐藏 WebView 标题栏 */
155
- export const hideWebViewTitle = (): void => MXWebui('hideWebViewTitle')
170
+ export const hideWebViewTitle = (): void => callMXWebui('hideWebViewTitle')
156
171
 
157
172
  /** 显示右上角菜单按钮 */
158
- export const showOptionMenu = (): void => MXWebui('showOptionMenu')
173
+ export const showOptionMenu = (): void => callMXWebui('showOptionMenu')
159
174
 
160
175
  /** 设置自定义头部菜单 */
161
176
  export const setCustomHeaderMenu = (...args: unknown[]): void =>
162
- MXWebui('setCustomHeaderMenu', ...args)
163
-
164
- /** 超时兜底:原生回调未触发时自动执行 onError */
165
- function withCallbackTimeout<T extends (...args: any[]) => void>(
166
- onSuccess: T,
167
- onError: (reason: string) => void,
168
- ms: number,
169
- label: string,
170
- ): T {
171
- const timer = setTimeout(() => onError(`[MX] ${label} 超时(${ms}ms)`), ms)
172
- return ((...args: unknown[]) => {
173
- clearTimeout(timer)
174
- onSuccess(...args)
175
- }) as unknown as T
176
- }
177
+ callMXWebui('setCustomHeaderMenu', ...args)
177
178
 
178
179
  /** 获取当前登录用户信息 */
179
- export const getCurrentUser = (
180
- onSuccess: (user: MXUser) => void,
181
- onError?: (reason: string) => void,
182
- timeout = 5000,
183
- ): void => {
184
- MXCommon('getCurrentUser', withCallbackTimeout(
185
- onSuccess,
186
- (reason) => { console.error(reason); onError?.(reason) },
187
- timeout,
188
- 'getCurrentUser',
189
- ))
180
+ export const getCurrentUser = (onSuccess: (user: MXUser) => void): void => {
181
+ MXCommon!.getCurrentUser(onSuccess)
190
182
  }
191
183
 
192
184
  /** 从原生客户端获取加密密钥 */
193
- export const getEncryptString = (
194
- onSuccess: (secret: string) => void,
195
- onError?: (reason: string) => void,
196
- timeout = 5000,
197
- ): void => {
198
- MXCommon('getEncryptString', withCallbackTimeout(
199
- onSuccess,
200
- (reason) => { console.error(reason); onError?.(reason) },
201
- timeout,
202
- 'getEncryptString',
203
- ))
185
+ export const getEncryptString = (onSuccess: (secret: string) => void): void => {
186
+ MXCommon!.getEncryptString(onSuccess)
204
187
  }
205
188
 
206
189
  /** 打开原生选人组件 */
@@ -208,7 +191,7 @@ export const MXSelectUsers = (
208
191
  options: MXSelectUsersOptions = { enableSelectDept: false, canSelectSelf: true }
209
192
  ): Promise<MXUser[]> =>
210
193
  new Promise((resolve) => {
211
- MXContacts('selectUsers', (result: MXUser[]) => {
194
+ callMXContacts('selectUsers', (result: MXUser[]) => {
212
195
  resolve(result)
213
196
  }, options)
214
197
  })
@@ -217,58 +200,88 @@ export const MXSelectUsers = (
217
200
  * 原生 AJAX 请求
218
201
  *
219
202
  * 使用原生应用的 HTTP 客户端发起请求,绕过浏览器同源策略限制。
220
- * ajax 本身通过 success/error 回调,保留 Promise 包装;回调未触发时 15s 超时 reject
203
+ * NXCommon.ajax 只支持回调,不返回 Promise。
221
204
  */
222
- export const ajax = <T = unknown>(params: Omit<MXAjaxParams, 'success' | 'error'>): Promise<MXAjaxResponse<T>> => {
223
- const request = new Promise<MXAjaxResponse<T>>((resolve, reject) => {
224
- const requestParams: MXAjaxParams = {
225
- ...params,
226
- url: `${import.meta.env.VITE_API_BASE_URL || ''}${params.url}`,
227
- async: true,
228
- success(data, status) {
229
- resolve({ data: JSON.parse(data) as T, status })
230
- },
231
- error(data, status) {
232
- reject({ data, status })
233
- },
234
- }
235
-
236
- MXCommon('ajax', requestParams)
237
- })
238
-
239
- const timeout = new Promise<MXAjaxResponse<T>>((_, reject) =>
240
- setTimeout(() => reject(new Error(`[MX] ajax ${params.type} ${params.url} 超时`)), 15000),
241
- )
205
+ export const ajax = (params: MXAjaxParams): void => {
206
+ const requestParams: MXAjaxParams = {
207
+ ...params,
208
+ url: normalizeAjaxUrl(params.url),
209
+ async: params.async ?? true,
210
+ dataType: params.dataType ?? 'text',
211
+ complete() {
212
+ params.complete?.()
213
+ },
214
+ success(data, status, xhr) {
215
+ params.success?.(data, status, xhr)
216
+ },
217
+ error(data, status, xhr) {
218
+ params.error?.(data, status, xhr)
219
+ },
220
+ }
242
221
 
243
- return Promise.race([request, timeout])
222
+ callNativeAjax(requestParams)
244
223
  }
245
224
 
246
225
  /** 原生 GET 请求 */
247
- export const ajaxGet = <T = unknown>(url: string, query?: Record<string, unknown>): Promise<MXAjaxResponse<T>> => {
248
- const queryString = query
249
- ? '?' + Object.entries(query)
250
- .filter(([, value]) => value !== undefined && value !== null)
251
- .map(([key, value]) => `${key}=${value}`)
252
- .join('&')
253
- : ''
254
-
255
- return ajax<T>({
226
+ export const ajaxGet = (
227
+ url: string,
228
+ query?: Record<string, unknown>,
229
+ options?: Pick<MXAjaxParams, 'headers' | 'complete' | 'dataType' | 'success' | 'error'>,
230
+ ): void =>
231
+ ajax({
232
+ ...options,
256
233
  type: 'GET',
257
- url: `${url}${queryString}`,
234
+ url: appendQuery(url, query),
258
235
  })
259
- }
260
236
 
261
237
  /** 原生 POST 请求 */
262
- export const ajaxPost = <T = unknown>(url: string, data?: unknown): Promise<MXAjaxResponse<T>> =>
263
- ajax<T>({ type: 'POST', url, data })
238
+ export const ajaxPost = (
239
+ url: string,
240
+ data?: unknown,
241
+ options?: Pick<MXAjaxParams, 'headers' | 'complete' | 'dataType' | 'success' | 'error'>,
242
+ ): void =>
243
+ ajax({ ...options, type: 'POST', url, data })
264
244
 
265
245
  /** 原生 PUT 请求 */
266
- export const ajaxPut = <T = unknown>(url: string, data?: unknown): Promise<MXAjaxResponse<T>> =>
267
- ajax<T>({ type: 'PUT', url, data })
246
+ export const ajaxPut = (
247
+ url: string,
248
+ data?: unknown,
249
+ options?: Pick<MXAjaxParams, 'headers' | 'complete' | 'dataType' | 'success' | 'error'>,
250
+ ): void =>
251
+ ajax({ ...options, type: 'PUT', url, data })
268
252
 
269
253
  /** 原生 DELETE 请求 */
270
- export const ajaxDelete = <T = unknown>(url: string, id: string): Promise<MXAjaxResponse<T>> =>
271
- ajax<T>({ type: 'DELETE', url: `${url}/${id}` })
254
+ export const ajaxDelete = (
255
+ url: string,
256
+ data?: unknown,
257
+ options?: Pick<MXAjaxParams, 'headers' | 'complete' | 'dataType' | 'success' | 'error'>,
258
+ ): void =>
259
+ ajax({
260
+ ...options,
261
+ type: 'DELETE',
262
+ url: typeof data === 'string' || typeof data === 'number'
263
+ ? `${url.replace(/\/+$/, '')}/${encodeURIComponent(String(data))}`
264
+ : url,
265
+ data: typeof data === 'string' || typeof data === 'number' ? undefined : data,
266
+ })
267
+
268
+ function callNativeAjax(params: MXAjaxParams): void {
269
+ if (NXCommon?.ajax) {
270
+ callNXCommon('ajax', params)
271
+ return
272
+ }
273
+
274
+ callMXCommon('ajax', params)
275
+ }
276
+
277
+ function normalizeAjaxUrl(url: string): string {
278
+ if (/^[a-z][a-z\d+\-.]*:\/\//i.test(url)) return url
279
+
280
+ const baseURL = import.meta.env.VITE_API_BASE_URL || ''
281
+ if (!baseURL) return url
282
+
283
+ return `${baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`
284
+ }
272
285
 
273
286
  let nativeReady = false
274
287
  let resolveNativeReady: (() => void) | null = null
@@ -285,7 +298,7 @@ document.addEventListener('deviceready', () => {
285
298
  /**
286
299
  * 等待原生设备就绪(deviceready 事件触发后 resolve)
287
300
  *
288
- * native 环境下 deviceready 触发后 window.MXCommon 才注入完成,
301
+ * native 环境下 deviceready 触发后 MXCommon 才注入完成,
289
302
  * 之后 isNativeApp() 才能拿到正确结果。
290
303
  */
291
304
  export const whenNativeReady = (): Promise<boolean> => {
@@ -299,4 +312,4 @@ export const whenNativeReady = (): Promise<boolean> => {
299
312
 
300
313
  /** 检测是否在原生环境(需在 deviceready 之后调用才准确) */
301
314
  export const isNativeApp = (): boolean =>
302
- typeof window.MXCommon !== 'undefined'
315
+ typeof MXCommon !== 'undefined' || typeof NXCommon !== 'undefined'
@@ -5,10 +5,14 @@
5
5
  * 根据环境自动选择 axios 或原生 AJAX
6
6
  */
7
7
 
8
- import { isNativeApp, ajaxGet, ajaxPost, ajaxPut, ajaxDelete } from '@/core/mxApi'
9
- import type { MXAjaxResponse } from '@/core/mxApi'
8
+ import { isNativeApp, ajax } from '@/core/mxApi'
9
+ import type { MXAjaxParams, MXAjaxResponse } from '@/core/mxApi'
10
10
  import { request as axiosRequest } from '@/utils/request'
11
+ import { appendQuery } from '@/utils/query'
11
12
  import type { ApiResponse } from '@/types/api'
13
+ import { showToast } from 'vant'
14
+ import NProgress from 'nprogress'
15
+ import router from '@/router'
12
16
 
13
17
  // ============================================================
14
18
  // Token 管理
@@ -79,6 +83,10 @@ export async function request<R = unknown, T = unknown>(
79
83
  ...customHeaders,
80
84
  }
81
85
 
86
+ if (auth && !headers.Authorization) {
87
+ headers.Authorization = `Basic ${window.btoa(`${auth.username}:${auth.password}`)}`
88
+ }
89
+
82
90
 
83
91
  if (isNativeApp()) {
84
92
  return await nativeRequest<R, T>({ url, method, data, params, headers })
@@ -98,32 +106,216 @@ export async function request<R = unknown, T = unknown>(
98
106
  async function nativeRequest<R, T>(
99
107
  config: RequestConfig<T>
100
108
  ): Promise<ApiResponse<R>> {
101
- const { url, method, data, params } = config
102
-
103
- let response: MXAjaxResponse<unknown>
104
-
105
- switch (method) {
106
- case 'GET':
107
- response = await ajaxGet<R>(url, params)
108
- break
109
- case 'POST':
110
- response = await ajaxPost<R>(url, data)
111
- break
112
- case 'PUT':
113
- response = await ajaxPut<R>(url, data)
114
- break
115
- case 'DELETE':
116
- const id = params?.id as string
117
- response = await ajaxDelete<R>(url, id)
118
- break
119
- default:
120
- throw new Error(`不支持的请求方法: ${method}`)
109
+ const { url, method, data, params, headers } = config
110
+
111
+ NProgress.start()
112
+
113
+ try {
114
+ const options = { headers, dataType: 'text' }
115
+ let response: MXAjaxResponse<ApiResponse<R>>
116
+
117
+ switch (method) {
118
+ case 'GET':
119
+ response = await nativeAjax<ApiResponse<R>>({
120
+ ...options,
121
+ type: 'GET',
122
+ url: appendQuery(url, params),
123
+ })
124
+ break
125
+ case 'POST':
126
+ response = await nativeAjax<ApiResponse<R>>({
127
+ ...options,
128
+ type: 'POST',
129
+ url,
130
+ data,
131
+ })
132
+ break
133
+ case 'PUT':
134
+ response = await nativeAjax<ApiResponse<R>>({
135
+ ...options,
136
+ type: 'PUT',
137
+ url,
138
+ data,
139
+ })
140
+ break
141
+ case 'DELETE':
142
+ response = await nativeAjax<ApiResponse<R>>({
143
+ ...options,
144
+ type: 'DELETE',
145
+ url: appendDeleteParams(url, params),
146
+ data,
147
+ })
148
+ break
149
+ default:
150
+ throw new Error(`不支持的请求方法: ${method}`)
151
+ }
152
+
153
+ const result = normalizeNativeResponse<R>(response)
154
+
155
+ if (result.code !== 0) {
156
+ showToast(result.message || '请求失败')
157
+
158
+ if (result.code === 401) {
159
+ clearToken()
160
+ router.push('/login')
161
+ }
162
+
163
+ const error = new Error(result.message || '请求失败') as Error & { handled?: boolean }
164
+ error.handled = true
165
+ throw error
166
+ }
167
+
168
+ return result
169
+ } catch (error) {
170
+ const nativeError = error as Error & { status?: number; data?: unknown; handled?: boolean }
171
+ if (nativeError.handled) {
172
+ throw error
173
+ }
174
+
175
+ const status = nativeError.status
176
+ const statusCodeMap: Record<number, string> = {
177
+ 400: '请求参数错误',
178
+ 401: '未授权,请重新登录',
179
+ 403: '拒绝访问',
180
+ 404: '请求资源不存在',
181
+ 500: '服务器内部错误',
182
+ }
183
+
184
+ showToast(status ? statusCodeMap[status] || `连接错误 ${status}` : nativeError.message || '网络连接异常')
185
+
186
+ if (status === 401) {
187
+ clearToken()
188
+ router.push('/login')
189
+ }
190
+
191
+ throw error
192
+ } finally {
193
+ NProgress.done()
194
+ }
195
+ }
196
+
197
+ function normalizeNativeResponse<R>(response: MXAjaxResponse<unknown>): ApiResponse<R> {
198
+ const data = response.data
199
+
200
+ if (isApiResponse<R>(data)) {
201
+ return data
121
202
  }
122
203
 
123
- // 包装成与 axios 拦截器一致的响应格式
124
204
  return {
125
205
  code: 0,
126
206
  message: 'success',
127
- data: response.data as R,
128
- } as ApiResponse<R>
207
+ data: data as R,
208
+ }
209
+ }
210
+
211
+ function isApiResponse<R>(value: unknown): value is ApiResponse<R> {
212
+ return typeof value === 'object'
213
+ && value !== null
214
+ && typeof (value as ApiResponse<R>).code === 'number'
215
+ && 'data' in value
216
+ }
217
+
218
+ function appendDeleteParams(url: string, params?: Record<string, unknown>): string {
219
+ if (!params) return url
220
+
221
+ const { id, ...rest } = params
222
+ const targetUrl = id === undefined || id === null
223
+ ? url
224
+ : `${url.replace(/\/+$/, '')}/${encodeURIComponent(String(id))}`
225
+
226
+ const query = new URLSearchParams()
227
+ Object.entries(rest).forEach(([key, value]) => {
228
+ if (value === undefined || value === null) return
229
+ query.append(key, String(value))
230
+ })
231
+
232
+ const queryString = query.toString()
233
+ if (!queryString) return targetUrl
234
+
235
+ return `${targetUrl}${targetUrl.includes('?') ? '&' : '?'}${queryString}`
236
+ }
237
+
238
+ function nativeAjax<T>(params: MXAjaxParams & { timeout?: number }): Promise<MXAjaxResponse<T>> {
239
+ const { timeout = 15000, ...ajaxParams } = params
240
+
241
+ return new Promise((resolve, reject) => {
242
+ let settled = false
243
+ const timer = window.setTimeout(() => {
244
+ if (settled) return
245
+ settled = true
246
+ reject(new Error(`[MX] ajax ${ajaxParams.type} ${ajaxParams.url} 超时(${timeout}ms)`))
247
+ }, timeout)
248
+
249
+ const settle = (callback: () => void) => {
250
+ if (settled) return
251
+ settled = true
252
+ window.clearTimeout(timer)
253
+ callback()
254
+ }
255
+
256
+ ajax({
257
+ ...ajaxParams,
258
+ success(data, status, xhr) {
259
+ settle(() => {
260
+ ajaxParams.success?.(data, status, xhr)
261
+ resolve({
262
+ data: parseAjaxData<T>(data, ajaxParams.dataType),
263
+ status,
264
+ xhr,
265
+ })
266
+ })
267
+ },
268
+ error(data, status, xhr) {
269
+ settle(() => {
270
+ ajaxParams.error?.(data, status, xhr)
271
+ reject(createNativeAjaxError(data, status, xhr, ajaxParams))
272
+ })
273
+ },
274
+ complete() {
275
+ ajaxParams.complete?.()
276
+ },
277
+ })
278
+ })
279
+ }
280
+
281
+ function parseAjaxData<T>(data: unknown, dataType?: string): T {
282
+ if (dataType === 'text') {
283
+ return parseJsonString(data) as T
284
+ }
285
+
286
+ if (typeof data !== 'string') return data as T
287
+
288
+ return parseJsonString(data) as T
289
+ }
290
+
291
+ function parseJsonString(data: unknown): unknown {
292
+ if (typeof data !== 'string') return data
293
+
294
+ const trimmed = data.trim()
295
+ if (!trimmed) return undefined
296
+
297
+ try {
298
+ return JSON.parse(trimmed)
299
+ } catch {
300
+ return data
301
+ }
302
+ }
303
+
304
+ function createNativeAjaxError(
305
+ data: unknown,
306
+ status: number | undefined,
307
+ xhr: unknown,
308
+ params: Omit<MXAjaxParams, 'success' | 'error'>,
309
+ ): Error & { data: unknown; status?: number; xhr?: unknown } {
310
+ const parsedData = parseAjaxData(data, params.dataType)
311
+ const message = typeof parsedData === 'object' && parsedData && 'message' in parsedData
312
+ ? String((parsedData as { message?: unknown }).message || '请求失败')
313
+ : typeof parsedData === 'string'
314
+ ? parsedData
315
+ : '请求失败'
316
+ const error = new Error(message) as Error & { data: unknown; status?: number; xhr?: unknown }
317
+ error.data = parsedData
318
+ error.status = status
319
+ error.xhr = xhr
320
+ return error
129
321
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 将 query 对象拼接到 URL 上
3
+ *
4
+ * 自动过滤 undefined/null 值,支持数组参数。
5
+ */
6
+ export function appendQuery(url: string, query?: Record<string, unknown>): string {
7
+ if (!query) return url
8
+
9
+ const searchParams = new URLSearchParams()
10
+ Object.entries(query).forEach(([key, value]) => {
11
+ if (value === undefined || value === null) return
12
+ if (Array.isArray(value)) {
13
+ value.forEach((item) => {
14
+ if (item !== undefined && item !== null) {
15
+ searchParams.append(key, String(item))
16
+ }
17
+ })
18
+ return
19
+ }
20
+ searchParams.append(key, String(value))
21
+ })
22
+
23
+ const queryString = searchParams.toString()
24
+ if (!queryString) return url
25
+
26
+ return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
27
+ }
@@ -22,19 +22,11 @@ onMounted(() => {
22
22
  if (!appStore.isNative) return
23
23
 
24
24
  loading.value = true
25
- getEncryptString(
26
- (secret) => {
27
- loading.value = false
28
- console.log(secret)
29
- // TODO: 用 secret 自动换取 token 并跳转首页
30
- },
31
- (reason) => {
32
- loading.value = false
33
- console.error(reason)
34
- showToast({ message: '获取设备凭证失败,请手动登录', duration: 3000 })
35
- appStore.isNative = false
36
- },
37
- )
25
+ getEncryptString((secret) => {
26
+ loading.value = false
27
+ console.log(secret)
28
+ // TODO: 用 secret 自动换取 token 并跳转首页
29
+ })
38
30
  })
39
31
 
40
32
  async function onSubmit() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verce-vue-test",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -264,68 +264,73 @@ function createCashflowOption(): EChartsOption {
264
264
  }
265
265
  }
266
266
 
267
- function createLiquidityRingOption(card: GaugeCard): EChartsOption {
268
- const progress = Math.min(Math.max(card.needle, 0), card.max)
269
- const rest = Math.max(card.max - progress, 0)
270
- const ringGradient = new echarts.graphic.LinearGradient(0, 0, 1, 1, [
271
- { offset: 0, color: '#1278ff' },
272
- { offset: 0.5, color: '#33f0ff' },
273
- { offset: 1, color: '#58ff86' },
274
- ])
275
-
267
+ function createStageGaugeOption(card: GaugeCard): EChartsOption {
276
268
  return {
277
269
  animationDuration: 900,
278
- graphic: [
279
- {
280
- type: 'text',
281
- left: 'center',
282
- top: '44%',
283
- style: {
284
- text: card.value,
285
- fill: '#34eaff',
286
- fontSize: 30,
287
- fontWeight: 800,
288
- textAlign: 'center',
289
- textShadowBlur: 14,
290
- textShadowColor: 'rgba(47, 233, 255, .55)',
291
- },
292
- },
293
- ],
294
270
  series: [
295
271
  {
296
- type: 'pie',
297
- radius: ['62%', '78%'],
298
- center: ['50%', '52%'],
299
- startAngle: 90,
300
- clockwise: true,
301
- silent: true,
302
- label: { show: false },
303
- labelLine: { show: false },
304
- itemStyle: {
305
- borderRadius: 10,
306
- borderColor: 'rgba(2, 14, 33, .5)',
307
- borderWidth: 3,
272
+ type: 'gauge',
273
+ min: 0,
274
+ max: card.max,
275
+ radius: '93%',
276
+ center: ['50%', '58%'],
277
+ splitNumber: 5,
278
+ axisLine: {
279
+ lineStyle: {
280
+ width: 16,
281
+ color: [
282
+ [0.3, '#67e0e3'],
283
+ [0.7, '#37a2da'],
284
+ [1, '#fd666d'],
285
+ ],
286
+ },
308
287
  },
309
- data: [
310
- { value: progress, itemStyle: { color: ringGradient } },
311
- { value: rest, itemStyle: { color: 'rgba(33, 79, 124, .28)' } },
312
- ],
313
- },
314
- {
315
- type: 'pie',
316
- radius: ['46%', '47%'],
317
- center: ['50%', '52%'],
318
- silent: true,
319
- label: { show: false },
320
- labelLine: { show: false },
321
- data: [
322
- {
323
- value: 1,
324
- itemStyle: {
325
- color: 'rgba(58, 216, 255, .18)',
326
- },
288
+ pointer: {
289
+ length: '58%',
290
+ width: 5,
291
+ itemStyle: { color: 'auto' },
292
+ },
293
+ axisTick: {
294
+ distance: -16,
295
+ length: 7,
296
+ lineStyle: {
297
+ color: '#ffffff',
298
+ width: 2,
327
299
  },
328
- ],
300
+ },
301
+ splitLine: {
302
+ distance: -16,
303
+ length: 16,
304
+ lineStyle: {
305
+ color: '#ffffff',
306
+ width: 3,
307
+ },
308
+ },
309
+ axisLabel: {
310
+ color: 'inherit',
311
+ distance: 22,
312
+ fontSize: 12,
313
+ },
314
+ anchor: {
315
+ show: true,
316
+ showAbove: true,
317
+ size: 9,
318
+ itemStyle: {
319
+ borderWidth: 2,
320
+ borderColor: '#7ef7ff',
321
+ color: '#001a34',
322
+ },
323
+ },
324
+ title: { show: false },
325
+ detail: {
326
+ valueAnimation: true,
327
+ offsetCenter: [0, '45%'],
328
+ color: '#30dcff',
329
+ fontSize: 26,
330
+ fontWeight: 800,
331
+ formatter: () => card.value,
332
+ },
333
+ data: [{ value: card.needle }],
329
334
  },
330
335
  ],
331
336
  }
@@ -414,9 +419,9 @@ onMounted(async () => {
414
419
  updateCockpitScale()
415
420
  await nextTick()
416
421
  createChart(cashflowChart.value, createCashflowOption())
417
- createChart(liquidityGauge.value, createLiquidityRingOption(gaugeCards[0]!))
418
- createChart(coverageGauge.value, createLiquidityRingOption(gaugeCards[1]!))
419
- createChart(stableGauge.value, createLiquidityRingOption(gaugeCards[2]!))
422
+ createChart(liquidityGauge.value, createStageGaugeOption(gaugeCards[0]!))
423
+ createChart(coverageGauge.value, createStageGaugeOption(gaugeCards[1]!))
424
+ createChart(stableGauge.value, createStageGaugeOption(gaugeCards[2]!))
420
425
  createChart(maturityChart.value, createMaturityOption())
421
426
  window.addEventListener('resize', handleViewportChange)
422
427
  window.visualViewport?.addEventListener('resize', handleViewportChange)
@@ -1032,17 +1037,17 @@ onUnmounted(() => {
1032
1037
 
1033
1038
  .gauge-chart {
1034
1039
  position: absolute;
1035
- left: -2px;
1036
- right: -2px;
1037
- top: 23px;
1038
- height: 150px;
1040
+ left: -8px;
1041
+ right: -8px;
1042
+ top: 24px;
1043
+ height: 156px;
1039
1044
  }
1040
1045
 
1041
1046
  .gauge-card p {
1042
1047
  position: absolute;
1043
1048
  left: 0;
1044
1049
  right: 0;
1045
- bottom: 8px;
1050
+ bottom: 0;
1046
1051
  color: #d7e6f6;
1047
1052
  text-align: center;
1048
1053
  font-size: 14px;