wui-components-v2 1.1.69 → 1.1.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/api/core/index.ts +74 -74
  2. package/api/feishu.ts +20 -20
  3. package/api/index.ts +4 -4
  4. package/api/login.ts +51 -51
  5. package/api/menu.ts +45 -45
  6. package/api/page.ts +114 -114
  7. package/api/sys.ts +12 -12
  8. package/api/tree.ts +28 -28
  9. package/components/action-popup/action-popup.vue +116 -116
  10. package/components/add-address-list/add-address-list.vue +187 -187
  11. package/components/add-address-page/add-address-page.vue +77 -77
  12. package/components/add-address-page/config.ts +297 -297
  13. package/components/audio-play/audio-play.vue +220 -220
  14. package/components/batch-upload-file/batch-upload-file.vue +75 -75
  15. package/components/card-botom-buttons/card-botom-buttons.vue +279 -279
  16. package/components/custom-date-picker/custom-date-picker.vue +106 -106
  17. package/components/custom-select-picker/custom-select-picker.vue +95 -95
  18. package/components/demo-block/demo-block.vue +63 -63
  19. package/components/demo-card/demo-card.vue +44 -44
  20. package/components/detail-popup/detail-popup.vue +99 -99
  21. package/components/evaluation-page/evaluation-page.vue +196 -196
  22. package/components/fold-card/fold-card.vue +171 -171
  23. package/components/form-control/form-control.vue +661 -661
  24. package/components/global-loading/GlobalLoading.md +366 -366
  25. package/components/global-loading/global-loading.vue +55 -55
  26. package/components/global-message/GlobalMessage.md +570 -570
  27. package/components/global-message/global-message.vue +68 -68
  28. package/components/global-toast/GlobalToast.md +261 -261
  29. package/components/global-toast/global-toast.vue +56 -56
  30. package/components/label-value/label-value.vue +144 -144
  31. package/components/label-value/open-value-more.vue +56 -56
  32. package/components/list-top-buttons/list-top-buttons.vue +19 -19
  33. package/components/loading/loading.vue +5 -5
  34. package/components/login-form/login-form.vue +126 -126
  35. package/components/login-form-box/login-form-box.vue +65 -65
  36. package/components/mulselect-picker/mulselect-picker.vue +86 -86
  37. package/components/phone-login-form/phone-login-form.vue +204 -204
  38. package/components/privacy-popup/privacy-popup.vue +191 -191
  39. package/components/product-card/product-card.vue +201 -201
  40. package/components/scan-input/scan-input.vue +406 -406
  41. package/components/search/search.vue +128 -128
  42. package/components/select-col-picker/select-col-picker.vue +168 -168
  43. package/components/tab-search/tab-search.vue +73 -73
  44. package/components/tree-select/components/TreeNode.vue +200 -200
  45. package/components/tree-select/components/index.vue +471 -471
  46. package/components/tree-select/components/tree-select.vue +56 -56
  47. package/components/tree-select/index.vue +93 -93
  48. package/components/user-choose/user-choose.vue +169 -151
  49. package/components/video-play/video-play.vue +56 -56
  50. package/components/wui-auto-update-component/wui-auto-update-component.vue +132 -132
  51. package/components/wui-default/wui-default.vue +49 -49
  52. package/components/wui-details-page/wui-details-page.vue +147 -147
  53. package/components/wui-edit-page/wui-edit-page-copy.vue +757 -757
  54. package/components/wui-edit-page/wui-edit-page.vue +793 -793
  55. package/components/wui-enume-select-control/wui-enume-select-control.vue +92 -92
  56. package/components/wui-list/wui-list.vue +235 -235
  57. package/components/wui-login/wui-login.vue +43 -43
  58. package/components/wui-login1/wui-login.vue +41 -41
  59. package/components/wui-menus/wui-menus-top.vue +137 -137
  60. package/components/wui-menus/wui-menus-top1.vue +8 -8
  61. package/components/wui-menus/wui-menus.vue +247 -247
  62. package/components/wui-menus1/components/banner-carousel.vue +320 -320
  63. package/components/wui-menus1/components/navbar.vue +43 -43
  64. package/components/wui-menus1/components/quick-panel.vue +55 -55
  65. package/components/wui-menus1/components/search-bar.vue +49 -49
  66. package/components/wui-menus1/components/section-menus.vue +208 -208
  67. package/components/wui-menus1/wui-menus-top.vue +718 -718
  68. package/components/wui-menus1/wui-menus-top1.vue +398 -398
  69. package/components/wui-menus1/wui-menus.vue +564 -564
  70. package/components/wui-notify-info/notify-handle.vue +53 -53
  71. package/components/wui-notify-info/wui-notify-info.vue +280 -280
  72. package/components/wui-scan-binding-sensor/wui-scan-binding-sensor.vue +75 -75
  73. package/components/wui-search-history-babbar/components/SearchBar.vue +70 -70
  74. package/components/wui-search-history-babbar/wui-search-history-babbar.vue +204 -204
  75. package/components/wui-select-list/wui-select-list.vue +310 -310
  76. package/components/wui-select-popup/wui-select-popup.vue +613 -612
  77. package/components/wui-system-settings/wui-system-settings.vue +144 -144
  78. package/components/wui-tabbar/wui-tabbar.vue +106 -106
  79. package/components/wui-tree-page/components/tree-item.vue +238 -238
  80. package/components/wui-tree-page/wui-tree-page.vue +297 -297
  81. package/components/wui-update-component/wui-update-component.vue +221 -221
  82. package/components/wui-user/wui-user.vue +202 -202
  83. package/composables/types/theme.ts +65 -65
  84. package/composables/types/user.ts +5 -5
  85. package/composables/useColorGenerator.ts +221 -221
  86. package/composables/useCompanyFieldFilter.ts +91 -91
  87. package/composables/useEnumes.ts +43 -43
  88. package/composables/useEventBus.ts +37 -37
  89. package/composables/useGlobalLoading.ts +47 -47
  90. package/composables/useGlobalMessage.ts +53 -53
  91. package/composables/useGlobalToast.ts +63 -63
  92. package/composables/useLocale.ts +54 -54
  93. package/composables/useManualTheme.ts +179 -179
  94. package/composables/useMenus.ts +193 -193
  95. package/composables/useSectionMenus.ts +120 -120
  96. package/composables/useTabbar.ts +50 -50
  97. package/composables/useTheme.ts +74 -74
  98. package/composables/useUser.ts +24 -24
  99. package/index.d.ts +90 -90
  100. package/index.ts +83 -83
  101. package/package.json +24 -24
  102. package/static/iconfont/iconfont.css +63 -63
  103. package/store/language.ts +151 -151
  104. package/store/manualThemeStore.ts +143 -143
  105. package/store/persist.ts +41 -41
  106. package/store/themeStore.ts +74 -74
  107. package/store/userStore.ts +22 -22
  108. package/styles/dark-mode.css +523 -523
  109. package/styles/dark-mode.min.css +1 -1
  110. package/styles/dark-mode.scss +498 -498
  111. package/type.ts +154 -154
  112. package/utils/control-tree.ts +353 -353
  113. package/utils/control-type-supportor.ts +148 -148
  114. package/utils/eventBus.ts +36 -36
  115. package/utils/idata-scan.ts +99 -99
  116. package/utils/index.ts +979 -979
  117. package/utils/rsaEncrypt.ts +10 -10
