change
This commit is contained in:
parent
293fa3df42
commit
80ef1e158a
11
index.html
11
index.html
|
@ -1,9 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -7,6 +7,8 @@ import SettingsButton from './settings/SettingsButton'
|
||||||
import SettingsOverlay from './settings/SettingsOverlay'
|
import SettingsOverlay from './settings/SettingsOverlay'
|
||||||
import Sider from './layout/sider'
|
import Sider from './layout/sider'
|
||||||
import Dock from './layout/dock'
|
import Dock from './layout/dock'
|
||||||
|
import DirModal from './layout/grid/DirModal'
|
||||||
|
import GlobalMenu from './layout/GlobalMenu'
|
||||||
import LoginModal from './user/LoginModal'
|
import LoginModal from './user/LoginModal'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import asyncLoader from './utils/asyncLoader'
|
import asyncLoader from './utils/asyncLoader'
|
||||||
|
@ -34,6 +36,8 @@ const layout = useLayoutStore()
|
||||||
<div class="fixed z-40 right-[14%] top-8">
|
<div class="fixed z-40 right-[14%] top-8">
|
||||||
<Fox />
|
<Fox />
|
||||||
</div>
|
</div>
|
||||||
|
<DirModal />
|
||||||
|
<GlobalMenu />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, defineComponent, onUnmounted, reactive } from 'vue'
|
||||||
|
import type { Block } from './layout.types'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import useLayoutStore from './useLayoutStore'
|
||||||
|
|
||||||
|
const defaultDisplay = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
type: 'global' as Block | 'global' | 'dock'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore('menu', () => {
|
||||||
|
const display = reactive(defaultDisplay)
|
||||||
|
const mPos = {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
const handle = (e: MouseEvent) => {
|
||||||
|
mPos.x = e.pageX
|
||||||
|
mPos.y = e.pageY
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', handle)
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('mousemove', handle)
|
||||||
|
})
|
||||||
|
const open = (type: (typeof defaultDisplay)['type']) => {
|
||||||
|
display.x = Math.min(window.innerWidth - 140, mPos.x)
|
||||||
|
display.y = Math.min(window.innerHeight - 240, mPos.y)
|
||||||
|
display.type = type
|
||||||
|
}
|
||||||
|
const show = computed(() => display.x > 0 && display.y > 0)
|
||||||
|
const dismiss = () => {
|
||||||
|
display.x = 0
|
||||||
|
display.y = 0
|
||||||
|
display.type = 'global'
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
display,
|
||||||
|
open,
|
||||||
|
dismiss,
|
||||||
|
show
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Item = defineComponent({
|
||||||
|
props: {
|
||||||
|
alert: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
click: () => true
|
||||||
|
},
|
||||||
|
setup(props, ctx) {
|
||||||
|
return () => (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'px-4 py-2 text-sm tracking-widest w-full overflow-hidden text-ellipsis whitespace-nowrap break-all transition-all rounded-lg cursor-pointer mb-2',
|
||||||
|
{
|
||||||
|
'bg-red-500/80 hover:bg-red-500 text-white': props.alert,
|
||||||
|
'bg-white/80 hover:bg-white text-black/80': !props.alert
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
ctx.emit('click')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ctx.slots.default?.()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineComponent(() => {
|
||||||
|
const menu = useMenuStore()
|
||||||
|
const layout = useLayoutStore()
|
||||||
|
return () =>
|
||||||
|
menu.show && (
|
||||||
|
<div
|
||||||
|
v-outside-click={() => {
|
||||||
|
console.log(2333)
|
||||||
|
menu.dismiss()
|
||||||
|
}}
|
||||||
|
class="fixed px-2 pt-4 pb-2 bg-white/60 backdrop-blur shadow-lg rounded-lg overflow-hidden w-[120px]"
|
||||||
|
style={{
|
||||||
|
zIndex: '999',
|
||||||
|
left: menu.display.x + 'px',
|
||||||
|
top: menu.display.y + 'px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
if (menu.display.type === 'global') {
|
||||||
|
// 全局菜单
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
if (menu.display.type === 'dock') {
|
||||||
|
// dock 栏
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
const block = menu.display.type
|
||||||
|
if (!block.link) {
|
||||||
|
// 小组件
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Item
|
||||||
|
alert
|
||||||
|
onClick={() => {
|
||||||
|
// 删除链接
|
||||||
|
const idx = layout.currentPage.list.findIndex((el) => el.id === block.id)
|
||||||
|
if (idx < 0) return
|
||||||
|
layout.currentPage.list.splice(idx, 1)
|
||||||
|
menu.dismiss()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (block.link.startsWith('id:')) {
|
||||||
|
// 文件夹
|
||||||
|
const id = block.link.slice(3)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Item
|
||||||
|
onClick={() => {
|
||||||
|
block.w = 1
|
||||||
|
block.h = 1
|
||||||
|
menu.dismiss()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
小
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
onClick={() => {
|
||||||
|
block.w = 2
|
||||||
|
block.h = 2
|
||||||
|
menu.dismiss()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
大
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
alert
|
||||||
|
onClick={() => {
|
||||||
|
// 删除文件夹
|
||||||
|
const idx = layout.currentPage.list.findIndex((el) => el.id === block.id)
|
||||||
|
if (idx < 0) return
|
||||||
|
layout.currentPage.list.splice(idx, 1)
|
||||||
|
delete layout.state.dir[id]
|
||||||
|
menu.dismiss()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 链接
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Item>测试</Item>
|
||||||
|
<Item
|
||||||
|
alert
|
||||||
|
onClick={() => {
|
||||||
|
// 删除链接
|
||||||
|
let idx = layout.currentPage.list.findIndex((el) => el.id === block.id)
|
||||||
|
if (idx < 0) {
|
||||||
|
// 查找文件夹
|
||||||
|
idx = layout.state.dir[layout.openDir]?.list?.findIndex(
|
||||||
|
(el) => el.id === block.id
|
||||||
|
)
|
||||||
|
if (idx === undefined || idx < 0) {
|
||||||
|
// 查找 dock
|
||||||
|
idx = layout.state.dock.findIndex((el) => el?.id === block.id)
|
||||||
|
if (idx >= 0) {
|
||||||
|
layout.state.dock[idx] = null
|
||||||
|
menu.dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const list = layout.state.dir[layout.openDir].list
|
||||||
|
list.splice(idx, 1)
|
||||||
|
layout.checkDir(layout.openDir)
|
||||||
|
menu.dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
layout.currentPage.list.splice(idx, 1)
|
||||||
|
menu.dismiss()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
|
@ -1,27 +1,41 @@
|
||||||
import { computed, defineComponent } from 'vue'
|
import { computed, defineComponent, ref, toRaw } from 'vue'
|
||||||
import useLayoutStore from '../useLayoutStore'
|
import useLayoutStore from '../useLayoutStore'
|
||||||
import useSortable, { dragging } from '../grid/useSortable'
|
import useSortable, { dragging, resetDragging } from '../grid/useSortable'
|
||||||
import LinkBlock from '../grid/LinkBlock'
|
import LinkBlock from '../grid/LinkBlock'
|
||||||
|
|
||||||
export default defineComponent(() => {
|
export default defineComponent(() => {
|
||||||
const layout = useLayoutStore()
|
const layout = useLayoutStore()
|
||||||
const container = useSortable(
|
const container = useSortable(
|
||||||
computed(() => layout.state.dock),
|
computed(() => layout.state.dock),
|
||||||
'dock'
|
ref('dock')
|
||||||
)
|
)
|
||||||
|
const current = ref(-1)
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div
|
<div
|
||||||
class="fixed bottom-4 left-1/2 -translate-x-1/2 p-4 rounded-lg bg-white/60 backdrop-blur flex gap-4 shadow-lg"
|
class="fixed bottom-4 left-1/2 -translate-x-1/2 p-4 rounded-lg bg-white/60 backdrop-blur flex gap-4 shadow-lg"
|
||||||
ref={container}
|
ref={container}
|
||||||
|
onMouseleave={() => {
|
||||||
|
current.value = -1
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{layout.state.dockLabels.split('').map((l, i) => {
|
{layout.state.dockLabels.split('').map((l, i) => {
|
||||||
const block = layout.state.dock[i]
|
const block = layout.state.dock[i]
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={'block-' + i + (block?.id || '')}
|
key={'block-' + i + (block?.id || '')}
|
||||||
class="w-[54px] h-[54px] rounded-lg overflow-hidden relative cursor-pointer"
|
class="w-[54px] h-[54px] rounded-lg overflow-hidden relative cursor-pointer transition-all"
|
||||||
|
style={
|
||||||
|
current.value >= 0
|
||||||
|
? {
|
||||||
|
transform: `translateY(${current.value === i - 1 || current.value === i + 1 ? '-5%' : current.value === i ? '-10%' : '0'}) scale(${current.value === i - 1 || current.value === i + 1 ? 1.1 : current.value === i ? 1.2 : 1})`
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
id={block?.id || ''}
|
id={block?.id || ''}
|
||||||
|
onMousemove={() => {
|
||||||
|
current.value = i
|
||||||
|
}}
|
||||||
onDragover={(e) => e.preventDefault()}
|
onDragover={(e) => e.preventDefault()}
|
||||||
onDrop={() => {
|
onDrop={() => {
|
||||||
// 处理移入(当前有内容不可移入)
|
// 处理移入(当前有内容不可移入)
|
||||||
|
@ -31,13 +45,19 @@ export default defineComponent(() => {
|
||||||
if (oldIdx < 0) return
|
if (oldIdx < 0) return
|
||||||
const block = layout.currentPage.list[oldIdx]
|
const block = layout.currentPage.list[oldIdx]
|
||||||
if (!block) return
|
if (!block) return
|
||||||
layout.state.dock[i] = block
|
layout.state.dock[i] = toRaw(block)
|
||||||
layout.currentPage.list.splice(oldIdx, 1)
|
layout.currentPage.list.splice(oldIdx, 1)
|
||||||
return
|
resetDragging()
|
||||||
}
|
} else if (dragging.type && dragging.type !== 'dock') {
|
||||||
if (dragging.type && dragging.type !== 'dock') {
|
const list = layout.state.dir[dragging.type]?.list
|
||||||
// TODO: 文件夹
|
if (!list) return
|
||||||
return
|
const idx = list.findIndex((el) => el.id === dragging.id)
|
||||||
|
if (idx < 0) return
|
||||||
|
const block = list[idx]
|
||||||
|
layout.state.dock[i] = toRaw(block)
|
||||||
|
list.splice(idx, 1)
|
||||||
|
layout.checkDir(dragging.type)
|
||||||
|
resetDragging()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { defineComponent, ref, toRaw } from 'vue'
|
||||||
|
import type { Block } from '../layout.types'
|
||||||
|
import { dragging, resetDragging } from './useSortable'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import useLayoutStore from '../useLayoutStore'
|
||||||
|
import LinkBlock from './LinkBlock'
|
||||||
|
import DirBlock from './DirBlock'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
block: {
|
||||||
|
type: Object as () => Block,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
idx: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const layout = useLayoutStore()
|
||||||
|
let it = 0
|
||||||
|
const hover = ref(false)
|
||||||
|
return () => (
|
||||||
|
<div
|
||||||
|
class="w-full h-full p-[var(--block-padding)] relative rounded-lg"
|
||||||
|
key={props.block.id}
|
||||||
|
id={props.block.id}
|
||||||
|
style={{
|
||||||
|
gridColumn: `span ${props.block.w}`,
|
||||||
|
gridRow: `span ${props.block.h}`,
|
||||||
|
transition: 'border .3s',
|
||||||
|
border: hover.value ? '2px solid rgba(255,255,255,.5)' : '2px solid rgba(255,255,255,0)'
|
||||||
|
}}
|
||||||
|
data-transportable={props.block.link && !props.block.link.startsWith('id:') ? '1' : ''}
|
||||||
|
onDragover={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
clearTimeout(it)
|
||||||
|
hover.value = true
|
||||||
|
it = setTimeout(() => {
|
||||||
|
hover.value = false
|
||||||
|
}, 300)
|
||||||
|
}}
|
||||||
|
onDrop={() => {
|
||||||
|
// 处理移入
|
||||||
|
if (!dragging.id) return
|
||||||
|
if (dragging.type === 'dock') {
|
||||||
|
const oldIdx = layout.state.dock.findIndex((el) => el?.id === dragging.id)
|
||||||
|
if (oldIdx < 0) return
|
||||||
|
const block = layout.state.dock[oldIdx]
|
||||||
|
if (!block) return
|
||||||
|
layout.currentPage.list.splice(props.idx, 0, block)
|
||||||
|
layout.state.dock[oldIdx] = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dragging.type === 'page' && dragging.id !== props.block.id) {
|
||||||
|
// 合并为文件夹
|
||||||
|
const link = props.block.link
|
||||||
|
// 小组件无法合并
|
||||||
|
if (!link) return
|
||||||
|
const oldIdx = layout.currentPage.list.findIndex((el) => el.id === dragging.id)
|
||||||
|
if (oldIdx < 0) return
|
||||||
|
const oldBlock = layout.currentPage.list[oldIdx]
|
||||||
|
// 文件夹不能移入文件夹
|
||||||
|
if (!oldBlock || oldBlock.link.startsWith('id:')) return
|
||||||
|
if (link.startsWith('id:')) {
|
||||||
|
// 本身就是文件夹
|
||||||
|
const id = link.slice(3)
|
||||||
|
const dir = layout.state.dir[id]
|
||||||
|
if (!dir) return
|
||||||
|
dir.list.push(toRaw(oldBlock))
|
||||||
|
layout.currentPage.list.splice(oldIdx, 1)
|
||||||
|
resetDragging()
|
||||||
|
} else {
|
||||||
|
// 本身不是文件夹
|
||||||
|
const id = props.block.id
|
||||||
|
layout.state.dir[id] = {
|
||||||
|
label: props.block.label,
|
||||||
|
list: [toRaw(props.block), toRaw(oldBlock)]
|
||||||
|
}
|
||||||
|
layout.currentPage.list.splice(props.idx, 1)
|
||||||
|
layout.currentPage.list.splice(props.idx, 0, {
|
||||||
|
id: uuid(),
|
||||||
|
link: `id:${id}`,
|
||||||
|
name: '',
|
||||||
|
label: '新建文件夹',
|
||||||
|
icon: '',
|
||||||
|
text: '',
|
||||||
|
background: '',
|
||||||
|
color: '',
|
||||||
|
w: 1,
|
||||||
|
h: 1
|
||||||
|
})
|
||||||
|
layout.currentPage.list.splice(oldIdx, 1)
|
||||||
|
resetDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dragging.type && dragging.type !== 'dock' && dragging.type !== 'page') {
|
||||||
|
const list = layout.state.dir[dragging.type]?.list
|
||||||
|
if (!list) return
|
||||||
|
const idx = list.findIndex((el) => el.id === dragging.id)
|
||||||
|
if (idx < 0) return
|
||||||
|
const block = list[idx]
|
||||||
|
layout.currentPage.list.splice(props.idx, 0, toRaw(block))
|
||||||
|
list.splice(idx, 1)
|
||||||
|
layout.checkDir(dragging.type)
|
||||||
|
resetDragging()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full overflow-hidden relative cursor-pointer shadow-lg"
|
||||||
|
style={{
|
||||||
|
borderRadius: `calc(var(--block-radius) * var(--block-size))`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.block.link ? (
|
||||||
|
props.block.link.startsWith('id:') ? (
|
||||||
|
// 文件夹
|
||||||
|
<DirBlock block={props.block} big={props.block.w !== 1 || props.block.h !== 1} />
|
||||||
|
) : (
|
||||||
|
// 链接
|
||||||
|
<LinkBlock block={props.block} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
// 小组件
|
||||||
|
<div></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 -bottom-2 text-sm text-white text-center w-full overflow-hidden text-ellipsis whitespace-nowrap break-all font-bold"
|
||||||
|
style="text-shadow: 0 0 4px rgba(0,0,0,.6)"
|
||||||
|
>
|
||||||
|
{layout.getLabel(props.block)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { computed, defineComponent } from 'vue'
|
||||||
|
import useLayoutStore from '../useLayoutStore'
|
||||||
|
import type { Block } from '../layout.types'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import LinkBlock from './LinkBlock'
|
||||||
|
import { useMenuStore } from '../GlobalMenu'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
block: {
|
||||||
|
type: Object as () => Block,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
big: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const layout = useLayoutStore()
|
||||||
|
const selectedDir = computed(() => {
|
||||||
|
const link = props.block.link
|
||||||
|
const id = link.slice(3)
|
||||||
|
const dir = layout.state.dir[id]
|
||||||
|
return (
|
||||||
|
dir || {
|
||||||
|
label: '',
|
||||||
|
list: []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const menu = useMenuStore()
|
||||||
|
return () => (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
layout.openDir = props.block.link.slice(3)
|
||||||
|
}}
|
||||||
|
class={clsx('w-full h-full bg-white/60 backdrop-blur grid', {
|
||||||
|
'grid-rows-3 grid-cols-3 gap-[6%] p-[8%]': props.big,
|
||||||
|
'grid-rows-2 grid-cols-2 gap-[8%] p-[10%]': !props.big
|
||||||
|
})}
|
||||||
|
onContextmenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
menu.open(props.block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedDir.value.list
|
||||||
|
.filter((_, idx) => idx < (props.big ? 9 : 4))
|
||||||
|
.map((el) => (
|
||||||
|
<div class="w-full h-full rounded-lg overflow-hidden">
|
||||||
|
<LinkBlock block={el} brief />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { computed, defineComponent, ref, Transition, watch } from 'vue'
|
||||||
|
import useLayoutStore from '../useLayoutStore'
|
||||||
|
import useSortable from './useSortable'
|
||||||
|
import LinkBlock from './LinkBlock'
|
||||||
|
import type { Block } from '../layout.types'
|
||||||
|
|
||||||
|
export default defineComponent(() => {
|
||||||
|
const layout = useLayoutStore()
|
||||||
|
const dir = ref({ label: '', list: [] as Block[] })
|
||||||
|
// 因为有拖拽,关闭弹框不能使内容消失
|
||||||
|
watch(
|
||||||
|
() => layout.state.dir[layout.openDir],
|
||||||
|
(val) => {
|
||||||
|
if (val) dir.value = val
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const container = useSortable(
|
||||||
|
computed(() => dir.value.list),
|
||||||
|
computed(() => layout.openDir)
|
||||||
|
)
|
||||||
|
return () => (
|
||||||
|
<Transition>
|
||||||
|
<div
|
||||||
|
class="fixed w-full h-screen left-0 top-0 flex flex-col justify-center items-center"
|
||||||
|
v-show={layout.openDir}
|
||||||
|
style="z-index: 300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-0 w-full h-full bg-black/40 backdrop-blur"
|
||||||
|
onClick={() => {
|
||||||
|
layout.openDir = ''
|
||||||
|
}}
|
||||||
|
onDragenter={() => {
|
||||||
|
layout.openDir = ''
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
class="relative h-[40px] mb-2 w-[240px] bg-transparent outline-none border-none text-center text-white"
|
||||||
|
v-model={dir.value.label}
|
||||||
|
/>
|
||||||
|
<div class="relative w-[50%] min-h-[280px] max-h-[60vh] overflow-y-auto bg-white rounded-lg shadow-lg p-2">
|
||||||
|
<div
|
||||||
|
class="w-full grid justify-center grid-flow-row-dense pb-[200px]"
|
||||||
|
style="grid-template-columns:repeat(auto-fill, var(--block-size));grid-auto-rows:var(--block-size)"
|
||||||
|
ref={container}
|
||||||
|
>
|
||||||
|
{dir.value.list.map((block) => (
|
||||||
|
<div
|
||||||
|
class="w-full h-full p-[var(--block-padding)] relative rounded-lg"
|
||||||
|
key={block.id}
|
||||||
|
id={block.id}
|
||||||
|
data-transportable="1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full overflow-hidden relative cursor-pointer shadow-lg"
|
||||||
|
style={{
|
||||||
|
borderRadius: `calc(var(--block-radius) * var(--block-size))`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkBlock block={block} key={block.id} />
|
||||||
|
</div>
|
||||||
|
<div class="absolute left-0 -bottom-2 text-sm text-black/60 text-center w-full overflow-hidden text-ellipsis whitespace-nowrap break-all">
|
||||||
|
{block.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
})
|
|
@ -1,25 +1,35 @@
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import type { Block } from '../layout.types'
|
import type { Block } from '../layout.types'
|
||||||
|
import { useMenuStore } from '../GlobalMenu'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
block: {
|
block: {
|
||||||
type: Object as () => Block,
|
type: Object as () => Block,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
brief: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
const menu = useMenuStore()
|
||||||
return () => (
|
return () => (
|
||||||
<div
|
<div
|
||||||
class="w-full h-full flex justify-center items-center font-bold bg-cover bg-center bg-no-repeat"
|
class="w-full h-full flex justify-center items-center font-bold bg-cover bg-center bg-no-repeat"
|
||||||
|
onContextmenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
menu.open(props.block)
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: props.block.background || 'white',
|
backgroundColor: props.block.background || 'white',
|
||||||
color: props.block.color || 'black',
|
color: props.block.color || 'black',
|
||||||
backgroundImage: props.block.icon ? `url('${props.block.icon}')` : '',
|
backgroundImage: props.block.icon ? `url('${props.block.icon}')` : '',
|
||||||
fontSize: 'calc(var(--block-size) / 4.4)'
|
fontSize: props.brief ? '12px' : 'calc(var(--block-size) / 5)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>{props.block.text}</div>
|
<div>{props.brief ? props.block.text[0] : props.block.text}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { computed, defineComponent, TransitionGroup } from 'vue'
|
import { computed, defineComponent, ref, toRaw } from 'vue'
|
||||||
import useLayoutStore from '../useLayoutStore'
|
import useLayoutStore from '../useLayoutStore'
|
||||||
import { OhVueIcon, addIcons } from 'oh-vue-icons'
|
import { OhVueIcon, addIcons } from 'oh-vue-icons'
|
||||||
import { MdAdd } from 'oh-vue-icons/icons'
|
import { MdAdd } from 'oh-vue-icons/icons'
|
||||||
import useRouterStore from '@/useRouterStore'
|
import useRouterStore from '@/useRouterStore'
|
||||||
import { globalToast } from '@/main'
|
import { globalToast } from '@/main'
|
||||||
import LinkBlock from './LinkBlock'
|
import useSortable, { dragging, resetDragging } from './useSortable'
|
||||||
import useSortable, { dragging } from './useSortable'
|
import BlockWrapper from './BlockWrapper'
|
||||||
|
|
||||||
addIcons(MdAdd)
|
addIcons(MdAdd)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export default defineComponent(() => {
|
||||||
const router = useRouterStore()
|
const router = useRouterStore()
|
||||||
const container = useSortable(
|
const container = useSortable(
|
||||||
computed(() => layout.currentPage.list),
|
computed(() => layout.currentPage.list),
|
||||||
'page'
|
ref('page')
|
||||||
)
|
)
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
|
@ -31,20 +31,26 @@ export default defineComponent(() => {
|
||||||
}}
|
}}
|
||||||
onDragover={(e) => e.preventDefault()}
|
onDragover={(e) => e.preventDefault()}
|
||||||
onDrop={() => {
|
onDrop={() => {
|
||||||
// 处理移入(当前有内容不可移入)
|
// 处理移入
|
||||||
if (!dragging.id) return
|
if (!dragging.id) return
|
||||||
if (dragging.type === 'dock') {
|
if (dragging.type === 'dock') {
|
||||||
const oldIdx = layout.state.dock.findIndex((el) => el?.id === dragging.id)
|
const oldIdx = layout.state.dock.findIndex((el) => el?.id === dragging.id)
|
||||||
if (oldIdx < 0) return
|
if (oldIdx < 0) return
|
||||||
const block = layout.state.dock[oldIdx]
|
const block = layout.state.dock[oldIdx]
|
||||||
if (!block) return
|
if (!block) return
|
||||||
layout.currentPage.list.push(block)
|
layout.currentPage.list.push(toRaw(block))
|
||||||
layout.state.dock[oldIdx] = null
|
layout.state.dock[oldIdx] = null
|
||||||
return
|
resetDragging()
|
||||||
}
|
} else if (dragging.type && dragging.type !== 'dock') {
|
||||||
if (dragging.type && dragging.type !== 'dock') {
|
const list = layout.state.dir[dragging.type]?.list
|
||||||
// TODO: 文件夹
|
if (!list) return
|
||||||
return
|
const idx = list.findIndex((el) => el.id === dragging.id)
|
||||||
|
if (idx < 0) return
|
||||||
|
const block = list[idx]
|
||||||
|
layout.currentPage.list.push(toRaw(block))
|
||||||
|
list.splice(idx, 1)
|
||||||
|
layout.checkDir(dragging.type)
|
||||||
|
resetDragging()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -53,65 +59,12 @@ export default defineComponent(() => {
|
||||||
style="grid-template-columns:repeat(auto-fill, var(--block-size));grid-auto-rows:var(--block-size)"
|
style="grid-template-columns:repeat(auto-fill, var(--block-size));grid-auto-rows:var(--block-size)"
|
||||||
ref={container}
|
ref={container}
|
||||||
>
|
>
|
||||||
<TransitionGroup>
|
{layout.currentPage.list.map((block, idx) => (
|
||||||
{layout.currentPage.list.map((block, idx) => (
|
<BlockWrapper key={block.id} idx={idx} block={block} />
|
||||||
<div
|
))}
|
||||||
class="w-full h-full p-[var(--block-padding)] relative"
|
|
||||||
key={block.id}
|
|
||||||
id={block.id}
|
|
||||||
style={{
|
|
||||||
gridColumn: `span ${block.w}`,
|
|
||||||
gridRow: `span ${block.h}`
|
|
||||||
}}
|
|
||||||
data-transportable={block.link && !block.link.startsWith('id:') ? '1' : ''}
|
|
||||||
onDragover={(e) => e.preventDefault()}
|
|
||||||
onDrop={() => {
|
|
||||||
// 处理移入(当前有内容不可移入)
|
|
||||||
if (!dragging.id) return
|
|
||||||
if (dragging.type === 'dock') {
|
|
||||||
const oldIdx = layout.state.dock.findIndex((el) => el?.id === dragging.id)
|
|
||||||
if (oldIdx < 0) return
|
|
||||||
const block = layout.state.dock[oldIdx]
|
|
||||||
if (!block) return
|
|
||||||
layout.currentPage.list.splice(idx, 0, block)
|
|
||||||
layout.state.dock[oldIdx] = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dragging.type && dragging.type !== 'dock') {
|
|
||||||
// TODO: 文件夹
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full h-full overflow-hidden relative cursor-pointer shadow-lg"
|
|
||||||
style="border-radius:calc(var(--block-radius) * var(--block-size))"
|
|
||||||
>
|
|
||||||
{block.link ? (
|
|
||||||
block.link.startsWith('id:') ? (
|
|
||||||
// 文件夹
|
|
||||||
<div></div>
|
|
||||||
) : (
|
|
||||||
// 链接
|
|
||||||
<LinkBlock block={block} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
// 小组件
|
|
||||||
<div></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="absolute left-0 -bottom-2 text-sm text-white text-center w-full overflow-hidden text-ellipsis whitespace-nowrap break-all font-bold"
|
|
||||||
style="text-shadow: 0 0 4px rgba(0,0,0,.6)"
|
|
||||||
>
|
|
||||||
{block.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</TransitionGroup>
|
|
||||||
<div class="w-full h-full flex justify-center items-center p-[var(--block-padding)] relative operation-button">
|
<div class="w-full h-full flex justify-center items-center p-[var(--block-padding)] relative operation-button">
|
||||||
<div
|
<div
|
||||||
class="w-full h-full overflow-hidden rounded-[calc(var(--block-radius)_*_var(--block-size))] bg-white/80 backdrop-blur flex justify-center items-center cursor-pointer hover:scale-105 transition-all"
|
class="w-full h-full overflow-hidden rounded-[calc(var(--block-radius)_*_var(--block-size))] bg-white/60 backdrop-blur flex justify-center items-center cursor-pointer hover:scale-105 transition-all"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (layout.state.content[layout.state.current].pages[layout.state.currentPage]) {
|
if (layout.state.content[layout.state.current].pages[layout.state.currentPage]) {
|
||||||
router.path = 'global-adder'
|
router.path = 'global-adder'
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { ref, watch, type Ref } from 'vue'
|
import { ref, watch, type Ref } from 'vue'
|
||||||
|
|
||||||
export const dragging = {
|
export const defaultDragging = {
|
||||||
type: '',
|
type: '',
|
||||||
id: '',
|
id: '',
|
||||||
transportable: false
|
transportable: false
|
||||||
}
|
}
|
||||||
|
export const dragging = {
|
||||||
|
...defaultDragging
|
||||||
|
}
|
||||||
|
|
||||||
export default function useSortable(list: Ref<any[]>, type = '') {
|
export function resetDragging() {
|
||||||
|
Object.assign(dragging, defaultDragging)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSortable(list: Ref<any[]>, type: Ref<string>) {
|
||||||
const container = ref<HTMLDivElement | null>(null)
|
const container = ref<HTMLDivElement | null>(null)
|
||||||
watch(
|
watch(
|
||||||
container,
|
container,
|
||||||
|
@ -18,7 +25,7 @@ export default function useSortable(list: Ref<any[]>, type = '') {
|
||||||
scroll: true,
|
scroll: true,
|
||||||
scrollSensitivity: 200,
|
scrollSensitivity: 200,
|
||||||
scrollSpeed: 10,
|
scrollSpeed: 10,
|
||||||
...(type === 'page'
|
...(type.value === 'page'
|
||||||
? {
|
? {
|
||||||
invertSwap: true,
|
invertSwap: true,
|
||||||
invertedSwapThreshold: 1,
|
invertedSwapThreshold: 1,
|
||||||
|
@ -27,7 +34,7 @@ export default function useSortable(list: Ref<any[]>, type = '') {
|
||||||
: {}),
|
: {}),
|
||||||
ghostClass: 'opacity-20',
|
ghostClass: 'opacity-20',
|
||||||
onStart: (e: any) => {
|
onStart: (e: any) => {
|
||||||
dragging.type = type
|
dragging.type = type.value
|
||||||
dragging.id = e.item.id || ''
|
dragging.id = e.item.id || ''
|
||||||
// 只有链接才能被拖拽到其他区域
|
// 只有链接才能被拖拽到其他区域
|
||||||
dragging.transportable = e.item.dataset?.transportable ? true : false
|
dragging.transportable = e.item.dataset?.transportable ? true : false
|
||||||
|
@ -36,8 +43,8 @@ export default function useSortable(list: Ref<any[]>, type = '') {
|
||||||
if (e.related.className.includes('operation-button') && e.willInsertAfter) return false
|
if (e.related.className.includes('operation-button') && e.willInsertAfter) return false
|
||||||
},
|
},
|
||||||
onEnd: (e) => {
|
onEnd: (e) => {
|
||||||
// 同区域拖拽,直接交换位置
|
// 如果已经被 onDrop 操作,不会进行交换
|
||||||
if (dragging.type !== type) return
|
if (!dragging.id) return
|
||||||
const oldIdx = e.oldIndex
|
const oldIdx = e.oldIndex
|
||||||
const newIdx = e.newIndex
|
const newIdx = e.newIndex
|
||||||
if (oldIdx === undefined || newIdx === undefined) return
|
if (oldIdx === undefined || newIdx === undefined) return
|
||||||
|
|
|
@ -36,7 +36,7 @@ export interface Layout {
|
||||||
]
|
]
|
||||||
current: 0 | 1 | 2 // 游戏,工作,轻娱
|
current: 0 | 1 | 2 // 游戏,工作,轻娱
|
||||||
currentPage: number
|
currentPage: number
|
||||||
dir: { [key: string]: Block[] }
|
dir: { [key: string]: { list: Block[]; label: string } }
|
||||||
dock: [
|
dock: [
|
||||||
Block | null,
|
Block | null,
|
||||||
Block | null,
|
Block | null,
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default defineStore('layout', () => {
|
||||||
[ready, state],
|
[ready, state],
|
||||||
([re, s]) => {
|
([re, s]) => {
|
||||||
if (!re) return
|
if (!re) return
|
||||||
|
console.log(toRaw(s))
|
||||||
db.setItem('layout', toRaw(s))
|
db.setItem('layout', toRaw(s))
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
|
@ -72,6 +73,30 @@ export default defineStore('layout', () => {
|
||||||
pageList.push(block)
|
pageList.push(block)
|
||||||
globalToast.success('添加成功')
|
globalToast.success('添加成功')
|
||||||
}
|
}
|
||||||
|
const openDir = ref('')
|
||||||
|
// 文件夹只有一个时,将当前界面的文件夹替换为图标
|
||||||
|
const checkDir = (id: string) => {
|
||||||
|
const dir = state.dir[id]
|
||||||
|
if (!dir) return
|
||||||
|
if (dir && dir.list.length === 1) {
|
||||||
|
const item = dir.list[0]
|
||||||
|
if (!item) return
|
||||||
|
const idx = currentPage.value.list.findIndex((el) => el.link === 'id:' + id)
|
||||||
|
if (idx < 0) return
|
||||||
|
currentPage.value.list.splice(idx, 1, toRaw(item))
|
||||||
|
if (openDir.value === id) {
|
||||||
|
openDir.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabel = (block: Block) => {
|
||||||
|
if (block.link.startsWith('id:')) {
|
||||||
|
const dir = state.dir[block.link.slice(3)]
|
||||||
|
if (dir) return dir.label
|
||||||
|
}
|
||||||
|
return block.label || ''
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
@ -80,6 +105,9 @@ export default defineStore('layout', () => {
|
||||||
currentPage,
|
currentPage,
|
||||||
isCompact,
|
isCompact,
|
||||||
background,
|
background,
|
||||||
addBlock
|
addBlock,
|
||||||
|
openDir,
|
||||||
|
checkDir,
|
||||||
|
getLabel
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { BlockType, type Block } from './layout.types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查 block 类型
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {Block} b
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
export function checkBlock(b: Block) {
|
|
||||||
if (!b.link) return BlockType.Widget
|
|
||||||
if (b.link.startsWith('id:')) return BlockType.Folder
|
|
||||||
return BlockType.Link
|
|
||||||
}
|
|
Loading…
Reference in New Issue