← 全部模板

分析仪表盘

期间和渠道筛选、收入与转化图表、精确表格及CSV导出。

示例数据 · 2026年9月 · USD。未连接实际服务。

成效仪表盘

在同一时间范围内查看同一数据源。

2026-09-17 – 2026-09-30 · 记录天数: 14 / 14

收入
US$60,644.00
访问
23,778
转化访问
1,210
转化率
5.09%

收入与目标

每日收入按货币显示。虚线表示目标。

收入: US$60,644.00 · 目标: US$75,810.00

查看精确数据
收入与目标
日期收入目标
2026-09-17US$3,682.00US$5,064.00
2026-09-18US$3,842.00US$5,118.00
2026-09-19US$4,292.00US$5,172.00
2026-09-20US$4,542.00US$5,226.00
2026-09-21US$3,640.00US$5,280.00
2026-09-22US$4,040.00US$5,334.00
2026-09-23US$4,142.00US$5,388.00
2026-09-24US$4,642.00US$5,442.00
2026-09-25US$4,842.00US$5,496.00
2026-09-26US$3,890.00US$5,550.00
2026-09-27US$4,340.00US$5,604.00
2026-09-28US$4,550.00US$5,658.00
2026-09-29US$5,000.00US$5,712.00
2026-09-30US$5,200.00US$5,766.00

渠道比较

所选期间的渠道总计。柱形从零开始。

访问: 23,778

查看精确数据
渠道比较
流量渠道访问转化访问转化率
Organic9,4703864.08%
Direct8,0114205.24%
Referral6,2974046.42%

访问转化率

转化访问数除以访问数。纵轴固定为0–100%。

5.09% · 1,210 / 23,778

查看精确数据
访问转化率
日期访问转化访问转化率
2026-09-171,557734.69%
2026-09-181,547774.98%
2026-09-191,622865.3%
2026-09-201,612915.65%
2026-09-211,602724.49%
2026-09-221,677804.77%
2026-09-231,667834.98%
2026-09-241,742935.34%
2026-09-251,732975.6%
2026-09-261,722774.47%
2026-09-271,797864.79%
2026-09-281,787915.09%
2026-09-291,8621005.37%
2026-09-301,8521045.62%

缺失日期显示为空白而非零。访问量为零时转化率未定义(—)。

总计仅涵盖提供的记录,不保证完整性。期间以提供的最后日期为准。

安装

按照安装指南设置一次基础主题和 Pretendard。模板不会覆盖已有主题、全局 CSS 或页面路由。

安装: template-analyticsnpx shadcn@latest add @neumorphism-ui/template-analytics

使用示例

使用示例
"use client";
import { AnalyticsDashboard, type AnalyticsRecord } from "@/components/blocks/analytics-dashboard";

export function Example({ records }: { records: AnalyticsRecord[] }) {
  return <AnalyticsDashboard records={records} locale="zh" currency="USD" />;
}

连接应用

这是只读视图。传入已认证服务的records。日期和渠道各一行,访问量为非负整数,货币为1/100单位。期间截止到最后提供的日期。所有示例数据均为虚构。

安装源码

@components/blocks/analytics-dashboard.tsx
@components/blocks/analytics-dashboard.tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectItem } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { RevenueChart } from "@/components/blocks/revenue-chart";
import { ChannelChart } from "@/components/blocks/channel-chart";
import { ConversionChart } from "@/components/blocks/conversion-chart";
import { summarizeAnalytics, AnalyticsDataError, analyticsCsv, type AnalyticsRecord } from "@/lib/analytics-model";
import { analyticsCopy, analyticsFormats, type AnalyticsLocale } from "@/lib/analytics-copy";
import { demoAnalytics } from "./analytics-demo";

export interface AnalyticsDashboardProps {
  records: readonly AnalyticsRecord[];
  locale?: AnalyticsLocale;
  currency?: string;
}

/** Read-only reporting: callers own authenticated loading and business data.
 * Props are live inputs; replacing records recalculates this view immediately.
 */
