wui-components-v2 1.1.56 → 1.1.57

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.
@@ -11,6 +11,7 @@ import CustomDatePicker from '../custom-date-picker/custom-date-picker.vue'
11
11
  import addAddressPage from '../add-address-page/add-address-page.vue'
12
12
  import userChoose from '../user-choose/user-choose.vue'
13
13
  import { generateHighResolutionID } from '../../utils/index'
14
+ import scanInput from '../scan-input/scan-input.vue'
14
15
  // import { enums } from '../../api/page'
15
16
  defineOptions({
16
17
  name: 'FormControl',
@@ -91,6 +92,12 @@ function initFormData() {
91
92
  return models[item.sourceId] = (props.entity && dayjs(props.entity[item.sourceId]).valueOf()) || (item.transDefaultValue && dayjs(item.transDefaultValue).valueOf()) || null
92
93
  }
93
94
 
95
+ // 评分
96
+ if (ControlTypeSupportor.getControlType(item, props.entity && props.entity[item.sourceId]) === 'progress') {
97
+ const val = props.entity?.[item.sourceId]
98
+ return models[item.sourceId] = (val !== undefined && val !== null && val !== '') ? Number(val) : 0
99
+ }
100
+
94
101
  return models[item.sourceId] = (props.entity && props.entity[item.sourceId]) || item.transDefaultValue || ''
95
102
  })
96
103
 
@@ -480,6 +487,22 @@ defineExpose({
480
487
  <wd-input-number v-model="model[item.sourceId]" :disabled="item.disabled || item.rowEditType === 'readonly'" :min="Number(item.min || 0)" :max="Number(item.max || Infinity)" />
481
488
  </view>
482
489
  </wd-form-item>
490
+ <wd-form-item
491
+ v-else-if="ControlTypeSupportor.getControlType(item, props.entity && props.entity[item.sourceId]) === 'QRCode'"
492
+ :class="{ 'no-border-top': fields.indexOf(item) === 0 }"
493
+ :prop="item.sourceId"
494
+ :title="item.title"
495
+ >
496
+ <scanInput v-model="model[item.sourceId]" :disabled="item.disabled || item.rowEditType === 'readonly'" />
497
+ </wd-form-item>
498
+ <wd-form-item
499
+ v-else-if="ControlTypeSupportor.getControlType(item, props.entity && props.entity[item.sourceId]) === 'progress'"
500
+ :class="{ 'no-border-top': fields.indexOf(item) === 0 }"
501
+ :prop="item.sourceId"
502
+ :title="item.title"
503
+ >
504
+ <wd-rate v-model="model[item.sourceId]" :disabled="item.disabled || item.rowEditType === 'readonly'" />
505
+ </wd-form-item>
483
506
  <!-- done -->
484
507
  <wd-form-item
485
508
  v-else-if="ControlTypeSupportor.getControlType(item, props.entity && props.entity[item.sourceId]) === 'yes-no-switch'"
@@ -1,34 +1,72 @@
1
1
  <script setup lang="ts">
2
- import { ref, onMounted, onUnmounted, nextTick } from 'vue'
2
+ import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
3
3
 
4
4
  defineOptions({
5
5
  name: 'ScanInput',
6
6
  })
7
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 ----
8
17
  const props = withDefaults(defineProps<{
18
+ /** v-model 绑定值 */
19
+ modelValue?: string
20
+ /** 自动启动扫码 */
9
21
  autoStart?: boolean
22
+ /** 占位文本 */
10
23
  placeholder?: string
24
+ /** 是否禁用 */
25
+ disabled?: boolean
26
+ /** 扫码成功后自动关闭弹窗 (H5) */
27
+ autoCloseOnSuccess?: boolean
28
+ /** 扫码成功后是否展示 toast 提示 */
29
+ showSuccessToast?: boolean
11
30
  }>(), {
31
+ modelValue: '',
12
32
  autoStart: false,
13
33
  placeholder: '请输入或扫描二维码/条形码',
34
+ disabled: false,
35
+ autoCloseOnSuccess: true,
36
+ showSuccessToast: true,
14
37
  })
15
38
 
16
39
  const emit = defineEmits<{
40
+ (e: 'update:modelValue', val: string): void
17
41
  (e: 'scan', data: string): void
18
42
  (e: 'input', data: string): void
19
43
  (e: 'error', error: string): void
20
44
  }>()
21
45
 
22
- const inputValue = ref('')
46
+ // ---- 状态 ----
47
+ const inputValue = ref(props.modelValue)
23
48
  const isScanning = ref(false)
24
49
  const showScanModal = ref(false)
25
50
  const isLoading = ref(false)
26
- let html5QrcodeScanner: any = null
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 })
27
63
 
