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">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -7,6 +7,8 @@ import SettingsButton from './settings/SettingsButton'
|
|||
import SettingsOverlay from './settings/SettingsOverlay'
|
||||
import Sider from './layout/sider'
|
||||
import Dock from './layout/dock'
|
||||
import DirModal from './layout/grid/DirModal'
|
||||
import GlobalMenu from './layout/GlobalMenu'
|
||||
import LoginModal from './user/LoginModal'
|
||||
import { computed } from 'vue'
|
||||
import asyncLoader from './utils/asyncLoader'
|
||||
|
@ -34,6 +36,8 @@ const layout = useLayoutStore()
|
|||
<div class="fixed z-40 right-[14%] top-8">
|
||||
<Fox />
|
||||
</div>
|
||||
<DirModal />
|
||||
<GlobalMenu />
|
||||
</div>
|
||||
</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 useSortable, { dragging } from '../grid/useSortable'
|
||||
import useSortable, { dragging, resetDragging } from '../grid/useSortable'
|
||||
import LinkBlock from '../grid/LinkBlock'
|
||||
|
||||
export default defineComponent(() => {
|
||||
const layout = useLayoutStore()
|
||||
const container = useSortable(
|
||||
computed(() => layout.state.dock),
|
||||
'dock'
|
||||
ref('dock')
|
||||
)
|
||||
const current = ref(-1)
|
||||
|
||||
return () => (
|
||||
<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"
|
||||
ref={container}
|
||||
onMouseleave={() => {
|
||||
current.value = -1
|
||||
}}
|
||||
>
|
||||
{layout.state.dockLabels.split('').map((l, i) => {
|
||||
const block = layout.state.dock[i]
|
||||
return (
|
||||
<div
|
||||
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 || ''}
|
||||
onMousemove={() => {
|
||||
current.value = i
|
||||
}}
|
||||
onDragover={(e) => e.preventDefault()}
|
||||
onDrop={() => {
|
||||
// 处理移入(当前有内容不可移入)
|
||||
|
@ -31,13 +45,19 @@ export default defineComponent(() => {
|
|||
if (oldIdx < 0) return
|
||||
const block = layout.currentPage.list[oldIdx]
|
||||
if (!block) return
|
||||
layout.state.dock[i] = block
|
||||
layout.state.dock[i] = toRaw(block)
|
||||
layout.currentPage.list.splice(oldIdx, 1)
|
||||
return
|
||||
}
|
||||
if (dragging.type && dragging.type !== 'dock') {
|
||||
// TODO: 文件夹
|
||||
return
|
||||
resetDragging()
|
||||
} else if (dragging.type && dragging.type !== 'dock') {
|
||||
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.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 type { Block } from '../layout.types'
|
||||
import { useMenuStore } from '../GlobalMenu'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
block: {
|
||||
type: Object as () => Block,
|
||||
required: true
|
||||
},
|
||||
brief: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const menu = useMenuStore()
|
||||
return () => (
|
||||
<div
|
||||
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={{
|
||||
backgroundColor: props.block.background || 'white',
|
||||
color: props.block.color || 'black',
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { computed, defineComponent, TransitionGroup } from 'vue'
|
||||
import { computed, defineComponent, ref, toRaw } from 'vue'
|
||||
import useLayoutStore from '../useLayoutStore'
|
||||
import { OhVueIcon, addIcons } from 'oh-vue-icons'
|
||||
import { MdAdd } from 'oh-vue-icons/icons'
|
||||
import useRouterStore from '@/useRouterStore'
|
||||
import { globalToast } from '@/main'
|
||||
import LinkBlock from './LinkBlock'
|
||||
import useSortable, { dragging } from './useSortable'
|
||||
import useSortable, { dragging, resetDragging } from './useSortable'
|
||||
import BlockWrapper from './BlockWrapper'
|
||||
|
||||
addIcons(MdAdd)
|
||||
|
||||
|
@ -14,7 +14,7 @@ export default defineComponent(() => {
|
|||
const router = useRouterStore()
|
||||
const container = useSortable(
|
||||
computed(() => layout.currentPage.list),
|
||||
'page'
|
||||
ref('page')
|
||||
)
|
||||
|
||||
return () => (
|
||||
|
@ -31,20 +31,26 @@ export default defineComponent(() => {
|
|||
}}
|
||||
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.push(block)
|
||||
layout.currentPage.list.push(toRaw(block))
|
||||
layout.state.dock[oldIdx] = null
|
||||
return
|
||||
}
|
||||
if (dragging.type && dragging.type !== 'dock') {
|
||||
// TODO: 文件夹
|
||||
return
|
||||
resetDragging()
|
||||
} else if (dragging.type && dragging.type !== 'dock') {
|
||||
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.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)"
|
||||
ref={container}
|
||||
>
|
||||
<TransitionGroup>
|
||||
{layout.currentPage.list.map((block, idx) => (
|
||||
<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>
|
||||
{layout.currentPage.list.map((block, idx) => (
|
||||
<BlockWrapper key={block.id} idx={idx} block={block} />
|
||||
))}
|
||||
<div class="w-full h-full flex justify-center items-center p-[var(--block-padding)] relative operation-button">
|
||||
<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={() => {
|
||||
if (layout.state.content[layout.state.current].pages[layout.state.currentPage]) {
|
||||
router.path = 'global-adder'
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import Sortable from 'sortablejs'
|
||||
import { ref, watch, type Ref } from 'vue'
|
||||
|
||||
export const dragging = {
|
||||
export const defaultDragging = {
|
||||
type: '',
|
||||
id: '',
|
||||
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)
|
||||
watch(
|
||||
container,
|
||||
|
@ -18,7 +25,7 @@ export default function useSortable(list: Ref<any[]>, type = '') {
|
|||
scroll: true,
|
||||
scrollSensitivity: 200,
|
||||
scrollSpeed: 10,
|
||||
...(type === 'page'
|
||||
...(type.value === 'page'
|
||||
? {
|
||||
invertSwap: true,
|
||||
invertedSwapThreshold: 1,
|
||||
|
@ -27,7 +34,7 @@ export default function useSortable(list: Ref<any[]>, type = '') {
|
|||
: {}),
|
||||
ghostClass: 'opacity-20',
|
||||
onStart: (e: any) => {
|
||||
dragging.type = type
|
||||
dragging.type = type.value
|
||||
dragging.id = e.item.id || ''
|
||||
// 只有链接才能被拖拽到其他区域
|
||||
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
|
||||
},
|
||||
onEnd: (e) => {
|
||||
// 同区域拖拽,直接交换位置
|
||||
if (dragging.type !== type) return
|
||||
// 如果已经被 onDrop 操作,不会进行交换
|
||||
if (!dragging.id) return
|
||||
const oldIdx = e.oldIndex
|
||||
const newIdx = e.newIndex
|
||||
if (oldIdx === undefined || newIdx === undefined) return
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface Layout {
|
|||
]
|
||||
current: 0 | 1 | 2 // 游戏,工作,轻娱
|
||||
currentPage: number
|
||||
dir: { [key: string]: Block[] }
|
||||
dir: { [key: string]: { list: Block[]; label: string } }
|
||||
dock: [
|
||||
Block | null,
|
||||
Block | null,
|
||||
|
|
|
@ -32,6 +32,7 @@ export default defineStore('layout', () => {
|
|||
[ready, state],
|
||||
([re, s]) => {
|
||||
if (!re) return
|
||||
console.log(toRaw(s))
|
||||
db.setItem('layout', toRaw(s))
|
||||
},
|
||||
{ deep: true }
|
||||
|
@ -72,6 +73,30 @@ export default defineStore('layout', () => {
|
|||
pageList.push(block)
|
||||
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 {
|
||||
state,
|
||||
|
@ -80,6 +105,9 @@ export default defineStore('layout', () => {
|
|||
currentPage,
|
||||
isCompact,
|
||||
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