Initial commit

This commit is contained in:
wfz
2026-05-13 16:24:00 +08:00
commit 5728d3cbda
55 changed files with 37267 additions and 0 deletions

View 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>