Migrate all Vue components from Options API to <script setup>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:10:06 +08:00
parent a619be7881
commit 3b7b6ae859
61 changed files with 10835 additions and 11750 deletions

View File

@@ -9,7 +9,7 @@
<!-- 這裡不使用迴圈是因為src值用變數的話會沒辦法顯示svg -->
<li v-if="isAdmin" id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnAcctMgmtClick">
items-center" @click="onBtnAcctMgmtClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-crown.svg" alt="accountManagement"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.acctMgmt")}}</span>
</li>
@@ -29,110 +29,90 @@
</div>
</template>
<script>
import { computed, onMounted, ref, } from 'vue';
import { mapActions, mapState, storeToRefs } from 'pinia';
<script setup>
import { computed, onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import i18next from '@/i18n/i18n';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import emitter from '@/utils/emitter';
export default {
setup() {
const { logOut } = useLoginStore();
const loginStore = useLoginStore();
const router = useRouter();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId } = storeToRefs(conformanceStore);
const router = useRouter();
const route = useRoute();
const loginStore = useLoginStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const loginUserData = ref(null);
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
const isAdmin = ref(false);
const { logOut } = loginStore;
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const { userData } = storeToRefs(loginStore);
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
const getIsAdminValue = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
await acctMgmtStore.getUserDetail(loginUserData.value.username);
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
};
const onBtnMyAccountClick = async() => {
acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push('/my-account');
}
const loginUserData = ref(null);
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
const isAdmin = ref(false);
onMounted(async () => {
await getIsAdminValue();
});
return {
logOut,
tempFilterId,
conformanceLogTempCheckId,
allMapDataStore,
conformanceStore,
isAdmin,
onBtnMyAccountClick,
};
},
data() {
return {
i18next: i18next,
}
},
computed: {
...mapState(useLoginStore, ['userData']),
...mapState(useAcctMgmtStore, ['isAcctMenuOpen']),
},
methods: {
clickOtherPlacesThenCloseMenu(){
const acctMgmtButton = document.getElementById('acct_mgmt_button');
const acctMgmtMenu = document.getElementById('account_menu');
document.addEventListener('click', (event) => {
if (!acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
this.closeAcctMenu();
}
});
},
onBtnAcctMgmtClick(){
this.$router.push({name: 'AcctAdmin'});
this.closeAcctMenu();
},
onLogoutBtnClick(){
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) {
// 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('leaveFilter', false);
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut)
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut)
} else {
this.logOut();
}
},
...mapActions(useLoginStore, ['getUserData']),
...mapActions(useAcctMgmtStore, ['closeAcctMenu']),
},
created() {
this.getUserData();
},
mounted(){
this.clickOtherPlacesThenCloseMenu();
},
const getIsAdminValue = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
await acctMgmtStore.getUserDetail(loginUserData.value.username);
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
};
const onBtnMyAccountClick = async () => {
acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push('/my-account');
};
const clickOtherPlacesThenCloseMenu = () => {
const acctMgmtButton = document.getElementById('acct_mgmt_button');
const acctMgmtMenu = document.getElementById('account_menu');
document.addEventListener('click', (event) => {
if (acctMgmtMenu && acctMgmtButton && !acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
acctMgmtStore.closeAcctMenu();
}
});
};
const onBtnAcctMgmtClick = () => {
router.push({name: 'AcctAdmin'});
acctMgmtStore.closeAcctMenu();
};
const onLogoutBtnClick = () => {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
} else {
logOut();
}
};
// created
loginStore.getUserData();
// mounted
onMounted(async () => {
await getIsAdminValue();
clickOtherPlacesThenCloseMenu();
});
</script>
<style>
#account_menu {
z-index: 2147483648;
}
</style>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div id="search_bar_container" class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full
<div id="search_bar_container" class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full
justify-between">
<input id="input_search" class="w-full outline-0" :placeholder="i18next.t('AcctMgmt.Search')"
v-model="inputQuery" @keypress="handleKeyPressOfSearch"
@@ -9,30 +9,22 @@
</div>
</template>
<script>
import { ref, } from 'vue';
<script setup>
import { ref } from 'vue';
import i18next from '@/i18n/i18n.js';
export default {
setup(props, { emit, }) {
const inputQuery = ref("");
const onSearchClick = (event) => {
event.preventDefault();
emit('on-search-account-button-click', inputQuery.value);
};
const emit = defineEmits(['on-search-account-button-click']);
const handleKeyPressOfSearch = (event) => {
if (event.key === 'Enter') {
emit('on-search-account-button-click', inputQuery.value);
}
}
const inputQuery = ref("");
return {
inputQuery,
onSearchClick,
handleKeyPressOfSearch,
i18next,
};
},
const onSearchClick = (event) => {
event.preventDefault();
emit('on-search-account-button-click', inputQuery.value);
};
</script>
const handleKeyPressOfSearch = (event) => {
if (event.key === 'Enter') {
emit('on-search-account-button-click', inputQuery.value);
}
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
:class="{
'badge-activated': isActivated,
'badge-deactivated': !isActivated,
@@ -11,28 +11,18 @@
{{ displayText }}
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
isActivated: {
type: Boolean,
required: true,
default: true,
},
displayText: {
type: String,
required: true,
default: "Status",
}
<script setup>
defineProps({
isActivated: {
type: Boolean,
required: true,
default: true,
},
setup(props) {
return {
isActivated: props.isActivated,
displayText: props.displayText,
};
displayText: {
type: String,
required: true,
default: "Status",
}
});
</script>
</script>

View File

@@ -15,35 +15,23 @@
</button>
</template>
<script>
import { ref, } from 'vue';
<script setup>
import { ref } from 'vue';
export default {
props: {
buttonText: {
type: String,
required: false,
},
defineProps({
buttonText: {
type: String,
required: false,
},
setup(props) {
const buttonText = props.buttonText;
});
const isPressed = ref(false);
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
}
const onMousedown = () => {
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
}
return {
buttonText,
onMousedown,
onMouseup,
isPressed,
};
},
}
</script>
const onMouseup = () => {
isPressed.value = false;
};
</script>

View File

@@ -16,35 +16,23 @@
</button>
</template>
<script>
import { ref, } from 'vue';
<script setup>
import { ref } from 'vue';
export default {
props: {
buttonText: {
type: String,
required: false,
},
defineProps({
buttonText: {
type: String,
required: false,
},
setup(props) {
const buttonText = props.buttonText;
});
const isPressed = ref(false);
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
}
const onMousedown = () => {
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
}
return {
buttonText,
onMousedown,
onMouseup,
isPressed,
};
},
}
</script>
const onMouseup = () => {
isPressed.value = false;
};
</script>

View File

@@ -146,128 +146,126 @@
</div>
</Sidebar>
</template>
<script>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useCompareStore } from '@/stores/compare';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
export default {
setup() {
const compareStore = useCompareStore();
const props = defineProps({
sidebarState: {
type: Boolean,
require: false,
},
});
return { compareStore };
},
props:{
sidebarState: {
type: Boolean,
require: false,
const route = useRoute();
const compareStore = useCompareStore();
const primaryValueCases = ref(0);
const primaryValueTraces = ref(0);
const primaryValueTaskInstances = ref(0);
const primaryValueTasks = ref(0);
const secondaryValueCases = ref(0);
const secondaryValueTraces = ref(0);
const secondaryValueTaskInstances = ref(0);
const secondaryValueTasks = ref(0);
const primaryStatData = ref(null);
const secondaryStatData = ref(null);
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
const getPercentLabel = (val) => {
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
};
/**
* setting stats data
* @param { object } data fetch API stats data
* @param { string } fileName file Name
* @returns { object } primaryStatData | secondaryStatData回傳 primaryStatData 或 secondaryStatData
*/
const getStatData = (data, fileName) => {
return {
name: fileName,
cases: {
count: data.cases.count.toLocaleString('en-US'),
total: data.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.cases.ratio)
},
},
data() {
return {
primaryValueCases: 0,
primaryValueTraces: 0,
primaryValueTaskInstances: 0,
primaryValueTasks: 0,
secondaryValueCases: 0,
secondaryValueTraces: 0,
secondaryValueTaskInstances: 0,
secondaryValueTasks: 0,
primaryStatData: null,
secondaryStatData: null,
traces: {
count: data.traces.count.toLocaleString('en-US'),
total: data.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.traces.ratio)
},
task_instances: {
count: data.task_instances.count.toLocaleString('en-US'),
total: data.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.task_instances.ratio)
},
tasks: {
count: data.tasks.count.toLocaleString('en-US'),
total: data.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.tasks.ratio)
},
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
case_duration: {
min: getTimeLabel(data.case_duration.min, 2),
max: getTimeLabel(data.case_duration.max, 2),
average: getTimeLabel(data.case_duration.average, 2),
median: getTimeLabel(data.case_duration.median, 2),
}
},
methods: {
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
},
/**
* setting stats data
* @param { object } data fetch API stats data
* @param { string } fileName file Name
* @returns { object } primaryStatData | secondaryStatData回傳 primaryStatData 或 secondaryStatData
*/
getStatData(data, fileName) {
return {
name: fileName,
cases: {
count: data.cases.count.toLocaleString('en-US'),
total: data.cases.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(data.cases.ratio)
},
traces: {
count: data.traces.count.toLocaleString('en-US'),
total: data.traces.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(data.traces.ratio)
},
task_instances: {
count: data.task_instances.count.toLocaleString('en-US'),
total: data.task_instances.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(data.task_instances.ratio)
},
tasks: {
count: data.tasks.count.toLocaleString('en-US'),
total: data.tasks.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(data.tasks.ratio)
},
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
case_duration: {
min: getTimeLabel(data.case_duration.min, 2),
max: getTimeLabel(data.case_duration.max, 2),
average: getTimeLabel(data.case_duration.average, 2),
median: getTimeLabel(data.case_duration.median, 2),
}
}
},
/**
* Behavior when show
*/
show(){
this.primaryValueCases = this.primaryStatData.cases.ratio;
this.primaryValueTraces= this.primaryStatData.traces.ratio;
this.primaryValueTaskInstances = this.primaryStatData.task_instances.ratio;
this.primaryValueTasks = this.primaryStatData.tasks.ratio;
this.secondaryValueCases = this.secondaryStatData.cases.ratio;
this.secondaryValueTraces= this.secondaryStatData.traces.ratio;
this.secondaryValueTaskInstances = this.secondaryStatData.task_instances.ratio;
this.secondaryValueTasks = this.secondaryStatData.tasks.ratio;
},
/**
* Behavior when hidden
*/
hide(){
this.primaryValueCases = 0;
this.primaryValueTraces= 0;
this.primaryValueTaskInstances = 0;
this.primaryValueTasks = 0;
this.secondaryValueCases = 0;
this.secondaryValueTraces= 0;
this.secondaryValueTaskInstances = 0;
this.secondaryValueTasks = 0;
},
},
async mounted() {
const routeParams = this.$route.params;
const primaryType = routeParams.primaryType;
const secondaryType = routeParams.secondaryType;
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const primaryData = await this.compareStore.getStateData(primaryType, primaryId);
const secondaryData = await this.compareStore.getStateData(secondaryType, secondaryId);
const primaryFileName = await this.compareStore.getFileName(primaryId)
const secondaryFileName = await this.compareStore.getFileName(secondaryId)
this.primaryStatData = await this.getStatData(primaryData, primaryFileName);
this.secondaryStatData = await this.getStatData(secondaryData, secondaryFileName);
}
}
};
/**
* Behavior when show
*/
const show = () => {
primaryValueCases.value = primaryStatData.value.cases.ratio;
primaryValueTraces.value = primaryStatData.value.traces.ratio;
primaryValueTaskInstances.value = primaryStatData.value.task_instances.ratio;
primaryValueTasks.value = primaryStatData.value.tasks.ratio;
secondaryValueCases.value = secondaryStatData.value.cases.ratio;
secondaryValueTraces.value = secondaryStatData.value.traces.ratio;
secondaryValueTaskInstances.value = secondaryStatData.value.task_instances.ratio;
secondaryValueTasks.value = secondaryStatData.value.tasks.ratio;
};
/**
* Behavior when hidden
*/
const hide = () => {
primaryValueCases.value = 0;
primaryValueTraces.value = 0;
primaryValueTaskInstances.value = 0;
primaryValueTasks.value = 0;
secondaryValueCases.value = 0;
secondaryValueTraces.value = 0;
secondaryValueTaskInstances.value = 0;
secondaryValueTasks.value = 0;
};
onMounted(async () => {
const routeParams = route.params;
const primaryType = routeParams.primaryType;
const secondaryType = routeParams.secondaryType;
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const primaryData = await compareStore.getStateData(primaryType, primaryId);
const secondaryData = await compareStore.getStateData(secondaryType, secondaryId);
const primaryFileName = await compareStore.getFileName(primaryId)
const secondaryFileName = await compareStore.getFileName(secondaryId)
primaryStatData.value = await getStatData(primaryData, primaryFileName);
secondaryStatData.value = await getStatData(secondaryData, secondaryFileName);
});
</script>
<style scoped>
:deep(.p-progressbar .p-progressbar-value) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,40 +9,33 @@
</div>
</div>
</template>
<script>
<script setup>
import { ref, watch } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
export default {
props: ['data', 'select'],
data() {
return {
sortData: [],
actList: this.select,
}
},
watch: {
data: {
handler: function(newValue) {
this.sortData = sortNumEngZhtw(newValue)
},
immediate: true, // 立即執行一次排序
},
select: function(newValue) {
this.actList = newValue;
}
},
methods: {
/**
* 將選取的 Activities 傳出去
*/
actListData() {
this.$emitter.emit('actListData', this.actList);
}
},
created() {
this.$emitter.on('reset', (data) => {
this.actList = data;
});
},
const props = defineProps(['data', 'select']);
const sortData = ref([]);
const actList = ref(props.select);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(() => props.select, (newValue) => {
actList.value = newValue;
});
/**
* 將選取的 Activities 傳出去
*/
function actListData() {
emitter.emit('actListData', actList.value);
}
// created
emitter.on('reset', (data) => {
actList.value = data;
});
</script>

View File

@@ -9,65 +9,55 @@
</div>
</div>
</template>
<script>
import { mapActions, } from 'pinia';
<script setup>
import { ref, computed, watch } from 'vue';
import { useConformanceInputStore } from "@/stores/conformanceInput";
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
export default {
props: ['title', 'select', 'data', 'category', 'task', 'isSubmit'],
data() {
return {
sortData: [],
localSelect: null,
selectedRadio: null,
}
},
watch: {
data: {
handler: function(newValue) {
this.sortData = sortNumEngZhtw(newValue)
},
immediate: true, // 立即執行一次排序
},
task: function(newValue) {
this.selectedRadio = newValue;
},
},
computed: {
inputActivityRadioData: {
get(){
return {
category: this.category,
task: this.selectedRadio, // For example, "a", or "出院"
};
},
} ,
},
methods: {
/**
* 將選取的 Activity 傳出去
*/
actRadioData() {
this.localSelect = null;
this.$emitter.emit('actRadioData', this.inputActivityRadioData);
this.$emit('selected-task', this.selectedRadio);
this.setActivityRadioStartEndData(this.inputActivityRadioData.task);
},
setGlobalActivityRadioDataState(){
//this.title: value might be "From" or "To"
this.setActivityRadioStartEndData(this.inputActivityRadioData.task, this.title);
},
...mapActions(useConformanceInputStore, ['setActivityRadioStartEndData']),
},
created() {
sortNumEngZhtw(this.sortData);
this.localSelect = this.isSubmit ? this.select : null;
this.selectedRadio = this.localSelect;
this.$emitter.on('reset', (data) => {
this.selectedRadio = data;
});
this.setGlobalActivityRadioDataState();
},
const props = defineProps(['title', 'select', 'data', 'category', 'task', 'isSubmit']);
const emit = defineEmits(['selected-task']);
const conformanceInputStore = useConformanceInputStore();
const sortData = ref([]);
const localSelect = ref(null);
const selectedRadio = ref(null);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(() => props.task, (newValue) => {
selectedRadio.value = newValue;
});
const inputActivityRadioData = computed(() => ({
category: props.category,
task: selectedRadio.value,
}));
/**
* 將選取的 Activity 傳出去
*/
function actRadioData() {
localSelect.value = null;
emitter.emit('actRadioData', inputActivityRadioData.value);
emit('selected-task', selectedRadio.value);
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task);
}
function setGlobalActivityRadioDataState() {
//this.title: value might be "From" or "To"
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task, props.title);
}
// created
sortNumEngZhtw(sortData.value);
localSelect.value = props.isSubmit ? props.select : null;
selectedRadio.value = localSelect.value;
emitter.on('reset', (data) => {
selectedRadio.value = data;
});
setGlobalActivityRadioDataState();
</script>

View File

