前端修复: - 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个页面全部渲染正常
311 lines
11 KiB
HTML
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">☀️</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">🌸</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">🌿</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">✏️</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>
|