64
+ // ======================== H5 平台扫码 ========================
28
65
  // #ifdef H5
29
66
  import { Html5Qrcode } from 'html5-qrcode'
30
67
 
31
- function checkCameraEnvironment(): { ok: boolean, reason?: string } {
68
+ /** 环境检测 */
69
+ function checkCameraEnvironment(): { ok: boolean; reason?: string } {
32
70
  const hostname = window.location.hostname
33
71
  if (hostname === 'localhost' || hostname === '127.0.0.1') return { ok: true }
34
72
  if (window.location.protocol !== 'https:') {
@@ -38,7 +76,7 @@ function checkCameraEnvironment(): { ok: boolean, reason?: string } {
38
76
  }
39
77
 
40
78
  async function startScan() {
41
- if (isScanning.value) return
79
+ if (isScanning.value || scanLocked) return
42
80
  const envCheck = checkCameraEnvironment()
43
81
  if (!envCheck.ok) {
44
82
  uni.showToast({ title: envCheck.reason || '环境不支持', icon: 'none' })
@@ -48,40 +86,48 @@ async function startScan() {
48
86
  isLoading.value = true
49
87
  isScanning.value = true
50
88
  showScanModal.value = true
89
+ scanLocked = false
51
90
 
52
91
  try {
53
92
  await nextTick()
54
- // 减少延时,库已打包进 bundle 无需等待加载
55
93
  await new Promise(r => setTimeout(r, 200))
56
94
 
57
95
  const container = document.getElementById('h5-scan-reader')
58
- if (!container) throw new Error('容器未找到')
96
+ if (!container) throw new Error('扫码容器未找到')
59
97
 
60
- html5QrcodeScanner = new Html5Qrcode('h5-scan-reader')
98
+ html5QrcodeScanner = new Html5Qrcode('h5-scan-reader') as unknown as Html5QrcodeInstance
61
99
 
62
100
  await html5QrcodeScanner.start(
63
101
  { facingMode: 'environment' },
64
- {
65
- fps: 5,
66
- // 不设置 qrbox,全屏显示摄像头画面
67
- },
102
+ { fps: 5 },
68
103
  (decodedText: string) => {
69
- console.log('扫码成功:', decodedText)
70
- // 防抖:短时间内多次触发只处理第一次
71
- if (!isScanning.value) return
104
+ if (scanLocked || !isScanning.value) return
105
+ scanLocked = true // 加锁防止重复触发
106
+
107
+ console.log('[ScanInput] 扫码成功:', decodedText)
72
108
  inputValue.value = decodedText
73
109
  emit('scan', decodedText)
74
- emit('input', decodedText)
75
- stopScan()
76
- uni.showToast({ title: '扫描成功', icon: 'success' })
110
+ if (props.showSuccessToast) uni.showToast({ title: '扫描成功', icon: 'success' })
111
+ if (props.autoCloseOnSuccess) stopScan()
77
112
  },
78
- () => {}, // 忽略中间帧错误
113
+ () => {}, // 忽略中间帧未识别错误
79
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
+ }
80
126
  }
81
- catch (error: any) {
82
- console.error('扫码启动失败:', error)
127
+ catch (error: unknown) {
128
+ console.error('[ScanInput] 启动失败:', error)
129
+ const errMsg = error instanceof Error ? error.message : String(error)
83
130
  let msg = '摄像头访问失败'
84
- const errMsg = error.message || ''
85
131
  if (/Permission|NotAllowed/i.test(errMsg)) msg = '请允许浏览器使用摄像头'
86
132
  else if (/NotFound|not found/i.test(errMsg)) msg = '未检测到摄像头设备'
87
133
  else if (/secure origin|insecure/i.test(errMsg)) msg = '需要HTTPS环境才能使用摄像头'
@@ -99,24 +145,25 @@ async function startScan() {
99
145
  async function stopScan() {
100
146
  isScanning.value = false
101
147
  showScanModal.value = false
148
+ scanLocked = false
102
149
  if (html5QrcodeScanner) {
103
150
  try {
104
151
  if (html5QrcodeScanner.isScanning) await html5QrcodeScanner.stop()
105
- html5QrcodeScanner.clear()
152
+ await html5QrcodeScanner.clear()
106
153
  }
107
- catch { /* ignore */ }
154
+ catch { /* ignore cleanup error */ }
108
155
  html5QrcodeScanner = null
109
156
  }
110
157
  }
111
158
  // #endif
112
159
 
160
+ // ======================== 非 H5 平台扫码 ========================
113
161
  function startUniScanCode() {
114
162
  // #ifndef H5
115
163
  uni.scanCode({
116
- success(res: any) {
164
+ success(res: UniApp.ScanCodeSuccessRes) {
117
165
  inputValue.value = res.result
118
166
  emit('scan', res.result)
119
- emit('input', res.result)
120
167
  uni.showToast({ title: '扫描成功', icon: 'success' })
121
168
  },
122
169
  fail() { emit('error', '扫描已取消') },
@@ -124,7 +171,9 @@ function startUniScanCode() {
124
171
  // #endif
125
172
  }
126
173
 
174
+ // ======================== 公共方法 ========================
127
175
  function handleStartScan() {
176
+ if (props.disabled) return
128
177
  // #ifdef H5
129
178
  isScanning.value ? stopScan() : startScan()
130
179
  // #endif
@@ -133,17 +182,30 @@ function handleStartScan() {
133
182
  // #endif
134
183
  }
135
184
 
185
+ /** 点击遮罩层关闭 (仅 H5) */
186
+ function handleModalMaskClick() {
187
+ // #ifdef H5
188
+ stopScan()
189
+ // #endif
190
+ }
191
+
136
192
  function handleConfirm() {
137
- if (inputValue.value.trim()) emit('scan', inputValue.value.trim())
193
+ const val = inputValue.value.trim()
194
+ if (val) emit('scan', val)
138
195
  }
139
196
 
140
- onMounted(() => { if (props.autoStart) handleStartScan() })
141
- onUnmounted(() => { /* #ifdef H5 */ stopScan() /* #endif */ })
197
+ // ---- 生命周期 ----
198
+ onMounted(() => { if (props.autoStart && !props.disabled) handleStartScan() })
199
+ onUnmounted(() => {
200
+ // #ifdef H5
201
+ stopScan()
202
+ // #endif
203
+ })
142
204
  </script>
143
205
 
144
206
  <template>
145
207
  <view class="si-wrap">
146
- <!-- 使用 wot-ui wd-input -->
208
+ <!-- 输入框 -->
147
209
  <wd-input
148
210
  v-model="inputValue"
149
211
  class="!pr-3"
@@ -152,37 +214,43 @@ onUnmounted(() => { /* #ifdef H5 */ stopScan() /* #endif */ })
152
214
  confirm-type="search"
153
215
  clearable
154
216
  no-border
217
+ :disabled="disabled"
155
218
  custom-class="si-wd-input"
156
219
  @confirm="handleConfirm"
157
- @change="(val: string) => emit('input', val)"
158
220
  suffix-icon="scan"
159
- @clicksuffixicon="handleStartScan"
160
- >
161
- </wd-input>
221
+ @clicksuffixicon="handleStartScan"
222
+ />
162
223
 
163
224
  <!-- #ifdef H5 -->
164
- <!-- 全屏扫码遮罩 -->
165
- <view v-if="showScanModal" class="si-modal" catchtouchmove>
225
+ <!-- 全屏扫码弹窗 -->
226
+ <view
227
+ v-if="showScanModal"
228
+ class="si-modal"
229
+ catchtouchmove
230
+ @click="handleModalMaskClick"
231
+ >
166
232
  <!-- 摄像头区域 -->
167
- <view class="si-camera-area">
233
+ <view class="si-camera-area" @click.stop>
168
234
  <!-- 加载中 -->
169
235
  <view v-if="isLoading" class="si-loading-mask">
170
236
  <view class="si-loading-box">
171
237
  <text class="si-loading-text">正在启动摄像头...</text>
172
238
  </view>
173
239
  </view>
174
- <!-- 渲染容器 -->
175
- <div id="h5-scan-reader" class="si-reader" />
176
- <!-- 扫描框装饰 -->
177
- <view v-show="isScanning && !isLoading" class="si-scan-frame">
178
- <view class="si-scan-line" />
179
- </view>
180
- </view>
181
240
 
182
- <!-- 底部提示 -->
183
- <view class="si-modal-footer">
184
- <text class="si-tip">识别二维码 / 条形码 / 商品码等</text>
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>
185
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
186
254
  </view>
187
255
  <!-- #endif -->
188
256
  </view>
@@ -191,17 +259,18 @@ onUnmounted(() => { /* #ifdef H5 */ stopScan() /* #endif */ })
191
259
  <!-- 全局样式:html5-qrcode 内部元素 -->
192
260
  <style lang="scss">
193
261
  #h5-scan-reader {
194
- width: 100%;
195
- height: 100%;
196
- border-radius: inherit;
197
- display: flex;
262
+ width: 100% !important;
263
+ height: 100vh !important;
198
264
 
199
265
  video {
200
- border-radius: inherit;
201
- object-fit: cover;
266
+ width: 100% !important;
267
+ height: 100vh !important;
268
+ object-fit: cover !important;
269
+ object-position: center !important;
202
270
  }
203
271
 
204
- #qr-shaded-region {
272
+ #qr-shaded-region,
273
+ #html5-qrcode-anchor-scan-region {
205
274
  border-radius: inherit;
206
275
  }
207
276
  }
@@ -210,76 +279,85 @@ onUnmounted(() => { /* #ifdef H5 */ stopScan() /* #endif */ })
210
279
  <style scoped lang="scss">
211
280
  .si-wrap {
212
281
  width: 100%;
213
- padding: 16rpx;
214
282
  box-sizing: border-box;
215
283
 
216
- // ---- wot-ui 输入框容器 ----
284
+ // ---- 输入框容器 ----
217
285
  :deep(.si-wd-input) {
218
- background: #f6f7f9;
286
+ background: transparent;
219
287
  border-radius: 10rpx;
220
- border: 2rpx solid #eaecef;
288
+ border: none;
221
289
  padding-right: 4rpx;
290
+
291
+ &.is-disabled {
292
+ opacity: 0.6;
293
+ }
222
294
  }
223
295
 
224
- // ---- 全屏扫码遮罩 ----
296
+ // ---- 全屏扫码弹窗 ----
225
297
  .si-modal {
226
298
  position: fixed;
227
- top: 0; left: 0; right: 0; bottom: 0;
299
+ inset: 0;
228
300
  z-index: 999;
229
301
  background: #000;
230
- display: flex;
231
- flex-direction: column;
232
302
  }
233
303
 
234
304
  .si-camera-area {
235
- flex: 1;
236
305
  width: 100%;
306
+ height: 100%;
237
307
  position: relative;
238
308
  overflow: hidden;
239
309
 
240
- .si-reader {
310
+ // 摄像头包装容器(全屏)
311
+ .si-reader-wrap {
241
312
  position: absolute;
242
- top: 50%;
243
- left: 0;
244
- transform: translateY(-50%);
313
+ inset: 0;
314
+ overflow: hidden;
315
+ }
316
+
317
+ // 摄像头渲染容器
318
+ .si-reader {
245
319
  width: 100%;
246
- height: 50vh;
320
+ height: 100%;
247
321
  }
248
322
 
249
- // ---- 扫描框(覆盖在 reader 上方) ----
323
+ // 扫描动画线(全屏时收缩在视频可视区域中间)
250
324
  .si-scan-frame {
251
325
  position: absolute;
252
- top: 50%;
326
+ top: 0;
327
+ bottom: 0;
253
328
  left: 0;
254
- transform: translateY(-50%);
255
- width: 100%;
256
- height: 50vh;
329
+ right:0;
257
330
  pointer-events: none;
258
331
  z-index: 5;
259
332
  overflow: hidden;
333
+ width:100vw;
334
+ height: 100vh;
260
335
 
261
336
  .si-scan-line {
262
337
  position: absolute;
263
- top: 0;
338
+ top: 2%;
264
339
  left: 10rpx;
265
340
  right: 10rpx;
266
341
  height: 6rpx;
267
- background: #07C160;
342
+ background: #07c160;
268
343
  border-radius: 3rpx;
269
- box-shadow: 0 0 20rpx 4rpx rgba(7,193,96,0.7);
344
+ box-shadow: 0 0 20rpx 4rpx rgba(7, 193, 96, 0.7);
270
345
  animation: si-sweep 2.5s ease-in-out infinite;
271
346
  }
272
347
  }
273
348
 
349
+ // 加载中遮罩
274
350
  .si-loading-mask {
275
351
  position: absolute;
276
- top: 50%;
277
- left: 50%;
278
- transform: translate(-50%, -50%);
352
+ inset: 0;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
279
356
  z-index: 10;
357
+ background: rgba(0, 0, 0, 0.5);
280
358
 
281
359
  .si-loading-box {
282
- background: rgba(0,0,0,0.75);
360
+ background: rgba(0, 0, 0, 0.75);
283
361
  padding: 30rpx 48rpx;
284
362
  border-radius: 16rpx;
285
363
 
@@ -291,22 +369,38 @@ onUnmounted(() => { /* #ifdef H5 */ stopScan() /* #endif */ })
291
369
  }
292
370
  }
293
371
 
372
+ // 底部提示栏(绝对定位在弹窗底部)
294
373
  .si-modal-footer {
295
- padding: 40rpx 32rpx 80rpx;
296
- text-align: center;
297
- flex-shrink: 0;
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;
298
384
 
299
385
  .si-tip {
300
- color: rgba(255,255,255,0.65);
386
+ color: rgba(255, 255, 255, 0.65);
301
387
  font-size: 26rpx;
302
388
  letter-spacing: 2rpx;
303
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
+ }
304
398
  }
305
399
  }
306
400
 
307
401
  @keyframes si-sweep {
308
- 0% { top: 0; opacity: 1; }
402
+ 0% { top:30%; opacity: 1; }
309
403
  50% { opacity: 1; }
310
- 100% { top: calc(100% - 4rpx); opacity: 0.3; }
404
+ 100% { top: 70%; opacity: 0.3; }
311
405
  }
312
406
  </style>
@@ -100,8 +100,9 @@ const emitIfNeeded = (val: string) => {
100
100
  }
101
101
 
102
102
  onMounted(() => {
103
- if (currentValue.value) {
104
- const requestId= currentValue.value.includes('$$user.')? currentValue.value.split('$$user.')[1] : currentValue.value
103
+ const raw = currentValue.value
104
+ if (raw && typeof raw === 'string') {
105
+ const requestId = raw.includes('$$user.') ? raw.split('$$user.')[1] : raw
105
106
  fetchDataById(requestId)
106
107
  }
107
108
 
@@ -99,8 +99,8 @@ function setting() {
99
99
  /> -->
100
100
  <!-- <wd-cell title="修改密码" icon="keywords" :is-link="true" :clickable="true" /> -->
101
101
  <wd-cell title="系统设置" icon="setting" :is-link="true" :clickable="true" @click="setting" />
102
- <wd-cell title="退出登录" icon="logout" :is-link="true" :clickable="true" @click="quit" />
103
102
  <wd-cell title="清空缓存" :is-link="true" :clickable="true" @click="clearCache" />
103
+ <wd-cell title="退出登录" icon="logout" :is-link="true" :clickable="true" @click="quit" />
104
104
  </wd-cell-group>
105
105
  <wd-dialog />
106
106
  </view>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wui-components-v2",
3
- "version": "1.1.56",
3
+ "version": "1.1.57",
4
4
  "description": "wui 组件库",
5
5
  "author": "wgxshh",
6
6
  "license": "MIT",
package/type.ts CHANGED
@@ -59,6 +59,7 @@ export interface Config extends Groups {
59
59
  defaultCriteriaValue?: { [key: string]: string }
60
60
  split2TabCriterias?: Split2TabCriterias[]
61
61
  extShowConfig?: { [key: string]: any }
62
+ extDisplayConfig?: { [key: string]: any }
62
63
  }
63
64
 
64
65
  // 列表数据
@@ -82,6 +82,9 @@ SupportInputTypes.add('ltree-entity-select-value')
82
82
  SupportInputTypes.add('ltree-entity-select')
83
83
  SupportInputTypes.add('field-history')
84
84
  SupportInputTypes.add('relfile')
85
+ SupportInputTypes.add('QRCode')
86
+ SupportInputTypes.add('progress')
87
+
85
88
 
86
89
  const CUSTOM_VIEW_CONTROL_MAP: { [key: string]: string } = {}
87
90
 
@@ -139,6 +142,7 @@ ControlTypeSupportor.getControlType = function (fieldConfig: Fields, fieldValue?
139
142
  if (fieldValue && Array.isArray(fieldValue) && itemType === 'relselect' && fieldValue.length > 0 && fieldValue[0].includes('valid')) {
140
143
  itemType = 'file'
141
144
  }
145
+ console.log('itemType', itemType)
142
146
  return itemType
143
147
  }
144
148