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>