1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-05 23:32:45 +08:00

feat(ImageViewer): enhance image viewer with button group navigation

This commit is contained in:
2026-02-25 23:24:46 +08:00
parent cad4130789
commit 68448d29fe
2 changed files with 61 additions and 126 deletions

View File

@@ -86,7 +86,7 @@ const show = () => {
// 延迟一帧触发动画,确保 CSS 过渡生效
isAnimating.value = true;
// 将焦点转移到查看器内部的关闭按钮
const closeBtn = document.querySelector<HTMLElement>(".btn-close");
const closeBtn = document.querySelector<HTMLElement>(".ImageViewer .close");
closeBtn?.focus();
});
};
@@ -113,8 +113,32 @@ const hide = () => {
const prevImage = () => hasPrevious.value && activeIndex.value--;
const nextImage = () => hasNext.value && activeIndex.value++;
/** 键盘交互处理 */
useEventListener("keydown", (e: KeyboardEvent) => {
/** 导航 ButtonGroup 按钮配置 */
const BUTTONS_NAV_CONFIG = computed(() => [
{ id: "prev", icon: "chevron_left", ariaLabel: "上一张", type: "normal" },
{
id: "index",
label: `${activeIndex.value + 1} / ${props.images.length}`,
color: "tonal",
ariaLabel: "当前页码",
},
{ id: "next", icon: "chevron_right", ariaLabel: "下一张", type: "normal" },
]);
/** 处理按钮组点击事件 */
const handleButtonGroupClick = (e: Event, item: any) => {
switch (item.id) {
case "prev":
prevImage();
break;
case "next":
nextImage();
break;
}
};
/** 处理键盘快捷键 */
const handleKeyboardShortcuts = (e: KeyboardEvent) => {
if (!isVisible.value) return;
// 陷阱焦点逻辑
@@ -146,7 +170,10 @@ useEventListener("keydown", (e: KeyboardEvent) => {
imageScale.value = Math.max(ZOOM_CONFIG.MIN, imageScale.value - ZOOM_CONFIG.STEP);
break;
}
});
};
/** 键盘交互处理 */
useEventListener("keydown", handleKeyboardShortcuts);
/** 滚轮缩放与翻页 */
const handleWheel = (e: WheelEvent) => {
@@ -202,7 +229,7 @@ const handleTouchMove = (e: TouchEvent) => {
const dist = Math.sqrt(dx * dx + dy * dy);
imageScale.value = Math.min(
Math.max((dist / initialTouchDist) * initialTouchScale, ZOOM_CONFIG.MIN),
ZOOM_CONFIG.MAX_TOUCH
ZOOM_CONFIG.MAX_TOUCH,
);
} else if (e.touches.length === 1) {
const touch = e.touches[0];
@@ -234,23 +261,15 @@ defineExpose({ show, hide });
aria-modal="true"
tabindex="-1"
>
<MaterialButton @click="hide" class="btn-close" icon="close" color="text" size="m" aria-label="关闭" />
<MaterialButton
v-if="hasPrevious"
@click="prevImage"
class="btn-nav prev"
icon="chevron_left"
size="l"
aria-label="上一张"
/>
<MaterialButton
v-if="hasNext"
@click="nextImage"
class="btn-nav next"
icon="chevron_right"
size="l"
aria-label="下一张"
<ButtonGroup
:links="BUTTONS_NAV_CONFIG"
layout="horizontal"
size="m"
class="nav-group"
@click="handleButtonGroupClick"
/>
<MaterialButton color="tonal" icon="close" aria-label="关闭" class="close" @click="hide"></MaterialButton>
<div
class="content"
@click.self="hide"
@@ -280,18 +299,6 @@ defineExpose({ show, hide });
}"
/>
</div>
<p class="index-text">{{ activeIndex + 1 }} / {{ images.length }}</p>
<div class="thumbnails" v-if="images.length > 1">
<button
v-for="(img, idx) in images"
:key="idx"
class="thumbnail"
:class="{ active: idx === activeIndex }"
@click="activeIndex = idx"
>
<img :src="img" alt="thumbnail" />
</button>
</div>
</div>
</template>

View File

@@ -22,47 +22,26 @@
-moz-user-select: none;
z-index: 9999;
.index-text {
padding-block: 3px;
padding-inline: 9px;
.nav-group {
position: absolute;
bottom: 0px;
color: var(--md-sys-color-surface-variant);
border-radius: var(--md-sys-shape-corner-medium);
background-color: var(--md-sys-color-on-surface-variant);
margin-block-end: 12px;
z-index: 3;
}
.btn-close,
.btn-nav {
display: flex;
align-items: center;
justify-content: center;
.close {
position: absolute;
right: 0px;
top: 0px;
position: absolute !important;
margin-inline-end: 12px;
margin-block-start: 12px;
z-index: 3;
}
.btn-close {
right: 20px;
top: 20px;
}
.btn-nav {
top: 50%;
&.prev {
left: 20px;
}
&.next {
right: 20px;
}
}
.content {
display: flex;
align-items: center;
@@ -74,70 +53,21 @@
padding-block-start: 5vh;
z-index: 2;
}
.content-image {
border-radius: var(--md-sys-shape-corner-full);
.content-image {
background-color: var(--md-sys-color-surface-variant);
background-color: var(--md-sys-color-surface-variant);
clip-path: circle(10%);
object-fit: contain;
transition: var(--md-sys-motion-spring-slow-spatial-duration) var(--md-sys-motion-spring-slow-spatial);
clip-path: circle(10%);
object-fit: contain;
transition: var(--md-sys-motion-spring-slow-spatial-duration) var(--md-sys-motion-spring-slow-spatial);
&.transitioning {
opacity: 0.6;
}
&.transitioning {
opacity: 0.6;
}
&.notransition {
transition: none !important;
}
}
.thumbnails {
display: flex;
gap: 8px;
max-width: calc(100% - 66px);
padding: 24px;
overflow-x: auto;
overflow-y: hidden;
z-index: 3;
}
.thumbnail {
flex-shrink: 0;
width: 66px;
height: 66px;
padding: 0px;
border: 0px;
border-radius: var(--md-sys-shape-corner-large);
cursor: pointer;
overflow: hidden;
transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
&.active {
@include mixin.focus-ring($thickness: 1, $offset: 2);
outline-color: var(--md-sys-color-on-surface-variant) !important;
transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
}
&:hover {
transform: scale(0.9);
}
img {
height: 100%;
width: 100%;
object-fit: cover;
&.notransition {
transition: none !important;
}
}
}
@@ -146,8 +76,6 @@
opacity: 1;
.content-image {
border-radius: var(--md-sys-shape-corner-large);
clip-path: circle(100%);
}
}