Files
lucia-frontend/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue
2024-08-14 09:59:09 +08:00

429 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex justify-center pt-2">
<div class="flex w-[1216px] flex-col items-center">
<div class="flex w-full justify-between py-2 items-center">
<button id="create_new_acct_btn" class="flex rounded-full bg-primary text-[#ffffff] flex justify-center
items-center px-6 py-[10px]" @click="onCreateNewClick">
{{ i18next.t("AcctMgmt.CreateNew") }}
</button>
<SearchBar @on-search-account-button-click="onSearchAccountButtonClick"/>
</div>
<div id="acct_mgmt_data_grid" class="flex w-full overflow-y-auto h-[570px]" @scroll="handleScroll">
<DataTable :value="accountSearchResults" dataKey="username" tableClass="w-full mt-4 text-sm relative table-fixed"
:rowClass="getRowClass"
>
<Column field="username" :header="i18next.t('AcctMgmt.Account')" bodyClass="font-medium" sortable>
<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
whitespace-nowrap overflow-hidden text-ellipsis">
{{ slotProps.data.username }}
</div>
<span id="just_create_badge" class="flex ml-4"
v-if="isOneAccountJustCreate && slotProps.data.username === justCreateUsername">
<img src="@/assets/icon-new.svg" alt="New">
</span>
</div>
</template>
</Column>
<Column field="name" :header="i18next.t('AcctMgmt.FullName')" bodyClass="text-neutral-500" sortable>
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div class="fullname-cell whitespace-nowrap overflow-hidden text-ellipsis">
{{ slotProps.data.name }}
</div>
</div>
</template>
</Column>
<Column field="is_admin" :header="i18next.t('AcctMgmt.AdminRights')" bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable flex w-full justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<img v-if="slotProps.data.is_admin" src="@/assets/radioOn.svg" alt="Radio On" class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, false)"
/>
<img v-else src="@/assets/radioOff.svg" alt="Radio Off" class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, true)"
/>
</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>
</template>
</Column>
<Column field="is_active" :header="i18next.t('AcctMgmt.AccountActivation')" bodyClass="text-neutral-500"
headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div class="w-full flex justify-center">
<img v-if="slotProps.data.is_active" src="@/assets/radioOn.svg" alt="Radio On" class="btn-activate cursor-pointer flex"
@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 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>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Detail')" bodyClass="text-neutral-500" headerClass="header-center">
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<img :src="slotProps.data.isDetailHovered ? iconDetailOn : iconDetailOff" alt="Detail" class="btn-detail cursor-pointer" @click="onDetailBtnClick(slotProps.data.username)"
@mouseover="handleDetailMouseOver(slotProps.data.username)"
@mouseout="handleDetailMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Edit')" bodyClass="text-neutral-500" headerClass="header-center">
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)">
<img :src="slotProps.data.isEditHovered ? iconEditOn : iconEditOff" alt="Edit" class="btn-edit cursor-pointer"
@click="onEditButtonClick(slotProps.data.username)"
@mouseover="handleEditMouseOver(slotProps.data.username)"
@mouseout="handleEditMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Delete')" bodyClass="text-neutral-500 flex justify-center" headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)">
<img :src="slotProps.data.isDeleteHovered ? iconDeleteRed : iconDeleteGray"
:alt="slotProps.data.isDeleteHovered ? 'hovered' : 'not-hovered'"
class="delete-account cursor-pointer flex"
@mouseover="handleDeleteMouseOver(slotProps.data.username)"
@mouseout="handleDeleteMouseOut(slotProps.data.username)"
@click="onDeleteBtnClick(slotProps.data.username)"
>
</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>
</template>
</Column>
</DataTable>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, watch, } from 'vue';
import { mapState, mapActions, } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import { useModalStore } from '@/stores/modal.js';
import useAcctMgmtStore from '@/stores/acctMgmt.ts';
import piniaLoginStore from '@/stores/login.ts';
import SearchBar from '../../../components/AccountMenu/SearchBar.vue';
import i18next from '@/i18n/i18n.js';
import { useToast } from 'vue-toast-notification';
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
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';
export default {
setup() {
const toast = useToast();
const acctMgmtStore = useAcctMgmtStore();
const loadingStore = LoadingStore();
const modalStore = useModalStore();
const loginStore = piniaLoginStore();
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 isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const inputQuery = ref('');
const fetchLoginUserData = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
};
const moveJustCreateUserToFirstRow = () => {
if(infiniteAcctData.value && infiniteAcctData.value.length){
const index = acctMgmtStore.allUserAccoutList.findIndex(user => user.username === acctMgmtStore.justCreateUsername);
if (index !== -1) {
// 移除匹配的對象(剛剛新增的使用者)並將其插入到陣列的第一位
const [justCreateUser] = acctMgmtStore.allUserAccoutList[index];
infiniteAcctData.value.unshift(justCreateUser);
}
}
};
const accountSearchResults = computed(() => {
if(!inputQuery.value) {
return infiniteAcctData.value;
}
return acctMgmtStore.allUserAccoutList.filter (user => user.username.includes(inputQuery.value));
});
const onCreateNewClick = () => {
acctMgmtStore.clearCurrentViewingUser();
modalStore.openModal(MODAL_CREATE_NEW);
};
const onAcctDoubleClick = (username) => {
acctMgmtStore.setCurrentViewingUser(username);
modalStore.openModal(MODAL_ACCT_INFO);
}
const handleDeleteMouseOver = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
};
const handleDeleteMouseOut = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
};
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);
};
const handleDetailMouseOver = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
};
const handleDetailMouseOut = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(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() {
},
};
</script>
<style>
/*為了讓 radio 按鈕可以置中,所以讓欄位的文字也置中 */
.header-center .p-column-header-content{
justify-content: center;
}
</style>