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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user