Browse Source

优化样式

master
LiLongLong 3 weeks ago
parent
commit
0ba3f5f563
  1. 75
      package-lock.json
  2. 1
      package.json
  3. 2
      src/assets/styles/element.scss
  4. 2
      src/assets/styles/variable.scss
  5. 5
      src/components/common/BTButton/index.vue
  6. 2
      src/components/common/CascadingSelectModal/index.vue
  7. 601
      src/components/common/DatePicker/index.vue
  8. 150
      src/components/common/DigitalKeyboard/index.vue
  9. 250
      src/components/common/SoftKeyboard/index.vue
  10. 56
      src/components/formula/FormulaConfig.vue
  11. 19
      src/components/formula/FormulaTable.vue
  12. 2
      src/components/home/HomeFormula.vue
  13. 3
      src/components/home/HomeLogLevel.vue
  14. 62
      src/components/home/HomeSetting.vue
  15. 6
      src/components/home/config.vue
  16. 2
      src/components/seal/DashboardChart.vue
  17. 1
      src/components/setting/Device.vue
  18. 4
      src/components/setting/History.vue
  19. 21
      src/components/setting/SystemDate.vue
  20. 2
      src/components/setting/User.vue
  21. 15
      src/lang/en.ts
  22. 18
      src/lang/index.ts
  23. 15
      src/lang/zh.ts
  24. 42
      src/layouts/default.vue
  25. 2
      src/libs/pinyinDict.json
  26. 2
      src/libs/utils.ts
  27. 19
      src/main.ts
  28. 6
      src/views/audit/index.vue
  29. 11
      src/views/formula/index.vue
  30. 4
      src/views/home/index.vue
  31. 7
      src/views/seal/index.vue

75
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "matrix-spray-web", "name": "matrix-spray-web",
"version": "0.0.8",
"version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "matrix-spray-web", "name": "matrix-spray-web",
"version": "0.0.8",
"version": "0.0.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@ -40,6 +40,7 @@
"postcss-write-svg": "^3.0.1", "postcss-write-svg": "^3.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-chart-3": "^3.1.8", "vue-chart-3": "^3.1.8",
"vue-i18n": "^11.1.6",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
@ -998,6 +999,50 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@intlify/core-base": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.6.tgz",
"integrity": "sha512-gfMLnoWGiQkA1BwK6Qbrog/e3I6Lnkhqk08XObJb0lMq6sLG1Ggl2MazVaMfGnv/E1Td8pCS5UwR54Ys+fOxmQ==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.6",
"@intlify/shared": "11.1.6"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.6.tgz",
"integrity": "sha512-w0LYo5sqgQZF3vEmjLlx+5PYk5EEiB+uigsBkka/DKoAIH2c5xlXcjAxhTgSw35Vrck+GOGriahFsfbHL+ZjPw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.6",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.6.tgz",
"integrity": "sha512-G1Pe4UILhiGOItuehRW+Pk9/NlnRaMFsdnhZ1fwBjiHvrzitmPNZdLx7Eo3GPfRrsk1mdkilZSfgH8SnM419vA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@ -11385,6 +11430,32 @@
"eslint": "^8.57.0 || ^9.0.0" "eslint": "^8.57.0 || ^9.0.0"
} }
}, },
"node_modules/vue-i18n": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.6.tgz",
"integrity": "sha512-+IbsW/sTZHj7U1w0rPOYJbuSB0/7DeO1nvUo3BxvO20OQgHs+ukJ3QeLqvoUA6DiLk+8SA9+djRmKC9+FC6cAg==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.6",
"@intlify/shared": "11.1.6",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",

1
package.json

@ -57,6 +57,7 @@
"postcss-write-svg": "^3.0.1", "postcss-write-svg": "^3.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-chart-3": "^3.1.8", "vue-chart-3": "^3.1.8",
"vue-i18n": "^11.1.6",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"ws": "^8.18.1" "ws": "^8.18.1"
}, },

2
src/assets/styles/element.scss

@ -2,7 +2,7 @@
--el-font-size-base: 18px; --el-font-size-base: 18px;
// --el-button-size: 80px; // --el-button-size: 80px;
--el-font-family-base: serif;
--el-color-primary: #1989fa; --el-color-primary: #1989fa;
//--el-button-active-bg-color: linear-gradient(90deg, #0657C0 24%, #096AE0 101%); //--el-button-active-bg-color: linear-gradient(90deg, #0657C0 24%, #096AE0 101%);
--text-color-primary: #17213c; --text-color-primary: #17213c;

2
src/assets/styles/variable.scss

