fix: modal inputs 無法編輯問題
This commit is contained in:
parent
29fd70a7fd
commit
4db268d86b
@ -1,197 +1,206 @@
|
||||
<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 { data } from "autoprefixer";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
> 6 頁 => 會有 input 跳頁,且前三後三顯示
|
||||
> 規格:
|
||||
- pageSize:每頁筆數
|
||||
- totalPages:外部可強制指定總頁數(可選)
|
||||
- dataSource:完整資料陣列(當未提供 onPageChange 時,會在內部分頁切片)
|
||||
- totalItems:顯示用總筆數(未提供則使用 dataSource.length)
|
||||
- sort:當排序變更時重算頁面
|
||||
- onPageChange(page: number):外部受控回呼(若提供,內部不切片,只回呼)
|
||||
---------------------------------------------------------------- */
|
||||
const props = defineProps({
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
}, // 幾頁為一組
|
||||
totalPages: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
onPageChange: Function, // 用來接收目前返回的頁數
|
||||
dataSource: Array,
|
||||
pageSize: { type: Number, default: 10 },
|
||||
totalPages: { type: Number, default: 0 },
|
||||
onPageChange: Function,
|
||||
dataSource: { type: Array, default: () => [] },
|
||||
totalItems: Number,
|
||||
sort: Object,
|
||||
sort: { type: Object, default: null },
|
||||
});
|
||||
|
||||
const current_table_data = inject("current_table_data");
|
||||
const currentPage = ref(0);
|
||||
const totalPage = ref(0);
|
||||
const emit = defineEmits(["page-change"]);
|
||||
|
||||
const beforeInputPage = computed(() => {
|
||||
if (totalPage.value > 6) {
|
||||
return 3;
|
||||
} else {
|
||||
return totalPage.value;
|
||||
}
|
||||
// 可能由上層 Table 透過 provide 注入(可選)
|
||||
const current_table_data = inject("current_table_data", null);
|
||||
|
||||
const currentPage = ref(1);
|
||||
|
||||
/** 總頁數:優先用 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) => {
|
||||
currentPage.value = parseInt(page);
|
||||
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,
|
||||
}
|
||||
);
|
||||
/** 首段頁碼(總頁數>6 顯示 1..3,否則顯示 1..totalPage) */
|
||||
const beforeInputPage = computed(() => (totalPage.value > 6 ? 3 : totalPage.value));
|
||||
|
||||
/** Input 欄位的顯示值:只有在中間區間才回填目前頁碼 */
|
||||
const pageInput = computed(() => {
|
||||
if (currentPage.value > 3 && currentPage.value < totalPage.value - 2) {
|
||||
return currentPage.value;
|
||||
} else {
|
||||
return "";
|
||||
return String(currentPage.value);
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex justify-end items-end my-5"
|
||||
v-if="dataSource.length > 0"
|
||||
>
|
||||
<div class="relative flex justify-end items-end my-5" v-if="dataSource.length > 0 || totalPage > 0">
|
||||
<!-- 上一頁 -->
|
||||
<span class="mx-1">
|
||||
<button
|
||||
type="button"
|
||||
class="prev focus:border-0 disabled:text-gray-500 hover:text-warning"
|
||||
:disabled="currentPage === 1"
|
||||
@click="
|
||||
() => {
|
||||
choosePage(currentPage - 1 > 0 ? currentPage - 1 : 1);
|
||||
}
|
||||
"
|
||||
@click="choosePage(currentPage - 1)"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'chevron-left']" class="text-3xl" />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<!-- 前三頁(或全部,若總頁數 <= 6) -->
|
||||
<ul class="flex items-center list-none">
|
||||
<li
|
||||
v-for="page in beforeInputPage"
|
||||
:key="`page${page}`"
|
||||
:class="
|
||||
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'
|
||||
)
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
choosePage(page);
|
||||
}
|
||||
"
|
||||
@click="choosePage(page)"
|
||||
:aria-current="currentPage === page ? 'page' : undefined"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
@keydown.enter.prevent="choosePage(page)"
|
||||
@keydown.space.prevent="choosePage(page)"
|
||||
>
|
||||
<span class="text-white font-extrabold italic">
|
||||
{{ page }}
|
||||
</span>
|
||||
<span class="text-white font-extrabold italic">{{ page }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span
|
||||
v-if="totalPage < 6"
|
||||
class="absolute -bottom-8 text-base text-center"
|
||||
>
|
||||
{{ dataSource.length }} {{ $t("table.in_otal") }}</span
|
||||
>
|
||||
<label
|
||||
v-if="totalPage > 6"
|
||||
class="mx-2 relative input border-2 border-sub-success flex items-center gap-2"
|
||||
>
|
||||
<!-- 總筆數(總頁數 <= 6 顯示簡版) -->
|
||||
<span v-if="totalPage <= 6" class="absolute -bottom-8 text-base text-center">
|
||||
{{ totalItems || dataSource.length }} {{ $t('table.in_otal') }}
|
||||
</span>
|
||||
|
||||
<!-- 跳頁輸入(總頁數 > 6) -->
|
||||
<label v-if="totalPage > 6" class="mx-2 relative input border-2 border-sub-success flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="6"
|
||||
class="bg-transparent h-full w-20 font-extrabold italic text-lg"
|
||||
:placeholder="t('table.skip_to')"
|
||||
:value="pageInput"
|
||||
@change="
|
||||
(e) => {
|
||||
choosePage(e.target.value);
|
||||
}
|
||||
"
|
||||
@change="handleInputChange"
|
||||
aria-label="Go to page"
|
||||
/>
|
||||
<span
|
||||
><font-awesome-icon
|
||||
:icon="['fas', 'search']"
|
||||
class="text-xl text-sub-success"
|
||||
/></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
|
||||
>
|
||||
<span>
|
||||
<font-awesome-icon :icon="['fas', 'search']" class="text-xl text-sub-success" />
|
||||
</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>
|
||||
<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
|
||||
v-for="(page, index) in 3"
|
||||
:key="`page${totalPage - index}`"
|
||||
:key="`tail${index}`"
|
||||
:class="
|
||||
twMerge(
|
||||
'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center',
|
||||
currentPage === totalPage - index
|
||||
? 'bg-sub-success'
|
||||
: 'bg-transparent'
|
||||
'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) ? 'bg-sub-success' : 'bg-transparent'
|
||||
)
|
||||
"
|
||||
@click.stop.prevent="
|
||||
() => {
|
||||
choosePage(totalPage - index);
|
||||
}
|
||||
"
|
||||
@click.stop.prevent="choosePage(totalPage - index)"
|
||||
:aria-current="currentPage === (totalPage - index) ? 'page' : undefined"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
@keydown.enter.prevent="choosePage(totalPage - index)"
|
||||
@keydown.space.prevent="choosePage(totalPage - index)"
|
||||
>
|
||||
<span class="text-white font-extrabold italic">
|
||||
{{ totalPage - index }}
|
||||
</span>
|
||||
<span class="text-white font-extrabold italic">{{ totalPage - index }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 下一頁 -->
|
||||
<span class="mx-1">
|
||||
<button
|
||||
type="button"
|
||||
class="next focus:border-0 disabled:text-gray-500 hover:text-warning"
|
||||
:disabled="currentPage === totalPage"
|
||||
@click="
|
||||
() => {
|
||||
choosePage(
|
||||
currentPage + 1 <= totalPage ? currentPage + 1 : totalPage
|
||||
);
|
||||
}
|
||||
"
|
||||
@click="choosePage(currentPage + 1)"
|
||||
aria-label="Next page"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'chevron-right']" class="text-3xl" />
|
||||
</button>
|
||||
|
@ -106,7 +106,7 @@ watch(locale, () => {
|
||||
<img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" />
|
||||
新創賦能
|
||||
</router-link>
|
||||
<!-- <NavbarBuilding class="hidden lg:block ms-8" /> -->
|
||||
<NavbarBuilding class="hidden lg:block ms-8" />
|
||||
</div>
|
||||
<div class="hidden flex-1 lg:block">
|
||||
<NavbarItem
|
||||
|
@ -1,51 +1,89 @@
|
||||
<script setup>
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useUserInfoStore();
|
||||
|
||||
const user = ref("");
|
||||
const open = ref(false);
|
||||
|
||||
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(() => {
|
||||
const name = store.user.user_name;
|
||||
if (name) {
|
||||
user.value = name;
|
||||
}
|
||||
const name = store.user?.user_name;
|
||||
if (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>
|
||||
|
||||
<template>
|
||||
<div class="dropdown dropdown-bottom dropdown-end">
|
||||
<!-- 用 open 狀態加上 dropdown-open,避免 focus 失效問題 -->
|
||||
<div class="dropdown dropdown-bottom dropdown-end" :class="{ 'dropdown-open': open }">
|
||||
<button
|
||||
tabindex="0"
|
||||
type="link"
|
||||
type="button"
|
||||
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
|
||||
:icon="['fas', 'user-circle']"
|
||||
class="text-lg lg:text-2xl mb-1"
|
||||
/>
|
||||
<span class="text-xs lg:text-sm block"> {{ user || "webUser" }}</span>
|
||||
<font-awesome-icon :icon="['fas', 'user-circle']" class="text-lg lg:text-2xl mb-1" />
|
||||
<span class="text-xs lg:text-sm block">{{ user || "webUser" }}</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
tabindex="0"
|
||||
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">
|
||||
<a
|
||||
<li class="text-white" role="none">
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col justify-center items-center"
|
||||
role="menuitem"
|
||||
@click="logout"
|
||||
>{{ $t("sign_out") }}
|
||||
</a>
|
||||
>
|
||||
{{ $t('sign_out') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -108,7 +108,6 @@ watch(
|
||||
JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
||||
) {
|
||||
getData(newValue);
|
||||
getElecUseDayData(newValue);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user