From 97fbe455517c9409a376fb667dd3afe624291582 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 01:10:09 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=E5=8D=A1=E7=89=87=E5=9C=A8=E5=B0=8F=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E4=B8=8B=E8=A3=81=E5=88=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统计卡片在 50vh 外框下改为可滚动,图表高度按可用空间自适应,避免 Legend 被裁切\n- 开发环境新增 /internal/ui-preview/statistics-chart 预览页(无需 DB)\n- 开发环境放行 /internal/ui-preview 免登录访问,方便本地 UI 检查 --- .../bento/statistics-chart-card.tsx | 13 ++- .../_components/statistics-chart-preview.tsx | 101 ++++++++++++++++++ .../ui-preview/statistics-chart/page.tsx | 12 +++ src/proxy.ts | 8 +- 4 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx create mode 100644 src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 0d67f4b42..0b47805fe 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -167,10 +167,15 @@ export function StatisticsChartCard({ return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale }); }; + const chartContainerClassName = enableUserFilter + ? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-248px),240px)] w-full" + : "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-138px),280px)] w-full"; + return ( @@ -259,8 +264,8 @@ export function StatisticsChartCard({ {/* Chart */} -
- +
+ {data.users.map((user, index) => { @@ -392,7 +397,7 @@ export function StatisticsChartCard({ {/* Legend */} {enableUserFilter && ( -
+
{/* Control buttons */}
{/* User list with max 3 rows and scroll - only show users with non-zero usage */} -
+
{data.users .map((user, originalIndex) => ({ user, originalIndex })) diff --git a/tests/unit/proxy/proxy-dev-public-path.test.ts b/tests/unit/proxy/proxy-dev-public-path.test.ts index f1a69f158..e05ad8f51 100644 --- a/tests/unit/proxy/proxy-dev-public-path.test.ts +++ b/tests/unit/proxy/proxy-dev-public-path.test.ts @@ -43,7 +43,7 @@ describe("proxy dev public paths", () => { mockIntlMiddleware.mockReturnValue(localeResponse); const { default: proxyHandler } = await import("@/proxy"); - const response = proxyHandler(makeRequest("/zh-CN/internal/ui-preview/statistics-chart")); + const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview/statistics-chart")); expect(response.headers.get("x-test")).toBe("dev-public-ok"); }); @@ -53,7 +53,7 @@ describe("proxy dev public paths", () => { mockIntlMiddleware.mockReturnValue(localeResponse); const { default: proxyHandler } = await import("@/proxy"); - const response = proxyHandler(makeRequest("/zh-CN/internal/ui-preview-xxx")); + const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview-xxx")); expect(response.status).toBeGreaterThanOrEqual(300); expect(response.status).toBeLessThan(400); From 7ad937cdc16997e24ac9c5e3dba15fa24ce64eab Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 02:40:40 +0800 Subject: [PATCH 06/26] =?UTF-8?q?=E9=A2=84=E8=A7=88=E9=A1=B5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20@/=20=E5=88=AB=E5=90=8D=E5=AF=BC=E5=85=A5=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx b/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx index ca7cde723..88c8c2fb9 100644 --- a/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx +++ b/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx @@ -1,5 +1,5 @@ import { notFound } from "next/navigation"; -import { StatisticsChartPreview } from "./_components/statistics-chart-preview"; +import { StatisticsChartPreview } from "@/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview"; export const dynamic = "force-dynamic"; From c22ae2765c62eddde756c707c4ffd80e512726d9 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 02:52:30 +0800 Subject: [PATCH 07/26] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E8=A1=8C=E5=AE=BD=E4=BB=A5=E4=BE=BF=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/bento/statistics-chart-card.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index b49068a95..dd851fb2e 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -167,12 +167,13 @@ export function StatisticsChartCard({ return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale }); }; - // 图表卡片整体 max-h 为 50vh(用于保持首页更紧凑的布局)。当启用多用户 Legend 时, - // 非图表部分(Header + Tabs + Padding + Legend)会占用更多空间;如果图表仍固定高度, - // 在小视口下会导致底部内容被裁切。这里用“可用高度估算 + clamp”让图表高度自适应: + // 图表卡片整体 max-h 为 50vh,用于保持首页更紧凑。 + // 当启用多用户 Legend 时,非图表部分(Header/Tabs/Padding/Legend)会占用更多空间。 + // 如果图表仍固定高度,在小视口下会导致底部内容被裁切。 + // 这里用“可用高度估算 + clamp”让图表高度自适应: // - 248px:Legend 可见时非图表区域的近似高度 // - 138px:Legend 不可见时非图表区域的近似高度 - // 由于外层已支持 overflow-y-auto,这里的估算偏差只会影响图表相对大小,不会再导致内容丢失。 + // 外层已支持 overflow-y-auto,这里的估算偏差只会影响图表相对大小,不会再导致内容丢失。 const chartContainerClassName = enableUserFilter ? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_248px),240px)] w-full" : "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_138px),280px)] w-full"; From 976e16cacfce9bc9aace1ec22dc90af51ee1a5c9 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 03:14:49 +0800 Subject: [PATCH 08/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=E9=AB=98=E5=BA=A6=E8=AE=A1=E7=AE=97=E5=8F=AF?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将图表高度与非图表区域偏移提取为常量,改用 style 计算 clamp() 高度 - 单测增加 beforeEach 重置模块与 mock,避免状态泄漏 --- .../bento/statistics-chart-card.tsx | 21 +++++++++++++++---- .../unit/proxy/proxy-dev-public-path.test.ts | 7 ++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index dd851fb2e..a9d5c47c0 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -27,6 +27,15 @@ const USER_COLOR_PALETTE = [ const getUserColor = (index: number) => USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length]; +const CHART_HEIGHT_MIN_PX = 140; +const CHART_HEIGHT_MAX_PX_WITH_LEGEND = 240; +const CHART_HEIGHT_MAX_PX_NO_LEGEND = 280; + +// Legend 可见时,非图表区域(Header/Tabs/Padding/Legend)占用的近似高度。 +const CHART_NON_GRAPH_HEIGHT_WITH_LEGEND_PX = 248; +// Legend 不可见时,非图表区域(Header/Tabs/Padding)占用的近似高度。 +const CHART_NON_GRAPH_HEIGHT_NO_LEGEND_PX = 138; + export interface StatisticsChartCardProps { data: UserStatisticsData; onTimeRangeChange?: (timeRange: TimeRange) => void; @@ -174,9 +183,9 @@ export function StatisticsChartCard({ // - 248px:Legend 可见时非图表区域的近似高度 // - 138px:Legend 不可见时非图表区域的近似高度 // 外层已支持 overflow-y-auto,这里的估算偏差只会影响图表相对大小,不会再导致内容丢失。 - const chartContainerClassName = enableUserFilter - ? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_248px),240px)] w-full" - : "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_138px),280px)] w-full"; + const chartContainerHeight = enableUserFilter + ? `clamp(${CHART_HEIGHT_MIN_PX}px, calc(var(--cch-viewport-height-50) - ${CHART_NON_GRAPH_HEIGHT_WITH_LEGEND_PX}px), ${CHART_HEIGHT_MAX_PX_WITH_LEGEND}px)` + : `clamp(${CHART_HEIGHT_MIN_PX}px, calc(var(--cch-viewport-height-50) - ${CHART_NON_GRAPH_HEIGHT_NO_LEGEND_PX}px), ${CHART_HEIGHT_MAX_PX_NO_LEGEND}px)`; return ( - + {data.users.map((user, index) => { diff --git a/tests/unit/proxy/proxy-dev-public-path.test.ts b/tests/unit/proxy/proxy-dev-public-path.test.ts index e05ad8f51..ccdaaf02a 100644 --- a/tests/unit/proxy/proxy-dev-public-path.test.ts +++ b/tests/unit/proxy/proxy-dev-public-path.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; // Hoist mocks before imports -- mock transitive dependencies to avoid // next-intl pulling in next/navigation (not resolvable in vitest) @@ -35,6 +35,11 @@ function makeRequest(pathname: string, cookies: Record = {}) { } describe("proxy dev public paths", () => { + beforeEach(() => { + vi.resetModules(); + mockIntlMiddleware.mockReset(); + }); + it("allows /internal/ui-preview/* without any cookie in development", async () => { const localeResponse = new Response(null, { status: 200, From 51d0b83ebe06e0b8eef11b69772179d341f3e4bc Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 13:26:01 +0800 Subject: [PATCH 09/26] =?UTF-8?q?chore(ui):=20=E6=94=B6=E6=95=9B=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E7=BB=9F=E8=AE=A1=E5=9B=BE=E8=A1=A8=E8=A3=81=E5=88=87?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=B0=E7=BB=84=E4=BB=B6=E5=86=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/statistics-chart-preview.tsx | 101 ------------------ .../ui-preview/statistics-chart/page.tsx | 12 --- src/proxy.ts | 8 +- .../unit/proxy/proxy-dev-public-path.test.ts | 68 ------------ 4 files changed, 1 insertion(+), 188 deletions(-) delete mode 100644 src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx delete mode 100644 src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx delete mode 100644 tests/unit/proxy/proxy-dev-public-path.test.ts diff --git a/src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx b/src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx deleted file mode 100644 index 4fb77a5a0..000000000 --- a/src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { StatisticsChartCard } from "@/app/[locale]/dashboard/_components/bento/statistics-chart-card"; -import type { - ChartDataItem, - StatisticsUser, - TimeRange, - UserStatisticsData, -} from "@/types/statistics"; - -function buildMockUsers(): StatisticsUser[] { - return Array.from({ length: 8 }, (_, index) => { - const id = index + 1; - return { id, name: `u${id}`, dataKey: `user-${id}` }; - }); -} - -function gaussianSpike(x: number, center: number, width: number, height: number): number { - const z = (x - center) / width; - return Math.exp(-(z * z)) * height; -} - -function buildMockChartData(timeRange: TimeRange, users: StatisticsUser[]): ChartDataItem[] { - const now = new Date(); - - if (timeRange === "today") { - const start = new Date(now); - start.setHours(0, 0, 0, 0); - - return Array.from({ length: 24 }, (_, hourIndex) => { - const date = new Date(start.getTime() + hourIndex * 60 * 60 * 1000); - const row: ChartDataItem = { date: date.toISOString() }; - - users.forEach((user, userIndex) => { - const base = - gaussianSpike(hourIndex, 8, 1.2, 3.2) + - gaussianSpike(hourIndex, 17, 0.9, 1.9) + - gaussianSpike(hourIndex, 21, 0.8, 0.8); - const scaled = Math.max(0, base * (1 / (1 + userIndex * 0.65))); - const cost = Number(scaled.toFixed(6)); - const calls = Math.round(scaled * 12); - - row[`${user.dataKey}_cost`] = cost; - row[`${user.dataKey}_calls`] = calls; - }); - - return row; - }); - } - - const days = timeRange === "7days" ? 7 : timeRange === "30days" ? 30 : Math.max(1, now.getDate()); - const start = new Date(now); - start.setHours(0, 0, 0, 0); - start.setDate(start.getDate() - (days - 1)); - - return Array.from({ length: days }, (_, dayIndex) => { - const date = new Date(start.getTime() + dayIndex * 24 * 60 * 60 * 1000); - const dateStr = date.toISOString().slice(0, 10); - const row: ChartDataItem = { date: dateStr }; - - users.forEach((user, userIndex) => { - const base = - gaussianSpike(dayIndex, Math.floor(days * 0.35), Math.max(1, days * 0.08), 12) + - gaussianSpike(dayIndex, Math.floor(days * 0.75), Math.max(1, days * 0.06), 7); - const scaled = Math.max(0, base * (1 / (1 + userIndex * 0.6))); - const cost = Number(scaled.toFixed(6)); - const calls = Math.round(scaled * 20); - - row[`${user.dataKey}_cost`] = cost; - row[`${user.dataKey}_calls`] = calls; - }); - - return row; - }); -} - -function buildMockStatistics(timeRange: TimeRange): UserStatisticsData { - const users = buildMockUsers(); - const chartData = buildMockChartData(timeRange, users); - const resolution = timeRange === "today" ? "hour" : "day"; - - return { - chartData, - users, - timeRange, - resolution, - mode: "users", - }; -} - -export function StatisticsChartPreview() { - const [timeRange, setTimeRange] = useState("today"); - const data = useMemo(() => buildMockStatistics(timeRange), [timeRange]); - - return ( -
- -
- ); -} diff --git a/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx b/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx deleted file mode 100644 index 88c8c2fb9..000000000 --- a/src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { notFound } from "next/navigation"; -import { StatisticsChartPreview } from "@/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview"; - -export const dynamic = "force-dynamic"; - -export default function Page() { - if (process.env.NODE_ENV === "production") { - notFound(); - } - - return ; -} diff --git a/src/proxy.ts b/src/proxy.ts index 01a95ac06..05cae00ac 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -9,7 +9,6 @@ import { logger } from "@/lib/logger"; // Public paths that don't require authentication // Note: These paths will be automatically prefixed with locale by next-intl middleware const PUBLIC_PATH_PATTERNS = ["/login", "/usage-doc", "/api/auth/login", "/api/auth/logout"]; -const DEV_PUBLIC_PATH_PATTERNS = ["/internal/ui-preview"]; const API_PROXY_PATH = "/v1"; @@ -52,14 +51,9 @@ function proxyHandler(request: NextRequest) { const isPublicPath = PUBLIC_PATH_PATTERNS.some( (pattern) => pathWithoutLocale === pattern || pathWithoutLocale.startsWith(pattern) ); - const isDevPublicPath = - isDevelopment() && - DEV_PUBLIC_PATH_PATTERNS.some( - (pattern) => pathWithoutLocale === pattern || pathWithoutLocale.startsWith(`${pattern}/`) - ); // Public paths don't require authentication - if (isPublicPath || isDevPublicPath) { + if (isPublicPath) { return localeResponse; } diff --git a/tests/unit/proxy/proxy-dev-public-path.test.ts b/tests/unit/proxy/proxy-dev-public-path.test.ts deleted file mode 100644 index ccdaaf02a..000000000 --- a/tests/unit/proxy/proxy-dev-public-path.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Hoist mocks before imports -- mock transitive dependencies to avoid -// next-intl pulling in next/navigation (not resolvable in vitest) -const mockIntlMiddleware = vi.hoisted(() => vi.fn()); -vi.mock("next-intl/middleware", () => ({ - default: () => mockIntlMiddleware, -})); - -vi.mock("@/i18n/routing", () => ({ - routing: { - locales: ["zh-CN", "en"], - defaultLocale: "zh-CN", - }, -})); - -vi.mock("@/lib/config/env.schema", () => ({ - isDevelopment: () => true, -})); - -vi.mock("@/lib/logger", () => ({ - logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); - -function makeRequest(pathname: string, cookies: Record = {}) { - const url = new URL(`http://localhost:13500${pathname}`); - return { - method: "GET", - nextUrl: { pathname, clone: () => url }, - cookies: { - get: (name: string) => (name in cookies ? { name, value: cookies[name] } : undefined), - }, - headers: new Headers(), - } as unknown as import("next/server").NextRequest; -} - -describe("proxy dev public paths", () => { - beforeEach(() => { - vi.resetModules(); - mockIntlMiddleware.mockReset(); - }); - - it("allows /internal/ui-preview/* without any cookie in development", async () => { - const localeResponse = new Response(null, { - status: 200, - headers: { "x-test": "dev-public-ok" }, - }); - mockIntlMiddleware.mockReturnValue(localeResponse); - - const { default: proxyHandler } = await import("@/proxy"); - const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview/statistics-chart")); - - expect(response.headers.get("x-test")).toBe("dev-public-ok"); - }); - - it("does not overmatch /internal/ui-preview-xxx", async () => { - const localeResponse = new Response(null, { status: 200 }); - mockIntlMiddleware.mockReturnValue(localeResponse); - - const { default: proxyHandler } = await import("@/proxy"); - const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview-xxx")); - - expect(response.status).toBeGreaterThanOrEqual(300); - expect(response.status).toBeLessThan(400); - const location = response.headers.get("location"); - expect(location).toContain("/login"); - }); -}); From eb69a77a891e269c7c7fe5739fd0bc983d6eff2c Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 14:44:09 +0800 Subject: [PATCH 10/26] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E8=AE=A1=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E7=A6=81=E7=94=A8=E6=BB=9A=E5=8A=A8=E5=B9=B6=E8=87=AA?= =?UTF-8?q?=E9=80=82=E5=BA=94=E5=9B=BE=E8=A1=A8=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bento/statistics-chart-card.tsx | 122 +++++++++++++----- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index a9d5c47c0..8310de1c3 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -27,14 +27,13 @@ const USER_COLOR_PALETTE = [ const getUserColor = (index: number) => USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length]; -const CHART_HEIGHT_MIN_PX = 140; const CHART_HEIGHT_MAX_PX_WITH_LEGEND = 240; const CHART_HEIGHT_MAX_PX_NO_LEGEND = 280; -// Legend 可见时,非图表区域(Header/Tabs/Padding/Legend)占用的近似高度。 -const CHART_NON_GRAPH_HEIGHT_WITH_LEGEND_PX = 248; -// Legend 不可见时,非图表区域(Header/Tabs/Padding)占用的近似高度。 -const CHART_NON_GRAPH_HEIGHT_NO_LEGEND_PX = 138; +function parsePx(value: string): number | null { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : null; +} export interface StatisticsChartCardProps { data: UserStatisticsData; @@ -67,6 +66,76 @@ export function StatisticsChartCard({ const isAdminMode = data.mode === "users"; const enableUserFilter = isAdminMode && data.users.length > 1; + const cardRef = React.useRef(null); + const headerRef = React.useRef(null); + const metricTabsRef = React.useRef(null); + const chartWrapperRef = React.useRef(null); + const legendRef = React.useRef(null); + + const [chartHeightPx, setChartHeightPx] = React.useState(() => + enableUserFilter ? CHART_HEIGHT_MAX_PX_WITH_LEGEND : CHART_HEIGHT_MAX_PX_NO_LEGEND + ); + + React.useLayoutEffect(() => { + const card = cardRef.current; + const header = headerRef.current; + const metricTabs = metricTabsRef.current; + const chartWrapper = chartWrapperRef.current; + + if (!card || !header || !metricTabs || !chartWrapper) { + return; + } + + const compute = () => { + const maxHeightPx = + parsePx(getComputedStyle(card).maxHeight) ?? + Math.floor((window.visualViewport?.height ?? window.innerHeight) * 0.5); + + const headerHeight = header.getBoundingClientRect().height; + const metricTabsHeight = metricTabs.getBoundingClientRect().height; + const legendHeight = enableUserFilter + ? (legendRef.current?.getBoundingClientRect().height ?? 0) + : 0; + + const chartWrapperStyle = getComputedStyle(chartWrapper); + const chartWrapperPadding = + (parsePx(chartWrapperStyle.paddingTop) ?? 0) + + (parsePx(chartWrapperStyle.paddingBottom) ?? 0); + + const reservedHeight = headerHeight + metricTabsHeight + legendHeight + chartWrapperPadding; + const availableHeight = Math.max(0, Math.floor(maxHeightPx - reservedHeight - 1)); + + const maxChartHeight = enableUserFilter + ? CHART_HEIGHT_MAX_PX_WITH_LEGEND + : CHART_HEIGHT_MAX_PX_NO_LEGEND; + + const nextHeight = Math.max(0, Math.min(maxChartHeight, availableHeight)); + setChartHeightPx((prev) => (prev === nextHeight ? prev : nextHeight)); + }; + + compute(); + + if (typeof ResizeObserver === "undefined") { + window.addEventListener("resize", compute); + return () => window.removeEventListener("resize", compute); + } + + const observer = new ResizeObserver(compute); + observer.observe(card); + observer.observe(header); + observer.observe(metricTabs); + observer.observe(chartWrapper); + if (enableUserFilter && legendRef.current) { + observer.observe(legendRef.current); + } + window.addEventListener("resize", compute); + + return () => { + window.removeEventListener("resize", compute); + observer.disconnect(); + }; + }, [enableUserFilter]); + const toggleUserSelection = (userId: number) => { setSelectedUserIds((prev) => { const next = new Set(prev); @@ -177,26 +246,17 @@ export function StatisticsChartCard({ }; // 图表卡片整体 max-h 为 50vh,用于保持首页更紧凑。 - // 当启用多用户 Legend 时,非图表部分(Header/Tabs/Padding/Legend)会占用更多空间。 - // 如果图表仍固定高度,在小视口下会导致底部内容被裁切。 - // 这里用“可用高度估算 + clamp”让图表高度自适应: - // - 248px:Legend 可见时非图表区域的近似高度 - // - 138px:Legend 不可见时非图表区域的近似高度 - // 外层已支持 overflow-y-auto,这里的估算偏差只会影响图表相对大小,不会再导致内容丢失。 - const chartContainerHeight = enableUserFilter - ? `clamp(${CHART_HEIGHT_MIN_PX}px, calc(var(--cch-viewport-height-50) - ${CHART_NON_GRAPH_HEIGHT_WITH_LEGEND_PX}px), ${CHART_HEIGHT_MAX_PX_WITH_LEGEND}px)` - : `clamp(${CHART_HEIGHT_MIN_PX}px, calc(var(--cch-viewport-height-50) - ${CHART_NON_GRAPH_HEIGHT_NO_LEGEND_PX}px), ${CHART_HEIGHT_MAX_PX_NO_LEGEND}px)`; + // 关键目标:不让卡片本身滚动,在小视口下通过收缩图表高度,确保底部 Legend/按钮不被裁切。 + // 这里使用 DOM 实测(Header/MetricTabs/Legend/ChartPadding)来计算可用高度,避免硬编码误差。 return ( - + {/* Header */} -
-
+
+

{t("title")}

{/* Chart Mode Toggle */} {visibleUsers.length > 1 && ( @@ -228,7 +288,7 @@ export function StatisticsChartCard({ data-active={data.timeRange === option.key} onClick={() => onTimeRangeChange(option.key)} className={cn( - "px-3 py-3 text-xs font-medium transition-colors cursor-pointer", + "px-3 py-2 text-xs font-medium transition-colors cursor-pointer", "border-l border-border/50 dark:border-white/[0.06] first:border-l-0", "hover:bg-muted/50 dark:hover:bg-white/[0.03]", "data-[active=true]:bg-primary/10 data-[active=true]:text-primary" @@ -242,12 +302,12 @@ export function StatisticsChartCard({
{/* Metric Tabs */} -
+
@@ -264,7 +324,7 @@ export function StatisticsChartCard({ data-active={activeChart === "calls"} onClick={() => setActiveChart("calls")} className={cn( - "flex-1 flex flex-col items-start gap-0.5 px-4 py-3 transition-colors cursor-pointer", + "flex-1 flex flex-col items-start gap-0.5 px-4 py-2.5 transition-colors cursor-pointer", "hover:bg-muted/30 dark:hover:bg-white/[0.02]", "data-[active=true]:bg-muted/50 dark:data-[active=true]:bg-white/[0.04]" )} @@ -272,18 +332,18 @@ export function StatisticsChartCard({ {t("totalCalls")} - + {visibleTotals.calls.toLocaleString()}
{/* Chart */} -
+
@@ -416,7 +476,7 @@ export function StatisticsChartCard({ {/* Legend */} {enableUserFilter && ( -
+
{/* Control buttons */}
@@ -358,7 +358,7 @@ export function StatisticsChartCard({ {t("totalCalls")} - + {visibleTotals.calls.toLocaleString()} From 45a6d7ab7bd4c030de0b6619494ac159b23dd9ba Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 16:59:38 +0800 Subject: [PATCH 15/26] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E8=AE=A1=20legend=20?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=8C=89=E9=92=AE=E6=82=AC=E6=B5=AE=E5=88=B0?= =?UTF-8?q?=E5=8F=B3=E4=B8=8A=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/bento/statistics-chart-card.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index ed49d8559..e1e218493 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -502,9 +502,9 @@ export function StatisticsChartCard({ {/* Legend */} {enableUserFilter && ( -
- {/* Control buttons */} -
+
+ {/* Control buttons (floating, does not take extra vertical space) */} +
- |
{/* User list with max 3 rows and scroll - only show users with non-zero usage */} -
+
{data.users .map((user, originalIndex) => ({ user, originalIndex })) From 55913a121eb48af310ae6dd17009d990853f7dea Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 17:07:17 +0800 Subject: [PATCH 16/26] =?UTF-8?q?fix(ui):=20legend=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E6=88=AA=E6=96=AD=E6=B7=BB=E5=8A=A0=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index e1e218493..a57cb1de5 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -562,7 +562,10 @@ export function StatisticsChartCard({ className="h-2 w-2 rounded-full flex-shrink-0" style={{ backgroundColor: color }} /> - + {user.name === "__others__" ? t("othersAggregate") : user.name} From c9f58602e1dce98ea164425f224c767cbb838b0c Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 17:21:41 +0800 Subject: [PATCH 17/26] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E8=AE=A1=20tooltip?= =?UTF-8?q?=20=E6=8F=90=E5=8D=87=E5=B1=82=E7=BA=A7=E5=B9=B6=E4=B8=8A?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index a57cb1de5..0995817c9 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -417,6 +417,11 @@ export function StatisticsChartCard({ /> { if (!active || !payload?.length) return null; const filteredPayload = payload.filter((entry) => { @@ -427,7 +432,7 @@ export function StatisticsChartCard({ if (!filteredPayload.length) return null; return ( -
+
{formatTooltipDate(String(label ?? ""))}
From 31a836e645d2535cfd528ee22420789b2c194e6d Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 18:16:56 +0800 Subject: [PATCH 18/26] =?UTF-8?q?fix(ui):=20tooltip=20=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=BB=9A=E8=BD=AE=E5=8F=AF=E6=BB=9A=E5=8A=A8=E5=B9=B6=E5=B0=BD?= =?UTF-8?q?=E9=87=8F=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bento/statistics-chart-card.tsx | 130 ++++++++++++------ 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 0995817c9..daf775062 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -78,6 +78,7 @@ export function StatisticsChartCard({ const metricTabsRef = React.useRef(null); const chartWrapperRef = React.useRef(null); const legendRef = React.useRef(null); + const tooltipScrollRef = React.useRef(null); const [chartHeightPx, setChartHeightPx] = React.useState(() => enableUserFilter ? CHART_HEIGHT_MAX_PX_WITH_LEGEND : CHART_HEIGHT_MAX_PX_NO_LEGEND @@ -159,6 +160,37 @@ export function StatisticsChartCard({ }; }, [enableUserFilter]); + React.useEffect(() => { + const chartWrapper = chartWrapperRef.current; + if (!chartWrapper) return; + + const handleWheel = (event: WheelEvent) => { + const scrollContainer = tooltipScrollRef.current; + if (!scrollContainer) return; + + const maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight; + if (maxScrollTop <= 0) return; + + const deltaY = + event.deltaMode === 1 + ? event.deltaY * 16 + : event.deltaMode === 2 + ? event.deltaY * 240 + : event.deltaY; + if (!Number.isFinite(deltaY) || deltaY === 0) return; + + const current = scrollContainer.scrollTop; + const next = Math.min(maxScrollTop, Math.max(0, current + deltaY)); + if (next === current) return; + + scrollContainer.scrollTop = next; + event.preventDefault(); + }; + + chartWrapper.addEventListener("wheel", handleWheel, { passive: false }); + return () => chartWrapper.removeEventListener("wheel", handleWheel); + }, []); + const toggleUserSelection = (userId: number) => { setSelectedUserIds((prev) => { const next = new Set(prev); @@ -275,7 +307,10 @@ export function StatisticsChartCard({ return ( {/* Header */}
{ if (!active || !payload?.length) return null; const filteredPayload = payload.filter((entry) => { @@ -432,45 +464,51 @@ export function StatisticsChartCard({ if (!filteredPayload.length) return null; return ( -
-
+
+
{formatTooltipDate(String(label ?? ""))}
-
- {[...filteredPayload] - .sort((a, b) => (Number(b.value ?? 0) || 0) - (Number(a.value ?? 0) || 0)) - .map((entry, index) => { - const baseKey = - entry.dataKey?.toString().replace(`_${activeChart}`, "") || ""; - const displayUser = userMap.get(baseKey); - const value = - typeof entry.value === "number" - ? entry.value - : Number(entry.value ?? 0); - return ( -
-
-
- - {displayUser?.name === "__others__" - ? t("othersAggregate") - : displayUser?.name || baseKey} +
+
+ {[...filteredPayload] + .sort((a, b) => (Number(b.value ?? 0) || 0) - (Number(a.value ?? 0) || 0)) + .map((entry, index) => { + const baseKey = + entry.dataKey?.toString().replace(`_${activeChart}`, "") || ""; + const displayUser = userMap.get(baseKey); + const value = + typeof entry.value === "number" + ? entry.value + : Number(entry.value ?? 0); + return ( +
+
+
+ + {displayUser?.name === "__others__" + ? t("othersAggregate") + : displayUser?.name || baseKey} + +
+ + {activeChart === "cost" + ? formatCurrency(value, currencyCode) + : value.toLocaleString()}
- - {activeChart === "cost" - ? formatCurrency(value, currencyCode) - : value.toLocaleString()} - -
- ); - })} + ); + })} +
); @@ -524,8 +562,14 @@ export function StatisticsChartCard({
{/* User list with max 3 rows and scroll - only show users with non-zero usage */} -
+
{data.users .map((user, originalIndex) => ({ user, originalIndex })) From ff6ab4fe7f402408ed5ce0b406a82b247a411d4f Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 20:00:26 +0800 Subject: [PATCH 21/26] =?UTF-8?q?fix(ui):=20legend=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E9=81=BF=E5=85=8D=E6=8D=A2=E8=A1=8C=E9=81=AE?= =?UTF-8?q?=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index bff8ce5e8..71856bb21 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -555,7 +555,7 @@ export function StatisticsChartCard({ onClick={() => setSelectedUserIds(new Set(data.users.map((u) => u.id)))} disabled={selectedUserIds.size === data.users.length} className={cn( - "text-[10px] px-2 py-0.5 rounded transition-colors cursor-pointer", + "text-[10px] px-2 py-0.5 rounded transition-colors cursor-pointer whitespace-nowrap", selectedUserIds.size === data.users.length ? "text-muted-foreground/50 cursor-not-allowed" : "text-primary hover:text-primary/80 hover:bg-primary/10" @@ -577,7 +577,7 @@ export function StatisticsChartCard({ }} disabled={selectedUserIds.size === 1} className={cn( - "text-[10px] px-2 py-0.5 rounded transition-colors cursor-pointer", + "text-[10px] px-2 py-0.5 rounded transition-colors cursor-pointer whitespace-nowrap", selectedUserIds.size === 1 ? "text-muted-foreground/50 cursor-not-allowed" : "text-primary hover:text-primary/80 hover:bg-primary/10" From 2d7733fb3e7bf688412ae065ea247c5e196538f4 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 20:20:49 +0800 Subject: [PATCH 22/26] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E8=AE=A1=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=8C=87=E6=A0=87=E5=AD=97=E5=8F=B7=E4=B8=8E=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 71856bb21..034522cca 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -125,6 +125,7 @@ export function StatisticsChartCard({ // - DOM 计算高度可能带小数(浏览器缩放 / 子像素) // - 卡片自身 border 也会占用 max-h 的可用空间 const reservedHeightRounded = Math.ceil(reservedHeight); + // 额外留 4px 兜底空间,覆盖不同浏览器/缩放下的 rounding、边框与子像素误差,避免底部内容露半截。 const safetyGapPx = 4; const availableHeight = Math.max( 0, @@ -380,7 +381,7 @@ export function StatisticsChartCard({ {t("totalCost")} - + {formatCurrency(visibleTotals.cost, currencyCode)} @@ -396,7 +397,7 @@ export function StatisticsChartCard({ {t("totalCalls")} - + {visibleTotals.calls.toLocaleString()} @@ -586,7 +587,7 @@ export function StatisticsChartCard({ {t("legend.deselectAll")}
- {/* User list with max 3 rows and scroll - only show users with non-zero usage */} + {/* User list with max 3 rows (3 * 24px = 72px) and scroll - only show users with non-zero usage */}
{data.users From 5629ed4c35b34ec3e09390b8f16358dc4fb12ffa Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 20:32:29 +0800 Subject: [PATCH 23/26] =?UTF-8?q?fix(ui):=20legend=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=8C=BA=E9=80=82=E9=85=8D=20RTL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 034522cca..9236dbd63 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -551,7 +551,7 @@ export function StatisticsChartCard({ {enableUserFilter && (
{/* Control buttons (floating, does not take extra vertical space) */} -
+
{/* User list with max 3 rows (3 * 24px = 72px) and scroll - only show users with non-zero usage */} -
+
{data.users .map((user, originalIndex) => ({ user, originalIndex })) From 1bbf14b0ee373bd2294e19dc4004737603b3cfae Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 20:47:39 +0800 Subject: [PATCH 24/26] =?UTF-8?q?fix(ui):=20=E7=BB=9F=E8=AE=A1=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E7=9B=91=E5=90=AC=20visualViewport=20=E5=8F=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/bento/statistics-chart-card.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 9236dbd63..29eb600c9 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -147,6 +147,14 @@ export function StatisticsChartCard({ return () => window.removeEventListener("resize", compute); } + const visualViewport = window.visualViewport; + // 即使有 ResizeObserver,也需要监听 viewport 变化: + // - 当内容高度小于 max-h 时,卡片本身不会因为视口变高而触发 RO + // - 但 vh/dvh 上限变大后,需要让图表高度回弹 + // 同时监听 visualViewport,覆盖移动端地址栏/键盘导致的可视区域变化。 + visualViewport?.addEventListener("resize", compute); + visualViewport?.addEventListener("scroll", compute); + const observer = new ResizeObserver(compute); observer.observe(card); observer.observe(header); @@ -159,6 +167,8 @@ export function StatisticsChartCard({ return () => { window.removeEventListener("resize", compute); + visualViewport?.removeEventListener("resize", compute); + visualViewport?.removeEventListener("scroll", compute); observer.disconnect(); }; }, [enableUserFilter]); From 36a22f41bcb9832dbb23689f2532c2f8efda1a21 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 20:59:43 +0800 Subject: [PATCH 25/26] =?UTF-8?q?fix(ui):=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=20visualViewport=20scroll=20?= =?UTF-8?q?=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 29eb600c9..d419b18e6 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -151,9 +151,8 @@ export function StatisticsChartCard({ // 即使有 ResizeObserver,也需要监听 viewport 变化: // - 当内容高度小于 max-h 时,卡片本身不会因为视口变高而触发 RO // - 但 vh/dvh 上限变大后,需要让图表高度回弹 - // 同时监听 visualViewport,覆盖移动端地址栏/键盘导致的可视区域变化。 + // 同时监听 visualViewport resize,覆盖移动端地址栏/键盘导致的可视区域变化。 visualViewport?.addEventListener("resize", compute); - visualViewport?.addEventListener("scroll", compute); const observer = new ResizeObserver(compute); observer.observe(card); @@ -168,7 +167,6 @@ export function StatisticsChartCard({ return () => { window.removeEventListener("resize", compute); visualViewport?.removeEventListener("resize", compute); - visualViewport?.removeEventListener("scroll", compute); observer.disconnect(); }; }, [enableUserFilter]); From 9aad41fe2bd22d5c98718981f6b3976579fbaef1 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Fri, 27 Feb 2026 21:30:07 +0800 Subject: [PATCH 26/26] =?UTF-8?q?fix(ui):=20=E5=87=8F=E5=B0=91=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8D=A1=E7=89=87=E9=AB=98=E5=BA=A6=E9=87=8D=E7=AE=97?= =?UTF-8?q?=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/bento/statistics-chart-card.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index d419b18e6..5692d53ff 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -97,7 +97,7 @@ export function StatisticsChartCard({ const compute = () => { const cardStyle = getComputedStyle(card); const viewportHeightPx = window.visualViewport?.height ?? window.innerHeight; - const fallbackMaxHeightPx = Math.floor(Math.min(viewportHeightPx * 0.6, 720)); + const fallbackMaxHeightPx = Math.min(Math.floor(viewportHeightPx * 0.6), 720); const maxHeightPx = parsePx(cardStyle.maxHeight) ?? fallbackMaxHeightPx; const minHeightPx = parsePx(cardStyle.minHeight) ?? 0; const effectiveMaxHeightPx = Math.max(maxHeightPx, minHeightPx); @@ -155,10 +155,8 @@ export function StatisticsChartCard({ visualViewport?.addEventListener("resize", compute); const observer = new ResizeObserver(compute); - observer.observe(card); observer.observe(header); observer.observe(metricTabs); - observer.observe(chartWrapper); if (enableUserFilter && legendRef.current) { observer.observe(legendRef.current); }