@@ -41,93 +41,92 @@
</div>
</div>
</template>
<script>
<script setup>
import { ref, computed } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
export default {
props: ['data', 'listSeq', 'isSubmit', 'category'],
data() {
return {
listSequence: [],
lastItemIndex: null,
isSelect: true
}
},
computed: {
datadata: function() {
// Activity List 要排序
let newData;
if(this.data !== null) {
newData = JSON.parse(JSON.stringify(this.data));
sortNumEngZhtw(newData);
}
return newData;
},
},
methods: {
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
moveActItem(index, element){
this.listSequence.push(element);
},
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
moveSeqItem(index, element){
this.listSequence.splice(index, 1);
},
/**
* get listSequence
*/
getComponentData(){
this.$emitter.emit('getListSequence',{
category: this.category,
task: this.listSequence,
});
},
/**
* Element dragging started
*/
onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
// 隱藏拖曳元素原位置
const originalElement = evt.item;
originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
const listIndex = this.listSequence.length - 1;
if(evt.oldIndex === listIndex) this.lastItemIndex = listIndex;
},
/**
* Element dragging ended
*/
onEnd(evt) {
// 顯示拖曳元素
const originalElement = evt.item;
originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用
const lastChild = evt.item.lastChild;
const listIndex = this.listSequence.length - 1
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
}
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
this.lastItemIndex = null;
},
},
created() {
const newlist = JSON.parse(JSON.stringify(this.listSeq));
this.listSequence = this.isSubmit ? newlist : [];
this.$emitter.on('reset', (data) => {
this.listSequence = [];
});
},
const props = defineProps(['data', 'listSeq', 'isSubmit', 'category']);
const listSequence = ref([]);
const lastItemIndex = ref(null);
const isSelect = ref(true);
const datadata = computed(() => {
// Activity List 要排序
let newData;
if(props.data !== null) {
newData = JSON.parse(JSON.stringify(props.data));
sortNumEngZhtw(newData);
}
return newData;
});
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
function moveActItem(index, element) {
listSequence.value.push(element);
}
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
function moveSeqItem(index, element) {
listSequence.value.splice(index, 1);
}
/**
* get listSequence
*/
function getComponentData() {
emitter.emit('getListSequence', {
category: props.category,
task: listSequence.value,
});
}
/**
* Element dragging started
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
// 隱藏拖曳元素原位置
const originalElement = evt.item;
originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
* Element dragging ended
*/
function onEnd(evt) {
// 顯示拖曳元素
const originalElement = evt.item;
originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
}
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
lastItemIndex.value = null;
}
// created
const newlist = JSON.parse(JSON.stringify(props.listSeq));
listSequence.value = props.isSubmit ? newlist : [];
emitter.on('reset', (data) => {
listSequence.value = [];
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";

View File

@@ -50,90 +50,81 @@
</div>
</section>
</template>
<script>
<script setup>
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
export default {
setup() {
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo }
},
data() {
return {
ruleType: [
{id: 1, name: 'Have activity'},
{id: 2, name: 'Activity sequence'},
{id: 3, name: 'Activity duration'},
{id: 4, name: 'Processing time'},
{id: 5, name: 'Waiting time'},
{id: 6, name: 'Cycle time'},
],
activitySequence: [
{id: 1, name: 'Start & End'},
{id: 2, name: 'Sequence'},
],
mode: [
{id: 1, name: 'Directly follows'},
{id: 2, name: 'Eventually follows'},
{id: 3, name: 'Short loop(s)'},
{id: 4, name: 'Self loop(s)'},
],
processScope: [
{id: 1, name: 'End to end'},
{id: 2, name: 'Partial'},
],
actSeqMore: [
{id: 1, name: 'All'},
{id: 2, name: 'Start'},
{id: 3, name: 'End'},
{id: 4, name: 'Start & End'},
],
actSeqFromTo: [
{id: 1, name: 'From'},
{id: 2, name: 'To'},
{id: 3, name: 'From & To'},
]
}
},
methods: {
/**
* 切換 Rule Type 的選項時的行為
*/
changeRadio() {
this.selectedActivitySequence = 'Start & End';
this.selectedMode = 'Directly follows';
this.selectedProcessScope = 'End to end';
this.selectedActSeqMore = 'All';
this.selectedActSeqFromTo = 'From';
this.$emitter.emit('isRadioChange', true); // Radio 切換時,資料要清空
},
/**
* 切換 Activity sequence 的選項時的行為
*/
changeRadioSeq() {
this.$emitter.emit('isRadioSeqChange',true);
},
/**
* 切換 Processing time 的選項時的行為
*/
changeRadioProcessScope() {
this.$emitter.emit('isRadioProcessScopeChange', true);
},
/**
* 切換 Process Scope 的選項時的行為
*/
changeRadioActSeqMore() {
this.$emitter.emit('isRadioActSeqMoreChange', true);
},
/**
* 切換 Activity Sequence 的選項時的行為
*/
changeRadioActSeqFromTo() {
this.$emitter.emit('isRadioActSeqFromToChange', true);
},
}
const ruleType = [
{id: 1, name: 'Have activity'},
{id: 2, name: 'Activity sequence'},
{id: 3, name: 'Activity duration'},
{id: 4, name: 'Processing time'},
{id: 5, name: 'Waiting time'},
{id: 6, name: 'Cycle time'},
];
const activitySequence = [
{id: 1, name: 'Start & End'},
{id: 2, name: 'Sequence'},
];
const mode = [
{id: 1, name: 'Directly follows'},
{id: 2, name: 'Eventually follows'},
{id: 3, name: 'Short loop(s)'},
{id: 4, name: 'Self loop(s)'},
];
const processScope = [
{id: 1, name: 'End to end'},
{id: 2, name: 'Partial'},
];
const actSeqMore = [
{id: 1, name: 'All'},
{id: 2, name: 'Start'},
{id: 3, name: 'End'},
{id: 4, name: 'Start & End'},
];
const actSeqFromTo = [
{id: 1, name: 'From'},
{id: 2, name: 'To'},
{id: 3, name: 'From & To'},
];
/**
* 切換 Rule Type 的選項時的行為
*/
function changeRadio() {
selectedActivitySequence.value = 'Start & End';
selectedMode.value = 'Directly follows';
selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All';
selectedActSeqFromTo.value = 'From';
emitter.emit('isRadioChange', true); // Radio 切換時,資料要清空
}
/**
* 切換 Activity sequence 的選項時的行為
*/
function changeRadioSeq() {
emitter.emit('isRadioSeqChange',true);
}
/**
* 切換 Processing time 的選項時的行為
*/
function changeRadioProcessScope() {
emitter.emit('isRadioProcessScopeChange', true);
}
/**
* 切換 Process Scope 的選項時的行為
*/
function changeRadioActSeqMore() {
emitter.emit('isRadioActSeqMoreChange', true);
}
/**
* 切換 Activity Sequence 的選項時的行為
*/
function changeRadioActSeqFromTo() {
emitter.emit('isRadioActSeqFromToChange', true);
}
</script>

View File

@@ -1,264 +1,258 @@
<template>
<div class="px-4 text-sm">
<!-- Have activity -->
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="containstTasksData" :select="isSubmitTask"></ResultCheck>
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="state.containstTasksData" :select="isSubmitTask"></ResultCheck>
<!-- Activity sequence -->
<ResultDot v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'" :timeResultData="selectCfmSeqSE" :select="isSubmitStartAndEnd"></ResultDot>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="state.selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="state.selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
<!-- Activity duration -->
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="durationData" :select="isSubmitDurationData"></ResultCheck>
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="state.durationData" :select="isSubmitDurationData"></ResultCheck>
<!-- Processing time -->
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmPtEteSE" :select="isSubmitCfmPtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmPtPSE" :select="isSubmitCfmPtPSE"></ResultDot>
<!-- Waiting time -->
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmWtEteSE" :select="isSubmitCfmWtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmWtPSE" :select="isSubmitCfmWtPSE"></ResultDot>
<!-- Cycle time -->
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmCtEteSE" :select="isSubmitCfmCtEteSE"></ResultDot>
</div>
</template>
<script>
<script setup>
import { reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ResultCheck from '@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue';
import ResultArrow from '@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue';
import ResultDot from '@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue';
export default {
setup() {
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected }
},
props: ['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE'],
components: {
ResultCheck,
ResultArrow,
ResultDot,
},
data() {
return {
containstTasksData: null,
startEndData: null,
selectCfmSeqStart: null,
selectCfmSeqEnd: null,
selectCfmSeqDirectly: [],
selectCfmSeqEventually: [],
durationData: null,
selectCfmPtEteStart: null, // Processing time
selectCfmPtEteEnd: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPStart: null,
selectCfmPtPEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteStart: null, // Waiting time
selectCfmWtEteEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPStart: null,
selectCfmWtPEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteStart: null, // Cycle time
selectCfmCtEteEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
startAndEndIsReset: false,
}
},
computed: {
selectCfmSeqSE: function() {
const data = [];
if(this.selectCfmSeqStart) data.push(this.selectCfmSeqStart);
if(this.selectCfmSeqEnd) data.push(this.selectCfmSeqEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
},
selectCfmPtEteSE: function() {
const data = [];
if(this.selectCfmPtEteSEStart) data.push(this.selectCfmPtEteSEStart);
if(this.selectCfmPtEteSEEnd) data.push(this.selectCfmPtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
},
selectCfmPtPSE: function() {
const data = [];
if(this.selectCfmPtPSEStart) data.push(this.selectCfmPtPSEStart);
if(this.selectCfmPtPSEEnd) data.push(this.selectCfmPtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
return order[a.category] - order[b.category];
});
return data;
},
selectCfmWtEteSE: function() {
const data = [];
if(this.selectCfmWtEteSEStart) data.push(this.selectCfmWtEteSEStart);
if(this.selectCfmWtEteSEEnd) data.push(this.selectCfmWtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
},
selectCfmWtPSE: function() {
const data = [];
if(this.selectCfmWtPSEStart) data.push(this.selectCfmWtPSEStart);
if(this.selectCfmWtPSEEnd) data.push(this.selectCfmWtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
return order[a.category] - order[b.category];
});
return data;
},
selectCfmCtEteSE: function() {
const data = [];
if(this.selectCfmCtEteSEStart) data.push(this.selectCfmCtEteSEStart);
if(this.selectCfmCtEteSEEnd) data.push(this.selectCfmCtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
},
},
methods: {
/**
* All reset
*/
reset() {
this.containstTasksData = null;
this.startEndData = null;
this.selectCfmSeqStart = null;
this.selectCfmSeqEnd = null;
this.selectCfmSeqDirectly = [];
this.selectCfmSeqEventually = [];
this.durationData = null;
this.selectCfmPtEteStart = null;
this.selectCfmPtEteEnd = null;
this.selectCfmPtEteSEStart = null;
this.selectCfmPtEteSEEnd = null;
this.selectCfmPtPStart = null;
this.selectCfmPtPEnd = null;
this.selectCfmPtPSEStart = null;
this.selectCfmPtPSEEnd = null;
this.selectCfmWtEteStart = null; // Waiting time
this.selectCfmWtEteEnd = null;
this.selectCfmWtEteSEStart = null;
this.selectCfmWtEteSEEnd = null;
this.selectCfmWtPStart = null;
this.selectCfmWtPEnd = null;
this.selectCfmWtPSEStart = null;
this.selectCfmWtPSEEnd = null;
this.selectCfmCtEteStart = null; // Cycle time
this.selectCfmCtEteEnd = null;
this.selectCfmCtEteSEStart = null;
this.selectCfmCtEteSEEnd = null;
this.startAndEndIsReset = true;
},
},
created() {
this.$emitter.on('actListData', (data) => {
this.containstTasksData = data;
});
this.$emitter.on('actRadioData', (newData) => {
const data = JSON.parse(JSON.stringify(newData)); // 深拷貝原始 cases 的內容
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE']);
const categoryMapping = {
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
};
const state = reactive({
containstTasksData: null,
startEndData: null,
selectCfmSeqStart: null,
selectCfmSeqEnd: null,
selectCfmSeqDirectly: [],
selectCfmSeqEventually: [],
durationData: null,
selectCfmPtEteStart: null, // Processing time
selectCfmPtEteEnd: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPStart: null,
selectCfmPtPEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteStart: null, // Waiting time
selectCfmWtEteEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPStart: null,
selectCfmWtPEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteStart: null, // Cycle time
selectCfmCtEteEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
startAndEndIsReset: false,
});
const updateSelection = (key, mainSelector, secondarySelector) => {
if (this[mainSelector]) {
if (data.task !== this[mainSelector]) this[secondarySelector] = null;
}
data.category = categoryMapping[key][0];
this[mainSelector] = data;
};
const selectCfmSeqSE = computed(() => {
const data = [];
if(state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if(state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
});
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
this[mainSelector] = [data];
}
} else if (this.selectedRuleType === 'Activity duration') {
this.durationData = [data.task];
}
});
this.$emitter.on('getListSequence', (data) => {
switch (data.category) {
case 'cfmSeqDirectly':
this.selectCfmSeqDirectly = data.task;
break;
case 'cfmSeqEventually':
this.selectCfmSeqEventually = data.task;
break;
default:
break;
}
});
this.$emitter.on('reset', (data) => {
this.reset();
});
// Radio 切換時,資料要清空
this.$emitter.on('isRadioChange', (data) => {
if(data) this.reset();
});
this.$emitter.on('isRadioProcessScopeChange', (data) => {
if(data) this.reset();
});
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) this.reset();
});
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) this.reset();
});
},
const selectCfmPtEteSE = computed(() => {
const data = [];
if(state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if(state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmPtPSE = computed(() => {
const data = [];
if(state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if(state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmWtEteSE = computed(() => {
const data = [];
if(state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if(state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmWtPSE = computed(() => {
const data = [];
if(state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if(state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmCtEteSE = computed(() => {
const data = [];
if(state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if(state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
return order[a.category] - order[b.category];
});
return data;
});
/**
* All reset
*/
function reset() {
state.containstTasksData = null;
state.startEndData = null;
state.selectCfmSeqStart = null;
state.selectCfmSeqEnd = null;
state.selectCfmSeqDirectly = [];
state.selectCfmSeqEventually = [];
state.durationData = null;
state.selectCfmPtEteStart = null;
state.selectCfmPtEteEnd = null;
state.selectCfmPtEteSEStart = null;
state.selectCfmPtEteSEEnd = null;
state.selectCfmPtPStart = null;
state.selectCfmPtPEnd = null;
state.selectCfmPtPSEStart = null;
state.selectCfmPtPSEEnd = null;
state.selectCfmWtEteStart = null; // Waiting time
state.selectCfmWtEteEnd = null;
state.selectCfmWtEteSEStart = null;
state.selectCfmWtEteSEEnd = null;
state.selectCfmWtPStart = null;
state.selectCfmWtPEnd = null;
state.selectCfmWtPSEStart = null;
state.selectCfmWtPSEEnd = null;
state.selectCfmCtEteStart = null; // Cycle time
state.selectCfmCtEteEnd = null;
state.selectCfmCtEteSEStart = null;
state.selectCfmCtEteSEEnd = null;
state.startAndEndIsReset = true;
}
// created() logic
emitter.on('actListData', (data) => {
state.containstTasksData = data;
});
emitter.on('actRadioData', (newData) => {
const data = JSON.parse(JSON.stringify(newData)); // 深拷貝原始 cases 的內容
const categoryMapping = {
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
};
const updateSelection = (key, mainSelector, secondarySelector) => {
if (state[mainSelector]) {
if (data.task !== state[mainSelector]) state[secondarySelector] = null;
}
data.category = categoryMapping[key][0];
state[mainSelector] = data;
};
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
state[mainSelector] = [data];
}
} else if (selectedRuleType.value === 'Activity duration') {
state.durationData = [data.task];
}
});
emitter.on('getListSequence', (data) => {
switch (data.category) {
case 'cfmSeqDirectly':
state.selectCfmSeqDirectly = data.task;
break;
case 'cfmSeqEventually':
state.selectCfmSeqEventually = data.task;
break;
default:
break;
}
});
emitter.on('reset', (data) => {
reset();
});
// Radio 切換時,資料要清空
emitter.on('isRadioChange', (data) => {
if(data) reset();
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) reset();
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) reset();
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) reset();
});
</script>
<style scoped>
:deep(.disc) {

View File

@@ -1,9 +1,9 @@
<template>
<section class="animate-fadein w-full h-full" >
<!-- Have activity -->
<ActList v-if="selectedRuleType === 'Have activity'" :data="conformanceTask" :select="isSubmitTask"></ActList>
<!-- Activity sequence -->
<div v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'"
class="flex justify-between items-center w-full h-full">
@@ -12,7 +12,7 @@
<ActRadio :title="'End activity'" :select="isSubmitStartAndEnd?.[1].task" :data="cfmSeqEndData"
:category="'cfmSeqEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" class="w-1/2" />
</div>
<!-- actSeqDrag -->
<ActSeqDrag v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'
&& selectedMode === 'Directly follows'" :data="conformanceTask" :listSeq="isSubmitCfmSeqDirectly"
@@ -24,7 +24,7 @@
<!-- Activity duration -->
<ActRadio v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'"
:select="isSubmitDurationData?.[0]" :data="conformanceTask" :category="'cfmDur'" :isSubmit="isSubmit"/>
<!-- Processing time -->
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmPtEteStart?.[0].task"
@@ -97,346 +97,316 @@
</div>
</section>
</template>
<script>
<script setup>
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ActList from './ActList.vue';
import ActRadio from './ActRadio.vue';
import ActSeqDrag from './ActSeqDrag.vue';
export default {
setup() {
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
} = storeToRefs(conformanceStore);
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
} = storeToRefs(conformanceStore);
return { isLoading, selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart,
cfmPtEteEnd, cfmPtEteSE, cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE,
cfmWtPStart, cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected,
isEndSelected
};
},
props: ['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
],
components: {
ActList,
ActRadio,
ActSeqDrag
},
data() {
return {
task: null,
taskStart: null,
taskEnd: null,
}
},
computed: {
// Activity sequence
cfmSeqStartData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataSeq.task;
return this.isEndSelected ? this.setSeqStartAndEndData(this.cfmSeqEnd, 'sources', this.task) : this.cfmSeqStart.map(i => i.label);
},
cfmSeqEndData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataSeq.task;
return this.isStartSelected ? this.setSeqStartAndEndData(this.cfmSeqStart, 'sinks', this.task) : this.cfmSeqEnd.map(i => i.label);
},
// Processing time
cfmPtEteStartData: function() {
return this.cfmPtEteStart.map(i => i.task);
},
cfmPtEteEndData: function() {
return this.cfmPtEteEnd.map(i => i.task);
},
cfmPtEteSEStartData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataPtEte.task;
return this.isEndSelected ? this.setStartAndEndData(this.cfmPtEteSE, 'end', this.task) : this.setTaskData(this.cfmPtEteSE, 'start');
},
cfmPtEteSEEndData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataPtEte.task;
return this.isStartSelected ? this.setStartAndEndData(this.cfmPtEteSE, 'start', this.task) : this.setTaskData(this.cfmPtEteSE, 'end');
},
cfmPtPStartData: function() {
return this.cfmPtPStart.map(i => i.task);
},
cfmPtPEndData: function() {
return this.cfmPtPEnd.map(i => i.task);
},
cfmPtPSEStartData: function() {
if(this.isSubmit && this.task === null) this.task = this.isSubmitShowDataPtP.task;
return this.isEndSelected ? this.setStartAndEndData(this.cfmPtPSE, 'end', this.task) : this.setTaskData(this.cfmPtPSE, 'start');
},
cfmPtPSEEndData: function() {
if(this.isSubmit && this.task === null) this.task = this.isSubmitShowDataPtP.task;
return this.isStartSelected ? this.setStartAndEndData(this.cfmPtPSE, 'start', this.task) : this.setTaskData(this.cfmPtPSE, 'end');
},
// Waiting time
cfmWtEteStartData: function() {
return this.cfmWtEteStart.map(i => i.task);
},
cfmWtEteEndData: function() {
return this.cfmWtEteEnd.map(i => i.task);
},
cfmWtEteSEStartData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtEte.task;
return this.isEndSelected ? this.setStartAndEndData(this.cfmWtEteSE, 'end', this.task) : this.setTaskData(this.cfmWtEteSE, 'start');
},
cfmWtEteSEEndData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtEte.task;
return this.isStartSelected ? this.setStartAndEndData(this.cfmWtEteSE, 'start', this.task) : this.setTaskData(this.cfmWtEteSE, 'end');
},
cfmWtPStartData: function() {
return this.cfmWtPStart.map(i => i.task);
},
cfmWtPEndData: function() {
return this.cfmWtPEnd.map(i => i.task);
},
cfmWtPSEStartData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtP.task;
return this.isEndSelected ? this.setStartAndEndData(this.cfmWtPSE, 'end', this.task) : this.setTaskData(this.cfmWtPSE, 'start');
},
cfmWtPSEEndData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtP.task;
return this.isStartSelected ? this.setStartAndEndData(this.cfmWtPSE, 'start', this.task) : this.setTaskData(this.cfmWtPSE, 'end');
},
// Cycle time
cfmCtEteStartData: function() {
return this.cfmCtEteStart.map(i => i.task);
},
cfmCtEteEndData: function() {
return this.cfmCtEteEnd.map(i => i.task);
},
cfmCtEteSEStartData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataCt.task;
return this.isEndSelected ? this.setStartAndEndData(this.cfmCtEteSE, 'end', this.task) : this.setTaskData(this.cfmCtEteSE, 'start');
},
cfmCtEteSEEndData: function() {
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataCt.task;
return this.isStartSelected ? this.setStartAndEndData(this.cfmCtEteSE, 'start', this.task) : this.setTaskData(this.cfmCtEteSE, 'end');
},
},
watch: { // 解決儲存後的 Rule 檔,無法重新更改規則之問題
isSubmitShowDataSeq: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
isSubmitShowDataPtEte: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
isSubmitShowDataPtP: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
isSubmitShowDataWtEte: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
isSubmitShowDataWtP: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
isSubmitShowDataCt: {
handler: function(newValue) {
this.taskStart = newValue.taskStart;
this.taskEnd = newValue.taskEnd;
}
},
},
methods: {
/**
* 設定 start and end 的 Radio Data
* @param {object} data cfmSeqStart | cfmSeqEnd | cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE
* 傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
* @returns {array}
*/
setTaskData(data, category) {
let newData = data.map(i => i[category]);
newData = [...new Set(newData)]; // Set 是一種集合型別,只會儲存獨特的值。
return newData;
},
/**
* 重新設定連動的 start and end 的 Radio Data
* @param {object} data cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE
* 傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
* @param {string} task 已選擇的 Activity task
* @returns {array}
*/
setStartAndEndData(data, category, task) {
let oppositeCategory = '';
if (category === 'start') {
oppositeCategory = 'end';
} else {
oppositeCategory = 'start';
};
let newData = data.filter(i => i[category] === task).map(i => i[oppositeCategory]);
newData = [...new Set(newData)];
return newData;
},
/**
* 重新設定 Activity sequence 連動的 start and end 的 Radio Data
* @param {object} data cfmSeqStart | cfmSeqEnd傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'sources' | 'sinks',傳入 'sources' 或 'sinks'。
* @param {string} task 已選擇的 Activity task
* @returns {array}
*/
setSeqStartAndEndData(data, category, task) {
let newData = data.filter(i => i.label === task).map(i => i[category]);
newData = [...new Set(...newData)];
return newData;
},
/**
* select start list's task
* @param {event} e 觸發 input 的詳細事件
*/
selectStart(e) {
this.taskStart = e;
if(this.isStartSelected === null || this.isStartSelected === true){
this.isStartSelected = true;
this.isEndSelected = false;
this.task = e;
this.taskEnd = null;
this.$emitter.emit('sratrAndEndToStart', {
start: true,
end: false,
});
};
},
/**
* select End list's task
* @param {event} e 觸發 input 的詳細事件
*/
selectEnd(e) {
this.taskEnd = e;
if(this.isEndSelected === null || this.isEndSelected === true){
this.isEndSelected = true;
this.isStartSelected = false;
this.task = e;
this.taskStart = null;
this.$emitter.emit('sratrAndEndToStart', {
start: false,
end: true,
});
}
},
/**
* reset all data.
*/
reset() {
this.task = null;
this.isStartSelected = null;
this.isEndSelected = null;
this.taskStart = null;
this.taskEnd = null;
},
/**
* Radio 切換時Start & End Data 連動改變
* @param {boolean} data true | false傳入 true 或 false
*/
setResetData(data) {
if(data) {
if(this.isSubmit) {
switch (this.selectedRuleType) {
case 'Activity sequence':
this.task = this.isSubmitShowDataSeq.task;
this.isStartSelected = this.isSubmitShowDataSeq.isStartSelected;
this.isEndSelected = this.isSubmitShowDataSeq.isEndSelected;
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
]);
const task = ref(null);
const taskStart = ref(null);
const taskEnd = ref(null);
// Activity sequence
const cfmSeqStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isEndSelected.value ? setSeqStartAndEndData(cfmSeqEnd.value, 'sources', task.value) : cfmSeqStart.value.map(i => i.label);
});
const cfmSeqEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isStartSelected.value ? setSeqStartAndEndData(cfmSeqStart.value, 'sinks', task.value) : cfmSeqEnd.value.map(i => i.label);
});
// Processing time
const cfmPtEteStartData = computed(() => {
return cfmPtEteStart.value.map(i => i.task);
});
const cfmPtEteEndData = computed(() => {
return cfmPtEteEnd.value.map(i => i.task);
});
const cfmPtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'end', task.value) : setTaskData(cfmPtEteSE.value, 'start');
});
const cfmPtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'start', task.value) : setTaskData(cfmPtEteSE.value, 'end');
});
const cfmPtPStartData = computed(() => {
return cfmPtPStart.value.map(i => i.task);
});
const cfmPtPEndData = computed(() => {
return cfmPtPEnd.value.map(i => i.task);
});
const cfmPtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isEndSelected.value ? setStartAndEndData(cfmPtPSE.value, 'end', task.value) : setTaskData(cfmPtPSE.value, 'start');
});
const cfmPtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isStartSelected.value ? setStartAndEndData(cfmPtPSE.value, 'start', task.value) : setTaskData(cfmPtPSE.value, 'end');
});
// Waiting time
const cfmWtEteStartData = computed(() => {
return cfmWtEteStart.value.map(i => i.task);
});
const cfmWtEteEndData = computed(() => {
return cfmWtEteEnd.value.map(i => i.task);
});
const cfmWtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'end', task.value) : setTaskData(cfmWtEteSE.value, 'start');
});
const cfmWtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'start', task.value) : setTaskData(cfmWtEteSE.value, 'end');
});
const cfmWtPStartData = computed(() => {
return cfmWtPStart.value.map(i => i.task);
});
const cfmWtPEndData = computed(() => {
return cfmWtPEnd.value.map(i => i.task);
});
const cfmWtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isEndSelected.value ? setStartAndEndData(cfmWtPSE.value, 'end', task.value) : setTaskData(cfmWtPSE.value, 'start');
});
const cfmWtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isStartSelected.value ? setStartAndEndData(cfmWtPSE.value, 'start', task.value) : setTaskData(cfmWtPSE.value, 'end');
});
// Cycle time
const cfmCtEteStartData = computed(() => {
return cfmCtEteStart.value.map(i => i.task);
});
const cfmCtEteEndData = computed(() => {
return cfmCtEteEnd.value.map(i => i.task);
});
const cfmCtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isEndSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'end', task.value) : setTaskData(cfmCtEteSE.value, 'start');
});
const cfmCtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end');
});
// Watchers - 解決儲存後的 Rule 檔,無法重新更改規則之問題
watch(() => props.isSubmitShowDataSeq, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataCt, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
/**
* 設定 start and end 的 Radio Data
* @param {object} data cfmSeqStart | cfmSeqEnd | cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE
* 傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
* @returns {array}
*/
function setTaskData(data, category) {
let newData = data.map(i => i[category]);
newData = [...new Set(newData)]; // Set 是一種集合型別,只會儲存獨特的值。
return newData;
}
/**
* 重新設定連動的 start and end 的 Radio Data
* @param {object} data cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE
* 傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
* @param {string} task 已選擇的 Activity task
* @returns {array}
*/
function setStartAndEndData(data, category, taskVal) {
let oppositeCategory = '';
if (category === 'start') {
oppositeCategory = 'end';
} else {
oppositeCategory = 'start';
};
let newData = data.filter(i => i[category] === taskVal).map(i => i[oppositeCategory]);
newData = [...new Set(newData)];
return newData;
}
/**
* 重新設定 Activity sequence 連動的 start and end 的 Radio Data
* @param {object} data cfmSeqStart | cfmSeqEnd傳入以上任一後端接到的 Activities 列表 Data。
* @param {string} category 'sources' | 'sinks',傳入 'sources' 或 'sinks'。
* @param {string} task 已選擇的 Activity task
* @returns {array}
*/
function setSeqStartAndEndData(data, category, taskVal) {
let newData = data.filter(i => i.label === taskVal).map(i => i[category]);
newData = [...new Set(...newData)];
return newData;
}
/**
* select start list's task
* @param {event} e 觸發 input 的詳細事件
*/
function selectStart(e) {
taskStart.value = e;
if(isStartSelected.value === null || isStartSelected.value === true){
isStartSelected.value = true;
isEndSelected.value = false;
task.value = e;
taskEnd.value = null;
emitter.emit('sratrAndEndToStart', {
start: true,
end: false,
});
};
}
/**
* select End list's task
* @param {event} e 觸發 input 的詳細事件
*/
function selectEnd(e) {
taskEnd.value = e;
if(isEndSelected.value === null || isEndSelected.value === true){
isEndSelected.value = true;
isStartSelected.value = false;
task.value = e;
taskStart.value = null;
emitter.emit('sratrAndEndToStart', {
start: false,
end: true,
});
}
}
/**
* reset all data.
*/
function reset() {
task.value = null;
isStartSelected.value = null;
isEndSelected.value = null;
taskStart.value = null;
taskEnd.value = null;
}
/**
* Radio 切換時Start & End Data 連動改變
* @param {boolean} data true | false傳入 true 或 false
*/
function setResetData(data) {
if(data) {
if(props.isSubmit) {
switch (selectedRuleType.value) {
case 'Activity sequence':
task.value = props.isSubmitShowDataSeq.task;
isStartSelected.value = props.isSubmitShowDataSeq.isStartSelected;
isEndSelected.value = props.isSubmitShowDataSeq.isEndSelected;
break;
case 'Processing time':
switch (selectedProcessScope.value) {
case 'End to end':
task.value = props.isSubmitShowDataPtEte.task;
isStartSelected.value = props.isSubmitShowDataPtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtEte.isEndSelected;
break;
case 'Processing time':
switch (this.selectedProcessScope) {
case 'End to end':
this.task = this.isSubmitShowDataPtEte.task;
this.isStartSelected = this.isSubmitShowDataPtEte.isStartSelected;
this.isEndSelected = this.isSubmitShowDataPtEte.isEndSelected;
break;
case 'Partial':
this.task = this.isSubmitShowDataPtP.task;
this.isStartSelected = this.isSubmitShowDataPtP.isStartSelected;
this.isEndSelected = this.isSubmitShowDataPtP.isEndSelected;
break;
default:
break;
}
break;
case 'Waiting time':
switch (this.selectedProcessScope) {
case 'End to end':
this.task = this.isSubmitShowDataWtEte.task;
this.isStartSelected = this.isSubmitShowDataWtEte.isStartSelected;
this.isEndSelected = this.isSubmitShowDataWtEte.isEndSelected;
break;
case 'Partial':
this.task = this.isSubmitShowDataWtP.task;
this.isStartSelected = this.isSubmitShowDataWtP.isStartSelected;
this.isEndSelected = this.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case 'Cycle time':
this.task = this.isSubmitShowDataCt.task;
this.isStartSelected = this.isSubmitShowDataCt.isStartSelected;
this.isEndSelected = this.isSubmitShowDataCt.isEndSelected;
case 'Partial':
task.value = props.isSubmitShowDataPtP.task;
isStartSelected.value = props.isSubmitShowDataPtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtP.isEndSelected;
break;
default:
break;
}
} else {
this.reset();
}
break;
case 'Waiting time':
switch (selectedProcessScope.value) {
case 'End to end':
task.value = props.isSubmitShowDataWtEte.task;
isStartSelected.value = props.isSubmitShowDataWtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
break;
case 'Partial':
task.value = props.isSubmitShowDataWtP.task;
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case 'Cycle time':
task.value = props.isSubmitShowDataCt.task;
isStartSelected.value = props.isSubmitShowDataCt.isStartSelected;
isEndSelected.value = props.isSubmitShowDataCt.isEndSelected;
break;
default:
break;
}
} else {
reset();
}
},
created() {
this.$emitter.on('isRadioChange', (data) => {
this.setResetData(data);
});
this.$emitter.on('isRadioSeqChange', (data) => {
this.setResetData(data);
});
this.$emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
this.setResetData(data);
};
});
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
this.setResetData(data);
};
});
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
this.setResetData(data);
};
});
this.$emitter.on('reset', data => {
this.reset();
});
}
}
// created() logic
emitter.on('isRadioChange', (data) => {
setResetData(data);
});
emitter.on('isRadioSeqChange', (data) => {
setResetData(data);
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
setResetData(data);
};
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
setResetData(data);
};
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
setResetData(data);
};
});
emitter.on('reset', data => {
reset();
});
</script>

