分析仪表盘
期间和渠道筛选、收入与转化图表、精确表格及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-17 | US$3,682.00 | US$5,064.00 |
| 2026-09-18 | US$3,842.00 | US$5,118.00 |
| 2026-09-19 | US$4,292.00 | US$5,172.00 |
| 2026-09-20 | US$4,542.00 | US$5,226.00 |
| 2026-09-21 | US$3,640.00 | US$5,280.00 |
| 2026-09-22 | US$4,040.00 | US$5,334.00 |
| 2026-09-23 | US$4,142.00 | US$5,388.00 |
| 2026-09-24 | US$4,642.00 | US$5,442.00 |
| 2026-09-25 | US$4,842.00 | US$5,496.00 |
| 2026-09-26 | US$3,890.00 | US$5,550.00 |
| 2026-09-27 | US$4,340.00 | US$5,604.00 |
| 2026-09-28 | US$4,550.00 | US$5,658.00 |
| 2026-09-29 | US$5,000.00 | US$5,712.00 |
| 2026-09-30 | US$5,200.00 | US$5,766.00 |
渠道比较
所选期间的渠道总计。柱形从零开始。
访问: 23,778
查看精确数据
| 流量渠道 | 访问 | 转化访问 | 转化率 |
|---|---|---|---|
| Organic | 9,470 | 386 | 4.08% |
| Direct | 8,011 | 420 | 5.24% |
| Referral | 6,297 | 404 | 6.42% |
访问转化率
转化访问数除以访问数。纵轴固定为0–100%。
5.09% · 1,210 / 23,778
查看精确数据
| 日期 | 访问 | 转化访问 | 转化率 |
|---|---|---|---|
| 2026-09-17 | 1,557 | 73 | 4.69% |
| 2026-09-18 | 1,547 | 77 | 4.98% |
| 2026-09-19 | 1,622 | 86 | 5.3% |
| 2026-09-20 | 1,612 | 91 | 5.65% |
| 2026-09-21 | 1,602 | 72 | 4.49% |
| 2026-09-22 | 1,677 | 80 | 4.77% |
| 2026-09-23 | 1,667 | 83 | 4.98% |
| 2026-09-24 | 1,742 | 93 | 5.34% |
| 2026-09-25 | 1,732 | 97 | 5.6% |
| 2026-09-26 | 1,722 | 77 | 4.47% |
| 2026-09-27 | 1,797 | 86 | 4.79% |
| 2026-09-28 | 1,787 | 91 | 5.09% |
| 2026-09-29 | 1,862 | 100 | 5.37% |
| 2026-09-30 | 1,852 | 104 | 5.62% |
安装
按照安装指南设置一次基础主题和 Pretendard。模板不会覆盖已有主题、全局 CSS 或页面路由。
安装: template-analytics
npx 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();