dma_handbook/docs/.vuepress/components/LongPicSplit.vue
2026-03-26 17:08:01 +08:00

250 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
src: { type: String, required: true },
alt: { type: String, default: '长图' },
chunkHeight: { type: Number, default: 1500 }
})
const imageList = ref([])
// 预览层
const showViewer = ref(false)
const currentIndex = ref(0)
const viewImg = ref('')
// 缩放
const scale = ref(1)
const startScale = ref(1)
const startDistance = ref(0)
// 滑动
const startX = ref(0)
// 打开预览
const openView = (idx) => {
currentIndex.value = idx
viewImg.value = imageList.value[idx]
showViewer.value = true
scale.value = 1
document.body.style.overflow = 'hidden'
}
// 关闭预览
const closeView = () => {
showViewer.value = false
document.body.style.overflow = ''
}
// 上一张
const prevImg = () => {
if (currentIndex.value > 0) {
currentIndex.value--
viewImg.value = imageList.value[currentIndex.value]
scale.value = 1
}
}
// 下一张
const nextImg = () => {
if (currentIndex.value < imageList.value.length - 1) {
currentIndex.value++
viewImg.value = imageList.value[currentIndex.value]
scale.value = 1
}
}
// 触摸开始
const onTouchStart = (e) => {
if (e.touches.length === 1) {
startX.value = e.touches[0].clientX
}
if (e.touches.length === 2) {
startDistance.value = getDistance(e.touches[0], e.touches[1])
startScale.value = scale.value
}
}
// 触摸移动
const onTouchMove = (e) => {
e.preventDefault()
if (e.touches.length === 2) {
const dist = getDistance(e.touches[0], e.touches[1])
scale.value = Math.min(5, Math.max(1, startScale.value * (dist / startDistance.value)))
}
}
// 触摸结束
const onTouchEnd = (e) => {
if (e.changedTouches.length === 1) {
const diff = e.changedTouches[0].clientX - startX.value
if (Math.abs(diff) > 60) {
diff > 0 ? prevImg() : nextImg()
}
}
}
// 计算双指距离
const getDistance = (p1, p2) => {
return Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY)
}
// 切割长图
const sliceImage = () => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const { width, height } = img
const num = Math.ceil(height / props.chunkHeight)
const slices = []
for (let i = 0; i < num; i++) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const h = Math.min(props.chunkHeight, height - i * props.chunkHeight)
canvas.width = width
canvas.height = h
ctx.drawImage(img, 0, i * props.chunkHeight, width, h, 0, 0, width, h)
slices.push(canvas.toDataURL('image/jpeg', 1.0))
}
imageList.value = slices
}
img.src = props.src
}
onMounted(() => {
sliceImage()
})
</script>
<template>
<div class="long-pic-slice">
<img
v-for="(data, idx) in imageList"
:key="idx"
:src="data"
:alt="`${alt}-${idx}`"
class="slice-img"
loading="eager"
@click="openView(idx)"
/>
<!-- 全屏预览层 -->
<div v-if="showViewer" class="viewer" @click="closeView">
<!-- 左箭头 -->
<div
class="arrow arrow-left"
@click.stop="prevImg"
:class="{ disabled: currentIndex <= 0 }"
>
</div>
<!-- 图片 -->
<img
:src="viewImg"
class="viewer-img"
:style="{ transform: `scale(${scale})` }"
@click.stop
@touchstart.prevent="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend.prevent="onTouchEnd"
/>
<!-- 右箭头 -->
<div
class="arrow arrow-right"
@click.stop="nextImg"
:class="{ disabled: currentIndex >= imageList.length - 1 }"
>
</div>
<!-- 底部指示器 -->
<div class="indicator">
{{ currentIndex + 1 }} / {{ imageList.length }}
</div>
</div>
</div>
</template>
<style scoped>
.long-pic-slice {
width: 100%;
max-width: 100%;
overflow: hidden;
margin: 12px 0;
}
.slice-img {
display: block;
width: 100%;
height: auto;
cursor: pointer;
}
/* 全屏预览 */
.viewer {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.97);
display: flex;
align-items: center;
justify-content: center;
z-index: 999999;
}
.viewer-img {
max-width: 90%;
max-height: 90vh;
object-fit: contain;
transition: transform 0.2s ease;
user-select: none;
}
/* 箭头 */
.arrow {
position: fixed;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
user-select: none;
}
.arrow-left {
left: 20px;
}
.arrow-right {
right: 20px;
}
.arrow.disabled {
opacity: 0.3;
pointer-events: none;
}
/* 指示器 */
.indicator {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 16px;
background: rgba(0, 0, 0, 0.4);
padding: 6px 14px;
border-radius: 20px;
z-index: 10;
}
</style>