267 lines
6.9 KiB
Vue
267 lines
6.9 KiB
Vue
<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>
|