export function AnalyticsDashboard({ records, locale = "en", currency = "USD" }: AnalyticsDashboardProps) {
  const [days, setDays] = React.useState(14);
  const [channel, setChannel] = React.useState<string | undefined>();
  const t = analyticsCopy[locale];
  const result = React.useMemo(() => {
    try { return { data: summarizeAnalytics(records, days, channel), error: false as const }; }
    catch (error) {
      if (!(error instanceof AnalyticsDataError)) throw error;
      return { data: null, error: true as const };
    }
  }, [records, days, channel]);
  const channels = React.useMemo(() => [...new Set(records.map(record => record.channel))].sort(), [records]);
  if (result.error) return <Alert variant="destructive" data-slot="analytics-error"><AlertDescription>{t.invalid}</AlertDescription></Alert>;
  const data = result.data;
  const f = analyticsFormats(locale, currency);
  const metrics = [
    { key: "revenue", label: t.revenue, value: data.observedDays ? f.money(data.totals.revenueCents) : "—" },
    { key: "visits", label: t.visits, value: data.observedDays ? f.number(data.totals.visits) : "—" },
    { key: "converted", label: t.conversions, value: data.observedDays ? f.number(data.totals.convertedVisits) : "—" },
    { key: "conversion", label: t.conversion, value: f.percent(data.conversion) },
  ];
  function exportCsv() {
    const url = URL.createObjectURL(new Blob([analyticsCsv(data)], { type: "text/csv;charset=utf-8" }));
    const link = document.createElement("a");
    link.href = url;
    link.download = `analytics-${data.start}-${data.end}.csv`;
    document.body.append(link);
    link.click();
    link.remove();
    // The download needs a task to consume the URL before it is released.
    window.setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
  return <section data-slot="analytics-dashboard" className="grid min-w-0 gap-6 text-[var(--foreground)]">
    <header className="flex flex-wrap items-start justify-between gap-4"><div className="grid gap-2"><h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">{t.title}</h2><p className="text-sm text-[var(--muted-foreground)]">{t.intro}</p></div><Button onClick={exportCsv} disabled={!data.observedDays}>{t.export}</Button></header>
    <div className="flex flex-wrap items-end gap-4 rounded-[var(--neu-radius-surface)] bg-transparent py-4 shadow-none">
      <label className="grid min-w-36 flex-1 gap-2 text-xs font-medium">{t.period}<Select value={days} onChange={event => setDays(Number(event.target.value))}>{[7, 14, 30].map(value => <SelectItem key={value} value={value}>{value} {t.days}</SelectItem>)}</Select></label>
      <label className="grid min-w-44 flex-1 gap-2 text-xs font-medium">{t.channel}<Select value={channel === undefined ? "all" : `channel:${channel}`} onChange={event => setChannel(event.target.value === "all" ? undefined : event.target.value.slice(8))}><SelectItem value="all">{t.all}</SelectItem>{channel !== undefined && !channels.includes(channel) && <SelectItem value={`channel:${channel}`}>{channel}</SelectItem>}{channels.map(value => <SelectItem key={value} value={`channel:${value}`}>{value}</SelectItem>)}</Select></label>
      <Button variant="ghost" onClick={() => { setDays(14); setChannel(undefined); }} disabled={days === 14 && channel === undefined}>{t.reset}</Button>
    </div>
    <p role="status" data-slot="analytics-window" className="text-xs leading-relaxed text-[var(--muted-foreground)]">{data.start && `${data.start} – ${data.end} · `}{t.window}: {data.observedDays} / {days}</p>
    <dl className="grid min-w-0 grid-cols-2 gap-4 xl:grid-cols-4">{metrics.map(metric => <div key={metric.key} className="grid min-w-0 gap-3 rounded-[var(--neu-radius-surface)] border border-[var(--neu-edge)] bg-[var(--neu-surface)] p-4 text-center [box-shadow:var(--neu-shadow-inset)] first:[box-shadow:var(--neu-shadow-raised-sm)] first:text-[var(--neu-accent-ink)] sm:p-5"><dt className="text-xs text-[var(--muted-foreground)]">{metric.label}</dt><dd data-metric={metric.key} className="break-words text-xl font-semibold tracking-tight tabular-nums sm:text-2xl">{metric.value}</dd></div>)}</dl>
    <RevenueChart data={data} locale={locale} currency={currency} />
    <div className="grid min-w-0 gap-6 xl:grid-cols-2"><ChannelChart data={data} locale={locale} /><ConversionChart data={data} locale={locale} /></div>
    <footer className="grid gap-2 text-xs leading-relaxed text-[var(--muted-foreground)]"><p>{t.missing}</p><p>{t.coverage}</p></footer>
  </section>;
}

export function AnalyticsExample({ locale = "en" }: { locale?: AnalyticsLocale }) {
  const [empty, setEmpty] = React.useState(false);
  const t = analyticsCopy[locale];
  return <div className="grid min-w-0 gap-5"><div className="flex flex-wrap items-center justify-between gap-3"><p className="max-w-prose text-xs leading-relaxed text-[var(--muted-foreground)]">{t.sample}</p><label className="flex items-center gap-2 text-xs"><Checkbox checked={empty} onCheckedChange={value => setEmpty(value === true)} />{t.showEmpty}</label></div><AnalyticsDashboard records={empty ? [] : demoAnalytics} locale={locale} /></div>;
}

export { demoAnalytics } from "./analytics-demo";
export type { AnalyticsRecord } from "@/lib/analytics-model";
@components/blocks/analytics-demo.ts
@components/blocks/analytics-demo.ts
import type { AnalyticsRecord } from "@/lib/analytics-model";

/** Deterministic synthetic example; this is not business or live-service data. */
export const demoAnalytics: readonly AnalyticsRecord[] = Array.from({ length: 30 }, (_, index) => {
  const date = new Date(Date.UTC(2026, 8, index + 1)).toISOString().slice(0, 10);
  return ["Organic", "Direct", "Referral"].map((channel, group) => {
    const visits = 230 + (2 - group) * 115 + index * 8 + ((index * 17 + group * 29) % 85);
    const convertedVisits = Math.floor(visits * (0.035 + group * 0.012 + (index % 5) * 0.003));
    return { date, channel, visits, convertedVisits, revenueCents: convertedVisits * (4200 + group * 800), targetCents: 115000 + (2 - group) * 25000 + index * 1800 };
  });
}).flat();