@@ -1,406 +1,406 @@
1
- <script setup lang="ts">
2
- import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
3
-
4
- defineOptions({
5
- name: 'ScanInput',
6
- })
7
-
8
- // ---- 类型 ----
9
- type Html5QrcodeInstance = {
10
- start: (config: object, opts: object, onSuccess: (text: string) => void, onFailure: () => void) => Promise<void>
11
- stop: () => Promise<void>
12
- clear: () => Promise<void>
13
- isScanning?: boolean
14
- }
15
-
16
- // ---- Props ----
17
- const props = withDefaults(defineProps<{
18
- /** v-model 绑定值 */
19
- modelValue?: string
20
- /** 自动启动扫码 */
21
- autoStart?: boolean
22
- /** 占位文本 */
23
- placeholder?: string
24
- /** 是否禁用 */
25
- disabled?: boolean
26
- /** 扫码成功后自动关闭弹窗 (H5) */
27
- autoCloseOnSuccess?: boolean
28
- /** 扫码成功后是否展示 toast 提示 */
29
- showSuccessToast?: boolean
30
- }>(), {
31
- modelValue: '',
32
- autoStart: false,
33
- placeholder: '请输入或扫描二维码/条形码',
34
- disabled: false,
35
- autoCloseOnSuccess: true,
36
- showSuccessToast: true,
37
- })
38
-
39
- const emit = defineEmits<{
40
- (e: 'update:modelValue', val: string): void
41
- (e: 'scan', data: string): void
42
- (e: 'input', data: string): void
43
- (e: 'error', error: string): void
44
- }>()
45
-
46
- // ---- 状态 ----
47
- const inputValue = ref(props.modelValue)
48
- const isScanning = ref(false)
49
- const showScanModal = ref(false)
50
- const isLoading = ref(false)
51
- let html5QrcodeScanner: Html5QrcodeInstance | null = null
52
- let scanLocked = false
53
-
54
- // v-model 同步
55
- watch(() => props.modelValue, (val) => { inputValue.value = val })
56
- watch(inputValue, (val) => { emit('update:modelValue', val); emit('input', val) })
57
-
58
- /** 外部可调用的方法 */
59
- function clear() { inputValue.value = '' }
60
- function focus() { /* wd-input 聚焦可通过 ref 实现 */ }
61
-
62
- defineExpose({ clear, focus, startScan: handleStartScan, stopScan })
63
-
64
- // ======================== H5 平台扫码 ========================
65
- // #ifdef H5
66
- import { Html5Qrcode } from 'html5-qrcode'
67
-
68
- /** 环境检测 */
69
- function checkCameraEnvironment(): { ok: boolean; reason?: string } {
70
- const hostname = window.location.hostname
71
- if (hostname === 'localhost' || hostname === '127.0.0.1') return { ok: true }
72
- if (window.location.protocol !== 'https:') {
73
- return { ok: false, reason: '当前非HTTPS环境,请在 https 下使用扫码功能' }
74
- }
75
- return { ok: true }
76
- }
77
-
78
- async function startScan() {
79
- if (isScanning.value || scanLocked) return
80
- const envCheck = checkCameraEnvironment()
81
- if (!envCheck.ok) {
82
- uni.showToast({ title: envCheck.reason || '环境不支持', icon: 'none' })
83
- return
84
- }
85
-
86
- isLoading.value = true
87
- isScanning.value = true
88
- showScanModal.value = true
89
- scanLocked = false
90
-
91
- try {
92
- await nextTick()
93
- await new Promise(r => setTimeout(r, 200))
94
-
95
- const container = document.getElementById('h5-scan-reader')
96
- if (!container) throw new Error('扫码容器未找到')
97
-
98
- html5QrcodeScanner = new Html5Qrcode('h5-scan-reader') as unknown as Html5QrcodeInstance
99
-
100
- await html5QrcodeScanner.start(
101
- { facingMode: 'environment' },
102
- { fps: 5 },
103
- (decodedText: string) => {
104
- if (scanLocked || !isScanning.value) return
105
- scanLocked = true // 加锁防止重复触发
106
-
107
- console.log('[ScanInput] 扫码成功:', decodedText)
108
- inputValue.value = decodedText
109
- emit('scan', decodedText)
110
- if (props.showSuccessToast) uni.showToast({ title: '扫描成功', icon: 'success' })
111
- if (props.autoCloseOnSuccess) stopScan()
112
- },
113
- () => {}, // 忽略中间帧未识别错误
114
- )
115
-
116
- // JS 强制全屏 video(html5-qrcode 库会覆盖 CSS,需运行时修正)
117
- const videoEl = document.querySelector('#h5-scan-reader video') as HTMLVideoElement | null
118
- if (videoEl) {
119
- Object.assign(videoEl.style, {
120
- width: '100%',
121
- height: '100vh',
122
- objectFit: 'cover',
123
- objectPosition: 'center',
124
- })
125
- }
126
- }
127
- catch (error: unknown) {
128
- console.error('[ScanInput] 启动失败:', error)
129
- const errMsg = error instanceof Error ? error.message : String(error)
130
- let msg = '摄像头访问失败'
131
- if (/Permission|NotAllowed/i.test(errMsg)) msg = '请允许浏览器使用摄像头'
132
- else if (/NotFound|not found/i.test(errMsg)) msg = '未检测到摄像头设备'
133
- else if (/secure origin|insecure/i.test(errMsg)) msg = '需要HTTPS环境才能使用摄像头'
134
-
135
- emit('error', msg)
136
- isScanning.value = false
137
- showScanModal.value = false
138
- uni.showModal({ title: '提示', content: msg, showCancel: false })
139
- }
140
- finally {
141
- isLoading.value = false
142
- }
143
- }
144
-
145
- async function stopScan() {
146
- isScanning.value = false
147
- showScanModal.value = false
148
- scanLocked = false
149
- if (html5QrcodeScanner) {
150
- try {
151
- if (html5QrcodeScanner.isScanning) await html5QrcodeScanner.stop()
152
- await html5QrcodeScanner.clear()
153
- }
154
- catch { /* ignore cleanup error */ }
155
- html5QrcodeScanner = null
156
- }
157
- }
158
- // #endif
159
-
160
- // ======================== 非 H5 平台扫码 ========================
161
- function startUniScanCode() {
162
- // #ifndef H5
163
- uni.scanCode({
164
- success(res: UniApp.ScanCodeSuccessRes) {
165
- inputValue.value = res.result
166
- emit('scan', res.result)
167
- uni.showToast({ title: '扫描成功', icon: 'success' })
168
- },
169
- fail() { emit('error', '扫描已取消') },
170
- })
171
- // #endif
172
- }
173
-
174
- // ======================== 公共方法 ========================
175
- function handleStartScan() {
176
- if (props.disabled) return
177
- // #ifdef H5
178
- isScanning.value ? stopScan() : startScan()
179
- // #endif
180
- // #ifndef H5
181
- startUniScanCode()
182
- // #endif
183
- }
184
-
185
- /** 点击遮罩层关闭 (仅 H5) */
186
- function handleModalMaskClick() {
187
- // #ifdef H5
188
- stopScan()
189
- // #endif
190
- }
191
-
192
- function handleConfirm() {
193
- const val = inputValue.value.trim()
194
- if (val) emit('scan', val)
195
- }
196
-
197
- // ---- 生命周期 ----
198
- onMounted(() => { if (props.autoStart && !props.disabled) handleStartScan() })
199
- onUnmounted(() => {
200
- // #ifdef H5
201
- stopScan()
202
- // #endif
203
- })
204
- </script>
205
-
206
- <template>
207
- <view class="si-wrap">
208
- <!-- 输入框 -->
209
- <wd-input
210
- v-model="inputValue"
211
- class="!pr-3"
212
- :placeholder="placeholder"
213
- type="text"
214
- confirm-type="search"
215
- clearable
216
- no-border
217
- :disabled="disabled"
218
- custom-class="si-wd-input"
219
- @confirm="handleConfirm"
220
- suffix-icon="scan"
221
- @clicksuffixicon="handleStartScan"
222
- />
223
-
224
- <!-- #ifdef H5 -->
225
- <!-- 全屏扫码弹窗 -->
226
- <view
227
- v-if="showScanModal"
228
- class="si-modal"
229
- catchtouchmove
230
- @click="handleModalMaskClick"
231
- >
232
- <!-- 摄像头区域 -->
233
- <view class="si-camera-area" @click.stop>
234
- <!-- 加载中 -->
235
- <view v-if="isLoading" class="si-loading-mask">
236
- <view class="si-loading-box">
237
- <text class="si-loading-text">正在启动摄像头...</text>
238
- </view>
239
- </view>
240
-
241
- <!-- 渲染容器 + 扫描线 -->
242
- <view class="si-reader-wrap overflow-hidden">
243
- <div id="h5-scan-reader" class="si-reader" />
244
- <view v-show="isScanning && !isLoading" class="si-scan-frame">
245
- <view class="si-scan-line" />
246
- </view>
247
- </view>
248
- </view>
249
- <!-- 底部提示栏 -->
250
- <view class="si-modal-footer" @click.stop>
251
- <text class="si-tip">将二维码 / 条形码放入框内即可自动识别</text>
252
- <text class="si-close-text" @click="stopScan">关闭</text>
253
- </view>s
254
- </view>
255
- <!-- #endif -->
256
- </view>
257
- </template>
258
-
259
- <!-- 全局样式:html5-qrcode 内部元素 -->
260
- <style lang="scss">
261
- #h5-scan-reader {
262
- width: 100% !important;
263
- height: 100vh !important;
264
-
265
- video {
266
- width: 100% !important;
267
- height: 100vh !important;
268
- object-fit: cover !important;
269
- object-position: center !important;
270
- }
271
-
272
- #qr-shaded-region,
273
- #html5-qrcode-anchor-scan-region {
274
- border-radius: inherit;
275
- }
276
- }
277
- </style>
278
-
279
- <style scoped lang="scss">
280
- .si-wrap {
281
- width: 100%;
282
- box-sizing: border-box;
283
-
284
- // ---- 输入框容器 ----
285
- :deep(.si-wd-input) {
286
- background: transparent;
287
- border-radius: 10rpx;
288
- border: none;
289
- padding-right: 4rpx;
290
-
291
- &.is-disabled {
292
- opacity: 0.6;
293
- }
294
- }
295
-
296
- // ---- 全屏扫码弹窗 ----
297
- .si-modal {
298
- position: fixed;
299
- inset: 0;
300
- z-index: 999;
301
- background: #000;
302
- }
303
-
304
- .si-camera-area {
305
- width: 100%;
306
- height: 100%;
307
- position: relative;
308
- overflow: hidden;
309
-
310
- // 摄像头包装容器(全屏)
311
- .si-reader-wrap {
312
- position: absolute;
313
- inset: 0;
314
- overflow: hidden;
315
- }
316
-
317
- // 摄像头渲染容器
318
- .si-reader {
319
- width: 100%;
320
- height: 100%;
321
- }
322
-
323
- // 扫描动画线(全屏时收缩在视频可视区域中间)
324
- .si-scan-frame {
325
- position: absolute;
326
- top: 0;
327
- bottom: 0;
328
- left: 0;
329
- right:0;
330
- pointer-events: none;
331
- z-index: 5;
332
- overflow: hidden;
333
- width:100vw;
334
- height: 100vh;
335
-
336
- .si-scan-line {
337
- position: absolute;
338
- top: 2%;
339
- left: 10rpx;
340
- right: 10rpx;
341
- height: 6rpx;
342
- background: #07c160;
343
- border-radius: 3rpx;
344
- box-shadow: 0 0 20rpx 4rpx rgba(7, 193, 96, 0.7);
345
- animation: si-sweep 2.5s ease-in-out infinite;
346
- }
347
- }
348
-
349
- // 加载中遮罩
350
- .si-loading-mask {
351
- position: absolute;
352
- inset: 0;
353
- display: flex;
354
- align-items: center;
355
- justify-content: center;
356
- z-index: 10;
357
- background: rgba(0, 0, 0, 0.5);
358
-
359
- .si-loading-box {
360
- background: rgba(0, 0, 0, 0.75);
361
- padding: 30rpx 48rpx;
362
- border-radius: 16rpx;
363
-
364
- .si-loading-text {
365
- color: #fff;
366
- font-size: 28rpx;
367
- }
368
- }
369
- }
370
- }
371
-
372
- // 底部提示栏(绝对定位在弹窗底部)
373
- .si-modal-footer {
374
- position: absolute;
375
- left: 0;
376
- right: 0;
377
- bottom: 0;
378
- z-index: 10;
379
- padding: 40rpx 32rpx calc(80rpx + env(safe-area-inset-bottom, 0));
380
- display: flex;
381
- flex-direction: column;
382
- align-items: center;
383
- gap: 24rpx;
384
-
385
- .si-tip {
386
- color: rgba(255, 255, 255, 0.65);
387
- font-size: 26rpx;
388
- letter-spacing: 2rpx;
389
- }
390
-
391
- .si-close-text {
392
- color: rgba(255, 255, 255, 0.85);
393
- font-size: 30rpx;
394
- padding: 12rpx 48rpx;
395
- border: 2rpx solid rgba(255, 255, 255, 0.3);
396
- border-radius: 40rpx;
397
- }
398
- }
399
- }
400
-
401
- @keyframes si-sweep {
402
- 0% { top:30%; opacity: 1; }
403
- 50% { opacity: 1; }
404
- 100% { top: 70%; opacity: 0.3; }
405
- }
406
- </style>
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
3
+
4
+ defineOptions({
5
+ name: 'ScanInput',
6
+ })
7
+
8
+ // ---- 类型 ----
9
+ type Html5QrcodeInstance = {
10
+ start: (config: object, opts: object, onSuccess: (text: string) => void, onFailure: () => void) => Promise<void>
11
+ stop: () => Promise<void>
12
+ clear: () => Promise<void>
13
+ isScanning?: boolean
14
+ }
15
+
16
+ // ---- Props ----
17
+ const props = withDefaults(defineProps<{
18
+ /** v-model 绑定值 */
19
+ modelValue?: string
20
+ /** 自动启动扫码 */
21
+ autoStart?: boolean
22
+ /** 占位文本 */
23
+ placeholder?: string
24
+ /** 是否禁用 */
25
+ disabled?: boolean
26
+ /** 扫码成功后自动关闭弹窗 (H5) */
27
+ autoCloseOnSuccess?: boolean
28
+ /** 扫码成功后是否展示 toast 提示 */
29
+ showSuccessToast?: boolean
30
+ }>(), {
31
+ modelValue: '',
32
+ autoStart: false,
33
+ placeholder: '请输入或扫描二维码/条形码',
34
+ disabled: false,
35
+ autoCloseOnSuccess: true,
36
+ showSuccessToast: true,
37
+ })
38
+
39
+ const emit = defineEmits<{
40
+ (e: 'update:modelValue', val: string): void
41
+ (e: 'scan', data: string): void
42
+ (e: 'input', data: string): void
43
+ (e: 'error', error: string): void
44
+ }>()
45
+
46
+ // ---- 状态 ----
47
+ const inputValue = ref(props.modelValue)
48
+ const isScanning = ref(false)
49
+ const showScanModal = ref(false)
50
+ const isLoading = ref(false)
51
+ let html5QrcodeScanner: Html5QrcodeInstance | null = null
52
+ let scanLocked = false
53
+
54
+ // v-model 同步
55
+ watch(() => props.modelValue, (val) => { inputValue.value = val })
56
+ watch(inputValue, (val) => { emit('update:modelValue', val); emit('input', val) })
57
+
58
+ /** 外部可调用的方法 */
59
+ function clear() { inputValue.value = '' }
60
+ function focus() { /* wd-input 聚焦可通过 ref 实现 */ }
61
+
62
+ defineExpose({ clear, focus, startScan: handleStartScan, stopScan })
63
+
64
+ // ======================== H5 平台扫码 ========================
65
+ // #ifdef H5
66
+ import { Html5Qrcode } from 'html5-qrcode'
67
+
68
+ /** 环境检测 */
69
+ function checkCameraEnvironment(): { ok: boolean; reason?: string } {
70
+ const hostname = window.location.hostname
71
+ if (hostname === 'localhost' || hostname === '127.0.0.1') return { ok: true }
72
+ if (window.location.protocol !== 'https:') {
73
+ return { ok: false, reason: '当前非HTTPS环境,请在 https 下使用扫码功能' }
74
+ }
75
+ return { ok: true }
76
+ }
77
+
78
+ async function startScan() {
79
+ if (isScanning.value || scanLocked) return
80
+ const envCheck = checkCameraEnvironment()
81
+ if (!envCheck.ok) {
82
+ uni.showToast({ title: envCheck.reason || '环境不支持', icon: 'none' })
83
+ return
84
+ }
85
+
86
+ isLoading.value = true
87
+ isScanning.value = true
88
+ showScanModal.value = true
89
+ scanLocked = false
90
+
91
+ try {
92
+ await nextTick()
93
+ await new Promise(r => setTimeout(r, 200))
94
+
95
+ const container = document.getElementById('h5-scan-reader')
96
+ if (!container) throw new Error('扫码容器未找到')
97
+
98
+ html5QrcodeScanner = new Html5Qrcode('h5-scan-reader') as unknown as Html5QrcodeInstance
99
+
100
+ await html5QrcodeScanner.start(
101
+ { facingMode: 'environment' },
102
+ { fps: 5 },
103
+ (decodedText: string) => {
104
+ if (scanLocked || !isScanning.value) return
105
+ scanLocked = true // 加锁防止重复触发
106
+
107
+ console.log('[ScanInput] 扫码成功:', decodedText)
108
+ inputValue.value = decodedText
109
+ emit('scan', decodedText)
110
+ if (props.showSuccessToast) uni.showToast({ title: '扫描成功', icon: 'success' })
111
+ if (props.autoCloseOnSuccess) stopScan()
112
+ },
113
+ () => {}, // 忽略中间帧未识别错误
114
+ )
115
+
116
+ // JS 强制全屏 video(html5-qrcode 库会覆盖 CSS,需运行时修正)
117
+ const videoEl = document.querySelector('#h5-scan-reader video') as HTMLVideoElement | null
118
+ if (videoEl) {
119
+ Object.assign(videoEl.style, {
120
+ width: '100%',
121
+ height: '100vh',
122
+ objectFit: 'cover',
123
+ objectPosition: 'center',
124
+ })
125
+ }
126
+ }
127
+ catch (error: unknown) {
128
+ console.error('[ScanInput] 启动失败:', error)
129
+ const errMsg = error instanceof Error ? error.message : String(error)
130
+ let msg = '摄像头访问失败'
131
+ if (/Permission|NotAllowed/i.test(errMsg)) msg = '请允许浏览器使用摄像头'
132
+ else if (/NotFound|not found/i.test(errMsg)) msg = '未检测到摄像头设备'
133
+ else if (/secure origin|insecure/i.test(errMsg)) msg = '需要HTTPS环境才能使用摄像头'
134
+
135
+ emit('error', msg)
136
+ isScanning.value = false
137
+ showScanModal.value = false
138
+ uni.showModal({ title: '提示', content: msg, showCancel: false })
139
+ }
140
+ finally {
141
+ isLoading.value = false
142
+ }
143
+ }
144
+
145
+ async function stopScan() {
146
+ isScanning.value = false
147
+ showScanModal.value = false
148
+ scanLocked = false
149
+ if (html5QrcodeScanner) {
150
+ try {
151
+ if (html5QrcodeScanner.isScanning) await html5QrcodeScanner.stop()
152
+ await html5QrcodeScanner.clear()
153
+ }
154
+ catch { /* ignore cleanup error */ }
155
+ html5QrcodeScanner = null
156
+ }
157
+ }
158
+ // #endif
159
+
160
+ // ======================== 非 H5 平台扫码 ========================
161
+ function startUniScanCode() {
162
+ // #ifndef H5
163
+ uni.scanCode({
164
+ success(res: UniApp.ScanCodeSuccessRes) {
165
+ inputValue.value = res.result
166
+ emit('scan', res.result)
167
+ uni.showToast({ title: '扫描成功', icon: 'success' })
168
+ },
169
+ fail() { emit('error', '扫描已取消') },
170
+ })
171
+ // #endif
172
+ }
173
+
174
+ // ======================== 公共方法 ========================
175
+ function handleStartScan() {
176
+ if (props.disabled) return
177
+ // #ifdef H5
178
+ isScanning.value ? stopScan() : startScan()
179
+ // #endif
180
+ // #ifndef H5
181
+ startUniScanCode()
182
+ // #endif
183
+ }
184
+
185
+ /** 点击遮罩层关闭 (仅 H5) */
186
+ function handleModalMaskClick() {
187
+ // #ifdef H5
188
+ stopScan()
189
+ // #endif
190
+ }
191
+
192
+ function handleConfirm() {
193
+ const val = inputValue.value.trim()
194
+ if (val) emit('scan', val)
195
+ }
196
+
197
+ // ---- 生命周期 ----
198
+ onMounted(() => { if (props.autoStart && !props.disabled) handleStartScan() })
199
+ onUnmounted(() => {
200
+ // #ifdef H5
201
+ stopScan()
202
+ // #endif
203
+ })
204
+ </script>
205
+
206
+ <template>
207
+ <view class="si-wrap">
208
+ <!-- 输入框 -->
209
+ <wd-input
210
+ v-model="inputValue"
211
+ class="!pr-3"
212
+ :placeholder="placeholder"
213
+ type="text"
214
+ confirm-type="search"
215
+ clearable
216
+ no-border
217
+ :disabled="disabled"
218
+ custom-class="si-wd-input"
219
+ @confirm="handleConfirm"
220
+ suffix-icon="scan"
221
+ @clicksuffixicon="handleStartScan"
222
+ />
223
+
224
+ <!-- #ifdef H5 -->
225
+ <!-- 全屏扫码弹窗 -->
226
+ <view
227
+ v-if="showScanModal"
228
+ class="si-modal"
229
+ catchtouchmove
230
+ @click="handleModalMaskClick"
231
+ >
232
+ <!-- 摄像头区域 -->
233
+ <view class="si-camera-area" @click.stop>
234
+ <!-- 加载中 -->
235
+ <view v-if="isLoading" class="si-loading-mask">
236
+ <view class="si-loading-box">
237
+ <text class="si-loading-text">正在启动摄像头...</text>
238
+ </view>
239
+ </view>
240
+
241
+ <!-- 渲染容器 + 扫描线 -->
242
+ <view class="si-reader-wrap overflow-hidden">
243
+ <div id="h5-scan-reader" class="si-reader" />
244
+ <view v-show="isScanning && !isLoading" class="si-scan-frame">
245
+ <view class="si-scan-line" />
246
+ </view>
247
+ </view>
248
+ </view>
249
+ <!-- 底部提示栏 -->
250
+ <view class="si-modal-footer" @click.stop>
251
+ <text class="si-tip">将二维码 / 条形码放入框内即可自动识别</text>
252
+ <text class="si-close-text" @click="stopScan">关闭</text>
253
+ </view>s
254
+ </view>
255
+ <!-- #endif -->
256
+ </view>
257
+ </template>
258
+
259
+ <!-- 全局样式:html5-qrcode 内部元素 -->
260
+ <style lang="scss">
261
+ #h5-scan-reader {
262
+ width: 100% !important;
263
+ height: 100vh !important;
264
+
265
+ video {
266
+ width: 100% !important;
267
+ height: 100vh !important;
268
+ object-fit: cover !important;
269
+ object-position: center !important;
270
+ }
271
+
272
+ #qr-shaded-region,
273
+ #html5-qrcode-anchor-scan-region {
274
+ border-radius: inherit;
275
+ }
276
+ }
277
+ </style>
278
+
279
+ <style scoped lang="scss">
280
+ .si-wrap {
281
+ width: 100%;
282
+ box-sizing: border-box;
283
+
284
+ // ---- 输入框容器 ----
285
+ :deep(.si-wd-input) {
286
+ background: transparent;
287
+ border-radius: 10rpx;
288
+ border: none;
289
+ padding-right: 4rpx;
290
+
291
+ &.is-disabled {
292
+ opacity: 0.6;
293
+ }
294
+ }
295
+
296
+ // ---- 全屏扫码弹窗 ----
297
+ .si-modal {
298
+ position: fixed;
299
+ inset: 0;
300
+ z-index: 999;
301
+ background: #000;
302
+ }
303
+
304
+ .si-camera-area {
305
+ width: 100%;
306
+ height: 100%;
307
+ position: relative;
308
+ overflow: hidden;
309
+
310
+ // 摄像头包装容器(全屏)
311
+ .si-reader-wrap {
312
+ position: absolute;
313
+ inset: 0;
314
+ overflow: hidden;
315
+ }
316
+
317
+ // 摄像头渲染容器
318
+ .si-reader {
319
+ width: 100%;
320
+ height: 100%;
321
+ }
322
+
323
+ // 扫描动画线(全屏时收缩在视频可视区域中间)
324
+ .si-scan-frame {
325
+ position: absolute;
326
+ top: 0;
327
+ bottom: 0;
328
+ left: 0;
329
+ right:0;
330
+ pointer-events: none;
331
+ z-index: 5;
332
+ overflow: hidden;
333
+ width:100vw;
334
+ height: 100vh;
335
+
336
+ .si-scan-line {
337
+ position: absolute;
338
+ top: 2%;
339
+ left: 10rpx;
340
+ right: 10rpx;
341
+ height: 6rpx;
342
+ background: #07c160;
343
+ border-radius: 3rpx;
344
+ box-shadow: 0 0 20rpx 4rpx rgba(7, 193, 96, 0.7);
345
+ animation: si-sweep 2.5s ease-in-out infinite;
346
+ }
347
+ }
348
+
349
+ // 加载中遮罩
350
+ .si-loading-mask {
351
+ position: absolute;
352
+ inset: 0;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ z-index: 10;
357
+ background: rgba(0, 0, 0, 0.5);
358
+
359
+ .si-loading-box {
360
+ background: rgba(0, 0, 0, 0.75);
361
+ padding: 30rpx 48rpx;
362
+ border-radius: 16rpx;
363
+
364
+ .si-loading-text {
365
+ color: #fff;
366
+ font-size: 28rpx;
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ // 底部提示栏(绝对定位在弹窗底部)
373
+ .si-modal-footer {
374
+ position: absolute;
375
+ left: 0;
376
+ right: 0;
377
+ bottom: 0;
378
+ z-index: 10;
379
+ padding: 40rpx 32rpx calc(80rpx + env(safe-area-inset-bottom, 0));
380
+ display: flex;
381
+ flex-direction: column;
382
+ align-items: center;
383
+ gap: 24rpx;
384
+
385
+ .si-tip {
386
+ color: rgba(255, 255, 255, 0.65);
387
+ font-size: 26rpx;
388
+ letter-spacing: 2rpx;
389
+ }
390
+
391
+ .si-close-text {
392
+ color: rgba(255, 255, 255, 0.85);
393
+ font-size: 30rpx;
394
+ padding: 12rpx 48rpx;
395
+ border: 2rpx solid rgba(255, 255, 255, 0.3);
396
+ border-radius: 40rpx;
397
+ }
398
+ }
399
+ }
400
+
401
+ @keyframes si-sweep {
402
+ 0% { top:30%; opacity: 1; }
403
+ 50% { opacity: 1; }
404
+ 100% { top: 70%; opacity: 0.3; }
405
+ }
406
+ </style>