31 changed files with 1256 additions and 151 deletions
-
75package-lock.json
-
1package.json
-
2src/assets/styles/element.scss
-
2src/assets/styles/variable.scss
-
5src/components/common/BTButton/index.vue
-
2src/components/common/CascadingSelectModal/index.vue
-
601src/components/common/DatePicker/index.vue
-
150src/components/common/DigitalKeyboard/index.vue
-
250src/components/common/SoftKeyboard/index.vue
-
56src/components/formula/FormulaConfig.vue
-
19src/components/formula/FormulaTable.vue
-
2src/components/home/HomeFormula.vue
-
3src/components/home/HomeLogLevel.vue
-
62src/components/home/HomeSetting.vue
-
6src/components/home/config.vue
-
2src/components/seal/DashboardChart.vue
-
1src/components/setting/Device.vue
-
4src/components/setting/History.vue
-
21src/components/setting/SystemDate.vue
-
2src/components/setting/User.vue
-
15src/lang/en.ts
-
18src/lang/index.ts
-
15src/lang/zh.ts
-
42src/layouts/default.vue
-
2src/libs/pinyinDict.json
-
2src/libs/utils.ts
-
19src/main.ts
-
6src/views/audit/index.vue
-
11src/views/formula/index.vue
-
4src/views/home/index.vue
-
7src/views/seal/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 // 6行7列共42个格子 |
|||
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> |
@ -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> |
@ -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', |
|||
}, |
|||
} |
@ -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' |
@ -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: '消毒液', |
|||
}, |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue