From 754621de045cd246cc81c8e7201727f32dfd24dc Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Tue, 16 Jun 2026 14:18:40 +0800 Subject: [PATCH] feat(webui): independent single-column mobile overview (B2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phones now render the overview from the dedicated `overviewMobile` ordering instead of deriving a 1-column view from the desktop grid — the two layouts are fully disjoint, so reordering on a phone never touches grid coords and vice versa. - View mode reuses DashboardGrid (drag disabled, cols=1), stacking each widget at its catalogue height via a pure prefix-sum, so cards keep their cell sizing. Per-widget config is shared with the desktop blocks. - Edit mode surfaces a motion `Reorder` drag-sort panel (grip + show/hide eye + config gear for configurable widgets) — no 2D drag on mobile, per plan. - The 编辑布局 toggle now appears on mobile too, with a mobile-specific hint. Order/visibility reconciles against the live widget catalogue, so a widget added later just appears on phones as well. --- .../src/components/pages/overview-page.tsx | 189 ++++++++++++++---- packages/webui/src/lib/dashboard-layout.ts | 7 + 2 files changed, 162 insertions(+), 34 deletions(-) diff --git a/packages/webui/src/components/pages/overview-page.tsx b/packages/webui/src/components/pages/overview-page.tsx index 1d741ce9..02c7e939 100644 --- a/packages/webui/src/components/pages/overview-page.tsx +++ b/packages/webui/src/components/pages/overview-page.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState, type ReactNode } from 'react'; -import { motion } from 'motion/react'; +import { motion, Reorder } from 'motion/react'; import { Link } from '@tanstack/react-router'; import { - Activity, ArrowRight, Bell, Cable, Check, Cpu, MemoryStick, MonitorCog, Pencil, PlugZap, - RefreshCw, RotateCcw, Server, Users, + Activity, ArrowRight, Bell, Cable, Check, Cpu, Eye, EyeOff, GripVertical, MemoryStick, + MonitorCog, Pencil, PlugZap, RefreshCw, RotateCcw, Server, Settings2, Users, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -18,10 +18,11 @@ import { useApi } from '@/lib/api'; import { useAppState } from '@/contexts/AppStateContext'; import { useSession } from '@/contexts/SessionContext'; import { useTheme } from '@/contexts/ThemeContext'; -import { useLayout } from '@/contexts/LayoutContext'; +import { reconcileLayoutItems, useLayout } from '@/contexts/LayoutContext'; import { useMediaQuery } from '@/hooks/use-media-query'; import { - CONFIGURABLE_WIDGETS, GRID_COLS, parseAlertsConfig, parseSessionsConfig, widgetLabel, + CONFIGURABLE_WIDGETS, GRID_COLS, MOBILE_WIDGET_IDS, mobileHeightOf, + parseAlertsConfig, parseSessionsConfig, widgetLabel, type AlertsConfig, type SessionsConfig, } from '@/lib/dashboard-layout'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -45,16 +46,32 @@ function renderWidget(block: UiLayoutItem): ReactNode { export function OverviewPage() { const { qqList, processList } = useAppState(); - const { overviewBlocks, setOverviewBlocks, resetLayout, editing: editingCtx, setEditing } = useLayout(); + const { + overviewBlocks, setOverviewBlocks, overviewMobile, setOverviewMobile, + resetLayout, editing, setEditing, + } = useLayout(); const off = useTheme().appearance.disableMotion; const isWide = useMediaQuery('(min-width: 768px)'); - // Free-grid editing is desktop-only (no room to drag/resize on a phone). - const editing = isWide && editingCtx; + // Free-grid 2D editing is desktop-only; on a phone, editing surfaces a + // single-column drag-sort panel (overviewMobile) instead. + const gridEditing = isWide && editing; const [configId, setConfigId] = useState(null); const visibleBlocks = useMemo(() => overviewBlocks.filter((b) => b.visible), [overviewBlocks]); // In edit mode show ALL blocks (hidden ones as re-enableable ghosts); else visible-only. - const gridBlocks = editing ? overviewBlocks : visibleBlocks; + const gridBlocks = gridEditing ? overviewBlocks : visibleBlocks; + + // Per-widget config lives on the desktop blocks; mobile widgets reuse it. + const configById = useMemo( + () => new Map(overviewBlocks.map((b) => [b.id, b.config])), + [overviewBlocks], + ); + // Reconcile the mobile order against the live widget catalogue (drop unknown, + // append new as visible) — so a widget added later just appears on phones too. + const mobileItems = useMemo( + () => reconcileLayoutItems(overviewMobile, MOBILE_WIDGET_IDS), + [overviewMobile], + ); const onGridChange = (coords: GridCoord[]) => { const cmap = new Map(coords.map((c) => [c.id, c])); @@ -68,6 +85,12 @@ export function OverviewPage() { const toggleBlock = (id: string) => setOverviewBlocks(overviewBlocks.map((b) => (b.id === id ? { ...b, visible: !b.visible } : b))); + const toggleMobile = (id: string) => + setOverviewMobile(mobileItems.map((b) => (b.id === id ? { ...b, visible: !b.visible } : b))); + const reorderMobile = (ids: string[]) => { + const byId = new Map(mobileItems.map((i) => [i.id, i])); + setOverviewMobile(ids.map((id) => byId.get(id)).filter((x): x is UiLayoutItem => !!x)); + }; const setBlockConfig = (id: string, config: Record) => setOverviewBlocks(overviewBlocks.map((b) => (b.id === id ? { ...b, config: { ...b.config, ...config } } : b))); @@ -77,25 +100,25 @@ export function OverviewPage() { return (
- {/* Toolbar: edit toggle is desktop-only. In edit mode the cards carry - their own floating drag/hide/settings overlays, and the sidebar nav - becomes drag-sortable — no separate editor panel. */} - {isWide && ( -
- {editing && ( - - )} - -
- )} + )} + +
{editing && (

- 拖动卡片顶部的「拖动」手柄移动位置、拖右下角缩放;眼睛图标显隐、齿轮改设置。左侧导航也可在编辑态拖动排序。 + {isWide + ? '拖动卡片顶部的「拖动」手柄移动位置、拖右下角缩放;眼睛图标显隐、齿轮改设置。左侧导航也可在编辑态拖动排序。' + : '拖动手柄调整卡片顺序,眼睛图标显隐,齿轮改设置。手机为单列布局,与桌面网格各自独立。'}

)} @@ -122,19 +145,31 @@ export function OverviewPage() { )} - {visibleBlocks.length === 0 && !editing ? ( - + {isWide ? ( + visibleBlocks.length === 0 && !gridEditing ? ( + + ) : ( + + ) ) : ( - )} @@ -161,6 +196,92 @@ function EmptyLayout({ onReset }: { onReset: () => void }) { ); } +// ─────────────── mobile single-column overview ─────────────── +// Phones get a one-dimensional layout (`overviewMobile`), fully disjoint from +// the desktop 2D grid. View mode reuses the grid renderer (drag disabled) so +// each widget keeps its cell height; edit mode is a drag-sort panel. +function MobileOverview({ + items, editing, configById, onReorder, onToggle, onConfigOpen, onReset, +}: { + items: UiLayoutItem[]; + editing: boolean; + configById: Map | undefined>; + onReorder: (ids: string[]) => void; + onToggle: (id: string) => void; + onConfigOpen: (id: string) => void; + onReset: () => void; +}) { + if (editing) { + return ( + i.id)} onReorder={onReorder} className="flex flex-col gap-2"> + {items.map((item) => ( + + + {widgetLabel(item.id)} + {CONFIGURABLE_WIDGETS.has(item.id) && item.visible && ( + + )} + + + ))} + + ); + } + + const visible = items.filter((i) => i.visible); + if (visible.length === 0) return ; + + // Stack widgets in a single column with cumulative y + catalogue heights + // (pure prefix-sum; the widget count is tiny so the O(n²) is negligible). + const heights = visible.map((item) => mobileHeightOf(item.id)); + const blocks: UiLayoutItem[] = visible.map((item, i) => ({ + id: item.id, + visible: true, + x: 0, + y: heights.slice(0, i).reduce((sum, h) => sum + h, 0), + w: 1, + h: heights[i], + config: configById.get(item.id), + })); + + return ( + { /* no drag on mobile — order edited via the panel */ }} + renderWidget={renderWidget} + labelFor={widgetLabel} + configurableIds={CONFIGURABLE_WIDGETS} + onToggleVisible={() => { /* unused in view mode */ }} + onConfigOpen={onConfigOpen} + /> + ); +} + // ─────────────── stat tiles (one widget each) ─────────────── function StatTile({ diff --git a/packages/webui/src/lib/dashboard-layout.ts b/packages/webui/src/lib/dashboard-layout.ts index f4b11506..caccd75d 100644 --- a/packages/webui/src/lib/dashboard-layout.ts +++ b/packages/webui/src/lib/dashboard-layout.ts @@ -110,6 +110,13 @@ export function minSizeOf(id: string): { minW: number; minH: number } { return { minW: spec?.minW ?? 1, minH: spec?.minH ?? 1 }; } +/** A widget's natural height (grid rows) for single-column mobile stacking — + * the catalogue default, so phones get sensible per-widget heights without + * carrying the desktop grid's stored coords. */ +export function mobileHeightOf(id: string): number { + return WIDGET_BY_ID.get(id)?.def.h ?? 1; +} + // ─── Per-widget config (interpreted client-side from the opaque block.config) ─── export const ALL_LOG_LEVELS: LogLevel[] = ['trace', 'debug', 'info', 'success', 'warn', 'error'];