View File

@@ -1,377 +1,384 @@
<template>
<div class="mt-2 mb-12" v-if="selectedRuleType === 'Activity duration' || selectedRuleType === 'Waiting time'
<div class="mt-2 mb-12" v-if="selectedRuleType === 'Activity duration' || selectedRuleType === 'Waiting time'
|| selectedRuleType === 'Processing time' || selectedRuleType === 'Cycle time'">
<p class="h2">Time Range</p>
<div class=" text-sm leading-normal">
<!-- Activity duration -->
<TimeRangeDuration
v-if="selectedRuleType === 'Activity duration'" :time="timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
v-if="selectedRuleType === 'Activity duration'" :time="state.timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Processing time -->
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Waiting time -->
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Cycle time -->
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
</div>
</div>
</template>
<script>
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
<script setup>
import { reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
export default {
setup() {
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
cfmWtEteWhole, cfmCtEteWhole
} = storeToRefs(conformanceStore);
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
cfmWtEteWhole, cfmCtEteWhole
} = storeToRefs(conformanceStore);
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
cfmWtEteWhole, cfmCtEteWhole
};
},
props: ['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
const props = defineProps(['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
'isSubmitTimeCfmPtEteEnd', 'isSubmitTimeCfmPtEteSE', 'isSubmitTimeCfmPtPStart',
'isSubmitTimeCfmPtPEnd', 'isSubmitTimeCfmPtPSE', 'isSubmitTimeCfmWtEteAll',
'isSubmitTimeCfmWtEteStart', 'isSubmitTimeCfmWtEteEnd', 'isSubmitTimeCfmWtEteSE',
'isSubmitTimeCfmWtPStart', 'isSubmitTimeCfmWtPEnd', 'isSubmitTimeCfmWtPSE', 'isSubmitTimeCfmCtEteAll',
'isSubmitTimeCfmCtEteStart', 'isSubmitTimeCfmCtEteEnd', 'isSubmitTimeCfmCtEteSE'
],
data() {
return {
timeDuration: null, // Activity duration
timeCfmPtEteAll: null, // Processing time
timeCfmPtEteAllDefault: null,
timeCfmPtEteStart: null,
timeCfmPtEteEnd: null,
timeCfmPtEteSE: null,
timeCfmPtPStart: null,
timeCfmPtPEnd: null,
timeCfmPtPSE: null,
timeCfmWtEteAll: null, // Waiting time
timeCfmWtEteAllDefault: null,
timeCfmWtEteStart: null,
timeCfmWtEteEnd: null,
timeCfmWtEteSE: null,
timeCfmWtPStart: null,
timeCfmWtPEnd: null,
timeCfmWtPSE: null,
timeCfmCtEteAll: null, // Cycle time
timeCfmCtEteAllDefault: null,
timeCfmCtEteStart: null,
timeCfmCtEteEnd: null,
timeCfmCtEteSE: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
}
},
components: {
TimeRangeDuration,
},
methods: {
/**
* get min total seconds
* @param {Number} e 最小值總秒數
*/
minTotalSeconds(e) {
this.$emit('min-total-seconds', e);
},
/**
* get min total seconds
* @param {Number} e 最大值總秒數
*/
maxTotalSeconds(e) {
this.$emit('max-total-seconds', e);
},
/**
* get Time Range(duration)
* @param {array} data API dataActivity 列表
* @param {string} category 'act' | 'single' | 'double',傳入以上任一值。
* @param {string} task select Radio task or start
* @param {string} taskTwo end
* @returns {object} {min:12, max:345}
*/
getDurationTime(data, category, task, taskTwo) {
let result = {min:0, max:0};
switch (category) {
case 'act':
data.forEach(i => {
if(i.label === task) {
result = i.duration;
}
});
break;
case 'single':
data.forEach(i => {
if(i.task === task) {
result = i.time;
}
});
break;
case 'double':
data.forEach(i => {
if(i.start === task && i.end === taskTwo) {
result = i.time;
}
});
break;
case 'all':
result = data;
break
default:
break;
};
return result;
},
/**
* All reset
*/
reset() {
this.timeDuration = null; // Activity duration
this.timeCfmPtEteAll = this.timeCfmPtEteAllDefault; // Processing time
this.timeCfmPtEteStart = null;
this.timeCfmPtEteEnd = null;
this.timeCfmPtEteSE = null;
this.timeCfmPtPStart = null;
this.timeCfmPtPEnd = null;
this.timeCfmPtPSE = null;
this.timeCfmWtEteAll = this.timeCfmWtEteAllDefault; // Waiting time
this.timeCfmWtEteStart = null;
this.timeCfmWtEteEnd = null;
this.timeCfmWtEteSE = null;
this.timeCfmWtPStart = null;
this.timeCfmWtPEnd = null;
this.timeCfmWtPSE = null;
this.timeCfmCtEteAll = this.timeCfmCtEteAllDefault; // Cycle time
this.timeCfmCtEteStart = null;
this.timeCfmCtEteEnd = null;
this.timeCfmCtEteSE = null;
this.selectCfmPtEteSEStart = null;
this.selectCfmPtEteSEEnd = null;
this.selectCfmPtPSEStart = null;
this.selectCfmPtPSEEnd = null;
this.selectCfmWtEteSEStart = null;
this.selectCfmWtEteSEEnd = null;
this.selectCfmWtPSEStart = null;
this.selectCfmWtPSEEnd = null;
this.selectCfmCtEteSEStart = null;
this.selectCfmCtEteSEEnd = null;
},
},
created() {
this.$emitter.on('actRadioData', (data) => {
const category = data.category;
const task = data.task;
]);
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
this[startKey] = task;
this[timeKey] = { min: 0, max: 0 };
if (this[endKey]) {
this[timeKey] = this.getDurationTime(this[durationType], 'double', task, this[endKey]);
}
};
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const handleSingleSelection = (key, timeKey, durationType) => {
this[timeKey] = this.getDurationTime(this[durationType], 'single', task);
};
const state = reactive({
timeDuration: null, // Activity duration
timeCfmPtEteAll: null, // Processing time
timeCfmPtEteAllDefault: null,
timeCfmPtEteStart: null,
timeCfmPtEteEnd: null,
timeCfmPtEteSE: null,
timeCfmPtPStart: null,
timeCfmPtPEnd: null,
timeCfmPtPSE: null,
timeCfmWtEteAll: null, // Waiting time
timeCfmWtEteAllDefault: null,
timeCfmWtEteStart: null,
timeCfmWtEteEnd: null,
timeCfmWtEteSE: null,
timeCfmWtPStart: null,
timeCfmWtPEnd: null,
timeCfmWtPSE: null,
timeCfmCtEteAll: null, // Cycle time
timeCfmCtEteAllDefault: null,
timeCfmCtEteStart: null,
timeCfmCtEteEnd: null,
timeCfmCtEteSE: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
});
switch (category) {
// Activity duration
case 'cfmDur':
this.timeDuration = this.getDurationTime(this.conformanceAllTasks, 'act', task);
break;
// Processing time
case 'cfmPtEteStart':
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
break;
case 'cfmPtEteEnd':
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
break;
case 'cfmPtEteSEStart':
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtEteSEEnd':
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtPStart':
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
break;
case 'cfmPtPEnd':
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
break;
case 'cfmPtPSEStart':
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
break;
case 'cfmPtPSEEnd':
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
break;
// Waiting time
case 'cfmWtEteStart':
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
break;
case 'cfmWtEteEnd':
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
break;
case 'cfmWtEteSEStart':
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtEteSEEnd':
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtPStart':
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
break;
case 'cfmWtPEnd':
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
break;
case 'cfmWtPSEStart':
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
break;
case 'cfmWtPSEEnd':
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
break;
// Cycle time
case 'cfmCtEteStart':
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
break;
case 'cfmCtEteEnd':
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
break;
case 'cfmCtEteSEStart':
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
case 'cfmCtEteSEEnd':
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
default:
break;
};
});
this.$emitter.on('reset', (data) => {
this.reset();
});
this.$emitter.on('isRadioChange', (data) => {
if(data) {
this.reset();
switch (this.selectedRuleType) {
case 'Processing time':
this.timeCfmPtEteAll = this.getDurationTime(this.cfmPtEteWhole, 'all');
this.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmPtEteAll));
break;
case 'Waiting time':
this.timeCfmWtEteAll = this.getDurationTime(this.cfmWtEteWhole, 'all');
this.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmWtEteAll));
break;
case 'Cycle time':
this.timeCfmCtEteAll = this.getDurationTime(this.cfmCtEteWhole, 'all');
this.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmCtEteAll));
break;
default:
break;
};
}
});
this.$emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
this.reset();
};
});
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
if(this.selectedActSeqMore === 'All') {
switch (this.selectedRuleType) {
case 'Processing time':
this.timeCfmPtEteAll = this.getDurationTime(this.cfmPtEteWhole, 'all');
this.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmPtEteAll));
break;
case 'Waiting time':
this.timeCfmWtEteAll = this.getDurationTime(this.cfmWtEteWhole, 'all');
this.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmWtEteAll));
break;
case 'Cycle time':
this.timeCfmCtEteAll = this.getDurationTime(this.cfmCtEteWhole, 'all');
this.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmCtEteAll));
break;
default:
break;
};
}else this.reset();
};
});
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
this.reset();
};
});
},
// Store refs lookup for dynamic access in handleSingleSelection/handleDoubleSelection
const storeRefs = {
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
};
/**
* get min total seconds
* @param {Number} e 最小值總秒數
*/
function minTotalSeconds(e) {
emit('min-total-seconds', e);
}
/**
* get min total seconds
* @param {Number} e 最大值總秒數
*/
function maxTotalSeconds(e) {
emit('max-total-seconds', e);
}
/**
* get Time Range(duration)
* @param {array} data API dataActivity 列表
* @param {string} category 'act' | 'single' | 'double',傳入以上任一值。
* @param {string} task select Radio task or start
* @param {string} taskTwo end
* @returns {object} {min:12, max:345}
*/
function getDurationTime(data, category, task, taskTwo) {
let result = {min:0, max:0};
switch (category) {
case 'act':
data.forEach(i => {
if(i.label === task) {
result = i.duration;
}
});
break;
case 'single':
data.forEach(i => {
if(i.task === task) {
result = i.time;
}
});
break;
case 'double':
data.forEach(i => {
if(i.start === task && i.end === taskTwo) {
result = i.time;
}
});
break;
case 'all':
result = data;
break
default:
break;
};
return result;
}
/**
* All reset
*/
function reset() {
state.timeDuration = null; // Activity duration
state.timeCfmPtEteAll = state.timeCfmPtEteAllDefault; // Processing time
state.timeCfmPtEteStart = null;
state.timeCfmPtEteEnd = null;
state.timeCfmPtEteSE = null;
state.timeCfmPtPStart = null;
state.timeCfmPtPEnd = null;
state.timeCfmPtPSE = null;
state.timeCfmWtEteAll = state.timeCfmWtEteAllDefault; // Waiting time
state.timeCfmWtEteStart = null;
state.timeCfmWtEteEnd = null;
state.timeCfmWtEteSE = null;
state.timeCfmWtPStart = null;
state.timeCfmWtPEnd = null;
state.timeCfmWtPSE = null;
state.timeCfmCtEteAll = state.timeCfmCtEteAllDefault; // Cycle time
state.timeCfmCtEteStart = null;
state.timeCfmCtEteEnd = null;
state.timeCfmCtEteSE = null;
state.selectCfmPtEteSEStart = null;
state.selectCfmPtEteSEEnd = null;
state.selectCfmPtPSEStart = null;
state.selectCfmPtPSEEnd = null;
state.selectCfmWtEteSEStart = null;
state.selectCfmWtEteSEEnd = null;
state.selectCfmWtPSEStart = null;
state.selectCfmWtPSEEnd = null;
state.selectCfmCtEteSEStart = null;
state.selectCfmCtEteSEEnd = null;
}
// created() logic
emitter.on('actRadioData', (data) => {
const category = data.category;
const task = data.task;
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
state[startKey] = task;
state[timeKey] = { min: 0, max: 0 };
if (state[endKey]) {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'double', task, state[endKey]);
}
};
const handleSingleSelection = (key, timeKey, durationType) => {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'single', task);
};
switch (category) {
// Activity duration
case 'cfmDur':
state.timeDuration = getDurationTime(conformanceAllTasks.value, 'act', task);
break;
// Processing time
case 'cfmPtEteStart':
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
break;
case 'cfmPtEteEnd':
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
break;
case 'cfmPtEteSEStart':
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtEteSEEnd':
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtPStart':
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
break;
case 'cfmPtPEnd':
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
break;
case 'cfmPtPSEStart':
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
break;
case 'cfmPtPSEEnd':
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
break;
// Waiting time
case 'cfmWtEteStart':
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
break;
case 'cfmWtEteEnd':
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
break;
case 'cfmWtEteSEStart':
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtEteSEEnd':
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtPStart':
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
break;
case 'cfmWtPEnd':
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
break;
case 'cfmWtPSEStart':
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
break;
case 'cfmWtPSEEnd':
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
break;
// Cycle time
case 'cfmCtEteStart':
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
break;
case 'cfmCtEteEnd':
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
break;
case 'cfmCtEteSEStart':
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
case 'cfmCtEteSEEnd':
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
default:
break;
};
});
emitter.on('reset', (data) => {
reset();
});
emitter.on('isRadioChange', (data) => {
if(data) {
reset();
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
break;
default:
break;
};
}
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
reset();
};
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
if(selectedActSeqMore.value === 'All') {
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
break;
default:
break;
};
}else reset();
};
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
reset();
};
});
</script>

