本页由 2 个真实组件拼成;提示词与源码都可直接复制带走。
复现「使用概览」整页场景(/showcase/demo/dashboard):
- 页面级规则按 /design#pages 的对应小节执行,不在此复述
- 本页由这些真实组件拼成(手写等价物不算):
- 滑动删除(Swipe Actions):四段链:1:1 跟手 → 越界阻尼 30% → 阈值 96px / 速度 0.5px/ms 双判 → 吸附或飞出|/showcase/component/drag/swipe-to-dismiss
- 分段控件(Segmented Control (iOS)):APG radiogroup:方向键遍历 + roving tabindex|/showcase/component/segmented/glass-apple
- 骨架:视口高度固定,页头不滚,正文在容器内滚;分区页的页头与正文共用同一条内容列
清单是扫 src/pages/showcase/demo/dashboard.astro 的 import 闭包得出的,
手写等价物不会出现在这里。
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import DemoChrome from '../../../components/DemoChrome.astro';
import PageHeader from '../../../components/PageHeader.astro';
import PageShell from '../../../components/PageShell.astro';
import SceneDocLink from '../../../components/SceneDocLink.astro';
import DashboardFilter from './DashboardFilter.tsx';
import ActivityFeed from './ActivityFeed.tsx';
import data from '../../../mocks/dashboard.json';
import type { DashboardData, Trend } from '../../../mocks/types';
const { metrics, chart, activity } = data as DashboardData;
const CHART_MAX = Math.max(...chart.values);
const CHART_MID = Math.round(CHART_MAX / 2);
const CHART_FIRST = chart.values[0];
const CHART_LAST = chart.values[chart.values.length - 1];
const CHART_TREND = CHART_LAST >= CHART_FIRST ? '整体上升' : '整体下降';
const TREND_COLOR: Record<Trend, string> = {
up: 'text-[var(--color-success-fg)]',
down: 'text-[var(--color-warning-fg)]',
};
// 白色内容面板:不透明、软阴影(玻璃留给悬浮层)
const panel = 'rounded-2xl bg-[var(--color-surface)] border border-[var(--glass-border)] shadow-[var(--shadow-sm)]';
---
<BaseLayout title="UI Echo · Dashboard">
<DemoChrome />
<PageShell demo col>
<PageHeader
slot="header"
title="使用概览"
description="核心指标、访问趋势与最近活动。"
>
<Fragment slot="actions">
<DashboardFilter client:visible />
<SceneDocLink id="dashboard" />
<span class="text-xs text-[var(--color-text-subtle)] font-mono whitespace-nowrap">
近 30 天
</span>
</Fragment>
</PageHeader>
<!-- 指标行:白色面板,数字是主角 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
{metrics.map((m) => (
<dl class={`${panel} p-5`}>
<dt class="text-xs text-[var(--color-text-muted)] mb-2">{m.label}</dt>
<dd class="text-[length:var(--text-2xl)] leading-none font-semibold tracking-tight tabular-nums" style={{ fontWeight: 'var(--fw-semibold)' }}>
{m.value}
</dd>
<dd class={`text-xs mt-2.5 font-mono tabular-nums ${TREND_COLOR[m.trend]}`}>
{m.trend === 'up' ? '↑' : '↓'} {m.delta}
</dd>
</dl>
))}
</div>
<!-- 图表:刻度 + 渐变柱;悬停可读,数据表兜底(USWDS:不要求悬停才能读懂) -->
<figure class={`${panel} p-6 mb-6`}>
<figcaption class="flex items-center justify-between mb-5">
<h3 class="text-sm font-medium">{chart.title}</h3>
<div class="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
<span class="size-2 rounded-sm bg-[var(--color-accent)]/80"></span>
<span>{chart.unit}</span>
</div>
</figcaption>
<div class="flex gap-3">
<!-- 刻度:与柱基线同一套数值 -->
<div class="flex flex-col justify-between h-40 pb-5 text-right text-[length:var(--text-2xs)] text-[var(--color-text-subtle)] font-mono tabular-nums" aria-hidden="true">
<span>{CHART_MAX}k</span>
<span>{CHART_MID}k</span>
<span>0</span>
</div>
<div
class="relative h-40 flex-1"
role="img"
aria-label={`1 至 12 月${chart.title}:从 ${CHART_FIRST} 千到 ${CHART_LAST} 千,${CHART_TREND}`}
>
<!-- 网格线 -->
<div class="absolute inset-x-0 top-0 bottom-5 flex flex-col justify-between" aria-hidden="true">
<div class="border-t border-dashed border-[var(--glass-border)]"></div>
<div class="border-t border-dashed border-[var(--glass-border)]"></div>
<div class="border-t border-[var(--glass-border)]"></div>
</div>
<!-- 柱 -->
<div class="relative flex items-end gap-1.5 h-[calc(100%-20px)]">
{chart.values.map((v, i) => (
<div
class="group flex-1 h-full flex items-end rounded-t-md bg-gradient-to-t from-[var(--color-accent)]/45 to-[var(--color-accent)]/85 hover:to-[var(--color-accent)] transition-colors relative"
style={{ height: `${(v / CHART_MAX) * 100}%` }}
title={`${i + 1}月 · ${v}k`}
>
<span class="
absolute -top-6 left-1/2 -translate-x-1/2
px-1.5 py-0.5 rounded text-[length:var(--text-2xs)] font-mono
bg-[var(--color-text)] text-[var(--color-bg)]
opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none
">{v}k</span>
</div>
))}
</div>
<!-- 稀疏月份标签 -->
<div class="flex gap-1.5 mt-1.5">
{chart.values.map((_, i) => (
<>
<span class="sm:hidden flex-1 text-center text-[length:var(--text-2xs)] text-[var(--color-text-subtle)] font-mono whitespace-nowrap">
{i % 3 === 0 ? `${i + 1}月` : ''}
</span>
<span class="hidden sm:block flex-1 text-center text-[length:var(--text-2xs)] text-[var(--color-text-subtle)] font-mono whitespace-nowrap">
{i % 2 === 0 ? `${i + 1}月` : ''}
</span>
</>
))}
</div>
</div>
</div>
<details class="mt-4 border-t border-[var(--glass-border)] pt-3">
<summary class="cursor-pointer text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)] w-fit">
查看数据表
</summary>
<table class="w-full mt-3 text-xs tabular-nums">
<caption class="sr-only">{chart.title}按月数据,单位为{chart.unit}</caption>
<thead>
<tr class="border-b border-[var(--glass-border)]">
<th scope="col" class="py-2 text-left font-medium text-[var(--color-text-muted)]">月份</th>
<th scope="col" class="py-2 text-right font-medium text-[var(--color-text-muted)]">{chart.unit}</th>
</tr>
</thead>
<tbody>
{chart.values.map((v, i) => (
<tr class="border-b border-[var(--glass-border)] last:border-b-0">
<th scope="row" class="py-1.5 text-left font-normal text-[var(--color-text-subtle)]">{i + 1} 月</th>
<td class="py-1.5 text-right font-mono">{v}</td>
</tr>
))}
</tbody>
</table>
</details>
</figure>
<!-- 最近活动:真实组件驱动,可左滑 / Delete 清除 -->
<ActivityFeed activities={activity} client:visible />
</PageShell>
</BaseLayout>
import { useState } from 'react';
import { GlassAppleSegmented } from '~/components/segmented/glass-apple/Segmented';
export default function DashboardFilter() {
const [value, setValue] = useState('all');
return (
<GlassAppleSegmented
value={value}
onChange={setValue}
options={[
{ value: 'all', label: '全部' },
{ value: 'users', label: '用户' },
{ value: 'traffic', label: '流量' },
{ value: 'revenue', label: '收入' },
]}
/>
);
}
import { useState } from 'react';
import { SwipeToDismiss } from '../../../components/drag/swipe-to-dismiss/SwipeToDismiss';
import type { Activity } from '../../../mocks/types';
export default function ActivityFeed({ activities }: { activities: Activity[] }) {
const [items, setItems] = useState(activities);
const [message, setMessage] = useState('');
const dismiss = (user: string, action: string) => {
setItems((prev) => prev.filter((i) => !(i.user === user && i.action === action)));
setMessage(`已清除 @${user} 的一条动态,还剩 ${items.length - 1} 条`);
};
const panel = 'rounded-2xl bg-[var(--color-surface)] border border-[var(--glass-border)] shadow-[var(--shadow-sm)]';
return (
<div className={`${panel} p-6`}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium">最近活动</h3>
{items.length < activities.length && (
<button
type="button"
onClick={() => {
setItems(activities);
setMessage('已恢复全部动态');
}}
className="press-feedback px-2.5 py-1 rounded-lg text-xs border border-[var(--glass-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors cursor-pointer"
>
恢复
</button>
)}
</div>
<div className="divide-y divide-[var(--glass-border)]">
{items.map((a) => (
<SwipeToDismiss key={`${a.user}-${a.action}`} onDismiss={() => dismiss(a.user, a.action)}>
<div className="flex items-center gap-3 py-3">
<div
className={`
size-8 rounded-full shrink-0
flex items-center justify-center text-xs font-mono
${a.bot
? 'bg-[var(--glass-recess)] text-[var(--color-text-muted)]'
: 'bg-[var(--color-accent)]/15 text-[var(--color-accent)]'}
`}
>
{a.user[0].toUpperCase()}
</div>
<p className="flex-1 min-w-0 text-sm truncate">
<span className="text-[var(--color-text-muted)]">@{a.user}</span>{' '}
<span>{a.action}</span>
</p>
<span className="text-xs text-[var(--color-text-subtle)] font-mono shrink-0">{a.time}</span>
</div>
</SwipeToDismiss>
))}
{items.length === 0 && (
<p className="py-8 text-center text-xs text-[var(--color-text-muted)]">没有新的活动</p>
)}
</div>
<p aria-live="polite" className="sr-only">
{message}
</p>
</div>
);
}
页面级规则(页头、工具栏、表格、设置、详情、流程、状态)只写在 页面模式 一处,这里不复述——复述就会漂移。