wangzhibo
2026-08-30 c8515170fd399b4d2dfaebb59128d4674e58915e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import smCrypto from 'sm-crypto'
import { pages, subPackages } from '@/pages.json'
 
import { isMpWeixin } from './platform'
 
/**
 * 运行时服务端地址覆盖存储键
 */
export const SERVER_BASE_URL_OVERRIDE_KEY = 'server_base_url_override'
 
/**
 * 设置/清除/获取 运行时覆盖的服务端地址
 */
export function setServerBaseUrlOverride(url: string) {
  uni.setStorageSync(SERVER_BASE_URL_OVERRIDE_KEY, url)
}
 
export function clearServerBaseUrlOverride() {
  uni.removeStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
}
 
export function getServerBaseUrlOverride(): string | null {
  const value = uni.getStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
  return value || null
}
 
export function getLastPage() {
  // getCurrentPages() 至少有1个元素,所以不再额外判断
  // const lastPage = getCurrentPages().at(-1)
  // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】
  const pages = getCurrentPages()
  return pages[pages.length - 1]
}
 
/**
 * 获取当前页面路由的 path 路径和 redirectPath 路径
 * path 如 '/pages/login/index'
 * redirectPath 如 '/pages/demo/base/route-interceptor'
 */
export function currRoute() {
  const lastPage = getLastPage()
  const currRoute = (lastPage as any).$page
  // console.log('lastPage.$page:', currRoute)
  // console.log('lastPage.$page.fullpath:', currRoute.fullPath)
  // console.log('lastPage.$page.options:', currRoute.options)
  // console.log('lastPage.options:', (lastPage as any).options)
  // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
  const { fullPath } = currRoute as { fullPath: string }
  // console.log(fullPath)
  // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
  // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
  return getUrlObj(fullPath)
}
 
function ensureDecodeURIComponent(url: string) {
  if (url.startsWith('%')) {
    return ensureDecodeURIComponent(decodeURIComponent(url))
  }
  return url
}
/**
 * 解析 url 得到 path 和 query
 * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
 * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
 */
export function getUrlObj(url: string) {
  const [path, queryStr] = url.split('?')
  // console.log(path, queryStr)
 
  if (!queryStr) {
    return {
      path,
      query: {},
    }
  }
  const query: Record<string, string> = {}
  queryStr.split('&').forEach((item) => {
    const [key, value] = item.split('=')
    // console.log(key, value)
    query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y
  })
  return { path, query }
}
/**
 * 得到所有的需要登录的 pages,包括主包和分包的
 * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用
 * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤
 */
export function getAllPages(key = 'needLogin') {
  // 这里处理主包
  const mainPages = pages
    .filter(page => !key || page[key])
    .map(page => ({
      ...page,
      path: `/${page.path}`,
    }))
 
  // 这里处理分包
  const subPages: any[] = []
  subPackages.forEach((subPageObj) => {
    // console.log(subPageObj)
    const { root } = subPageObj
 
    subPageObj.pages
      .filter(page => !key || page[key])
      .forEach((page: { path: string } & Record<string, any>) => {
        subPages.push({
          ...page,
          path: `/${root}/${page.path}`,
        })
      })
  })
  const result = [...mainPages, ...subPages]
  // console.log(`getAllPages by ${key} result: `, result)
  return result
}
 
/**
 * 得到所有的需要登录的 pages,包括主包和分包的
 * 只得到 path 数组
 */
export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
 
/**
 * 得到所有的需要登录的 pages,包括主包和分包的
 * 只得到 path 数组
 */
export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
 
/**
 * 根据微信小程序当前环境,判断应该获取的 baseUrl
 */
export function getEnvBaseUrl() {
  // 若存在用户设置的覆盖地址,优先返回
  const override = getServerBaseUrlOverride()
  if (override)
    return override
 
  // 请求基准地址(默认来源于 env)
  let baseUrl = import.meta.env.VITE_SERVER_BASEURL
 
  // # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
  const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
  const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
  const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
 
  // 微信小程序端环境区分
  if (isMpWeixin) {
    const {
      miniProgram: { envVersion },
    } = uni.getAccountInfoSync()
 
    switch (envVersion) {
      case 'develop':
        baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl
        break
      case 'trial':
        baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl
        break
      case 'release':
        baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl
        break
    }
  }
 
  return baseUrl
}
 
