|
|
@@ -9,11 +9,19 @@
|
|
|
fullscreen
|
|
|
height="100%"
|
|
|
>
|
|
|
- <div class="preview_image">
|
|
|
+ <div
|
|
|
+ class="preview_image"
|
|
|
+ @wheel.prevent="HandleWheel"
|
|
|
+ @mousedown="HandleMouseDown"
|
|
|
+ @mousemove="HandleMouseMove"
|
|
|
+ @mouseup="HandleMouseUp"
|
|
|
+ @mouseleave="HandleMouseUp"
|
|
|
+ >
|
|
|
<img
|
|
|
:src="currenImageUrl"
|
|
|
alt=""
|
|
|
- :style="{ transform: `scale(${zoomLevel}) rotate(${rotateAngle}deg)` }"
|
|
|
+ :style="imageStyle"
|
|
|
+ draggable="false"
|
|
|
/>
|
|
|
|
|
|
<div class="preview_close" @click="CloseDialog()">
|
|
|
@@ -40,176 +48,218 @@
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { ref, watch, computed } from 'vue';
|
|
|
+import { ref, watch, computed } from 'vue'
|
|
|
|
|
|
+// ==================== 类型定义 ====================
|
|
|
|
|
|
+/** 图片项接口 */
|
|
|
interface ImageItem {
|
|
|
- url: string;
|
|
|
- rotateAngle?: number;
|
|
|
- pageNo?: number;
|
|
|
+ url: string
|
|
|
+ rotateAngle?: number
|
|
|
+ pageNo?: number
|
|
|
}
|
|
|
|
|
|
+/** 组件 Props 接口 */
|
|
|
interface Props {
|
|
|
- modelValue: boolean;
|
|
|
- imageList: ImageItem[];
|
|
|
- index?: number;
|
|
|
- showTool?: boolean;
|
|
|
+ modelValue: boolean
|
|
|
+ imageList: ImageItem[]
|
|
|
+ index?: number
|
|
|
}
|
|
|
|
|
|
+/** 组件 Emits 接口 */
|
|
|
interface Emits {
|
|
|
- (e: 'update:modelValue', value: boolean): void;
|
|
|
- (e: 'GetRotateAngle', data: { currentIndex: number; rotateAngle: number }): void;
|
|
|
+ (e: 'update:modelValue', value: boolean): void
|
|
|
+ (e: 'GetRotateAngle', data: { currentIndex: number; rotateAngle: number }): void
|
|
|
}
|
|
|
|
|
|
+// ==================== Props & Emits ====================
|
|
|
+
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
imageList: () => [],
|
|
|
index: 0,
|
|
|
- showTool: false,
|
|
|
-});
|
|
|
-
|
|
|
-const emit = defineEmits<Emits>();
|
|
|
-
|
|
|
-const currenImageUrl = ref('');
|
|
|
-const currentIndex = ref(0);
|
|
|
-const zoomLevel = ref(1);
|
|
|
-const maxZoom = ref(3);
|
|
|
-const minZoom = ref(0.5);
|
|
|
-const zoomStep = ref(0.1);
|
|
|
-const rotateAngle = ref(0);
|
|
|
-
|
|
|
-const toolList = [
|
|
|
- {
|
|
|
- title: '放大',
|
|
|
- icon: 'icon_fangda',
|
|
|
- value: '1',
|
|
|
- },
|
|
|
- {
|
|
|
- title: '缩小',
|
|
|
- icon: 'icon_suoxiao',
|
|
|
- value: '2',
|
|
|
- },
|
|
|
- {
|
|
|
- title: '逆时针90°',
|
|
|
- icon: 'icon_nishizhen90',
|
|
|
- value: '5',
|
|
|
- },
|
|
|
- {
|
|
|
- title: '顺时针90°',
|
|
|
- icon: 'icon_shunshizhen90',
|
|
|
- value: '6',
|
|
|
- },
|
|
|
-];
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits<Emits>()
|
|
|
|
|
|
+// ==================== 响应式状态 ====================
|
|
|
+
|
|
|
+/** 当前图片URL */
|
|
|
+const currenImageUrl = ref('')
|
|
|
+/** 当前图片索引 */
|
|
|
+const currentIndex = ref(0)
|
|
|
+/** 缩放比例 */
|
|
|
+const zoomLevel = ref(1)
|
|
|
+/** 最大缩放比例 */
|
|
|
+const maxZoom = 3
|
|
|
+/** 最小缩放比例 */
|
|
|
+const minZoom = 0.5
|
|
|
+/** 缩放步进值 */
|
|
|
+const zoomStep = 0.1
|
|
|
+/** 旋转角度 */
|
|
|
+const rotateAngle = ref(0)
|
|
|
+
|
|
|
+/** 拖拽相关状态 */
|
|
|
+const translateX = ref(0)
|
|
|
+const translateY = ref(0)
|
|
|
+const isDragging = ref(false)
|
|
|
+const startX = ref(0)
|
|
|
+const startY = ref(0)
|
|
|
+
|
|
|
+// ==================== 计算属性 ====================
|
|
|
+
|
|
|
+/** 弹窗显示状态(双向绑定) */
|
|
|
const showDialog = computed({
|
|
|
get() {
|
|
|
- return props.modelValue;
|
|
|
+ return props.modelValue
|
|
|
},
|
|
|
set(val) {
|
|
|
- emit('update:modelValue', val);
|
|
|
+ emit('update:modelValue', val)
|
|
|
},
|
|
|
-});
|
|
|
+})
|
|
|
+
|
|
|
+/** 图片内联样式(缩放 + 旋转 + 拖拽位移) */
|
|
|
+const imageStyle = computed(() => ({
|
|
|
+ transform: `translate(${translateX.value}px, ${translateY.value}px) scale(${zoomLevel.value}) rotate(${rotateAngle.value}deg)`,
|
|
|
+ transition: isDragging.value ? 'none' : 'transform 0.15s',
|
|
|
+ cursor: isDragging.value ? 'grabbing' : 'grab',
|
|
|
+}))
|
|
|
+
|
|
|
+// ==================== 监听器 ====================
|
|
|
|
|
|
+/** 监听弹窗打开,加载当前索引图片信息 */
|
|
|
watch(
|
|
|
() => props.modelValue,
|
|
|
(newVal) => {
|
|
|
if (newVal && props.imageList.length > 0) {
|
|
|
- const targetIndex = props.index || 0;
|
|
|
- currenImageUrl.value = props.imageList[targetIndex].url;
|
|
|
- rotateAngle.value = props.imageList[targetIndex].rotateAngle || 0;
|
|
|
- currentIndex.value = targetIndex;
|
|
|
+ LoadImageByIndex(props.index || 0)
|
|
|
}
|
|
|
}
|
|
|
-);
|
|
|
+)
|
|
|
|
|
|
-const CloseDialog = () => {
|
|
|
- showDialog.value = false;
|
|
|
- let obj = {
|
|
|
- currentIndex: currentIndex.value,
|
|
|
- rotateAngle: rotateAngle.value,
|
|
|
- };
|
|
|
- emit('GetRotateAngle', obj);
|
|
|
-};
|
|
|
+// ==================== 图片切换 ====================
|
|
|
|
|
|
-const GetReturn = () => {
|
|
|
- let obj = {
|
|
|
- currentIndex: currentIndex.value,
|
|
|
- rotateAngle: rotateAngle.value,
|
|
|
- };
|
|
|
- emit('GetRotateAngle', obj);
|
|
|
-};
|
|
|
+/**
|
|
|
+ * 根据索引加载图片信息
|
|
|
+ * @param targetIndex 目标图片索引
|
|
|
+ */
|
|
|
+const LoadImageByIndex = (targetIndex: number) => {
|
|
|
+ currenImageUrl.value = props.imageList[targetIndex].url
|
|
|
+ rotateAngle.value = props.imageList[targetIndex].rotateAngle || 0
|
|
|
+ currentIndex.value = targetIndex
|
|
|
+ ResetTransform()
|
|
|
+}
|
|
|
|
|
|
+/** 重置缩放和位移状态 */
|
|
|
+const ResetTransform = () => {
|
|
|
+ zoomLevel.value = 1
|
|
|
+ translateX.value = 0
|
|
|
+ translateY.value = 0
|
|
|
+}
|
|
|
+
|
|
|
+/** 切换到上一张图片 */
|
|
|
const LastImage = () => {
|
|
|
if (currentIndex.value > 0) {
|
|
|
- currentIndex.value--;
|
|
|
+ LoadImageByIndex(currentIndex.value - 1)
|
|
|
}
|
|
|
- currenImageUrl.value = props.imageList[currentIndex.value].url;
|
|
|
- rotateAngle.value = props.imageList[currentIndex.value].rotateAngle || 0;
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
+/** 切换到下一张图片 */
|
|
|
const NextImage = () => {
|
|
|
if (currentIndex.value < props.imageList.length - 1) {
|
|
|
- currentIndex.value++;
|
|
|
+ LoadImageByIndex(currentIndex.value + 1)
|
|
|
}
|
|
|
- currenImageUrl.value = props.imageList[currentIndex.value].url;
|
|
|
- rotateAngle.value = props.imageList[currentIndex.value].rotateAngle || 0;
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
-const ToolClick = (item: any) => {
|
|
|
- if (item.value === '1') {
|
|
|
- ZoomIn();
|
|
|
- }
|
|
|
- if (item.value === '2') {
|
|
|
- ZoomOut();
|
|
|
- }
|
|
|
- if (item.value === '5') {
|
|
|
- RotateCounterClockwise90();
|
|
|
- }
|
|
|
- if (item.value === '6') {
|
|
|
- RotateClockwise90();
|
|
|
+// ==================== 弹窗控制 ====================
|
|
|
+
|
|
|
+/** 关闭弹窗并回传旋转角度信息 */
|
|
|
+const CloseDialog = () => {
|
|
|
+ showDialog.value = false
|
|
|
+ emit('GetRotateAngle', {
|
|
|
+ currentIndex: currentIndex.value,
|
|
|
+ rotateAngle: rotateAngle.value,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 鼠标滚轮缩放 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 滚轮事件处理:向上滚放大,向下滚缩小
|
|
|
+ * @param e 滚轮事件对象
|
|
|
+ */
|
|
|
+const HandleWheel = (e: WheelEvent) => {
|
|
|
+ if (e.deltaY < 0) {
|
|
|
+ ZoomIn()
|
|
|
+ } else {
|
|
|
+ ZoomOut()
|
|
|
}
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
+/** 放大图片(最大 3x) */
|
|
|
const ZoomIn = () => {
|
|
|
- if (zoomLevel.value < maxZoom.value) {
|
|
|
- zoomLevel.value += zoomStep.value;
|
|
|
+ if (zoomLevel.value < maxZoom) {
|
|
|
+ zoomLevel.value = Math.round((zoomLevel.value + zoomStep) * 100) / 100
|
|
|
}
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
+/** 缩小图片(最小 0.5x) */
|
|
|
const ZoomOut = () => {
|
|
|
- if (zoomLevel.value > minZoom.value) {
|
|
|
- zoomLevel.value -= zoomStep.value;
|
|
|
+ if (zoomLevel.value > minZoom) {
|
|
|
+ zoomLevel.value = Math.round((zoomLevel.value - zoomStep) * 100) / 100
|
|
|
}
|
|
|
-};
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 鼠标拖拽移动 ====================
|
|
|
|
|
|
+/**
|
|
|
+ * 鼠标按下:记录起始位置,进入拖拽状态
|
|
|
+ * @param e 鼠标事件对象
|
|
|
+ */
|
|
|
+const HandleMouseDown = (e: MouseEvent) => {
|
|
|
+ // 仅左键触发拖拽
|
|
|
+ if (e.button !== 0) return
|
|
|
+ isDragging.value = true
|
|
|
+ startX.value = e.clientX - translateX.value
|
|
|
+ startY.value = e.clientY - translateY.value
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 鼠标移动:计算拖拽偏移量,更新图片位置
|
|
|
+ * @param e 鼠标事件对象
|
|
|
+ */
|
|
|
+const HandleMouseMove = (e: MouseEvent) => {
|
|
|
+ if (!isDragging.value) return
|
|
|
+ translateX.value = e.clientX - startX.value
|
|
|
+ translateY.value = e.clientY - startY.value
|
|
|
+}
|
|
|
+
|
|
|
+/** 鼠标松开 / 移出区域:结束拖拽 */
|
|
|
+const HandleMouseUp = () => {
|
|
|
+ isDragging.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 旋转 ====================
|
|
|
+
|
|
|
+/** 逆时针旋转90度 */
|
|
|
const RotateCounterClockwise90 = () => {
|
|
|
- rotateAngle.value -= 90;
|
|
|
- GetReturn();
|
|
|
-};
|
|
|
+ rotateAngle.value -= 90
|
|
|
+}
|
|
|
|
|
|
+/** 顺时针旋转90度 */
|
|
|
const RotateClockwise90 = () => {
|
|
|
- rotateAngle.value += 90;
|
|
|
- GetReturn();
|
|
|
-};
|
|
|
+ rotateAngle.value += 90
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.preview_canvas {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- .main_container {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- position: relative;
|
|
|
- background: transparent;
|
|
|
- user-select: none;
|
|
|
- overflow: hidden;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
+.preview_image {
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
- .paper_template {
|
|
|
- position: absolute;
|
|
|
- }
|
|
|
+ img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ user-select: none;
|
|
|
}
|
|
|
}
|
|
|
</style>
|