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