2022-11-14 17:11:23 +08:00
|
|
|
<template>
|
2022-11-16 23:01:23 +08:00
|
|
|
<div :id="`scroll-div-${id}`" class="scroll-main">
|
|
|
|
<div class="main-div">
|
2022-11-14 17:11:23 +08:00
|
|
|
<div :id="`content-${id}`" class="content">
|
|
|
|
<slot></slot>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<canvas :id="`scroll-${id}`" class="scroll"></canvas>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2022-11-16 23:01:23 +08:00
|
|
|
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
2022-11-19 11:30:14 +08:00
|
|
|
import { cancelGlobalDrag, isMobile, useDrag, useWheel } from '../plugin/use';
|
2022-11-14 17:11:23 +08:00
|
|
|
|
2022-11-16 23:01:23 +08:00
|
|
|
const props = defineProps<{
|
2022-11-19 11:30:14 +08:00
|
|
|
now?: number;
|
2022-11-16 23:01:23 +08:00
|
|
|
type?: 'vertical' | 'horizontal';
|
2022-11-19 11:30:14 +08:00
|
|
|
drag?: boolean;
|
|
|
|
}>();
|
|
|
|
|
|
|
|
const emits = defineEmits<{
|
|
|
|
(e: 'update:now', value: number): void;
|
|
|
|
(e: 'update:drag', value: boolean): void;
|
2022-11-16 23:01:23 +08:00
|
|
|
}>();
|
|
|
|
|
2022-11-14 17:11:23 +08:00
|
|
|
let now = 0;
|
|
|
|
let total = 0;
|
|
|
|
|
|
|
|
const id = (1e8 * Math.random()).toFixed(0);
|
|
|
|
const scale = window.devicePixelRatio;
|
|
|
|
|
2022-11-16 23:01:23 +08:00
|
|
|
const cssTarget = props.type === 'horizontal' ? 'left' : 'top';
|
|
|
|
const canvasAttr = props.type === 'horizontal' ? 'width' : 'height';
|
|
|
|
|
2022-11-14 17:11:23 +08:00
|
|
|
let ctx: CanvasRenderingContext2D;
|
|
|
|
let content: HTMLDivElement;
|
2022-11-20 00:00:30 +08:00
|
|
|
let fromSelf = false;
|
2022-11-14 17:11:23 +08:00
|
|
|
|
2022-11-16 23:01:23 +08:00
|
|
|
const resize = () => {
|
|
|
|
calHeight();
|
|
|
|
draw();
|
|
|
|
};
|
|
|
|
|
2022-11-14 17:11:23 +08:00
|
|
|
/** 绘制 */
|
|
|
|
function draw() {
|
|
|
|
if (total === 0) return;
|
2022-11-16 23:01:23 +08:00
|
|
|
if (total < ctx.canvas[canvasAttr] / scale) {
|
|
|
|
now = 0;
|
|
|
|
} else if (now > total - ctx.canvas[canvasAttr] / scale) {
|
|
|
|
now = total - ctx.canvas[canvasAttr] / scale;
|
2022-11-14 17:11:23 +08:00
|
|
|
} else if (now < 0) {
|
|
|
|
now = 0;
|
|
|
|
}
|
2022-11-19 11:30:14 +08:00
|
|
|
emits('update:now', now);
|
2022-11-14 17:11:23 +08:00
|
|
|
const length =
|
2022-11-16 23:01:23 +08:00
|
|
|
Math.min(ctx.canvas[canvasAttr] / total / scale, 1) *
|
|
|
|
ctx.canvas[canvasAttr];
|
|
|
|
const py = (now / total) * ctx.canvas[canvasAttr];
|
2022-11-14 17:11:23 +08:00
|
|
|
|
|
|
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
|
|
ctx.beginPath();
|
2022-11-16 23:01:23 +08:00
|
|
|
if (props.type === 'horizontal') {
|
2022-11-19 11:30:14 +08:00
|
|
|
ctx.moveTo(Math.max(py + 5, 5), 10 * scale);
|
|
|
|
ctx.lineTo(Math.min(py + length - 5, ctx.canvas.width - 5), 10 * scale);
|
2022-11-16 23:01:23 +08:00
|
|
|
} else {
|
2022-11-19 11:30:14 +08:00
|
|
|
ctx.moveTo(10 * scale, Math.max(py + 5, 5));
|
2022-11-16 23:01:23 +08:00
|
|
|
ctx.lineTo(
|
2022-11-19 11:30:14 +08:00
|
|
|
10 * scale,
|
2022-11-16 23:01:23 +08:00
|
|
|
Math.min(py + length - 5, ctx.canvas.height - 5)
|
|
|
|
);
|
|
|
|
}
|
2022-11-14 17:11:23 +08:00
|
|
|
ctx.lineCap = 'round';
|
|
|
|
ctx.lineWidth = 6;
|
|
|
|
ctx.strokeStyle = '#fff';
|
|
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 计算元素总长度
|
|
|
|
*/
|
|
|
|
function calHeight() {
|
2022-11-16 23:01:23 +08:00
|
|
|
const style = getComputedStyle(content);
|
|
|
|
total = parseFloat(style[canvasAttr]);
|
2022-11-14 17:11:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function scroll() {
|
|
|
|
draw();
|
2022-11-16 23:01:23 +08:00
|
|
|
content.style[cssTarget] = `${-now}px`;
|
2022-11-14 17:11:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
onUpdated(() => {
|
2022-11-20 00:00:30 +08:00
|
|
|
if (fromSelf) return;
|
2022-11-19 11:30:14 +08:00
|
|
|
now = props.now ?? now;
|
2022-11-19 18:15:42 +08:00
|
|
|
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
2022-11-14 17:11:23 +08:00
|
|
|
calHeight();
|
2022-11-19 18:15:42 +08:00
|
|
|
scroll();
|
2022-11-14 17:11:23 +08:00
|
|
|
});
|
|
|
|
|
2022-11-19 11:30:14 +08:00
|
|
|
let last: number;
|
|
|
|
let contentLast: number;
|
|
|
|
|
|
|
|
function canvasDrag(x: number, y: number) {
|
|
|
|
emits('update:drag', true);
|
|
|
|
const d = props.type === 'horizontal' ? x : y;
|
|
|
|
const dy = d - last;
|
|
|
|
last = d;
|
|
|
|
if (ctx.canvas[canvasAttr] < total * scale)
|
|
|
|
now += ((dy * total) / ctx.canvas[canvasAttr]) * scale;
|
|
|
|
scroll();
|
|
|
|
}
|
|
|
|
|
|
|
|
function contentDrag(x: number, y: number) {
|
|
|
|
emits('update:drag', true);
|
|
|
|
const d = props.type === 'horizontal' ? x : y;
|
|
|
|
const dy = d - contentLast;
|
|
|
|
contentLast = d;
|
|
|
|
if (ctx.canvas[canvasAttr] < total * scale) now -= dy;
|
|
|
|
scroll();
|
|
|
|
}
|
|
|
|
|
2022-11-14 17:11:23 +08:00
|
|
|
onMounted(() => {
|
2022-11-16 23:01:23 +08:00
|
|
|
const div = document.getElementById(`scroll-div-${id}`) as HTMLDivElement;
|
2022-11-14 17:11:23 +08:00
|
|
|
const canvas = document.getElementById(`scroll-${id}`) as HTMLCanvasElement;
|
2022-11-16 23:01:23 +08:00
|
|
|
const d = document.getElementById(`content-${id}`) as HTMLDivElement;
|
2022-11-14 17:11:23 +08:00
|
|
|
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
2022-11-16 23:01:23 +08:00
|
|
|
content = d;
|
|
|
|
calHeight();
|
|
|
|
|
|
|
|
content.addEventListener('resize', resize);
|
2022-11-14 17:11:23 +08:00
|
|
|
|
|
|
|
const style = getComputedStyle(canvas);
|
2022-11-19 11:30:14 +08:00
|
|
|
canvas.width = 20 * scale;
|
2022-11-14 17:11:23 +08:00
|
|
|
canvas.height = parseFloat(style.height) * scale;
|
|
|
|
|
2022-11-16 23:01:23 +08:00
|
|
|
if (props.type === 'horizontal') {
|
|
|
|
div.style.flexDirection = 'column';
|
|
|
|
canvas.style.height = '40px';
|
|
|
|
canvas.style.width = '98%';
|
|
|
|
canvas.style.margin = '0 1% 0 1%';
|
|
|
|
canvas.width = parseFloat(style.width) * scale;
|
2022-11-19 11:30:14 +08:00
|
|
|
canvas.height = 20 * scale;
|
2022-11-16 23:01:23 +08:00
|
|
|
}
|
|
|
|
|
2022-11-14 17:11:23 +08:00
|
|
|
draw();
|
|
|
|
|
|
|
|
// 绑定滚动条拖拽事件
|
|
|
|
useDrag(
|
|
|
|
canvas,
|
2022-11-19 11:30:14 +08:00
|
|
|
canvasDrag,
|
2022-11-14 17:11:23 +08:00
|
|
|
(x, y) => {
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = true;
|
2022-11-16 23:01:23 +08:00
|
|
|
last = props.type === 'horizontal' ? x : y;
|
2022-11-20 00:00:30 +08:00
|
|
|
content.style.transition = '';
|
2022-11-14 17:11:23 +08:00
|
|
|
},
|
2022-11-19 11:30:14 +08:00
|
|
|
() => {
|
|
|
|
setTimeout(() => emits('update:drag', false));
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = false;
|
2022-11-19 11:30:14 +08:00
|
|
|
},
|
2022-11-14 17:11:23 +08:00
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
// 绑定文本拖拽事件
|
|
|
|
useDrag(
|
|
|
|
content,
|
2022-11-19 11:30:14 +08:00
|
|
|
contentDrag,
|
2022-11-14 17:11:23 +08:00
|
|
|
(x, y) => {
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = true;
|
2022-11-16 23:01:23 +08:00
|
|
|
contentLast = props.type === 'horizontal' ? x : y;
|
2022-11-20 00:00:30 +08:00
|
|
|
content.style.transition = '';
|
2022-11-14 17:11:23 +08:00
|
|
|
},
|
2022-11-19 11:30:14 +08:00
|
|
|
() => {
|
|
|
|
setTimeout(() => emits('update:drag', false));
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = false;
|
2022-11-19 11:30:14 +08:00
|
|
|
},
|
2022-11-14 17:11:23 +08:00
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
useWheel(content, (x, y) => {
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = true;
|
2022-11-16 23:01:23 +08:00
|
|
|
const d = x !== 0 ? x : y;
|
2022-11-19 11:30:14 +08:00
|
|
|
if (Math.abs(d) > 50) {
|
|
|
|
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
2022-11-14 17:11:23 +08:00
|
|
|
} else {
|
|
|
|
content.style.transition = '';
|
|
|
|
}
|
2022-11-16 23:01:23 +08:00
|
|
|
now += d;
|
2022-11-14 17:11:23 +08:00
|
|
|
scroll();
|
2022-11-20 00:00:30 +08:00
|
|
|
fromSelf = false;
|
2022-11-14 17:11:23 +08:00
|
|
|
});
|
|
|
|
});
|
2022-11-16 23:01:23 +08:00
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
content.removeEventListener('resize', resize);
|
2022-11-19 11:30:14 +08:00
|
|
|
cancelGlobalDrag(canvasDrag);
|
|
|
|
cancelGlobalDrag(contentDrag);
|
2022-11-16 23:01:23 +08:00
|
|
|
});
|
2022-11-14 17:11:23 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
.scroll {
|
|
|
|
opacity: 0.2;
|
2022-11-19 11:30:14 +08:00
|
|
|
width: 20px;
|
2022-11-14 17:11:23 +08:00
|
|
|
transition: opacity 0.2s linear;
|
|
|
|
}
|
|
|
|
|
|
|
|
.scroll:hover {
|
|
|
|
opacity: 0.4;
|
|
|
|
}
|
|
|
|
|
|
|
|
.scroll:active {
|
|
|
|
opacity: 0.6;
|
|
|
|
}
|
|
|
|
|
2022-11-16 23:01:23 +08:00
|
|
|
.scroll-main {
|
2022-11-14 17:11:23 +08:00
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
2022-11-16 23:01:23 +08:00
|
|
|
max-width: 100%;
|
|
|
|
max-height: 100%;
|
|
|
|
justify-content: stretch;
|
2022-11-14 17:11:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
.content {
|
2022-11-16 23:01:23 +08:00
|
|
|
width: 100%;
|
2022-11-14 17:11:23 +08:00
|
|
|
position: relative;
|
|
|
|
}
|
2022-11-16 23:01:23 +08:00
|
|
|
|
|
|
|
.main-div {
|
|
|
|
flex-basis: 100%;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
2022-11-14 17:11:23 +08:00
|
|
|
</style>
|