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

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

View File

@@ -29,106 +29,86 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onMounted, ref, } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { mapActions, mapState, storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import i18next from '@/i18n/i18n'; import i18next from '@/i18n/i18n';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useAllMapDataStore } from '@/stores/allMapData'; import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance'; import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js'; import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import emitter from '@/utils/emitter';
export default { const router = useRouter();
setup() { const route = useRoute();
const { logOut } = useLoginStore(); const loginStore = useLoginStore();
const loginStore = useLoginStore(); const allMapDataStore = useAllMapDataStore();
const router = useRouter(); const conformanceStore = useConformanceStore();
const allMapDataStore = useAllMapDataStore(); const acctMgmtStore = useAcctMgmtStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId } = storeToRefs(conformanceStore);
const loginUserData = ref(null); const { logOut } = loginStore;
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail); const { tempFilterId } = storeToRefs(allMapDataStore);
const isAdmin = ref(false); const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const { userData } = storeToRefs(loginStore);
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
const getIsAdminValue = async () => { const loginUserData = ref(null);
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
const isAdmin = ref(false);
const getIsAdminValue = async () => {
await loginStore.getUserData(); await loginStore.getUserData();
loginUserData.value = loginStore.userData; loginUserData.value = loginStore.userData;
await acctMgmtStore.getUserDetail(loginUserData.value.username); await acctMgmtStore.getUserDetail(loginUserData.value.username);
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin; isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
}; };
const onBtnMyAccountClick = async() => { const onBtnMyAccountClick = async () => {
acctMgmtStore.closeAcctMenu(); acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username); await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push('/my-account'); await router.push('/my-account');
} };
onMounted(async () => { const clickOtherPlacesThenCloseMenu = () => {
await getIsAdminValue();
});
return {
logOut,
tempFilterId,
conformanceLogTempCheckId,
allMapDataStore,
conformanceStore,
isAdmin,
onBtnMyAccountClick,
};
},
data() {
return {
i18next: i18next,
}
},
computed: {
...mapState(useLoginStore, ['userData']),
...mapState(useAcctMgmtStore, ['isAcctMenuOpen']),
},
methods: {
clickOtherPlacesThenCloseMenu(){
const acctMgmtButton = document.getElementById('acct_mgmt_button'); const acctMgmtButton = document.getElementById('acct_mgmt_button');
const acctMgmtMenu = document.getElementById('account_menu'); const acctMgmtMenu = document.getElementById('account_menu');
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
if (!acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) { if (acctMgmtMenu && acctMgmtButton && !acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
this.closeAcctMenu(); acctMgmtStore.closeAcctMenu();
} }
}); });
},
onBtnAcctMgmtClick(){
this.$router.push({name: 'AcctAdmin'});
this.closeAcctMenu();
},
onLogoutBtnClick(){
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) {
// 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('leaveFilter', false);
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut)
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut)
} else {
this.logOut();
}
},
...mapActions(useLoginStore, ['getUserData']),
...mapActions(useAcctMgmtStore, ['closeAcctMenu']),
},
created() {
this.getUserData();
},
mounted(){
this.clickOtherPlacesThenCloseMenu();
},
}; };
const onBtnAcctMgmtClick = () => {
router.push({name: 'AcctAdmin'});
acctMgmtStore.closeAcctMenu();
};
const onLogoutBtnClick = () => {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
} else {
logOut();
}
};
// created
loginStore.getUserData();
// mounted
onMounted(async () => {
await getIsAdminValue();
clickOtherPlacesThenCloseMenu();
});
</script> </script>
<style> <style>

View File

@@ -9,30 +9,22 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, } from 'vue'; import { ref } from 'vue';
import i18next from '@/i18n/i18n.js'; import i18next from '@/i18n/i18n.js';
export default {
setup(props, { emit, }) {
const inputQuery = ref("");
const onSearchClick = (event) => { const emit = defineEmits(['on-search-account-button-click']);
const inputQuery = ref("");
const onSearchClick = (event) => {
event.preventDefault(); event.preventDefault();
emit('on-search-account-button-click', inputQuery.value); emit('on-search-account-button-click', inputQuery.value);
}; };
const handleKeyPressOfSearch = (event) => { const handleKeyPressOfSearch = (event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
emit('on-search-account-button-click', inputQuery.value); emit('on-search-account-button-click', inputQuery.value);
} }
}
return {
inputQuery,
onSearchClick,
handleKeyPressOfSearch,
i18next,
};
},
}; };
</script> </script>

View File