@ -4,4 +4,4 @@ $danger-color: #DF1515;
$warn-color: #EE8223; $warn-color: #EE8223;
$info-color: #909399; $info-color: #909399;
$gradient-color: linear-gradient(185deg, rgb(175 216 255) -90%, #fff 24%); $gradient-color: linear-gradient(185deg, rgb(175 216 255) -90%, #fff 24%);
$main-container-height: calc(100vh - 16vh)
$main-container-height: calc(100vh - 14vh)

5
src/components/common/BTButton/index.vue

@ -86,6 +86,7 @@ const handleClick = (event: MouseEvent) => {
:size="buttonSize" :size="buttonSize"
:disabled="disabled" :disabled="disabled"
:loading="loading" :loading="loading"
class="button"
:style="{ :style="{
backgroundColor: disabled ? '#e8e8e8' : bgColor, backgroundColor: disabled ? '#e8e8e8' : bgColor,
color: disabled ? '#939393' : textColor, color: disabled ? '#939393' : textColor,
@ -93,7 +94,6 @@ const handleClick = (event: MouseEvent) => {
height, height,
borderRadius, borderRadius,
fontSize: textSize, fontSize: textSize,
fontWeight: 400,
padding, padding,
}" }"
@click="handleClick" @click="handleClick"
@ -107,4 +107,7 @@ const handleClick = (event: MouseEvent) => {
.pl{ .pl{
padding-left: 5px; padding-left: 5px;
} }
.button{
font-family: serif;
}
</style> </style>

2
src/components/common/CascadingSelectModal/index.vue

@ -204,9 +204,9 @@ watchEffect(() => {
.modal-content { .modal-content {
flex: 1; flex: 1;
overflow-y: auto;
padding: 10px 0; padding: 10px 0;
max-height: 15vw; max-height: 15vw;
overflow: hidden
} }
.modal-content-right{ .modal-content-right{
flex: 1; flex: 1;

601
src/components/common/DatePicker/index.vue

@ -0,0 +1,601 @@
<script lang="ts" setup>
import { computed, defineEmits, defineProps, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Date, null],
default: null,
},
minDate: {
type: [String, Date],
default: null,
},
maxDate: {
type: [String, Date],
default: null,
},
format: {
type: String,
default: 'YYYY-MM-DD HH:mm:ss',
},
})
const emits = defineEmits(['update:modelValue', 'change'])
const dateText = ref()
const hoursOptions = ref([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
//
const selectedDate = ref<Date | null>(null)
const currentYear = ref(0)
const currentMonth = ref(0)
const today = new Date()
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const isDropdownVisible = ref(false)
const selectedHour = ref('00')
const selectedMinute = ref('00')
const selectedSecond = ref('00')
//
const displayValue = () => {
console.log('-----------------')
if (!selectedDate.value) {
return ''
}
console.log('---selectedDate--', selectedDate.value)
const year = selectedDate.value.getFullYear()
const month = (selectedDate.value.getMonth() + 1).toString().padStart(2, '0')
const date = selectedDate.value.getDate().toString().padStart(2, '0')
const hour = selectedDate.value.getHours().toString().padStart(2, '0')
const minute = selectedDate.value.getMinutes().toString().padStart(2, '0')
const second = selectedDate.value.getSeconds().toString().padStart(2, '0')
dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}`
}
watch(selectedDate, (newVal) => {
if (newVal) {
const year = newVal.getFullYear()
const month = (newVal.getMonth() + 1).toString().padStart(2, '0')
const date = newVal.getDate().toString().padStart(2, '0')
const hour = newVal.getHours().toString().padStart(2, '0')
const minute = newVal.getMinutes().toString().padStart(2, '0')
const second = newVal.getSeconds().toString().padStart(2, '0')
dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}`
}
})
//
const daysInMonth: any = computed(() => {
const days: Record<string, any>[] = []
//
const firstDay = new Date(currentYear.value, currentMonth.value, 1).getDay()
//
const prevMonthLastDay = new Date(currentYear.value, currentMonth.value, 0).getDate()
for (let i = firstDay; i > 0; i--) {
const date = new Date(currentYear.value, currentMonth.value - 1, prevMonthLastDay - i + 1)
days.push({ date, month: currentMonth.value - 1 })
}
//
const currentMonthDays = new Date(currentYear.value, currentMonth.value + 1, 0).getDate()
for (let i = 1; i <= currentMonthDays; i++) {
const date = new Date(currentYear.value, currentMonth.value, i)
days.push({ date, month: currentMonth.value })
}
//
const remainingDays = 42 - days.length // 6742
for (let i = 1; i <= remainingDays; i++) {
const date = new Date(currentYear.value, currentMonth.value + 1, i)
days.push({ date, month: currentMonth.value + 1 })
}
return days
})
//
onMounted(() => {
const initialDate = new Date()
selectedDate.value = initialDate
currentYear.value = initialDate.getFullYear()
currentMonth.value = initialDate.getMonth()
selectedHour.value = initialDate.getHours().toString().padStart(2, '0')
selectedMinute.value = initialDate.getMinutes().toString().padStart(2, '0')
selectedSecond.value = initialDate.getSeconds().toString().padStart(2, '0')
})
// props
watch(() => props.modelValue, (newVal) => {
if (newVal) {
const date = new Date(newVal)
if (!Number.isNaN(date.getTime())) {
selectedDate.value = date
currentYear.value = date.getFullYear()
currentMonth.value = date.getMonth()
selectedHour.value = date.getHours().toString().padStart(2, '0')
selectedMinute.value = date.getMinutes().toString().padStart(2, '0')
selectedSecond.value = date.getSeconds().toString().padStart(2, '0')
}
}
else {
selectedDate.value = null
}
})
//
const prevMonth = () => {
if (currentMonth.value === 0) {
currentMonth.value = 11
currentYear.value--
}
else {
currentMonth.value--
}
}
const nextMonth = () => {
if (currentMonth.value === 11) {
currentMonth.value = 0
currentYear.value++
}
else {
currentMonth.value++
}
}
const prevYear = () => {
currentYear.value--
}
const nextYear = () => {
currentYear.value++
}
//
const selectDate = (date: Date) => {
if (!isDateInRange(date)) {
return
}
if (!selectedDate.value) {
selectedDate.value = new Date(date)
}
else {
//
selectedDate.value.setFullYear(date.getFullYear())
selectedDate.value.setMonth(date.getMonth())
selectedDate.value.setDate(date.getDate())
}
displayValue()
}
//
const updateTime = () => {
console.log('selectedDate.value--', selectedDate.value, selectedHour.value)
if (selectedDate.value) {
selectedDate.value.setHours(Number(selectedHour.value))
selectedDate.value.setMinutes(Number(selectedMinute.value))
selectedDate.value.setSeconds(Number(selectedSecond.value))
}
displayValue()
}
//
const confirmSelection = () => {
console.log('selectedDate.value---', selectedDate.value)
if (selectedDate.value) {
const dateValue = formatDate(selectedDate.value, props.format)
emits('update:modelValue', dateValue)
emits('change', dateValue)
}
isDropdownVisible.value = false
}
//
const formatDate = (date: Date, format: string): string => {
if (!date) {
return ''
}
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
console.log('---year--', year)
console.log('---month--', month)
return format
.replace('YYYY', year.toString())
.replace('MM', month.toString().padStart(2, '0'))
.replace('DD', day.toString().padStart(2, '0'))
.replace('HH', hour.toString().padStart(2, '0'))
.replace('mm', minute.toString().padStart(2, '0'))
.replace('ss', second.toString().padStart(2, '0'))
}
//
const cancelSelection = () => {
//
// if (props.modelValue) {
// const date = new Date(props.modelValue)
// if (!isNaN(date.getTime())) {
// selectedDate.value = date
// selectedHour.value = date.getHours().toString().padStart(2, '0')
// selectedMinute.value = date.getMinutes().toString().padStart(2, '0')
// selectedSecond.value = date.getSeconds().toString().padStart(2, '0')
// }
// } else {
// selectedDate.value = null
// }
isDropdownVisible.value = false
}
//
const toggleDropdown = (event?: MouseEvent) => {
if (event) {
event.stopPropagation()
}
isDropdownVisible.value = !isDropdownVisible.value
}
//
const isSameDay = (date1: Date, date2: Date | null) => {
if (!date2) {
return false
}
return (
date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate()
)
}
const isDateInRange = (date: Date) => {
const min = props.minDate ? new Date(props.minDate) : null
const max = props.maxDate ? new Date(props.maxDate) : null
if (min && date < min) {
return false
}
if (max && date > max) {
return false
}
return true
}
//
const closeDropdownOnClickOutside = (event: MouseEvent) => {
if (!isDropdownVisible.value) {
return
}
const target = event.target as HTMLElement
const container = document.querySelector('.date-time-picker')
if (container && !container.contains(target)) {
isDropdownVisible.value = false
}
}
//
onMounted(() => {
document.addEventListener('click', closeDropdownOnClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', closeDropdownOnClickOutside)
})
</script>
<template>
<div class="date-time-picker">
<div class="input-group">
<input
type="text"
v-model="dateText"
@click="toggleDropdown"
readonly
placeholder="请选择日期时间"
class="form-input"
>
<span class="input-icon" @click="toggleDropdown">
<i class="fa fa-calendar"></i>
</span>
</div>
<transition name="fade">
<div v-show="isDropdownVisible" class="dropdown-container">
<div class="calendar-header">
<button @click="prevYear" class="nav-btn">
<el-icon><DArrowLeft /></el-icon>
</button>
<button @click="prevMonth" class="nav-btn">
<el-icon><ArrowLeft /></el-icon>
</button>
<span class="current-date">
{{ currentYear }} {{ currentMonth + 1 }}
</span>
<button @click="nextMonth" class="nav-btn">
<el-icon><ArrowRight /></el-icon>
</button>
<button @click="nextYear" class="nav-btn">
<el-icon><DArrowRight /></el-icon>
</button>
</div>
<div class="weekdays">
<div v-for="day in weekdays" :key="day" class="weekday">
{{ day }}
</div>
</div>
<div class="days">
<div
v-for="(day, index) in daysInMonth"
:key="index"
class="day"
:class="{
'other-month': day.month !== currentMonth,
'selected': isSameDay(day.date, selectedDate),
'today': isSameDay(day.date, today),
'disabled': !isDateInRange(day.date),
}"
@click="selectDate(day.date)"
>
{{ day.date.getDate() }}
</div>
</div>
<div class="time-selector">
<div class="time-label">时间:</div>
<el-select v-model="selectedHour" @change="updateTime" class="time-select" >
<el-option v-for="i in hoursOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
{{ i.toString().padStart(2, '0') }}
</el-option>
</el-select>
<span class="time-separator">:</span>
<el-select v-model="selectedMinute" @change="updateTime" class="time-select">
<el-option v-for="i in 60" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
{{ i.toString().padStart(2, '0') }}
</el-option>
</el-select>
<span class="time-separator">:</span>
<el-select v-model="selectedSecond" @change="updateTime" class="time-select">
<el-option v-for="i in 60" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
{{ i.toString().padStart(2, '0') }}
</el-option>
</el-select>
</div>
<div class="calendar-footer">
<button @click="confirmSelection" class="confirm-btn">确定</button>
<button @click="cancelSelection" class="cancel-btn">取消</button>
</div>
</div>
</transition>
</div>
</template>
<style lang="scss" scoped>
$fontSize: 1.5rem;
:deep(body) {
--el-font-size-base: 20px;
}
.date-time-picker {
position: relative;
width: 340px;
}
.input-group {
display: flex;
position: relative;
}
.form-input {
flex: 1;
padding: 10px 14px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: $fontSize;
outline: none;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: #4f46e5;
}
.input-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
cursor: pointer;
font-size: $fontSize;
}
.dropdown-container {
position: absolute;
top: calc(100% + 4px);
right: 0;
width: 28rem;
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 4px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
z-index: 100;
padding: 12px;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.nav-btn {
background: none;
border: none;
cursor: pointer;
color: #64748b;
font-size: $fontSize;
padding: 6px;
border-radius: 4px;
transition: background-color 0.2s;
}
.nav-btn:hover {
background-color: #f1f5f9;
}
.current-date {
font-weight: 500;
color: #334155;
font-size: $fontSize;
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-bottom: 6px;
}
.weekday {
text-align: center;
font-size: $fontSize;
color: #64748b;
padding: 6px;
}
.days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
margin-bottom: 12px;
}
.day {
text-align: center;
padding: 8px;
border-radius: 4px;
font-size: $fontSize;
cursor: pointer;
transition: background-color 0.2s;
}
.day:hover:not(.disabled) {
background-color: #e0f2fe;
}
.day.other-month {
color: #94a3b8;
}
.day.selected {
background-color: #3b82f6;
color: white;
}
.day.today:not(.selected) {
font-weight: 500;
color: #3b82f6;
}
.day.disabled {
color: #cbd5e1;
cursor: not-allowed;
}
.time-selector {
display: flex;
align-items: center;
margin-bottom: 12px;
.time-select {
padding: 4px 8px;
border-radius: 4px;
font-size: 20px;
outline: none;
background-color: white;
margin-right: 4px;
/* 最大高度限制,溢出滚动 */
max-height: 3rem;
width: 6rem;
border: none;
}
}
select option {
max-height: 3rem;
border: 1px solid red;
}
.time-label {
margin-right: 12px;
font-size: $fontSize;
color: #334155;
}
.select-option{
max-height: 3rem;
}
select {
padding: 6px 10px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: $fontSize;
outline: none;
background-color: white;
margin-right: 6px;
width: 60px;
}
.time-separator {
margin: 0 3px;
font-size: $fontSize;
color: #334155;
}
.calendar-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.confirm-btn, .cancel-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: $fontSize;
cursor: pointer;
transition: background-color 0.2s;
}
.confirm-btn {
background-color: #3b82f6;
color: white;
}
.confirm-btn:hover {
background-color: #2563eb;
}
.cancel-btn {
background-color: #f1f5f9;
color: #334155;
}
.cancel-btn:hover {
background-color: #e2e8f0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
//
:deep(.el-select__wrapper){
height: 3rem;
font-size: 20px;
}
</style>

150
src/components/common/DigitalKeyboard/index.vue

@ -0,0 +1,150 @@
<script lang="ts" setup>
import type { Ref } from 'vue'
import { onMounted, ref } from 'vue'
//
const inputValue = ref('')//
const keyboardRef = ref() as Ref<HTMLDivElement> // DOM
//
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 keyboardKeys = ref(['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', '删除'])
//
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
}
//
const handleKeyClick = (key: string) => {
if (key === '删除') {
//
inputValue.value = inputValue.value.slice(0, -1)
}
else {
// /
inputValue.value += key
}
}
//
onMounted(() => {
const { width, height } = keyboardRef.value.getBoundingClientRect()
x.value = (document.documentElement.clientWidth - width) / 2
y.value = (document.documentElement.clientHeight - height) / 2
})
</script>
<template>
<!-- 软键盘容器支持拖动点击穿透若需 -->
<div
ref="keyboardRef"
class="keyboard-container"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:style="{
transform: `translate(${x}px, ${y}px)`,
transition: isDragging ? 'none' : 'transform 0.3s ease',
}"
>
<!-- 输入显示框 -->
<div class="input-box">
<input
v-model="inputValue"
type="text"
placeholder="点击输入"
class="input-field"
@click.stop
/>
</div>
<!-- 数字按键区域 -->
<div class="keys">
<div
v-for="(key, index) in keyboardKeys"
:key="index"
class="key"
@click="handleKeyClick(key)"
>
{{ key }}
</div>
</div>
</div>
</template>
<style scoped>
.keyboard-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
max-width: 500px; /* 限制最大宽度 */
background: #fff;
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 12px rgba(0,0,0,0.1);
overflow: hidden;
z-index: 9999;
/* 让拖动更顺滑 */
will-change: transform;
touch-action: none; /* 禁止浏览器默认触摸行为(如滚动) */
}
.input-box {
padding: 16px;
background: #f5f5f5;
}
.input-field {
width: 100%;
padding: 12px;
text-align: right;
font-size: 18px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
}
.keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.key {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
border: 1px solid #eee;
background: #fff;
touch-action: manipulation; /* 加速点击响应 */
}
.key:active {
background: #f8f8f8;
}
</style>

