From 80ef1e158a62f2de921d9018e626add428ab88d3 Mon Sep 17 00:00:00 2001 From: plightfield <1207120484@qq.com> Date: Wed, 25 Sep 2024 18:23:54 +0800 Subject: [PATCH] change --- index.html | 11 +- src/App.vue | 4 + src/layout/GlobalMenu.tsx | 201 +++++++++++++++++++++++++++++++ src/layout/dock/index.tsx | 40 ++++-- src/layout/grid/BlockWrapper.tsx | 139 +++++++++++++++++++++ src/layout/grid/DirBlock.tsx | 57 +++++++++ src/layout/grid/DirModal.tsx | 72 +++++++++++ src/layout/grid/LinkBlock.tsx | 14 ++- src/layout/grid/index.tsx | 89 ++++---------- src/layout/grid/useSortable.ts | 19 ++- src/layout/layout.types.ts | 2 +- src/layout/useLayoutStore.ts | 30 ++++- src/layout/utils.ts | 14 --- 13 files changed, 586 insertions(+), 106 deletions(-) create mode 100644 src/layout/GlobalMenu.tsx create mode 100644 src/layout/grid/BlockWrapper.tsx create mode 100644 src/layout/grid/DirBlock.tsx create mode 100644 src/layout/grid/DirModal.tsx delete mode 100644 src/layout/utils.ts diff --git a/index.html b/index.html index a888544..46fc7ef 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,12 @@ - + - - - + + + Vite App diff --git a/src/App.vue b/src/App.vue index cb94d98..453b80e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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()
+ + diff --git a/src/layout/GlobalMenu.tsx b/src/layout/GlobalMenu.tsx new file mode 100644 index 0000000..33a2c35 --- /dev/null +++ b/src/layout/GlobalMenu.tsx @@ -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 () => ( +
{ + ctx.emit('click') + }} + > + {ctx.slots.default?.()} +
+ ) + } +}) + +export default defineComponent(() => { + const menu = useMenuStore() + const layout = useLayoutStore() + return () => + menu.show && ( +
{ + 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 ( + <> + { + // 删除链接 + const idx = layout.currentPage.list.findIndex((el) => el.id === block.id) + if (idx < 0) return + layout.currentPage.list.splice(idx, 1) + menu.dismiss() + }} + > + 删除 + + + ) + } + if (block.link.startsWith('id:')) { + // 文件夹 + const id = block.link.slice(3) + return ( + <> + { + block.w = 1 + block.h = 1 + menu.dismiss() + }} + > + 小 + + { + block.w = 2 + block.h = 2 + menu.dismiss() + }} + > + 大 + + { + // 删除文件夹 + 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() + }} + > + 删除 + + + ) + } + // 链接 + return ( + <> + 测试 + { + // 删除链接 + 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() + } + }} + > + 删除 + + + ) + })()} +
+ ) +}) diff --git a/src/layout/dock/index.tsx b/src/layout/dock/index.tsx index 89fd047..4f5a911 100644 --- a/src/layout/dock/index.tsx +++ b/src/layout/dock/index.tsx @@ -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 () => (
{ + current.value = -1 + }} > {layout.state.dockLabels.split('').map((l, i) => { const block = layout.state.dock[i] return (
= 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() } }} > diff --git a/src/layout/grid/BlockWrapper.tsx b/src/layout/grid/BlockWrapper.tsx new file mode 100644 index 0000000..33add39 --- /dev/null +++ b/src/layout/grid/BlockWrapper.tsx @@ -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 () => ( +
{ + 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() + } + }} + > +
+ {props.block.link ? ( + props.block.link.startsWith('id:') ? ( + // 文件夹 + + ) : ( + // 链接 + + ) + ) : ( + // 小组件 +
+ )} +
+
+ {layout.getLabel(props.block)} +
+
+ ) + } +}) diff --git a/src/layout/grid/DirBlock.tsx b/src/layout/grid/DirBlock.tsx new file mode 100644 index 0000000..4103f8e --- /dev/null +++ b/src/layout/grid/DirBlock.tsx @@ -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 () => ( +
{ + 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) => ( +
+ +
+ ))} +
+ ) + } +}) diff --git a/src/layout/grid/DirModal.tsx b/src/layout/grid/DirModal.tsx new file mode 100644 index 0000000..640d305 --- /dev/null +++ b/src/layout/grid/DirModal.tsx @@ -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 () => ( + +
+
{ + layout.openDir = '' + }} + onDragenter={() => { + layout.openDir = '' + }} + >
+ +
+
+ {dir.value.list.map((block) => ( +
+
+ +
+
+ {block.label} +
+
+ ))} +
+
+
+
+ ) +}) diff --git a/src/layout/grid/LinkBlock.tsx b/src/layout/grid/LinkBlock.tsx index 552db9d..4dff436 100644 --- a/src/layout/grid/LinkBlock.tsx +++ b/src/layout/grid/LinkBlock.tsx @@ -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 () => (
{ + 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)' }} > -
{props.block.text}
+
{props.brief ? props.block.text[0] : props.block.text}
) } diff --git a/src/layout/grid/index.tsx b/src/layout/grid/index.tsx index ac294b9..9972367 100644 --- a/src/layout/grid/index.tsx +++ b/src/layout/grid/index.tsx @@ -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} > - - {layout.currentPage.list.map((block, idx) => ( -
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 - } - }} - > -
- {block.link ? ( - block.link.startsWith('id:') ? ( - // 文件夹 -
- ) : ( - // 链接 - - ) - ) : ( - // 小组件 -
- )} -
-
- {block.label} -
-
- ))} -
+ {layout.currentPage.list.map((block, idx) => ( + + ))}
{ if (layout.state.content[layout.state.current].pages[layout.state.currentPage]) { router.path = 'global-adder' diff --git a/src/layout/grid/useSortable.ts b/src/layout/grid/useSortable.ts index 3c25808..24fddea 100644 --- a/src/layout/grid/useSortable.ts +++ b/src/layout/grid/useSortable.ts @@ -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, type = '') { +export function resetDragging() { + Object.assign(dragging, defaultDragging) +} + +export default function useSortable(list: Ref, type: Ref) { const container = ref(null) watch( container, @@ -18,7 +25,7 @@ export default function useSortable(list: Ref, 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, 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, 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 diff --git a/src/layout/layout.types.ts b/src/layout/layout.types.ts index cff394a..0d68d54 100644 --- a/src/layout/layout.types.ts +++ b/src/layout/layout.types.ts @@ -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, diff --git a/src/layout/useLayoutStore.ts b/src/layout/useLayoutStore.ts index 0176a25..c557f2e 100644 --- a/src/layout/useLayoutStore.ts +++ b/src/layout/useLayoutStore.ts @@ -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 } }) diff --git a/src/layout/utils.ts b/src/layout/utils.ts deleted file mode 100644 index 7c6a0e1..0000000 --- a/src/layout/utils.ts +++ /dev/null @@ -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 -}