View File

@@ -8,9 +8,6 @@
</li>
</ul>
</template>
<script>
export default {
name: 'ResultArrow',
props:['data', 'select'],
}
<script setup>
defineProps(['data', 'select']);
</script>

View File

@@ -8,26 +8,21 @@
</li>
</ul>
</template>
<script>
export default {
name: 'ResultCheck',
props:['data', 'select'],
data() {
return {
datadata: null,
}
},
watch: {
data: function(newValue) {
this.datadata = newValue;
},
select: function(newValue) {
this.datadata = newValue;
}
},
created() {
this.datadata = this.select;
this.$emitter.on('reset', data => this.datadata = data);
},
}
<script setup>
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
const props = defineProps(['data', 'select']);
const datadata = ref(props.select);
watch(() => props.data, (newValue) => {
datadata.value = newValue;
});
watch(() => props.select, (newValue) => {
datadata.value = newValue;
});
emitter.on('reset', (val) => datadata.value = val);
</script>

View File

@@ -7,27 +7,17 @@
</li>
</ul>
</template>
<script>
export default {
name: 'ResultDot',
props:['timeResultData', 'select'],
data() {
return {
data: null,
}
},
watch: {
timeResultData: {
handler(newValue) {
this.data = newValue;
},
immediate: true,
deep: true,
},
},
created() {
this.data = this.select;
this.$emitter.on('reset', data => this.data = data);
},
}
<script setup>
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
const props = defineProps(['timeResultData', 'select']);
const data = ref(props.select);
watch(() => props.timeResultData, (newValue) => {
data.value = newValue;
}, { deep: true });
emitter.on('reset', (val) => data.value = val);
</script>

View File

