This commit is contained in:
plightfield 2024-09-25 18:23:54 +08:00
parent 293fa3df42
commit 80ef1e158a
13 changed files with 586 additions and 106 deletions

View File

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

View File

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

201
src/layout/GlobalMenu.tsx Normal file
View File

@ -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>
)
})

View File

@ -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()
} }
}} }}
> >

View File

@ -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>
)
}
})

View File

@ -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>
)
}
})

View File

@ -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>
)
})

View File

@ -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>
) )
} }

View File

@ -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) => (
<div <BlockWrapper key={block.id} idx={idx} block={block} />
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'

View File

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

View File

@ -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,

View File

@ -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
} }
}) })

View File

@ -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
}