update chart
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="chart-range-slider">
|
||||
<div ref="trackRef" class="track" @pointerdown="handleTrackPointerDown">
|
||||
<div
|
||||
class="selection"
|
||||
:style="selectionStyle"
|
||||
@pointerdown.stop="handleSelectionPointerDown"
|
||||
/>
|
||||
<button
|
||||
class="thumb start"
|
||||
:style="startThumbStyle"
|
||||
type="button"
|
||||
aria-label="调整开始日期"
|
||||
@pointerdown.stop="event => handleThumbPointerDown('start', event)"
|
||||
/>
|
||||
<button
|
||||
class="thumb end"
|
||||
:style="endThumbStyle"
|
||||
type="button"
|
||||
aria-label="调整结束日期"
|
||||
@pointerdown.stop="event => handleThumbPointerDown('end', event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="label" v-text="label" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type DragMode = "start" | "end" | "range";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
label?: string;
|
||||
max: number;
|
||||
min?: number;
|
||||
minRange?: number;
|
||||
modelValue: number[];
|
||||
}>(), {
|
||||
label: "",
|
||||
minRange: 0,
|
||||
min: 0
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: number[]];
|
||||
change: [value: number[]];
|
||||
}>();
|
||||
|
||||
const trackRef = ref<HTMLElement>();
|
||||
const draggingMode = ref<DragMode>();
|
||||
const dragStartX = ref(0);
|
||||
const dragStartValue = ref<[number, number]>([0, 0]);
|
||||
|
||||
const safeMin = computed(() => Math.min(props.min, props.max));
|
||||
const safeMax = computed(() => Math.max(props.min, props.max));
|
||||
const valueRange = computed(() => Math.max(1, safeMax.value - safeMin.value));
|
||||
const safeMinRange = computed(() => Math.min(Math.max(0, Math.round(props.minRange)), valueRange.value));
|
||||
const normalizedValue = computed<[number, number]>(() => normalizeRange(props.modelValue));
|
||||
const startPercent = computed(() => valueToPercent(normalizedValue.value[0]));
|
||||
const endPercent = computed(() => valueToPercent(normalizedValue.value[1]));
|
||||
const selectionStyle = computed(() => ({
|
||||
left: `${startPercent.value}%`,
|
||||
width: `${Math.max(0, endPercent.value - startPercent.value)}%`
|
||||
}));
|
||||
const startThumbStyle = computed(() => ({
|
||||
left: `${startPercent.value}%`
|
||||
}));
|
||||
const endThumbStyle = computed(() => ({
|
||||
left: `${endPercent.value}%`
|
||||
}));
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopDocumentDrag();
|
||||
});
|
||||
|
||||
function handleThumbPointerDown(mode: "start" | "end", event: PointerEvent): void {
|
||||
startDocumentDrag(mode, event);
|
||||
}
|
||||
|
||||
function handleSelectionPointerDown(event: PointerEvent): void {
|
||||
startDocumentDrag("range", event);
|
||||
}
|
||||
|
||||
function handleTrackPointerDown(event: PointerEvent): void {
|
||||
const value = pointToValue(event.clientX);
|
||||
const [start, end] = normalizedValue.value;
|
||||
const targetMode: DragMode = Math.abs(value - start) <= Math.abs(value - end) ? "start" : "end";
|
||||
updateByMode(targetMode, value);
|
||||
startDocumentDrag(targetMode, event);
|
||||
}
|
||||
|
||||
function startDocumentDrag(mode: DragMode, event: PointerEvent): void {
|
||||
draggingMode.value = mode;
|
||||
dragStartX.value = event.clientX;
|
||||
dragStartValue.value = normalizedValue.value;
|
||||
window.addEventListener("pointermove", handleDocumentPointerMove);
|
||||
window.addEventListener("pointerup", handleDocumentPointerUp);
|
||||
window.addEventListener("pointercancel", handleDocumentPointerUp);
|
||||
}
|
||||
|
||||
function handleDocumentPointerMove(event: PointerEvent): void {
|
||||
const mode = draggingMode.value;
|
||||
if (!mode) {
|
||||
return;
|
||||
}
|
||||
if (mode === "range") {
|
||||
const nextValue = dragStartValue.value[0] + pointDeltaToStep(event.clientX - dragStartX.value);
|
||||
const width = dragStartValue.value[1] - dragStartValue.value[0];
|
||||
const start = clamp(nextValue, safeMin.value, safeMax.value - width);
|
||||
commitRange([start, start + width]);
|
||||
return;
|
||||
}
|
||||
updateByMode(mode, pointToValue(event.clientX));
|
||||
}
|
||||
|
||||
function handleDocumentPointerUp(): void {
|
||||
stopDocumentDrag();
|
||||
}
|
||||
|
||||
function stopDocumentDrag(): void {
|
||||
window.removeEventListener("pointermove", handleDocumentPointerMove);
|
||||
window.removeEventListener("pointerup", handleDocumentPointerUp);
|
||||
window.removeEventListener("pointercancel", handleDocumentPointerUp);
|
||||
draggingMode.value = undefined;
|
||||
}
|
||||
|
||||
function updateByMode(mode: "start" | "end", value: number): void {
|
||||
const [start, end] = normalizedValue.value;
|
||||
if (mode === "start") {
|
||||
commitRange([Math.min(value, end - safeMinRange.value), end]);
|
||||
return;
|
||||
}
|
||||
commitRange([start, Math.max(start + safeMinRange.value, value)]);
|
||||
}
|
||||
|
||||
function commitRange(value: [number, number]): void {
|
||||
const normalized = normalizeRange(value);
|
||||
emit("update:modelValue", normalized);
|
||||
emit("change", normalized);
|
||||
}
|
||||
|
||||
function normalizeRange(value: number[]): [number, number] {
|
||||
const rawStart = value[0] ?? safeMin.value;
|
||||
const rawEnd = value[1] ?? rawStart;
|
||||
const start = clamp(Math.round(Math.min(rawStart, rawEnd)), safeMin.value, safeMax.value);
|
||||
const end = clamp(Math.round(Math.max(rawStart, rawEnd)), start + safeMinRange.value, safeMax.value);
|
||||
if (safeMax.value < start + safeMinRange.value) {
|
||||
return [safeMin.value, safeMax.value];
|
||||
}
|
||||
return [Math.min(start, end - safeMinRange.value), end];
|
||||
}
|
||||
|
||||
function pointToValue(clientX: number): number {
|
||||
const rect = trackRef.value?.getBoundingClientRect();
|
||||
if (!rect || rect.width <= 0) {
|
||||
return safeMin.value;
|
||||
}
|
||||
const percent = clamp((clientX - rect.left) / rect.width, 0, 1);
|
||||
return Math.round(safeMin.value + percent * valueRange.value);
|
||||
}
|
||||
|
||||
function pointDeltaToStep(deltaX: number): number {
|
||||
const rect = trackRef.value?.getBoundingClientRect();
|
||||
if (!rect || rect.width <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(deltaX / rect.width * valueRange.value);
|
||||
}
|
||||
|
||||
function valueToPercent(value: number): number {
|
||||
return (clamp(value, safeMin.value, safeMax.value) - safeMin.value) / valueRange.value * 100;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart-range-slider {
|
||||
padding: .25rem .25rem 0 .25rem;
|
||||
|
||||
.track {
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
touch-action: none;
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: .25rem;
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: rgba(148, 163, 184, .24);
|
||||
border-radius: 999rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selection {
|
||||
height: .25rem;
|
||||
position: absolute;
|
||||
background: var(--td-brand-color);
|
||||
border-radius: 999rem;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
top: 50%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: .125rem solid var(--td-brand-color);
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 .125rem .5rem rgba(15, 23, 42, .18);
|
||||
border-radius: 999rem;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #52607a;
|
||||
font-size: .75rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.chart-range-slider {
|
||||
.track::before {
|
||||
background: rgba(148, 163, 184, .28);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
background: #10161f;
|
||||
box-shadow: 0 .125rem .5rem rgba(2, 6, 23, .38);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user