fix: modal inputs 無法編輯問題

This commit is contained in:
MJM_2025_05\polly 2025-09-24 15:38:34 +08:00
parent 29fd70a7fd
commit 4db268d86b
4 changed files with 198 additions and 152 deletions

View File

@ -1,197 +1,206 @@
<script setup> <script setup>
import { defineProps, ref, computed, inject, watch } from "vue"; import { defineProps, defineEmits, ref, computed, inject, watch } from "vue";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { data } from "autoprefixer";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
/* ------------------------------------------------------------- /* -------------------------------------------------------------
> 6 => 會有 input 跳頁且前三後三顯示 > 規格
- pageSize每頁筆數
- totalPages外部可強制指定總頁數可選
- dataSource完整資料陣列當未提供 onPageChange 會在內部分頁切片
- totalItems顯示用總筆數未提供則使用 dataSource.length
- sort當排序變更時重算頁面
- onPageChange(page: number)外部受控回呼若提供內部不切片只回呼
---------------------------------------------------------------- */ ---------------------------------------------------------------- */
const props = defineProps({ const props = defineProps({
pageSize: { pageSize: { type: Number, default: 10 },
type: Number, totalPages: { type: Number, default: 0 },
default: 10, onPageChange: Function,
}, // dataSource: { type: Array, default: () => [] },
totalPages: {
type: Number,
default: 0,
},
onPageChange: Function, //
dataSource: Array,
totalItems: Number, totalItems: Number,
sort: Object, sort: { type: Object, default: null },
}); });
const current_table_data = inject("current_table_data"); const emit = defineEmits(["page-change"]);
const currentPage = ref(0);
const totalPage = ref(0);
const beforeInputPage = computed(() => { // Table provide
if (totalPage.value > 6) { const current_table_data = inject("current_table_data", null);
return 3;
} else { const currentPage = ref(1);
return totalPage.value;
} /** 總頁數:優先用 props.totalPages否則用 dataSource.length 推導 */
const totalPage = computed(() => {
const ps = Math.max(1, props.pageSize || 1);
const derived = Math.ceil((props.dataSource?.length || 0) / ps);
return props.totalPages > 0 ? props.totalPages : derived;
}); });
const choosePage = (page) => { /** 首段頁碼(總頁數>6 顯示 1..3,否則顯示 1..totalPage */
currentPage.value = parseInt(page); const beforeInputPage = computed(() => (totalPage.value > 6 ? 3 : totalPage.value));
props.onPageChange
? props.onPageChange(parseInt(page))
: changePageData(parseInt(page));
};
//
const changePageData = (currentPage) => {
const start = (currentPage - 1) * props.pageSize;
const end = currentPage * props.pageSize;
const data = props.dataSource.slice(start, end);
current_table_data.updateDataSource(data);
};
watch(
() => [props.dataSource, props.sort],
([newVal, newVal2]) => {
const totalPageNumber =
props.totalPages || Math.ceil(props.dataSource.length / props.pageSize);
if (currentPage.value > totalPageNumber) {
currentPage.value = 1;
} else {
//
currentPage.value = currentPage.value;
}
totalPage.value = totalPageNumber;
props.onPageChange
? current_table_data.updateDataSource(props.dataSource)
: changePageData(currentPage.value || 1);
},
{
deep: true,
}
);
/** Input 欄位的顯示值:只有在中間區間才回填目前頁碼 */
const pageInput = computed(() => { const pageInput = computed(() => {
if (currentPage.value > 3 && currentPage.value < totalPage.value - 2) { if (currentPage.value > 3 && currentPage.value < totalPage.value - 2) {
return currentPage.value; return String(currentPage.value);
} else {
return "";
} }
return "";
}); });
/** 夾住頁碼到合法區間 */
const clampPage = (n) => {
if (!Number.isFinite(n)) return 1;
n = Math.floor(n);
return Math.min(Math.max(n, 1), Math.max(totalPage.value, 1));
};
/** 切頁核心:外部受控就呼叫 onPageChange否則本地切片並更新注入的資料 */
const choosePage = (page) => {
const next = clampPage(Number(page));
if (currentPage.value === next) return;
currentPage.value = next;
if (typeof props.onPageChange === "function") {
props.onPageChange(next);
} else {
changePageData(next);
}
emit("page-change", next);
};
/** 無 onPageChange 時,內部分頁切片 */
const changePageData = (page) => {
if (!Array.isArray(props.dataSource)) return;
const ps = Math.max(1, props.pageSize || 1);
const start = (page - 1) * ps;
const end = page * ps;
const sliced = props.dataSource.slice(start, end);
//
if (current_table_data?.updateDataSource) current_table_data.updateDataSource(sliced);
};
/** 監看資料長度、排序、每頁筆數、外部總頁數變化 */
watch(
() => [props.dataSource?.length, props.sort, props.pageSize, props.totalPages],
() => {
// 調
currentPage.value = clampPage(currentPage.value);
if (typeof props.onPageChange === "function") {
// data
if (current_table_data?.updateDataSource)
current_table_data.updateDataSource(props.dataSource || []);
} else {
//
changePageData(currentPage.value);
}
},
{ immediate: true }
);
/** 處理 input 變更:僅允許正整數 */
const handleInputChange = (e) => {
const v = String(e.target.value || "").trim();
const n = Number(v);
if (!/^\d+$/.test(v) || !Number.isFinite(n)) return; //
choosePage(n);
};
</script> </script>
<template> <template>
<div <div class="relative flex justify-end items-end my-5" v-if="dataSource.length > 0 || totalPage > 0">
class="relative flex justify-end items-end my-5" <!-- 上一頁 -->
v-if="dataSource.length > 0"
>
<span class="mx-1"> <span class="mx-1">
<button <button
type="button" type="button"
class="prev focus:border-0 disabled:text-gray-500 hover:text-warning" class="prev focus:border-0 disabled:text-gray-500 hover:text-warning"
:disabled="currentPage === 1" :disabled="currentPage === 1"
@click=" @click="choosePage(currentPage - 1)"
() => { aria-label="Previous page"
choosePage(currentPage - 1 > 0 ? currentPage - 1 : 1);
}
"
> >
<font-awesome-icon :icon="['fas', 'chevron-left']" class="text-3xl" /> <font-awesome-icon :icon="['fas', 'chevron-left']" class="text-3xl" />
</button> </button>
</span> </span>
<!-- 前三頁或全部若總頁數 <= 6 -->
<ul class="flex items-center list-none"> <ul class="flex items-center list-none">
<li <li
v-for="page in beforeInputPage" v-for="page in beforeInputPage"
:key="`page${page}`" :key="`page${page}`"
:class=" :class="
twMerge( twMerge(
'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center cursor-pointer', 'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center cursor-pointer select-none',
currentPage === page ? 'bg-sub-success' : 'bg-transparent' currentPage === page ? 'bg-sub-success' : 'bg-transparent'
) )
" "
@click=" @click="choosePage(page)"
() => { :aria-current="currentPage === page ? 'page' : undefined"
choosePage(page); role="button"
} :tabindex="0"
" @keydown.enter.prevent="choosePage(page)"
@keydown.space.prevent="choosePage(page)"
> >
<span class="text-white font-extrabold italic"> <span class="text-white font-extrabold italic">{{ page }}</span>
{{ page }}
</span>
</li> </li>
</ul> </ul>
<span <!-- 總筆數總頁數 <= 6 顯示簡版 -->
v-if="totalPage < 6" <span v-if="totalPage <= 6" class="absolute -bottom-8 text-base text-center">
class="absolute -bottom-8 text-base text-center" {{ totalItems || dataSource.length }} {{ $t('table.in_otal') }}
> </span>
{{ dataSource.length }} {{ $t("table.in_otal") }}</span
> <!-- 跳頁輸入總頁數 > 6 -->
<label <label v-if="totalPage > 6" class="mx-2 relative input border-2 border-sub-success flex items-center gap-2">
v-if="totalPage > 6"
class="mx-2 relative input border-2 border-sub-success flex items-center gap-2"
>
<input <input
type="text" type="text"
inputmode="numeric"
pattern="[0-9]*"
maxlength="6" maxlength="6"
class="bg-transparent h-full w-20 font-extrabold italic text-lg" class="bg-transparent h-full w-20 font-extrabold italic text-lg"
:placeholder="t('table.skip_to')" :placeholder="t('table.skip_to')"
:value="pageInput" :value="pageInput"
@change=" @change="handleInputChange"
(e) => { aria-label="Go to page"
choosePage(e.target.value);
}
"
/> />
<span <span>
><font-awesome-icon <font-awesome-icon :icon="['fas', 'search']" class="text-xl text-sub-success" />
:icon="['fas', 'search']" </span>
class="text-xl text-sub-success" <span class="w-full text-center absolute -bottom-8 left-1/2 -translate-x-1/2 text-base">
/></span> {{ totalItems || dataSource.length }} {{ $t('table.in_otal') }}
<span </span>
class="w-full text-center absolute -bottom-8 left-1/2 -translate-x-1/2 text-base"
>
{{ totalItems || dataSource.length }} {{ $t("table.in_otal") }}</span
>
</label> </label>
<ul
v-if="totalPage > 6" <!-- 後三頁倒序 -->
class="flex flex-row-reverse items-center list-none" <ul v-if="totalPage > 6" class="flex flex-row-reverse items-center list-none">
>
<li <li
v-for="(page, index) in 3" v-for="(page, index) in 3"
:key="`page${totalPage - index}`" :key="`tail${index}`"
:class=" :class="
twMerge( twMerge(
'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center', 'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center cursor-pointer select-none',
currentPage === totalPage - index currentPage === (totalPage - index) ? 'bg-sub-success' : 'bg-transparent'
? 'bg-sub-success'
: 'bg-transparent'
) )
" "
@click.stop.prevent=" @click.stop.prevent="choosePage(totalPage - index)"
() => { :aria-current="currentPage === (totalPage - index) ? 'page' : undefined"
choosePage(totalPage - index); role="button"
} :tabindex="0"
" @keydown.enter.prevent="choosePage(totalPage - index)"
@keydown.space.prevent="choosePage(totalPage - index)"
> >
<span class="text-white font-extrabold italic"> <span class="text-white font-extrabold italic">{{ totalPage - index }}</span>
{{ totalPage - index }}
</span>
</li> </li>
</ul> </ul>
<!-- 下一頁 -->
<span class="mx-1"> <span class="mx-1">
<button <button
type="button" type="button"
class="next focus:border-0 disabled:text-gray-500 hover:text-warning" class="next focus:border-0 disabled:text-gray-500 hover:text-warning"
:disabled="currentPage === totalPage" :disabled="currentPage === totalPage"
@click=" @click="choosePage(currentPage + 1)"
() => { aria-label="Next page"
choosePage(
currentPage + 1 <= totalPage ? currentPage + 1 : totalPage
);
}
"
> >
<font-awesome-icon :icon="['fas', 'chevron-right']" class="text-3xl" /> <font-awesome-icon :icon="['fas', 'chevron-right']" class="text-3xl" />
</button> </button>

