437 lines
9.9 KiB
Vue
437 lines
9.9 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||
import { getElementRect, calcRing, capsuleHeight } from "@/util";
|
||
|
||
const props = defineProps({
|
||
id: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
src: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
arrows: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
onChange: {
|
||
type: Function,
|
||
default: null,
|
||
},
|
||
});
|
||
|
||
const rect = ref({});
|
||
const arrow = ref(null);
|
||
const isDragging = ref(false);
|
||
const dragStartPos = ref({ x: 0, y: 0 });
|
||
const scale = ref(1);
|
||
const scrollTop = ref(0);
|
||
const selected = ref(null);
|
||
let lastMoveTime = 0;
|
||
|
||
// 点击靶纸创建新的点
|
||
const onClick = async (e) => {
|
||
if (
|
||
arrow.value !== null ||
|
||
!props.onChange ||
|
||
Date.now() - lastMoveTime < 300
|
||
) {
|
||
return;
|
||
}
|
||
if (props.id === 7 || props.id === 9) {
|
||
if (
|
||
e.detail.x < rect.value.width * 0.2 ||
|
||
e.detail.x > rect.value.width * 0.8
|
||
)
|
||
return;
|
||
// 放大并通过滚动将点击位置置于视窗中心
|
||
scale.value = 1.4;
|
||
const viewportH = rect.value.width; // 容器高度等于宽度(100vw)
|
||
const contentH = scale.value * rect.value.width; // 内容高度
|
||
const clickYInContainer = e.detail.y - rect.value.top;
|
||
let target = clickYInContainer * scale.value - viewportH / 2;
|
||
target = Math.max(0, Math.min(contentH - viewportH, target));
|
||
setTimeout(() => {
|
||
scrollTop.value = target > 180 ? target + 10 : target;
|
||
}, 200);
|
||
}
|
||
const newArrow = {
|
||
x: (e.detail.x - 6) * scale.value,
|
||
y: (e.detail.y - rect.value.top - capsuleHeight - 6) * scale.value,
|
||
};
|
||
|
||
const side = rect.value.width;
|
||
newArrow.ring = calcRing(
|
||
props.id,
|
||
newArrow.x / scale.value - side * 0.05,
|
||
newArrow.y / scale.value - side * 0.05,
|
||
side * 0.9
|
||
);
|
||
arrow.value = {
|
||
...newArrow,
|
||
x: newArrow.x / side,
|
||
y: newArrow.y / side,
|
||
};
|
||
};
|
||
|
||
// 确认添加箭矢
|
||
const confirmAdd = () => {
|
||
if (props.onChange) {
|
||
props.onChange({
|
||
x: arrow.value.x / scale.value,
|
||
y: arrow.value.y / scale.value,
|
||
ring: arrow.value.ring || "M",
|
||
});
|
||
}
|
||
arrow.value = null;
|
||
scale.value = 1;
|
||
scrollTop.value = 0;
|
||
};
|
||
|
||
// 删除箭矢
|
||
const deleteArrow = () => {
|
||
arrow.value = null;
|
||
scale.value = 1;
|
||
scrollTop.value = 0;
|
||
};
|
||
|
||
// 开始拖拽 - 同样修复坐标获取
|
||
const startDrag = async (e) => {
|
||
if (!e.touches[0]) return;
|
||
isDragging.value = true;
|
||
dragStartPos.value = {
|
||
x: e.touches[0].clientX,
|
||
y: e.touches[0].clientY,
|
||
};
|
||
};
|
||
|
||
// 拖拽移动 - 同样修复坐标获取
|
||
const onDrag = async (e) => {
|
||
lastMoveTime = Date.now();
|
||
if (!isDragging.value || !e.touches[0] || !arrow.value) return;
|
||
let clientX = e.touches[0].clientX;
|
||
let clientY = e.touches[0].clientY;
|
||
|
||
// 计算移动距离
|
||
const deltaX = clientX - dragStartPos.value.x;
|
||
const deltaY = clientY - dragStartPos.value.y;
|
||
const side = rect.value.width;
|
||
// 更新坐标
|
||
arrow.value.x = Math.max(
|
||
0,
|
||
Math.min(side * scale.value, arrow.value.x * side + deltaX)
|
||
);
|
||
arrow.value.y = Math.max(
|
||
0,
|
||
Math.min(side * scale.value, arrow.value.y * side + deltaY)
|
||
);
|
||
arrow.value.ring = calcRing(
|
||
props.id,
|
||
arrow.value.x / scale.value - side * 0.05,
|
||
arrow.value.y / scale.value - side * 0.05,
|
||
side * 0.9
|
||
);
|
||
|
||
arrow.value.x = arrow.value.x / side;
|
||
arrow.value.y = arrow.value.y / side;
|
||
|
||
// 更新拖拽起始位置
|
||
dragStartPos.value = { x: clientX, y: clientY };
|
||
};
|
||
|
||
// 结束拖拽
|
||
const endDrag = (e) => {
|
||
isDragging.value = false;
|
||
};
|
||
|
||
const getNewPos = () => {
|
||
if (props.id === 7 || props.id === 9) {
|
||
if (arrow.value.y >= 1.33)
|
||
return { left: "-12px", bottom: "calc(50% - 12px)" };
|
||
} else {
|
||
if (arrow.value.y > 0.88) {
|
||
if (arrow.value.x < 0.05) {
|
||
return { left: "calc(100% - 12px)", bottom: "calc(100% - 12px)" };
|
||
}
|
||
return { left: "-12px", bottom: "calc(50% - 12px)" };
|
||
}
|
||
}
|
||
return { left: "calc(50% - 12px)", bottom: "-12px" };
|
||
};
|
||
|
||
const setEditArrow = (data) => {
|
||
selected.value = data;
|
||
// if (data === null) {
|
||
// arrow.value = null;
|
||
// scale.value = 1;
|
||
// scrollTop.value = 0;
|
||
// return;
|
||
// }
|
||
// if (props.id === 7 || props.id === 9) {
|
||
// scale.value = 1.4;
|
||
// const viewportH = rect.value.width; // 容器高度等于宽度(100vw)
|
||
// const contentH = scale.value * rect.value.width; // 内容高度
|
||
// const clickYInContainer = contentH * data.y - rect.value.top;
|
||
// let target = clickYInContainer * scale.value - viewportH / 2;
|
||
// target = Math.max(0, Math.min(contentH - viewportH, target));
|
||
// setTimeout(() => {
|
||
// scrollTop.value = target > 180 ? target + 10 : target;
|
||
// }, 200);
|
||
// }
|
||
// arrow.value = {
|
||
// ...data,
|
||
// x: data.x * scale.value,
|
||
// y: data.y * scale.value,
|
||
// };
|
||
};
|
||
|
||
onMounted(async () => {
|
||
const result = await getElementRect(".container");
|
||
rect.value = result;
|
||
uni.$on("set-edit-arrow", setEditArrow);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
uni.$off("set-edit-arrow", setEditArrow);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<scroll-view
|
||
:scroll-y="scale > 1"
|
||
scroll-with-animation
|
||
:scroll-top="scrollTop"
|
||
:show-scrollbar="false"
|
||
:enhanced="true"
|
||
class="container"
|
||
@tap="onClick"
|
||
@touchmove="onDrag"
|
||
@touchend="endDrag"
|
||
>
|
||
<movable-area
|
||
class="move-area"
|
||
:style="{
|
||
width: scale * 100 + 'vw',
|
||
height: scale * 100 + 'vw',
|
||
transform: `translateX(${(100 - scale * 100) / 2}vw)`,
|
||
}"
|
||
>
|
||
<image :src="src" mode="widthFix" />
|
||
<view
|
||
v-for="(arrow, index) in arrows"
|
||
:key="index"
|
||
:class="`arrow-point ${
|
||
selected !== null && index === selected ? 'selected-arrow-point' : ''
|
||
}`"
|
||
:style="{
|
||
left: (arrow.x !== undefined ? arrow.x : 0) * 100 + '%',
|
||
top: (arrow.y !== undefined ? arrow.y : 0) * 100 + '%',
|
||
}"
|
||
>
|
||
<view
|
||
v-if="arrow.x !== undefined && arrow.y !== undefined"
|
||
class="point"
|
||
>
|
||
<text>{{ index + 1 }}</text>
|
||
</view>
|
||
</view>
|
||
<movable-view
|
||
v-if="arrow"
|
||
class="arrow-point"
|
||
direction="all"
|
||
:animation="false"
|
||
:out-of-bounds="true"
|
||
:x="arrow ? rect.width * arrow.x : 0"
|
||
:y="arrow ? rect.width * arrow.y : 0"
|
||
>
|
||
<view
|
||
class="point"
|
||
:style="{ minWidth: 10 * scale + 'px', minHeight: 10 * scale + 'px' }"
|
||
>
|
||
<view v-if="arrow" class="edit-buttons" @touchstart.stop>
|
||
<view class="edit-btn-text">
|
||
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
|
||
<text
|
||
v-if="arrow.ring > 0"
|
||
:style="{
|
||
fontSize: '16px',
|
||
marginLeft: '2px',
|
||
}"
|
||
>环</text
|
||
>
|
||
</view>
|
||
<view
|
||
class="edit-btn confirm-btn"
|
||
@touchstart.stop="confirmAdd"
|
||
:style="{ ...getNewPos() }"
|
||
>
|
||
<image src="../static/arrow-edit-save.png" mode="widthFix" />
|
||
</view>
|
||
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
|
||
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
|
||
</view>
|
||
<view
|
||
class="edit-btn drag-btn"
|
||
@touchstart.stop="startDrag($event)"
|
||
>
|
||
<image src="../static/arrow-edit-move.png" mode="widthFix" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</movable-view>
|
||
<!-- <view class="test-view"></view> -->
|
||
</movable-area>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.container {
|
||
width: 100vw;
|
||
height: 100vw;
|
||
overflow-x: hidden;
|
||
}
|
||
.container::-webkit-scrollbar {
|
||
width: 0;
|
||
height: 0;
|
||
color: transparent;
|
||
}
|
||
|
||
.move-area {
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.move-area > image {
|
||
width: 90%;
|
||
height: 90%;
|
||
margin: 5%;
|
||
}
|
||
|
||
.move-view {
|
||
width: 90vw;
|
||
height: 90vw;
|
||
padding: 5vw;
|
||
position: relative;
|
||
}
|
||
|
||
.move-view > image {
|
||
width: 100%;
|
||
}
|
||
|
||
.arrow-point {
|
||
position: absolute;
|
||
}
|
||
|
||
.point {
|
||
min-width: 10px;
|
||
min-height: 10px;
|
||
border-radius: 50%;
|
||
border: 1px solid #fff;
|
||
color: #fff;
|
||
text-align: center;
|
||
line-height: 10px;
|
||
box-sizing: border-box;
|
||
background-color: #00bf04;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||
transition: all 0.1s linear;
|
||
position: relative;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.point > text {
|
||
display: block;
|
||
font-size: 16rpx;
|
||
line-height: 10px;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
font-family: "DINCondensed", "PingFang SC", "Helvetica Neue", Arial,
|
||
sans-serif;
|
||
transform: translate(-50%, -50%);
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.edit-buttons {
|
||
position: absolute;
|
||
top: calc(50% - 44px);
|
||
left: calc(50% - 44px);
|
||
background: #18ff6899;
|
||
width: 88px;
|
||
height: 88px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
transition: all 0.1s linear;
|
||
}
|
||
|
||
.edit-btn-text {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.edit-btn-text > text {
|
||
line-height: 50px;
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
text-align: center;
|
||
}
|
||
|
||
.edit-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
}
|
||
|
||
.edit-btn > image {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.confirm-btn {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.delete-btn {
|
||
left: calc(50% - 12px);
|
||
top: -12px;
|
||
}
|
||
|
||
.drag-btn {
|
||
right: -12px;
|
||
bottom: -12px;
|
||
}
|
||
.test-view {
|
||
position: absolute;
|
||
top: 29px;
|
||
left: 138px;
|
||
width: 115px;
|
||
height: 115px;
|
||
background-color: #ff000055;
|
||
}
|
||
.selected-arrow-point .point {
|
||
background: linear-gradient(180deg, #ffdaa6 0%, #e9a333 100%) !important;
|
||
box-shadow: 0rpx 2rpx 4rpx 0rpx rgba(0, 0, 0, 0.18);
|
||
animation: duang 0.35s ease-out;
|
||
}
|
||
@keyframes duang {
|
||
0% {
|
||
transform: translate(-50%, -50%) scale(0.7);
|
||
}
|
||
45% {
|
||
transform: translate(-50%, -50%) scale(1.4);
|
||
}
|
||
70% {
|
||
transform: translate(-50%, -50%) scale(0.9);
|
||
}
|
||
100% {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
}
|
||
</style>
|