/**
 * 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL
 */
export function getEnvBaseUploadUrl() {
  // 请求基准地址
  let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL
 
  const VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload'
  const VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload'
  const VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload'
 
  // 微信小程序端环境区分
  if (isMpWeixin) {
    const {
      miniProgram: { envVersion },
    } = uni.getAccountInfoSync()
 
    switch (envVersion) {
      case 'develop':
        baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl
        break
      case 'trial':
        baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl
        break
      case 'release':
        baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl
        break
    }
  }
 
  return baseUploadUrl
}
 
/**
 * 生成SM2密钥对(十六进制格式)
 * @returns {object} 包含公钥和私钥的对象
 */
export function generateSm2KeyPairHex() {
  // 使用sm-crypto库生成SM2密钥对
  const sm2 = smCrypto.sm2
  const keypair = sm2.generateKeyPairHex()
 
  return {
    publicKey: keypair.publicKey,
    privateKey: keypair.privateKey,
    clientPublicKey: keypair.publicKey, // 客户端公钥
    clientPrivateKey: keypair.privateKey, // 客户端私钥
  }
}
 
/**
 * SM2公钥加密
 * @param {string} publicKey 公钥(十六进制格式)
 * @param {string} plainText 明文
 * @returns {string} 加密后的密文(十六进制格式)
 */
export function sm2Encrypt(publicKey: string, plainText: string): string {
  if (!publicKey) {
    throw new Error('公钥不能为null或undefined')
  }
 
  if (!plainText) {
    throw new Error('明文不能为空')
  }
 
  const sm2 = smCrypto.sm2
  // SM2加密,添加04前缀表示未压缩公钥
  const encrypted = sm2.doEncrypt(plainText, publicKey, 1)
  // 转换为十六进制格式(与后端保持一致,添加04前缀)
  const result = `04${encrypted}`
 
  return result
}
 
/**
 * SM2私钥解密
 * @param {string} privateKey 私钥(十六进制格式)
 * @param {string} cipherText 密文(十六进制格式)
 * @returns {string} 解密后的明文
 */
export function sm2Decrypt(privateKey: string, cipherText: string): string {
  const sm2 = smCrypto.sm2
  // 移除04前缀(与后端保持一致)
  const dataWithoutPrefix = cipherText.startsWith('04') ? cipherText.substring(2) : cipherText
  // SM2解密
  return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1)
}
 
type AnyFunction = (...args: any[]) => any
 
interface DebouncedFunction extends AnyFunction {
  cancel: () => void
}
 
/**
 * 防抖函数
 * @param fn 要防抖的函数
 * @param delay 延迟时间(毫秒),默认500ms
 * @param immediate 是否立即执行,默认false
 * @returns 防抖处理后的函数
 */
export function debounce<T extends AnyFunction>(
  fn: T,
  delay = 500,
  immediate = false,
): DebouncedFunction {
  let timer: ReturnType<typeof setTimeout> | null = null
 
  const debounced = function (this: any, ...args: Parameters<T>) {
    if (timer) {
      clearTimeout(timer)
    }
 
    if (immediate && !timer) {
      fn.apply(this, args)
    }
 
    timer = setTimeout(() => {
      if (!immediate) {
        fn.apply(this, args)
      }
      timer = null
    }, delay)
  } as DebouncedFunction
 
  debounced.cancel = () => {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
  }
 
  return debounced
}
 
type DeepCloneTarget = string | number | boolean | null | undefined | object
 
/**
 * 深拷贝方法
 * @param target 要拷贝的目标
 * @returns 拷贝后的新对象
 */
export function deepClone<T extends DeepCloneTarget>(target: T): T {
  if (target === null || typeof target !== 'object') {
    return target
  }
 
  if (target instanceof Date) {
    return new Date(target.getTime()) as any
  }
 
  if (Array.isArray(target)) {
    return target.map(item => deepClone(item)) as any
  }
 
  if (target instanceof Object) {
    const clonedObj = {} as T
    for (const key in target) {
      if (Object.prototype.hasOwnProperty.call(target, key)) {
        (clonedObj as any)[key] = deepClone((target as any)[key])
      }
    }
    return clonedObj
  }
 
  return target
}