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

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

View File

@@ -16,7 +16,7 @@
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div @dblclick="onAcctDoubleClick(slotProps.data.username)" class="account-cell cursor-pointer
<div @dblclick="onAcctDoubleClick(slotProps.data.username)" class="account-cell cursor-pointer
whitespace-nowrap overflow-hidden text-ellipsis">
{{ slotProps.data.username }}
</div>
@@ -63,10 +63,10 @@
@click="setIsActiveInput(slotProps.data, false)"/>
<img v-else src="@/assets/radioOff.svg" alt="Radio Off" class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, true)"/>
</div>
</div>
</div>
<div v-else @mouseenter="handleRowMouseOver(slotProps.data.username)" @mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"></div>
class="row-container flex-w-full-hoverable flex w-full h-6"></div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Detail')" bodyClass="text-neutral-500" headerClass="header-center">
@@ -88,7 +88,7 @@
@mouseover="handleEditMouseOver(slotProps.data.username)"
@mouseout="handleEditMouseOut(slotProps.data.username)"
/>
</div>
</div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Delete')" bodyClass="text-neutral-500 flex justify-center" headerClass="header-center">
@@ -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,304 +128,193 @@ 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 allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList);
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
const loginUserData = ref(null);
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccoutList);
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
const loginUserData = ref(null);
const inputQuery = ref('');
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const fetchLoginUserData = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
};
const inputQuery = ref('');
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 按鈕可以置中,所以讓欄位的文字也置中 */
.header-center .p-column-header-content{
justify-content: center;
}
</style>
</style>

View File

@@ -2,7 +2,7 @@
<div id="modal_account_edit_or_create_new" class="w-[640px] bg-[#FFFFFF] rounded
shadow-lg flex flex-col">
<ModalHeader :headerText="modalTitle"/>
<main class="flex row min-h-[96px] my-[32px] flex-col px-6">
<div class="input-row w-full flex py-2 h-[40px] mb-4 items-center
justify-between">
@@ -12,7 +12,7 @@
</span>
</div>
<div class="input-and-error flex flex-col">
<input id="input_account_field"
<input id="input_account_field"
class="w-[454px] rounded p-1 border border-[1px] border-[#64748B] flex items-center h-[40px] outline-none"
v-model="inputUserAccount"
:class="{
@@ -23,14 +23,14 @@
:readonly="!isEditable" autocomplete="off"
@dblclick="onInputDoubleClick"
/>
<div v-show="!isAccountUnique" class="error-wrapper my-2">
<div v-show="!isAccountUnique" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="input-row w-full flex py-2 h-[40px] mb-4 items-center
@@ -108,7 +108,7 @@
}">
{{ i18next.t("AcctMgmt.ConfirmPassword") }}
</span>
<div v-show="false" class="input-and-toggle-btn w-[454px] flex flex-row rounded border border-[1px] relative items-center
h-[40px] mb-4"
:class="{
@@ -148,7 +148,7 @@
<IconChecked :isChecked="isSetAsAdminChecked" @click="toggleIsAdmin"/>
<span class="flex checkbox-text">
{{ i18next.t("AcctMgmt.SetAsAdmin") }}
</span>
</span>
</div>
<div class="checkbox-and-text flex">
<IconChecked :isChecked="isSetActivedChecked" @click="toggleIsActivated"/>
@@ -159,7 +159,7 @@
</section>
</div>
</main>
<footer class="flex row footer justify-end pr-[32px] pb-8">
<footer class="flex row footer justify-end pr-[32px] pb-8">
<button class="cancel-btn rounded-full w-[92px] h-10 px-2.5 py-6 border border-[1px] border-[#64748B]
flex justify-center items-center text-[#4E5969] font-medium
hover:bg-[#64748B] hover:border-[#CBD5E1] hover:text-[#FFFFFF]"
@@ -177,14 +177,13 @@
>
{{ i18next.t("Global.Confirm") }}
</button>
</footer>
</footer>
</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,228 +192,178 @@ 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 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 isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso);
const username = computed(() => acctMgmtStore.currentViewingUser.username);
const name = computed(() => acctMgmtStore.currentViewingUser.name);
// 自從加入這段 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 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 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 {
background-color: #ffffff;
backdrop-filter: opacity(1); /*防止子元件繼承父元件的透明度 */
}
</style>
</style>

View File