@@ -9,97 +9,78 @@
</Durationjs>
</div>
</template>
<script>
<script setup>
import { ref, watch } from 'vue';
import Durationjs from '@/components/durationjs.vue';
export default {
props: ['time', 'select'],
data() {
return {
timeData: {
min: 0,
max: 0,
},
timeRangeMin: 0,
timeRangeMax: 0,
minVuemin: 0,
minVuemax: 0,
maxVuemin: 0,
maxVuemax: 0,
updateMax: null,
updateMin: null,
durationMin: null,
durationMax: null,
}
},
components: {
Durationjs,
},
watch: {
time: {
handler: function(newValue, oldValue) {
this.durationMax = null
this.durationMin = null
if(newValue === null) {
this.timeData = {
min: 0,
max: 0
};
}else if(newValue !== null) {
this.timeData = {
min: newValue.min,
max: newValue.max
};
this.$emit('min-total-seconds', newValue.min);
this.$emit('max-total-seconds', newValue.max);
}
this.setTimeValue();
},
deep: true,
immediate: true,
},
},
methods: {
/**
* set props values
*/
setTimeValue() {
// 深拷貝原始 timeData 的內容
this.minVuemin = JSON.parse(JSON.stringify(this.timeData.min));
this.minVuemax = JSON.parse(JSON.stringify(this.timeData.max));
this.maxVuemin = JSON.parse(JSON.stringify(this.timeData.min));
this.maxVuemax = JSON.parse(JSON.stringify(this.timeData.max));
},
/**
* get min total seconds
* @param {Number} e 元件傳來的最小值總秒數
*/
minTotalSeconds(e) {
this.timeRangeMin = e;
this.updateMin = e;
this.$emit('min-total-seconds', e);
},
/**
* get min total seconds
* @param {Number} e 元件傳來的最大值總秒數
*/
maxTotalSeconds(e) {
this.timeRangeMax = e;
this.updateMax = e;
this.$emit('max-total-seconds', e);
},
},
created() {
if(this.select){
if(Object.keys(this.select.base).length !== 0) {
this.timeData = this.select.base;
this.setTimeValue();
}
if(Object.keys(this.select.rule).length !== 0) {
this.durationMin = this.select.rule.min;
this.durationMax = this.select.rule.max;
}
}
const props = defineProps(['time', 'select']);
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const timeData = ref({ min: 0, max: 0 });
const timeRangeMin = ref(0);
const timeRangeMax = ref(0);
const minVuemin = ref(0);
const minVuemax = ref(0);
const maxVuemin = ref(0);
const maxVuemax = ref(0);
const updateMax = ref(null);
const updateMin = ref(null);
const durationMin = ref(null);
const durationMax = ref(null);
/**
* set props values
*/
function setTimeValue() {
// 深拷貝原始 timeData 的內容
minVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));
minVuemax.value = JSON.parse(JSON.stringify(timeData.value.max));
maxVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));
maxVuemax.value = JSON.parse(JSON.stringify(timeData.value.max));
}
/**
* get min total seconds
* @param {Number} e 元件傳來的最小值總秒數
*/
function minTotalSeconds(e) {
timeRangeMin.value = e;
updateMin.value = e;
emit('min-total-seconds', e);
}
/**
* get min total seconds
* @param {Number} e 元件傳來的最大值總秒數
*/
function maxTotalSeconds(e) {
timeRangeMax.value = e;
updateMax.value = e;
emit('max-total-seconds', e);
}
watch(() => props.time, (newValue, oldValue) => {
durationMax.value = null;
durationMin.value = null;
if(newValue === null) {
timeData.value = { min: 0, max: 0 };
}else if(newValue !== null) {
timeData.value = { min: newValue.min, max: newValue.max };
emit('min-total-seconds', newValue.min);
emit('max-total-seconds', newValue.max);
}
setTimeValue();
}, { deep: true, immediate: true });
// created
if(props.select){
if(Object.keys(props.select.base).length !== 0) {
timeData.value = props.select.base;
setTimeValue();
}
if(Object.keys(props.select.rule).length !== 0) {
durationMin.value = props.select.rule.min;
durationMax.value = props.select.rule.max;
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<Dialog :visible="listModal" @update:visible="$emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<Dialog :visible="listModal" @update:visible="emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<template #header>
<div class=" py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
@@ -61,219 +61,225 @@
</div>
</Dialog>
</template>
<script>
<script setup>
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
props: ['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category'],
setup() {
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
const props = defineProps(['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category']);
const emit = defineEmits(['closeModal']);
return { infinite404, conformanceStore }
},
data() {
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
// template ref
const cfmTrace = useTemplateRef('cfmTrace');
// data
const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
const showTraceId = ref(null);
const infiniteData = ref(null);
const maxItems = ref(false);
const infiniteFinish = ref(true); // 無限滾動是否載入完成
const startNum = ref(0);
const processMap = ref({
nodes:[],
edges:[],
});
// computed
const traceTotal = computed(() => {
return traceList.value.length;
});
const traceList = computed(() => {
const sum = props.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
return props.listTraces.map(trace => {
return {
contentClass: '!bg-neutral-100 border-t border-neutral-300 h-full',
showTraceId: null,
infiniteData: null,
maxItems: false,
infiniteFinish: true, // 無限滾動是否載入完成
startNum: 0,
processMap:{
nodes:[],
edges:[],
},
}
},
computed: {
traceTotal: function() {
return this.traceList.length;
},
traceList: function() {
const sum = this.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
id: trace.id,
value: Number((getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
});
return this.listTraces.map(trace => {
return {
id: trace.id,
value: Number((this.getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: this.getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
},
caseData: function() {
if(this.infiniteData !== null){
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
});
delete item.facets; // 刪除原本的 facets 屬性
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
}
},
columnData: function() {
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
return result
},
},
watch: {
listModal: function(newValue) { // 第一次打開 Modal 要繪圖
if(newValue) this.createCy();
},
taskSeq: function(newValue){
if (newValue !== null) this.createCy();
},
traceId: function(newValue) {
// 當 traceId 屬性變化時更新 showTraceId
this.showTraceId = newValue;
},
showTraceId: function(newValue, oldValue) {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
},
firstCases: function(newValue, oldValue){
this.infiniteData = newValue;
},
infinite404: function(newValue, oldValue){
if (newValue === 404) this.maxItems = true;
},
},
methods: {
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
},
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id case id
*/
async switchCaseData(id) {
if(id == this.showTraceId) return;
this.infinite404 = null;
this.maxItems = false;
this.startNum = 0;
let result;
if(this.category === 'issue') result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, id, 0);
else if(this.category === 'loop') result = await this.conformanceStore.getConformanceLoopsTraceDetail(this.listNo, id, 0);
this.infiniteData = await result;
this.showTraceId = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
if(this.taskSeq !== null) {
this.taskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
});
};
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
if(this.taskSeq !== null) {
this.taskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
};
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
this.$nextTick(() => {
const graphId = this.$refs.cfmTrace;
this.setNodesData();
this.setEdgesData();
if(graphId !== null) cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
const caseData = computed(() => {
if(infiniteData.value !== null){
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
});
},
/**
* 無限滾動: 載入數據
*/
async fetchData() {
try {
this.infiniteFinish = false;
this.startNum += 20
const result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, this.showTraceId, this.startNum);
this.infiniteData = await [...this.infiniteData, ...result];
this.infiniteFinish = await true;
} catch(error) {
console.error('Failed to load data:', error);
}
},
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 監聽時回傳的事件
*/
handleScroll(event) {
if(this.maxItems || this.infiniteData.length < 20 || this.infiniteFinish === false) return;
delete item.facets; // 刪除原本的 facets 屬性
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
}
});
if (overScrollHeight) this.fetchData();
},
},
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
return result
});
// watch
watch(() => props.listModal, (newValue) => { // 第一次打開 Modal 要繪圖
if(newValue) createCy();
});
watch(() => props.taskSeq, (newValue) => {
if (newValue !== null) createCy();
});
watch(() => props.traceId, (newValue) => {
// 當 traceId 屬性變化時更新 showTraceId
showTraceId.value = newValue;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(() => props.firstCases, (newValue) => {
infiniteData.value = newValue;
});
watch(infinite404, (newValue) => {
if (newValue === 404) maxItems.value = true;
});
// methods
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
function progressWidth(value){
return `width:${value}%;`
}
/**
* switch case data
* @param {number} id case id
*/
async function switchCaseData(id) {
if(id == showTraceId.value) return;
infinite404.value = null;
maxItems.value = false;
startNum.value = 0;
let result;
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0);
else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
infiniteData.value = await result;
showTraceId.value = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
}
/**
* 將 trace element nodes 資料彙整
*/
function setNodesData(){
// 避免每次渲染都重複累加
processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node
if(props.taskSeq !== null) {
props.taskSeq.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
});
};
}
/**
* 將 trace edge line 資料彙整
*/
function setEdgesData(){
processMap.value.edges = [];
if(props.taskSeq !== null) {
props.taskSeq.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
};
// 關係線數量筆節點少一個
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
nextTick(() => {
const graphId = cfmTrace.value;
setNodesData();
setEdgesData();
if(graphId !== null) cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
});
}
/**
* 無限滾動: 載入數據
*/
async function fetchData() {
try {
infiniteFinish.value = false;
startNum.value += 20
const result = await conformanceStore.getConformanceTraceDetail(props.listNo, showTraceId.value, startNum.value);
infiniteData.value = await [...infiniteData.value, ...result];
infiniteFinish.value = await true;
} catch(error) {
console.error('Failed to load data:', error);
}
}
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 監聽時回傳的事件
*/
function handleScroll(event) {
if(maxItems.value || infiniteData.value.length < 20 || infiniteFinish.value === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
</script>
<style scoped>

View File

@@ -57,104 +57,104 @@
</div>
</div>
</template>
<script>
<script setup>
import { ref, computed, watch } from 'vue';
import { sortNumEngZhtwForFilter } from '@/module/sortNumEngZhtw.js';
export default {
props: {
filterTaskData: {
type: Array,
required: true,
},
progressWidth: {
type: Function,
required: false,
},
listSeq: {
type: Array,
required: true,
}
const props = defineProps({
filterTaskData: {
type: Array,
required: true,
},
data() {
return {
listSequence: [],
filteredData: this.filterTaskData,
lastItemIndex: null,
}
progressWidth: {
type: Function,
required: false,
},
computed: {
data: function() {
// Activity List 要排序
this.filteredData = this.filteredData.sort((x, y) => {
const diff = y.occurrences - x.occurrences;
return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label);
});
return this.filteredData;
}
},
watch: {
listSeq(newval){
this.listSequence = newval;
},
filterTaskData(newval){
this.filteredData = newval;
}
},
methods: {
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
moveActItem(index, element){
this.listSequence.push(element);
},
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
moveSeqItem(index, element){
this.listSequence.splice(index, 1);
},
/**
* get listSequence
*/
getComponentData(){
this.$emit('update:listSeq', this.listSequence);
},
/**
* Element dragging started
* @param {event} evt input 傳入的事件
*/
onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
// 隱藏拖曳元素原位置
const originalElement = evt.item;
originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
const listIndex = this.listSequence.length - 1;
if(evt.oldIndex === listIndex) this.lastItemIndex = listIndex;
},
/**
* Element dragging ended
* @param {event} evt input 傳入的事件
*/
onEnd(evt) {
// 顯示拖曳元素
const originalElement = evt.item;
originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用
const lastChild = evt.item.lastChild;
const listIndex = this.listSequence.length - 1
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
}
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
this.lastItemIndex = null;
},
listSeq: {
type: Array,
required: true,
}
});
const emit = defineEmits(['update:listSeq']);
const listSequence = ref([]);
const filteredData = ref(props.filterTaskData);
const lastItemIndex = ref(null);
const data = computed(() => {
// Activity List 要排序
filteredData.value = filteredData.value.sort((x, y) => {
const diff = y.occurrences - x.occurrences;
return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label);
});
return filteredData.value;
});
watch(() => props.listSeq, (newval) => {
listSequence.value = newval;
});
watch(() => props.filterTaskData, (newval) => {
filteredData.value = newval;
});
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
function moveActItem(index, element) {
listSequence.value.push(element);
}
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
function moveSeqItem(index, element) {
listSequence.value.splice(index, 1);
}
/**
* get listSequence
*/
function getComponentData() {
emit('update:listSeq', listSequence.value);
}
/**
* Element dragging started
* @param {event} evt input 傳入的事件
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
// 隱藏拖曳元素原位置
const originalElement = evt.item;
originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
* Element dragging ended
* @param {event} evt input 傳入的事件
*/
function onEnd(evt) {
// 顯示拖曳元素
const originalElement = evt.item;
originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
}
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
lastItemIndex.value = null;
}
</script>
<style scoped>

View File

@@ -28,50 +28,42 @@
</div>
</div>
</template>
<script>
import Search from '@/components/Search.vue';
<script setup>
import { ref, watch } from 'vue';
export default {
props: {
tableTitle: {
type: String,
required: true,
},
tableData: {
type: Array,
required: true,
},
tableSelect: {
type: [Object, Array],
default: null
},
progressWidth: {
type: Function,
required: false,
}
const props = defineProps({
tableTitle: {
type: String,
required: true,
},
data() {
return {
select: null,
metaKey: true
}
tableData: {
type: Array,
required: true,
},
components: {
Search,
},
watch: {
tableSelect(newval){
this.select = newval;
}
},
methods: {
/**
* 將選取的 row 傳到父層
* @param {event} e input 傳入的事件
*/
onRowSelect(e) {
this.$emit('on-row-select', e)
}
tableSelect: {
type: [Object, Array],
default: null
},
progressWidth: {
type: Function,
required: false,
}
});
const emit = defineEmits(['on-row-select']);
const select = ref(null);
const metaKey = ref(true);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
/**
* 將選取的 row 傳到父層
* @param {event} e input 傳入的事件
*/
function onRowSelect(e) {
emit('on-row-select', e);
}
</script>

View File

@@ -39,54 +39,49 @@
</div>
</template>
<script>
import Search from '@/components/Search.vue';
<script setup>
import { ref, watch } from 'vue';
export default {
props: ['tableTitle', 'tableData', 'tableSelect', 'progressWidth'],
data() {
return {
select: null,
data: this.tableData
}
},
components: {
Search,
},
watch: {
tableSelect(newval){
this.select = newval;
}
},
methods: {
/**
* 選擇 Row 的行為
*/
onRowSelect() {
this.$emit('on-row-select', this.select);
},
/**
* 取消選取 Row 的行為
*/
onRowUnselect() {
this.$emit('on-row-select', this.select);
},
/**
* 全選 Row 的行為
* @param {event} e input 傳入的事件
*/
onRowSelectAll(e) {
this.select = e.data;
this.$emit('on-row-select', this.select);
},
/**
* 取消全選 Row 的行為
* @param {event} e input 傳入的事件
*/
onRowUnelectAll(e) {
this.select = null;
this.$emit('on-row-select', this.select)
}
},
const props = defineProps(['tableTitle', 'tableData', 'tableSelect', 'progressWidth']);
const emit = defineEmits(['on-row-select']);
const select = ref(null);
const data = ref(props.tableData);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
/**
* 選擇 Row 的行為
*/
function onRowSelect() {
emit('on-row-select', select.value);
}
/**
* 取消選取 Row 的行為
*/
function onRowUnselect() {
emit('on-row-select', select.value);
}
/**
* 全選 Row 的行為
* @param {event} e input 傳入的事件
*/
function onRowSelectAll(e) {
select.value = e.data;
emit('on-row-select', select.value);
}
/**
* 取消全選 Row 的行為
* @param {event} e input 傳入的事件
*/
function onRowUnelectAll(e) {
select.value = null;
emit('on-row-select', select.value);
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -38,90 +38,89 @@
</div>
</template>
<script>
<script setup>
import { storeToRefs } from 'pinia';
import { useToast } from 'vue-toast-notification';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { delaySecond, } from '@/utils/timeUtil.js';
export default {
setup() {
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
const emit = defineEmits(['submit-all']);
const $toast = useToast();
return { isLoading, hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, allMapDataStore, tempFilterId }
},
methods: {
/**
* @param {boolean} e ture | false可選 ture 或 false
* @param {numble} index rule's index
*/
isRule(e, index){
const rule = this.isRuleData[index];
// 先取得 rule object
// 為了讓 data 順序不亂掉,將值指向 0submitAll 時再刪掉
if(!e) this.temporaryData[index] = 0;
else this.temporaryData[index] = rule;
},
/**
* header:Funnel 刪除全部的 Funnel
* @param {numble|string} index rule's index 或 全部
*/
async deleteRule(index) {
if(index === 'all') {
this.temporaryData = [];
this.isRuleData = [];
this.ruleData = [];
if(this.tempFilterId) {
this.isLoading = true;
this.tempFilterId = await null;
await this.allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllTrace(); // SidebarTrace 要連動
await this.$emit('submit-all');
this.isLoading = false;
}
this.$toast.success('Filter(s) deleted.');
}else{
this.$toast.success(`Filter deleted.`);
this.temporaryData.splice(index, 1);
this.isRuleData.splice(index, 1);
this.ruleData.splice(index, 1);
}
},
/**
* header:Funnel 發送暫存的選取資料
*/
async submitAll() {
this.postRuleData = this.temporaryData.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目
if(!this.postRuleData?.length) return this.$toast.error('Not selected');
await this.allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
if(this.hasResultRule === null) {
return;
} else if(this.hasResultRule) {
this.isLoading = true;
await this.allMapDataStore.addTempFilterId();
await this.allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllTrace(); // SidebarTrace 要連動
if(this.temporaryData[0]?.type) {
this.allMapDataStore.traceId = await this.allMapDataStore.traces[0]?.id;
}
await this.$emit('submit-all');
this.isLoading = false;
this.$toast.success('Filter(s) applied.');
return;
}
/**
* @param {boolean} e ture | false可選 ture 或 false
* @param {numble} index rule's index
*/
function isRule(e, index){
const rule = isRuleData.value[index];
// 先取得 rule object
// 為了讓 data 順序不亂掉,將值指向 0submitAll 時再刪掉
if(!e) temporaryData.value[index] = 0;
else temporaryData.value[index] = rule;
}
// sonar-qube "This statement will not be executed conditionally"
this.isLoading = true;
await delaySecond(1);
this.isLoading = false;
this.$toast.warning('No result.');
},
/**
* header:Funnel 刪除全部的 Funnel
* @param {numble|string} index rule's index 或 全部
*/
async function deleteRule(index) {
if(index === 'all') {
temporaryData.value = [];
isRuleData.value = [];
ruleData.value = [];
if(tempFilterId.value) {
isLoading.value = true;
tempFilterId.value = await null;
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動
await emit('submit-all');
isLoading.value = false;
}
$toast.success('Filter(s) deleted.');
}else{
$toast.success(`Filter deleted.`);
temporaryData.value.splice(index, 1);
isRuleData.value.splice(index, 1);
ruleData.value.splice(index, 1);
}
}
/**
* header:Funnel 發送暫存的選取資料
*/
async function submitAll() {
postRuleData.value = temporaryData.value.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目
if(!postRuleData.value?.length) return $toast.error('Not selected');
await allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果
if(hasResultRule.value === null) {
return;
} else if(hasResultRule.value) {
isLoading.value = true;
await allMapDataStore.addTempFilterId();
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動
if(temporaryData.value[0]?.type) {
allMapDataStore.traceId = await allMapDataStore.traces[0]?.id;
}
await emit('submit-all');
isLoading.value = false;
$toast.success('Filter(s) applied.');
return;
}
// sonar-qube "This statement will not be executed conditionally"
isLoading.value = true;
await delaySecond(1);
isLoading.value = false;
$toast.warning('No result.');
}
</script>
<style scoped>

View File

@@ -30,327 +30,326 @@
</section>
</div>
</template>
<script>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-moment';
import getMoment from 'moment';
export default{
props:['selectValue'],
setup() {
const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
const props = defineProps(['selectValue']);
return {allMapDataStore, filterTimeframe, selectTimeFrame }
const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
const selectRange = ref(1000); // 更改 select 的切分數
const selectArea = ref(null);
const chart = ref(null);
const canvasId = ref(null);
const startTime = ref(null);
const endTime = ref(null);
const startMinDate = ref(null);
const startMaxDate = ref(null);
const endMinDate = ref(null);
const endMaxDate = ref(null);
const panelProps = ref({
onClick: (event) => {
event.stopPropagation();
},
data() {
return {
selectRange: 1000, // 更改 select 的切分數
selectArea: null,
chart: null,
canvasId: null,
startTime: null,
endTime: null,
startMinDate: null,
startMaxDate: null,
endMinDate: null,
endMaxDate: null,
panelProps: {
onClick: (event) => {
event.stopPropagation();
},
},
}
},
computed: {
// user select time start and end
timeFrameStartEnd: function() {
const start = getMoment(this.startTime).format('YYYY-MM-DDTHH:mm:00');
const end = getMoment(this.endTime).format('YYYY-MM-DDTHH:mm:00');
this.selectTimeFrame = [start, end]; // 傳給後端的資料
});
return [start, end];
},
// 找出 slidrData時間格式:毫秒時間戳
sliderData: function() {
const xAxisMin = new Date(this.filterTimeframe.x_axis.min).getTime();
const xAxisMax = new Date(this.filterTimeframe.x_axis.max).getTime();
const range = xAxisMax - xAxisMin;
const step = range / this.selectRange;
const sliderData = []
// user select time start and end
const timeFrameStartEnd = computed(() => {
const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
selectTimeFrame.value = [start, end]; // 傳給後端的資料
for (let i = 0; i <= this.selectRange; i++) {
sliderData.push(xAxisMin + (step * i));
}
return [start, end];
});
return sliderData;
},
// 加入最大、最小值
timeFrameData: function(){
const data = this.filterTimeframe.data.map(i=>({x:i.x,y:i.y}))
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
// x 值為 0 ~ 11,
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a)
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c)
// 找出 slidrData,時間格式:毫秒時間戳
const sliderData = computed(() => {
const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
const data = []
// y 軸最小值
const a = 0;
let b;
const c = 1;
const d = this.filterTimeframe.data[0].y;
const e = 2;
const f = this.filterTimeframe.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a);
if(b < 0) {
b = 0;
}
// y 軸最大值
const ma = 9;
const mb = this.filterTimeframe.data[8].y;
const mc = 10;
const md = this.filterTimeframe.data[9].y;
const me = 11;
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
if(mf < 0) {
mf = 0;
}
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
}
// 添加最小值
data.unshift({
x: this.filterTimeframe.x_axis.min_base,
y: b,
})
// 添加最大值
data.push({
x: this.filterTimeframe.x_axis.max_base,
y: mf,
})
return data;
});
return data;
},
labelsData: function() {
const min = new Date(this.filterTimeframe.x_axis.min_base).getTime();
const max = new Date(this.filterTimeframe.x_axis.max_base).getTime();
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
const x = min + i * step;
data.push(x);
}
return data;
},
},
watch:{
selectTimeFrame(newValue, oldValue) {
if(newValue.length === 0) {
this.startTime = new Date(this.filterTimeframe.x_axis.min);
this.endTime = new Date(this.filterTimeframe.x_axis.max);
this.selectArea = [0, this.selectRange];
this.resizeMask(this.chart);
}
},
},
methods: {
/**
* 調整遮罩大小
* @param {object} chart 取得 chart.js 資料
*/
resizeMask(chart) {
const from = (this.selectArea[0] * 0.01) / (this.selectRange * 0.01);
const to = (this.selectArea[1] * 0.01) / (this.selectRange * 0.01);
if(this.selectValue[0] === 'Timeframes') {
this.resizeLeftMask(chart, from);
this.resizeRightMask(chart, to);
}
},
/**
* 調整左邊遮罩大小
* @param {object} chart 取得 chart.js 資料
*/
resizeLeftMask(chart, from) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
},
/**
* 調整右邊遮罩大小
* @param {object} chart 取得 chart.js 資料
*/
resizeRightMask(chart, to) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
},
/**
* create chart
*/
createChart() {
const max = this.filterTimeframe.y_axis.max * 1.1;
const minX = this.timeFrameData[0]?.x;
const maxX = this.timeFrameData[this.timeFrameData.length - 1]?.x;
// 加入最大、最小值
const timeFrameData = computed(() => {
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
// x 值為 0 ~ 11,
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a)
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c)
const data = {
labels:this.labelsData,
datasets: [
{
label: 'Case',
data: this.timeFrameData,
fill: 'start',
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
pointRadius: 0,
x: 'x',
y: 'y',
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
filler: {
propagate: false
},
title: false
},
// animations: false, // 取消動畫
animation: {
onComplete: e => {
this.resizeMask(e.chart);
}
},
interaction: {
intersect: true,
},
scales: {
x: {
type: 'time',
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#334155',
display: true,
source: 'labels',
},
grid: {
display: false, // 隱藏 x 軸網格
},
time: {
minUnit: 'day', // 顯示最小單位
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
},
y: {
beginAtZero: true, // scale 包含 0
max: max,
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
},
},
};
const config = {
type: 'line',
data: data,
options: options,
};
this.canvasId = document.getElementById("chartCanvasId");
this.chart = new Chart(this.canvasId, config);
},
/**
* 滑塊改變的時候
* @param {array} e [1, 100]
*/
changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變
const sliderData = this.sliderData;
const start = sliderData[e[0].toFixed()];
const end = sliderData[e[1].toFixed()]; // 取得 index須為整數。
// y 軸最小值
const a = 0;
let b;
const c = 1;
const d = filterTimeframe.value.data[0].y;
const e = 2;
const f = filterTimeframe.value.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a);
if(b < 0) {
b = 0;
}
// y 軸最大值
const ma = 9;
const mb = filterTimeframe.value.data[8].y;
const mc = 10;
const md = filterTimeframe.value.data[9].y;
const me = 11;
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
if(mf < 0) {
mf = 0;
}
this.startTime = new Date(start);
this.endTime = new Date(end);
// 重新設定 start end 日曆選取範圍
this.endMinDate = new Date(start);
this.startMaxDate = new Date(end);
// 重新算圖
this.resizeMask(this.chart);
// 執行 timeFrameStartEnd 才會改變數據
this.timeFrameStartEnd();
},
/**
* 選取開始或結束時間時,要改變滑塊跟圖表
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
* @param {string} direction start or end
*/
sliderTimeRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳
const sliderData = this.sliderData;
const targetTime = [new Date(this.timeFrameStartEnd[0]).getTime(), new Date(this.timeFrameStartEnd[1]).getTime()];
const closestIndexes = targetTime.map(target => {
let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
let result = Math.round(Math.abs(closestIndex));
result = result > this.selectRange ? this.selectRange : result;
return result
});
// 添加最小值
data.unshift({
x: filterTimeframe.value.x_axis.min_base,
y: b,
})
// 添加最大值
data.push({
x: filterTimeframe.value.x_axis.max_base,
y: mf,
})
// 改變滑塊
this.selectArea = closestIndexes;
// 重新設定 start end 日曆選取範圍
if(direction === 'start') this.endMinDate = e;
else if(direction === 'end') this.startMaxDate = e;
// 重新算圖
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) this.resizeMask(this.chart);
else return;
},
},
mounted() {
// Chart.js
Chart.register(...registerables);
this.createChart();
// Slider
this.selectArea = [0, this.selectRange];
// Calendar
this.startMinDate = new Date(this.filterTimeframe.x_axis.min);
this.startMaxDate = new Date(this.filterTimeframe.x_axis.max);
this.endMinDate = new Date(this.filterTimeframe.x_axis.min);
this.endMaxDate = new Date(this.filterTimeframe.x_axis.max);
// 讓日曆的範圍等於時間軸的範圍
this.startTime = this.startMinDate;
this.endTime = this.startMaxDate;
this.timeFrameStartEnd();
},
return data;
});
const labelsData = computed(() => {
const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
const x = min + i * step;
data.push(x);
}
return data;
});
watch(selectTimeFrame, (newValue, oldValue) => {
if(newValue.length === 0) {
startTime.value = new Date(filterTimeframe.value.x_axis.min);
endTime.value = new Date(filterTimeframe.value.x_axis.max);
selectArea.value = [0, selectRange.value];
resizeMask(chart.value);
}
});
/**
* 調整遮罩大小
* @param {object} chartInstance 取得 chart.js 資料
*/
function resizeMask(chartInstance) {
const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
if(props.selectValue[0] === 'Timeframes') {
resizeLeftMask(chartInstance, from);
resizeRightMask(chartInstance, to);
}
}
/**
* 調整左邊遮罩大小
* @param {object} chartInstance 取得 chart.js 資料
*/
function resizeLeftMask(chartInstance, from) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left}px`;
mask.style.width = `${chartInstance.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chartInstance.chartArea.height}px`;
}
/**
* 調整右邊遮罩大小
* @param {object} chartInstance 取得 chart.js 資料
*/
function resizeRightMask(chartInstance, to) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left + chartInstance.chartArea.width * to}px`;
mask.style.width = `${chartInstance.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chartInstance.chartArea.height}px`;
}
/**
* create chart
*/
function createChart() {
const max = filterTimeframe.value.y_axis.max * 1.1;
const minX = timeFrameData.value[0]?.x;
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
const data = {
labels:labelsData.value,
datasets: [
{
label: 'Case',
data: timeFrameData.value,
fill: 'start',
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
pointRadius: 0,
x: 'x',
y: 'y',
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
filler: {
propagate: false
},
title: false
},
// animations: false, // 取消動畫
animation: {
onComplete: e => {
resizeMask(e.chart);
}
},
interaction: {
intersect: true,
},
scales: {
x: {
type: 'time',
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#334155',
display: true,
source: 'labels',
},
grid: {
display: false, // 隱藏 x 軸網格
},
time: {
minUnit: 'day', // 顯示最小單位
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
},
y: {
beginAtZero: true, // scale 包含 0
max: max,
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
},
},
};
const config = {
type: 'line',
data: data,
options: options,
};
canvasId.value = document.getElementById("chartCanvasId");
chart.value = new Chart(canvasId.value, config);
}
/**
* 滑塊改變的時候
* @param {array} e [1, 100]
*/
function changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變
const sliderDataVal = sliderData.value;
const start = sliderDataVal[e[0].toFixed()];
const end = sliderDataVal[e[1].toFixed()]; // 取得 index須為整數。
startTime.value = new Date(start);
endTime.value = new Date(end);
// 重新設定 start end 日曆選取範圍
endMinDate.value = new Date(start);
startMaxDate.value = new Date(end);
// 重新算圖
resizeMask(chart.value);
// 執行 timeFrameStartEnd 才會改變數據
timeFrameStartEnd.value;
}
/**
* 選取開始或結束時間時,要改變滑塊跟圖表
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
* @param {string} direction start or end
*/
function sliderTimeRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳
const sliderDataVal = sliderData.value;
const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
const closestIndexes = targetTime.map(target => {
let closestIndex = 0;
closestIndex = ((target - sliderDataVal[0])/(sliderDataVal[sliderDataVal.length-1]-sliderDataVal[0])) * sliderDataVal.length;
let result = Math.round(Math.abs(closestIndex));
result = result > selectRange.value ? selectRange.value : result;
return result
});
// 改變滑塊
selectArea.value = closestIndexes;
// 重新設定 start end 日曆選取範圍
if(direction === 'start') endMinDate.value = e;
else if(direction === 'end') startMaxDate.value = e;
// 重新算圖
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
else return;
}
onMounted(() => {
// Chart.js
Chart.register(...registerables);
createChart();
// Slider
selectArea.value = [0, selectRange.value];
// Calendar
startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
// 讓日曆的範圍等於時間軸的範圍
startTime.value = startMinDate.value;
endTime.value = startMaxDate.value;
timeFrameStartEnd.value;
});
</script>

