Files
lucia-frontend/src/components/durationjs.vue
2024-03-26 17:08:51 +08:00

432 lines
13 KiB
Vue

<template>
<div class="relative">
<div class="w-32 px-1 border border-neutral-500 cursor-pointer" @click="openTimeSelect = !openTimeSelect" :id="size">
<div v-if="totalSeconds === 0" class="text-center">
<p>0</p>
</div>
<div v-else class="flex justify-center items-center gap-1" id="cyp-timerange-show">
<p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p>
<p v-show="minutes != 0">{{ minutes }}m</p>
<p v-show="seconds != 0">{{ seconds }}s</p>
</div>
</div>
<div class="duration-container absolute left-0 top-full translate-y-2" v-show="openTimeSelect" v-closable="{id: size, handler: onClose}">
<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,
},
value: {
type: Number,
required: false,
validator(value) {
return value >= 0;
},
}
},
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'); // 前綴要補 0
input.value = newValues[unit].val.toString();
}
}
},
},
},
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: {
onClose () {
this.openTimeSelect = false;
},
/**
* 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'); // 前綴要補 0
event.target.value = event.target.value.toString();
// 手 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();
// 數值更新, 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'); // 前綴要補 0
input.value = newVal.toString();
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');
this.totalSeconds = totalSeconds;
} else if (totalSeconds <= this.minTotal) { // 小於最小值時要等於最小值
totalSeconds = this.minTotal;
this.secondToDate(this.minTotal, 'min');
this.totalSeconds = totalSeconds;
} 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;
if(this.value !== null) {
this.totalSeconds = this.value;
this.secondToDate(this.value);
}
break;
case 'min':
this.secondToDate(this.maxTotal, 'max');
this.secondToDate(this.minTotal, 'min');
this.totalSeconds = this.minTotal;
if(this.value !== null) {
this.totalSeconds = this.value;
this.secondToDate(this.value);
}
break;
}
}
},
},
created() {
this.$emitter.on('reset', (data) => {
this.createData();
});
},
mounted() {
this.inputTypes = this.display.split('');
},
directives: {
'closable': {
mounted(el, {value}) {
let handleOutsideClick = function(e) {
let target = e.target;
while (target && target.id !== value.id) {
target = target.parentElement;
};
const isClickOutside = target?.id !== value.id && !el.contains(e.target)
if (isClickOutside) value.handler();
e.stopPropagation();
}
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
},
}
},
};
</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>