vue3 infinite scroll

This commit is contained in:
Cindy Chang
2024-06-24 09:07:11 +08:00
parent 9b0d54bf5e
commit b55cc0a6d6
4 changed files with 90 additions and 46 deletions

View File

@@ -34,7 +34,8 @@
"ActivateNow": "Activate now.", "ActivateNow": "Activate now.",
"DelteQuestion": "Are you sure to delete ?", "DelteQuestion": "Are you sure to delete ?",
"DeleteFirstClause": "deletion is irreversible !", "DeleteFirstClause": "deletion is irreversible !",
"DeleteSecondClause": "You will delete all data and content on this account." "DeleteSecondClause": "You will delete all data and content on this account.",
"MsgAccountAdded": "Account added."
}, },
"Compare": { "Compare": {
"timeUsage": "Time Usage", "timeUsage": "Time Usage",

View File

@@ -5,7 +5,9 @@ export default defineStore('acctMgmtStore', {
state: () => ({ state: () => ({
allUserAccoutList: [], allUserAccoutList: [],
isAcctMenuOpen: false, isAcctMenuOpen: false,
currentViewingUser: {} currentViewingUser: {
detail: null,
}
}), }),
getters: { getters: {
}, },
@@ -85,10 +87,24 @@ export default defineStore('acctMgmtStore', {
try{ try{
const response = await this.$axios.post(apiCreateAccount, userToCreate); const response = await this.$axios.post(apiCreateAccount, userToCreate);
console.log('TODO: response:', response, response.status);
} }
catch(error) { catch(error) {
apiError(error, 'Failed to add a new account'); apiError(error, 'Failed to add a new account.');
};
},
/**
* Get user detail by unique username.
* @param {string} uniqueUsername
*/
async getUserDetail(uniqueUsername) {
const apiUserDetail = `/api/users/${uniqueUsername}`;
try{
const response = await this.$axios.get(apiUserDetail);
this.currentViewingUser.detail = response.data;
}
catch(error) {
apiError(error, 'Failed to get user detail.');
}; };
}, },
/** /**

View File

@@ -9,7 +9,7 @@
<SearchBar/> <SearchBar/>
</div> </div>
<div id="acct_mgmt_data_grid" class="flex w-full overflow-y-auto h-[570px]" @scroll="handleScroll"> <div id="acct_mgmt_data_grid" class="flex w-full overflow-y-auto h-[570px]" @scroll="handleScroll">
<DataTable :value="internalInfiniteAcctData" dataKey="username" tableClass="w-full mt-4 text-sm relative table-fixed" <DataTable :value="infiniteAcctData" dataKey="username" tableClass="w-full mt-4 text-sm relative table-fixed"
:rowClass="getRowClass" :rowClass="getRowClass"
> >
<Column field="username" :header="i18next.t('AcctMgmt.Account')" bodyClass="font-medium" sortable> <Column field="username" :header="i18next.t('AcctMgmt.Account')" bodyClass="font-medium" sortable>
@@ -149,7 +149,8 @@ export default {
const modalStore = useModalStore(); const modalStore = useModalStore();
const { isLoading } = storeToRefs(loadingStore); const { isLoading } = storeToRefs(loadingStore);
const loginStore = piniaLoginStore(); const loginStore = piniaLoginStore();
const internalInfiniteAcctData = ref([]); const infiniteAcctData = ref([]);
const infiniteStart = ref(0);
const loginUserData = ref(null); const loginUserData = ref(null);
const allUserAccoutList = computed(() => acctMgmtStore.allUserAccoutList); const allUserAccoutList = computed(() => acctMgmtStore.allUserAccoutList);
@@ -161,13 +162,13 @@ export default {
const moveCurrentLoginUserToFirstRow = () => { const moveCurrentLoginUserToFirstRow = () => {
const currentLoginUsername = loginUserData.value.username; const currentLoginUsername = loginUserData.value.username;
if(internalInfiniteAcctData.value && internalInfiniteAcctData.value.length){ if(infiniteAcctData.value && infiniteAcctData.value.length){
const index = internalInfiniteAcctData.value.findIndex(user => user.username === currentLoginUsername); const index = infiniteAcctData.value.findIndex(user => user.username === currentLoginUsername);
if (index !== -1) { if (index !== -1) {
// 移除匹配的對象(現正登入的使用者)並將其插入到陣列的第一位" // 移除匹配的對象(現正登入的使用者)並將其插入到陣列的第一位"
const [user] = internalInfiniteAcctData.value.splice(index, 1); const [user] = infiniteAcctData.value.splice(index, 1);
internalInfiniteAcctData.value.unshift(user); infiniteAcctData.value.unshift(user);
} }
} }
}; };
@@ -179,7 +180,7 @@ export default {
const getFirstPageUserData = async() => { const getFirstPageUserData = async() => {
await acctMgmtStore.getAllUserAccounts(); await acctMgmtStore.getAllUserAccounts();
internalInfiniteAcctData.value = allUserAccoutList.value.slice(0, ONCE_RENDER_NUM_OF_DATA) infiniteAcctData.value = allUserAccoutList.value.slice(0, ONCE_RENDER_NUM_OF_DATA)
}; };
const isInfiniteFinish = ref(true); const isInfiniteFinish = ref(true);
@@ -234,34 +235,45 @@ export default {
/** /**
* 無限滾動: 監聽 scroll 有沒有滾到底部 * 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event 滾動傳入的事件 * @param {element} event 滾動傳入的事件
scrollTop表示容器的垂直滾動位置。具體來說它是以像素為單位的數值
表示當前內容視窗(可見區域)的頂部距離整個可滾動內容的頂部的距離。
簡單來說scrollTop 指的是滾動條的位置當滾動條在最上面時scrollTop 為 0
當滾動條向下移動時scrollTop 會增加。
可是作為:我們目前已經滾動了多少。
clientHeight表示容器的可見高度不包括滾動條的高度。它是以像素為單位的數值
表示容器內部的可見區域的高度。
與 offsetHeight 不同的是clientHeight 不包含邊框、內邊距和滾動條的高度,只計算內容區域的高度。
scrollHeight表示容器內部的總內容高度。它是以像素為單位的數值
包括看不見的(需要滾動才能看到的)部分。
簡單來說scrollHeight 是整個可滾動內容的總高度,包括可見區域和需要滾動才能看到的部分。
*/ */
const handleScroll = (event) => { const handleScroll = (event) => {
if(internalInfiniteAcctData.value.length < ONCE_RENDER_NUM_OF_DATA || isInfiniteFinish.value === false) {
return;
}
const container = event.target; const container = event.target;
const smallValue = 4; const smallValue = 3;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue; const isOverScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue;
if(isOverScrollHeight){
if(overScrollHeight){
fetchMoreDataVue3(); fetchMoreDataVue3();
} }
}; };
const fetchMoreDataVue3 = () => { const fetchMoreDataVue3 = () => {
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA; infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
internalInfiniteAcctData.value = [internalInfiniteAcctData.value, acctMgmtStore.allUserAccoutList.slice( if(infiniteAcctData.value.length < acctMgmtStore.allUserAccoutList.length) {
infiniteStart.value, this.infiniteStart + ONCE_RENDER_NUM_OF_DATA)]; infiniteAcctData.value = [...infiniteAcctData.value, ...acctMgmtStore.allUserAccoutList.slice(
isInfiniteFinish.value = true; infiniteStart.value, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA)];
}
}; };
return { return {
isLoading, isLoading,
modalStore, modalStore,
loginUserData, loginUserData,
internalInfiniteAcctData, infiniteAcctData,
onCreateNewClick, onCreateNewClick,
handleScroll, handleScroll,
getRowClass, getRowClass,
@@ -286,7 +298,7 @@ export default {
return { return {
i18next: i18next, i18next: i18next,
repeatedAccountList: repeatedAccountList, repeatedAccountList: repeatedAccountList,
infiniteAcctData: [], infiniteAcctDataVue2: [],
infiniteStart: 0, infiniteStart: 0,
isInfiniteFinish: true, isInfiniteFinish: true,
isInfinitMaxItemsMet: false, isInfinitMaxItemsMet: false,
@@ -311,7 +323,7 @@ export default {
* @param {element} event 滾動傳入的事件 * @param {element} event 滾動傳入的事件
*/ */
handleScrollVue2(event) { handleScrollVue2(event) {
if(this.infinitMaxItems || this.infiniteAcctData.length < ONCE_RENDER_NUM_OF_DATA || this.isInfiniteFinish === false) { if(this.infinitMaxItems || this.infiniteAcctDataVue2.length < ONCE_RENDER_NUM_OF_DATA || this.isInfiniteFinish === false) {
return; return;
} }
@@ -332,7 +344,7 @@ export default {
this.infiniteFinish = false; this.infiniteFinish = false;
this.infiniteStart += ONCE_RENDER_NUM_OF_DATA; this.infiniteStart += ONCE_RENDER_NUM_OF_DATA;
// await this.acctMgmtStore.getAccountDetail(); // await this.acctMgmtStore.getAccountDetail();
this.infiniteAcctData = await [...this.infiniteAcctData, ...this.allUserAccoutList.slice( this.infiniteAcctDataVue2 = await [...this.infiniteAcctDataVue2, ...this.allUserAccoutList.slice(
this.infiniteStart, this.infiniteStart + ONCE_RENDER_NUM_OF_DATA)]; this.infiniteStart, this.infiniteStart + ONCE_RENDER_NUM_OF_DATA)];
this.isInfiniteFinish = true; this.isInfiniteFinish = true;
this.isLoading = false; this.isLoading = false;

View File

@@ -119,6 +119,8 @@ import { defineComponent, computed, ref, watch, } from 'vue';
import i18next from "@/i18n/i18n.js"; import i18next from "@/i18n/i18n.js";
import { mapActions, } from 'pinia'; import { mapActions, } from 'pinia';
import { useModalStore } from '@/stores/modal.js'; import { useModalStore } from '@/stores/modal.js';
import { useRouter } from 'vue-router';
import { useToast } from 'vue-toast-notification';
import useAcctMgmtStore from '@/stores/acctMgmt.js'; import useAcctMgmtStore from '@/stores/acctMgmt.js';
import ModalHeader from "./ModalHeader.vue"; import ModalHeader from "./ModalHeader.vue";
import IconChecked from "@/components/icons/IconChecked.vue"; import IconChecked from "@/components/icons/IconChecked.vue";
@@ -129,6 +131,9 @@ export default defineComponent({
const acctMgmtStore = useAcctMgmtStore(); const acctMgmtStore = useAcctMgmtStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
const router = useRouter();
const toast = useToast();
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser); const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const isPwdEyeOn = ref(false); const isPwdEyeOn = ref(false);
const isPwdConfirmEyeOn = ref(false); const isPwdConfirmEyeOn = ref(false);
@@ -184,7 +189,10 @@ export default defineComponent({
return validateResult && isPwdMatched.value; return validateResult && isPwdMatched.value;
} }
const onConfirmBtnClick = () => { const onConfirmBtnClick = async () => {
switch(whichCurrentModal.value) {
case MODAL_CREATE_NEW:
const validateResult = validateConfirmPwd(); const validateResult = validateConfirmPwd();
if(!validateResult){ if(!validateResult){
return; return;
@@ -198,13 +206,20 @@ export default defineComponent({
isSetAsAdminChecked.value, isSetAsAdminChecked.value,
isSetActivedChecked.value, isSetActivedChecked.value,
); );
acctMgmtStore.createNewAccount({ await acctMgmtStore.createNewAccount({
username: inputUserAccount.value, username: inputUserAccount.value,
password: inputPwd.value, password: inputPwd.value,
name: inputName.value, name: inputName.value,
is_admin: isSetAsAdminChecked.value, is_admin: isSetAsAdminChecked.value,
is_active: isSetActivedChecked.value, is_active: isSetActivedChecked.value,
}); });
await toast.success(i18next.t("AcctMgmt.MsgAccountAdded"));
await modalStore.closeModal();
await router.push('/account/account-admin');
break;
default:
break;
}
} }