View File

@@ -48,7 +48,7 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
@@ -68,293 +68,303 @@
</div>
</template>
<script>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useLoadingStore } from '@/stores/loading';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
expose: ['selectArea', 'showTraceId', 'traceTotal'],
setup() {
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
const emit = defineEmits(['filter-trace-selectArea']);
return {allMapDataStore, infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases, isLoading}
},
data() {
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
const processMap = ref({
nodes:[],
edges:[],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
const infiniteData = ref([]);
const infiniteFinish = ref(true); // 無限滾動是否載入完成
const chartOptions = ref(null);
const selectArea = ref([0, 1]);
const cyTraceRef = ref(null);
const traceTotal = computed(() => {
return baseTraces.value.length;
});
defineExpose({ selectArea, showTraceId, traceTotal });
const traceCountTotal = computed(() => {
return baseTraces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
});
const traceList = computed(() => {
return baseTraces.value.map(trace => {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: null,
infinitMaxItems: false,
infiniteData: [],
infiniteFinish: true, // 無限滾動是否載入完成
chartOptions: null,
selectArea: [0, 1]
}
},
computed: {
traceTotal: function() {
return this.baseTraces.length;
},
traceCountTotal: function() {
return this.baseTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
},
traceList: function() {
return this.baseTraces.map(trace => {
return {
id: trace.id,
value: this.progressWidth(Number(((trace.count / this.traceCountTotal) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: this.getPercentLabel(trace.count / this.traceCountTotal),
};
}).slice(this.selectArea[0], this.selectArea[1]);
},
caseTotalPercent: function() {
const ratioSum = this.traceList.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / this.traceCountTotal;
return this.getPercentLabel(ratioSum)
},
chartData: function() {
const start = this.selectArea[0];
const end = this.selectArea[1] - 1;
const labels = this.baseTraces.map(trace => `#${trace.id}`);
const data = this.baseTraces.map(trace => this.getPercentLabel(trace.count / this.traceCountTotal));
const selectAreaData = this.baseTraces.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
id: trace.id,
value: progressWidth(Number(((trace.count / traceCountTotal.value) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
}).slice(selectArea.value[0], selectArea.value[1]);
});
return { // 要呈現的資料
labels,
datasets: [
{
label: 'Trace', // 資料的標題標籤
data,
backgroundColor: selectAreaData,
categoryPercentage: 1.0,
barPercentage: 1.0
},
]
};
},
caseData: function() {
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
},
columnData: function() {
const data = JSON.parse(JSON.stringify(this.baseCases)); // 深拷貝原始 cases 的內容
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
const caseTotalPercent = computed(() => {
const ratioSum = traceList.value.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
return getPercentLabel(ratioSum)
});
const chartData = computed(() => {
const start = selectArea.value[0];
const end = selectArea.value[1] - 1;
const labels = baseTraces.value.map(trace => `#${trace.id}`);
const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value));
const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
return { // 要呈現的資料
labels,
datasets: [
{
label: 'Trace', // 資料的標題標籤
data,
backgroundColor: selectAreaData,
categoryPercentage: 1.0,
barPercentage: 1.0
},
]
};
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(baseCases.value)); // 深拷貝原始 cases 的內容
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
}
return result
});
watch(selectArea, (newValue, oldValue) => {
const roundValue = Math.round(newValue[1].toFixed());
if(newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // 判斷 Apply 是否 disable
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
});
/**
* Set bar chart Options
*/
function barOptions(){
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
return result
},
},
watch: {
selectArea: function(newValue, oldValue) {
const roundValue = Math.round(newValue[1].toFixed());
if(newValue[1] !== roundValue) this.selectArea[1] = roundValue;
if(newValue != oldValue) this.$emit('filter-trace-selectArea', newValue); // 判斷 Apply 是否 disable
},
infinite404: function(newValue) {
if(newValue === 404) this.infinitMaxItems = true;
},
showTraceId: function(newValue, oldValue) {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
},
},
methods: {
/**
* Set bar chart Options
*/
barOptions(){
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: { // 圖例
display: false,
},
tooltip: {
callbacks: {
label: (tooltipItems) =>{
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
}
}
}
},
animations: false,
scales: {
x: {
display:false
},
y: {
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
min: 0,
max: this.traceList[0]?.ratio,
stepSize: (this.traceList[0]?.ratio)/4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
plugins: {
legend: { // 圖例
display: false,
},
tooltip: {
callbacks: {
label: (tooltipItems) =>{
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
}
}
};
}
},
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
},
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id case id
* @param {number} count 所有的 case 數量
*/
async switchCaseData(id, count) {
// 點同一筆 id 不要有動作
if(id == this.showTraceId) return;
this.isLoading = true; // 都要 loading 畫面
this.infinit404 = null;
this.infinitMaxItems = false;
this.baseInfiniteStart = 0;
this.allMapDataStore.baseTraceId = id;
this.infiniteData = await this.allMapDataStore.getBaseTraceDetail();
this.showTraceId = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
this.createCy();
this.isLoading = false;
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.baseTraceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.baseTraceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
const graphId = this.$refs.cyTrace;
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件
*/
handleScroll(event) {
if(this.infinitMaxItems || this.baseCases.length < 20 || this.infiniteFinish === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) this.fetchData();
},
/**
* 無限滾動: 滾到底後,要載入數據
*/
async fetchData() {
try {
this.isLoading = true;
this.infiniteFinish = false;
this.baseInfiniteStart += 20;
await this.allMapDataStore.getBaseTraceDetail();
this.infiniteData = await [...this.infiniteData, ...this.baseCases];
this.infiniteFinish = await true;
this.isLoading = await false;
} catch(error) {
console.error('Failed to load data:', error);
animations: false,
scales: {
x: {
display:false
},
y: {
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
min: 0,
max: traceList.value[0]?.ratio,
stepSize: (traceList.value[0]?.ratio)/4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
}
}
},
mounted() {
this.isLoading = true; // createCy 執行完關閉
this.setNodesData();
this.setEdgesData();
this.createCy();
this.chartOptions = this.barOptions();
this.selectArea = [0, this.traceTotal]
this.isLoading = false;
},
};
}
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
function progressWidth(value){
return `width:${value}%;`
}
/**
* switch case data
* @param {number} id case id
* @param {number} count 所有的 case 數量
*/
async function switchCaseData(id, count) {
// 點同一筆 id 不要有動作
if(id == showTraceId.value) return;
isLoading.value = true; // 都要 loading 畫面
infinit404.value = null;
infinitMaxItems.value = false;
baseInfiniteStart.value = 0;
allMapDataStore.baseTraceId = id;
infiniteData.value = await allMapDataStore.getBaseTraceDetail();
showTraceId.value = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
createCy();
isLoading.value = false;
}
/**
* 將 trace element nodes 資料彙整
*/
function setNodesData(){
// 避免每次渲染都重複累加
processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node
baseTraceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
}
/**
* 將 trace edge line 資料彙整
*/
function setEdgesData(){
processMap.value.edges = [];
baseTraceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
const graphId = cyTraceRef.value;
setNodesData();
setEdgesData();
cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
}
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件
*/
function handleScroll(event) {
if(infinitMaxItems.value || baseCases.value.length < 20 || infiniteFinish.value === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
}
/**
* 無限滾動: 滾到底後,要載入數據
*/
async function fetchData() {
try {
isLoading.value = true;
infiniteFinish.value = false;
baseInfiniteStart.value += 20;
await allMapDataStore.getBaseTraceDetail();
infiniteData.value = await [...infiniteData.value, ...baseCases.value];
infiniteFinish.value = await true;
isLoading.value = await false;
} catch(error) {
console.error('Failed to load data:', error);
}
}
onMounted(() => {
isLoading.value = true; // createCy 執行完關閉
setNodesData();
setEdgesData();
createCy();
chartOptions.value = barOptions();
selectArea.value = [0, traceTotal.value]
isLoading.value = false;
});
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
<div class="blue-dot w-3 h-3 bg-[#0099FF] rounded-full mr-2"></div><span>{{ currentMapFile }}</span>
</div>
</li>
</ul>
</ul>
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
@@ -121,16 +121,16 @@
<p class="h2">Most Frequent</p>
<ul class="list-disc ml-6 mb-2 text-sm">
<li class="leading-5">Activity:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_tasks" :key="key">{{ value }}</span>
</li>
<li class="leading-5">Inbound connections:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_in" :key="key">{{ value }}
</span>
</li>
<li class="leading-5">Outbound connections:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_out" :key="key">{{ value }}
</span>
</li>
@@ -138,7 +138,7 @@
<p class="h2">Most Time-Consuming</p>
<ul class="list-disc ml-6 mb-4 text-sm">
<li class="w-full leading-5">Activity:&nbsp;
<span class="text-primary break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key)
<span class="text-primary break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key)
in insights.most_time_tasks" :key="key">{{ value }}
</span>
</li>
@@ -241,8 +241,8 @@
</Sidebar>
</template>
<script>
import { computed, ref, } from 'vue';
<script setup>
import { computed, ref } from 'vue';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js';
@@ -252,120 +252,106 @@ import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants';
// 刪除第一個和第二個元素
const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2);
export default {
props:{
sidebarState: {
type: Boolean,
require: false,
},
stats: {
type: Object,
required: false,
},
insights: {
type: Object,
required: false,
}
const props = defineProps({
sidebarState: {
type: Boolean,
require: false,
},
setup(props){
const pageAdmin = usePageAdminStore();
const mapPathStore = useMapPathStore();
const activeTrace = ref(0);
const currentMapFile = computed(() => pageAdmin.currentMapFile);
const clickedPathListIndex = ref(0);
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
const onActiveTraceClick = (clickedActiveTraceIndex) => {
mapPathStore.clearAllHighlight();
activeTrace.value = clickedActiveTraceIndex;
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
}
const onPathOptionClick = (clickedPath) => {
clickedPathListIndex.value = clickedPath;
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
};
const onResetTraceBtnClick = () => {
if(isBPMNOn.value) {
return;
}
clickedPathListIndex.value = undefined;
}
return {
currentMapFile,
i18next,
fieldNamesAndLabelNames,
clickedPathListIndex,
onPathOptionClick,
onActiveTraceClick,
onResetTraceBtnClick,
activeTrace,
isBPMNOn,
i18next,
};
},
data() {
return {
tab: 'summary',
valueCases: 0,
valueTraces: 0,
valueTaskInstances: 0,
valueTasks: 0,
}
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* @param {number} time use timeLabel.js
*/
timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
//
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
return result;
},
/**
* @param {number} time use moment
*/
moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm');
},
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
},
/**
* Behavior when show
*/
show(){
this.valueCases = this.stats.cases.ratio * 100;
this.valueTraces= this.stats.traces.ratio * 100;
this.valueTaskInstances = this.stats.task_instances.ratio * 100;
this.valueTasks = this.stats.tasks.ratio * 100;
},
/**
* Behavior when hidden
*/
hide(){
this.valueCases = 0;
this.valueTraces= 0;
this.valueTaskInstances = 0;
this.valueTasks = 0;
},
stats: {
type: Object,
required: false,
},
insights: {
type: Object,
required: false,
}
});
const pageAdmin = usePageAdminStore();
const mapPathStore = useMapPathStore();
const activeTrace = ref(0);
const currentMapFile = computed(() => pageAdmin.currentMapFile);
const clickedPathListIndex = ref(0);
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
const tab = ref('summary');
const valueCases = ref(0);
const valueTraces = ref(0);
const valueTaskInstances = ref(0);
const valueTasks = ref(0);
function onActiveTraceClick(clickedActiveTraceIndex) {
mapPathStore.clearAllHighlight();
activeTrace.value = clickedActiveTraceIndex;
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
}
function onPathOptionClick(clickedPath) {
clickedPathListIndex.value = clickedPath;
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
}
function onResetTraceBtnClick() {
if(isBPMNOn.value) {
return;
}
clickedPathListIndex.value = undefined;
}
/**
* @param {string} newTab Summary or Insight
*/
function switchTab(newTab) {
tab.value = newTab;
}
/**
* @param {number} time use timeLabel.js
*/
function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
//
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
return result;
}
/**
* @param {number} time use moment
*/
function moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm');
}
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
/**
* Behavior when show
*/
function show(){
valueCases.value = props.stats.cases.ratio * 100;
valueTraces.value = props.stats.traces.ratio * 100;
valueTaskInstances.value = props.stats.task_instances.ratio * 100;
valueTasks.value = props.stats.tasks.ratio * 100;
}
/**
* Behavior when hidden
*/
function hide(){
valueCases.value = 0;
valueTraces.value = 0;
valueTaskInstances.value = 0;
valueTasks.value = 0;
}
</script>

View File

@@ -39,7 +39,7 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] infiniteTable " @scroll="handleScroll">
@@ -59,221 +59,224 @@
</div>
</Sidebar>
</template>
<script>
<script setup>
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
props: ['sidebarTraces', 'cases'],
setup() {
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
const props = defineProps(['sidebarTraces', 'cases']);
const emit = defineEmits(['switch-Trace-Id']);
return {allMapDataStore, infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases, isLoading }
},
data() {
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
const processMap = ref({
nodes:[],
edges:[],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
const infiniteData = ref([]);
const infiniteFinish = ref(true); // 無限滾動是否載入完成
const cyTraceRef = ref(null);
const traceTotal = computed(() => {
return traces.value.length;
});
const traceList = computed(() => {
const sum = traces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const result = traces.value.map(trace => {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: null,
infinitMaxItems: false,
infiniteData: [],
infiniteFinish: true, // 無限滾動是否載入完成
}
},
computed: {
traceTotal: function() {
return this.traces.length;
},
traceList: function() {
const sum = this.traces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const result = this.traces.map(trace => {
return {
id: trace.id,
value: this.progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: this.getPercentLabel(trace.count / sum),
};
})
return result;
},
caseData: function() {
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
},
columnData: function() {
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
id: trace.id,
value: progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
})
return result;
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
}
return result
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
});
watch(traceId, (newValue) => {
showTraceId.value = newValue;
}, { immediate: true });
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(infiniteFirstCases, (newValue) => {
if(infiniteFirstCases.value) infiniteData.value = JSON.parse(JSON.stringify(newValue));
});
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
function progressWidth(value){
return `width:${value}%;`
}
/**
* switch case data
* @param {number} id case id
* @param {number} count 總 case 數量
*/
async function switchCaseData(id, count) {
// 點同一筆 id 不要有動作
if(id == showTraceId.value) return;
isLoading.value = true; // 都要 loading 畫面
infinit404.value = null;
infinitMaxItems.value = false;
showTraceId.value = id;
infiniteStart.value = 0;
emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // 傳遞到 Map index 再關掉 loading
}
/**
* 將 trace element nodes 資料彙整
*/
function setNodesData(){
// 避免每次渲染都重複累加
processMap.value.nodes = [];
// 將 api call 回來的資料帶進 node
traceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
return result
},
},
watch: {
infinite404: function(newValue) {
if(newValue === 404) this.infinitMaxItems = true;
},
traceId: {
handler(newValue) {
this.showTraceId = newValue;
},
immediate: true
},
showTraceId: function(newValue, oldValue) {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
},
infiniteFirstCases: function(newValue){
if(this.infiniteFirstCases) this.infiniteData = JSON.parse(JSON.stringify(newValue));
},
},
methods: {
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id case id
* @param {number} count 總 case 數量
*/
async switchCaseData(id, count) {
// 點同一筆 id 不要有動作
if(id == this.showTraceId) return;
this.isLoading = true; // 都要 loading 畫面
this.infinit404 = null;
this.infinitMaxItems = false;
this.showTraceId = id;
this.infiniteStart = 0;
this.$emit('switch-Trace-Id', {id: this.showTraceId, count: count}); // 傳遞到 Map index 再關掉 loading
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.traceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.traceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
const graphId = this.$refs.cyTrace;
});
})
}
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
/**
* create map
*/
async show() {
this.isLoading = await true; // createCy 執行完關閉
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id
this.showTraceId = await this.traces[0]?.id;
this.infiniteStart = await 0;
this.setNodesData();
this.setEdgesData();
this.createCy();
this.isLoading = false;
},
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件
*/
handleScroll(event) {
if(this.infinitMaxItems || this.cases.length < 20 || this.infiniteFinish === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) this.fetchData();
},
/**
* 無限滾動: 滾到底後,要載入數據
*/
async fetchData() {
try {
this.isLoading = true;
this.infiniteFinish = false;
this.infiniteStart += 20;
await this.allMapDataStore.getTraceDetail();
this.infiniteData = await [...this.infiniteData, ...this.cases];
this.infiniteFinish = await true;
this.isLoading = await false;
} catch(error) {
console.error('Failed to load data:', error);
/**
* 將 trace edge line 資料彙整
*/
function setEdgesData(){
processMap.value.edges = [];
traceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
}
},
});
});
// 關係線數量筆節點少一個
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
const graphId = cyTraceRef.value;
setNodesData();
setEdgesData();
cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
}
/**
* create map
*/
async function show() {
isLoading.value = await true; // createCy 執行完關閉
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id
showTraceId.value = await traces.value[0]?.id;
infiniteStart.value = await 0;
setNodesData();
setEdgesData();
createCy();
isLoading.value = false;
}
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件
*/
function handleScroll(event) {
if(infinitMaxItems.value || props.cases.length < 20 || infiniteFinish.value === false) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
}
/**
* 無限滾動: 滾到底後,要載入數據
*/
async function fetchData() {
try {
isLoading.value = true;
infiniteFinish.value = false;
infiniteStart.value += 20;
await allMapDataStore.getTraceDetail();
infiniteData.value = await [...infiniteData.value, ...props.cases];
infiniteFinish.value = await true;
isLoading.value = await false;
} catch(error) {
console.error('Failed to load data:', error);
}
}
</script>

