diff --git a/ScreenShot_2026-01-29_094743_454.png b/ScreenShot_2026-01-29_094743_454.png new file mode 100644 index 0000000..9599c25 Binary files /dev/null and b/ScreenShot_2026-01-29_094743_454.png differ diff --git a/ScreenShot_2026-01-29_101729_394.png b/ScreenShot_2026-01-29_101729_394.png new file mode 100644 index 0000000..97530a4 Binary files /dev/null and b/ScreenShot_2026-01-29_101729_394.png differ diff --git a/app/src/App.vue b/app/src/App.vue index 216a05a..7c8c563 100644 --- a/app/src/App.vue +++ b/app/src/App.vue @@ -4,12 +4,13 @@ :groups="sidebarGroups" :base-overrides="sidebarOverrides" :hidden-base-ids="hiddenBaseIds" + @select="handleSidebarSelect" @add="openAddGroup" @edit-group="openEditGroup" @remove-group="handleRemoveGroup" @settings="toggleSettings" /> - + ('add'); const addIconInitialTab = ref<'widgets' | 'sites' | 'custom'>('widgets'); const replaceWidgetId = ref(null); +const defaultGroupId = 'home'; +const activeGroupId = ref(defaultGroupId); const editTarget = ref<{ id: string; source: 'base' | 'group' } | null>(null); const layoutStore = useLayoutStore(); const widgetsStore = useWidgetsStore(); @@ -154,6 +157,10 @@ const toggleSettings = () => { isSettingsOpen.value = !isSettingsOpen.value; }; +const handleSidebarSelect = (id: string) => { + activeGroupId.value = id; +}; + const openAddIcon = () => { addIconMode.value = 'add'; addIconInitialTab.value = 'widgets'; @@ -252,6 +259,7 @@ const handleAddIcon = (payload: { name: string; url: string; bgColor: string; im bgColor: payload.bgColor, img: payload.img, text: payload.text, + groupId: activeGroupId.value ?? defaultGroupId, }); }; @@ -270,6 +278,7 @@ const handleAddWidget = (payload: { component: string; size?: '1x1' | '1x2' | '2 component: payload.component, size: payload.size, data: payload.data, + groupId: activeGroupId.value ?? defaultGroupId, }); }; diff --git a/app/src/components/GridCanvas/index.vue b/app/src/components/GridCanvas/index.vue index 553806d..6475d99 100644 --- a/app/src/components/GridCanvas/index.vue +++ b/app/src/components/GridCanvas/index.vue @@ -19,6 +19,7 @@ interface Icon { text?: string; bgColor?: string; size?: GridItemSize; + groupId?: string; } type WidgetSize = GridItemSize; @@ -27,6 +28,7 @@ interface Widget { component: string; size: WidgetSize; data?: any; + groupId?: string; } interface GridOrderEntry { @@ -39,6 +41,11 @@ type OrderedGridItem = | { type: 'widget'; id: string; widget: Widget }; const GRID_ORDER_STORAGE_KEY = 'itab_grid_order'; +const DEFAULT_GROUP_ID = 'home'; + +const props = defineProps<{ + activeGroupId?: string | null; +}>(); const layoutStore = useLayoutStore(); const widgetsStore = useWidgetsStore(); @@ -59,6 +66,22 @@ const previousIconIds = ref(new Set()); const previousWidgetIds = ref(new Set()); const layoutAnimationMs = 240; const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)'; +const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID; +const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId)); + +const normalizeGridOrder = (order: GridOrderEntry[]) => { + const seen = new Set(); + const normalized: GridOrderEntry[] = []; + for (const entry of order) { + if (!entry || typeof entry.id !== 'string') continue; + if (entry.type !== 'icon' && entry.type !== 'widget') continue; + const key = `${entry.type}:${entry.id}`; + if (seen.has(key)) continue; + seen.add(key); + normalized.push(entry); + } + return normalized; +}; const buildDefaultOrder = (): GridOrderEntry[] => [ ...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })), @@ -85,7 +108,7 @@ const loadGridOrder = () => { try { const parsed = JSON.parse(saved) as GridOrderEntry[]; if (Array.isArray(parsed)) { - gridOrder.value = parsed.filter(item => item?.id && (item.type === 'icon' || item.type === 'widget')); + gridOrder.value = normalizeGridOrder(parsed); } } catch { gridOrder.value = []; @@ -100,26 +123,36 @@ 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 seen = new Set(); const nextOrder: GridOrderEntry[] = []; + const pushEntry = (entry: GridOrderEntry) => { + const key = `${entry.type}:${entry.id}`; + if (seen.has(key)) return; + seen.add(key); + nextOrder.push(entry); + }; + for (const entry of gridOrder.value) { if (entry.type === 'icon' && iconIds.has(entry.id)) { - nextOrder.push(entry); + pushEntry(entry); } if (entry.type === 'widget' && widgetIds.has(entry.id)) { - nextOrder.push(entry); + pushEntry(entry); } } for (const widget of widgetsStore.widgets) { - if (!nextOrder.some(item => item.type === 'widget' && item.id === widget.id)) { - nextOrder.push({ id: widget.id, type: 'widget' }); + const key = `widget:${widget.id}`; + if (!seen.has(key)) { + pushEntry({ id: widget.id, type: 'widget' }); } } for (const icon of layoutStore.icons) { - if (!nextOrder.some(item => item.type === 'icon' && item.id === icon.id)) { - nextOrder.push({ id: icon.id, type: 'icon' }); + const key = `icon:${icon.id}`; + if (!seen.has(key)) { + pushEntry({ id: icon.id, type: 'icon' }); } } @@ -137,6 +170,9 @@ const ensureOrderConsistency = () => { } }; +const isInActiveGroup = (groupId?: string | null) => + normalizeGroupId(groupId) === activeGroupId.value; + 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])); @@ -146,13 +182,13 @@ const orderedItems = computed(() => { for (const entry of gridOrder.value) { if (entry.type === 'icon') { const icon = iconsById.get(entry.id); - if (icon) { + if (icon && isInActiveGroup(icon.groupId)) { items.push({ type: 'icon', id: icon.id, icon }); used.add(`icon:${icon.id}`); } } else { const widget = widgetsById.get(entry.id); - if (widget) { + if (widget && isInActiveGroup(widget.groupId)) { items.push({ type: 'widget', id: widget.id, widget }); used.add(`widget:${widget.id}`); } @@ -161,14 +197,14 @@ const orderedItems = computed(() => { for (const widget of widgetsStore.widgets) { const key = `widget:${widget.id}`; - if (!used.has(key)) { + if (!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)) { + if (!used.has(key) && isInActiveGroup(icon.groupId)) { items.push({ type: 'icon', id: icon.id, icon }); } } @@ -222,7 +258,36 @@ const persistOrderFromGrid = async () => { } } if (nextOrder.length) { - applyGridOrder(nextOrder, true); + if (!gridOrder.value.length) { + applyGridOrder(nextOrder, true); + } else { + const entryKey = (entry: GridOrderEntry) => `${entry.type}:${entry.id}`; + const visibleKeys = new Set(nextOrder.map(entryKey)); + const visibleQueue = [...nextOrder]; + const merged: GridOrderEntry[] = []; + + for (const entry of gridOrder.value) { + if (visibleKeys.has(entryKey(entry))) { + const nextEntry = visibleQueue.shift(); + if (nextEntry) { + merged.push(nextEntry); + } + } else { + merged.push(entry); + } + } + + const mergedKeys = new Set(merged.map(entryKey)); + for (const entry of visibleQueue) { + const key = entryKey(entry); + if (!mergedKeys.has(key)) { + merged.push(entry); + mergedKeys.add(key); + } + } + + applyGridOrder(merged, true); + } } await nextTick(); grid.value?.synchronize(); @@ -247,7 +312,11 @@ const syncGridItems = () => { const refreshLayout = async (instant = false) => { await nextTick(); syncGridItems(); + grid.value?.synchronize(); grid.value?.refreshItems(); + if (grid.value?._settings?.layout) { + grid.value._settings.layout.fillGaps = true; + } if (instant) { grid.value?.layout(true); } else { @@ -283,6 +352,7 @@ const syncSizeMap = ( onMounted(async () => { loadGridOrder(); + ensureOrderConsistency(); previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id)); previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id)); await nextTick(); @@ -296,7 +366,7 @@ onMounted(async () => { }, dragSort: true, layout: { - fillGaps: settingsStore.autoAlign, + fillGaps: true, rounding: true, }, layoutDuration: layoutAnimationMs, @@ -388,12 +458,10 @@ watch( ); watch( - () => settingsStore.autoAlign, - value => { + () => props.activeGroupId, + () => { if (!grid.value) return; - // Muuri has no public setter for fillGaps; update internal setting and relayout. - grid.value._settings.layout.fillGaps = value; - grid.value.layout(true); + refreshLayout(true); } ); @@ -475,8 +543,7 @@ onUnmounted(() => { } .grid-item.is-resized { - overflow: hidden; - border-radius: $border-radius-small; + overflow: visible; } .grid-item.is-entering { diff --git a/app/src/components/IconCard/index.vue b/app/src/components/IconCard/index.vue index 640cd4a..ae6ce95 100644 --- a/app/src/components/IconCard/index.vue +++ b/app/src/components/IconCard/index.vue @@ -10,7 +10,6 @@ {{ props.icon.text?.trim() || props.icon.name.charAt(0) }} -
{{ props.icon.name }}
@@ -38,9 +37,8 @@ const props = defineProps<{ @import '@/styles/tokens.scss'; .icon-card-wrapper { - display: grid; - grid-template-rows: minmax(0, 1fr) var(--icon-label-margin-top) auto; - align-items: stretch; + position: relative; + display: block; width: 100%; height: 100%; text-decoration: none; @@ -83,27 +81,21 @@ const props = defineProps<{ } } -.card-spacer { - height: 100%; - pointer-events: none; -} - -.icon-card-wrapper.size-1x1 .icon-card { - width: auto; - height: 100%; - aspect-ratio: 1 / 1; - justify-self: center; -} - .label { + position: absolute; + top: 100%; + left: 50%; + margin-top: var(--icon-label-margin-top); + transform: translateX(-50%); font-size: var(--icon-label-font-size); color: $color-text-primary; text-align: center; width: 100%; - display: var(--icon-label-display, block); - white-space: normal; - word-break: break-word; + max-width: 100%; + visibility: var(--icon-label-visibility, visible); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; line-height: 1.2; - pointer-events: none; } diff --git a/app/src/components/MainContent/index.vue b/app/src/components/MainContent/index.vue index 66f0b0a..71ea6a6 100644 --- a/app/src/components/MainContent/index.vue +++ b/app/src/components/MainContent/index.vue @@ -1,14 +1,16 @@ @@ -24,6 +26,10 @@ import GridCanvas from '@/components/GridCanvas/index.vue'; import { useSettingsStore } from '@/store/useSettingsStore'; import { storeToRefs } from 'pinia'; +defineProps<{ + activeGroupId?: string | null; +}>(); + const uiStore = useUIStore(); const settingsStore = useSettingsStore(); const { showSearch, showClock, showDate, showSeconds, use24Hour } = storeToRefs(settingsStore); @@ -49,6 +55,13 @@ const handleDesktopContextMenu = (event: MouseEvent) => { justify-content: center; align-items: flex-start; // 内容顶部对齐 overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.main-content::-webkit-scrollbar { + width: 0; + height: 0; } .main-content-container { @@ -59,4 +72,15 @@ const handleDesktopContextMenu = (event: MouseEvent) => { flex-direction: column; align-items: center; } + +.content-header { + position: sticky; + top: var(--content-padding); + z-index: 2; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + align-self: stretch; +} diff --git a/app/src/components/SettingsPanel/index.vue b/app/src/components/SettingsPanel/index.vue index a9a3aee..d38e5b8 100644 --- a/app/src/components/SettingsPanel/index.vue +++ b/app/src/components/SettingsPanel/index.vue @@ -179,12 +179,12 @@
用网格密度统一控制图标与组件大小。
@@ -362,6 +362,7 @@ import { useSettingsStore } from '@/store/useSettingsStore'; import { iconDensityRange, iconRadiusRange, + layoutDensityRange, searchProviders, themePresets, } from '@/config/settingsPresets'; @@ -441,9 +442,13 @@ const themeId = computed({ set: value => settingsStore.setSetting('themeId', value as typeof settingsStore.themeId), }); -const layoutDensity = computed({ - get: () => settingsStore.layoutDensity, - set: value => settingsStore.setSetting('layoutDensity', value), +const invertLayoutDensity = (value: number) => + layoutDensityRange.max + layoutDensityRange.min - + Math.min(Math.max(value, layoutDensityRange.min), layoutDensityRange.max); + +const iconSizeDensity = computed({ + get: () => invertLayoutDensity(settingsStore.layoutDensity), + set: value => settingsStore.setSetting('layoutDensity', invertLayoutDensity(value)), }); const compactSidebar = computed({ @@ -656,7 +661,6 @@ const clampRange = (value: number, min: number, max: number) => const applyImportedSettings = (settings: Partial) => { const providerIds = new Set(searchProviders.map(item => item.id)); const themeIds = new Set(themePresets.map(item => item.id)); - const layoutDensityValues = new Set([1, 2, 3, 4]); if (typeof settings.showSearch === 'boolean') { settingsStore.setSetting('showSearch', settings.showSearch); @@ -709,8 +713,11 @@ const applyImportedSettings = (settings: Partial) = clampRange(settings.iconDensity, iconDensityRange.min, iconDensityRange.max) ); } - if (typeof settings.layoutDensity === 'number' && layoutDensityValues.has(settings.layoutDensity)) { - settingsStore.setSetting('layoutDensity', settings.layoutDensity as typeof settingsStore.layoutDensity); + if (typeof settings.layoutDensity === 'number') { + settingsStore.setSetting( + 'layoutDensity', + clampRange(settings.layoutDensity, layoutDensityRange.min, layoutDensityRange.max) + ); } }; diff --git a/app/src/components/TheSidebar/index.vue b/app/src/components/TheSidebar/index.vue index d503f68..1d5e1ea 100644 --- a/app/src/components/TheSidebar/index.vue +++ b/app/src/components/TheSidebar/index.vue @@ -169,6 +169,16 @@ const setActive = (id: string) => { localStorage.setItem(STORAGE_KEY, id); }; +watch( + () => activeId.value, + id => { + if (selectableItems.value.some(item => item.id === id)) { + emit('select', id); + } + }, + { immediate: true } +); + const handleItemClick = (item: NavItem) => { if (item.action === 'add') { emit('add'); diff --git a/app/src/components/WidgetCard/index.vue b/app/src/components/WidgetCard/index.vue index 7685d98..112500f 100644 --- a/app/src/components/WidgetCard/index.vue +++ b/app/src/components/WidgetCard/index.vue @@ -5,7 +5,6 @@
-
{{ label }}
@@ -41,29 +40,22 @@ const label = computed(() => { @import '@/styles/tokens.scss'; .widget-card-wrapper { + position: relative; width: 100%; height: 100%; - display: grid; - grid-template-rows: minmax(0, 1fr) var(--icon-label-margin-top) auto; padding: var(--widget-card-padding, var(--icon-card-padding)); - - // 鏍规嵁灏哄绫诲悕鎺у埗灏哄 - &.size-1x1 { grid-column: span 1; grid-row: span 1; } - &.size-1x2 { grid-column: span 1; grid-row: span 2; } - &.size-2x1 { grid-column: span 2; grid-row: span 1; } - &.size-2x2 { grid-column: span 2; grid-row: span 2; } - &.size-2x4 { grid-column: span 2; grid-row: span 4; } - + box-sizing: border-box; } .widget-card { + box-sizing: border-box; width: 100%; height: 100%; min-height: 0; background-color: $color-surface-1; backdrop-filter: blur($backdrop-filter-blur) saturate($backdrop-filter-saturation); border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: $border-radius-small; + border-radius: var(--icon-radius, #{$border-radius-small}); box-shadow: $shadow-md; display: flex; flex-direction: column; @@ -72,31 +64,26 @@ const label = computed(() => { transform-origin: center; } -.card-spacer { - height: 100%; - pointer-events: none; -} - -.widget-card-wrapper.size-1x1 .widget-card { - width: auto; - height: 100%; - aspect-ratio: 1 / 1; - justify-self: center; -} - .widget-content { flex-grow: 1; padding: var(--widget-content-padding); } .label { + position: absolute; + top: 100%; + left: 50%; + margin-top: var(--icon-label-margin-top); + transform: translateX(-50%); font-size: var(--widget-label-font-size); color: $color-text-primary; text-align: center; width: 100%; - display: var(--widget-label-display, block); - white-space: normal; - word-break: break-word; + max-width: 100%; + visibility: var(--widget-label-visibility, visible); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; line-height: 1.2; flex-shrink: 0; } diff --git a/app/src/composables/useSettingsSync.ts b/app/src/composables/useSettingsSync.ts index f543a2c..378182f 100644 --- a/app/src/composables/useSettingsSync.ts +++ b/app/src/composables/useSettingsSync.ts @@ -53,8 +53,9 @@ export const useSettingsSync = () => { const baseGrid = layoutConfig.grid; const sizeScale = layoutPreset.cellSize / baseGrid.cellSize; const labelScale = clampRange(sizeScale, 0.7, 1); + const densityScale = getIconDensityScale(settingsStore.iconDensity); const iconPadding = Math.max(0, baseIcon.padding * sizeScale); - const widgetScale = sizeScale; + const widgetScale = 1; const widgetPadding = Math.max(0, baseIcon.padding * sizeScale); const iconFontSize = Math.max(14, Math.round(baseIcon.fontSize * sizeScale)); const iconLabelFontSize = Math.max(10, Math.round(baseIcon.labelFontSize * labelScale)); @@ -67,11 +68,7 @@ export const useSettingsSync = () => { 4, Math.round(layoutConfig.widget.labelPaddingY * labelScale) ); - const densityScale = getIconDensityScale(settingsStore.iconDensity); - const baseVisualGap = (layoutPreset.gap + iconPadding * 2) * densityScale; - // Keep density purely controlling spacing; allow deeper negative gap for extra-tight layouts. - const minGap = -layoutPreset.cellSize * 0.75; - const gridGap = Math.max(minGap, baseVisualGap - iconPadding * 2); + const gridGap = Math.max(0, Math.round(baseGrid.gap * densityScale)); setPx(root, '--grid-cell-size', layoutPreset.cellSize); setPx(root, '--grid-gap', gridGap); @@ -82,16 +79,13 @@ export const useSettingsSync = () => { setPx(root, '--icon-font-size', iconFontSize); setPx(root, '--icon-label-font-size', iconLabelFontSize); setPx(root, '--icon-radius', iconRadius); - setPx( - root, - '--icon-label-margin-top', - settingsStore.showIconLabels ? iconLabelMarginTop : 0 - ); + setPx(root, '--icon-label-margin-top', iconLabelMarginTop); setPx(root, '--widget-label-font-size', widgetLabelFontSize); setPx(root, '--widget-label-padding-y', widgetLabelPaddingY); - setVar(root, '--icon-label-display', settingsStore.showIconLabels ? 'block' : 'none'); - setVar(root, '--widget-label-display', settingsStore.showIconLabels ? 'block' : 'none'); + const labelVisibility = settingsStore.showIconLabels ? 'visible' : 'hidden'; + setVar(root, '--icon-label-visibility', labelVisibility); + setVar(root, '--widget-label-visibility', labelVisibility); const sidebarVars = getSidebarVars(settingsStore.compactSidebar); setPx(root, '--sidebar-width', sidebarVars.width); diff --git a/app/src/config/layout.ts b/app/src/config/layout.ts index dfe6f18..566a208 100644 --- a/app/src/config/layout.ts +++ b/app/src/config/layout.ts @@ -54,7 +54,7 @@ export const layoutConfig = { // 图标首字母字体大小(像素)。 fontSize: 28, // 卡片内边距(像素)。 - padding: 8, + padding: 0, // 图标卡片圆角(像素)。 radius: 12, // 标签字体大小(像素)。 diff --git a/app/src/config/settingsPresets.ts b/app/src/config/settingsPresets.ts index 4e11102..767016c 100644 --- a/app/src/config/settingsPresets.ts +++ b/app/src/config/settingsPresets.ts @@ -83,7 +83,7 @@ const clampRange = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); export type LayoutDensityPreset = { - value: 1 | 2 | 3 | 4; + value: number; label: string; cellSize: number; gap: number; @@ -119,19 +119,46 @@ export const layoutDensityPresets: LayoutDensityPreset[] = [ }, ]; -export const getLayoutDensityPreset = (value?: number) => - layoutDensityPresets.find(item => item.value === value) ?? layoutDensityPresets[1]; +export const layoutDensityRange = { + min: 1, + max: 4, + step: 0.1, + default: 2, +}; + +export const getLayoutDensityPreset = (value?: number) => { + const normalized = clampRange( + typeof value === 'number' ? value : layoutDensityRange.default, + layoutDensityRange.min, + layoutDensityRange.max + ); + const lower = Math.floor(normalized); + const upper = Math.ceil(normalized); + if (lower === upper) { + return layoutDensityPresets[lower - 1] ?? layoutDensityPresets[1]; + } + const lowerPreset = layoutDensityPresets[lower - 1] ?? layoutDensityPresets[0]; + const upperPreset = layoutDensityPresets[upper - 1] ?? + layoutDensityPresets[layoutDensityPresets.length - 1]; + const t = (normalized - lower) / (upper - lower); + return { + value: normalized, + label: t < 0.5 ? lowerPreset.label : upperPreset.label, + cellSize: Math.round(lowerPreset.cellSize + (upperPreset.cellSize - lowerPreset.cellSize) * t), + gap: Math.round(lowerPreset.gap + (upperPreset.gap - lowerPreset.gap) * t), + }; +}; export const iconDensityRange = { min: 0, - max: 10, + max: 50, step: 1, - default: 5, + default: 25, }; const iconDensityScaleRange = { - loose: 1.3, - dense: 0.05, + loose: 2, + dense: 0, }; export const getIconDensityScale = (value?: number) => { @@ -140,6 +167,7 @@ export const getIconDensityScale = (value?: number) => { iconDensityRange.min, iconDensityRange.max ); + const ease = (t: number) => Math.pow(t, 0.5); if (normalized === iconDensityRange.default) { return 1; } @@ -147,11 +175,12 @@ export const getIconDensityScale = (value?: number) => { // Smaller slider value = denser布局 const t = (iconDensityRange.default - normalized) / (iconDensityRange.default - iconDensityRange.min); - return 1 - t * (1 - iconDensityScaleRange.dense); + return 1 - ease(t) * (1 - iconDensityScaleRange.dense); } + const t = (normalized - iconDensityRange.default) / (iconDensityRange.max - iconDensityRange.default); - return 1 + t * (iconDensityScaleRange.loose - 1); + return 1 + ease(t) * (iconDensityScaleRange.loose - 1); }; export const iconSizeRange = { diff --git a/app/src/store/useLayoutStore.ts b/app/src/store/useLayoutStore.ts index bb4f834..75e55cc 100644 --- a/app/src/store/useLayoutStore.ts +++ b/app/src/store/useLayoutStore.ts @@ -1,6 +1,7 @@ import { defineStore, acceptHMRUpdate } from 'pinia'; type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4'; +const DEFAULT_GROUP_ID = 'home'; // 模拟数据(来自截图参考) interface Icon { @@ -11,6 +12,7 @@ interface Icon { img?: string; // 可选:用于图片图标(如徽标) text?: string; // 可选:用于文字图标 bgColor?: string; // 可选:纯色背景 + groupId?: string; } interface DragState { @@ -47,10 +49,33 @@ const defaultIcons: Icon[] = [ { id: '16', name: '书签管理', url: '#', bgColor: '#f1c40f' }, ]; +const defaultGroupById: Record = { + '1': 'product', + '2': 'product', + '3': 'home', + '4': 'product', + '5': 'fun', + '6': 'home', + '7': 'fun', + '8': 'fun', + '9': 'ai', + '10': 'fun', + '11': 'design', + '12': 'design', + '13': 'ai', + '14': 'code', + '15': 'home', + '16': 'home', +}; + const savedIcons = localStorage.getItem('itab_icons'); const normalizeIcons = (icons: Icon[]): Icon[] => - icons.map(icon => ({ ...icon, size: icon.size ?? '1x1' })); + icons.map(icon => ({ + ...icon, + size: icon.size ?? '1x1', + groupId: icon.groupId ?? defaultGroupById[icon.id] ?? DEFAULT_GROUP_ID, + })); const loadIcons = (): Icon[] => { if (!savedIcons) return normalizeIcons(defaultIcons); @@ -77,7 +102,7 @@ export const useLayoutStore = defineStore('layout', { actions: { addIcon(icon: Omit) { const nextId = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; - this.icons.push({ ...icon, id: nextId }); + this.icons.push({ ...icon, id: nextId, groupId: icon.groupId ?? DEFAULT_GROUP_ID }); localStorage.setItem('itab_icons', JSON.stringify(this.icons)); }, updateIcon(iconId: string, updates: Partial>) { diff --git a/app/src/store/useSettingsStore.ts b/app/src/store/useSettingsStore.ts index 77ef221..d5b84c8 100644 --- a/app/src/store/useSettingsStore.ts +++ b/app/src/store/useSettingsStore.ts @@ -4,7 +4,7 @@ import { getThemePreset, iconDensityRange, iconRadiusRange, - layoutDensityPresets, + layoutDensityRange, searchProviders, themePresets, type SearchProviderId, @@ -12,7 +12,7 @@ import { } from '@/config/settingsPresets'; type IconDensity = number; -type LayoutDensity = 1 | 2 | 3 | 4; +type LayoutDensity = number; type SettingsState = { showSearch: boolean; @@ -54,7 +54,6 @@ const defaultSettings: SettingsState = { const searchProviderIds = new Set(searchProviders.map(item => item.id)); const themePresetIds = new Set(themePresets.map(item => item.id)); -const layoutDensityValues = new Set(layoutDensityPresets.map(item => item.value)); const clampRange = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); @@ -92,10 +91,10 @@ const loadSettings = (): SettingsState => { ? parsed.themeId : defaultSettings.themeId, layoutDensity: - typeof parsed.layoutDensity === 'number' && layoutDensityValues.has(parsed.layoutDensity) - ? parsed.layoutDensity + typeof parsed.layoutDensity === 'number' + ? clampRange(parsed.layoutDensity, layoutDensityRange.min, layoutDensityRange.max) : defaultSettings.layoutDensity, - autoAlign: typeof parsed.autoAlign === 'boolean' ? parsed.autoAlign : defaultSettings.autoAlign, + autoAlign: defaultSettings.autoAlign, compactSidebar: typeof parsed.compactSidebar === 'boolean' ? parsed.compactSidebar : defaultSettings.compactSidebar, showGroupLabels: diff --git a/app/src/store/useWidgetsStore.ts b/app/src/store/useWidgetsStore.ts index 91a881b..1b38a10 100644 --- a/app/src/store/useWidgetsStore.ts +++ b/app/src/store/useWidgetsStore.ts @@ -2,12 +2,14 @@ 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; // 组件数据 + groupId?: string; } interface WidgetsState { @@ -46,13 +48,34 @@ const defaultWidgets: Widget[] = [ const savedWidgets = localStorage.getItem('itab_widgets'); +const defaultGroupById: Record = { + 'widget-1': 'home', + 'widget-2': 'fun', + 'widget-3': 'product', +}; + +const normalizeWidgets = (widgets: Widget[]): Widget[] => + widgets.map(widget => ({ + ...widget, + groupId: widget.groupId ?? defaultGroupById[widget.id] ?? DEFAULT_GROUP_ID, + })); + +const loadWidgets = (): Widget[] => { + if (!savedWidgets) return normalizeWidgets(defaultWidgets); + try { + return normalizeWidgets(JSON.parse(savedWidgets) as Widget[]); + } catch { + return normalizeWidgets(defaultWidgets); + } +}; + // 模拟数据(来自截图参考) export const useWidgetsStore = defineStore('widgets', { state: (): WidgetsState => ({ - widgets: savedWidgets ? JSON.parse(savedWidgets) : defaultWidgets, + widgets: loadWidgets(), }), actions: { - addWidget(payload: { component: string; size?: WidgetSize; data?: any }) { + addWidget(payload: { component: string; size?: WidgetSize; data?: any; groupId?: string }) { const nextId = `widget-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; const nextWidget: Widget = { id: nextId, @@ -60,6 +83,7 @@ export const useWidgetsStore = defineStore('widgets', { size: payload.size ?? '1x1', gridPosition: { x: 0, y: 0 }, data: payload.data, + groupId: payload.groupId ?? DEFAULT_GROUP_ID, }; this.widgets.push(nextWidget); localStorage.setItem('itab_widgets', JSON.stringify(this.widgets));