Initial commit
This commit is contained in:
266
docs/.vitepress/theme/components/DraggableWidget.vue
Normal file
266
docs/.vitepress/theme/components/DraggableWidget.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, useSlots, nextTick } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { clamp } from '../utils'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
title?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
width: 300,
|
||||
height: 200
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const slots = useSlots()
|
||||
|
||||
const elementRef = ref<HTMLElement | null>(null)
|
||||
const isMounted = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const dragStartPos = ref({ x: 0, y: 0 })
|
||||
const elementStartPos = ref({ x: 0, y: 0 })
|
||||
|
||||
const isMobile = computed(() => settingsStore.isMobile)
|
||||
|
||||
const widgetState = computed(() => {
|
||||
return settingsStore.widgets.find(w => w.id === props.id)
|
||||
})
|
||||
|
||||
const position = computed(() => {
|
||||
if (isMobile.value || !isMounted.value) return { x: 0, y: 0 }
|
||||
const storedPos = widgetState.value?.position
|
||||
if (storedPos && storedPos.x >= 0 && storedPos.y >= 0) {
|
||||
return storedPos
|
||||
}
|
||||
return getDefaultPosition()
|
||||
})
|
||||
|
||||
const order = computed(() => {
|
||||
return widgetState.value?.order ?? 0
|
||||
})
|
||||
|
||||
const blurAmount = computed(() => settingsStore.theme.blurAmount ?? 20)
|
||||
const backgroundColor = computed(() => settingsStore.theme.backgroundColor ?? 'rgba(30, 30, 30, 0.7)')
|
||||
|
||||
function getDefaultPosition() {
|
||||
const screenWidth = typeof window !== 'undefined' ? window.innerWidth : 1920
|
||||
const screenHeight = typeof window !== 'undefined' ? window.innerHeight : 1080
|
||||
|
||||
const positions: Record<string, { x: number; y: number }> = {
|
||||
calendar: { x: screenWidth - 340, y: 20 },
|
||||
taoxin: { x: screenWidth - 320, y: screenHeight - 380 },
|
||||
cafe: { x: screenWidth - 660, y: 20 }
|
||||
}
|
||||
|
||||
return positions[props.id] ?? { x: 20, y: 20 }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
const storedPos = widgetState.value?.position
|
||||
if (!storedPos || storedPos.x < 0 || storedPos.y < 0) {
|
||||
const defaultPos = getDefaultPosition()
|
||||
settingsStore.updateWidgetPosition(props.id, defaultPos)
|
||||
}
|
||||
|
||||
isMounted.value = true
|
||||
})
|
||||
|
||||
function handleDragStart(e: MouseEvent | TouchEvent, clientX: number, clientY: number) {
|
||||
if (isMobile.value || !isMounted.value) return
|
||||
e.preventDefault()
|
||||
isDragging.value = true
|
||||
dragStartPos.value = { x: clientX, y: clientY }
|
||||
elementStartPos.value = { ...position.value }
|
||||
|
||||
if (e instanceof MouseEvent) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
} else {
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
handleDragStart(e, e.clientX, e.clientY)
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
if (e.touches.length !== 1) return
|
||||
handleDragStart(e, e.touches[0].clientX, e.touches[0].clientY)
|
||||
}
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
updatePosition(e.clientX, e.clientY)
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent) {
|
||||
if (!isDragging.value || e.touches.length !== 1) return
|
||||
e.preventDefault()
|
||||
updatePosition(e.touches[0].clientX, e.touches[0].clientY)
|
||||
}
|
||||
|
||||
function updatePosition(clientX: number, clientY: number) {
|
||||
const deltaX = clientX - dragStartPos.value.x
|
||||
const deltaY = clientY - dragStartPos.value.y
|
||||
|
||||
const maxX = window.innerWidth - props.width - 20
|
||||
const maxY = window.innerHeight - props.height - 100
|
||||
|
||||
const newX = clamp(elementStartPos.value.x + deltaX, 20, maxX)
|
||||
const newY = clamp(elementStartPos.value.y + deltaY, 20, maxY)
|
||||
|
||||
settingsStore.updateWidgetPosition(props.id, { x: newX, y: newY })
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isMobile"
|
||||
class="widget-mobile"
|
||||
:style="{ order: order }"
|
||||
>
|
||||
<div class="widget-mobile-header" v-if="title">
|
||||
<span class="widget-title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="widget-mobile-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isMounted"
|
||||
ref="elementRef"
|
||||
class="widget"
|
||||
:class="{ 'widget-dragging': isDragging }"
|
||||
:style="{
|
||||
left: position.x + 'px',
|
||||
top: position.y + 'px',
|
||||
width: width + 'px',
|
||||
height: height + 'px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="widget-drag-handle"
|
||||
@mousedown="handleMouseDown"
|
||||
@touchstart="handleTouchStart"
|
||||
>
|
||||
<div class="drag-indicator"></div>
|
||||
<span class="widget-title" v-if="title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="widget-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.widget {
|
||||
position: absolute;
|
||||
border-radius: 16px;
|
||||
background: v-bind('settingsStore.theme.backgroundColor');
|
||||
backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
|
||||
-webkit-backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.widget:hover {
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.widget-dragging {
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.widget-drag-handle {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: grab;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.widget-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.widget-mobile {
|
||||
width: calc(100% - 20px);
|
||||
margin: 10px;
|
||||
border-radius: 16px;
|
||||
background: v-bind('settingsStore.theme.backgroundColor');
|
||||
backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
|
||||
-webkit-backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.widget-mobile-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.widget-mobile-content {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user