View File

@@ -69,110 +69,118 @@
</Sidebar>
</template>
<script>
<script setup>
import { ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useMapPathStore } from '@/stores/mapPathStore';
import { mapState, mapActions, } from 'pinia';
export default {
props: {
sidebarView: {
type: Boolean,
require: true,
},
},
data() {
return {
selectFrequency: [
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_freq", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value:"cases", label:"Number of cases", disabled:false, },
],
selectDuration:[
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_duration", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
],
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
dataLayerType: null, // freq | duration
dataLayerOption: null,
selectedFreq: '',
selectedDuration: '',
rank: 'LR', // 直向 TB | 橫向 LR
}
},
computed: {
...mapState(useMapPathStore, ['isBPMNOn']),
},
methods: {
/**
* switch map type
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。
*/
switchMapType(type) {
this.mapType = type;
this.$emit('switch-map-type', this.mapType);
},
/**
* switch curve style
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
*/
switchCurveStyles(style) {
this.curveStyle = style;
this.$emit('switch-curve-styles', this.curveStyle);
},
/**
* switch rank
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
*/
switchRank(rank) {
this.rank = rank;
this.$emit('switch-rank', this.rank);
},
/**
* switch Data Layoer Type or Option.
* @param {string} e 切換時傳入的選項
* @param {string} type 'freq' | 'duration',可傳入以上任一。
*/
switchDataLayerType(e, type){
let value = '';
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
switch (type) {
case 'freq':
value = value || this.selectedFreq || 'total';
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedFreq = value;
break;
case 'duration':
value = value || this.selectedDuration || 'total';
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedDuration = value;
break;
}
this.$emit('switch-data-layer-type', this.dataLayerType, this.dataLayerOption);
},
onProcessMapClick() {
this.setIsBPMNOn(false);
this.switchMapType('processMap');
},
onBPMNClick() {
this.setIsBPMNOn(true);
this.switchMapType('bpmn');
},
...mapActions(useMapPathStore, ['setIsBPMNOn',]),
defineProps({
sidebarView: {
type: Boolean,
require: true,
},
mounted() {
this.dataLayerType = 'freq';
this.dataLayerOption = 'total';
}
});
const emit = defineEmits([
'switch-map-type',
'switch-curve-styles',
'switch-rank',
'switch-data-layer-type',
]);
const mapPathStore = useMapPathStore();
const { isBPMNOn } = storeToRefs(mapPathStore);
const selectFrequency = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_freq", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value:"cases", label:"Number of cases", disabled:false, },
]);
const selectDuration = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_duration", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
]);
const curveStyle = ref('unbundled-bezier'); // unbundled-bezier | taxi
const mapType = ref('processMap'); // processMap | bpmn
const dataLayerType = ref(null); // freq | duration
const dataLayerOption = ref(null);
const selectedFreq = ref('');
const selectedDuration = ref('');
const rank = ref('LR'); // 直向 TB | 橫向 LR
/**
* switch map type
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。
*/
function switchMapType(type) {
mapType.value = type;
emit('switch-map-type', mapType.value);
}
/**
* switch curve style
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
*/
function switchCurveStyles(style) {
curveStyle.value = style;
emit('switch-curve-styles', curveStyle.value);
}
/**
* switch rank
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
*/
function switchRank(rankValue) {
rank.value = rankValue;
emit('switch-rank', rank.value);
}
/**
* switch Data Layoer Type or Option.
* @param {string} e 切換時傳入的選項
* @param {string} type 'freq' | 'duration',可傳入以上任一。
*/
function switchDataLayerType(e, type) {
let value = '';
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
switch (type) {
case 'freq':
value = value || selectedFreq.value || 'total';
dataLayerType.value = type;
dataLayerOption.value = value;
selectedFreq.value = value;
break;
case 'duration':
value = value || selectedDuration.value || 'total';
dataLayerType.value = type;
dataLayerOption.value = value;
selectedDuration.value = value;
break;
}
emit('switch-data-layer-type', dataLayerType.value, dataLayerOption.value);
}
function onProcessMapClick() {
mapPathStore.setIsBPMNOn(false);
switchMapType('processMap');
}
function onBPMNClick() {
mapPathStore.setIsBPMNOn(true);
switchMapType('bpmn');
}
onMounted(() => {
dataLayerType.value = 'freq';
dataLayerOption.value = 'total';
});
</script>

View File

@@ -78,89 +78,85 @@
</section>
</template>
<script>
<script setup>
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
export default {
setup() {
const allMapDataStore = useAllMapDataStore();
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
const route = useRoute();
return { logId, stats, createFilterId, allMapDataStore };
},
data() {
return {
isPanel: false,
statData: null,
}
},
methods: {
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
const allMapDataStore = useAllMapDataStore();
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
const isPanel = ref(false);
const statData = ref(null);
/**
* Number to percentage
* @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
* setting stats data
*/
function getStatData() {
statData.value = {
cases: {
count: stats.value.cases.count.toLocaleString('en-US'),
total: stats.value.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.cases.ratio)
},
/**
* setting stats data
*/
getStatData() {
this.statData = {
cases: {
count: this.stats.cases.count.toLocaleString('en-US'),
total: this.stats.cases.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(this.stats.cases.ratio)
},
traces: {
count: this.stats.traces.count.toLocaleString('en-US'),
total: this.stats.traces.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(this.stats.traces.ratio)
},
task_instances: {
count: this.stats.task_instances.count.toLocaleString('en-US'),
total: this.stats.task_instances.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(this.stats.task_instances.ratio)
},
tasks: {
count: this.stats.tasks.count.toLocaleString('en-US'),
total: this.stats.tasks.total.toLocaleString('en-US'),
ratio: this.getPercentLabel(this.stats.tasks.ratio)
},
started_at: getMoment(this.stats.started_at).format('YYYY-MM-DD HH:mm'),
completed_at: getMoment(this.stats.completed_at).format('YYYY-MM-DD HH:mm'),
case_duration: {
min: getTimeLabel(this.stats.case_duration.min),
max: getTimeLabel(this.stats.case_duration.max),
average: getTimeLabel(this.stats.case_duration.average),
median: getTimeLabel(this.stats.case_duration.median),
}
}
traces: {
count: stats.value.traces.count.toLocaleString('en-US'),
total: stats.value.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.traces.ratio)
},
task_instances: {
count: stats.value.task_instances.count.toLocaleString('en-US'),
total: stats.value.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.task_instances.ratio)
},
tasks: {
count: stats.value.tasks.count.toLocaleString('en-US'),
total: stats.value.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.tasks.ratio)
},
started_at: getMoment(stats.value.started_at).format('YYYY-MM-DD HH:mm'),
completed_at: getMoment(stats.value.completed_at).format('YYYY-MM-DD HH:mm'),
case_duration: {
min: getTimeLabel(stats.value.case_duration.min),
max: getTimeLabel(stats.value.case_duration.max),
average: getTimeLabel(stats.value.case_duration.average),
median: getTimeLabel(stats.value.case_duration.median),
}
},
async mounted() {
const params = this.$route.params;
const file = this.$route.meta.file;
const isCheckPage = this.$route.name.includes('Check');
switch (params.type) {
case 'log':
this.logId = isCheckPage ? file.parent.id : params.fileId;
break;
case 'filter':
this.createFilterId = isCheckPage ? file.parent.id : params.fileId;
break;
}
await this.allMapDataStore.getAllMapData();
await this.getStatData();
this.isPanel = false; // 預設不打開
}
}
onMounted(async () => {
const params = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
switch (params.type) {
case 'log':
logId.value = isCheckPage ? file.parent.id : params.fileId;
break;
case 'filter':
createFilterId.value = isCheckPage ? file.parent.id : params.fileId;
break;
}
await allMapDataStore.getAllMapData();
await getStatData();
isPanel.value = false; // 預設不打開
});
</script>
<style scoped>
@reference "../../assets/tailwind.css";

View File

@@ -1,5 +1,5 @@
<template>
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="$emit('closeModal', $event)">
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="emit('closeModal', $event)">
<template #header>
<div class="py-5">
</div>
@@ -14,70 +14,61 @@
</label>
</Dialog>
</template>
<script>
import IconUploarding from '../icons/IconUploarding.vue';
import { uploadFailedFirst } from '@/module/alertModal.js'
<script setup>
import { onBeforeUnmount, } from 'vue';
import { storeToRefs } from 'pinia';
import IconUploarding from '../icons/IconUploarding.vue';
import { uploadFailedFirst } from '@/module/alertModal.js';
import { useFilesStore } from '@/stores/files';
export default {
props: ['uploadModal'],
setup() {
const filesStore = useFilesStore();
const { uploadFileName } = storeToRefs(filesStore);
defineProps(['uploadModal']);
const emit = defineEmits(['closeModal']);
return { filesStore, uploadFileName }
},
data() {
return {
contentClass: 'h-full',
}
},
components: {
IconUploarding,
},
methods: {
/**
* 上傳的行為
* @param {event} event input 傳入的事件
*/
async upload(event) {
const fileInput = document.getElementById('uploadFiles');
const target = event.target;
const formData = new FormData();
let uploadFile;
const filesStore = useFilesStore();
const { uploadFileName } = storeToRefs(filesStore);
// 判斷是否有檔案
if(target && target.files) {
uploadFile = target.files[0];
}
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840)
if(uploadFile.size >= 94371840) {
fileInput.value = '';
return uploadFailedFirst('size');
}
// 將檔案加進 formData欄位一定要「csv」
formData.append('csv', uploadFile);
// 呼叫第一階段上傳 API
if(uploadFile) {
await this.filesStore.upload(formData);
}
if (uploadFile.name.endsWith('.csv')) {
this.uploadFileName = uploadFile.name.slice(0, -4);
} else {
// 處理錯誤或無效的文件格式
this.uploadFileName = ''; // 或者其他適合的錯誤處理方式
}
// 清除選擇文件
if(fileInput) {
fileInput.value = '';
}
}
},
beforeUnmount() {
this.$emit('closeModal', false);
const contentClass = 'h-full';
/**
* 上傳的行為
* @param {event} event input 傳入的事件
*/
async function upload(event) {
const fileInput = document.getElementById('uploadFiles');
const target = event.target;
const formData = new FormData();
let uploadFile;
// 判斷是否有檔案
if(target && target.files) {
uploadFile = target.files[0];
}
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840)
if(uploadFile.size >= 94371840) {
fileInput.value = '';
return uploadFailedFirst('size');
}
// 將檔案加進 formData欄位一定要「csv」
formData.append('csv', uploadFile);
// 呼叫第一階段上傳 API
if(uploadFile) {
await filesStore.upload(formData);
}
if (uploadFile.name.endsWith('.csv')) {
uploadFileName.value = uploadFile.name.slice(0, -4);
} else {
// 處理錯誤或無效的文件格式
uploadFileName.value = ''; // 或者其他適合的錯誤處理方式
}
// 清除選擇文件
if(fileInput) {
fileInput.value = '';
}
}
onBeforeUnmount(() => {
emit('closeModal', false);
});
</script>
<style scoped>
.loader-arrow-upward {

View File

@@ -13,16 +13,17 @@
<img id="acct_mgmt_button" v-else src="@/assets/icon-head-blue.svg" @mouseleave='isHeadHovered = false'
width="32" height="32" @click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50" alt="user-head"
/>
/>
</div>
</div>
</div>
</template>
<script>
import { ref, } from 'vue';
<script setup>
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs, } from 'pinia';
import i18next from '@/i18n/i18n';
import emitter from '@/utils/emitter';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import DspLogo from '@/components/icons/DspLogo.vue';
@@ -30,64 +31,44 @@ import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
export default {
data() {
return {
showMember: false,
i18next: i18next,
}
},
setup() {
const store = useLoginStore();
const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
const isHeadHovered = ref(false);
const route = useRoute();
const toggleIsAcctMenuOpen = () => {
acctMgmtStore.toggleIsAcctMenuOpen();
}
const store = useLoginStore();
const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
return { logOut, temporaryData, tempFilterId,
postRuleData, ruleData,
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
allMapDataStore, conformanceStore,
conformanceFileName,
toggleIsAcctMenuOpen,
isHeadHovered,
};
},
components: {
DspLogo,
},
methods: {
/**
* 登出的行為
*/
logOutButton() {
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) {
// 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('leaveFilter', false);
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut)
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut)
} else {
this.logOut();
}
},
},
mounted() {
if (this.$route.name === 'Login' || this.$route.name === 'NotFound404') {
this.showMember = false
} else {
this.showMember = true;
}
const isHeadHovered = ref(false);
const showMember = ref(false);
const toggleIsAcctMenuOpen = () => {
acctMgmtStore.toggleIsAcctMenuOpen();
};
/**
* 登出的行為
*/
function logOutButton() {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
} else {
logOut();
}
}
onMounted(() => {
if (route.name === 'Login' || route.name === 'NotFound404') {
showMember.value = false
} else {
showMember.value = true;
}
});
</script>

View File