@@ -12,11 +12,8 @@
</div> </div>
</template> </template>
<script> <script setup>
import { defineComponent } from 'vue'; defineProps({
export default defineComponent({
props: {
isActivated: { isActivated: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -27,12 +24,5 @@
required: true, required: true,
default: "Status", default: "Status",
} }
},
setup(props) {
return {
isActivated: props.isActivated,
displayText: props.displayText,
};
}
}); });
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -191,7 +191,8 @@
<MoreModal :listModal="loopModal" @closeModal="loopModal = $event" :listTraces="loopTraces" :taskSeq="loopTaskSeq" :cases="loopCases" :listNo="loopNo" :traceId="looptraceId" :firstCases="loopFirstCases" :category="'loop'"></MoreModal> <MoreModal :listModal="loopModal" @closeModal="loopModal = $event" :listTraces="loopTraces" :taskSeq="loopTaskSeq" :cases="loopCases" :listNo="loopNo" :traceId="looptraceId" :firstCases="loopFirstCases" :category="'loop'"></MoreModal>
</section> </section>
</template> </template>
<script> <script setup>
import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance'; import { useConformanceStore } from '@/stores/conformance';
import iconNA from '@/components/icons/IconNA.vue'; import iconNA from '@/components/icons/IconNA.vue';
@@ -200,17 +201,12 @@ import getNumberLabel from '@/module/numberLabel.js';
import { setLineChartData, setBarChartData, timeRange, yTimeRange, getXIndex, formatTime, formatMaxTwo } from '@/module/setChartData.js'; import { setLineChartData, setBarChartData, timeRange, yTimeRange, getXIndex, formatTime, formatMaxTwo } from '@/module/setChartData.js';
import shortScaleNumber from '@/module/shortScaleNumber.js'; import shortScaleNumber from '@/module/shortScaleNumber.js';
import getMoment from 'moment'; import getMoment from 'moment';
import emitter from '@/utils/emitter';
export default { const conformanceStore = useConformanceStore();
setup() { const { conformanceTempReportData, issueTraces, taskSeq, cases, loopTraces, loopTaskSeq, loopCases } = storeToRefs(conformanceStore);
const conformanceStore = useConformanceStore();
const { conformanceTempReportData, issueTraces, taskSeq, cases, loopTraces, loopTaskSeq, loopCases } = storeToRefs(conformanceStore);
return { conformanceTempReportData, issueTraces, taskSeq, cases, loopTraces, loopTaskSeq, loopCases, conformanceStore } const data = ref({
},
data() {
return {
data: {
total: '--', total: '--',
counts: { counts: {
conforming: '--', conforming: '--',
@@ -240,24 +236,24 @@ export default {
total: '--', total: '--',
chart: {}, chart: {},
}, },
}, });
isCoverPlate: false, const isCoverPlate = ref(false);
issuesModal: false, const issuesModal = ref(false);
loopModal: false, const loopModal = ref(false);
rateChartData: null, const rateChartData = ref(null);
rateChartOptions: null, const rateChartOptions = ref(null);
casesChartData: null, const casesChartData = ref(null);
casesChartOptions: null, const casesChartOptions = ref(null);
timeChartData: null, const timeChartData = ref(null);
timeChartOptions: null, const timeChartOptions = ref(null);
issuesNo: null, const issuesNo = ref(null);
traceId: null, const traceId = ref(null);
firstCases: null, const firstCases = ref(null);
loopNo: null, const loopNo = ref(null);
looptraceId: null, const looptraceId = ref(null);
loopFirstCases: null, const loopFirstCases = ref(null);
selectDurationTime: null, const selectDurationTime = ref(null);
tooltip: { const tooltip = ref({
rate: { rate: {
value: '= Conforming / (Conforming + Not Conforming) * 100%', value: '= Conforming / (Conforming + Not Conforming) * 100%',
class: '!max-w-[36rem] !text-[10px] !opacity-90', class: '!max-w-[36rem] !text-[10px] !opacity-90',
@@ -274,102 +270,88 @@ export default {
value: 'Percentage of Issue Type (%) = Cases of Issue Type / Total Cases of All Issue Types.', value: 'Percentage of Issue Type (%) = Cases of Issue Type / Total Cases of All Issue Types.',
class: '!max-w-[36rem] !text-[10px] !opacity-90', class: '!max-w-[36rem] !text-[10px] !opacity-90',
} }
} });
}
}, /**
components: {
iconNA,
MoreModal,
},
watch: {
conformanceTempReportData: {
handler: function(newValue) {
if(newValue?.rule && newValue.rule.min !== null) {
this.selectDurationTime = {
min: newValue.rule.min,
max: newValue.rule.max,
}
}
this.data = this.setConformanceTempReportData(newValue);
},
},
},
methods: {
/**
* set progress bar width * set progress bar width
* @param {number} value 百分比數字 * @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定 * @returns {string} 樣式的寬度設定
*/ */
progressWidth(value){ const progressWidth = (value) => {
return `width:${value}%;` return `width:${value}%;`
}, };
/**
/**
* Number to percentage * Number to percentage
* @param {number} val 原始數字 * @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串 * @returns {string} 轉換完成的百分比字串
*/ */
getPercentLabel(val){ const getPercentLabel = (val) => {
if((val * 100).toFixed(1) >= 100) return 100; if((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1)); else return parseFloat((val * 100).toFixed(1));
}, };
/**
/**
* Convert seconds to days * Convert seconds to days
* @param {number} sec 秒數 * @param {number} sec 秒數
* @returns {number} day * @returns {number} day
*/ */
convertSecToDay(sec) { const convertSecToDay = (sec) => {
return (sec / 86400) return (sec / 86400)
}, };
/**
/**
* Open Issues Modal. * Open Issues Modal.
* @param {number} no trace 編號 * @param {number} no trace 編號
*/ */
async openMore(no) { const openMore = async (no) => {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題 // async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題
this.issuesNo = no; issuesNo.value = no;
await this.conformanceStore.getConformanceIssue(no); await conformanceStore.getConformanceIssue(no);
this.traceId = await this.issueTraces[0].id; traceId.value = await issueTraces.value[0].id;
this.firstCases = await this.conformanceStore.getConformanceTraceDetail(no, this.issueTraces[0].id, 0); firstCases.value = await conformanceStore.getConformanceTraceDetail(no, issueTraces.value[0].id, 0);
this.issuesModal = await true; issuesModal.value = await true;
}, };
/**
/**
* Open Loop Modal. * Open Loop Modal.
* @param {number} no trace 編號 * @param {number} no trace 編號
*/ */
async openLoopMore(no) { const openLoopMore = async (no) => {
// async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題 // async await 解決非同步資料延遲傳遞導致未讀取到而出錯的問題
this.loopNo = no; loopNo.value = no;
await this.conformanceStore.getConformanceLoop(no); await conformanceStore.getConformanceLoop(no);
this.looptraceId = await this.loopTraces[0].id; looptraceId.value = await loopTraces.value[0].id;
this.loopFirstCases = await this.conformanceStore.getConformanceLoopsTraceDetail(no, this.loopTraces[0].id, 0); loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail(no, loopTraces.value[0].id, 0);
this.loopModal = await true; loopModal.value = await true;
}, };
/**
/**
* set conformance report data * set conformance report data
* @param {object} data new watch's value 監聽到後端傳來的報告 data * @param {object} data new watch's value 監聽到後端傳來的報告 data
*/ */
setConformanceTempReportData(data){ const setConformanceTempReportData = (newData) => {
const total = getNumberLabel(Object.values(data.counts).reduce((acc, val) => acc + val, 0)); const total = getNumberLabel(Object.values(newData.counts).reduce((acc, val) => acc + val, 0));
const sum = data.counts.conforming + data.counts.not_conforming; const sum = newData.counts.conforming + newData.counts.not_conforming;
const rate = ((data.counts.conforming / sum) * 100).toFixed(1); const rate = ((newData.counts.conforming / sum) * 100).toFixed(1);
const isNullTime = value => value === null ? null : getNumberLabel((value / 86400).toFixed(1)); const isNullTime = value => value === null ? null : getNumberLabel((value / 86400).toFixed(1));
const isNullCase = value => value === null ? null : getNumberLabel(value.toFixed(1)); const isNullCase = value => value === null ? null : getNumberLabel(value.toFixed(1));
const setLoopData = value => value.map(item => { const setLoopData = value => value.map(item => {
return { return {
no: item.no, no: item.no,
label: item.description, label: item.description,
value: `width:${this.getPercentLabel(item.count / data.counts.conforming)}%;`, value: `width:${getPercentLabel(item.count / newData.counts.conforming)}%;`,
count: getNumberLabel(item.count), count: getNumberLabel(item.count),
ratio: this.getPercentLabel(item.count / data.counts.conforming), ratio: getPercentLabel(item.count / newData.counts.conforming),
} }
}); });
const setIssueData = value => value.map(item => { const setIssueData = value => value.map(item => {
return { return {
no: item.no, no: item.no,
label: item.description, label: item.description,
value: `width:${this.getPercentLabel(item.count / data.counts.not_conforming)}%;`, value: `width:${getPercentLabel(item.count / newData.counts.not_conforming)}%;`,
count: getNumberLabel(item.count), count: getNumberLabel(item.count),
ratio: this.getPercentLabel(item.count / data.counts.not_conforming), ratio: getPercentLabel(item.count / newData.counts.not_conforming),
} }
}); });
const isNullLoops = value => value === null ? null : setLoopData(value); const isNullLoops = value => value === null ? null : setLoopData(value);
@@ -378,45 +360,45 @@ export default {
const result = { const result = {
total: `Total ${total}`, total: `Total ${total}`,
counts: { counts: {
conforming: getNumberLabel(data.counts.conforming), conforming: getNumberLabel(newData.counts.conforming),
not_conforming: getNumberLabel(data.counts.not_conforming), not_conforming: getNumberLabel(newData.counts.not_conforming),
not_applicable: getNumberLabel(data.counts.not_applicable), not_applicable: getNumberLabel(newData.counts.not_applicable),
}, },
charts: { charts: {
rate: { rate: {
rate: rate, rate: rate,
data: setLineChartData(data.charts.rate.data, data.charts.rate.x_axis.max, data.charts.rate.x_axis.min, true), data: setLineChartData(newData.charts.rate.data, newData.charts.rate.x_axis.max, newData.charts.rate.x_axis.min, true),
xMax: getMoment(data.charts.rate.x_axis.max).format('YYYY/M/D'), xMax: getMoment(newData.charts.rate.x_axis.max).format('YYYY/M/D'),
xMin: getMoment(data.charts.rate.x_axis.min).format('YYYY/M/D'), xMin: getMoment(newData.charts.rate.x_axis.min).format('YYYY/M/D'),
}, },
cases: { cases: {
conforming: getNumberLabel(data.counts.conforming), conforming: getNumberLabel(newData.counts.conforming),
total: getNumberLabel(sum), total: getNumberLabel(sum),
data: { data: {
conforming: setBarChartData(data.charts.cases.data.filter(item => item.label === 'conforming').map(item => item.data)[0]), conforming: setBarChartData(newData.charts.cases.data.filter(item => item.label === 'conforming').map(item => item.data)[0]),
not_conforming: setBarChartData(data.charts.cases.data.filter(item => item.label === 'not-conforming').map(item => item.data)[0]), not_conforming: setBarChartData(newData.charts.cases.data.filter(item => item.label === 'not-conforming').map(item => item.data)[0]),
}, },
xMax: getMoment(data.charts.cases.x_axis.max).format('YYYY/M/D'), xMax: getMoment(newData.charts.cases.x_axis.max).format('YYYY/M/D'),
xMin: getMoment(data.charts.cases.x_axis.min).format('YYYY/M/D'), xMin: getMoment(newData.charts.cases.x_axis.min).format('YYYY/M/D'),
}, },
fitness: getNumberLabel(data.charts.fitness), fitness: getNumberLabel(newData.charts.fitness),
}, },
effect: { effect: {
time: { time: {
conforming: isNullTime(data.effect.time.conforming), conforming: isNullTime(newData.effect.time.conforming),
not_conforming: isNullTime(data.effect.time.not_conforming), not_conforming: isNullTime(newData.effect.time.not_conforming),
difference: (isNullTime(data.effect.time.conforming) - isNullTime(data.effect.time.not_conforming)).toFixed(1), difference: (isNullTime(newData.effect.time.conforming) - isNullTime(newData.effect.time.not_conforming)).toFixed(1),
}, },
tasks: { tasks: {
conforming: isNullCase(data.effect.tasks.conforming), conforming: isNullCase(newData.effect.tasks.conforming),
not_conforming: isNullCase(data.effect.tasks.not_conforming), not_conforming: isNullCase(newData.effect.tasks.not_conforming),
difference: (isNullCase(data.effect.tasks.conforming) - isNullCase(data.effect.tasks.not_conforming)).toFixed(1), difference: (isNullCase(newData.effect.tasks.conforming) - isNullCase(newData.effect.tasks.not_conforming)).toFixed(1),
}, },
}, },
loops: isNullLoops(data.loops), loops: isNullLoops(newData.loops),
issues: isNullIsssue(data.issues), issues: isNullIsssue(newData.issues),
timeTrend: { timeTrend: {
not_conforming: getNumberLabel(data.counts.not_conforming), not_conforming: getNumberLabel(newData.counts.not_conforming),
total: getNumberLabel(sum), total: getNumberLabel(sum),
chart: null, chart: null,
xMax: null, xMax: null,
@@ -426,30 +408,31 @@ export default {
} }
}; };
if (data.charts.time) { if (newData.charts.time) {
result.timeTrend.chart = setLineChartData(data.charts.time.data, data.charts.time.x_axis.max, data.charts.time.x_axis.min, false, data.charts.time.y_axis.max, data.charts.time.y_axis.min); result.timeTrend.chart = setLineChartData(newData.charts.time.data, newData.charts.time.x_axis.max, newData.charts.time.x_axis.min, false, newData.charts.time.y_axis.max, newData.charts.time.y_axis.min);
result.timeTrend.xMax = data.charts.time.x_axis.max; result.timeTrend.xMax = newData.charts.time.x_axis.max;
result.timeTrend.xMin = data.charts.time.x_axis.min; result.timeTrend.xMin = newData.charts.time.x_axis.min;
result.timeTrend.yMax = data.charts.time.y_axis.max; result.timeTrend.yMax = newData.charts.time.y_axis.max;
result.timeTrend.yMin = data.charts.time.y_axis.min; result.timeTrend.yMin = newData.charts.time.y_axis.min;
} }
this.setRateChartData(result.charts.rate.data); // 建立圖表 Rate Chart.js setRateChartData(result.charts.rate.data); // 建立圖表 Rate Chart.js
this.setCasesChartData(result.charts.cases.data.conforming, result.charts.cases.data.not_conforming, data.charts.cases.x_axis.max, data.charts.cases.x_axis.min); // 建立圖表 Cases Chart.js setCasesChartData(result.charts.cases.data.conforming, result.charts.cases.data.not_conforming, newData.charts.cases.x_axis.max, newData.charts.cases.x_axis.min); // 建立圖表 Cases Chart.js
if(data.charts.time) this.setTimeChartData(result.timeTrend.chart, result.timeTrend.xMax, result.timeTrend.xMin, result.timeTrend.yMax, result.timeTrend.yMax, result.timeTrend.yMin); // 建立圖表 Time Chart.js if(newData.charts.time) setTimeChartData(result.timeTrend.chart, result.timeTrend.xMax, result.timeTrend.xMin, result.timeTrend.yMax, result.timeTrend.yMax, result.timeTrend.yMin); // 建立圖表 Time Chart.js
return result; return result;
}, };
/**
/**
* set Rate Chart Data * set Rate Chart Data
* @param {object} data new rate chart data * @param {object} data new rate chart data
*/ */
setRateChartData(data){ const setRateChartData = (chartData) => {
this.rateChartData = { rateChartData.value = {
labels:[], labels:[],
datasets: [ datasets: [
{ {
label: 'Rate', label: 'Rate',
data: data, data: chartData,
fill: false, fill: false,
pointRadius: 0, // 隱藏點 pointRadius: 0, // 隱藏點
pointHoverRadius: 0, // 隱藏點的 hover pointHoverRadius: 0, // 隱藏點的 hover
@@ -460,7 +443,7 @@ export default {
} }
] ]
}; };
this.rateChartOptions = { rateChartOptions.value = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
aspectRatio: 0.6, aspectRatio: 0.6,
@@ -480,22 +463,8 @@ export default {
scales: { scales: {
x: { x: {
type: 'time', type: 'time',
// time: {
// max: '2022-01-12T02:29:42',
// min: '2022-01-03T00:56:25',
// displayFormats: {
// day: 'yyyy/M/d'
// }
// },
ticks: { ticks: {
display: false, display: false,
// maxRotation: 0, // 不旋轉 lable 0~50
// color: '#334155',
// source: 'data',
// align: 'inner', // label 在軸線的位置
// callback: function(value, index, values) {
// return (index === 0 || index === values.length - 1) ? getMoment(value).format('YYYY/M/D') : null;
// },
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // 隱藏 x 軸網格
@@ -527,16 +496,17 @@ export default {
}, },
}, },
}; };
}, };
/**
/**
* set Cases Chart Data * set Cases Chart Data
* @param {array} data new cases chart conforming data * @param {array} conformingData new cases chart conforming data
* @param {array} data new cases chart not conforming data * @param {array} notConformingData new cases chart not conforming data
* @param {number} data new cases chart xMax * @param {number} xMax new cases chart xMax
* @param {number} data new cases chart xMin * @param {number} xMin new cases chart xMin
*/ */
setCasesChartData(conformingData, notConformingData, xMax, xMin){ const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
this.casesChartData = { casesChartData.value = {
datasets: [ datasets: [
{ {
type: 'bar', type: 'bar',
@@ -552,7 +522,7 @@ export default {
}, },
] ]
}; };
this.casesChartOptions = { casesChartOptions.value = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
aspectRatio: 0.8, aspectRatio: 0.8,
@@ -575,15 +545,6 @@ export default {
stacked: true, stacked: true,
ticks: { ticks: {
display: false, display: false,
// autoSkip: false,
// maxRotation: 0, // 不旋轉 lable 0~50
// color: '#334155',
// align: 'center', // label 在軸線的位置
// callback: function(value, index, values) {
// if(index === 0) return getMoment(xMin).format('yyyy/M/D');
// else if(index === values.length - 1) return getMoment(xMax).format('yyyy/M/D');
// else return null;
// },
}, },
grid: { grid: {
display: false, // 隱藏 x 軸網格 display: false, // 隱藏 x 軸網格
@@ -617,30 +578,31 @@ export default {
}, },
}, },
}; };
}, };
/**
/**
* set Time Trend chart data * set Time Trend chart data
* @param {array} data Time Trend chart conforming data * @param {array} chartData Time Trend chart conforming data
* @param {number} xMax Time Trend xMax * @param {number} xMax Time Trend xMax
* @param {number} xMin Time Trend xMin * @param {number} xMin Time Trend xMin
* @param {number} yMax Time Trend yMax * @param {number} yMax Time Trend yMax
* @param {number} yMin Time Trend yMin * @param {number} yMin Time Trend yMin
*/ */
setTimeChartData(data, xMax, xMin, yMax, yMin) { const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
const max = yMax * 1.1; const max = yMax * 1.1;
const xVal = timeRange(xMin, xMax, 100); const xVal = timeRange(xMin, xMax, 100);
const yVal = yTimeRange(data, 100, yMin, yMax); const yVal = yTimeRange(chartData, 100, yMin, yMax);
xVal.map((x, index) => ({ x, y: yVal[index] })); xVal.map((x, index) => ({ x, y: yVal[index] }));
let formattedXVal = xVal.map(value => formatTime(value)); let formattedXVal = xVal.map(value => formatTime(value));
formattedXVal = formatMaxTwo(formattedXVal); formattedXVal = formatMaxTwo(formattedXVal);
const selectTimeMinIndex = getXIndex(xVal, this.selectDurationTime.min); const selectTimeMinIndex = getXIndex(xVal, selectDurationTime.value.min);
const selectTimeMaxIndex = getXIndex(xVal, this.selectDurationTime.max); const selectTimeMaxIndex = getXIndex(xVal, selectDurationTime.value.max);
const start = selectTimeMinIndex; const start = selectTimeMinIndex;
const end = selectTimeMaxIndex; const end = selectTimeMaxIndex;
const inside = (ctx, value) => ctx.p0DataIndex >= start && ctx.p1DataIndex <= end ? value : undefined; const inside = (ctx, value) => ctx.p0DataIndex >= start && ctx.p1DataIndex <= end ? value : undefined;
const outside = (ctx, value) => ctx.p0DataIndex < start || ctx.p1DataIndex > end ? value : undefined; const outside = (ctx, value) => ctx.p0DataIndex < start || ctx.p1DataIndex > end ? value : undefined;
this.timeChartData = { timeChartData.value = {
labels: formattedXVal, labels: formattedXVal,
datasets: [ datasets: [
{ {
@@ -662,7 +624,7 @@ export default {
] ]
}; };
this.timeChartOptions = { timeChartOptions.value = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
layout: { layout: {
@@ -679,8 +641,6 @@ export default {
scales: { scales: {
x: { x: {
ticks: { ticks: {
// autoSkip: false, // 彈性顯示或全部顯示 lable
// autoSkipPadding: 4, // lable 之間的距離
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0, // 不旋轉 lable 0~50
color: '#334155', color: '#334155',
display: true, display: true,
@@ -716,16 +676,25 @@ export default {
}, },
}, },
}; };
}, };
},
created() { // watch
this.$emitter.on('coverPlate', boolean => { watch(conformanceTempReportData, (newValue) => {
this.isCoverPlate = boolean; if(newValue?.rule && newValue.rule.min !== null) {
}); selectDurationTime.value = {
// 取得 selectTimeTange 給 Tiem Trend 使用 min: newValue.rule.min,
this.$emitter.on('timeRangeMaxMin', data => this.selectDurationTime = data); max: newValue.rule.max,
}, }
} }
data.value = setConformanceTempReportData(newValue);
});
// created - emitter listeners
emitter.on('coverPlate', boolean => {
isCoverPlate.value = boolean;
});
// 取得 selectTimeTange 給 Tiem Trend 使用
emitter.on('timeRangeMaxMin', newData => selectDurationTime.value = newData);
</script> </script>
<style scoped> <style scoped>
:deep(.disc) { :deep(.disc) {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -119,93 +119,88 @@
</div> </div>
</section> </section>
</template> </template>
<script> <script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData'; import { useAllMapDataStore } from '@/stores/allMapData';
import { setLineChartData } from '@/module/setChartData.js'; import { setLineChartData } from '@/module/setChartData.js';
import getMoment from 'moment'; import getMoment from 'moment';
import InputNumber from 'primevue/inputnumber'; import InputNumber from 'primevue/inputnumber';
import { Decimal } from 'decimal.js'; import { Decimal } from 'decimal.js';
import emitter from '@/utils/emitter';
export default { const emit = defineEmits(['select-attribute']);
setup() {
const allMapDataStore = useAllMapDataStore();
const { filterAttrs } = storeToRefs(allMapDataStore);
return { filterAttrs } const allMapDataStore = useAllMapDataStore();
}, const { filterAttrs } = storeToRefs(allMapDataStore);
components: {
InputNumber, const selectedAttName = ref({});
}, const selectedAttRange = ref(null);
data() { const valueTypes = ['int', 'float', 'date'];
return { const classTypes = ['boolean', 'string'];
selectedAttName: {}, const chartData = ref({});
selectedAttRange: null, const chartOptions = ref({});
valueTypes: ['int', 'float', 'date'], const chartComplete = ref(null); // 已出圖的 chart.js 資料
classTypes: ['boolean', 'string'], const selectArea = ref(null);
chartData: {}, const selectRange = ref(1000); // 更改 select 的切分數
chartOptions: {}, const startTime = ref(null); // PrimeVue Calendar v-model
chartComplete: null, // 已出圖的 chart.js 資料 const endTime = ref(null); // PrimeVue Calendar v-model
selectArea: null, const startMinDate = ref(null);
selectRange: 1000, // 更改 select 的切分數 const startMaxDate = ref(null);
startTime: null, // PrimeVue Calendar v-model const endMinDate = ref(null);
endTime: null, // PrimeVue Calendar v-model const endMaxDate = ref(null);
startMinDate: null, const valueStart = ref(null); // PrimeVue InputNumber v-model
startMaxDate: null, const valueEnd = ref(null); // PrimeVue InputNumber v-model
endMinDate: null, const valueStartMin = ref(null);
endMaxDate: null, const valueStartMax = ref(null);
valueStart: null, // PrimeVue InputNumber v-model const valueEndMin = ref(null);
valueEnd: null, // PrimeVue InputNumber v-model const valueEndMax = ref(null);
valueStartMin: null, const tableClass = 'w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm';
valueStartMax: null, const headerModeClass = 'w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10';
valueEndMin: null, const headerClass = '!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10';
valueEndMax: null, const bodyModeClass = '!p-2 !border-0';
tableClass: 'w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm', const bodyClass = 'break-words !py-2 !border-0';
headerModeClass: 'w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10', const panelProps = {
headerClass: '!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10',
bodyModeClass: '!p-2 !border-0',
bodyClass: 'break-words !py-2 !border-0',
panelProps: {
onClick: (event) => { onClick: (event) => {
event.stopPropagation(); event.stopPropagation();
}, },
}, };
tooltip: { const tooltip = {
attributeName: { attributeName: {
value: 'Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.', value: 'Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.',
class: '!max-w-[212px] !text-[10px] !opacity-90', class: '!max-w-[212px] !text-[10px] !opacity-90',
}, },
} };
}
}, const attTotal = computed(() => {
computed: { return filterAttrs.value.length;
attTotal: function() { });
return this.filterAttrs.length;
}, const attRangeTotal = computed(() => {
attRangeTotal: function() { const type = selectedAttName.value.type;
const type = this.selectedAttName.type;
let result = null; // Initialize the result variable with null let result = null; // Initialize the result variable with null
if (this.classTypes.includes(type) && this.attRangeData) { if (classTypes.includes(type) && attRangeData.value) {
result = `(${this.attRangeData.length})`; // Assign the length of attRangeData if it exists result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
} }
return result; return result;
}, });
attRangeData: function() {
const attRangeData = computed(() => {
let data = []; let data = [];
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
const sum = this.selectedAttName.options.map(item => item.freq).reduce((acc, cur) => acc + cur, 0); const sum = selectedAttName.value.options.map(item => item.freq).reduce((acc, cur) => acc + cur, 0);
data = this.selectedAttName.options.map((item, index) => { data = selectedAttName.value.options.map((item, index) => {
const ratio = item.freq / sum; const ratio = item.freq / sum;
const result = { const result = {
id: index + 1, id: index + 1,
key: this.selectedAttName.key, key: selectedAttName.value.key,
type: type, type: type,
value: item.value, value: item.value,
occ_progress_bar: ratio * 100, occ_progress_bar: ratio * 100,
occ_value: item.freq.toLocaleString('en-US'), occ_value: item.freq.toLocaleString('en-US'),
occ_ratio: this.getPercentLabel(ratio), occ_ratio: getPercentLabel(ratio),
freq: item.freq freq: item.freq
}; };
result.label = null; result.label = null;
@@ -217,22 +212,24 @@ export default {
return result; return result;
}) })
return data.sort((x, y) => y.freq - x.freq); return data.sort((x, y) => y.freq - x.freq);
}, });
// 取出選取的 Attribute radio 數值型的資料
valueData: function() { // 取出選取的 Attribute radio 數值型的資料
const valueData = computed(() => {
// filter 回傳陣列find 回傳遞一個找到的元素,因此使用 find 方法。 // filter 回傳陣列find 回傳遞一個找到的元素,因此使用 find 方法。
if(this.valueTypes.includes(this.selectedAttName.type)){ if(valueTypes.includes(selectedAttName.value.type)){
const valueData = this.filterAttrs.find(item => item.type === this.selectedAttName.type && item.key === this.selectedAttName.key); const data = filterAttrs.value.find(item => item.type === selectedAttName.value.type && item.key === selectedAttName.value.key);
return valueData return data
} }
}, });
// 找出 slidrData時間格式:毫秒時間戳
sliderData: function() { // 找出 slidrData,時間格式:毫秒時間戳
const sliderDataComputed = computed(() => {
let xAxisMin; let xAxisMin;
let xAxisMax; let xAxisMax;
const min = this.valueData.min; const min = valueData.value.min;
const max = this.valueData.max; const max = valueData.value.max;
const type = this.valueData.type; const type = valueData.value.type;
switch (type) { switch (type) {
case 'dummy': case 'dummy':
case 'date': case 'date':
@@ -245,22 +242,22 @@ export default {
break; break;
} }
const range = xAxisMax - xAxisMin; const range = xAxisMax - xAxisMin;
const step = range / this.selectRange; const step = range / selectRange.value;
let sliderData = [] let data = []
for (let i = 0; i <= this.selectRange; i++) { for (let i = 0; i <= selectRange.value; i++) {
sliderData.push(xAxisMin + (step * i)); data.push(xAxisMin + (step * i));
} }
switch (type) { switch (type) {
case 'int': case 'int':
sliderData = sliderData.map(value => { data = data.map(value => {
let result = Math.round(value); let result = Math.round(value);
result = result === -0 ? 0 : result; result = result === -0 ? 0 : result;
return result; return result;
}); });
break; break;
case 'float': case 'float':
sliderData = sliderData.map(value => { data = data.map(value => {
let result = new Decimal(value.toFixed(2)).toNumber(); let result = new Decimal(value.toFixed(2)).toNumber();
result = result === -0 ? 0 : result; result = result === -0 ? 0 : result;
return result; return result;
@@ -269,40 +266,42 @@ export default {
default: default:
break; break;
} }
return sliderData; return data;
}, });
// user select value type start and end
attValueTypeStartEnd: function() { // user select value type start and end
const attValueTypeStartEnd = computed(() => {
let start; let start;
let end; let end;
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
switch (type) { switch (type) {
case 'dummy': //sonar-qube case 'dummy': //sonar-qube
case 'date': case 'date':
start = getMoment(this.startTime).format('YYYY-MM-DDTHH:mm:00'); start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
end = getMoment(this.endTime).format('YYYY-MM-DDTHH:mm:00'); end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
break; break;
default: default:
start = this.valueStart; start = valueStart.value;
end = this.valueEnd; end = valueEnd.value;
break; break;
} }
const data = { // 傳給後端的資料 const data = { // 傳給後端的資料
type: type, type: type,
data: { data: {
key: this.selectedAttName.key, key: selectedAttName.value.key,
min: start, min: start,
max: end, max: end,
} }
} }
this.$emit('select-attribute', data); emit('select-attribute', data);
return [start, end]; return [start, end];
}, });
labelsData: function() {
const min = new Date(this.valueData.min).getTime(); const labelsData = computed(() => {
const max = new Date(this.valueData.max).getTime(); const min = new Date(valueData.value.min).getTime();
const max = new Date(valueData.value.max).getTime();
const numPoints = 11; const numPoints = 11;
const step = (max - min) / (numPoints - 1); const step = (max - min) / (numPoints - 1);
const data = []; const data = [];
@@ -311,172 +310,181 @@ export default {
data.push(x); data.push(x);
} }
return data; return data;
}, });
},
methods: { /**
/**
* 選取類別型 table 的選項 * 選取類別型 table 的選項
*/ */
onRowSelect() { function onRowSelect() {
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
const data = { const data = {
type: type, type: type,
data: this.selectedAttRange, data: selectedAttRange.value,
}; };
this.$emit('select-attribute', data); emit('select-attribute', data);
}, }
/**
/**
* 取消類別型 table 的選項 * 取消類別型 table 的選項
*/ */
onRowUnselect() { function onRowUnselect() {
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
const data = { const data = {
type: type, type: type,
data: this.selectedAttRange, data: selectedAttRange.value,
}; };
this.$emit('select-attribute', data); emit('select-attribute', data);
}, }
/**
/**
* 選取類別型 table 的全選項 * 選取類別型 table 的全選項
* @param {event} e input 傳入的事件 * @param {event} e input 傳入的事件
*/ */
onRowSelectAll(e) { function onRowSelectAll(e) {
this.selectedAttRange = e.data; selectedAttRange.value = e.data;
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
const data = { const data = {
type: type, type: type,
data: this.selectedAttRange, data: selectedAttRange.value,
}; };
this.$emit('select-attribute', data); emit('select-attribute', data);
}, }
/**
/**
* 取消類別型 table 的全選項 * 取消類別型 table 的全選項
*/ */
onRowUnelectAll() { function onRowUnelectAll() {
this.selectedAttRange = null; selectedAttRange.value = null;
const type = this.selectedAttName.type; const type = selectedAttName.value.type;
const data = { const data = {
type: type, type: type,
data: this.selectedAttRange, data: selectedAttRange.value,
}; };
this.$emit('select-attribute', data) emit('select-attribute', data)
}, }
/**
/**
* 切換 Attribute Name Radio * 切換 Attribute Name Radio
* @param {event} e input 傳入的事件 * @param {event} e input 傳入的事件
*/ */
switchAttNameRadio(e) { function switchAttNameRadio(e) {
this.selectedAttRange = null; selectedAttRange.value = null;
this.startTime = null; startTime.value = null;
this.endTime = null; endTime.value = null;
this.valueStart = null; valueStart.value = null;
this.valueEnd = null; valueEnd.value = null;
if(this.valueData) { // 切換 Attribute Name if(valueData.value) { // 切換 Attribute Name
// 初始化雙向綁定 // 初始化雙向綁定
this.selectArea = [0, this.selectRange]; selectArea.value = [0, selectRange.value];
const min = this.valueData.min; const min = valueData.value.min;
const max = this.valueData.max; const max = valueData.value.max;
switch (this.selectedAttName.type) { switch (selectedAttName.value.type) {
case 'dummy': //sonar-qube case 'dummy': //sonar-qube
case 'date': case 'date':
// 除了 date 外雙向綁定為空 // 除了 date 外雙向綁定為空
this.valueStart = null; valueStart.value = null;
this.valueEnd = null; valueEnd.value = null;
// 初始化: Calendar // 初始化: Calendar
this.startMinDate = new Date(min); startMinDate.value = new Date(min);
this.startMaxDate = new Date(max); startMaxDate.value = new Date(max);
this.endMinDate = new Date(min); endMinDate.value = new Date(min);
this.endMaxDate = new Date(max); endMaxDate.value = new Date(max);
// 初始化: 讓日曆的範圍等於時間軸的範圍 // 初始化: 讓日曆的範圍等於時間軸的範圍
this.startTime = new Date(min); startTime.value = new Date(min);
this.endTime = new Date(max); endTime.value = new Date(max);
break; break;
default: default:
// date 雙向綁定為空 // date 雙向綁定為空
this.startTime = null; startTime.value = null;
this.endTime = null; endTime.value = null;
// 初始化InputNumber // 初始化InputNumber
this.valueStartMin = min; valueStartMin.value = min;
this.valueStartMax = max; valueStartMax.value = max;
this.valueEndMin = min; valueEndMin.value = min;
this.valueEndMax = max; valueEndMax.value = max;
// 初始化: 讓 InputNumber 的範圍等於時間軸的範圍 // 初始化: 讓 InputNumber 的範圍等於時間軸的範圍
this.valueStart = min; valueStart.value = min;
this.valueEnd = max; valueEnd.value = max;
break; break;
} }
// 傳給後端 // 傳給後端
// this.attValueTypeStartEnd; 是否有要呼叫函數? sonar-qube // attValueTypeStartEnd.value; 是否有要呼叫函數? sonar-qube
// 建立圖表 // 建立圖表
this.createChart(); createChart();
} }
}, }
/**
/**
* set progress bar width * set progress bar width
* @param {number} value 百分比數字 * @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定 * @returns {string} 樣式的寬度設定
*/ */
progressWidth(value){ function progressWidth(value){
return `width:${value}%;` return `width:${value}%;`
}, }
/**
/**
* Number to percentage * Number to percentage
* @param {number} val 原始數字 * @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串 * @returns {string} 轉換完成的百分比字串
*/ */
getPercentLabel(val){ function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`; if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`; else return `${(val * 100).toFixed(1)}%`;
}, }
/**
/**
* 調整遮罩大小 * 調整遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chart 取得 chart.js 資料
*/ */
resizeMask(chart) { function resizeMask(chart) {
const from = (this.selectArea[0] * 0.01) / (this.selectRange * 0.01); const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (this.selectArea[1] * 0.01) / (this.selectRange * 0.01); const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
this.resizeLeftMask(chart, from); resizeLeftMask(chart, from);
this.resizeRightMask(chart, to); resizeRightMask(chart, to);
}, }
/**
/**
* 調整左邊遮罩大小 * 調整左邊遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chart 取得 chart.js 資料
*/ */
resizeLeftMask(chart, from) { function resizeLeftMask(chart, from) {
const canvas = document.querySelector('#chartCanvasId canvas'); const canvas = document.querySelector('#chartCanvasId canvas');
const mask = document.getElementById("chart-mask-left"); const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`; mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`; mask.style.width = `${chart.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`; mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`; mask.style.height = `${chart.chartArea.height}px`;
}, }
/**
/**
* 調整右邊遮罩大小 * 調整右邊遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chart 取得 chart.js 資料
*/ */
resizeRightMask(chart, to) { function resizeRightMask(chart, to) {
const canvas = document.querySelector('#chartCanvasId canvas'); const canvas = document.querySelector('#chartCanvasId canvas');
const mask = document.getElementById("chart-mask-right"); const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`; mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`; mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`; mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`; mask.style.height = `${chart.chartArea.height}px`;
}, }
/**
/**
* create chart * create chart
*/ */
createChart() { function createChart() {
const valueData = this.valueData; const vData = valueData.value;
const max = valueData.chart.y_axis.max * 1.1; const max = vData.chart.y_axis.max * 1.1;
const data = setLineChartData(valueData.chart.data, valueData.chart.x_axis.max, valueData.chart.x_axis.min); const data = setLineChartData(vData.chart.data, vData.chart.x_axis.max, vData.chart.x_axis.min);
const isDateType = valueData.type === 'date'; const isDateType = vData.type === 'date';
const minX = valueData.chart.x_axis.min; const minX = vData.chart.x_axis.min;
const maxX = valueData.chart.x_axis.max; const maxX = vData.chart.x_axis.max;
let setChartData= {}; let setChartData= {};
let setChartOptions= {}; let setChartOptions= {};
let setLabels = []; let setLabels = [];
switch (valueData.type) { switch (vData.type) {
case 'int': case 'int':
setLabels = data.map(item => Math.round(item.x)); setLabels = data.map(item => Math.round(item.x));
break; break;
@@ -495,7 +503,7 @@ export default {
}); });
break; break;
case 'date': case 'date':
setLabels = this.labelsData; setLabels = labelsData.value;
break; break;
default: default:
break; break;
@@ -533,8 +541,8 @@ export default {
}, },
animation: { animation: {
onComplete: e => { onComplete: e => {
this.chartComplete = e.chart; chartComplete.value = e.chart;
this.resizeMask(e.chart); resizeMask(e.chart);
} }
}, },
interaction: { interaction: {
@@ -593,7 +601,7 @@ export default {
color: '#334155', color: '#334155',
callback: ((value, index, values) => { callback: ((value, index, values) => {
let x; let x;
switch (valueData.type) { switch (vData.type) {
case 'int': case 'int':
return Math.round(value); return Math.round(value);
case 'float': case 'float':
@@ -619,111 +627,113 @@ export default {
}, },
} }
} }
this.chartData = setChartData; chartData.value = setChartData;
this.chartOptions = setChartOptions; chartOptions.value = setChartOptions;
}, }
/**
/**
* 滑塊改變的時候 * 滑塊改變的時候
* @param {array} e [1, 100] * @param {array} e [1, 100]
*/ */
changeSelectArea(e) { function changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變 // 日曆改變時,滑塊跟著改變
const sliderData = this.sliderData; const sliderData = sliderDataComputed.value;
const start = sliderData[e[0].toFixed()]; const start = sliderData[e[0].toFixed()];
const end = sliderData[e[1].toFixed()]; // 取得 index須為整數。 const end = sliderData[e[1].toFixed()]; // 取得 index須為整數。
switch (this.selectedAttName.type) { switch (selectedAttName.value.type) {
case 'dummy': case 'dummy':
case 'date': case 'date':
this.startTime = new Date(start); startTime.value = new Date(start);
this.endTime = new Date(end); endTime.value = new Date(end);
// 重新設定 start end 日曆選取範圍 // 重新設定 start end 日曆選取範圍
this.endMinDate = new Date(start); endMinDate.value = new Date(start);
this.startMaxDate = new Date(end); startMaxDate.value = new Date(end);
break; break;
default: default:
this.valueStart = start; valueStart.value = start;
this.valueEnd = end; valueEnd.value = end;
// 重新設定 start end 日曆選取範圍 // 重新設定 start end 日曆選取範圍
this.valueEndMin = start; valueEndMin.value = start;
this.valueStartMax = end; valueStartMax.value = end;
break; break;
} }
// 重新算圖 // 重新算圖
this.resizeMask(this.chartComplete); resizeMask(chartComplete.value);
// 執行 timeFrameStartEnd 才會改變數據 // 執行 timeFrameStartEnd 才會改變數據
// this.attValueTypeStartEnd; 是否有要呼叫函數? sonar-qube // attValueTypeStartEnd.value; 是否有要呼叫函數? sonar-qube
}, }
/**
/**
* 選取開始或結束時間時,要改變滑塊跟圖表 * 選取開始或結束時間時,要改變滑塊跟圖表
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間) | Blur Event * @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間) | Blur Event
* @param {string} direction start or end * @param {string} direction start or end
*/ */
sliderValueRange(e, direction) { function sliderValueRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳 // 找到最鄰近的 index時間格式: 毫秒時間戳
const sliderData = this.sliderData; const sliderData = sliderDataComputed.value;
const isDateType = this.selectedAttName.type === 'date'; const isDateType = selectedAttName.value.type === 'date';
let targetTime = []; let targetTime = [];
let inputValue; let inputValue;
if(isDateType) targetTime = [new Date(this.attValueTypeStartEnd[0]).getTime(), new Date(this.attValueTypeStartEnd[1]).getTime()]; if(isDateType) targetTime = [new Date(attValueTypeStartEnd.value[0]).getTime(), new Date(attValueTypeStartEnd.value[1]).getTime()];
else targetTime = [this.attValueTypeStartEnd[0], this.attValueTypeStartEnd[1]] else targetTime = [attValueTypeStartEnd.value[0], attValueTypeStartEnd.value[1]]
const closestIndexes = targetTime.map(target => { const closestIndexes = targetTime.map(target => {
let closestIndex = 0; let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length; closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
let result = Math.round(Math.abs(closestIndex)); let result = Math.round(Math.abs(closestIndex));
result = result > this.selectRange ? this.selectRange : result; result = result > selectRange.value ? selectRange.value : result;
return result return result
}); });
// 改變滑塊 // 改變滑塊
this.selectArea = closestIndexes; selectArea.value = closestIndexes;
// 重新設定 start end 日曆選取範圍 // 重新設定 start end 日曆選取範圍
if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ; if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ;
if(direction === 'start') { if(direction === 'start') {
if(isDateType){ if(isDateType){
this.endMinDate = e; endMinDate.value = e;
} else { } else {
this.valueEndMin = inputValue; valueEndMin.value = inputValue;
} }
} }
else if(direction === 'end') { else if(direction === 'end') {
if(isDateType) { if(isDateType) {
this.startMaxDate = e; startMaxDate.value = e;
} else { } else {
this.valueStartMax = inputValue; valueStartMax.value = inputValue;
}; };
} }
// 重新算圖 // 重新算圖
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) this.resizeMask(this.chartComplete); if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chartComplete.value);
else return; else return;
},
},
created() {
this.$emitter.on('map-filter-reset', value => {
if(value) {
this.selectedAttRange = null;
if(this.valueData && this.valueTypes.includes(this.selectedAttName.type)){
const min = this.valueData.min;
const max = this.valueData.max;
this.startTime = new Date(min);
this.endTime = new Date(max);
this.valueStart = min;
this.valueEnd = max;
this.selectArea = [0, this.selectRange];
this.resizeMask(this.chartComplete);
}
}
});
},
mounted() {
// Slider
this.selectArea = [0, this.selectRange]; // 初始化滑塊
},
beforeUnmount() {
this.selectedAttName = {};
}
} }
// created() equivalent
emitter.on('map-filter-reset', value => {
if(value) {
selectedAttRange.value = null;
if(valueData.value && valueTypes.includes(selectedAttName.value.type)){
const min = valueData.value.min;
const max = valueData.value.max;
startTime.value = new Date(min);
endTime.value = new Date(max);
valueStart.value = min;
valueEnd.value = max;
selectArea.value = [0, selectRange.value];
resizeMask(chartComplete.value);
}
}
});
onMounted(() => {
// Slider
selectArea.value = [0, selectRange.value]; // 初始化滑塊
});
onBeforeUnmount(() => {
selectedAttName.value = {};
});
</script> </script>
<style scoped> <style scoped>
@reference "../../../../assets/tailwind.css"; @reference "../../../../assets/tailwind.css";

View File

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

View File

@@ -30,66 +30,62 @@
</section> </section>
</div> </div>
</template> </template>
<script> <script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData'; import { useAllMapDataStore } from '@/stores/allMapData';
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-moment'; import 'chartjs-adapter-moment';
import getMoment from 'moment'; import getMoment from 'moment';
export default{ const props = defineProps(['selectValue']);
props:['selectValue'],
setup() {
const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
return {allMapDataStore, filterTimeframe, selectTimeFrame } const allMapDataStore = useAllMapDataStore();
}, const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
data() {
return { const selectRange = ref(1000); // 更改 select 的切分數
selectRange: 1000, // 更改 select 的切分數 const selectArea = ref(null);
selectArea: null, const chart = ref(null);
chart: null, const canvasId = ref(null);
canvasId: null, const startTime = ref(null);
startTime: null, const endTime = ref(null);
endTime: null, const startMinDate = ref(null);
startMinDate: null, const startMaxDate = ref(null);
startMaxDate: null, const endMinDate = ref(null);
endMinDate: null, const endMaxDate = ref(null);
endMaxDate: null, const panelProps = ref({
panelProps: {
onClick: (event) => { onClick: (event) => {
event.stopPropagation(); event.stopPropagation();
}, },
}, });
}
}, // user select time start and end
computed: { const timeFrameStartEnd = computed(() => {
// user select time start and end const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
timeFrameStartEnd: function() { const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
const start = getMoment(this.startTime).format('YYYY-MM-DDTHH:mm:00'); selectTimeFrame.value = [start, end]; // 傳給後端的資料
const end = getMoment(this.endTime).format('YYYY-MM-DDTHH:mm:00');
this.selectTimeFrame = [start, end]; // 傳給後端的資料
return [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 = []
for (let i = 0; i <= this.selectRange; i++) { // 找出 slidrData時間格式:毫秒時間戳
sliderData.push(xAxisMin + (step * i)); 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 = []
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
} }
return sliderData; return data;
}, });
// 加入最大、最小值
timeFrameData: function(){ // 加入最大、最小值
const data = this.filterTimeframe.data.map(i=>({x:i.x,y:i.y})) const timeFrameData = computed(() => {
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖 // y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
// x 值為 0 ~ 11, // x 值為 0 ~ 11,
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f) // 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
@@ -100,18 +96,18 @@ export default{
const a = 0; const a = 0;
let b; let b;
const c = 1; const c = 1;
const d = this.filterTimeframe.data[0].y; const d = filterTimeframe.value.data[0].y;
const e = 2; const e = 2;
const f = this.filterTimeframe.data[1].y; const f = filterTimeframe.value.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a); b = (e*d - a*d - f*a - f*c) / (e - c - a);
if(b < 0) { if(b < 0) {
b = 0; b = 0;
} }
// y 軸最大值 // y 軸最大值
const ma = 9; const ma = 9;
const mb = this.filterTimeframe.data[8].y; const mb = filterTimeframe.value.data[8].y;
const mc = 10; const mc = 10;
const md = this.filterTimeframe.data[9].y; const md = filterTimeframe.value.data[9].y;
const me = 11; const me = 11;
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc); let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
if(mf < 0) { if(mf < 0) {
@@ -120,20 +116,21 @@ export default{
// 添加最小值 // 添加最小值
data.unshift({ data.unshift({
x: this.filterTimeframe.x_axis.min_base, x: filterTimeframe.value.x_axis.min_base,
y: b, y: b,
}) })
// 添加最大值 // 添加最大值
data.push({ data.push({
x: this.filterTimeframe.x_axis.max_base, x: filterTimeframe.value.x_axis.max_base,
y: mf, y: mf,
}) })
return data; return data;
}, });
labelsData: function() {
const min = new Date(this.filterTimeframe.x_axis.min_base).getTime(); const labelsData = computed(() => {
const max = new Date(this.filterTimeframe.x_axis.max_base).getTime(); 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 numPoints = 11;
const step = (max - min) / (numPoints - 1); const step = (max - min) / (numPoints - 1);
const data = []; const data = [];
@@ -142,69 +139,70 @@ export default{
data.push(x); data.push(x);
} }
return data; return data;
}, });
},
watch:{ watch(selectTimeFrame, (newValue, oldValue) => {
selectTimeFrame(newValue, oldValue) {
if(newValue.length === 0) { if(newValue.length === 0) {
this.startTime = new Date(this.filterTimeframe.x_axis.min); startTime.value = new Date(filterTimeframe.value.x_axis.min);
this.endTime = new Date(this.filterTimeframe.x_axis.max); endTime.value = new Date(filterTimeframe.value.x_axis.max);
this.selectArea = [0, this.selectRange]; selectArea.value = [0, selectRange.value];
this.resizeMask(this.chart); resizeMask(chart.value);
} }
}, });
},
methods: { /**
/**
* 調整遮罩大小 * 調整遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chartInstance 取得 chart.js 資料
*/ */
resizeMask(chart) { function resizeMask(chartInstance) {
const from = (this.selectArea[0] * 0.01) / (this.selectRange * 0.01); const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (this.selectArea[1] * 0.01) / (this.selectRange * 0.01); const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
if(this.selectValue[0] === 'Timeframes') { if(props.selectValue[0] === 'Timeframes') {
this.resizeLeftMask(chart, from); resizeLeftMask(chartInstance, from);
this.resizeRightMask(chart, to); resizeRightMask(chartInstance, to);
} }
}, }
/**
/**
* 調整左邊遮罩大小 * 調整左邊遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chartInstance 取得 chart.js 資料
*/ */
resizeLeftMask(chart, from) { function resizeLeftMask(chartInstance, from) {
const canvas = document.getElementById("chartCanvasId"); const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-left"); const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`; mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`; mask.style.width = `${chartInstance.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`; mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`; mask.style.height = `${chartInstance.chartArea.height}px`;
}, }
/**
/**
* 調整右邊遮罩大小 * 調整右邊遮罩大小
* @param {object} chart 取得 chart.js 資料 * @param {object} chartInstance 取得 chart.js 資料
*/ */
resizeRightMask(chart, to) { function resizeRightMask(chartInstance, to) {
const canvas = document.getElementById("chartCanvasId"); const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-right"); const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`; mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left + chartInstance.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`; mask.style.width = `${chartInstance.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`; mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`; mask.style.height = `${chartInstance.chartArea.height}px`;
}, }
/**
/**
* create chart * create chart
*/ */
createChart() { function createChart() {
const max = this.filterTimeframe.y_axis.max * 1.1; const max = filterTimeframe.value.y_axis.max * 1.1;
const minX = this.timeFrameData[0]?.x; const minX = timeFrameData.value[0]?.x;
const maxX = this.timeFrameData[this.timeFrameData.length - 1]?.x; const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
const data = { const data = {
labels:this.labelsData, labels:labelsData.value,
datasets: [ datasets: [
{ {
label: 'Case', label: 'Case',
data: this.timeFrameData, data: timeFrameData.value,
fill: 'start', fill: 'start',
showLine: false, showLine: false,
tension: 0.4, tension: 0.4,
@@ -235,7 +233,7 @@ export default{
// animations: false, // 取消動畫 // animations: false, // 取消動畫
animation: { animation: {
onComplete: e => { onComplete: e => {
this.resizeMask(e.chart); resizeMask(e.chart);
} }
}, },
interaction: { interaction: {
@@ -286,71 +284,72 @@ export default{
data: data, data: data,
options: options, options: options,
}; };
this.canvasId = document.getElementById("chartCanvasId"); canvasId.value = document.getElementById("chartCanvasId");
this.chart = new Chart(this.canvasId, config); chart.value = new Chart(canvasId.value, config);
}, }
/**
/**
* 滑塊改變的時候 * 滑塊改變的時候
* @param {array} e [1, 100] * @param {array} e [1, 100]
*/ */
changeSelectArea(e) { function changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變 // 日曆改變時,滑塊跟著改變
const sliderData = this.sliderData; const sliderDataVal = sliderData.value;
const start = sliderData[e[0].toFixed()]; const start = sliderDataVal[e[0].toFixed()];
const end = sliderData[e[1].toFixed()]; // 取得 index須為整數。 const end = sliderDataVal[e[1].toFixed()]; // 取得 index須為整數。
this.startTime = new Date(start); startTime.value = new Date(start);
this.endTime = new Date(end); endTime.value = new Date(end);
// 重新設定 start end 日曆選取範圍 // 重新設定 start end 日曆選取範圍
this.endMinDate = new Date(start); endMinDate.value = new Date(start);
this.startMaxDate = new Date(end); startMaxDate.value = new Date(end);
// 重新算圖 // 重新算圖
this.resizeMask(this.chart); resizeMask(chart.value);
// 執行 timeFrameStartEnd 才會改變數據 // 執行 timeFrameStartEnd 才會改變數據
this.timeFrameStartEnd(); timeFrameStartEnd.value;
}, }
/**
/**
* 選取開始或結束時間時,要改變滑塊跟圖表 * 選取開始或結束時間時,要改變滑塊跟圖表
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間) * @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
* @param {string} direction start or end * @param {string} direction start or end
*/ */
sliderTimeRange(e, direction) { function sliderTimeRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳 // 找到最鄰近的 index時間格式: 毫秒時間戳
const sliderData = this.sliderData; const sliderDataVal = sliderData.value;
const targetTime = [new Date(this.timeFrameStartEnd[0]).getTime(), new Date(this.timeFrameStartEnd[1]).getTime()]; const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
const closestIndexes = targetTime.map(target => { const closestIndexes = targetTime.map(target => {
let closestIndex = 0; let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length; closestIndex = ((target - sliderDataVal[0])/(sliderDataVal[sliderDataVal.length-1]-sliderDataVal[0])) * sliderDataVal.length;
let result = Math.round(Math.abs(closestIndex)); let result = Math.round(Math.abs(closestIndex));
result = result > this.selectRange ? this.selectRange : result; result = result > selectRange.value ? selectRange.value : result;
return result return result
}); });
// 改變滑塊 // 改變滑塊
this.selectArea = closestIndexes; selectArea.value = closestIndexes;
// 重新設定 start end 日曆選取範圍 // 重新設定 start end 日曆選取範圍
if(direction === 'start') this.endMinDate = e; if(direction === 'start') endMinDate.value = e;
else if(direction === 'end') this.startMaxDate = e; else if(direction === 'end') startMaxDate.value = e;
// 重新算圖 // 重新算圖
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) this.resizeMask(this.chart); if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
else return; else return;
}, }
},
mounted() { onMounted(() => {
// Chart.js // Chart.js
Chart.register(...registerables); Chart.register(...registerables);
this.createChart(); createChart();
// Slider // Slider
this.selectArea = [0, this.selectRange]; selectArea.value = [0, selectRange.value];
// Calendar // Calendar
this.startMinDate = new Date(this.filterTimeframe.x_axis.min); startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
this.startMaxDate = new Date(this.filterTimeframe.x_axis.max); startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
this.endMinDate = new Date(this.filterTimeframe.x_axis.min); endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
this.endMaxDate = new Date(this.filterTimeframe.x_axis.max); endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
// 讓日曆的範圍等於時間軸的範圍 // 讓日曆的範圍等於時間軸的範圍
this.startTime = this.startMinDate; startTime.value = startMinDate.value;
this.endTime = this.startMaxDate; endTime.value = startMaxDate.value;
this.timeFrameStartEnd(); timeFrameStartEnd.value;
}, });
}
</script> </script>

View File

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

View File

@@ -79,7 +79,7 @@
<!-- title: Attributes --> <!-- title: Attributes -->
<Attributes v-if="selectValue[0] === 'Attributes'" @select-attribute="getSelectAttribute"></Attributes> <Attributes v-if="selectValue[0] === 'Attributes'" @select-attribute="getSelectAttribute"></Attributes>
<!-- title: Trace --> <!-- title: Trace -->
<Trace v-if="selectValue[0] === 'Trace'" ref="filterTraceView" @filter-trace-selectArea="selectTraceArea = $event"></Trace> <Trace v-if="selectValue[0] === 'Trace'" ref="filterTraceViewRef" @filter-trace-selectArea="selectTraceArea = $event"></Trace>
<!-- title: Timeframes --> <!-- title: Timeframes -->
<Timeframes v-if="selectValue[0] === 'Timeframes'" :selectValue="selectValue"></Timeframes> <Timeframes v-if="selectValue[0] === 'Timeframes'" :selectValue="selectValue"></Timeframes>
</div> </div>
@@ -87,7 +87,7 @@
<!-- Button --> <!-- Button -->
<div class="float-right space-x-4 px-4 py-2"> <div class="float-right space-x-4 px-4 py-2">
<button type="button" class="btn btn-sm btn-neutral" @click="reset">Clear</button> <button type="button" class="btn btn-sm btn-neutral" @click="reset">Clear</button>
<button type="button" class="btn btn-sm" @click="submit" :disabled="isDisabledButton" :class="this.isDisabled ? 'btn-disable' : 'btn-neutral'">Apply</button> <button type="button" class="btn btn-sm" @click="submit" :disabled="isDisabledButton" :class="isDisabled ? 'btn-disable' : 'btn-neutral'">Apply</button>
</div> </div>
</div> </div>
</div> </div>
@@ -96,8 +96,10 @@
</Sidebar> </Sidebar>
</template> </template>
<script> <script setup>
import { ref, reactive, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useToast } from 'vue-toast-notification';
import { useLoadingStore } from '@/stores/loading'; import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData'; import { useAllMapDataStore } from '@/stores/allMapData';
import ActOccCase from '@/components/Discover/Map/Filter/ActOccCase.vue'; import ActOccCase from '@/components/Discover/Map/Filter/ActOccCase.vue';
@@ -107,21 +109,19 @@ import Attributes from '@/components/Discover/Map/Filter/Attributes.vue';
import Funnel from '@/components/Discover/Map/Filter/Funnel.vue'; import Funnel from '@/components/Discover/Map/Filter/Funnel.vue';
import Trace from '@/components/Discover/Map/Filter/Trace.vue'; import Trace from '@/components/Discover/Map/Filter/Trace.vue';
import Timeframes from '@/components/Discover/Map/Filter/Timeframes.vue'; import Timeframes from '@/components/Discover/Map/Filter/Timeframes.vue';
import emitter from '@/utils/emitter';
import getMoment from 'moment'; import getMoment from 'moment';
export default { const props = defineProps(['sidebarFilter', 'filterTasks', 'filterStartToEnd', 'filterEndToStart', 'filterTimeframe', 'filterTrace']);
props: ['sidebarFilter', 'filterTasks', 'filterStartToEnd', 'filterEndToStart', 'filterTimeframe', 'filterTrace'], const emit = defineEmits(['submit-all']);
setup() { const $toast = useToast();
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, selectTimeFrame } = storeToRefs(allMapDataStore);
return { isLoading, hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, allMapDataStore, selectTimeFrame } const loadingStore = useLoadingStore();
}, const allMapDataStore = useAllMapDataStore();
data() { const { isLoading } = storeToRefs(loadingStore);
return { const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, selectTimeFrame } = storeToRefs(allMapDataStore);
selectFilter: {
const selectFilter = {
'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'], 'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'],
// 'Filter Type': ['Sequence', 'Trace', 'Timeframes'], // 'Filter Type': ['Sequence', 'Trace', 'Timeframes'],
'Activity Sequence':['Have activity(s)', 'Start & End', 'Sequence'], 'Activity Sequence':['Have activity(s)', 'Start & End', 'Sequence'],
@@ -130,9 +130,9 @@ export default {
'ModeAtt': ['Case', 'Activity'], 'ModeAtt': ['Case', 'Activity'],
'Refine': ['Include', 'Exclude'], 'Refine': ['Include', 'Exclude'],
'Containment': ['Contained in', 'Started in', 'Ended in', 'Active in'], 'Containment': ['Contained in', 'Started in', 'Ended in', 'Active in'],
}, };
tab: 'filter', // filter | funnel const tab = ref('filter'); // filter | funnel
selectValue: { const selectValue = reactive({
0: 'Sequence', 0: 'Sequence',
1: 'Have activity(s)', 1: 'Have activity(s)',
2: 'Start', 2: 'Start',
@@ -140,23 +140,24 @@ export default {
4: 'Case', 4: 'Case',
5: 'Include', 5: 'Include',
6: 'Contained in', 6: 'Contained in',
}, });
selectFilterTask: null, const selectFilterTask = ref(null);
selectAttType: '', const selectAttType = ref('');
selectAttribute: null, const selectAttribute = ref(null);
selectFilterStart: null, const selectFilterStart = ref(null);
selectFilterEnd: null, const selectFilterEnd = ref(null);
selectFilterStartToEnd: null, const selectFilterStartToEnd = ref(null);
selectFilterEndToStart: null, const selectFilterEndToStart = ref(null);
listSeq: [], const listSeq = ref([]);
//若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變 //若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
isStartSelected: null, const isStartSelected = ref(null);
isEndSelected: null, const isEndSelected = ref(null);
isActAllTask: true, const isActAllTask = ref(true);
rowData: [], const rowData = ref([]);
selectTraceArea: [], // Trace 滑快 const selectTraceArea = ref([]); // Trace 滑快
isDisabled: true, // Apply Button disabled setting const isDisabled = ref(true); // Apply Button disabled setting
tooltip: { const filterTraceViewRef = ref(null);
const tooltip = {
containment: { containment: {
value: '<img src="/filterContainment.png" alt="filterContainment" class="block" width="108" height="250">', value: '<img src="/filterContainment.png" alt="filterContainment" class="block" width="108" height="250">',
escape: false, escape: false,
@@ -172,127 +173,129 @@ export default {
class: 'p-0 bg-transparent' class: 'p-0 bg-transparent'
} }
} }
}
}, },
} };
},
components: { // All Task
ActOccCase, const filterAllTaskData = computed(() => {
ActOcc, return setHaveAct([...props.filterTasks]);
ActAndSeq, });
Attributes,
Funnel, // Act And Seq
Trace, const filterTaskData = computed(() => {
Timeframes, return isActAllTask.value? setHaveAct([...props.filterTasks]) : filterTaskData.value;
}, });
computed: {
// All Task // Start and End Task
filterAllTaskData: function() { const filterStartData = computed(() => {
return this.setHaveAct([...this.filterTasks]); return setActData(props.filterStartToEnd);
}, });
// Act And Seq
filterTaskData: function() { const filterEndData = computed(() => {
return this.isActAllTask? this.setHaveAct([...this.filterTasks]) : this.filterTaskData; return setActData(props.filterEndToStart);
}, });
// Start and End Task
filterStartData: function() { const filterStartToEndData = computed(() => {
return this.setActData(this.filterStartToEnd); return isEndSelected.value ? setStartAndEndData(props.filterEndToStart, rowData.value, 'sources') : setActData(props.filterStartToEnd);
}, });
filterEndData: function() {
return this.setActData(this.filterEndToStart); const filterEndToStartData = computed(() => {
}, return isStartSelected.value ? setStartAndEndData(props.filterStartToEnd, rowData.value, 'sinks') : setActData(props.filterEndToStart);
filterStartToEndData: function() { });
return this.isEndSelected ? this.setStartAndEndData(this.filterEndToStart, this.rowData, 'sources') : this.setActData(this.filterStartToEnd);
}, // Apply Button disabled setting
filterEndToStartData: function() { const isDisabledButton = computed(() => {
return this.isStartSelected ? this.setStartAndEndData(this.filterStartToEnd, this.rowData, 'sinks') : this.setActData(this.filterEndToStart);
},
// Apply Button disabled setting
isDisabledButton: function() {
let disabled = true; let disabled = true;
const { selectValue: sele, selectAttType: type } = this; const sele = selectValue;
const type = selectAttType.value;
const firstSelection = sele[0]; const firstSelection = sele[0];
if (firstSelection === 'Sequence') { if (firstSelection === 'Sequence') {
disabled = this.handleSequenceSelection(sele); disabled = handleSequenceSelection(sele);
} else if (firstSelection === 'Attributes') { } else if (firstSelection === 'Attributes') {
disabled = this.handleAttributesSelection(type); disabled = handleAttributesSelection(type);
} else if (firstSelection === 'Trace') { } else if (firstSelection === 'Trace') {
disabled = this.handleTraceSelection(); disabled = handleTraceSelection();
} else if (firstSelection === 'Timeframes') { } else if (firstSelection === 'Timeframes') {
disabled = this.handleTimeframesSelection(); disabled = handleTimeframesSelection();
} }
this.isDisabled = disabled; isDisabled.value = disabled;
return disabled; return disabled;
}, });
},
methods: { /**
/**
* Change Radio Filter Type * Change Radio Filter Type
*/ */
radioFilterType() { function radioFilterType() {
this.reset(); reset();
}, }
/**
/**
* Change Radio Act Seq * Change Radio Act Seq
*/ */
radioActSeq() { function radioActSeq() {
this.reset(); reset();
}, }
/**
/**
* Change Radio Start And End * Change Radio Start And End
*/ */
radioStartAndEnd() { function radioStartAndEnd() {
this.reset(); reset();
}, }
/**
/**
* @param {string} switch Summary or Insight * @param {string} switch Summary or Insight
*/ */
switchTab(tab) { function switchTab(newTab) {
this.tab = tab; tab.value = newTab;
}, }
/**
/**
* Number to percentage * Number to percentage
* @param {number} val 原始數字 * @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串 * @returns {string} 轉換完成的百分比字串
*/ */
getPercentLabel(val){ function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`; if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`; else return `${(val * 100).toFixed(1)}%`;
}, }
/**
/**
* set progress bar width * set progress bar width
* @param {number} value 百分比數字 * @param {number} value 百分比數字
* @returns {string} 樣式的寬度設定 * @returns {string} 樣式的寬度設定
*/ */
progressWidth(value){ function progressWidth(value){
return `width:${value}%;` return `width:${value}%;`
}, }
//設定 Have activity(s) 內容
/** //設定 Have activity(s) 內容
/**
* @param {array} data filterTaskData * @param {array} data filterTaskData
*/ */
setHaveAct(data){ function setHaveAct(data){
return data.map(task => { return data.map(task => {
return { return {
label: task.label, label: task.label,
occ_value: Number(task.occurrence_ratio * 100), occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'), occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrences_base: task.occurrences, occurrences_base: task.occurrences,
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio), occurrence_ratio: getPercentLabel(task.occurrence_ratio),
case_value: Number(task.case_ratio * 100), case_value: Number(task.case_ratio * 100),
cases: task.cases.toLocaleString('en-US'), cases: task.cases.toLocaleString('en-US'),
cases_base: task.cases, cases_base: task.cases,
case_ratio: this.getPercentLabel(task.case_ratio), case_ratio: getPercentLabel(task.case_ratio),
}; };
}).sort((x, y) => y.occurrences_base - x.occurrences_base); }).sort((x, y) => y.occurrences_base - x.occurrences_base);
}, }
// 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容
/** // 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容
/**
* @param {array} array filterStartToEnd / filterEndToStart可傳入以上任一。 * @param {array} array filterStartToEnd / filterEndToStart可傳入以上任一。
*/ */
setActData(array) { function setActData(array) {
const list = []; const list = [];
array.forEach((task, index) => { array.forEach((task, index) => {
const data = { const data = {
@@ -300,78 +303,86 @@ export default {
occ_value: Number(task.occurrence_ratio * 100), occ_value: Number(task.occurrence_ratio * 100),
occurrences: task.occurrences.toLocaleString('en-US'), occurrences: task.occurrences.toLocaleString('en-US'),
occurrences_base: task.occurrences, occurrences_base: task.occurrences,
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio), occurrence_ratio: getPercentLabel(task.occurrence_ratio),
}; };
list.push(data); list.push(data);
}); });
list.sort((x, y) => y.occurrences_base - x.occurrences_base) list.sort((x, y) => y.occurrences_base - x.occurrences_base)
return list; return list;
}, }
/**
/**
* @param {array} e select Attribute * @param {array} e select Attribute
*/ */
getSelectAttribute(e){ function getSelectAttribute(e){
this.selectAttType = e.type; selectAttType.value = e.type;
this.selectAttribute = e.data; selectAttribute.value = e.data;
}, }
/**
/**
* @param {array} select select Have activity(s) rows * @param {array} select select Have activity(s) rows
*/ */
onRowAct(select){ function onRowAct(select){
this.selectFilterTask = select; selectFilterTask.value = select;
}, }
/**
/**
* @param {object} e select Start rows * @param {object} e select Start rows
*/ */
onRowStart(e){ function onRowStart(e){
this.selectFilterStart = e.data; selectFilterStart.value = e.data;
}, }
/**
/**
* @param {object} e select End rows * @param {object} e select End rows
*/ */
onRowEnd(e){ function onRowEnd(e){
this.selectFilterEnd = e.data; selectFilterEnd.value = e.data;
}, }
/**
* @param {array} listSeq Update List Seq /**
* @param {array} newListSeq Update List Seq
*/ */
onUpdateListSeq(listSeq) { function onUpdateListSeq(newListSeq) {
this.listSeq = listSeq; listSeq.value = newListSeq;
this.isActAllTask = false; isActAllTask.value = false;
}, }
// 在 Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
/** // 在 Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
/**
* @param {object} e object contains selected row's data * @param {object} e object contains selected row's data
*/ */
startRow(e){ function startRow(e){
this.selectFilterStartToEnd = e.data; selectFilterStartToEnd.value = e.data;
if(this.isStartSelected === null || this.isStartSelected === true){ if(isStartSelected.value === null || isStartSelected.value === true){
this.isStartSelected = true; isStartSelected.value = true;
this.isEndSelected = false; isEndSelected.value = false;
this.rowData = e.data; rowData.value = e.data;
} }
}, }
/**
/**
* @param {object} e object contains selected row's data * @param {object} e object contains selected row's data
*/ */
endRow(e) { function endRow(e) {
this.selectFilterEndToStart = e.data; selectFilterEndToStart.value = e.data;
if(this.isEndSelected === null || this.isEndSelected === true){ if(isEndSelected.value === null || isEndSelected.value === true){
this.isEndSelected = true; isEndSelected.value = true;
this.isStartSelected = false; isStartSelected.value = false;
this.rowData = e.data; rowData.value = e.data;
} }
}, }
// 重新設定連動的 filterStartToEndData / filterEndToStartData 內容
/** // 重新設定連動的 filterStartToEndData / filterEndToStartData 內容
/**
* @param {array} eventData Start or End List * @param {array} eventData Start or End List
* @param {object} rowData 所選擇的 row's data * @param {object} rowDataVal 所選擇的 row's data
* @param {string} event sinks / sources * @param {string} event sinks / sources
*/ */
setStartAndEndData(eventData, rowData, event){ function setStartAndEndData(eventData, rowDataVal, event){
const filterData = event === 'sinks' ? this.filterEndToStart : this.filterStartToEnd; const filterData = event === 'sinks' ? props.filterEndToStart : props.filterStartToEnd;
const relatedItems = eventData const relatedItems = eventData
.find(task => task.label === rowData.label) .find(task => task.label === rowDataVal.label)
?.[event]?.filter(item => filterData.some(ele => ele.label === item)) ?.[event]?.filter(item => filterData.some(ele => ele.label === item))
?.map(item => filterData.find(ele => ele.label === item)); ?.map(item => filterData.find(ele => ele.label === item));
@@ -380,13 +391,14 @@ export default {
label: item.label, label: item.label,
occ_value: Number(item.occurrence_ratio * 100), occ_value: Number(item.occurrence_ratio * 100),
occurrences: Number(item.occurrences).toLocaleString('en-US'), occurrences: Number(item.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(item.occurrence_ratio), occurrence_ratio: getPercentLabel(item.occurrence_ratio),
})); }));
}, }
/**
/**
* @param {object} e task's object * @param {object} e task's object
*/ */
setRule(e) { function setRule(e) {
let label, type; let label, type;
const includeStr = e.is_exclude? "exclude" : "include"; const includeStr = e.is_exclude? "exclude" : "include";
const containmentMap = { const containmentMap = {
@@ -402,7 +414,7 @@ export default {
case "ends-with": case "ends-with":
case "directly-follows": case "directly-follows":
case "eventually-follows": case "eventually-follows":
label = `${includeStr}, ${this.getTaskLabel(e)}`; label = `${includeStr}, ${getTaskLabel(e)}`;
type = "Sequence"; type = "Sequence";
break; break;
case "start-end": case "start-end":
@@ -434,44 +446,46 @@ export default {
label, label,
toggle: true, toggle: true,
}; };
}, }
/**
* @param {boolean} massage true | false 清空選項,可選以上任一。 /**
* @param {boolean} message true | false 清空選項,可選以上任一。
*/ */
reset(massage) { function reset(message) {
// Sequence // Sequence
this.selectFilterTask = null; selectFilterTask.value = null;
this.selectFilterStart = null; selectFilterStart.value = null;
this.selectFilterEnd = null; selectFilterEnd.value = null;
this.selectFilterStartToEnd = null; selectFilterStartToEnd.value = null;
this.selectFilterEndToStart = null; selectFilterEndToStart.value = null;
this.listSeq = []; listSeq.value = [];
this.isStartSelected = null; isStartSelected.value = null;
this.isEndSelected = null; isEndSelected.value = null;
this.isActAllTask = true; isActAllTask.value = true;
// Attributes // Attributes
this.selectAttType = ''; selectAttType.value = '';
this.selectAttribute = null; selectAttribute.value = null;
this.$emitter.emit('map-filter-reset', true); emitter.emit('map-filter-reset', true);
// Timeframes // Timeframes
this.selectTimeFrame = []; selectTimeFrame.value = [];
// Trace // Trace
if (this.$refs.filterTraceView) { if (filterTraceViewRef.value) {
this.$refs.filterTraceView.showTraceId = null; filterTraceViewRef.value.showTraceId = null;
this.$refs.filterTraceView.selectArea = [0, this.$refs.filterTraceView.traceTotal]; filterTraceViewRef.value.selectArea = [0, filterTraceViewRef.value.traceTotal];
}; };
// 成功訊息 // 成功訊息
if(message) { if(message) {
this.$toast.success('Filter cleared.') $toast.success('Filter cleared.')
} }
}, }
/**
/**
* header:Filter 發送選取的資料 * header:Filter 發送選取的資料
*/ */
async submit() { async function submit() {
this.isLoading = true; isLoading.value = true;
const sele = this.selectValue; const sele = selectValue;
const isExclude = sele[5] === 'Exclude'; const isExclude = sele[5] === 'Exclude';
const containmentMap = { const containmentMap = {
'Contained in': 'occurred-in', 'Contained in': 'occurred-in',
@@ -480,81 +494,88 @@ export default {
'Active in': 'occurred-around' 'Active in': 'occurred-around'
}; };
const data = this.getFilterData(sele, isExclude, containmentMap); const data = getFilterData(sele, isExclude, containmentMap);
const postData = Array.isArray(data) ? data : [data]; const postData = Array.isArray(data) ? data : [data];
this.postRuleData = postData; postRuleData.value = postData;
await this.allMapDataStore.checkHasResult(); await allMapDataStore.checkHasResult();
if (this.hasResultRule === null) { if (hasResultRule.value === null) {
this.isLoading = false; isLoading.value = false;
} else if (this.hasResultRule) { } else if (hasResultRule.value) {
this.updateRules(postData); updateRules(postData);
this.reset(false); reset(false);
await this.delay(1000); await delay(1000);
this.isLoading = false; isLoading.value = false;
this.$toast.success('Filter applied. Go to Funnel to verify.'); $toast.success('Filter applied. Go to Funnel to verify.');
} else { } else {
this.reset(false); reset(false);
await this.delay(1000); await delay(1000);
this.isLoading = false; isLoading.value = false;
this.$toast.warning('No result.'); $toast.warning('No result.');
} }
}, }
/**
/**
* create map * create map
*/ */
sumbitAll() { function sumbitAll() {
this.$emit('submit-all'); emit('submit-all');
}, }
handleSequenceSelection(sele) {
function handleSequenceSelection(sele) {
const secondSelection = sele[1]; const secondSelection = sele[1];
switch (secondSelection) { switch (secondSelection) {
case 'Have activity(s)': case 'Have activity(s)':
return !(this.selectFilterTask && this.selectFilterTask.length !== 0); return !(selectFilterTask.value && selectFilterTask.value.length !== 0);
case 'Start & End': case 'Start & End':
return this.handleStartEndSelection(sele[2]); return handleStartEndSelection(sele[2]);
case 'Sequence': case 'Sequence':
return this.listSeq.length < 2; return listSeq.value.length < 2;
default: default:
return true; return true;
} }
}, }
handleStartEndSelection(option) {
function handleStartEndSelection(option) {
switch (option) { switch (option) {
case 'Start': case 'Start':
return !this.selectFilterStart; return !selectFilterStart.value;
case 'End': case 'End':
return !this.selectFilterEnd; return !selectFilterEnd.value;
case 'Start & End': case 'Start & End':
return !(this.selectFilterStartToEnd && this.selectFilterEndToStart); return !(selectFilterStartToEnd.value && selectFilterEndToStart.value);
default: default:
return true; return true;
} }
}, }
handleAttributesSelection(type) {
function handleAttributesSelection(type) {
switch (type) { switch (type) {
case 'string': case 'string':
return !(this.selectAttribute && this.selectAttribute.length > 0); return !(selectAttribute.value && selectAttribute.value.length > 0);
case 'boolean': case 'boolean':
return !(this.selectAttribute?.key && this.selectAttribute?.label); return !(selectAttribute.value?.key && selectAttribute.value?.label);
case 'int': case 'int':
case 'float': case 'float':
case 'date': case 'date':
return !(this.selectAttribute?.key && this.selectAttribute?.min !== null && this.selectAttribute?.max !== null); return !(selectAttribute.value?.key && selectAttribute.value?.min !== null && selectAttribute.value?.max !== null);
default: default:
return true; return true;
} }
}, }
handleTraceSelection() {
return this.selectTraceArea[0] === this.selectTraceArea[1]; function handleTraceSelection() {
}, return selectTraceArea.value[0] === selectTraceArea.value[1];
handleTimeframesSelection() { }
return this.selectTimeFrame.length === 0;
}, function handleTimeframesSelection() {
getTaskLabel(e) { return selectTimeFrame.value.length === 0;
}
function getTaskLabel(e) {
switch (e.type) { switch (e.type) {
case "contains-task": case "contains-task":
return `${e.task}`; return `${e.task}`;
@@ -566,85 +587,90 @@ export default {
case "eventually-follows": case "eventually-follows":
return `${e.type.replace('-', ' ')}, ${e.task_seq.join(' -> ')}`; return `${e.type.replace('-', ' ')}, ${e.task_seq.join(' -> ')}`;
} }
}, }
getAttributeLabel(e) {
function getAttributeLabel(e) {
switch (e.type) { switch (e.type) {
case 'string-attr': case 'string-attr':
return `${e.key}, ${e.value}`; return `${e.key}, ${e.value}`;
case 'boolean-attr': case 'boolean-attr':
return `${e.key}, ${this.selectAttribute?.label}`; return `${e.key}, ${selectAttribute.value?.label}`;
case 'int-attr': case 'int-attr':
case 'float-attr': case 'float-attr':
return `${e.key}, from ${e.min} to ${e.max}`; return `${e.key}, from ${e.min} to ${e.max}`;
case 'date-attr': case 'date-attr':
return `${e.key}, from ${getMoment(e.min).format('YYYY-MM-DD HH:mm')} to ${getMoment(e.max).format('YYYY-MM-DD HH:mm')}`; return `${e.key}, from ${getMoment(e.min).format('YYYY-MM-DD HH:mm')} to ${getMoment(e.max).format('YYYY-MM-DD HH:mm')}`;
} }
}, }
getFilterData(sele, isExclude, containmentMap) {
function getFilterData(sele, isExclude, containmentMap) {
switch (sele[0]) { switch (sele[0]) {
case 'Sequence': case 'Sequence':
return this.getSequenceData(sele, isExclude); return getSequenceData(sele, isExclude);
case 'Attributes': case 'Attributes':
return this.getAttributesData(isExclude); return getAttributesData(isExclude);
case 'Trace': case 'Trace':
return this.getTraceData(isExclude); return getTraceData(isExclude);
case 'Timeframes': case 'Timeframes':
return { return {
type: containmentMap[sele[6]], type: containmentMap[sele[6]],
start: this.selectTimeFrame[0], start: selectTimeFrame.value[0],
end: this.selectTimeFrame[1], end: selectTimeFrame.value[1],
is_exclude: isExclude is_exclude: isExclude
}; };
default: default:
return null; return null;
} }
}, }
getSequenceData(sele, isExclude) {
function getSequenceData(sele, isExclude) {
switch (sele[1]) { switch (sele[1]) {
case 'Have activity(s)': case 'Have activity(s)':
return this.selectFilterTask.map(task => ({ return selectFilterTask.value.map(task => ({
type: 'contains-task', type: 'contains-task',
task: task.label, task: task.label,
is_exclude: isExclude is_exclude: isExclude
})); }));
case 'Start & End': case 'Start & End':
return this.getStartEndData(sele, isExclude); return getStartEndData(sele, isExclude);
case 'Sequence': case 'Sequence':
return { return {
type: sele[3] === 'Directly follows' ? 'directly-follows' : 'eventually-follows', type: sele[3] === 'Directly follows' ? 'directly-follows' : 'eventually-follows',
task_seq: this.listSeq.map(task => task.label), task_seq: listSeq.value.map(task => task.label),
is_exclude: isExclude is_exclude: isExclude
}; };
default: default:
return null; return null;
} }
}, }
getStartEndData(sele, isExclude) {
function getStartEndData(sele, isExclude) {
switch (sele[2]) { switch (sele[2]) {
case 'Start': case 'Start':
return { return {
type: 'starts-with', type: 'starts-with',
task: this.selectFilterStart.label, task: selectFilterStart.value.label,
is_exclude: isExclude is_exclude: isExclude
}; };
case 'End': case 'End':
return { return {
type: 'ends-with', type: 'ends-with',
task: this.selectFilterEnd.label, task: selectFilterEnd.value.label,
is_exclude: isExclude is_exclude: isExclude
}; };
case 'Start & End': case 'Start & End':
return { return {
type: 'start-end', type: 'start-end',
starts_with: this.selectFilterStartToEnd.label, starts_with: selectFilterStartToEnd.value.label,
ends_with: this.selectFilterEndToStart.label, ends_with: selectFilterEndToStart.value.label,
is_exclude: isExclude is_exclude: isExclude
}; };
default: default:
return null; return null;
} }
}, }
getAttributesData(isExclude) {
function getAttributesData(isExclude) {
const attrTypeMap = { const attrTypeMap = {
'string': 'string-attr', 'string': 'string-attr',
'boolean': 'boolean-attr', 'boolean': 'boolean-attr',
@@ -653,9 +679,9 @@ export default {
'date': 'date-attr' 'date': 'date-attr'
}; };
switch (this.selectAttType) { switch (selectAttType.value) {
case 'string': case 'string':
return this.selectAttribute.map(task => ({ return selectAttribute.value.map(task => ({
type: attrTypeMap['string'], type: attrTypeMap['string'],
key: task.key, key: task.key,
value: task.value, value: task.value,
@@ -666,42 +692,43 @@ export default {
case 'float': case 'float':
case 'date': case 'date':
return { return {
type: attrTypeMap[this.selectAttType], type: attrTypeMap[selectAttType.value],
key: this.selectAttribute.key, key: selectAttribute.value.key,
value: this.selectAttribute.value, value: selectAttribute.value.value,
min: this.selectAttribute.min, min: selectAttribute.value.min,
max: this.selectAttribute.max, max: selectAttribute.value.max,
is_exclude: isExclude is_exclude: isExclude
}; };
default: default:
return null; return null;
} }
}, }
getTraceData(isExclude) {
const lowerIndex = this.$refs.filterTraceView.selectArea[0]; function getTraceData(isExclude) {
const upperIndex = this.$refs.filterTraceView.selectArea[1] - 1; const lowerIndex = filterTraceViewRef.value.selectArea[0];
const upperIndex = filterTraceViewRef.value.selectArea[1] - 1;
return { return {
type: 'trace-freq', type: 'trace-freq',
lower: this.allMapDataStore.traces[lowerIndex].id, lower: allMapDataStore.traces[lowerIndex].id,
upper: this.allMapDataStore.traces[upperIndex].id, upper: allMapDataStore.traces[upperIndex].id,
is_exclude: isExclude is_exclude: isExclude
}; };
}, }
updateRules(postData) {
if (!this.temporaryData?.length) { function updateRules(postData) {
this.temporaryData.push(...postData); if (!temporaryData.value?.length) {
this.isRuleData = Array.from(this.temporaryData); temporaryData.value.push(...postData);
this.ruleData = this.isRuleData.map(e => this.setRule(e)); isRuleData.value = Array.from(temporaryData.value);
ruleData.value = isRuleData.value.map(e => setRule(e));
} else { } else {
this.temporaryData.push(...postData); temporaryData.value.push(...postData);
this.isRuleData.push(...postData); isRuleData.value.push(...postData);
this.ruleData.push(...postData.map(e => this.setRule(e))); ruleData.value.push(...postData.map(e => setRule(e)));
} }
}, }
delay(ms) {
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
}
},
} }
</script> </script>

View File

@@ -241,8 +241,8 @@
</Sidebar> </Sidebar>
</template> </template>
<script> <script setup>
import { computed, ref, } from 'vue'; import { computed, ref } from 'vue';
import { usePageAdminStore } from '@/stores/pageAdmin'; import { usePageAdminStore } from '@/stores/pageAdmin';
import { useMapPathStore } from '@/stores/mapPathStore'; import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js'; import { getTimeLabel } from '@/module/timeLabel.js';
@@ -252,8 +252,8 @@ import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants';
// 刪除第一個和第二個元素 // 刪除第一個和第二個元素
const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2); const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2);
export default {
props:{ const props = defineProps({
sidebarState: { sidebarState: {
type: Boolean, type: Boolean,
require: false, require: false,
@@ -266,106 +266,92 @@ export default {
type: Object, type: Object,
required: false, required: false,
} }
}, });
setup(props){
const pageAdmin = usePageAdminStore();
const mapPathStore = useMapPathStore();
const activeTrace = ref(0); const pageAdmin = usePageAdminStore();
const currentMapFile = computed(() => pageAdmin.currentMapFile); const mapPathStore = useMapPathStore();
const clickedPathListIndex = ref(0);
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
const onActiveTraceClick = (clickedActiveTraceIndex) => { 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(); mapPathStore.clearAllHighlight();
activeTrace.value = clickedActiveTraceIndex; activeTrace.value = clickedActiveTraceIndex;
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value); mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
} }
const onPathOptionClick = (clickedPath) => { function onPathOptionClick(clickedPath) {
clickedPathListIndex.value = clickedPath; clickedPathListIndex.value = clickedPath;
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath); mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
}; }
const onResetTraceBtnClick = () => { function onResetTraceBtnClick() {
if(isBPMNOn.value) { if(isBPMNOn.value) {
return; return;
} }
clickedPathListIndex.value = undefined; clickedPathListIndex.value = undefined;
} }
return { /**
currentMapFile, * @param {string} newTab Summary or Insight
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) { function switchTab(newTab) {
this.tab = tab; tab.value = newTab;
}, }
/**
/**
* @param {number} time use timeLabel.js * @param {number} time use timeLabel.js
*/ */
timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ? function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
// //
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白 const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
return result; return result;
}, }
/**
/**
* @param {number} time use moment * @param {number} time use moment
*/ */
moment(time){ function moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm'); return getMoment(time).format('YYYY-MM-DD HH:mm');
}, }
/**
/**
* Number to percentage * Number to percentage
* @param {number} val 原始數字 * @param {number} val 原始數字
* @returns {string} 轉換完成的百分比字串 * @returns {string} 轉換完成的百分比字串
*/ */
getPercentLabel(val){ function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`; if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`; else return `${(val * 100).toFixed(1)}%`;
}, }
/**
/**
* Behavior when show * Behavior when show
*/ */
show(){ function show(){
this.valueCases = this.stats.cases.ratio * 100; valueCases.value = props.stats.cases.ratio * 100;
this.valueTraces= this.stats.traces.ratio * 100; valueTraces.value = props.stats.traces.ratio * 100;
this.valueTaskInstances = this.stats.task_instances.ratio * 100; valueTaskInstances.value = props.stats.task_instances.ratio * 100;
this.valueTasks = this.stats.tasks.ratio * 100; valueTasks.value = props.stats.tasks.ratio * 100;
}, }
/**
/**
* Behavior when hidden * Behavior when hidden
*/ */
hide(){ function hide(){
this.valueCases = 0; valueCases.value = 0;
this.valueTraces= 0; valueTraces.value = 0;
this.valueTaskInstances = 0; valueTaskInstances.value = 0;
this.valueTasks = 0; valueTasks.value = 0;
},
},
} }
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="$emit('closeModal', $event)"> <Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="emit('closeModal', $event)">
<template #header> <template #header>
<div class="py-5"> <div class="py-5">
</div> </div>
@@ -14,34 +14,26 @@
</label> </label>
</Dialog> </Dialog>
</template> </template>
<script> <script setup>
import IconUploarding from '../icons/IconUploarding.vue'; import { onBeforeUnmount, } from 'vue';
import { uploadFailedFirst } from '@/module/alertModal.js'
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import IconUploarding from '../icons/IconUploarding.vue';
import { uploadFailedFirst } from '@/module/alertModal.js';
import { useFilesStore } from '@/stores/files'; import { useFilesStore } from '@/stores/files';
export default { defineProps(['uploadModal']);
props: ['uploadModal'], const emit = defineEmits(['closeModal']);
setup() {
const filesStore = useFilesStore();
const { uploadFileName } = storeToRefs(filesStore);
return { filesStore, uploadFileName } const filesStore = useFilesStore();
}, const { uploadFileName } = storeToRefs(filesStore);
data() {
return { const contentClass = 'h-full';
contentClass: 'h-full',
} /**
},
components: {
IconUploarding,
},
methods: {
/**
* 上傳的行為 * 上傳的行為
* @param {event} event input 傳入的事件 * @param {event} event input 傳入的事件
*/ */
async upload(event) { async function upload(event) {
const fileInput = document.getElementById('uploadFiles'); const fileInput = document.getElementById('uploadFiles');
const target = event.target; const target = event.target;
const formData = new FormData(); const formData = new FormData();
@@ -60,24 +52,23 @@ export default {
formData.append('csv', uploadFile); formData.append('csv', uploadFile);
// 呼叫第一階段上傳 API // 呼叫第一階段上傳 API
if(uploadFile) { if(uploadFile) {
await this.filesStore.upload(formData); await filesStore.upload(formData);
} }
if (uploadFile.name.endsWith('.csv')) { if (uploadFile.name.endsWith('.csv')) {
this.uploadFileName = uploadFile.name.slice(0, -4); uploadFileName.value = uploadFile.name.slice(0, -4);
} else { } else {
// 處理錯誤或無效的文件格式 // 處理錯誤或無效的文件格式
this.uploadFileName = ''; // 或者其他適合的錯誤處理方式 uploadFileName.value = ''; // 或者其他適合的錯誤處理方式
} }
// 清除選擇文件 // 清除選擇文件
if(fileInput) { if(fileInput) {
fileInput.value = ''; fileInput.value = '';
} }
}
},
beforeUnmount() {
this.$emit('closeModal', false);
}
} }
onBeforeUnmount(() => {
emit('closeModal', false);
});
</script> </script>
<style scoped> <style scoped>
.loader-arrow-upward { .loader-arrow-upward {

View File

@@ -19,10 +19,11 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, } from 'vue'; import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs, } from 'pinia'; import { storeToRefs, } from 'pinia';
import i18next from '@/i18n/i18n'; import emitter from '@/utils/emitter';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
import DspLogo from '@/components/icons/DspLogo.vue'; import DspLogo from '@/components/icons/DspLogo.vue';
@@ -30,64 +31,44 @@ import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance'; import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js'; import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
export default { const route = useRoute();
data() {
return {
showMember: false,
i18next: i18next,
}
},
setup() {
const store = useLoginStore();
const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
const isHeadHovered = ref(false);
const toggleIsAcctMenuOpen = () => { const store = useLoginStore();
const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
const isHeadHovered = ref(false);
const showMember = ref(false);
const toggleIsAcctMenuOpen = () => {
acctMgmtStore.toggleIsAcctMenuOpen(); acctMgmtStore.toggleIsAcctMenuOpen();
} };
return { logOut, temporaryData, tempFilterId, /**
postRuleData, ruleData,
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
allMapDataStore, conformanceStore,
conformanceFileName,
toggleIsAcctMenuOpen,
isHeadHovered,
};
},
components: {
DspLogo,
},
methods: {
/**
* 登出的行為 * 登出的行為
*/ */
logOutButton() { function logOutButton() {
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) { if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
// 傳給 Map通知 Sidebar 要關閉。 // 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('leaveFilter', false); emitter.emit('leaveFilter', false);
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut) leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance') } else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) { && (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut) leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
} else { } else {
this.logOut(); logOut();
}
},
},
mounted() {
if (this.$route.name === 'Login' || this.$route.name === 'NotFound404') {
this.showMember = false
} else {
this.showMember = true;
}
} }
} }
onMounted(() => {
if (route.name === 'Login' || route.name === 'NotFound404') {
showMember.value = false
} else {
showMember.value = true;
}
});
</script> </script>

View File

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

View File

@@ -16,14 +16,7 @@
</form> </form>
</template> </template>
<script> <script setup>
import IconSearch from '@/components/icons/IconSearch.vue'; import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue'; import IconSetting from '@/components/icons/IconSetting.vue';
export default {
components: {
IconSearch,
IconSetting
}
}
</script> </script>

View File

@@ -39,13 +39,11 @@
</div> </div>
</template> </template>
<script> <script setup>
//:value="tUnits[unit].val.toString().padStart(2, '0')" import { ref, computed, watch, onMounted } from 'vue';
import { mapActions, } from 'pinia'; import emitter from '@/utils/emitter';
import { useConformanceInputStore } from '@/stores/conformanceInput';
export default { const props = defineProps({
props: {
max: { max: {
type: Number, type: Number,
default: 0, default: 0,
@@ -88,114 +86,69 @@ export default {
return value >= 0; return value >= 0;
}, },
} }
}, });
data() {
return { const emit = defineEmits(['total-seconds']);
display: 'dhms', // d: day; h: hour; m: month; s: second.
seconds: 0, const display = ref('dhms');
minutes: 0, const seconds = ref(0);
hours: 0, const minutes = ref(0);
days: 0, const hours = ref(0);
maxDays: 0, const days = ref(0);
minDays: 0, const maxDays = ref(0);
totalSeconds: 0, const minDays = ref(0);
maxTotal: null, const totalSeconds = ref(0);
minTotal: null, const maxTotal = ref(null);
inputTypes: [], const minTotal = ref(null);
lastInput: null, const inputTypes = ref([]);
openTimeSelect: false, const lastInput = ref(null);
}; const openTimeSelect = ref(false);
},
computed: { const tUnits = computed({
tUnits: {
get() { get() {
return { return {
s: { dsp: 's', inc: 1, val: this.seconds, max: 59, rate: 1, min: 0 }, s: { dsp: 's', inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: 'm', inc: 1, val: this.minutes, max: 59, rate: 60, min: 0 }, m: { dsp: 'm', inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: 'h', inc: 1, val: this.hours, max: 23, rate: 3600, min: 0 }, h: { dsp: 'h', inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: { dsp: 'd', inc: 1, val: this.days, max: this.maxDays, rate: 86400, min: this.minDays } d: { dsp: 'd', inc: 1, val: days.value, max: maxDays.value, rate: 86400, min: minDays.value }
}; };
}, },
set(newValues) { set(newValues) {
// When the input value exceeds the acceptable maximum value, the front end
// should set the value to be equal to the maximum value.
// 當輸入的數值大於可接受的最大值時,前端要將數值設定成等同於最大值
for (const unit in newValues) { for (const unit in newValues) {
this[unit] = newValues[unit].val; switch (unit) {
case 's': seconds.value = newValues[unit].val; break;
case 'm': minutes.value = newValues[unit].val; break;
case 'h': hours.value = newValues[unit].val; break;
case 'd': days.value = newValues[unit].val; break;
}
const input = document.querySelector(`[data-tunit="${unit}"]`); const input = document.querySelector(`[data-tunit="${unit}"]`);
if (input) { if (input) {
input.value = newValues[unit].val.toString(); input.value = newValues[unit].val.toString();
} }
} }
}, },
}, });
inputTimeFields: {
get() { const inputTimeFields = computed(() => {
const paddedTimeFields = []; const paddedTimeFields = [];
this.inputTypes.forEach(inputTypeUnit => { inputTypes.value.forEach(inputTypeUnit => {
// Pad the dd/hh/mm/ss field string to 2 digits and add it to the list paddedTimeFields.push(tUnits.value[inputTypeUnit].val.toString().padStart(2, '0'));
paddedTimeFields.push(this.tUnits[inputTypeUnit].val.toString().padStart(2, '0'));
}); });
return paddedTimeFields; return paddedTimeFields;
}, });
}
}, function onClose() {
watch: { openTimeSelect.value = false;
max: { }
handler: function(newValue, oldValue) {
this.maxTotal = newValue; function onFocus(event) {
if(this.size === 'max' && newValue !== oldValue) { lastInput.value = event.target;
this.createData(); lastInput.value.select();
}; }
},
immediate: true, function onChange(event) {
},
min: {
handler: function(newValue, oldValue) {
this.minTotal = newValue;
if( this.size === 'min' && newValue !== oldValue){
this.createData();
}
},
immediate: true,
},
// min 的最大值要等於 max 的總秒數
updateMax: {
handler: function(newValue, oldValue) {
this.maxTotal = newValue;
this.calculateTotalSeconds();
},
},
updateMin: {
handler: function(newValue, oldValue) {
this.minTotal = newValue;
this.calculateTotalSeconds();
},
},
},
methods: {
/**
* 關閉選單視窗
*/
onClose () {
this.openTimeSelect = false;
},
/**
* get focus element
* @param {event} event input 傳入的事件
*/
onFocus(event) {
this.lastInput = event.target;
this.lastInput.select(); // 當呼叫該方法時,文本框內的文字會被自動選中,這樣使用者可以方便地進行複製或刪除等操作。
},
/**
* when blur update input value and show number
* @param {event} event input 傳入的事件
*/
onChange(event) {
const baseInputValue = event.target.value; const baseInputValue = event.target.value;
let decoratedInputValue; let decoratedInputValue;
// 讓前綴數字自動補 0
if(isNaN(event.target.value)){ if(isNaN(event.target.value)){
event.target.value = '00'; event.target.value = '00';
} else { } else {
@@ -203,240 +156,187 @@ export default {
} }
decoratedInputValue = event.target.value.toString(); decoratedInputValue = event.target.value.toString();
// 手 key 數值大於最大值時,要等於最大值
// 先將字串轉為數字才能比大小
const inputValue = parseInt(event.target.value, 10); const inputValue = parseInt(event.target.value, 10);
const max = parseInt(event.target.dataset.max, 10); // 設定最大值 const max = parseInt(event.target.dataset.max, 10);
const min = parseInt(event.target.dataset.min, 10); const min = parseInt(event.target.dataset.min, 10);
if(inputValue > max) { if(inputValue > max) {
decoratedInputValue = max.toString().padStart(2, '0'); decoratedInputValue = max.toString().padStart(2, '0');
}else if(inputValue < min) { }else if(inputValue < min) {
decoratedInputValue= min.toString(); decoratedInputValue= min.toString();
} }
// 數值更新, tUnits 也更新, 並計算 totalSeconds
const dsp = event.target.dataset.tunit; const dsp = event.target.dataset.tunit;
this.tUnits[dsp].val = decoratedInputValue; tUnits.value[dsp].val = decoratedInputValue;
switch (dsp) { switch (dsp) {
case 'd': case 'd':
this.days = baseInputValue; days.value = baseInputValue;
break; break;
case 'h': case 'h':
this.hours = decoratedInputValue; hours.value = decoratedInputValue;
break; break;
case 'm': case 'm':
this.minutes = decoratedInputValue; minutes.value = decoratedInputValue;
break; break;
case 's': case 's':
this.seconds = decoratedInputValue; seconds.value = decoratedInputValue;
break; break;
}; };
this.calculateTotalSeconds(); calculateTotalSeconds();
}, }
/**
* 上下箭頭時的行為
* @param {event} event input 傳入的事件
*/
onKeyUp(event) {
// 正規表達式 \D 即不是 0-9 的字符
event.target.value = event.target.value.replace(/\D/g, '');
// 38上箭頭鍵Arrow Up function onKeyUp(event) {
// 40下箭頭鍵Arrow Down event.target.value = event.target.value.replace(/\D/g, '');
if (event.keyCode === 38 || event.keyCode === 40) { if (event.keyCode === 38 || event.keyCode === 40) {
this.actionUpDown(event.target, event.keyCode === 38, true); actionUpDown(event.target, event.keyCode === 38, true);
}; };
}, }
/**
* 上下箭頭時的行為 function actionUpDown(input, goUp, selectIt = false) {
* @param {element} input input 傳入的事件
* @param {number} goUp 上箭頭的鍵盤代號
* @param {boolean} selectIt 是否已執行
*/
actionUpDown(input, goUp, selectIt = false) {
const tUnit = input.dataset.tunit; const tUnit = input.dataset.tunit;
let newVal = this.getNewValue(input); let newVal = getNewValue(input);
if (goUp) { if (goUp) {
newVal = this.handleArrowUp(newVal, tUnit, input); newVal = handleArrowUp(newVal, tUnit, input);
} else { } else {
newVal = this.handleArrowDown(newVal, tUnit); newVal = handleArrowDown(newVal, tUnit);
} }
this.updateInputValue(input, newVal, tUnit); updateInputValue(input, newVal, tUnit);
if (selectIt) { if (selectIt) {
input.select(); input.select();
} }
this.calculateTotalSeconds(); calculateTotalSeconds();
}, }
/** function getNewValue(input) {
* 獲取新的數值
* @param {element} input 輸入的元素
* @returns {number} 新的數值
*/
getNewValue(input) {
const newVal = parseInt(input.value, 10); const newVal = parseInt(input.value, 10);
return isNaN(newVal) ? 0 : newVal; return isNaN(newVal) ? 0 : newVal;
}, }
/** function handleArrowUp(newVal, tUnit, input) {
* 處理向上箭頭的行為 newVal += tUnits.value[tUnit].inc;
* @param {number} newVal 當前數值 if (newVal > tUnits.value[tUnit].max) {
* @param {string} tUnit 時間單位 if (tUnits.value[tUnit].dsp === 'd') {
* @param {element} input 輸入的元素 totalSeconds.value = maxTotal.value;
* @returns {number} 更新後的數值
*/
handleArrowUp(newVal, tUnit, input) {
newVal += this.tUnits[tUnit].inc;
if (newVal > this.tUnits[tUnit].max) {
if (this.tUnits[tUnit].dsp === 'd') {
this.totalSeconds = this.maxTotal;
} else { } else {
newVal = newVal % (this.tUnits[tUnit].max + 1); newVal = newVal % (tUnits.value[tUnit].max + 1);
this.incrementPreviousUnit(input); incrementPreviousUnit(input);
} }
} }
return newVal; return newVal;
}, }
/** function handleArrowDown(newVal, tUnit) {
* 處理向下箭頭的行為 newVal -= tUnits.value[tUnit].inc;
* @param {number} newVal 當前數值
* @param {string} tUnit 時間單位
* @returns {number} 更新後的數值
*/
handleArrowDown(newVal, tUnit) {
newVal -= this.tUnits[tUnit].inc;
if (newVal < 0) { if (newVal < 0) {
newVal = (this.tUnits[tUnit].max + 1) - this.tUnits[tUnit].inc; newVal = (tUnits.value[tUnit].max + 1) - tUnits.value[tUnit].inc;
} }
return newVal; return newVal;
}, }
/** function incrementPreviousUnit(input) {
* 進位前一個更大的單位
* @param {element} input 輸入的元素
*/
incrementPreviousUnit(input) {
if (input.dataset.index > 0) { if (input.dataset.index > 0) {
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`); const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
this.actionUpDown(prevUnit, true); actionUpDown(prevUnit, true);
} }
}, }
/** function updateInputValue(input, newVal, tUnit) {
* 更新輸入框的數值
* @param {element} input 輸入的元素
* @param {number} newVal 新的數值
* @param {string} tUnit 時間單位
*/
updateInputValue(input, newVal, tUnit) {
input.value = newVal.toString(); input.value = newVal.toString();
switch (tUnit) { switch (tUnit) {
case 'd': case 'd':
this.days = input.value; days.value = input.value;
break; break;
case 'h': case 'h':
this.hours = input.value; hours.value = input.value;
break; break;
case 'm': case 'm':
this.minutes = input.value; minutes.value = input.value;
break; break;
case 's': case 's':
this.seconds = input.value; seconds.value = input.value;
break; break;
} }
}, }
/**
* 設定 dhms 的數值 function secondToDate(totalSec, size) {
* @param {number} totalSeconds 總秒數 totalSec = parseInt(totalSec);
* @param {string} size 'min' | 'max',可選以上任一,最大值或最小值 if(!isNaN(totalSec)) {
*/ seconds.value = totalSec % 60;
secondToDate(totalSeconds, size) { minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
totalSeconds = parseInt(totalSeconds); hours.value = (Math.floor(totalSec / 3600)) % 24;
if(!isNaN(totalSeconds)) { days.value = Math.floor(totalSec / (3600 * 24));
this.seconds = totalSeconds % 60;
this.minutes = (Math.floor(totalSeconds - this.seconds) / 60) % 60;
this.hours = (Math.floor(totalSeconds / 3600)) % 24;
this.days = Math.floor(totalSeconds / (3600 * 24));
if(size === 'max') { if(size === 'max') {
this.maxDays = Math.floor(totalSeconds / (3600 * 24)); maxDays.value = Math.floor(totalSec / (3600 * 24));
} }
else if(size === 'min') { else if(size === 'min') {
this.minDays = Math.floor(totalSeconds / (3600 * 24)); minDays.value = Math.floor(totalSec / (3600 * 24));
} }
}; };
}, }
/**
* 計算總秒數
*/
calculateTotalSeconds() {
let totalSeconds = 0;
for (const unit in this.tUnits) { function calculateTotalSeconds() {
const val = parseInt(this.tUnits[unit].val, 10); let total = 0;
for (const unit in tUnits.value) {
const val = parseInt(tUnits.value[unit].val, 10);
if (!isNaN(val)) { if (!isNaN(val)) {
totalSeconds += val * this.tUnits[unit].rate; total += val * tUnits.value[unit].rate;
} }
} }
if(totalSeconds >= this.maxTotal){ // 大於最大值時要等於最大值 if(total >= maxTotal.value){
totalSeconds = this.maxTotal; total = maxTotal.value;
this.secondToDate(this.maxTotal, 'max'); secondToDate(maxTotal.value, 'max');
} else if (totalSeconds <= this.minTotal) { // 小於最小值時要等於最小值 } else if (total <= minTotal.value) {
totalSeconds = this.minTotal; total = minTotal.value;
this.secondToDate(this.minTotal, 'min'); secondToDate(minTotal.value, 'min');
} else if((this.size === 'min' && totalSeconds <= this.maxTotal)) { } else if((props.size === 'min' && total <= maxTotal.value)) {
this.maxDays = Math.floor(this.maxTotal / (3600 * 24)); maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
} }
this.totalSeconds = totalSeconds; totalSeconds.value = total;
this.$emit('total-seconds', totalSeconds); emit('total-seconds', total);
}, }
/**
* 初始化
*/
async createData() {
const size = this.size;
if (this.maxTotal !== await null && this.minTotal !== await null) { async function createData() {
const size = props.size;
if (maxTotal.value !== await null && minTotal.value !== await null) {
switch (size) { switch (size) {
case 'max': case 'max':
this.secondToDate(this.minTotal, 'min'); secondToDate(minTotal.value, 'min');
this.secondToDate(this.maxTotal, 'max'); secondToDate(maxTotal.value, 'max');
this.totalSeconds = this.maxTotal; totalSeconds.value = maxTotal.value;
if(this.value !== null) { if(props.value !== null) {
this.totalSeconds = this.value; totalSeconds.value = props.value;
this.secondToDate(this.value); secondToDate(props.value);
} }
break; break;
case 'min': case 'min':
this.secondToDate(this.maxTotal, 'max'); secondToDate(maxTotal.value, 'max');
this.secondToDate(this.minTotal, 'min'); secondToDate(minTotal.value, 'min');
this.totalSeconds = this.minTotal; totalSeconds.value = minTotal.value;
if(this.value !== null) { if(props.value !== null) {
this.totalSeconds = this.value; totalSeconds.value = props.value;
this.secondToDate(this.value); secondToDate(props.value);
} }
break; break;
} }
} }
}, }
...mapActions(
useConformanceInputStore,[] // created
), emitter.on('reset', () => {
}, createData();
created() { });
this.$emitter.on('reset', (data) => {
this.createData(); // mounted
}); onMounted(() => {
}, inputTypes.value = display.value.split('');
mounted() { });
this.inputTypes = this.display.split('');
}, const vClosable = {
directives: {
'closable': {
mounted(el, {value}) { mounted(el, {value}) {
const handleOutsideClick = function(e) { const handleOutsideClick = function(e) {
let target = e.target; let target = e.target;
@@ -454,8 +354,6 @@ export default {
document.removeEventListener('click', handleOutsideClick); document.removeEventListener('click', handleOutsideClick);
}; };
}, },
}
},
}; };
</script> </script>
<style scoped> <style scoped>

View File

@@ -8,27 +8,15 @@
</template> </template>
<script> <script setup>
import { defineComponent, computed, } from 'vue';
import ImgCheckboxBlueFrame from "@/assets/icon-blue-checkbox.svg"; import ImgCheckboxBlueFrame from "@/assets/icon-blue-checkbox.svg";
import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg"; import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg";
import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg"; import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg";
export default defineComponent({ defineProps({
props: {
isChecked: { isChecked: {
type: Boolean, type: Boolean,
required: true // 表示这个 props 是必需的 required: true,
},
},
setup(props) {
const isChecked = computed(() => props.isChecked);
return {
ImgCheckboxBlueFrame,
ImgCheckboxCheckedMark,
ImgCheckboxGrayFrame,
isChecked,
};
}, },
}); });
</script> </script>

View File

@@ -4,7 +4,7 @@ import App from "./App.vue";
import router from "./router"; import router from "./router";
import pinia from '@/stores/main'; import pinia from '@/stores/main';
import moment from 'moment'; import moment from 'moment';
import mitt from 'mitt'; import emitter from '@/utils/emitter';
import ToastPlugin from 'vue-toast-notification'; import ToastPlugin from 'vue-toast-notification';
import cytoscape from 'cytoscape'; import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre'; import dagre from 'cytoscape-dagre';
@@ -44,7 +44,6 @@ import Checkbox from 'primevue/checkbox';
import Dialog from 'primevue/dialog'; import Dialog from 'primevue/dialog';
import ContextMenu from 'primevue/contextmenu'; import ContextMenu from 'primevue/contextmenu';
const emitter = mitt();
const app = createApp(App); const app = createApp(App);
// Pinia Set // Pinia Set

5
src/utils/emitter.ts Normal file
View File

@@ -0,0 +1,5 @@
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

View File

@@ -112,9 +112,8 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, computed, onMounted, watch, } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { mapState, mapActions, } from 'pinia';
import { useLoadingStore } from '@/stores/loading'; import { useLoadingStore } from '@/stores/loading';
import { useModalStore } from '@/stores/modal'; import { useModalStore } from '@/stores/modal';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
@@ -129,134 +128,132 @@ import {
MODAL_DELETE, MODAL_DELETE,
ONCE_RENDER_NUM_OF_DATA, ONCE_RENDER_NUM_OF_DATA,
} from "@/constants/constants.js"; } from "@/constants/constants.js";
import iconDeleteGray from '@/assets/icon-delete-gray.svg'; import iconDeleteGray from '@/assets/icon-delete-gray.svg';
import iconDeleteRed from '@/assets/icon-delete-red.svg'; import iconDeleteRed from '@/assets/icon-delete-red.svg';
import iconEditOff from '@/assets/icon-edit-off.svg'; import iconEditOff from '@/assets/icon-edit-off.svg';
import iconEditOn from '@/assets/icon-edit-on.svg'; import iconEditOn from '@/assets/icon-edit-on.svg';
import iconDetailOn from '@/assets/icon-detail-on.svg'; import iconDetailOn from '@/assets/icon-detail-on.svg';
import iconDetailOff from '@/assets/icon-detail-card.svg'; import iconDetailOff from '@/assets/icon-detail-card.svg';
export default { const toast = useToast();
setup() { const acctMgmtStore = useAcctMgmtStore();
const toast = useToast(); const loadingStore = useLoadingStore();
const acctMgmtStore = useAcctMgmtStore(); const modalStore = useModalStore();
const loadingStore = useLoadingStore(); const loginStore = useLoginStore();
const modalStore = useModalStore(); const infiniteStart = ref(0);
const loginStore = useLoginStore();
const infiniteStart = ref(0);
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList); const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList); const allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList);
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA)); const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
const loginUserData = ref(null); const loginUserData = ref(null);
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate); const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername); const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const inputQuery = ref(''); const inputQuery = ref('');
const fetchLoginUserData = async () => { const fetchLoginUserData = async () => {
await loginStore.getUserData(); await loginStore.getUserData();
loginUserData.value = loginStore.userData; loginUserData.value = loginStore.userData;
}; };
const moveJustCreateUserToFirstRow = () => { const moveJustCreateUserToFirstRow = () => {
if(infiniteAcctData.value && infiniteAcctData.value.length){ if(infiniteAcctData.value && infiniteAcctData.value.length){
const index = acctMgmtStore.allUserAccoutList.findIndex(user => user.username === acctMgmtStore.justCreateUsername); const index = acctMgmtStore.allUserAccoutList.findIndex(user => user.username === acctMgmtStore.justCreateUsername);
if (index !== -1) { if (index !== -1) {
// 移除匹配的對象(剛剛新增的使用者)並將其插入到陣列的第一位
const [justCreateUser] = acctMgmtStore.allUserAccoutList[index]; const [justCreateUser] = acctMgmtStore.allUserAccoutList[index];
infiniteAcctData.value.unshift(justCreateUser); infiniteAcctData.value.unshift(justCreateUser);
} }
} }
}; };
const accountSearchResults = computed(() => { const accountSearchResults = computed(() => {
if(!inputQuery.value) { if(!inputQuery.value) {
return infiniteAcctData.value; return infiniteAcctData.value;
} }
return acctMgmtStore.allUserAccoutList.filter (user => user.username.includes(inputQuery.value)); return acctMgmtStore.allUserAccoutList.filter (user => user.username.includes(inputQuery.value));
}); });
const onCreateNewClick = () => { const onCreateNewClick = () => {
acctMgmtStore.clearCurrentViewingUser(); acctMgmtStore.clearCurrentViewingUser();
modalStore.openModal(MODAL_CREATE_NEW); modalStore.openModal(MODAL_CREATE_NEW);
}; };
const onAcctDoubleClick = (username) => { const onAcctDoubleClick = (username) => {
acctMgmtStore.setCurrentViewingUser(username); acctMgmtStore.setCurrentViewingUser(username);
modalStore.openModal(MODAL_ACCT_INFO); modalStore.openModal(MODAL_ACCT_INFO);
} }
const handleDeleteMouseOver = (username) => { const handleDeleteMouseOver = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, true); acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
}; };
const handleDeleteMouseOut = (username) => { const handleDeleteMouseOut = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, false); acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false); acctMgmtStore.changeIsRowHoveredByUser(username, false);
}; };
const handleRowMouseOver = (username) => { const handleRowMouseOver = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, true); acctMgmtStore.changeIsRowHoveredByUser(username, true);
}; };
const handleRowMouseOut = (username) => { const handleRowMouseOut = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, false); acctMgmtStore.changeIsRowHoveredByUser(username, false);
}; };
const handleEditMouseOver = (username) => { const handleEditMouseOver = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, true); acctMgmtStore.changeIsEditHoveredByUser(username, true);
}; };
const handleEditMouseOut = (username) => { const handleEditMouseOut = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, false); acctMgmtStore.changeIsEditHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false); acctMgmtStore.changeIsRowHoveredByUser(username, false);
}; };
const handleDetailMouseOver = (username) => { const handleDetailMouseOver = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, true); acctMgmtStore.changeIsDetailHoveredByUser(username, true);
}; };
const handleDetailMouseOut = (username) => { const handleDetailMouseOut = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, false); acctMgmtStore.changeIsDetailHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false); acctMgmtStore.changeIsRowHoveredByUser(username, false);
}; };
const onEditButtonClick = userNameToEdit => { const onEditButtonClick = userNameToEdit => {
acctMgmtStore.setCurrentViewingUser(userNameToEdit); acctMgmtStore.setCurrentViewingUser(userNameToEdit);
modalStore.openModal(MODAL_ACCT_EDIT); modalStore.openModal(MODAL_ACCT_EDIT);
} }
const onDeleteBtnClick = (usernameToDelete) => { const onDeleteBtnClick = (usernameToDelete) => {
acctMgmtStore.setCurrentViewingUser(usernameToDelete); acctMgmtStore.setCurrentViewingUser(usernameToDelete);
modalStore.openModal(MODAL_DELETE); modalStore.openModal(MODAL_DELETE);
}; };
const getRowClass = (curData) => { const getRowClass = (curData) => {
return curData?.isRowHovered ? 'bg-[#F1F5F9]' : ''; return curData?.isRowHovered ? 'bg-[#F1F5F9]' : '';
}; };
watch(shouldUpdateList, async(newShouldUpdateList) => { const onDetailBtnClick = (dataKey) => {
acctMgmtStore.setCurrentViewingUser(dataKey);
modalStore.openModal(MODAL_ACCT_INFO);
};
watch(shouldUpdateList, async(newShouldUpdateList) => {
if (newShouldUpdateList) { if (newShouldUpdateList) {
await acctMgmtStore.getAllUserAccounts(); await acctMgmtStore.getAllUserAccounts();
// 當夾帶有infiniteStart.value就表示依然考慮到無限捲動的需求
infiniteAcctData.value = acctMgmtStore.allUserAccoutList.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA);
moveJustCreateUserToFirstRow(); moveJustCreateUserToFirstRow();
accountSearchResults.value = infiniteAcctData.value;
} }
acctMgmtStore.setShouldUpdateList(false); acctMgmtStore.setShouldUpdateList(false);
}); });
const onSearchAccountButtonClick = (inputQueryString) => { const onSearchAccountButtonClick = (inputQueryString) => {
inputQuery.value = inputQueryString; inputQuery.value = inputQueryString;
}; };
const setIsActiveInput = async(userData, inputIsActiveToSet) => { const setIsActiveInput = async(userData, inputIsActiveToSet) => {
const userDataToReplace = { const userDataToReplace = {
username: userData.username, username: userData.username,
name: userData.name, name: userData.name,
@@ -265,9 +262,9 @@ export default {
await acctMgmtStore.editAccount(userData.username, userDataToReplace); await acctMgmtStore.editAccount(userData.username, userDataToReplace);
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace); acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
} }
const onAdminInputClick = async(userData, inputIsAdminOn) => { const onAdminInputClick = async(userData, inputIsAdminOn) => {
const ADMIN_ROLE_NAME = 'admin'; const ADMIN_ROLE_NAME = 'admin';
switch(inputIsAdminOn) { switch(inputIsAdminOn) {
case true: case true:
@@ -287,34 +284,13 @@ export default {
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace); acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
} }
onMounted(async () => { /**
loadingStore.setIsLoading(false); * 無限滾動: 監聯 scroll 有沒有滾到底部
await fetchLoginUserData();
await acctMgmtStore.getAllUserAccounts();
});
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件 * @param {element} event 滾動傳入的事件
scrollTop表示容器的垂直滾動位置。具體來說它是以像素為單位的數值
表示當前內容視窗(可見區域)的頂部距離整個可滾動內容的頂部的距離。
簡單來說scrollTop 指的是滾動條的位置當滾動條在最上面時scrollTop 為 0
當滾動條向下移動時scrollTop 會增加。
可是作為:我們目前已經滾動了多少。
clientHeight表示容器的可見高度不包括滾動條的高度。它是以像素為單位的數值
表示容器內部的可見區域的高度。
與 offsetHeight 不同的是clientHeight 不包含邊框、內邊距和滾動條的高度,只計算內容區域的高度。
scrollHeight表示容器內部的總內容高度。它是以像素為單位的數值
包括看不見的(需要滾動才能看到的)部分。
簡單來說scrollHeight 是整個可滾動內容的總高度,包括可見區域和需要滾動才能看到的部分。
*/ */
const handleScroll = (event) => {
const handleScroll = (event) => {
const container = event.target; const container = event.target;
const smallValue = 3; const smallValue = 3;
@@ -322,107 +298,19 @@ export default {
if(isOverScrollHeight){ if(isOverScrollHeight){
fetchMoreDataVue3(); fetchMoreDataVue3();
} }
}; };
const fetchMoreDataVue3 = () => { const fetchMoreDataVue3 = () => {
if(infiniteAcctData.value.length < acctMgmtStore.allUserAccoutList.length) { if(infiniteAcctData.value.length < acctMgmtStore.allUserAccoutList.length) {
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA; infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
} }
};
return {
accountSearchResults,
modalStore,
loginUserData,
infiniteAcctData,
isOneAccountJustCreate,
justCreateUsername,
onEditButtonClick,
onCreateNewClick,
onAcctDoubleClick,
onSearchAccountButtonClick,
handleScroll,
getRowClass,
onDeleteBtnClick,
onAdminInputClick,
handleDeleteMouseOver,
handleDeleteMouseOut,
handleRowMouseOver,
handleRowMouseOut,
handleEditMouseOver,
handleEditMouseOut,
handleDetailMouseOver,
handleDetailMouseOut,
setIsActiveInput,
iconDeleteGray,
iconDeleteRed,
iconEditOff,
iconEditOn,
iconDetailOn,
iconDetailOff,
};
},
data() {
return {
i18next: i18next,
infiniteAcctDataVue2: [],
infiniteStart: 0,
isInfiniteFinish: true,
isInfinitMaxItemsMet: false,
};
},
components: {
SearchBar,
},
computed: {
...mapState(useAcctMgmtStore, ['allUserAccoutList']),
},
methods: {
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件
*/
handleScrollVue2(event) {
if(this.infinitMaxItems || this.infiniteAcctDataVue2.length < ONCE_RENDER_NUM_OF_DATA || this.isInfiniteFinish === false) {
return;
}
const container = event.target;
const smallValue = 4;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue;
if(overScrollHeight){
this.fetchMoreDataVue2();
}
},
/**
* 無限滾動: 滾到底後,要載入數據
*/
async fetchMoreDataVue2() {
this.infiniteFinish = false;
this.infiniteStart += ONCE_RENDER_NUM_OF_DATA;
this.infiniteAcctDataVue2 = await [...this.infiniteAcctDataVue2, ...this.allUserAccoutList.slice(
this.infiniteStart, this.infiniteStart + ONCE_RENDER_NUM_OF_DATA)];
this.isInfiniteFinish = true;
},
onDetailBtnClick(dataKey){
this.openModal(MODAL_ACCT_INFO);
this.setCurrentViewingUser(dataKey);
},
onEditBtnClickVue2(clickedUserName){
this.setCurrentViewingUser(clickedUserName);
this.openModal(MODAL_ACCT_EDIT);
},
...mapActions(useModalStore, ['openModal']),
...mapActions(useAcctMgmtStore, [
'setCurrentViewingUser',
'getAllUserAccounts',
]),
},
created() {
},
}; };
onMounted(async () => {
loadingStore.setIsLoading(false);
await fetchLoginUserData();
await acctMgmtStore.getAllUserAccounts();
});
</script> </script>
<style> <style>
/*為了讓 radio 按鈕可以置中,所以讓欄位的文字也置中 */ /*為了讓 radio 按鈕可以置中,所以讓欄位的文字也置中 */

View File

@@ -181,10 +181,9 @@
</div> </div>
</template> </template>
<script> <script setup>
import { defineComponent, computed, ref, watch, onMounted, } from 'vue'; import { computed, ref, watch } from 'vue';
import i18next from "@/i18n/i18n.js"; import i18next from "@/i18n/i18n.js";
import { mapActions, } from 'pinia';
import { useModalStore } from '@/stores/modal'; import { useModalStore } from '@/stores/modal';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'vue-toast-notification'; import { useToast } from 'vue-toast-notification';
@@ -193,37 +192,35 @@ import ModalHeader from "./ModalHeader.vue";
import IconChecked from "@/components/icons/IconChecked.vue"; import IconChecked from "@/components/icons/IconChecked.vue";
import { MODAL_CREATE_NEW, MODAL_ACCT_EDIT, PWD_VALID_LENGTH } from '@/constants/constants.js'; import { MODAL_CREATE_NEW, MODAL_ACCT_EDIT, PWD_VALID_LENGTH } from '@/constants/constants.js';
export default defineComponent({ const acctMgmtStore = useAcctMgmtStore();
setup() { const modalStore = useModalStore();
const acctMgmtStore = useAcctMgmtStore();
const modalStore = useModalStore();
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser); const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const isPwdEyeOn = ref(false); const isPwdEyeOn = ref(false);
const isConfirmDisabled = ref(true); const isConfirmDisabled = ref(true);
const isPwdLengthValid = ref(true); const isPwdLengthValid = ref(true);
const isResetPwdSectionShow = ref(false); const isResetPwdSectionShow = ref(false);
const isSetAsAdminChecked = ref(false); const isSetAsAdminChecked = ref(false);
const isSetActivedChecked = ref(true); const isSetActivedChecked = ref(true);
const whichCurrentModal = computed(() => modalStore.whichModal); const whichCurrentModal = computed(() => modalStore.whichModal);
const isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso); const isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso);
const username = computed(() => acctMgmtStore.currentViewingUser.username); const username = computed(() => acctMgmtStore.currentViewingUser.username);
const name = computed(() => acctMgmtStore.currentViewingUser.name); const name = computed(() => acctMgmtStore.currentViewingUser.name);
const inputUserAccount = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.username); const inputUserAccount = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.username);
const inputName = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.name); const inputName = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.name);
const inputPwd = ref(""); const inputPwd = ref("");
const isAccountUnique = ref(true); const isAccountUnique = ref(true);
const isEditable = ref(true); const isEditable = ref(true);
// 自從加入這段 watch 之後,填寫密碼欄位之時,就不會胡亂清空掉 account 或是 full name 欄位了。 // 自從加入這段 watch 之後,填寫密碼欄位之時,就不會胡亂清空掉 account 或是 full name 欄位了。
watch(whichCurrentModal, (newVal) => { watch(whichCurrentModal, (newVal) => {
if (newVal === MODAL_CREATE_NEW) { if (newVal === MODAL_CREATE_NEW) {
inputUserAccount.value = ''; inputUserAccount.value = '';
inputName.value = ''; inputName.value = '';
@@ -231,26 +228,26 @@ export default defineComponent({
inputUserAccount.value = currentViewingUser.value.username; inputUserAccount.value = currentViewingUser.value.username;
inputName.value = currentViewingUser.value.name; inputName.value = currentViewingUser.value.name;
} }
}); });
const modalTitle = computed(() => { const modalTitle = computed(() => {
return modalStore.whichModal === MODAL_CREATE_NEW ? i18next.t('AcctMgmt.CreateNew') : i18next.t('AcctMgmt.AccountEdit'); return modalStore.whichModal === MODAL_CREATE_NEW ? i18next.t('AcctMgmt.CreateNew') : i18next.t('AcctMgmt.AccountEdit');
}); });
const togglePwdEyeBtn = (toBeOpen) => { const togglePwdEyeBtn = (toBeOpen) => {
isPwdEyeOn.value = toBeOpen; isPwdEyeOn.value = toBeOpen;
}; };
const validatePwdLength = () => { const validatePwdLength = () => {
isPwdLengthValid.value = !isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH; isPwdLengthValid.value = !isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH;
} }
const onInputDoubleClick = () => { const onInputDoubleClick = () => {
// 允許編輯模式 // 允許編輯模式
isEditable.value = true; isEditable.value = true;
} }
const onConfirmBtnClick = async () => { const onConfirmBtnClick = async () => {
// rule for minimum length // rule for minimum length
validatePwdLength(); validatePwdLength();
if(!isPwdLengthValid.value) { if(!isPwdLengthValid.value) {
@@ -305,9 +302,9 @@ export default defineComponent({
default: default:
break; break;
} }
} }
const checkAccountIsUnique = async() => { const checkAccountIsUnique = async() => {
// 如果使用者沒有更動過欄位那就不用調用任何後端的API // 如果使用者沒有更動過欄位那就不用調用任何後端的API
if(inputUserAccount.value === username.value) { if(inputUserAccount.value === username.value) {
return true; return true;
@@ -315,33 +312,33 @@ export default defineComponent({
const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(inputUserAccount.value); const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(inputUserAccount.value);
isAccountUnique.value = !isAccountAlreadyExistAPISuccess; isAccountUnique.value = !isAccountAlreadyExistAPISuccess;
return isAccountUnique.value; return isAccountUnique.value;
}; };
const toggleIsAdmin = () => { const toggleIsAdmin = () => {
if(isEditable){ if(isEditable){
isSetAsAdminChecked.value = !isSetAsAdminChecked.value; isSetAsAdminChecked.value = !isSetAsAdminChecked.value;
} }
} }
const toggleIsActivated = () => { const toggleIsActivated = () => {
if(isEditable){ if(isEditable){
isSetActivedChecked.value = !isSetActivedChecked.value; isSetActivedChecked.value = !isSetActivedChecked.value;
} }
} }
const onInputNameFocus = () => { const onInputNameFocus = () => {
if(isConfirmDisabled.value){ if(isConfirmDisabled.value){
isConfirmDisabled.value = false; isConfirmDisabled.value = false;
} }
} }
const onResetPwdButtonClick = () => { const onResetPwdButtonClick = () => {
isResetPwdSectionShow.value = !isResetPwdSectionShow.value; isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
// 必須清空密碼欄位輸入的字串 // 必須清空密碼欄位輸入的字串
inputPwd.value = ''; inputPwd.value = '';
} }
watch( watch(
[inputPwd, inputUserAccount, inputName], [inputPwd, inputUserAccount, inputName],
([newPwd, newAccount, newName]) => { ([newPwd, newAccount, newName]) => {
// 只要[確認密碼]或[密碼]欄位有更動且所有欄位都不是空的confirm 按鈕就可點選 // 只要[確認密碼]或[密碼]欄位有更動且所有欄位都不是空的confirm 按鈕就可點選
@@ -358,59 +355,11 @@ export default defineComponent({
} }
} }
} }
); );
onMounted(() => {
});
return {
isConfirmDisabled,
username,
name,
isSSO,
isPwdEyeOn,
togglePwdEyeBtn,
isPwdLengthValid,
inputUserAccount,
inputName,
inputPwd,
onConfirmBtnClick,
onInputDoubleClick,
onInputNameFocus,
onResetPwdButtonClick,
isSetAsAdminChecked,
isSetActivedChecked,
isResetPwdSectionShow,
toggleIsAdmin,
toggleIsActivated,
whichCurrentModal,
MODAL_CREATE_NEW,
modalTitle,
isAccountUnique,
isEditable,
};
},
data() {
return {
i18next: i18next,
};
},
components: {
ModalHeader,
IconChecked,
},
methods: {
onCloseBtnClick(){
this.closeModal();
},
onCancelBtnClick(){
this.closeModal();
},
...mapActions(useModalStore, ['closeModal']),
}
});
function onCancelBtnClick(){
modalStore.closeModal();
}
</script> </script>
<style> <style>
#modal_account_edit { #modal_account_edit {

View File

@@ -22,42 +22,25 @@
</div> </div>
</template> </template>
<script> <script setup>
import { onBeforeMount, computed, ref } from 'vue'; import { onBeforeMount, computed, ref } from 'vue';
import i18next from '@/i18n/i18n.js'; import i18next from '@/i18n/i18n.js';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
import ModalHeader from './ModalHeader.vue'; import ModalHeader from './ModalHeader.vue';
import Badge from '../../components/Badge.vue'; import Badge from '../../components/Badge.vue';
export default { const acctMgmtStore = useAcctMgmtStore();
setup(){ const visitTime = ref(0);
const acctMgmtStore = useAcctMgmtStore(); const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const visitTime = ref(0); const {
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const {
username, username,
name, name,
is_admin, is_admin,
is_active, is_active,
} = currentViewingUser.value; } = currentViewingUser.value;
onBeforeMount(async() => { onBeforeMount(async() => {
await acctMgmtStore.getUserDetail(currentViewingUser.value.username); await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
visitTime.value = currentViewingUser.value.detail.visits; visitTime.value = currentViewingUser.value.detail.visits;
}); });
return {
i18next,
username,
name,
is_admin,
is_active,
visitTime,
};
},
components: {
ModalHeader,
Badge,
}
}
</script> </script>

View File

@@ -10,8 +10,8 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, } from 'vue'; import { computed } from 'vue';
import { useModalStore } from '@/stores/modal'; import { useModalStore } from '@/stores/modal';
import ModalAccountEditCreate from './ModalAccountEditCreate.vue'; import ModalAccountEditCreate from './ModalAccountEditCreate.vue';
import ModalAccountInfo from './ModalAccountInfo.vue'; import ModalAccountInfo from './ModalAccountInfo.vue';
@@ -23,27 +23,8 @@
MODAL_DELETE, MODAL_DELETE,
} from "@/constants/constants.js"; } from "@/constants/constants.js";
export default {
setup() {
const modalStore = useModalStore(); const modalStore = useModalStore();
const whichModal = computed(() => modalStore.whichModal); const whichModal = computed(() => modalStore.whichModal);
return {
modalStore,
whichModal,
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
};
},
components: {
ModalAccountEditCreate,
ModalAccountInfo,
ModalDeleteAlert,
}
};
</script> </script>
<style> <style>
#modal_container { #modal_container {

View File

@@ -27,39 +27,28 @@
</div> </div>
</template> </template>
<script> <script setup>
import { defineComponent, } from 'vue';
import { useModalStore } from '@/stores/modal'; import { useModalStore } from '@/stores/modal';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
import i18next from '@/i18n/i18n.js'; import i18next from '@/i18n/i18n.js';
import { useToast } from 'vue-toast-notification'; import { useToast } from 'vue-toast-notification';
export default defineComponent({ const acctMgmtStore = useAcctMgmtStore();
setup() { const modalStore = useModalStore();
const acctMgmtStore = useAcctMgmtStore(); const toast = useToast();
const modalStore = useModalStore(); const router = useRouter();
const toast = useToast();
const router = useRouter();
const onDeleteConfirmBtnClick = async() => { const onDeleteConfirmBtnClick = async() => {
if(await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)){ if(await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)){
toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess")); toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess"));
modalStore.closeModal(); modalStore.closeModal();
acctMgmtStore.setShouldUpdateList(true); acctMgmtStore.setShouldUpdateList(true);
await router.push("/account-admin"); await router.push("/account-admin");
} }
} };
const onNoBtnClick = () => { const onNoBtnClick = () => {
modalStore.closeModal(); modalStore.closeModal();
} };
return {
i18next,
onDeleteConfirmBtnClick,
onNoBtnClick,
};
},
});
</script> </script>

View File

@@ -11,24 +11,16 @@
</header> </header>
</template> </template>
<script> <script setup>
import { useModalStore } from '@/stores/modal'; import { useModalStore } from '@/stores/modal';
export default {
props: { defineProps({
headerText: { headerText: {
type: String, type: String,
required: true // 确保 headerText 是必填的 required: true,
} }
}, });
setup(props) {
const modalStore = useModalStore();
const { headerText, } = props;
const { closeModal } = modalStore;
return { const modalStore = useModalStore();
headerText, const { closeModal } = modalStore;
closeModal,
};
}
}
</script> </script>

View File

@@ -114,8 +114,8 @@
</div> </div>
</template> </template>
<script> <script setup>
import { onMounted, computed, ref, } from 'vue'; import { onMounted, computed, ref } from 'vue';
import i18next from '@/i18n/i18n.js'; import i18next from '@/i18n/i18n.js';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt'; import { useAcctMgmtStore } from '@/stores/acctMgmt';
@@ -126,109 +126,77 @@ import ButtonFilled from '@/components/ButtonFilled.vue';
import { useToast } from 'vue-toast-notification'; import { useToast } from 'vue-toast-notification';
import { PWD_VALID_LENGTH } from '@/constants/constants.js'; import { PWD_VALID_LENGTH } from '@/constants/constants.js';
export default { const loadingStore = useLoadingStore();
setup() { const loginStore = useLoginStore();
const loadingStore = useLoadingStore(); const acctMgmtStore = useAcctMgmtStore();
const loginStore = useLoginStore(); const toast = useToast();
const acctMgmtStore = useAcctMgmtStore();
const toast = useToast();
const visitTime = ref(0); const visitTime = ref(0);
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser); const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const name = computed(() => currentViewingUser.value.name); const name = computed(() => currentViewingUser.value.name);
const { const {
username, username,
is_admin, is_admin,
is_active, is_active,
} = currentViewingUser.value; } = currentViewingUser.value;
const inputName = ref(name.value); // remember to add .value postfix const inputName = ref(name.value);
const inputPwd = ref(''); const inputPwd = ref('');
const isNameEditable = ref(false); const isNameEditable = ref(false);
const isPwdEditable = ref(false); const isPwdEditable = ref(false);
const isPwdEyeOn = ref(false); const isPwdEyeOn = ref(false);
const isPwdLengthValid = ref(true); const isPwdLengthValid = ref(true);
const onEditNameClick = () => { const onEditNameClick = () => {
isNameEditable.value = true; isNameEditable.value = true;
} };
const onResetPwdClick = () => { const onResetPwdClick = () => {
isPwdEditable.value = true; isPwdEditable.value = true;
} };
const onSaveNameClick = async() => { const validatePwdLength = () => {
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
};
const onSaveNameClick = async() => {
if(inputName.value.length > 0) { if(inputName.value.length > 0) {
await acctMgmtStore.editAccountName(username, inputName.value); await acctMgmtStore.editAccountName(username, inputName.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
await acctMgmtStore.getUserDetail(username); await acctMgmtStore.getUserDetail(username);
isNameEditable.value = false; isNameEditable.value = false;
inputName.value = name.value; // updated value inputName.value = name.value;
} }
}; };
const onSavePwdClick = async() => { const onSavePwdClick = async() => {
validatePwdLength(); validatePwdLength();
if (isPwdLengthValid.value) { if (isPwdLengthValid.value) {
isPwdEditable.value = false; isPwdEditable.value = false;
await acctMgmtStore.editAccountPwd(username, inputPwd.value); await acctMgmtStore.editAccountPwd(username, inputPwd.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
inputPwd.value = ''; inputPwd.value = '';
// remember to force update
await acctMgmtStore.getUserDetail(loginStore.userData.username); await acctMgmtStore.getUserDetail(loginStore.userData.username);
} }
} };
const onCancelNameClick = () => { const onCancelNameClick = () => {
isNameEditable.value = false; isNameEditable.value = false;
inputName.value = name.value; inputName.value = name.value;
}; };
const onCancelPwdClick = () => { const onCancelPwdClick = () => {
isPwdEditable.value = false; isPwdEditable.value = false;
inputPwd.value = ''; inputPwd.value = '';
isPwdLengthValid.value = true; isPwdLengthValid.value = true;
}; };
const togglePwdEyeBtn = (toBeOpen) => { const togglePwdEyeBtn = (toBeOpen) => {
isPwdEyeOn.value = toBeOpen; isPwdEyeOn.value = toBeOpen;
}; };
const validatePwdLength = () => { onMounted(async() => {
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
}
onMounted(async() => {
loadingStore.setIsLoading(false); loadingStore.setIsLoading(false);
await acctMgmtStore.getUserDetail(loginStore.userData.username); await acctMgmtStore.getUserDetail(loginStore.userData.username);
}); });
return {
i18next,
username,
name,
is_admin,
is_active,
visitTime,
inputName,
inputPwd,
isNameEditable,
isPwdEditable,
isPwdEyeOn,
isPwdLengthValid,
onEditNameClick,
onResetPwdClick,
onSavePwdClick,
onSaveNameClick,
onCancelPwdClick,
onCancelNameClick,
togglePwdEyeBtn,
};
},
components: {
Badge,
Button,
ButtonFilled,
}
}
</script> </script>

View File

@@ -8,15 +8,7 @@
</main> </main>
</template> </template>
<script> <script setup>
import Header from "@/components/Header.vue"; import Header from "@/components/Header.vue";
import Navbar from "@/components/Navbar.vue"; import Navbar from "@/components/Navbar.vue";
export default {
name: 'AuthContainer',
components: {
Header,
Navbar,
},
};
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,29 @@
<!-- Sidebar: Switch data type -->
<template> <template>
<!-- Sidebar: Switch data type --> <div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10"
<div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10" :class="sidebarLeftValue? 'bg-neutral-50':''"> :class="sidebarLeftValue ? 'bg-neutral-50' : ''">
<ul class="space-y-4 flex flex-col justify-center items-center"> <ul class="space-y-4 flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarView = !sidebarView" :class="{'border-primary': sidebarView}" v-tooltip="tooltip.sidebarView"> hover:border-primary" @click="sidebarView = !sidebarView" :class="{ 'border-primary': sidebarView }"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']"> v-tooltip="tooltip.sidebarView">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
track_changes track_changes
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': sidebarFilter}" v-tooltip="tooltip.sidebarFilter"> hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{ 'border-primary': sidebarFilter }"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter"> v-tooltip="tooltip.sidebarFilter">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
tornado tornado
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50
drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces" :class="{'border-primary': sidebarTraces}" v-tooltip="tooltip.sidebarTraces"> drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']"> :class="{ 'border-primary': sidebarTraces }" v-tooltip="tooltip.sidebarTraces">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
rebase rebase
</span> </span>
</li> </li>
@@ -33,7 +40,7 @@
<ul class="flex flex-col justify-center items-center"> <ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer
bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState" bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState"
:class="{'border-primary': sidebarState}" id="iconState" v-tooltip.left="tooltip.sidebarState"> :class="{ 'border-primary': sidebarState }" id="iconState" v-tooltip.left="tooltip.sidebarState">
<span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5" <span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5"
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']"> :class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info info
@@ -43,116 +50,128 @@
</div> </div>
<!-- Sidebar Model --> <!-- Sidebar Model -->
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank" <SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles"
@switch-data-layer-type="switchDataLayerType" ></SidebarView> @switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType"></SidebarView>
<SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState> <SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState>
<SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesView"></SidebarTraces> <SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesViewRef">
</SidebarTraces>
<SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd" <SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd"
:filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace" :filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidebarFilterRef"></SidebarFilter> @submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidebarFilterRefComp"></SidebarFilter>
</template> </template>
<script> <script>
import { onBeforeMount, computed, } from 'vue'; import { useConformanceStore } from '@/stores/conformance';
import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMap from '@/module/cytoscapeMap.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { useMapPathStore } from '@/stores/mapPathStore';
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
import SidebarFilter from '@/components/Discover/Map/SidebarFilter.vue';
import ImgCapsule1 from '@/assets/capsule1.svg';
import ImgCapsule2 from '@/assets/capsule2.svg';
import ImgCapsule3 from '@/assets/capsule3.svg';
import ImgCapsule4 from '@/assets/capsule4.svg';
const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4]; export default {
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
export default { if (isCheckPage) {
setup() { const conformanceStore = useConformanceStore();
const loadingStore = useLoadingStore(); switch (to.params.type) {
const allMapDataStore = useAllMapDataStore(); case 'log':
const { isLoading } = storeToRefs(loadingStore); conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
const route = useRoute(); break;
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, case 'filter':
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile;
}
next();
}
}
</script>
<script setup>
import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMap from '@/module/cytoscapeMap.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { useMapPathStore } from '@/stores/mapPathStore';
import emitter from '@/utils/emitter';
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
import SidebarFilter from '@/components/Discover/Map/SidebarFilter.vue';
import ImgCapsule1 from '@/assets/capsule1.svg';
import ImgCapsule2 from '@/assets/capsule2.svg';
import ImgCapsule3 from '@/assets/capsule3.svg';
import ImgCapsule4 from '@/assets/capsule4.svg';
const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4];
const props = defineProps(['type', 'checkType', 'checkId', 'checkFileId']);
const route = useRoute();
// Stores
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId,
filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace,
temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases, temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases,
postRuleData postRuleData
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
const cytoscapeStore = useCytoscapeStore(); const cytoscapeStore = useCytoscapeStore();
const { setCurrentGraphId } = cytoscapeStore;
const mapPathStore = useMapPathStore();
const { setCurrentGraphId } = cytoscapeStore; const numberBeforeMapInRoute = computed(() => {
const numberBeforeMapInRoute = computed(() => {
// 取得當前路由的路徑
const path = route.path; const path = route.path;
// 使用斜線分割路徑
const segments = path.split('/'); const segments = path.split('/');
// 查找包含 'map' 的片段索引
const mapIndex = segments.findIndex(segment => segment.includes('map')); const mapIndex = segments.findIndex(segment => segment.includes('map'));
if (mapIndex > 0) { if (mapIndex > 0) {
// 定位到 'map' 片段的左邊片段
const previousSegment = segments[mapIndex - 1]; const previousSegment = segments[mapIndex - 1];
// 萃取左邊片段中的數字
const match = previousSegment.match(/\d+/); const match = previousSegment.match(/\d+/);
return match ? match[0] : 'No number found'; return match ? match[0] : 'No number found';
} }
return 'No map segment found'; return 'No map segment found';
}); });
onBeforeMount(() => { onBeforeMount(() => {
setCurrentGraphId(numberBeforeMapInRoute); setCurrentGraphId(numberBeforeMapInRoute);
}); });
// Data
return { isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces, const processMapData = ref({
baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe,
filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData,
ruleData, allMapDataStore, cases, postRuleData,
setCurrentGraphId,
};
},
props:['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
components: {
SidebarView,
SidebarState,
SidebarTraces,
SidebarFilter,
},
data() {
return {
processMapData: {
startId: 0, startId: 0,
endId: 1, endId: 1,
nodes: [], nodes: [],
edges: [], edges: [],
}, });
bpmnData: { const bpmnData = ref({
startId: 0, startId: 0,
endId: 1, endId: 1,
nodes: [], nodes: [],
edges: [], edges: [],
}, });
cytoscapeGraph: null, const cytoscapeGraph = ref(null);
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi const curveStyle = ref('unbundled-bezier');
mapType: 'processMap', // processMap | bpmn const mapType = ref('processMap');
mapPathStore: useMapPathStore(), const dataLayerType = ref('freq');
dataLayerType: 'freq', // freq | duration const dataLayerOption = ref('total');
dataLayerOption: 'total', const rank = ref('LR');
rank: 'LR', // 直向 TB | 橫向 LR const localTraceId = ref(1);
traceId: 1, const sidebarView = ref(false);
sidebarView: false, // SideBar: Visualization Setting const sidebarState = ref(false);
sidebarState: false, // SideBar: Summary & Insight const sidebarTraces = ref(false);
sidebarTraces: false, // SideBar: Traces const sidebarFilter = ref(false);
sidebarFilter: false, // SideBar: Filter const infiniteFirstCases = ref(null);
infiniteFirstCases: null, const startNodeId = ref(-1);
tooltip: { const endNodeId = ref(-1);
const tracesViewRef = ref(null);
const sidebarFilterRefComp = ref(null);
const tooltip = {
sidebarView: { sidebarView: {
value: 'Visualization Setting', value: 'Visualization Setting',
class: 'ml-1', class: 'ml-1',
@@ -181,99 +200,79 @@
text: 'text-[10px] p-1' text: 'text-[10px] p-1'
} }
}, },
};
// Computed
const sidebarLeftValue = computed(() => {
return sidebarView.value === true || sidebarTraces.value === true || sidebarFilter.value === true;
});
// Watch
watch(sidebarView, (newValue) => {
if (newValue) {
sidebarFilter.value = false;
sidebarTraces.value = false;
} }
});
watch(sidebarFilter, (newValue) => {
if (newValue) {
sidebarView.value = false;
sidebarState.value = false;
sidebarTraces.value = false;
sidebarState.value = false;
} }
}, });
computed:{
sidebarLeftValue: function() { watch(sidebarTraces, (newValue) => {
const result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true; if (newValue) {
return result; sidebarView.value = false;
sidebarState.value = false;
sidebarFilter.value = false;
sidebarState.value = false;
} }
}, });
watch: {
sidebarView: function(newValue) { watch(sidebarState, (newValue) => {
if(newValue) { if (newValue) {
this.sidebarFilter = false; sidebarFilter.value = false;
this.sidebarTraces = false; sidebarTraces.value = false;
} }
}, });
sidebarFilter: function(newValue) {
if(newValue) { // Methods
this.sidebarView = false; async function switchMapType(type) {
this.sidebarState = false; mapType.value = type;
this.sidebarTraces = false; createCy(type);
this.sidebarState = false; }
}
}, async function switchCurveStyles(style) {
sidebarTraces: function(newValue) { curveStyle.value = style;
if(newValue) { createCy(mapType.value);
this.sidebarView = false; }
this.sidebarState = false;
this.sidebarFilter = false; async function switchRank(rankValue) {
this.sidebarState = false; rank.value = rankValue;
} createCy(mapType.value);
}, }
sidebarState: function(newValue) {
if(newValue) { async function switchDataLayerType(type, option) {
this.sidebarFilter = false; dataLayerType.value = type;
this.sidebarTraces = false; dataLayerOption.value = option;
} createCy(mapType.value);
}, }
},
methods: { async function switchTraceId(e) {
/** if (e.id == traceId.value) return;
* switch map type isLoading.value = true;
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。 traceId.value = e.id;
*/ await allMapDataStore.getTraceDetail();
async switchMapType(type) { tracesViewRef.value.createCy();
this.mapType = type; isLoading.value = false;
this.createCy(type); }
},
/** function setNodesData(mapData) {
* switch curve style const mapTypeVal = mapType.value;
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
*/
async switchCurveStyles(style) {
this.curveStyle = style;
this.createCy(this.mapType);
},
/**
* switch rank
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
*/
async switchRank(rank) {
this.rank = rank;
this.createCy(this.mapType);
},
/**
* switch Data Layoer Type or Option.
* @param {string} type freq | duration
* @param {string} option 下拉選單中的選項
*/
async switchDataLayerType(type, option){
this.dataLayerType = type;
this.dataLayerOption = option;
this.createCy(this.mapType);
},
/**
* switch trace id and data
* @param {event} e input 傳入的事件
*/
async switchTraceId(e) {
if(e.id == this.traceId) return;
// 超過 1000 筆要 loading 畫面
this.isLoading = true; // 都要 loading 畫面
this.traceId = e.id;
await this.allMapDataStore.getTraceDetail();
this.$refs.tracesView.createCy();
this.isLoading = false;
},
/**
* 將 element nodes 資料彙整
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。
*/
setNodesData(mapData) {
const mapType = this.mapType;
const logFreq = { const logFreq = {
"total": "", "total": "",
"rel_freq": "", "rel_freq": "",
@@ -291,71 +290,72 @@
"max": "", "max": "",
"min": "", "min": "",
}; };
// BPMN 才有 gateway 類別
const gateway = { const gateway = {
parallel: "+", parallel: "+",
exclusive: "x", exclusive: "x",
inclusive: "o", inclusive: "o",
}; };
// 避免每次渲染都重複累加
mapData.nodes = []; mapData.nodes = [];
// 將 api call 回來的資料帶進 node const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
this[mapType].vertices.forEach(node => { mapSource.vertices.forEach(node => {
switch (node.type) { switch (node.type) {
// add type of 'bpmn gateway' node
case 'gateway': case 'gateway':
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:gateway[node.gateway_type], label: gateway[node.gateway_type],
height:60, height: 60,
width:60, width: 60,
backgroundColor:'#FFF', backgroundColor: '#FFF',
bordercolor:'#003366', bordercolor: '#003366',
shape:"diamond", shape: "diamond",
freq:logFreq, freq: logFreq,
duration:logDuration, duration: logDuration,
} }
}) })
break; break;
// add type of 'event' node
case 'event': case 'event':
if(node.event_type === 'start') mapData.startId = node.id; if (node.event_type === 'start') {
else if(node.event_type === 'end') mapData.endId = node.id; mapData.startId = node.id;
startNodeId.value = node.id;
}
else if (node.event_type === 'end') {
mapData.endId = node.id;
endNodeId.value = node.id;
}
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:node.event_type, label: node.event_type,
height: 48, height: 48,
width: 48, width: 48,
backgroundColor:'#FFFFFF', backgroundColor: '#FFFFFF',
bordercolor:'#0F172A', bordercolor: '#0F172A',
textColor: '#FF3366', textColor: '#FF3366',
shape:"ellipse", shape: "ellipse",
freq:logFreq, freq: logFreq,
duration:logDuration, duration: logDuration,
} }
}); });
break; break;
// add type of 'activity' node
default: default:
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:node.label, label: node.label,
height: 48, height: 48,
width: 216, width: 216,
textColor: '#0F172A', textColor: '#0F172A',
backgroundColor:'rgba(0, 0, 0, 0)', backgroundColor: 'rgba(0, 0, 0, 0)',
borderradius: 999, borderradius: 999,
shape:"round-rectangle", shape: "round-rectangle",
freq:node.freq, freq: node.freq,
duration:node.duration, duration: node.duration,
backgroundOpacity: 0, backgroundOpacity: 0,
borderOpacity: 0, borderOpacity: 0,
} }
@@ -363,14 +363,10 @@
break; break;
} }
}); });
}, }
/**
* 將 element edges 資料彙整 function setEdgesData(mapData) {
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。 const mapTypeVal = mapType.value;
*/
setEdgesData(mapData) {
const mapType = this.mapType;
//add event duration is empty
const logDuration = { const logDuration = {
"total": "", "total": "",
"rel_duration": "", "rel_duration": "",
@@ -382,60 +378,54 @@
}; };
mapData.edges = []; mapData.edges = [];
this[mapType].edges.forEach(edge => { const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
mapSource.edges.forEach(edge => {
mapData.edges.push({ mapData.edges.push({
data: { data: {
source:edge.tail, source: edge.tail,
target:edge.head, target: edge.head,
freq:edge.freq, freq: edge.freq,
duration:edge.duration === null ? logDuration : edge.duration, duration: edge.duration === null ? logDuration : edge.duration,
style:'dotted', edgeStyle: edge.tail === startNodeId.value || edge.head === endNodeId.value ? 'dotted' : 'solid',
lineWidth:1, lineWidth: 1,
}, },
}); });
}); });
}, }
/**
* create cytoscape's map
* @param {string} type this.mapType 'processMap' | 'bpmn',可傳入以上任一。
*/
async createCy(type) {
const graphId = document.getElementById('cy');
const mapData = type === 'processMap'? this.processMapData: this.bpmnData;
if(this[type].vertices.length !== 0){ async function createCy(type) {
this.setNodesData(mapData); const graphId = document.getElementById('cy');
this.setEdgesData(mapData); const mapData = type === 'processMap' ? processMapData.value : bpmnData.value;
this.setActivityBgImage(mapData); const mapSource = type === 'processMap' ? processMap.value : bpmn.value;
this.cytoscapeGraph = await cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
const processOrBPMN = this.mapType === 'processMap' ? 'process' : 'bpmn'; if (mapSource.vertices.length !== 0) {
const curveType = this.curveStyle === 'taxi' ? 'elbow' : 'curved'; setNodesData(mapData);
const directionType = this.rank === 'LR' ? 'horizontal' : 'vertical'; setEdgesData(mapData);
await this.mapPathStore.setCytoscape(this.cytoscapeGraph, processOrBPMN, curveType, directionType); setActivityBgImage(mapData);
cytoscapeGraph.value = await cytoscapeMap(mapData, dataLayerType.value, dataLayerOption.value, curveStyle.value, rank.value, graphId);
const processOrBPMN = mapType.value === 'processMap' ? 'process' : 'bpmn';
const curveType = curveStyle.value === 'taxi' ? 'elbow' : 'curved';
const directionType = rank.value === 'LR' ? 'horizontal' : 'vertical';
await mapPathStore.setCytoscape(cytoscapeGraph.value, processOrBPMN, curveType, directionType);
}; };
}, }
setActivityBgImage(mapData) {
function setActivityBgImage(mapData) {
const nodes = mapData.nodes; const nodes = mapData.nodes;
// 一組有多少個activities
const groupSize = Math.floor(nodes.length / ImgCapsules.length); const groupSize = Math.floor(nodes.length / ImgCapsules.length);
let nodeOptionArr = []; let nodeOptionArr = [];
const leveledGroups = []; // 每一個level會使用不同的膠囊圖片 const leveledGroups = [];
// 設定除了 start, end 的 node 顏色
// 找出 type activity's node
const activityNodeArray = nodes.filter(node => node.data.type === 'activity'); const activityNodeArray = nodes.filter(node => node.data.type === 'activity');
// 找出除了 start, end 以外所有的 node 的 option value activityNodeArray.forEach(node => nodeOptionArr.push(node.data[dataLayerType.value][dataLayerOption.value]));
activityNodeArray.forEach(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption]));
// 將node的option值從小到大排序(映對色階淺到深)
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b); nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
for(let i = 0; i < ImgCapsules.length; i++) { for (let i = 0; i < ImgCapsules.length; i++) {
const startIdx = i * groupSize; const startIdx = i * groupSize;
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize; const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx)); leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
} }
for(let level = 0; level < leveledGroups.length; level++) { for (let level = 0; level < leveledGroups.length; level++) {
leveledGroups[level].forEach(option => { leveledGroups[level].forEach(option => {
// 考慮可能有名次一樣的情形 const curNodes = activityNodeArray.filter(activityNode => activityNode.data[dataLayerType.value][dataLayerOption.value] === option);
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option);
curNodes.forEach(curNode => { curNodes.forEach(curNode => {
curNode.data = { curNode.data = {
...curNode.data, ...curNode.data,
@@ -445,91 +435,65 @@
}); });
}); });
} }
}, }
},
async created() {
const routeParams = this.$route.params;
const file = this.$route.meta.file;
const isCheckPage = this.$route.name.includes('Check');
// 先 loading 再執行以下程式 // Created logic
this.isLoading = true; (async () => {
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁 const routeParams = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
isLoading.value = true;
switch (routeParams.type) { switch (routeParams.type) {
case 'log': case 'log':
if(!isCheckPage) { if (!isCheckPage) {
this.logId = await routeParams.fileId; logId.value = await routeParams.fileId;
this.baseLogId = await routeParams.fileId; baseLogId.value = await routeParams.fileId;
} else { } else {
this.logId = await file.parent.id; logId.value = await file.parent.id;
this.baseLogId = await file.parent.id; baseLogId.value = await file.parent.id;
} }
break; break;
case 'filter': case 'filter':
if(!isCheckPage) { if (!isCheckPage) {
this.createFilterId = await routeParams.fileId; createFilterId.value = await routeParams.fileId;
} else { } else {
this.createFilterId = await file.parent.id; createFilterId.value = await file.parent.id;
} }
// 取得 logID 和上次儲存的 Funnel await allMapDataStore.fetchFunnel(createFilterId.value);
await this.allMapDataStore.fetchFunnel(this.createFilterId); isRuleData.value = await Array.from(temporaryData.value);
this.isRuleData = await Array.from(this.temporaryData); ruleData.value = await isRuleData.value.map(e => sidebarFilterRefComp.value.setRule(e));
this.ruleData = await this.isRuleData.map(e => this.$refs.sidebarFilterRef.setRule(e));
break; break;
} }
// 取得 logId 後才 call api await allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllMapData(); await allMapDataStore.getAllTrace();
await this.allMapDataStore.getAllTrace();
// log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。 traceId.value = await traces.value[0]?.id;
this.traceId = await this.traces[0]?.id; baseTraceId.value = await baseTraces.value[0]?.id;
this.baseTraceId = await this.baseTraces[0]?.id; await createCy(mapType.value);
await this.createCy(this.mapType); await allMapDataStore.getFilterParams();
await this.allMapDataStore.getFilterParams(); await allMapDataStore.getTraceDetail();
await this.allMapDataStore.getTraceDetail();
// 執行完後才取消 loading isLoading.value = false;
this.isLoading = false; emitter.on('saveModal', boolean => {
// 存檔 Modal 打開時,側邊欄要關閉 sidebarView.value = boolean;
this.$emitter.on('saveModal', boolean => { sidebarFilter.value = boolean;
this.sidebarView = boolean; sidebarTraces.value = boolean;
this.sidebarFilter = boolean; sidebarState.value = boolean;
this.sidebarTraces = boolean;
this.sidebarState = boolean;
}); });
this.$emitter.on('leaveFilter', boolean => { emitter.on('leaveFilter', boolean => {
this.sidebarView = boolean; sidebarView.value = boolean;
this.sidebarFilter = boolean; sidebarFilter.value = boolean;
this.sidebarTraces = boolean; sidebarTraces.value = boolean;
this.sidebarState = boolean; sidebarState.value = boolean;
}); });
}, })();
beforeUnmount() {
this.logId = null;
this.createFilterId = null;
this.tempFilterId = null;
this.temporaryData = [];
this.postRuleData = [];
this.ruleData = [];
},
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
if (isCheckPage) {
const conformanceStore = useConformanceStore();
switch (to.params.type) {
case 'log':
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
break;
case 'filter':
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile; // 將 file data 存到 route
}
next();
}
}
</script>
onBeforeUnmount(() => {
logId.value = null;
createFilterId.value = null;
temporaryData.value = [];
postRuleData.value = [];
ruleData.value = [];
});
</script>

View File

@@ -8,95 +8,13 @@
</main> </main>
</template> </template>
<script> <script>
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useConformanceStore } from '@/stores/conformance'; import { useConformanceStore } from '@/stores/conformance';
import StatusBar from '@/components/Discover/StatusBar.vue';
import ConformanceResults from '@/components/Discover/Conformance/ConformanceResults.vue';
import ConformanceSidebar from '@/components/Discover/Conformance/ConformanceSidebar.vue';
export default { export default {
setup() {
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { conformanceLogId, conformanceFilterId, conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
conformanceLogTempCheckId, conformanceFilterTempCheckId, selectedRuleType, selectedActivitySequence,
selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, conformanceRuleData,
conformanceTempReportData, conformanceFileName,
} = storeToRefs(conformanceStore);
return { isLoading, conformanceLogId, conformanceFilterId, conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceStore, selectedRuleType,
selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo,
conformanceRuleData, conformanceTempReportData, conformanceFileName
};
},
components: {
StatusBar,
ConformanceResults,
ConformanceSidebar,
},
async created() {
this.isLoading = true;
const params = this.$route.params;
const file = this.$route.meta.file;
const isCheckPage = this.$route.name.includes('Check');
if(!isCheckPage) {
switch (params.type) {
case 'log': // FILES page 來的 log
this.conformanceLogId = params.fileId;
break;
case 'filter': // FILES page 來的 filter
this.conformanceFilterId = params.fileId;
break;
}
} else {
switch (params.type) {
case 'log': // FILES page 來的已存檔 rule(log-check)
this.conformanceLogId = file.parent.id;
this.conformanceFileName = file.name;
break;
case 'filter': // FILES page 來的已存檔 rule(filter-check)
this.conformanceFilterId = file.parent.id;
this.conformanceFileName = file.name;
break;
}
await this.conformanceStore.getConformanceReport();
}
await this.conformanceStore.getConformanceParams();
// 給 rule 檔取得 ShowBar 一些時間
setTimeout(() => this.isLoading = false, 500);
},
mounted() {
this.selectedRuleType = 'Have activity';
this.selectedActivitySequence = 'Start & End';
this.selectedMode = 'Directly follows';
this.selectedProcessScope = 'End to end';
this.selectedActSeqMore = 'All';
this.selectedActSeqFromTo = 'From';
},
beforeUnmount() {
// 離開 conformance 時將 id 為 null避免污染其他檔案
this.conformanceLogId = null;
this.conformanceFilterId = null;
this.conformanceLogCreateCheckId = null;
this.conformanceFilterCreateCheckId = null;
this.conformanceRuleData = null;
this.conformanceFileName = null;
},
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check'); const isCheckPage = to.name.includes('Check');
if (isCheckPage) { if (isCheckPage) {
const conformanceStore = useConformanceStore(); const conformanceStore = useConformanceStore();
// Save token in Headers.
// (?:^|.;\s):匹配 "luciaToken" 之前的內容,允許它在字符串開頭或某個分號之後。
// luciaToken\s=\s**:匹配 "luciaToken=",並忽略兩邊的空格。
// ([^;]*):捕獲 "luciaToken" 的值,直到遇到下一個分號或字符串結尾。
// .*$:匹配剩餘的字符,確保完整的提取。
// |^.*$:在找不到 "luciaToken" 的情況下,匹配整個字符串。
switch (to.params.type) { switch (to.params.type) {
case 'log': case 'log':
conformanceStore.setConformanceLogCreateCheckId(to.params.fileId); conformanceStore.setConformanceLogCreateCheckId(to.params.fileId);
@@ -106,9 +24,84 @@ export default {
break; break;
} }
await conformanceStore.getConformanceReport(); await conformanceStore.getConformanceReport();
to.meta.file = await conformanceStore.conformanceTempReportData?.file; // 將 file data 存到 route 給 Navbar, StatusBar 使用 to.meta.file = await conformanceStore.conformanceTempReportData?.file;
} }
next(); next();
} }
} }
</script> </script>
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useConformanceStore } from '@/stores/conformance';
import StatusBar from '@/components/Discover/StatusBar.vue';
import ConformanceResults from '@/components/Discover/Conformance/ConformanceResults.vue';
import ConformanceSidebar from '@/components/Discover/Conformance/ConformanceSidebar.vue';
const route = useRoute();
// Stores
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { conformanceLogId, conformanceFilterId, conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
conformanceLogTempCheckId, conformanceFilterTempCheckId, selectedRuleType, selectedActivitySequence,
selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, conformanceRuleData,
conformanceTempReportData, conformanceFileName,
} = storeToRefs(conformanceStore);
// Created logic
(async () => {
isLoading.value = true;
const params = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
if(!isCheckPage) {
switch (params.type) {
case 'log':
conformanceLogId.value = params.fileId;
break;
case 'filter':
conformanceFilterId.value = params.fileId;
break;
}
} else {
switch (params.type) {
case 'log':
conformanceLogId.value = file.parent.id;
conformanceFileName.value = file.name;
break;
case 'filter':
conformanceFilterId.value = file.parent.id;
conformanceFileName.value = file.name;
break;
}
await conformanceStore.getConformanceReport();
}
await conformanceStore.getConformanceParams();
setTimeout(() => isLoading.value = false, 500);
})();
// Mounted
onMounted(() => {
selectedRuleType.value = 'Have activity';
selectedActivitySequence.value = 'Start & End';
selectedMode.value = 'Directly follows';
selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All';
selectedActSeqFromTo.value = 'From';
});
onBeforeUnmount(() => {
conformanceLogId.value = null;
conformanceFilterId.value = null;
conformanceLogCreateCheckId.value = null;
conformanceFilterCreateCheckId.value = null;
conformanceRuleData.value = null;
conformanceFileName.value = null;
});
</script>

View File

@@ -1,29 +1,22 @@
<template> <template>
<!-- Sidebar: Switch data type --> <!-- Sidebar: Switch data type -->
<div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10" <div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10" :class="sidebarLeftValue? 'bg-neutral-50':''">
:class="sidebarLeftValue ? 'bg-neutral-50' : ''">
<ul class="space-y-4 flex flex-col justify-center items-center"> <ul class="space-y-4 flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarView = !sidebarView" :class="{ 'border-primary': sidebarView }" hover:border-primary" @click="sidebarView = !sidebarView" :class="{'border-primary': sidebarView}" v-tooltip="tooltip.sidebarView">
v-tooltip="tooltip.sidebarView"> <span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
track_changes track_changes
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{ 'border-primary': sidebarFilter }" hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': sidebarFilter}" v-tooltip="tooltip.sidebarFilter">
v-tooltip="tooltip.sidebarFilter"> <span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
tornado tornado
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50
drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces" drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces" :class="{'border-primary': sidebarTraces}" v-tooltip="tooltip.sidebarTraces">
:class="{ 'border-primary': sidebarTraces }" v-tooltip="tooltip.sidebarTraces"> <span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
rebase rebase
</span> </span>
</li> </li>
@@ -40,7 +33,7 @@
<ul class="flex flex-col justify-center items-center"> <ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer
bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState" bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState"
:class="{ 'border-primary': sidebarState }" id="iconState" v-tooltip.left="tooltip.sidebarState"> :class="{'border-primary': sidebarState}" id="iconState" v-tooltip.left="tooltip.sidebarState">
<span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5" <span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5"
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']"> :class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info info
@@ -50,26 +43,50 @@
</div> </div>
<!-- Sidebar Model --> <!-- Sidebar Model -->
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" <SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank"
@switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType"></SidebarView> @switch-data-layer-type="switchDataLayerType" ></SidebarView>
<SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState> <SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState>
<SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesView"> <SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesViewRef"></SidebarTraces>
</SidebarTraces>
<SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd" <SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd"
:filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace" :filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidebarFilterRef"></SidebarFilter> @submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidebarFilterRefComp"></SidebarFilter>
</template> </template>
<script> <script>
import { onBeforeMount, computed, } from 'vue'; import { useConformanceStore } from '@/stores/conformance';
import { storeToRefs } from 'pinia';
export default {
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
if (isCheckPage) {
const conformanceStore = useConformanceStore();
switch (to.params.type) {
case 'log':
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
break;
case 'filter':
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile;
}
next();
}
}
</script>
<script setup>
import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading'; import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData'; import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMap from '@/module/cytoscapeMap.js'; import cytoscapeMap from '@/module/cytoscapeMap.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore'; import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { useMapPathStore } from '@/stores/mapPathStore'; import { useMapPathStore } from '@/stores/mapPathStore';
import emitter from '@/utils/emitter';
import SidebarView from '@/components/Discover/Map/SidebarView.vue'; import SidebarView from '@/components/Discover/Map/SidebarView.vue';
import SidebarState from '@/components/Discover/Map/SidebarState.vue'; import SidebarState from '@/components/Discover/Map/SidebarState.vue';
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue'; import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
@@ -81,89 +98,69 @@ import ImgCapsule4 from '@/assets/capsule4.svg';
const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4]; const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4];
export default { const props = defineProps(['type', 'checkType', 'checkId', 'checkFileId']);
setup() {
const loadingStore = useLoadingStore(); const route = useRoute();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore); // Stores
const route = useRoute(); const loadingStore = useLoadingStore();
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId,
filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace,
temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases, temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases,
postRuleData postRuleData
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
const cytoscapeStore = useCytoscapeStore(); const cytoscapeStore = useCytoscapeStore();
const { setCurrentGraphId } = cytoscapeStore;
const mapPathStore = useMapPathStore();
const { setCurrentGraphId } = cytoscapeStore; const numberBeforeMapInRoute = computed(() => {
const numberBeforeMapInRoute = computed(() => {
// 取得當前路由的路徑
const path = route.path; const path = route.path;
// 使用斜線分割路徑
const segments = path.split('/'); const segments = path.split('/');
// 查找包含 'map' 的片段索引
const mapIndex = segments.findIndex(segment => segment.includes('map')); const mapIndex = segments.findIndex(segment => segment.includes('map'));
if (mapIndex > 0) { if (mapIndex > 0) {
// 定位到 'map' 片段的左邊片段
const previousSegment = segments[mapIndex - 1]; const previousSegment = segments[mapIndex - 1];
// 萃取左邊片段中的數字
const match = previousSegment.match(/\d+/); const match = previousSegment.match(/\d+/);
return match ? match[0] : 'No number found'; return match ? match[0] : 'No number found';
} }
return 'No map segment found'; return 'No map segment found';
}); });
onBeforeMount(() => { onBeforeMount(() => {
setCurrentGraphId(numberBeforeMapInRoute); setCurrentGraphId(numberBeforeMapInRoute);
}); });
// Data
return { const processMapData = ref({
isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces,
baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe,
filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData,
ruleData, allMapDataStore, cases, postRuleData,
setCurrentGraphId,
};
},
props: ['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
components: {
SidebarView,
SidebarState,
SidebarTraces,
SidebarFilter,
},
data() {
return {
processMapData: {
startId: 0, startId: 0,
endId: 1, endId: 1,
nodes: [], nodes: [],
edges: [], edges: [],
}, });
bpmnData: { const bpmnData = ref({
startId: 0, startId: 0,
endId: 1, endId: 1,
nodes: [], nodes: [],
edges: [], edges: [],
}, });
cytoscapeGraph: null, const cytoscapeGraph = ref(null);
curveStyle: 'unbundled-bezier', // unbundled-bezier | taxi const curveStyle = ref('unbundled-bezier');
mapType: 'processMap', // processMap | bpmn const mapType = ref('processMap');
mapPathStore: useMapPathStore(), const dataLayerType = ref('freq');
dataLayerType: 'freq', // freq | duration const dataLayerOption = ref('total');
dataLayerOption: 'total', const rank = ref('LR');
rank: 'LR', // 直向 TB | 橫向 LR const localTraceId = ref(1);
traceId: 1, const sidebarView = ref(false);
sidebarView: false, // SideBar: Visualization Setting const sidebarState = ref(false);
sidebarState: false, // SideBar: Summary & Insight const sidebarTraces = ref(false);
sidebarTraces: false, // SideBar: Traces const sidebarFilter = ref(false);
sidebarFilter: false, // SideBar: Filter const infiniteFirstCases = ref(null);
infiniteFirstCases: null, const tracesViewRef = ref(null);
startNodeId: -1, const sidebarFilterRefComp = ref(null);
endNodeId: -1,
tooltip: { const tooltip = {
sidebarView: { sidebarView: {
value: 'Visualization Setting', value: 'Visualization Setting',
class: 'ml-1', class: 'ml-1',
@@ -192,99 +189,79 @@ export default {
text: 'text-[10px] p-1' text: 'text-[10px] p-1'
} }
}, },
};
// Computed
const sidebarLeftValue = computed(() => {
return sidebarView.value === true || sidebarTraces.value === true || sidebarFilter.value === true;
});
// Watch
watch(sidebarView, (newValue) => {
if(newValue) {
sidebarFilter.value = false;
sidebarTraces.value = false;
} }
});
watch(sidebarFilter, (newValue) => {
if(newValue) {
sidebarView.value = false;
sidebarState.value = false;
sidebarTraces.value = false;
sidebarState.value = false;
} }
}, });
computed: {
sidebarLeftValue: function () { watch(sidebarTraces, (newValue) => {
const result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true; if(newValue) {
return result; sidebarView.value = false;
sidebarState.value = false;
sidebarFilter.value = false;
sidebarState.value = false;
} }
}, });
watch: {
sidebarView: function (newValue) { watch(sidebarState, (newValue) => {
if (newValue) { if(newValue) {
this.sidebarFilter = false; sidebarFilter.value = false;
this.sidebarTraces = false; sidebarTraces.value = false;
} }
}, });
sidebarFilter: function (newValue) {
if (newValue) { // Methods
this.sidebarView = false; async function switchMapType(type) {
this.sidebarState = false; mapType.value = type;
this.sidebarTraces = false; createCy(type);
this.sidebarState = false; }
}
}, async function switchCurveStyles(style) {
sidebarTraces: function (newValue) { curveStyle.value = style;
if (newValue) { createCy(mapType.value);
this.sidebarView = false; }
this.sidebarState = false;
this.sidebarFilter = false; async function switchRank(rankValue) {
this.sidebarState = false; rank.value = rankValue;
} createCy(mapType.value);
}, }
sidebarState: function (newValue) {
if (newValue) { async function switchDataLayerType(type, option){
this.sidebarFilter = false; dataLayerType.value = type;
this.sidebarTraces = false; dataLayerOption.value = option;
} createCy(mapType.value);
}, }
},
methods: { async function switchTraceId(e) {
/** if(e.id == traceId.value) return;
* switch map type isLoading.value = true;
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。 traceId.value = e.id;
*/ await allMapDataStore.getTraceDetail();
async switchMapType(type) { tracesViewRef.value.createCy();
this.mapType = type; isLoading.value = false;
this.createCy(type); }
},
/** function setNodesData(mapData) {
* switch curve style const mapTypeVal = mapType.value;
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
*/
async switchCurveStyles(style) {
this.curveStyle = style;
this.createCy(this.mapType);
},
/**
* switch rank
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
*/
async switchRank(rank) {
this.rank = rank;
this.createCy(this.mapType);
},
/**
* switch Data Layoer Type or Option.
* @param {string} type freq | duration
* @param {string} option 下拉選單中的選項
*/
async switchDataLayerType(type, option) {
this.dataLayerType = type;
this.dataLayerOption = option;
this.createCy(this.mapType);
},
/**
* switch trace id and data
* @param {event} e input 傳入的事件
*/
async switchTraceId(e) {
if (e.id == this.traceId) return;
// 超過 1000 筆要 loading 畫面
this.isLoading = true; // 都要 loading 畫面
this.traceId = e.id;
await this.allMapDataStore.getTraceDetail();
this.$refs.tracesView.createCy();
this.isLoading = false;
},
/**
* 將 element nodes 資料彙整
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。
*/
setNodesData(mapData) {
const mapType = this.mapType;
const logFreq = { const logFreq = {
"total": "", "total": "",
"rel_freq": "", "rel_freq": "",
@@ -302,77 +279,66 @@ export default {
"max": "", "max": "",
"min": "", "min": "",
}; };
// BPMN 才有 gateway 類別
const gateway = { const gateway = {
parallel: "+", parallel: "+",
exclusive: "x", exclusive: "x",
inclusive: "o", inclusive: "o",
}; };
// 避免每次渲染都重複累加
mapData.nodes = []; mapData.nodes = [];
// 將 api call 回來的資料帶進 node const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
this[mapType].vertices.forEach(node => { mapSource.vertices.forEach(node => {
switch (node.type) { switch (node.type) {
// add type of 'bpmn gateway' node
case 'gateway': case 'gateway':
mapData.nodes.push({ mapData.nodes.push({
data: { data:{
id: node.id, id:node.id,
type: node.type, type:node.type,
label: gateway[node.gateway_type], label:gateway[node.gateway_type],
height: 60, height:60,
width: 60, width:60,
backgroundColor: '#FFF', backgroundColor:'#FFF',
bordercolor: '#003366', bordercolor:'#003366',
shape: "diamond", shape:"diamond",
freq: logFreq, freq:logFreq,
duration: logDuration, duration:logDuration,
} }
}) })
break; break;
// add type of 'event' node
case 'event': case 'event':
if (node.event_type === 'start') { if(node.event_type === 'start') mapData.startId = node.id;
mapData.startId = node.id; else if(node.event_type === 'end') mapData.endId = node.id;
this.startNodeId = node.id;
}
else if (node.event_type === 'end') {
mapData.endId = node.id;
this.endNodeId = node.id;
}
mapData.nodes.push({ mapData.nodes.push({
data: { data:{
id: node.id, id:node.id,
type: node.type, type:node.type,
label: node.event_type, label:node.event_type,
height: 48, height: 48,
width: 48, width: 48,
backgroundColor: '#FFFFFF', backgroundColor:'#FFFFFF',
bordercolor: '#0F172A', bordercolor:'#0F172A',
textColor: '#FF3366', textColor: '#FF3366',
shape: "ellipse", shape:"ellipse",
freq: logFreq, freq:logFreq,
duration: logDuration, duration:logDuration,
} }
}); });
break; break;
// add type of 'activity' node
default: default:
mapData.nodes.push({ mapData.nodes.push({
data: { data:{
id: node.id, id:node.id,
type: node.type, type:node.type,
label: node.label, label:node.label,
height: 48, height: 48,
width: 216, width: 216,
textColor: '#0F172A', textColor: '#0F172A',
backgroundColor: 'rgba(0, 0, 0, 0)', backgroundColor:'rgba(0, 0, 0, 0)',
borderradius: 999, borderradius: 999,
shape: "round-rectangle", shape:"round-rectangle",
freq: node.freq, freq:node.freq,
duration: node.duration, duration:node.duration,
backgroundOpacity: 0, backgroundOpacity: 0,
borderOpacity: 0, borderOpacity: 0,
} }
@@ -380,14 +346,10 @@ export default {
break; break;
} }
}); });
}, }
/**
* 將 element edges 資料彙整 function setEdgesData(mapData) {
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。 const mapTypeVal = mapType.value;
*/
setEdgesData(mapData) {
const mapType = this.mapType;
//add event duration is empty
const logDuration = { const logDuration = {
"total": "", "total": "",
"rel_duration": "", "rel_duration": "",
@@ -399,61 +361,54 @@ export default {
}; };
mapData.edges = []; mapData.edges = [];
this[mapType].edges.forEach(edge => { const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
mapSource.edges.forEach(edge => {
mapData.edges.push({ mapData.edges.push({
data: { data: {
source: edge.tail, source:edge.tail,
target: edge.head, target:edge.head,
freq: edge.freq, freq:edge.freq,
duration: edge.duration === null ? logDuration : edge.duration, duration:edge.duration === null ? logDuration : edge.duration,
// Don't know why but tail is related to start and head is related to end style:'dotted',
edgeStyle: edge.tail === this.startNodeId || edge.head === this.endNodeId ? 'dotted' : 'solid', lineWidth:1,
lineWidth: 1,
}, },
}); });
}); });
}, }
/**
* create cytoscape's map
* @param {string} type this.mapType 'processMap' | 'bpmn',可傳入以上任一。
*/
async createCy(type) {
const graphId = document.getElementById('cy');
const mapData = type === 'processMap' ? this.processMapData : this.bpmnData;
if (this[type].vertices.length !== 0) { async function createCy(type) {
this.setNodesData(mapData); const graphId = document.getElementById('cy');
this.setEdgesData(mapData); const mapData = type === 'processMap'? processMapData.value: bpmnData.value;
this.setActivityBgImage(mapData); const mapSource = type === 'processMap' ? processMap.value : bpmn.value;
this.cytoscapeGraph = await cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
const processOrBPMN = this.mapType === 'processMap' ? 'process' : 'bpmn'; if(mapSource.vertices.length !== 0){
const curveType = this.curveStyle === 'taxi' ? 'elbow' : 'curved'; setNodesData(mapData);
const directionType = this.rank === 'LR' ? 'horizontal' : 'vertical'; setEdgesData(mapData);
await this.mapPathStore.setCytoscape(this.cytoscapeGraph, processOrBPMN, curveType, directionType); setActivityBgImage(mapData);
cytoscapeGraph.value = await cytoscapeMap(mapData, dataLayerType.value, dataLayerOption.value, curveStyle.value, rank.value, graphId);
const processOrBPMN = mapType.value === 'processMap' ? 'process' : 'bpmn';
const curveType = curveStyle.value === 'taxi' ? 'elbow' : 'curved';
const directionType = rank.value === 'LR' ? 'horizontal' : 'vertical';
await mapPathStore.setCytoscape(cytoscapeGraph.value, processOrBPMN, curveType, directionType);
}; };
}, }
setActivityBgImage(mapData) {
function setActivityBgImage(mapData) {
const nodes = mapData.nodes; const nodes = mapData.nodes;
// 一組有多少個activities
const groupSize = Math.floor(nodes.length / ImgCapsules.length); const groupSize = Math.floor(nodes.length / ImgCapsules.length);
let nodeOptionArr = []; let nodeOptionArr = [];
const leveledGroups = []; // 每一個level會使用不同的膠囊圖片 const leveledGroups = [];
// 設定除了 start, end 的 node 顏色
// 找出 type activity's node
const activityNodeArray = nodes.filter(node => node.data.type === 'activity'); const activityNodeArray = nodes.filter(node => node.data.type === 'activity');
// 找出除了 start, end 以外所有的 node 的 option value activityNodeArray.forEach(node => nodeOptionArr.push(node.data[dataLayerType.value][dataLayerOption.value]));
activityNodeArray.forEach(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption]));
// 將node的option值從小到大排序(映對色階淺到深)
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b); nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
for (let i = 0; i < ImgCapsules.length; i++) { for(let i = 0; i < ImgCapsules.length; i++) {
const startIdx = i * groupSize; const startIdx = i * groupSize;
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize; const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx)); leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
} }
for (let level = 0; level < leveledGroups.length; level++) { for(let level = 0; level < leveledGroups.length; level++) {
leveledGroups[level].forEach(option => { leveledGroups[level].forEach(option => {
// 考慮可能有名次一樣的情形 const curNodes = activityNodeArray.filter(activityNode => activityNode.data[dataLayerType.value][dataLayerOption.value] === option);
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option);
curNodes.forEach(curNode => { curNodes.forEach(curNode => {
curNode.data = { curNode.data = {
...curNode.data, ...curNode.data,
@@ -463,90 +418,65 @@ export default {
}); });
}); });
} }
}, }
},
async created() {
const routeParams = this.$route.params;
const file = this.$route.meta.file;
const isCheckPage = this.$route.name.includes('Check');
// 先 loading 再執行以下程式 // Created logic
this.isLoading = true; (async () => {
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁 const routeParams = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
isLoading.value = true;
switch (routeParams.type) { switch (routeParams.type) {
case 'log': case 'log':
if (!isCheckPage) { if(!isCheckPage) {
this.logId = await routeParams.fileId; logId.value = await routeParams.fileId;
this.baseLogId = await routeParams.fileId; baseLogId.value = await routeParams.fileId;
} else { } else {
this.logId = await file.parent.id; logId.value = await file.parent.id;
this.baseLogId = await file.parent.id; baseLogId.value = await file.parent.id;
} }
break; break;
case 'filter': case 'filter':
if (!isCheckPage) { if(!isCheckPage) {
this.createFilterId = await routeParams.fileId; createFilterId.value = await routeParams.fileId;
} else { } else {
this.createFilterId = await file.parent.id; createFilterId.value = await file.parent.id;
} }
// 取得 logID 和上次儲存的 Funnel await allMapDataStore.fetchFunnel(createFilterId.value);
await this.allMapDataStore.fetchFunnel(this.createFilterId); isRuleData.value = await Array.from(temporaryData.value);
this.isRuleData = await Array.from(this.temporaryData); ruleData.value = await isRuleData.value.map(e => sidebarFilterRefComp.value.setRule(e));
this.ruleData = await this.isRuleData.map(e => this.$refs.sidebarFilterRef.setRule(e));
break; break;
} }
// 取得 logId 後才 call api await allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllMapData(); await allMapDataStore.getAllTrace();
await this.allMapDataStore.getAllTrace();
// log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。 traceId.value = await traces.value[0]?.id;
this.traceId = await this.traces[0]?.id; baseTraceId.value = await baseTraces.value[0]?.id;
this.baseTraceId = await this.baseTraces[0]?.id; await createCy(mapType.value);
await this.createCy(this.mapType); await allMapDataStore.getFilterParams();
await this.allMapDataStore.getFilterParams(); await allMapDataStore.getTraceDetail();
await this.allMapDataStore.getTraceDetail();
// 執行完後才取消 loading isLoading.value = false;
this.isLoading = false; emitter.on('saveModal', boolean => {
// 存檔 Modal 打開時,側邊欄要關閉 sidebarView.value = boolean;
this.$emitter.on('saveModal', boolean => { sidebarFilter.value = boolean;
this.sidebarView = boolean; sidebarTraces.value = boolean;
this.sidebarFilter = boolean; sidebarState.value = boolean;
this.sidebarTraces = boolean;
this.sidebarState = boolean;
}); });
this.$emitter.on('leaveFilter', boolean => { emitter.on('leaveFilter', boolean => {
this.sidebarView = boolean; sidebarView.value = boolean;
this.sidebarFilter = boolean; sidebarFilter.value = boolean;
this.sidebarTraces = boolean; sidebarTraces.value = boolean;
this.sidebarState = boolean; sidebarState.value = boolean;
}); });
}, })();
beforeUnmount() {
this.logId = null;
this.createFilterId = null;
this.tempFilterId = null;
this.temporaryData = [];
this.postRuleData = [];
this.ruleData = [];
},
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
if (isCheckPage) { onBeforeUnmount(() => {
const conformanceStore = useConformanceStore(); logId.value = null;
switch (to.params.type) { createFilterId.value = null;
case 'log': temporaryData.value = [];
conformanceStore.conformanceLogCreateCheckId = to.params.fileId; postRuleData.value = [];
break; ruleData.value = [];
case 'filter': });
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile; // 將 file data 存到 route
}
next();
}
}
</script> </script>

View File

@@ -2,7 +2,7 @@
<Chart type="line" :data="primeVueSetDataState" :options="primeVueSetOptionsState" class="h-96" /> <Chart type="line" :data="primeVueSetDataState" :options="primeVueSetOptionsState" class="h-96" />
</template> </template>
<script> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { import {
setTimeStringFormatBaseOnTimeDifference, setTimeStringFormatBaseOnTimeDifference,
@@ -65,10 +65,7 @@ y: {
}, },
}; };
// 試著把 chart 獨立成一個 vue component const props = defineProps({
// 企圖防止 PrimeVue 誤用其他圖表 option 值的 bug
export default {
props: {
chartData: { chartData: {
type: Object, type: Object,
}, },
@@ -81,22 +78,21 @@ export default {
pageName: { pageName: {
type: String, type: String,
}, },
}, });
setup(props) {
const primeVueSetDataState = ref(null); const primeVueSetDataState = ref(null);
const primeVueSetOptionsState = ref(null); const primeVueSetOptionsState = ref(null);
const colorPrimary = ref('#0099FF'); const colorPrimary = ref('#0099FF');
const colorSecondary = ref('#FFAA44'); const colorSecondary = ref('#FFAA44');
/** /**
* Compare page and Performance have this same function. * Compare page and Performance have this same function.
* @param whichScaleObj PrimeVue scale option object to reference to * @param whichScaleObj PrimeVue scale option object to reference to
* @param customizeOptions * @param customizeOptions
* @param customizeOptions.content * @param customizeOptions.content
* @param customizeOptions.ticksOfXAxis * @param customizeOptions.ticksOfXAxis
*/ */
const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: { const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: {
content, content,
ticksOfXAxis, ticksOfXAxis,
}, },
@@ -105,15 +101,15 @@ export default {
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content); resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis); resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
return resultScaleObj; return resultScaleObj;
}; };
/** /**
* Compare page and Performance have this same function. * Compare page and Performance have this same function.
* @param {object} scaleObjectToAlter this object follows the format of prive vue chart * @param {object} scaleObjectToAlter this object follows the format of prive vue chart
* @param {Array<string>} ticksOfXAxis For example, ['05/06', '05,07', '05/08'] * @param {Array<string>} ticksOfXAxis For example, ['05/06', '05,07', '05/08']
* or ['08:03:01', '08:11:18', '09:03:41', ], and so on. * or ['08:03:01', '08:11:18', '09:03:41', ], and so on.
*/ */
const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => { const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
return { return {
...scaleObjectToAlter, ...scaleObjectToAlter,
x: { x: {
@@ -127,9 +123,9 @@ export default {
}, },
}, },
}; };
}; };
/** Compare page and Performance have this same function. /** Compare page and Performance have this same function.
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 content 的內容 * 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 content 的內容
* 之所以有辦法這樣撰寫,是因為我們知道物件的順序是先 x 再 title 再 text * 之所以有辦法這樣撰寫,是因為我們知道物件的順序是先 x 再 title 再 text
* This function alters the title property of known scales object of Chart option * This function alters the title property of known scales object of Chart option
@@ -139,7 +135,7 @@ export default {
* *
* @returns { object } an object modified with two titles * @returns { object } an object modified with two titles
*/ */
const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => { const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => {
if (!content) { if (!content) {
// Early return // Early return
return whichScaleObj; return whichScaleObj;
@@ -162,9 +158,9 @@ export default {
} }
} }
}; };
}; };
const getLineChartPrimeVueSetting = (chartData, content, pageName) => { const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
let datasetsArr; let datasetsArr;
let datasets; let datasets;
let datasetsPrimary; // For Compare page case let datasetsPrimary; // For Compare page case
@@ -268,17 +264,9 @@ export default {
}; };
primeVueSetDataState.value = primeVueSetData; primeVueSetDataState.value = primeVueSetData;
primeVueSetOptionsState.value = primeVueSetOption; primeVueSetOptionsState.value = primeVueSetOption;
};
onMounted(() => {
getLineChartPrimeVueSetting(props.chartData, props.content, props.pageName);
});
return {
...props,
primeVueSetDataState,
primeVueSetOptionsState,
};
}
}; };
onMounted(() => {
getLineChartPrimeVueSetting(props.chartData, props.content, props.pageName);
});
</script> </script>

View File

@@ -136,11 +136,36 @@
</main> </main>
</template> </template>
<script> <script>
import { storeToRefs, mapActions, } from 'pinia'; import { useConformanceStore } from '@/stores/conformance';
export default {
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
if (isCheckPage) {
const conformanceStore = useConformanceStore();
switch (to.params.type) {
case 'log':
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
break;
case 'filter':
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile;
}
next();
}
}
</script>
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import moment from 'moment'; import moment from 'moment';
import { useLoadingStore } from '@/stores/loading'; import { useLoadingStore } from '@/stores/loading';
import { usePerformanceStore } from '@/stores/performance'; import { usePerformanceStore } from '@/stores/performance';
import { useConformanceStore } from '@/stores/conformance';
import StatusBar from '@/components/Discover/StatusBar.vue'; import StatusBar from '@/components/Discover/StatusBar.vue';
import { setLineChartData } from '@/module/setChartData.js'; import { setLineChartData } from '@/module/setChartData.js';
import { simpleTimeLabel, followTimeLabel, import { simpleTimeLabel, followTimeLabel,
@@ -154,32 +179,24 @@ import { PRIME_VUE_TICKS_LIMIT } from '../../../constants/constants.js';
const primeVueTicksLimit = PRIME_VUE_TICKS_LIMIT; const primeVueTicksLimit = PRIME_VUE_TICKS_LIMIT;
export default { const route = useRoute();
setup() {
const loadingStore = useLoadingStore();
const performanceStore = usePerformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { performanceData } = storeToRefs(performanceStore);
return { isLoading, performanceStore, performanceData } // Stores
}, const loadingStore = useLoadingStore();
components: { const performanceStore = usePerformanceStore();
StatusBar, const { isLoading } = storeToRefs(loadingStore);
FreqChart, const { performanceData } = storeToRefs(performanceStore);
},
data() { // Data
return { const timeUsageData = [
timeUsageData: [
{tagId: '#cycleTime', label: 'Cycle Time & Efficiency'}, {tagId: '#cycleTime', label: 'Cycle Time & Efficiency'},
{tagId: '#processingTime', label: 'Processing Time'}, {tagId: '#processingTime', label: 'Processing Time'},
{tagId: '#waitingTime', label: 'Waiting Time'}, {tagId: '#waitingTime', label: 'Waiting Time'},
], ];
frequencyData: [ const frequencyData = [
{tagId: '#cases', label: 'Number of Cases'}, {tagId: '#cases', label: 'Number of Cases'},
// {tagId: '#trace', label: 'Number of Trace'}, ];
// {tagId: '#resource', label: 'Resource'}, const contentData = {
],
contentData: {
avgCycleTime: {title: 'Average Cycle Time', x: 'Date', y: 'Cycle time'}, avgCycleTime: {title: 'Average Cycle Time', x: 'Date', y: 'Cycle time'},
avgCycleEfficiency: {title: 'Cycle Efficiency', x: 'Date', y: 'Cycle efficiency (%)'}, avgCycleEfficiency: {title: 'Cycle Efficiency', x: 'Date', y: 'Cycle efficiency (%)'},
avgProcessTime: {title: 'Average Processing Time', x: 'Date', y: 'Processing time'}, avgProcessTime: {title: 'Average Processing Time', x: 'Date', y: 'Processing time'},
@@ -188,8 +205,8 @@ export default {
avgWaitingTimeByEdge: {title: 'Average Waiting Time between Activity', x: 'Waiting time', y: 'Activities'}, avgWaitingTimeByEdge: {title: 'Average Waiting Time between Activity', x: 'Waiting time', y: 'Activities'},
freq: {title: 'New Cases', x: 'Date', y: 'Count'}, freq: {title: 'New Cases', x: 'Date', y: 'Count'},
casesByTask: {title: 'Number of Cases by Activity', x: 'Count', y: 'Activity'}, casesByTask: {title: 'Number of Cases by Activity', x: 'Count', y: 'Activity'},
}, };
tooltip: { const tooltip = {
avgCycleEfficiency: { avgCycleEfficiency: {
value: 'Cycle Efficiency: The ratio of the total productive time to the total cycle time of a process. Productive time refers to the time during which value-adding activities are performed, while cycle time is the total time from the start to the end of the process, including both productive and non-productive periods.', value: 'Cycle Efficiency: The ratio of the total productive time to the total cycle time of a process. Productive time refers to the time during which value-adding activities are performed, while cycle time is the total time from the start to the end of the process, including both productive and non-productive periods.',
class: '!max-w-[212px] !text-[10px] !opacity-80', class: '!max-w-[212px] !text-[10px] !opacity-80',
@@ -205,54 +222,47 @@ export default {
class: '!max-w-[212px] !text-[10px] !opacity-80', class: '!max-w-[212px] !text-[10px] !opacity-80',
autoHide: false, autoHide: false,
}, },
}, };
isActive: null,
avgCycleTimeData: null,
avgCycleTimeOptions: null,
avgCycleEfficiencyData: null,
avgCycleEfficiencyOptions: null,
avgProcessTimeData: null,
avgProcessTimeOptions: null,
avgProcessTimeByTaskData: null,
avgProcessTimeByTaskOptions: null,
avgWaitingTimeData: null,
avgWaitingTimeOptions: null,
avgWaitingTimeByEdgeData: null,
avgWaitingTimeByEdgeOptions: null,
freqData: null,
freqOptions: null,
casesByTaskData: null,
casesByTaskOptions: null,
horizontalBarHeight: 500, // horizontal Bar default height
avgProcessTimeByTaskHeight: 500,
avgWaitingTimeByEdgeHeight: 500,
casesByTaskHeight: 500
}
},
methods: {
handleClick(tagId) {
this.isActive = tagId;
// 在進行導航前,檢查或處理 tagId 的值 const isActive = ref(null);
if (this.isSafeTagId(tagId)) { const avgCycleTimeData = ref(null);
window.location.href = tagId; // 確保這個路徑是安全的 const avgCycleTimeOptions = ref(null);
// 或者使用 Vue Router 進行導航 const avgCycleEfficiencyData = ref(null);
// this.$router.push({ path: tagId }); const avgCycleEfficiencyOptions = ref(null);
const avgProcessTimeData = ref(null);
const avgProcessTimeOptions = ref(null);
const avgProcessTimeByTaskData = ref(null);
const avgProcessTimeByTaskOptions = ref(null);
const avgWaitingTimeData = ref(null);
const avgWaitingTimeOptions = ref(null);
const avgWaitingTimeByEdgeData = ref(null);
const avgWaitingTimeByEdgeOptions = ref(null);
const freqData = ref(null);
const freqOptions = ref(null);
const casesByTaskData = ref(null);
const casesByTaskOptions = ref(null);
const horizontalBarHeight = 500;
const avgProcessTimeByTaskHeight = ref(500);
const avgWaitingTimeByEdgeHeight = ref(500);
const casesByTaskHeight = ref(500);
// Methods
function handleClick(tagId) {
isActive.value = tagId;
if (isSafeTagId(tagId)) {
window.location.href = tagId;
} else { } else {
console.warn("不安全的 tagId: ", tagId); console.warn("不安全的 tagId: ", tagId);
} }
}, }
// 避免直接使用動態 href改用安全的方法來處理動態導航避免直接將未經驗證的數據綁定到 href 屬性。
isSafeTagId(tagId) { function isSafeTagId(tagId) {
// 檢查 tagId 是否符合安全的格式(例如只允許特定的模式或路徑) const pattern = /^#?[a-zA-Z0-9-]*$/;
const pattern = /^#?[a-zA-Z0-9-]*$/; // 例如: #waitingTime
return pattern.test(tagId); return pattern.test(tagId);
}, }
/**
* 手刻折線圖 x label 時間刻度 function setXLabelsData(valueData) {
* @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'}
*/
setXLabelsData(valueData) {
const min = new Date(valueData.min).getTime(); const min = new Date(valueData.min).getTime();
const max = new Date(valueData.max).getTime(); const max = new Date(valueData.max).getTime();
const numPoints = 12; const numPoints = 12;
@@ -263,26 +273,19 @@ export default {
data.push(x); data.push(x);
} }
return data; return data;
}, }
/**
* 讓長條圖依 data 數量增加高度 function getHorizontalBarHeight(chartData) {
* @param { object } chartData chart data
*/
getHorizontalBarHeight(chartData) {
const totalBars = chartData.data.length; const totalBars = chartData.data.length;
let horizontalBar = this.horizontalBarHeight; let hBarHeight = horizontalBarHeight;
if(totalBars > 10) horizontalBar = (totalBars - 10) * 16 + this.horizontalBarHeight; if(totalBars > 10) hBarHeight = (totalBars - 10) * 16 + horizontalBarHeight;
return horizontalBar + 'px' return hBarHeight + 'px'
}, }
/**
* 建立長條圖 function getBarChart(chartData, content) {
* @param { object } chartData chart data const getMoment = (time)=> moment(time).format('YYYY/M/D hh:mm:ss');
* @param { object } content titels 標題文字
*/
getBarChart(chartData, content) {
const getMoment = (time)=> this.$moment(time).format('YYYY/M/D hh:mm:ss');
let primeVueSetData = {}; let primeVueSetData = {};
let primeVueSetOption = {}; let primeVueSetOption = {};
@@ -291,7 +294,7 @@ export default {
x: getMoment(value.x), x: getMoment(value.x),
y: value.y * 100 y: value.y * 100
} }
}); // 轉為百分比 });
const xData = datasets.map(i => i.x); const xData = datasets.map(i => i.x);
const yData = datasets.map(i => i.y) const yData = datasets.map(i => i.y)
@@ -316,7 +319,7 @@ export default {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false,
tooltip: { tooltip: {
displayColors: false, displayColors: false,
titleFont: {weight: 'normal'}, titleFont: {weight: 'normal'},
@@ -359,7 +362,7 @@ export default {
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true,
title: { title: {
display: true, display: true,
text: content.y, text: content.y,
@@ -378,29 +381,22 @@ export default {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false,
}, },
}, },
}, },
}; };
return [primeVueSetData, primeVueSetOption] return [primeVueSetData, primeVueSetOption]
}, }
/**
* 建立水平長條圖 function getHorizontalBarChart(chartData, content, isSingle, xUnit) {
* @param { object } chartData chart data
* @param { object } content titels 標題文字
* @param { boolean } isSingle 單個或雙數 activity
* @param { string } xUnit x 軸單位 'date' | 'count',可傳入以上任一。
*/
getHorizontalBarChart(chartData, content, isSingle, xUnit) {
const maxY = chartData.y_axis.max; const maxY = chartData.y_axis.max;
const getSimpleTimeLabel = simpleTimeLabel; const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel; const getFollowTimeLabel = followTimeLabel;
let primeVueSetData = {}; let primeVueSetData = {};
let primeVueSetOption = {}; let primeVueSetOption = {};
// 大到小排序
chartData.data.sort((a, b) => b.y - a.y); chartData.data.sort((a, b) => b.y - a.y);
const xData = chartData.data.map(item => item.x); const xData = chartData.data.map(item => item.x);
const yData = chartData.data.map(item => item.y); const yData = chartData.data.map(item => item.y);
@@ -427,7 +423,7 @@ export default {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false,
tooltip: { tooltip: {
displayColors: false, displayColors: false,
titleFont: {weight: 'normal'}, titleFont: {weight: 'normal'},
@@ -450,7 +446,7 @@ export default {
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0,
color: '#64748b', color: '#64748b',
}, },
grid: { grid: {
@@ -461,7 +457,7 @@ export default {
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true,
type: 'category', type: 'category',
title: { title: {
display: true, display: true,
@@ -482,7 +478,7 @@ export default {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false,
}, },
}, },
}, },
@@ -498,13 +494,13 @@ export default {
break; break;
case 'count': case 'count':
default: default:
primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位 primeVueSetOption.scales.x.ticks.precision = 0;
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.x}: ${context.parsed.x}`; return `${content.x}: ${context.parsed.x}`;
} }
break; break;
} }
if(isSingle) { // 設定一個活動的 y label、提示框文字 if(isSingle) {
primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { primeVueSetOption.plugins.tooltip.callbacks.title = function(context) {
return `${content.y}: ${context[0].label}`; return `${content.y}: ${context[0].label}`;
}; };
@@ -512,7 +508,7 @@ export default {
const label = xData[index]; const label = xData[index];
return label.length > 21 ? `${label.substring(0, 18)}...` : label return label.length > 21 ? `${label.substring(0, 18)}...` : label
}; };
}else { // 設定「活動」到「活動」的 y label、提示框文字 }else {
primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { primeVueSetOption.plugins.tooltip.callbacks.title = function(context) {
return `${content.y}: ${context[0].label.replace(',', ' - ')}` return `${content.y}: ${context[0].label.replace(',', ' - ')}`
}; };
@@ -528,37 +524,21 @@ export default {
} }
return [primeVueSetData, primeVueSetOption] return [primeVueSetData, primeVueSetOption]
}, }
/**
* Compare page and Performance have this same function. function getCustomizedScaleOption(whichScaleObj, {customizeOptions: {
* @param whichScaleObj PrimeVue scale option object to reference to
* @param customizeOptions
* @param customizeOptions.content
* @param customizeOptions.ticksOfXAxis
*/
getCustomizedScaleOption(whichScaleObj, {customizeOptions: {
content, content,
ticksOfXAxis, ticksOfXAxis,
}, },
}) { }) {
let resultScaleObj; let resultScaleObj;
resultScaleObj = this.customizeScaleChartOptionTitleByContent(whichScaleObj, content); resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
resultScaleObj = this.customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis); resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
return resultScaleObj; return resultScaleObj;
}, }
/** Compare page and Performance have this same function.
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 content 的內容 function customizeScaleChartOptionTitleByContent(whichScaleObj, content){
* 之所以有辦法這樣撰寫,是因為我們知道物件的順序是先 x 再 title 再 text
* This function alters the title property of known scales object of Chart option
* This is based on the fact that we know the order must be x -> title -> text.
* @param {object} whichScaleObj PrimeVue scale option object to reference to
* @param content whose property includes x and y and stand for titles
*
* @returns { object } an object modified with two titles
*/
customizeScaleChartOptionTitleByContent(whichScaleObj, content){
if (!content) { if (!content) {
// Early return
return whichScaleObj; return whichScaleObj;
} }
@@ -579,14 +559,9 @@ export default {
} }
} }
}; };
}, }
/**
* Compare page and Performance have this same function. function customizeScaleChartOptionTicks(scaleObjectToAlter, ticksOfXAxis) {
* @param {object} scaleObjectToAlter this object follows the format of prive vue chart
* @param {Array<string>} ticksOfXAxis For example, ['05/06', '05,07', '05/08']
* or ['08:03:01', '08:11:18', '09:03:41', ], and so on.
*/
customizeScaleChartOptionTicks(scaleObjectToAlter, ticksOfXAxis) {
return { return {
...scaleObjectToAlter, ...scaleObjectToAlter,
x: { x: {
@@ -599,14 +574,9 @@ export default {
}, },
}, },
}; };
}, }
/**
* 建立Performance頁面的折線圖並且避免同一個畫面中的設定值彼此覆蓋 function getExplicitDeclaredLineChart(chartData, content, yUnit) {
* @param { object } chartData chart data
* @param { object } content titels 標題文字
* @param { string } yUnit y 軸單位 'date'
*/
getExplicitDeclaredLineChart(chartData, content, yUnit) {
const minX = chartData.x_axis.min; const minX = chartData.x_axis.min;
const maxX = chartData.x_axis.max; const maxX = chartData.x_axis.max;
let primeVueSetData = {}; let primeVueSetData = {};
@@ -615,11 +585,9 @@ export default {
const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max,
chartData.y_axis.min); chartData.y_axis.min);
const xData = this.setXLabelsData(chartData.x_axis); const xData = setXLabelsData(chartData.x_axis);
// Customize X axis ticks due to different differences between min and max of data group
// Compare page and Performance page share the same logic
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX); const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX);
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet); const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
primeVueSetData = { primeVueSetData = {
@@ -629,7 +597,7 @@ export default {
label: content.title, label: content.title,
data: datasets, data: datasets,
fill: false, fill: false,
tension: 0, // 貝茲曲線張力 tension: 0,
borderColor: '#0099FF', borderColor: '#0099FF',
} }
] ]
@@ -645,7 +613,7 @@ export default {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false,
tooltip: { tooltip: {
displayColors: false, displayColors: false,
titleFont: {weight: 'normal'}, titleFont: {weight: 'normal'},
@@ -675,11 +643,11 @@ export default {
}, },
time: { time: {
displayFormats: { displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11 second: 'h:mm:ss',
minute: 'M/d h:mm', // ex: 1/1 1:11 minute: 'M/d h:mm',
hour: 'M/d h:mm', // ex: 1/1 1:11 hour: 'M/d h:mm',
day: 'M/d h', // ex: 1/1 1 day: 'M/d h',
month: 'y/M/d', // ex: 1911/1/1 month: 'y/M/d',
}, },
round: true round: true
}, },
@@ -687,9 +655,9 @@ export default {
maxTicksLimit: primeVueTicksLimit, maxTicksLimit: primeVueTicksLimit,
padding: 8, padding: 8,
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0,
color: '#64748b', color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量 source: 'labels',
callback: function(value, index) { callback: function(value, index) {
return ticksOfXAxis[index]; return ticksOfXAxis[index];
}, },
@@ -699,7 +667,7 @@ export default {
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true,
title: { title: {
display: true, display: true,
color: '#334155', color: '#334155',
@@ -713,14 +681,13 @@ export default {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false,
}, },
ticks: { ticks: {
color: '#64748b', color: '#64748b',
padding: 8, padding: 8,
callback: function (value, index, ticks) { callback: function (value, index, ticks) {
// resultStepSize: Y 軸一個刻度的高度的純數值部分unitToUse則可能是 d,h,m,s 四者之一
const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length); const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length);
return getYTicksByIndex(resultStepSize, index, unitToUse); return getYTicksByIndex(resultStepSize, index, unitToUse);
}, },
@@ -730,27 +697,21 @@ export default {
}; };
return [primeVueSetData, primeVueSetOption] return [primeVueSetData, primeVueSetOption]
}, }
/**
* 建立Average Waiting Time折線圖 function getAvgWaitingTimeLineChart(chartData, content, yUnit) {
* @param { object } chartData chart data const getMoment = (time)=> moment(time).format('YYYY/M/D hh:mm:ss');
* @param { object } content titels 標題文字
* @param { string } yUnit y 軸單位 'date'
*/
getAvgWaitingTimeLineChart(chartData, content, yUnit) {
const getMoment = (time)=> this.$moment(time).format('YYYY/M/D hh:mm:ss');
const minX = chartData.x_axis.min; const minX = chartData.x_axis.min;
const maxX = chartData.x_axis.max;
let primeVueSetData = {}; let primeVueSetData = {};
let primeVueSetOption = {}; let primeVueSetOption = {};
const getSimpleTimeLabel = simpleTimeLabel; const getSimpleTimeLabel = simpleTimeLabel;
const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max,
chartData.y_axis.min); chartData.y_axis.min);
const xData = this.setXLabelsData(chartData.x_axis); const xData = setXLabelsData(chartData.x_axis);
// Customize X axis ticks due to different differences between min and max of data group
// Compare page and Performance page share the same logic
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX); const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX);
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet); const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
@@ -761,7 +722,7 @@ export default {
label: content.title, label: content.title,
data: datasets, data: datasets,
fill: false, fill: false,
tension: 0, // 貝茲曲線張力 tension: 0,
borderColor: '#0099FF', borderColor: '#0099FF',
} }
] ]
@@ -777,7 +738,7 @@ export default {
} }
}, },
plugins: { plugins: {
legend: false, // 圖例 legend: false,
tooltip: { tooltip: {
displayColors: false, displayColors: false,
titleFont: {weight: 'normal'}, titleFont: {weight: 'normal'},
@@ -810,19 +771,19 @@ export default {
}, },
time: { time: {
displayFormats: { displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11 second: 'h:mm:ss',
minute: 'M/d h:mm', // ex: 1/1 1:11 minute: 'M/d h:mm',
hour: 'M/d h:mm', // ex: 1/1 1:11 hour: 'M/d h:mm',
day: 'M/d h', // ex: 1/1 1 day: 'M/d h',
month: 'y/M/d', // ex: 1911/1/1 month: 'y/M/d',
}, },
round: true round: true
}, },
ticks: { ticks: {
display: true, display: true,
maxRotation: 0, // 不旋轉 lable 0~50 maxRotation: 0,
color: '#64748b', color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量 source: 'labels',
callback: function(value, index) { callback: function(value, index) {
return ticksOfXAxis[index]; return ticksOfXAxis[index];
}, },
@@ -832,7 +793,7 @@ export default {
}, },
}, },
y: { y: {
beginAtZero: true, // scale 包含 0 beginAtZero: true,
title: { title: {
display: true, display: true,
color: '#334155', color: '#334155',
@@ -846,14 +807,13 @@ export default {
color: '#64748b', color: '#64748b',
}, },
border: { border: {
display: false, // 隱藏左側多出來的線 display: false,
}, },
ticks: { ticks: {
color: '#64748b', color: '#64748b',
padding: 8, padding: 8,
callback: function (value, index, ticks) { callback: function (value, index, ticks) {
// resultStepSize: Y 軸一個刻度的高度的純數值部分unitToUse則可能是 d,h,m,s 四者之一
const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length); const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length);
return getYTicksByIndex(resultStepSize, index, unitToUse); return getYTicksByIndex(resultStepSize, index, unitToUse);
}, },
@@ -863,19 +823,15 @@ export default {
}; };
return [primeVueSetData, primeVueSetOption] return [primeVueSetData, primeVueSetOption]
}, }
...mapActions(usePerformanceStore, [
'setFreqChartData', // Created logic
'setFreqChartOptions', (async () => {
'setFreqChartXData' isLoading.value = true;
]), const routeParams = route.params;
}, const isCheckPage = route.name.includes('Check');
async created() {
this.isLoading = true; // moubeted 才停止 loading
const routeParams = this.$route.params;
const isCheckPage = this.$route.name.includes('Check');
const type = routeParams.type; const type = routeParams.type;
const file = this.$route.meta.file; const file = route.meta.file;
let id; let id;
if(!isCheckPage) { if(!isCheckPage) {
@@ -885,54 +841,35 @@ export default {
} }
// 取得 Performance Data // 取得 Performance Data
await this.performanceStore.getPerformance(type, id); await performanceStore.getPerformance(type, id);
this.avgProcessTimeByTaskHeight = await this.getHorizontalBarHeight(this.performanceData.time.avg_process_time_by_task); avgProcessTimeByTaskHeight.value = await getHorizontalBarHeight(performanceData.value.time.avg_process_time_by_task);
if(this.performanceData.time.avg_waiting_time_by_edge !== null) { if(performanceData.value.time.avg_waiting_time_by_edge !== null) {
this.avgWaitingTimeByEdgeHeight = await this.getHorizontalBarHeight(this.performanceData.time.avg_waiting_time_by_edge); avgWaitingTimeByEdgeHeight.value = await getHorizontalBarHeight(performanceData.value.time.avg_waiting_time_by_edge);
} }
this.casesByTaskHeight = await this.getHorizontalBarHeight(this.performanceData.freq.cases_by_task); casesByTaskHeight.value = await getHorizontalBarHeight(performanceData.value.freq.cases_by_task);
// create chart // create chart
[this.avgCycleTimeData, this.avgCycleTimeOptions] = this.getExplicitDeclaredLineChart(this.performanceData.time.avg_cycle_time, [avgCycleTimeData.value, avgCycleTimeOptions.value] = getExplicitDeclaredLineChart(performanceData.value.time.avg_cycle_time,
this.contentData.avgCycleTime, 'date'); contentData.avgCycleTime, 'date');
[this.avgCycleEfficiencyData, this.avgCycleEfficiencyOptions] = this.getBarChart( [avgCycleEfficiencyData.value, avgCycleEfficiencyOptions.value] = getBarChart(
this.performanceData.time.avg_cycle_efficiency, this.contentData.avgCycleEfficiency); performanceData.value.time.avg_cycle_efficiency, contentData.avgCycleEfficiency);
[this.avgProcessTimeData, this.avgProcessTimeOptions] = this.getExplicitDeclaredLineChart(this.performanceData.time.avg_process_time, [avgProcessTimeData.value, avgProcessTimeOptions.value] = getExplicitDeclaredLineChart(performanceData.value.time.avg_process_time,
this.contentData.avgProcessTime, 'date'); contentData.avgProcessTime, 'date');
[this.avgProcessTimeByTaskData, this.avgProcessTimeByTaskOptions] = this.getHorizontalBarChart( [avgProcessTimeByTaskData.value, avgProcessTimeByTaskOptions.value] = getHorizontalBarChart(
this.performanceData.time.avg_process_time_by_task, this.contentData.avgProcessTimeByTask, true, 'date'); performanceData.value.time.avg_process_time_by_task, contentData.avgProcessTimeByTask, true, 'date');
[this.avgWaitingTimeData, this.avgWaitingTimeOptions] = this.getExplicitDeclaredLineChart( [avgWaitingTimeData.value, avgWaitingTimeOptions.value] = getExplicitDeclaredLineChart(
this.performanceData.time.avg_waiting_time, this.contentData.avgWaitingTime, 'date'); performanceData.value.time.avg_waiting_time, contentData.avgWaitingTime, 'date');
if(this.performanceData.time.avg_waiting_time_by_edge !== null) { if(performanceData.value.time.avg_waiting_time_by_edge !== null) {
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = this.getHorizontalBarChart( [avgWaitingTimeByEdgeData.value, avgWaitingTimeByEdgeOptions.value] = getHorizontalBarChart(
this.performanceData.time.avg_waiting_time_by_edge, this.contentData.avgWaitingTimeByEdge, false, 'date'); performanceData.value.time.avg_waiting_time_by_edge, contentData.avgWaitingTimeByEdge, false, 'date');
} else { } else {
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = [null, null] [avgWaitingTimeByEdgeData.value, avgWaitingTimeByEdgeOptions.value] = [null, null]
} }
[this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.performanceData.freq.cases_by_task, [casesByTaskData.value, casesByTaskOptions.value] = getHorizontalBarChart(performanceData.value.freq.cases_by_task,
this.contentData.casesByTask, true, 'count'); contentData.casesByTask, true, 'count');
// 停止 loading // 停止 loading
this.isLoading = false; isLoading.value = false;
}, })();
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
if (isCheckPage) {
const conformanceStore = useConformanceStore();
switch (to.params.type) {
case 'log':
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
break;
case 'filter':
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.routeFile; // 將 file data 存到 route
}
next();
}
}
</script> </script>
<style scoped> <style scoped>
@reference "../../../assets/tailwind.css"; @reference "../../../assets/tailwind.css";