View File

@ -106,7 +106,7 @@ watch(locale, () => {
<img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" /> <img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" />
新創賦能 新創賦能
</router-link> </router-link>
<!-- <NavbarBuilding class="hidden lg:block ms-8" /> --> <NavbarBuilding class="hidden lg:block ms-8" />
</div> </div>
<div class="hidden flex-1 lg:block"> <div class="hidden flex-1 lg:block">
<NavbarItem <NavbarItem

View File

@ -1,51 +1,89 @@
<script setup> <script setup>
import useUserInfoStore from "@/stores/useUserInfoStore"; import useUserInfoStore from "@/stores/useUserInfoStore";
import { ref, onMounted } from "vue"; import { ref, onMounted, onBeforeUnmount } from "vue";
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute();
const store = useUserInfoStore(); const store = useUserInfoStore();
const user = ref(""); const user = ref("");
const open = ref(false);
function logout() { function logout() {
router.push('/logout'); open.value = false;
router.push("/logout");
}
function toggleOpen() {
open.value = !open.value;
}
function close() {
open.value = false;
}
// //
function onClickOutside(e) {
//
// closest('.dropdown') dropdown
const target = e.target;
if (!target.closest(".dropdown")) {
close();
}
}
// Esc //
function onKeydown(e) {
if (e.key === "Escape") close();
} }
onMounted(() => { onMounted(() => {
const name = store.user.user_name; const name = store.user?.user_name;
if (name) { if (name) user.value = name;
user.value = name;
} document.addEventListener("click", onClickOutside, true);
document.addEventListener("keydown", onKeydown);
//
router.afterEach(() => close());
});
onBeforeUnmount(() => {
document.removeEventListener("click", onClickOutside, true);
document.removeEventListener("keydown", onKeydown);
}); });
</script> </script>
<template> <template>
<div class="dropdown dropdown-bottom dropdown-end"> <!-- open 狀態加上 dropdown-open避免 focus 失效問題 -->
<div class="dropdown dropdown-bottom dropdown-end" :class="{ 'dropdown-open': open }">
<button <button
tabindex="0" type="button"
type="link"
class="flex flex-col justify-center items-center btn-group text-white text-center" class="flex flex-col justify-center items-center btn-group text-white text-center"
aria-haspopup="menu"
:aria-expanded="open ? 'true' : 'false'"
@click="toggleOpen"
> >
<font-awesome-icon <font-awesome-icon :icon="['fas', 'user-circle']" class="text-lg lg:text-2xl mb-1" />
:icon="['fas', 'user-circle']"
class="text-lg lg:text-2xl mb-1"
/>
<span class="text-xs lg:text-sm block">{{ user || "webUser" }}</span> <span class="text-xs lg:text-sm block">{{ user || "webUser" }}</span>
</button> </button>
<ul <ul
tabindex="0"
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center" class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
role="menu"
> >
<li class="text-white"> <li class="text-white" role="none">
<a <button
type="button" type="button"
class="flex flex-col justify-center items-center" class="flex flex-col justify-center items-center"
role="menuitem"
@click="logout" @click="logout"
>{{ $t("sign_out") }} >
</a> {{ $t('sign_out') }}
</button>
</li> </li>
</ul> </ul>
</div> </div>
</template> </template>
<style lang="scss" scoped></style>

View File

@ -108,7 +108,6 @@ watch(
JSON.stringify(newValue) !== JSON.stringify(oldValue) JSON.stringify(newValue) !== JSON.stringify(oldValue)
) { ) {
getData(newValue); getData(newValue);
getElecUseDayData(newValue);
} }
}, },
{ {