Files
nj/docs/opendesign/screens/onboarding.html
iven b320641d9c
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 全链路验证修复 — 编译错误/CORS/迁移/启动脚本
前端修复:
- calendar_page: 移除不存在的 JournalEntry.content getter
- responsive_scaffold: 移除不存在的 notchThickness 参数
- splash_page: SingleTickerProvider → TickerProvider (多 AnimationController)
- profile_page: UserRoleType.name → .code (修复运行时崩溃)
- 导入缺失的 user.dart

后端修复:
- class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞
- diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT

基础设施:
- config/default.toml: CORS 改为通配符(开发模式)
- scripts/dev.sh: 统一启动脚本(自动清理端口)
- docs/opendesign/: Open Design 设计规格 HTML 原型稿

验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
2026-06-02 01:03:58 +08:00

311 lines
11 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=390, height=844, initial-scale=1, viewport-fit=cover">
<title>暖记 — 引导页</title>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/tokens.css">
<link rel="stylesheet" href="../css/components.css">
<style>
body {
width: 390px;
height: 844px;
overflow: hidden;
background: var(--bg);
position: relative;
}
/* Global focus styles */
button:focus-visible, a:focus-visible, [role="tab"]:focus-visible {
box-shadow: 0 0 0 3px var(--focus-ring);
outline: none;
}
.skip-btn {
position: absolute;
top: calc(var(--safe-top) + 8px);
right: var(--space-5);
background: none;
border: none;
font-size: var(--text-base);
color: var(--muted);
cursor: pointer;
z-index: 50;
padding: var(--space-2);
min-height: 44px;
min-width: 44px;
}
.slides-wrapper {
position: absolute;
top: 0;
left: 0;
width: 300%;
height: 100%;
display: flex;
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.slide {
width: 390px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: calc(var(--safe-top) + 48px) var(--space-8) calc(var(--safe-bottom) + 120px);
position: relative;
}
.slide-illustration {
width: 260px;
height: 260px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-10);
position: relative;
}
.slide-illustration svg {
position: relative;
z-index: 1;
}
.slide-illustration::after {
content: '';
position: absolute;
width: 110%;
height: 110%;
border-radius: 50%;
border: 2px dashed var(--border);
animation: spin 20s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.slide-title {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--fg);
text-align: center;
margin-bottom: var(--space-3);
}
.slide-desc {
font-size: var(--text-base);
color: var(--muted);
text-align: center;
line-height: var(--leading-body);
max-width: 280px;
}
/* Bottom controls */
.onboarding-bottom {
position: absolute;
bottom: calc(var(--safe-bottom) + 24px);
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-6);
padding: 0 var(--space-8);
z-index: 50;
}
.dots {
display: flex;
gap: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border);
transition: all var(--motion-base) var(--ease-bounce);
}
.dot.active {
width: 28px;
border-radius: 4px;
background: var(--accent);
}
.next-btn {
width: 100%;
padding: 16px;
background: var(--accent);
color: var(--accent-on);
border: none;
border-radius: var(--radius-pill);
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 600;
cursor: pointer;
min-height: 44px;
box-shadow: 0 4px 20px var(--shadow-accent);
transition: all var(--motion-fast) var(--ease-bounce);
}
.next-btn:hover { transform: translateY(-1px); }
.next-btn:active { transform: scale(0.98); }
/* Step-specific backgrounds */
.step-1 .slide-illustration { background: linear-gradient(135deg, var(--surface-warm), color-mix(in oklab, var(--accent), transparent 70%)); }
.step-2 .slide-illustration { background: linear-gradient(135deg, var(--secondary-soft), color-mix(in oklab, var(--secondary), transparent 85%)); }
.step-3 .slide-illustration { background: linear-gradient(135deg, var(--tertiary-soft), color-mix(in oklab, var(--tertiary), transparent 80%)); }
/* Floating deco for each slide */
.slide-deco {
position: absolute;
pointer-events: none;
opacity: 0.12;
}
.page-num {
font-family: var(--font-display);
font-size: 120px;
font-weight: 800;
opacity: 0.04;
position: absolute;
top: calc(var(--safe-top) + 20px);
left: var(--space-5);
line-height: 1;
}
</style>
</head>
<body>
<button class="skip-btn" aria-label="跳过引导" onclick="goToSlide(2)">跳过</button>
<div class="slides-wrapper" id="slidesWrapper">
<!-- Step 1: Record -->
<div class="slide step-1">
<div class="page-num" aria-hidden="true">01</div>
<div class="slide-illustration anim-scale" style="animation-delay: 0.2s">
<svg width="140" height="140" viewBox="0 0 140 140" fill="none" aria-hidden="true">
<!-- Notebook -->
<rect x="25" y="15" width="90" height="110" rx="8" fill="white" stroke="var(--accent)" stroke-width="2.5"/>
<rect x="35" y="15" width="6" height="110" fill="var(--accent)" opacity="0.2"/>
<path d="M50 40h42M50 55h35M50 70h42M50 85h28" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
<!-- Pen -->
<g transform="translate(95, 50) rotate(30)">
<rect x="0" y="0" width="8" height="50" rx="4" fill="var(--secondary)"/>
<polygon points="2,50 6,50 4,58" fill="#2D2420"/>
</g>
<!-- Heart sticker -->
<g transform="translate(80, 25)">
<path d="M8 4C8 1.8 6.2 0 4 0S0 1.8 0 4c0 4 8 8 8 8s8-4 8-8c0-2.2-1.8-4-4-4z" fill="var(--accent)" opacity="0.7" transform="scale(1.2)"/>
</g>
</svg>
</div>
<div class="slide-title anim-fade" style="animation-delay: 0.3s">用手账的方式<br>记录每一天</div>
<p class="slide-desc anim-fade" style="animation-delay: 0.4s">
文字、贴纸、涂鸦、照片 — 选择你喜欢的方式,把日常变成一本温暖的手账
</p>
</div>
<!-- Step 2: Decorate -->
<div class="slide step-2">
<div class="page-num" aria-hidden="true">02</div>
<div class="slide-illustration anim-scale" style="animation-delay: 0.2s">
<svg width="140" height="140" viewBox="0 0 140 140" fill="none" aria-hidden="true">
<!-- Sticker sheet -->
<rect x="20" y="20" width="100" height="100" rx="12" fill="white" stroke="var(--secondary)" stroke-width="2"/>
<!-- Stickers -->
<circle cx="50" cy="48" r="14" fill="var(--tertiary)" opacity="0.8"/>
<text x="50" y="53" text-anchor="middle" font-size="16" aria-hidden="true">&#9728;&#65039;</text>
<circle cx="85" cy="48" r="14" fill="var(--rose)" opacity="0.6"/>
<text x="85" y="53" text-anchor="middle" font-size="16" aria-hidden="true">&#127800;</text>
<circle cx="50" cy="85" r="14" fill="var(--secondary)" opacity="0.5"/>
<text x="50" y="90" text-anchor="middle" font-size="16" aria-hidden="true">&#127807;</text>
<circle cx="85" cy="85" r="14" fill="var(--accent)" opacity="0.5"/>
<text x="85" y="90" text-anchor="middle" font-size="16" aria-hidden="true">&#9999;&#65039;</text>
<!-- Sparkles -->
<path d="M30 25l2 4 4 2-4 2-2 4-2-4-4-2 4-2z" fill="var(--tertiary)"/>
<path d="M108 30l2 3 3 2-3 1.5-2 3-1.5-3-3-1.5 3-2z" fill="var(--secondary)"/>
</svg>
</div>
<div class="slide-title anim-fade" style="animation-delay: 0.3s">海量贴纸与模板<br>随心装饰</div>
<p class="slide-desc anim-fade" style="animation-delay: 0.4s">
数百款手绘贴纸、精美模板和装饰素材,让你的日记独一无二
</p>
</div>
<!-- Step 3: Growth -->
<div class="slide step-3">
<div class="page-num" aria-hidden="true">03</div>
<div class="slide-illustration anim-scale" style="animation-delay: 0.2s">
<svg width="140" height="140" viewBox="0 0 140 140" fill="none" aria-hidden="true">
<!-- Calendar grid -->
<rect x="20" y="25" width="100" height="95" rx="10" fill="white" stroke="var(--tertiary)" stroke-width="2"/>
<rect x="20" y="25" width="100" height="24" rx="10" fill="var(--tertiary)" opacity="0.2"/>
<text x="70" y="42" text-anchor="middle" font-size="11" fill="#2D2420" font-weight="600" aria-hidden="true">心情日历</text>
<!-- Grid dots (mood indicators) -->
<g transform="translate(30, 58)">
<circle cx="0" cy="0" r="5" fill="var(--secondary)"/><circle cx="16" cy="0" r="5" fill="var(--accent)"/>
<circle cx="32" cy="0" r="5" fill="var(--tertiary)"/><circle cx="48" cy="0" r="5" fill="var(--secondary)"/>
<circle cx="64" cy="0" r="5" fill="var(--secondary)"/><circle cx="80" cy="0" r="5" fill="var(--rose)"/>
<circle cx="0" cy="18" r="5" fill="var(--accent)"/><circle cx="16" cy="18" r="5" fill="var(--secondary)"/>
<circle cx="32" cy="18" r="5" fill="var(--secondary)"/><circle cx="48" cy="18" r="5" fill="var(--tertiary)"/>
<circle cx="64" cy="18" r="5" fill="var(--secondary)"/><circle cx="80" cy="18" r="5" fill="var(--accent)"/>
<circle cx="0" cy="36" r="5" fill="var(--secondary)"/><circle cx="16" cy="36" r="5" fill="var(--tertiary)"/>
<circle cx="32" cy="36" r="5" fill="var(--secondary)"/>
</g>
<!-- Growth arrow -->
<g transform="translate(100, 105)">
<path d="M0 0 L10 -20 L20 0" stroke="var(--secondary)" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="10" cy="-22" r="3" fill="var(--secondary)"/>
</g>
</svg>
</div>
<div class="slide-title anim-fade" style="animation-delay: 0.3s">回顾成长轨迹<br>看见自己的变化</div>
<p class="slide-desc anim-fade" style="animation-delay: 0.4s">
心情追踪、日历回顾、统计洞察 — 不仅仅是记录,更是了解自己的旅程
</p>
</div>
</div>
<!-- Bottom controls -->
<div class="onboarding-bottom">
<div class="dots" aria-hidden="true">
<div class="dot active" id="dot0"></div>
<div class="dot" id="dot1"></div>
<div class="dot" id="dot2"></div>
</div>
<button class="next-btn" id="nextBtn" aria-label="下一步" onclick="nextSlide()">下一步</button>
</div>
<div class="home-indicator" style="position:absolute;bottom:8px;left:50%;transform:translateX(-50%);z-index:50" aria-hidden="true"></div>
<script>
let currentSlide = 0;
const totalSlides = 3;
const wrapper = document.getElementById('slidesWrapper');
const dots = [document.getElementById('dot0'), document.getElementById('dot1'), document.getElementById('dot2')];
const nextBtn = document.getElementById('nextBtn');
function goToSlide(index) {
currentSlide = Math.min(index, totalSlides - 1);
wrapper.style.transform = `translateX(-${currentSlide * 390}px)`;
dots.forEach((d, i) => d.classList.toggle('active', i === currentSlide));
nextBtn.textContent = currentSlide === totalSlides - 1 ? '开始使用' : '下一步';
nextBtn.setAttribute('aria-label', currentSlide === totalSlides - 1 ? '开始使用暖记' : '下一步');
}
function nextSlide() {
if (currentSlide < totalSlides - 1) {
goToSlide(currentSlide + 1);
}
}
</script>
<script src="../js/theme-switcher.js"></script>
</body>
</html>