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/public/icons/calendarIcon.png b/public/icons/calendarIcon.png new file mode 100644 index 0000000..fe49a73 Binary files /dev/null and b/public/icons/calendarIcon.png differ 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/adder/AdderPage.tsx b/src/layout/adder/AdderPage.tsx index 19869bd..987111d 100644 --- a/src/layout/adder/AdderPage.tsx +++ b/src/layout/adder/AdderPage.tsx @@ -1,4 +1,12 @@ -import { computed, defineComponent, ref, Transition } from 'vue' +import { + computed, + defineComponent, + provide, + ref, + Transition, + type InjectionKey, + type Ref +} from 'vue' import AdderPageBack from './AdderPageBack' import useLayoutStore from '../useLayoutStore' import { OhVueIcon, addIcons } from 'oh-vue-icons' @@ -6,6 +14,8 @@ import { MdKeyboardcommandkey, FaCompass, FaPencilRuler } from 'oh-vue-icons/ico import CustomAdder from './CustomAdder' import clsx from 'clsx' import ThemeProvider from '@/utils/ThemeProvider' +import WidgetAdder from './WidgetAdder' +import { Form, Input, Select } from 'ant-design-vue' addIcons(MdKeyboardcommandkey, FaCompass, FaPencilRuler) const ItemButton = defineComponent({ @@ -50,10 +60,14 @@ const ItemButton = defineComponent({ } }) +export const AddToToken = Symbol('addTo') as InjectionKey> + export default defineComponent(() => { const layout = useLayoutStore() const isGame = computed(() => layout.state.current === 0) const type = ref(1) + const addTo = ref(layout.state.currentPage) + provide(AddToToken, addTo) return () => (
@@ -96,11 +110,25 @@ export default defineComponent(() => { } onContextmenu={(e) => e.stopPropagation()} > -
+
+ + +
+
+ {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..2351fe1 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 } @@ -63,6 +64,7 @@ export default defineStore('layout', () => { // 添加图标 const addBlock = (block: Block, _page: number = -1) => { const page = _page >= 0 ? _page : state.currentPage + console.log(page) console.log(state.content[state.current].pages[page]) if (!state.content[state.current].pages[page]) { globalToast.warning('请先添加页面') @@ -72,6 +74,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 +106,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 -} diff --git a/src/widgets/calendar/Large.tsx b/src/widgets/calendar/Large.tsx new file mode 100644 index 0000000..9c16be7 --- /dev/null +++ b/src/widgets/calendar/Large.tsx @@ -0,0 +1,5 @@ +import { defineComponent } from 'vue' + +export default defineComponent(() => { + return () =>
+}) diff --git a/src/widgets/calendar/Middle.tsx b/src/widgets/calendar/Middle.tsx new file mode 100644 index 0000000..9c16be7 --- /dev/null +++ b/src/widgets/calendar/Middle.tsx @@ -0,0 +1,5 @@ +import { defineComponent } from 'vue' + +export default defineComponent(() => { + return () =>
+}) diff --git a/src/widgets/calendar/Modal.tsx b/src/widgets/calendar/Modal.tsx new file mode 100644 index 0000000..9c16be7 --- /dev/null +++ b/src/widgets/calendar/Modal.tsx @@ -0,0 +1,5 @@ +import { defineComponent } from 'vue' + +export default defineComponent(() => { + return () =>
+}) diff --git a/src/widgets/calendar/Small.tsx b/src/widgets/calendar/Small.tsx new file mode 100644 index 0000000..9c16be7 --- /dev/null +++ b/src/widgets/calendar/Small.tsx @@ -0,0 +1,5 @@ +import { defineComponent } from 'vue' + +export default defineComponent(() => { + return () =>
+}) diff --git a/src/widgets/calendar/index.ts b/src/widgets/calendar/index.ts new file mode 100644 index 0000000..efa0e19 --- /dev/null +++ b/src/widgets/calendar/index.ts @@ -0,0 +1,29 @@ +import type { Widget } from '..' + +export default { + name: 'calendar', + label: '日历', + description: '日历信息', + icon: '/icons/calendarIcon.png', + modal: () => import('./Modal'), + list: [ + { + w: 2, + h: 1, + label: '小', + component: () => import('./Small') + }, + { + w: 2, + h: 2, + label: '中', + component: () => import('./Middle') + }, + { + w: 4, + h: 2, + label: '大', + component: () => import('./Large') + } + ] +} as Widget diff --git a/src/widgets/index.ts b/src/widgets/index.ts new file mode 100644 index 0000000..5dd7fb1 --- /dev/null +++ b/src/widgets/index.ts @@ -0,0 +1,17 @@ +import calendar from './calendar' + +export interface Widget { + name: string // 小组件类型唯一标识 + label: string // 小组件名称 + description: string // 小组件描述 + icon: string // 小组件图标 + modal: () => any // 弹框组件 + list: { + w: number + h: number + label: string + component: () => any + }[] // 不同尺寸小组件块 +} + +export default [calendar] as Widget[]