diff --git a/ScreenShot_2026-01-22_174129_211.png b/ScreenShot_2026-01-22_174129_211.png deleted file mode 100644 index d5de95e..0000000 Binary files a/ScreenShot_2026-01-22_174129_211.png and /dev/null differ diff --git a/ScreenShot_2026-01-23_175720_764.png b/ScreenShot_2026-01-23_175720_764.png deleted file mode 100644 index 66f35c7..0000000 Binary files a/ScreenShot_2026-01-23_175720_764.png and /dev/null differ diff --git a/ScreenShot_2026-01-26_101148_856.png b/ScreenShot_2026-01-26_101148_856.png deleted file mode 100644 index 16d1829..0000000 Binary files a/ScreenShot_2026-01-26_101148_856.png and /dev/null differ diff --git a/ScreenShot_2026-01-26_151019_180.png b/ScreenShot_2026-01-26_151019_180.png deleted file mode 100644 index b65fb9a..0000000 Binary files a/ScreenShot_2026-01-26_151019_180.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_140219_283.png b/ScreenShot_2026-01-27_140219_283.png deleted file mode 100644 index 4d2be8a..0000000 Binary files a/ScreenShot_2026-01-27_140219_283.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_142115_699.png b/ScreenShot_2026-01-27_142115_699.png deleted file mode 100644 index bfcab38..0000000 Binary files a/ScreenShot_2026-01-27_142115_699.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_142125_810.png b/ScreenShot_2026-01-27_142125_810.png deleted file mode 100644 index 6d3c9ce..0000000 Binary files a/ScreenShot_2026-01-27_142125_810.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_142143_653.png b/ScreenShot_2026-01-27_142143_653.png deleted file mode 100644 index 9587ce9..0000000 Binary files a/ScreenShot_2026-01-27_142143_653.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_161350_530.png b/ScreenShot_2026-01-27_161350_530.png deleted file mode 100644 index 6cd0379..0000000 Binary files a/ScreenShot_2026-01-27_161350_530.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164351_352.png b/ScreenShot_2026-01-27_164351_352.png deleted file mode 100644 index 6b1d5bc..0000000 Binary files a/ScreenShot_2026-01-27_164351_352.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164407_879.png b/ScreenShot_2026-01-27_164407_879.png deleted file mode 100644 index 2db576a..0000000 Binary files a/ScreenShot_2026-01-27_164407_879.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164417_272.png b/ScreenShot_2026-01-27_164417_272.png deleted file mode 100644 index 68b7fba..0000000 Binary files a/ScreenShot_2026-01-27_164417_272.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164425_826.png b/ScreenShot_2026-01-27_164425_826.png deleted file mode 100644 index b2dcae4..0000000 Binary files a/ScreenShot_2026-01-27_164425_826.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164433_919.png b/ScreenShot_2026-01-27_164433_919.png deleted file mode 100644 index 849ea7c..0000000 Binary files a/ScreenShot_2026-01-27_164433_919.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164441_143.png b/ScreenShot_2026-01-27_164441_143.png deleted file mode 100644 index 20a601c..0000000 Binary files a/ScreenShot_2026-01-27_164441_143.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164454_148.png b/ScreenShot_2026-01-27_164454_148.png deleted file mode 100644 index a1f40e6..0000000 Binary files a/ScreenShot_2026-01-27_164454_148.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164508_593.png b/ScreenShot_2026-01-27_164508_593.png deleted file mode 100644 index 1b295c7..0000000 Binary files a/ScreenShot_2026-01-27_164508_593.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164516_311.png b/ScreenShot_2026-01-27_164516_311.png deleted file mode 100644 index cde3d74..0000000 Binary files a/ScreenShot_2026-01-27_164516_311.png and /dev/null differ diff --git a/ScreenShot_2026-01-27_164535_751.png b/ScreenShot_2026-01-27_164535_751.png deleted file mode 100644 index 8ccdfcb..0000000 Binary files a/ScreenShot_2026-01-27_164535_751.png and /dev/null differ diff --git a/ScreenShot_2026-01-28_161321_172.png b/ScreenShot_2026-01-28_161321_172.png deleted file mode 100644 index 1c82f6d..0000000 Binary files a/ScreenShot_2026-01-28_161321_172.png and /dev/null differ diff --git a/ScreenShot_2026-01-29_094743_454.png b/ScreenShot_2026-01-29_094743_454.png deleted file mode 100644 index 9599c25..0000000 Binary files a/ScreenShot_2026-01-29_094743_454.png and /dev/null differ diff --git a/ScreenShot_2026-01-29_101729_394.png b/ScreenShot_2026-01-29_101729_394.png deleted file mode 100644 index 97530a4..0000000 Binary files a/ScreenShot_2026-01-29_101729_394.png and /dev/null differ diff --git a/app/src/components/FolderCard/index.vue b/app/src/components/FolderCard/index.vue new file mode 100644 index 0000000..76e3711 --- /dev/null +++ b/app/src/components/FolderCard/index.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/app/src/components/FolderDialog/index.vue b/app/src/components/FolderDialog/index.vue new file mode 100644 index 0000000..3baa3b5 --- /dev/null +++ b/app/src/components/FolderDialog/index.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/app/src/components/GridCanvas/index.vue b/app/src/components/GridCanvas/index.vue index 6475d99..3419d65 100644 --- a/app/src/components/GridCanvas/index.vue +++ b/app/src/components/GridCanvas/index.vue @@ -3,12 +3,15 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import Muuri from 'muuri'; import { useLayoutStore } from '@/store/useLayoutStore'; import { useWidgetsStore } from '@/store/useWidgetsStore'; +import { useFoldersStore, type Folder, type FolderItem } from '@/store/useFoldersStore'; import { useUIStore } from '@/store/useUIStore'; import { useSettingsStore } from '@/store/useSettingsStore'; import IconCard from '@/components/IconCard/index.vue'; import WidgetCard from '@/components/WidgetCard/index.vue'; +import FolderCard from '@/components/FolderCard/index.vue'; +import FolderDialog from '@/components/FolderDialog/index.vue'; -type GridItemType = 'icon' | 'widget'; +type GridItemType = 'icon' | 'widget' | 'folder'; type GridItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4'; interface Icon { @@ -20,6 +23,7 @@ interface Icon { bgColor?: string; size?: GridItemSize; groupId?: string; + folderId?: string; } type WidgetSize = GridItemSize; @@ -29,8 +33,13 @@ interface Widget { size: WidgetSize; data?: any; groupId?: string; + folderId?: string; } +type FolderChild = + | { type: 'icon'; id: string; icon: Icon } + | { type: 'widget'; id: string; widget: Widget }; + interface GridOrderEntry { id: string; type: GridItemType; @@ -38,10 +47,12 @@ interface GridOrderEntry { type OrderedGridItem = | { type: 'icon'; id: string; icon: Icon } - | { type: 'widget'; id: string; widget: Widget }; + | { type: 'widget'; id: string; widget: Widget } + | { type: 'folder'; id: string; folder: Folder; children: FolderChild[] }; const GRID_ORDER_STORAGE_KEY = 'itab_grid_order'; const DEFAULT_GROUP_ID = 'home'; +const FOLDER_SIZE: GridItemSize = '2x2'; const props = defineProps<{ activeGroupId?: string | null; @@ -49,6 +60,7 @@ const props = defineProps<{ const layoutStore = useLayoutStore(); const widgetsStore = useWidgetsStore(); +const foldersStore = useFoldersStore(); const uiStore = useUIStore(); const settingsStore = useSettingsStore(); const gridRef = ref(null); @@ -64,6 +76,12 @@ const iconSizeMap = new Map(); const enteringKeys = ref(new Set()); const previousIconIds = ref(new Set()); const previousWidgetIds = ref(new Set()); +const openFolderId = ref(null); +const hoverTargetEl = ref(null); +const draggingMeta = ref<{ id: string | null; type: GridItemType | null }>({ + id: null, + type: null, +}); const layoutAnimationMs = 240; const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)'; const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID; @@ -74,7 +92,7 @@ const normalizeGridOrder = (order: GridOrderEntry[]) => { const normalized: GridOrderEntry[] = []; for (const entry of order) { if (!entry || typeof entry.id !== 'string') continue; - if (entry.type !== 'icon' && entry.type !== 'widget') continue; + if (entry.type !== 'icon' && entry.type !== 'widget' && entry.type !== 'folder') continue; const key = `${entry.type}:${entry.id}`; if (seen.has(key)) continue; seen.add(key); @@ -84,8 +102,9 @@ const normalizeGridOrder = (order: GridOrderEntry[]) => { }; const buildDefaultOrder = (): GridOrderEntry[] => [ - ...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })), - ...layoutStore.icons.map(icon => ({ id: icon.id, type: 'icon' as const })), + ...widgetsStore.widgets.filter(widget => !widget.folderId).map(widget => ({ id: widget.id, type: 'widget' as const })), + ...layoutStore.icons.filter(icon => !icon.folderId).map(icon => ({ id: icon.id, type: 'icon' as const })), + ...foldersStore.folders.map(folder => ({ id: folder.id, type: 'folder' as const })), ]; const applyGridOrder = (order: GridOrderEntry[], syncStoreOrder = true) => { @@ -121,8 +140,9 @@ const loadGridOrder = () => { }; const ensureOrderConsistency = () => { - const iconIds = new Set(layoutStore.icons.map(icon => icon.id)); - const widgetIds = new Set(widgetsStore.widgets.map(widget => widget.id)); + const iconIds = new Set(layoutStore.icons.filter(icon => !icon.folderId).map(icon => icon.id)); + const widgetIds = new Set(widgetsStore.widgets.filter(widget => !widget.folderId).map(widget => widget.id)); + const folderIds = new Set(foldersStore.folders.map(folder => folder.id)); const seen = new Set(); const nextOrder: GridOrderEntry[] = []; @@ -140,22 +160,32 @@ const ensureOrderConsistency = () => { if (entry.type === 'widget' && widgetIds.has(entry.id)) { pushEntry(entry); } + if (entry.type === 'folder' && folderIds.has(entry.id)) { + pushEntry(entry); + } } for (const widget of widgetsStore.widgets) { const key = `widget:${widget.id}`; - if (!seen.has(key)) { + if (!widget.folderId && !seen.has(key)) { pushEntry({ id: widget.id, type: 'widget' }); } } for (const icon of layoutStore.icons) { const key = `icon:${icon.id}`; - if (!seen.has(key)) { + if (!icon.folderId && !seen.has(key)) { pushEntry({ id: icon.id, type: 'icon' }); } } + for (const folder of foldersStore.folders) { + const key = `folder:${folder.id}`; + if (!seen.has(key)) { + pushEntry({ id: folder.id, type: 'folder' }); + } + } + const orderChanged = nextOrder.length !== gridOrder.value.length || nextOrder.some((entry, index) => { @@ -176,42 +206,103 @@ const isInActiveGroup = (groupId?: string | null) => const orderedItems = computed(() => { const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon])); const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget])); + const foldersById = new Map(foldersStore.folders.map(folder => [folder.id, folder])); const used = new Set(); const items: OrderedGridItem[] = []; + const buildChildren = (folder: Folder): FolderChild[] => { + const children: FolderChild[] = []; + for (const child of folder.items) { + if (child.type === 'icon') { + const icon = iconsById.get(child.id); + if (icon) children.push({ type: 'icon', id: icon.id, icon }); + } else if (child.type === 'widget') { + const widget = widgetsById.get(child.id); + if (widget) children.push({ type: 'widget', id: widget.id, widget }); + } + } + return children; + }; + for (const entry of gridOrder.value) { if (entry.type === 'icon') { const icon = iconsById.get(entry.id); - if (icon && isInActiveGroup(icon.groupId)) { + if (icon && !icon.folderId && isInActiveGroup(icon.groupId)) { items.push({ type: 'icon', id: icon.id, icon }); used.add(`icon:${icon.id}`); } - } else { + } else if (entry.type === 'widget') { const widget = widgetsById.get(entry.id); - if (widget && isInActiveGroup(widget.groupId)) { + if (widget && !widget.folderId && isInActiveGroup(widget.groupId)) { items.push({ type: 'widget', id: widget.id, widget }); used.add(`widget:${widget.id}`); } + } else if (entry.type === 'folder') { + const folder = foldersById.get(entry.id); + if (folder && isInActiveGroup(folder.groupId)) { + items.push({ type: 'folder', id: folder.id, folder, children: buildChildren(folder) }); + used.add(`folder:${folder.id}`); + } } } for (const widget of widgetsStore.widgets) { const key = `widget:${widget.id}`; - if (!used.has(key) && isInActiveGroup(widget.groupId)) { + if (!widget.folderId && !used.has(key) && isInActiveGroup(widget.groupId)) { items.push({ type: 'widget', id: widget.id, widget }); } } for (const icon of layoutStore.icons) { const key = `icon:${icon.id}`; - if (!used.has(key) && isInActiveGroup(icon.groupId)) { + if (!icon.folderId && !used.has(key) && isInActiveGroup(icon.groupId)) { items.push({ type: 'icon', id: icon.id, icon }); } } + for (const folder of foldersStore.folders) { + const key = `folder:${folder.id}`; + if (!used.has(key) && isInActiveGroup(folder.groupId)) { + items.push({ type: 'folder', id: folder.id, folder, children: buildChildren(folder) }); + } + } + return items; }); +const resolveFolderChildren = (folderId: string): FolderChild[] => { + const folder = foldersStore.folders.find(f => f.id === folderId); + if (!folder) return []; + const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon])); + const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget])); + const children: FolderChild[] = []; + for (const child of folder.items) { + if (child.type === 'icon') { + const icon = iconsById.get(child.id); + if (icon) children.push({ type: 'icon', id: icon.id, icon }); + } else if (child.type === 'widget') { + const widget = widgetsById.get(child.id); + if (widget) children.push({ type: 'widget', id: widget.id, widget }); + } + } + return children; +}; + +const sizeClassFor = (item: OrderedGridItem) => { + if (item.type === 'widget') return `size-${item.widget.size}`; + if (item.type === 'folder') return `size-${FOLDER_SIZE}`; + return `size-${item.icon.size ?? '1x1'}`; +}; + +const currentFolder = computed(() => { + if (!openFolderId.value) return null; + return foldersStore.folders.find(folder => folder.id === openFolderId.value) ?? null; +}); + +const currentFolderChildren = computed(() => + currentFolder.value ? resolveFolderChildren(currentFolder.value.id) : [] +); + const handleClick = (event: MouseEvent) => { if (suppressClick.value || isDragging.value) return; const target = event.target as HTMLElement; @@ -240,11 +331,163 @@ const handleContextMenu = (event: MouseEvent) => { } const id = itemEl.dataset.id; const type = itemEl.dataset.type as GridItemType | undefined; - if (id && type) { + if (id && (type === 'icon' || type === 'widget')) { uiStore.openContextMenu(event.clientX, event.clientY, id, type); + } else { + uiStore.openContextMenu(event.clientX, event.clientY, null, 'desktop'); } }; +const setHoverTarget = (el: HTMLElement | null) => { + if (hoverTargetEl.value === el) return; + if (hoverTargetEl.value) { + hoverTargetEl.value.classList.remove('is-drop-target'); + } + hoverTargetEl.value = el; + if (el) { + el.classList.add('is-drop-target'); + } +}; + +const clearHoverTarget = () => setHoverTarget(null); + +const findDropTarget = (x: number, y: number, ignoreEl?: HTMLElement | null) => { + if (!gridRef.value) return null; + const elements = Array.from(gridRef.value.querySelectorAll('.grid-item')) as HTMLElement[]; + for (const el of elements) { + if (ignoreEl && el === ignoreEl) continue; + const rect = el.getBoundingClientRect(); + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + return el; + } + } + return null; +}; + +const resolveGroupIdByItem = (item: { id: string; type: GridItemType }) => { + if (item.type === 'icon') { + return layoutStore.icons.find(icon => icon.id === item.id)?.groupId ?? activeGroupId.value; + } + if (item.type === 'widget') { + return widgetsStore.widgets.find(widget => widget.id === item.id)?.groupId ?? activeGroupId.value; + } + if (item.type === 'folder') { + return foldersStore.folders.find(folder => folder.id === item.id)?.groupId ?? activeGroupId.value; + } + return activeGroupId.value; +}; + +const addItemToFolderState = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => { + const folderGroup = foldersStore.folders.find(f => f.id === folderId)?.groupId ?? activeGroupId.value; + if (item.type === 'icon') { + layoutStore.updateIcon(item.id, { folderId, groupId: folderGroup }); + } else { + widgetsStore.updateWidgetFolder(item.id, folderId, folderGroup); + } + foldersStore.addItem(folderId, { id: item.id, type: item.type }); +}; + +const removeItemFromFolderState = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => { + if (item.type === 'icon') { + layoutStore.updateIcon(item.id, { folderId: undefined }); + } else { + widgetsStore.updateWidgetFolder(item.id, undefined); + } + foldersStore.removeItem(folderId, item.id, item.type); +}; + +const removeEntryFromOrder = (order: GridOrderEntry[], item: { id: string; type: GridItemType }) => + order.filter(entry => !(entry.id === item.id && entry.type === item.type)); + +const createFolderFromItems = ( + source: { id: string; type: 'icon' | 'widget' }, + target: { id: string; type: 'icon' | 'widget' } +) => { + const groupId = resolveGroupIdByItem(target); + const folder = foldersStore.addFolder({ + name: '组', + groupId, + items: [ + { id: target.id, type: target.type }, + { id: source.id, type: source.type }, + ], + }); + addItemToFolderState(folder.id, target); + addItemToFolderState(folder.id, source); + + const cleaned = removeEntryFromOrder(removeEntryFromOrder(gridOrder.value, source), target); + const targetIndex = gridOrder.value.findIndex( + entry => entry.id === target.id && entry.type === target.type + ); + const insertIndex = targetIndex >= 0 ? targetIndex : cleaned.length; + cleaned.splice(insertIndex, 0, { id: folder.id, type: 'folder' }); + applyGridOrder(cleaned, true); + refreshLayout(true); +}; + +const moveItemIntoFolder = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => { + const folder = foldersStore.folders.find(f => f.id === folderId); + if (!folder) return; + addItemToFolderState(folderId, item); + const nextOrder = removeEntryFromOrder(gridOrder.value, item); + applyGridOrder(nextOrder, true); + refreshLayout(true); +}; + +const handleMergeDrop = () => { + const dragId = draggingMeta.value.id; + const dragType = draggingMeta.value.type; + if (!dragId || !dragType) return; + const targetEl = hoverTargetEl.value; + clearHoverTarget(); + if (!targetEl) return; + const targetId = targetEl.dataset.id; + const targetType = targetEl.dataset.type as GridItemType | undefined; + if (!targetId || !targetType) return; + if (targetId === dragId && targetType === dragType) return; + if (dragType === 'folder') return; // no nesting folders for now + + if (targetType === 'folder') { + moveItemIntoFolder(targetId, { id: dragId, type: dragType }); + return; + } + if (targetType === 'icon' || targetType === 'widget') { + if (dragType === 'icon' || dragType === 'widget') { + createFolderFromItems({ id: dragId, type: dragType }, { id: targetId, type: targetType }); + } + } +}; + +const openFolder = (folderId: string) => { + openFolderId.value = folderId; +}; + +const handleRenameFolder = (payload: { id: string; name: string }) => { + foldersStore.renameFolder(payload.id, payload.name); +}; + +const handleOpenChild = (child: FolderChild) => { + if (child.type === 'icon') { + const targetMode = settingsStore.openInNewTab ? '_blank' : '_self'; + window.open(child.icon.url, targetMode); + } +}; + +const handleRemoveFromFolder = (payload: { folderId: string; child: FolderChild }) => { + const { folderId, child } = payload; + removeItemFromFolderState(folderId, { id: child.id, type: child.type }); + let nextOrder = removeEntryFromOrder(gridOrder.value, { id: child.id, type: child.type }); + const folderExists = foldersStore.folders.some(f => f.id === folderId); + if (!folderExists) { + nextOrder = removeEntryFromOrder(nextOrder, { id: folderId, type: 'folder' }); + } + const folderIndex = nextOrder.findIndex(entry => entry.id === folderId && entry.type === 'folder'); + const insertIndex = folderIndex >= 0 ? folderIndex + 1 : nextOrder.length; + nextOrder.splice(insertIndex, 0, { id: child.id, type: child.type }); + applyGridOrder(nextOrder, true); + refreshLayout(true); +}; + const persistOrderFromGrid = async () => { if (!grid.value) return; const items = grid.value.getItems(); @@ -253,7 +496,7 @@ const persistOrderFromGrid = async () => { const element = item.getElement() as HTMLElement; const id = element.dataset.id; const type = element.dataset.type as GridItemType | undefined; - if (id && (type === 'icon' || type === 'widget')) { + if (id && (type === 'icon' || type === 'widget' || type === 'folder')) { nextOrder.push({ id, type }); } } @@ -373,21 +616,36 @@ onMounted(async () => { layoutEasing, }); - grid.value.on('dragStart', () => { + grid.value.on('dragStart', (item: any) => { isDragging.value = true; suppressClick.value = true; + const el = item.getElement() as HTMLElement; + draggingMeta.value = { + id: el?.dataset?.id ?? null, + type: (el?.dataset?.type as GridItemType) ?? null, + }; + }); + + grid.value.on('dragMove', (item: any) => { + const el = item.getElement() as HTMLElement; + const rect = el.getBoundingClientRect(); + const target = findDropTarget(rect.left + rect.width / 2, rect.top + rect.height / 2, el); + setHoverTarget(target); }); grid.value.on('dragEnd', () => { isDragging.value = false; + handleMergeDrop(); clickBlockUntil.value = Date.now() + 180; window.setTimeout(() => { suppressClick.value = false; }, 0); + draggingMeta.value = { id: null, type: null }; }); grid.value.on('dragReleaseEnd', () => { persistOrderFromGrid(); + clearHoverTarget(); }); grid.value.on('layoutStart', () => { @@ -457,6 +715,15 @@ watch( } ); +watch( + () => foldersStore.folders.map(folder => ({ id: folder.id, items: folder.items.length, groupId: folder.groupId })), + () => { + ensureOrderConsistency(); + refreshLayout(true); + }, + { deep: true } +); + watch( () => props.activeGroupId, () => { @@ -498,10 +765,11 @@ onUnmounted(() => { :key="`${item.type}-${item.id}`" class="grid-item" :class="[ - `size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`, + sizeClassFor(item), { 'is-resized': resizedKeys.has(`${item.type}:${item.id}`), - 'is-entering': enteringKeys.has(`${item.type}:${item.id}`) + 'is-entering': enteringKeys.has(`${item.type}:${item.id}`), + 'is-drop-target': hoverTargetEl && hoverTargetEl.dataset?.id === item.id } ]" :data-id="item.id" @@ -509,10 +777,25 @@ onUnmounted(() => { >
- + +
+ diff --git a/app/src/store/useFoldersStore.ts b/app/src/store/useFoldersStore.ts new file mode 100644 index 0000000..68613c6 --- /dev/null +++ b/app/src/store/useFoldersStore.ts @@ -0,0 +1,111 @@ +import { defineStore, acceptHMRUpdate } from 'pinia'; + +type FolderItemType = 'icon' | 'widget'; + +export interface FolderItem { + id: string; + type: FolderItemType; +} + +export interface Folder { + id: string; + name: string; + items: FolderItem[]; + groupId?: string; +} + +interface FolderState { + folders: Folder[]; +} + +const DEFAULT_GROUP_ID = 'home'; +const STORAGE_KEY = 'itab_folders'; + +const normalizeFolder = (folder: Folder): Folder => ({ + id: folder.id, + name: folder.name || '组', + groupId: folder.groupId ?? DEFAULT_GROUP_ID, + items: Array.isArray(folder.items) + ? folder.items + .filter(item => item && typeof item.id === 'string' && (item.type === 'icon' || item.type === 'widget')) + .map(item => ({ id: item.id, type: item.type })) + : [] +}); + +const loadFolders = (): Folder[] => { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + try { + const parsed = JSON.parse(raw) as Folder[]; + if (!Array.isArray(parsed)) return []; + return parsed.map(normalizeFolder); + } catch { + return []; + } +}; + +export const useFoldersStore = defineStore('folders', { + state: (): FolderState => ({ + folders: loadFolders(), + }), + actions: { + persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.folders)); + }, + addFolder(payload: { name?: string; items?: FolderItem[]; groupId?: string }) { + const nextId = `folder-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const folder: Folder = normalizeFolder({ + id: nextId, + name: payload.name || '组', + items: payload.items ?? [], + groupId: payload.groupId ?? DEFAULT_GROUP_ID, + }); + this.folders.push(folder); + this.persist(); + return folder; + }, + renameFolder(folderId: string, name: string) { + const folder = this.folders.find(item => item.id === folderId); + if (!folder) return; + folder.name = name || '组'; + this.persist(); + }, + addItem(folderId: string, item: FolderItem) { + const folder = this.folders.find(f => f.id === folderId); + if (!folder) return; + const exists = folder.items.some(it => it.id === item.id && it.type === item.type); + if (!exists) { + folder.items.push({ id: item.id, type: item.type }); + this.persist(); + } + }, + removeItem(folderId: string, itemId: string, itemType: FolderItemType) { + const folder = this.folders.find(f => f.id === folderId); + if (!folder) return; + folder.items = folder.items.filter(item => !(item.id === itemId && item.type === itemType)); + if (!folder.items.length) { + this.deleteFolder(folderId); + } else { + this.persist(); + } + }, + deleteFolder(folderId: string) { + const idx = this.folders.findIndex(f => f.id === folderId); + if (idx !== -1) { + this.folders.splice(idx, 1); + this.persist(); + } + }, + clearEmptyFolders() { + const before = this.folders.length; + this.folders = this.folders.filter(folder => folder.items.length > 0); + if (this.folders.length !== before) { + this.persist(); + } + }, + } +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useFoldersStore, import.meta.hot)); +} diff --git a/app/src/store/useLayoutStore.ts b/app/src/store/useLayoutStore.ts index 75e55cc..3b17517 100644 --- a/app/src/store/useLayoutStore.ts +++ b/app/src/store/useLayoutStore.ts @@ -1,18 +1,18 @@ -import { defineStore, acceptHMRUpdate } from 'pinia'; +import { defineStore, acceptHMRUpdate } from 'pinia'; type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4'; const DEFAULT_GROUP_ID = 'home'; -// 模拟数据(来自截图参考) interface Icon { id: string; name: string; url: string; size?: IconSize; - img?: string; // 可选:用于图片图标(如徽标) - text?: string; // 可选:用于文字图标 - bgColor?: string; // 可选:纯色背景 + img?: string; + text?: string; + bgColor?: string; groupId?: string; + folderId?: string; } interface DragState { @@ -32,21 +32,21 @@ interface LayoutState { const defaultIcons: Icon[] = [ { id: '1', name: '淘宝', url: 'https://taobao.com', bgColor: '#ff4f00' }, - { id: '2', name: '京东商城', url: 'https://jd.com', bgColor: '#e4393c' }, + { id: '2', name: '京东', url: 'https://jd.com', bgColor: '#e4393c' }, { id: '3', name: '百度', url: 'https://baidu.com', bgColor: '#3388ff' }, - { id: '4', name: '备忘录', url: '#', bgColor: '#f9ca24' }, + { id: '4', name: '电影', url: '#', bgColor: '#f9ca24' }, { id: '5', name: '爱奇艺', url: 'https://iqiyi.com', bgColor: '#00be06' }, { id: '6', name: '文件夹', url: '#', bgColor: '#4285f4' }, { id: '7', name: '抖音', url: 'https://douyin.com', bgColor: '#222' }, - { id: '8', name: '小浣熊', url: '#', bgColor: '#f0932b' }, + { id: '8', name: '微博', url: '#', bgColor: '#f0932b' }, { id: '9', name: 'AiPPT', url: '#', bgColor: '#d63031' }, - { id: '10', name: '电影日历', url: '#', bgColor: 'transparent', img: 'https://example.com/movie_poster.png' }, // 图片示例 - { id: '11', name: '稿定设计', url: '#', bgColor: '#00aaff' }, - { id: '12', name: '壁纸', url: '#', bgColor: '#1dd1a1' }, - { id: '13', 'name': '即梦AI', url: '#', bgColor: '#6c5ce7' }, - { id: '14', name: '码上掘金', url: '#', bgColor: '#1e80ff' }, - { id: '15', name: '扩展管理', url: '#', bgColor: '#7f8c8d' }, - { id: '16', name: '书签管理', url: '#', bgColor: '#f1c40f' }, + { id: '10', name: '电影日历', url: '#', bgColor: 'transparent', img: 'https://example.com/movie_poster.png' }, + { id: '11', name: '产品设计', url: '#', bgColor: '#00aaff' }, + { id: '12', name: '插画', url: '#', bgColor: '#1dd1a1' }, + { id: '13', name: '科普AI', url: '#', bgColor: '#6c5ce7' }, + { id: '14', name: '代码助手', url: '#', bgColor: '#1e80ff' }, + { id: '15', name: '拓展管理', url: '#', bgColor: '#7f8c8d' }, + { id: '16', name: '签名管理', url: '#', bgColor: '#f1c40f' }, ]; const defaultGroupById: Record = { @@ -75,6 +75,7 @@ const normalizeIcons = (icons: Icon[]): Icon[] => ...icon, size: icon.size ?? '1x1', groupId: icon.groupId ?? defaultGroupById[icon.id] ?? DEFAULT_GROUP_ID, + folderId: icon.folderId, })); const loadIcons = (): Icon[] => { @@ -116,12 +117,9 @@ export const useLayoutStore = defineStore('layout', { const targetIndex = this.icons.findIndex(p => p.id === targetId); if (draggedIndex !== -1 && targetIndex !== -1) { - // 直接交换 const draggedItem = this.icons[draggedIndex]; this.icons[draggedIndex] = this.icons[targetIndex]; this.icons[targetIndex] = draggedItem; - - // 持久化顺序 localStorage.setItem('itab_icons', JSON.stringify(this.icons)); } }, @@ -154,7 +152,6 @@ export const useLayoutStore = defineStore('layout', { const index = this.icons.findIndex(p => p.id === itemId); if (index !== -1) { this.icons.splice(index, 1); - // 持久化顺序 localStorage.setItem('itab_icons', JSON.stringify(this.icons)); } }, diff --git a/app/src/store/useWidgetsStore.ts b/app/src/store/useWidgetsStore.ts index 1b38a10..92bc5b1 100644 --- a/app/src/store/useWidgetsStore.ts +++ b/app/src/store/useWidgetsStore.ts @@ -1,15 +1,16 @@ -import { defineStore, acceptHMRUpdate } from 'pinia'; +import { defineStore, acceptHMRUpdate } from 'pinia'; -// 组件数据结构 +// 缁勪欢鏁版嵁缁撴瀯 type WidgetSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4'; const DEFAULT_GROUP_ID = 'home'; interface Widget { id: string; - component: string; // 渲染的组件名 - size: WidgetSize; // 组件尺寸 - gridPosition: { x: number; y: number }; // 网格位置 - data?: any; // 组件数据 + component: string; // 娓叉煋鐨勭粍浠跺悕 + size: WidgetSize; // 缁勪欢灏哄 + gridPosition: { x: number; y: number }; // 缃戞牸浣嶇疆 + data?: any; // 缁勪欢鏁版嵁 groupId?: string; + folderId?: string; } interface WidgetsState { @@ -29,12 +30,12 @@ const defaultWidgets: Widget[] = [ size: '2x1', gridPosition: { x: 1, y: 0 }, data: { - tabs: ['百度', '微博', '抖音'], + tabs: ['热点', '新闻', '视频'], items: [ - { title: '茅台确认“马茅”包装少写一撇', value: '780.9万' }, - { title: '双休不应成为奢侈品', value: '771.2万' }, - { title: '突破8100亿元这场双向奔赴很燃', value: '761.8万' }, - { title: '第一个2万亿经济大区要来了', value: '752.2万' } + { title: '平台确认新品发售', value: '780.9万' }, + { title: '网友热议某话题', value: '771.2万' }, + { title: '新机性能实测数据', value: '761.8万' }, + { title: '首个万亿级经济区新闻', value: '752.2万' } ] } }, @@ -69,7 +70,7 @@ const loadWidgets = (): Widget[] => { } }; -// 模拟数据(来自截图参考) +// 妯℃嫙鏁版嵁锛堟潵鑷埅鍥惧弬鑰冿級 export const useWidgetsStore = defineStore('widgets', { state: (): WidgetsState => ({ widgets: loadWidgets(), @@ -119,6 +120,15 @@ export const useWidgetsStore = defineStore('widgets', { localStorage.setItem('itab_widgets', JSON.stringify(this.widgets)); } }, + updateWidgetFolder(widgetId: string, folderId?: string, groupId?: string) { + const widget = this.widgets.find(w => w.id === widgetId); + if (!widget) return; + widget.folderId = folderId; + if (groupId) { + widget.groupId = groupId; + } + localStorage.setItem('itab_widgets', JSON.stringify(this.widgets)); + }, setWidgetOrder(orderIds: string[]) { const ordered: Widget[] = []; const seen = new Set(); @@ -143,3 +153,4 @@ export const useWidgetsStore = defineStore('widgets', { if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useWidgetsStore, import.meta.hot)); } + diff --git a/wechat_2026-01-22_152229_725.png b/wechat_2026-01-22_152229_725.png deleted file mode 100644 index fed7c90..0000000 Binary files a/wechat_2026-01-22_152229_725.png and /dev/null differ diff --git a/wechat_2026-01-22_173911_794.png b/wechat_2026-01-22_173911_794.png deleted file mode 100644 index 4df4d59..0000000 Binary files a/wechat_2026-01-22_173911_794.png and /dev/null differ diff --git a/wechat_2026-01-27_113656_942.png b/wechat_2026-01-27_113656_942.png deleted file mode 100644 index 2b9063d..0000000 Binary files a/wechat_2026-01-27_113656_942.png and /dev/null differ