diff --git a/ScreenShot_2026-01-23_175720_764.png b/ScreenShot_2026-01-23_175720_764.png new file mode 100644 index 0000000..66f35c7 Binary files /dev/null and b/ScreenShot_2026-01-23_175720_764.png differ diff --git a/app/src/components/GridCanvas/index.vue b/app/src/components/GridCanvas/index.vue index 9fc0465..f7b85b2 100644 --- a/app/src/components/GridCanvas/index.vue +++ b/app/src/components/GridCanvas/index.vue @@ -45,7 +45,14 @@ const gridRef = ref(null); const grid = ref(null); const isDragging = ref(false); const suppressClick = ref(false); +const clickBlockUntil = ref(0); const gridOrder = ref([]); +const resizedKeys = ref(new Set()); +const pendingResizedKeys = new Set(); +const widgetSizeMap = new Map(); +const iconSizeMap = new Map(); +const layoutAnimationMs = 240; +const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)'; const buildDefaultOrder = (): GridOrderEntry[] => [ ...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })), @@ -173,6 +180,13 @@ const handleClick = (event: MouseEvent) => { } }; +const handleClickCapture = (event: MouseEvent) => { + if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) { + event.preventDefault(); + event.stopPropagation(); + } +}; + const handleContextMenu = (event: MouseEvent) => { const target = event.target as HTMLElement; const itemEl = target.closest('.grid-item') as HTMLElement | null; @@ -204,10 +218,40 @@ const persistOrderFromGrid = async () => { grid.value?.layout(); }; -const refreshLayout = async () => { +const refreshLayout = async (instant = false) => { await nextTick(); grid.value?.refreshItems(); - grid.value?.layout(); + if (instant) { + grid.value?.layout(true); + } else { + grid.value?.layout(); + } +}; + +const markResized = (type: GridItemType, id: string) => { + pendingResizedKeys.add(`${type}:${id}`); +}; + +const syncSizeMap = ( + items: { id: string; size: GridItemSize }[], + map: Map +) => { + const changed: string[] = []; + const seen = new Set(); + for (const item of items) { + const prev = map.get(item.id); + if (prev && prev !== item.size) { + changed.push(item.id); + } + map.set(item.id, item.size); + seen.add(item.id); + } + for (const id of Array.from(map.keys())) { + if (!seen.has(id)) { + map.delete(id); + } + } + return changed; }; onMounted(async () => { @@ -226,6 +270,8 @@ onMounted(async () => { fillGaps: true, rounding: true, }, + layoutDuration: layoutAnimationMs, + layoutEasing, }); grid.value.on('dragStart', () => { @@ -235,6 +281,7 @@ onMounted(async () => { grid.value.on('dragEnd', () => { isDragging.value = false; + clickBlockUntil.value = Date.now() + 180; window.setTimeout(() => { suppressClick.value = false; }, 0); @@ -244,6 +291,17 @@ onMounted(async () => { persistOrderFromGrid(); }); + grid.value.on('layoutStart', () => { + if (!pendingResizedKeys.size) return; + resizedKeys.value = new Set(pendingResizedKeys); + }); + + grid.value.on('layoutEnd', () => { + if (!pendingResizedKeys.size) return; + pendingResizedKeys.clear(); + resizedKeys.value = new Set(); + }); + grid.value.layout(true); }); @@ -256,20 +314,35 @@ watch( ); watch( - () => widgetsStore.widgets.map(widget => widget.size).join('|'), + () => widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })), async () => { + const changed = syncSizeMap( + widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })), + widgetSizeMap + ); + for (const id of changed) { + markResized('widget', id); + } await refreshLayout(); } ); watch( - () => layoutStore.icons.map(icon => icon.size ?? '1x1').join('|'), + () => layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })), async () => { + const changed = syncSizeMap( + layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })), + iconSizeMap + ); + for (const id of changed) { + markResized('icon', id); + } await refreshLayout(); } ); onUnmounted(() => { + pendingResizedKeys.clear(); grid.value?.destroy(); grid.value = null; }); @@ -279,6 +352,8 @@ onUnmounted(() => {
@@ -286,7 +361,10 @@ onUnmounted(() => { v-for="item in orderedItems" :key="`${item.type}-${item.id}`" class="grid-item" - :class="`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`" + :class="[ + `size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`, + { 'is-resized': resizedKeys.has(`${item.type}:${item.id}`) } + ]" :data-id="item.id" :data-type="item.type" > @@ -320,6 +398,12 @@ onUnmounted(() => { height: var(--cell-size); margin: calc(var(--cell-gap) / 2); cursor: grab; + will-change: transform; +} + +.grid-item.is-resized { + overflow: hidden; + border-radius: $border-radius-small; } .grid-item.size-1x2 { @@ -343,10 +427,25 @@ onUnmounted(() => { .grid-item-content { width: 100%; height: 100%; + transform-origin: center; +} + +.grid-item.is-resized .grid-item-content { + animation: grid-item-resize-zoom var(--layout-anim-ms) $motion-easing-standard; +} + +@keyframes grid-item-resize-zoom { + 0% { + transform: scale(0.97); + } + 100% { + transform: scale(1); + } } .grid-item.muuri-item-dragging { z-index: $z-index-menu; cursor: grabbing; + transition: none; } diff --git a/app/src/components/IconCard/index.vue b/app/src/components/IconCard/index.vue index 87fa552..796a50e 100644 --- a/app/src/components/IconCard/index.vue +++ b/app/src/components/IconCard/index.vue @@ -99,9 +99,9 @@ const props = defineProps<{ color: $color-text-primary; text-align: center; width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: normal; + word-break: break-word; + line-height: 1.2; pointer-events: none; } diff --git a/app/src/components/WidgetCard/index.vue b/app/src/components/WidgetCard/index.vue index 9114bc4..4e772e6 100644 --- a/app/src/components/WidgetCard/index.vue +++ b/app/src/components/WidgetCard/index.vue @@ -2,7 +2,7 @@
- +
{{ label }}
@@ -26,7 +26,7 @@ const props = defineProps<{ // 根据名称动态加载组件 const widgetComponent = defineAsyncComponent(() => - import(`@/components/widgets/${props.widget.component}.vue`) + import(`@/components/widgets/${props.widget.component}/index.vue`) ); // 从组件名称提取展示标签 @@ -90,9 +90,9 @@ const label = computed(() => { color: $color-text-primary; text-align: center; width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: normal; + word-break: break-word; + line-height: 1.2; flex-shrink: 0; } diff --git a/app/src/components/widgets/CalendarWidget.vue b/app/src/components/widgets/CalendarWidget.vue deleted file mode 100644 index a19cafa..0000000 --- a/app/src/components/widgets/CalendarWidget.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/app/src/components/widgets/CalendarWidget/dialog.vue b/app/src/components/widgets/CalendarWidget/dialog.vue new file mode 100644 index 0000000..976b341 --- /dev/null +++ b/app/src/components/widgets/CalendarWidget/dialog.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/components/widgets/CalendarWidget/index.vue b/app/src/components/widgets/CalendarWidget/index.vue new file mode 100644 index 0000000..f3cfb38 --- /dev/null +++ b/app/src/components/widgets/CalendarWidget/index.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/app/src/components/widgets/CountdownWidget.vue b/app/src/components/widgets/CountdownWidget.vue deleted file mode 100644 index 83682fc..0000000 --- a/app/src/components/widgets/CountdownWidget.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/app/src/components/widgets/CountdownWidget/dialog.vue b/app/src/components/widgets/CountdownWidget/dialog.vue new file mode 100644 index 0000000..65a9bbc --- /dev/null +++ b/app/src/components/widgets/CountdownWidget/dialog.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/components/widgets/CountdownWidget/index.vue b/app/src/components/widgets/CountdownWidget/index.vue new file mode 100644 index 0000000..f167661 --- /dev/null +++ b/app/src/components/widgets/CountdownWidget/index.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/app/src/components/widgets/HotSearchWidget.vue b/app/src/components/widgets/HotSearchWidget.vue deleted file mode 100644 index cf9f856..0000000 --- a/app/src/components/widgets/HotSearchWidget.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/app/src/components/widgets/HotSearchWidget/dialog.vue b/app/src/components/widgets/HotSearchWidget/dialog.vue new file mode 100644 index 0000000..2df54c1 --- /dev/null +++ b/app/src/components/widgets/HotSearchWidget/dialog.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/components/widgets/HotSearchWidget/index.vue b/app/src/components/widgets/HotSearchWidget/index.vue new file mode 100644 index 0000000..80616fe --- /dev/null +++ b/app/src/components/widgets/HotSearchWidget/index.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/app/src/components/widgets/WidgetDialog.vue b/app/src/components/widgets/WidgetDialog.vue new file mode 100644 index 0000000..0f29aec --- /dev/null +++ b/app/src/components/widgets/WidgetDialog.vue @@ -0,0 +1,113 @@ + + + + +