Merge remote-tracking branch 'origin/master'

besure_bit
zhongwenkai 6 days ago
commit 5b5d69d353

@ -0,0 +1,203 @@
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
const androidKeyCodeMap = {
7: '0',
8: '1',
9: '2',
10: '3',
11: '4',
12: '5',
13: '6',
14: '7',
15: '8',
16: '9',
29: 'A',
30: 'B',
31: 'C',
32: 'D',
33: 'E',
34: 'F',
35: 'G',
36: 'H',
37: 'I',
38: 'J',
39: 'K',
40: 'L',
41: 'M',
42: 'N',
43: 'O',
44: 'P',
45: 'Q',
46: 'R',
47: 'S',
48: 'T',
49: 'U',
50: 'V',
51: 'W',
52: 'X',
53: 'Y',
54: 'Z',
55: ',',
56: '.',
69: '-',
70: '=',
71: '[',
72: ']',
73: '\\',
74: ';',
75: "'",
76: '/',
81: '+'
}
export function useScannerInput(options = {}) {
const minLength = Number(options.minLength ?? 3)
const commitDelay = Number(options.commitDelay ?? 120)
const uppercase = options.uppercase !== false
const ignoreInputTarget = options.ignoreInputTarget !== false
const hideKeyboardOnScan = options.hideKeyboardOnScan !== false
const hideKeyboardOnLeave = options.hideKeyboardOnLeave !== false
const onScan = typeof options.onScan === 'function' ? options.onScan : () => {}
let scannerBuffer = ''
let scannerTimer = null
let scannerListening = false
let plusKeyListening = false
function getScannerEventTarget() {
if (typeof document !== 'undefined') return document
if (typeof window !== 'undefined') return window
return null
}
function registerScannerListener() {
if (scannerListening) return
const target = getScannerEventTarget()
if (target?.addEventListener) {
target.addEventListener('keydown', handleScannerKeydown)
scannerListening = true
}
registerPlusKeyListener()
}
function unregisterScannerListener() {
const target = getScannerEventTarget()
if (scannerListening && target?.removeEventListener) {
target.removeEventListener('keydown', handleScannerKeydown)
}
scannerListening = false
unregisterPlusKeyListener()
clearScannerBuffer()
if (hideKeyboardOnLeave) hideSoftKeyboard()
}
function registerPlusKeyListener() {
if (plusKeyListening) return
if (typeof plus === 'undefined' || !plus?.key?.addEventListener) return
plus.key.addEventListener('keydown', handlePlusScannerKeydown)
plusKeyListening = true
}
function unregisterPlusKeyListener() {
if (!plusKeyListening) return
if (typeof plus !== 'undefined' && plus?.key?.removeEventListener) {
plus.key.removeEventListener('keydown', handlePlusScannerKeydown)
}
plusKeyListening = false
}
function handlePlusScannerKeydown(event) {
handleScannerKeydown({ ...event, isPlusKeyEvent: true })
}
function handleScannerKeydown(event) {
if (ignoreInputTarget) {
const targetTag = String(event?.target?.tagName || '').toLowerCase()
if (targetTag === 'input' || targetTag === 'textarea') return
}
const key = normalizeScannerKey(event)
if (!key) return
if (key === 'Enter') {
commitScannerBuffer()
return
}
scannerBuffer += key
if (scannerTimer) clearTimeout(scannerTimer)
scannerTimer = setTimeout(() => {
commitScannerBuffer()
}, commitDelay)
}
function normalizeScannerKey(event) {
if (event?.ctrlKey || event?.altKey || event?.metaKey) return ''
if (isAndroidPlusKeyEvent(event)) return normalizeAndroidKeyCode(event)
const key = event?.key
if (key === 'Enter' || key === 'Tab') return 'Enter'
if (typeof key === 'string' && key.length === 1) return uppercase ? key.toUpperCase() : key
const code = Number(event?.keyCode || event?.which || 0)
if (code === 13 || code === 9) return 'Enter'
if (code >= 48 && code <= 90) {
const value = String.fromCharCode(code)
return uppercase ? value.toUpperCase() : value
}
if (code >= 96 && code <= 105) return String(code - 96)
if (code === 189 || code === 109) return '-'
if (code === 190 || code === 110) return '.'
return ''
}
function isAndroidPlusKeyEvent(event) {
return Boolean(event?.isPlusKeyEvent)
}
function normalizeAndroidKeyCode(event) {
const code = Number(event?.keyCode || event?.which || 0)
if (code === 66 || code === 61 || code === 160) return 'Enter'
const value = androidKeyCodeMap[code] || ''
return uppercase ? value.toUpperCase() : value.toLowerCase()
}
function clearScannerBuffer() {
scannerBuffer = ''
if (scannerTimer) {
clearTimeout(scannerTimer)
scannerTimer = null
}
}
async function commitScannerBuffer() {
const value = scannerBuffer.trim()
clearScannerBuffer()
if (value.length < minLength) return
if (hideKeyboardOnScan) hideSoftKeyboard()
await onScan(value)
}
function hideSoftKeyboard() {
try {
uni.hideKeyboard()
} catch (e) {}
}
onShow(() => {
registerScannerListener()
})
onHide(() => {
unregisterScannerListener()
})
onUnload(() => {
unregisterScannerListener()
})
return {
clearScannerBuffer,
registerScannerListener,
unregisterScannerListener
}
}