@@ -44,8 +44,11 @@
</div>
</nav>
</template>
<script>
import { storeToRefs, mapState, mapActions, } from 'pinia';
<script setup>
import { ref, computed, watch, onMounted, } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { storeToRefs, } from 'pinia';
import emitter from '@/utils/emitter';
import { useFilesStore } from '@/stores/files';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
@@ -57,296 +60,274 @@ import { saveFilter, savedSuccessfully, saveConformance } from '@/module/alertMo
import UploadModal from './File/UploadModal.vue';
import AcctMenu from './AccountMenu/AcctMenu.vue';
export default {
setup() {
const store = useFilesStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
conformanceLogTempCheckId, conformanceFilterTempCheckId,
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
isUpdateConformance, conformanceFileName
} = storeToRefs(conformanceStore);
const route = useRoute();
const router = useRouter();
return {
store, allMapDataStore, logId, tempFilterId, createFilterId,
filterName, postRuleData, isUpdateFilter, conformanceStore, conformanceRuleData,
conformanceLogId, conformanceFilterId, conformanceLogTempCheckId,
conformanceFilterTempCheckId, conformanceLogCreateCheckId,
conformanceFilterCreateCheckId, isUpdateConformance, conformanceFileName,
};
},
components: {
IconSearch,
IconSetting,
UploadModal,
AcctMenu,
},
data() {
return {
mapCompareStore: useMapCompareStore(),
showNavbarBreadcrumb: false,
navViewData:
{
// 舉例FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
// 舉例DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
// 舉例COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ['MAP', 'PERFORMANCE'],
'ACCOUNT MANAGEMENT': [],
'MY ACCOUNT': [],
},
navViewName: 'FILES',
uploadModal: false,
};
},
computed: {
disabledSave: function () {
switch (this.$route.name) {
case 'Map':
case 'CheckMap':
// 沒有 filter Id, 沒有暫存 tempFilterId Id 就不能存檔
return !this.tempFilterId;
case 'Conformance':
case 'CheckConformance':
return !(this.conformanceFilterTempCheckId || this.conformanceLogTempCheckId);
}
},
showIcon: function() {
let result = true;
const store = useFilesStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const mapCompareStore = useMapCompareStore();
const pageAdminStore = usePageAdminStore();
result = !['FILES', 'UPLOAD'].includes(this.navViewName);
return result;
},
noShowSaveButton: function() {
return this.navViewName === 'UPLOAD' || this.navViewName === 'COMPARE' ||
this.navViewName === 'ACCOUNT MANAGEMENT' ||
this.activePage === 'PERFORMANCE';
},
...mapState(usePageAdminStore, [
'activePage',
'pendingActivePage',
'activePageComputedByRoute',
'shouldKeepPreviousPage',
]),
},
watch: {
'$route':'getNavViewName',
filterName: function(newVal,) {
this.filterName = newVal;
},
},
mounted() {
this.handleNavItemBtn();
if(this.$route.params.type === 'filter') {
this.createFilterId= this.$route.params.fileId;
}
this.showNavbarBreadcrumb = this.$route.matched[0].name !== ('AuthContainer');
this.getNavViewName();
},
methods: {
/**
* switch navbar item
* @param {event} event 選取 Navbar 選項後傳入的值
*/
onNavItemBtnClick(event) {
let type;
let fileId;
let isCheckPage;
const navItemCandidate = event.target.innerText;
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
conformanceLogTempCheckId, conformanceFilterTempCheckId,
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
isUpdateConformance, conformanceFileName
} = storeToRefs(conformanceStore);
const { activePage, pendingActivePage, activePageComputedByRoute, shouldKeepPreviousPage } = storeToRefs(pageAdminStore);
const { setPendingActivePage, setPreviousPage, setActivePage, setActivePageComputedByRoute, setIsPagePendingBoolean } = pageAdminStore;
this.setPendingActivePage(navItemCandidate);
const showNavbarBreadcrumb = ref(false);
const navViewData = {
// 舉例FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
// 舉例DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
// 舉例COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ['MAP', 'PERFORMANCE'],
'ACCOUNT MANAGEMENT': [],
'MY ACCOUNT': [],
};
const navViewName = ref('FILES');
const uploadModal = ref(false);
switch (this.navViewName) {
case 'FILES':
this.store.filesTag = navItemCandidate;
break;
case 'DISCOVER':
type = this.$route.params.type;
fileId = this.$route.params.fileId;
isCheckPage = this.$route.name.includes('Check');
const disabledSave = computed(() => {
switch (route.name) {
case 'Map':
case 'CheckMap':
// 沒有 filter Id, 沒有暫存 tempFilterId Id 就不能存檔
return !tempFilterId.value;
case 'Conformance':
case 'CheckConformance':
return !(conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value);
}
});
switch (navItemCandidate) {
case 'MAP':
if(isCheckPage) {
this.$router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
}
else {
this.$router.push({name: 'Map', params: { type: type, fileId: fileId }});
}
break;
case 'CONFORMANCE':
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
this.$router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
}
else { // Beware of Swal popup, it might disturb which is the current active page
this.$router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
}
break
case 'PERFORMANCE':
if(isCheckPage) {
this.$router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
}
else {
this.$router.push({name: 'Performance', params: { type: type, fileId: fileId }});
}
break;
const showIcon = computed(() => {
return !['FILES', 'UPLOAD'].includes(navViewName.value);
});
const noShowSaveButton = computed(() => {
return navViewName.value === 'UPLOAD' || navViewName.value === 'COMPARE' ||
navViewName.value === 'ACCOUNT MANAGEMENT' ||
activePage.value === 'PERFORMANCE';
});
watch(() => route, () => {
getNavViewName();
}, { deep: true });
watch(filterName, (newVal) => {
filterName.value = newVal;
});
/**
* switch navbar item
* @param {event} event 選取 Navbar 選項後傳入的值
*/
function onNavItemBtnClick(event) {
let type;
let fileId;
let isCheckPage;
const navItemCandidate = event.target.innerText;
setPendingActivePage(navItemCandidate);
switch (navViewName.value) {
case 'FILES':
store.filesTag = navItemCandidate;
break;
case 'DISCOVER':
type = route.params.type;
fileId = route.params.fileId;
isCheckPage = route.name.includes('Check');
switch (navItemCandidate) {
case 'MAP':
if(isCheckPage) {
router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Map', params: { type: type, fileId: fileId }});
}
break;
case 'COMPARE':
switch (navItemCandidate) {
case 'MAP':
this.$router.push({name: 'MapCompare', params: this.mapCompareStore.routeParam});
break;
case 'PERFORMANCE':
this.$router.push({name: 'CompareDashboard', params: this.mapCompareStore.routeParam});
break;
default:
break;
case 'CONFORMANCE':
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
}
};
},
/**
* Based on the route.name, decide the navViewName.
* @returns {string} the string of navigation name to return
*/
getNavViewName() {
const name = this.$route.name;
let valueToSet;
if(this.$route.name === 'NotFound404') {
return;
}
// 說明this.$route.matched[1] 表示當前路由匹配的第二個路由記錄
this.navViewName = this.$route.matched[1].name.toUpperCase();
this.store.filesTag = 'ALL';
switch (this.navViewName) {
case 'FILES':
valueToSet = this.navItemCandidate;
break;
case 'DISCOVER':
switch (name) {
case 'Map':
case 'CheckMap':
valueToSet = 'MAP';
break;
case 'Conformance':
case 'CheckConformance':
valueToSet = 'CONFORMANCE';
break;
case 'Performance':
case 'CheckPerformance':
valueToSet = 'PERFORMANCE';
break;
else { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
}
break;
case 'COMPARE':
switch(name) {
case 'dummy':
case 'CompareDashboard':
valueToSet = 'DASHBOARD';
break;
default:
break;
break
case 'PERFORMANCE':
if(isCheckPage) {
router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Performance', params: { type: type, fileId: fileId }});
}
break;
}
// Frontend is not sure which button will the user press on the modal,
// so here we need to save to a pending state
// 前端無法確定用戶稍後會按下彈窗上的哪個按鈕(取消還是確認、儲存)
// 因此我們需要將其保存到待處理狀態
if(!this.shouldKeepPreviousPage) { // 若使用者不是按下取消按鈕或是點選按鈕時
this.setPendingActivePage(valueToSet);
}
return valueToSet;
},
/**
* Save button' modal
*/
async saveModal() {
// 協助判斷 MAP, CONFORMANCE 儲存有「送出」或「取消」。
// 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('saveModal', false);
switch (this.$route.name) {
case 'Map':
await this.handleMapSave();
break;
case 'CheckMap':
await this.handleCheckMapSave();
break;
case 'Conformance':
case 'CheckConformance':
await this.handleConformanceSave();
break;
break;
case 'COMPARE':
switch (navItemCandidate) {
case 'MAP':
router.push({name: 'MapCompare', params: mapCompareStore.routeParam});
break;
case 'PERFORMANCE':
router.push({name: 'CompareDashboard', params: mapCompareStore.routeParam});
break;
default:
break;
break;
}
},
/**
* Set nav item button background color in case the variable is an empty string
*/
handleNavItemBtn() {
if(this.activePageComputedByRoute === "") {
this.setActivePageComputedByRoute(this.$route.matched[this.$route.matched.length - 1].name);
}
},
async handleMapSave() {
if (this.createFilterId) {
await this.allMapDataStore.updateFilter();
if (this.isUpdateFilter) {
await savedSuccessfully(this.filterName);
}
} else if (this.logId) {
const isSaved = await saveFilter(this.allMapDataStore.addFilterId);
if (isSaved) {
this.setActivePage('MAP');
await this.$router.push(`/discover/filter/${this.createFilterId}/map`);
}
}
},
async handleCheckMapSave() {
const isSaved = await saveFilter(this.allMapDataStore.addFilterId);
if (isSaved) {
this.setActivePage('MAP');
await this.$router.push(`/discover/filter/${this.createFilterId}/map`);
}
},
async handleConformanceSave() {
if (this.conformanceFilterCreateCheckId || this.conformanceLogCreateCheckId) {
await this.conformanceStore.updateConformance();
if (this.isUpdateConformance) {
await savedSuccessfully(this.conformanceFileName);
}
} else {
const isSaved = await saveConformance(this.conformanceStore.addConformanceCreateCheckId);
if (isSaved) {
if (this.conformanceLogId) {
this.setActivePage('CONFORMANCE');
await this.$router.push(`/discover/conformance/log/${this.conformanceLogCreateCheckId}/conformance`);
} else if (this.conformanceFilterId) {
this.setActivePage('CONFORMANCE');
await this.$router.push(`/discover/conformance/filter/${this.conformanceFilterCreateCheckId}/conformance`);
}
}
}
},
...mapActions(usePageAdminStore, [
'setPendingActivePage',
'setPreviousPage',
'setActivePage',
'setActivePageComputedByRoute',
'setIsPagePendingBoolean',
],),
},
};
}
/**
* Based on the route.name, decide the navViewName.
* @returns {string} the string of navigation name to return
*/
function getNavViewName() {
const name = route.name;
let valueToSet;
if(route.name === 'NotFound404' || !route.matched[1]) {
return;
}
// 說明route.matched[1] 表示當前路由匹配的第二個路由記錄
navViewName.value = route.matched[1].name.toUpperCase();
store.filesTag = 'ALL';
switch (navViewName.value) {
case 'FILES':
valueToSet = activePage.value;
break;
case 'DISCOVER':
switch (name) {
case 'Map':
case 'CheckMap':
valueToSet = 'MAP';
break;
case 'Conformance':
case 'CheckConformance':
valueToSet = 'CONFORMANCE';
break;
case 'Performance':
case 'CheckPerformance':
valueToSet = 'PERFORMANCE';
break;
}
break;
case 'COMPARE':
switch(name) {
case 'dummy':
case 'CompareDashboard':
valueToSet = 'DASHBOARD';
break;
default:
break;
}
break;
}
// Frontend is not sure which button will the user press on the modal,
// so here we need to save to a pending state
// 前端無法確定用戶稍後會按下彈窗上的哪個按鈕(取消還是確認、儲存)
// 因此我們需要將其保存到待處理狀態
if(!shouldKeepPreviousPage.value) { // 若使用者不是按下取消按鈕或是點選按鈕時
setPendingActivePage(valueToSet);
}
return valueToSet;
}
/**
* Save button' modal
*/
async function saveModal() {
// 協助判斷 MAP, CONFORMANCE 儲存有「送出」或「取消」。
// 傳給 Map通知 Sidebar 要關閉。
emitter.emit('saveModal', false);
switch (route.name) {
case 'Map':
await handleMapSave();
break;
case 'CheckMap':
await handleCheckMapSave();
break;
case 'Conformance':
case 'CheckConformance':
await handleConformanceSave();
break;
default:
break;
}
}
/**
* Set nav item button background color in case the variable is an empty string
*/
function handleNavItemBtn() {
if(activePageComputedByRoute.value === "") {
setActivePageComputedByRoute(route.matched[route.matched.length - 1].name);
}
}
async function handleMapSave() {
if (createFilterId.value) {
await allMapDataStore.updateFilter();
if (isUpdateFilter.value) {
await savedSuccessfully(filterName.value);
}
} else if (logId.value) {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
}
async function handleCheckMapSave() {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
async function handleConformanceSave() {
if (conformanceFilterCreateCheckId.value || conformanceLogCreateCheckId.value) {
await conformanceStore.updateConformance();
if (isUpdateConformance.value) {
await savedSuccessfully(conformanceFileName.value);
}
} else {
const isSaved = await saveConformance(conformanceStore.addConformanceCreateCheckId);
if (isSaved) {
if (conformanceLogId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`);
} else if (conformanceFilterId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`);
}
}
}
}
onMounted(() => {
handleNavItemBtn();
if(route.params.type === 'filter') {
createFilterId.value = route.params.fileId;
}
showNavbarBreadcrumb.value = route.matched[0].name !== ('AuthContainer');
getNavViewName();
});
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{

View File

@@ -2,8 +2,8 @@
<form role="search">
<label for="searchFiles" class="mr-4 relative" htmlFor="searchFiles">
Search
<input type="search" id="searchFiles" placeholder="Search Activity" class="px-5 py-2 w-52 rounded-full text-sm align-middle
duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring
<input type="search" id="searchFiles" placeholder="Search Activity" class="px-5 py-2 w-52 rounded-full text-sm align-middle
duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring
focus:border-neutral-500"/>
<span class="absolute top-2 bottom-0.5 right-0.5 flex justify-center items-center gap-2">
<IconSetting class="w-6 h-6 cursor-pointer"></IconSetting>
@@ -16,14 +16,7 @@
</form>
</template>
<script>
<script setup>
import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue';
export default {
components: {
IconSearch,
IconSetting
}
}
</script>

View File

@@ -27,7 +27,7 @@
:data-max="tUnits[unit].max"
:data-min="tUnits[unit].min"
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
@focus="onFocus"
@change="onChange"
@keyup="onKeyUp"
@@ -39,422 +39,320 @@
</div>
</template>
<script>
//:value="tUnits[unit].val.toString().padStart(2, '0')"
import { mapActions, } from 'pinia';
import { useConformanceInputStore } from '@/stores/conformanceInput';
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import emitter from '@/utils/emitter';
export default {
props: {
max: {
type: Number,
default: 0,
required: true,
validator(value) {
return value >= 0;
},
const props = defineProps({
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() {
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;
},
}
});
const emit = defineEmits(['total-seconds']);
const display = ref('dhms');
const seconds = ref(0);
const minutes = ref(0);
const hours = ref(0);
const days = ref(0);
const maxDays = ref(0);
const minDays = ref(0);
const totalSeconds = ref(0);
const maxTotal = ref(null);
const minTotal = ref(null);
const inputTypes = ref([]);
const lastInput = ref(null);
const openTimeSelect = ref(false);
const tUnits = computed({
get() {
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,
s: { dsp: 's', inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: 'm', inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: 'h', inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: { dsp: 'd', inc: 1, val: days.value, max: maxDays.value, rate: 86400, min: minDays.value }
};
},
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();
}
}
},
},
inputTimeFields: {
get() {
const paddedTimeFields = [];
this.inputTypes.forEach(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;
},
set(newValues) {
for (const unit in newValues) {
switch (unit) {
case 's': seconds.value = newValues[unit].val; break;
case 'm': minutes.value = newValues[unit].val; break;
case 'h': hours.value = newValues[unit].val; break;
case 'd': days.value = newValues[unit].val; break;
}
const input = document.querySelector(`[data-tunit="${unit}"]`);
if (input) {
input.value = newValues[unit].val.toString();
}
}
},
watch: {
max: {
handler: function(newValue, oldValue) {
this.maxTotal = newValue;
if(this.size === 'max' && newValue !== oldValue) {
this.createData();
};
},
immediate: true,
},
min: {
handler: function(newValue, oldValue) {
this.minTotal = newValue;
if( this.size === 'min' && newValue !== oldValue){
this.createData();
}
},
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) {
const baseInputValue = event.target.value;
let decoratedInputValue;
// 讓前綴數字自動補 0
if(isNaN(event.target.value)){
event.target.value = '00';
} else {
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;
};
const inputTimeFields = computed(() => {
const paddedTimeFields = [];
inputTypes.value.forEach(inputTypeUnit => {
paddedTimeFields.push(tUnits.value[inputTypeUnit].val.toString().padStart(2, '0'));
});
return paddedTimeFields;
});
this.calculateTotalSeconds();
},
/**
* 上下箭頭時的行為
* @param {event} event input 傳入的事件
*/
onKeyUp(event) {
// 正規表達式 \D 即不是 0-9 的字符
event.target.value = event.target.value.replace(/\D/g, '');
function onClose() {
openTimeSelect.value = false;
}
// 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 = this.getNewValue(input);
function onFocus(event) {
lastInput.value = event.target;
lastInput.value.select();
}
if (goUp) {
newVal = this.handleArrowUp(newVal, tUnit, input);
} else {
newVal = this.handleArrowDown(newVal, tUnit);
}
function onChange(event) {
const baseInputValue = event.target.value;
let decoratedInputValue;
if(isNaN(event.target.value)){
event.target.value = '00';
} else {
event.target.value = event.target.value.toString();
}
decoratedInputValue = event.target.value.toString();
this.updateInputValue(input, newVal, tUnit);
if (selectIt) {
input.select();
}
this.calculateTotalSeconds();
},
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();
}
const dsp = event.target.dataset.tunit;
tUnits.value[dsp].val = decoratedInputValue;
switch (dsp) {
case 'd':
days.value = baseInputValue;
break;
case 'h':
hours.value = decoratedInputValue;
break;
case 'm':
minutes.value = decoratedInputValue;
break;
case 's':
seconds.value = decoratedInputValue;
break;
};
/**
* 獲取新的數值
* @param {element} input 輸入的元素
* @returns {number} 新的數值
*/
getNewValue(input) {
const newVal = parseInt(input.value, 10);
return isNaN(newVal) ? 0 : newVal;
},
calculateTotalSeconds();
}
/**
* 處理向上箭頭的行為
* @param {number} newVal 當前數值
* @param {string} tUnit 時間單位
* @param {element} input 輸入的元素
* @returns {number} 更新後的數值
*/
handleArrowUp(newVal, tUnit, input) {
newVal += this.tUnits[tUnit].inc;
if (newVal > this.tUnits[tUnit].max) {
if (this.tUnits[tUnit].dsp === 'd') {
this.totalSeconds = this.maxTotal;
} else {
newVal = newVal % (this.tUnits[tUnit].max + 1);
this.incrementPreviousUnit(input);
}
}
return newVal;
},
function onKeyUp(event) {
event.target.value = event.target.value.replace(/\D/g, '');
if (event.keyCode === 38 || event.keyCode === 40) {
actionUpDown(event.target, event.keyCode === 38, true);
};
}
/**
* 處理向下箭頭的行為
* @param {number} newVal 當前數值
* @param {string} tUnit 時間單位
* @returns {number} 更新後的數值
*/
handleArrowDown(newVal, tUnit) {
newVal -= this.tUnits[tUnit].inc;
if (newVal < 0) {
newVal = (this.tUnits[tUnit].max + 1) - this.tUnits[tUnit].inc;
}
return newVal;
},
function actionUpDown(input, goUp, selectIt = false) {
const tUnit = input.dataset.tunit;
let newVal = getNewValue(input);
/**
* 進位前一個更大的單位
* @param {element} input 輸入的元素
*/
incrementPreviousUnit(input) {
if (input.dataset.index > 0) {
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
this.actionUpDown(prevUnit, true);
}
},
if (goUp) {
newVal = handleArrowUp(newVal, tUnit, input);
} else {
newVal = handleArrowDown(newVal, tUnit);
}
/**
* 更新輸入框的數值
* @param {element} input 輸入的元素
* @param {number} newVal 新的數值
* @param {string} tUnit 時間單位
*/
updateInputValue(input, newVal, tUnit) {
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;
}
},
/**
* 設定 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));
updateInputValue(input, newVal, tUnit);
if (selectIt) {
input.select();
}
calculateTotalSeconds();
}
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;
function getNewValue(input) {
const newVal = parseInt(input.value, 10);
return isNaN(newVal) ? 0 : newVal;
}
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 if((this.size === 'min' && totalSeconds <= this.maxTotal)) {
this.maxDays = Math.floor(this.maxTotal / (3600 * 24));
}
this.totalSeconds = totalSeconds;
this.$emit('total-seconds', totalSeconds);
},
/**
* 初始化
*/
async createData() {
const 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(
useConformanceInputStore,[]
),
},
created() {
this.$emitter.on('reset', (data) => {
this.createData();
});
},
mounted() {
this.inputTypes = this.display.split('');
},
directives: {
'closable': {
mounted(el, {value}) {
const 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);
};
},
function handleArrowUp(newVal, tUnit, input) {
newVal += tUnits.value[tUnit].inc;
if (newVal > tUnits.value[tUnit].max) {
if (tUnits.value[tUnit].dsp === 'd') {
totalSeconds.value = maxTotal.value;
} else {
newVal = newVal % (tUnits.value[tUnit].max + 1);
incrementPreviousUnit(input);
}
}
return newVal;
}
function handleArrowDown(newVal, tUnit) {
newVal -= tUnits.value[tUnit].inc;
if (newVal < 0) {
newVal = (tUnits.value[tUnit].max + 1) - tUnits.value[tUnit].inc;
}
return newVal;
}
function incrementPreviousUnit(input) {
if (input.dataset.index > 0) {
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
actionUpDown(prevUnit, true);
}
}
function updateInputValue(input, newVal, tUnit) {
input.value = newVal.toString();
switch (tUnit) {
case 'd':
days.value = input.value;
break;
case 'h':
hours.value = input.value;
break;
case 'm':
minutes.value = input.value;
break;
case 's':
seconds.value = input.value;
break;
}
}
function secondToDate(totalSec, size) {
totalSec = parseInt(totalSec);
if(!isNaN(totalSec)) {
seconds.value = totalSec % 60;
minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
hours.value = (Math.floor(totalSec / 3600)) % 24;
days.value = Math.floor(totalSec / (3600 * 24));
if(size === 'max') {
maxDays.value = Math.floor(totalSec / (3600 * 24));
}
else if(size === 'min') {
minDays.value = Math.floor(totalSec / (3600 * 24));
}
};
}
function calculateTotalSeconds() {
let total = 0;
for (const unit in tUnits.value) {
const val = parseInt(tUnits.value[unit].val, 10);
if (!isNaN(val)) {
total += val * tUnits.value[unit].rate;
}
}
if(total >= maxTotal.value){
total = maxTotal.value;
secondToDate(maxTotal.value, 'max');
} else if (total <= minTotal.value) {
total = minTotal.value;
secondToDate(minTotal.value, 'min');
} else if((props.size === 'min' && total <= maxTotal.value)) {
maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
}
totalSeconds.value = total;
emit('total-seconds', total);
}
async function createData() {
const size = props.size;
if (maxTotal.value !== await null && minTotal.value !== await null) {
switch (size) {
case 'max':
secondToDate(minTotal.value, 'min');
secondToDate(maxTotal.value, 'max');
totalSeconds.value = maxTotal.value;
if(props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
break;
case 'min':
secondToDate(maxTotal.value, 'max');
secondToDate(minTotal.value, 'min');
totalSeconds.value = minTotal.value;
if(props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
break;
}
}
}
// created
emitter.on('reset', () => {
createData();
});
// mounted
onMounted(() => {
inputTypes.value = display.value.split('');
});
const vClosable = {
mounted(el, {value}) {
const 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>

View File

@@ -5,30 +5,18 @@
<img v-if="isChecked" :src="ImgCheckboxBlueFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxCheckedMark" class="absolute top-[11x] left-[2px] h-[16px] w-[14px]" alt="checkbox"/>
</div>
</template>
<script>
import { defineComponent, computed, } from 'vue';
<script setup>
import ImgCheckboxBlueFrame from "@/assets/icon-blue-checkbox.svg";
import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg";
import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg";
export default defineComponent({
props: {
isChecked: {
defineProps({
isChecked: {
type: Boolean,
required: true // 表示这个 props 是必需的
},
},
setup(props) {
const isChecked = computed(() => props.isChecked);
return {
ImgCheckboxBlueFrame,
ImgCheckboxCheckedMark,
ImgCheckboxGrayFrame,
isChecked,
};
required: true,
},
});
</script>
</script>