oeos-components 0.4.8 → 0.4.10

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.
@@ -0,0 +1,1419 @@
1
+ const n = `import { unref, isRef, toRaw } from '@vue/reactivity'
2
+ import type { AppContext, VNode } from 'vue'
3
+ import type { Ref } from '@vue/reactivity'
4
+ import { consola } from 'consola'
5
+ import { cloneDeep } from 'es-toolkit' // 这里不要lodash-es的原因是, 体积太大, 超过500kb无法打包
6
+ import { formatTime } from './format'
7
+ import { ElMessage, ElMessageBox, type ElMessageBoxOptions, type MessageOptions } from 'element-plus'
8
+
9
+ type Func<Args extends any[] = any[], Return = any> = (...args: Args) => Return
10
+ type StorageValue = unknown
11
+ type StorageMap = Record<string, any>
12
+ type MaybeRef<T> = T | Ref<T>
13
+ type WidthStyleResult = { width: string }
14
+ type ValidateTriggerType = 'blur' | 'change'
15
+ type ValidateInput = ValidateRules | ValidatePrimitiveValue
16
+ type ValidateRuleResult = {
17
+ required?: boolean
18
+ message?: string
19
+ trigger?: ValidateTriggerType[]
20
+ validator?: (rule: any, value: any, callback: (error?: Error) => void) => void
21
+ min?: number
22
+ max?: number
23
+ }
24
+ type ValidatePrimitiveValue = string | number | boolean | null | undefined
25
+ type UuidOptionItem<T = any> = { label: string; value: T }
26
+
27
+ interface ToastOptions extends Partial<MessageOptions> {
28
+ /**
29
+ * 调用前是否先关闭全部消息提示。
30
+ */
31
+ closeAll?: boolean
32
+ }
33
+
34
+ interface ClearStorageExcludeOptions {
35
+ /**
36
+ * 清空时需要保留的 key 列表。
37
+ */
38
+ exclude: string[]
39
+ }
40
+
41
+ type ClearStorageInput = string | string[] | ClearStorageExcludeOptions
42
+
43
+ interface ValidFormOptions {
44
+ /**
45
+ * 校验失败时的提示文案。
46
+ */
47
+ message?: string
48
+ /**
49
+ * 是否在提示文案后追加字段名。
50
+ */
51
+ detail?: boolean
52
+ /**
53
+ * 是否显示错误提示。
54
+ */
55
+ showMessage?: boolean
56
+ }
57
+
58
+ interface FormValidateTarget {
59
+ validate: (callback: (valid: boolean, status: StorageMap) => void) => void
60
+ }
61
+
62
+ interface UuidOptions {
63
+ /**
64
+ * \`email\` 模式下的邮箱后缀,默认 \`@qq.com\`。
65
+ */
66
+ emailStr?: string
67
+ /**
68
+ * \`time\` 模式下追加的时间格式,默认 \`{y}-{m}-{d} {h}:{i}:{s}\`。
69
+ */
70
+ timeStr?: string
71
+ /**
72
+ * 生成结果前缀。
73
+ */
74
+ startStr?: string
75
+ /**
76
+ * 数组选项模式下的固定索引;不传时随机取值。
77
+ */
78
+ optionsIndex?: number | null
79
+ }
80
+
81
+ interface ValidateRules {
82
+ /**
83
+ * 校验失败提示文案。
84
+ */
85
+ message?: string
86
+ /**
87
+ * 最小值或最小长度。
88
+ */
89
+ min?: number
90
+ /**
91
+ * 最大值或最大长度。
92
+ */
93
+ max?: number
94
+ /**
95
+ * 对比值,例如 \`same\` 校验时使用。
96
+ */
97
+ value?: any
98
+ /**
99
+ * 自定义正则。
100
+ */
101
+ reg?: RegExp
102
+ /**
103
+ * 是否必填,默认 \`true\`。
104
+ */
105
+ required?: boolean
106
+ /**
107
+ * 触发时机,常用 \`['blur', 'change']\`。
108
+ */
109
+ trigger?: ValidateTriggerType[]
110
+ }
111
+
112
+ interface CopyOptions extends ToastOptions {
113
+ /**
114
+ * 是否隐藏复制成功提示。
115
+ */
116
+ hideToast?: boolean
117
+ }
118
+
119
+ type WidthInput = string | number | Ref<string | number>
120
+ type ConfirmMessage = string | VNode | (() => VNode)
121
+ type ConfirmAppendTarget = string | HTMLElement | null
122
+
123
+ interface ConfirmOptions extends ElMessageBoxOptions {
124
+ /**
125
+ * 追加到的挂载节点。支持 css 选择器、DOM 节点或 \`null\`。
126
+ */
127
+ appendTo?: ConfirmAppendTarget
128
+ /**
129
+ * 手动传入 appContext,处理多应用或嵌套弹窗场景。
130
+ */
131
+ appContext?: AppContext | null
132
+ }
133
+
134
+ interface TryCatchResult<T> {
135
+ /**
136
+ * 执行成功时返回的数据;失败时为 \`null\`。
137
+ */
138
+ data: T | null
139
+ /**
140
+ * 执行失败时返回的异常;成功时为 \`null\`。
141
+ */
142
+ error: any
143
+ }
144
+
145
+ type TryCatchTask<T> = Promise<T> | (() => T | Promise<T>)
146
+
147
+ const DEFAULT_CONFIRM_BUTTON_CLASS = 'o-message-box__confirm-btn'
148
+ const DEFAULT_CANCEL_BUTTON_CLASS = 'o-message-box__cancel-btn'
149
+
150
+ function _getBrowserStorage(isSession = false): Storage | null {
151
+ if (typeof window === 'undefined') {
152
+ return null
153
+ }
154
+
155
+ try {
156
+ return isSession ? window.sessionStorage : window.localStorage
157
+ } catch (error) {
158
+ return null
159
+ }
160
+ }
161
+
162
+ function _parseStorageValue<T = any>(value: string | null): T | string | number | null {
163
+ if (value == null) {
164
+ return null
165
+ }
166
+
167
+ try {
168
+ const parsed = JSON.parse(value)
169
+ if (typeof parsed !== 'number') {
170
+ return parsed
171
+ }
172
+ } catch (error) {}
173
+
174
+ return value
175
+ }
176
+
177
+ /**
178
+ * 显示消息提示。
179
+ *
180
+ * 支持三种常见写法:
181
+ * 1. \`$toast('保存成功')\`
182
+ * 2. \`$toast('保存失败', 'e')\`
183
+ * 3. \`$toast({ message: '自定义', type: 'warning' })\`
184
+ *
185
+ * @param message 提示内容,支持纯文本、VNode、渲染函数,或完整配置对象。
186
+ * @param type 提示类型,支持 \`success/info/error/warning\` 和简写 \`s/i/e/w\`,也支持直接传配置对象。
187
+ * @param otherParams 额外配置,例如 \`duration\`、\`customClass\`、\`closeAll\`。
188
+ * @returns 无返回值。
189
+ *
190
+ * @example
191
+ * $toast('保存成功')
192
+ *
193
+ * @example
194
+ * $toast('保存失败', 'e')
195
+ *
196
+ * @example
197
+ * $toast({
198
+ * message: '自定义提示',
199
+ * type: 'warning',
200
+ * duration: 1000,
201
+ * closeAll: true,
202
+ * })
203
+ */
204
+ type MessageType = 'success' | 'info' | 'error' | 'warning'
205
+ type ShortType = 's' | 'i' | 'e' | 'w'
206
+ type ToastType = MessageType | ShortType
207
+
208
+ export function $toast(
209
+ message: string | ToastOptions | VNode | (() => VNode),
210
+ type: ToastType | ToastOptions = 'success',
211
+ otherParams: ToastOptions = {},
212
+ ): void {
213
+ const typeMap: Record<ShortType, MessageType> = {
214
+ s: 'success',
215
+ i: 'info',
216
+ e: 'error',
217
+ w: 'warning',
218
+ }
219
+
220
+ function isShortType(t: any): t is ShortType {
221
+ return ['s', 'i', 'e', 'w'].includes(t)
222
+ }
223
+
224
+ function isToastOptions(obj: any): obj is ToastOptions {
225
+ return typeof obj === 'object' && obj !== null
226
+ }
227
+
228
+ // Case 1: message is options object
229
+ if (isToastOptions(message)) {
230
+ if (message.closeAll) {
231
+ ElMessage.closeAll()
232
+ }
233
+ message.customClass = message.customClass === 'el' ? '' : 'o-antd-message'
234
+ ElMessage(message)
235
+ return
236
+ }
237
+
238
+ // Case 2: type is options object
239
+ if (isToastOptions(type)) {
240
+ if (type.closeAll) {
241
+ ElMessage.closeAll()
242
+ }
243
+ type.customClass = type.customClass === 'el' ? '' : 'o-antd-message'
244
+ ElMessage({
245
+ message,
246
+ type: 'success',
247
+ ...type,
248
+ })
249
+ return
250
+ }
251
+
252
+ // Case 3: regular message with type and options
253
+ if (otherParams.closeAll) {
254
+ ElMessage.closeAll()
255
+ }
256
+
257
+ const resolvedType = isShortType(type) ? typeMap[type] : type
258
+
259
+ otherParams.customClass = otherParams.customClass === 'el' ? '' : 'o-antd-message'
260
+ ElMessage({
261
+ message,
262
+ type: resolvedType,
263
+ ...otherParams,
264
+ })
265
+ }
266
+
267
+ /**
268
+ * \`$toast.success(...)\` 的快捷调用。
269
+ */
270
+ $toast.success = (message: string | ToastOptions | VNode | (() => VNode), otherParams: ToastOptions = {}) =>
271
+ $toast(message, 'success', otherParams)
272
+ /**
273
+ * \`$toast.info(...)\` 的快捷调用。
274
+ */
275
+ $toast.info = (message: string | ToastOptions | VNode | (() => VNode), otherParams: ToastOptions = {}) =>
276
+ $toast(message, 'info', otherParams)
277
+ /**
278
+ * \`$toast.error(...)\` 的快捷调用。
279
+ */
280
+ $toast.error = (message: string | ToastOptions | VNode | (() => VNode), otherParams: ToastOptions = {}) =>
281
+ $toast(message, 'error', otherParams)
282
+ /**
283
+ * \`$toast.warning(...)\` 的快捷调用。
284
+ */
285
+ $toast.warning = (message: string | ToastOptions | VNode | (() => VNode), otherParams: ToastOptions = {}) =>
286
+ $toast(message, 'warning', otherParams)
287
+
288
+ /**
289
+ * 写入浏览器缓存。
290
+ *
291
+ * @param storageName 缓存 key。
292
+ * @param params 要保存的值;对象和数组会自动序列化。
293
+ * @param isSession 是否写入 \`sessionStorage\`;默认写入 \`localStorage\`。
294
+ * 在 SSR / Node 环境下会自动跳过,不会抛错。
295
+ *
296
+ * @example
297
+ * setStorage('token', 'abc123')
298
+ *
299
+ * @example
300
+ * setStorage('userInfo', { id: 1, name: 'andy' }, true)
301
+ */
302
+ export function setStorage(storageName: string, params: StorageValue, isSession = false): void {
303
+ const storage = _getBrowserStorage(isSession)
304
+ if (!storage) {
305
+ return
306
+ }
307
+
308
+ let handleParams
309
+ if (typeof params === 'number' || typeof params === 'string') {
310
+ handleParams = params
311
+ } else {
312
+ handleParams = JSON.stringify(params)
313
+ }
314
+ storage.setItem(storageName, handleParams)
315
+ }
316
+
317
+ /**
318
+ * 读取浏览器缓存。
319
+ *
320
+ * @param data 缓存 key。
321
+ * @param isSession 是否从 \`sessionStorage\` 读取;默认从 \`localStorage\` 读取。
322
+ * @returns 读取到的缓存值;不存在或在 SSR 环境下时返回 \`null\`。
323
+ *
324
+ * @example
325
+ * const token = getStorage('token')
326
+ *
327
+ * @example
328
+ * const userInfo = getStorage('userInfo', true)
329
+ */
330
+ export function getStorage<T = any>(data: string, isSession = false): T | string | number | null {
331
+ const storage = _getBrowserStorage(isSession)
332
+ if (!storage) {
333
+ return null
334
+ }
335
+
336
+ return _parseStorageValue<T>(storage.getItem(data))
337
+ }
338
+
339
+ /**
340
+ * 清空浏览器缓存。
341
+ *
342
+ * @param str 要清空的 key;不传时清空全部,传 \`exclude\` 时表示保留指定 key。
343
+ * @returns 无返回值。
344
+ * 在 SSR / Node 环境下会自动跳过。
345
+ *
346
+ * @example
347
+ * clearStorage()
348
+ *
349
+ * @example
350
+ * clearStorage('loginId')
351
+ *
352
+ * @example
353
+ * clearStorage({ exclude: ['token', 'theme'] })
354
+ */
355
+ export function clearStorage(str: ClearStorageInput = ''): void {
356
+ const sessionStorageRef = _getBrowserStorage(true)
357
+ const localStorageRef = _getBrowserStorage(false)
358
+
359
+ if (!sessionStorageRef && !localStorageRef) {
360
+ return
361
+ }
362
+
363
+ if (isEmpty(str)) {
364
+ sessionStorageRef?.clear()
365
+ localStorageRef?.clear()
366
+ }
367
+ if (!isEmpty(str) && getType(str) !== 'object') {
368
+ let strArr = Array.isArray(str) ? str : [str]
369
+ for (let i = 0; i < strArr.length; i++) {
370
+ sessionStorageRef?.removeItem(strArr[i])
371
+ localStorageRef?.removeItem(strArr[i])
372
+ }
373
+ }
374
+ if (_isObjectWithExclude(str)) {
375
+ if (!isEmpty(str.exclude) && getType(str) === 'object') {
376
+ let sessionStorageObj = {}
377
+ let localStorageObj = {}
378
+ for (const key in str.exclude) {
379
+ if (Object.prototype.hasOwnProperty.call(str.exclude, key)) {
380
+ const name = str.exclude[key]
381
+ if (getStorage(name)) {
382
+ localStorageObj[name] = getStorage(name)
383
+ }
384
+ if (getStorage(name, true)) {
385
+ sessionStorageObj[name] = getStorage(name, true)
386
+ }
387
+ }
388
+ }
389
+ sessionStorageRef?.clear()
390
+ localStorageRef?.clear()
391
+ for (const key in sessionStorageObj) {
392
+ setStorage(key, sessionStorageObj[key], true)
393
+ }
394
+ for (const key in localStorageObj) {
395
+ setStorage(key, localStorageObj[key])
396
+ }
397
+ }
398
+ }
399
+ }
400
+ // 自定义类型守卫函数
401
+ function _isObjectWithExclude(obj: ClearStorageInput): obj is ClearStorageExcludeOptions {
402
+ return typeof obj === 'object' && obj !== null && 'exclude' in obj && typeof obj.exclude === 'object'
403
+ }
404
+
405
+ /**
406
+ * 将 Element Plus 表单校验封装为 Promise。
407
+ *
408
+ * @param ref 表单实例或表单实例的 \`ref\`。
409
+ * @param options 校验配置。
410
+ * @returns 校验通过时返回表单状态对象,失败时 reject 对应状态对象。
411
+ *
412
+ * @example
413
+ * await validForm(formRef)
414
+ *
415
+ * @example
416
+ * await validForm(formRef, { message: '请检查表单信息', detail: true })
417
+ */
418
+ export function validForm(
419
+ ref: MaybeRef<FormValidateTarget>,
420
+ { message = '表单校验错误, 请检查', detail = false, showMessage = true }: ValidFormOptions = {},
421
+ ): Promise<StorageMap> {
422
+ return new Promise((resolve, reject) => {
423
+ unref(ref).validate((valid, status) => {
424
+ if (valid) {
425
+ resolve(status)
426
+ } else {
427
+ if (message && showMessage) {
428
+ let errorText = Object.keys(status)
429
+ let toastMessage = message
430
+ if (detail) {
431
+ toastMessage = message + errorText.join(',')
432
+ }
433
+ $toast(toastMessage, 'e')
434
+ }
435
+ reject(status)
436
+ }
437
+ })
438
+ })
439
+ }
440
+
441
+ function isPlainObject(data: any): boolean {
442
+ if (Object.prototype.toString.call(data) !== '[object Object]') {
443
+ return false
444
+ }
445
+
446
+ const proto = Object.getPrototypeOf(data)
447
+ return proto === Object.prototype || proto === null
448
+ }
449
+
450
+ /**
451
+ * 判断值是否为空。
452
+ *
453
+ * 默认采用更安全的“结构空值”语义:
454
+ * \`undefined\`、\`null\`、空字符串、空数组、空对象、\`NaN\`、空 \`Map\` / \`Set\`、无效日期会返回 \`true\`。
455
+ *
456
+ * @param data 要判断的值。
457
+ * @param strict 是否使用严格模式。默认 \`true\`;传 \`false\` 时会沿用旧语义,把 \`0\` 和 \`false\` 也视为空值。
458
+ * @returns 是否为空。
459
+ *
460
+ * @example
461
+ * isEmpty(' ')
462
+ * // => true
463
+ *
464
+ * @example
465
+ * isEmpty(0)
466
+ * // => false
467
+ *
468
+ * @example
469
+ * isEmpty(0, false)
470
+ * // => true
471
+ */
472
+ export function isEmpty(data: any, strict = true): boolean {
473
+ if (isRef(data)) {
474
+ data = unref(data)
475
+ }
476
+
477
+ // 处理 null/undefined
478
+ if (data == null) return true
479
+ // 如果是日期对象,检查它是否是有效的日期
480
+ if (data instanceof Date) {
481
+ return Number.isNaN(data.getTime())
482
+ }
483
+ // 处理基础类型
484
+ switch (typeof data) {
485
+ case 'string':
486
+ return data.trim().length === 0
487
+ case 'boolean':
488
+ return strict ? false : !data
489
+ case 'number':
490
+ return Number.isNaN(data) || (!strict && data === 0)
491
+ case 'symbol':
492
+ return false
493
+ case 'bigint':
494
+ return strict ? false : data === BigInt(0)
495
+ case 'function':
496
+ return false
497
+ }
498
+ // 处理集合类型
499
+ if (data instanceof Map || data instanceof Set) return data.size === 0
500
+ // 处理数组
501
+ if (Array.isArray(data)) {
502
+ return data.length === 0
503
+ }
504
+
505
+ // 只把 plain object 当作普通对象判断,避免误伤 Promise / RegExp / Error 等实例
506
+ if (isPlainObject(data)) {
507
+ return Reflect.ownKeys(data).length === 0
508
+ }
509
+
510
+ return false
511
+ }
512
+
513
+ /**
514
+ * 合并两个对象。
515
+ *
516
+ * 当同名字段同时有值时,以第二个对象为准;当其中一个字段为空时,保留有值的一侧。
517
+ *
518
+ * @param obj1 第一个对象。
519
+ * @param obj2 第二个对象。
520
+ * @returns 合并后的对象。
521
+ *
522
+ * @example
523
+ * merge(
524
+ * { name: '', age: 18, city: 'beijing' },
525
+ * { name: 'andy', age: 20, city: '' },
526
+ * )
527
+ */
528
+ export function merge<T extends StorageMap, U extends StorageMap>(obj1: T, obj2: U): T & U {
529
+ let merged = { ...obj1, ...obj2 }
530
+ for (let key in merged) {
531
+ if (!isEmpty(obj1[key]) && !isEmpty(obj2[key])) {
532
+ merged[key] = obj2[key]
533
+ } else if (isEmpty(obj1[key]) && !isEmpty(obj2[key])) {
534
+ merged[key] = obj2[key]
535
+ } else if (!isEmpty(obj1[key]) && isEmpty(obj2[key])) {
536
+ merged[key] = obj1[key]
537
+ }
538
+ }
539
+ return merged as T & U
540
+ }
541
+
542
+ /**
543
+ * 深拷贝数据;传入数组时可按次数重复展开。
544
+ *
545
+ * @param data 要克隆的数据。
546
+ * @param times 当 \`data\` 是数组时的复制次数,默认 \`1\`。
547
+ * @returns 克隆后的数据。
548
+ *
549
+ * @example
550
+ * clone({ name: 'andy', info: { id: 1 } })
551
+ *
552
+ * @example
553
+ * clone([1, 2, { name: 'andy' }], 2)
554
+ * // => [1, 2, { name: 'andy' }, 1, 2, { name: 'andy' }]
555
+ */
556
+ export function clone<T>(data: T[], times?: number): T[]
557
+ export function clone<T>(data: T, times?: number): T
558
+ export function clone<T>(data: T, times = 1): T {
559
+ if (isRef(data)) {
560
+ data = unref(data)
561
+ }
562
+ // Check if the data is not an array
563
+ if (getType(data) !== 'array') {
564
+ // If not an array, return a deep clone of the data
565
+ return cloneDeep(data)
566
+ }
567
+ const clonedData = cloneDeep(data)
568
+ const result: typeof clonedData = []
569
+ for (let i = 0; i < times; i++) {
570
+ result.push(...clonedData)
571
+ }
572
+ return result
573
+ }
574
+
575
+ /**
576
+ * 生成随机字符串,也支持手机号、邮箱、时间、数字、IP、端口等特殊模式。
577
+ *
578
+ * @param type 生成模式,支持空字符串、\`phone\`、\`email\`、\`time\`、\`number\`、\`ip\`、\`port\`,也支持传选项数组。
579
+ * @param length 随机字符串或数字的长度,默认 \`4\`。
580
+ * @param options 额外配置。
581
+ * @returns 生成结果。
582
+ *
583
+ * @example
584
+ * uuid()
585
+ * // => 'aB3d'
586
+ *
587
+ * @example
588
+ * uuid('phone')
589
+ * // => '13603312460'
590
+ *
591
+ * @example
592
+ * uuid('time', 0, { startStr: 'andy', timeStr: '{h}:{i}:{s}' })
593
+ */
594
+ export function uuid(
595
+ type: string | Array<UuidOptionItem> = '',
596
+ length = 4,
597
+ options: UuidOptions = {},
598
+ ): string | number | any {
599
+ const { emailStr = '@qq.com', timeStr = '{y}-{m}-{d} {h}:{i}:{s}', startStr = '', optionsIndex = null } = options
600
+
601
+ // 辅助函数:判断是否为ref对象
602
+ function isRef(obj: any): obj is Ref<any> {
603
+ return obj && typeof obj === 'object' && obj._isRef === true
604
+ }
605
+
606
+ // 辅助函数:获取ref的实际值
607
+ function unref<T>(ref: Ref<T> | T): T {
608
+ return isRef(ref) ? ref.value : ref
609
+ }
610
+
611
+ // 辅助函数:生成随机数
612
+ function random(min: number, max: number): number {
613
+ return Math.floor(Math.random() * (max - min + 1) + min)
614
+ }
615
+
616
+ // 解包可能为ref的参数
617
+ type = unref(type)
618
+
619
+ // 处理数组类型参数(下拉框选项)
620
+ if (Array.isArray(type)) {
621
+ if (type.length === 0) return ''
622
+
623
+ const randIndex = optionsIndex ?? random(0, type.length - 1)
624
+ const selectedItem = type[randIndex]
625
+
626
+ // 如果数组项是对象且有value属性,则返回value
627
+ if (typeof selectedItem === 'object' && selectedItem !== null && 'value' in selectedItem) {
628
+ return selectedItem.value
629
+ }
630
+
631
+ // 否则直接返回数组项
632
+ return selectedItem
633
+ }
634
+
635
+ // 生成随机字符串
636
+ let randomChars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
637
+ let result = startStr
638
+
639
+ // 生成手机号
640
+ if (type === 'phone') {
641
+ const prefixes = ['130', '131', '132', '133', '135', '136', '137', '138', '170', '187', '189']
642
+ result = prefixes[random(0, prefixes.length - 1)]
643
+
644
+ for (let i = 0; i < 8; i++) {
645
+ result += Math.floor(Math.random() * 10)
646
+ }
647
+ return result
648
+ }
649
+
650
+ // 生成邮箱
651
+ if (type === 'email') {
652
+ result = uuid(startStr, length) + emailStr
653
+ return result
654
+ }
655
+
656
+ // 生成时间
657
+ if (type === 'time') {
658
+ return uuid(startStr, length, options) + ' ' + formatTime(new Date(), timeStr)
659
+ }
660
+
661
+ // 生成数字
662
+ if (type === 'number') {
663
+ const numChars = '123456789'
664
+ result = ''
665
+
666
+ for (let i = 0; i < length; i++) {
667
+ result += numChars[random(0, numChars.length - 1)]
668
+ }
669
+ return Number(result)
670
+ }
671
+
672
+ // 生成IP地址
673
+ if (type === 'ip') {
674
+ const randomNum = random(1, 99)
675
+ return \`10.0.11.\${randomNum}\`
676
+ }
677
+
678
+ // 生成端口号
679
+ if (type === 'port') {
680
+ return random(1, 65535)
681
+ }
682
+
683
+ // 生成普通随机字符串
684
+ for (let i = 0; i < length; i++) {
685
+ result += randomChars[random(0, randomChars.length - 1)]
686
+ }
687
+
688
+ return result
689
+ }
690
+
691
+ /**
692
+ * 获取值的原始类型名称,返回值统一为小写字符串。
693
+ *
694
+ * @param type 要判断的值。
695
+ * @returns 类型名称,例如 \`array\`、\`date\`、\`object\`、\`null\`。
696
+ *
697
+ * @example
698
+ * getType(new RegExp())
699
+ * // => 'regexp'
700
+ *
701
+ * @example
702
+ * getType([])
703
+ * // => 'array'
704
+ */
705
+ export function getType(type: unknown): string {
706
+ if (typeof type === 'object') {
707
+ const objType = Object.prototype.toString.call(type).slice(8, -1).toLowerCase()
708
+ return objType
709
+ } else {
710
+ return typeof type
711
+ }
712
+ }
713
+
714
+ /**
715
+ * 一个辅助函数,用于在代码中创建一个暂停(延迟)。
716
+ * 它返回一个 Promise,你可以在 \`await\` 后使用它来实现类似 "sleep" 的效果。
717
+ *
718
+ * @param delay - 等待的毫秒数。默认值为 0,表示不延迟。
719
+ * @param fn - (可选) 一个在延迟结束后立即执行的函数。
720
+ *
721
+ * @returns 一个 Promise,当延迟结束后解析(resolve)。
722
+ *
723
+ * @example
724
+ * // 基本用法:延迟 2 秒后打印消息
725
+ * console.log('开始');
726
+ * await sleep(2000);
727
+ * console.log('2秒后执行');
728
+ *
729
+ * @example
730
+ * // 带回调函数的用法:延迟 1 秒后执行清理工作
731
+ * sleep(1000, () => {
732
+ * console.log('执行清理操作...');
733
+ * // 清理代码...
734
+ * });
735
+ *
736
+ * @example
737
+ * // 在循环中使用:每次迭代后延迟 500 毫秒
738
+ * for (let i = 0; i < 5; i++) {
739
+ * console.log(\`当前值: \${i}\`);
740
+ * await sleep(500);
741
+ * }
742
+ */
743
+ export function sleep(delay: number = 0, fn?: () => void) {
744
+ return new Promise<void>((resolve) =>
745
+ setTimeout(() => {
746
+ fn?.()
747
+ resolve()
748
+ }, delay),
749
+ )
750
+ }
751
+
752
+ /**
753
+ * 为 \`validate\` 预置默认触发时机 \`['blur', 'change']\`。
754
+ *
755
+ * @param type 校验类型。
756
+ * @param rules 校验规则。
757
+ * @param pureValid 是否直接返回布尔值。
758
+ * @returns 与 \`validate\` 一致。
759
+ *
760
+ * @example
761
+ * const rule = validateTrigger('required', { message: '请输入名称' })
762
+ */
763
+ export function validateTrigger(type = 'required', rules: ValidateRules = {}, pureValid = false) {
764
+ let mergeRules = {
765
+ trigger: ['blur', 'change'] as ValidateTriggerType[],
766
+ ...rules,
767
+ }
768
+ return validate(type, mergeRules, pureValid)
769
+ }
770
+
771
+ /**
772
+ * 生成 Element Plus 表单校验规则,或直接执行纯校验。
773
+ *
774
+ * @example
775
+ * const mobileRule = validate('mobile')
776
+ *
777
+ * @example
778
+ * const rangeRule = validate('between', { min: 1, max: 99 })
779
+ *
780
+ * @example
781
+ * const isIp = validate('ip', '192.168.1.1', true)
782
+ *
783
+ * @param type 校验类型;如果传入的值不在内置类型列表中,则会直接作为错误提示文案使用。
784
+ * @param rules 校验规则配置,或在 \`pureValid=true\` 时直接传待校验值。
785
+ * @param pureValid 是否直接返回布尔值。
786
+ * @returns \`pureValid=true\` 时返回布尔值,否则返回 Element Plus 规则对象。
787
+ */
788
+ // 定义验证类型枚举
789
+ enum ValidateType {
790
+ REQUIRED = 'required',
791
+ PASSWORD = 'password',
792
+ NUMBER = 'number',
793
+ POSITIVE = 'positive',
794
+ ZERO_POSITIVE = 'zeroPositive',
795
+ INTEGER = 'integer',
796
+ DECIMAL = 'decimal',
797
+ MOBILE = 'mobile',
798
+ EMAIL = 'email',
799
+ IP = 'ip',
800
+ PORT = 'port',
801
+ BETWEEN = 'between',
802
+ LENGTH = 'length',
803
+ SAME = 'same',
804
+ CUSTOM = 'custom',
805
+ }
806
+
807
+ export function validate(type: string = 'required', rules: ValidateInput = {}, pureValid = false): ValidateRuleResult | boolean {
808
+ const rulesObject: ValidateRules = typeof rules === 'object' && rules !== null ? rules : {}
809
+ let trigger = rulesObject.trigger || []
810
+ // 使用枚举值组成的联合类型来确保类型安全
811
+ const typeMaps = Object.values(ValidateType) as string[]
812
+ let parseRequired = rulesObject.required ?? true
813
+
814
+ // 如果不包含typeMaps中的类型, 直接将第一个参数作为message
815
+ if (!typeMaps.includes(type)) {
816
+ return {
817
+ required: parseRequired,
818
+ message: type,
819
+ trigger: trigger,
820
+ }
821
+ }
822
+ if (type === ValidateType.REQUIRED) {
823
+ return {
824
+ required: parseRequired,
825
+ message: rulesObject.message ?? '请输入',
826
+ trigger: trigger,
827
+ }
828
+ }
829
+
830
+ // validator: this.validateName,
831
+ if (type === ValidateType.PASSWORD) {
832
+ const validateName = (rule: any, value: any, callback: (error?: Error) => void) => {
833
+ let validFlag = /^[a-zA-Z0-9_-]+$/.test(value)
834
+ if (!validFlag) {
835
+ callback(new Error(rulesObject.message || '密码只能由英文、数字、下划线、中划线组成'))
836
+ } else {
837
+ callback()
838
+ }
839
+ }
840
+ return {
841
+ validator: validateName,
842
+ trigger: trigger,
843
+ }
844
+ }
845
+ if (type === ValidateType.POSITIVE || type === ValidateType.NUMBER) {
846
+ // 正整数
847
+ return _validValue(rules, '请输入正整数', pureValid, /^[1-9]+\\d*$/)
848
+ }
849
+ if (type === ValidateType.ZERO_POSITIVE) {
850
+ // 正整数且包含0
851
+ return _validValue(rules, '请输入非负整数', pureValid, /^(0|[1-9]+\\d*)$/)
852
+ }
853
+ // 整数, 包含负数和0
854
+ if (type === ValidateType.INTEGER) {
855
+ return _validValue(rules, '请输入整数', pureValid, /^(0|[-]?[1-9]\\d*)$/)
856
+ }
857
+ // 非负数, 整数和最多2位小数
858
+ if (type === ValidateType.DECIMAL) {
859
+ return _validValue(rules, '请输入非负数字, 包含小数且最多2位', pureValid, /(0|[1-9]\\d*)(\\.\\d{1, 2})?|0\\.\\d{1,2}/)
860
+ }
861
+ if (type === ValidateType.MOBILE) {
862
+ return _validValue(rules, '请输入正确的手机号', pureValid, /^[1][0-9]{10}$/)
863
+ }
864
+ if (type === ValidateType.EMAIL) {
865
+ return _validValue(rules, '请输入正确的email', pureValid, /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)
866
+ }
867
+ if (type === ValidateType.IP) {
868
+ return _validValue(
869
+ rules,
870
+ '请输入正确的ip地址',
871
+ pureValid,
872
+ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
873
+ )
874
+ }
875
+ if (type === ValidateType.PORT) {
876
+ return _validValue(
877
+ rules,
878
+ '请输入1-65535的端口号',
879
+ pureValid,
880
+ /^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-5][0-5][0-3][0-5])$/,
881
+ )
882
+ }
883
+ if (type === ValidateType.BETWEEN) {
884
+ let min = rulesObject.min
885
+ let max = rulesObject.max
886
+ const validateBetween = (rule: any, value: any, callback: (error?: Error) => void) => {
887
+ let validFlag = /^-?[0-9]+$/.test(value)
888
+ if (!validFlag) {
889
+ callback(new Error('请输入数字'))
890
+ }
891
+ if (value < min && min !== undefined) {
892
+ callback(new Error(\`数字不能小于\${min}\`))
893
+ }
894
+ if (value > max && max !== undefined) {
895
+ callback(new Error(\`数字不能大于\${max}\`))
896
+ }
897
+ callback()
898
+ }
899
+ return {
900
+ validator: validateBetween,
901
+ trigger: trigger,
902
+ required: parseRequired,
903
+ }
904
+ }
905
+ if (type === ValidateType.LENGTH) {
906
+ return {
907
+ min: rulesObject.min,
908
+ max: rulesObject.max,
909
+ message: rulesObject.message ?? \`请输入\${rulesObject.min}到\${rulesObject.max}个字符\`,
910
+ trigger: trigger,
911
+ required: parseRequired,
912
+ }
913
+ }
914
+
915
+ if (type === ValidateType.SAME) {
916
+ const validateSame = (rule: any, value: any, callback: (error?: Error) => void) => {
917
+ let isSame = value === rulesObject.value
918
+ if (!isSame) {
919
+ const errMessage = rulesObject.message || '密码和确认密码要一致'
920
+ callback(new Error(errMessage))
921
+ }
922
+ if (parseRequired && !value) {
923
+ callback(new Error(rulesObject.message || '请输入'))
924
+ }
925
+ callback()
926
+ }
927
+ let res = {
928
+ validator: validateSame,
929
+ trigger: trigger,
930
+ required: parseRequired,
931
+ }
932
+ return res
933
+ }
934
+ if (type === ValidateType.CUSTOM) {
935
+ // _validValue(rules, '请输入正确的手机号', pureValid, /^[1][0-9]{10}$/)
936
+ if (pureValid) {
937
+ return _validValue(rulesObject.value, rulesObject.message, pureValid, rulesObject.reg!)
938
+ } else {
939
+ return _validValue(rulesObject, rulesObject.message, pureValid, rulesObject.reg!)
940
+ }
941
+ }
942
+
943
+ function _validValue(rules: any, msg: string | undefined, pureValid: boolean, reg: RegExp) {
944
+ if (pureValid === true) {
945
+ return reg.test(rules)
946
+ }
947
+ const validatePhone = (rule: any, value: any, callback: (error?: Error) => void) => {
948
+ let validFlag = reg.test(value)
949
+ if (!validFlag) {
950
+ callback(new Error(rules.message ?? msg))
951
+ } else {
952
+ callback()
953
+ }
954
+ }
955
+ return {
956
+ validator: validatePhone,
957
+ required: rules.required ?? true,
958
+ trigger: trigger,
959
+ }
960
+ }
961
+ }
962
+
963
+ /**
964
+ * 复制文本到剪贴板。
965
+ *
966
+ * @param text 需要复制的文本。
967
+ * @param toastParams 提示配置;传 \`hideToast: true\` 时不显示成功提示。
968
+ * @returns 是否复制成功。
969
+ *
970
+ * @example
971
+ * copy('这是要复制的文本')
972
+ *
973
+ * @example
974
+ * copy('静默复制', { hideToast: true })
975
+ */
976
+ export const copy = (text: string, toastParams: CopyOptions = {}): boolean => {
977
+ const textarea = document.createElement('textarea')
978
+ textarea.value = text
979
+ textarea.style.position = 'fixed'
980
+ document.body.appendChild(textarea)
981
+ textarea.select()
982
+ document.execCommand('copy')
983
+ document.body.removeChild(textarea)
984
+ if (!toastParams.hideToast) {
985
+ // 确保我们传递给$toast的是一个有效的ToastOptions对象
986
+ const { hideToast, ...toastOptions } = toastParams
987
+ $toast(text + '复制成功', toastOptions)
988
+ return true
989
+ }
990
+ return true
991
+ }
992
+
993
+ /**
994
+ * 带变量名的日志输出封装。
995
+ *
996
+ * @param variableStr 变量名或标签名。
997
+ * @param variable 需要打印的值。
998
+ * @param otherInfo 附加信息,常用于手动传入文件路径。
999
+ *
1000
+ * @example
1001
+ * const formData = { id: 1, name: 'andy' }
1002
+ * log('formData', formData)
1003
+ */
1004
+ export function log(variableStr: string, variable: unknown, otherInfo = ''): void {
1005
+ const stack = new Error().stack.split('\\n')[2].trim() // 获取调用堆栈的第二行
1006
+ const matchResult = stack.match(/\\((.*):(\\d+):(\\d+)\\)/)
1007
+ let fileInfo = ''
1008
+ try {
1009
+ if (matchResult && otherInfo) {
1010
+ const lineNumber = matchResult[2]
1011
+ fileInfo = \`vscode://file\${JSON.parse(otherInfo)}:\${lineNumber}\`
1012
+ }
1013
+ } catch (error) {
1014
+ fileInfo = otherInfo
1015
+ }
1016
+
1017
+ if (isRef(variable)) {
1018
+ let unrefVariable = unref(variable)
1019
+ _log(toRaw(unrefVariable))
1020
+ } else {
1021
+ _log(variable)
1022
+ }
1023
+ function _log(consoleData) {
1024
+ if (getType(consoleData) === 'object' || getType(consoleData) === 'array') {
1025
+ consola.log(
1026
+ \`%c\${variableStr} \`,
1027
+ 'background:#fff; color: blue;font-size: 0.8em',
1028
+ JSON.stringify(consoleData, null, '\\t'),
1029
+ \`\${fileInfo}\`,
1030
+ )
1031
+ } else {
1032
+ consola.log(\`%c\${variableStr} \`, 'background:#fff; color: blue;font-size: 0.8em', consoleData, \`\${fileInfo}\`)
1033
+ }
1034
+ }
1035
+ function getType(type) {
1036
+ if (typeof type === 'object') {
1037
+ const objType = Object.prototype.toString.call(type).slice(8, -1).toLowerCase()
1038
+ return objType
1039
+ } else {
1040
+ return typeof type
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ /**
1046
+ * 生成指定范围内的随机整数。
1047
+ *
1048
+ * @param min 最小值,默认 \`0\`。
1049
+ * @param max 最大值,默认 \`10\`。
1050
+ * @returns 随机整数。
1051
+ *
1052
+ * @example
1053
+ * random()
1054
+ *
1055
+ * @example
1056
+ * random(100, 999)
1057
+ */
1058
+ export function random(min = 0, max = 10): number {
1059
+ return Math.floor(Math.random() * (max - min + 1)) + min
1060
+ }
1061
+
1062
+ /**
1063
+ * 将驼峰命名转换为连接符命名。
1064
+ *
1065
+ * @param text 要转换的文本。
1066
+ * @param connect 连接符,默认 \`-\`。
1067
+ * @returns 转换后的文本。
1068
+ *
1069
+ * @example
1070
+ * toLine('NameAndy')
1071
+ * // => 'name-andy'
1072
+ *
1073
+ * @example
1074
+ * toLine('CompTitle', '_')
1075
+ * // => 'comp_title'
1076
+ */
1077
+ export function toLine(text: string, connect = '-'): string {
1078
+ let translateText = text
1079
+ .replace(/([A-Z])/g, (match, p1, offset, origin) => {
1080
+ if (offset === 0) {
1081
+ return \`\${match.toLocaleLowerCase()}\`
1082
+ } else {
1083
+ return \`\${connect}\${match.toLocaleLowerCase()}\`
1084
+ }
1085
+ })
1086
+ .toLocaleLowerCase()
1087
+ return translateText
1088
+ }
1089
+
1090
+ const _CSS_UNIT_RE = /^[0-9]+(\\.[0-9]+)?(px|%|em|rem|vw|vh|ch)$/
1091
+ /**
1092
+ * 将宽度值规范化为可直接用于样式的结果。
1093
+ *
1094
+ * @param initValue 宽度值,支持数字、数字字符串、CSS 长度字符串和 \`ref\`。
1095
+ * @param isBase 为 \`true\` 时直接返回宽度字符串;否则返回 \`{ width }\` 对象。
1096
+ * @returns 宽度字符串或宽度样式对象;无效值返回空字符串或空对象。
1097
+ *
1098
+ * @example
1099
+ * processWidth(200)
1100
+ * // => { width: '200px' }
1101
+ *
1102
+ * @example
1103
+ * processWidth('50%', true)
1104
+ * // => '50%'
1105
+ */
1106
+ export function processWidth(initValue: WidthInput, isBase: true): string
1107
+ export function processWidth(initValue: WidthInput, isBase?: false): WidthStyleResult | {}
1108
+ export function processWidth(initValue: WidthInput, isBase = false): WidthStyleResult | {} | string {
1109
+ const raw = unref(initValue)
1110
+
1111
+ if (!raw) {
1112
+ return isBase ? '' : {}
1113
+ }
1114
+
1115
+ const str = typeof raw === 'number' ? \`\${raw}\` : raw
1116
+
1117
+ let res: string
1118
+ if (!isNaN(Number(str))) {
1119
+ res = str + 'px'
1120
+ } else if (_CSS_UNIT_RE.test(str)) {
1121
+ res = str
1122
+ } else {
1123
+ return isBase ? '' : {}
1124
+ }
1125
+
1126
+ return isBase ? res : { width: res }
1127
+ }
1128
+
1129
+ /**
1130
+ * 创建节流函数。
1131
+ *
1132
+ * @param fn 需要节流执行的函数。
1133
+ * @param delay 节流间隔,单位毫秒,默认 \`1000\`。
1134
+ * @returns 节流后的函数。
1135
+ *
1136
+ * @example
1137
+ * const onResize = throttle(() => {
1138
+ * console.log('resize')
1139
+ * }, 300)
1140
+ */
1141
+ export function throttle<T extends Func>(fn: T, delay = 1000): (...args: Parameters<T>) => void {
1142
+ // last为上一次触发毁掉的时间,timer是定时器
1143
+ let last = 0
1144
+ let timer: ReturnType<typeof setTimeout> | undefined = undefined
1145
+ // 将throttle处理结果当做函数返回
1146
+ return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
1147
+ // 保留调用时的this上下文
1148
+ let context = this
1149
+ // 记录本次触发回调的时间
1150
+ let now = +new Date()
1151
+ // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
1152
+ if (now - last < delay) {
1153
+ // 如果时间间隔小于设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
1154
+ clearTimeout(timer)
1155
+ timer = setTimeout(function () {
1156
+ last = now
1157
+ fn.apply(context, args as any)
1158
+ }, delay)
1159
+ } else {
1160
+ // 如果时间间隔超出了设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
1161
+ last = now
1162
+ fn.apply(context, args as any)
1163
+ }
1164
+ }
1165
+ }
1166
+
1167
+ /**
1168
+ * 统一处理 Promise 或任务函数执行结果。
1169
+ *
1170
+ * 推荐优先传入函数,这样可以同时捕获同步 \`throw\` 和异步 \`reject\`。
1171
+ *
1172
+ * @param task Promise,或返回值 / Promise 的函数。
1173
+ * @param sendLoading 可选的 loading \`ref\`。
1174
+ * @returns 包含 \`data\` 和 \`error\` 的结果对象。
1175
+ *
1176
+ * @example
1177
+ * const loading = ref(false)
1178
+ * const { data, error } = await tryCatch(() => fetchUserData(), loading)
1179
+ *
1180
+ * @example
1181
+ * const { data, error } = await tryCatch(Promise.resolve({ id: 1 }))
1182
+ *
1183
+ * @example
1184
+ * const { data, error } = await tryCatch(() => {
1185
+ * if (!form.name) throw new Error('请输入名称')
1186
+ * return submitForm(form)
1187
+ * })
1188
+ */
1189
+ export function tryCatch<T>(
1190
+ task: Promise<T>,
1191
+ sendLoading?: Ref<boolean> | null,
1192
+ ): Promise<TryCatchResult<T>>
1193
+ export function tryCatch<T>(
1194
+ task: () => T | Promise<T>,
1195
+ sendLoading?: Ref<boolean> | null,
1196
+ ): Promise<TryCatchResult<T>>
1197
+ export async function tryCatch<T>(
1198
+ task: TryCatchTask<T>,
1199
+ sendLoading?: Ref<boolean> | null,
1200
+ ): Promise<TryCatchResult<T>> {
1201
+ const updateLoading = (value: boolean): void => {
1202
+ if (isRef(sendLoading)) {
1203
+ sendLoading.value = value
1204
+ } else if (sendLoading !== undefined && sendLoading !== null) {
1205
+ console.warn('Cannot modify non-ref sendLoading directly!')
1206
+ }
1207
+ }
1208
+
1209
+ updateLoading(true)
1210
+
1211
+ try {
1212
+ const data = typeof task === 'function' ? await task() : await task
1213
+ return { data, error: null }
1214
+ } catch (error: any) {
1215
+ return { data: null, error }
1216
+ } finally {
1217
+ updateLoading(false)
1218
+ }
1219
+ }
1220
+
1221
+ /**
1222
+ * 防抖函数返回值类型。
1223
+ */
1224
+ type DebouncedFunction<T extends Func> = ((...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>>) & {
1225
+ cancel: () => void
1226
+ }
1227
+
1228
+ /**
1229
+ * 创建防抖函数。
1230
+ *
1231
+ * @param func 需要防抖执行的函数。
1232
+ * @param delay 延迟时间,单位毫秒,默认 \`500\`。
1233
+ * @param immediate 是否在第一次调用时立即执行。
1234
+ * @param resultCallback 每次真正执行后触发的结果回调。
1235
+ * @returns 带 \`cancel()\` 方法的防抖函数。
1236
+ *
1237
+ * @example
1238
+ * const search = debounce((keyword: string) => {
1239
+ * return keyword.trim()
1240
+ * }, 300)
1241
+ *
1242
+ * await search('oeos')
1243
+ */
1244
+ export function debounce<T extends Func>(
1245
+ func: T,
1246
+ delay: number = 500,
1247
+ immediate?: boolean,
1248
+ resultCallback?: (result: ReturnType<T>) => void,
1249
+ ): DebouncedFunction<T> {
1250
+ let timer: null | ReturnType<typeof setTimeout> = null
1251
+ let isInvoke = false
1252
+ const _debounce = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
1253
+ return new Promise((resolve, reject) => {
1254
+ if (timer) clearTimeout(timer)
1255
+ if (immediate && !isInvoke) {
1256
+ try {
1257
+ const result = func.apply(this, args)
1258
+ if (resultCallback) resultCallback(result)
1259
+ resolve(result)
1260
+ } catch (e) {
1261
+ reject(e)
1262
+ }
1263
+ isInvoke = true
1264
+ } else {
1265
+ timer = setTimeout(() => {
1266
+ try {
1267
+ const result = func.apply(this, args)
1268
+ if (resultCallback) resultCallback(result)
1269
+ resolve(result)
1270
+ } catch (e) {
1271
+ reject(e)
1272
+ }
1273
+ isInvoke = false
1274
+ timer = null
1275
+ }, delay)
1276
+ }
1277
+ })
1278
+ } as DebouncedFunction<T>
1279
+ _debounce.cancel = function () {
1280
+ if (timer) clearTimeout(timer)
1281
+ isInvoke = false
1282
+ timer = null
1283
+ }
1284
+ return _debounce
1285
+ }
1286
+
1287
+ /**
1288
+ * 打开确认框。
1289
+ *
1290
+ * 相比直接调用 \`ElMessageBox.confirm\`,这里额外处理了默认参数、嵌套弹窗挂载点和 \`appContext\`。
1291
+ *
1292
+ * @param message 确认框内容,支持字符串、VNode 或渲染函数。
1293
+ * @param options MessageBox 配置项。
1294
+ * @param appContext 可选的 Vue 应用上下文。
1295
+ * @returns \`ElMessageBox.confirm\` 返回的 Promise。
1296
+ *
1297
+ * @example
1298
+ * await confirm('确定删除吗?')
1299
+ *
1300
+ * @example
1301
+ * await confirm('确认提交?', {
1302
+ * showCancelButton: true,
1303
+ * appendTo: '#dialogRoot',
1304
+ * })
1305
+ */
1306
+ export function confirm(message: ConfirmMessage, options: ConfirmOptions = {}, appContext: AppContext | null = null) {
1307
+ const resolvedMessage = typeof message === 'function' ? message() : message
1308
+ const resolvedAppendTo = _resolveAppendTarget(options?.appendTo)
1309
+ const resolvedAppContext = _resolveAppContext(options?.appContext || appContext)
1310
+
1311
+ const mergeOptions = {
1312
+ title: '提示',
1313
+ draggable: true,
1314
+ showCancelButton: true,
1315
+ cancelButtonText: '取消',
1316
+ confirmButtonText: '确定',
1317
+ dangerouslyUseHTMLString: true,
1318
+ ...options,
1319
+ appendTo: resolvedAppendTo,
1320
+ appContext: resolvedAppContext,
1321
+ confirmButtonClass: _mergeClassNames(DEFAULT_CONFIRM_BUTTON_CLASS, options?.confirmButtonClass),
1322
+ cancelButtonClass: _mergeClassNames(DEFAULT_CANCEL_BUTTON_CLASS, options?.cancelButtonClass),
1323
+ }
1324
+
1325
+ return ElMessageBox.confirm(resolvedMessage, mergeOptions)
1326
+ }
1327
+
1328
+ function _mergeClassNames(...classNames: Array<string | undefined | null | false>) {
1329
+ return classNames.filter(Boolean).join(' ')
1330
+ }
1331
+
1332
+ function _resolveAppendTarget(appendTo?: ConfirmAppendTarget) {
1333
+ if (typeof document === 'undefined') {
1334
+ return appendTo
1335
+ }
1336
+
1337
+ if (appendTo instanceof HTMLElement) {
1338
+ return appendTo
1339
+ }
1340
+
1341
+ if (typeof appendTo === 'string' && appendTo.trim()) {
1342
+ const rawSelector = appendTo.trim()
1343
+
1344
+ try {
1345
+ const selector =
1346
+ rawSelector.startsWith('#') || rawSelector.startsWith('.') || rawSelector.startsWith('[')
1347
+ ? rawSelector
1348
+ : \`#\${rawSelector}\`
1349
+
1350
+ return document.querySelector(selector) || appendTo
1351
+ } catch (error) {
1352
+ return appendTo
1353
+ }
1354
+ }
1355
+
1356
+ const overlayList = Array.from(document.querySelectorAll<HTMLElement>('.el-overlay'))
1357
+ const dialogOverlay = overlayList
1358
+ .filter((item) => {
1359
+ if (!item.isConnected || item.style.display === 'none') {
1360
+ return false
1361
+ }
1362
+
1363
+ const containsDialog = item.querySelector('.el-dialog, .el-drawer')
1364
+ const containsMessageBox = item.querySelector('.el-message-box')
1365
+
1366
+ return !!containsDialog && !containsMessageBox
1367
+ })
1368
+ .at(-1)
1369
+
1370
+ return dialogOverlay || appendTo
1371
+ }
1372
+
1373
+ function _resolveAppContext(appContext: AppContext | null | undefined) {
1374
+ if (appContext) {
1375
+ return appContext
1376
+ }
1377
+
1378
+ if (typeof document === 'undefined') {
1379
+ return ElMessageBox.install?.context || ElMessageBox._context
1380
+ }
1381
+
1382
+ return ElMessageBox.install?.context || ElMessageBox._context || document.querySelector('#app')?._vue_app?._context
1383
+ }
1384
+
1385
+ /**
1386
+ * 读取根节点上的 CSS 自定义变量。
1387
+ *
1388
+ * @param propertyName CSS 变量名,例如 \`--blue\`。
1389
+ * @returns CSS 变量的值。
1390
+ *
1391
+ * @example
1392
+ * getVariable('--vp-c-brand-1')
1393
+ */
1394
+ export function getVariable(propertyName: string, fallback = ''): string {
1395
+ if (typeof document === 'undefined') {
1396
+ return fallback
1397
+ }
1398
+
1399
+ const res = getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim()
1400
+ return res || fallback
1401
+ }
1402
+
1403
+ declare const __OEOS_UTILS_BUILD_TIME__: string
1404
+
1405
+ /**
1406
+ * 返回当前 utils 包的构建时间。
1407
+ *
1408
+ * @returns 构建时间字符串。
1409
+ *
1410
+ * @example
1411
+ * test()
1412
+ */
1413
+ export function test(): string {
1414
+ return \`build time: \${__OEOS_UTILS_BUILD_TIME__}\`
1415
+ }
1416
+ `;
1417
+ export {
1418
+ n as default
1419
+ };