Initial commit
This commit is contained in:
365
docs/.vitepress/theme/components/Window.vue
Normal file
365
docs/.vitepress/theme/components/Window.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { getOS, clamp } from '../utils'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
title?: string
|
||||
defaultWidth?: number
|
||||
defaultHeight?: number
|
||||
minWidth?: number
|
||||
minHeight?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
defaultWidth: 800,
|
||||
defaultHeight: 600,
|
||||
minWidth: 400,
|
||||
minHeight: 300
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
|
||||
|
||||
const isDragging = ref(false)
|
||||
const isResizing = ref(false)
|
||||
const resizeDirection = ref<ResizeDirection | null>(null)
|
||||
const dragStartPos = ref({ x: 0, y: 0 })
|
||||
const elementStartPos = ref({ x: 0, y: 0 })
|
||||
const elementStartSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const os = computed(() => getOS())
|
||||
const isOpen = computed(() => settingsStore.isWindowOpen(props.id))
|
||||
const windowState = computed(() => settingsStore.getWindowState(props.id))
|
||||
|
||||
const position = computed(() => windowState.value?.position ?? { x: 100, y: 100 })
|
||||
const size = computed(() => windowState.value?.size ?? { width: props.defaultWidth, height: props.defaultHeight })
|
||||
const zIndex = computed(() => windowState.value?.zIndex ?? 100)
|
||||
|
||||
const blurAmount = computed(() => settingsStore.theme.blurAmount ?? 20)
|
||||
const backgroundColor = computed(() => settingsStore.theme.backgroundColor ?? 'rgba(30, 30, 30, 0.7)')
|
||||
|
||||
function close() {
|
||||
settingsStore.closeWindow(props.id)
|
||||
}
|
||||
|
||||
function bringToFront() {
|
||||
settingsStore.bringWindowToFront(props.id)
|
||||
}
|
||||
|
||||
function handleDragStart(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).closest('.window-close-btn')) return
|
||||
if ((e.target as HTMLElement).closest('.resize-handle')) return
|
||||
e.preventDefault()
|
||||
bringToFront()
|
||||
isDragging.value = true
|
||||
dragStartPos.value = { x: e.clientX, y: e.clientY }
|
||||
elementStartPos.value = { ...position.value }
|
||||
document.addEventListener('mousemove', handleDragMove)
|
||||
document.addEventListener('mouseup', handleDragEnd)
|
||||
}
|
||||
|
||||
function handleDragMove(e: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const deltaX = e.clientX - dragStartPos.value.x
|
||||
const deltaY = e.clientY - dragStartPos.value.y
|
||||
|
||||
const maxX = window.innerWidth - size.value.width - 20
|
||||
const maxY = window.innerHeight - size.value.height - 100
|
||||
|
||||
const newX = clamp(elementStartPos.value.x + deltaX, 20, maxX)
|
||||
const newY = clamp(elementStartPos.value.y + deltaY, 20, maxY)
|
||||
|
||||
settingsStore.updateWindowPosition(props.id, { x: newX, y: newY })
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
}
|
||||
|
||||
function handleResizeStart(e: MouseEvent, direction: ResizeDirection) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
bringToFront()
|
||||
isResizing.value = true
|
||||
resizeDirection.value = direction
|
||||
dragStartPos.value = { x: e.clientX, y: e.clientY }
|
||||
elementStartPos.value = { ...position.value }
|
||||
elementStartSize.value = { ...size.value }
|
||||
document.addEventListener('mousemove', handleResizeMove)
|
||||
document.addEventListener('mouseup', handleResizeEnd)
|
||||
}
|
||||
|
||||
function handleResizeMove(e: MouseEvent) {
|
||||
if (!isResizing.value || !resizeDirection.value) return
|
||||
|
||||
const deltaX = e.clientX - dragStartPos.value.x
|
||||
const deltaY = e.clientY - dragStartPos.value.y
|
||||
const dir = resizeDirection.value
|
||||
|
||||
let newWidth = elementStartSize.value.width
|
||||
let newHeight = elementStartSize.value.height
|
||||
let newX = elementStartPos.value.x
|
||||
let newY = elementStartPos.value.y
|
||||
|
||||
if (dir.includes('e')) {
|
||||
newWidth = clamp(elementStartSize.value.width + deltaX, props.minWidth, window.innerWidth - position.value.x - 20)
|
||||
}
|
||||
if (dir.includes('w')) {
|
||||
const maxDeltaX = elementStartSize.value.width - props.minWidth
|
||||
const actualDeltaX = clamp(deltaX, -maxDeltaX, elementStartPos.value.x - 20)
|
||||
newWidth = elementStartSize.value.width - actualDeltaX
|
||||
newX = elementStartPos.value.x + actualDeltaX
|
||||
}
|
||||
if (dir.includes('s')) {
|
||||
newHeight = clamp(elementStartSize.value.height + deltaY, props.minHeight, window.innerHeight - position.value.y - 100)
|
||||
}
|
||||
if (dir.includes('n')) {
|
||||
const maxDeltaY = elementStartSize.value.height - props.minHeight
|
||||
const actualDeltaY = clamp(deltaY, -maxDeltaY, elementStartPos.value.y - 20)
|
||||
newHeight = elementStartSize.value.height - actualDeltaY
|
||||
newY = elementStartPos.value.y + actualDeltaY
|
||||
}
|
||||
|
||||
settingsStore.updateWindowPosition(props.id, { x: newX, y: newY })
|
||||
settingsStore.updateWindowSize(props.id, { width: newWidth, height: newHeight })
|
||||
}
|
||||
|
||||
function handleResizeEnd() {
|
||||
isResizing.value = false
|
||||
resizeDirection.value = null
|
||||
document.removeEventListener('mousemove', handleResizeMove)
|
||||
document.removeEventListener('mouseup', handleResizeEnd)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
document.removeEventListener('mousemove', handleResizeMove)
|
||||
document.removeEventListener('mouseup', handleResizeEnd)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="window-fade">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="window"
|
||||
:class="{ 'window-dragging': isDragging }"
|
||||
:style="{
|
||||
left: position.x + 'px',
|
||||
top: position.y + 'px',
|
||||
width: size.width + 'px',
|
||||
height: size.height + 'px',
|
||||
zIndex: zIndex,
|
||||
background: backgroundColor,
|
||||
backdropFilter: 'blur(' + blurAmount + 'px)',
|
||||
WebkitBackdropFilter: 'blur(' + blurAmount + 'px)'
|
||||
}"
|
||||
@mousedown="bringToFront"
|
||||
>
|
||||
<div class="window-header" @mousedown="handleDragStart">
|
||||
<div class="window-controls" :class="{ 'controls-left': os === 'mac', 'controls-right': os !== 'mac' }">
|
||||
<button class="window-close-btn" @click="close" title="关闭">
|
||||
<svg v-if="os === 'mac'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="window-title">{{ title }}</span>
|
||||
<div class="window-controls-placeholder"></div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="resize-handle resize-n" @mousedown="(e) => handleResizeStart(e, 'n')"></div>
|
||||
<div class="resize-handle resize-s" @mousedown="(e) => handleResizeStart(e, 's')"></div>
|
||||
<div class="resize-handle resize-e" @mousedown="(e) => handleResizeStart(e, 'e')"></div>
|
||||
<div class="resize-handle resize-w" @mousedown="(e) => handleResizeStart(e, 'w')"></div>
|
||||
<div class="resize-handle resize-ne" @mousedown="(e) => handleResizeStart(e, 'ne')"></div>
|
||||
<div class="resize-handle resize-nw" @mousedown="(e) => handleResizeStart(e, 'nw')"></div>
|
||||
<div class="resize-handle resize-se" @mousedown="(e) => handleResizeStart(e, 'se')"></div>
|
||||
<div class="resize-handle resize-sw" @mousedown="(e) => handleResizeStart(e, 'sw')"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.window {
|
||||
position: fixed;
|
||||
border-radius: 12px;
|
||||
background: v-bind('backgroundColor');
|
||||
backdrop-filter: blur(v-bind('blurAmount + "px"'));
|
||||
-webkit-backdrop-filter: blur(v-bind('blurAmount + "px"'));
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.window-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.window-controls-left {
|
||||
order: -1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.window-controls-right {
|
||||
order: 1;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.window-close-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #ff5f57;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.window-close-btn:hover {
|
||||
background: #ff3b30;
|
||||
}
|
||||
|
||||
.window-close-btn svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.window-close-btn:hover svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.window-controls-placeholder {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resize-n, .resize-s {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.resize-n {
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.resize-s {
|
||||
bottom: -3px;
|
||||
}
|
||||
|
||||
.resize-e, .resize-w {
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.resize-e {
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
.resize-w {
|
||||
left: -3px;
|
||||
}
|
||||
|
||||
.resize-ne, .resize-nw, .resize-se, .resize-sw {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.resize-ne {
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resize-nw {
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.resize-se {
|
||||
bottom: -3px;
|
||||
right: -3px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.resize-sw {
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.window-fade-enter-active,
|
||||
.window-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.window-fade-enter-from,
|
||||
.window-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user