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

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";