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:
@@ -29,106 +29,86 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, onMounted, ref, } from 'vue';
|
||||
import { mapActions, mapState, storeToRefs } from 'pinia';
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import i18next from '@/i18n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { logOut } = useLoginStore();
|
||||
const loginStore = useLoginStore();
|
||||
const router = useRouter();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const { tempFilterId } = storeToRefs(allMapDataStore);
|
||||
const { conformanceLogTempCheckId } = storeToRefs(conformanceStore);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const loginStore = useLoginStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
|
||||
const loginUserData = ref(null);
|
||||
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
|
||||
const isAdmin = ref(false);
|
||||
const { logOut } = loginStore;
|
||||
const { tempFilterId } = storeToRefs(allMapDataStore);
|
||||
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
|
||||
const { userData } = storeToRefs(loginStore);
|
||||
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
|
||||
|
||||
const getIsAdminValue = async () => {
|
||||
await loginStore.getUserData();
|
||||
loginUserData.value = loginStore.userData;
|
||||
await acctMgmtStore.getUserDetail(loginUserData.value.username);
|
||||
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
|
||||
};
|
||||
const loginUserData = ref(null);
|
||||
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
|
||||
const isAdmin = ref(false);
|
||||
|
||||
const onBtnMyAccountClick = async() => {
|
||||
acctMgmtStore.closeAcctMenu();
|
||||
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
|
||||
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
|
||||
await router.push('/my-account');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getIsAdminValue();
|
||||
});
|
||||
|
||||
return {
|
||||
logOut,
|
||||
tempFilterId,
|
||||
conformanceLogTempCheckId,
|
||||
allMapDataStore,
|
||||
conformanceStore,
|
||||
isAdmin,
|
||||
onBtnMyAccountClick,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
i18next: i18next,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useLoginStore, ['userData']),
|
||||
...mapState(useAcctMgmtStore, ['isAcctMenuOpen']),
|
||||
},
|
||||
methods: {
|
||||
clickOtherPlacesThenCloseMenu(){
|
||||
const acctMgmtButton = document.getElementById('acct_mgmt_button');
|
||||
const acctMgmtMenu = document.getElementById('account_menu');
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
|
||||
this.closeAcctMenu();
|
||||
}
|
||||
});
|
||||
},
|
||||
onBtnAcctMgmtClick(){
|
||||
this.$router.push({name: 'AcctAdmin'});
|
||||
this.closeAcctMenu();
|
||||
},
|
||||
onLogoutBtnClick(){
|
||||
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) {
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
this.$emitter.emit('leaveFilter', false);
|
||||
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut)
|
||||
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
|
||||
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
|
||||
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut)
|
||||
} else {
|
||||
this.logOut();
|
||||
}
|
||||
},
|
||||
...mapActions(useLoginStore, ['getUserData']),
|
||||
...mapActions(useAcctMgmtStore, ['closeAcctMenu']),
|
||||
},
|
||||
created() {
|
||||
this.getUserData();
|
||||
},
|
||||
mounted(){
|
||||
this.clickOtherPlacesThenCloseMenu();
|
||||
},
|
||||
const getIsAdminValue = async () => {
|
||||
await loginStore.getUserData();
|
||||
loginUserData.value = loginStore.userData;
|
||||
await acctMgmtStore.getUserDetail(loginUserData.value.username);
|
||||
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
|
||||
};
|
||||
|
||||
const onBtnMyAccountClick = async () => {
|
||||
acctMgmtStore.closeAcctMenu();
|
||||
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
|
||||
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
|
||||
await router.push('/my-account');
|
||||
};
|
||||
|
||||
const clickOtherPlacesThenCloseMenu = () => {
|
||||
const acctMgmtButton = document.getElementById('acct_mgmt_button');
|
||||
const acctMgmtMenu = document.getElementById('account_menu');
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (acctMgmtMenu && acctMgmtButton && !acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
|
||||
acctMgmtStore.closeAcctMenu();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onBtnAcctMgmtClick = () => {
|
||||
router.push({name: 'AcctAdmin'});
|
||||
acctMgmtStore.closeAcctMenu();
|
||||
};
|
||||
|
||||
const onLogoutBtnClick = () => {
|
||||
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
emitter.emit('leaveFilter', false);
|
||||
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
|
||||
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
|
||||
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
|
||||
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
|
||||
} else {
|
||||
logOut();
|
||||
}
|
||||
};
|
||||
|
||||
// created
|
||||
loginStore.getUserData();
|
||||
|
||||
// mounted
|
||||
onMounted(async () => {
|
||||
await getIsAdminValue();
|
||||
clickOtherPlacesThenCloseMenu();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -9,30 +9,22 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, } from 'vue';
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import i18next from '@/i18n/i18n.js';
|
||||
export default {
|
||||
setup(props, { emit, }) {
|
||||
const inputQuery = ref("");
|
||||
|
||||
const onSearchClick = (event) => {
|
||||
event.preventDefault();
|
||||
emit('on-search-account-button-click', inputQuery.value);
|
||||
};
|
||||
const emit = defineEmits(['on-search-account-button-click']);
|
||||
|
||||
const handleKeyPressOfSearch = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
emit('on-search-account-button-click', inputQuery.value);
|
||||
}
|
||||
}
|
||||
const inputQuery = ref("");
|
||||
|
||||
return {
|
||||
inputQuery,
|
||||
onSearchClick,
|
||||
handleKeyPressOfSearch,
|
||||
i18next,
|
||||
};
|
||||
},
|
||||
const onSearchClick = (event) => {
|
||||
event.preventDefault();
|
||||
emit('on-search-account-button-click', inputQuery.value);
|
||||
};
|
||||
|
||||
const handleKeyPressOfSearch = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
emit('on-search-account-button-click', inputQuery.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -12,27 +12,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
isActivated: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true,
|
||||
},
|
||||
displayText: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "Status",
|
||||
}
|
||||
<script setup>
|
||||
defineProps({
|
||||
isActivated: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true,
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
isActivated: props.isActivated,
|
||||
displayText: props.displayText,
|
||||
};
|
||||
displayText: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "Status",
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -15,35 +15,23 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, } from 'vue';
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
setup(props) {
|
||||
const buttonText = props.buttonText;
|
||||
});
|
||||
|
||||
const isPressed = ref(false);
|
||||
const isPressed = ref(false);
|
||||
|
||||
const onMousedown = () => {
|
||||
isPressed.value = true;
|
||||
}
|
||||
const onMousedown = () => {
|
||||
isPressed.value = true;
|
||||
};
|
||||
|
||||
const onMouseup = () => {
|
||||
isPressed.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
buttonText,
|
||||
onMousedown,
|
||||
onMouseup,
|
||||
isPressed,
|
||||
};
|
||||
},
|
||||
}
|
||||
const onMouseup = () => {
|
||||
isPressed.value = false;
|
||||
};
|
||||
</script>
|
||||
@@ -16,35 +16,23 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, } from 'vue';
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
setup(props) {
|
||||
const buttonText = props.buttonText;
|
||||
});
|
||||
|
||||
const isPressed = ref(false);
|
||||
const isPressed = ref(false);
|
||||
|
||||
const onMousedown = () => {
|
||||
isPressed.value = true;
|
||||
}
|
||||
const onMousedown = () => {
|
||||
isPressed.value = true;
|
||||
};
|
||||
|
||||
const onMouseup = () => {
|
||||
isPressed.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
buttonText,
|
||||
onMousedown,
|
||||
onMouseup,
|
||||
isPressed,
|
||||
};
|
||||
},
|
||||
}
|
||||
const onMouseup = () => {
|
||||
isPressed.value = false;
|
||||
};
|
||||
</script>
|
||||
@@ -146,128 +146,126 @@
|
||||
</div>
|
||||
</Sidebar>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useCompareStore } from '@/stores/compare';
|
||||
import { getTimeLabel } from '@/module/timeLabel.js';
|
||||
import getMoment from 'moment';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const compareStore = useCompareStore();
|
||||
const props = defineProps({
|
||||
sidebarState: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
},
|
||||
});
|
||||
|
||||
return { compareStore };
|
||||
},
|
||||
props:{
|
||||
sidebarState: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
const route = useRoute();
|
||||
const compareStore = useCompareStore();
|
||||
|
||||
const primaryValueCases = ref(0);
|
||||
const primaryValueTraces = ref(0);
|
||||
const primaryValueTaskInstances = ref(0);
|
||||
const primaryValueTasks = ref(0);
|
||||
const secondaryValueCases = ref(0);
|
||||
const secondaryValueTraces = ref(0);
|
||||
const secondaryValueTaskInstances = ref(0);
|
||||
const secondaryValueTasks = ref(0);
|
||||
const primaryStatData = ref(null);
|
||||
const secondaryStatData = ref(null);
|
||||
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
const getPercentLabel = (val) => {
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
};
|
||||
|
||||
/**
|
||||
* setting stats data
|
||||
* @param { object } data fetch API stats data
|
||||
* @param { string } fileName file Name
|
||||
* @returns { object } primaryStatData | secondaryStatData,回傳 primaryStatData 或 secondaryStatData
|
||||
*/
|
||||
const getStatData = (data, fileName) => {
|
||||
return {
|
||||
name: fileName,
|
||||
cases: {
|
||||
count: data.cases.count.toLocaleString('en-US'),
|
||||
total: data.cases.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(data.cases.ratio)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryValueCases: 0,
|
||||
primaryValueTraces: 0,
|
||||
primaryValueTaskInstances: 0,
|
||||
primaryValueTasks: 0,
|
||||
secondaryValueCases: 0,
|
||||
secondaryValueTraces: 0,
|
||||
secondaryValueTaskInstances: 0,
|
||||
secondaryValueTasks: 0,
|
||||
primaryStatData: null,
|
||||
secondaryStatData: null,
|
||||
traces: {
|
||||
count: data.traces.count.toLocaleString('en-US'),
|
||||
total: data.traces.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(data.traces.ratio)
|
||||
},
|
||||
task_instances: {
|
||||
count: data.task_instances.count.toLocaleString('en-US'),
|
||||
total: data.task_instances.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(data.task_instances.ratio)
|
||||
},
|
||||
tasks: {
|
||||
count: data.tasks.count.toLocaleString('en-US'),
|
||||
total: data.tasks.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(data.tasks.ratio)
|
||||
},
|
||||
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
|
||||
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
|
||||
case_duration: {
|
||||
min: getTimeLabel(data.case_duration.min, 2),
|
||||
max: getTimeLabel(data.case_duration.max, 2),
|
||||
average: getTimeLabel(data.case_duration.average, 2),
|
||||
median: getTimeLabel(data.case_duration.median, 2),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
},
|
||||
/**
|
||||
* setting stats data
|
||||
* @param { object } data fetch API stats data
|
||||
* @param { string } fileName file Name
|
||||
* @returns { object } primaryStatData | secondaryStatData,回傳 primaryStatData 或 secondaryStatData
|
||||
*/
|
||||
getStatData(data, fileName) {
|
||||
return {
|
||||
name: fileName,
|
||||
cases: {
|
||||
count: data.cases.count.toLocaleString('en-US'),
|
||||
total: data.cases.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(data.cases.ratio)
|
||||
},
|
||||
traces: {
|
||||
count: data.traces.count.toLocaleString('en-US'),
|
||||
total: data.traces.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(data.traces.ratio)
|
||||
},
|
||||
task_instances: {
|
||||
count: data.task_instances.count.toLocaleString('en-US'),
|
||||
total: data.task_instances.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(data.task_instances.ratio)
|
||||
},
|
||||
tasks: {
|
||||
count: data.tasks.count.toLocaleString('en-US'),
|
||||
total: data.tasks.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(data.tasks.ratio)
|
||||
},
|
||||
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
|
||||
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
|
||||
case_duration: {
|
||||
min: getTimeLabel(data.case_duration.min, 2),
|
||||
max: getTimeLabel(data.case_duration.max, 2),
|
||||
average: getTimeLabel(data.case_duration.average, 2),
|
||||
median: getTimeLabel(data.case_duration.median, 2),
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Behavior when show
|
||||
*/
|
||||
show(){
|
||||
this.primaryValueCases = this.primaryStatData.cases.ratio;
|
||||
this.primaryValueTraces= this.primaryStatData.traces.ratio;
|
||||
this.primaryValueTaskInstances = this.primaryStatData.task_instances.ratio;
|
||||
this.primaryValueTasks = this.primaryStatData.tasks.ratio;
|
||||
this.secondaryValueCases = this.secondaryStatData.cases.ratio;
|
||||
this.secondaryValueTraces= this.secondaryStatData.traces.ratio;
|
||||
this.secondaryValueTaskInstances = this.secondaryStatData.task_instances.ratio;
|
||||
this.secondaryValueTasks = this.secondaryStatData.tasks.ratio;
|
||||
},
|
||||
/**
|
||||
* Behavior when hidden
|
||||
*/
|
||||
hide(){
|
||||
this.primaryValueCases = 0;
|
||||
this.primaryValueTraces= 0;
|
||||
this.primaryValueTaskInstances = 0;
|
||||
this.primaryValueTasks = 0;
|
||||
this.secondaryValueCases = 0;
|
||||
this.secondaryValueTraces= 0;
|
||||
this.secondaryValueTaskInstances = 0;
|
||||
this.secondaryValueTasks = 0;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
const routeParams = this.$route.params;
|
||||
const primaryType = routeParams.primaryType;
|
||||
const secondaryType = routeParams.secondaryType;
|
||||
const primaryId = routeParams.primaryId;
|
||||
const secondaryId = routeParams.secondaryId;
|
||||
const primaryData = await this.compareStore.getStateData(primaryType, primaryId);
|
||||
const secondaryData = await this.compareStore.getStateData(secondaryType, secondaryId);
|
||||
|
||||
const primaryFileName = await this.compareStore.getFileName(primaryId)
|
||||
const secondaryFileName = await this.compareStore.getFileName(secondaryId)
|
||||
this.primaryStatData = await this.getStatData(primaryData, primaryFileName);
|
||||
this.secondaryStatData = await this.getStatData(secondaryData, secondaryFileName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior when show
|
||||
*/
|
||||
const show = () => {
|
||||
primaryValueCases.value = primaryStatData.value.cases.ratio;
|
||||
primaryValueTraces.value = primaryStatData.value.traces.ratio;
|
||||
primaryValueTaskInstances.value = primaryStatData.value.task_instances.ratio;
|
||||
primaryValueTasks.value = primaryStatData.value.tasks.ratio;
|
||||
secondaryValueCases.value = secondaryStatData.value.cases.ratio;
|
||||
secondaryValueTraces.value = secondaryStatData.value.traces.ratio;
|
||||
secondaryValueTaskInstances.value = secondaryStatData.value.task_instances.ratio;
|
||||
secondaryValueTasks.value = secondaryStatData.value.tasks.ratio;
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior when hidden
|
||||
*/
|
||||
const hide = () => {
|
||||
primaryValueCases.value = 0;
|
||||
primaryValueTraces.value = 0;
|
||||
primaryValueTaskInstances.value = 0;
|
||||
primaryValueTasks.value = 0;
|
||||
secondaryValueCases.value = 0;
|
||||
secondaryValueTraces.value = 0;
|
||||
secondaryValueTaskInstances.value = 0;
|
||||
secondaryValueTasks.value = 0;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const routeParams = route.params;
|
||||
const primaryType = routeParams.primaryType;
|
||||
const secondaryType = routeParams.secondaryType;
|
||||
const primaryId = routeParams.primaryId;
|
||||
const secondaryId = routeParams.secondaryId;
|
||||
const primaryData = await compareStore.getStateData(primaryType, primaryId);
|
||||
const secondaryData = await compareStore.getStateData(secondaryType, secondaryId);
|
||||
|
||||
const primaryFileName = await compareStore.getFileName(primaryId)
|
||||
const secondaryFileName = await compareStore.getFileName(secondaryId)
|
||||
primaryStatData.value = await getStatData(primaryData, primaryFileName);
|
||||
secondaryStatData.value = await getStatData(secondaryData, secondaryFileName);
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.p-progressbar .p-progressbar-value) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,40 +9,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
props: ['data', 'select'],
|
||||
data() {
|
||||
return {
|
||||
sortData: [],
|
||||
actList: this.select,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler: function(newValue) {
|
||||
this.sortData = sortNumEngZhtw(newValue)
|
||||
},
|
||||
immediate: true, // 立即執行一次排序
|
||||
},
|
||||
select: function(newValue) {
|
||||
this.actList = newValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 將選取的 Activities 傳出去
|
||||
*/
|
||||
actListData() {
|
||||
this.$emitter.emit('actListData', this.actList);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.actList = data;
|
||||
});
|
||||
},
|
||||
const props = defineProps(['data', 'select']);
|
||||
|
||||
const sortData = ref([]);
|
||||
const actList = ref(props.select);
|
||||
|
||||
watch(() => props.data, (newValue) => {
|
||||
sortData.value = sortNumEngZhtw(newValue);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.select, (newValue) => {
|
||||
actList.value = newValue;
|
||||
});
|
||||
|
||||
/**
|
||||
* 將選取的 Activities 傳出去
|
||||
*/
|
||||
function actListData() {
|
||||
emitter.emit('actListData', actList.value);
|
||||
}
|
||||
|
||||
// created
|
||||
emitter.on('reset', (data) => {
|
||||
actList.value = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,65 +9,55 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, } from 'pinia';
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useConformanceInputStore } from "@/stores/conformanceInput";
|
||||
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
props: ['title', 'select', 'data', 'category', 'task', 'isSubmit'],
|
||||
data() {
|
||||
return {
|
||||
sortData: [],
|
||||
localSelect: null,
|
||||
selectedRadio: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler: function(newValue) {
|
||||
this.sortData = sortNumEngZhtw(newValue)
|
||||
},
|
||||
immediate: true, // 立即執行一次排序
|
||||
},
|
||||
task: function(newValue) {
|
||||
this.selectedRadio = newValue;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inputActivityRadioData: {
|
||||
get(){
|
||||
return {
|
||||
category: this.category,
|
||||
task: this.selectedRadio, // For example, "a", or "出院"
|
||||
};
|
||||
},
|
||||
} ,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 將選取的 Activity 傳出去
|
||||
*/
|
||||
actRadioData() {
|
||||
this.localSelect = null;
|
||||
this.$emitter.emit('actRadioData', this.inputActivityRadioData);
|
||||
this.$emit('selected-task', this.selectedRadio);
|
||||
this.setActivityRadioStartEndData(this.inputActivityRadioData.task);
|
||||
},
|
||||
setGlobalActivityRadioDataState(){
|
||||
//this.title: value might be "From" or "To"
|
||||
this.setActivityRadioStartEndData(this.inputActivityRadioData.task, this.title);
|
||||
},
|
||||
...mapActions(useConformanceInputStore, ['setActivityRadioStartEndData']),
|
||||
},
|
||||
created() {
|
||||
sortNumEngZhtw(this.sortData);
|
||||
this.localSelect = this.isSubmit ? this.select : null;
|
||||
this.selectedRadio = this.localSelect;
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.selectedRadio = data;
|
||||
});
|
||||
this.setGlobalActivityRadioDataState();
|
||||
},
|
||||
const props = defineProps(['title', 'select', 'data', 'category', 'task', 'isSubmit']);
|
||||
const emit = defineEmits(['selected-task']);
|
||||
|
||||
const conformanceInputStore = useConformanceInputStore();
|
||||
|
||||
const sortData = ref([]);
|
||||
const localSelect = ref(null);
|
||||
const selectedRadio = ref(null);
|
||||
|
||||
watch(() => props.data, (newValue) => {
|
||||
sortData.value = sortNumEngZhtw(newValue);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.task, (newValue) => {
|
||||
selectedRadio.value = newValue;
|
||||
});
|
||||
|
||||
const inputActivityRadioData = computed(() => ({
|
||||
category: props.category,
|
||||
task: selectedRadio.value,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 將選取的 Activity 傳出去
|
||||
*/
|
||||
function actRadioData() {
|
||||
localSelect.value = null;
|
||||
emitter.emit('actRadioData', inputActivityRadioData.value);
|
||||
emit('selected-task', selectedRadio.value);
|
||||
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task);
|
||||
}
|
||||
|
||||
function setGlobalActivityRadioDataState() {
|
||||
//this.title: value might be "From" or "To"
|
||||
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task, props.title);
|
||||
}
|
||||
|
||||
// created
|
||||
sortNumEngZhtw(sortData.value);
|
||||
localSelect.value = props.isSubmit ? props.select : null;
|
||||
selectedRadio.value = localSelect.value;
|
||||
emitter.on('reset', (data) => {
|
||||
selectedRadio.value = data;
|
||||
});
|
||||
setGlobalActivityRadioDataState();
|
||||
</script>
|
||||
|
||||
@@ -41,93 +41,92 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
props: ['data', 'listSeq', 'isSubmit', 'category'],
|
||||
data() {
|
||||
return {
|
||||
listSequence: [],
|
||||
lastItemIndex: null,
|
||||
isSelect: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
datadata: function() {
|
||||
// Activity List 要排序
|
||||
let newData;
|
||||
if(this.data !== null) {
|
||||
newData = JSON.parse(JSON.stringify(this.data));
|
||||
sortNumEngZhtw(newData);
|
||||
}
|
||||
return newData;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* double click Activity List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
moveActItem(index, element){
|
||||
this.listSequence.push(element);
|
||||
},
|
||||
/**
|
||||
* double click Sequence List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
moveSeqItem(index, element){
|
||||
this.listSequence.splice(index, 1);
|
||||
},
|
||||
/**
|
||||
* get listSequence
|
||||
*/
|
||||
getComponentData(){
|
||||
this.$emitter.emit('getListSequence',{
|
||||
category: this.category,
|
||||
task: this.listSequence,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Element dragging started
|
||||
*/
|
||||
onStart(evt) {
|
||||
const lastChild = evt.to.lastChild.lastChild;
|
||||
lastChild.style.display = 'none';
|
||||
// 隱藏拖曳元素原位置
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = 'none';
|
||||
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
const listIndex = this.listSequence.length - 1;
|
||||
if(evt.oldIndex === listIndex) this.lastItemIndex = listIndex;
|
||||
},
|
||||
/**
|
||||
* Element dragging ended
|
||||
*/
|
||||
onEnd(evt) {
|
||||
// 顯示拖曳元素
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = '';
|
||||
// 拖曳結束要顯示箭頭,但最後一個不用
|
||||
const lastChild = evt.item.lastChild;
|
||||
const listIndex = this.listSequence.length - 1
|
||||
if (evt.oldIndex !== listIndex) {
|
||||
lastChild.style.display = '';
|
||||
}
|
||||
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
this.lastItemIndex = null;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const newlist = JSON.parse(JSON.stringify(this.listSeq));
|
||||
this.listSequence = this.isSubmit ? newlist : [];
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.listSequence = [];
|
||||
});
|
||||
},
|
||||
const props = defineProps(['data', 'listSeq', 'isSubmit', 'category']);
|
||||
|
||||
const listSequence = ref([]);
|
||||
const lastItemIndex = ref(null);
|
||||
const isSelect = ref(true);
|
||||
|
||||
const datadata = computed(() => {
|
||||
// Activity List 要排序
|
||||
let newData;
|
||||
if(props.data !== null) {
|
||||
newData = JSON.parse(JSON.stringify(props.data));
|
||||
sortNumEngZhtw(newData);
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
/**
|
||||
* double click Activity List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
function moveActItem(index, element) {
|
||||
listSequence.value.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* double click Sequence List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
function moveSeqItem(index, element) {
|
||||
listSequence.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* get listSequence
|
||||
*/
|
||||
function getComponentData() {
|
||||
emitter.emit('getListSequence', {
|
||||
category: props.category,
|
||||
task: listSequence.value,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Element dragging started
|
||||
*/
|
||||
function onStart(evt) {
|
||||
const lastChild = evt.to.lastChild.lastChild;
|
||||
lastChild.style.display = 'none';
|
||||
// 隱藏拖曳元素原位置
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = 'none';
|
||||
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
const listIndex = listSequence.value.length - 1;
|
||||
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element dragging ended
|
||||
*/
|
||||
function onEnd(evt) {
|
||||
// 顯示拖曳元素
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = '';
|
||||
// 拖曳結束要顯示箭頭,但最後一個不用
|
||||
const lastChild = evt.item.lastChild;
|
||||
const listIndex = listSequence.value.length - 1;
|
||||
if (evt.oldIndex !== listIndex) {
|
||||
lastChild.style.display = '';
|
||||
}
|
||||
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
lastItemIndex.value = null;
|
||||
}
|
||||
|
||||
// created
|
||||
const newlist = JSON.parse(JSON.stringify(props.listSeq));
|
||||
listSequence.value = props.isSubmit ? newlist : [];
|
||||
emitter.on('reset', (data) => {
|
||||
listSequence.value = [];
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
@reference "../../../../assets/tailwind.css";
|
||||
|
||||
@@ -50,90 +50,81 @@
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
|
||||
|
||||
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ruleType: [
|
||||
{id: 1, name: 'Have activity'},
|
||||
{id: 2, name: 'Activity sequence'},
|
||||
{id: 3, name: 'Activity duration'},
|
||||
{id: 4, name: 'Processing time'},
|
||||
{id: 5, name: 'Waiting time'},
|
||||
{id: 6, name: 'Cycle time'},
|
||||
],
|
||||
activitySequence: [
|
||||
{id: 1, name: 'Start & End'},
|
||||
{id: 2, name: 'Sequence'},
|
||||
],
|
||||
mode: [
|
||||
{id: 1, name: 'Directly follows'},
|
||||
{id: 2, name: 'Eventually follows'},
|
||||
{id: 3, name: 'Short loop(s)'},
|
||||
{id: 4, name: 'Self loop(s)'},
|
||||
],
|
||||
processScope: [
|
||||
{id: 1, name: 'End to end'},
|
||||
{id: 2, name: 'Partial'},
|
||||
],
|
||||
actSeqMore: [
|
||||
{id: 1, name: 'All'},
|
||||
{id: 2, name: 'Start'},
|
||||
{id: 3, name: 'End'},
|
||||
{id: 4, name: 'Start & End'},
|
||||
],
|
||||
actSeqFromTo: [
|
||||
{id: 1, name: 'From'},
|
||||
{id: 2, name: 'To'},
|
||||
{id: 3, name: 'From & To'},
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 切換 Rule Type 的選項時的行為
|
||||
*/
|
||||
changeRadio() {
|
||||
this.selectedActivitySequence = 'Start & End';
|
||||
this.selectedMode = 'Directly follows';
|
||||
this.selectedProcessScope = 'End to end';
|
||||
this.selectedActSeqMore = 'All';
|
||||
this.selectedActSeqFromTo = 'From';
|
||||
this.$emitter.emit('isRadioChange', true); // Radio 切換時,資料要清空
|
||||
},
|
||||
/**
|
||||
* 切換 Activity sequence 的選項時的行為
|
||||
*/
|
||||
changeRadioSeq() {
|
||||
this.$emitter.emit('isRadioSeqChange',true);
|
||||
},
|
||||
/**
|
||||
* 切換 Processing time 的選項時的行為
|
||||
*/
|
||||
changeRadioProcessScope() {
|
||||
this.$emitter.emit('isRadioProcessScopeChange', true);
|
||||
},
|
||||
/**
|
||||
* 切換 Process Scope 的選項時的行為
|
||||
*/
|
||||
changeRadioActSeqMore() {
|
||||
this.$emitter.emit('isRadioActSeqMoreChange', true);
|
||||
},
|
||||
/**
|
||||
* 切換 Activity Sequence 的選項時的行為
|
||||
*/
|
||||
changeRadioActSeqFromTo() {
|
||||
this.$emitter.emit('isRadioActSeqFromToChange', true);
|
||||
},
|
||||
}
|
||||
const ruleType = [
|
||||
{id: 1, name: 'Have activity'},
|
||||
{id: 2, name: 'Activity sequence'},
|
||||
{id: 3, name: 'Activity duration'},
|
||||
{id: 4, name: 'Processing time'},
|
||||
{id: 5, name: 'Waiting time'},
|
||||
{id: 6, name: 'Cycle time'},
|
||||
];
|
||||
const activitySequence = [
|
||||
{id: 1, name: 'Start & End'},
|
||||
{id: 2, name: 'Sequence'},
|
||||
];
|
||||
const mode = [
|
||||
{id: 1, name: 'Directly follows'},
|
||||
{id: 2, name: 'Eventually follows'},
|
||||
{id: 3, name: 'Short loop(s)'},
|
||||
{id: 4, name: 'Self loop(s)'},
|
||||
];
|
||||
const processScope = [
|
||||
{id: 1, name: 'End to end'},
|
||||
{id: 2, name: 'Partial'},
|
||||
];
|
||||
const actSeqMore = [
|
||||
{id: 1, name: 'All'},
|
||||
{id: 2, name: 'Start'},
|
||||
{id: 3, name: 'End'},
|
||||
{id: 4, name: 'Start & End'},
|
||||
];
|
||||
const actSeqFromTo = [
|
||||
{id: 1, name: 'From'},
|
||||
{id: 2, name: 'To'},
|
||||
{id: 3, name: 'From & To'},
|
||||
];
|
||||
|
||||
/**
|
||||
* 切換 Rule Type 的選項時的行為
|
||||
*/
|
||||
function changeRadio() {
|
||||
selectedActivitySequence.value = 'Start & End';
|
||||
selectedMode.value = 'Directly follows';
|
||||
selectedProcessScope.value = 'End to end';
|
||||
selectedActSeqMore.value = 'All';
|
||||
selectedActSeqFromTo.value = 'From';
|
||||
emitter.emit('isRadioChange', true); // Radio 切換時,資料要清空
|
||||
}
|
||||
/**
|
||||
* 切換 Activity sequence 的選項時的行為
|
||||
*/
|
||||
function changeRadioSeq() {
|
||||
emitter.emit('isRadioSeqChange',true);
|
||||
}
|
||||
/**
|
||||
* 切換 Processing time 的選項時的行為
|
||||
*/
|
||||
function changeRadioProcessScope() {
|
||||
emitter.emit('isRadioProcessScopeChange', true);
|
||||
}
|
||||
/**
|
||||
* 切換 Process Scope 的選項時的行為
|
||||
*/
|
||||
function changeRadioActSeqMore() {
|
||||
emitter.emit('isRadioActSeqMoreChange', true);
|
||||
}
|
||||
/**
|
||||
* 切換 Activity Sequence 的選項時的行為
|
||||
*/
|
||||
function changeRadioActSeqFromTo() {
|
||||
emitter.emit('isRadioActSeqFromToChange', true);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,264 +1,258 @@
|
||||
<template>
|
||||
<div class="px-4 text-sm">
|
||||
<!-- Have activity -->
|
||||
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="containstTasksData" :select="isSubmitTask"></ResultCheck>
|
||||
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="state.containstTasksData" :select="isSubmitTask"></ResultCheck>
|
||||
<!-- Activity sequence -->
|
||||
<ResultDot v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'" :timeResultData="selectCfmSeqSE" :select="isSubmitStartAndEnd"></ResultDot>
|
||||
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
|
||||
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
|
||||
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="state.selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
|
||||
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="state.selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
|
||||
<!-- Activity duration -->
|
||||
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="durationData" :select="isSubmitDurationData"></ResultCheck>
|
||||
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="state.durationData" :select="isSubmitDurationData"></ResultCheck>
|
||||
<!-- Processing time -->
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmPtEteSE" :select="isSubmitCfmPtEteSE"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmPtPSE" :select="isSubmitCfmPtPSE"></ResultDot>
|
||||
<!-- Waiting time -->
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmWtEteSE" :select="isSubmitCfmWtEteSE"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmWtPSE" :select="isSubmitCfmWtPSE"></ResultDot>
|
||||
<!-- Cycle time -->
|
||||
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
|
||||
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmCtEteSE" :select="isSubmitCfmCtEteSE"></ResultDot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import emitter from '@/utils/emitter';
|
||||
import ResultCheck from '@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue';
|
||||
import ResultArrow from '@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue';
|
||||
import ResultDot from '@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
|
||||
|
||||
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected }
|
||||
},
|
||||
props: ['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE'],
|
||||
components: {
|
||||
ResultCheck,
|
||||
ResultArrow,
|
||||
ResultDot,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containstTasksData: null,
|
||||
startEndData: null,
|
||||
selectCfmSeqStart: null,
|
||||
selectCfmSeqEnd: null,
|
||||
selectCfmSeqDirectly: [],
|
||||
selectCfmSeqEventually: [],
|
||||
durationData: null,
|
||||
selectCfmPtEteStart: null, // Processing time
|
||||
selectCfmPtEteEnd: null,
|
||||
selectCfmPtEteSEStart: null,
|
||||
selectCfmPtEteSEEnd: null,
|
||||
selectCfmPtPStart: null,
|
||||
selectCfmPtPEnd: null,
|
||||
selectCfmPtPSEStart: null,
|
||||
selectCfmPtPSEEnd: null,
|
||||
selectCfmWtEteStart: null, // Waiting time
|
||||
selectCfmWtEteEnd: null,
|
||||
selectCfmWtEteSEStart: null,
|
||||
selectCfmWtEteSEEnd: null,
|
||||
selectCfmWtPStart: null,
|
||||
selectCfmWtPEnd: null,
|
||||
selectCfmWtPSEStart: null,
|
||||
selectCfmWtPSEEnd: null,
|
||||
selectCfmCtEteStart: null, // Cycle time
|
||||
selectCfmCtEteEnd: null,
|
||||
selectCfmCtEteSEStart: null,
|
||||
selectCfmCtEteSEEnd: null,
|
||||
startAndEndIsReset: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectCfmSeqSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmSeqStart) data.push(this.selectCfmSeqStart);
|
||||
if(this.selectCfmSeqEnd) data.push(this.selectCfmSeqEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
selectCfmPtEteSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmPtEteSEStart) data.push(this.selectCfmPtEteSEStart);
|
||||
if(this.selectCfmPtEteSEEnd) data.push(this.selectCfmPtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
selectCfmPtPSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmPtPSEStart) data.push(this.selectCfmPtPSEStart);
|
||||
if(this.selectCfmPtPSEEnd) data.push(this.selectCfmPtPSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'From': 1, 'To': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
selectCfmWtEteSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmWtEteSEStart) data.push(this.selectCfmWtEteSEStart);
|
||||
if(this.selectCfmWtEteSEEnd) data.push(this.selectCfmWtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
selectCfmWtPSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmWtPSEStart) data.push(this.selectCfmWtPSEStart);
|
||||
if(this.selectCfmWtPSEEnd) data.push(this.selectCfmWtPSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'From': 1, 'To': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
selectCfmCtEteSE: function() {
|
||||
const data = [];
|
||||
if(this.selectCfmCtEteSEStart) data.push(this.selectCfmCtEteSEStart);
|
||||
if(this.selectCfmCtEteSEEnd) data.push(this.selectCfmCtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* All reset
|
||||
*/
|
||||
reset() {
|
||||
this.containstTasksData = null;
|
||||
this.startEndData = null;
|
||||
this.selectCfmSeqStart = null;
|
||||
this.selectCfmSeqEnd = null;
|
||||
this.selectCfmSeqDirectly = [];
|
||||
this.selectCfmSeqEventually = [];
|
||||
this.durationData = null;
|
||||
this.selectCfmPtEteStart = null;
|
||||
this.selectCfmPtEteEnd = null;
|
||||
this.selectCfmPtEteSEStart = null;
|
||||
this.selectCfmPtEteSEEnd = null;
|
||||
this.selectCfmPtPStart = null;
|
||||
this.selectCfmPtPEnd = null;
|
||||
this.selectCfmPtPSEStart = null;
|
||||
this.selectCfmPtPSEEnd = null;
|
||||
this.selectCfmWtEteStart = null; // Waiting time
|
||||
this.selectCfmWtEteEnd = null;
|
||||
this.selectCfmWtEteSEStart = null;
|
||||
this.selectCfmWtEteSEEnd = null;
|
||||
this.selectCfmWtPStart = null;
|
||||
this.selectCfmWtPEnd = null;
|
||||
this.selectCfmWtPSEStart = null;
|
||||
this.selectCfmWtPSEEnd = null;
|
||||
this.selectCfmCtEteStart = null; // Cycle time
|
||||
this.selectCfmCtEteEnd = null;
|
||||
this.selectCfmCtEteSEStart = null;
|
||||
this.selectCfmCtEteSEEnd = null;
|
||||
this.startAndEndIsReset = true;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$emitter.on('actListData', (data) => {
|
||||
this.containstTasksData = data;
|
||||
});
|
||||
this.$emitter.on('actRadioData', (newData) => {
|
||||
const data = JSON.parse(JSON.stringify(newData)); // 深拷貝原始 cases 的內容
|
||||
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE']);
|
||||
|
||||
const categoryMapping = {
|
||||
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
|
||||
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
|
||||
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
|
||||
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
|
||||
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
|
||||
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
|
||||
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
|
||||
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
|
||||
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
|
||||
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
|
||||
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
|
||||
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
|
||||
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
|
||||
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
|
||||
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
|
||||
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
|
||||
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
|
||||
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
|
||||
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
|
||||
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
|
||||
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
|
||||
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
|
||||
};
|
||||
const state = reactive({
|
||||
containstTasksData: null,
|
||||
startEndData: null,
|
||||
selectCfmSeqStart: null,
|
||||
selectCfmSeqEnd: null,
|
||||
selectCfmSeqDirectly: [],
|
||||
selectCfmSeqEventually: [],
|
||||
durationData: null,
|
||||
selectCfmPtEteStart: null, // Processing time
|
||||
selectCfmPtEteEnd: null,
|
||||
selectCfmPtEteSEStart: null,
|
||||
selectCfmPtEteSEEnd: null,
|
||||
selectCfmPtPStart: null,
|
||||
selectCfmPtPEnd: null,
|
||||
selectCfmPtPSEStart: null,
|
||||
selectCfmPtPSEEnd: null,
|
||||
selectCfmWtEteStart: null, // Waiting time
|
||||
selectCfmWtEteEnd: null,
|
||||
selectCfmWtEteSEStart: null,
|
||||
selectCfmWtEteSEEnd: null,
|
||||
selectCfmWtPStart: null,
|
||||
selectCfmWtPEnd: null,
|
||||
selectCfmWtPSEStart: null,
|
||||
selectCfmWtPSEEnd: null,
|
||||
selectCfmCtEteStart: null, // Cycle time
|
||||
selectCfmCtEteEnd: null,
|
||||
selectCfmCtEteSEStart: null,
|
||||
selectCfmCtEteSEEnd: null,
|
||||
startAndEndIsReset: false,
|
||||
});
|
||||
|
||||
const updateSelection = (key, mainSelector, secondarySelector) => {
|
||||
if (this[mainSelector]) {
|
||||
if (data.task !== this[mainSelector]) this[secondarySelector] = null;
|
||||
}
|
||||
data.category = categoryMapping[key][0];
|
||||
this[mainSelector] = data;
|
||||
};
|
||||
const selectCfmSeqSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
|
||||
if(state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
if (categoryMapping[data.category]) {
|
||||
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
|
||||
if (secondarySelector) {
|
||||
updateSelection(data.category, mainSelector, secondarySelector);
|
||||
} else {
|
||||
data.category = category;
|
||||
this[mainSelector] = [data];
|
||||
}
|
||||
} else if (this.selectedRuleType === 'Activity duration') {
|
||||
this.durationData = [data.task];
|
||||
}
|
||||
});
|
||||
this.$emitter.on('getListSequence', (data) => {
|
||||
switch (data.category) {
|
||||
case 'cfmSeqDirectly':
|
||||
this.selectCfmSeqDirectly = data.task;
|
||||
break;
|
||||
case 'cfmSeqEventually':
|
||||
this.selectCfmSeqEventually = data.task;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.reset();
|
||||
});
|
||||
// Radio 切換時,資料要清空
|
||||
this.$emitter.on('isRadioChange', (data) => {
|
||||
if(data) this.reset();
|
||||
});
|
||||
this.$emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) this.reset();
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) this.reset();
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) this.reset();
|
||||
});
|
||||
},
|
||||
const selectCfmPtEteSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
|
||||
if(state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
const selectCfmPtPSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
|
||||
if(state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'From': 1, 'To': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
const selectCfmWtEteSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
|
||||
if(state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
const selectCfmWtPSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
|
||||
if(state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'From': 1, 'To': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
const selectCfmCtEteSE = computed(() => {
|
||||
const data = [];
|
||||
if(state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
|
||||
if(state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
|
||||
data.sort((a, b) => {
|
||||
const order = { 'Start': 1, 'End': 2};
|
||||
return order[a.category] - order[b.category];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
/**
|
||||
* All reset
|
||||
*/
|
||||
function reset() {
|
||||
state.containstTasksData = null;
|
||||
state.startEndData = null;
|
||||
state.selectCfmSeqStart = null;
|
||||
state.selectCfmSeqEnd = null;
|
||||
state.selectCfmSeqDirectly = [];
|
||||
state.selectCfmSeqEventually = [];
|
||||
state.durationData = null;
|
||||
state.selectCfmPtEteStart = null;
|
||||
state.selectCfmPtEteEnd = null;
|
||||
state.selectCfmPtEteSEStart = null;
|
||||
state.selectCfmPtEteSEEnd = null;
|
||||
state.selectCfmPtPStart = null;
|
||||
state.selectCfmPtPEnd = null;
|
||||
state.selectCfmPtPSEStart = null;
|
||||
state.selectCfmPtPSEEnd = null;
|
||||
state.selectCfmWtEteStart = null; // Waiting time
|
||||
state.selectCfmWtEteEnd = null;
|
||||
state.selectCfmWtEteSEStart = null;
|
||||
state.selectCfmWtEteSEEnd = null;
|
||||
state.selectCfmWtPStart = null;
|
||||
state.selectCfmWtPEnd = null;
|
||||
state.selectCfmWtPSEStart = null;
|
||||
state.selectCfmWtPSEEnd = null;
|
||||
state.selectCfmCtEteStart = null; // Cycle time
|
||||
state.selectCfmCtEteEnd = null;
|
||||
state.selectCfmCtEteSEStart = null;
|
||||
state.selectCfmCtEteSEEnd = null;
|
||||
state.startAndEndIsReset = true;
|
||||
}
|
||||
|
||||
// created() logic
|
||||
emitter.on('actListData', (data) => {
|
||||
state.containstTasksData = data;
|
||||
});
|
||||
emitter.on('actRadioData', (newData) => {
|
||||
const data = JSON.parse(JSON.stringify(newData)); // 深拷貝原始 cases 的內容
|
||||
|
||||
const categoryMapping = {
|
||||
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
|
||||
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
|
||||
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
|
||||
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
|
||||
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
|
||||
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
|
||||
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
|
||||
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
|
||||
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
|
||||
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
|
||||
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
|
||||
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
|
||||
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
|
||||
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
|
||||
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
|
||||
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
|
||||
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
|
||||
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
|
||||
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
|
||||
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
|
||||
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
|
||||
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
|
||||
};
|
||||
|
||||
const updateSelection = (key, mainSelector, secondarySelector) => {
|
||||
if (state[mainSelector]) {
|
||||
if (data.task !== state[mainSelector]) state[secondarySelector] = null;
|
||||
}
|
||||
data.category = categoryMapping[key][0];
|
||||
state[mainSelector] = data;
|
||||
};
|
||||
|
||||
if (categoryMapping[data.category]) {
|
||||
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
|
||||
if (secondarySelector) {
|
||||
updateSelection(data.category, mainSelector, secondarySelector);
|
||||
} else {
|
||||
data.category = category;
|
||||
state[mainSelector] = [data];
|
||||
}
|
||||
} else if (selectedRuleType.value === 'Activity duration') {
|
||||
state.durationData = [data.task];
|
||||
}
|
||||
});
|
||||
emitter.on('getListSequence', (data) => {
|
||||
switch (data.category) {
|
||||
case 'cfmSeqDirectly':
|
||||
state.selectCfmSeqDirectly = data.task;
|
||||
break;
|
||||
case 'cfmSeqEventually':
|
||||
state.selectCfmSeqEventually = data.task;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
emitter.on('reset', (data) => {
|
||||
reset();
|
||||
});
|
||||
// Radio 切換時,資料要清空
|
||||
emitter.on('isRadioChange', (data) => {
|
||||
if(data) reset();
|
||||
});
|
||||
emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) reset();
|
||||
});
|
||||
emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) reset();
|
||||
});
|
||||
emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) reset();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.disc) {
|
||||
|
||||
@@ -97,346 +97,316 @@
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import emitter from '@/utils/emitter';
|
||||
import ActList from './ActList.vue';
|
||||
import ActRadio from './ActRadio.vue';
|
||||
import ActSeqDrag from './ActSeqDrag.vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
|
||||
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
|
||||
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
|
||||
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
|
||||
} = storeToRefs(conformanceStore);
|
||||
const loadingStore = useLoadingStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
|
||||
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
|
||||
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
|
||||
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
|
||||
} = storeToRefs(conformanceStore);
|
||||
|
||||
return { isLoading, selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
|
||||
selectedActSeqMore, selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart,
|
||||
cfmPtEteEnd, cfmPtEteSE, cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE,
|
||||
cfmWtPStart, cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected,
|
||||
isEndSelected
|
||||
};
|
||||
},
|
||||
props: ['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
|
||||
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
|
||||
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
|
||||
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
|
||||
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
|
||||
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
|
||||
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
|
||||
],
|
||||
components: {
|
||||
ActList,
|
||||
ActRadio,
|
||||
ActSeqDrag
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
task: null,
|
||||
taskStart: null,
|
||||
taskEnd: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// Activity sequence
|
||||
cfmSeqStartData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataSeq.task;
|
||||
return this.isEndSelected ? this.setSeqStartAndEndData(this.cfmSeqEnd, 'sources', this.task) : this.cfmSeqStart.map(i => i.label);
|
||||
},
|
||||
cfmSeqEndData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataSeq.task;
|
||||
return this.isStartSelected ? this.setSeqStartAndEndData(this.cfmSeqStart, 'sinks', this.task) : this.cfmSeqEnd.map(i => i.label);
|
||||
},
|
||||
// Processing time
|
||||
cfmPtEteStartData: function() {
|
||||
return this.cfmPtEteStart.map(i => i.task);
|
||||
},
|
||||
cfmPtEteEndData: function() {
|
||||
return this.cfmPtEteEnd.map(i => i.task);
|
||||
},
|
||||
cfmPtEteSEStartData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataPtEte.task;
|
||||
return this.isEndSelected ? this.setStartAndEndData(this.cfmPtEteSE, 'end', this.task) : this.setTaskData(this.cfmPtEteSE, 'start');
|
||||
},
|
||||
cfmPtEteSEEndData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataPtEte.task;
|
||||
return this.isStartSelected ? this.setStartAndEndData(this.cfmPtEteSE, 'start', this.task) : this.setTaskData(this.cfmPtEteSE, 'end');
|
||||
},
|
||||
cfmPtPStartData: function() {
|
||||
return this.cfmPtPStart.map(i => i.task);
|
||||
},
|
||||
cfmPtPEndData: function() {
|
||||
return this.cfmPtPEnd.map(i => i.task);
|
||||
},
|
||||
cfmPtPSEStartData: function() {
|
||||
if(this.isSubmit && this.task === null) this.task = this.isSubmitShowDataPtP.task;
|
||||
return this.isEndSelected ? this.setStartAndEndData(this.cfmPtPSE, 'end', this.task) : this.setTaskData(this.cfmPtPSE, 'start');
|
||||
},
|
||||
cfmPtPSEEndData: function() {
|
||||
if(this.isSubmit && this.task === null) this.task = this.isSubmitShowDataPtP.task;
|
||||
return this.isStartSelected ? this.setStartAndEndData(this.cfmPtPSE, 'start', this.task) : this.setTaskData(this.cfmPtPSE, 'end');
|
||||
},
|
||||
// Waiting time
|
||||
cfmWtEteStartData: function() {
|
||||
return this.cfmWtEteStart.map(i => i.task);
|
||||
},
|
||||
cfmWtEteEndData: function() {
|
||||
return this.cfmWtEteEnd.map(i => i.task);
|
||||
},
|
||||
cfmWtEteSEStartData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtEte.task;
|
||||
return this.isEndSelected ? this.setStartAndEndData(this.cfmWtEteSE, 'end', this.task) : this.setTaskData(this.cfmWtEteSE, 'start');
|
||||
},
|
||||
cfmWtEteSEEndData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtEte.task;
|
||||
return this.isStartSelected ? this.setStartAndEndData(this.cfmWtEteSE, 'start', this.task) : this.setTaskData(this.cfmWtEteSE, 'end');
|
||||
},
|
||||
cfmWtPStartData: function() {
|
||||
return this.cfmWtPStart.map(i => i.task);
|
||||
},
|
||||
cfmWtPEndData: function() {
|
||||
return this.cfmWtPEnd.map(i => i.task);
|
||||
},
|
||||
cfmWtPSEStartData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtP.task;
|
||||
return this.isEndSelected ? this.setStartAndEndData(this.cfmWtPSE, 'end', this.task) : this.setTaskData(this.cfmWtPSE, 'start');
|
||||
},
|
||||
cfmWtPSEEndData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataWtP.task;
|
||||
return this.isStartSelected ? this.setStartAndEndData(this.cfmWtPSE, 'start', this.task) : this.setTaskData(this.cfmWtPSE, 'end');
|
||||
},
|
||||
// Cycle time
|
||||
cfmCtEteStartData: function() {
|
||||
return this.cfmCtEteStart.map(i => i.task);
|
||||
},
|
||||
cfmCtEteEndData: function() {
|
||||
return this.cfmCtEteEnd.map(i => i.task);
|
||||
},
|
||||
cfmCtEteSEStartData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataCt.task;
|
||||
return this.isEndSelected ? this.setStartAndEndData(this.cfmCtEteSE, 'end', this.task) : this.setTaskData(this.cfmCtEteSE, 'start');
|
||||
},
|
||||
cfmCtEteSEEndData: function() {
|
||||
if(this.isSubmit && this.task === null)this.task = this.isSubmitShowDataCt.task;
|
||||
return this.isStartSelected ? this.setStartAndEndData(this.cfmCtEteSE, 'start', this.task) : this.setTaskData(this.cfmCtEteSE, 'end');
|
||||
},
|
||||
},
|
||||
watch: { // 解決儲存後的 Rule 檔,無法重新更改規則之問題
|
||||
isSubmitShowDataSeq: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
isSubmitShowDataPtEte: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
isSubmitShowDataPtP: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
isSubmitShowDataWtEte: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
isSubmitShowDataWtP: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
isSubmitShowDataCt: {
|
||||
handler: function(newValue) {
|
||||
this.taskStart = newValue.taskStart;
|
||||
this.taskEnd = newValue.taskEnd;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 設定 start and end 的 Radio Data
|
||||
* @param {object} data cfmSeqStart | cfmSeqEnd | cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE,
|
||||
* 傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
|
||||
* @returns {array}
|
||||
*/
|
||||
setTaskData(data, category) {
|
||||
let newData = data.map(i => i[category]);
|
||||
newData = [...new Set(newData)]; // Set 是一種集合型別,只會儲存獨特的值。
|
||||
return newData;
|
||||
},
|
||||
/**
|
||||
* 重新設定連動的 start and end 的 Radio Data
|
||||
* @param {object} data cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE,
|
||||
* 傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
|
||||
* @param {string} task 已選擇的 Activity task
|
||||
* @returns {array}
|
||||
*/
|
||||
setStartAndEndData(data, category, task) {
|
||||
let oppositeCategory = '';
|
||||
if (category === 'start') {
|
||||
oppositeCategory = 'end';
|
||||
} else {
|
||||
oppositeCategory = 'start';
|
||||
};
|
||||
let newData = data.filter(i => i[category] === task).map(i => i[oppositeCategory]);
|
||||
newData = [...new Set(newData)];
|
||||
return newData;
|
||||
},
|
||||
/**
|
||||
* 重新設定 Activity sequence 連動的 start and end 的 Radio Data
|
||||
* @param {object} data cfmSeqStart | cfmSeqEnd,傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'sources' | 'sinks',傳入 'sources' 或 'sinks'。
|
||||
* @param {string} task 已選擇的 Activity task
|
||||
* @returns {array}
|
||||
*/
|
||||
setSeqStartAndEndData(data, category, task) {
|
||||
let newData = data.filter(i => i.label === task).map(i => i[category]);
|
||||
newData = [...new Set(...newData)];
|
||||
return newData;
|
||||
},
|
||||
/**
|
||||
* select start list's task
|
||||
* @param {event} e 觸發 input 的詳細事件
|
||||
*/
|
||||
selectStart(e) {
|
||||
this.taskStart = e;
|
||||
if(this.isStartSelected === null || this.isStartSelected === true){
|
||||
this.isStartSelected = true;
|
||||
this.isEndSelected = false;
|
||||
this.task = e;
|
||||
this.taskEnd = null;
|
||||
this.$emitter.emit('sratrAndEndToStart', {
|
||||
start: true,
|
||||
end: false,
|
||||
});
|
||||
};
|
||||
},
|
||||
/**
|
||||
* select End list's task
|
||||
* @param {event} e 觸發 input 的詳細事件
|
||||
*/
|
||||
selectEnd(e) {
|
||||
this.taskEnd = e;
|
||||
if(this.isEndSelected === null || this.isEndSelected === true){
|
||||
this.isEndSelected = true;
|
||||
this.isStartSelected = false;
|
||||
this.task = e;
|
||||
this.taskStart = null;
|
||||
this.$emitter.emit('sratrAndEndToStart', {
|
||||
start: false,
|
||||
end: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* reset all data.
|
||||
*/
|
||||
reset() {
|
||||
this.task = null;
|
||||
this.isStartSelected = null;
|
||||
this.isEndSelected = null;
|
||||
this.taskStart = null;
|
||||
this.taskEnd = null;
|
||||
},
|
||||
/**
|
||||
* Radio 切換時,Start & End Data 連動改變
|
||||
* @param {boolean} data true | false,傳入 true 或 false
|
||||
*/
|
||||
setResetData(data) {
|
||||
if(data) {
|
||||
if(this.isSubmit) {
|
||||
switch (this.selectedRuleType) {
|
||||
case 'Activity sequence':
|
||||
this.task = this.isSubmitShowDataSeq.task;
|
||||
this.isStartSelected = this.isSubmitShowDataSeq.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataSeq.isEndSelected;
|
||||
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
|
||||
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
|
||||
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
|
||||
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
|
||||
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
|
||||
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
|
||||
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
|
||||
]);
|
||||
|
||||
const task = ref(null);
|
||||
const taskStart = ref(null);
|
||||
const taskEnd = ref(null);
|
||||
|
||||
// Activity sequence
|
||||
const cfmSeqStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
|
||||
return isEndSelected.value ? setSeqStartAndEndData(cfmSeqEnd.value, 'sources', task.value) : cfmSeqStart.value.map(i => i.label);
|
||||
});
|
||||
const cfmSeqEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
|
||||
return isStartSelected.value ? setSeqStartAndEndData(cfmSeqStart.value, 'sinks', task.value) : cfmSeqEnd.value.map(i => i.label);
|
||||
});
|
||||
// Processing time
|
||||
const cfmPtEteStartData = computed(() => {
|
||||
return cfmPtEteStart.value.map(i => i.task);
|
||||
});
|
||||
const cfmPtEteEndData = computed(() => {
|
||||
return cfmPtEteEnd.value.map(i => i.task);
|
||||
});
|
||||
const cfmPtEteSEStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
|
||||
return isEndSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'end', task.value) : setTaskData(cfmPtEteSE.value, 'start');
|
||||
});
|
||||
const cfmPtEteSEEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
|
||||
return isStartSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'start', task.value) : setTaskData(cfmPtEteSE.value, 'end');
|
||||
});
|
||||
const cfmPtPStartData = computed(() => {
|
||||
return cfmPtPStart.value.map(i => i.task);
|
||||
});
|
||||
const cfmPtPEndData = computed(() => {
|
||||
return cfmPtPEnd.value.map(i => i.task);
|
||||
});
|
||||
const cfmPtPSEStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
|
||||
return isEndSelected.value ? setStartAndEndData(cfmPtPSE.value, 'end', task.value) : setTaskData(cfmPtPSE.value, 'start');
|
||||
});
|
||||
const cfmPtPSEEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
|
||||
return isStartSelected.value ? setStartAndEndData(cfmPtPSE.value, 'start', task.value) : setTaskData(cfmPtPSE.value, 'end');
|
||||
});
|
||||
// Waiting time
|
||||
const cfmWtEteStartData = computed(() => {
|
||||
return cfmWtEteStart.value.map(i => i.task);
|
||||
});
|
||||
const cfmWtEteEndData = computed(() => {
|
||||
return cfmWtEteEnd.value.map(i => i.task);
|
||||
});
|
||||
const cfmWtEteSEStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
|
||||
return isEndSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'end', task.value) : setTaskData(cfmWtEteSE.value, 'start');
|
||||
});
|
||||
const cfmWtEteSEEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
|
||||
return isStartSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'start', task.value) : setTaskData(cfmWtEteSE.value, 'end');
|
||||
});
|
||||
const cfmWtPStartData = computed(() => {
|
||||
return cfmWtPStart.value.map(i => i.task);
|
||||
});
|
||||
const cfmWtPEndData = computed(() => {
|
||||
return cfmWtPEnd.value.map(i => i.task);
|
||||
});
|
||||
const cfmWtPSEStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
|
||||
return isEndSelected.value ? setStartAndEndData(cfmWtPSE.value, 'end', task.value) : setTaskData(cfmWtPSE.value, 'start');
|
||||
});
|
||||
const cfmWtPSEEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
|
||||
return isStartSelected.value ? setStartAndEndData(cfmWtPSE.value, 'start', task.value) : setTaskData(cfmWtPSE.value, 'end');
|
||||
});
|
||||
// Cycle time
|
||||
const cfmCtEteStartData = computed(() => {
|
||||
return cfmCtEteStart.value.map(i => i.task);
|
||||
});
|
||||
const cfmCtEteEndData = computed(() => {
|
||||
return cfmCtEteEnd.value.map(i => i.task);
|
||||
});
|
||||
const cfmCtEteSEStartData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
|
||||
return isEndSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'end', task.value) : setTaskData(cfmCtEteSE.value, 'start');
|
||||
});
|
||||
const cfmCtEteSEEndData = computed(() => {
|
||||
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
|
||||
return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end');
|
||||
});
|
||||
|
||||
// Watchers - 解決儲存後的 Rule 檔,無法重新更改規則之問題
|
||||
watch(() => props.isSubmitShowDataSeq, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
watch(() => props.isSubmitShowDataPtEte, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
watch(() => props.isSubmitShowDataPtP, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
watch(() => props.isSubmitShowDataWtEte, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
watch(() => props.isSubmitShowDataWtP, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
watch(() => props.isSubmitShowDataCt, (newValue) => {
|
||||
taskStart.value = newValue.taskStart;
|
||||
taskEnd.value = newValue.taskEnd;
|
||||
});
|
||||
|
||||
/**
|
||||
* 設定 start and end 的 Radio Data
|
||||
* @param {object} data cfmSeqStart | cfmSeqEnd | cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE,
|
||||
* 傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
|
||||
* @returns {array}
|
||||
*/
|
||||
function setTaskData(data, category) {
|
||||
let newData = data.map(i => i[category]);
|
||||
newData = [...new Set(newData)]; // Set 是一種集合型別,只會儲存獨特的值。
|
||||
return newData;
|
||||
}
|
||||
/**
|
||||
* 重新設定連動的 start and end 的 Radio Data
|
||||
* @param {object} data cfmPtEteSE | cfmPtPSE | cfmWtEteSE | cfmWtPSE | cfmCtEteSE,
|
||||
* 傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'start' | 'end',傳入 'start' 或 'end'。
|
||||
* @param {string} task 已選擇的 Activity task
|
||||
* @returns {array}
|
||||
*/
|
||||
function setStartAndEndData(data, category, taskVal) {
|
||||
let oppositeCategory = '';
|
||||
if (category === 'start') {
|
||||
oppositeCategory = 'end';
|
||||
} else {
|
||||
oppositeCategory = 'start';
|
||||
};
|
||||
let newData = data.filter(i => i[category] === taskVal).map(i => i[oppositeCategory]);
|
||||
newData = [...new Set(newData)];
|
||||
return newData;
|
||||
}
|
||||
/**
|
||||
* 重新設定 Activity sequence 連動的 start and end 的 Radio Data
|
||||
* @param {object} data cfmSeqStart | cfmSeqEnd,傳入以上任一後端接到的 Activities 列表 Data。
|
||||
* @param {string} category 'sources' | 'sinks',傳入 'sources' 或 'sinks'。
|
||||
* @param {string} task 已選擇的 Activity task
|
||||
* @returns {array}
|
||||
*/
|
||||
function setSeqStartAndEndData(data, category, taskVal) {
|
||||
let newData = data.filter(i => i.label === taskVal).map(i => i[category]);
|
||||
newData = [...new Set(...newData)];
|
||||
return newData;
|
||||
}
|
||||
/**
|
||||
* select start list's task
|
||||
* @param {event} e 觸發 input 的詳細事件
|
||||
*/
|
||||
function selectStart(e) {
|
||||
taskStart.value = e;
|
||||
if(isStartSelected.value === null || isStartSelected.value === true){
|
||||
isStartSelected.value = true;
|
||||
isEndSelected.value = false;
|
||||
task.value = e;
|
||||
taskEnd.value = null;
|
||||
emitter.emit('sratrAndEndToStart', {
|
||||
start: true,
|
||||
end: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
/**
|
||||
* select End list's task
|
||||
* @param {event} e 觸發 input 的詳細事件
|
||||
*/
|
||||
function selectEnd(e) {
|
||||
taskEnd.value = e;
|
||||
if(isEndSelected.value === null || isEndSelected.value === true){
|
||||
isEndSelected.value = true;
|
||||
isStartSelected.value = false;
|
||||
task.value = e;
|
||||
taskStart.value = null;
|
||||
emitter.emit('sratrAndEndToStart', {
|
||||
start: false,
|
||||
end: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* reset all data.
|
||||
*/
|
||||
function reset() {
|
||||
task.value = null;
|
||||
isStartSelected.value = null;
|
||||
isEndSelected.value = null;
|
||||
taskStart.value = null;
|
||||
taskEnd.value = null;
|
||||
}
|
||||
/**
|
||||
* Radio 切換時,Start & End Data 連動改變
|
||||
* @param {boolean} data true | false,傳入 true 或 false
|
||||
*/
|
||||
function setResetData(data) {
|
||||
if(data) {
|
||||
if(props.isSubmit) {
|
||||
switch (selectedRuleType.value) {
|
||||
case 'Activity sequence':
|
||||
task.value = props.isSubmitShowDataSeq.task;
|
||||
isStartSelected.value = props.isSubmitShowDataSeq.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataSeq.isEndSelected;
|
||||
break;
|
||||
case 'Processing time':
|
||||
switch (selectedProcessScope.value) {
|
||||
case 'End to end':
|
||||
task.value = props.isSubmitShowDataPtEte.task;
|
||||
isStartSelected.value = props.isSubmitShowDataPtEte.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataPtEte.isEndSelected;
|
||||
break;
|
||||
case 'Processing time':
|
||||
switch (this.selectedProcessScope) {
|
||||
case 'End to end':
|
||||
this.task = this.isSubmitShowDataPtEte.task;
|
||||
this.isStartSelected = this.isSubmitShowDataPtEte.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataPtEte.isEndSelected;
|
||||
break;
|
||||
case 'Partial':
|
||||
this.task = this.isSubmitShowDataPtP.task;
|
||||
this.isStartSelected = this.isSubmitShowDataPtP.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataPtP.isEndSelected;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'Waiting time':
|
||||
switch (this.selectedProcessScope) {
|
||||
case 'End to end':
|
||||
this.task = this.isSubmitShowDataWtEte.task;
|
||||
this.isStartSelected = this.isSubmitShowDataWtEte.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataWtEte.isEndSelected;
|
||||
break;
|
||||
case 'Partial':
|
||||
this.task = this.isSubmitShowDataWtP.task;
|
||||
this.isStartSelected = this.isSubmitShowDataWtP.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataWtP.isEndSelected;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'Cycle time':
|
||||
this.task = this.isSubmitShowDataCt.task;
|
||||
this.isStartSelected = this.isSubmitShowDataCt.isStartSelected;
|
||||
this.isEndSelected = this.isSubmitShowDataCt.isEndSelected;
|
||||
case 'Partial':
|
||||
task.value = props.isSubmitShowDataPtP.task;
|
||||
isStartSelected.value = props.isSubmitShowDataPtP.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataPtP.isEndSelected;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
break;
|
||||
case 'Waiting time':
|
||||
switch (selectedProcessScope.value) {
|
||||
case 'End to end':
|
||||
task.value = props.isSubmitShowDataWtEte.task;
|
||||
isStartSelected.value = props.isSubmitShowDataWtEte.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
|
||||
break;
|
||||
case 'Partial':
|
||||
task.value = props.isSubmitShowDataWtP.task;
|
||||
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'Cycle time':
|
||||
task.value = props.isSubmitShowDataCt.task;
|
||||
isStartSelected.value = props.isSubmitShowDataCt.isStartSelected;
|
||||
isEndSelected.value = props.isSubmitShowDataCt.isEndSelected;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emitter.on('isRadioChange', (data) => {
|
||||
this.setResetData(data);
|
||||
});
|
||||
this.$emitter.on('isRadioSeqChange', (data) => {
|
||||
this.setResetData(data);
|
||||
});
|
||||
this.$emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) {
|
||||
this.setResetData(data);
|
||||
};
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) {
|
||||
this.setResetData(data);
|
||||
};
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) {
|
||||
this.setResetData(data);
|
||||
};
|
||||
});
|
||||
this.$emitter.on('reset', data => {
|
||||
this.reset();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// created() logic
|
||||
emitter.on('isRadioChange', (data) => {
|
||||
setResetData(data);
|
||||
});
|
||||
emitter.on('isRadioSeqChange', (data) => {
|
||||
setResetData(data);
|
||||
});
|
||||
emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) {
|
||||
setResetData(data);
|
||||
};
|
||||
});
|
||||
emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) {
|
||||
setResetData(data);
|
||||
};
|
||||
});
|
||||
emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) {
|
||||
setResetData(data);
|
||||
};
|
||||
});
|
||||
emitter.on('reset', data => {
|
||||
reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,373 +5,380 @@
|
||||
<div class=" text-sm leading-normal">
|
||||
<!-- Activity duration -->
|
||||
<TimeRangeDuration
|
||||
v-if="selectedRuleType === 'Activity duration'" :time="timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
|
||||
v-if="selectedRuleType === 'Activity duration'" :time="state.timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<!-- Processing time -->
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'All'" :time="timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'All'" :time="state.timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start'" :time="timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start'" :time="state.timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'End'" :time="timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'End'" :time="state.timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start & End'" :time="timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'From'" :time="timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'To'" :time="timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'From & To'" :time="timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<!-- Waiting time -->
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'All'" :time="timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'All'" :time="state.timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start'" :time="timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start'" :time="state.timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'End'" :time="timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'End'" :time="state.timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start & End'" :time="timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'From'" :time="timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'To'" :time="timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
|
||||
&& selectedActSeqFromTo === 'From & To'" :time="timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<!-- Cycle time -->
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'All'" :time="timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'All'" :time="state.timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start'" :time="timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start'" :time="state.timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'End'" :time="timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'End'" :time="state.timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
|
||||
&& selectedActSeqMore === 'Start & End'" :time="timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
|
||||
@max-total-seconds="maxTotalSeconds" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import emitter from '@/utils/emitter';
|
||||
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
|
||||
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
|
||||
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
|
||||
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
|
||||
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
|
||||
cfmWtEteWhole, cfmCtEteWhole
|
||||
} = storeToRefs(conformanceStore);
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
|
||||
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
|
||||
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
|
||||
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
|
||||
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
|
||||
cfmWtEteWhole, cfmCtEteWhole
|
||||
} = storeToRefs(conformanceStore);
|
||||
|
||||
return { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
|
||||
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
|
||||
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
|
||||
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
|
||||
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
|
||||
cfmWtEteWhole, cfmCtEteWhole
|
||||
};
|
||||
},
|
||||
props: ['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
|
||||
const props = defineProps(['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
|
||||
'isSubmitTimeCfmPtEteEnd', 'isSubmitTimeCfmPtEteSE', 'isSubmitTimeCfmPtPStart',
|
||||
'isSubmitTimeCfmPtPEnd', 'isSubmitTimeCfmPtPSE', 'isSubmitTimeCfmWtEteAll',
|
||||
'isSubmitTimeCfmWtEteStart', 'isSubmitTimeCfmWtEteEnd', 'isSubmitTimeCfmWtEteSE',
|
||||
'isSubmitTimeCfmWtPStart', 'isSubmitTimeCfmWtPEnd', 'isSubmitTimeCfmWtPSE', 'isSubmitTimeCfmCtEteAll',
|
||||
'isSubmitTimeCfmCtEteStart', 'isSubmitTimeCfmCtEteEnd', 'isSubmitTimeCfmCtEteSE'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
timeDuration: null, // Activity duration
|
||||
timeCfmPtEteAll: null, // Processing time
|
||||
timeCfmPtEteAllDefault: null,
|
||||
timeCfmPtEteStart: null,
|
||||
timeCfmPtEteEnd: null,
|
||||
timeCfmPtEteSE: null,
|
||||
timeCfmPtPStart: null,
|
||||
timeCfmPtPEnd: null,
|
||||
timeCfmPtPSE: null,
|
||||
timeCfmWtEteAll: null, // Waiting time
|
||||
timeCfmWtEteAllDefault: null,
|
||||
timeCfmWtEteStart: null,
|
||||
timeCfmWtEteEnd: null,
|
||||
timeCfmWtEteSE: null,
|
||||
timeCfmWtPStart: null,
|
||||
timeCfmWtPEnd: null,
|
||||
timeCfmWtPSE: null,
|
||||
timeCfmCtEteAll: null, // Cycle time
|
||||
timeCfmCtEteAllDefault: null,
|
||||
timeCfmCtEteStart: null,
|
||||
timeCfmCtEteEnd: null,
|
||||
timeCfmCtEteSE: null,
|
||||
selectCfmPtEteSEStart: null,
|
||||
selectCfmPtEteSEEnd: null,
|
||||
selectCfmPtPSEStart: null,
|
||||
selectCfmPtPSEEnd: null,
|
||||
selectCfmWtEteSEStart: null,
|
||||
selectCfmWtEteSEEnd: null,
|
||||
selectCfmWtPSEStart: null,
|
||||
selectCfmWtPSEEnd: null,
|
||||
selectCfmCtEteSEStart: null,
|
||||
selectCfmCtEteSEEnd: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TimeRangeDuration,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 最小值總秒數
|
||||
*/
|
||||
minTotalSeconds(e) {
|
||||
this.$emit('min-total-seconds', e);
|
||||
},
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 最大值總秒數
|
||||
*/
|
||||
maxTotalSeconds(e) {
|
||||
this.$emit('max-total-seconds', e);
|
||||
},
|
||||
/**
|
||||
* get Time Range(duration)
|
||||
* @param {array} data API data,Activity 列表
|
||||
* @param {string} category 'act' | 'single' | 'double',傳入以上任一值。
|
||||
* @param {string} task select Radio task or start
|
||||
* @param {string} taskTwo end
|
||||
* @returns {object} {min:12, max:345}
|
||||
*/
|
||||
getDurationTime(data, category, task, taskTwo) {
|
||||
let result = {min:0, max:0};
|
||||
switch (category) {
|
||||
case 'act':
|
||||
data.forEach(i => {
|
||||
if(i.label === task) {
|
||||
result = i.duration;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'single':
|
||||
data.forEach(i => {
|
||||
if(i.task === task) {
|
||||
result = i.time;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'double':
|
||||
data.forEach(i => {
|
||||
if(i.start === task && i.end === taskTwo) {
|
||||
result = i.time;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'all':
|
||||
result = data;
|
||||
break
|
||||
default:
|
||||
break;
|
||||
};
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* All reset
|
||||
*/
|
||||
reset() {
|
||||
this.timeDuration = null; // Activity duration
|
||||
this.timeCfmPtEteAll = this.timeCfmPtEteAllDefault; // Processing time
|
||||
this.timeCfmPtEteStart = null;
|
||||
this.timeCfmPtEteEnd = null;
|
||||
this.timeCfmPtEteSE = null;
|
||||
this.timeCfmPtPStart = null;
|
||||
this.timeCfmPtPEnd = null;
|
||||
this.timeCfmPtPSE = null;
|
||||
this.timeCfmWtEteAll = this.timeCfmWtEteAllDefault; // Waiting time
|
||||
this.timeCfmWtEteStart = null;
|
||||
this.timeCfmWtEteEnd = null;
|
||||
this.timeCfmWtEteSE = null;
|
||||
this.timeCfmWtPStart = null;
|
||||
this.timeCfmWtPEnd = null;
|
||||
this.timeCfmWtPSE = null;
|
||||
this.timeCfmCtEteAll = this.timeCfmCtEteAllDefault; // Cycle time
|
||||
this.timeCfmCtEteStart = null;
|
||||
this.timeCfmCtEteEnd = null;
|
||||
this.timeCfmCtEteSE = null;
|
||||
this.selectCfmPtEteSEStart = null;
|
||||
this.selectCfmPtEteSEEnd = null;
|
||||
this.selectCfmPtPSEStart = null;
|
||||
this.selectCfmPtPSEEnd = null;
|
||||
this.selectCfmWtEteSEStart = null;
|
||||
this.selectCfmWtEteSEEnd = null;
|
||||
this.selectCfmWtPSEStart = null;
|
||||
this.selectCfmWtPSEEnd = null;
|
||||
this.selectCfmCtEteSEStart = null;
|
||||
this.selectCfmCtEteSEEnd = null;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$emitter.on('actRadioData', (data) => {
|
||||
const category = data.category;
|
||||
const task = data.task;
|
||||
]);
|
||||
|
||||
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
|
||||
this[startKey] = task;
|
||||
this[timeKey] = { min: 0, max: 0 };
|
||||
if (this[endKey]) {
|
||||
this[timeKey] = this.getDurationTime(this[durationType], 'double', task, this[endKey]);
|
||||
}
|
||||
};
|
||||
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
|
||||
|
||||
const handleSingleSelection = (key, timeKey, durationType) => {
|
||||
this[timeKey] = this.getDurationTime(this[durationType], 'single', task);
|
||||
};
|
||||
const state = reactive({
|
||||
timeDuration: null, // Activity duration
|
||||
timeCfmPtEteAll: null, // Processing time
|
||||
timeCfmPtEteAllDefault: null,
|
||||
timeCfmPtEteStart: null,
|
||||
timeCfmPtEteEnd: null,
|
||||
timeCfmPtEteSE: null,
|
||||
timeCfmPtPStart: null,
|
||||
timeCfmPtPEnd: null,
|
||||
timeCfmPtPSE: null,
|
||||
timeCfmWtEteAll: null, // Waiting time
|
||||
timeCfmWtEteAllDefault: null,
|
||||
timeCfmWtEteStart: null,
|
||||
timeCfmWtEteEnd: null,
|
||||
timeCfmWtEteSE: null,
|
||||
timeCfmWtPStart: null,
|
||||
timeCfmWtPEnd: null,
|
||||
timeCfmWtPSE: null,
|
||||
timeCfmCtEteAll: null, // Cycle time
|
||||
timeCfmCtEteAllDefault: null,
|
||||
timeCfmCtEteStart: null,
|
||||
timeCfmCtEteEnd: null,
|
||||
timeCfmCtEteSE: null,
|
||||
selectCfmPtEteSEStart: null,
|
||||
selectCfmPtEteSEEnd: null,
|
||||
selectCfmPtPSEStart: null,
|
||||
selectCfmPtPSEEnd: null,
|
||||
selectCfmWtEteSEStart: null,
|
||||
selectCfmWtEteSEEnd: null,
|
||||
selectCfmWtPSEStart: null,
|
||||
selectCfmWtPSEEnd: null,
|
||||
selectCfmCtEteSEStart: null,
|
||||
selectCfmCtEteSEEnd: null,
|
||||
});
|
||||
|
||||
switch (category) {
|
||||
// Activity duration
|
||||
case 'cfmDur':
|
||||
this.timeDuration = this.getDurationTime(this.conformanceAllTasks, 'act', task);
|
||||
break;
|
||||
// Processing time
|
||||
case 'cfmPtEteStart':
|
||||
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
|
||||
break;
|
||||
case 'cfmPtEteEnd':
|
||||
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
|
||||
break;
|
||||
case 'cfmPtEteSEStart':
|
||||
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
|
||||
break;
|
||||
case 'cfmPtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
|
||||
break;
|
||||
case 'cfmPtPStart':
|
||||
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
|
||||
break;
|
||||
case 'cfmPtPEnd':
|
||||
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
|
||||
break;
|
||||
case 'cfmPtPSEStart':
|
||||
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
|
||||
break;
|
||||
case 'cfmPtPSEEnd':
|
||||
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
|
||||
break;
|
||||
// Waiting time
|
||||
case 'cfmWtEteStart':
|
||||
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
|
||||
break;
|
||||
case 'cfmWtEteEnd':
|
||||
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
|
||||
break;
|
||||
case 'cfmWtEteSEStart':
|
||||
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
|
||||
break;
|
||||
case 'cfmWtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
|
||||
break;
|
||||
case 'cfmWtPStart':
|
||||
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
|
||||
break;
|
||||
case 'cfmWtPEnd':
|
||||
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
|
||||
break;
|
||||
case 'cfmWtPSEStart':
|
||||
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
|
||||
break;
|
||||
case 'cfmWtPSEEnd':
|
||||
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
|
||||
break;
|
||||
// Cycle time
|
||||
case 'cfmCtEteStart':
|
||||
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
|
||||
break;
|
||||
case 'cfmCtEteEnd':
|
||||
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
|
||||
break;
|
||||
case 'cfmCtEteSEStart':
|
||||
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
|
||||
break;
|
||||
case 'cfmCtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
});
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.reset();
|
||||
});
|
||||
this.$emitter.on('isRadioChange', (data) => {
|
||||
if(data) {
|
||||
this.reset();
|
||||
switch (this.selectedRuleType) {
|
||||
case 'Processing time':
|
||||
this.timeCfmPtEteAll = this.getDurationTime(this.cfmPtEteWhole, 'all');
|
||||
this.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmPtEteAll));
|
||||
break;
|
||||
case 'Waiting time':
|
||||
this.timeCfmWtEteAll = this.getDurationTime(this.cfmWtEteWhole, 'all');
|
||||
this.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmWtEteAll));
|
||||
break;
|
||||
case 'Cycle time':
|
||||
this.timeCfmCtEteAll = this.getDurationTime(this.cfmCtEteWhole, 'all');
|
||||
this.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmCtEteAll));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
});
|
||||
this.$emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) {
|
||||
this.reset();
|
||||
};
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) {
|
||||
if(this.selectedActSeqMore === 'All') {
|
||||
switch (this.selectedRuleType) {
|
||||
case 'Processing time':
|
||||
this.timeCfmPtEteAll = this.getDurationTime(this.cfmPtEteWhole, 'all');
|
||||
this.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmPtEteAll));
|
||||
break;
|
||||
case 'Waiting time':
|
||||
this.timeCfmWtEteAll = this.getDurationTime(this.cfmWtEteWhole, 'all');
|
||||
this.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmWtEteAll));
|
||||
break;
|
||||
case 'Cycle time':
|
||||
this.timeCfmCtEteAll = this.getDurationTime(this.cfmCtEteWhole, 'all');
|
||||
this.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(this.timeCfmCtEteAll));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}else this.reset();
|
||||
};
|
||||
});
|
||||
this.$emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) {
|
||||
this.reset();
|
||||
};
|
||||
});
|
||||
},
|
||||
// Store refs lookup for dynamic access in handleSingleSelection/handleDoubleSelection
|
||||
const storeRefs = {
|
||||
cfmPtEteStart,
|
||||
cfmPtEteEnd,
|
||||
cfmPtEteSE,
|
||||
cfmPtPStart,
|
||||
cfmPtPEnd,
|
||||
cfmPtPSE,
|
||||
cfmWtEteStart,
|
||||
cfmWtEteEnd,
|
||||
cfmWtEteSE,
|
||||
cfmWtPStart,
|
||||
cfmWtPEnd,
|
||||
cfmWtPSE,
|
||||
cfmCtEteStart,
|
||||
cfmCtEteEnd,
|
||||
cfmCtEteSE,
|
||||
};
|
||||
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 最小值總秒數
|
||||
*/
|
||||
function minTotalSeconds(e) {
|
||||
emit('min-total-seconds', e);
|
||||
}
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 最大值總秒數
|
||||
*/
|
||||
function maxTotalSeconds(e) {
|
||||
emit('max-total-seconds', e);
|
||||
}
|
||||
/**
|
||||
* get Time Range(duration)
|
||||
* @param {array} data API data,Activity 列表
|
||||
* @param {string} category 'act' | 'single' | 'double',傳入以上任一值。
|
||||
* @param {string} task select Radio task or start
|
||||
* @param {string} taskTwo end
|
||||
* @returns {object} {min:12, max:345}
|
||||
*/
|
||||
function getDurationTime(data, category, task, taskTwo) {
|
||||
let result = {min:0, max:0};
|
||||
switch (category) {
|
||||
case 'act':
|
||||
data.forEach(i => {
|
||||
if(i.label === task) {
|
||||
result = i.duration;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'single':
|
||||
data.forEach(i => {
|
||||
if(i.task === task) {
|
||||
result = i.time;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'double':
|
||||
data.forEach(i => {
|
||||
if(i.start === task && i.end === taskTwo) {
|
||||
result = i.time;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'all':
|
||||
result = data;
|
||||
break
|
||||
default:
|
||||
break;
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* All reset
|
||||
*/
|
||||
function reset() {
|
||||
state.timeDuration = null; // Activity duration
|
||||
state.timeCfmPtEteAll = state.timeCfmPtEteAllDefault; // Processing time
|
||||
state.timeCfmPtEteStart = null;
|
||||
state.timeCfmPtEteEnd = null;
|
||||
state.timeCfmPtEteSE = null;
|
||||
state.timeCfmPtPStart = null;
|
||||
state.timeCfmPtPEnd = null;
|
||||
state.timeCfmPtPSE = null;
|
||||
state.timeCfmWtEteAll = state.timeCfmWtEteAllDefault; // Waiting time
|
||||
state.timeCfmWtEteStart = null;
|
||||
state.timeCfmWtEteEnd = null;
|
||||
state.timeCfmWtEteSE = null;
|
||||
state.timeCfmWtPStart = null;
|
||||
state.timeCfmWtPEnd = null;
|
||||
state.timeCfmWtPSE = null;
|
||||
state.timeCfmCtEteAll = state.timeCfmCtEteAllDefault; // Cycle time
|
||||
state.timeCfmCtEteStart = null;
|
||||
state.timeCfmCtEteEnd = null;
|
||||
state.timeCfmCtEteSE = null;
|
||||
state.selectCfmPtEteSEStart = null;
|
||||
state.selectCfmPtEteSEEnd = null;
|
||||
state.selectCfmPtPSEStart = null;
|
||||
state.selectCfmPtPSEEnd = null;
|
||||
state.selectCfmWtEteSEStart = null;
|
||||
state.selectCfmWtEteSEEnd = null;
|
||||
state.selectCfmWtPSEStart = null;
|
||||
state.selectCfmWtPSEEnd = null;
|
||||
state.selectCfmCtEteSEStart = null;
|
||||
state.selectCfmCtEteSEEnd = null;
|
||||
}
|
||||
|
||||
// created() logic
|
||||
emitter.on('actRadioData', (data) => {
|
||||
const category = data.category;
|
||||
const task = data.task;
|
||||
|
||||
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
|
||||
state[startKey] = task;
|
||||
state[timeKey] = { min: 0, max: 0 };
|
||||
if (state[endKey]) {
|
||||
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'double', task, state[endKey]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSingleSelection = (key, timeKey, durationType) => {
|
||||
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'single', task);
|
||||
};
|
||||
|
||||
switch (category) {
|
||||
// Activity duration
|
||||
case 'cfmDur':
|
||||
state.timeDuration = getDurationTime(conformanceAllTasks.value, 'act', task);
|
||||
break;
|
||||
// Processing time
|
||||
case 'cfmPtEteStart':
|
||||
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
|
||||
break;
|
||||
case 'cfmPtEteEnd':
|
||||
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
|
||||
break;
|
||||
case 'cfmPtEteSEStart':
|
||||
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
|
||||
break;
|
||||
case 'cfmPtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
|
||||
break;
|
||||
case 'cfmPtPStart':
|
||||
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
|
||||
break;
|
||||
case 'cfmPtPEnd':
|
||||
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
|
||||
break;
|
||||
case 'cfmPtPSEStart':
|
||||
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
|
||||
break;
|
||||
case 'cfmPtPSEEnd':
|
||||
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
|
||||
break;
|
||||
// Waiting time
|
||||
case 'cfmWtEteStart':
|
||||
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
|
||||
break;
|
||||
case 'cfmWtEteEnd':
|
||||
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
|
||||
break;
|
||||
case 'cfmWtEteSEStart':
|
||||
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
|
||||
break;
|
||||
case 'cfmWtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
|
||||
break;
|
||||
case 'cfmWtPStart':
|
||||
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
|
||||
break;
|
||||
case 'cfmWtPEnd':
|
||||
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
|
||||
break;
|
||||
case 'cfmWtPSEStart':
|
||||
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
|
||||
break;
|
||||
case 'cfmWtPSEEnd':
|
||||
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
|
||||
break;
|
||||
// Cycle time
|
||||
case 'cfmCtEteStart':
|
||||
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
|
||||
break;
|
||||
case 'cfmCtEteEnd':
|
||||
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
|
||||
break;
|
||||
case 'cfmCtEteSEStart':
|
||||
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
|
||||
break;
|
||||
case 'cfmCtEteSEEnd':
|
||||
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
});
|
||||
emitter.on('reset', (data) => {
|
||||
reset();
|
||||
});
|
||||
emitter.on('isRadioChange', (data) => {
|
||||
if(data) {
|
||||
reset();
|
||||
switch (selectedRuleType.value) {
|
||||
case 'Processing time':
|
||||
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
|
||||
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
|
||||
break;
|
||||
case 'Waiting time':
|
||||
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
|
||||
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
|
||||
break;
|
||||
case 'Cycle time':
|
||||
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
|
||||
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
});
|
||||
emitter.on('isRadioProcessScopeChange', (data) => {
|
||||
if(data) {
|
||||
reset();
|
||||
};
|
||||
});
|
||||
emitter.on('isRadioActSeqMoreChange', (data) => {
|
||||
if(data) {
|
||||
if(selectedActSeqMore.value === 'All') {
|
||||
switch (selectedRuleType.value) {
|
||||
case 'Processing time':
|
||||
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
|
||||
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
|
||||
break;
|
||||
case 'Waiting time':
|
||||
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
|
||||
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
|
||||
break;
|
||||
case 'Cycle time':
|
||||
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
|
||||
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}else reset();
|
||||
};
|
||||
});
|
||||
emitter.on('isRadioActSeqFromToChange', (data) => {
|
||||
if(data) {
|
||||
reset();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResultArrow',
|
||||
props:['data', 'select'],
|
||||
}
|
||||
<script setup>
|
||||
defineProps(['data', 'select']);
|
||||
</script>
|
||||
|
||||
@@ -8,26 +8,21 @@
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResultCheck',
|
||||
props:['data', 'select'],
|
||||
data() {
|
||||
return {
|
||||
datadata: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: function(newValue) {
|
||||
this.datadata = newValue;
|
||||
},
|
||||
select: function(newValue) {
|
||||
this.datadata = newValue;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.datadata = this.select;
|
||||
this.$emitter.on('reset', data => this.datadata = data);
|
||||
},
|
||||
}
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
const props = defineProps(['data', 'select']);
|
||||
|
||||
const datadata = ref(props.select);
|
||||
|
||||
watch(() => props.data, (newValue) => {
|
||||
datadata.value = newValue;
|
||||
});
|
||||
|
||||
watch(() => props.select, (newValue) => {
|
||||
datadata.value = newValue;
|
||||
});
|
||||
|
||||
emitter.on('reset', (val) => datadata.value = val);
|
||||
</script>
|
||||
|
||||
@@ -7,27 +7,17 @@
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResultDot',
|
||||
props:['timeResultData', 'select'],
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
timeResultData: {
|
||||
handler(newValue) {
|
||||
this.data = newValue;
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.data = this.select;
|
||||
this.$emitter.on('reset', data => this.data = data);
|
||||
},
|
||||
}
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
const props = defineProps(['timeResultData', 'select']);
|
||||
|
||||
const data = ref(props.select);
|
||||
|
||||
watch(() => props.timeResultData, (newValue) => {
|
||||
data.value = newValue;
|
||||
}, { deep: true });
|
||||
|
||||
emitter.on('reset', (val) => data.value = val);
|
||||
</script>
|
||||
|
||||
@@ -9,97 +9,78 @@
|
||||
</Durationjs>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Durationjs from '@/components/durationjs.vue';
|
||||
|
||||
export default {
|
||||
props: ['time', 'select'],
|
||||
data() {
|
||||
return {
|
||||
timeData: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
timeRangeMin: 0,
|
||||
timeRangeMax: 0,
|
||||
minVuemin: 0,
|
||||
minVuemax: 0,
|
||||
maxVuemin: 0,
|
||||
maxVuemax: 0,
|
||||
updateMax: null,
|
||||
updateMin: null,
|
||||
durationMin: null,
|
||||
durationMax: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Durationjs,
|
||||
},
|
||||
watch: {
|
||||
time: {
|
||||
handler: function(newValue, oldValue) {
|
||||
this.durationMax = null
|
||||
this.durationMin = null
|
||||
if(newValue === null) {
|
||||
this.timeData = {
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
}else if(newValue !== null) {
|
||||
this.timeData = {
|
||||
min: newValue.min,
|
||||
max: newValue.max
|
||||
};
|
||||
this.$emit('min-total-seconds', newValue.min);
|
||||
this.$emit('max-total-seconds', newValue.max);
|
||||
}
|
||||
this.setTimeValue();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* set props values
|
||||
*/
|
||||
setTimeValue() {
|
||||
// 深拷貝原始 timeData 的內容
|
||||
this.minVuemin = JSON.parse(JSON.stringify(this.timeData.min));
|
||||
this.minVuemax = JSON.parse(JSON.stringify(this.timeData.max));
|
||||
this.maxVuemin = JSON.parse(JSON.stringify(this.timeData.min));
|
||||
this.maxVuemax = JSON.parse(JSON.stringify(this.timeData.max));
|
||||
},
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 元件傳來的最小值總秒數
|
||||
*/
|
||||
minTotalSeconds(e) {
|
||||
this.timeRangeMin = e;
|
||||
this.updateMin = e;
|
||||
this.$emit('min-total-seconds', e);
|
||||
},
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 元件傳來的最大值總秒數
|
||||
*/
|
||||
maxTotalSeconds(e) {
|
||||
this.timeRangeMax = e;
|
||||
this.updateMax = e;
|
||||
this.$emit('max-total-seconds', e);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if(this.select){
|
||||
if(Object.keys(this.select.base).length !== 0) {
|
||||
this.timeData = this.select.base;
|
||||
this.setTimeValue();
|
||||
}
|
||||
if(Object.keys(this.select.rule).length !== 0) {
|
||||
this.durationMin = this.select.rule.min;
|
||||
this.durationMax = this.select.rule.max;
|
||||
}
|
||||
}
|
||||
const props = defineProps(['time', 'select']);
|
||||
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
|
||||
|
||||
const timeData = ref({ min: 0, max: 0 });
|
||||
const timeRangeMin = ref(0);
|
||||
const timeRangeMax = ref(0);
|
||||
const minVuemin = ref(0);
|
||||
const minVuemax = ref(0);
|
||||
const maxVuemin = ref(0);
|
||||
const maxVuemax = ref(0);
|
||||
const updateMax = ref(null);
|
||||
const updateMin = ref(null);
|
||||
const durationMin = ref(null);
|
||||
const durationMax = ref(null);
|
||||
|
||||
/**
|
||||
* set props values
|
||||
*/
|
||||
function setTimeValue() {
|
||||
// 深拷貝原始 timeData 的內容
|
||||
minVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));
|
||||
minVuemax.value = JSON.parse(JSON.stringify(timeData.value.max));
|
||||
maxVuemin.value = JSON.parse(JSON.stringify(timeData.value.min));
|
||||
maxVuemax.value = JSON.parse(JSON.stringify(timeData.value.max));
|
||||
}
|
||||
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 元件傳來的最小值總秒數
|
||||
*/
|
||||
function minTotalSeconds(e) {
|
||||
timeRangeMin.value = e;
|
||||
updateMin.value = e;
|
||||
emit('min-total-seconds', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* get min total seconds
|
||||
* @param {Number} e 元件傳來的最大值總秒數
|
||||
*/
|
||||
function maxTotalSeconds(e) {
|
||||
timeRangeMax.value = e;
|
||||
updateMax.value = e;
|
||||
emit('max-total-seconds', e);
|
||||
}
|
||||
|
||||
watch(() => props.time, (newValue, oldValue) => {
|
||||
durationMax.value = null;
|
||||
durationMin.value = null;
|
||||
if(newValue === null) {
|
||||
timeData.value = { min: 0, max: 0 };
|
||||
}else if(newValue !== null) {
|
||||
timeData.value = { min: newValue.min, max: newValue.max };
|
||||
emit('min-total-seconds', newValue.min);
|
||||
emit('max-total-seconds', newValue.max);
|
||||
}
|
||||
setTimeValue();
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
// created
|
||||
if(props.select){
|
||||
if(Object.keys(props.select.base).length !== 0) {
|
||||
timeData.value = props.select.base;
|
||||
setTimeValue();
|
||||
}
|
||||
if(Object.keys(props.select.rule).length !== 0) {
|
||||
durationMin.value = props.select.rule.min;
|
||||
durationMax.value = props.select.rule.max;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :visible="listModal" @update:visible="$emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
|
||||
<Dialog :visible="listModal" @update:visible="emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
|
||||
<template #header>
|
||||
<div class=" py-5">
|
||||
<p class="text-base font-bold">Non-conformance Issue</p>
|
||||
@@ -61,219 +61,225 @@
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
|
||||
|
||||
export default {
|
||||
props: ['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category'],
|
||||
setup() {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { infinite404 } = storeToRefs(conformanceStore);
|
||||
const props = defineProps(['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category']);
|
||||
const emit = defineEmits(['closeModal']);
|
||||
|
||||
return { infinite404, conformanceStore }
|
||||
},
|
||||
data() {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { infinite404 } = storeToRefs(conformanceStore);
|
||||
|
||||
// template ref
|
||||
const cfmTrace = useTemplateRef('cfmTrace');
|
||||
|
||||
// data
|
||||
const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
|
||||
const showTraceId = ref(null);
|
||||
const infiniteData = ref(null);
|
||||
const maxItems = ref(false);
|
||||
const infiniteFinish = ref(true); // 無限滾動是否載入完成
|
||||
const startNum = ref(0);
|
||||
const processMap = ref({
|
||||
nodes:[],
|
||||
edges:[],
|
||||
});
|
||||
|
||||
// computed
|
||||
const traceTotal = computed(() => {
|
||||
return traceList.value.length;
|
||||
});
|
||||
|
||||
const traceList = computed(() => {
|
||||
const sum = props.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
|
||||
return props.listTraces.map(trace => {
|
||||
return {
|
||||
contentClass: '!bg-neutral-100 border-t border-neutral-300 h-full',
|
||||
showTraceId: null,
|
||||
infiniteData: null,
|
||||
maxItems: false,
|
||||
infiniteFinish: true, // 無限滾動是否載入完成
|
||||
startNum: 0,
|
||||
processMap:{
|
||||
nodes:[],
|
||||
edges:[],
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
traceTotal: function() {
|
||||
return this.traceList.length;
|
||||
},
|
||||
traceList: function() {
|
||||
const sum = this.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
id: trace.id,
|
||||
value: Number((getPercentLabel(trace.count / sum))),
|
||||
count: trace.count.toLocaleString('en-US'),
|
||||
count_base: trace.count,
|
||||
ratio: getPercentLabel(trace.count / sum),
|
||||
};
|
||||
}).sort((x, y) => x.id - y.id);
|
||||
});
|
||||
|
||||
return this.listTraces.map(trace => {
|
||||
return {
|
||||
id: trace.id,
|
||||
value: Number((this.getPercentLabel(trace.count / sum))),
|
||||
count: trace.count.toLocaleString('en-US'),
|
||||
count_base: trace.count,
|
||||
ratio: this.getPercentLabel(trace.count / sum),
|
||||
};
|
||||
}).sort((x, y) => x.id - y.id);
|
||||
},
|
||||
caseData: function() {
|
||||
if(this.infiniteData !== null){
|
||||
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.facets.forEach((facet, index) => {
|
||||
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.facets; // 刪除原本的 facets 屬性
|
||||
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columnData: function() {
|
||||
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
|
||||
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
|
||||
|
||||
const result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
|
||||
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
return result
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
listModal: function(newValue) { // 第一次打開 Modal 要繪圖
|
||||
if(newValue) this.createCy();
|
||||
},
|
||||
taskSeq: function(newValue){
|
||||
if (newValue !== null) this.createCy();
|
||||
},
|
||||
traceId: function(newValue) {
|
||||
// 當 traceId 屬性變化時更新 showTraceId
|
||||
this.showTraceId = newValue;
|
||||
},
|
||||
showTraceId: function(newValue, oldValue) {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
},
|
||||
firstCases: function(newValue, oldValue){
|
||||
this.infiniteData = newValue;
|
||||
},
|
||||
infinite404: function(newValue, oldValue){
|
||||
if (newValue === 404) this.maxItems = true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
},
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
},
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
*/
|
||||
async switchCaseData(id) {
|
||||
if(id == this.showTraceId) return;
|
||||
this.infinite404 = null;
|
||||
this.maxItems = false;
|
||||
this.startNum = 0;
|
||||
|
||||
let result;
|
||||
if(this.category === 'issue') result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, id, 0);
|
||||
else if(this.category === 'loop') result = await this.conformanceStore.getConformanceLoopsTraceDetail(this.listNo, id, 0);
|
||||
this.infiniteData = await result;
|
||||
this.showTraceId = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
|
||||
},
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
this.processMap.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
if(this.taskSeq !== null) {
|
||||
this.taskSeq.forEach((node, index) => {
|
||||
this.processMap.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
setEdgesData(){
|
||||
this.processMap.edges = [];
|
||||
if(this.taskSeq !== null) {
|
||||
this.taskSeq.forEach((edge, index) => {
|
||||
this.processMap.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
// 關係線數量筆節點少一個
|
||||
this.processMap.edges.pop();
|
||||
},
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
createCy(){
|
||||
this.$nextTick(() => {
|
||||
const graphId = this.$refs.cfmTrace;
|
||||
|
||||
this.setNodesData();
|
||||
this.setEdgesData();
|
||||
if(graphId !== null) cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
|
||||
const caseData = computed(() => {
|
||||
if(infiniteData.value !== null){
|
||||
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.facets.forEach((facet, index) => {
|
||||
item[`fac_${index}`] = facet.value; // 建立新的 key-value pair
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 載入數據
|
||||
*/
|
||||
async fetchData() {
|
||||
try {
|
||||
this.infiniteFinish = false;
|
||||
this.startNum += 20
|
||||
const result = await this.conformanceStore.getConformanceTraceDetail(this.listNo, this.showTraceId, this.startNum);
|
||||
this.infiniteData = await [...this.infiniteData, ...result];
|
||||
this.infiniteFinish = await true;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 監聽時回傳的事件
|
||||
*/
|
||||
handleScroll(event) {
|
||||
if(this.maxItems || this.infiniteData.length < 20 || this.infiniteFinish === false) return;
|
||||
delete item.facets; // 刪除原本的 facets 屬性
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
if (overScrollHeight) this.fetchData();
|
||||
},
|
||||
},
|
||||
const columnData = computed(() => {
|
||||
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容
|
||||
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
|
||||
|
||||
const result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
|
||||
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
return result
|
||||
});
|
||||
|
||||
// watch
|
||||
watch(() => props.listModal, (newValue) => { // 第一次打開 Modal 要繪圖
|
||||
if(newValue) createCy();
|
||||
});
|
||||
|
||||
watch(() => props.taskSeq, (newValue) => {
|
||||
if (newValue !== null) createCy();
|
||||
});
|
||||
|
||||
watch(() => props.traceId, (newValue) => {
|
||||
// 當 traceId 屬性變化時更新 showTraceId
|
||||
showTraceId.value = newValue;
|
||||
});
|
||||
|
||||
watch(showTraceId, (newValue, oldValue) => {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
});
|
||||
|
||||
watch(() => props.firstCases, (newValue) => {
|
||||
infiniteData.value = newValue;
|
||||
});
|
||||
|
||||
watch(infinite404, (newValue) => {
|
||||
if (newValue === 404) maxItems.value = true;
|
||||
});
|
||||
|
||||
// methods
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
function getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
}
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
function progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
}
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
*/
|
||||
async function switchCaseData(id) {
|
||||
if(id == showTraceId.value) return;
|
||||
infinite404.value = null;
|
||||
maxItems.value = false;
|
||||
startNum.value = 0;
|
||||
|
||||
let result;
|
||||
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0);
|
||||
else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
|
||||
infiniteData.value = await result;
|
||||
showTraceId.value = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
|
||||
}
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
function setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
processMap.value.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
if(props.taskSeq !== null) {
|
||||
props.taskSeq.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
function setEdgesData(){
|
||||
processMap.value.edges = [];
|
||||
if(props.taskSeq !== null) {
|
||||
props.taskSeq.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
// 關係線數量筆節點少一個
|
||||
processMap.value.edges.pop();
|
||||
}
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
function createCy(){
|
||||
nextTick(() => {
|
||||
const graphId = cfmTrace.value;
|
||||
|
||||
setNodesData();
|
||||
setEdgesData();
|
||||
if(graphId !== null) cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 無限滾動: 載入數據
|
||||
*/
|
||||
async function fetchData() {
|
||||
try {
|
||||
infiniteFinish.value = false;
|
||||
startNum.value += 20
|
||||
const result = await conformanceStore.getConformanceTraceDetail(props.listNo, showTraceId.value, startNum.value);
|
||||
infiniteData.value = await [...infiniteData.value, ...result];
|
||||
infiniteFinish.value = await true;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 監聽時回傳的事件
|
||||
*/
|
||||
function handleScroll(event) {
|
||||
if(maxItems.value || infiniteData.value.length < 20 || infiniteFinish.value === false) return;
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
|
||||
|
||||
if (overScrollHeight) fetchData();
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -57,104 +57,104 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { sortNumEngZhtwForFilter } from '@/module/sortNumEngZhtw.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filterTaskData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
progressWidth: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
listSeq: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
const props = defineProps({
|
||||
filterTaskData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listSequence: [],
|
||||
filteredData: this.filterTaskData,
|
||||
lastItemIndex: null,
|
||||
}
|
||||
progressWidth: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
computed: {
|
||||
data: function() {
|
||||
// Activity List 要排序
|
||||
this.filteredData = this.filteredData.sort((x, y) => {
|
||||
const diff = y.occurrences - x.occurrences;
|
||||
return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label);
|
||||
});
|
||||
return this.filteredData;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
listSeq(newval){
|
||||
this.listSequence = newval;
|
||||
},
|
||||
filterTaskData(newval){
|
||||
this.filteredData = newval;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* double click Activity List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
moveActItem(index, element){
|
||||
this.listSequence.push(element);
|
||||
},
|
||||
/**
|
||||
* double click Sequence List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
moveSeqItem(index, element){
|
||||
this.listSequence.splice(index, 1);
|
||||
},
|
||||
/**
|
||||
* get listSequence
|
||||
*/
|
||||
getComponentData(){
|
||||
this.$emit('update:listSeq', this.listSequence);
|
||||
},
|
||||
/**
|
||||
* Element dragging started
|
||||
* @param {event} evt input 傳入的事件
|
||||
*/
|
||||
onStart(evt) {
|
||||
const lastChild = evt.to.lastChild.lastChild;
|
||||
lastChild.style.display = 'none';
|
||||
// 隱藏拖曳元素原位置
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = 'none';
|
||||
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
const listIndex = this.listSequence.length - 1;
|
||||
if(evt.oldIndex === listIndex) this.lastItemIndex = listIndex;
|
||||
},
|
||||
/**
|
||||
* Element dragging ended
|
||||
* @param {event} evt input 傳入的事件
|
||||
*/
|
||||
onEnd(evt) {
|
||||
// 顯示拖曳元素
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = '';
|
||||
// 拖曳結束要顯示箭頭,但最後一個不用
|
||||
const lastChild = evt.item.lastChild;
|
||||
const listIndex = this.listSequence.length - 1
|
||||
if (evt.oldIndex !== listIndex) {
|
||||
lastChild.style.display = '';
|
||||
}
|
||||
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
this.lastItemIndex = null;
|
||||
},
|
||||
listSeq: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:listSeq']);
|
||||
|
||||
const listSequence = ref([]);
|
||||
const filteredData = ref(props.filterTaskData);
|
||||
const lastItemIndex = ref(null);
|
||||
|
||||
const data = computed(() => {
|
||||
// Activity List 要排序
|
||||
filteredData.value = filteredData.value.sort((x, y) => {
|
||||
const diff = y.occurrences - x.occurrences;
|
||||
return diff !== 0 ? diff : sortNumEngZhtwForFilter(x.label, y.label);
|
||||
});
|
||||
return filteredData.value;
|
||||
});
|
||||
|
||||
watch(() => props.listSeq, (newval) => {
|
||||
listSequence.value = newval;
|
||||
});
|
||||
|
||||
watch(() => props.filterTaskData, (newval) => {
|
||||
filteredData.value = newval;
|
||||
});
|
||||
|
||||
/**
|
||||
* double click Activity List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
function moveActItem(index, element) {
|
||||
listSequence.value.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* double click Sequence List
|
||||
* @param {number} index data item index
|
||||
* @param {object} element data item
|
||||
*/
|
||||
function moveSeqItem(index, element) {
|
||||
listSequence.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* get listSequence
|
||||
*/
|
||||
function getComponentData() {
|
||||
emit('update:listSeq', listSequence.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Element dragging started
|
||||
* @param {event} evt input 傳入的事件
|
||||
*/
|
||||
function onStart(evt) {
|
||||
const lastChild = evt.to.lastChild.lastChild;
|
||||
lastChild.style.display = 'none';
|
||||
// 隱藏拖曳元素原位置
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = 'none';
|
||||
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
const listIndex = listSequence.value.length - 1;
|
||||
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element dragging ended
|
||||
* @param {event} evt input 傳入的事件
|
||||
*/
|
||||
function onEnd(evt) {
|
||||
// 顯示拖曳元素
|
||||
const originalElement = evt.item;
|
||||
originalElement.style.display = '';
|
||||
// 拖曳結束要顯示箭頭,但最後一個不用
|
||||
const lastChild = evt.item.lastChild;
|
||||
const listIndex = listSequence.value.length - 1
|
||||
if (evt.oldIndex !== listIndex) {
|
||||
lastChild.style.display = '';
|
||||
}
|
||||
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
|
||||
lastItemIndex.value = null;
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -28,50 +28,42 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Search from '@/components/Search.vue';
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tableTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tableSelect: {
|
||||
type: [Object, Array],
|
||||
default: null
|
||||
},
|
||||
progressWidth: {
|
||||
type: Function,
|
||||
required: false,
|
||||
}
|
||||
const props = defineProps({
|
||||
tableTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
select: null,
|
||||
metaKey: true
|
||||
}
|
||||
tableData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
components: {
|
||||
Search,
|
||||
},
|
||||
watch: {
|
||||
tableSelect(newval){
|
||||
this.select = newval;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 將選取的 row 傳到父層
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
onRowSelect(e) {
|
||||
this.$emit('on-row-select', e)
|
||||
}
|
||||
tableSelect: {
|
||||
type: [Object, Array],
|
||||
default: null
|
||||
},
|
||||
progressWidth: {
|
||||
type: Function,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['on-row-select']);
|
||||
|
||||
const select = ref(null);
|
||||
const metaKey = ref(true);
|
||||
|
||||
watch(() => props.tableSelect, (newval) => {
|
||||
select.value = newval;
|
||||
});
|
||||
|
||||
/**
|
||||
* 將選取的 row 傳到父層
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
function onRowSelect(e) {
|
||||
emit('on-row-select', e);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,54 +39,49 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from '@/components/Search.vue';
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
export default {
|
||||
props: ['tableTitle', 'tableData', 'tableSelect', 'progressWidth'],
|
||||
data() {
|
||||
return {
|
||||
select: null,
|
||||
data: this.tableData
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Search,
|
||||
},
|
||||
watch: {
|
||||
tableSelect(newval){
|
||||
this.select = newval;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 選擇 Row 的行為
|
||||
*/
|
||||
onRowSelect() {
|
||||
this.$emit('on-row-select', this.select);
|
||||
},
|
||||
/**
|
||||
* 取消選取 Row 的行為
|
||||
*/
|
||||
onRowUnselect() {
|
||||
this.$emit('on-row-select', this.select);
|
||||
},
|
||||
/**
|
||||
* 全選 Row 的行為
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
onRowSelectAll(e) {
|
||||
this.select = e.data;
|
||||
this.$emit('on-row-select', this.select);
|
||||
},
|
||||
/**
|
||||
* 取消全選 Row 的行為
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
onRowUnelectAll(e) {
|
||||
this.select = null;
|
||||
this.$emit('on-row-select', this.select)
|
||||
}
|
||||
},
|
||||
const props = defineProps(['tableTitle', 'tableData', 'tableSelect', 'progressWidth']);
|
||||
|
||||
const emit = defineEmits(['on-row-select']);
|
||||
|
||||
const select = ref(null);
|
||||
const data = ref(props.tableData);
|
||||
|
||||
watch(() => props.tableSelect, (newval) => {
|
||||
select.value = newval;
|
||||
});
|
||||
|
||||
/**
|
||||
* 選擇 Row 的行為
|
||||
*/
|
||||
function onRowSelect() {
|
||||
emit('on-row-select', select.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消選取 Row 的行為
|
||||
*/
|
||||
function onRowUnselect() {
|
||||
emit('on-row-select', select.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全選 Row 的行為
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
function onRowSelectAll(e) {
|
||||
select.value = e.data;
|
||||
emit('on-row-select', select.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消全選 Row 的行為
|
||||
* @param {event} e input 傳入的事件
|
||||
*/
|
||||
function onRowUnelectAll(e) {
|
||||
select.value = null;
|
||||
emit('on-row-select', select.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,90 +38,89 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { delaySecond, } from '@/utils/timeUtil.js';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
|
||||
const emit = defineEmits(['submit-all']);
|
||||
const $toast = useToast();
|
||||
|
||||
return { isLoading, hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, allMapDataStore, tempFilterId }
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {boolean} e ture | false,可選 ture 或 false
|
||||
* @param {numble} index rule's index
|
||||
*/
|
||||
isRule(e, index){
|
||||
const rule = this.isRuleData[index];
|
||||
// 先取得 rule object
|
||||
// 為了讓 data 順序不亂掉,將值指向 0,submitAll 時再刪掉
|
||||
if(!e) this.temporaryData[index] = 0;
|
||||
else this.temporaryData[index] = rule;
|
||||
},
|
||||
/**
|
||||
* header:Funnel 刪除全部的 Funnel
|
||||
* @param {numble|string} index rule's index 或 全部
|
||||
*/
|
||||
async deleteRule(index) {
|
||||
if(index === 'all') {
|
||||
this.temporaryData = [];
|
||||
this.isRuleData = [];
|
||||
this.ruleData = [];
|
||||
if(this.tempFilterId) {
|
||||
this.isLoading = true;
|
||||
this.tempFilterId = await null;
|
||||
await this.allMapDataStore.getAllMapData();
|
||||
await this.allMapDataStore.getAllTrace(); // SidebarTrace 要連動
|
||||
await this.$emit('submit-all');
|
||||
this.isLoading = false;
|
||||
}
|
||||
this.$toast.success('Filter(s) deleted.');
|
||||
}else{
|
||||
this.$toast.success(`Filter deleted.`);
|
||||
this.temporaryData.splice(index, 1);
|
||||
this.isRuleData.splice(index, 1);
|
||||
this.ruleData.splice(index, 1);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* header:Funnel 發送暫存的選取資料
|
||||
*/
|
||||
async submitAll() {
|
||||
this.postRuleData = this.temporaryData.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目
|
||||
if(!this.postRuleData?.length) return this.$toast.error('Not selected');
|
||||
await this.allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果
|
||||
const loadingStore = useLoadingStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
|
||||
|
||||
if(this.hasResultRule === null) {
|
||||
return;
|
||||
} else if(this.hasResultRule) {
|
||||
this.isLoading = true;
|
||||
await this.allMapDataStore.addTempFilterId();
|
||||
await this.allMapDataStore.getAllMapData();
|
||||
await this.allMapDataStore.getAllTrace(); // SidebarTrace 要連動
|
||||
if(this.temporaryData[0]?.type) {
|
||||
this.allMapDataStore.traceId = await this.allMapDataStore.traces[0]?.id;
|
||||
}
|
||||
await this.$emit('submit-all');
|
||||
this.isLoading = false;
|
||||
this.$toast.success('Filter(s) applied.');
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* @param {boolean} e ture | false,可選 ture 或 false
|
||||
* @param {numble} index rule's index
|
||||
*/
|
||||
function isRule(e, index){
|
||||
const rule = isRuleData.value[index];
|
||||
// 先取得 rule object
|
||||
// 為了讓 data 順序不亂掉,將值指向 0,submitAll 時再刪掉
|
||||
if(!e) temporaryData.value[index] = 0;
|
||||
else temporaryData.value[index] = rule;
|
||||
}
|
||||
|
||||
// sonar-qube "This statement will not be executed conditionally"
|
||||
this.isLoading = true;
|
||||
await delaySecond(1);
|
||||
this.isLoading = false;
|
||||
this.$toast.warning('No result.');
|
||||
},
|
||||
/**
|
||||
* header:Funnel 刪除全部的 Funnel
|
||||
* @param {numble|string} index rule's index 或 全部
|
||||
*/
|
||||
async function deleteRule(index) {
|
||||
if(index === 'all') {
|
||||
temporaryData.value = [];
|
||||
isRuleData.value = [];
|
||||
ruleData.value = [];
|
||||
if(tempFilterId.value) {
|
||||
isLoading.value = true;
|
||||
tempFilterId.value = await null;
|
||||
await allMapDataStore.getAllMapData();
|
||||
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動
|
||||
await emit('submit-all');
|
||||
isLoading.value = false;
|
||||
}
|
||||
$toast.success('Filter(s) deleted.');
|
||||
}else{
|
||||
$toast.success(`Filter deleted.`);
|
||||
temporaryData.value.splice(index, 1);
|
||||
isRuleData.value.splice(index, 1);
|
||||
ruleData.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* header:Funnel 發送暫存的選取資料
|
||||
*/
|
||||
async function submitAll() {
|
||||
postRuleData.value = temporaryData.value.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目
|
||||
if(!postRuleData.value?.length) return $toast.error('Not selected');
|
||||
await allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果
|
||||
|
||||
if(hasResultRule.value === null) {
|
||||
return;
|
||||
} else if(hasResultRule.value) {
|
||||
isLoading.value = true;
|
||||
await allMapDataStore.addTempFilterId();
|
||||
await allMapDataStore.getAllMapData();
|
||||
await allMapDataStore.getAllTrace(); // SidebarTrace 要連動
|
||||
if(temporaryData.value[0]?.type) {
|
||||
allMapDataStore.traceId = await allMapDataStore.traces[0]?.id;
|
||||
}
|
||||
await emit('submit-all');
|
||||
isLoading.value = false;
|
||||
$toast.success('Filter(s) applied.');
|
||||
return;
|
||||
}
|
||||
|
||||
// sonar-qube "This statement will not be executed conditionally"
|
||||
isLoading.value = true;
|
||||
await delaySecond(1);
|
||||
isLoading.value = false;
|
||||
$toast.warning('No result.');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -30,327 +30,326 @@
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import 'chartjs-adapter-moment';
|
||||
import getMoment from 'moment';
|
||||
|
||||
export default{
|
||||
props:['selectValue'],
|
||||
setup() {
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
|
||||
const props = defineProps(['selectValue']);
|
||||
|
||||
return {allMapDataStore, filterTimeframe, selectTimeFrame }
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
|
||||
|
||||
const selectRange = ref(1000); // 更改 select 的切分數
|
||||
const selectArea = ref(null);
|
||||
const chart = ref(null);
|
||||
const canvasId = ref(null);
|
||||
const startTime = ref(null);
|
||||
const endTime = ref(null);
|
||||
const startMinDate = ref(null);
|
||||
const startMaxDate = ref(null);
|
||||
const endMinDate = ref(null);
|
||||
const endMaxDate = ref(null);
|
||||
const panelProps = ref({
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectRange: 1000, // 更改 select 的切分數
|
||||
selectArea: null,
|
||||
chart: null,
|
||||
canvasId: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
startMinDate: null,
|
||||
startMaxDate: null,
|
||||
endMinDate: null,
|
||||
endMaxDate: null,
|
||||
panelProps: {
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// user select time start and end
|
||||
timeFrameStartEnd: function() {
|
||||
const start = getMoment(this.startTime).format('YYYY-MM-DDTHH:mm:00');
|
||||
const end = getMoment(this.endTime).format('YYYY-MM-DDTHH:mm:00');
|
||||
this.selectTimeFrame = [start, end]; // 傳給後端的資料
|
||||
});
|
||||
|
||||
return [start, end];
|
||||
},
|
||||
// 找出 slidrData,時間格式:毫秒時間戳
|
||||
sliderData: function() {
|
||||
const xAxisMin = new Date(this.filterTimeframe.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(this.filterTimeframe.x_axis.max).getTime();
|
||||
const range = xAxisMax - xAxisMin;
|
||||
const step = range / this.selectRange;
|
||||
const sliderData = []
|
||||
// user select time start and end
|
||||
const timeFrameStartEnd = computed(() => {
|
||||
const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
|
||||
const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
|
||||
selectTimeFrame.value = [start, end]; // 傳給後端的資料
|
||||
|
||||
for (let i = 0; i <= this.selectRange; i++) {
|
||||
sliderData.push(xAxisMin + (step * i));
|
||||
}
|
||||
return [start, end];
|
||||
});
|
||||
|
||||
return sliderData;
|
||||
},
|
||||
// 加入最大、最小值
|
||||
timeFrameData: function(){
|
||||
const data = this.filterTimeframe.data.map(i=>({x:i.x,y:i.y}))
|
||||
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
|
||||
// x 值為 0 ~ 11,
|
||||
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
|
||||
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a)
|
||||
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c)
|
||||
// 找出 slidrData,時間格式:毫秒時間戳
|
||||
const sliderData = computed(() => {
|
||||
const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
|
||||
const range = xAxisMax - xAxisMin;
|
||||
const step = range / selectRange.value;
|
||||
const data = []
|
||||
|
||||
// y 軸最小值
|
||||
const a = 0;
|
||||
let b;
|
||||
const c = 1;
|
||||
const d = this.filterTimeframe.data[0].y;
|
||||
const e = 2;
|
||||
const f = this.filterTimeframe.data[1].y;
|
||||
b = (e*d - a*d - f*a - f*c) / (e - c - a);
|
||||
if(b < 0) {
|
||||
b = 0;
|
||||
}
|
||||
// y 軸最大值
|
||||
const ma = 9;
|
||||
const mb = this.filterTimeframe.data[8].y;
|
||||
const mc = 10;
|
||||
const md = this.filterTimeframe.data[9].y;
|
||||
const me = 11;
|
||||
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
|
||||
if(mf < 0) {
|
||||
mf = 0;
|
||||
}
|
||||
for (let i = 0; i <= selectRange.value; i++) {
|
||||
data.push(xAxisMin + (step * i));
|
||||
}
|
||||
|
||||
// 添加最小值
|
||||
data.unshift({
|
||||
x: this.filterTimeframe.x_axis.min_base,
|
||||
y: b,
|
||||
})
|
||||
// 添加最大值
|
||||
data.push({
|
||||
x: this.filterTimeframe.x_axis.max_base,
|
||||
y: mf,
|
||||
})
|
||||
return data;
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
labelsData: function() {
|
||||
const min = new Date(this.filterTimeframe.x_axis.min_base).getTime();
|
||||
const max = new Date(this.filterTimeframe.x_axis.max_base).getTime();
|
||||
const numPoints = 11;
|
||||
const step = (max - min) / (numPoints - 1);
|
||||
const data = [];
|
||||
for(let i = 0; i< numPoints; i++) {
|
||||
const x = min + i * step;
|
||||
data.push(x);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
watch:{
|
||||
selectTimeFrame(newValue, oldValue) {
|
||||
if(newValue.length === 0) {
|
||||
this.startTime = new Date(this.filterTimeframe.x_axis.min);
|
||||
this.endTime = new Date(this.filterTimeframe.x_axis.max);
|
||||
this.selectArea = [0, this.selectRange];
|
||||
this.resizeMask(this.chart);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 調整遮罩大小
|
||||
* @param {object} chart 取得 chart.js 資料
|
||||
*/
|
||||
resizeMask(chart) {
|
||||
const from = (this.selectArea[0] * 0.01) / (this.selectRange * 0.01);
|
||||
const to = (this.selectArea[1] * 0.01) / (this.selectRange * 0.01);
|
||||
if(this.selectValue[0] === 'Timeframes') {
|
||||
this.resizeLeftMask(chart, from);
|
||||
this.resizeRightMask(chart, to);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 調整左邊遮罩大小
|
||||
* @param {object} chart 取得 chart.js 資料
|
||||
*/
|
||||
resizeLeftMask(chart, from) {
|
||||
const canvas = document.getElementById("chartCanvasId");
|
||||
const mask = document.getElementById("chart-mask-left");
|
||||
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
|
||||
mask.style.width = `${chart.chartArea.width * from}px`;
|
||||
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
|
||||
mask.style.height = `${chart.chartArea.height}px`;
|
||||
},
|
||||
/**
|
||||
* 調整右邊遮罩大小
|
||||
* @param {object} chart 取得 chart.js 資料
|
||||
*/
|
||||
resizeRightMask(chart, to) {
|
||||
const canvas = document.getElementById("chartCanvasId");
|
||||
const mask = document.getElementById("chart-mask-right");
|
||||
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
|
||||
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
|
||||
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
|
||||
mask.style.height = `${chart.chartArea.height}px`;
|
||||
},
|
||||
/**
|
||||
* create chart
|
||||
*/
|
||||
createChart() {
|
||||
const max = this.filterTimeframe.y_axis.max * 1.1;
|
||||
const minX = this.timeFrameData[0]?.x;
|
||||
const maxX = this.timeFrameData[this.timeFrameData.length - 1]?.x;
|
||||
// 加入最大、最小值
|
||||
const timeFrameData = computed(() => {
|
||||
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
|
||||
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
|
||||
// x 值為 0 ~ 11,
|
||||
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
|
||||
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a)
|
||||
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c)
|
||||
|
||||
const data = {
|
||||
labels:this.labelsData,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Case',
|
||||
data: this.timeFrameData,
|
||||
fill: 'start',
|
||||
showLine: false,
|
||||
tension: 0.4,
|
||||
backgroundColor: 'rgba(0,153,255)',
|
||||
pointRadius: 0,
|
||||
x: 'x',
|
||||
y: 'y',
|
||||
}
|
||||
]
|
||||
};
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: false, // 圖例
|
||||
filler: {
|
||||
propagate: false
|
||||
},
|
||||
title: false
|
||||
},
|
||||
// animations: false, // 取消動畫
|
||||
animation: {
|
||||
onComplete: e => {
|
||||
this.resizeMask(e.chart);
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: true,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
min: minX,
|
||||
max: maxX,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0, // 不旋轉 lable 0~50
|
||||
color: '#334155',
|
||||
display: true,
|
||||
source: 'labels',
|
||||
},
|
||||
grid: {
|
||||
display: false, // 隱藏 x 軸網格
|
||||
},
|
||||
time: {
|
||||
minUnit: 'day', // 顯示最小單位
|
||||
// displayFormats: {
|
||||
// minute: 'HH:mm MMM d',
|
||||
// hour: 'HH:mm MMM d',
|
||||
// }
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true, // scale 包含 0
|
||||
max: max,
|
||||
ticks: { // 設定間隔數值
|
||||
display: false, // 隱藏數值,只顯示格線
|
||||
stepSize: max / 4,
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(100,116,139)',
|
||||
z: 1,
|
||||
},
|
||||
border: {
|
||||
display: false, // 隱藏左側多出來的線
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options,
|
||||
};
|
||||
this.canvasId = document.getElementById("chartCanvasId");
|
||||
this.chart = new Chart(this.canvasId, config);
|
||||
},
|
||||
/**
|
||||
* 滑塊改變的時候
|
||||
* @param {array} e [1, 100]
|
||||
*/
|
||||
changeSelectArea(e) {
|
||||
// 日曆改變時,滑塊跟著改變
|
||||
const sliderData = this.sliderData;
|
||||
const start = sliderData[e[0].toFixed()];
|
||||
const end = sliderData[e[1].toFixed()]; // 取得 index,須為整數。
|
||||
// y 軸最小值
|
||||
const a = 0;
|
||||
let b;
|
||||
const c = 1;
|
||||
const d = filterTimeframe.value.data[0].y;
|
||||
const e = 2;
|
||||
const f = filterTimeframe.value.data[1].y;
|
||||
b = (e*d - a*d - f*a - f*c) / (e - c - a);
|
||||
if(b < 0) {
|
||||
b = 0;
|
||||
}
|
||||
// y 軸最大值
|
||||
const ma = 9;
|
||||
const mb = filterTimeframe.value.data[8].y;
|
||||
const mc = 10;
|
||||
const md = filterTimeframe.value.data[9].y;
|
||||
const me = 11;
|
||||
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
|
||||
if(mf < 0) {
|
||||
mf = 0;
|
||||
}
|
||||
|
||||
this.startTime = new Date(start);
|
||||
this.endTime = new Date(end);
|
||||
// 重新設定 start end 日曆選取範圍
|
||||
this.endMinDate = new Date(start);
|
||||
this.startMaxDate = new Date(end);
|
||||
// 重新算圖
|
||||
this.resizeMask(this.chart);
|
||||
// 執行 timeFrameStartEnd 才會改變數據
|
||||
this.timeFrameStartEnd();
|
||||
},
|
||||
/**
|
||||
* 選取開始或結束時間時,要改變滑塊跟圖表
|
||||
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
|
||||
* @param {string} direction start or end
|
||||
*/
|
||||
sliderTimeRange(e, direction) {
|
||||
// 找到最鄰近的 index,時間格式: 毫秒時間戳
|
||||
const sliderData = this.sliderData;
|
||||
const targetTime = [new Date(this.timeFrameStartEnd[0]).getTime(), new Date(this.timeFrameStartEnd[1]).getTime()];
|
||||
const closestIndexes = targetTime.map(target => {
|
||||
let closestIndex = 0;
|
||||
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
|
||||
let result = Math.round(Math.abs(closestIndex));
|
||||
result = result > this.selectRange ? this.selectRange : result;
|
||||
return result
|
||||
});
|
||||
// 添加最小值
|
||||
data.unshift({
|
||||
x: filterTimeframe.value.x_axis.min_base,
|
||||
y: b,
|
||||
})
|
||||
// 添加最大值
|
||||
data.push({
|
||||
x: filterTimeframe.value.x_axis.max_base,
|
||||
y: mf,
|
||||
})
|
||||
|
||||
// 改變滑塊
|
||||
this.selectArea = closestIndexes;
|
||||
// 重新設定 start end 日曆選取範圍
|
||||
if(direction === 'start') this.endMinDate = e;
|
||||
else if(direction === 'end') this.startMaxDate = e;
|
||||
// 重新算圖
|
||||
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) this.resizeMask(this.chart);
|
||||
else return;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Chart.js
|
||||
Chart.register(...registerables);
|
||||
this.createChart();
|
||||
// Slider
|
||||
this.selectArea = [0, this.selectRange];
|
||||
// Calendar
|
||||
this.startMinDate = new Date(this.filterTimeframe.x_axis.min);
|
||||
this.startMaxDate = new Date(this.filterTimeframe.x_axis.max);
|
||||
this.endMinDate = new Date(this.filterTimeframe.x_axis.min);
|
||||
this.endMaxDate = new Date(this.filterTimeframe.x_axis.max);
|
||||
// 讓日曆的範圍等於時間軸的範圍
|
||||
this.startTime = this.startMinDate;
|
||||
this.endTime = this.startMaxDate;
|
||||
this.timeFrameStartEnd();
|
||||
},
|
||||
return data;
|
||||
});
|
||||
|
||||
const labelsData = computed(() => {
|
||||
const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
|
||||
const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
|
||||
const numPoints = 11;
|
||||
const step = (max - min) / (numPoints - 1);
|
||||
const data = [];
|
||||
for(let i = 0; i< numPoints; i++) {
|
||||
const x = min + i * step;
|
||||
data.push(x);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
watch(selectTimeFrame, (newValue, oldValue) => {
|
||||
if(newValue.length === 0) {
|
||||
startTime.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
endTime.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
selectArea.value = [0, selectRange.value];
|
||||
resizeMask(chart.value);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 調整遮罩大小
|
||||
* @param {object} chartInstance 取得 chart.js 資料
|
||||
*/
|
||||
function resizeMask(chartInstance) {
|
||||
const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
|
||||
const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
|
||||
if(props.selectValue[0] === 'Timeframes') {
|
||||
resizeLeftMask(chartInstance, from);
|
||||
resizeRightMask(chartInstance, to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 調整左邊遮罩大小
|
||||
* @param {object} chartInstance 取得 chart.js 資料
|
||||
*/
|
||||
function resizeLeftMask(chartInstance, from) {
|
||||
const canvas = document.getElementById("chartCanvasId");
|
||||
const mask = document.getElementById("chart-mask-left");
|
||||
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left}px`;
|
||||
mask.style.width = `${chartInstance.chartArea.width * from}px`;
|
||||
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
|
||||
mask.style.height = `${chartInstance.chartArea.height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 調整右邊遮罩大小
|
||||
* @param {object} chartInstance 取得 chart.js 資料
|
||||
*/
|
||||
function resizeRightMask(chartInstance, to) {
|
||||
const canvas = document.getElementById("chartCanvasId");
|
||||
const mask = document.getElementById("chart-mask-right");
|
||||
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left + chartInstance.chartArea.width * to}px`;
|
||||
mask.style.width = `${chartInstance.chartArea.width * (1 - to)}px`;
|
||||
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
|
||||
mask.style.height = `${chartInstance.chartArea.height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* create chart
|
||||
*/
|
||||
function createChart() {
|
||||
const max = filterTimeframe.value.y_axis.max * 1.1;
|
||||
const minX = timeFrameData.value[0]?.x;
|
||||
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
|
||||
|
||||
const data = {
|
||||
labels:labelsData.value,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Case',
|
||||
data: timeFrameData.value,
|
||||
fill: 'start',
|
||||
showLine: false,
|
||||
tension: 0.4,
|
||||
backgroundColor: 'rgba(0,153,255)',
|
||||
pointRadius: 0,
|
||||
x: 'x',
|
||||
y: 'y',
|
||||
}
|
||||
]
|
||||
};
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: false, // 圖例
|
||||
filler: {
|
||||
propagate: false
|
||||
},
|
||||
title: false
|
||||
},
|
||||
// animations: false, // 取消動畫
|
||||
animation: {
|
||||
onComplete: e => {
|
||||
resizeMask(e.chart);
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: true,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
min: minX,
|
||||
max: maxX,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0, // 不旋轉 lable 0~50
|
||||
color: '#334155',
|
||||
display: true,
|
||||
source: 'labels',
|
||||
},
|
||||
grid: {
|
||||
display: false, // 隱藏 x 軸網格
|
||||
},
|
||||
time: {
|
||||
minUnit: 'day', // 顯示最小單位
|
||||
// displayFormats: {
|
||||
// minute: 'HH:mm MMM d',
|
||||
// hour: 'HH:mm MMM d',
|
||||
// }
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true, // scale 包含 0
|
||||
max: max,
|
||||
ticks: { // 設定間隔數值
|
||||
display: false, // 隱藏數值,只顯示格線
|
||||
stepSize: max / 4,
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(100,116,139)',
|
||||
z: 1,
|
||||
},
|
||||
border: {
|
||||
display: false, // 隱藏左側多出來的線
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options,
|
||||
};
|
||||
canvasId.value = document.getElementById("chartCanvasId");
|
||||
chart.value = new Chart(canvasId.value, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑塊改變的時候
|
||||
* @param {array} e [1, 100]
|
||||
*/
|
||||
function changeSelectArea(e) {
|
||||
// 日曆改變時,滑塊跟著改變
|
||||
const sliderDataVal = sliderData.value;
|
||||
const start = sliderDataVal[e[0].toFixed()];
|
||||
const end = sliderDataVal[e[1].toFixed()]; // 取得 index,須為整數。
|
||||
|
||||
startTime.value = new Date(start);
|
||||
endTime.value = new Date(end);
|
||||
// 重新設定 start end 日曆選取範圍
|
||||
endMinDate.value = new Date(start);
|
||||
startMaxDate.value = new Date(end);
|
||||
// 重新算圖
|
||||
resizeMask(chart.value);
|
||||
// 執行 timeFrameStartEnd 才會改變數據
|
||||
timeFrameStartEnd.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 選取開始或結束時間時,要改變滑塊跟圖表
|
||||
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
|
||||
* @param {string} direction start or end
|
||||
*/
|
||||
function sliderTimeRange(e, direction) {
|
||||
// 找到最鄰近的 index,時間格式: 毫秒時間戳
|
||||
const sliderDataVal = sliderData.value;
|
||||
const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
|
||||
const closestIndexes = targetTime.map(target => {
|
||||
let closestIndex = 0;
|
||||
closestIndex = ((target - sliderDataVal[0])/(sliderDataVal[sliderDataVal.length-1]-sliderDataVal[0])) * sliderDataVal.length;
|
||||
let result = Math.round(Math.abs(closestIndex));
|
||||
result = result > selectRange.value ? selectRange.value : result;
|
||||
return result
|
||||
});
|
||||
|
||||
// 改變滑塊
|
||||
selectArea.value = closestIndexes;
|
||||
// 重新設定 start end 日曆選取範圍
|
||||
if(direction === 'start') endMinDate.value = e;
|
||||
else if(direction === 'end') startMaxDate.value = e;
|
||||
// 重新算圖
|
||||
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
|
||||
else return;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Chart.js
|
||||
Chart.register(...registerables);
|
||||
createChart();
|
||||
// Slider
|
||||
selectArea.value = [0, selectRange.value];
|
||||
// Calendar
|
||||
startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
// 讓日曆的範圍等於時間軸的範圍
|
||||
startTime.value = startMinDate.value;
|
||||
endTime.value = startMaxDate.value;
|
||||
timeFrameStartEnd.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
|
||||
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
|
||||
<div class="h-full w-full">
|
||||
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
|
||||
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
|
||||
@@ -68,293 +68,303 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
|
||||
|
||||
export default {
|
||||
expose: ['selectArea', 'showTraceId', 'traceTotal'],
|
||||
setup() {
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const emit = defineEmits(['filter-trace-selectArea']);
|
||||
|
||||
return {allMapDataStore, infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases, isLoading}
|
||||
},
|
||||
data() {
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
|
||||
const processMap = ref({
|
||||
nodes:[],
|
||||
edges:[],
|
||||
});
|
||||
const showTraceId = ref(null);
|
||||
const infinitMaxItems = ref(false);
|
||||
const infiniteData = ref([]);
|
||||
const infiniteFinish = ref(true); // 無限滾動是否載入完成
|
||||
const chartOptions = ref(null);
|
||||
const selectArea = ref([0, 1]);
|
||||
const cyTraceRef = ref(null);
|
||||
|
||||
const traceTotal = computed(() => {
|
||||
return baseTraces.value.length;
|
||||
});
|
||||
|
||||
defineExpose({ selectArea, showTraceId, traceTotal });
|
||||
|
||||
const traceCountTotal = computed(() => {
|
||||
return baseTraces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
});
|
||||
|
||||
const traceList = computed(() => {
|
||||
return baseTraces.value.map(trace => {
|
||||
return {
|
||||
processMap:{
|
||||
nodes:[],
|
||||
edges:[],
|
||||
},
|
||||
showTraceId: null,
|
||||
infinitMaxItems: false,
|
||||
infiniteData: [],
|
||||
infiniteFinish: true, // 無限滾動是否載入完成
|
||||
chartOptions: null,
|
||||
selectArea: [0, 1]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
traceTotal: function() {
|
||||
return this.baseTraces.length;
|
||||
},
|
||||
traceCountTotal: function() {
|
||||
return this.baseTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
},
|
||||
traceList: function() {
|
||||
return this.baseTraces.map(trace => {
|
||||
return {
|
||||
id: trace.id,
|
||||
value: this.progressWidth(Number(((trace.count / this.traceCountTotal) * 100).toFixed(1))),
|
||||
count: trace.count.toLocaleString(),
|
||||
base_count: trace.count,
|
||||
ratio: this.getPercentLabel(trace.count / this.traceCountTotal),
|
||||
};
|
||||
}).slice(this.selectArea[0], this.selectArea[1]);
|
||||
},
|
||||
caseTotalPercent: function() {
|
||||
const ratioSum = this.traceList.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / this.traceCountTotal;
|
||||
return this.getPercentLabel(ratioSum)
|
||||
},
|
||||
chartData: function() {
|
||||
const start = this.selectArea[0];
|
||||
const end = this.selectArea[1] - 1;
|
||||
const labels = this.baseTraces.map(trace => `#${trace.id}`);
|
||||
const data = this.baseTraces.map(trace => this.getPercentLabel(trace.count / this.traceCountTotal));
|
||||
const selectAreaData = this.baseTraces.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
|
||||
id: trace.id,
|
||||
value: progressWidth(Number(((trace.count / traceCountTotal.value) * 100).toFixed(1))),
|
||||
count: trace.count.toLocaleString(),
|
||||
base_count: trace.count,
|
||||
ratio: getPercentLabel(trace.count / traceCountTotal.value),
|
||||
};
|
||||
}).slice(selectArea.value[0], selectArea.value[1]);
|
||||
});
|
||||
|
||||
return { // 要呈現的資料
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Trace', // 資料的標題標籤
|
||||
data,
|
||||
backgroundColor: selectAreaData,
|
||||
categoryPercentage: 1.0,
|
||||
barPercentage: 1.0
|
||||
},
|
||||
]
|
||||
};
|
||||
},
|
||||
caseData: function() {
|
||||
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
},
|
||||
columnData: function() {
|
||||
const data = JSON.parse(JSON.stringify(this.baseCases)); // 深拷貝原始 cases 的內容
|
||||
let result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
];
|
||||
if(data.length !== 0){
|
||||
result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
const caseTotalPercent = computed(() => {
|
||||
const ratioSum = traceList.value.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
|
||||
return getPercentLabel(ratioSum)
|
||||
});
|
||||
|
||||
const chartData = computed(() => {
|
||||
const start = selectArea.value[0];
|
||||
const end = selectArea.value[1] - 1;
|
||||
const labels = baseTraces.value.map(trace => `#${trace.id}`);
|
||||
const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value));
|
||||
const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
|
||||
|
||||
return { // 要呈現的資料
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Trace', // 資料的標題標籤
|
||||
data,
|
||||
backgroundColor: selectAreaData,
|
||||
categoryPercentage: 1.0,
|
||||
barPercentage: 1.0
|
||||
},
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const caseData = computed(() => {
|
||||
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
});
|
||||
|
||||
const columnData = computed(() => {
|
||||
const data = JSON.parse(JSON.stringify(baseCases.value)); // 深拷貝原始 cases 的內容
|
||||
let result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
];
|
||||
if(data.length !== 0){
|
||||
result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
}
|
||||
return result
|
||||
});
|
||||
|
||||
watch(selectArea, (newValue, oldValue) => {
|
||||
const roundValue = Math.round(newValue[1].toFixed());
|
||||
if(newValue[1] !== roundValue) selectArea.value[1] = roundValue;
|
||||
if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // 判斷 Apply 是否 disable
|
||||
});
|
||||
|
||||
watch(infinit404, (newValue) => {
|
||||
if(newValue === 404) infinitMaxItems.value = true;
|
||||
});
|
||||
|
||||
watch(showTraceId, (newValue, oldValue) => {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* Set bar chart Options
|
||||
*/
|
||||
function barOptions(){
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
aspectRatio: 0.8,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectArea: function(newValue, oldValue) {
|
||||
const roundValue = Math.round(newValue[1].toFixed());
|
||||
if(newValue[1] !== roundValue) this.selectArea[1] = roundValue;
|
||||
if(newValue != oldValue) this.$emit('filter-trace-selectArea', newValue); // 判斷 Apply 是否 disable
|
||||
},
|
||||
infinite404: function(newValue) {
|
||||
if(newValue === 404) this.infinitMaxItems = true;
|
||||
},
|
||||
showTraceId: function(newValue, oldValue) {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Set bar chart Options
|
||||
*/
|
||||
barOptions(){
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
aspectRatio: 0.8,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { // 圖例
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItems) =>{
|
||||
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animations: false,
|
||||
scales: {
|
||||
x: {
|
||||
display:false
|
||||
},
|
||||
y: {
|
||||
ticks: { // 設定間隔數值
|
||||
display: false, // 隱藏數值,只顯示格線
|
||||
min: 0,
|
||||
max: this.traceList[0]?.ratio,
|
||||
stepSize: (this.traceList[0]?.ratio)/4,
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(100,116,139)',
|
||||
z: 1,
|
||||
},
|
||||
border: {
|
||||
display: false, // 隱藏左側多出來的線
|
||||
}
|
||||
plugins: {
|
||||
legend: { // 圖例
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItems) =>{
|
||||
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
},
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
},
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
* @param {number} count 所有的 case 數量
|
||||
*/
|
||||
async switchCaseData(id, count) {
|
||||
// 點同一筆 id 不要有動作
|
||||
if(id == this.showTraceId) return;
|
||||
this.isLoading = true; // 都要 loading 畫面
|
||||
this.infinit404 = null;
|
||||
this.infinitMaxItems = false;
|
||||
this.baseInfiniteStart = 0;
|
||||
this.allMapDataStore.baseTraceId = id;
|
||||
this.infiniteData = await this.allMapDataStore.getBaseTraceDetail();
|
||||
this.showTraceId = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
|
||||
this.createCy();
|
||||
this.isLoading = false;
|
||||
},
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
this.processMap.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
this.baseTraceTaskSeq.forEach((node, index) => {
|
||||
this.processMap.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
setEdgesData(){
|
||||
this.processMap.edges = [];
|
||||
this.baseTraceTaskSeq.forEach((edge, index) => {
|
||||
this.processMap.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
});
|
||||
});
|
||||
// 關係線數量筆節點少一個
|
||||
this.processMap.edges.pop();
|
||||
},
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
createCy(){
|
||||
const graphId = this.$refs.cyTrace;
|
||||
|
||||
this.setNodesData();
|
||||
this.setEdgesData();
|
||||
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
*/
|
||||
handleScroll(event) {
|
||||
if(this.infinitMaxItems || this.baseCases.length < 20 || this.infiniteFinish === false) return;
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
|
||||
|
||||
if(overScrollHeight) this.fetchData();
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 滾到底後,要載入數據
|
||||
*/
|
||||
async fetchData() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.infiniteFinish = false;
|
||||
this.baseInfiniteStart += 20;
|
||||
await this.allMapDataStore.getBaseTraceDetail();
|
||||
this.infiniteData = await [...this.infiniteData, ...this.baseCases];
|
||||
this.infiniteFinish = await true;
|
||||
this.isLoading = await false;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
animations: false,
|
||||
scales: {
|
||||
x: {
|
||||
display:false
|
||||
},
|
||||
y: {
|
||||
ticks: { // 設定間隔數值
|
||||
display: false, // 隱藏數值,只顯示格線
|
||||
min: 0,
|
||||
max: traceList.value[0]?.ratio,
|
||||
stepSize: (traceList.value[0]?.ratio)/4,
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(100,116,139)',
|
||||
z: 1,
|
||||
},
|
||||
border: {
|
||||
display: false, // 隱藏左側多出來的線
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isLoading = true; // createCy 執行完關閉
|
||||
this.setNodesData();
|
||||
this.setEdgesData();
|
||||
this.createCy();
|
||||
this.chartOptions = this.barOptions();
|
||||
this.selectArea = [0, this.traceTotal]
|
||||
this.isLoading = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
function getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
function progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
}
|
||||
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
* @param {number} count 所有的 case 數量
|
||||
*/
|
||||
async function switchCaseData(id, count) {
|
||||
// 點同一筆 id 不要有動作
|
||||
if(id == showTraceId.value) return;
|
||||
isLoading.value = true; // 都要 loading 畫面
|
||||
infinit404.value = null;
|
||||
infinitMaxItems.value = false;
|
||||
baseInfiniteStart.value = 0;
|
||||
allMapDataStore.baseTraceId = id;
|
||||
infiniteData.value = await allMapDataStore.getBaseTraceDetail();
|
||||
showTraceId.value = id; // 放 getDetail 為了 case table 載入完再切換 showTraceId
|
||||
createCy();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
function setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
processMap.value.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
baseTraceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
function setEdgesData(){
|
||||
processMap.value.edges = [];
|
||||
baseTraceTaskSeq.value.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
});
|
||||
});
|
||||
// 關係線數量筆節點少一個
|
||||
processMap.value.edges.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
function createCy(){
|
||||
const graphId = cyTraceRef.value;
|
||||
|
||||
setNodesData();
|
||||
setEdgesData();
|
||||
cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
*/
|
||||
function handleScroll(event) {
|
||||
if(infinitMaxItems.value || baseCases.value.length < 20 || infiniteFinish.value === false) return;
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
|
||||
|
||||
if(overScrollHeight) fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 無限滾動: 滾到底後,要載入數據
|
||||
*/
|
||||
async function fetchData() {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
infiniteFinish.value = false;
|
||||
baseInfiniteStart.value += 20;
|
||||
await allMapDataStore.getBaseTraceDetail();
|
||||
infiniteData.value = await [...infiniteData.value, ...baseCases.value];
|
||||
infiniteFinish.value = await true;
|
||||
isLoading.value = await false;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
isLoading.value = true; // createCy 執行完關閉
|
||||
setNodesData();
|
||||
setEdgesData();
|
||||
createCy();
|
||||
chartOptions.value = barOptions();
|
||||
selectArea.value = [0, traceTotal.value]
|
||||
isLoading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -241,8 +241,8 @@
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref, } from 'vue';
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { usePageAdminStore } from '@/stores/pageAdmin';
|
||||
import { useMapPathStore } from '@/stores/mapPathStore';
|
||||
import { getTimeLabel } from '@/module/timeLabel.js';
|
||||
@@ -252,120 +252,106 @@ import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants';
|
||||
|
||||
// 刪除第一個和第二個元素
|
||||
const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2);
|
||||
export default {
|
||||
props:{
|
||||
sidebarState: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
insights: {
|
||||
type: Object,
|
||||
required: false,
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
sidebarState: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
},
|
||||
setup(props){
|
||||
const pageAdmin = usePageAdminStore();
|
||||
const mapPathStore = useMapPathStore();
|
||||
|
||||
const activeTrace = ref(0);
|
||||
const currentMapFile = computed(() => pageAdmin.currentMapFile);
|
||||
const clickedPathListIndex = ref(0);
|
||||
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
|
||||
|
||||
const onActiveTraceClick = (clickedActiveTraceIndex) => {
|
||||
mapPathStore.clearAllHighlight();
|
||||
activeTrace.value = clickedActiveTraceIndex;
|
||||
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
|
||||
}
|
||||
|
||||
const onPathOptionClick = (clickedPath) => {
|
||||
clickedPathListIndex.value = clickedPath;
|
||||
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
|
||||
};
|
||||
|
||||
const onResetTraceBtnClick = () => {
|
||||
if(isBPMNOn.value) {
|
||||
return;
|
||||
}
|
||||
clickedPathListIndex.value = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
currentMapFile,
|
||||
i18next,
|
||||
fieldNamesAndLabelNames,
|
||||
clickedPathListIndex,
|
||||
onPathOptionClick,
|
||||
onActiveTraceClick,
|
||||
onResetTraceBtnClick,
|
||||
activeTrace,
|
||||
isBPMNOn,
|
||||
i18next,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: 'summary',
|
||||
valueCases: 0,
|
||||
valueTraces: 0,
|
||||
valueTaskInstances: 0,
|
||||
valueTasks: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {string} switch Summary or Insight
|
||||
*/
|
||||
switchTab(tab) {
|
||||
this.tab = tab;
|
||||
},
|
||||
/**
|
||||
* @param {number} time use timeLabel.js
|
||||
*/
|
||||
timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
|
||||
//
|
||||
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
|
||||
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* @param {number} time use moment
|
||||
*/
|
||||
moment(time){
|
||||
return getMoment(time).format('YYYY-MM-DD HH:mm');
|
||||
},
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return `100%`;
|
||||
else return `${(val * 100).toFixed(1)}%`;
|
||||
},
|
||||
/**
|
||||
* Behavior when show
|
||||
*/
|
||||
show(){
|
||||
this.valueCases = this.stats.cases.ratio * 100;
|
||||
this.valueTraces= this.stats.traces.ratio * 100;
|
||||
this.valueTaskInstances = this.stats.task_instances.ratio * 100;
|
||||
this.valueTasks = this.stats.tasks.ratio * 100;
|
||||
},
|
||||
/**
|
||||
* Behavior when hidden
|
||||
*/
|
||||
hide(){
|
||||
this.valueCases = 0;
|
||||
this.valueTraces= 0;
|
||||
this.valueTaskInstances = 0;
|
||||
this.valueTasks = 0;
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
insights: {
|
||||
type: Object,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
const pageAdmin = usePageAdminStore();
|
||||
const mapPathStore = useMapPathStore();
|
||||
|
||||
const activeTrace = ref(0);
|
||||
const currentMapFile = computed(() => pageAdmin.currentMapFile);
|
||||
const clickedPathListIndex = ref(0);
|
||||
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
|
||||
|
||||
const tab = ref('summary');
|
||||
const valueCases = ref(0);
|
||||
const valueTraces = ref(0);
|
||||
const valueTaskInstances = ref(0);
|
||||
const valueTasks = ref(0);
|
||||
|
||||
function onActiveTraceClick(clickedActiveTraceIndex) {
|
||||
mapPathStore.clearAllHighlight();
|
||||
activeTrace.value = clickedActiveTraceIndex;
|
||||
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
|
||||
}
|
||||
|
||||
function onPathOptionClick(clickedPath) {
|
||||
clickedPathListIndex.value = clickedPath;
|
||||
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
|
||||
}
|
||||
|
||||
function onResetTraceBtnClick() {
|
||||
if(isBPMNOn.value) {
|
||||
return;
|
||||
}
|
||||
clickedPathListIndex.value = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} newTab Summary or Insight
|
||||
*/
|
||||
function switchTab(newTab) {
|
||||
tab.value = newTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} time use timeLabel.js
|
||||
*/
|
||||
function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
|
||||
//
|
||||
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
|
||||
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} time use moment
|
||||
*/
|
||||
function moment(time){
|
||||
return getMoment(time).format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
function getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return `100%`;
|
||||
else return `${(val * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behavior when show
|
||||
*/
|
||||
function show(){
|
||||
valueCases.value = props.stats.cases.ratio * 100;
|
||||
valueTraces.value = props.stats.traces.ratio * 100;
|
||||
valueTaskInstances.value = props.stats.task_instances.ratio * 100;
|
||||
valueTasks.value = props.stats.tasks.ratio * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behavior when hidden
|
||||
*/
|
||||
function hide(){
|
||||
valueCases.value = 0;
|
||||
valueTraces.value = 0;
|
||||
valueTaskInstances.value = 0;
|
||||
valueTasks.value = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
|
||||
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
|
||||
<div class="h-full w-full">
|
||||
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
|
||||
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] infiniteTable " @scroll="handleScroll">
|
||||
@@ -59,221 +59,224 @@
|
||||
</div>
|
||||
</Sidebar>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
|
||||
|
||||
export default {
|
||||
props: ['sidebarTraces', 'cases'],
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
|
||||
const props = defineProps(['sidebarTraces', 'cases']);
|
||||
const emit = defineEmits(['switch-Trace-Id']);
|
||||
|
||||
return {allMapDataStore, infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases, isLoading }
|
||||
},
|
||||
data() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
|
||||
|
||||
const processMap = ref({
|
||||
nodes:[],
|
||||
edges:[],
|
||||
});
|
||||
const showTraceId = ref(null);
|
||||
const infinitMaxItems = ref(false);
|
||||
const infiniteData = ref([]);
|
||||
const infiniteFinish = ref(true); // 無限滾動是否載入完成
|
||||
const cyTraceRef = ref(null);
|
||||
|
||||
const traceTotal = computed(() => {
|
||||
return traces.value.length;
|
||||
});
|
||||
|
||||
const traceList = computed(() => {
|
||||
const sum = traces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
const result = traces.value.map(trace => {
|
||||
return {
|
||||
processMap:{
|
||||
nodes:[],
|
||||
edges:[],
|
||||
},
|
||||
showTraceId: null,
|
||||
infinitMaxItems: false,
|
||||
infiniteData: [],
|
||||
infiniteFinish: true, // 無限滾動是否載入完成
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
traceTotal: function() {
|
||||
return this.traces.length;
|
||||
},
|
||||
traceList: function() {
|
||||
const sum = this.traces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
|
||||
const result = this.traces.map(trace => {
|
||||
return {
|
||||
id: trace.id,
|
||||
value: this.progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
|
||||
count: trace.count.toLocaleString(),
|
||||
base_count: trace.count,
|
||||
ratio: this.getPercentLabel(trace.count / sum),
|
||||
};
|
||||
})
|
||||
return result;
|
||||
},
|
||||
caseData: function() {
|
||||
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
},
|
||||
columnData: function() {
|
||||
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
|
||||
let result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
];
|
||||
if(data.length !== 0){
|
||||
result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
id: trace.id,
|
||||
value: progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
|
||||
count: trace.count.toLocaleString(),
|
||||
base_count: trace.count,
|
||||
ratio: getPercentLabel(trace.count / sum),
|
||||
};
|
||||
})
|
||||
return result;
|
||||
});
|
||||
|
||||
const caseData = computed(() => {
|
||||
const data = JSON.parse(JSON.stringify(infiniteData.value)); // 深拷貝原始 cases 的內容
|
||||
data.forEach(item => {
|
||||
item.attributes.forEach((attribute, index) => {
|
||||
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
|
||||
});
|
||||
delete item.attributes; // 刪除原本的 attributes 屬性
|
||||
})
|
||||
return data;
|
||||
});
|
||||
|
||||
const columnData = computed(() => {
|
||||
const data = JSON.parse(JSON.stringify(props.cases)); // 深拷貝原始 cases 的內容
|
||||
let result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
];
|
||||
if(data.length !== 0){
|
||||
result = [
|
||||
{ field: 'id', header: 'Case Id' },
|
||||
{ field: 'started_at', header: 'Start time' },
|
||||
{ field: 'completed_at', header: 'End time' },
|
||||
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
|
||||
];
|
||||
}
|
||||
return result
|
||||
});
|
||||
|
||||
watch(infinit404, (newValue) => {
|
||||
if(newValue === 404) infinitMaxItems.value = true;
|
||||
});
|
||||
|
||||
watch(traceId, (newValue) => {
|
||||
showTraceId.value = newValue;
|
||||
}, { immediate: true });
|
||||
|
||||
watch(showTraceId, (newValue, oldValue) => {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
});
|
||||
|
||||
watch(infiniteFirstCases, (newValue) => {
|
||||
if(infiniteFirstCases.value) infiniteData.value = JSON.parse(JSON.stringify(newValue));
|
||||
});
|
||||
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
function getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return `100%`;
|
||||
else return `${(val * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
function progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
}
|
||||
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
* @param {number} count 總 case 數量
|
||||
*/
|
||||
async function switchCaseData(id, count) {
|
||||
// 點同一筆 id 不要有動作
|
||||
if(id == showTraceId.value) return;
|
||||
isLoading.value = true; // 都要 loading 畫面
|
||||
infinit404.value = null;
|
||||
infinitMaxItems.value = false;
|
||||
showTraceId.value = id;
|
||||
infiniteStart.value = 0;
|
||||
emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // 傳遞到 Map index 再關掉 loading
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
function setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
processMap.value.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
traceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
infinite404: function(newValue) {
|
||||
if(newValue === 404) this.infinitMaxItems = true;
|
||||
},
|
||||
traceId: {
|
||||
handler(newValue) {
|
||||
this.showTraceId = newValue;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
showTraceId: function(newValue, oldValue) {
|
||||
const isScrollTop = document.querySelector('.infiniteTable');
|
||||
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
|
||||
},
|
||||
infiniteFirstCases: function(newValue){
|
||||
if(this.infiniteFirstCases) this.infiniteData = JSON.parse(JSON.stringify(newValue));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return `100%`;
|
||||
else return `${(val * 100).toFixed(1)}%`;
|
||||
},
|
||||
/**
|
||||
* set progress bar width
|
||||
* @param {number} value 百分比數字
|
||||
* @returns {string} 樣式的寬度設定
|
||||
*/
|
||||
progressWidth(value){
|
||||
return `width:${value}%;`
|
||||
},
|
||||
/**
|
||||
* switch case data
|
||||
* @param {number} id case id
|
||||
* @param {number} count 總 case 數量
|
||||
*/
|
||||
async switchCaseData(id, count) {
|
||||
// 點同一筆 id 不要有動作
|
||||
if(id == this.showTraceId) return;
|
||||
this.isLoading = true; // 都要 loading 畫面
|
||||
this.infinit404 = null;
|
||||
this.infinitMaxItems = false;
|
||||
this.showTraceId = id;
|
||||
this.infiniteStart = 0;
|
||||
this.$emit('switch-Trace-Id', {id: this.showTraceId, count: count}); // 傳遞到 Map index 再關掉 loading
|
||||
},
|
||||
/**
|
||||
* 將 trace element nodes 資料彙整
|
||||
*/
|
||||
setNodesData(){
|
||||
// 避免每次渲染都重複累加
|
||||
this.processMap.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
this.traceTaskSeq.forEach((node, index) => {
|
||||
this.processMap.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
label: node,
|
||||
backgroundColor: '#CCE5FF',
|
||||
bordercolor: '#003366',
|
||||
shape: 'round-rectangle',
|
||||
height: 80,
|
||||
width: 100
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
setEdgesData(){
|
||||
this.processMap.edges = [];
|
||||
this.traceTaskSeq.forEach((edge, index) => {
|
||||
this.processMap.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
});
|
||||
});
|
||||
// 關係線數量筆節點少一個
|
||||
this.processMap.edges.pop();
|
||||
},
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
createCy(){
|
||||
const graphId = this.$refs.cyTrace;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this.setNodesData();
|
||||
this.setEdgesData();
|
||||
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
|
||||
},
|
||||
/**
|
||||
* create map
|
||||
*/
|
||||
async show() {
|
||||
this.isLoading = await true; // createCy 執行完關閉
|
||||
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id
|
||||
this.showTraceId = await this.traces[0]?.id;
|
||||
this.infiniteStart = await 0;
|
||||
this.setNodesData();
|
||||
this.setEdgesData();
|
||||
this.createCy();
|
||||
this.isLoading = false;
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
*/
|
||||
handleScroll(event) {
|
||||
if(this.infinitMaxItems || this.cases.length < 20 || this.infiniteFinish === false) return;
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
|
||||
|
||||
if(overScrollHeight) this.fetchData();
|
||||
},
|
||||
/**
|
||||
* 無限滾動: 滾到底後,要載入數據
|
||||
*/
|
||||
async fetchData() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.infiniteFinish = false;
|
||||
this.infiniteStart += 20;
|
||||
await this.allMapDataStore.getTraceDetail();
|
||||
this.infiniteData = await [...this.infiniteData, ...this.cases];
|
||||
this.infiniteFinish = await true;
|
||||
this.isLoading = await false;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
/**
|
||||
* 將 trace edge line 資料彙整
|
||||
*/
|
||||
function setEdgesData(){
|
||||
processMap.value.edges = [];
|
||||
traceTaskSeq.value.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
target: `${index + 1}`,
|
||||
lineWidth: 1,
|
||||
style: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
// 關係線數量筆節點少一個
|
||||
processMap.value.edges.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* create trace cytoscape's map
|
||||
*/
|
||||
function createCy(){
|
||||
const graphId = cyTraceRef.value;
|
||||
|
||||
setNodesData();
|
||||
setEdgesData();
|
||||
cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
|
||||
}
|
||||
|
||||
/**
|
||||
* create map
|
||||
*/
|
||||
async function show() {
|
||||
isLoading.value = await true; // createCy 執行完關閉
|
||||
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id
|
||||
showTraceId.value = await traces.value[0]?.id;
|
||||
infiniteStart.value = await 0;
|
||||
setNodesData();
|
||||
setEdgesData();
|
||||
createCy();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
*/
|
||||
function handleScroll(event) {
|
||||
if(infinitMaxItems.value || props.cases.length < 20 || infiniteFinish.value === false) return;
|
||||
|
||||
const container = event.target;
|
||||
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
|
||||
|
||||
if(overScrollHeight) fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 無限滾動: 滾到底後,要載入數據
|
||||
*/
|
||||
async function fetchData() {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
infiniteFinish.value = false;
|
||||
infiniteStart.value += 20;
|
||||
await allMapDataStore.getTraceDetail();
|
||||
infiniteData.value = await [...infiniteData.value, ...props.cases];
|
||||
infiniteFinish.value = await true;
|
||||
isLoading.value = await false;
|
||||
} catch(error) {
|
||||
console.error('Failed to load data:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -69,110 +69,118 @@
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMapPathStore } from '@/stores/mapPathStore';
|
||||
import { mapState, mapActions, } from 'pinia';
|
||||
export default {
|
||||
props: {
|
||||
sidebarView: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectFrequency: [
|
||||
{ value:"total", label:"Total", disabled:false, },
|
||||
{ value:"rel_freq", label:"Relative", disabled:false, },
|
||||
{ value:"average", label:"Average", disabled:false, },
|
||||
{ value:"median", label:"Median", disabled:false, },
|
||||
{ value:"max", label:"Max", disabled:false, },
|
||||
{ value:"min", label:"Min", disabled:false, },
|
||||
{ value:"cases", label:"Number of cases", disabled:false, },
|
||||
],
|
||||
selectDuration:[
|
||||
{ value:"total", label:"Total", disabled:false, },
|
||||
{ value:"rel_duration", label:"Relative", disabled:false, },
|
||||
{ value:"average", label:"Average", disabled:false, },
|
||||
{ value:"median", label:"Median", disabled:false, },
|
||||
{ value:"max", label:"Max", disabled:false, },
|
||||
{ value:"min", label:"Min", disabled:false, },
|
||||
],
|
||||
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
|
||||
mapType: 'processMap', // processMap | bpmn
|
||||
dataLayerType: null, // freq | duration
|
||||
dataLayerOption: null,
|
||||
selectedFreq: '',
|
||||
selectedDuration: '',
|
||||
rank: 'LR', // 直向 TB | 橫向 LR
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useMapPathStore, ['isBPMNOn']),
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* switch map type
|
||||
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。
|
||||
*/
|
||||
switchMapType(type) {
|
||||
this.mapType = type;
|
||||
this.$emit('switch-map-type', this.mapType);
|
||||
},
|
||||
/**
|
||||
* switch curve style
|
||||
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
|
||||
*/
|
||||
switchCurveStyles(style) {
|
||||
this.curveStyle = style;
|
||||
this.$emit('switch-curve-styles', this.curveStyle);
|
||||
},
|
||||
/**
|
||||
* switch rank
|
||||
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
|
||||
*/
|
||||
switchRank(rank) {
|
||||
this.rank = rank;
|
||||
this.$emit('switch-rank', this.rank);
|
||||
},
|
||||
/**
|
||||
* switch Data Layoer Type or Option.
|
||||
* @param {string} e 切換時傳入的選項
|
||||
* @param {string} type 'freq' | 'duration',可傳入以上任一。
|
||||
*/
|
||||
switchDataLayerType(e, type){
|
||||
let value = '';
|
||||
|
||||
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
|
||||
switch (type) {
|
||||
case 'freq':
|
||||
value = value || this.selectedFreq || 'total';
|
||||
this.dataLayerType = type;
|
||||
this.dataLayerOption = value;
|
||||
this.selectedFreq = value;
|
||||
break;
|
||||
case 'duration':
|
||||
value = value || this.selectedDuration || 'total';
|
||||
this.dataLayerType = type;
|
||||
this.dataLayerOption = value;
|
||||
this.selectedDuration = value;
|
||||
break;
|
||||
}
|
||||
this.$emit('switch-data-layer-type', this.dataLayerType, this.dataLayerOption);
|
||||
},
|
||||
onProcessMapClick() {
|
||||
this.setIsBPMNOn(false);
|
||||
this.switchMapType('processMap');
|
||||
},
|
||||
onBPMNClick() {
|
||||
this.setIsBPMNOn(true);
|
||||
this.switchMapType('bpmn');
|
||||
},
|
||||
...mapActions(useMapPathStore, ['setIsBPMNOn',]),
|
||||
defineProps({
|
||||
sidebarView: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
},
|
||||
mounted() {
|
||||
this.dataLayerType = 'freq';
|
||||
this.dataLayerOption = 'total';
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'switch-map-type',
|
||||
'switch-curve-styles',
|
||||
'switch-rank',
|
||||
'switch-data-layer-type',
|
||||
]);
|
||||
|
||||
const mapPathStore = useMapPathStore();
|
||||
const { isBPMNOn } = storeToRefs(mapPathStore);
|
||||
|
||||
const selectFrequency = ref([
|
||||
{ value:"total", label:"Total", disabled:false, },
|
||||
{ value:"rel_freq", label:"Relative", disabled:false, },
|
||||
{ value:"average", label:"Average", disabled:false, },
|
||||
{ value:"median", label:"Median", disabled:false, },
|
||||
{ value:"max", label:"Max", disabled:false, },
|
||||
{ value:"min", label:"Min", disabled:false, },
|
||||
{ value:"cases", label:"Number of cases", disabled:false, },
|
||||
]);
|
||||
const selectDuration = ref([
|
||||
{ value:"total", label:"Total", disabled:false, },
|
||||
{ value:"rel_duration", label:"Relative", disabled:false, },
|
||||
{ value:"average", label:"Average", disabled:false, },
|
||||
{ value:"median", label:"Median", disabled:false, },
|
||||
{ value:"max", label:"Max", disabled:false, },
|
||||
{ value:"min", label:"Min", disabled:false, },
|
||||
]);
|
||||
const curveStyle = ref('unbundled-bezier'); // unbundled-bezier | taxi
|
||||
const mapType = ref('processMap'); // processMap | bpmn
|
||||
const dataLayerType = ref(null); // freq | duration
|
||||
const dataLayerOption = ref(null);
|
||||
const selectedFreq = ref('');
|
||||
const selectedDuration = ref('');
|
||||
const rank = ref('LR'); // 直向 TB | 橫向 LR
|
||||
|
||||
/**
|
||||
* switch map type
|
||||
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。
|
||||
*/
|
||||
function switchMapType(type) {
|
||||
mapType.value = type;
|
||||
emit('switch-map-type', mapType.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* switch curve style
|
||||
* @param {string} style 直角 'unbundled-bezier' | 'taxi',可傳入以上任一。
|
||||
*/
|
||||
function switchCurveStyles(style) {
|
||||
curveStyle.value = style;
|
||||
emit('switch-curve-styles', curveStyle.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* switch rank
|
||||
* @param {string} rank 直向 'TB' | 橫向 'LR',可傳入以上任一。
|
||||
*/
|
||||
function switchRank(rankValue) {
|
||||
rank.value = rankValue;
|
||||
emit('switch-rank', rank.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* switch Data Layoer Type or Option.
|
||||
* @param {string} e 切換時傳入的選項
|
||||
* @param {string} type 'freq' | 'duration',可傳入以上任一。
|
||||
*/
|
||||
function switchDataLayerType(e, type) {
|
||||
let value = '';
|
||||
|
||||
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
|
||||
switch (type) {
|
||||
case 'freq':
|
||||
value = value || selectedFreq.value || 'total';
|
||||
dataLayerType.value = type;
|
||||
dataLayerOption.value = value;
|
||||
selectedFreq.value = value;
|
||||
break;
|
||||
case 'duration':
|
||||
value = value || selectedDuration.value || 'total';
|
||||
dataLayerType.value = type;
|
||||
dataLayerOption.value = value;
|
||||
selectedDuration.value = value;
|
||||
break;
|
||||
}
|
||||
emit('switch-data-layer-type', dataLayerType.value, dataLayerOption.value);
|
||||
}
|
||||
|
||||
function onProcessMapClick() {
|
||||
mapPathStore.setIsBPMNOn(false);
|
||||
switchMapType('processMap');
|
||||
}
|
||||
|
||||
function onBPMNClick() {
|
||||
mapPathStore.setIsBPMNOn(true);
|
||||
switchMapType('bpmn');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
dataLayerType.value = 'freq';
|
||||
dataLayerOption.value = 'total';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -78,89 +78,85 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted, } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { getTimeLabel } from '@/module/timeLabel.js';
|
||||
import getMoment from 'moment';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
|
||||
const route = useRoute();
|
||||
|
||||
return { logId, stats, createFilterId, allMapDataStore };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPanel: false,
|
||||
statData: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
|
||||
|
||||
const isPanel = ref(false);
|
||||
const statData = ref(null);
|
||||
|
||||
/**
|
||||
* Number to percentage
|
||||
* @param {number} val 原始數字
|
||||
* @returns {string} 轉換完成的百分比字串
|
||||
*/
|
||||
function getPercentLabel(val){
|
||||
if((val * 100).toFixed(1) >= 100) return 100;
|
||||
else return parseFloat((val * 100).toFixed(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* setting stats data
|
||||
*/
|
||||
function getStatData() {
|
||||
statData.value = {
|
||||
cases: {
|
||||
count: stats.value.cases.count.toLocaleString('en-US'),
|
||||
total: stats.value.cases.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(stats.value.cases.ratio)
|
||||
},
|
||||
/**
|
||||
* setting stats data
|
||||
*/
|
||||
getStatData() {
|
||||
this.statData = {
|
||||
cases: {
|
||||
count: this.stats.cases.count.toLocaleString('en-US'),
|
||||
total: this.stats.cases.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(this.stats.cases.ratio)
|
||||
},
|
||||
traces: {
|
||||
count: this.stats.traces.count.toLocaleString('en-US'),
|
||||
total: this.stats.traces.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(this.stats.traces.ratio)
|
||||
},
|
||||
task_instances: {
|
||||
count: this.stats.task_instances.count.toLocaleString('en-US'),
|
||||
total: this.stats.task_instances.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(this.stats.task_instances.ratio)
|
||||
},
|
||||
tasks: {
|
||||
count: this.stats.tasks.count.toLocaleString('en-US'),
|
||||
total: this.stats.tasks.total.toLocaleString('en-US'),
|
||||
ratio: this.getPercentLabel(this.stats.tasks.ratio)
|
||||
},
|
||||
started_at: getMoment(this.stats.started_at).format('YYYY-MM-DD HH:mm'),
|
||||
completed_at: getMoment(this.stats.completed_at).format('YYYY-MM-DD HH:mm'),
|
||||
case_duration: {
|
||||
min: getTimeLabel(this.stats.case_duration.min),
|
||||
max: getTimeLabel(this.stats.case_duration.max),
|
||||
average: getTimeLabel(this.stats.case_duration.average),
|
||||
median: getTimeLabel(this.stats.case_duration.median),
|
||||
}
|
||||
}
|
||||
traces: {
|
||||
count: stats.value.traces.count.toLocaleString('en-US'),
|
||||
total: stats.value.traces.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(stats.value.traces.ratio)
|
||||
},
|
||||
task_instances: {
|
||||
count: stats.value.task_instances.count.toLocaleString('en-US'),
|
||||
total: stats.value.task_instances.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(stats.value.task_instances.ratio)
|
||||
},
|
||||
tasks: {
|
||||
count: stats.value.tasks.count.toLocaleString('en-US'),
|
||||
total: stats.value.tasks.total.toLocaleString('en-US'),
|
||||
ratio: getPercentLabel(stats.value.tasks.ratio)
|
||||
},
|
||||
started_at: getMoment(stats.value.started_at).format('YYYY-MM-DD HH:mm'),
|
||||
completed_at: getMoment(stats.value.completed_at).format('YYYY-MM-DD HH:mm'),
|
||||
case_duration: {
|
||||
min: getTimeLabel(stats.value.case_duration.min),
|
||||
max: getTimeLabel(stats.value.case_duration.max),
|
||||
average: getTimeLabel(stats.value.case_duration.average),
|
||||
median: getTimeLabel(stats.value.case_duration.median),
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const params = this.$route.params;
|
||||
const file = this.$route.meta.file;
|
||||
const isCheckPage = this.$route.name.includes('Check');
|
||||
|
||||
switch (params.type) {
|
||||
case 'log':
|
||||
this.logId = isCheckPage ? file.parent.id : params.fileId;
|
||||
break;
|
||||
case 'filter':
|
||||
this.createFilterId = isCheckPage ? file.parent.id : params.fileId;
|
||||
break;
|
||||
}
|
||||
await this.allMapDataStore.getAllMapData();
|
||||
await this.getStatData();
|
||||
this.isPanel = false; // 預設不打開
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const params = route.params;
|
||||
const file = route.meta.file;
|
||||
const isCheckPage = route.name.includes('Check');
|
||||
|
||||
switch (params.type) {
|
||||
case 'log':
|
||||
logId.value = isCheckPage ? file.parent.id : params.fileId;
|
||||
break;
|
||||
case 'filter':
|
||||
createFilterId.value = isCheckPage ? file.parent.id : params.fileId;
|
||||
break;
|
||||
}
|
||||
await allMapDataStore.getAllMapData();
|
||||
await getStatData();
|
||||
isPanel.value = false; // 預設不打開
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
@reference "../../assets/tailwind.css";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="$emit('closeModal', $event)">
|
||||
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="emit('closeModal', $event)">
|
||||
<template #header>
|
||||
<div class="py-5">
|
||||
</div>
|
||||
@@ -14,70 +14,61 @@
|
||||
</label>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
import IconUploarding from '../icons/IconUploarding.vue';
|
||||
import { uploadFailedFirst } from '@/module/alertModal.js'
|
||||
<script setup>
|
||||
import { onBeforeUnmount, } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import IconUploarding from '../icons/IconUploarding.vue';
|
||||
import { uploadFailedFirst } from '@/module/alertModal.js';
|
||||
import { useFilesStore } from '@/stores/files';
|
||||
|
||||
export default {
|
||||
props: ['uploadModal'],
|
||||
setup() {
|
||||
const filesStore = useFilesStore();
|
||||
const { uploadFileName } = storeToRefs(filesStore);
|
||||
defineProps(['uploadModal']);
|
||||
const emit = defineEmits(['closeModal']);
|
||||
|
||||
return { filesStore, uploadFileName }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contentClass: 'h-full',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
IconUploarding,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 上傳的行為
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
async upload(event) {
|
||||
const fileInput = document.getElementById('uploadFiles');
|
||||
const target = event.target;
|
||||
const formData = new FormData();
|
||||
let uploadFile;
|
||||
const filesStore = useFilesStore();
|
||||
const { uploadFileName } = storeToRefs(filesStore);
|
||||
|
||||
// 判斷是否有檔案
|
||||
if(target && target.files) {
|
||||
uploadFile = target.files[0];
|
||||
}
|
||||
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840)
|
||||
if(uploadFile.size >= 94371840) {
|
||||
fileInput.value = '';
|
||||
return uploadFailedFirst('size');
|
||||
}
|
||||
// 將檔案加進 formData,欄位一定要「csv」
|
||||
formData.append('csv', uploadFile);
|
||||
// 呼叫第一階段上傳 API
|
||||
if(uploadFile) {
|
||||
await this.filesStore.upload(formData);
|
||||
}
|
||||
if (uploadFile.name.endsWith('.csv')) {
|
||||
this.uploadFileName = uploadFile.name.slice(0, -4);
|
||||
} else {
|
||||
// 處理錯誤或無效的文件格式
|
||||
this.uploadFileName = ''; // 或者其他適合的錯誤處理方式
|
||||
}
|
||||
// 清除選擇文件
|
||||
if(fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$emit('closeModal', false);
|
||||
const contentClass = 'h-full';
|
||||
|
||||
/**
|
||||
* 上傳的行為
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
async function upload(event) {
|
||||
const fileInput = document.getElementById('uploadFiles');
|
||||
const target = event.target;
|
||||
const formData = new FormData();
|
||||
let uploadFile;
|
||||
|
||||
// 判斷是否有檔案
|
||||
if(target && target.files) {
|
||||
uploadFile = target.files[0];
|
||||
}
|
||||
// 判斷檔案大小不可超過 90MB (90(MB)*1024(KB)*1024(Bytes)=94,371,840)
|
||||
if(uploadFile.size >= 94371840) {
|
||||
fileInput.value = '';
|
||||
return uploadFailedFirst('size');
|
||||
}
|
||||
// 將檔案加進 formData,欄位一定要「csv」
|
||||
formData.append('csv', uploadFile);
|
||||
// 呼叫第一階段上傳 API
|
||||
if(uploadFile) {
|
||||
await filesStore.upload(formData);
|
||||
}
|
||||
if (uploadFile.name.endsWith('.csv')) {
|
||||
uploadFileName.value = uploadFile.name.slice(0, -4);
|
||||
} else {
|
||||
// 處理錯誤或無效的文件格式
|
||||
uploadFileName.value = ''; // 或者其他適合的錯誤處理方式
|
||||
}
|
||||
// 清除選擇文件
|
||||
if(fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emit('closeModal', false);
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.loader-arrow-upward {
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, } from 'vue';
|
||||
<script setup>
|
||||
import { ref, onMounted, } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs, } from 'pinia';
|
||||
import i18next from '@/i18n/i18n';
|
||||
import emitter from '@/utils/emitter';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
import DspLogo from '@/components/icons/DspLogo.vue';
|
||||
@@ -30,64 +31,44 @@ import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showMember: false,
|
||||
i18next: i18next,
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const store = useLoginStore();
|
||||
const { logOut } = store;
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
|
||||
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
|
||||
const isHeadHovered = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
const toggleIsAcctMenuOpen = () => {
|
||||
acctMgmtStore.toggleIsAcctMenuOpen();
|
||||
}
|
||||
const store = useLoginStore();
|
||||
const { logOut } = store;
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
|
||||
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
|
||||
|
||||
return { logOut, temporaryData, tempFilterId,
|
||||
postRuleData, ruleData,
|
||||
conformanceLogTempCheckId,
|
||||
conformanceFilterTempCheckId,
|
||||
allMapDataStore, conformanceStore,
|
||||
conformanceFileName,
|
||||
toggleIsAcctMenuOpen,
|
||||
isHeadHovered,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
DspLogo,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 登出的行為
|
||||
*/
|
||||
logOutButton() {
|
||||
if ((this.$route.name === 'Map' || this.$route.name === 'CheckMap') && this.tempFilterId) {
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
this.$emitter.emit('leaveFilter', false);
|
||||
leaveFilter(false, this.allMapDataStore.addFilterId, false, this.logOut)
|
||||
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
|
||||
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
|
||||
leaveConformance(false, this.conformanceStore.addConformanceCreateCheckId, false, this.logOut)
|
||||
} else {
|
||||
this.logOut();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.name === 'Login' || this.$route.name === 'NotFound404') {
|
||||
this.showMember = false
|
||||
} else {
|
||||
this.showMember = true;
|
||||
}
|
||||
const isHeadHovered = ref(false);
|
||||
const showMember = ref(false);
|
||||
|
||||
const toggleIsAcctMenuOpen = () => {
|
||||
acctMgmtStore.toggleIsAcctMenuOpen();
|
||||
};
|
||||
|
||||
/**
|
||||
* 登出的行為
|
||||
*/
|
||||
function logOutButton() {
|
||||
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
emitter.emit('leaveFilter', false);
|
||||
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
|
||||
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
|
||||
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
|
||||
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
|
||||
} else {
|
||||
logOut();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name === 'Login' || route.name === 'NotFound404') {
|
||||
showMember.value = false
|
||||
} else {
|
||||
showMember.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -44,8 +44,11 @@
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
<script>
|
||||
import { storeToRefs, mapState, mapActions, } from 'pinia';
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { storeToRefs, } from 'pinia';
|
||||
import emitter from '@/utils/emitter';
|
||||
import { useFilesStore } from '@/stores/files';
|
||||
import { useAllMapDataStore } from '@/stores/allMapData';
|
||||
import { useConformanceStore } from '@/stores/conformance';
|
||||
@@ -57,296 +60,274 @@ import { saveFilter, savedSuccessfully, saveConformance } from '@/module/alertMo
|
||||
import UploadModal from './File/UploadModal.vue';
|
||||
import AcctMenu from './AccountMenu/AcctMenu.vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useFilesStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
|
||||
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
|
||||
conformanceLogTempCheckId, conformanceFilterTempCheckId,
|
||||
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
|
||||
isUpdateConformance, conformanceFileName
|
||||
} = storeToRefs(conformanceStore);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
return {
|
||||
store, allMapDataStore, logId, tempFilterId, createFilterId,
|
||||
filterName, postRuleData, isUpdateFilter, conformanceStore, conformanceRuleData,
|
||||
conformanceLogId, conformanceFilterId, conformanceLogTempCheckId,
|
||||
conformanceFilterTempCheckId, conformanceLogCreateCheckId,
|
||||
conformanceFilterCreateCheckId, isUpdateConformance, conformanceFileName,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
IconSearch,
|
||||
IconSetting,
|
||||
UploadModal,
|
||||
AcctMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mapCompareStore: useMapCompareStore(),
|
||||
showNavbarBreadcrumb: false,
|
||||
navViewData:
|
||||
{
|
||||
// 舉例:FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
|
||||
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
|
||||
// 舉例:DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
|
||||
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
|
||||
// 舉例:COMPARE: ['PROCESS MAP', 'DASHBOARD']
|
||||
COMPARE: ['MAP', 'PERFORMANCE'],
|
||||
'ACCOUNT MANAGEMENT': [],
|
||||
'MY ACCOUNT': [],
|
||||
},
|
||||
navViewName: 'FILES',
|
||||
uploadModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
disabledSave: function () {
|
||||
switch (this.$route.name) {
|
||||
case 'Map':
|
||||
case 'CheckMap':
|
||||
// 沒有 filter Id, 沒有暫存 tempFilterId Id 就不能存檔
|
||||
return !this.tempFilterId;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
return !(this.conformanceFilterTempCheckId || this.conformanceLogTempCheckId);
|
||||
}
|
||||
},
|
||||
showIcon: function() {
|
||||
let result = true;
|
||||
const store = useFilesStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
const mapCompareStore = useMapCompareStore();
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
|
||||
result = !['FILES', 'UPLOAD'].includes(this.navViewName);
|
||||
return result;
|
||||
},
|
||||
noShowSaveButton: function() {
|
||||
return this.navViewName === 'UPLOAD' || this.navViewName === 'COMPARE' ||
|
||||
this.navViewName === 'ACCOUNT MANAGEMENT' ||
|
||||
this.activePage === 'PERFORMANCE';
|
||||
},
|
||||
...mapState(usePageAdminStore, [
|
||||
'activePage',
|
||||
'pendingActivePage',
|
||||
'activePageComputedByRoute',
|
||||
'shouldKeepPreviousPage',
|
||||
]),
|
||||
},
|
||||
watch: {
|
||||
'$route':'getNavViewName',
|
||||
filterName: function(newVal,) {
|
||||
this.filterName = newVal;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleNavItemBtn();
|
||||
if(this.$route.params.type === 'filter') {
|
||||
this.createFilterId= this.$route.params.fileId;
|
||||
}
|
||||
this.showNavbarBreadcrumb = this.$route.matched[0].name !== ('AuthContainer');
|
||||
this.getNavViewName();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* switch navbar item
|
||||
* @param {event} event 選取 Navbar 選項後傳入的值
|
||||
*/
|
||||
onNavItemBtnClick(event) {
|
||||
let type;
|
||||
let fileId;
|
||||
let isCheckPage;
|
||||
const navItemCandidate = event.target.innerText;
|
||||
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
|
||||
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
|
||||
conformanceLogTempCheckId, conformanceFilterTempCheckId,
|
||||
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
|
||||
isUpdateConformance, conformanceFileName
|
||||
} = storeToRefs(conformanceStore);
|
||||
const { activePage, pendingActivePage, activePageComputedByRoute, shouldKeepPreviousPage } = storeToRefs(pageAdminStore);
|
||||
const { setPendingActivePage, setPreviousPage, setActivePage, setActivePageComputedByRoute, setIsPagePendingBoolean } = pageAdminStore;
|
||||
|
||||
this.setPendingActivePage(navItemCandidate);
|
||||
const showNavbarBreadcrumb = ref(false);
|
||||
const navViewData = {
|
||||
// 舉例:FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
|
||||
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
|
||||
// 舉例:DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
|
||||
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
|
||||
// 舉例:COMPARE: ['PROCESS MAP', 'DASHBOARD']
|
||||
COMPARE: ['MAP', 'PERFORMANCE'],
|
||||
'ACCOUNT MANAGEMENT': [],
|
||||
'MY ACCOUNT': [],
|
||||
};
|
||||
const navViewName = ref('FILES');
|
||||
const uploadModal = ref(false);
|
||||
|
||||
switch (this.navViewName) {
|
||||
case 'FILES':
|
||||
this.store.filesTag = navItemCandidate;
|
||||
break;
|
||||
case 'DISCOVER':
|
||||
type = this.$route.params.type;
|
||||
fileId = this.$route.params.fileId;
|
||||
isCheckPage = this.$route.name.includes('Check');
|
||||
const disabledSave = computed(() => {
|
||||
switch (route.name) {
|
||||
case 'Map':
|
||||
case 'CheckMap':
|
||||
// 沒有 filter Id, 沒有暫存 tempFilterId Id 就不能存檔
|
||||
return !tempFilterId.value;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
return !(conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value);
|
||||
}
|
||||
});
|
||||
|
||||
switch (navItemCandidate) {
|
||||
case 'MAP':
|
||||
if(isCheckPage) {
|
||||
this.$router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
else {
|
||||
this.$router.push({name: 'Map', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break;
|
||||
case 'CONFORMANCE':
|
||||
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
|
||||
this.$router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
else { // Beware of Swal popup, it might disturb which is the current active page
|
||||
this.$router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break
|
||||
case 'PERFORMANCE':
|
||||
if(isCheckPage) {
|
||||
this.$router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
else {
|
||||
this.$router.push({name: 'Performance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break;
|
||||
const showIcon = computed(() => {
|
||||
return !['FILES', 'UPLOAD'].includes(navViewName.value);
|
||||
});
|
||||
|
||||
const noShowSaveButton = computed(() => {
|
||||
return navViewName.value === 'UPLOAD' || navViewName.value === 'COMPARE' ||
|
||||
navViewName.value === 'ACCOUNT MANAGEMENT' ||
|
||||
activePage.value === 'PERFORMANCE';
|
||||
});
|
||||
|
||||
watch(() => route, () => {
|
||||
getNavViewName();
|
||||
}, { deep: true });
|
||||
|
||||
watch(filterName, (newVal) => {
|
||||
filterName.value = newVal;
|
||||
});
|
||||
|
||||
/**
|
||||
* switch navbar item
|
||||
* @param {event} event 選取 Navbar 選項後傳入的值
|
||||
*/
|
||||
function onNavItemBtnClick(event) {
|
||||
let type;
|
||||
let fileId;
|
||||
let isCheckPage;
|
||||
const navItemCandidate = event.target.innerText;
|
||||
|
||||
setPendingActivePage(navItemCandidate);
|
||||
|
||||
switch (navViewName.value) {
|
||||
case 'FILES':
|
||||
store.filesTag = navItemCandidate;
|
||||
break;
|
||||
case 'DISCOVER':
|
||||
type = route.params.type;
|
||||
fileId = route.params.fileId;
|
||||
isCheckPage = route.name.includes('Check');
|
||||
|
||||
switch (navItemCandidate) {
|
||||
case 'MAP':
|
||||
if(isCheckPage) {
|
||||
router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
else {
|
||||
router.push({name: 'Map', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break;
|
||||
case 'COMPARE':
|
||||
switch (navItemCandidate) {
|
||||
case 'MAP':
|
||||
this.$router.push({name: 'MapCompare', params: this.mapCompareStore.routeParam});
|
||||
break;
|
||||
case 'PERFORMANCE':
|
||||
this.$router.push({name: 'CompareDashboard', params: this.mapCompareStore.routeParam});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case 'CONFORMANCE':
|
||||
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
|
||||
router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Based on the route.name, decide the navViewName.
|
||||
* @returns {string} the string of navigation name to return
|
||||
*/
|
||||
getNavViewName() {
|
||||
|
||||
const name = this.$route.name;
|
||||
let valueToSet;
|
||||
|
||||
if(this.$route.name === 'NotFound404') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 說明this.$route.matched[1] 表示當前路由匹配的第二個路由記錄
|
||||
this.navViewName = this.$route.matched[1].name.toUpperCase();
|
||||
this.store.filesTag = 'ALL';
|
||||
switch (this.navViewName) {
|
||||
case 'FILES':
|
||||
valueToSet = this.navItemCandidate;
|
||||
break;
|
||||
case 'DISCOVER':
|
||||
switch (name) {
|
||||
case 'Map':
|
||||
case 'CheckMap':
|
||||
valueToSet = 'MAP';
|
||||
break;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
valueToSet = 'CONFORMANCE';
|
||||
break;
|
||||
case 'Performance':
|
||||
case 'CheckPerformance':
|
||||
valueToSet = 'PERFORMANCE';
|
||||
break;
|
||||
else { // Beware of Swal popup, it might disturb which is the current active page
|
||||
router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break;
|
||||
case 'COMPARE':
|
||||
switch(name) {
|
||||
case 'dummy':
|
||||
case 'CompareDashboard':
|
||||
valueToSet = 'DASHBOARD';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
break
|
||||
case 'PERFORMANCE':
|
||||
if(isCheckPage) {
|
||||
router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
else {
|
||||
router.push({name: 'Performance', params: { type: type, fileId: fileId }});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Frontend is not sure which button will the user press on the modal,
|
||||
// so here we need to save to a pending state
|
||||
// 前端無法確定用戶稍後會按下彈窗上的哪個按鈕(取消還是確認、儲存),
|
||||
// 因此我們需要將其保存到待處理狀態
|
||||
if(!this.shouldKeepPreviousPage) { // 若使用者不是按下取消按鈕或是點選按鈕時
|
||||
this.setPendingActivePage(valueToSet);
|
||||
}
|
||||
|
||||
|
||||
return valueToSet;
|
||||
},
|
||||
/**
|
||||
* Save button' modal
|
||||
*/
|
||||
async saveModal() {
|
||||
// 協助判斷 MAP, CONFORMANCE 儲存有「送出」或「取消」。
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
this.$emitter.emit('saveModal', false);
|
||||
|
||||
switch (this.$route.name) {
|
||||
case 'Map':
|
||||
await this.handleMapSave();
|
||||
break;
|
||||
case 'CheckMap':
|
||||
await this.handleCheckMapSave();
|
||||
break;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
await this.handleConformanceSave();
|
||||
break;
|
||||
break;
|
||||
case 'COMPARE':
|
||||
switch (navItemCandidate) {
|
||||
case 'MAP':
|
||||
router.push({name: 'MapCompare', params: mapCompareStore.routeParam});
|
||||
break;
|
||||
case 'PERFORMANCE':
|
||||
router.push({name: 'CompareDashboard', params: mapCompareStore.routeParam});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Set nav item button background color in case the variable is an empty string
|
||||
*/
|
||||
handleNavItemBtn() {
|
||||
if(this.activePageComputedByRoute === "") {
|
||||
this.setActivePageComputedByRoute(this.$route.matched[this.$route.matched.length - 1].name);
|
||||
}
|
||||
},
|
||||
async handleMapSave() {
|
||||
if (this.createFilterId) {
|
||||
await this.allMapDataStore.updateFilter();
|
||||
if (this.isUpdateFilter) {
|
||||
await savedSuccessfully(this.filterName);
|
||||
}
|
||||
} else if (this.logId) {
|
||||
const isSaved = await saveFilter(this.allMapDataStore.addFilterId);
|
||||
if (isSaved) {
|
||||
this.setActivePage('MAP');
|
||||
await this.$router.push(`/discover/filter/${this.createFilterId}/map`);
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleCheckMapSave() {
|
||||
const isSaved = await saveFilter(this.allMapDataStore.addFilterId);
|
||||
if (isSaved) {
|
||||
this.setActivePage('MAP');
|
||||
await this.$router.push(`/discover/filter/${this.createFilterId}/map`);
|
||||
}
|
||||
},
|
||||
async handleConformanceSave() {
|
||||
if (this.conformanceFilterCreateCheckId || this.conformanceLogCreateCheckId) {
|
||||
await this.conformanceStore.updateConformance();
|
||||
if (this.isUpdateConformance) {
|
||||
await savedSuccessfully(this.conformanceFileName);
|
||||
}
|
||||
} else {
|
||||
const isSaved = await saveConformance(this.conformanceStore.addConformanceCreateCheckId);
|
||||
if (isSaved) {
|
||||
if (this.conformanceLogId) {
|
||||
this.setActivePage('CONFORMANCE');
|
||||
await this.$router.push(`/discover/conformance/log/${this.conformanceLogCreateCheckId}/conformance`);
|
||||
} else if (this.conformanceFilterId) {
|
||||
this.setActivePage('CONFORMANCE');
|
||||
await this.$router.push(`/discover/conformance/filter/${this.conformanceFilterCreateCheckId}/conformance`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...mapActions(usePageAdminStore, [
|
||||
'setPendingActivePage',
|
||||
'setPreviousPage',
|
||||
'setActivePage',
|
||||
'setActivePageComputedByRoute',
|
||||
'setIsPagePendingBoolean',
|
||||
],),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the route.name, decide the navViewName.
|
||||
* @returns {string} the string of navigation name to return
|
||||
*/
|
||||
function getNavViewName() {
|
||||
|
||||
const name = route.name;
|
||||
let valueToSet;
|
||||
|
||||
if(route.name === 'NotFound404' || !route.matched[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 說明route.matched[1] 表示當前路由匹配的第二個路由記錄
|
||||
navViewName.value = route.matched[1].name.toUpperCase();
|
||||
store.filesTag = 'ALL';
|
||||
switch (navViewName.value) {
|
||||
case 'FILES':
|
||||
valueToSet = activePage.value;
|
||||
break;
|
||||
case 'DISCOVER':
|
||||
switch (name) {
|
||||
case 'Map':
|
||||
case 'CheckMap':
|
||||
valueToSet = 'MAP';
|
||||
break;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
valueToSet = 'CONFORMANCE';
|
||||
break;
|
||||
case 'Performance':
|
||||
case 'CheckPerformance':
|
||||
valueToSet = 'PERFORMANCE';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'COMPARE':
|
||||
switch(name) {
|
||||
case 'dummy':
|
||||
case 'CompareDashboard':
|
||||
valueToSet = 'DASHBOARD';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Frontend is not sure which button will the user press on the modal,
|
||||
// so here we need to save to a pending state
|
||||
// 前端無法確定用戶稍後會按下彈窗上的哪個按鈕(取消還是確認、儲存),
|
||||
// 因此我們需要將其保存到待處理狀態
|
||||
if(!shouldKeepPreviousPage.value) { // 若使用者不是按下取消按鈕或是點選按鈕時
|
||||
setPendingActivePage(valueToSet);
|
||||
}
|
||||
|
||||
|
||||
return valueToSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save button' modal
|
||||
*/
|
||||
async function saveModal() {
|
||||
// 協助判斷 MAP, CONFORMANCE 儲存有「送出」或「取消」。
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
emitter.emit('saveModal', false);
|
||||
|
||||
switch (route.name) {
|
||||
case 'Map':
|
||||
await handleMapSave();
|
||||
break;
|
||||
case 'CheckMap':
|
||||
await handleCheckMapSave();
|
||||
break;
|
||||
case 'Conformance':
|
||||
case 'CheckConformance':
|
||||
await handleConformanceSave();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nav item button background color in case the variable is an empty string
|
||||
*/
|
||||
function handleNavItemBtn() {
|
||||
if(activePageComputedByRoute.value === "") {
|
||||
setActivePageComputedByRoute(route.matched[route.matched.length - 1].name);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMapSave() {
|
||||
if (createFilterId.value) {
|
||||
await allMapDataStore.updateFilter();
|
||||
if (isUpdateFilter.value) {
|
||||
await savedSuccessfully(filterName.value);
|
||||
}
|
||||
} else if (logId.value) {
|
||||
const isSaved = await saveFilter(allMapDataStore.addFilterId);
|
||||
if (isSaved) {
|
||||
setActivePage('MAP');
|
||||
await router.push(`/discover/filter/${createFilterId.value}/map`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCheckMapSave() {
|
||||
const isSaved = await saveFilter(allMapDataStore.addFilterId);
|
||||
if (isSaved) {
|
||||
setActivePage('MAP');
|
||||
await router.push(`/discover/filter/${createFilterId.value}/map`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConformanceSave() {
|
||||
if (conformanceFilterCreateCheckId.value || conformanceLogCreateCheckId.value) {
|
||||
await conformanceStore.updateConformance();
|
||||
if (isUpdateConformance.value) {
|
||||
await savedSuccessfully(conformanceFileName.value);
|
||||
}
|
||||
} else {
|
||||
const isSaved = await saveConformance(conformanceStore.addConformanceCreateCheckId);
|
||||
if (isSaved) {
|
||||
if (conformanceLogId.value) {
|
||||
setActivePage('CONFORMANCE');
|
||||
await router.push(`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`);
|
||||
} else if (conformanceFilterId.value) {
|
||||
setActivePage('CONFORMANCE');
|
||||
await router.push(`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleNavItemBtn();
|
||||
if(route.params.type === 'filter') {
|
||||
createFilterId.value = route.params.fileId;
|
||||
}
|
||||
showNavbarBreadcrumb.value = route.matched[0].name !== ('AuthContainer');
|
||||
getNavViewName();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
#searchFiles::-webkit-search-cancel-button{
|
||||
|
||||
@@ -16,14 +16,7 @@
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import IconSearch from '@/components/icons/IconSearch.vue';
|
||||
import IconSetting from '@/components/icons/IconSetting.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconSearch,
|
||||
IconSetting
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,422 +39,320 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//:value="tUnits[unit].val.toString().padStart(2, '0')"
|
||||
import { mapActions, } from 'pinia';
|
||||
import { useConformanceInputStore } from '@/stores/conformanceInput';
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import emitter from '@/utils/emitter';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
const props = defineProps({
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMax: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMin: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMax: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
updateMin: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return value >= 0;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['total-seconds']);
|
||||
|
||||
const display = ref('dhms');
|
||||
const seconds = ref(0);
|
||||
const minutes = ref(0);
|
||||
const hours = ref(0);
|
||||
const days = ref(0);
|
||||
const maxDays = ref(0);
|
||||
const minDays = ref(0);
|
||||
const totalSeconds = ref(0);
|
||||
const maxTotal = ref(null);
|
||||
const minTotal = ref(null);
|
||||
const inputTypes = ref([]);
|
||||
const lastInput = ref(null);
|
||||
const openTimeSelect = ref(false);
|
||||
|
||||
const tUnits = computed({
|
||||
get() {
|
||||
return {
|
||||
display: 'dhms', // d: day; h: hour; m: month; s: second.
|
||||
seconds: 0,
|
||||
minutes: 0,
|
||||
hours: 0,
|
||||
days: 0,
|
||||
maxDays: 0,
|
||||
minDays: 0,
|
||||
totalSeconds: 0,
|
||||
maxTotal: null,
|
||||
minTotal: null,
|
||||
inputTypes: [],
|
||||
lastInput: null,
|
||||
openTimeSelect: false,
|
||||
s: { dsp: 's', inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
|
||||
m: { dsp: 'm', inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
|
||||
h: { dsp: 'h', inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
|
||||
d: { dsp: 'd', inc: 1, val: days.value, max: maxDays.value, rate: 86400, min: minDays.value }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tUnits: {
|
||||
get() {
|
||||
return {
|
||||
s: { dsp: 's', inc: 1, val: this.seconds, max: 59, rate: 1, min: 0 },
|
||||
m: { dsp: 'm', inc: 1, val: this.minutes, max: 59, rate: 60, min: 0 },
|
||||
h: { dsp: 'h', inc: 1, val: this.hours, max: 23, rate: 3600, min: 0 },
|
||||
d: { dsp: 'd', inc: 1, val: this.days, max: this.maxDays, rate: 86400, min: this.minDays }
|
||||
};
|
||||
},
|
||||
set(newValues) {
|
||||
// When the input value exceeds the acceptable maximum value, the front end
|
||||
// should set the value to be equal to the maximum value.
|
||||
// 當輸入的數值大於可接受的最大值時,前端要將數值設定成等同於最大值
|
||||
for (const unit in newValues) {
|
||||
this[unit] = newValues[unit].val;
|
||||
const input = document.querySelector(`[data-tunit="${unit}"]`);
|
||||
if (input) {
|
||||
input.value = newValues[unit].val.toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
inputTimeFields: {
|
||||
get() {
|
||||
const paddedTimeFields = [];
|
||||
this.inputTypes.forEach(inputTypeUnit => {
|
||||
// Pad the dd/hh/mm/ss field string to 2 digits and add it to the list
|
||||
paddedTimeFields.push(this.tUnits[inputTypeUnit].val.toString().padStart(2, '0'));
|
||||
});
|
||||
return paddedTimeFields;
|
||||
},
|
||||
set(newValues) {
|
||||
for (const unit in newValues) {
|
||||
switch (unit) {
|
||||
case 's': seconds.value = newValues[unit].val; break;
|
||||
case 'm': minutes.value = newValues[unit].val; break;
|
||||
case 'h': hours.value = newValues[unit].val; break;
|
||||
case 'd': days.value = newValues[unit].val; break;
|
||||
}
|
||||
const input = document.querySelector(`[data-tunit="${unit}"]`);
|
||||
if (input) {
|
||||
input.value = newValues[unit].val.toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
max: {
|
||||
handler: function(newValue, oldValue) {
|
||||
this.maxTotal = newValue;
|
||||
if(this.size === 'max' && newValue !== oldValue) {
|
||||
this.createData();
|
||||
};
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
min: {
|
||||
handler: function(newValue, oldValue) {
|
||||
this.minTotal = newValue;
|
||||
if( this.size === 'min' && newValue !== oldValue){
|
||||
this.createData();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
// min 的最大值要等於 max 的總秒數
|
||||
updateMax: {
|
||||
handler: function(newValue, oldValue) {
|
||||
this.maxTotal = newValue;
|
||||
this.calculateTotalSeconds();
|
||||
},
|
||||
},
|
||||
updateMin: {
|
||||
handler: function(newValue, oldValue) {
|
||||
this.minTotal = newValue;
|
||||
this.calculateTotalSeconds();
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 關閉選單視窗
|
||||
*/
|
||||
onClose () {
|
||||
this.openTimeSelect = false;
|
||||
},
|
||||
/**
|
||||
* get focus element
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
onFocus(event) {
|
||||
this.lastInput = event.target;
|
||||
this.lastInput.select(); // 當呼叫該方法時,文本框內的文字會被自動選中,這樣使用者可以方便地進行複製或刪除等操作。
|
||||
},
|
||||
/**
|
||||
* when blur update input value and show number
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
onChange(event) {
|
||||
const baseInputValue = event.target.value;
|
||||
let decoratedInputValue;
|
||||
// 讓前綴數字自動補 0
|
||||
if(isNaN(event.target.value)){
|
||||
event.target.value = '00';
|
||||
} else {
|
||||
event.target.value = event.target.value.toString();
|
||||
}
|
||||
decoratedInputValue = event.target.value.toString();
|
||||
});
|
||||
|
||||
// 手 key 數值大於最大值時,要等於最大值
|
||||
// 先將字串轉為數字才能比大小
|
||||
const inputValue = parseInt(event.target.value, 10);
|
||||
const max = parseInt(event.target.dataset.max, 10); // 設定最大值
|
||||
const min = parseInt(event.target.dataset.min, 10);
|
||||
if(inputValue > max) {
|
||||
decoratedInputValue = max.toString().padStart(2, '0');
|
||||
}else if(inputValue < min) {
|
||||
decoratedInputValue= min.toString();
|
||||
}
|
||||
// 數值更新, tUnits 也更新, 並計算 totalSeconds
|
||||
const dsp = event.target.dataset.tunit;
|
||||
this.tUnits[dsp].val = decoratedInputValue;
|
||||
switch (dsp) {
|
||||
case 'd':
|
||||
this.days = baseInputValue;
|
||||
break;
|
||||
case 'h':
|
||||
this.hours = decoratedInputValue;
|
||||
break;
|
||||
case 'm':
|
||||
this.minutes = decoratedInputValue;
|
||||
break;
|
||||
case 's':
|
||||
this.seconds = decoratedInputValue;
|
||||
break;
|
||||
};
|
||||
const inputTimeFields = computed(() => {
|
||||
const paddedTimeFields = [];
|
||||
inputTypes.value.forEach(inputTypeUnit => {
|
||||
paddedTimeFields.push(tUnits.value[inputTypeUnit].val.toString().padStart(2, '0'));
|
||||
});
|
||||
return paddedTimeFields;
|
||||
});
|
||||
|
||||
this.calculateTotalSeconds();
|
||||
},
|
||||
/**
|
||||
* 上下箭頭時的行為
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
onKeyUp(event) {
|
||||
// 正規表達式 \D 即不是 0-9 的字符
|
||||
event.target.value = event.target.value.replace(/\D/g, '');
|
||||
function onClose() {
|
||||
openTimeSelect.value = false;
|
||||
}
|
||||
|
||||
// 38:上箭頭鍵(Arrow Up)
|
||||
// 40:下箭頭鍵(Arrow Down)
|
||||
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||
this.actionUpDown(event.target, event.keyCode === 38, true);
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 上下箭頭時的行為
|
||||
* @param {element} input input 傳入的事件
|
||||
* @param {number} goUp 上箭頭的鍵盤代號
|
||||
* @param {boolean} selectIt 是否已執行
|
||||
*/
|
||||
actionUpDown(input, goUp, selectIt = false) {
|
||||
const tUnit = input.dataset.tunit;
|
||||
let newVal = this.getNewValue(input);
|
||||
function onFocus(event) {
|
||||
lastInput.value = event.target;
|
||||
lastInput.value.select();
|
||||
}
|
||||
|
||||
if (goUp) {
|
||||
newVal = this.handleArrowUp(newVal, tUnit, input);
|
||||
} else {
|
||||
newVal = this.handleArrowDown(newVal, tUnit);
|
||||
}
|
||||
function onChange(event) {
|
||||
const baseInputValue = event.target.value;
|
||||
let decoratedInputValue;
|
||||
if(isNaN(event.target.value)){
|
||||
event.target.value = '00';
|
||||
} else {
|
||||
event.target.value = event.target.value.toString();
|
||||
}
|
||||
decoratedInputValue = event.target.value.toString();
|
||||
|
||||
this.updateInputValue(input, newVal, tUnit);
|
||||
if (selectIt) {
|
||||
input.select();
|
||||
}
|
||||
this.calculateTotalSeconds();
|
||||
},
|
||||
const inputValue = parseInt(event.target.value, 10);
|
||||
const max = parseInt(event.target.dataset.max, 10);
|
||||
const min = parseInt(event.target.dataset.min, 10);
|
||||
if(inputValue > max) {
|
||||
decoratedInputValue = max.toString().padStart(2, '0');
|
||||
}else if(inputValue < min) {
|
||||
decoratedInputValue= min.toString();
|
||||
}
|
||||
const dsp = event.target.dataset.tunit;
|
||||
tUnits.value[dsp].val = decoratedInputValue;
|
||||
switch (dsp) {
|
||||
case 'd':
|
||||
days.value = baseInputValue;
|
||||
break;
|
||||
case 'h':
|
||||
hours.value = decoratedInputValue;
|
||||
break;
|
||||
case 'm':
|
||||
minutes.value = decoratedInputValue;
|
||||
break;
|
||||
case 's':
|
||||
seconds.value = decoratedInputValue;
|
||||
break;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取新的數值
|
||||
* @param {element} input 輸入的元素
|
||||
* @returns {number} 新的數值
|
||||
*/
|
||||
getNewValue(input) {
|
||||
const newVal = parseInt(input.value, 10);
|
||||
return isNaN(newVal) ? 0 : newVal;
|
||||
},
|
||||
calculateTotalSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* 處理向上箭頭的行為
|
||||
* @param {number} newVal 當前數值
|
||||
* @param {string} tUnit 時間單位
|
||||
* @param {element} input 輸入的元素
|
||||
* @returns {number} 更新後的數值
|
||||
*/
|
||||
handleArrowUp(newVal, tUnit, input) {
|
||||
newVal += this.tUnits[tUnit].inc;
|
||||
if (newVal > this.tUnits[tUnit].max) {
|
||||
if (this.tUnits[tUnit].dsp === 'd') {
|
||||
this.totalSeconds = this.maxTotal;
|
||||
} else {
|
||||
newVal = newVal % (this.tUnits[tUnit].max + 1);
|
||||
this.incrementPreviousUnit(input);
|
||||
}
|
||||
}
|
||||
return newVal;
|
||||
},
|
||||
function onKeyUp(event) {
|
||||
event.target.value = event.target.value.replace(/\D/g, '');
|
||||
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||
actionUpDown(event.target, event.keyCode === 38, true);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 處理向下箭頭的行為
|
||||
* @param {number} newVal 當前數值
|
||||
* @param {string} tUnit 時間單位
|
||||
* @returns {number} 更新後的數值
|
||||
*/
|
||||
handleArrowDown(newVal, tUnit) {
|
||||
newVal -= this.tUnits[tUnit].inc;
|
||||
if (newVal < 0) {
|
||||
newVal = (this.tUnits[tUnit].max + 1) - this.tUnits[tUnit].inc;
|
||||
}
|
||||
return newVal;
|
||||
},
|
||||
function actionUpDown(input, goUp, selectIt = false) {
|
||||
const tUnit = input.dataset.tunit;
|
||||
let newVal = getNewValue(input);
|
||||
|
||||
/**
|
||||
* 進位前一個更大的單位
|
||||
* @param {element} input 輸入的元素
|
||||
*/
|
||||
incrementPreviousUnit(input) {
|
||||
if (input.dataset.index > 0) {
|
||||
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
|
||||
this.actionUpDown(prevUnit, true);
|
||||
}
|
||||
},
|
||||
if (goUp) {
|
||||
newVal = handleArrowUp(newVal, tUnit, input);
|
||||
} else {
|
||||
newVal = handleArrowDown(newVal, tUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新輸入框的數值
|
||||
* @param {element} input 輸入的元素
|
||||
* @param {number} newVal 新的數值
|
||||
* @param {string} tUnit 時間單位
|
||||
*/
|
||||
updateInputValue(input, newVal, tUnit) {
|
||||
input.value = newVal.toString();
|
||||
switch (tUnit) {
|
||||
case 'd':
|
||||
this.days = input.value;
|
||||
break;
|
||||
case 'h':
|
||||
this.hours = input.value;
|
||||
break;
|
||||
case 'm':
|
||||
this.minutes = input.value;
|
||||
break;
|
||||
case 's':
|
||||
this.seconds = input.value;
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 設定 dhms 的數值
|
||||
* @param {number} totalSeconds 總秒數
|
||||
* @param {string} size 'min' | 'max',可選以上任一,最大值或最小值
|
||||
*/
|
||||
secondToDate(totalSeconds, size) {
|
||||
totalSeconds = parseInt(totalSeconds);
|
||||
if(!isNaN(totalSeconds)) {
|
||||
this.seconds = totalSeconds % 60;
|
||||
this.minutes = (Math.floor(totalSeconds - this.seconds) / 60) % 60;
|
||||
this.hours = (Math.floor(totalSeconds / 3600)) % 24;
|
||||
this.days = Math.floor(totalSeconds / (3600 * 24));
|
||||
updateInputValue(input, newVal, tUnit);
|
||||
if (selectIt) {
|
||||
input.select();
|
||||
}
|
||||
calculateTotalSeconds();
|
||||
}
|
||||
|
||||
if(size === 'max') {
|
||||
this.maxDays = Math.floor(totalSeconds / (3600 * 24));
|
||||
}
|
||||
else if(size === 'min') {
|
||||
this.minDays = Math.floor(totalSeconds / (3600 * 24));
|
||||
}
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 計算總秒數
|
||||
*/
|
||||
calculateTotalSeconds() {
|
||||
let totalSeconds = 0;
|
||||
function getNewValue(input) {
|
||||
const newVal = parseInt(input.value, 10);
|
||||
return isNaN(newVal) ? 0 : newVal;
|
||||
}
|
||||
|
||||
for (const unit in this.tUnits) {
|
||||
const val = parseInt(this.tUnits[unit].val, 10);
|
||||
if (!isNaN(val)) {
|
||||
totalSeconds += val * this.tUnits[unit].rate;
|
||||
}
|
||||
}
|
||||
|
||||
if(totalSeconds >= this.maxTotal){ // 大於最大值時要等於最大值
|
||||
totalSeconds = this.maxTotal;
|
||||
this.secondToDate(this.maxTotal, 'max');
|
||||
} else if (totalSeconds <= this.minTotal) { // 小於最小值時要等於最小值
|
||||
totalSeconds = this.minTotal;
|
||||
this.secondToDate(this.minTotal, 'min');
|
||||
} else if((this.size === 'min' && totalSeconds <= this.maxTotal)) {
|
||||
this.maxDays = Math.floor(this.maxTotal / (3600 * 24));
|
||||
}
|
||||
this.totalSeconds = totalSeconds;
|
||||
this.$emit('total-seconds', totalSeconds);
|
||||
},
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
async createData() {
|
||||
const size = this.size;
|
||||
|
||||
if (this.maxTotal !== await null && this.minTotal !== await null) {
|
||||
switch (size) {
|
||||
case 'max':
|
||||
this.secondToDate(this.minTotal, 'min');
|
||||
this.secondToDate(this.maxTotal, 'max');
|
||||
this.totalSeconds = this.maxTotal;
|
||||
if(this.value !== null) {
|
||||
this.totalSeconds = this.value;
|
||||
this.secondToDate(this.value);
|
||||
}
|
||||
break;
|
||||
case 'min':
|
||||
this.secondToDate(this.maxTotal, 'max');
|
||||
this.secondToDate(this.minTotal, 'min');
|
||||
this.totalSeconds = this.minTotal;
|
||||
if(this.value !== null) {
|
||||
this.totalSeconds = this.value;
|
||||
this.secondToDate(this.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
...mapActions(
|
||||
useConformanceInputStore,[]
|
||||
),
|
||||
},
|
||||
created() {
|
||||
this.$emitter.on('reset', (data) => {
|
||||
this.createData();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.inputTypes = this.display.split('');
|
||||
},
|
||||
directives: {
|
||||
'closable': {
|
||||
mounted(el, {value}) {
|
||||
const handleOutsideClick = function(e) {
|
||||
let target = e.target;
|
||||
while (target && target.id !== value.id) {
|
||||
target = target.parentElement;
|
||||
};
|
||||
const isClickOutside = target?.id !== value.id && !el.contains(e.target)
|
||||
if (isClickOutside) {
|
||||
value.handler();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
},
|
||||
function handleArrowUp(newVal, tUnit, input) {
|
||||
newVal += tUnits.value[tUnit].inc;
|
||||
if (newVal > tUnits.value[tUnit].max) {
|
||||
if (tUnits.value[tUnit].dsp === 'd') {
|
||||
totalSeconds.value = maxTotal.value;
|
||||
} else {
|
||||
newVal = newVal % (tUnits.value[tUnit].max + 1);
|
||||
incrementPreviousUnit(input);
|
||||
}
|
||||
}
|
||||
return newVal;
|
||||
}
|
||||
|
||||
function handleArrowDown(newVal, tUnit) {
|
||||
newVal -= tUnits.value[tUnit].inc;
|
||||
if (newVal < 0) {
|
||||
newVal = (tUnits.value[tUnit].max + 1) - tUnits.value[tUnit].inc;
|
||||
}
|
||||
return newVal;
|
||||
}
|
||||
|
||||
function incrementPreviousUnit(input) {
|
||||
if (input.dataset.index > 0) {
|
||||
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
|
||||
actionUpDown(prevUnit, true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputValue(input, newVal, tUnit) {
|
||||
input.value = newVal.toString();
|
||||
switch (tUnit) {
|
||||
case 'd':
|
||||
days.value = input.value;
|
||||
break;
|
||||
case 'h':
|
||||
hours.value = input.value;
|
||||
break;
|
||||
case 'm':
|
||||
minutes.value = input.value;
|
||||
break;
|
||||
case 's':
|
||||
seconds.value = input.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function secondToDate(totalSec, size) {
|
||||
totalSec = parseInt(totalSec);
|
||||
if(!isNaN(totalSec)) {
|
||||
seconds.value = totalSec % 60;
|
||||
minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
|
||||
hours.value = (Math.floor(totalSec / 3600)) % 24;
|
||||
days.value = Math.floor(totalSec / (3600 * 24));
|
||||
|
||||
if(size === 'max') {
|
||||
maxDays.value = Math.floor(totalSec / (3600 * 24));
|
||||
}
|
||||
else if(size === 'min') {
|
||||
minDays.value = Math.floor(totalSec / (3600 * 24));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function calculateTotalSeconds() {
|
||||
let total = 0;
|
||||
|
||||
for (const unit in tUnits.value) {
|
||||
const val = parseInt(tUnits.value[unit].val, 10);
|
||||
if (!isNaN(val)) {
|
||||
total += val * tUnits.value[unit].rate;
|
||||
}
|
||||
}
|
||||
|
||||
if(total >= maxTotal.value){
|
||||
total = maxTotal.value;
|
||||
secondToDate(maxTotal.value, 'max');
|
||||
} else if (total <= minTotal.value) {
|
||||
total = minTotal.value;
|
||||
secondToDate(minTotal.value, 'min');
|
||||
} else if((props.size === 'min' && total <= maxTotal.value)) {
|
||||
maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
|
||||
}
|
||||
totalSeconds.value = total;
|
||||
emit('total-seconds', total);
|
||||
}
|
||||
|
||||
async function createData() {
|
||||
const size = props.size;
|
||||
|
||||
if (maxTotal.value !== await null && minTotal.value !== await null) {
|
||||
switch (size) {
|
||||
case 'max':
|
||||
secondToDate(minTotal.value, 'min');
|
||||
secondToDate(maxTotal.value, 'max');
|
||||
totalSeconds.value = maxTotal.value;
|
||||
if(props.value !== null) {
|
||||
totalSeconds.value = props.value;
|
||||
secondToDate(props.value);
|
||||
}
|
||||
break;
|
||||
case 'min':
|
||||
secondToDate(maxTotal.value, 'max');
|
||||
secondToDate(minTotal.value, 'min');
|
||||
totalSeconds.value = minTotal.value;
|
||||
if(props.value !== null) {
|
||||
totalSeconds.value = props.value;
|
||||
secondToDate(props.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// created
|
||||
emitter.on('reset', () => {
|
||||
createData();
|
||||
});
|
||||
|
||||
// mounted
|
||||
onMounted(() => {
|
||||
inputTypes.value = display.value.split('');
|
||||
});
|
||||
|
||||
const vClosable = {
|
||||
mounted(el, {value}) {
|
||||
const handleOutsideClick = function(e) {
|
||||
let target = e.target;
|
||||
while (target && target.id !== value.id) {
|
||||
target = target.parentElement;
|
||||
};
|
||||
const isClickOutside = target?.id !== value.id && !el.contains(e.target)
|
||||
if (isClickOutside) {
|
||||
value.handler();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,27 +8,15 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed, } from 'vue';
|
||||
<script setup>
|
||||
import ImgCheckboxBlueFrame from "@/assets/icon-blue-checkbox.svg";
|
||||
import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg";
|
||||
import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
isChecked: {
|
||||
defineProps({
|
||||
isChecked: {
|
||||
type: Boolean,
|
||||
required: true // 表示这个 props 是必需的
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const isChecked = computed(() => props.isChecked);
|
||||
return {
|
||||
ImgCheckboxBlueFrame,
|
||||
ImgCheckboxCheckedMark,
|
||||
ImgCheckboxGrayFrame,
|
||||
isChecked,
|
||||
};
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@ import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import pinia from '@/stores/main';
|
||||
import moment from 'moment';
|
||||
import mitt from 'mitt';
|
||||
import emitter from '@/utils/emitter';
|
||||
import ToastPlugin from 'vue-toast-notification';
|
||||
import cytoscape from 'cytoscape';
|
||||
import dagre from 'cytoscape-dagre';
|
||||
@@ -44,7 +44,6 @@ import Checkbox from 'primevue/checkbox';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import ContextMenu from 'primevue/contextmenu';
|
||||
|
||||
const emitter = mitt();
|
||||
const app = createApp(App);
|
||||
|
||||
// Pinia Set
|
||||
|
||||
5
src/utils/emitter.ts
Normal file
5
src/utils/emitter.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import mitt from 'mitt';
|
||||
|
||||
const emitter = mitt();
|
||||
|
||||
export default emitter;
|
||||
@@ -112,9 +112,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch, } from 'vue';
|
||||
import { mapState, mapActions, } from 'pinia';
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
@@ -129,300 +128,189 @@ import {
|
||||
MODAL_DELETE,
|
||||
ONCE_RENDER_NUM_OF_DATA,
|
||||
} from "@/constants/constants.js";
|
||||
import iconDeleteGray from '@/assets/icon-delete-gray.svg';
|
||||
import iconDeleteRed from '@/assets/icon-delete-red.svg';
|
||||
import iconEditOff from '@/assets/icon-edit-off.svg';
|
||||
import iconEditOn from '@/assets/icon-edit-on.svg';
|
||||
import iconDetailOn from '@/assets/icon-detail-on.svg';
|
||||
import iconDetailOff from '@/assets/icon-detail-card.svg';
|
||||
import iconDeleteGray from '@/assets/icon-delete-gray.svg';
|
||||
import iconDeleteRed from '@/assets/icon-delete-red.svg';
|
||||
import iconEditOff from '@/assets/icon-edit-off.svg';
|
||||
import iconEditOn from '@/assets/icon-edit-on.svg';
|
||||
import iconDetailOn from '@/assets/icon-detail-on.svg';
|
||||
import iconDetailOff from '@/assets/icon-detail-card.svg';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const toast = useToast();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
const modalStore = useModalStore();
|
||||
const loginStore = useLoginStore();
|
||||
const infiniteStart = ref(0);
|
||||
const toast = useToast();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
const modalStore = useModalStore();
|
||||
const loginStore = useLoginStore();
|
||||
const infiniteStart = ref(0);
|
||||
|
||||
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
|
||||
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
|
||||
|
||||
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList);
|
||||
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
|
||||
const loginUserData = ref(null);
|
||||
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList);
|
||||
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
|
||||
const loginUserData = ref(null);
|
||||
|
||||
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
|
||||
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
|
||||
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
|
||||
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
|
||||
|
||||
const inputQuery = ref('');
|
||||
const inputQuery = ref('');
|
||||
|
||||
const fetchLoginUserData = async () => {
|
||||
await loginStore.getUserData();
|
||||
loginUserData.value = loginStore.userData;
|
||||
};
|
||||
|
||||
const moveJustCreateUserToFirstRow = () => {
|
||||
if(infiniteAcctData.value && infiniteAcctData.value.length){
|
||||
const index = acctMgmtStore.allUserAccoutList.findIndex(user => user.username === acctMgmtStore.justCreateUsername);
|
||||
if (index !== -1) {
|
||||
// 移除匹配的對象(剛剛新增的使用者)並將其插入到陣列的第一位
|
||||
const [justCreateUser] = acctMgmtStore.allUserAccoutList[index];
|
||||
infiniteAcctData.value.unshift(justCreateUser);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const accountSearchResults = computed(() => {
|
||||
if(!inputQuery.value) {
|
||||
return infiniteAcctData.value;
|
||||
}
|
||||
return acctMgmtStore.allUserAccoutList.filter (user => user.username.includes(inputQuery.value));
|
||||
});
|
||||
|
||||
const onCreateNewClick = () => {
|
||||
acctMgmtStore.clearCurrentViewingUser();
|
||||
modalStore.openModal(MODAL_CREATE_NEW);
|
||||
};
|
||||
|
||||
|
||||
const onAcctDoubleClick = (username) => {
|
||||
acctMgmtStore.setCurrentViewingUser(username);
|
||||
modalStore.openModal(MODAL_ACCT_INFO);
|
||||
}
|
||||
|
||||
const handleDeleteMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleDeleteMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const handleRowMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleRowMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
|
||||
const handleEditMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsEditHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleEditMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsEditHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const handleDetailMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleDetailMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsDetailHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const onEditButtonClick = userNameToEdit => {
|
||||
acctMgmtStore.setCurrentViewingUser(userNameToEdit);
|
||||
modalStore.openModal(MODAL_ACCT_EDIT);
|
||||
}
|
||||
|
||||
const onDeleteBtnClick = (usernameToDelete) => {
|
||||
acctMgmtStore.setCurrentViewingUser(usernameToDelete);
|
||||
modalStore.openModal(MODAL_DELETE);
|
||||
};
|
||||
|
||||
const getRowClass = (curData) => {
|
||||
return curData?.isRowHovered ? 'bg-[#F1F5F9]' : '';
|
||||
};
|
||||
|
||||
watch(shouldUpdateList, async(newShouldUpdateList) => {
|
||||
if (newShouldUpdateList) {
|
||||
await acctMgmtStore.getAllUserAccounts();
|
||||
// 當夾帶有infiniteStart.value,就表示依然考慮到無限捲動的需求
|
||||
infiniteAcctData.value = acctMgmtStore.allUserAccoutList.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA);
|
||||
moveJustCreateUserToFirstRow();
|
||||
accountSearchResults.value = infiniteAcctData.value;
|
||||
|
||||
}
|
||||
acctMgmtStore.setShouldUpdateList(false);
|
||||
});
|
||||
|
||||
const onSearchAccountButtonClick = (inputQueryString) => {
|
||||
inputQuery.value = inputQueryString;
|
||||
};
|
||||
|
||||
const setIsActiveInput = async(userData, inputIsActiveToSet) => {
|
||||
const userDataToReplace = {
|
||||
username: userData.username,
|
||||
name: userData.name,
|
||||
is_active: inputIsActiveToSet,
|
||||
};
|
||||
await acctMgmtStore.editAccount(userData.username, userDataToReplace);
|
||||
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
}
|
||||
|
||||
const onAdminInputClick = async(userData, inputIsAdminOn) => {
|
||||
const ADMIN_ROLE_NAME = 'admin';
|
||||
switch(inputIsAdminOn) {
|
||||
case true:
|
||||
await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME);
|
||||
break;
|
||||
case false:
|
||||
await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const userDataToReplace = {
|
||||
username: userData.username,
|
||||
name: userData.name,
|
||||
is_admin: inputIsAdminOn,
|
||||
};
|
||||
|
||||
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadingStore.setIsLoading(false);
|
||||
await fetchLoginUserData();
|
||||
await acctMgmtStore.getAllUserAccounts();
|
||||
});
|
||||
|
||||
/**
|
||||
* 無限滾動: 監聽 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
|
||||
scrollTop,表示容器的垂直滾動位置。具體來說,它是以像素為單位的數值,
|
||||
表示當前內容視窗(可見區域)的頂部距離整個可滾動內容的頂部的距離。
|
||||
簡單來說,scrollTop 指的是滾動條的位置:當滾動條在最上面時,scrollTop 為 0;
|
||||
當滾動條向下移動時,scrollTop 會增加。
|
||||
可是作為:我們目前已經滾動了多少。
|
||||
|
||||
clientHeight:表示容器的可見高度(不包括滾動條的高度)。它是以像素為單位的數值,
|
||||
表示容器內部的可見區域的高度。
|
||||
與 offsetHeight 不同的是,clientHeight 不包含邊框、內邊距和滾動條的高度,只計算內容區域的高度。
|
||||
|
||||
scrollHeight:表示容器內部的總內容高度。它是以像素為單位的數值,
|
||||
包括看不見的(需要滾動才能看到的)部分。
|
||||
簡單來說,scrollHeight 是整個可滾動內容的總高度,包括可見區域和需要滾動才能看到的部分。
|
||||
*/
|
||||
|
||||
const handleScroll = (event) => {
|
||||
const container = event.target;
|
||||
const smallValue = 3;
|
||||
|
||||
const isOverScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue;
|
||||
if(isOverScrollHeight){
|
||||
fetchMoreDataVue3();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMoreDataVue3 = () => {
|
||||
if(infiniteAcctData.value.length < acctMgmtStore.allUserAccoutList.length) {
|
||||
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() {
|
||||
},
|
||||
const fetchLoginUserData = async () => {
|
||||
await loginStore.getUserData();
|
||||
loginUserData.value = loginStore.userData;
|
||||
};
|
||||
|
||||
const moveJustCreateUserToFirstRow = () => {
|
||||
if(infiniteAcctData.value && infiniteAcctData.value.length){
|
||||
const index = acctMgmtStore.allUserAccoutList.findIndex(user => user.username === acctMgmtStore.justCreateUsername);
|
||||
if (index !== -1) {
|
||||
const [justCreateUser] = acctMgmtStore.allUserAccoutList[index];
|
||||
infiniteAcctData.value.unshift(justCreateUser);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const accountSearchResults = computed(() => {
|
||||
if(!inputQuery.value) {
|
||||
return infiniteAcctData.value;
|
||||
}
|
||||
return acctMgmtStore.allUserAccoutList.filter (user => user.username.includes(inputQuery.value));
|
||||
});
|
||||
|
||||
const onCreateNewClick = () => {
|
||||
acctMgmtStore.clearCurrentViewingUser();
|
||||
modalStore.openModal(MODAL_CREATE_NEW);
|
||||
};
|
||||
|
||||
|
||||
const onAcctDoubleClick = (username) => {
|
||||
acctMgmtStore.setCurrentViewingUser(username);
|
||||
modalStore.openModal(MODAL_ACCT_INFO);
|
||||
}
|
||||
|
||||
const handleDeleteMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleDeleteMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const handleRowMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleRowMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
|
||||
const handleEditMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsEditHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleEditMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsEditHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const handleDetailMouseOver = (username) => {
|
||||
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
|
||||
};
|
||||
|
||||
const handleDetailMouseOut = (username) => {
|
||||
acctMgmtStore.changeIsDetailHoveredByUser(username, false);
|
||||
acctMgmtStore.changeIsRowHoveredByUser(username, false);
|
||||
};
|
||||
|
||||
const onEditButtonClick = userNameToEdit => {
|
||||
acctMgmtStore.setCurrentViewingUser(userNameToEdit);
|
||||
modalStore.openModal(MODAL_ACCT_EDIT);
|
||||
}
|
||||
|
||||
const onDeleteBtnClick = (usernameToDelete) => {
|
||||
acctMgmtStore.setCurrentViewingUser(usernameToDelete);
|
||||
modalStore.openModal(MODAL_DELETE);
|
||||
};
|
||||
|
||||
const getRowClass = (curData) => {
|
||||
return curData?.isRowHovered ? 'bg-[#F1F5F9]' : '';
|
||||
};
|
||||
|
||||
const onDetailBtnClick = (dataKey) => {
|
||||
acctMgmtStore.setCurrentViewingUser(dataKey);
|
||||
modalStore.openModal(MODAL_ACCT_INFO);
|
||||
};
|
||||
|
||||
watch(shouldUpdateList, async(newShouldUpdateList) => {
|
||||
if (newShouldUpdateList) {
|
||||
await acctMgmtStore.getAllUserAccounts();
|
||||
moveJustCreateUserToFirstRow();
|
||||
}
|
||||
acctMgmtStore.setShouldUpdateList(false);
|
||||
});
|
||||
|
||||
const onSearchAccountButtonClick = (inputQueryString) => {
|
||||
inputQuery.value = inputQueryString;
|
||||
};
|
||||
|
||||
const setIsActiveInput = async(userData, inputIsActiveToSet) => {
|
||||
const userDataToReplace = {
|
||||
username: userData.username,
|
||||
name: userData.name,
|
||||
is_active: inputIsActiveToSet,
|
||||
};
|
||||
await acctMgmtStore.editAccount(userData.username, userDataToReplace);
|
||||
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
}
|
||||
|
||||
const onAdminInputClick = async(userData, inputIsAdminOn) => {
|
||||
const ADMIN_ROLE_NAME = 'admin';
|
||||
switch(inputIsAdminOn) {
|
||||
case true:
|
||||
await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME);
|
||||
break;
|
||||
case false:
|
||||
await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const userDataToReplace = {
|
||||
username: userData.username,
|
||||
name: userData.name,
|
||||
is_admin: inputIsAdminOn,
|
||||
};
|
||||
|
||||
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 無限滾動: 監聯 scroll 有沒有滾到底部
|
||||
* @param {element} event 滾動傳入的事件
|
||||
*/
|
||||
const handleScroll = (event) => {
|
||||
const container = event.target;
|
||||
const smallValue = 3;
|
||||
|
||||
const isOverScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue;
|
||||
if(isOverScrollHeight){
|
||||
fetchMoreDataVue3();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMoreDataVue3 = () => {
|
||||
if(infiniteAcctData.value.length < acctMgmtStore.allUserAccoutList.length) {
|
||||
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
loadingStore.setIsLoading(false);
|
||||
await fetchLoginUserData();
|
||||
await acctMgmtStore.getAllUserAccounts();
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
/*為了讓 radio 按鈕可以置中,所以讓欄位的文字也置中 */
|
||||
|
||||
@@ -181,10 +181,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed, ref, watch, onMounted, } from 'vue';
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import i18next from "@/i18n/i18n.js";
|
||||
import { mapActions, } from 'pinia';
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
@@ -193,224 +192,174 @@ import ModalHeader from "./ModalHeader.vue";
|
||||
import IconChecked from "@/components/icons/IconChecked.vue";
|
||||
import { MODAL_CREATE_NEW, MODAL_ACCT_EDIT, PWD_VALID_LENGTH } from '@/constants/constants.js';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const modalStore = useModalStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const modalStore = useModalStore();
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const isPwdEyeOn = ref(false);
|
||||
const isConfirmDisabled = ref(true);
|
||||
const isPwdLengthValid = ref(true);
|
||||
const isResetPwdSectionShow = ref(false);
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const isPwdEyeOn = ref(false);
|
||||
const isConfirmDisabled = ref(true);
|
||||
const isPwdLengthValid = ref(true);
|
||||
const isResetPwdSectionShow = ref(false);
|
||||
|
||||
const isSetAsAdminChecked = ref(false);
|
||||
const isSetActivedChecked = ref(true);
|
||||
const isSetAsAdminChecked = ref(false);
|
||||
const isSetActivedChecked = ref(true);
|
||||
|
||||
const whichCurrentModal = computed(() => modalStore.whichModal);
|
||||
const whichCurrentModal = computed(() => modalStore.whichModal);
|
||||
|
||||
const isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso);
|
||||
const username = computed(() => acctMgmtStore.currentViewingUser.username);
|
||||
const name = computed(() => acctMgmtStore.currentViewingUser.name);
|
||||
const isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso);
|
||||
const username = computed(() => acctMgmtStore.currentViewingUser.username);
|
||||
const name = computed(() => acctMgmtStore.currentViewingUser.name);
|
||||
|
||||
const inputUserAccount = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.username);
|
||||
const inputName = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.name);
|
||||
const inputPwd = ref("");
|
||||
const isAccountUnique = ref(true);
|
||||
const isEditable = ref(true);
|
||||
const inputUserAccount = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.username);
|
||||
const inputName = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.name);
|
||||
const inputPwd = ref("");
|
||||
const isAccountUnique = ref(true);
|
||||
const isEditable = ref(true);
|
||||
|
||||
// 自從加入這段 watch 之後,填寫密碼欄位之時,就不會胡亂清空掉 account 或是 full name 欄位了。
|
||||
watch(whichCurrentModal, (newVal) => {
|
||||
if (newVal === MODAL_CREATE_NEW) {
|
||||
inputUserAccount.value = '';
|
||||
inputName.value = '';
|
||||
} else {
|
||||
inputUserAccount.value = currentViewingUser.value.username;
|
||||
inputName.value = currentViewingUser.value.name;
|
||||
}
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.whichModal === MODAL_CREATE_NEW ? i18next.t('AcctMgmt.CreateNew') : i18next.t('AcctMgmt.AccountEdit');
|
||||
});
|
||||
|
||||
const togglePwdEyeBtn = (toBeOpen) => {
|
||||
isPwdEyeOn.value = toBeOpen;
|
||||
};
|
||||
|
||||
const validatePwdLength = () => {
|
||||
isPwdLengthValid.value = !isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH;
|
||||
}
|
||||
|
||||
const onInputDoubleClick = () => {
|
||||
// 允許編輯模式
|
||||
isEditable.value = true;
|
||||
}
|
||||
|
||||
const onConfirmBtnClick = async () => {
|
||||
// rule for minimum length
|
||||
validatePwdLength();
|
||||
if(!isPwdLengthValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// rule for account uniqueness
|
||||
switch(whichCurrentModal.value) {
|
||||
case MODAL_CREATE_NEW:
|
||||
await checkAccountIsUnique();
|
||||
if(!isAccountUnique.value) {
|
||||
return;
|
||||
}
|
||||
await acctMgmtStore.createNewAccount({
|
||||
username: inputUserAccount.value,
|
||||
password: inputPwd.value === undefined ? '' : inputPwd.value,
|
||||
name: inputName.value,
|
||||
is_admin: isSetAsAdminChecked.value,
|
||||
is_active: isSetActivedChecked.value,
|
||||
});
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountAdded"));
|
||||
await modalStore.closeModal();
|
||||
acctMgmtStore.setShouldUpdateList(true);
|
||||
await router.push('/account-admin');
|
||||
break;
|
||||
case MODAL_ACCT_EDIT:
|
||||
await checkAccountIsUnique();
|
||||
if(!isAccountUnique.value) {
|
||||
return;
|
||||
}
|
||||
// 要注意的是舊的username跟新的username可以是不同的
|
||||
// 區分有無傳入密碼的情況
|
||||
if(isResetPwdSectionShow.value) {
|
||||
await acctMgmtStore.editAccount(
|
||||
currentViewingUser.value.username, {
|
||||
newUsername: inputUserAccount.value,
|
||||
password: inputPwd.value,
|
||||
name: inputName.value === undefined ? '' : inputName.value,
|
||||
is_active: true,
|
||||
});
|
||||
} else {
|
||||
await acctMgmtStore.editAccount(
|
||||
currentViewingUser.value.username, {
|
||||
newUsername: inputUserAccount.value,
|
||||
name: inputName.value === undefined ? '' : inputName.value,
|
||||
is_active: true,
|
||||
});
|
||||
}
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
isEditable.value = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const checkAccountIsUnique = async() => {
|
||||
// 如果使用者沒有更動過欄位,那就不用調用任何後端的API
|
||||
if(inputUserAccount.value === username.value) {
|
||||
return true;
|
||||
}
|
||||
const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(inputUserAccount.value);
|
||||
isAccountUnique.value = !isAccountAlreadyExistAPISuccess;
|
||||
return isAccountUnique.value;
|
||||
};
|
||||
|
||||
const toggleIsAdmin = () => {
|
||||
if(isEditable){
|
||||
isSetAsAdminChecked.value = !isSetAsAdminChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
const toggleIsActivated = () => {
|
||||
if(isEditable){
|
||||
isSetActivedChecked.value = !isSetActivedChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
const onInputNameFocus = () => {
|
||||
if(isConfirmDisabled.value){
|
||||
isConfirmDisabled.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onResetPwdButtonClick = () => {
|
||||
isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
|
||||
// 必須清空密碼欄位輸入的字串
|
||||
inputPwd.value = '';
|
||||
}
|
||||
|
||||
watch(
|
||||
[inputPwd, inputUserAccount, inputName],
|
||||
([newPwd, newAccount, newName]) => {
|
||||
// 只要[確認密碼]或[密碼]欄位有更動,且所有欄位都不是空的,confirm 按鈕就可點選
|
||||
if(newAccount.length > 0 && newName.length > 0) {
|
||||
isConfirmDisabled.value = false;
|
||||
}
|
||||
if(whichCurrentModal.value !== MODAL_CREATE_NEW) {
|
||||
if(isResetPwdSectionShow.value && newPwd.length < PWD_VALID_LENGTH) {
|
||||
isConfirmDisabled.value = true;
|
||||
}
|
||||
}else {
|
||||
if(newPwd.length < PWD_VALID_LENGTH) {
|
||||
isConfirmDisabled.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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']),
|
||||
// 自從加入這段 watch 之後,填寫密碼欄位之時,就不會胡亂清空掉 account 或是 full name 欄位了。
|
||||
watch(whichCurrentModal, (newVal) => {
|
||||
if (newVal === MODAL_CREATE_NEW) {
|
||||
inputUserAccount.value = '';
|
||||
inputName.value = '';
|
||||
} else {
|
||||
inputUserAccount.value = currentViewingUser.value.username;
|
||||
inputName.value = currentViewingUser.value.name;
|
||||
}
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.whichModal === MODAL_CREATE_NEW ? i18next.t('AcctMgmt.CreateNew') : i18next.t('AcctMgmt.AccountEdit');
|
||||
});
|
||||
|
||||
const togglePwdEyeBtn = (toBeOpen) => {
|
||||
isPwdEyeOn.value = toBeOpen;
|
||||
};
|
||||
|
||||
const validatePwdLength = () => {
|
||||
isPwdLengthValid.value = !isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH;
|
||||
}
|
||||
|
||||
const onInputDoubleClick = () => {
|
||||
// 允許編輯模式
|
||||
isEditable.value = true;
|
||||
}
|
||||
|
||||
const onConfirmBtnClick = async () => {
|
||||
// rule for minimum length
|
||||
validatePwdLength();
|
||||
if(!isPwdLengthValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// rule for account uniqueness
|
||||
switch(whichCurrentModal.value) {
|
||||
case MODAL_CREATE_NEW:
|
||||
await checkAccountIsUnique();
|
||||
if(!isAccountUnique.value) {
|
||||
return;
|
||||
}
|
||||
await acctMgmtStore.createNewAccount({
|
||||
username: inputUserAccount.value,
|
||||
password: inputPwd.value === undefined ? '' : inputPwd.value,
|
||||
name: inputName.value,
|
||||
is_admin: isSetAsAdminChecked.value,
|
||||
is_active: isSetActivedChecked.value,
|
||||
});
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountAdded"));
|
||||
await modalStore.closeModal();
|
||||
acctMgmtStore.setShouldUpdateList(true);
|
||||
await router.push('/account-admin');
|
||||
break;
|
||||
case MODAL_ACCT_EDIT:
|
||||
await checkAccountIsUnique();
|
||||
if(!isAccountUnique.value) {
|
||||
return;
|
||||
}
|
||||
// 要注意的是舊的username跟新的username可以是不同的
|
||||
// 區分有無傳入密碼的情況
|
||||
if(isResetPwdSectionShow.value) {
|
||||
await acctMgmtStore.editAccount(
|
||||
currentViewingUser.value.username, {
|
||||
newUsername: inputUserAccount.value,
|
||||
password: inputPwd.value,
|
||||
name: inputName.value === undefined ? '' : inputName.value,
|
||||
is_active: true,
|
||||
});
|
||||
} else {
|
||||
await acctMgmtStore.editAccount(
|
||||
currentViewingUser.value.username, {
|
||||
newUsername: inputUserAccount.value,
|
||||
name: inputName.value === undefined ? '' : inputName.value,
|
||||
is_active: true,
|
||||
});
|
||||
}
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
isEditable.value = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const checkAccountIsUnique = async() => {
|
||||
// 如果使用者沒有更動過欄位,那就不用調用任何後端的API
|
||||
if(inputUserAccount.value === username.value) {
|
||||
return true;
|
||||
}
|
||||
const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(inputUserAccount.value);
|
||||
isAccountUnique.value = !isAccountAlreadyExistAPISuccess;
|
||||
return isAccountUnique.value;
|
||||
};
|
||||
|
||||
const toggleIsAdmin = () => {
|
||||
if(isEditable){
|
||||
isSetAsAdminChecked.value = !isSetAsAdminChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
const toggleIsActivated = () => {
|
||||
if(isEditable){
|
||||
isSetActivedChecked.value = !isSetActivedChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
const onInputNameFocus = () => {
|
||||
if(isConfirmDisabled.value){
|
||||
isConfirmDisabled.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onResetPwdButtonClick = () => {
|
||||
isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
|
||||
// 必須清空密碼欄位輸入的字串
|
||||
inputPwd.value = '';
|
||||
}
|
||||
|
||||
watch(
|
||||
[inputPwd, inputUserAccount, inputName],
|
||||
([newPwd, newAccount, newName]) => {
|
||||
// 只要[確認密碼]或[密碼]欄位有更動,且所有欄位都不是空的,confirm 按鈕就可點選
|
||||
if(newAccount.length > 0 && newName.length > 0) {
|
||||
isConfirmDisabled.value = false;
|
||||
}
|
||||
if(whichCurrentModal.value !== MODAL_CREATE_NEW) {
|
||||
if(isResetPwdSectionShow.value && newPwd.length < PWD_VALID_LENGTH) {
|
||||
isConfirmDisabled.value = true;
|
||||
}
|
||||
}else {
|
||||
if(newPwd.length < PWD_VALID_LENGTH) {
|
||||
isConfirmDisabled.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function onCancelBtnClick(){
|
||||
modalStore.closeModal();
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#modal_account_edit {
|
||||
|
||||
@@ -22,42 +22,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { onBeforeMount, computed, ref } from 'vue';
|
||||
import i18next from '@/i18n/i18n.js';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
import ModalHeader from './ModalHeader.vue';
|
||||
import Badge from '../../components/Badge.vue';
|
||||
|
||||
export default {
|
||||
setup(){
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const visitTime = ref(0);
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const {
|
||||
username,
|
||||
name,
|
||||
is_admin,
|
||||
is_active,
|
||||
} = currentViewingUser.value;
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const visitTime = ref(0);
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const {
|
||||
username,
|
||||
name,
|
||||
is_admin,
|
||||
is_active,
|
||||
} = currentViewingUser.value;
|
||||
|
||||
onBeforeMount(async() => {
|
||||
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
|
||||
visitTime.value = currentViewingUser.value.detail.visits;
|
||||
});
|
||||
|
||||
return {
|
||||
i18next,
|
||||
username,
|
||||
name,
|
||||
is_admin,
|
||||
is_active,
|
||||
visitTime,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
ModalHeader,
|
||||
Badge,
|
||||
}
|
||||
}
|
||||
onBeforeMount(async() => {
|
||||
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
|
||||
visitTime.value = currentViewingUser.value.detail.visits;
|
||||
});
|
||||
</script>
|
||||
@@ -10,8 +10,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, } from 'vue';
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
import ModalAccountEditCreate from './ModalAccountEditCreate.vue';
|
||||
import ModalAccountInfo from './ModalAccountInfo.vue';
|
||||
@@ -23,27 +23,8 @@
|
||||
MODAL_DELETE,
|
||||
} from "@/constants/constants.js";
|
||||
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const modalStore = useModalStore();
|
||||
const whichModal = computed(() => modalStore.whichModal);
|
||||
|
||||
return {
|
||||
modalStore,
|
||||
whichModal,
|
||||
MODAL_CREATE_NEW,
|
||||
MODAL_ACCT_EDIT,
|
||||
MODAL_ACCT_INFO,
|
||||
MODAL_DELETE,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
ModalAccountEditCreate,
|
||||
ModalAccountInfo,
|
||||
ModalDeleteAlert,
|
||||
}
|
||||
};
|
||||
const modalStore = useModalStore();
|
||||
const whichModal = computed(() => modalStore.whichModal);
|
||||
</script>
|
||||
<style>
|
||||
#modal_container {
|
||||
|
||||
@@ -27,39 +27,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, } from 'vue';
|
||||
<script setup>
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
import i18next from '@/i18n/i18n.js';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const modalStore = useModalStore();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const modalStore = useModalStore();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const onDeleteConfirmBtnClick = async() => {
|
||||
if(await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)){
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess"));
|
||||
modalStore.closeModal();
|
||||
acctMgmtStore.setShouldUpdateList(true);
|
||||
await router.push("/account-admin");
|
||||
}
|
||||
}
|
||||
const onDeleteConfirmBtnClick = async() => {
|
||||
if(await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)){
|
||||
toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess"));
|
||||
modalStore.closeModal();
|
||||
acctMgmtStore.setShouldUpdateList(true);
|
||||
await router.push("/account-admin");
|
||||
}
|
||||
};
|
||||
|
||||
const onNoBtnClick = () => {
|
||||
modalStore.closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
i18next,
|
||||
onDeleteConfirmBtnClick,
|
||||
onNoBtnClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
const onNoBtnClick = () => {
|
||||
modalStore.closeModal();
|
||||
};
|
||||
</script>
|
||||
@@ -11,24 +11,16 @@
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
export default {
|
||||
props: {
|
||||
headerText: {
|
||||
type: String,
|
||||
required: true // 确保 headerText 是必填的
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const modalStore = useModalStore();
|
||||
const { headerText, } = props;
|
||||
const { closeModal } = modalStore;
|
||||
|
||||
return {
|
||||
headerText,
|
||||
closeModal,
|
||||
};
|
||||
}
|
||||
}
|
||||
defineProps({
|
||||
headerText: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const modalStore = useModalStore();
|
||||
const { closeModal } = modalStore;
|
||||
</script>
|
||||
@@ -114,8 +114,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onMounted, computed, ref, } from 'vue';
|
||||
<script setup>
|
||||
import { onMounted, computed, ref } from 'vue';
|
||||
import i18next from '@/i18n/i18n.js';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
import { useAcctMgmtStore } from '@/stores/acctMgmt';
|
||||
@@ -126,109 +126,77 @@ import ButtonFilled from '@/components/ButtonFilled.vue';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import { PWD_VALID_LENGTH } from '@/constants/constants.js';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const loginStore = useLoginStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const toast = useToast();
|
||||
const loadingStore = useLoadingStore();
|
||||
const loginStore = useLoginStore();
|
||||
const acctMgmtStore = useAcctMgmtStore();
|
||||
const toast = useToast();
|
||||
|
||||
const visitTime = ref(0);
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const name = computed(() => currentViewingUser.value.name);
|
||||
const {
|
||||
username,
|
||||
is_admin,
|
||||
is_active,
|
||||
} = currentViewingUser.value;
|
||||
const visitTime = ref(0);
|
||||
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
|
||||
const name = computed(() => currentViewingUser.value.name);
|
||||
const {
|
||||
username,
|
||||
is_admin,
|
||||
is_active,
|
||||
} = currentViewingUser.value;
|
||||
|
||||
const inputName = ref(name.value); // remember to add .value postfix
|
||||
const inputPwd = ref('');
|
||||
const isNameEditable = ref(false);
|
||||
const isPwdEditable = ref(false);
|
||||
const isPwdEyeOn = ref(false);
|
||||
const isPwdLengthValid = ref(true);
|
||||
const inputName = ref(name.value);
|
||||
const inputPwd = ref('');
|
||||
const isNameEditable = ref(false);
|
||||
const isPwdEditable = ref(false);
|
||||
const isPwdEyeOn = ref(false);
|
||||
const isPwdLengthValid = ref(true);
|
||||
|
||||
const onEditNameClick = () => {
|
||||
isNameEditable.value = true;
|
||||
}
|
||||
const onEditNameClick = () => {
|
||||
isNameEditable.value = true;
|
||||
};
|
||||
|
||||
const onResetPwdClick = () => {
|
||||
isPwdEditable.value = true;
|
||||
}
|
||||
const onResetPwdClick = () => {
|
||||
isPwdEditable.value = true;
|
||||
};
|
||||
|
||||
const onSaveNameClick = async() => {
|
||||
if(inputName.value.length > 0) {
|
||||
await acctMgmtStore.editAccountName(username, inputName.value);
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
await acctMgmtStore.getUserDetail(username);
|
||||
isNameEditable.value = false;
|
||||
inputName.value = name.value; // updated value
|
||||
}
|
||||
};
|
||||
const validatePwdLength = () => {
|
||||
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
|
||||
};
|
||||
|
||||
const onSavePwdClick = async() => {
|
||||
validatePwdLength();
|
||||
if (isPwdLengthValid.value) {
|
||||
isPwdEditable.value = false;
|
||||
await acctMgmtStore.editAccountPwd(username, inputPwd.value);
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
inputPwd.value = '';
|
||||
// remember to force update
|
||||
await acctMgmtStore.getUserDetail(loginStore.userData.username);
|
||||
}
|
||||
}
|
||||
|
||||
const onCancelNameClick = () => {
|
||||
isNameEditable.value = false;
|
||||
inputName.value = name.value;
|
||||
};
|
||||
|
||||
const onCancelPwdClick = () => {
|
||||
isPwdEditable.value = false;
|
||||
inputPwd.value = '';
|
||||
isPwdLengthValid.value = true;
|
||||
};
|
||||
|
||||
const togglePwdEyeBtn = (toBeOpen) => {
|
||||
isPwdEyeOn.value = toBeOpen;
|
||||
};
|
||||
|
||||
const validatePwdLength = () => {
|
||||
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
loadingStore.setIsLoading(false);
|
||||
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,
|
||||
const onSaveNameClick = async() => {
|
||||
if(inputName.value.length > 0) {
|
||||
await acctMgmtStore.editAccountName(username, inputName.value);
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
await acctMgmtStore.getUserDetail(username);
|
||||
isNameEditable.value = false;
|
||||
inputName.value = name.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSavePwdClick = async() => {
|
||||
validatePwdLength();
|
||||
if (isPwdLengthValid.value) {
|
||||
isPwdEditable.value = false;
|
||||
await acctMgmtStore.editAccountPwd(username, inputPwd.value);
|
||||
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
|
||||
inputPwd.value = '';
|
||||
await acctMgmtStore.getUserDetail(loginStore.userData.username);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancelNameClick = () => {
|
||||
isNameEditable.value = false;
|
||||
inputName.value = name.value;
|
||||
};
|
||||
|
||||
const onCancelPwdClick = () => {
|
||||
isPwdEditable.value = false;
|
||||
inputPwd.value = '';
|
||||
isPwdLengthValid.value = true;
|
||||
};
|
||||
|
||||
const togglePwdEyeBtn = (toBeOpen) => {
|
||||
isPwdEyeOn.value = toBeOpen;
|
||||
};
|
||||
|
||||
onMounted(async() => {
|
||||
loadingStore.setIsLoading(false);
|
||||
await acctMgmtStore.getUserDetail(loginStore.userData.username);
|
||||
});
|
||||
</script>
|
||||
@@ -8,15 +8,7 @@
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import Header from "@/components/Header.vue";
|
||||
import Navbar from "@/components/Navbar.vue";
|
||||
|
||||
export default {
|
||||
name: 'AuthContainer',
|
||||
components: {
|
||||
Header,
|
||||
Navbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,95 +8,13 @@
|
||||
</main>
|
||||
</template>
|
||||
<script>
|
||||
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';
|
||||
|
||||
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) {
|
||||
const isCheckPage = to.name.includes('Check');
|
||||
if (isCheckPage) {
|
||||
const conformanceStore = useConformanceStore();
|
||||
// Save token in Headers.
|
||||
// (?:^|.;\s):匹配 "luciaToken" 之前的內容,允許它在字符串開頭或某個分號之後。
|
||||
// luciaToken\s=\s**:匹配 "luciaToken=",並忽略兩邊的空格。
|
||||
// ([^;]*):捕獲 "luciaToken" 的值,直到遇到下一個分號或字符串結尾。
|
||||
// .*$:匹配剩餘的字符,確保完整的提取。
|
||||
// |^.*$:在找不到 "luciaToken" 的情況下,匹配整個字符串。
|
||||
switch (to.params.type) {
|
||||
case 'log':
|
||||
conformanceStore.setConformanceLogCreateCheckId(to.params.fileId);
|
||||
@@ -106,9 +24,84 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport();
|
||||
to.meta.file = await conformanceStore.conformanceTempReportData?.file; // 將 file data 存到 route 給 Navbar, StatusBar 使用
|
||||
to.meta.file = await conformanceStore.conformanceTempReportData?.file;
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
<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"
|
||||
:class="sidebarLeftValue ? 'bg-neutral-50' : ''">
|
||||
<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':''">
|
||||
<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
|
||||
hover:border-primary" @click="sidebarView = !sidebarView" :class="{ 'border-primary': 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']">
|
||||
hover:border-primary" @click="sidebarView = !sidebarView" :class="{'border-primary': 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']">
|
||||
track_changes
|
||||
</span>
|
||||
</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
|
||||
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{ 'border-primary': 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">
|
||||
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': 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">
|
||||
tornado
|
||||
</span>
|
||||
</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 hover:border-primary" @click="sidebarTraces = !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']">
|
||||
drop-shadow hover:border-primary" @click="sidebarTraces = !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']">
|
||||
rebase
|
||||
</span>
|
||||
</li>
|
||||
@@ -40,7 +33,7 @@
|
||||
<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
|
||||
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"
|
||||
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
|
||||
info
|
||||
@@ -50,486 +43,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Model -->
|
||||
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles"
|
||||
@switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType"></SidebarView>
|
||||
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank"
|
||||
@switch-data-layer-type="switchDataLayerType" ></SidebarView>
|
||||
<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"
|
||||
: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>
|
||||
|
||||
<script>
|
||||
import { onBeforeMount, computed, } from 'vue';
|
||||
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 {
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const route = useRoute();
|
||||
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId,
|
||||
filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace,
|
||||
temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases,
|
||||
postRuleData
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
const cytoscapeStore = useCytoscapeStore();
|
||||
|
||||
const { setCurrentGraphId } = cytoscapeStore;
|
||||
|
||||
const numberBeforeMapInRoute = computed(() => {
|
||||
// 取得當前路由的路徑
|
||||
const path = route.path;
|
||||
// 使用斜線分割路徑
|
||||
const segments = path.split('/');
|
||||
// 查找包含 'map' 的片段索引
|
||||
const mapIndex = segments.findIndex(segment => segment.includes('map'));
|
||||
if (mapIndex > 0) {
|
||||
// 定位到 'map' 片段的左邊片段
|
||||
const previousSegment = segments[mapIndex - 1];
|
||||
// 萃取左邊片段中的數字
|
||||
const match = previousSegment.match(/\d+/);
|
||||
return match ? match[0] : 'No number found';
|
||||
}
|
||||
return 'No map segment found';
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
setCurrentGraphId(numberBeforeMapInRoute);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
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,
|
||||
endId: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
bpmnData: {
|
||||
startId: 0,
|
||||
endId: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
cytoscapeGraph: null,
|
||||
curveStyle: 'unbundled-bezier', // unbundled-bezier | taxi
|
||||
mapType: 'processMap', // processMap | bpmn
|
||||
mapPathStore: useMapPathStore(),
|
||||
dataLayerType: 'freq', // freq | duration
|
||||
dataLayerOption: 'total',
|
||||
rank: 'LR', // 直向 TB | 橫向 LR
|
||||
traceId: 1,
|
||||
sidebarView: false, // SideBar: Visualization Setting
|
||||
sidebarState: false, // SideBar: Summary & Insight
|
||||
sidebarTraces: false, // SideBar: Traces
|
||||
sidebarFilter: false, // SideBar: Filter
|
||||
infiniteFirstCases: null,
|
||||
startNodeId: -1,
|
||||
endNodeId: -1,
|
||||
tooltip: {
|
||||
sidebarView: {
|
||||
value: 'Visualization Setting',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarTraces: {
|
||||
value: 'Trace',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarFilter: {
|
||||
value: 'Filter',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarState: {
|
||||
value: 'Summary',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sidebarLeftValue: function () {
|
||||
const result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sidebarView: function (newValue) {
|
||||
if (newValue) {
|
||||
this.sidebarFilter = false;
|
||||
this.sidebarTraces = false;
|
||||
}
|
||||
},
|
||||
sidebarFilter: function (newValue) {
|
||||
if (newValue) {
|
||||
this.sidebarView = false;
|
||||
this.sidebarState = false;
|
||||
this.sidebarTraces = false;
|
||||
this.sidebarState = false;
|
||||
}
|
||||
},
|
||||
sidebarTraces: function (newValue) {
|
||||
if (newValue) {
|
||||
this.sidebarView = false;
|
||||
this.sidebarState = false;
|
||||
this.sidebarFilter = false;
|
||||
this.sidebarState = false;
|
||||
}
|
||||
},
|
||||
sidebarState: function (newValue) {
|
||||
if (newValue) {
|
||||
this.sidebarFilter = false;
|
||||
this.sidebarTraces = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* switch map type
|
||||
* @param {string} type 'processMap' | 'bpmn',可傳入以上任一。
|
||||
*/
|
||||
async switchMapType(type) {
|
||||
this.mapType = type;
|
||||
this.createCy(type);
|
||||
},
|
||||
/**
|
||||
* switch curve style
|
||||
* @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 = {
|
||||
"total": "",
|
||||
"rel_freq": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"cases": ""
|
||||
};
|
||||
const logDuration = {
|
||||
"total": "",
|
||||
"rel_duration": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
};
|
||||
// BPMN 才有 gateway 類別
|
||||
const gateway = {
|
||||
parallel: "+",
|
||||
exclusive: "x",
|
||||
inclusive: "o",
|
||||
};
|
||||
|
||||
// 避免每次渲染都重複累加
|
||||
mapData.nodes = [];
|
||||
// 將 api call 回來的資料帶進 node
|
||||
this[mapType].vertices.forEach(node => {
|
||||
switch (node.type) {
|
||||
// add type of 'bpmn gateway' node
|
||||
case 'gateway':
|
||||
mapData.nodes.push({
|
||||
data: {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
label: gateway[node.gateway_type],
|
||||
height: 60,
|
||||
width: 60,
|
||||
backgroundColor: '#FFF',
|
||||
bordercolor: '#003366',
|
||||
shape: "diamond",
|
||||
freq: logFreq,
|
||||
duration: logDuration,
|
||||
}
|
||||
})
|
||||
break;
|
||||
// add type of 'event' node
|
||||
case 'event':
|
||||
if (node.event_type === 'start') {
|
||||
mapData.startId = node.id;
|
||||
this.startNodeId = node.id;
|
||||
}
|
||||
else if (node.event_type === 'end') {
|
||||
mapData.endId = node.id;
|
||||
this.endNodeId = node.id;
|
||||
}
|
||||
|
||||
mapData.nodes.push({
|
||||
data: {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
label: node.event_type,
|
||||
height: 48,
|
||||
width: 48,
|
||||
backgroundColor: '#FFFFFF',
|
||||
bordercolor: '#0F172A',
|
||||
textColor: '#FF3366',
|
||||
shape: "ellipse",
|
||||
freq: logFreq,
|
||||
duration: logDuration,
|
||||
}
|
||||
});
|
||||
break;
|
||||
// add type of 'activity' node
|
||||
default:
|
||||
mapData.nodes.push({
|
||||
data: {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
label: node.label,
|
||||
height: 48,
|
||||
width: 216,
|
||||
textColor: '#0F172A',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
borderradius: 999,
|
||||
shape: "round-rectangle",
|
||||
freq: node.freq,
|
||||
duration: node.duration,
|
||||
backgroundOpacity: 0,
|
||||
borderOpacity: 0,
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 將 element edges 資料彙整
|
||||
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。
|
||||
*/
|
||||
setEdgesData(mapData) {
|
||||
const mapType = this.mapType;
|
||||
//add event duration is empty
|
||||
const logDuration = {
|
||||
"total": "",
|
||||
"rel_duration": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"cases": ""
|
||||
};
|
||||
|
||||
mapData.edges = [];
|
||||
this[mapType].edges.forEach(edge => {
|
||||
mapData.edges.push({
|
||||
data: {
|
||||
source: edge.tail,
|
||||
target: edge.head,
|
||||
freq: edge.freq,
|
||||
duration: edge.duration === null ? logDuration : edge.duration,
|
||||
// Don't know why but tail is related to start and head is related to end
|
||||
edgeStyle: edge.tail === this.startNodeId || edge.head === this.endNodeId ? 'dotted' : 'solid',
|
||||
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) {
|
||||
this.setNodesData(mapData);
|
||||
this.setEdgesData(mapData);
|
||||
this.setActivityBgImage(mapData);
|
||||
this.cytoscapeGraph = await cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
|
||||
const processOrBPMN = this.mapType === 'processMap' ? 'process' : 'bpmn';
|
||||
const curveType = this.curveStyle === 'taxi' ? 'elbow' : 'curved';
|
||||
const directionType = this.rank === 'LR' ? 'horizontal' : 'vertical';
|
||||
await this.mapPathStore.setCytoscape(this.cytoscapeGraph, processOrBPMN, curveType, directionType);
|
||||
};
|
||||
},
|
||||
setActivityBgImage(mapData) {
|
||||
const nodes = mapData.nodes;
|
||||
// 一組有多少個activities
|
||||
const groupSize = Math.floor(nodes.length / ImgCapsules.length);
|
||||
let nodeOptionArr = [];
|
||||
const leveledGroups = []; // 每一個level會使用不同的膠囊圖片
|
||||
// 設定除了 start, end 的 node 顏色
|
||||
// 找出 type activity's node
|
||||
const activityNodeArray = nodes.filter(node => node.data.type === 'activity');
|
||||
// 找出除了 start, end 以外所有的 node 的 option value
|
||||
activityNodeArray.forEach(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption]));
|
||||
// 將node的option值從小到大排序(映對色階淺到深)
|
||||
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
|
||||
for (let i = 0; i < ImgCapsules.length; i++) {
|
||||
const startIdx = i * groupSize;
|
||||
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
|
||||
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
|
||||
}
|
||||
for (let level = 0; level < leveledGroups.length; level++) {
|
||||
leveledGroups[level].forEach(option => {
|
||||
// 考慮可能有名次一樣的情形
|
||||
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option);
|
||||
curNodes.forEach(curNode => {
|
||||
curNode.data = {
|
||||
...curNode.data,
|
||||
nodeImageUrl: ImgCapsules[level],
|
||||
level,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
const routeParams = this.$route.params;
|
||||
const file = this.$route.meta.file;
|
||||
const isCheckPage = this.$route.name.includes('Check');
|
||||
|
||||
// 先 loading 再執行以下程式
|
||||
this.isLoading = true;
|
||||
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁
|
||||
switch (routeParams.type) {
|
||||
case 'log':
|
||||
if (!isCheckPage) {
|
||||
this.logId = await routeParams.fileId;
|
||||
this.baseLogId = await routeParams.fileId;
|
||||
} else {
|
||||
this.logId = await file.parent.id;
|
||||
this.baseLogId = await file.parent.id;
|
||||
}
|
||||
break;
|
||||
case 'filter':
|
||||
if (!isCheckPage) {
|
||||
this.createFilterId = await routeParams.fileId;
|
||||
} else {
|
||||
this.createFilterId = await file.parent.id;
|
||||
}
|
||||
// 取得 logID 和上次儲存的 Funnel
|
||||
await this.allMapDataStore.fetchFunnel(this.createFilterId);
|
||||
this.isRuleData = await Array.from(this.temporaryData);
|
||||
this.ruleData = await this.isRuleData.map(e => this.$refs.sidebarFilterRef.setRule(e));
|
||||
break;
|
||||
}
|
||||
// 取得 logId 後才 call api
|
||||
await this.allMapDataStore.getAllMapData();
|
||||
await this.allMapDataStore.getAllTrace();
|
||||
|
||||
// log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。
|
||||
this.traceId = await this.traces[0]?.id;
|
||||
this.baseTraceId = await this.baseTraces[0]?.id;
|
||||
await this.createCy(this.mapType);
|
||||
await this.allMapDataStore.getFilterParams();
|
||||
await this.allMapDataStore.getTraceDetail();
|
||||
|
||||
// 執行完後才取消 loading
|
||||
this.isLoading = false;
|
||||
// 存檔 Modal 打開時,側邊欄要關閉
|
||||
this.$emitter.on('saveModal', boolean => {
|
||||
this.sidebarView = boolean;
|
||||
this.sidebarFilter = boolean;
|
||||
this.sidebarTraces = boolean;
|
||||
this.sidebarState = boolean;
|
||||
});
|
||||
this.$emitter.on('leaveFilter', boolean => {
|
||||
this.sidebarView = boolean;
|
||||
this.sidebarFilter = boolean;
|
||||
this.sidebarTraces = boolean;
|
||||
this.sidebarState = 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');
|
||||
|
||||
@@ -544,9 +70,413 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport(true);
|
||||
to.meta.file = conformanceStore.routeFile; // 將 file data 存到 route
|
||||
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 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,
|
||||
temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases,
|
||||
postRuleData
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
const cytoscapeStore = useCytoscapeStore();
|
||||
const { setCurrentGraphId } = cytoscapeStore;
|
||||
const mapPathStore = useMapPathStore();
|
||||
|
||||
const numberBeforeMapInRoute = computed(() => {
|
||||
const path = route.path;
|
||||
const segments = path.split('/');
|
||||
const mapIndex = segments.findIndex(segment => segment.includes('map'));
|
||||
if (mapIndex > 0) {
|
||||
const previousSegment = segments[mapIndex - 1];
|
||||
const match = previousSegment.match(/\d+/);
|
||||
return match ? match[0] : 'No number found';
|
||||
}
|
||||
return 'No map segment found';
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
setCurrentGraphId(numberBeforeMapInRoute);
|
||||
});
|
||||
|
||||
// Data
|
||||
const processMapData = ref({
|
||||
startId: 0,
|
||||
endId: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
const bpmnData = ref({
|
||||
startId: 0,
|
||||
endId: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
const cytoscapeGraph = ref(null);
|
||||
const curveStyle = ref('unbundled-bezier');
|
||||
const mapType = ref('processMap');
|
||||
const dataLayerType = ref('freq');
|
||||
const dataLayerOption = ref('total');
|
||||
const rank = ref('LR');
|
||||
const localTraceId = ref(1);
|
||||
const sidebarView = ref(false);
|
||||
const sidebarState = ref(false);
|
||||
const sidebarTraces = ref(false);
|
||||
const sidebarFilter = ref(false);
|
||||
const infiniteFirstCases = ref(null);
|
||||
const tracesViewRef = ref(null);
|
||||
const sidebarFilterRefComp = ref(null);
|
||||
|
||||
const tooltip = {
|
||||
sidebarView: {
|
||||
value: 'Visualization Setting',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarTraces: {
|
||||
value: 'Trace',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarFilter: {
|
||||
value: 'Filter',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
text: 'text-[10px] p-1'
|
||||
}
|
||||
},
|
||||
sidebarState: {
|
||||
value: 'Summary',
|
||||
class: 'ml-1',
|
||||
pt: {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarTraces, (newValue) => {
|
||||
if(newValue) {
|
||||
sidebarView.value = false;
|
||||
sidebarState.value = false;
|
||||
sidebarFilter.value = false;
|
||||
sidebarState.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarState, (newValue) => {
|
||||
if(newValue) {
|
||||
sidebarFilter.value = false;
|
||||
sidebarTraces.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
async function switchMapType(type) {
|
||||
mapType.value = type;
|
||||
createCy(type);
|
||||
}
|
||||
|
||||
async function switchCurveStyles(style) {
|
||||
curveStyle.value = style;
|
||||
createCy(mapType.value);
|
||||
}
|
||||
|
||||
async function switchRank(rankValue) {
|
||||
rank.value = rankValue;
|
||||
createCy(mapType.value);
|
||||
}
|
||||
|
||||
async function switchDataLayerType(type, option){
|
||||
dataLayerType.value = type;
|
||||
dataLayerOption.value = option;
|
||||
createCy(mapType.value);
|
||||
}
|
||||
|
||||
async function switchTraceId(e) {
|
||||
if(e.id == traceId.value) return;
|
||||
isLoading.value = true;
|
||||
traceId.value = e.id;
|
||||
await allMapDataStore.getTraceDetail();
|
||||
tracesViewRef.value.createCy();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
function setNodesData(mapData) {
|
||||
const mapTypeVal = mapType.value;
|
||||
const logFreq = {
|
||||
"total": "",
|
||||
"rel_freq": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"cases": ""
|
||||
};
|
||||
const logDuration = {
|
||||
"total": "",
|
||||
"rel_duration": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
};
|
||||
const gateway = {
|
||||
parallel: "+",
|
||||
exclusive: "x",
|
||||
inclusive: "o",
|
||||
};
|
||||
|
||||
mapData.nodes = [];
|
||||
const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
|
||||
mapSource.vertices.forEach(node => {
|
||||
switch (node.type) {
|
||||
case 'gateway':
|
||||
mapData.nodes.push({
|
||||
data:{
|
||||
id:node.id,
|
||||
type:node.type,
|
||||
label:gateway[node.gateway_type],
|
||||
height:60,
|
||||
width:60,
|
||||
backgroundColor:'#FFF',
|
||||
bordercolor:'#003366',
|
||||
shape:"diamond",
|
||||
freq:logFreq,
|
||||
duration:logDuration,
|
||||
}
|
||||
})
|
||||
break;
|
||||
case 'event':
|
||||
if(node.event_type === 'start') mapData.startId = node.id;
|
||||
else if(node.event_type === 'end') mapData.endId = node.id;
|
||||
|
||||
mapData.nodes.push({
|
||||
data:{
|
||||
id:node.id,
|
||||
type:node.type,
|
||||
label:node.event_type,
|
||||
height: 48,
|
||||
width: 48,
|
||||
backgroundColor:'#FFFFFF',
|
||||
bordercolor:'#0F172A',
|
||||
textColor: '#FF3366',
|
||||
shape:"ellipse",
|
||||
freq:logFreq,
|
||||
duration:logDuration,
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
mapData.nodes.push({
|
||||
data:{
|
||||
id:node.id,
|
||||
type:node.type,
|
||||
label:node.label,
|
||||
height: 48,
|
||||
width: 216,
|
||||
textColor: '#0F172A',
|
||||
backgroundColor:'rgba(0, 0, 0, 0)',
|
||||
borderradius: 999,
|
||||
shape:"round-rectangle",
|
||||
freq:node.freq,
|
||||
duration:node.duration,
|
||||
backgroundOpacity: 0,
|
||||
borderOpacity: 0,
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setEdgesData(mapData) {
|
||||
const mapTypeVal = mapType.value;
|
||||
const logDuration = {
|
||||
"total": "",
|
||||
"rel_duration": "",
|
||||
"average": "",
|
||||
"median": "",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"cases": ""
|
||||
};
|
||||
|
||||
mapData.edges = [];
|
||||
const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
|
||||
mapSource.edges.forEach(edge => {
|
||||
mapData.edges.push({
|
||||
data: {
|
||||
source:edge.tail,
|
||||
target:edge.head,
|
||||
freq:edge.freq,
|
||||
duration:edge.duration === null ? logDuration : edge.duration,
|
||||
style:'dotted',
|
||||
lineWidth:1,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createCy(type) {
|
||||
const graphId = document.getElementById('cy');
|
||||
const mapData = type === 'processMap'? processMapData.value: bpmnData.value;
|
||||
const mapSource = type === 'processMap' ? processMap.value : bpmn.value;
|
||||
|
||||
if(mapSource.vertices.length !== 0){
|
||||
setNodesData(mapData);
|
||||
setEdgesData(mapData);
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
function setActivityBgImage(mapData) {
|
||||
const nodes = mapData.nodes;
|
||||
const groupSize = Math.floor(nodes.length / ImgCapsules.length);
|
||||
let nodeOptionArr = [];
|
||||
const leveledGroups = [];
|
||||
const activityNodeArray = nodes.filter(node => node.data.type === 'activity');
|
||||
activityNodeArray.forEach(node => nodeOptionArr.push(node.data[dataLayerType.value][dataLayerOption.value]));
|
||||
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
|
||||
for(let i = 0; i < ImgCapsules.length; i++) {
|
||||
const startIdx = i * groupSize;
|
||||
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
|
||||
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
|
||||
}
|
||||
for(let level = 0; level < leveledGroups.length; level++) {
|
||||
leveledGroups[level].forEach(option => {
|
||||
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[dataLayerType.value][dataLayerOption.value] === option);
|
||||
curNodes.forEach(curNode => {
|
||||
curNode.data = {
|
||||
...curNode.data,
|
||||
nodeImageUrl: ImgCapsules[level],
|
||||
level,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Created logic
|
||||
(async () => {
|
||||
const routeParams = route.params;
|
||||
const file = route.meta.file;
|
||||
const isCheckPage = route.name.includes('Check');
|
||||
|
||||
isLoading.value = true;
|
||||
switch (routeParams.type) {
|
||||
case 'log':
|
||||
if(!isCheckPage) {
|
||||
logId.value = await routeParams.fileId;
|
||||
baseLogId.value = await routeParams.fileId;
|
||||
} else {
|
||||
logId.value = await file.parent.id;
|
||||
baseLogId.value = await file.parent.id;
|
||||
}
|
||||
break;
|
||||
case 'filter':
|
||||
if(!isCheckPage) {
|
||||
createFilterId.value = await routeParams.fileId;
|
||||
} else {
|
||||
createFilterId.value = await file.parent.id;
|
||||
}
|
||||
await allMapDataStore.fetchFunnel(createFilterId.value);
|
||||
isRuleData.value = await Array.from(temporaryData.value);
|
||||
ruleData.value = await isRuleData.value.map(e => sidebarFilterRefComp.value.setRule(e));
|
||||
break;
|
||||
}
|
||||
await allMapDataStore.getAllMapData();
|
||||
await allMapDataStore.getAllTrace();
|
||||
|
||||
traceId.value = await traces.value[0]?.id;
|
||||
baseTraceId.value = await baseTraces.value[0]?.id;
|
||||
await createCy(mapType.value);
|
||||
await allMapDataStore.getFilterParams();
|
||||
await allMapDataStore.getTraceDetail();
|
||||
|
||||
isLoading.value = false;
|
||||
emitter.on('saveModal', boolean => {
|
||||
sidebarView.value = boolean;
|
||||
sidebarFilter.value = boolean;
|
||||
sidebarTraces.value = boolean;
|
||||
sidebarState.value = boolean;
|
||||
});
|
||||
emitter.on('leaveFilter', boolean => {
|
||||
sidebarView.value = boolean;
|
||||
sidebarFilter.value = boolean;
|
||||
sidebarTraces.value = boolean;
|
||||
sidebarState.value = boolean;
|
||||
});
|
||||
})();
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
logId.value = null;
|
||||
createFilterId.value = null;
|
||||
temporaryData.value = [];
|
||||
postRuleData.value = [];
|
||||
ruleData.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Chart type="line" :data="primeVueSetDataState" :options="primeVueSetOptionsState" class="h-96" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {
|
||||
setTimeStringFormatBaseOnTimeDifference,
|
||||
@@ -65,220 +65,208 @@ y: {
|
||||
},
|
||||
};
|
||||
|
||||
// 試著把 chart 獨立成一個 vue component
|
||||
// 企圖防止 PrimeVue 誤用其他圖表 option 值的 bug
|
||||
export default {
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
},
|
||||
content: {
|
||||
type: Object,
|
||||
},
|
||||
yUnit: {
|
||||
type: String,
|
||||
},
|
||||
pageName: {
|
||||
type: String,
|
||||
},
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Object,
|
||||
},
|
||||
setup(props) {
|
||||
content: {
|
||||
type: Object,
|
||||
},
|
||||
yUnit: {
|
||||
type: String,
|
||||
},
|
||||
pageName: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const primeVueSetDataState = ref(null);
|
||||
const primeVueSetOptionsState = ref(null);
|
||||
const colorPrimary = ref('#0099FF');
|
||||
const colorSecondary = ref('#FFAA44');
|
||||
const primeVueSetDataState = ref(null);
|
||||
const primeVueSetOptionsState = ref(null);
|
||||
const colorPrimary = ref('#0099FF');
|
||||
const colorSecondary = ref('#FFAA44');
|
||||
|
||||
/**
|
||||
* Compare page and Performance have this same function.
|
||||
* @param whichScaleObj PrimeVue scale option object to reference to
|
||||
* @param customizeOptions
|
||||
* @param customizeOptions.content
|
||||
* @param customizeOptions.ticksOfXAxis
|
||||
*/
|
||||
const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: {
|
||||
content,
|
||||
ticksOfXAxis,
|
||||
},
|
||||
}) => {
|
||||
let resultScaleObj;
|
||||
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
|
||||
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
|
||||
return resultScaleObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare page and Performance have this same function.
|
||||
* @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.
|
||||
*/
|
||||
const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
|
||||
return {
|
||||
...scaleObjectToAlter,
|
||||
x: {
|
||||
...scaleObjectToAlter.x,
|
||||
ticks: {
|
||||
...scaleObjectToAlter.x.ticks,
|
||||
callback: function(value, index) {
|
||||
// 根據不同的級距客製化 x 軸的時間刻度
|
||||
return ticksOfXAxis[index];
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Compare page and Performance have this same function.
|
||||
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 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
|
||||
*/
|
||||
const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => {
|
||||
if (!content) {
|
||||
// Early return
|
||||
return whichScaleObj;
|
||||
}
|
||||
|
||||
return {
|
||||
...whichScaleObj,
|
||||
x: {
|
||||
...whichScaleObj.x,
|
||||
title: {
|
||||
...whichScaleObj.x.title,
|
||||
text: content.x
|
||||
}
|
||||
},
|
||||
y: {
|
||||
...whichScaleObj.y,
|
||||
title: {
|
||||
...whichScaleObj.y.title,
|
||||
text: content.y
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
|
||||
let datasetsArr;
|
||||
let datasets;
|
||||
let datasetsPrimary; // For Compare page case
|
||||
let datasetsSecondary; // For Compare page case
|
||||
const minX = chartData?.x_axis?.min;
|
||||
const maxX = chartData?.x_axis?.max;
|
||||
let xData;
|
||||
let primeVueSetData = {};
|
||||
let primeVueSetOption = {};
|
||||
|
||||
// 考慮 chartData.data 的dimension
|
||||
// 當我們遇到了 Compare 頁面的案例
|
||||
if(pageName === "Compare"){
|
||||
datasetsPrimary = chartData.data[0].data;
|
||||
datasetsSecondary = chartData.data[1].data;
|
||||
|
||||
datasetsArr = [
|
||||
{
|
||||
label: chartData.data[0].label,
|
||||
data: datasetsPrimary,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: colorPrimary,
|
||||
pointBackgroundColor: colorPrimary,
|
||||
},
|
||||
{
|
||||
label: chartData.data[1].label,
|
||||
data: datasetsSecondary,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: colorSecondary,
|
||||
pointBackgroundColor: colorSecondary,
|
||||
}
|
||||
];
|
||||
xData = chartData.data[0].data.map(item => new Date(item.x).getTime());
|
||||
} else {
|
||||
datasets = chartData.data;
|
||||
datasetsArr = [
|
||||
{
|
||||
label: content.title,
|
||||
data: datasets,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: '#0099FF',
|
||||
}
|
||||
];
|
||||
xData = chartData.data.map(item => new Date(item.x).getTime());
|
||||
}
|
||||
|
||||
|
||||
// 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 ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
|
||||
const customizedScaleOption = getCustomizedScaleOption(
|
||||
knownScaleLineChartOptions, {
|
||||
customizeOptions: {
|
||||
content, ticksOfXAxis,
|
||||
}
|
||||
});
|
||||
|
||||
primeVueSetData = {
|
||||
labels: xData,
|
||||
datasets: datasetsArr,
|
||||
};
|
||||
primeVueSetOption = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: false, // 圖例
|
||||
tooltip: {
|
||||
displayColors: true,
|
||||
titleFont: {weight: 'normal'},
|
||||
callbacks: {
|
||||
label: function(tooltipItem) {
|
||||
// 取得數據
|
||||
const label = tooltipItem.dataset.label || '';
|
||||
|
||||
// 建立一個小方塊顯示顏色
|
||||
return `${label}: ${tooltipItem.parsed.y}`; // 使用 Unicode 方塊表示顏色
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: customizedScaleOption,
|
||||
};
|
||||
|
||||
primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位
|
||||
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
|
||||
return value; //這裡的Y軸刻度沒有後綴代表時間的英文字母
|
||||
};
|
||||
primeVueSetDataState.value = primeVueSetData;
|
||||
primeVueSetOptionsState.value = primeVueSetOption;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getLineChartPrimeVueSetting(props.chartData, props.content, props.pageName);
|
||||
});
|
||||
|
||||
return {
|
||||
...props,
|
||||
primeVueSetDataState,
|
||||
primeVueSetOptionsState,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Compare page and Performance have this same function.
|
||||
* @param whichScaleObj PrimeVue scale option object to reference to
|
||||
* @param customizeOptions
|
||||
* @param customizeOptions.content
|
||||
* @param customizeOptions.ticksOfXAxis
|
||||
*/
|
||||
const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: {
|
||||
content,
|
||||
ticksOfXAxis,
|
||||
},
|
||||
}) => {
|
||||
let resultScaleObj;
|
||||
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
|
||||
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
|
||||
return resultScaleObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare page and Performance have this same function.
|
||||
* @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.
|
||||
*/
|
||||
const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
|
||||
return {
|
||||
...scaleObjectToAlter,
|
||||
x: {
|
||||
...scaleObjectToAlter.x,
|
||||
ticks: {
|
||||
...scaleObjectToAlter.x.ticks,
|
||||
callback: function(value, index) {
|
||||
// 根據不同的級距客製化 x 軸的時間刻度
|
||||
return ticksOfXAxis[index];
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Compare page and Performance have this same function.
|
||||
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 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
|
||||
*/
|
||||
const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => {
|
||||
if (!content) {
|
||||
// Early return
|
||||
return whichScaleObj;
|
||||
}
|
||||
|
||||
return {
|
||||
...whichScaleObj,
|
||||
x: {
|
||||
...whichScaleObj.x,
|
||||
title: {
|
||||
...whichScaleObj.x.title,
|
||||
text: content.x
|
||||
}
|
||||
},
|
||||
y: {
|
||||
...whichScaleObj.y,
|
||||
title: {
|
||||
...whichScaleObj.y.title,
|
||||
text: content.y
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
|
||||
let datasetsArr;
|
||||
let datasets;
|
||||
let datasetsPrimary; // For Compare page case
|
||||
let datasetsSecondary; // For Compare page case
|
||||
const minX = chartData?.x_axis?.min;
|
||||
const maxX = chartData?.x_axis?.max;
|
||||
let xData;
|
||||
let primeVueSetData = {};
|
||||
let primeVueSetOption = {};
|
||||
|
||||
// 考慮 chartData.data 的dimension
|
||||
// 當我們遇到了 Compare 頁面的案例
|
||||
if(pageName === "Compare"){
|
||||
datasetsPrimary = chartData.data[0].data;
|
||||
datasetsSecondary = chartData.data[1].data;
|
||||
|
||||
datasetsArr = [
|
||||
{
|
||||
label: chartData.data[0].label,
|
||||
data: datasetsPrimary,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: colorPrimary,
|
||||
pointBackgroundColor: colorPrimary,
|
||||
},
|
||||
{
|
||||
label: chartData.data[1].label,
|
||||
data: datasetsSecondary,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: colorSecondary,
|
||||
pointBackgroundColor: colorSecondary,
|
||||
}
|
||||
];
|
||||
xData = chartData.data[0].data.map(item => new Date(item.x).getTime());
|
||||
} else {
|
||||
datasets = chartData.data;
|
||||
datasetsArr = [
|
||||
{
|
||||
label: content.title,
|
||||
data: datasets,
|
||||
fill: false,
|
||||
tension: 0, // 貝茲曲線張力
|
||||
borderColor: '#0099FF',
|
||||
}
|
||||
];
|
||||
xData = chartData.data.map(item => new Date(item.x).getTime());
|
||||
}
|
||||
|
||||
|
||||
// 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 ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
|
||||
const customizedScaleOption = getCustomizedScaleOption(
|
||||
knownScaleLineChartOptions, {
|
||||
customizeOptions: {
|
||||
content, ticksOfXAxis,
|
||||
}
|
||||
});
|
||||
|
||||
primeVueSetData = {
|
||||
labels: xData,
|
||||
datasets: datasetsArr,
|
||||
};
|
||||
primeVueSetOption = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: false, // 圖例
|
||||
tooltip: {
|
||||
displayColors: true,
|
||||
titleFont: {weight: 'normal'},
|
||||
callbacks: {
|
||||
label: function(tooltipItem) {
|
||||
// 取得數據
|
||||
const label = tooltipItem.dataset.label || '';
|
||||
|
||||
// 建立一個小方塊顯示顏色
|
||||
return `${label}: ${tooltipItem.parsed.y}`; // 使用 Unicode 方塊表示顏色
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: customizedScaleOption,
|
||||
};
|
||||
|
||||
primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位
|
||||
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
|
||||
return value; //這裡的Y軸刻度沒有後綴代表時間的英文字母
|
||||
};
|
||||
primeVueSetDataState.value = primeVueSetData;
|
||||
primeVueSetOptionsState.value = primeVueSetOption;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getLineChartPrimeVueSetting(props.chartData, props.content, props.pageName);
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
">do_not_disturb_on</span>
|
||||
</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>
|
||||
</section>
|
||||
<!-- Recently Used -->
|
||||
@@ -209,7 +209,7 @@
|
||||
</section>
|
||||
</div>
|
||||
<!-- 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 }">
|
||||
<a class="flex align-items-center px-4 py-2 duration-300 hover:bg-primary/20">
|
||||
<span class="material-symbols-outlined">{{ item.icon }}</span>
|
||||
@@ -220,8 +220,10 @@
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { storeToRefs, mapActions, } from 'pinia';
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMapCompareStore } from '@/stores/mapCompareStore';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
import { useFilesStore } from '@/stores/files';
|
||||
@@ -237,390 +239,388 @@
|
||||
import IconGrid from '@/components/icons/IconGrid.vue';
|
||||
import { renameModal, deleteFileModal, reallyDeleteInformation } from '@/module/alertModal.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mapCompareStore: useMapCompareStore(),
|
||||
isActive: null,
|
||||
isHover: null,
|
||||
switchListOrGrid: false,
|
||||
selectedTableFile: null, // table 右鍵選單 item
|
||||
selectedFile: null, // 右鍵選單 item
|
||||
selectedType: null,
|
||||
selectedId: null,
|
||||
selectedName: null,
|
||||
items: [
|
||||
{
|
||||
label: 'Rename',
|
||||
icon: 'edit_square',
|
||||
command: this.rename,
|
||||
},
|
||||
{
|
||||
label: 'Download',
|
||||
icon: 'download',
|
||||
command: this.download,
|
||||
},
|
||||
{
|
||||
separator: true // 分隔符號
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: 'delete',
|
||||
command: this.deleteFile,
|
||||
},
|
||||
],
|
||||
compareData: null,
|
||||
primaryDragData: [],
|
||||
secondaryDragData: [],
|
||||
gridSort: null,
|
||||
columnType: [
|
||||
{ name: 'By File Name (A to Z)', code: 'nameAscending'},
|
||||
{ name: 'By File Name (Z to A)', code: 'nameDescending'},
|
||||
{ name: 'By Dependency (A to Z)', code: 'parentLogAscending'},
|
||||
{ name: 'By Dependency (Z to A)', code: 'parentLogDescending'},
|
||||
{ name: 'By File Type (A to Z)', code: 'fileAscending'},
|
||||
{ name: 'By File Type (Z to A)', code: 'fileDescending'},
|
||||
{ name: 'By Last Update (A to Z)', code: 'updatedAscending'},
|
||||
{ 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);
|
||||
const router = useRouter();
|
||||
|
||||
return { loginStore, store, dependentsData, filesTag, allMapDataStore, createFilterId, baseLogId, isLoading }
|
||||
},
|
||||
components: {
|
||||
IconDataFormat,
|
||||
IconRule,
|
||||
IconsFilter,
|
||||
IconFlowChart,
|
||||
IconVector,
|
||||
IconList,
|
||||
IconGrid
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Read allFiles
|
||||
*/
|
||||
allFiles: function() {
|
||||
if(this.store.allFiles.length !== 0){
|
||||
const sortFiles = Array.from(this.store.allFiles);
|
||||
sortFiles.sort((x,y) => new Date(y.updated_base) - new Date(x.updated_base));
|
||||
return sortFiles;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 時間排序,如果沒有 accessed_at 就不加入 data
|
||||
*/
|
||||
recentlyUsedFiles: function() {
|
||||
let recentlyUsedFiles = Array.from(this.store.allFiles);
|
||||
recentlyUsedFiles = recentlyUsedFiles.filter(item => item.accessed_at !== null);
|
||||
recentlyUsedFiles.sort((x, y) => new Date(y.accessed_base) - new Date(x.accessed_base));
|
||||
return recentlyUsedFiles;
|
||||
},
|
||||
/**
|
||||
* Compare Submit button disabled
|
||||
*/
|
||||
isCompareDisabledButton: function() {
|
||||
const result = this.primaryDragData.length === 0 || this.secondaryDragData.length === 0;
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Really deleted information
|
||||
*/
|
||||
reallyDeleteData: function() {
|
||||
let result = [];
|
||||
// Stores
|
||||
const mapCompareStore = useMapCompareStore();
|
||||
const loginStore = useLoginStore();
|
||||
const store = useFilesStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
if(this.store.allFiles.length !== 0){
|
||||
result = JSON.parse(JSON.stringify(this.store.allFiles));
|
||||
result = result.filter(file => file.is_deleted === true);
|
||||
}
|
||||
const { dependentsData, filesTag } = storeToRefs(store);
|
||||
const { createFilterId, baseLogId } = storeToRefs(allMapDataStore);
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
|
||||
return result
|
||||
}
|
||||
// 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',
|
||||
icon: 'edit_square',
|
||||
command: rename,
|
||||
},
|
||||
watch: {
|
||||
filesTag: {
|
||||
handler(newValue) {
|
||||
if(newValue !== 'COMPARE'){
|
||||
this.primaryDragData = [];
|
||||
this.secondaryDragData = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
allFiles: {
|
||||
handler(newValue) {
|
||||
if(newValue !== null) this.compareData = JSON.parse(JSON.stringify(newValue));
|
||||
}
|
||||
},
|
||||
reallyDeleteData: {
|
||||
handler(newValue, oldValue) {
|
||||
if(newValue.length !== 0 && oldValue.length === 0){
|
||||
this.showReallyDelete();
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
{
|
||||
label: 'Download',
|
||||
icon: 'download',
|
||||
command: download,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Set Row Style
|
||||
*/
|
||||
setRowClass() {
|
||||
return ['group']
|
||||
},
|
||||
/**
|
||||
* Set Compare Row Style
|
||||
*/
|
||||
setCompareRowClass() {
|
||||
return ['leading-6']
|
||||
},
|
||||
/**
|
||||
* 選擇該 files 進入 Discover/Compare/Design 頁面
|
||||
* @param {object} file 該 file 的詳細資料
|
||||
*/
|
||||
enterDiscover(file){
|
||||
let type;
|
||||
let fileId;
|
||||
let params;
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: 'delete',
|
||||
command: deleteFile,
|
||||
},
|
||||
];
|
||||
|
||||
this.setCurrentMapFile(file.name);
|
||||
const columnType = [
|
||||
{ name: 'By File Name (A to Z)', code: 'nameAscending'},
|
||||
{ name: 'By File Name (Z to A)', code: 'nameDescending'},
|
||||
{ name: 'By Dependency (A to Z)', code: 'parentLogAscending'},
|
||||
{ name: 'By Dependency (Z to A)', code: 'parentLogDescending'},
|
||||
{ name: 'By File Type (A to Z)', code: 'fileAscending'},
|
||||
{ name: 'By File Type (Z to A)', code: 'fileDescending'},
|
||||
{ name: 'By Last Update (A to Z)', code: 'updatedAscending'},
|
||||
{ name: 'By Last Update (Z to A)', code: 'updatedDescending'},
|
||||
];
|
||||
|
||||
switch (file.type) {
|
||||
case 'log':
|
||||
this.createFilterId = null;
|
||||
this.baseLogId = file.id;
|
||||
fileId = file.id;
|
||||
type = file.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
this.$router.push({name: 'Map', params: params});
|
||||
break;
|
||||
case 'filter':
|
||||
this.createFilterId = file.id;
|
||||
this.baseLogId = file.parent.id;
|
||||
fileId = file.id;
|
||||
type = file.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
this.$router.push({name: 'Map', params: params});
|
||||
break;
|
||||
// Computed
|
||||
/**
|
||||
* Read allFiles
|
||||
*/
|
||||
const allFiles = computed(() => {
|
||||
if(store.allFiles.length !== 0){
|
||||
const sortFiles = Array.from(store.allFiles);
|
||||
sortFiles.sort((x,y) => new Date(y.updated_base) - new Date(x.updated_base));
|
||||
return sortFiles;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 時間排序,如果沒有 accessed_at 就不加入 data
|
||||
*/
|
||||
const recentlyUsedFiles = computed(() => {
|
||||
let recentlyUsed = Array.from(store.allFiles);
|
||||
recentlyUsed = recentlyUsed.filter(item => item.accessed_at !== null);
|
||||
recentlyUsed.sort((x, y) => new Date(y.accessed_base) - new Date(x.accessed_base));
|
||||
return recentlyUsed;
|
||||
});
|
||||
|
||||
/**
|
||||
* Compare Submit button disabled
|
||||
*/
|
||||
const isCompareDisabledButton = computed(() => {
|
||||
return primaryDragData.value.length === 0 || secondaryDragData.value.length === 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* Really deleted information
|
||||
*/
|
||||
const reallyDeleteData = computed(() => {
|
||||
let result = [];
|
||||
if(store.allFiles.length !== 0){
|
||||
result = JSON.parse(JSON.stringify(store.allFiles));
|
||||
result = result.filter(file => file.is_deleted === true);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// Watch
|
||||
watch(filesTag, (newValue) => {
|
||||
if(newValue !== 'COMPARE'){
|
||||
primaryDragData.value = [];
|
||||
secondaryDragData.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
watch(allFiles, (newValue) => {
|
||||
if(newValue !== null) compareData.value = JSON.parse(JSON.stringify(newValue));
|
||||
});
|
||||
|
||||
watch(reallyDeleteData, (newValue, oldValue) => {
|
||||
if(newValue.length !== 0 && oldValue.length === 0){
|
||||
showReallyDelete();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Methods
|
||||
/**
|
||||
* Set Row Style
|
||||
*/
|
||||
function setRowClass() {
|
||||
return ['group'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Compare Row Style
|
||||
*/
|
||||
function setCompareRowClass() {
|
||||
return ['leading-6'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 選擇該 files 進入 Discover/Compare/Design 頁面
|
||||
* @param {object} file 該 file 的詳細資料
|
||||
*/
|
||||
function enterDiscover(file){
|
||||
let type;
|
||||
let fileId;
|
||||
let params;
|
||||
|
||||
pageAdminStore.setCurrentMapFile(file.name);
|
||||
|
||||
switch (file.type) {
|
||||
case 'log':
|
||||
createFilterId.value = null;
|
||||
baseLogId.value = file.id;
|
||||
fileId = file.id;
|
||||
type = file.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
router.push({name: 'Map', params: params});
|
||||
break;
|
||||
case 'filter':
|
||||
createFilterId.value = file.id;
|
||||
baseLogId.value = file.parent.id;
|
||||
fileId = file.id;
|
||||
type = file.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
router.push({name: 'Map', params: params});
|
||||
break;
|
||||
case 'log-check':
|
||||
case 'filter-check':
|
||||
fileId = file.id;
|
||||
type = file.parent.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
router.push({name: 'CheckConformance', params: params});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Right Click DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
* @param {string} file file's name
|
||||
*/
|
||||
function onRightClick(event, file) {
|
||||
selectedType.value = file.type;
|
||||
selectedId.value = file.id;
|
||||
selectedName.value = file.name;
|
||||
fileRightMenuRef.value.show(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Right Click Table DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
*/
|
||||
function onRightClickTable(event) {
|
||||
selectedType.value = event.data.type;
|
||||
selectedId.value = event.data.id;
|
||||
selectedName.value = event.data.name;
|
||||
fileRightMenuRef.value.show(event.originalEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Right Click Gride Card DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
* @param {number} index 該 file 的 index
|
||||
*/
|
||||
function onGridCardClick(file, index) {
|
||||
selectedType.value = file.type;
|
||||
selectedId.value = file.id;
|
||||
selectedName.value = file.name;
|
||||
isActive.value = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* File's Rename
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
* @param {string} fileName file's name
|
||||
*/
|
||||
function rename(type, id, source, fileName) {
|
||||
if(type && id && source === 'list-hover') {
|
||||
selectedType.value = type;
|
||||
selectedId.value = id;
|
||||
selectedName.value = fileName;
|
||||
}
|
||||
renameModal(store.rename, selectedType.value, selectedId.value, selectedName.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
*/
|
||||
async function deleteFile(type, id, name, source) {
|
||||
let srt = '';
|
||||
let data = [];
|
||||
// 判斷是否來自 hover icon 選單
|
||||
if(type && id && name && source === 'list-hover') {
|
||||
selectedType.value = type;
|
||||
selectedId.value = id;
|
||||
selectedName.value = name;
|
||||
}
|
||||
// 取得相依性檔案
|
||||
await store.getDependents(selectedType.value, selectedId.value);
|
||||
if(dependentsData.value.length !== 0) {
|
||||
data = [...dependentsData.value];
|
||||
data.forEach(i => {
|
||||
switch (i.type) {
|
||||
case 'log-check':
|
||||
i.type = 'rule';
|
||||
break;
|
||||
case 'filter-check':
|
||||
fileId = file.id;
|
||||
type = file.parent.type;
|
||||
params = { type: type, fileId: fileId };
|
||||
this.$router.push({name: 'CheckConformance', params: params});
|
||||
i.type = 'rule';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Right Click DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
* @param {string} file file's name
|
||||
*/
|
||||
onRightClick(event, file) {
|
||||
this.selectedType = file.type;
|
||||
this.selectedId = file.id;
|
||||
this.selectedName = file.name;
|
||||
this.$refs.fileRightMenu.show(event)
|
||||
},
|
||||
/**
|
||||
* Right Click Table DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
*/
|
||||
onRightClickTable(event) {
|
||||
this.selectedType = event.data.type;
|
||||
this.selectedId = event.data.id;
|
||||
this.selectedName = event.data.name;
|
||||
this.$refs.fileRightMenu.show(event.originalEvent)
|
||||
},
|
||||
/**
|
||||
* Right Click Gride Card DOM Event
|
||||
* @param {event} event 該 file 的詳細資料
|
||||
* @param {number} index 該 file 的 index
|
||||
*/
|
||||
onGridCardClick(file, index) {
|
||||
this.selectedType = file.type;
|
||||
this.selectedId = file.id;
|
||||
this.selectedName = file.name;
|
||||
this.isActive = index;
|
||||
},
|
||||
/**
|
||||
* File's Rename
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
* @param {string} fileName file's name
|
||||
*/
|
||||
rename(type, id, source, fileName) {
|
||||
if(type && id && source === 'list-hover') {
|
||||
this.selectedType = type;
|
||||
this.selectedId = id;
|
||||
this.selectedName = fileName;
|
||||
}
|
||||
renameModal(this.store.rename, this.selectedType, this.selectedId, this.selectedName);
|
||||
},
|
||||
/**
|
||||
* Delete file
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
*/
|
||||
async deleteFile(type, id, name, source) {
|
||||
let srt = '';
|
||||
let data = [];
|
||||
// 判斷是否來自 hover icon 選單
|
||||
if(type && id && name && source === 'list-hover') {
|
||||
this.selectedType = type;
|
||||
this.selectedId = id;
|
||||
this.selectedName = name;
|
||||
}
|
||||
// 取得相依性檔案
|
||||
await this.store.getDependents(this.selectedType, this.selectedId);
|
||||
if(this.dependentsData.length !== 0) {
|
||||
data = [...this.dependentsData];
|
||||
data.forEach(i => {
|
||||
switch (i.type) {
|
||||
case 'log-check':
|
||||
i.type = 'rule';
|
||||
break;
|
||||
case 'filter-check':
|
||||
i.type = 'rule';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const content = `<li>[${i.type}] ${i.name}</li>`;
|
||||
srt += content;
|
||||
});
|
||||
}
|
||||
deleteFileModal(srt, this.selectedType, this.selectedId, this.selectedName);
|
||||
srt = '';
|
||||
},
|
||||
/**
|
||||
* 顯示被 Admin 或被其他帳號刪除的檔案
|
||||
*/
|
||||
showReallyDelete(){
|
||||
let srt = '';
|
||||
|
||||
if(this.reallyDeleteData.length !== 0) {
|
||||
this.reallyDeleteData.forEach(file => {
|
||||
switch (file.type) {
|
||||
case 'log-check':
|
||||
case 'filter-check':
|
||||
default:
|
||||
file.type = 'rule';
|
||||
break;
|
||||
}
|
||||
|
||||
const content = `<li>[${file.type}] ${file.name}</li>`;
|
||||
srt += content;
|
||||
});
|
||||
}
|
||||
reallyDeleteInformation(srt, this.reallyDeleteData);
|
||||
srt = '';
|
||||
},
|
||||
/**
|
||||
* Download file as CSV
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
*/
|
||||
download(type, id, source, name) {
|
||||
if(type && id && source === 'list-hover' && name) {
|
||||
this.selectedType = type;
|
||||
this.selectedId = id;
|
||||
this.selectedName = name;
|
||||
}
|
||||
this.store.downloadFileCSV(this.selectedType, this.selectedId, this.selectedName);
|
||||
},
|
||||
/**
|
||||
* Delete Compare Primary log
|
||||
*/
|
||||
primaryDragDelete() {
|
||||
this.compareData.unshift(this.primaryDragData[0]);
|
||||
this.primaryDragData.length = 0;
|
||||
},
|
||||
/**
|
||||
* Delete Compare Secondary log
|
||||
*/
|
||||
secondaryDragDelete() {
|
||||
this.compareData.unshift(this.secondaryDragData[0]);
|
||||
this.secondaryDragData.length = 0;
|
||||
},
|
||||
/**
|
||||
* Enter the Compare page
|
||||
*/
|
||||
compareSubmit() {
|
||||
const primaryType = this.primaryDragData[0].type;
|
||||
const secondaryType = this.secondaryDragData[0].type;
|
||||
const primaryId = this.primaryDragData[0].id;
|
||||
const secondaryId = this.secondaryDragData[0].id;
|
||||
const params = { primaryType: primaryType, primaryId: primaryId, secondaryType: secondaryType, secondaryId: secondaryId };
|
||||
|
||||
this.mapCompareStore.setCompareRouteParam(primaryType, primaryId, secondaryType, secondaryId);
|
||||
this.$router.push({name: 'CompareDashboard', params: params});
|
||||
},
|
||||
/**
|
||||
* Grid 模板時的篩選器
|
||||
* @param {event} event choose columnType item
|
||||
*/
|
||||
getGridSortData(event) {
|
||||
const code = event.value.code;
|
||||
|
||||
// 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較
|
||||
switch (code) {
|
||||
case 'nameAscending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
||||
break;
|
||||
case 'nameDescending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'parentLogAscending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase()));
|
||||
break;
|
||||
case 'parentLogDescending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'fileAscending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase()));
|
||||
break;
|
||||
case 'fileDescending':
|
||||
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'updatedAscending':
|
||||
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base));
|
||||
break;
|
||||
case 'updatedDescending':
|
||||
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)).reverse();
|
||||
break;
|
||||
}
|
||||
},
|
||||
...mapActions(
|
||||
usePageAdminStore, ['setCurrentMapFile',],
|
||||
)
|
||||
},
|
||||
mounted() {
|
||||
this.isLoading = true;
|
||||
this.store.fetchAllFiles();
|
||||
window.addEventListener('click', (e) => {
|
||||
const clickedLi = e.target.closest('li');
|
||||
if(!clickedLi || !clickedLi.id.startsWith('li')) this.isActive = null;
|
||||
})
|
||||
// 為 DataTable tbody 加入 .scrollbar 選擇器
|
||||
const tbodyElement = document.querySelector('.p-datatable-tbody');
|
||||
tbodyElement.classList.add('scrollbar');
|
||||
this.isLoading = false;
|
||||
},
|
||||
const content = `<li>[${i.type}] ${i.name}</li>`;
|
||||
srt += content;
|
||||
});
|
||||
}
|
||||
deleteFileModal(srt, selectedType.value, selectedId.value, selectedName.value);
|
||||
srt = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示被 Admin 或被其他帳號刪除的檔案
|
||||
*/
|
||||
function showReallyDelete(){
|
||||
let srt = '';
|
||||
|
||||
if(reallyDeleteData.value.length !== 0) {
|
||||
reallyDeleteData.value.forEach(file => {
|
||||
switch (file.type) {
|
||||
case 'log-check':
|
||||
case 'filter-check':
|
||||
default:
|
||||
file.type = 'rule';
|
||||
break;
|
||||
}
|
||||
|
||||
const content = `<li>[${file.type}] ${file.name}</li>`;
|
||||
srt += content;
|
||||
});
|
||||
}
|
||||
reallyDeleteInformation(srt, reallyDeleteData.value);
|
||||
srt = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file as CSV
|
||||
* @param {string} type 該檔案的 type
|
||||
* @param {number} id 該檔案的 id
|
||||
* @param {string} source hover icon 該檔案的 icon
|
||||
*/
|
||||
function download(type, id, source, name) {
|
||||
if(type && id && source === 'list-hover' && name) {
|
||||
selectedType.value = type;
|
||||
selectedId.value = id;
|
||||
selectedName.value = name;
|
||||
}
|
||||
store.downloadFileCSV(selectedType.value, selectedId.value, selectedName.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Compare Primary log
|
||||
*/
|
||||
function primaryDragDelete() {
|
||||
compareData.value.unshift(primaryDragData.value[0]);
|
||||
primaryDragData.value.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Compare Secondary log
|
||||
*/
|
||||
function secondaryDragDelete() {
|
||||
compareData.value.unshift(secondaryDragData.value[0]);
|
||||
secondaryDragData.value.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the Compare page
|
||||
*/
|
||||
function compareSubmit() {
|
||||
const primaryType = primaryDragData.value[0].type;
|
||||
const secondaryType = secondaryDragData.value[0].type;
|
||||
const primaryId = primaryDragData.value[0].id;
|
||||
const secondaryId = secondaryDragData.value[0].id;
|
||||
const params = { primaryType: primaryType, primaryId: primaryId, secondaryType: secondaryType, secondaryId: secondaryId };
|
||||
|
||||
mapCompareStore.setCompareRouteParam(primaryType, primaryId, secondaryType, secondaryId);
|
||||
router.push({name: 'CompareDashboard', params: params});
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid 模板時的篩選器
|
||||
* @param {event} event choose columnType item
|
||||
*/
|
||||
function getGridSortData(event) {
|
||||
const code = event.value.code;
|
||||
|
||||
// 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較
|
||||
switch (code) {
|
||||
case 'nameAscending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
||||
break;
|
||||
case 'nameDescending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'parentLogAscending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase()));
|
||||
break;
|
||||
case 'parentLogDescending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'fileAscending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase()));
|
||||
break;
|
||||
case 'fileDescending':
|
||||
compareData.value = compareData.value.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())).reverse();
|
||||
break;
|
||||
case 'updatedAscending':
|
||||
compareData.value = compareData.value.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base));
|
||||
break;
|
||||
case 'updatedDescending':
|
||||
compareData.value = compareData.value.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)).reverse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Mounted
|
||||
onMounted(() => {
|
||||
isLoading.value = true;
|
||||
store.fetchAllFiles();
|
||||
window.addEventListener('click', (e) => {
|
||||
const clickedLi = e.target.closest('li');
|
||||
if(!clickedLi || !clickedLi.id.startsWith('li')) isActive.value = null;
|
||||
});
|
||||
// 為 DataTable tbody 加入 .scrollbar 選擇器
|
||||
const tbodyElement = document.querySelector('.p-datatable-tbody');
|
||||
tbodyElement.classList.add('scrollbar');
|
||||
isLoading.value = false;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
@reference "../../assets/tailwind.css";
|
||||
|
||||
@@ -46,9 +46,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, } from 'vue';
|
||||
import { storeToRefs, mapActions } from 'pinia';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
import IconMember from '@/components/icons/IconMember.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 IconWarnTriangle from '@/components/icons/IconWarnTriangle.vue';
|
||||
|
||||
export default {
|
||||
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);
|
||||
const route = useRoute();
|
||||
|
||||
return {
|
||||
auth,
|
||||
isInvalid,
|
||||
signIn,
|
||||
isJustFocus,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
IconMember,
|
||||
IconLockKey,
|
||||
IconEyeOpen,
|
||||
IconEyeClose,
|
||||
IconWarnTriangle
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* if input no value , disabled.
|
||||
*/
|
||||
isDisabledButton() {
|
||||
return this.auth.username === '' || this.auth.password === '' || this.isInvalid;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* when input onChange value , isInvalid === false.
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
changeHandler(event) {
|
||||
const inputValue = event.target.value;
|
||||
if(inputValue !== '') {
|
||||
this.isInvalid = false;
|
||||
}
|
||||
},
|
||||
onInputAccountFocus(){
|
||||
},
|
||||
onInputPwdFocus(){
|
||||
},
|
||||
...mapActions(useLoginStore, ['setRememberedReturnToUrl']),
|
||||
},
|
||||
created() {
|
||||
// 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器
|
||||
// btoa: 對字串進行 Base64 編碼
|
||||
if(this.$route.query['return-to']) {
|
||||
this.setRememberedReturnToUrl(this.$route.query['return-to']);
|
||||
}
|
||||
},
|
||||
};
|
||||
// Store
|
||||
const store = useLoginStore();
|
||||
const { auth, isInvalid } = storeToRefs(store);
|
||||
const { signIn, setRememberedReturnToUrl } = store;
|
||||
|
||||
// Data
|
||||
const isDisabled = ref(true);
|
||||
const showPassword = ref(false);
|
||||
const isJustFocus = ref(true);
|
||||
|
||||
// Computed
|
||||
const isDisabledButton = computed(() => {
|
||||
return auth.value.username === '' || auth.value.password === '' || isInvalid.value;
|
||||
});
|
||||
|
||||
// Methods
|
||||
/**
|
||||
* when input onChange value , isInvalid === false.
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
function changeHandler(event) {
|
||||
const inputValue = event.target.value;
|
||||
if(inputValue !== '') {
|
||||
isInvalid.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onInputAccountFocus(){
|
||||
}
|
||||
|
||||
function onInputPwdFocus(){
|
||||
}
|
||||
|
||||
// Created logic
|
||||
// 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器
|
||||
// btoa: 對字串進行 Base64 編碼
|
||||
if(route.query['return-to']) {
|
||||
setRememberedReturnToUrl(route.query['return-to']);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,80 +11,15 @@
|
||||
</template>
|
||||
|
||||
<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 { usePageAdminStore } from "@/stores/pageAdmin";
|
||||
import { useAllMapDataStore } from "@/stores/allMapData";
|
||||
import { useConformanceStore } from "@/stores/conformance";
|
||||
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 {
|
||||
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不會被執行
|
||||
// PSEUDOCODE
|
||||
// if (not logged in) {
|
||||
@@ -102,7 +37,7 @@ export default {
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
if (!getCookie("isLuciaLoggedIn")) { //這裡不要用pinia的isLoggedIn來檢查,因為會有重新整理時撈不到Persisted value的值的bug
|
||||
if (!getCookie("isLuciaLoggedIn")) {
|
||||
if (getCookie('luciaRefreshToken')) {
|
||||
try {
|
||||
await loginStore.refreshToken();
|
||||
@@ -121,7 +56,6 @@ export default {
|
||||
next({
|
||||
path: '/login',
|
||||
query: {
|
||||
// 記憶未來登入後要進入的網址,且記憶的時候要用base64編碼包裹住
|
||||
'return-to': btoa(window.location.href),
|
||||
}
|
||||
});
|
||||
@@ -132,31 +66,69 @@ export default {
|
||||
},
|
||||
// Remember, Swal modal handling is called before beforeRouteUpdate
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.setPreviousPage(from.name);
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const conformanceStore = useConformanceStore();
|
||||
|
||||
pageAdminStore.setPreviousPage(from.name);
|
||||
|
||||
// 離開 Map 頁時判斷是否有無資料和需要存檔
|
||||
if ((from.name === 'Map' || from.name === 'CheckMap') && this.tempFilterId) {
|
||||
if ((from.name === 'Map' || from.name === 'CheckMap') && allMapDataStore.tempFilterId) {
|
||||
// 傳給 Map,通知 Sidebar 要關閉。
|
||||
this.$emitter.emit('leaveFilter', false);
|
||||
leaveFilter(next, this.allMapDataStore.addFilterId, to.path)
|
||||
} else if((this.$route.name === 'Conformance' || this.$route.name === 'CheckConformance')
|
||||
&& (this.conformanceLogTempCheckId || this.conformanceFilterTempCheckId)) {
|
||||
leaveConformance(next, this.conformanceStore.addConformanceCreateCheckId, to.path);
|
||||
} else if(this.shouldKeepPreviousPage) {
|
||||
// pass on and reset boolean for future use
|
||||
this.clearShouldKeepPreviousPageBoolean();
|
||||
emitter.emit('leaveFilter', false);
|
||||
leaveFilter(next, allMapDataStore.addFilterId, to.path)
|
||||
} else if((from.name === 'Conformance' || from.name === 'CheckConformance')
|
||||
&& (conformanceStore.conformanceLogTempCheckId || conformanceStore.conformanceFilterTempCheckId)) {
|
||||
leaveConformance(next, conformanceStore.addConformanceCreateCheckId, to.path);
|
||||
} else if(pageAdminStore.shouldKeepPreviousPage) {
|
||||
pageAdminStore.clearShouldKeepPreviousPageBoolean();
|
||||
} else {
|
||||
// most cases go this road
|
||||
|
||||
// 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();
|
||||
pageAdminStore.copyPendingPageToActivePage();
|
||||
next();
|
||||
}
|
||||
},
|
||||
};
|
||||
</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>
|
||||
|
||||
@@ -10,24 +10,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useLoginStore();
|
||||
const { userData } = storeToRefs(store);
|
||||
const { getUserData } = store;
|
||||
|
||||
return {
|
||||
userData,
|
||||
getUserData,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getUserData();
|
||||
}
|
||||
};
|
||||
const store = useLoginStore();
|
||||
const { userData } = storeToRefs(store);
|
||||
|
||||
onMounted(() => {
|
||||
store.getUserData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,13 +13,7 @@
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import Header from "@/components/Header.vue";
|
||||
import Navbar from "@/components/Navbar.vue";
|
||||
export default {
|
||||
components: {
|
||||
Header,
|
||||
Navbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -78,248 +78,14 @@
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useLoadingStore } from '@/stores/loading';
|
||||
import { useFilesStore } from '@/stores/files';
|
||||
import { uploadFailedFirst, uploadSuccess, uploadConfirm } from '@/module/alertModal.js'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loadingStore = useLoadingStore();
|
||||
const filesStore = useFilesStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const { uploadDetail, uploadId, uploadFileName } = storeToRefs(filesStore);
|
||||
|
||||
return { isLoading, filesStore, uploadDetail, uploadId, uploadFileName }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tooltipUpload: {
|
||||
value: `1. Case ID: A unique identifier for each case.
|
||||
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.
|
||||
4. Timestamp: The time of occurrence of a particular event, such as the start or end of an activity.
|
||||
5. Status: Activity status, such as Start or Complete.
|
||||
6. Attribute: A property that can be associated with a case to provide additional information about that case.`,
|
||||
// 暫時沒有 Resource
|
||||
// 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',
|
||||
autoHide: false,
|
||||
},
|
||||
columnType: [
|
||||
{ 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: 'Status*', code: 'status', color: '!text-secondary', value: '', label: 'Status', required: true },
|
||||
{ name: 'Activity*', code: 'name', color: '!text-secondary', value: '', label: 'Activity', required: true },
|
||||
{ name: 'Activity Instance ID*', code: 'instance', color: '!text-secondary', value: '', label: 'Activity Instance ID', required: true },
|
||||
{ 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: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false },
|
||||
],
|
||||
selectedColumns: [],
|
||||
informData: [], // 藍字提示,尚未選擇的 type
|
||||
repeatedData: [], // 紅字提示,重複選擇的 type
|
||||
fileName: this.uploadFileName,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDisabled: function() {
|
||||
// 1. 長度一樣,強制每一個都要選
|
||||
// 2. 不為 null undefind
|
||||
const hasValue = !this.selectedColumns.includes(undefined);
|
||||
const result = !(this.selectedColumns.length === this.uploadDetail?.columns.length
|
||||
&& this.informData.length === 0 && this.repeatedData.length === 0 && hasValue);
|
||||
return result
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedColumns: {
|
||||
deep: true, // 監聽陣列內部的變化
|
||||
handler(newVal, oldVal) {
|
||||
this.updateValidationData(newVal);
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uploadFailedFirst,
|
||||
uploadSuccess,
|
||||
uploadConfirm,
|
||||
/**
|
||||
* Rename 離開 input 的行為
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
onBlur(e) {
|
||||
const baseWidth = 20;
|
||||
|
||||
if(e.target.value === '') {
|
||||
e.target.value = this.uploadFileName;
|
||||
const textWidth = this.getTextWidth(e.target.value, e.target);
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
}else if(e.target.value !== e.target.value.trim()) {
|
||||
e.target.value = e.target.value.trim();
|
||||
const textWidth = this.getTextWidth(e.target.value, e.target);
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Rename 輸入 input 的行為
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
onInput(e) {
|
||||
const baseWidth = 20;
|
||||
const textWidth = this.getTextWidth(e.target.value, e.target);
|
||||
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
},
|
||||
/**
|
||||
* input 寬度隨著 value 響應式改變
|
||||
* @param {String} text file's name
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
getTextWidth(text, e) {
|
||||
// 替換空格為不斷行的空格
|
||||
const processedText = text.replace(/ /g, '\u00a0');
|
||||
const hiddenSpan = document.createElement('span');
|
||||
|
||||
hiddenSpan.innerHTML = processedText;
|
||||
hiddenSpan.style.font = window.getComputedStyle(e).font;
|
||||
hiddenSpan.style.visibility = 'hidden';
|
||||
document.body.appendChild(hiddenSpan);
|
||||
const width = hiddenSpan.getBoundingClientRect().width;
|
||||
document.body.removeChild(hiddenSpan);
|
||||
|
||||
return width;
|
||||
},
|
||||
/**
|
||||
* 驗證,根據新的 selectedColumns 更新 informData 和 repeatedData
|
||||
* @param {Array} data 已選擇的 type 的 data
|
||||
*/
|
||||
updateValidationData(data) {
|
||||
const nameOccurrences = {};
|
||||
const noSortedRepeatedData = []; // 未排序的重複選擇的 data
|
||||
const selectedData = [] // 已經選擇的 data
|
||||
|
||||
this.informData = []; // 尚未選擇的 data
|
||||
this.repeatedData = []; // 重複選擇的 data
|
||||
|
||||
data.forEach(item => {
|
||||
const { name, code } = item;
|
||||
|
||||
if(nameOccurrences[name]) {
|
||||
// 'Not Assigned'、'Case Attribute' 不列入驗證
|
||||
if(!code || code === 'case_attributes') return;
|
||||
nameOccurrences[name]++;
|
||||
// 重複的選項只出現一次
|
||||
if(nameOccurrences[name] === 2){
|
||||
noSortedRepeatedData.push(item)
|
||||
}
|
||||
// 要按照選單的順序排序
|
||||
this.repeatedData = this.columnType.filter(column => noSortedRepeatedData.includes(column));
|
||||
}else {
|
||||
nameOccurrences[name] = 1;
|
||||
selectedData.push(name);
|
||||
this.informData = this.columnType.filter(item => item.required ? !selectedData.includes(item.name) : false);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Reset Button
|
||||
*/
|
||||
reset() {
|
||||
// 路徑不列入歷史紀錄
|
||||
this.selectedColumns = [];
|
||||
},
|
||||
/**
|
||||
* Cancel Button
|
||||
*/
|
||||
cancel() {
|
||||
// 路徑不列入歷史紀錄
|
||||
this.$router.push({name: 'Files', replace: true});
|
||||
},
|
||||
/**
|
||||
* Upload Button
|
||||
*/
|
||||
async submit() {
|
||||
// Post API Data
|
||||
const fetchData = {
|
||||
timestamp: '',
|
||||
case_id: '',
|
||||
name: '',
|
||||
instance: '',
|
||||
status: '',
|
||||
case_attributes: []
|
||||
};
|
||||
// 給值
|
||||
const haveValueData = this.selectedColumns.map((column, i) => {
|
||||
if (column && this.uploadDetail.columns[i]) {
|
||||
return {
|
||||
name: column.name,
|
||||
code: column.code,
|
||||
color: column.color,
|
||||
value: this.uploadDetail.columns[i]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 取得欲更改的檔名,
|
||||
this.uploadFileName = this.fileName;
|
||||
// 設定第二階段上傳的 data
|
||||
haveValueData.forEach(column => {
|
||||
if(column !== undefined) {
|
||||
switch (column.code) {
|
||||
case 'timestamp':
|
||||
fetchData.timestamp = column.value;
|
||||
break;
|
||||
case 'case_id':
|
||||
fetchData.case_id = column.value;
|
||||
break;
|
||||
case 'name':
|
||||
fetchData.name = column.value;
|
||||
break;
|
||||
case 'instance':
|
||||
fetchData.instance = column.value;
|
||||
break;
|
||||
case 'status':
|
||||
fetchData.status = column.value;
|
||||
break;
|
||||
case 'case_attributes':
|
||||
fetchData.case_attributes.push(column.value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.uploadConfirm(fetchData);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
// 只監聽第一次
|
||||
const unwatch = this.$watch('fileName', (newValue) => {
|
||||
if (newValue) {
|
||||
const inputElement = document.getElementById('fileNameInput');
|
||||
const baseWidth = 20;
|
||||
const textWidth = this.getTextWidth(this.fileName, inputElement);
|
||||
inputElement.style.width = baseWidth + textWidth + 'px';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
this.showEdit = true;
|
||||
if(this.uploadId) await this.filesStore.getUploadDetail();
|
||||
this.selectedColumns = await Array.from({ length: this.uploadDetail.columns.length }, () => this.columnType[this.columnType.length - 1]); // 預設選 Not Assigned
|
||||
unwatch();
|
||||
this.isLoading = false;
|
||||
},
|
||||
beforeUnmount() {
|
||||
// 離開頁面要刪 uploadID
|
||||
this.uploadId = null;
|
||||
this.uploadFileName = null;
|
||||
},
|
||||
beforeRouteEnter(to, from, next){
|
||||
// 要有 uploadID 才能進來
|
||||
next(vm => {
|
||||
if(vm.uploadId === null) {
|
||||
const filesStore = useFilesStore();
|
||||
if(filesStore.uploadId === null) {
|
||||
vm.$router.push({name: 'Files', replace: true});
|
||||
vm.$toast.default('Please upload your file.', {position: 'bottom'});
|
||||
}
|
||||
@@ -327,3 +93,246 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<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.
|
||||
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.
|
||||
4. Timestamp: The time of occurrence of a particular event, such as the start or end of an activity.
|
||||
5. Status: Activity status, such as Start or Complete.
|
||||
6. Attribute: A property that can be associated with a case to provide additional information about that case.`,
|
||||
// 暫時沒有 Resource
|
||||
// 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',
|
||||
autoHide: false,
|
||||
};
|
||||
|
||||
const columnType = [
|
||||
{ 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: 'Status*', code: 'status', color: '!text-secondary', value: '', label: 'Status', required: true },
|
||||
{ name: 'Activity*', code: 'name', color: '!text-secondary', value: '', label: 'Activity', required: true },
|
||||
{ name: 'Activity Instance ID*', code: 'instance', color: '!text-secondary', value: '', label: 'Activity Instance ID', required: true },
|
||||
{ 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: 'Not Assigned', code: '', color: '!text-neutral-700', value: '', label: 'Not Assigned', required: false },
|
||||
];
|
||||
|
||||
const selectedColumns = ref([]);
|
||||
const informData = ref([]);
|
||||
const repeatedData = ref([]);
|
||||
const fileName = ref(uploadFileName.value);
|
||||
const showEdit = ref(false);
|
||||
|
||||
// Computed
|
||||
const isDisabled = computed(() => {
|
||||
// 1. 長度一樣,強制每一個都要選
|
||||
// 2. 不為 null undefind
|
||||
const hasValue = !selectedColumns.value.includes(undefined);
|
||||
const result = !(selectedColumns.value.length === uploadDetail.value?.columns.length
|
||||
&& informData.value.length === 0 && repeatedData.value.length === 0 && hasValue);
|
||||
return result;
|
||||
});
|
||||
|
||||
// Watch
|
||||
watch(selectedColumns, (newVal) => {
|
||||
updateValidationData(newVal);
|
||||
}, { deep: true });
|
||||
|
||||
// Methods
|
||||
/**
|
||||
* Rename 離開 input 的行為
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
function onBlur(e) {
|
||||
const baseWidth = 20;
|
||||
|
||||
if(e.target.value === '') {
|
||||
e.target.value = uploadFileName.value;
|
||||
const textWidth = getTextWidth(e.target.value, e.target);
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
}else if(e.target.value !== e.target.value.trim()) {
|
||||
e.target.value = e.target.value.trim();
|
||||
const textWidth = getTextWidth(e.target.value, e.target);
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename 輸入 input 的行為
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
function onInput(e) {
|
||||
const baseWidth = 20;
|
||||
const textWidth = getTextWidth(e.target.value, e.target);
|
||||
|
||||
e.target.style.width = baseWidth + textWidth + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* input 寬度隨著 value 響應式改變
|
||||
* @param {String} text file's name
|
||||
* @param {Event} e input 傳入的事件
|
||||
*/
|
||||
function getTextWidth(text, e) {
|
||||
// 替換空格為不斷行的空格
|
||||
const processedText = text.replace(/ /g, '\u00a0');
|
||||
const hiddenSpan = document.createElement('span');
|
||||
|
||||
hiddenSpan.innerHTML = processedText;
|
||||
hiddenSpan.style.font = window.getComputedStyle(e).font;
|
||||
hiddenSpan.style.visibility = 'hidden';
|
||||
document.body.appendChild(hiddenSpan);
|
||||
const width = hiddenSpan.getBoundingClientRect().width;
|
||||
document.body.removeChild(hiddenSpan);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證,根據新的 selectedColumns 更新 informData 和 repeatedData
|
||||
* @param {Array} data 已選擇的 type 的 data
|
||||
*/
|
||||
function updateValidationData(data) {
|
||||
const nameOccurrences = {};
|
||||
const noSortedRepeatedData = []; // 未排序的重複選擇的 data
|
||||
const selectedData = [] // 已經選擇的 data
|
||||
|
||||
informData.value = []; // 尚未選擇的 data
|
||||
repeatedData.value = []; // 重複選擇的 data
|
||||
|
||||
data.forEach(item => {
|
||||
const { name, code } = item;
|
||||
|
||||
if(nameOccurrences[name]) {
|
||||
// 'Not Assigned'、'Case Attribute' 不列入驗證
|
||||
if(!code || code === 'case_attributes') return;
|
||||
nameOccurrences[name]++;
|
||||
// 重複的選項只出現一次
|
||||
if(nameOccurrences[name] === 2){
|
||||
noSortedRepeatedData.push(item)
|
||||
}
|
||||
// 要按照選單的順序排序
|
||||
repeatedData.value = columnType.filter(column => noSortedRepeatedData.includes(column));
|
||||
}else {
|
||||
nameOccurrences[name] = 1;
|
||||
selectedData.push(name);
|
||||
informData.value = columnType.filter(item => item.required ? !selectedData.includes(item.name) : false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Button
|
||||
*/
|
||||
function reset() {
|
||||
// 路徑不列入歷史紀錄
|
||||
selectedColumns.value = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel Button
|
||||
*/
|
||||
function cancel() {
|
||||
// 路徑不列入歷史紀錄
|
||||
router.push({name: 'Files', replace: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload Button
|
||||
*/
|
||||
async function submit() {
|
||||
// Post API Data
|
||||
const fetchData = {
|
||||
timestamp: '',
|
||||
case_id: '',
|
||||
name: '',
|
||||
instance: '',
|
||||
status: '',
|
||||
case_attributes: []
|
||||
};
|
||||
// 給值
|
||||
const haveValueData = selectedColumns.value.map((column, i) => {
|
||||
if (column && uploadDetail.value.columns[i]) {
|
||||
return {
|
||||
name: column.name,
|
||||
code: column.code,
|
||||
color: column.color,
|
||||
value: uploadDetail.value.columns[i]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 取得欲更改的檔名,
|
||||
uploadFileName.value = fileName.value;
|
||||
// 設定第二階段上傳的 data
|
||||
haveValueData.forEach(column => {
|
||||
if(column !== undefined) {
|
||||
switch (column.code) {
|
||||
case 'timestamp':
|
||||
fetchData.timestamp = column.value;
|
||||
break;
|
||||
case 'case_id':
|
||||
fetchData.case_id = column.value;
|
||||
break;
|
||||
case 'name':
|
||||
fetchData.name = column.value;
|
||||
break;
|
||||
case 'instance':
|
||||
fetchData.instance = column.value;
|
||||
break;
|
||||
case 'status':
|
||||
fetchData.status = column.value;
|
||||
break;
|
||||
case 'case_attributes':
|
||||
fetchData.case_attributes.push(column.value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
uploadConfirm(fetchData);
|
||||
}
|
||||
|
||||
// Mounted
|
||||
onMounted(async () => {
|
||||
// 只監聯第一次
|
||||
const unwatch = watch(fileName, (newValue) => {
|
||||
if (newValue) {
|
||||
const inputElement = document.getElementById('fileNameInput');
|
||||
const baseWidth = 20;
|
||||
const textWidth = getTextWidth(fileName.value, inputElement);
|
||||
inputElement.style.width = baseWidth + textWidth + 'px';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
showEdit.value = true;
|
||||
if(uploadId.value) await filesStore.getUploadDetail();
|
||||
selectedColumns.value = await Array.from({ length: uploadDetail.value.columns.length }, () => columnType[columnType.length - 1]); // 預設選 Not Assigned
|
||||
unwatch();
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 離開頁面要刪 uploadID
|
||||
uploadId.value = null;
|
||||
uploadFileName.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,14 @@ vi.mock('@/module/apiError.js', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockRoute = vi.hoisted(() => ({
|
||||
query: {},
|
||||
}));
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => mockRoute,
|
||||
}));
|
||||
|
||||
import Login from '@/views/Login/Login.vue';
|
||||
import { useLoginStore } from '@/stores/login';
|
||||
|
||||
@@ -15,18 +23,16 @@ describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
mockRoute.query = {};
|
||||
});
|
||||
|
||||
const mountLogin = (options = {}) => {
|
||||
if (options.route?.query) {
|
||||
Object.assign(mockRoute.query, options.route.query);
|
||||
}
|
||||
return mount(Login, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
mocks: {
|
||||
$route: {
|
||||
query: {},
|
||||
...options.route,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user