View File

@@ -71,7 +71,7 @@
<span v-show="secondaryDragData.length > 0" class="material-symbols-outlined material-fill bg-neutral-10 text-neutral-500 block rounded-full absolute -top-[5%] -right-[5%] cursor-pointer hover:text-danger" @click="secondaryDragDelete <span v-show="secondaryDragData.length > 0" class="material-symbols-outlined material-fill bg-neutral-10 text-neutral-500 block rounded-full absolute -top-[5%] -right-[5%] cursor-pointer hover:text-danger" @click="secondaryDragDelete
">do_not_disturb_on</span> ">do_not_disturb_on</span>
</div> </div>
<button class="btn btn-sm" :class="this.isCompareDisabledButton ? 'btn-disable' : 'btn-c-primary'" :disabled="isCompareDisabledButton" @click="compareSubmit">Compare</button> <button class="btn btn-sm" :class="isCompareDisabledButton ? 'btn-disable' : 'btn-c-primary'" :disabled="isCompareDisabledButton" @click="compareSubmit">Compare</button>
</div> </div>
</section> </section>
<!-- Recently Used --> <!-- Recently Used -->
@@ -209,7 +209,7 @@
</section> </section>
</div> </div>
<!-- ContextMenu --> <!-- ContextMenu -->
<ContextMenu ref="fileRightMenu" :model="items" @hide="selectedFile = null" class="cursor-pointer"> <ContextMenu ref="fileRightMenuRef" :model="items" @hide="selectedFile = null" class="cursor-pointer">
<template #item="{ item }"> <template #item="{ item }">
<a class="flex align-items-center px-4 py-2 duration-300 hover:bg-primary/20"> <a class="flex align-items-center px-4 py-2 duration-300 hover:bg-primary/20">
<span class="material-symbols-outlined">{{ item.icon }}</span> <span class="material-symbols-outlined">{{ item.icon }}</span>
@@ -220,8 +220,10 @@
</ContextMenu> </ContextMenu>
</div> </div>
</template> </template>
<script> <script setup>
import { storeToRefs, mapActions, } from 'pinia'; import { ref, computed, watch, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useMapCompareStore } from '@/stores/mapCompareStore'; import { useMapCompareStore } from '@/stores/mapCompareStore';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
import { useFilesStore } from '@/stores/files'; import { useFilesStore } from '@/stores/files';
@@ -237,43 +239,57 @@
import IconGrid from '@/components/icons/IconGrid.vue'; import IconGrid from '@/components/icons/IconGrid.vue';
import { renameModal, deleteFileModal, reallyDeleteInformation } from '@/module/alertModal.js'; import { renameModal, deleteFileModal, reallyDeleteInformation } from '@/module/alertModal.js';
export default { const router = useRouter();
data() {
return { // Stores
mapCompareStore: useMapCompareStore(), const mapCompareStore = useMapCompareStore();
isActive: null, const loginStore = useLoginStore();
isHover: null, const store = useFilesStore();
switchListOrGrid: false, const allMapDataStore = useAllMapDataStore();
selectedTableFile: null, // table 右鍵選單 item const pageAdminStore = usePageAdminStore();
selectedFile: null, // 右鍵選單 item const loadingStore = useLoadingStore();
selectedType: null,
selectedId: null, const { dependentsData, filesTag } = storeToRefs(store);
selectedName: null, const { createFilterId, baseLogId } = storeToRefs(allMapDataStore);
items: [ const { isLoading } = storeToRefs(loadingStore);
// Data
const isActive = ref(null);
const isHover = ref(null);
const switchListOrGrid = ref(false);
const selectedTableFile = ref(null);
const selectedFile = ref(null);
const selectedType = ref(null);
const selectedId = ref(null);
const selectedName = ref(null);
const compareData = ref(null);
const primaryDragData = ref([]);
const secondaryDragData = ref([]);
const gridSort = ref(null);
const fileRightMenuRef = ref(null);
const items = [
{ {
label: 'Rename', label: 'Rename',
icon: 'edit_square', icon: 'edit_square',
command: this.rename, command: rename,
}, },
{ {
label: 'Download', label: 'Download',
icon: 'download', icon: 'download',
command: this.download, command: download,
}, },
{ {
separator: true // 分隔符號 separator: true
}, },
{ {
label: 'Delete', label: 'Delete',
icon: 'delete', icon: 'delete',
command: this.deleteFile, command: deleteFile,
}, },
], ];
compareData: null,
primaryDragData: [], const columnType = [
secondaryDragData: [],
gridSort: null,
columnType: [
{ name: 'By File Name (A to Z)', code: 'nameAscending'}, { name: 'By File Name (A to Z)', code: 'nameAscending'},
{ name: 'By File Name (Z to A)', code: 'nameDescending'}, { name: 'By File Name (Z to A)', code: 'nameDescending'},
{ name: 'By Dependency (A to Z)', code: 'parentLogAscending'}, { name: 'By Dependency (A to Z)', code: 'parentLogAscending'},
@@ -282,177 +298,157 @@
{ name: 'By File Type (Z to A)', code: 'fileDescending'}, { name: 'By File Type (Z to A)', code: 'fileDescending'},
{ name: 'By Last Update (A to Z)', code: 'updatedAscending'}, { name: 'By Last Update (A to Z)', code: 'updatedAscending'},
{ name: 'By Last Update (Z to A)', code: 'updatedDescending'}, { name: 'By Last Update (Z to A)', code: 'updatedDescending'},
], ];
}
},
setup() {
const loginStore = useLoginStore();
const store = useFilesStore();
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const { dependentsData, filesTag } = storeToRefs(store);
const { createFilterId, baseLogId } = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
return { loginStore, store, dependentsData, filesTag, allMapDataStore, createFilterId, baseLogId, isLoading } // Computed
},
components: {
IconDataFormat,
IconRule,
IconsFilter,
IconFlowChart,
IconVector,
IconList,
IconGrid
},
computed: {
/** /**
* Read allFiles * Read allFiles
*/ */
allFiles: function() { const allFiles = computed(() => {
if(this.store.allFiles.length !== 0){ if(store.allFiles.length !== 0){
const sortFiles = Array.from(this.store.allFiles); const sortFiles = Array.from(store.allFiles);
sortFiles.sort((x,y) => new Date(y.updated_base) - new Date(x.updated_base)); sortFiles.sort((x,y) => new Date(y.updated_base) - new Date(x.updated_base));
return sortFiles; return sortFiles;
} }
}, });
/** /**
* 時間排序,如果沒有 accessed_at 就不加入 data * 時間排序,如果沒有 accessed_at 就不加入 data
*/ */
recentlyUsedFiles: function() { const recentlyUsedFiles = computed(() => {
let recentlyUsedFiles = Array.from(this.store.allFiles); let recentlyUsed = Array.from(store.allFiles);
recentlyUsedFiles = recentlyUsedFiles.filter(item => item.accessed_at !== null); recentlyUsed = recentlyUsed.filter(item => item.accessed_at !== null);
recentlyUsedFiles.sort((x, y) => new Date(y.accessed_base) - new Date(x.accessed_base)); recentlyUsed.sort((x, y) => new Date(y.accessed_base) - new Date(x.accessed_base));
return recentlyUsedFiles; return recentlyUsed;
}, });
/** /**
* Compare Submit button disabled * Compare Submit button disabled
*/ */
isCompareDisabledButton: function() { const isCompareDisabledButton = computed(() => {
const result = this.primaryDragData.length === 0 || this.secondaryDragData.length === 0; return primaryDragData.value.length === 0 || secondaryDragData.value.length === 0;
return result; });
},
/** /**
* Really deleted information * Really deleted information
*/ */
reallyDeleteData: function() { const reallyDeleteData = computed(() => {
let result = []; let result = [];
if(store.allFiles.length !== 0){
if(this.store.allFiles.length !== 0){ result = JSON.parse(JSON.stringify(store.allFiles));
result = JSON.parse(JSON.stringify(this.store.allFiles));
result = result.filter(file => file.is_deleted === true); result = result.filter(file => file.is_deleted === true);
} }
return result;
});
return result // Watch
} watch(filesTag, (newValue) => {
},
watch: {
filesTag: {
handler(newValue) {
if(newValue !== 'COMPARE'){ if(newValue !== 'COMPARE'){
this.primaryDragData = []; primaryDragData.value = [];
this.secondaryDragData = []; secondaryDragData.value = [];
} }
} });
},
allFiles: { watch(allFiles, (newValue) => {
handler(newValue) { if(newValue !== null) compareData.value = JSON.parse(JSON.stringify(newValue));
if(newValue !== null) this.compareData = JSON.parse(JSON.stringify(newValue)); });
}
}, watch(reallyDeleteData, (newValue, oldValue) => {
reallyDeleteData: {
handler(newValue, oldValue) {
if(newValue.length !== 0 && oldValue.length === 0){ if(newValue.length !== 0 && oldValue.length === 0){
this.showReallyDelete(); showReallyDelete();
} }
}, }, { immediate: true });
immediate: true
} // Methods
},
methods: {
/** /**
* Set Row Style * Set Row Style
*/ */
setRowClass() { function setRowClass() {
return ['group'] return ['group'];
}, }
/** /**
* Set Compare Row Style * Set Compare Row Style
*/ */
setCompareRowClass() { function setCompareRowClass() {
return ['leading-6'] return ['leading-6'];
}, }
/** /**
* 選擇該 files 進入 Discover/Compare/Design 頁面 * 選擇該 files 進入 Discover/Compare/Design 頁面
* @param {object} file 該 file 的詳細資料 * @param {object} file 該 file 的詳細資料
*/ */
enterDiscover(file){ function enterDiscover(file){
let type; let type;
let fileId; let fileId;
let params; let params;
this.setCurrentMapFile(file.name); pageAdminStore.setCurrentMapFile(file.name);
switch (file.type) { switch (file.type) {
case 'log': case 'log':
this.createFilterId = null; createFilterId.value = null;
this.baseLogId = file.id; baseLogId.value = file.id;
fileId = file.id; fileId = file.id;
type = file.type; type = file.type;
params = { type: type, fileId: fileId }; params = { type: type, fileId: fileId };
this.$router.push({name: 'Map', params: params}); router.push({name: 'Map', params: params});
break; break;
case 'filter': case 'filter':
this.createFilterId = file.id; createFilterId.value = file.id;
this.baseLogId = file.parent.id; baseLogId.value = file.parent.id;
fileId = file.id; fileId = file.id;
type = file.type; type = file.type;
params = { type: type, fileId: fileId }; params = { type: type, fileId: fileId };
this.$router.push({name: 'Map', params: params}); router.push({name: 'Map', params: params});
break; break;
case 'log-check': case 'log-check':
case 'filter-check': case 'filter-check':
fileId = file.id; fileId = file.id;
type = file.parent.type; type = file.parent.type;
params = { type: type, fileId: fileId }; params = { type: type, fileId: fileId };
this.$router.push({name: 'CheckConformance', params: params}); router.push({name: 'CheckConformance', params: params});
break; break;
default: default:
break; break;
} }
}, }
/** /**
* Right Click DOM Event * Right Click DOM Event
* @param {event} event 該 file 的詳細資料 * @param {event} event 該 file 的詳細資料
* @param {string} file file's name * @param {string} file file's name
*/ */
onRightClick(event, file) { function onRightClick(event, file) {
this.selectedType = file.type; selectedType.value = file.type;
this.selectedId = file.id; selectedId.value = file.id;
this.selectedName = file.name; selectedName.value = file.name;
this.$refs.fileRightMenu.show(event) fileRightMenuRef.value.show(event);
}, }
/** /**
* Right Click Table DOM Event * Right Click Table DOM Event
* @param {event} event 該 file 的詳細資料 * @param {event} event 該 file 的詳細資料
*/ */
onRightClickTable(event) { function onRightClickTable(event) {
this.selectedType = event.data.type; selectedType.value = event.data.type;
this.selectedId = event.data.id; selectedId.value = event.data.id;
this.selectedName = event.data.name; selectedName.value = event.data.name;
this.$refs.fileRightMenu.show(event.originalEvent) fileRightMenuRef.value.show(event.originalEvent);
}, }
/** /**
* Right Click Gride Card DOM Event * Right Click Gride Card DOM Event
* @param {event} event 該 file 的詳細資料 * @param {event} event 該 file 的詳細資料
* @param {number} index 該 file 的 index * @param {number} index 該 file 的 index
*/ */
onGridCardClick(file, index) { function onGridCardClick(file, index) {
this.selectedType = file.type; selectedType.value = file.type;
this.selectedId = file.id; selectedId.value = file.id;
this.selectedName = file.name; selectedName.value = file.name;
this.isActive = index; isActive.value = index;
}, }
/** /**
* File's Rename * File's Rename
* @param {string} type 該檔案的 type * @param {string} type 該檔案的 type
@@ -460,33 +456,34 @@
* @param {string} source hover icon 該檔案的 icon * @param {string} source hover icon 該檔案的 icon
* @param {string} fileName file's name * @param {string} fileName file's name
*/ */
rename(type, id, source, fileName) { function rename(type, id, source, fileName) {
if(type && id && source === 'list-hover') { if(type && id && source === 'list-hover') {
this.selectedType = type; selectedType.value = type;
this.selectedId = id; selectedId.value = id;
this.selectedName = fileName; selectedName.value = fileName;
} }
renameModal(this.store.rename, this.selectedType, this.selectedId, this.selectedName); renameModal(store.rename, selectedType.value, selectedId.value, selectedName.value);
}, }
/** /**
* Delete file * Delete file
* @param {string} type 該檔案的 type * @param {string} type 該檔案的 type
* @param {number} id 該檔案的 id * @param {number} id 該檔案的 id
* @param {string} source hover icon 該檔案的 icon * @param {string} source hover icon 該檔案的 icon
*/ */
async deleteFile(type, id, name, source) { async function deleteFile(type, id, name, source) {
let srt = ''; let srt = '';
let data = []; let data = [];
// 判斷是否來自 hover icon 選單 // 判斷是否來自 hover icon 選單
if(type && id && name && source === 'list-hover') { if(type && id && name && source === 'list-hover') {
this.selectedType = type; selectedType.value = type;
this.selectedId = id; selectedId.value = id;
this.selectedName = name; selectedName.value = name;
} }
// 取得相依性檔案 // 取得相依性檔案
await this.store.getDependents(this.selectedType, this.selectedId); await store.getDependents(selectedType.value, selectedId.value);
if(this.dependentsData.length !== 0) { if(dependentsData.value.length !== 0) {
data = [...this.dependentsData]; data = [...dependentsData.value];
data.forEach(i => { data.forEach(i => {
switch (i.type) { switch (i.type) {
case 'log-check': case 'log-check':
@@ -502,17 +499,18 @@
srt += content; srt += content;
}); });
} }
deleteFileModal(srt, this.selectedType, this.selectedId, this.selectedName); deleteFileModal(srt, selectedType.value, selectedId.value, selectedName.value);
srt = ''; srt = '';
}, }
/** /**
* 顯示被 Admin 或被其他帳號刪除的檔案 * 顯示被 Admin 或被其他帳號刪除的檔案
*/ */
showReallyDelete(){ function showReallyDelete(){
let srt = ''; let srt = '';
if(this.reallyDeleteData.length !== 0) { if(reallyDeleteData.value.length !== 0) {
this.reallyDeleteData.forEach(file => { reallyDeleteData.value.forEach(file => {
switch (file.type) { switch (file.type) {
case 'log-check': case 'log-check':
case 'filter-check': case 'filter-check':
@@ -525,102 +523,104 @@
srt += content; srt += content;
}); });
} }
reallyDeleteInformation(srt, this.reallyDeleteData); reallyDeleteInformation(srt, reallyDeleteData.value);
srt = ''; srt = '';
}, }
/** /**
* Download file as CSV * Download file as CSV
* @param {string} type 該檔案的 type * @param {string} type 該檔案的 type
* @param {number} id 該檔案的 id * @param {number} id 該檔案的 id
* @param {string} source hover icon 該檔案的 icon * @param {string} source hover icon 該檔案的 icon
*/ */
download(type, id, source, name) { function download(type, id, source, name) {
if(type && id && source === 'list-hover' && name) { if(type && id && source === 'list-hover' && name) {
this.selectedType = type; selectedType.value = type;
this.selectedId = id; selectedId.value = id;
this.selectedName = name; selectedName.value = name;
} }
this.store.downloadFileCSV(this.selectedType, this.selectedId, this.selectedName); store.downloadFileCSV(selectedType.value, selectedId.value, selectedName.value);
}, }
/** /**
* Delete Compare Primary log * Delete Compare Primary log
*/ */
primaryDragDelete() { function primaryDragDelete() {
this.compareData.unshift(this.primaryDragData[0]); compareData.value.unshift(primaryDragData.value[0]);
this.primaryDragData.length = 0; primaryDragData.value.length = 0;
}, }
/** /**
* Delete Compare Secondary log * Delete Compare Secondary log
*/ */
secondaryDragDelete() { function secondaryDragDelete() {
this.compareData.unshift(this.secondaryDragData[0]); compareData.value.unshift(secondaryDragData.value[0]);
this.secondaryDragData.length = 0; secondaryDragData.value.length = 0;
}, }
/** /**
* Enter the Compare page * Enter the Compare page
*/ */
compareSubmit() { function compareSubmit() {
const primaryType = this.primaryDragData[0].type; const primaryType = primaryDragData.value[0].type;
const secondaryType = this.secondaryDragData[0].type; const secondaryType = secondaryDragData.value[0].type;
const primaryId = this.primaryDragData[0].id; const primaryId = primaryDragData.value[0].id;
const secondaryId = this.secondaryDragData[0].id; const secondaryId = secondaryDragData.value[0].id;
const params = { primaryType: primaryType, primaryId: primaryId, secondaryType: secondaryType, secondaryId: secondaryId }; const params = { primaryType: primaryType, primaryId: primaryId, secondaryType: secondaryType, secondaryId: secondaryId };
this.mapCompareStore.setCompareRouteParam(primaryType, primaryId, secondaryType, secondaryId); mapCompareStore.setCompareRouteParam(primaryType, primaryId, secondaryType, secondaryId);
this.$router.push({name: 'CompareDashboard', params: params}); router.push({name: 'CompareDashboard', params: params});
}, }
/** /**
* Grid 模板時的篩選器 * Grid 模板時的篩選器
* @param {event} event choose columnType item * @param {event} event choose columnType item
*/ */
getGridSortData(event) { function getGridSortData(event) {
const code = event.value.code; const code = event.value.code;
// 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較 // 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較
switch (code) { switch (code) {
case 'nameAscending': case 'nameAscending':
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
break; break;
case 'nameDescending': case 'nameDescending':
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).reverse(); compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).reverse();
break; break;
case 'parentLogAscending': case 'parentLogAscending':
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())); compareData.value = compareData.value.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase()));
break; break;
case 'parentLogDescending': case 'parentLogDescending':
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())).reverse(); compareData.value = compareData.value.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())).reverse();
break; break;
case 'fileAscending': case 'fileAscending':
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())); compareData.value = compareData.value.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase()));
break; break;
case 'fileDescending': case 'fileDescending':
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())).reverse(); compareData.value = compareData.value.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())).reverse();
break; break;
case 'updatedAscending': case 'updatedAscending':
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)); compareData.value = compareData.value.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base));
break; break;
case 'updatedDescending': case 'updatedDescending':
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)).reverse(); compareData.value = compareData.value.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)).reverse();
break; break;
} }
}, }
...mapActions(
usePageAdminStore, ['setCurrentMapFile',], // Mounted
) onMounted(() => {
}, isLoading.value = true;
mounted() { store.fetchAllFiles();
this.isLoading = true;
this.store.fetchAllFiles();
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
const clickedLi = e.target.closest('li'); const clickedLi = e.target.closest('li');
if(!clickedLi || !clickedLi.id.startsWith('li')) this.isActive = null; if(!clickedLi || !clickedLi.id.startsWith('li')) isActive.value = null;
}) });
// 為 DataTable tbody 加入 .scrollbar 選擇器 // 為 DataTable tbody 加入 .scrollbar 選擇器
const tbodyElement = document.querySelector('.p-datatable-tbody'); const tbodyElement = document.querySelector('.p-datatable-tbody');
tbodyElement.classList.add('scrollbar'); tbodyElement.classList.add('scrollbar');
this.isLoading = false; isLoading.value = false;
}, });
}
</script> </script>
<style scoped> <style scoped>
@reference "../../assets/tailwind.css"; @reference "../../assets/tailwind.css";

