Files
shoot-miniprograms/src/components/BowTargetEdit.vue

367 lines
7.9 KiB
Vue
Raw Normal View History

2025-07-31 14:32:14 +08:00
<script setup>
import { ref, onMounted } from "vue";
import { getElementRect, calcRing } from "@/util";
const props = defineProps({
2025-08-04 16:28:34 +08:00
id: {
type: Number,
default: 0,
},
src: {
type: String,
default: "",
},
2025-07-31 14:32:14 +08:00
arrows: {
type: Array,
default: () => [],
},
onChange: {
type: Function,
2025-08-05 11:51:09 +08:00
default: null,
2025-07-31 14:32:14 +08:00
},
});
const rect = ref({});
const arrow = ref(null);
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
2025-08-07 18:13:14 +08:00
const capsuleHeight = ref(0);
2025-08-09 12:19:39 +08:00
const scale = ref(1);
const zoomPos = ref({ x: 0, y: 0 });
const targetPos = ref({ x: 0, y: 0 });
let lastMoveTime = 0;
2025-07-31 14:32:14 +08:00
// 点击靶纸创建新的点
const onClick = async (e) => {
2025-08-09 12:19:39 +08:00
if (
arrow.value !== null ||
!props.onChange ||
Date.now() - lastMoveTime < 300
) {
return;
}
2025-07-31 14:32:14 +08:00
const newArrow = {
2025-08-09 12:19:39 +08:00
x: e.detail.x - zoomPos.value.x - 6 / scale.value,
y:
e.detail.y -
rect.value.top -
capsuleHeight.value -
zoomPos.value.y -
6 / scale.value,
};
targetPos.value = {
x: zoomPos.value.x,
y: zoomPos.value.y,
2025-07-31 14:32:14 +08:00
};
2025-08-07 18:13:14 +08:00
const side = rect.value.width;
newArrow.ring = calcRing(
props.id,
2025-08-09 12:19:39 +08:00
newArrow.x / scale.value - rect.value.width * 0.05,
newArrow.y / scale.value - rect.value.width * 0.05,
2025-08-07 18:13:14 +08:00
rect.value.width * 0.9
);
2025-08-06 10:56:57 +08:00
arrow.value = {
...newArrow,
2025-08-06 17:34:38 +08:00
x: newArrow.x / side,
y: newArrow.y / side,
2025-08-06 10:56:57 +08:00
};
2025-07-31 14:32:14 +08:00
};
// 确认添加箭矢
const confirmAdd = () => {
2025-08-09 12:19:39 +08:00
if (props.onChange) {
2025-08-11 09:13:43 +08:00
targetPos.value = {
x: zoomPos.value.x,
y: zoomPos.value.y,
};
2025-08-09 12:19:39 +08:00
props.onChange({
x: arrow.value.x / scale.value,
y: arrow.value.y / scale.value,
ring: arrow.value.ring || "M",
});
}
2025-07-31 14:32:14 +08:00
arrow.value = null;
};
// 删除箭矢
const deleteArrow = () => {
arrow.value = null;
2025-08-09 12:19:39 +08:00
targetPos.value = {
x: zoomPos.value.x,
y: zoomPos.value.y,
};
2025-07-31 14:32:14 +08:00
};
// 开始拖拽 - 同样修复坐标获取
2025-08-09 12:19:39 +08:00
const startDrag = async (e) => {
2025-07-31 14:32:14 +08:00
if (!e.touches[0]) return;
isDragging.value = true;
dragStartPos.value = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
};
// 拖拽移动 - 同样修复坐标获取
const onDrag = async (e) => {
2025-08-09 12:19:39 +08:00
lastMoveTime = Date.now();
2025-07-31 14:32:14 +08:00
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;
2025-08-07 18:13:14 +08:00
const side = rect.value.width;
2025-07-31 14:32:14 +08:00
// 更新坐标
2025-08-09 12:19:39 +08:00
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)
);
2025-08-04 16:28:34 +08:00
arrow.value.ring = calcRing(
props.id,
2025-08-09 12:19:39 +08:00
arrow.value.x / scale.value - rect.value.width * 0.05,
arrow.value.y / scale.value - rect.value.width * 0.05,
2025-08-07 18:13:14 +08:00
rect.value.width * 0.9
2025-08-04 16:28:34 +08:00
);
2025-08-09 12:19:39 +08:00
2025-08-06 17:34:38 +08:00
arrow.value.x = arrow.value.x / side;
arrow.value.y = arrow.value.y / side;
2025-07-31 14:32:14 +08:00
// 更新拖拽起始位置
dragStartPos.value = { x: clientX, y: clientY };
};
// 结束拖拽
2025-08-09 12:19:39 +08:00
const endDrag = (e) => {
2025-07-31 14:32:14 +08:00
isDragging.value = false;
};
2025-08-07 18:13:14 +08:00
const onScale = (e) => {
2025-08-09 12:19:39 +08:00
lastMoveTime = Date.now();
const lastScale = scale.value;
scale.value = e.detail.scale;
zoomPos.value = { x: e.detail.x, y: e.detail.y };
if (arrow.value) {
arrow.value.x = arrow.value.x * (scale.value / lastScale);
arrow.value.y = arrow.value.y * (scale.value / lastScale);
}
2025-08-07 18:13:14 +08:00
};
const onMove = (e) => {
2025-08-09 12:19:39 +08:00
if (e.detail.source) {
zoomPos.value = { x: e.detail.x, y: e.detail.y };
}
2025-08-07 18:13:14 +08:00
};
2025-07-31 14:32:14 +08:00
onMounted(async () => {
2025-08-07 18:13:14 +08:00
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
const result = await getElementRect(".container");
2025-07-31 14:32:14 +08:00
rect.value = result;
});
</script>
2025-07-30 17:38:48 +08:00
<template>
2025-08-07 18:13:14 +08:00
<view
class="container"
2025-08-09 12:19:39 +08:00
@tap="onClick"
2025-08-07 18:13:14 +08:00
@touchmove="onDrag"
@touchend="endDrag"
>
<movable-area class="move-area" scale-area>
<movable-view
class="move-view"
direction="all"
scale
2025-08-09 12:19:39 +08:00
:x="targetPos.x"
:y="targetPos.y"
2025-08-07 18:13:14 +08:00
:scale-min="1"
2025-08-12 11:05:04 +08:00
:scale-max="2"
2025-08-07 18:13:14 +08:00
:scale-value="scale"
:animation="false"
@scale="onScale"
@change="onMove"
:out-of-bounds="true"
>
<image :src="src" mode="widthFix" />
2025-07-31 14:32:14 +08:00
<view
2025-08-09 12:19:39 +08:00
v-for="(arrow, index) in arrows"
:key="index"
class="arrow-point"
:style="{
left: (arrow.x !== undefined ? arrow.x : 0) * 100 + '%',
top: (arrow.y !== undefined ? arrow.y : 0) * 100 + '%',
}"
2025-07-31 14:32:14 +08:00
>
2025-08-09 12:19:39 +08:00
<view
v-if="arrow.x !== undefined && arrow.y !== undefined"
class="point"
>
<text>{{ index + 1 }}</text>
2025-08-06 17:34:38 +08:00
</view>
2025-08-09 12:19:39 +08:00
</view>
<movable-view
v-if="arrow"
class="arrow-point"
direction="all"
:animation="false"
:out-of-bounds="true"
:x="arrow ? (rect.width * arrow.x) / scale : 0"
:y="arrow ? (rect.width * arrow.y) / scale : 0"
>
2025-08-12 10:16:11 +08:00
<view class="point"> </view>
2025-08-06 17:34:38 +08:00
<view
2025-08-09 12:19:39 +08:00
v-if="arrow"
class="edit-buttons"
@touchstart.stop
:style="{ transform: `scale(${1 / scale})` }"
2025-08-06 17:34:38 +08:00
>
2025-08-09 12:19:39 +08:00
<view class="edit-btn-text">
2025-08-12 10:16:11 +08:00
<!-- <text v-if="arrow.ring === 0" :style="{ width: '100%' }"
2025-08-09 12:19:39 +08:00
>未上靶</text
2025-08-12 10:16:11 +08:00
> -->
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
<!-- <text
2025-08-09 12:19:39 +08:00
v-if="arrow.ring > 0"
:style="{
fontSize: '16px',
marginLeft: '2px',
}"
></text
2025-08-12 10:16:11 +08:00
> -->
2025-08-09 12:19:39 +08:00
</view>
<view class="edit-btn confirm-btn" @touchstart.stop="confirmAdd">
<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>
2025-08-06 17:34:38 +08:00
</view>
2025-08-09 12:19:39 +08:00
</movable-view>
2025-08-06 17:34:38 +08:00
</movable-view>
2025-08-07 18:13:14 +08:00
</movable-area>
</view>
2025-07-30 17:38:48 +08:00
</template>
<style scoped>
2025-08-06 17:34:38 +08:00
.container {
2025-08-07 18:13:14 +08:00
width: 100vw;
height: 100vw;
overflow: hidden;
2025-08-09 12:19:39 +08:00
transform: translateY(-10px);
2025-08-07 18:13:14 +08:00
}
.move-area {
2025-08-06 17:34:38 +08:00
width: 100%;
2025-08-07 18:13:14 +08:00
height: 100%;
2025-07-31 14:32:14 +08:00
position: relative;
2025-07-30 17:38:48 +08:00
}
2025-07-31 14:32:14 +08:00
2025-08-07 18:13:14 +08:00
.move-view {
width: 90vw;
height: 90vw;
2025-08-09 12:19:39 +08:00
padding: 5vw;
2025-08-07 18:13:14 +08:00
position: relative;
}
.move-view > image {
2025-07-30 17:38:48 +08:00
width: 100%;
2025-07-31 14:32:14 +08:00
}
.arrow-point {
position: absolute;
2025-08-09 12:19:39 +08:00
display: flex;
justify-content: center;
align-items: center;
2025-07-31 14:32:14 +08:00
}
.point {
2025-08-09 12:19:39 +08:00
min-width: 12px;
min-height: 12px;
2025-07-31 14:32:14 +08:00
border-radius: 50%;
border: 1px solid #fff;
color: #fff;
font-size: 8px;
text-align: center;
line-height: 10px;
box-sizing: border-box;
background-color: #ff4444;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
2025-08-09 12:19:39 +08:00
transition: all 0.1s linear;
2025-07-31 14:32:14 +08:00
}
.point > text {
transform: scaleX(0.7);
display: block;
font-weight: bold;
}
.edit-buttons {
position: absolute;
2025-08-09 12:19:39 +08:00
top: calc(50% - 44px);
left: calc(50% - 44px);
2025-07-31 14:32:14 +08:00
background: #18ff6899;
width: 88px;
height: 88px;
display: flex;
align-items: flex-end;
2025-08-09 12:19:39 +08:00
transition: all 0.1s linear;
2025-07-31 14:32:14 +08:00
}
2025-08-06 14:58:53 +08:00
.edit-btn-text {
2025-07-31 14:32:14 +08:00
width: 100%;
2025-08-06 14:58:53 +08:00
display: flex;
2025-08-12 10:16:11 +08:00
/* justify-content: center; */
margin-left: 10px;
2025-08-06 14:58:53 +08:00
}
.edit-btn-text > text {
2025-07-31 14:32:14 +08:00
line-height: 50px;
2025-08-01 09:20:10 +08:00
font-size: 24px;
2025-07-31 14:32:14 +08:00
font-weight: bold;
color: #fff;
2025-08-07 09:21:30 +08:00
text-align: center;
2025-07-31 14:32:14 +08:00
}
.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 {
2025-08-06 14:58:53 +08:00
left: calc(50% - 12px);
bottom: -12px;
2025-07-31 14:32:14 +08:00
}
.delete-btn {
2025-08-06 14:58:53 +08:00
left: calc(50% - 12px);
top: -12px;
2025-07-31 14:32:14 +08:00
}
.drag-btn {
2025-08-06 14:58:53 +08:00
right: -12px;
bottom: -12px;
2025-07-30 17:38:48 +08:00
}
</style>