Files
lucia-frontend/src/components/durationjs.vue
Cindy Chang 7e362d8740 fix #217; this time finally found the root cause.
If user didn't click any start-end radio button this time,
the start, end value might be null as their initial values are.
So we need to use the earliest value stored in pinia. (In ActRadio.vue created phase)
2024-06-07 15:31:53 +08:00

473 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 id="cyp-timerange-show" v-else class="flex justify-center items-center gap-1">
<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
dhms-input-popup-container"
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 input-dhms-field"
: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"
@change="onChange"
@keyup="onKeyUp"
v-model="inputTimeFields[index]"
/>
<label class="duration">{{ tUnits[unit].dsp }}</label>
</div>
</div>
</div>
</template>
<script>
import { mapActions, } from 'pinia';
import ConformanceInputStore from '@/stores/conformanceInput';
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;
},
},
updateMin: {
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', // d: day; h: hour; m: month; s: second.
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) {
// When the input value exceeds the acceptable maximum value, the front end
// should set the value to be equal to the maximum value.
// 當輸入的數值大於可接受的最大值時,前端要將數值設定成等同於最大值
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();
}
}
},
},
inputTimeFields: {
get() {
let paddedTimeFields = [];
this.inputTypes.map(inputTypeUnit => {
// Pad the dd/hh/mm/ss field string to 2 digits and add it to the list
paddedTimeFields.push(this.tUnits[inputTypeUnit].val.toString().padStart(2, '0'));
});
return paddedTimeFields;
},
}
},
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();
},
},
updateMin: {
handler: function(newValue, oldValue) {
this.minTotal = newValue;
this.calculateTotalSeconds();
},
},
},
methods: {
/**
* 關閉選單視窗
*/
onClose () {
this.openTimeSelect = false;
},
/**
* get focus element
* @param {event} event input 傳入的事件
*/
onFocus(event) {
this.lastInput = event.target;
this.lastInput.select(); // 當呼叫該方法時,文本框內的文字會被自動選中,這樣使用者可以方便地進行複製或刪除等操作。
},
/**
* when blur update input value and show number
* @param {event} event input 傳入的事件
*/
onChange(event) {
let baseInputValue = event.target.value;
let decoratedInputValue;
// 讓前綴數字自動補 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();
decoratedInputValue = 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) {
decoratedInputValue = max.toString().padStart(2, '0');
}else if(inputValue < min) {
decoratedInputValue= min.toString();
}
// 數值更新, tUnits 也更新, 並計算 totalSeconds
const dsp = event.target.dataset.tunit;
this.tUnits[dsp].val = decoratedInputValue;
switch (dsp) {
case 'd':
this.days = baseInputValue;
break;
case 'h':
this.hours = decoratedInputValue;
break;
case 'm':
this.minutes = decoratedInputValue;
break;
case 's':
this.seconds = decoratedInputValue;
break;
};
this.calculateTotalSeconds();
},
/**
* 上下箭頭時的行為
* @param {event} event input 傳入的事件
*/
onKeyUp(event) {
// 正規表達式 \D 即不是 0-9 的字符
event.target.value = event.target.value.replace(/\D/g, '');
// 38上箭頭鍵Arrow Up
// 40下箭頭鍵Arrow Down
if (event.keyCode === 38 || event.keyCode === 40) {
this.actionUpDown(event.target, event.keyCode === 38, true);
};
},
/**
* 上下箭頭時的行為
* @param {element} input 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(goUp) {
// 箭頭向上,數字加一
newVal += this.tUnits[tUnit].inc;
if(newVal > this.tUnits[tUnit].max) {
if(this.tUnits[tUnit].dsp === 'd'){
// 超過 maxDays 要等於最大值
this.totalSeconds = this.maxTotal;
} else {
// 超過該單位最大值時要進位為零
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 'min' | 'max',可選以上任一,最大值或最小值
*/
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 if((this.size === 'min' && totalSeconds <= this.maxTotal)) {
this.maxDays = Math.floor(this.maxTotal / (3600 * 24));
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;
}
}
},
...mapActions(
ConformanceInputStore,[]
),
},
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>