442 lines
20 KiB
Vue
442 lines
20 KiB
Vue
<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 flex" @mouseenter="handleRowMouseOver(slotProps.data.username)"
|
||
@mouseout="handleRowMouseOut(slotProps.data.username)">
|
||
<div @dblclick="onAcctDoubleClick(slotProps.data.username)" class="account-cell cursor-pointer flex">
|
||
{{ 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">
|
||
{{ 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 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>
|
||
</template>
|
||
</Column>
|
||
<Column field="is_active" :header="i18next.t('AcctMgmt.AccountActivation')" bodyClass="text-neutral-500"
|
||
headerClass="header-center">
|
||
<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="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>
|
||
</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 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>
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, computed, onMounted, watch, } from 'vue';
|
||
import { storeToRefs, 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';
|
||
|
||
// 這是製作假資料,在正式環境不會用到
|
||
function repeatAccountList(accountList, N) {
|
||
const repeatedList = [];
|
||
|
||
for (let i = 0; i < N; i++) {
|
||
accountList.forEach(account => {
|
||
const newAccount = { ...account, id: account.id + i };
|
||
repeatedList.push(newAccount);
|
||
});
|
||
}
|
||
|
||
return repeatedList;
|
||
}
|
||
|
||
// 這是製作假資料,在正式環境不會用到
|
||
// const repeatedAccountList = repeatAccountList(accountList, 20);
|
||
|
||
export default {
|
||
setup() {
|
||
const toast = useToast();
|
||
const acctMgmtStore = useAcctMgmtStore();
|
||
const loadingStore = LoadingStore();
|
||
const modalStore = useModalStore();
|
||
const { isLoading } = storeToRefs(loadingStore);
|
||
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();
|
||
acctMgmtStore.setShouldUpdateList(false);
|
||
|
||
// 當夾帶有infiniteStart.value,就表示依然考慮到無限捲動的需求
|
||
infiniteAcctData.value = allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA);
|
||
moveJustCreateUserToFirstRow();
|
||
}
|
||
});
|
||
|
||
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 () => {
|
||
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,
|
||
isLoading,
|
||
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.isLoading = true;
|
||
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;
|
||
this.isLoading = false;
|
||
|
||
},
|
||
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> |