View File

@@ -46,9 +46,10 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, } from 'vue'; import { ref, computed } from 'vue';
import { storeToRefs, mapActions } from 'pinia'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
import IconMember from '@/components/icons/IconMember.vue'; import IconMember from '@/components/icons/IconMember.vue';
import IconLockKey from '@/components/icons/IconLockKey.vue'; import IconLockKey from '@/components/icons/IconLockKey.vue';
@@ -56,69 +57,47 @@ import IconEyeOpen from '@/components/icons/IconEyeOpen.vue';
import IconEyeClose from '@/components/icons/IconEyeClose.vue'; import IconEyeClose from '@/components/icons/IconEyeClose.vue';
import IconWarnTriangle from '@/components/icons/IconWarnTriangle.vue'; import IconWarnTriangle from '@/components/icons/IconWarnTriangle.vue';
export default { const route = useRoute();
data(){
return {
isDisabled: true,
showPassword: false,
}
},
setup() {
// 調用函數,獲取 Store
const store = useLoginStore();
// 調用 store 裡的 state
const { auth, isInvalid } = storeToRefs(store);
// 調用 store 裡的 action
const { signIn } = store;
const isJustFocus = ref(true);
return { // Store
auth, const store = useLoginStore();
isInvalid, const { auth, isInvalid } = storeToRefs(store);
signIn, const { signIn, setRememberedReturnToUrl } = store;
isJustFocus,
} // Data
}, const isDisabled = ref(true);
components: { const showPassword = ref(false);
IconMember, const isJustFocus = ref(true);
IconLockKey,
IconEyeOpen, // Computed
IconEyeClose, const isDisabledButton = computed(() => {
IconWarnTriangle return auth.value.username === '' || auth.value.password === '' || isInvalid.value;
}, });
computed: {
/** // Methods
* if input no value , disabled. /**
*/
isDisabledButton() {
return this.auth.username === '' || this.auth.password === '' || this.isInvalid;
},
},
methods: {
/**
* when input onChange value , isInvalid === false. * when input onChange value , isInvalid === false.
* @param {event} event input 傳入的事件 * @param {event} event input 傳入的事件
*/ */
changeHandler(event) { function changeHandler(event) {
const inputValue = event.target.value; const inputValue = event.target.value;
if(inputValue !== '') { if(inputValue !== '') {
this.isInvalid = false; isInvalid.value = false;
} }
}, }
onInputAccountFocus(){
}, function onInputAccountFocus(){
onInputPwdFocus(){ }
},
...mapActions(useLoginStore, ['setRememberedReturnToUrl']), function onInputPwdFocus(){
}, }
created() {
// 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器 // Created logic
// btoa: 對字串進行 Base64 編碼 // 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器
if(this.$route.query['return-to']) { // btoa: 對字串進行 Base64 編碼
this.setRememberedReturnToUrl(this.$route.query['return-to']); if(route.query['return-to']) {
} setRememberedReturnToUrl(route.query['return-to']);
}, }
};
</script> </script>
<style scoped> <style scoped>

View File

@@ -11,80 +11,15 @@
</template> </template>
<script lang='ts'> <script lang='ts'>
import { onBeforeMount, } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs, mapActions, mapState, } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import Header from "@/components/Header.vue";
import Navbar from "@/components/Navbar.vue";
import Loading from '@/components/Loading.vue';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useLoginStore } from "@/stores/login"; import { useLoginStore } from "@/stores/login";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { getCookie, setCookie } from "@/utils/cookieUtil.js"; import { getCookie, setCookie } from "@/utils/cookieUtil.js";
import ModalContainer from './AccountManagement/ModalContainer.vue'; import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
import emitter from "@/utils/emitter";
export default { export default {
name: 'MainContainer',
setup() {
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const pageAdminStore = usePageAdminStore();
const { tempFilterId, createFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const router = useRouter();
const setHighlightedNavItemOnLanding = () => {
const currentPath = router.currentRoute.value.path;
const pathSegments: string[] = currentPath.split('/').filter(segment => segment !== '');
if(pathSegments.length === 1) {
if(pathSegments[0] === 'files') {
pageAdminStore.setActivePage('ALL');
}
} else if (pathSegments.length > 1){
pageAdminStore.setActivePage(pathSegments[1].toUpperCase());
}
};
onBeforeMount(() => {
setHighlightedNavItemOnLanding();
});
return {
loadingStore, temporaryData, tempFilterId,
createFilterId, postRuleData, ruleData,
conformanceLogTempCheckId, conformanceFilterTempCheckId,
allMapDataStore, conformanceStore,
};
},
components: {
Header,
Navbar,
Loading,
ModalContainer,
},
computed: {
...mapState(usePageAdminStore, [
'shouldKeepPreviousPage',
'activePageComputedByRoute'
]),
...mapState(useLoginStore, [
'isLoggedIn',
'auth',
])
},
methods: {
...mapActions(usePageAdminStore, [
'copyPendingPageToActivePage',
'setPreviousPage',
'clearShouldKeepPreviousPageBoolean',
'setActivePageComputedByRoute',
],),
...mapActions(useLoginStore, [
'refreshToken',
],),
},
// 重新整理畫面以及第一次進入網頁時beforeRouteEnter這個hook會被執行然而beforeRouteUpdate不會被執行 // 重新整理畫面以及第一次進入網頁時beforeRouteEnter這個hook會被執行然而beforeRouteUpdate不會被執行
// PSEUDOCODE // PSEUDOCODE
// if (not logged in) { // if (not logged in) {
@@ -102,7 +37,7 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const loginStore = useLoginStore(); const loginStore = useLoginStore();
if (!getCookie("isLuciaLoggedIn")) { //這裡不要用pinia的isLoggedIn來檢查因為會有重新整理時撈不到Persisted value的值的bug if (!getCookie("isLuciaLoggedIn")) {
if (getCookie('luciaRefreshToken')) { if (getCookie('luciaRefreshToken')) {
try { try {
await loginStore.refreshToken(); await loginStore.refreshToken();
@@ -121,7 +56,6 @@ export default {
next({ next({
path: '/login', path: '/login',
query: { query: {
// 記憶未來登入後要進入的網址且記憶的時候要用base64編碼包裹住
'return-to': btoa(window.location.href), 'return-to': btoa(window.location.href),
} }
}); });
@@ -132,31 +66,69 @@ export default {
}, },
// Remember, Swal modal handling is called before beforeRouteUpdate // Remember, Swal modal handling is called before beforeRouteUpdate
beforeRouteUpdate(to, from, next) { beforeRouteUpdate(to, from, next) {
this.setPreviousPage(from.name); const pageAdminStore = usePageAdminStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
pageAdminStore.setPreviousPage(from.name);
// 離開 Map 頁時判斷是否有無資料和需要存檔 // 離開 Map 頁時判斷是否有無資料和需要存檔
if ((from.name === 'Map' || from.name === 'CheckMap') && this.tempFilterId) { if ((from.name === 'Map' || from.name === 'CheckMap') && allMapDataStore.tempFilterId) {
// 傳給 Map通知 Sidebar 要關閉。 // 傳給 Map通知 Sidebar 要關閉。
this.$emitter.emit('leaveFilter', false); emitter.emit('leaveFilter', false);
leaveFilter(next, this.allMapDataStore.addFilterId, to.path) leaveFilter(next, allMapDataStore.addFilterId, to.path)
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance') } else if((from.name === 'Conformance' || from.name === 'CheckConformance')
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) { && (conformanceStore.conformanceLogTempCheckId || conformanceStore.conformanceFilterTempCheckId)) {
leaveConformance(next, this.conformanceStore.addConformanceCreateCheckId, to.path); leaveConformance(next, conformanceStore.addConformanceCreateCheckId, to.path);
} else if(this.shouldKeepPreviousPage) { } else if(pageAdminStore.shouldKeepPreviousPage) {
// pass on and reset boolean for future use pageAdminStore.clearShouldKeepPreviousPageBoolean();
this.clearShouldKeepPreviousPageBoolean();
} else { } else {
// most cases go this road pageAdminStore.copyPendingPageToActivePage();
// In this else block:
// for those pages who don't need popup modals, we handle page administration right now.
// By calling the following code, we decide the next visiting page.
// 在這個 else 區塊中:
// 對於那些不需要彈窗的頁面,我們現在就處理頁面管理。
// 透過呼叫以下代碼,我們決定出下一個將要走訪的頁面。
this.copyPendingPageToActivePage();
next(); next();
} }
}, },
}; };
</script> </script>
<script setup lang='ts'>
import { onBeforeMount } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import Header from "@/components/Header.vue";
import Navbar from "@/components/Navbar.vue";
import Loading from '@/components/Loading.vue';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useLoginStore } from "@/stores/login";
import emitter from '@/utils/emitter';
import ModalContainer from './AccountManagement/ModalContainer.vue';
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const pageAdminStore = usePageAdminStore();
const loginStore = useLoginStore();
const router = useRouter();
const { tempFilterId, createFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const setHighlightedNavItemOnLanding = () => {
const currentPath = router.currentRoute.value.path;
const pathSegments: string[] = currentPath.split('/').filter(segment => segment !== '');
if(pathSegments.length === 1) {
if(pathSegments[0] === 'files') {
pageAdminStore.setActivePage('ALL');
}
} else if (pathSegments.length > 1){
pageAdminStore.setActivePage(pathSegments[1].toUpperCase());
}
};
onBeforeMount(() => {
setHighlightedNavItemOnLanding();
});
</script>

View File

@@ -10,24 +10,15 @@
</div> </div>
</template> </template>
<script> <script setup>
import { onMounted } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
export default { const store = useLoginStore();
setup() { const { userData } = storeToRefs(store);
const store = useLoginStore();
const { userData } = storeToRefs(store);
const { getUserData } = store;
return {
userData,
getUserData,
}
},
mounted() {
this.getUserData();
}
};
onMounted(() => {
store.getUserData();
});
</script> </script>

View File

@@ -13,13 +13,7 @@
</main> </main>
</template> </template>
<script> <script setup>
import Header from "@/components/Header.vue"; import Header from "@/components/Header.vue";
import Navbar from "@/components/Navbar.vue"; import Navbar from "@/components/Navbar.vue";
export default {
components: {
Header,
Navbar,
},
};
</script> </script>

View File

@@ -78,23 +78,38 @@
</section> </section>
</template> </template>
<script> <script>
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useFilesStore } from '@/stores/files'; import { useFilesStore } from '@/stores/files';
import { uploadFailedFirst, uploadSuccess, uploadConfirm } from '@/module/alertModal.js'
export default { export default {
setup() { beforeRouteEnter(to, from, next){
const loadingStore = useLoadingStore(); // 要有 uploadID 才能進來
next(vm => {
const filesStore = useFilesStore(); const filesStore = useFilesStore();
const { isLoading } = storeToRefs(loadingStore); if(filesStore.uploadId === null) {
const { uploadDetail, uploadId, uploadFileName } = storeToRefs(filesStore); vm.$router.push({name: 'Files', replace: true});
vm.$toast.default('Please upload your file.', {position: 'bottom'});
return { isLoading, filesStore, uploadDetail, uploadId, uploadFileName } }
})
}, },
data() { }
return { </script>
tooltipUpload: { <script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { uploadFailedFirst, uploadSuccess, uploadConfirm } from '@/module/alertModal.js'
const router = useRouter();
// Stores
const loadingStore = useLoadingStore();
const filesStore = useFilesStore();
const { isLoading } = storeToRefs(loadingStore);
const { uploadDetail, uploadId, uploadFileName } = storeToRefs(filesStore);
// Data
const tooltipUpload = {
value: `1. Case ID: A unique identifier for each case. value: `1. Case ID: A unique identifier for each case.
2. Activity: A process step executed by either a system (automated) or humans (manual). 2. Activity: A process step executed by either a system (automated) or humans (manual).
3. Activity Instance ID: A unique identifier for a single occurrence of an activity. 3. Activity Instance ID: A unique identifier for a single occurrence of an activity.
@@ -105,8 +120,9 @@ export default {
// 7. Resource: A resource refers to any entity that is required to carry out a business process. This can include people, equipment, software, or any other type of asset. // 7. Resource: A resource refers to any entity that is required to carry out a business process. This can include people, equipment, software, or any other type of asset.
class: '!max-w-[400px] !text-[10px] !opacity-80', class: '!max-w-[400px] !text-[10px] !opacity-80',
autoHide: false, autoHide: false,
}, };
columnType: [
const columnType = [
{ name: 'Case ID*', code: 'case_id', color: '!text-secondary', value: '', label: 'Case ID', required: true }, { name: 'Case ID*', code: 'case_id', color: '!text-secondary', value: '', label: 'Case ID', required: true },
{ name: 'Timestamp*', code: 'timestamp', color: '!text-secondary', value: '', label: 'Timestamp', required: true }, { name: 'Timestamp*', code: 'timestamp', color: '!text-secondary', value: '', label: 'Timestamp', required: true },
{ name: 'Status*', code: 'status', color: '!text-secondary', value: '', label: 'Status', required: true }, { name: 'Status*', code: 'status', color: '!text-secondary', value: '', label: 'Status', required: true },
@@ -115,68 +131,65 @@ export default {
{ name: 'Case Attribute', code: 'case_attributes', color: '!text-primary', value: '', label: 'Case Attribute', required: false }, { name: 'Case Attribute', code: 'case_attributes', color: '!text-primary', value: '', label: 'Case Attribute', required: false },
// { name: 'Resource', code: '', color: '', value: '', label: 'Resource', required: false }, // 現階段沒有,未來可能有 // { name: 'Resource', code: '', color: '', value: '', label: 'Resource', required: false }, // 現階段沒有,未來可能有
{ name: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false }, { name: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false },
], ];
selectedColumns: [],
informData: [], // 藍字提示,尚未選擇的 type const selectedColumns = ref([]);
repeatedData: [], // 紅字提示,重複選擇的 type const informData = ref([]);
fileName: this.uploadFileName, const repeatedData = ref([]);
}; const fileName = ref(uploadFileName.value);
}, const showEdit = ref(false);
computed: {
isDisabled: function() { // Computed
const isDisabled = computed(() => {
// 1. 長度一樣,強制每一個都要選 // 1. 長度一樣,強制每一個都要選
// 2. 不為 null undefind // 2. 不為 null undefind
const hasValue = !this.selectedColumns.includes(undefined); const hasValue = !selectedColumns.value.includes(undefined);
const result = !(this.selectedColumns.length === this.uploadDetail?.columns.length const result = !(selectedColumns.value.length === uploadDetail.value?.columns.length
&& this.informData.length === 0 && this.repeatedData.length === 0 && hasValue); && informData.value.length === 0 && repeatedData.value.length === 0 && hasValue);
return result return result;
}, });
},
watch: { // Watch
selectedColumns: { watch(selectedColumns, (newVal) => {
deep: true, // 監聽陣列內部的變化 updateValidationData(newVal);
handler(newVal, oldVal) { }, { deep: true });
this.updateValidationData(newVal);
}, // Methods
} /**
},
methods: {
uploadFailedFirst,
uploadSuccess,
uploadConfirm,
/**
* Rename 離開 input 的行為 * Rename 離開 input 的行為
* @param {Event} e input 傳入的事件 * @param {Event} e input 傳入的事件
*/ */
onBlur(e) { function onBlur(e) {
const baseWidth = 20; const baseWidth = 20;
if(e.target.value === '') { if(e.target.value === '') {
e.target.value = this.uploadFileName; e.target.value = uploadFileName.value;
const textWidth = this.getTextWidth(e.target.value, e.target); const textWidth = getTextWidth(e.target.value, e.target);
e.target.style.width = baseWidth + textWidth + 'px'; e.target.style.width = baseWidth + textWidth + 'px';
}else if(e.target.value !== e.target.value.trim()) { }else if(e.target.value !== e.target.value.trim()) {
e.target.value = e.target.value.trim(); e.target.value = e.target.value.trim();
const textWidth = this.getTextWidth(e.target.value, e.target); const textWidth = getTextWidth(e.target.value, e.target);
e.target.style.width = baseWidth + textWidth + 'px'; e.target.style.width = baseWidth + textWidth + 'px';
} }
}, }
/**
/**
* Rename 輸入 input 的行為 * Rename 輸入 input 的行為
* @param {Event} e input 傳入的事件 * @param {Event} e input 傳入的事件
*/ */
onInput(e) { function onInput(e) {
const baseWidth = 20; const baseWidth = 20;
const textWidth = this.getTextWidth(e.target.value, e.target); const textWidth = getTextWidth(e.target.value, e.target);
e.target.style.width = baseWidth + textWidth + 'px'; e.target.style.width = baseWidth + textWidth + 'px';
}, }
/**
/**
* input 寬度隨著 value 響應式改變 * input 寬度隨著 value 響應式改變
* @param {String} text file's name * @param {String} text file's name
* @param {Event} e input 傳入的事件 * @param {Event} e input 傳入的事件
*/ */
getTextWidth(text, e) { function getTextWidth(text, e) {
// 替換空格為不斷行的空格 // 替換空格為不斷行的空格
const processedText = text.replace(/ /g, '\u00a0'); const processedText = text.replace(/ /g, '\u00a0');
const hiddenSpan = document.createElement('span'); const hiddenSpan = document.createElement('span');
@@ -189,18 +202,19 @@ export default {
document.body.removeChild(hiddenSpan); document.body.removeChild(hiddenSpan);
return width; return width;
}, }
/**
/**
* 驗證,根據新的 selectedColumns 更新 informData 和 repeatedData * 驗證,根據新的 selectedColumns 更新 informData 和 repeatedData
* @param {Array} data 已選擇的 type 的 data * @param {Array} data 已選擇的 type 的 data
*/ */
updateValidationData(data) { function updateValidationData(data) {
const nameOccurrences = {}; const nameOccurrences = {};
const noSortedRepeatedData = []; // 未排序的重複選擇的 data const noSortedRepeatedData = []; // 未排序的重複選擇的 data
const selectedData = [] // 已經選擇的 data const selectedData = [] // 已經選擇的 data
this.informData = []; // 尚未選擇的 data informData.value = []; // 尚未選擇的 data
this.repeatedData = []; // 重複選擇的 data repeatedData.value = []; // 重複選擇的 data
data.forEach(item => { data.forEach(item => {
const { name, code } = item; const { name, code } = item;
@@ -214,32 +228,35 @@ export default {
noSortedRepeatedData.push(item) noSortedRepeatedData.push(item)
} }
// 要按照選單的順序排序 // 要按照選單的順序排序
this.repeatedData = this.columnType.filter(column => noSortedRepeatedData.includes(column)); repeatedData.value = columnType.filter(column => noSortedRepeatedData.includes(column));
}else { }else {
nameOccurrences[name] = 1; nameOccurrences[name] = 1;
selectedData.push(name); selectedData.push(name);
this.informData = this.columnType.filter(item => item.required ? !selectedData.includes(item.name) : false); informData.value = columnType.filter(item => item.required ? !selectedData.includes(item.name) : false);
} }
}); });
}, }
/**
/**
* Reset Button * Reset Button
*/ */
reset() { function reset() {
// 路徑不列入歷史紀錄 // 路徑不列入歷史紀錄
this.selectedColumns = []; selectedColumns.value = [];
}, }
/**
/**
* Cancel Button * Cancel Button
*/ */
cancel() { function cancel() {
// 路徑不列入歷史紀錄 // 路徑不列入歷史紀錄
this.$router.push({name: 'Files', replace: true}); router.push({name: 'Files', replace: true});
}, }
/**
/**
* Upload Button * Upload Button
*/ */
async submit() { async function submit() {
// Post API Data // Post API Data
const fetchData = { const fetchData = {
timestamp: '', timestamp: '',
@@ -250,19 +267,19 @@ export default {
case_attributes: [] case_attributes: []
}; };
// 給值 // 給值
const haveValueData = this.selectedColumns.map((column, i) => { const haveValueData = selectedColumns.value.map((column, i) => {
if (column && this.uploadDetail.columns[i]) { if (column && uploadDetail.value.columns[i]) {
return { return {
name: column.name, name: column.name,
code: column.code, code: column.code,
color: column.color, color: column.color,
value: this.uploadDetail.columns[i] value: uploadDetail.value.columns[i]
} }
} }
}); });
// 取得欲更改的檔名, // 取得欲更改的檔名,
this.uploadFileName = this.fileName; uploadFileName.value = fileName.value;
// 設定第二階段上傳的 data // 設定第二階段上傳的 data
haveValueData.forEach(column => { haveValueData.forEach(column => {
if(column !== undefined) { if(column !== undefined) {
@@ -290,40 +307,32 @@ export default {
} }
} }
}); });
this.uploadConfirm(fetchData); uploadConfirm(fetchData);
}, }
},
async mounted() { // Mounted
// 只監聽第一次 onMounted(async () => {
const unwatch = this.$watch('fileName', (newValue) => { // 只監聯第一次
const unwatch = watch(fileName, (newValue) => {
if (newValue) { if (newValue) {
const inputElement = document.getElementById('fileNameInput'); const inputElement = document.getElementById('fileNameInput');
const baseWidth = 20; const baseWidth = 20;
const textWidth = this.getTextWidth(this.fileName, inputElement); const textWidth = getTextWidth(fileName.value, inputElement);
inputElement.style.width = baseWidth + textWidth + 'px'; inputElement.style.width = baseWidth + textWidth + 'px';
} }
}, },
{ immediate: true } { immediate: true }
); );
this.showEdit = true; showEdit.value = true;
if(this.uploadId) await this.filesStore.getUploadDetail(); if(uploadId.value) await filesStore.getUploadDetail();
this.selectedColumns = await Array.from({ length: this.uploadDetail.columns.length }, () => this.columnType[this.columnType.length - 1]); // 預設選 Not Assigned selectedColumns.value = await Array.from({ length: uploadDetail.value.columns.length }, () => columnType[columnType.length - 1]); // 預設選 Not Assigned
unwatch(); unwatch();
this.isLoading = false; isLoading.value = false;
}, });
beforeUnmount() {
onBeforeUnmount(() => {
// 離開頁面要刪 uploadID // 離開頁面要刪 uploadID
this.uploadId = null; uploadId.value = null;
this.uploadFileName = null; uploadFileName.value = null;
}, });
beforeRouteEnter(to, from, next){
// 要有 uploadID 才能進來
next(vm => {
if(vm.uploadId === null) {
vm.$router.push({name: 'Files', replace: true});
vm.$toast.default('Please upload your file.', {position: 'bottom'});
}
})
},
}
</script> </script>

View File

@@ -6,6 +6,14 @@ vi.mock('@/module/apiError.js', () => ({
default: vi.fn(), default: vi.fn(),
})); }));
const mockRoute = vi.hoisted(() => ({
query: {},
}));
vi.mock('vue-router', () => ({
useRoute: () => mockRoute,
}));
import Login from '@/views/Login/Login.vue'; import Login from '@/views/Login/Login.vue';
import { useLoginStore } from '@/stores/login'; import { useLoginStore } from '@/stores/login';
@@ -15,18 +23,16 @@ describe('Login', () => {
beforeEach(() => { beforeEach(() => {
pinia = createPinia(); pinia = createPinia();
setActivePinia(pinia); setActivePinia(pinia);
mockRoute.query = {};
}); });
const mountLogin = (options = {}) => { const mountLogin = (options = {}) => {
if (options.route?.query) {
Object.assign(mockRoute.query, options.route.query);
}
return mount(Login, { return mount(Login, {
global: { global: {
plugins: [pinia], plugins: [pinia],
mocks: {
$route: {
query: {},
...options.route,
},
},
}, },
}); });
}; };