191 lines
4.2 KiB
Vue
191 lines
4.2 KiB
Vue
|
|
<template>
|
||
|
|
<div class="offset-tool">
|
||
|
|
<div
|
||
|
|
class="dish"
|
||
|
|
@mousedown="mousedown"
|
||
|
|
@touchstart="mousedown"
|
||
|
|
ref="dishRef"
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
:style="{ top: data.top + '%', left: data.left + '%' }"
|
||
|
|
></span>
|
||
|
|
</div>
|
||
|
|
<input
|
||
|
|
class="top"
|
||
|
|
type="range"
|
||
|
|
:min="0"
|
||
|
|
:max="100"
|
||
|
|
:step="0.1"
|
||
|
|
v-model="data.top"
|
||
|
|
@input="onInput"
|
||
|
|
@change="onChange"
|
||
|
|
/>
|
||
|
|
<input
|
||
|
|
class="left"
|
||
|
|
type="range"
|
||
|
|
:min="0"
|
||
|
|
:max="100"
|
||
|
|
:step="0.1"
|
||
|
|
v-model="data.left"
|
||
|
|
@input="onInput"
|
||
|
|
@change="onChange"
|
||
|
|
/>
|
||
|
|
<span class="tip"
|
||
|
|
>x:{{ tofix(data.left) }}% y:{{ tofix(data.top) }}%</span
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||
|
|
const props = defineProps({
|
||
|
|
top: {
|
||
|
|
type: Number,
|
||
|
|
default: 50,
|
||
|
|
},
|
||
|
|
left: {
|
||
|
|
type: Number,
|
||
|
|
default: 50,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const tofix = (v: number | string) => Number(Number(v).toFixed(1));
|
||
|
|
const emit = defineEmits(["change", "input"]);
|
||
|
|
const data = reactive({
|
||
|
|
top: tofix(props.top),
|
||
|
|
left: tofix(props.left),
|
||
|
|
});
|
||
|
|
watch(
|
||
|
|
() => props.top,
|
||
|
|
(v) => (data.top = tofix(v))
|
||
|
|
);
|
||
|
|
watch(
|
||
|
|
() => props.left,
|
||
|
|
(v) => (data.left = tofix(v))
|
||
|
|
);
|
||
|
|
const dishRef = ref<HTMLDivElement>();
|
||
|
|
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||
|
|
if (!dishRef.value) return;
|
||
|
|
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||
|
|
if (!dishRef.value) return;
|
||
|
|
const { left, top, width, height } =
|
||
|
|
dishRef.value.getBoundingClientRect();
|
||
|
|
const X = e.clientX || (e as TouchEvent).touches[0].clientX;
|
||
|
|
const Y = e.clientY || (e as TouchEvent).touches[0].clientY;
|
||
|
|
var x = ((X - left) / width) * 100;
|
||
|
|
var y = ((Y - top) / height) * 100;
|
||
|
|
if (x < 0) x = 0;
|
||
|
|
if (x > 100) x = 100;
|
||
|
|
if (y < 0) y = 0;
|
||
|
|
if (y > 100) y = 100;
|
||
|
|
data.left = tofix(x);
|
||
|
|
data.top = tofix(y);
|
||
|
|
onInput();
|
||
|
|
};
|
||
|
|
mousemove(e);
|
||
|
|
const mouseup = () => {
|
||
|
|
onChange();
|
||
|
|
document.removeEventListener("mousemove", mousemove);
|
||
|
|
document.removeEventListener("touchmove", mousemove);
|
||
|
|
document.removeEventListener("mouseup", mouseup);
|
||
|
|
document.removeEventListener("touchend", mouseup);
|
||
|
|
};
|
||
|
|
document.addEventListener("mousemove", mousemove);
|
||
|
|
document.addEventListener("touchmove", mousemove);
|
||
|
|
document.addEventListener("mouseup", mouseup);
|
||
|
|
document.addEventListener("touchend", mouseup);
|
||
|
|
};
|
||
|
|
const onInput = () => emit("input", { ...data });
|
||
|
|
var changeTime: any = null;
|
||
|
|
const onChange = () => {
|
||
|
|
clearTimeout(changeTime);
|
||
|
|
changeTime = setTimeout(() => emit("change", { ...data }), 500);
|
||
|
|
};
|
||
|
|
// var offsetTime = null;
|
||
|
|
// watch(data, (v) => {
|
||
|
|
// const obj = { ...v };
|
||
|
|
// emit("input", obj);
|
||
|
|
// clearTimeout(offsetTime);
|
||
|
|
// offsetTime = setTimeout(() => emit("change", obj), 50);
|
||
|
|
// });
|
||
|
|
|
||
|
|
// defineExpose({
|
||
|
|
// open,
|
||
|
|
// close,
|
||
|
|
// });
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="less">
|
||
|
|
.offset-tool {
|
||
|
|
width: 125px;
|
||
|
|
height: 125px;
|
||
|
|
display: flex;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
--gap: 15px;
|
||
|
|
> .dish {
|
||
|
|
margin: var(--gap) 0 0 var(--gap);
|
||
|
|
flex: 1;
|
||
|
|
border: 1px solid #000;
|
||
|
|
border-radius: 5px;
|
||
|
|
cursor: pointer;
|
||
|
|
position: relative;
|
||
|
|
background-color: #fff;
|
||
|
|
> span {
|
||
|
|
pointer-events: none;
|
||
|
|
user-select: none;
|
||
|
|
position: absolute;
|
||
|
|
top: 0%;
|
||
|
|
left: 0%;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
background-color: #000;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
> .tip {
|
||
|
|
position: absolute;
|
||
|
|
right: 4px;
|
||
|
|
bottom: 0;
|
||
|
|
font-size: 10px;
|
||
|
|
pointer-events: none;
|
||
|
|
user-select: none;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
> input.left {
|
||
|
|
right: 0;
|
||
|
|
}
|
||
|
|
> input.top {
|
||
|
|
bottom: 0;
|
||
|
|
left: 0;
|
||
|
|
transform-origin: left bottom;
|
||
|
|
transform: rotate(90deg) translateX(-100%);
|
||
|
|
}
|
||
|
|
> input {
|
||
|
|
position: absolute;
|
||
|
|
width: calc(100% - var(--gap));
|
||
|
|
-webkit-appearance: none;
|
||
|
|
appearance: none;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||
|
|
// outline: none;
|
||
|
|
&::-webkit-slider-thumb {
|
||
|
|
-webkit-appearance: none;
|
||
|
|
appearance: none;
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: #4285f4; /* 蓝色滑块 */
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.2s;
|
||
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||
|
|
}
|
||
|
|
&::-webkit-slider-thumb:hover {
|
||
|
|
background: #3b77db;
|
||
|
|
transform: scale(1.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|