33 changed files with 810 additions and 210 deletions
-
1src/assets/images/wifi-conn.svg
-
1src/assets/images/wifi-unconn.svg
-
1src/assets/styles/common.scss
-
5src/components/common/BTButton/index.vue
-
17src/components/common/CascadingSelectModal/index.vue
-
48src/components/common/SoftKeyboard/index.vue
-
354src/components/common/SoftKeyboard/moveKeyboard.vue
-
246src/components/formula/FormulaConfig.vue
-
4src/components/formula/FormulaTable.vue
-
5src/components/home/HomeFormula.vue
-
3src/components/home/HomeLogLevel.vue
-
2src/components/home/HomeOperation.vue
-
9src/components/home/HomeSetting.vue
-
6src/components/home/config.vue
-
24src/components/setting/AddUser.vue
-
1src/components/setting/History.vue
-
1src/components/setting/HistoryDetail.vue
-
71src/components/setting/User.vue
-
1src/components/system/NetReconnection.vue
-
19src/layouts/default.vue
-
4src/main.ts
-
5src/router/index.ts
-
19src/stores/deviceStore.ts
-
13src/stores/formulaStore.ts
-
9src/stores/homeStore.ts
-
1src/stores/systemStore.ts
-
6src/types/user.d.ts
-
58src/views/audit/index.vue
-
15src/views/formula/index.vue
-
2src/views/home/chart.vue
-
32src/views/liquid/index.vue
-
15src/views/login/index.vue
-
22src/views/seal/index.vue
@ -0,0 +1 @@ |
|||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="42" height="32" viewBox="0 0 42 32"><g><path d="M0,9.368L3.81937,13.4827C13.3061,3.256,28.6939,3.256,38.1806,13.4827L42,9.368C30.4106,-3.12267,11.6078,-3.12267,0,9.368ZM15.2722,25.8267L21,32L26.7277,25.8267C23.5778,22.4133,18.4406,22.4133,15.2722,25.8267ZM7.63875,17.6L11.4581,21.7147C16.7265,16.0347,25.2787,16.0347,30.5471,21.7147L34.3639,17.6C26.9955,9.65867,15.0255,9.65867,7.63612,17.6L7.63875,17.6Z" fill="#2892F3" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg> |
@ -0,0 +1 @@ |
|||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="42" height="32" viewBox="0 0 42 32"><g><path d="M37.9313,13.4957L41.7381,9.3913C30.2499,-3.13043,11.5561,-3.13043,0,9.3913L3.80673,13.4957C13.2556,3.26957,28.5505,3.26957,37.9313,13.4957ZM26.7853,18.9216C27.873,17.252,29.4365,15.8607,31.2039,14.9564C23.9303,9.73897,14.0736,10.6433,7.61573,17.5998L11.4225,21.7042C15.5691,17.1824,21.755,16.2781,26.7853,18.9216ZM37.9991,25.461L41.5339,29.0784C42.1457,29.7045,42.1457,30.748,41.4659,31.2349C40.9221,31.7915,40.1063,31.861,39.4945,31.4436L39.2906,31.2349L35.7558,27.6175L32.221,31.2349C31.6092,31.861,30.5895,31.861,29.9777,31.2349C29.4339,30.6784,29.3659,29.8436,29.7738,29.2175L29.9777,29.0088L33.5125,25.3915L29.9777,21.7741C29.3659,21.148,29.3659,20.1045,29.9777,19.4784C30.5215,18.9219,31.3373,18.8523,31.9491,19.2697L32.153,19.4784L35.7558,23.2349L39.2906,19.6175C39.9024,18.9915,40.9221,18.9915,41.5339,19.6175C42.0777,20.1741,42.1457,21.0088,41.7378,21.6349L41.5339,21.8436L37.9991,25.461ZM25.0157,25.0435L25.0157,24.5565C21.9567,22.5391,17.878,22.9565,15.2269,25.8087L20.937,32L25.2876,27.2696C25.0836,26.5739,25.0157,25.8087,25.0157,25.0435Z" fill-rule="evenodd" fill="#2892F3" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg> |
@ -0,0 +1,354 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import type { Ref } from 'vue' |
||||
|
import pinyinDict from 'libs/pinyinDict.json' |
||||
|
import { computed, defineEmits, defineProps, nextTick, onMounted, ref, watch, watchEffect } from 'vue' |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
modelValue: string |
||||
|
keyboardType: 'text' | 'number' |
||||
|
isVisible: boolean |
||||
|
targetInput: HTMLInputElement |
||||
|
}>() |
||||
|
const emits = defineEmits<{ |
||||
|
(e: 'update:modelValue', value: string): void |
||||
|
(e: 'updateKeyboardVisible', value: boolean): void |
||||
|
(e: 'confirm', value: string): void |
||||
|
(e: 'close'): void |
||||
|
}>() |
||||
|
const languageType = ref('en') |
||||
|
const inputValue = ref(props.modelValue) |
||||
|
const cnList = ref<string[]>([]) |
||||
|
const pinyinMap: Record<string, string[]> = pinyinDict |
||||
|
const pinyinValue = ref('') |
||||
|
// 拖动相关状态 |
||||
|
const isDragging = ref(false)// 是否正在拖动 |
||||
|
const startX = ref(0)// 触摸起始 X |
||||
|
const startY = ref(0)// 触摸起始 Y |
||||
|
const x = ref(0)// 容器偏移 X |
||||
|
const y = ref(0)// 容器偏移 Y |
||||
|
const inputX = ref(0) |
||||
|
const inputY = ref(0) |
||||
|
const keyboardRef = ref() as Ref<HTMLDivElement> // 软键盘容器 DOM |
||||
|
|
||||
|
onMounted(() => { |
||||
|
document.addEventListener('click', (e: any) => { |
||||
|
if (isOpen.value && !e.target?.name) { |
||||
|
isOpen.value = false |
||||
|
emits('updateKeyboardVisible', false) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
const isOpen = ref(false) |
||||
|
watchEffect(() => { |
||||
|
// 在焦点内二次点击时不触发EventListener,做一下延迟处理 |
||||
|
setTimeout(() => { |
||||
|
isOpen.value = props.isVisible |
||||
|
if (props.targetInput) { |
||||
|
const rect = props.targetInput.getBoundingClientRect() |
||||
|
const windowWidth = window.innerWidth |
||||
|
const windowHeight = window.innerHeight |
||||
|
// 确保 keyboardRef 已挂载,获取实际宽高 |
||||
|
nextTick(() => { |
||||
|
const keyboardWidth = keyboardRef.value ? keyboardRef.value.offsetWidth : 0 |
||||
|
const keyboardHeight = keyboardRef.value ? keyboardRef.value.offsetHeight : 0 |
||||
|
|
||||
|
let targetX = rect.left - x.value |
||||
|
let targetY = rect.bottom - y.value |
||||
|
|
||||
|
// 左侧边界修正 |
||||
|
if (targetX < 0) { |
||||
|
targetX = 0 |
||||
|
} |
||||
|
// 右侧边界修正 |
||||
|
else if (targetX + keyboardWidth > windowWidth) { |
||||
|
targetX = windowWidth - keyboardWidth |
||||
|
} |
||||
|
|
||||
|
// 下侧边界修正(超出则放上面) |
||||
|
if (targetY + keyboardHeight > windowHeight) { |
||||
|
targetY = rect.top - keyboardHeight |
||||
|
// 上面也超出则贴顶 |
||||
|
if (targetY < 0) { |
||||
|
targetY = 0 |
||||
|
} |
||||
|
} |
||||
|
console.log('targetX--', targetX) |
||||
|
console.log('targetY--', targetY) |
||||
|
inputX.value = targetX |
||||
|
inputY.value = targetY |
||||
|
}) |
||||
|
} |
||||
|
}, 100) |
||||
|
inputValue.value = props.modelValue |
||||
|
}) |
||||
|
|
||||
|
const activeKey = ref('') |
||||
|
const keyboardLayout = computed(() => { |
||||
|
if (props.keyboardType === 'number') { |
||||
|
return [ |
||||
|
['1', '2', '3'], |
||||
|
['4', '5', '6'], |
||||
|
['7', '8', '9'], |
||||
|
['.', '0', 'del'], |
||||
|
] |
||||
|
} |
||||
|
return [ |
||||
|
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'del'], |
||||
|
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '/'], |
||||
|
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 'enter'], |
||||
|
['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '?', '.', ':'], |
||||
|
['close', ' ', 'en'], |
||||
|
] |
||||
|
}) |
||||
|
const specialKeys = ['del', 'enter', ' '] |
||||
|
const handleKeyCn = (cn: string) => { |
||||
|
emits('update:modelValue', props.modelValue + cn) |
||||
|
cnList.value = [] |
||||
|
pinyinValue.value = '' |
||||
|
} |
||||
|
const handleKeyPress = (key: string) => { |
||||
|
activeKey.value = key |
||||
|
setTimeout(() => { |
||||
|
activeKey.value = '' |
||||
|
}, 150) |
||||
|
|
||||
|
if (key === 'del') { |
||||
|
if (props.keyboardType === 'text' && languageType.value === 'cn' && pinyinValue.value) { |
||||
|
pinyinValue.value = pinyinValue.value.slice(0, -1) |
||||
|
// 中文 |
||||
|
onHandlePinyinToCn(pinyinValue.value) |
||||
|
} |
||||
|
else { |
||||
|
emits('update:modelValue', props.modelValue.slice(0, -1)) |
||||
|
} |
||||
|
} |
||||
|
else if (key === 'enter') { |
||||
|
emits('confirm', props.modelValue) |
||||
|
closeKeyboard() |
||||
|
} |
||||
|
else if (key === 'close') { |
||||
|
closeKeyboard() |
||||
|
} |
||||
|
else if ((key === 'en' || key === 'cn') && props.keyboardType === 'text') { |
||||
|
languageType.value = key === 'en' ? 'cn' : 'en' |
||||
|
keyboardLayout.value[4][2] = key === 'en' ? 'cn' : 'en' |
||||
|
cnList.value = [] |
||||
|
} |
||||
|
else { |
||||
|
if (props.keyboardType === 'text' && languageType.value === 'cn') { |
||||
|
// 中文 |
||||
|
pinyinValue.value = pinyinValue.value + key |
||||
|
onHandlePinyinToCn(pinyinValue.value) |
||||
|
} |
||||
|
else { |
||||
|
emits('update:modelValue', props.modelValue + key) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const onHandlePinyinToCn = (keyValue: string) => { |
||||
|
const cn: string[] = pinyinMap[keyValue] |
||||
|
if (cn && cn.length) { |
||||
|
cnList.value = cn |
||||
|
} |
||||
|
else { |
||||
|
cnList.value = [] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const closeKeyboard = () => { |
||||
|
isOpen.value = false |
||||
|
emits('close') |
||||
|
} |
||||
|
|
||||
|
watch(() => props.isVisible, (newVal) => { |
||||
|
isOpen.value = newVal |
||||
|
}) |
||||
|
|
||||
|
// 触摸开始:记录初始位置 |
||||
|
const handleTouchStart = (e: TouchEvent) => { |
||||
|
isDragging.value = true |
||||
|
const touch = e.touches[0] |
||||
|
startX.value = touch.clientX - x.value |
||||
|
startY.value = touch.clientY - y.value |
||||
|
} |
||||
|
|
||||
|
// 触摸移动:计算偏移量 |
||||
|
const handleTouchMove = (e: TouchEvent) => { |
||||
|
if (isDragging.value) { |
||||
|
const touch = e.touches[0] |
||||
|
x.value = touch.clientX - startX.value |
||||
|
y.value = touch.clientY - startY.value |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 触摸结束:停止拖动 |
||||
|
const handleTouchEnd = () => { |
||||
|
isDragging.value = false |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div |
||||
|
v-if="isOpen" |
||||
|
:style="{ |
||||
|
position: 'absolute', |
||||
|
left: `${inputX}px`, |
||||
|
top: `${inputY}px`, |
||||
|
}" |
||||
|
> |
||||
|
<!-- <div class="keyboard-header"> |
||||
|
<button @click="closeKeyboard"> |
||||
|
关闭键盘 |
||||
|
</button> |
||||
|
</div> --> |
||||
|
<div |
||||
|
ref="keyboardRef" |
||||
|
class="keyboard-container keyboard-body" |
||||
|
@touchstart="handleTouchStart" |
||||
|
@touchmove="handleTouchMove" |
||||
|
@touchend="handleTouchEnd" |
||||
|
:style="{ |
||||
|
transform: `translate(${x}px, ${y}px)`, |
||||
|
transition: isDragging ? 'none' : 'transform 0.3s ease', |
||||
|
width: keyboardType === 'number' ? '30vw' : '66vw', |
||||
|
height: keyboardType === 'number' ? '46vh' : '46vh', |
||||
|
}" |
||||
|
> |
||||
|
<div> |
||||
|
<div v-if="keyboardType === 'text'" class="pinyin-container"> |
||||
|
<span v-if="pinyinValue" style="font-size:12px">拼音:{{ pinyinValue }}</span> |
||||
|
<div v-if="cnList && cnList.length" class="pinyin-cn"> |
||||
|
<div |
||||
|
v-for="(cnName, cnIndex) in cnList" |
||||
|
:key="cnIndex" |
||||
|
class="cn-name" |
||||
|
@click="(e) => { e.stopPropagation(); handleKeyCn(cnName) }"> |
||||
|
{{ cnName }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-for="(row, index) in keyboardLayout" :key="index" class="keyboard-row"> |
||||
|
<button |
||||
|
v-for="(key, keyIndex) in row" |
||||
|
:key="keyIndex" |
||||
|
:class="{ |
||||
|
'key-space': key === ' ', |
||||
|
'key-special': specialKeys.includes(key), |
||||
|
'key-active': activeKey === key, |
||||
|
'key-number': keyboardType === 'number', |
||||
|
'key-text': key !== ' ' && keyboardType === 'text', |
||||
|
}" |
||||
|
:style="keyboardType === 'number' ? 'height: 10vh' : 'height:3rem;'" |
||||
|
@click="(e) => { |
||||
|
e.stopPropagation() |
||||
|
handleKeyPress(key) |
||||
|
}" |
||||
|
> |
||||
|
{{ key === ' ' ? '空格' : key === 'del' ? '删除' : key === 'enter' ? '确认' : key === 'close' ? '关闭' : key === 'cn' ? '英文' : key === 'en' ? '拼音' : key }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.soft-keyboard { |
||||
|
//background-color: #f5f5f5; |
||||
|
border-top: 1px solid #ddd; |
||||
|
padding: 10px; |
||||
|
transition: all 0.3s ease; |
||||
|
user-select: none; |
||||
|
z-index: 9999; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.keyboard-open { |
||||
|
/* 这里可以根据需要补充打开时的过渡效果等,原 bottom 相关去掉 */ |
||||
|
} |
||||
|
|
||||
|
.keyboard-body { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
.pinyin-container{ |
||||
|
display: flex; |
||||
|
width: 80%; |
||||
|
height: 3rem; |
||||
|
padding-left: 2rem; |
||||
|
.pinyin-cn{ |
||||
|
color: #1890ff; |
||||
|
padding-left: 1rem; |
||||
|
display: flex; |
||||
|
width: 1rem; |
||||
|
position: relative; |
||||
|
gap:5px; |
||||
|
font-family: fangsong; |
||||
|
.cn-name{ |
||||
|
font-size: 2.5rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.keyboard-row { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 5px; |
||||
|
} |
||||
|
|
||||
|
.keyboard-row button { |
||||
|
min-width: 50px; |
||||
|
font-size: 18px; |
||||
|
border: none; |
||||
|
border-radius: 5px; |
||||
|
background-color: #fff; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s; |
||||
|
height: 10vh; |
||||
|
} |
||||
|
|
||||
|
.keyboard-row button:active { |
||||
|
transform: scale(0.95); |
||||
|
} |
||||
|
|
||||
|
.key-space { |
||||
|
margin-top: 3.5px; |
||||
|
width: 75vw;; |
||||
|
} |
||||
|
|
||||
|
.key-special { |
||||
|
background-color: #e0e0e0; |
||||
|
} |
||||
|
|
||||
|
.key-active { |
||||
|
background-color: #a0c4ff; |
||||
|
transform: scale(0.95); |
||||
|
} |
||||
|
|
||||
|
.key-number{ |
||||
|
width: 30vw; |
||||
|
height: 6vh; |
||||
|
margin: 5px; |
||||
|
} |
||||
|
|
||||
|
.key-text { |
||||
|
width: 8vw; |
||||
|
height: 5vh; |
||||
|
margin: 5px; |
||||
|
} |
||||
|
|
||||
|
.keyboard-container { |
||||
|
border-radius: 16px; |
||||
|
box-shadow: 0 -4px 12px rgba(0,0,0,0.1); |
||||
|
overflow: hidden; |
||||
|
z-index: 9999; |
||||
|
/* 让拖动更顺滑 */ |
||||
|
will-change: transform; |
||||
|
touch-action: none; /* 禁止浏览器默认触摸行为(如滚动) */ |
||||
|
background: #c8c8c8; |
||||
|
} |
||||
|
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue