381 lines
11 KiB
Vue
381 lines
11 KiB
Vue
<template>
|
|
<div class=" relative">
|
|
<div class="flex justify-center items-center gap-1 px-1 w-32 border border-neutral-500 cursor-pointer" @click="openTimeSelect = !openTimeSelect">
|
|
<p>{{ days }}d</p>
|
|
<p>{{ hours }}h</p>
|
|
<p>{{ minutes }}m</p>
|
|
<p>{{ seconds }}s</p>
|
|
</div>
|
|
<div class="duration-container absolute left-0 top-full translate-y-2" v-show="openTimeSelect">
|
|
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
|
|
<input
|
|
type="text"
|
|
class="duration duration-val"
|
|
:data-index="index"
|
|
:data-tunit="unit"
|
|
:data-max="tUnits[unit].max"
|
|
:data-min="tUnits[unit].min"
|
|
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
|
|
:value="tUnits[unit].val.toString().padStart(2, '0')"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@keyup="onKeyUp"
|
|
/>
|
|
<label class="duration">{{ tUnits[unit].dsp }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
props: {
|
|
max: {
|
|
type: Number,
|
|
default: 0,
|
|
required: true,
|
|
validator(value) {
|
|
return value >= 0;
|
|
},
|
|
},
|
|
min: {
|
|
type: Number,
|
|
default: 0,
|
|
required: true,
|
|
validator(value) {
|
|
return value >= 0;
|
|
},
|
|
},
|
|
updateMax: {
|
|
type: Number,
|
|
required: false,
|
|
validator(value) {
|
|
return value >= 0;
|
|
},
|
|
},
|
|
size: {
|
|
type: String,
|
|
default: false,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
display: 'dhms',
|
|
seconds: 0,
|
|
minutes: 0,
|
|
hours: 0,
|
|
days: 0,
|
|
maxDays: 0,
|
|
minDays: 0,
|
|
totalSeconds: 0,
|
|
maxTotal: null,
|
|
minTotal: null,
|
|
inputTypes: [],
|
|
lastInput: null,
|
|
openTimeSelect: false,
|
|
};
|
|
},
|
|
computed: {
|
|
tUnits: {
|
|
get() {
|
|
return {
|
|
s: { dsp: 's', inc: 1, val: this.seconds, max: 59, rate: 1, min: 0 },
|
|
m: { dsp: 'm', inc: 1, val: this.minutes, max: 59, rate: 60, min: 0 },
|
|
h: { dsp: 'h', inc: 1, val: this.hours, max: 23, rate: 3600, min: 0 },
|
|
d: { dsp: 'd', inc: 1, val: this.days, max: this.maxDays, rate: 86400, min: this.minDays }
|
|
};
|
|
},
|
|
set(newValues) {
|
|
// 大於最大值時要等於最大值
|
|
for (const unit in newValues) {
|
|
this[unit] = newValues[unit].val;
|
|
const input = document.querySelector(`[data-tunit="${unit}"]`);
|
|
if (input) {
|
|
input.value = newValues[unit].val.toString().padStart(2, '0');
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
watch: {
|
|
max: {
|
|
handler: function(newValue, oldValue) {
|
|
this.maxTotal = newValue;
|
|
this.size === 'max' && newValue !== oldValue ? this.createData() : null;
|
|
},
|
|
immediate: true,
|
|
},
|
|
min: {
|
|
handler: function(newValue, oldValue) {
|
|
this.minTotal = newValue;
|
|
this.size === 'min' && newValue !== oldValue ? this.createData() : null;
|
|
},
|
|
immediate: true,
|
|
},
|
|
// min 的最大值要等於 max 的總秒數
|
|
updateMax: {
|
|
handler: function(newValue, oldValue) {
|
|
this.maxTotal = newValue;
|
|
this.calculateTotalSeconds();
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
/**
|
|
* get focus element
|
|
* @param {event} event
|
|
*/
|
|
onFocus(event) {
|
|
this.lastInput = event.target;
|
|
this.lastInput.select(); // 當呼叫該方法時,文本框內的文字會被自動選中,這樣使用者可以方便地進行複製或刪除等操作。
|
|
},
|
|
/**
|
|
* when blur update input value and show number
|
|
* @param {event} event
|
|
*/
|
|
onBlur(event) {
|
|
let baseInputValue = event.target.value;
|
|
// 讓前綴數字自動補 0
|
|
isNaN(event.target.value) ?
|
|
event.target.value = '00' :
|
|
event.target.value = event.target.value.toString().padStart(2, '0');
|
|
|
|
// 手 key 數值大於最大值時,要等於最大值
|
|
// 先將字串轉為數字才能比大小
|
|
const inputValue = parseInt(event.target.value, 10);
|
|
const max = parseInt(event.target.dataset.max, 10); // 設定最大值
|
|
const min = parseInt(event.target.dataset.min, 10);
|
|
if(inputValue > max) {
|
|
event.target.value = max.toString().padStart(2, '0');
|
|
}else if(inputValue < min) event.target.value= min.toString().padStart(2, '0');
|
|
|
|
// 數值更新, tUnits 也更新, 並計算 totalSeconds
|
|
const dsp = event.target.dataset.tunit;
|
|
this.tUnits[dsp].val = event.target.value;
|
|
switch (dsp) {
|
|
case 'd':
|
|
this.days = baseInputValue;
|
|
break;
|
|
case 'h':
|
|
this.hours = event.target.value;
|
|
break;
|
|
case 'm':
|
|
this.minutes = event.target.value;
|
|
break;
|
|
case 's':
|
|
this.seconds = event.target.value;
|
|
break;
|
|
};
|
|
|
|
this.calculateTotalSeconds();
|
|
},
|
|
/**
|
|
* 上下箭頭時的行為
|
|
* @param {event} event
|
|
*/
|
|
onKeyUp(event) {
|
|
event.target.value = event.target.value.replace(/\D/g, '');
|
|
|
|
if (event.keyCode === 38 || event.keyCode === 40) {
|
|
this.actionUpDown(event.target, event.keyCode === 38, true);
|
|
};
|
|
},
|
|
/**
|
|
* 上下箭頭時的行為
|
|
* @param {element} input
|
|
* @param {number} goUp
|
|
* @param {boolean} selectIt
|
|
*/
|
|
actionUpDown(input, goUp, selectIt = false) {
|
|
const tUnit = input.dataset.tunit;
|
|
let newVal = parseInt(input.value, 10);
|
|
newVal = isNaN(newVal) ? 0 : newVal;
|
|
|
|
// if (newVal <= 0 || newVal > this.tUnits[tUnit].max) {
|
|
// if (newVal === 0 || (newVal < 0 && input.dataset.index < 1)) {
|
|
// newVal = '00';
|
|
// } else if (input.dataset.index >= 1) {
|
|
// const nextUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
|
|
// let nextUnitVal = parseInt(nextUnit.value);
|
|
|
|
// if (newVal < 0 && nextUnitVal > 0) {
|
|
// nextUnit.value = nextUnitVal - 1;
|
|
// nextUnit.dispatchEvent(new Event('blur'));
|
|
// newVal = this.tUnits[tUnit].max;
|
|
// } else if (newVal > 0) {
|
|
// nextUnit.value = nextUnitVal + 1;
|
|
// nextUnit.dispatchEvent(new Event('blur'));
|
|
// newVal = '00';
|
|
// } else {
|
|
// newVal = '00';
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
if(goUp) {
|
|
// 箭頭向上,數字加一
|
|
newVal += this.tUnits[tUnit].inc;
|
|
if(newVal > this.tUnits[tUnit].max) {
|
|
// 超過該單位最大值時要進位為零
|
|
newVal = newVal % (this.tUnits[tUnit].max + 1);
|
|
// 前一個更大的單位要進位
|
|
if(input.dataset.index > 0) {
|
|
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
|
|
this.actionUpDown(prevUnit, true);
|
|
}
|
|
}
|
|
} else {
|
|
// 箭頭向下,數字減一
|
|
newVal -= this.tUnits[tUnit].inc;
|
|
if (newVal < 0) {
|
|
// 小於零要調整為該單位最大值,但下一個單位不動
|
|
newVal = (this.tUnits[tUnit].max + 1) - this.tUnits[tUnit].inc;
|
|
}
|
|
}
|
|
|
|
input.value = newVal.toString().padStart(2, '0');
|
|
switch (tUnit) {
|
|
case 'd':
|
|
this.days = input.value;
|
|
break;
|
|
case 'h':
|
|
this.hours = input.value;
|
|
break;
|
|
case 'm':
|
|
this.minutes = input.value;
|
|
break;
|
|
case 's':
|
|
this.seconds = input.value;
|
|
break;
|
|
};
|
|
if (selectIt) input.select();
|
|
this.calculateTotalSeconds();
|
|
},
|
|
/**
|
|
* 設定 dhms 的數值
|
|
* @param {number} totalSeconds
|
|
* @param {string} size
|
|
*/
|
|
secondToDate(totalSeconds, size) {
|
|
totalSeconds = parseInt(totalSeconds);
|
|
if(!isNaN(totalSeconds)) {
|
|
this.seconds = totalSeconds % 60;
|
|
this.minutes = (Math.floor(totalSeconds - this.seconds) / 60) % 60;
|
|
this.hours = (Math.floor(totalSeconds / 3600)) % 24;
|
|
this.days = Math.floor(totalSeconds / (3600 * 24));
|
|
|
|
if(size === 'max') this.maxDays = Math.floor(totalSeconds / (3600 * 24));
|
|
else if(size === 'min') this.minDays = Math.floor(totalSeconds / (3600 * 24));
|
|
};
|
|
},
|
|
/**
|
|
* 計算總秒數
|
|
*/
|
|
calculateTotalSeconds() {
|
|
let totalSeconds = 0;
|
|
|
|
for (const unit in this.tUnits) {
|
|
const val = parseInt(this.tUnits[unit].val, 10);
|
|
if (!isNaN(val)) {
|
|
totalSeconds += val * this.tUnits[unit].rate;
|
|
}
|
|
}
|
|
|
|
if(totalSeconds >= this.maxTotal){ // 大於最大值時要等於最大值
|
|
totalSeconds = this.maxTotal;
|
|
this.secondToDate(this.maxTotal, 'max');
|
|
} else if (totalSeconds <= this.minTotal) { // 小於最小值時要等於最小值
|
|
totalSeconds = this.minTotal;
|
|
this.secondToDate(this.minTotal, 'min');
|
|
} else {
|
|
this.totalSeconds = totalSeconds;
|
|
};
|
|
this.$emit('total-seconds', totalSeconds);
|
|
},
|
|
/**
|
|
* 初始化
|
|
*/
|
|
async createData() {
|
|
let size = this.size;
|
|
if (this.maxTotal !== await null && this.minTotal !== await null) {
|
|
switch (size) {
|
|
case 'max':
|
|
this.secondToDate(this.minTotal, 'min');
|
|
this.secondToDate(this.maxTotal, 'max');
|
|
this.totalSeconds = this.maxTotal;
|
|
break;
|
|
case 'min':
|
|
this.secondToDate(this.maxTotal, 'max');
|
|
this.secondToDate(this.minTotal, 'min');
|
|
this.totalSeconds = this.minTotal;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// this.$emit('total-seconds', this.totalSeconds);
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.inputTypes = this.display.split('');
|
|
},
|
|
};
|
|
</script>
|
|
<style scoped>
|
|
.duration-container {
|
|
margin: 4px;
|
|
border-radius: 4px;
|
|
background: var(--10, #FFF);
|
|
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25);
|
|
height: 36px;
|
|
width: 221px;
|
|
}
|
|
.duration-box {
|
|
float:left;
|
|
overflow: auto;
|
|
height: var(--main-input-height);
|
|
padding: 4px;
|
|
}
|
|
.duration-box > .duration {
|
|
border: 1px solid var(--main-bg-light);
|
|
border-right: 0;
|
|
border-left: 0;
|
|
background-color:transparent;
|
|
color: var(--main-bg-light);
|
|
}
|
|
.duration-box > .duration:nth-child(1) {
|
|
border-left: 1px solid var(--main-bg-light);
|
|
border-top-left-radius: var(--main-input-br);
|
|
border-bottom-left-radius: var(--main-input-br);
|
|
}
|
|
.duration-box > .duration:nth-last-child(1) {
|
|
border-right: 1px solid var(--main-bg-light);
|
|
border-top-right-radius: var(--main-input-br);
|
|
border-bottom-right-radius: var(--main-input-br);
|
|
}
|
|
.duration {
|
|
float:left;
|
|
display: block;
|
|
overflow: auto;
|
|
height: var(--main-input-height);
|
|
outline: none;
|
|
font-size: 14px;
|
|
margin: 0px 2px;
|
|
}
|
|
.duration-box > label.duration {
|
|
line-height: 28px;
|
|
width: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.duration-box > input[type="text"].duration {
|
|
text-align: right;
|
|
width: 26px;
|
|
padding: 3px 2px 0px 0px;
|
|
}
|
|
.duration-box > input[type="button"].duration {
|
|
width: 60px;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|