fix(ui): 三处实机问题 + hero 改为三圆环
fix: 标题在窄栏里一行一个字 - Framework7 对裸 <button> 有全局样式,把「看数据」按钮拉成整行宽, 标题/副标题容器被挤成 0 宽,文字只能竖排。给按钮显式 width:auto fix: 标题旁边有个灰色方块 - 今日页没有右侧动作,但 NavRight 容器仍然渲染并带半透明白底。 改为有内容才渲染 hero 改为三圆环(步数 / 睡眠 / HRV): - 每个环以「进入自己的参考区间」为满环,所以三个环的满度含义一致, 尽管单位不可比 - 并列而非同心:Apple 的同心环成立是因为三环共享一个概念 (动/练/站),而这三项是互不相关的度量,分开更好读 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,11 +45,10 @@ function Screen({
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Navbar large={large} transparent={large} title={title} subtitle={subtitle} backLink={backLink ? '返回' : undefined}>
|
<Navbar large={large} transparent={large} title={title} subtitle={subtitle} backLink={backLink ? '返回' : undefined}>
|
||||||
<NavRight>
|
{/* Rendered only when the screen actually has an action: an empty
|
||||||
{/* Only what this screen actually offers — a fixed set of icons on
|
NavRight still paints a container, which showed up as a stray
|
||||||
every page turns the navbar into decoration. */}
|
grey box beside the title. */}
|
||||||
{right}
|
{right && <NavRight>{right}</NavRight>}
|
||||||
</NavRight>
|
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className="page-inner">{children}</div>
|
<div className="page-inner">{children}</div>
|
||||||
|
|||||||
@@ -217,6 +217,11 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Framework7 styles bare <button> elements globally and stretched this one to
|
||||||
|
the full row, which squeezed the title/subtitle container to zero width —
|
||||||
|
the text then rendered one character per line. */
|
||||||
.viz-toggle {
|
.viz-toggle {
|
||||||
flex-shrink: 0;
|
flex: 0 0 auto;
|
||||||
|
width: auto;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
/* Hero -------------------------------------------------------------------- */
|
/* Hero -------------------------------------------------------------------- */
|
||||||
.hero {
|
.hero {
|
||||||
display: flex;
|
padding: 1.35rem 1rem 1.15rem;
|
||||||
align-items: center;
|
|
||||||
gap: 1.75rem;
|
|
||||||
padding: 1.5rem 1.75rem;
|
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.5rem;
|
||||||
animation: hero-in 0.5s var(--ease) both;
|
animation: hero-in 0.5s var(--ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,59 +14,81 @@
|
|||||||
to { opacity: 1; transform: none; }
|
to { opacity: 1; transform: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The one hero figure on the view. Proportional figures, and the app's own
|
.ring-row {
|
||||||
sans — a display face here would read as decoration rather than data. */
|
display: grid;
|
||||||
.hero-value {
|
grid-template-columns: repeat(3, 1fr);
|
||||||
font-size: 2.6rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Proportional figures: tabular-nums makes a large standalone number look
|
||||||
|
loose, and these sit at display size inside the rings. */
|
||||||
|
.ring-value {
|
||||||
|
font-size: 1.18rem;
|
||||||
font-weight: 660;
|
font-weight: 660;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-caption {
|
.ring-unit {
|
||||||
font-size: 0.76rem;
|
font-size: 0.62rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-facts {
|
.ring-label {
|
||||||
flex: 1;
|
font-size: 0.82rem;
|
||||||
min-width: 0;
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-goal {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
padding-top: 0.9rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-headline {
|
.hero-headline {
|
||||||
font-size: 1.05rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-list {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
margin: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-list dt {
|
|
||||||
font-size: 0.74rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-list dd {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 620;
|
font-weight: 620;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-aside {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.hero-skeleton {
|
.hero-skeleton {
|
||||||
height: 164px;
|
height: 186px;
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.hero { padding: 1.15rem 0.6rem 1rem; }
|
||||||
|
.ring-row { gap: 0.25rem; }
|
||||||
|
.ring-value { font-size: 1.02rem; }
|
||||||
|
.ring-goal { font-size: 0.63rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Charts ------------------------------------------------------------------ */
|
/* Charts ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -7,52 +7,115 @@ import MetricCard from '../components/charts/MetricCard';
|
|||||||
import MetricStrip from '../components/charts/MetricStrip';
|
import MetricStrip from '../components/charts/MetricStrip';
|
||||||
import Skeleton from '../components/Skeleton';
|
import Skeleton from '../components/Skeleton';
|
||||||
import { useCountUp } from '../lib/motion';
|
import { useCountUp } from '../lib/motion';
|
||||||
|
import { RANGES } from '../lib/ranges';
|
||||||
import './Today.css';
|
import './Today.css';
|
||||||
|
|
||||||
const DAYS = 30;
|
const DAYS = 30;
|
||||||
|
|
||||||
function StepHero({ today, history }: { today: HealthDay; history: HealthDay[] }) {
|
interface RingSpec {
|
||||||
const goal = today.stepGoal ?? null;
|
label: string;
|
||||||
const steps = today.steps ?? null;
|
value: number | null;
|
||||||
const progress = steps != null && goal ? steps / goal : null;
|
/** Reaching this counts as a full ring. */
|
||||||
const animated = useCountUp(steps);
|
target: number | null;
|
||||||
|
unit: string;
|
||||||
|
decimals?: number;
|
||||||
|
goalText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three rings: steps, sleep and HRV.
|
||||||
|
*
|
||||||
|
* Each is normalised against the point where it enters its own reference band,
|
||||||
|
* so a full ring means the same thing for all three even though the units do
|
||||||
|
* not compare. Rings sit side by side rather than concentric — Apple's nested
|
||||||
|
* form works because its three rings share one idea (move/exercise/stand);
|
||||||
|
* these three are unrelated measures and read more clearly apart.
|
||||||
|
*/
|
||||||
|
function RingRow({ today, history }: { today: HealthDay; history: HealthDay[] }) {
|
||||||
|
const stepGoal = today.stepGoal ?? RANGES.steps.goodFrom;
|
||||||
|
|
||||||
|
const specs: RingSpec[] = [
|
||||||
|
{
|
||||||
|
label: '步数',
|
||||||
|
value: today.steps,
|
||||||
|
target: stepGoal,
|
||||||
|
unit: '步',
|
||||||
|
goalText: `目标 ${stepGoal.toLocaleString()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '睡眠',
|
||||||
|
value: today.sleepDuration,
|
||||||
|
target: RANGES.sleepDuration.goodFrom,
|
||||||
|
unit: '小时',
|
||||||
|
decimals: 1,
|
||||||
|
goalText: `目标 ${RANGES.sleepDuration.goodFrom} 小时`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'HRV',
|
||||||
|
value: today.heartRateVariability,
|
||||||
|
target: RANGES.heartRateVariability.goodFrom,
|
||||||
|
unit: 'ms',
|
||||||
|
goalText: `参考 ≥${RANGES.heartRateVariability.goodFrom} ms`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const week = history.slice(-7).map((d) => d.steps).filter((v): v is number => v != null);
|
const week = history.slice(-7).map((d) => d.steps).filter((v): v is number => v != null);
|
||||||
const weekAvg = week.length
|
const weekAvg = week.length
|
||||||
? Math.round(week.reduce((a, b) => a + b, 0) / week.length)
|
? Math.round(week.reduce((a, b) => a + b, 0) / week.length)
|
||||||
: null;
|
: null;
|
||||||
const remaining = steps != null && goal ? goal - steps : null;
|
|
||||||
|
const done = specs.filter((s) => s.value != null && s.target && s.value >= s.target).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="hero">
|
<section className="hero">
|
||||||
<Ring
|
<div className="ring-row">
|
||||||
progress={progress}
|
{specs.map((s) => (
|
||||||
label={`步数完成度 ${progress != null ? Math.round(progress * 100) : 0}%`}
|
<RingCell key={s.label} spec={s} />
|
||||||
>
|
))}
|
||||||
<span className="hero-value">
|
</div>
|
||||||
{steps == null ? '—' : Math.round(animated ?? steps).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
<span className="hero-caption">步</span>
|
|
||||||
</Ring>
|
|
||||||
|
|
||||||
<div className="hero-facts">
|
<div className="hero-summary">
|
||||||
<div className="hero-headline">
|
<span className="hero-headline">
|
||||||
{progress == null
|
{done === 3 ? '三项全部达标' : done === 0 ? '今日尚无达标项' : `${done} / 3 项达标`}
|
||||||
? '今日暂无步数记录'
|
</span>
|
||||||
: progress >= 1
|
{weekAvg != null && (
|
||||||
? '今日目标已完成'
|
<span className="hero-aside">步数近 7 日均 {weekAvg.toLocaleString()}</span>
|
||||||
: `距目标还差 ${remaining!.toLocaleString()} 步`}
|
)}
|
||||||
</div>
|
|
||||||
<dl className="hero-list">
|
|
||||||
<div><dt>目标</dt><dd>{goal ? goal.toLocaleString() : '—'}</dd></div>
|
|
||||||
<div><dt>近 7 日均</dt><dd>{weekAvg ? weekAvg.toLocaleString() : '—'}</dd></div>
|
|
||||||
<div><dt>完成度</dt><dd>{progress != null ? `${Math.round(progress * 100)}%` : '—'}</dd></div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RingCell({ spec }: { spec: RingSpec }) {
|
||||||
|
const animated = useCountUp(spec.value);
|
||||||
|
const progress =
|
||||||
|
spec.value != null && spec.target ? spec.value / spec.target : null;
|
||||||
|
|
||||||
|
const shown =
|
||||||
|
spec.value == null
|
||||||
|
? '—'
|
||||||
|
: (animated ?? spec.value).toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: spec.decimals ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ring-cell">
|
||||||
|
<Ring
|
||||||
|
progress={progress}
|
||||||
|
size={96}
|
||||||
|
thickness={8}
|
||||||
|
label={`${spec.label} ${progress != null ? Math.round(progress * 100) : 0}%`}
|
||||||
|
>
|
||||||
|
<span className="ring-value">{shown}</span>
|
||||||
|
<span className="ring-unit">{spec.unit}</span>
|
||||||
|
</Ring>
|
||||||
|
<div className="ring-label">{spec.label}</div>
|
||||||
|
<div className="ring-goal">{spec.goalText}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TodayPage() {
|
function TodayPage() {
|
||||||
const [days, setDays] = useState<HealthDay[]>([]);
|
const [days, setDays] = useState<HealthDay[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -99,7 +162,7 @@ function TodayPage() {
|
|||||||
|
|
||||||
{!loading && !error && today && (
|
{!loading && !error && today && (
|
||||||
<>
|
<>
|
||||||
<StepHero today={today} history={days} />
|
<RingRow today={today} history={days} />
|
||||||
|
|
||||||
<section className="sec">
|
<section className="sec">
|
||||||
<h3 className="sec-title">活动</h3>
|
<h3 className="sec-title">活动</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user