@ -7,14 +7,14 @@
<text :class="['line-filter-text', selectedLineId === '' ? 'placeholder' : '']">{{ selectedLineLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
<view class="keyword-wrap">
<view :class="['keyword-wrap', isKeywordInputActive ? 'active' : '']">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('equipmentLedger.searchPlaceholder')"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@blur="handleKeywordBlur"
@tap="handleKeywordTap"
@confirm="handleSearch"
/>
</view>
@ -82,7 +82,7 @@
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ref, computed } from 'vue'
import { onLoad, onPageScroll, onReachBottom, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
@ -90,11 +90,13 @@ import { getDeviceLedgerPage, updateDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
import useDictStore from '@/store/modules/dict'
import { useScannerInput } from '@/hooks/useScannerInput'
const { t } = useI18n()
const dictStore = useDictStore()
const resetFilterText = computed(() => t('functionCommon.reset'))
const searchKeyword = ref('')
const isKeywordInputActive = ref(true)
const selectedStatus = ref('')
const selectedLineId = ref('')
const lineTree = ref([])
@ -110,7 +112,19 @@ const pageSize = ref(10)
const total = ref(0)
const showGoTop = ref(false)
const statusUpdatingMap = ref({})
const keywordFocus = ref(false)
const { clearScannerBuffer } = useScannerInput({
minLength: 3,
commitDelay: 120,
uppercase: true,
hideKeyboardOnScan: true,
hideKeyboardOnLeave: true,
onScan: async (value) => {
searchKeyword.value = value
isKeywordInputActive.value = true
await fetchList(true)
}
})
const statusOptions = computed(() => {
const dicts = dictStore.getDict(DICT_TYPE.MES_TZ_STATUS) || []
@ -142,14 +156,13 @@ const selectedLineLabel = computed(() => {
})
onLoad(async () => {
activateKeywordFocus()
await initAllDict()
await fetchLineTree()
await fetchList(true)
})
onShow(() => {
activateKeywordFocus()
isKeywordInputActive.value = true
})
onReachBottom(() => {
@ -226,11 +239,13 @@ function findLinePath(nodes, id, parents = []) {
return []
}
function activateKeywordFocus() {
keywordFocus.value = false
nextTick(() => {
keywordFocus.value = true
})
function handleKeywordTap() {
isKeywordInputActive.value = true
clearScannerBuffer()
}
function handleKeywordBlur() {
isKeywordInputActive.value = true
}
async function fetchList(reset) {
@ -299,7 +314,6 @@ async function resetFilters() {
lineCascaderValue.value = []
lineCascaderShow.value = false
lineCascaderKey.value += 1
activateKeywordFocus()
await fetchList(true)
}
@ -308,6 +322,7 @@ async function resetLineFilter() {
}
async function handleSearch() {
uni.hideKeyboard()
await fetchList(true)
}
@ -466,6 +481,11 @@ function formatDateValue(value) {
align-items: center;
}
.keyword-wrap.active {
border-color: #2f7dff;
box-shadow: 0 0 0 2rpx rgba(47, 125, 255, 0.12);
}
.keyword-input {
width: 100%;
height: 64rpx;

@ -5,6 +5,7 @@
<view class="filter-bar">
<view class="keyword-box">
<input
id="mold-ledger-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
@ -90,12 +91,14 @@
</scroll-view>
</view>
</uni-popup>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { onLoad, onReady } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import CopyButton from '@/components/common/CopyButton.vue'
@ -116,6 +119,8 @@ const selectedStatus = ref(undefined)
const scrollTop = ref(0)
const showGoTop = ref(false)
const keywordFocus = ref(false)
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#mold-ledger-keyword-input input, input#mold-ledger-keyword-input'
const statusOptions = computed(() => {
const options = []
@ -134,13 +139,12 @@ const selectedStatusLabel = computed(() => {
})
onLoad(async () => {
activateKeywordFocus()
await initAllDict()
await fetchList(true)
})
onShow(() => {
activateKeywordFocus()
onReady(() => {
focusKeywordNoKeyboard()
})
function activateKeywordFocus() {
@ -150,6 +154,14 @@ function activateKeywordFocus() {
})
}
function focusKeywordNoKeyboard() {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 80)
})
}
async function handleSearch() {
await fetchList(true)
}

@ -0,0 +1,9 @@
## 1.0.22025-08-29
1. 更新示例工程
## 1.0.12025-08-29
1. 优化兼容性情况
2. 更新文档
## 1.0.02025-05-26
1. 正式版发布
2. 示例工程发布
3. 文档发布

@ -0,0 +1,100 @@
<template>
<text
:ban="banSelector"
:change:ban="focusNoKeyboard.watchBan"
:focus="focusSelector"
:change:focus="focusNoKeyboard.watchFocus"
></text>
</template>
<script>
export default {
props: {
//
banSelector: {
type: String,
default: ''
}
},
data() {
return {
focusSelector: ''
}
},
methods: {
focus(selector) {
this.focusSelector = selector
},
resetFocus() {
this.focusSelector = ''
}
}
}
</script>
<script module="focusNoKeyboard" lang="renderjs">
export default {
data() {
return { }
},
methods: {
watchFocus(newValue, oldValue, ownerInstance, instance) {
if (newValue) {
this.changeFocus(newValue)
}
},
watchBan(newValue, oldValue, ownerInstance, instance) {
if (newValue) {
this.changeBan(newValue)
}
},
changeFocus(fSelector) {
if (!fSelector) return
const el = document.querySelector(fSelector);
if (!el) return console.warn(`===== input 未渲染完成,请检查 ${fSelector} 是否有误 =====`)
this.changeInputMode(el, 'noKeyboard')
el.focus()
setTimeout(() => {
this.changeInputMode(el, 'hasKeyboard')
})
this.$ownerInstance.callMethod('resetFocus');
},
changeBan(bSelector) {
if (!bSelector) return
this.$nextTick(() => {
const els = document.querySelectorAll(bSelector);
if(els.length > 0) {
els.forEach(el => {
this.changeInputMode(el, 'noKeyboard')
})
}
})
},
/**
* 通过增加或移出inputmode属性来控制是否允许键盘弹出
* @param {String} type hasKeyboard | noKeyboard
* @description 要关闭软键盘的话需要给 inputmode 属性设置 none如果要打开软键盘的话需要移除 inputmode 属性
* @tutorial https://ask.dcloud.net.cn/article/39915
*/
changeInputMode(el, type) {
if (!el) return
//
const isAppleDev = () => {
//
const appleUA = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)
const applePlatform = /(iPhone|iPad|iPod|iOS|Mac)/i.test(navigator.platform)
return appleUA && applePlatform
}
if (type == 'noKeyboard') {
el.setAttribute('inputmode', 'none')
if (isAppleDev()) el.setAttribute('readonly', 'readonly')
}
if (type == 'hasKeyboard') {
el.removeAttribute('inputmode')
if (isAppleDev()) el.removeAttribute('readonly')
}
},
}
}
</script>

@ -0,0 +1,88 @@
{
"id": "sv-focus-no-keyboard",
"displayName": "输入框聚焦且阻止键盘弹出",
"version": "1.0.2",
"description": "使输入框在聚焦的同时,从根本上阻止软键盘弹出,并非是单纯的隐藏键盘",
"keywords": [
"聚焦",
"键盘",
"光标",
"focus",
"keyboard"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n",
"app-uvue": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

@ -0,0 +1,134 @@
# sv-focus-no-keyboard
### 兼容性
✅已兼容,❌未兼容
| VUE2 |VUE3 | Android(APP/H5) | iOS(APP/H5) | 小程序 |
|:---: |:---:| :---: | :---: | :---: |
| ✅ | ✅️ | ✅ | ❌ | ❌ |
- 本插件使用 [renderjs](https://uniapp.dcloud.net.cn/tutorial/renderjs.html#renderjs)
- nvue小程序无法使用 renderjs
- ios 由于固件限制聚焦必弹出键盘,本插件虽然能用,但本质上是通过设置输入框的 readonly 属性进行控制,会导致光标丢失,并非能像安卓那样完美兼容
### props
| 属性名 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| banSelector | String | | 禁止软键盘弹出的输入框 选择器,可见示例 |
`被 banSelector 禁止的输入框,将永久不再弹出软键盘,除非通过下列 focus 方法重新主动聚焦`
### event
| 事件名 | 参数 | 说明 |
| :--- | :--- | :--- |
| focus | selector: String| 指定 选择器 的输入框聚焦,且禁止软键盘弹出,可见示例 |
`focus 不影响用户手动点击输入框的聚焦行为,即用户仍可手动点击输入框进行聚焦并弹出软键盘`
### 使用示例
```
<template>
<view class="index-page">
<!-- 输入框 -->
<view>1. 普通组件输入框,聚焦伴随键盘弹出</view>
<uni-easyinput placeholder="请输入内容"></uni-easyinput>
<view style="margin-top: 20px">2. 处理后的组件输入框,主动聚焦后可阻止键盘弹出</view>
<uni-easyinput id="fnk-input" placeholder="请输入内容"></uni-easyinput>
<button size="mini" @click="focusNoKeyboard('#fnk-input input')">主动聚焦</button>
<view style="margin-top: 20px">3. 组件输入框,聚焦永久阻止键盘弹出</view>
<uni-easyinput id="fnk-always" placeholder="请输入内容"></uni-easyinput>
<view style="margin-top: 20px; color: red">
注意在vue2下部分非原生input标签如uni-easyinput插件由于内部特殊处理可能无法兼容阻止键盘弹出的问题
但是该问题在vue3下可能不会出现所以建议使用vue3或者尽量使用原生input等标签
</view>
<hr style="margin-top: 20px" />
<view style="margin-top: 20px">4. 普通原生输入框,聚焦伴随键盘弹出</view>
<input class="input-native" placeholder="请输入内容" />
<view style="margin-top: 20px">5. 处理后的原生输入框,主动聚焦后可阻止键盘弹出</view>
<input class="input-native" id="fnk-input-native" placeholder="请输入内容" />
<button size="mini" @click="focusNoKeyboard('#fnk-input-native input')">
主动聚焦
</button>
<view style="margin-top: 20px">6. 原生输入框,聚焦永久阻止键盘弹出</view>
<input id="fnk-always-native" class="input-native" placeholder="请输入内容" />
<hr style="margin-top: 20px" />
<!-- 文本域 -->
<view style="margin-top: 20px">7. 原生文本域,主动聚焦后可阻止键盘弹出</view>
<textarea id="textarea-box" placeholder="请输入内容"></textarea>
<button size="mini" @click="focusNoKeyboard('#textarea-box textarea')">
主动聚焦
</button>
<hr style="margin-top: 20px" />
<!-- 富文本编辑器 -->
<view style="margin-top: 20px">
8. 富文本编辑器(无法永久阻止键盘弹出,只推荐主动聚焦后可阻止键盘弹出;
但是在聚焦过其他标签之后重新聚焦回editor会导致光标初始到开始位置
</view>
<editor id="editor-box" placeholder="请输入内容"></editor>
<button size="mini" @click="focusNoKeyboard('#editor-box .ql-editor')">
主动聚焦
</button>
<hr style="margin-top: 20px" />
<!-- 注意 -->
<view style="margin-top: 20px; color: red">
注意必须指定给到实际可以聚焦的dom盒子
实际的inputtextarea标签或者editor组件下的.ql-editor盒子等
</view>
<!-- 使用 uni_modules 插件 -->
<sv-focus-no-keyboard
banSelector="#fnk-always input, #fnk-always-native input"
ref="fnkRef"
></sv-focus-no-keyboard>
</view>
</template>
<script>
export default {
data() {
return {}
},
methods: {
focusNoKeyboard(focusSelector) {
this.$refs.fnkRef.focus(focusSelector)
}
}
}
</script>
```
### 注意
1. 必须指定给到实际可以聚焦的dom盒子实际的inputtextarea标签或者editor组件下的.ql-editor盒子等
2. 在vue2下部分非原生input标签如uni-easyinput插件由于内部特殊处理可能无法兼容阻止键盘弹出的问题但是该问题在vue3下可能不会出现所以建议使用vue3或者尽量使用原生input等标签
### 结语
本插件免费开源,如若借鉴源码还请注明出处,未经授权禁止转载售卖等侵犯版权行为,谢谢!
感谢您使用本插件,如果在使用过程中遇到任何问题,欢迎在评论区留言或加群讨论,制作不易,还望五星好评 🌟🌟🌟🌟🌟
欢迎进群🍌交流🐧Q群
- ①群852637893
- ②群816646292
- ③群704990626
💬WX群可通过🐧Q群进入
Loading…
Cancel
Save