Initial commit
This commit is contained in:
97
docs/.vitepress/theme/components/Cafe.vue
Normal file
97
docs/.vitepress/theme/components/Cafe.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const links = [
|
||||
{ name: 'Java 笔记', path: '/java/', icon: '☕', description: 'Java技术栈学习笔记' },
|
||||
{ name: 'Vue 笔记', path: '/vue/', icon: '💚', description: 'Vue框架学习笔记' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cafe">
|
||||
<div class="cafe-header">
|
||||
<span class="cafe-icon">☕</span>
|
||||
<span class="cafe-title">咖啡厅</span>
|
||||
</div>
|
||||
<div class="cafe-links">
|
||||
<a
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
:href="link.path"
|
||||
class="cafe-link"
|
||||
>
|
||||
<span class="link-icon">{{ link.icon }}</span>
|
||||
<div class="link-info">
|
||||
<span class="link-name">{{ link.name }}</span>
|
||||
<span class="link-desc">{{ link.description }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cafe {
|
||||
height: 100%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cafe-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cafe-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cafe-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cafe-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cafe-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cafe-link:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.link-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.link-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
240
docs/.vitepress/theme/components/Calendar.vue
Normal file
240
docs/.vitepress/theme/components/Calendar.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
|
||||
const currentTime = ref(new Date())
|
||||
const selectedDate = ref(new Date())
|
||||
|
||||
let timer: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
currentTime.value = new Date()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
const timeString = computed(() => {
|
||||
const hours = currentTime.value.getHours().toString().padStart(2, '0')
|
||||
const minutes = currentTime.value.getMinutes().toString().padStart(2, '0')
|
||||
const seconds = currentTime.value.getSeconds().toString().padStart(2, '0')
|
||||
return `${hours}:${minutes}:${seconds}`
|
||||
})
|
||||
|
||||
const dateString = computed(() => {
|
||||
const year = currentTime.value.getFullYear()
|
||||
const month = (currentTime.value.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = currentTime.value.getDate().toString().padStart(2, '0')
|
||||
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
const weekDay = weekDays[currentTime.value.getDay()]
|
||||
return `${year}年${month}月${day}日 ${weekDay}`
|
||||
})
|
||||
|
||||
const currentYear = computed(() => selectedDate.value.getFullYear())
|
||||
const currentMonth = computed(() => selectedDate.value.getMonth())
|
||||
|
||||
const monthNames = [
|
||||
'一月', '二月', '三月', '四月', '五月', '六月',
|
||||
'七月', '八月', '九月', '十月', '十一月', '十二月'
|
||||
]
|
||||
|
||||
const weekDayNames = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
const daysInMonth = computed(() => {
|
||||
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate()
|
||||
})
|
||||
|
||||
const firstDayOfMonth = computed(() => {
|
||||
return new Date(currentYear.value, currentMonth.value, 1).getDay()
|
||||
})
|
||||
|
||||
const calendarDays = computed(() => {
|
||||
const days = []
|
||||
|
||||
for (let i = 0; i < firstDayOfMonth.value; i++) {
|
||||
days.push(null)
|
||||
}
|
||||
|
||||
for (let i = 1; i <= daysInMonth.value; i++) {
|
||||
days.push(i)
|
||||
}
|
||||
|
||||
return days
|
||||
})
|
||||
|
||||
function prevMonth() {
|
||||
selectedDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
selectedDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
|
||||
}
|
||||
|
||||
function isToday(day: number | null) {
|
||||
if (!day) return false
|
||||
return (
|
||||
day === currentTime.value.getDate() &&
|
||||
currentMonth.value === currentTime.value.getMonth() &&
|
||||
currentYear.value === currentTime.value.getFullYear()
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="calendar">
|
||||
<div class="calendar-header">
|
||||
<div class="time">{{ timeString }}</div>
|
||||
<div class="date">{{ dateString }}</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-body">
|
||||
<div class="month-nav">
|
||||
<button class="nav-btn" @click="prevMonth">‹</button>
|
||||
<span class="month-title">{{ monthNames[currentMonth] }} {{ currentYear }}</span>
|
||||
<button class="nav-btn" @click="nextMonth">›</button>
|
||||
</div>
|
||||
|
||||
<div class="weekdays">
|
||||
<div v-for="day in weekDayNames" :key="day" class="weekday">{{ day }}</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grid">
|
||||
<div
|
||||
v-for="(day, index) in calendarDays"
|
||||
:key="index"
|
||||
class="calendar-day"
|
||||
:class="{ 'calendar-day-today': isToday(day), 'calendar-day-empty': !day }"
|
||||
>
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.calendar {
|
||||
color: white;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 28px;
|
||||
font-weight: 200;
|
||||
letter-spacing: 2px;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.calendar-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.month-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-day:hover:not(.calendar-day-empty) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.calendar-day-today {
|
||||
background: rgba(0, 122, 255, 0.4);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendar-day-empty {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
63
docs/.vitepress/theme/components/DocSidebar.vue
Normal file
63
docs/.vitepress/theme/components/DocSidebar.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import sidebarData from '../data/sidebar.json'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const currentSidebar = computed(() => {
|
||||
const path = page.value.relativePath
|
||||
const pathPrefix = path.split('/')[0]
|
||||
return sidebarData.find(group => group.path === `/${pathPrefix}/`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar" v-if="currentSidebar">
|
||||
<h2 class="sidebar-title">{{ currentSidebar.title }} 笔记</h2>
|
||||
<ul class="sidebar-list">
|
||||
<li v-for="item in currentSidebar.items" :key="item.link">
|
||||
<a :href="item.link" class="sidebar-link">{{ item.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-list li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
204
docs/.vitepress/theme/components/Dock.vue
Normal file
204
docs/.vitepress/theme/components/Dock.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const dockItems = [
|
||||
{
|
||||
id: 'openlist',
|
||||
name: 'OpenList',
|
||||
url: 'https://openlist.wufangzhen.com',
|
||||
icon: 'M4 6h16M4 12h16M4 18h16'
|
||||
},
|
||||
{
|
||||
id: 'gitea',
|
||||
name: 'Gitea',
|
||||
url: 'https://gitea.wufangzhen.com',
|
||||
icon: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'
|
||||
},
|
||||
{
|
||||
id: 'baota',
|
||||
name: '宝塔',
|
||||
url: 'https://baota.wufangzhen.com',
|
||||
icon: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
|
||||
},
|
||||
{
|
||||
id: 'jrebel',
|
||||
name: 'JRebel',
|
||||
url: 'https://jrebel.wufangzhen.com',
|
||||
icon: 'M13 10V3L4 14h7v7l9-11h-7z'
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
name: 'OpenCode',
|
||||
url: 'https://opencode.wufangzhen.com',
|
||||
icon: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'
|
||||
}
|
||||
]
|
||||
|
||||
const isMobile = computed(() => settingsStore.isMobile)
|
||||
|
||||
function openSettings() {
|
||||
settingsStore.openWindow('settings')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dock" :class="{ 'dock-mobile': isMobile }">
|
||||
<div class="dock-container">
|
||||
<a
|
||||
v-for="item in dockItems"
|
||||
:key="item.id"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="dock-item"
|
||||
>
|
||||
<div class="dock-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path :d="item.icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dock-tooltip">{{ item.name }}</div>
|
||||
</a>
|
||||
<button class="dock-item settings-btn" @click="openSettings">
|
||||
<div class="dock-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dock-tooltip">设置</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dock {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dock-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dock-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dock-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dock-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dock-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dock-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dock-item:hover .dock-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dock-mobile {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.dock-mobile .dock-container {
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.dock-mobile .dock-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dock-mobile .dock-tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dock {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.dock-container {
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.dock-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dock-tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
491
docs/.vitepress/theme/components/Settings.vue
Normal file
491
docs/.vitepress/theme/components/Settings.vue
Normal file
@@ -0,0 +1,491 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const primaryColor = ref(settingsStore.theme.primaryColor)
|
||||
const backgroundColor = ref(settingsStore.theme.backgroundColor)
|
||||
const blurAmount = ref(settingsStore.theme.blurAmount)
|
||||
const videoBrightness = ref(settingsStore.theme.videoBrightness)
|
||||
|
||||
watch(() => settingsStore.theme, (newTheme) => {
|
||||
primaryColor.value = newTheme.primaryColor
|
||||
backgroundColor.value = newTheme.backgroundColor
|
||||
blurAmount.value = newTheme.blurAmount
|
||||
videoBrightness.value = newTheme.videoBrightness
|
||||
}, { deep: true })
|
||||
|
||||
const presetColors = [
|
||||
{ name: '蓝色', value: '#007AFF' },
|
||||
{ name: '绿色', value: '#34C759' },
|
||||
{ name: '橙色', value: '#FF9500' },
|
||||
{ name: '红色', value: '#FF3B30' },
|
||||
{ name: '紫色', value: '#AF52DE' },
|
||||
{ name: '粉色', value: '#FF2D55' }
|
||||
]
|
||||
|
||||
const backgroundPresets = [
|
||||
{ name: '深色', value: 'rgba(30, 30, 30, 0.7)' },
|
||||
{ name: '灰色', value: 'rgba(100, 100, 100, 0.7)' },
|
||||
{ name: '深蓝', value: 'rgba(20, 40, 80, 0.7)' }
|
||||
]
|
||||
|
||||
const videoList = computed(() => settingsStore.videoList)
|
||||
const currentVideo = computed(() => settingsStore.currentVideo)
|
||||
|
||||
const activeSection = ref('video')
|
||||
|
||||
const sections = [
|
||||
{ id: 'video', name: '视频背景', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' },
|
||||
{ id: 'brightness', name: '视频亮度', icon: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z' },
|
||||
{ id: 'theme', name: '主题颜色', icon: 'M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01' },
|
||||
{ id: 'background', name: '窗口背景', icon: 'M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z' },
|
||||
{ id: 'blur', name: '模糊程度', icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z' }
|
||||
]
|
||||
|
||||
function selectVideo(video: typeof currentVideo.value) {
|
||||
if (video) {
|
||||
settingsStore.setCurrentVideo(video)
|
||||
}
|
||||
}
|
||||
|
||||
function updatePrimaryColor(color: string) {
|
||||
primaryColor.value = color
|
||||
settingsStore.updateTheme({ primaryColor: color })
|
||||
}
|
||||
|
||||
function updateBackground(bg: string) {
|
||||
backgroundColor.value = bg
|
||||
settingsStore.updateTheme({ backgroundColor: bg })
|
||||
}
|
||||
|
||||
function updateBlur(amount: number) {
|
||||
blurAmount.value = amount
|
||||
settingsStore.updateTheme({ blurAmount: amount })
|
||||
}
|
||||
|
||||
function updateBrightness(value: number) {
|
||||
videoBrightness.value = value
|
||||
settingsStore.updateTheme({ videoBrightness: value })
|
||||
}
|
||||
|
||||
function resetSettings() {
|
||||
settingsStore.resetTheme()
|
||||
primaryColor.value = settingsStore.theme.primaryColor
|
||||
backgroundColor.value = settingsStore.theme.backgroundColor
|
||||
blurAmount.value = settingsStore.theme.blurAmount
|
||||
videoBrightness.value = settingsStore.theme.videoBrightness
|
||||
}
|
||||
|
||||
function getVideoThumb(thumb: string) {
|
||||
return thumb
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings">
|
||||
<div class="settings-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>设置</h3>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item-active': activeSection === section.id }"
|
||||
@click="activeSection = section.id"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path :d="section.icon" />
|
||||
</svg>
|
||||
<span>{{ section.name }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="reset-btn" @click="resetSettings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>恢复默认</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<div v-if="activeSection === 'video'" class="content-section">
|
||||
<h3 class="section-title">视频背景</h3>
|
||||
<p class="section-desc">选择喜欢的视频作为背景</p>
|
||||
<div class="video-list">
|
||||
<div
|
||||
v-for="video in videoList"
|
||||
:key="video.id"
|
||||
class="video-item"
|
||||
:class="{ 'video-item-active': currentVideo?.id === video.id }"
|
||||
@click="selectVideo(video)"
|
||||
>
|
||||
<img
|
||||
v-if="video.thumb"
|
||||
:src="getVideoThumb(video.thumb)"
|
||||
:alt="video.name"
|
||||
class="video-thumb"
|
||||
/>
|
||||
<div class="video-name">{{ video.name.replace('.mp4', '') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeSection === 'brightness'" class="content-section">
|
||||
<h3 class="section-title">视频亮度</h3>
|
||||
<p class="section-desc">调整视频遮罩亮度</p>
|
||||
<div class="blur-control">
|
||||
<div class="blur-preview" :style="{ background: 'rgba(0, 0, 0, ' + videoBrightness + ')' }">
|
||||
预览效果
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
:value="videoBrightness"
|
||||
@input="updateBrightness(Number(($event.target as HTMLInputElement).value))"
|
||||
class="blur-slider"
|
||||
/>
|
||||
<span class="slider-value">{{ Math.round(videoBrightness * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeSection === 'theme'" class="content-section">
|
||||
<h3 class="section-title">主题颜色</h3>
|
||||
<p class="section-desc">自定义主题强调色</p>
|
||||
<div class="color-grid">
|
||||
<button
|
||||
v-for="color in presetColors"
|
||||
:key="color.value"
|
||||
class="color-btn"
|
||||
:class="{ 'color-btn-active': primaryColor === color.value }"
|
||||
:style="{ backgroundColor: color.value }"
|
||||
@click="updatePrimaryColor(color.value)"
|
||||
:title="color.name"
|
||||
>
|
||||
<svg v-if="primaryColor === color.value" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="16" height="16">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeSection === 'background'" class="content-section">
|
||||
<h3 class="section-title">窗口背景</h3>
|
||||
<p class="section-desc">选择窗口背景样式</p>
|
||||
<div class="bg-grid">
|
||||
<button
|
||||
v-for="bg in backgroundPresets"
|
||||
:key="bg.value"
|
||||
class="bg-btn"
|
||||
:class="{ 'bg-btn-active': backgroundColor === bg.value }"
|
||||
:style="{ backgroundColor: bg.value }"
|
||||
@click="updateBackground(bg.value)"
|
||||
>
|
||||
{{ bg.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeSection === 'blur'" class="content-section">
|
||||
<h3 class="section-title">模糊程度</h3>
|
||||
<p class="section-desc">调整背景模糊效果</p>
|
||||
<div class="blur-control">
|
||||
<div class="blur-preview" :style="{ backdropFilter: `blur(${blurAmount}px)` }">
|
||||
预览效果
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="40"
|
||||
:value="blurAmount"
|
||||
@input="updateBlur(Number(($event.target as HTMLInputElement).value))"
|
||||
class="blur-slider"
|
||||
/>
|
||||
<span class="slider-value">{{ blurAmount }}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 180px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 12px 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item-active {
|
||||
background: v-bind('settingsStore.theme.primaryColor');
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 12px 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reset-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.video-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.video-item:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.video-item-active {
|
||||
border-color: v-bind('settingsStore.theme.primaryColor');
|
||||
}
|
||||
|
||||
.video-thumb {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-name {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.color-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.color-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.color-btn-active {
|
||||
border-color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bg-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bg-btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bg-btn-active {
|
||||
border-color: v-bind('settingsStore.theme.primaryColor');
|
||||
}
|
||||
|
||||
.blur-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.blur-preview {
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.blur-slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.blur-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: v-bind('settingsStore.theme.primaryColor');
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
107
docs/.vitepress/theme/components/TaoXin.vue
Normal file
107
docs/.vitepress/theme/components/TaoXin.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getRandomQuote, type TaoXinQuote } from '../data/quotes'
|
||||
|
||||
const quote = ref<TaoXinQuote | null>(null)
|
||||
|
||||
function refreshQuote() {
|
||||
quote.value = getRandomQuote()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshQuote()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="taoxin">
|
||||
<div class="taoxin-header">
|
||||
<span class="taoxin-icon">🌸</span>
|
||||
<span class="taoxin-title">桃信</span>
|
||||
</div>
|
||||
<div class="quote-container" @click="refreshQuote">
|
||||
<div class="quote-text" v-if="quote">
|
||||
"{{ quote.quote }}"
|
||||
</div>
|
||||
<div class="quote-meta" v-if="quote">
|
||||
<div class="quote-author">—— {{ quote.character }}</div>
|
||||
<div class="quote-chapter" v-if="quote?.chapter">{{ quote.chapter }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="refresh-hint">点击换一条</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.taoxin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.taoxin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.taoxin-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.taoxin-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quote-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 16px 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
font-style: italic;
|
||||
opacity: 0.95;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.quote-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.quote-author {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.quote-chapter {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.refresh-hint {
|
||||
font-size: 11px;
|
||||
opacity: 0.4;
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.taoxin:hover .refresh-hint {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
108
docs/.vitepress/theme/components/VideoBackground.vue
Normal file
108
docs/.vitepress/theme/components/VideoBackground.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { getVideoUrl } from '../utils'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const brightness = computed(() => settingsStore.theme.videoBrightness ?? 0.3)
|
||||
|
||||
onMounted(async () => {
|
||||
await settingsStore.fetchVideoList()
|
||||
})
|
||||
|
||||
watch(() => settingsStore.currentVideo, () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.load()
|
||||
isLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function onVideoCanPlay() {
|
||||
isLoading.value = false
|
||||
if (videoRef.value) {
|
||||
videoRef.value.play().catch(() => {})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="video-background">
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
<video
|
||||
ref="videoRef"
|
||||
class="video-element"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
@canplay="onVideoCanPlay"
|
||||
>
|
||||
<source
|
||||
v-if="settingsStore.currentVideo"
|
||||
:src="getVideoUrl(settingsStore.currentVideo.name)"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
<div class="video-overlay" :style="{ background: 'rgba(0, 0, 0, ' + brightness + ')' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.video-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #007AFF;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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