消毒机设备
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
8.5 KiB

4 weeks ago
2 months ago
4 weeks ago
4 weeks ago
2 months ago
4 weeks ago
4 weeks ago
4 weeks ago
2 months ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
2 months ago
2 months ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
2 months ago
4 weeks ago
2 months ago
4 weeks ago
2 months ago
  1. <script lang="ts" setup>
  2. import type { Ref } from 'vue'
  3. import pinyinDict from 'libs/pinyinDict.json'
  4. import { computed, defineEmits, defineProps, onMounted, ref, watch, watchEffect } from 'vue'
  5. const props = defineProps<{
  6. modelValue: string
  7. keyboardType: 'text' | 'number'
  8. isVisible: boolean
  9. }>()
  10. const emits = defineEmits<{
  11. (e: 'update:modelValue', value: string): void
  12. (e: 'updateKeyboardVisible', value: boolean): void
  13. (e: 'confirm', value: string): void
  14. (e: 'close'): void
  15. }>()
  16. const languageType = ref('en')
  17. const inputValue = ref(props.modelValue)
  18. const cnList = ref<string[]>([])
  19. const pinyinMap: Record<string, string[]> = pinyinDict
  20. const pinyinValue = ref('')
  21. // 拖动相关状态
  22. const isDragging = ref(false)// 是否正在拖动
  23. const startX = ref(0)// 触摸起始 X
  24. const startY = ref(0)// 触摸起始 Y
  25. const x = ref(0)// 容器偏移 X
  26. const y = ref(0)// 容器偏移 Y
  27. const keyboardRef = ref() as Ref<HTMLDivElement> // 软键盘容器 DOM
  28. onMounted(() => {
  29. document.addEventListener('click', (e: any) => {
  30. if (isOpen.value && !e.target?.name) {
  31. isOpen.value = false
  32. emits('updateKeyboardVisible', false)
  33. }
  34. })
  35. })
  36. const isOpen = ref(false)
  37. watchEffect(() => {
  38. // 在焦点内二次点击时不触发EventListener,做一下延迟处理
  39. setTimeout(() => {
  40. isOpen.value = props.isVisible
  41. }, 100)
  42. inputValue.value = props.modelValue
  43. })
  44. const activeKey = ref('')
  45. const keyboardLayout = computed(() => {
  46. if (props.keyboardType === 'number') {
  47. return [
  48. ['1', '2', '3'],
  49. ['4', '5', '6'],
  50. ['7', '8', '9'],
  51. ['.', '0', 'del'],
  52. ]
  53. }
  54. return [
  55. ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'del'],
  56. ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '/'],
  57. ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 'enter'],
  58. ['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '?', '.', ':'],
  59. ['close', ' ', 'en'],
  60. ]
  61. })
  62. const specialKeys = ['del', 'enter', ' ']
  63. const handleKeyCn = (cn: string) => {
  64. emits('update:modelValue', props.modelValue + cn)
  65. cnList.value = []
  66. pinyinValue.value = ''
  67. }
  68. const handleKeyPress = (key: string) => {
  69. activeKey.value = key
  70. setTimeout(() => {
  71. activeKey.value = ''
  72. }, 150)
  73. if (key === 'del') {
  74. if (props.keyboardType === 'text' && languageType.value === 'cn' && pinyinValue.value) {
  75. pinyinValue.value = pinyinValue.value.slice(0, -1)
  76. // 中文
  77. onHandlePinyinToCn(pinyinValue.value)
  78. }
  79. else {
  80. emits('update:modelValue', props.modelValue.slice(0, -1))
  81. }
  82. }
  83. else if (key === 'enter') {
  84. emits('confirm', props.modelValue)
  85. closeKeyboard()
  86. }
  87. else if (key === 'close') {
  88. closeKeyboard()
  89. }
  90. else if ((key === 'en' || key === 'cn') && props.keyboardType === 'text') {
  91. languageType.value = key === 'en' ? 'cn' : 'en'
  92. keyboardLayout.value[4][2] = key === 'en' ? 'cn' : 'en'
  93. cnList.value = []
  94. }
  95. else {
  96. if (props.keyboardType === 'text' && languageType.value === 'cn') {
  97. // 中文
  98. pinyinValue.value = pinyinValue.value + key
  99. onHandlePinyinToCn(pinyinValue.value)
  100. }
  101. else {
  102. emits('update:modelValue', props.modelValue + key)
  103. }
  104. }
  105. }
  106. const onHandlePinyinToCn = (keyValue: string) => {
  107. const cn: string[] = pinyinMap[keyValue]
  108. if (cn && cn.length) {
  109. cnList.value = cn
  110. }
  111. else {
  112. cnList.value = []
  113. }
  114. }
  115. const closeKeyboard = () => {
  116. isOpen.value = false
  117. emits('close')
  118. }
  119. watch(() => props.isVisible, (newVal) => {
  120. isOpen.value = newVal
  121. })
  122. // 触摸开始:记录初始位置
  123. const handleTouchStart = (e: TouchEvent) => {
  124. isDragging.value = true
  125. const touch = e.touches[0]
  126. startX.value = touch.clientX - x.value
  127. startY.value = touch.clientY - y.value
  128. }
  129. // 触摸移动:计算偏移量
  130. const handleTouchMove = (e: TouchEvent) => {
  131. if (isDragging.value) {
  132. const touch = e.touches[0]
  133. x.value = touch.clientX - startX.value
  134. y.value = touch.clientY - startY.value
  135. }
  136. }
  137. // 触摸结束:停止拖动
  138. const handleTouchEnd = () => {
  139. isDragging.value = false
  140. }
  141. </script>
  142. <template>
  143. <div v-if="isOpen" class="soft-keyboard" :class="{ 'keyboard-open': isOpen }">
  144. <!-- <div class="keyboard-header">
  145. <button @click="closeKeyboard">
  146. 关闭键盘
  147. </button>
  148. </div> -->
  149. <div
  150. ref="keyboardRef"
  151. class="keyboard-container keyboard-body"
  152. @touchstart="handleTouchStart"
  153. @touchmove="handleTouchMove"
  154. @touchend="handleTouchEnd"
  155. :style="{
  156. transform: `translate(${x}px, ${y}px)`,
  157. transition: isDragging ? 'none' : 'transform 0.3s ease',
  158. width: keyboardType === 'number' ? '50vw' : '',
  159. height: keyboardType === 'number' ? '45vh' : '50vh',
  160. }"
  161. >
  162. <div>
  163. <div v-if="keyboardType === 'text'" class="pinyin-container">
  164. <span v-if="pinyinValue" style="font-size:12px">拼音{{ pinyinValue }}</span>
  165. <div v-if="cnList && cnList.length" class="pinyin-cn">
  166. <div
  167. v-for="(cnName, cnIndex) in cnList"
  168. :key="cnIndex"
  169. class="cn-name"
  170. @click="(e) => { e.stopPropagation(); handleKeyCn(cnName) }">
  171. {{ cnName }}
  172. </div>
  173. </div>
  174. </div>
  175. <div v-for="(row, index) in keyboardLayout" :key="index" class="keyboard-row">
  176. <button
  177. v-for="(key, keyIndex) in row"
  178. :key="keyIndex"
  179. :class="{
  180. 'key-space': key === ' ',
  181. 'key-special': specialKeys.includes(key),
  182. 'key-active': activeKey === key,
  183. 'key-number': keyboardType === 'number',
  184. 'key-text': key !== ' ' && keyboardType === 'text',
  185. }"
  186. :style="keyboardType === 'number' ? 'height: 10vh' : 'height:4rem'"
  187. @click="(e) => {
  188. e.stopPropagation()
  189. handleKeyPress(key)
  190. }"
  191. >
  192. {{ key === ' ' ? '空格' : key === 'del' ? '删除' : key === 'enter' ? '确认' : key === 'close' ? '关闭' : key === 'cn' ? '英文' : key === 'en' ? '拼音' : key }}
  193. </button>
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. </template>
  199. <style lang="scss" scoped>
  200. .soft-keyboard {
  201. bottom: -300px;
  202. left: 0;
  203. right: 0;
  204. background-color: #f5f5f5;
  205. border-top: 1px solid #ddd;
  206. padding: 10px;
  207. transition: bottom 0.3s ease;
  208. user-select: none;
  209. z-index: 9999;
  210. }
  211. .keyboard-open {
  212. bottom: 0;
  213. }
  214. .keyboard-header {
  215. margin-bottom: 10px;
  216. position: absolute;
  217. float:right;
  218. }
  219. .keyboard-header button {
  220. padding: 8px 15px;
  221. background-color: #e0e0e0;
  222. border: none;
  223. border-radius: 5px;
  224. cursor: pointer;
  225. }
  226. .keyboard-body {
  227. display: flex;
  228. flex-direction: column;
  229. gap: 8px;
  230. .pinyin-container{
  231. display: flex;
  232. width: 80%;
  233. height: 4rem;
  234. .pinyin-cn{
  235. display: flex;
  236. width: 1rem;
  237. position: relative;
  238. gap:5px;
  239. .cn-name{
  240. font-size: 2.5rem;
  241. }
  242. }
  243. }
  244. }
  245. .keyboard-row {
  246. display: flex;
  247. justify-content: center;
  248. gap: 5px;
  249. }
  250. .keyboard-row button {
  251. min-width: 50px;
  252. font-size: 18px;
  253. border: none;
  254. border-radius: 5px;
  255. background-color: #fff;
  256. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  257. cursor: pointer;
  258. transition: all 0.2s;
  259. height: 10vh;
  260. }
  261. .keyboard-row button:hover {
  262. /* background-color: #e0e0e0; */
  263. }
  264. .keyboard-row button:active {
  265. transform: scale(0.95);
  266. }
  267. .key-space {
  268. margin-top: 3.5px;
  269. width: 75vw;;
  270. }
  271. .key-special {
  272. background-color: #e0e0e0;
  273. }
  274. .key-active {
  275. background-color: #a0c4ff;
  276. transform: scale(0.95);
  277. }
  278. .key-number{
  279. width: 30vw;
  280. height: 6vh;
  281. margin: 5px;
  282. }
  283. .key-text {
  284. width: 8vw;
  285. height: 5vh;
  286. margin: 5px;
  287. }
  288. .input-w{
  289. width: 20%;
  290. height: 4rem;
  291. font-size: 2rem;
  292. margin-left: 3rem;
  293. }
  294. .keyboard-container {
  295. position: fixed;
  296. bottom: 0;
  297. left: 0;
  298. width: 90%;
  299. border-radius: 16px;
  300. box-shadow: 0 -4px 12px rgba(0,0,0,0.1);
  301. overflow: hidden;
  302. z-index: 9999;
  303. /* 让拖动更顺滑 */
  304. will-change: transform;
  305. touch-action: none; /* 禁止浏览器默认触摸行为(如滚动) */
  306. background: #c8c8c8;
  307. }
  308. .input-box {
  309. padding: 16px;
  310. background: #f5f5f5;
  311. }
  312. .input-field {
  313. width: 100%;
  314. padding: 12px;
  315. text-align: right;
  316. font-size: 18px;
  317. border: 1px solid #ddd;
  318. border-radius: 8px;
  319. background: #fff;
  320. }
  321. .keys {
  322. display: grid;
  323. grid-template-columns: repeat(3, 1fr);
  324. }
  325. .key {
  326. height: 60px;
  327. display: flex;
  328. justify-content: center;
  329. align-items: center;
  330. font-size: 20px;
  331. border: 1px solid #eee;
  332. background: #fff;
  333. touch-action: manipulation; /* 加速点击响应 */
  334. }
  335. .key:active {
  336. background: #f8f8f8;
  337. }
  338. </style>