@@ -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,
}
}
</script>
onBeforeMount(async() => {
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
visitTime.value = currentViewingUser.value.detail.visits;
});
</script>

View File

@@ -7,11 +7,11 @@
<ModalAccountInfo v-if="whichModal === MODAL_ACCT_INFO"/>
<ModalDeleteAlert v-if="whichModal === MODAL_DELETE" />
</div>
</div>
</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,31 +23,12 @@
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 {
z-index: 9999;
background-color: rgba(254,254,254, 0.8);
}
</style>
</style>

View File

@@ -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,
};
},
});
</script>
const onNoBtnClick = () => {
modalStore.closeModal();
};
</script>

View File

@@ -7,28 +7,20 @@
<img src="@/assets/icon-x.svg" alt="X" class="flex cursor-pointer absolute"
@click="closeModal"
/>
</div>
</div>
</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,
};
}
}
</script>
defineProps({
headerText: {
type: String,
required: true,
}
});
const modalStore = useModalStore();
const { closeModal } = modalStore;
</script>

View File

@@ -27,13 +27,13 @@
</div>
<div class="account flex flex-col">
<span class="flex text-[#0F172A] text-[16px]">{{ username }}</span>
<div v-show="false" class="error-wrapper my-2">
<div v-show="false" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
</div>
</div>
</div>
</div>
@@ -87,23 +87,23 @@
<span class="error-msg-text flex text-[#FF3366] h-[24px] text-[14px]">
{{ isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough") }}
</span>
</div>
</div>
</div>
<div class="button-and-dummy-column flex flex-col text-[14px]">
<div class="button-group flex w-min-[80px] h-[40px]">
<div v-if='isPwdEditable' class="save-btn-container flex ml-[112px]"
@click="onSavePwdClick">
<ButtonFilled :buttonText='i18next.t("Global.Save")' />
</div>
</div>
<div v-if='isPwdEditable' class="cancel-btn btn-container flex ml-[8px]" @click="onCancelPwdClick" >
<Button :buttonText='i18next.t("Global.Cancel")'/>
</div>
<div v-else class="reset-btn btn-container flex ml-[460px]" @click="onResetPwdClick" >
<Button :buttonText='i18next.t("Global.Reset")'/>
</div>
</div>
</div>
<div class="flex dummy-cell h-[40px]"></div>
</div>
</div>
</div>
</main>
<main class="session flex flex-col pt-6 px-8 w-[720px]">
@@ -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;
}
}
</script>
};
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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
const props = defineProps({
chartData: {
type: Object,
},
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');
/**
* 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,
},
content: {
type: Object,
},
yUnit: {
type: String,
},
pageName: {
type: String,
},
},
setup(props) {
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,
};
}
}) => {
let resultScaleObj;
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
return resultScaleObj;
};
</script>
/**
* 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

View File

@@ -71,7 +71,7 @@
<span v-show="secondaryDragData.length > 0" class="material-symbols-outlined material-fill bg-neutral-10 text-neutral-500 block rounded-full absolute -top-[5%] -right-[5%] cursor-pointer hover:text-danger" @click="secondaryDragDelete
">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 -->
@@ -117,7 +117,7 @@
</div>
<!-- All Files type of grid -->
<ul>
<draggable tag="ul" :list="compareData" :group="{ name: 'files' }" itemKey="name" class="flex justify-start items-start gap-4
<draggable tag="ul" :list="compareData" :group="{ name: 'files' }" itemKey="name" class="flex justify-start items-start gap-4
flex-wrap overflow-y-scroll overflow-x-hidden max-h-[calc(100vh_-_440px)] scrollbar" id="compareGridCards">
<template #item="{ element, index }">
<li class="w-[216px] h-[168px] p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300
@@ -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";

View File

@@ -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>

View File

@@ -5,86 +5,21 @@
<Navbar/>
</header>
<main id='loading_and_router_view_container_in_maincontainer' class="w-full">
<Loading v-if="loadingStore.isLoading" />
<Loading v-if="loadingStore.isLoading" />
<router-view></router-view>
</main>
</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) {
@@ -101,8 +36,8 @@ 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,42 +56,79 @@ export default {
next({
path: '/login',
query: {
// 記憶未來登入後要進入的網址且記憶的時候要用base64編碼包裹住
'return-to': btoa(window.location.href),
}
});
}
} else {
} else {
next();
}
},
// 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>