250
src/components/common/SoftKeyboard/index.vue

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// import pinyinDict from 'libs/pinyinDict.json'
import type { Ref } from 'vue'
import pinyinDict from 'libs/pinyinDict.json'
import { computed, defineEmits, defineProps, onMounted, ref, watch, watchEffect } from 'vue' import { computed, defineEmits, defineProps, onMounted, ref, watch, watchEffect } from 'vue'
const props = defineProps<{ const props = defineProps<{
@ -7,7 +8,6 @@ const props = defineProps<{
keyboardType: 'text' | 'number' keyboardType: 'text' | 'number'
isVisible: boolean isVisible: boolean
}>() }>()
const emits = defineEmits<{ const emits = defineEmits<{
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
(e: 'updateKeyboardVisible', value: boolean): void (e: 'updateKeyboardVisible', value: boolean): void
@ -15,8 +15,18 @@ const emits = defineEmits<{
(e: 'close'): void (e: 'close'): void
}>() }>()
const languageType = ref('en') const languageType = ref('en')
// const cnList = ref<string[]>([])
// const pinyinMap: Record<string, string> = pinyinDict as unknown as Record<string, string>
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 keyboardRef = ref() as Ref<HTMLDivElement> // DOM
onMounted(() => { onMounted(() => {
document.addEventListener('click', (e: any) => { document.addEventListener('click', (e: any) => {
if (isOpen.value && !e.target?.name) { if (isOpen.value && !e.target?.name) {
@ -32,6 +42,7 @@ watchEffect(() => {
setTimeout(() => { setTimeout(() => {
isOpen.value = props.isVisible isOpen.value = props.isVisible
}, 100) }, 100)
inputValue.value = props.modelValue
}) })
const activeKey = ref('') const activeKey = ref('')
@ -49,13 +60,15 @@ const keyboardLayout = computed(() => {
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '/'], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '/'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 'enter'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 'enter'],
['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '?', '.', ':'], ['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '?', '.', ':'],
[' ', 'en', 'close'],
['close', ' ', 'en'],
] ]
}) })
const specialKeys = ['del', 'enter', ' '] const specialKeys = ['del', 'enter', ' ']
// const handleKeyCn = (cn: string) => {
// console.log('cn===', cn)
// }
const handleKeyCn = (cn: string) => {
emits('update:modelValue', props.modelValue + cn)
cnList.value = []
pinyinValue.value = ''
}
const handleKeyPress = (key: string) => { const handleKeyPress = (key: string) => {
activeKey.value = key activeKey.value = key
setTimeout(() => { setTimeout(() => {
@ -63,7 +76,14 @@ const handleKeyPress = (key: string) => {
}, 150) }, 150)
if (key === 'del') { if (key === 'del') {
emits('update:modelValue', props.modelValue.slice(0, -1))
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') { else if (key === 'enter') {
emits('confirm', props.modelValue) emits('confirm', props.modelValue)
@ -74,17 +94,28 @@ const handleKeyPress = (key: string) => {
} }
else if ((key === 'en' || key === 'cn') && props.keyboardType === 'text') { else if ((key === 'en' || key === 'cn') && props.keyboardType === 'text') {
languageType.value = key === 'en' ? 'cn' : 'en' languageType.value = key === 'en' ? 'cn' : 'en'
keyboardLayout.value[4][1] = key === 'en' ? 'cn' : 'en'
keyboardLayout.value[4][2] = key === 'en' ? 'cn' : 'en'
cnList.value = []
} }
else { else {
const keyValue = props.modelValue + key
emits('update:modelValue', keyValue)
// if (props.keyboardType === 'text' && languageType.value === 'cn') {
// const cn = pinyinMap[keyValue]
// if (cn.length) {
// cnList.value = cn as unknown as string[]
// }
// }
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 = []
} }
} }
@ -96,54 +127,90 @@ const closeKeyboard = () => {
watch(() => props.isVisible, (newVal) => { watch(() => props.isVisible, (newVal) => {
isOpen.value = 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> </script>
<template> <template>
<div class="soft-keyboard" :class="{ 'keyboard-open': isOpen }">
<div v-if="isOpen" class="soft-keyboard" :class="{ 'keyboard-open': isOpen }">
<!-- <div class="keyboard-header"> <!-- <div class="keyboard-header">
<button @click="closeKeyboard"> <button @click="closeKeyboard">
关闭键盘 关闭键盘
</button> </button>
</div> --> </div> -->
<div class="keyboard-body">
<!-- <div v-if="keyboardType === 'text' && cnList.length">
<button
v-for="(cnName, cnIndex) in cnList"
:key="cnIndex"
@click="(e) => {
e.stopPropagation()
handleKeyCn(cnName)
}"
>
{{ cnName }}
</button>
</div> -->
<div class="keyboard-row" v-for="(row, index) in keyboardLayout" :key="index">
<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',
}"
@click="(e) => {
e.stopPropagation()
handleKeyPress(key)
}"
>
{{ key === ' ' ? '空格' : key === 'del' ? '删除' : key === 'enter' ? '确认' : key === 'close' ? '关闭' : key === 'en' ? '英文' : key === 'cn' ? '中文' : key }}
</button>
<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' ? '50vw' : '',
height: keyboardType === 'number' ? '45vh' : '50vh',
}"
>
<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:4rem'"
@click="(e) => {
e.stopPropagation()
handleKeyPress(key)
}"
>
{{ key === ' ' ? '空格' : key === 'del' ? '删除' : key === 'enter' ? '确认' : key === 'close' ? '关闭' : key === 'cn' ? '英文' : key === 'en' ? '拼音' : key }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped>
<style lang="scss" scoped>
.soft-keyboard { .soft-keyboard {
position: fixed;
bottom: -300px; bottom: -300px;
left: 0; left: 0;
right: 0; right: 0;
@ -177,6 +244,20 @@ watch(() => props.isVisible, (newVal) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
.pinyin-container{
display: flex;
width: 80%;
height: 4rem;
.pinyin-cn{
display: flex;
width: 1rem;
position: relative;
gap:5px;
.cn-name{
font-size: 2.5rem;
}
}
}
} }
.keyboard-row { .keyboard-row {
@ -187,7 +268,6 @@ watch(() => props.isVisible, (newVal) => {
.keyboard-row button { .keyboard-row button {
min-width: 50px; min-width: 50px;
height: 50px;
font-size: 18px; font-size: 18px;
border: none; border: none;
border-radius: 5px; border-radius: 5px;
@ -195,6 +275,7 @@ watch(() => props.isVisible, (newVal) => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
height: 10vh;
} }
.keyboard-row button:hover { .keyboard-row button:hover {
@ -206,7 +287,8 @@ watch(() => props.isVisible, (newVal) => {
} }
.key-space { .key-space {
width: 45vw;;
margin-top: 3.5px;
width: 75vw;;
} }
.key-special { .key-special {
@ -220,9 +302,69 @@ watch(() => props.isVisible, (newVal) => {
.key-number{ .key-number{
width: 30vw; width: 30vw;
height: 6vh;
margin: 5px;
} }
.key-text { .key-text {
width: 8vw; width: 8vw;
height: 5vh;
margin: 5px;
}
.input-w{
width: 20%;
height: 4rem;
font-size: 2rem;
margin-left: 3rem;
}
.keyboard-container {
position: fixed;
bottom: 0;
left: 0;
width: 90%;
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;
}
.input-box {
padding: 16px;
background: #f5f5f5;
}
.input-field {
width: 100%;
padding: 12px;
text-align: right;
font-size: 18px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
}
.keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.key {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
border: 1px solid #eee;
background: #fff;
touch-action: manipulation; /* 加速点击响应 */
}
.key:active {
background: #f8f8f8;
} }
</style> </style>

56
src/components/formula/FormulaConfig.vue

@ -87,6 +87,8 @@ const labelUnitMap: Record<string, any> = {
loglevel: 'Log', loglevel: 'Log',
} }
const currentFormulaItem = ref()
/** /**
* 组件挂载时注册方法供父组件调用 * 组件挂载时注册方法供父组件调用
*/ */
@ -134,6 +136,10 @@ watch(inputValue, (newVal: string | number) => {
if (focusedInput.value !== 'name') { if (focusedInput.value !== 'name') {
newVal = Number(newVal) newVal = Number(newVal)
} }
console.log('currentFormulaItem.value--', currentFormulaItem.value)
if (currentFormulaItem.value && newVal > currentFormulaItem.value.val_upper_limit) {
newVal = currentFormulaItem.value.val_upper_limit
}
formData.value[focusedInput.value] = newVal formData.value[focusedInput.value] = newVal
} }
}) })
@ -270,8 +276,9 @@ const onEditFormula = (formula_id: string, formulaForm: Formula.FormulaItem) =>
/** /**
* 打开软键盘 * 打开软键盘
* @param {Event} e - 事件对象 * @param {Event} e - 事件对象
* @param item
*/ */
const openKeyboard = (e: any) => {
const openKeyboard = (e: any, item: Formula.FormulaItem) => {
setTimeout(() => { setTimeout(() => {
keyboardVisible.value = true keyboardVisible.value = true
const labelName: string = e.target.name const labelName: string = e.target.name
@ -279,6 +286,7 @@ const openKeyboard = (e: any) => {
const formValue = formData.value[labelName] const formValue = formData.value[labelName]
inputValue.value = formValue.toString() inputValue.value = formValue.toString()
focusedInput.value = e.target.name focusedInput.value = e.target.name
currentFormulaItem.value = item
}, 100) }, 100)
} }
@ -297,7 +305,7 @@ const handleResetDefault = async () => {
className: 'SettingMgrService', className: 'SettingMgrService',
fnName: 'factoryResetSettings', fnName: 'factoryResetSettings',
}) })
formulaStore.getFormualDefaultData()
await formulaStore.getFormualDefaultData()
} }
/** /**
@ -336,7 +344,7 @@ const openKeyboardType = (labelName: string) => {
<template> <template>
<div class="formula-form"> <div class="formula-form">
<el-form :model="formData" label-width="auto" label-position="left" class="formulaFormItem" inline>
<el-form :model="formData" label-width="auto" label-position="right" class="formulaFormItem" inline>
<el-form-item v-if="type !== 'setting'" label="配方名称" style="margin-top:20px"> <el-form-item v-if="type !== 'setting'" label="配方名称" style="margin-top:20px">
<el-input v-model="formData.name" v-prevent-keyboard name="name" placeholder="配方名称" class="formdata-input-home" @focus="openKeyboard" /> <el-input v-model="formData.name" v-prevent-keyboard name="name" placeholder="配方名称" class="formdata-input-home" @focus="openKeyboard" />
</el-form-item> </el-form-item>
@ -347,20 +355,20 @@ const openKeyboardType = (labelName: string) => {
style="margin-top:20px" style="margin-top:20px"
> >
<template v-if="item.val_type === 'int'"> <template v-if="item.val_type === 'int'">
<el-input-number
<el-input
v-model.number="formData[item.setting_id]" v-model.number="formData[item.setting_id]"
v-prevent-keyboard v-prevent-keyboard
type="number"
:name="item.setting_id" :name="item.setting_id"
:controls="false" :controls="false"
:min="Number(item.val_lower_limit)" :min="Number(item.val_lower_limit)"
:max="Number(item.val_upper_limit)" :max="Number(item.val_upper_limit)"
class="formdata-input-home" class="formdata-input-home"
:disabled="!item.is_visible_in_setting_page" :disabled="!item.is_visible_in_setting_page"
@focus="openKeyboard"
/>
<br/>
<div>{{ labelUnitMap[item.setting_id] }}</div>
<!-- <div>{{ `取消范围:${item.val_lower_limit}-${item.val_upper_limit}` }}</div> -->
@focus="(e) => openKeyboard(e, item)"
>
<template v-if="labelUnitMap[item.setting_id]" #append>{{ labelUnitMap[item.setting_id] }}</template>
</el-input>
</template> </template>
<template v-else-if="item.val_type === 'enum'"> <template v-else-if="item.val_type === 'enum'">
<el-input <el-input
@ -369,8 +377,9 @@ const openKeyboardType = (labelName: string) => {
placeholder="请选择" placeholder="请选择"
class="formdata-input-home" class="formdata-input-home"
@focus="openModal" @focus="openModal"
/>
<span>{{ labelUnitMap[item.setting_id] }}</span>
>
<template #append>{{ labelUnitMap[item.setting_id] }}</template>
</el-input>
</template> </template>
<template v-else-if="item.val_type === 'boolean'"> <template v-else-if="item.val_type === 'boolean'">
<el-radio-group <el-radio-group
@ -388,16 +397,16 @@ const openKeyboardType = (labelName: string) => {
</template> </template>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div v-if="type !== 'home'" class="formula-form-btn">
<div v-if="type !== 'home'" class="formula-form-btn" :style="{ marginLeft: type === 'setting' ? '20%' : '33%' }">
<slot name="formulaBtn"> <slot name="formulaBtn">
<div class="default-btn"> <div class="default-btn">
<el-button type="primary" @click="handleSubmit">
<el-button type="primary" class="config-btn" @click="handleSubmit">
确定 确定
</el-button> </el-button>
<el-button v-if="type === 'setting'" @click="handleResetDefault">
<el-button v-if="type === 'setting'" class="config-btn" @click="handleResetDefault">
恢复默认值 恢复默认值
</el-button> </el-button>
<el-button v-else @click="handleCancel">
<el-button v-else class="config-btn" @click="handleCancel">
取消 取消
</el-button> </el-button>
</div> </div>
@ -430,26 +439,29 @@ const openKeyboardType = (labelName: string) => {
font-size: 20px !important; font-size: 20px !important;
padding: 5px; padding: 5px;
padding-left: 15px; padding-left: 15px;
max-height: 80vh;
max-height: 75vh;
height: 73vh;
overflow: auto; overflow: auto;
.formulaFormItem{ .formulaFormItem{
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.formula-form-btn{ .formula-form-btn{
bottom: 5rem;
display: flex;
justify-content: center;
position:absolute;
margin-left: 20rem;
bottom: 0rem;
height: 7rem;
position: absolute;
} }
.default-btn{ .default-btn{
margin-top: 1rem; margin-top: 1rem;
} }
.config-btn{
height: 3rem;
width: 7rem;
}
} }
.formdata-input-home{ .formdata-input-home{
width: 10vw;
width: 15vw;
} }
.formula-form-item{ .formula-form-item{

19
src/components/formula/FormulaTable.vue

@ -95,14 +95,14 @@ const deleteRecipe = (item: Formula.FormulaItem) => {
:class="{ selected: selectedIndex === index }" :class="{ selected: selectedIndex === index }"
@click="selectRecipe(item, index)" @click="selectRecipe(item, index)"
> >
<span style="font-size: 1.5rem">{{ item.name }}</span>
<span class="formula-name">{{ item.name }}</span>
<div class="actions"> <div class="actions">
<button class="view-button" @click.stop="onStartFormula(item)">
<el-button class="view-button" @click.stop="onStartFormula(item)">
执行配方 执行配方
</button>
<button class="delete-button" @click.stop="deleteRecipe(item)">
</el-button>
<el-button class="delete-button" @click.stop="deleteRecipe(item)">
删除 删除
</button>
</el-button>
<span v-if="selectedIndex === index" class="selected-icon"> <span v-if="selectedIndex === index" class="selected-icon">
<i class="fa fa-check-circle" /> <i class="fa fa-check-circle" />
</span> </span>
@ -121,6 +121,7 @@ const deleteRecipe = (item: Formula.FormulaItem) => {
<style scoped> <style scoped>
.recipe-management { .recipe-management {
font-family: serif;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
@ -148,6 +149,11 @@ const deleteRecipe = (item: Formula.FormulaItem) => {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
.formula-name{
font-size: 1.5rem;
color: var(--el-text-color-regular);
width: 8rem;
}
} }
.recipe-list li { .recipe-list li {
@ -183,10 +189,11 @@ const deleteRecipe = (item: Formula.FormulaItem) => {
cursor: pointer; cursor: pointer;
margin-right: 5px; margin-right: 5px;
transition: all 0.3s; transition: all 0.3s;
font-family: serif;
} }
.view-button { .view-button {
background-color: #e6f7ff;
background-color: #e6f0ff;
color: #1890ff; color: #1890ff;
} }

2
src/components/home/HomeFormula.vue

@ -37,6 +37,7 @@ watchEffect(() => {
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 10px; padding: 10px;
font-size: 16px;
} }
.title-spend{ .title-spend{
justify-self: end; justify-self: end;
@ -44,6 +45,7 @@ watchEffect(() => {
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 10px; padding: 10px;
font-size: 16px;
} }
} }
</style> </style>

3
src/components/home/HomeLogLevel.vue

@ -36,6 +36,7 @@ const handleCancel = () => {
v-prevent-keyboard v-prevent-keyboard
class="input" class="input"
placeholder="请选择" placeholder="请选择"
style="height: 4rem"
@focus="openModal" @focus="openModal"
> >
<template #append> <template #append>
@ -43,7 +44,7 @@ const handleCancel = () => {
type="primary" type="primary"
button-text="Log" button-text="Log"
text-color="#ffffff" text-color="#ffffff"
height="3rem"
height="4rem"
text-size="16px" text-size="16px"
/> />
</template> </template>

62
src/components/home/HomeSetting.vue

@ -3,9 +3,10 @@ import { FtMessage } from '@/libs/message'
import { useFormulaStore } from '@/stores/formulaStore' import { useFormulaStore } from '@/stores/formulaStore'
import { useHomeStore } from '@/stores/homeStore' import { useHomeStore } from '@/stores/homeStore'
import { sendCmd, syncSendCmd } from 'apis/system' import { sendCmd, syncSendCmd } from 'apis/system'
import homeChart from 'assets/images/home/home-chart.svg'
// import homeChart from 'assets/images/home/home-chart.svg'
import homeRunSvg from 'assets/images/home/home-run.svg' import homeRunSvg from 'assets/images/home/home-run.svg'
import homeSettingSvg from 'assets/images/home/home-setting.svg' import homeSettingSvg from 'assets/images/home/home-setting.svg'
import BtButton from 'components/common/BTButton/index.vue'
import CascadingSelectModal from 'components/common/CascadingSelectModal/index.vue' import CascadingSelectModal from 'components/common/CascadingSelectModal/index.vue'
import Config from 'components/home/Config.vue' import Config from 'components/home/Config.vue'
import { compareJSON } from 'libs/utils' import { compareJSON } from 'libs/utils'
@ -216,55 +217,52 @@ const onClose = () => {
<div class="home-start-opt"> <div class="home-start-opt">
<div class="home-opt-flex"> <div class="home-opt-flex">
<div> <div>
<bt-button
<BtButton
button-text="压力控制" button-text="压力控制"
text-size="1.3rem" text-size="1.3rem"
border-radius="5px" border-radius="5px"
width="8rem"
width="7.5rem"
text-color="#1989fa" text-color="#1989fa"
padding="0.2vw"
height="3rem" height="3rem"
@click="onSetPressure" @click="onSetPressure"
> >
<template #icon> <template #icon>
<el-icon><Sort /></el-icon> <el-icon><Sort /></el-icon>
</template> </template>
</bt-button>
</BtButton>
</div> </div>
<div class="home-opt-ml"> <div class="home-opt-ml">
<bt-button
<BtButton
button-text="查看图表" button-text="查看图表"
text-size="1.3rem" text-size="1.3rem"
border-radius="5px" border-radius="5px"
width="7rem"
width="7.5rem"
height="3rem" height="3rem"
text-color="#1989fa" text-color="#1989fa"
padding="0.2vw"
:disabled="deviceState" :disabled="deviceState"
@click="onShowChart" @click="onShowChart"
> >
<template #icon> <template #icon>
<img :src="homeChart" width="12" alt="">
<el-icon><Picture /></el-icon>
</template> </template>
</bt-button>
</BtButton>
</div> </div>
<div class="home-opt-ml"> <div class="home-opt-ml">
<bt-button
<BtButton
v-if="deviceState" v-if="deviceState"
button-text="消毒设置" button-text="消毒设置"
text-size="1.3rem" text-size="1.3rem"
border-radius="5px" border-radius="5px"
width="8rem"
width="7.5rem"
text-color="#1989fa" text-color="#1989fa"
padding="0.2vw"
height="3rem" height="3rem"
@click="onDisinfectConfig" @click="onDisinfectConfig"
> >
<template #icon> <template #icon>
<img :src="homeSettingSvg" width="15" alt="">
<img :src="homeSettingSvg" width="12" style="margin-right: 5px" alt="">
</template> </template>
</bt-button>
<bt-button
</BtButton>
<BtButton
v-else v-else
button-text="运行参数" button-text="运行参数"
text-size="1rem" text-size="1rem"
@ -277,14 +275,39 @@ const onClose = () => {
<template #icon> <template #icon>
<img :src="homeRunSvg" width="15" alt=""> <img :src="homeRunSvg" width="15" alt="">
</template> </template>
</bt-button>
</BtButton>
</div> </div>
</div> </div>
</div> </div>
<ft-dialog v-model="disinfectFormulaVisible" width="80vw" :ok-handle="onSave" @cancel="onClose">
<ft-dialog v-model="disinfectFormulaVisible" width="80vw" style="height: 95vh">
<div> <div>
<Config ref="configRef" /> <Config ref="configRef" />
</div> </div>
<template #footer>
<div class="config-btn">
<BtButton
bgColor="#1989fa"
button-text="确认"
text-size="1rem"
border-radius="5px"
width="7rem"
textSize="1.5rem"
text-color="#ffffff"
height="3rem"
@click="onSave"
/>
<BtButton
button-text="取消"
text-size="1rem"
border-radius="5px"
width="7rem"
textSize="1.5rem"
text-color="#1989fa"
height="3rem"
@click="onClose"
/>
</div>
</template>
</ft-dialog> </ft-dialog>
<CascadingSelectModal <CascadingSelectModal
v-if="isModalOpen" v-if="isModalOpen"
@ -312,4 +335,7 @@ const onClose = () => {
} }
} }
} }
.config-btn{
margin-top: -3rem
}
</style> </style>

6
src/components/home/config.vue

@ -104,7 +104,8 @@ const onDefaultFormula = () => {
<bt-button <bt-button
type="primary" type="primary"
button-text="选择配方" button-text="选择配方"
width="10vw"
width="12vw"
height="5vh"
@click="onChooseFormula" @click="onChooseFormula"
> >
<template #icon> <template #icon>
@ -115,7 +116,8 @@ const onDefaultFormula = () => {
</bt-button> </bt-button>
<bt-button <bt-button
button-text="默认配置" button-text="默认配置"
width="10vw"
height="5vh"
width="12vw"
@click="onDefaultFormula" @click="onDefaultFormula"
> >
<template #icon> <template #icon>

2
src/components/seal/DashboardChart.vue

@ -74,7 +74,7 @@ onUnmounted(() => {
<style> <style>
canvas{ canvas{
width: 50vw !important;
width: 60vw !important;
height: 60vh !important; height: 60vh !important;
} }
</style> </style>

1
src/components/setting/Device.vue

@ -38,5 +38,6 @@ const screenHeight = ref(window.screen.height)
border-radius: 10px; border-radius: 10px;
margin-top: 1rem; margin-top: 1rem;
padding-left: 2rem; padding-left: 2rem;
font-size: 1.5rem;
} }
</style> </style>

4
src/components/setting/History.vue

@ -114,8 +114,8 @@ const onClose = () => {
<div class="history-table"> <div class="history-table">
<el-table :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> <el-table :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="name" label="操作人" />
<el-table-column prop="detail" label="操作">
<el-table-column prop="name" label="消毒日期" />
<el-table-column prop="detail" label="操作" width="60">
<template #default="scoped"> <template #default="scoped">
<el-link type="primary" @click="showDetail(scoped.row)"> <el-link type="primary" @click="showDetail(scoped.row)">
查看 查看

21
src/components/setting/SystemDate.vue

@ -1,11 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { syncSendCmd } from '@/apis/system' import { syncSendCmd } from '@/apis/system'
import DatePicker from '@/components/common/DatePicker/index.vue'
import { FtMessage } from '@/libs/message' import { FtMessage } from '@/libs/message'
import { formatDateTime } from '@/libs/utils'
import { ref } from 'vue'
const dateVal = ref(formatDateTime())
const onChangeDate = async (value: string) => { const onChangeDate = async (value: string) => {
console.log('value---', value)
if (value) { if (value) {
const splitDate = value.split(' ') // const splitDate = value.split(' ') //
const YMD = splitDate[0].split('-') const YMD = splitDate[0].split('-')
@ -52,14 +51,9 @@ const onChangeDate = async (value: string) => {
<div class="date-main"> <div class="date-main">
<div class="date-com"> <div class="date-com">
设置日期 设置日期
<el-date-picker
v-model="dateVal"
style="width: 20rem"
type="datetime"
placeholder="日期:年-月-日 时:分:秒"
<DatePicker
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="onChangeDate"
@change="(val) => onChangeDate(val)"
/> />
</div> </div>
</div> </div>
@ -69,5 +63,12 @@ const onChangeDate = async (value: string) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.date-main{ .date-main{
margin: 2rem; margin: 2rem;
display: flex;
justify-content: center;
.date-com{
font-size: 1.5rem;
display: flex;
align-items: center;
}
} }
</style> </style>

2
src/components/setting/User.vue

@ -83,7 +83,7 @@ const handleSelectionChange = (users: User.UserItem[]) => {
<el-table :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> <el-table :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="name" label="用户名" /> <el-table-column prop="name" label="用户名" />
<el-table-column prop="detail" label="操作">
<el-table-column prop="detail" label="操作" width="100">
<template #default="scoped"> <template #default="scoped">
<el-link type="primary" @click="updatePwd(scoped.row)"> <el-link type="primary" @click="updatePwd(scoped.row)">
修改密码 修改密码

15
src/lang/en.ts

@ -0,0 +1,15 @@
// src/lang/en.ts
export default {
message: {
welcome: 'Welcome to Vue 3 + TypeScript Internationalization Demo',
switch: 'Switch Language',
greeting: 'Hello!',
content: 'This is a demonstration of implementing internationalization in Vue3 + TypeScript.',
features: 'Features',
feature1: 'Language switching between Chinese and English',
feature2: 'Automatically save user language preferences',
feature3: 'Supports dynamic language switching without page refresh',
footer: '© 2025 Vue3 Internationalization Demo',
disinfectant: 'Disinfectant',
},
}

18
src/lang/index.ts

@ -0,0 +1,18 @@
// src/lang/index.ts
import en from './en'
import zh from './zh'
// 定义语言类型
export type LocaleType = 'zh' | 'en'
// 定义语言包类型
export type MessageSchema = typeof en
// 导出语言包
export const messages = {
en,
zh,
}
// 导出默认语言
export const defaultLocale: LocaleType = 'zh'

15
src/lang/zh.ts

@ -0,0 +1,15 @@
// src/lang/zh.ts
export default {
message: {
welcome: '欢迎来到 Vue 3 + TypeScript 国际化演示',
switch: '切换语言',
greeting: '你好!',
content: '这是一个在 Vue3 + TypeScript 中实现国际化的演示。',
features: '功能特点',
feature1: '中英文语言切换',
feature2: '自动保存用户语言偏好',
feature3: '支持动态切换语言无需刷新页面',
footer: '© 2025 Vue3 国际化演示',
disinfectant: '消毒液',
},
}

42
src/layouts/default.vue

@ -13,8 +13,10 @@ import { formatDateTime, openFullscreen } from 'libs/utils'
import { authRoutes } from 'router/routes' import { authRoutes } from 'router/routes'
import { useDeviceStore } from 'stores/deviceStore' import { useDeviceStore } from 'stores/deviceStore'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue' import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const { locale } = useI18n()
const router = useRouter() const router = useRouter()
const liquidStore = useLiquidStore() const liquidStore = useLiquidStore()
const sealStore = useSealStore() const sealStore = useSealStore()
@ -28,7 +30,7 @@ const deviceInfo = ref<Device.DeviceInfo>(deviceStore.deviceInfo)
const timeInterval = setInterval(() => { const timeInterval = setInterval(() => {
currentTime.value = formatDateTime() currentTime.value = formatDateTime()
}, 1000) }, 1000)
const wordStateName = ref<string>('空闲')
const workStateName = ref<string>('空闲')
const disinfectState = ref(homeStore.disinfectionState.state) const disinfectState = ref(homeStore.disinfectionState.state)
const liquidAddState = ref(liquidStore.liquidAddWorkState) const liquidAddState = ref(liquidStore.liquidAddWorkState)
const liquidDrainState = ref(liquidStore.liquidDrainWorkState) const liquidDrainState = ref(liquidStore.liquidDrainWorkState)
@ -65,20 +67,20 @@ const onFullScreen = () => {
const showDeviceStateName = () => { const showDeviceStateName = () => {
if (deviceStete.value.state.toLocaleLowerCase() !== 'idle') { if (deviceStete.value.state.toLocaleLowerCase() !== 'idle') {
if (disinfectState.value !== 'idle' && disinfectState.value !== 'finished') { if (disinfectState.value !== 'idle' && disinfectState.value !== 'finished') {
wordStateName.value = homeStore.disinfectionState.statedisplayName
workStateName.value = homeStore.disinfectionState.statedisplayName
} }
else if (liquidAddState.value.workState !== 'idle') { else if (liquidAddState.value.workState !== 'idle') {
wordStateName.value = liquidAddState.value.workStateDisplay
workStateName.value = liquidAddState.value.workStateDisplay
} }
else if (liquidDrainState.value.workState !== 'idle') { else if (liquidDrainState.value.workState !== 'idle') {
wordStateName.value = liquidDrainState.value.workStateDisplay
workStateName.value = liquidDrainState.value.workStateDisplay
} }
else if (sealInfo.value.workState !== 'idle') { else if (sealInfo.value.workState !== 'idle') {
wordStateName.value = sealInfo.value.workStateDisplay
workStateName.value = sealInfo.value.workStateDisplay
} }
} }
else { else {
wordStateName.value = '空闲'
workStateName.value = '空闲'
} }
} }
@ -113,6 +115,12 @@ const onLogout = () => {
router.push('/login') router.push('/login')
}) })
} }
//
const toggleLanguage = () => {
locale.value = locale.value === 'zh' ? 'en' : 'zh'
localStorage.setItem('locale', locale.value) //
}
</script> </script>
<template> <template>
@ -141,7 +149,7 @@ const onLogout = () => {
</div> </div>
</div> </div>
<div class="user"> <div class="user">
<el-select v-model="languageType" style="width: 100px; border-radius: 5px;" disabled>
<el-select v-model="languageType" class="select-language" @change="toggleLanguage">
<el-option v-for="language in languages" :key="language.value" :value="language.value" :label="language.name"> <el-option v-for="language in languages" :key="language.value" :value="language.value" :label="language.name">
{{ language.name }} {{ language.name }}
</el-option> </el-option>
@ -169,7 +177,7 @@ const onLogout = () => {
</div> </div>
<div class="alarm-info"> <div class="alarm-info">
<img :src="HomeAlarmSvg" width="20" alt="报警"> <img :src="HomeAlarmSvg" width="20" alt="报警">
<div>{{ wordStateName || '空闲' }}</div>
<div class="alarm-workState">{{ workStateName || '空闲' }}</div>
</div> </div>
<div class="time"> <div class="time">
{{ currentTime }} {{ currentTime }}
@ -241,6 +249,12 @@ const onLogout = () => {
.user { .user {
width: 20vw; width: 20vw;
text-align: right; text-align: right;
right: 5px;
.select-language {
width: 100px;
border-radius: 5px;
margin-right: 5px;
}
.user-logout { .user-logout {
margin-left: auto; margin-left: auto;
} }
@ -380,27 +394,31 @@ const onLogout = () => {
} }
.ip-info{ .ip-info{
font-size: 1rem;
font-size: 1.5rem;
width: 20vw; width: 20vw;
padding-left: 1.3vw; padding-left: 1.3vw;
} }
.alarm-info { .alarm-info {
font-size: 1rem;
width: 60vw;
font-size: 1.5rem;
width: 55vw;
padding-left: 1.3vw; padding-left: 1.3vw;
background: #F5F5F5; background: #F5F5F5;
height: 5vh; height: 5vh;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
.alarm-workState{
margin-left: 5px;
}
} }
.time { .time {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 5px; border-radius: 5px;
margin-left: 2.5vw;
margin-left: 3vw;
padding: 1vw; padding: 1vw;
font-size: 1.5rem;
} }
} }
.aside-item { .aside-item {

2
src/libs/pinyinDict.json

@ -395,4 +395,4 @@
"zui": ["最", "嘴", "醉", "罪", "赘", "啐", "淬", "萃", "瘁", "粹"], "zui": ["最", "嘴", "醉", "罪", "赘", "啐", "淬", "萃", "瘁", "粹"],
"zun": ["尊", "遵", "樽", "鳟", "僔", "噂", "撙", "墫", "罇", "譐"], "zun": ["尊", "遵", "樽", "鳟", "僔", "噂", "撙", "墫", "罇", "譐"],
"zuo": ["做", "左", "坐", "座", "作", "昨", "琢", "佐", "唑", "柞"] "zuo": ["做", "左", "坐", "座", "作", "昨", "琢", "佐", "唑", "柞"]
}
}

2
src/libs/utils.ts

@ -1,4 +1,4 @@
export function formatDateTime(template: string = 'YYYY/MM/DD HH:mm:ss'): string {
export function formatDateTime(template: string = 'YYYY-MM-DD HH:mm:ss'): string {
const now = new Date() const now = new Date()
const tokens: Record<string, string> = { const tokens: Record<string, string> = {
YYYY: String(now.getFullYear()), YYYY: String(now.getFullYear()),

19
src/main.ts

@ -1,3 +1,4 @@
import type { LocaleType } from './lang'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import BTButton from 'components/common/BTButton/index.vue' import BTButton from 'components/common/BTButton/index.vue'
import FtButton from 'components/common/FTButton/index.vue' import FtButton from 'components/common/FTButton/index.vue'
@ -8,11 +9,27 @@ import locale from 'element-plus/es/locale/lang/zh-cn'
import ErrorBox from 'libs/modalUtil' import ErrorBox from 'libs/modalUtil'
import pinia from 'stores/index' import pinia from 'stores/index'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './app.vue' import App from './app.vue'
import { defaultLocale, messages } from './lang'
import router from './router' import router from './router'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'assets/styles/main.scss' import 'assets/styles/main.scss'
// 创建 i18n 实例
const i18n = createI18n<{
locale: LocaleType
messages: {
en: typeof messages.en
zh: typeof messages.zh
}
}>({
legacy: false,
locale: localStorage.getItem('locale') as LocaleType || defaultLocale,
fallbackLocale: 'en',
messages,
})
const app = createApp(App) const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component)
@ -23,6 +40,7 @@ app.directive('prevent-keyboard', {
el.querySelector('input').setAttribute('readonly', 'readonly') el.querySelector('input').setAttribute('readonly', 'readonly')
}, },
}) })
app.config.warnHandler = () => null
app.use(pinia) app.use(pinia)
app.use(ErrorBox) app.use(ErrorBox)
app.component('BtButton', BTButton) app.component('BtButton', BTButton)
@ -31,5 +49,6 @@ app.component('FtButton', FtButton)
app.component('FtDialog', FtDialog) app.component('FtDialog', FtDialog)
app app
.use(router) .use(router)
.use(i18n)
.use(ElementPlus, { locale, zIndex: 3000 }) .use(ElementPlus, { locale, zIndex: 3000 })
.mount('#app') .mount('#app')

6
src/views/audit/index.vue

@ -106,8 +106,8 @@ const handleSelectionChange = (users: Audit.AuditItem[]) => {
<el-table class="audit-table" :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> <el-table class="audit-table" :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="usrName" label="操作人" width="150" /> <el-table-column prop="usrName" label="操作人" width="150" />
<el-table-column prop="behaviorinfo" label="操作内容" align="center" />
<el-table-column prop="date" label="操作时间" width="300" />
<el-table-column prop="behaviorinfo" label="操作内容" />
<el-table-column prop="date" label="操作时间" width="150" />
</el-table> </el-table>
<div class="audit-pagination"> <div class="audit-pagination">
<el-pagination layout="prev, pager, next" :total="totle" @current-change="handleCurrentChange" /> <el-pagination layout="prev, pager, next" :total="totle" @current-change="handleCurrentChange" />
@ -127,7 +127,7 @@ const handleSelectionChange = (users: Audit.AuditItem[]) => {
margin: 2vw; margin: 2vw;
} }
.audit-pagination{ .audit-pagination{
bottom: 1rem;
bottom: 2rem;
position: absolute; position: absolute;
right: 2rem; right: 2rem;
} }

11
src/views/formula/index.vue

@ -23,8 +23,9 @@ const onAddFormula = () => {
<div class="formula-left"> <div class="formula-left">
<div class="formula-add"> <div class="formula-add">
<bt-button <bt-button
type="primary"
button-text="新增配方" button-text="新增配方"
border-radius="10px"
class="formula-add-btn"
@click="onAddFormula" @click="onAddFormula"
> >
<template #icon> <template #icon>
@ -61,14 +62,14 @@ const onAddFormula = () => {
.formula-add-btn{ .formula-add-btn{
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px;
border-radius: 10px;
height: 3.5rem;
border-radius: 5px;
} }
.formula-list{ .formula-list{
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 70vh;
max-height: 70vh;
height: 75vh;
max-height: 75vh;
} }
} }
.formula-right{ .formula-right{

4
src/views/home/index.vue

@ -84,7 +84,7 @@ const nowLiquid = computed(() => {
<template #default> <template #default>
<!-- <div class="title-line" /> --> <!-- <div class="title-line" /> -->
<div class="card-title-name"> <div class="card-title-name">
<img :src="homeLiquid"> 消毒液
<img :src="homeLiquid"> {{ $t('message.disinfectant') }}
</div> </div>
<div class="card-progress"> <div class="card-progress">
<div class="card-progress-content"> <div class="card-progress-content">
@ -145,7 +145,7 @@ const nowLiquid = computed(() => {
gap:1.5rem; gap:1.5rem;
.card { .card {
text-align: center; text-align: center;
height: 40.1vh;
height: 41.5vh;
width: 30.5vw; width: 30.5vw;
border: 1px solid rgb(225, 225, 225); border: 1px solid rgb(225, 225, 225);
position: relative; position: relative;

7
src/views/seal/index.vue

@ -268,18 +268,17 @@ const stopDisabled = computed(() => {
box-shadow: 0px 1px 5px 0px rgba(9, 39, 62, 0.15); box-shadow: 0px 1px 5px 0px rgba(9, 39, 62, 0.15);
background: $gradient-color; background: $gradient-color;
.seal-chart{ .seal-chart{
margin-left: 3rem;
} }
.chart-ml{ .chart-ml{
margin: 3rem;
height: 25rem;
margin: 2rem;
height: 32rem;
} }
} }
} }
.seal-opt{ .seal-opt{
display: flex; display: flex;
gap: 2rem; gap: 2rem;
margin-left: 6rem;
margin-left: 8rem;
.seal-status{ .seal-status{
display: flex; display: flex;
justify-content: center; justify-content: center;

Loading…
Cancel
Save