Files
xianyan/docs/preview/widescreen-narrow-transition.html
Developer 5a49d20c8a chore: 完成项目品牌域名批量替换与功能迭代
本次提交包含多项核心更新:
1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等
2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果
3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线
4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题
5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航
6. 新增收藏与时间线数据合并导入功能
7. 修复多处UI样式问题,统一主题颜色使用规范
8. 新增日历同步与跨平台触觉反馈依赖库
9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
2026-05-29 10:06:55 +08:00

891 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>闲言 - 窄屏过渡动画</title>
<style>
:root {
--primary: #007AFF;
--primary-light: rgba(0,122,255,0.12);
--secondary: #5856D6;
--background: #FAFAFA;
--surface: #FFFFFF;
--surface-secondary: #F2F2F7;
--text: #1C1C1E;
--text-secondary: #8E8E93;
--text-tertiary: #AEAEB2;
--separator: rgba(60,60,67,0.12);
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--space-1: 4px;
--space-2: 8px;
--space-3: 16px;
--space-4: 24px;
--space-5: 32px;
--font-family: -apple-system, system-ui, 'SF Pro Display', 'Segoe UI', sans-serif;
--glass-bg: rgba(255,255,255,0.72);
--glass-blur: 20px;
--card-bg: rgba(255,255,255,0.8);
--red: #FF3B30;
--orange: #FF9500;
--green: #34C759;
--teal: #5AC8FA;
}
[data-theme="dark"] {
--background: #1A1A2E;
--surface: #16213E;
--surface-secondary: #0F3460;
--text: #F5F5F7;
--text-secondary: #98989D;
--text-tertiary: #636366;
--separator: rgba(84,84,88,0.65);
--glass-bg: rgba(30,30,50,0.72);
--card-bg: rgba(30,30,50,0.8);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-family);
background: #2C2C2E;
color: var(--text);
height: 100vh;
overflow: hidden;
}
.page-layout {
display: flex;
height: 100vh;
width: 100%;
}
.control-panel {
width: 300px;
min-width: 300px;
background: var(--surface);
border-right: 1px solid var(--separator);
display: flex;
flex-direction: column;
overflow-y: auto;
padding: var(--space-4);
transition: background 0.3s, color 0.3s;
}
.control-title {
font-size: 24px;
font-weight: 700;
margin-bottom: var(--space-2);
}
.control-subtitle {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: var(--space-5);
line-height: 1.5;
}
.control-section {
margin-bottom: var(--space-4);
}
.control-label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: var(--space-2);
}
.rotate-btn {
width: 100%;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-xl);
border: none;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: #fff;
font-size: 16px;
font-weight: 600;
font-family: var(--font-family);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
transition: all 0.2s;
box-shadow: var(--shadow-md);
}
.rotate-btn:hover { opacity: 0.9; transform: translateY(-1px); }
.rotate-btn:active { transform: translateY(0); }
.state-indicator {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
background: var(--card-bg);
border-radius: var(--radius-xl);
border: 1px solid var(--separator);
margin-top: var(--space-3);
}
.state-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--green);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.state-text { font-size: 13px; font-weight: 500; }
.speed-control {
margin-top: var(--space-3);
}
.speed-slider {
width: 100%;
-webkit-appearance: none;
height: 4px;
border-radius: 2px;
background: var(--separator);
outline: none;
margin-top: var(--space-2);
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
box-shadow: var(--shadow-md);
}
.speed-label {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--text-tertiary);
margin-top: var(--space-1);
}
.animation-steps {
margin-top: var(--space-3);
}
.step-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) 0;
font-size: 13px;
color: var(--text-secondary);
transition: color 0.3s;
}
.step-item.active { color: var(--primary); font-weight: 600; }
.step-number {
width: 24px; height: 24px;
border-radius: 50%;
background: var(--surface-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700;
flex-shrink: 0;
transition: all 0.3s;
}
.step-item.active .step-number {
background: var(--primary);
color: #fff;
}
.info-card {
background: var(--primary-light);
border-radius: var(--radius-xl);
padding: var(--space-3);
border: 1px solid rgba(0,122,255,0.2);
margin-top: var(--space-3);
}
.info-card-title {
font-size: 13px;
font-weight: 600;
color: var(--primary);
margin-bottom: var(--space-1);
}
.info-card-text {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.preview-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-5);
background: #2C2C2E;
position: relative;
perspective: 1200px;
}
.device-container {
position: relative;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.device-frame {
background: #000;
border-radius: 24px;
padding: 8px;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
position: relative;
overflow: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.device-frame.wide {
width: 900px;
height: 560px;
}
.device-frame.narrow {
width: 390px;
height: 700px;
}
.device-screen {
width: 100%;
height: 100%;
background: var(--background);
border-radius: 18px;
overflow: hidden;
display: flex;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.device-frame.wide .device-screen {
flex-direction: row;
}
.device-frame.narrow .device-screen {
flex-direction: column;
}
.device-notch {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 28px;
background: #000;
border-radius: 0 0 16px 16px;
z-index: 100;
transition: all 0.6s;
}
.device-frame.narrow .device-notch {
width: 150px;
height: 32px;
}
.device-frame.wide .device-notch {
width: 0;
height: 0;
}
.nav-sidebar {
width: 72px;
min-width: 72px;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0 var(--space-3);
gap: var(--space-2);
border-right: 1px solid var(--separator);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.device-frame.narrow .nav-sidebar {
width: 100%;
min-width: 100%;
height: 64px;
min-height: 64px;
flex-direction: row;
padding: 0 var(--space-4);
border-right: none;
border-bottom: 1px solid var(--separator);
border-top: none;
order: 2;
}
.nav-logo {
width: 36px; height: 36px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex; align-items: center; justify-content: center;
font-size: 18px; color: #fff;
margin-bottom: var(--space-3);
box-shadow: var(--shadow-md);
transition: all 0.4s;
}
.device-frame.narrow .nav-logo {
margin-bottom: 0;
margin-right: var(--space-4);
width: 32px; height: 32px;
}
.nav-item {
width: 44px; height: 44px;
border-radius: var(--radius-lg);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s;
color: var(--text-secondary);
gap: 2px; font-size: 9px;
}
.nav-item .nav-icon { font-size: 20px; line-height: 1; }
.nav-item .nav-label { font-weight: 500; }
.nav-item:hover { background: var(--primary-light); color: var(--primary); }
.nav-item.active { background: var(--primary-light); color: var(--primary); }
.device-frame.narrow .nav-item {
width: auto; height: auto;
flex-direction: row;
gap: 4px; font-size: 11px;
padding: var(--space-1) var(--space-2);
}
.nav-spacer { flex: 1; }
.content-body {
flex: 1;
display: flex;
overflow: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.master-panel {
width: 40%;
min-width: 200px;
background: var(--background);
border-right: 1px solid var(--separator);
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.device-frame.narrow .master-panel {
width: 100%;
min-width: 100%;
border-right: none;
}
.master-header {
padding: var(--space-3) var(--space-4);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
border-bottom: 1px solid var(--separator);
padding-top: 36px;
}
.device-frame.narrow .master-header {
padding-top: 40px;
}
.master-title { font-size: 20px; font-weight: 700; }
.item-list {
flex: 1;
overflow-y: auto;
padding: var(--space-2);
}
.list-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-lg);
cursor: pointer;
transition: background 0.15s;
margin-bottom: var(--space-1);
}
.list-item:hover { background: var(--primary-light); }
.list-item.selected { background: var(--primary-light); }
.list-item-icon {
width: 32px; height: 32px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 16px; color: #fff;
flex-shrink: 0;
}
.list-item-text { flex: 1; min-width: 0; }
.list-item-title { font-size: 14px; font-weight: 500; }
.list-item-subtitle { font-size: 11px; color: var(--text-secondary); }
.detail-panel {
flex: 1;
background: var(--background);
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(0);
opacity: 1;
}
.device-frame.narrow .detail-panel {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
transform: translateX(100%);
opacity: 0;
z-index: 50;
}
.device-frame.narrow .detail-panel.slide-in {
transform: translateX(0);
opacity: 1;
}
.detail-header {
padding: var(--space-3) var(--space-4);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
border-bottom: 1px solid var(--separator);
display: flex;
align-items: center;
gap: var(--space-2);
padding-top: 36px;
}
.device-frame.narrow .detail-header {
padding-top: 40px;
}
.back-btn {
display: none;
width: 32px; height: 32px;
border-radius: 50%;
border: none;
background: var(--surface-secondary);
color: var(--primary);
font-size: 16px;
cursor: pointer;
align-items: center;
justify-content: center;
}
.device-frame.narrow .back-btn { display: flex; }
.detail-title { font-size: 16px; font-weight: 600; }
.detail-content {
flex: 1;
padding: var(--space-4);
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.detail-sentence {
font-size: 20px;
line-height: 1.7;
font-weight: 500;
text-align: center;
max-width: 400px;
padding: var(--space-4);
background: var(--card-bg);
border-radius: var(--radius-xl);
border: 1px solid var(--separator);
}
.detail-author {
margin-top: var(--space-3);
font-size: 14px;
color: var(--text-secondary);
text-align: center;
}
.detail-actions {
display: flex;
gap: var(--space-3);
margin-top: var(--space-4);
}
.action-btn {
padding: var(--space-2) var(--space-4);
border-radius: 20px;
border: none;
background: var(--surface-secondary);
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
font-family: var(--font-family);
transition: all 0.2s;
}
.action-btn:hover { background: var(--primary-light); color: var(--primary); }
.transition-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
border-radius: 18px;
}
.transition-overlay.active { display: flex; }
.rotation-icon {
font-size: 64px;
animation: rotateDevice 1s ease-in-out;
}
@keyframes rotateDevice {
0% { transform: rotate(0deg); }
50% { transform: rotate(90deg); }
100% { transform: rotate(0deg); }
}
.size-label {
position: absolute;
bottom: -32px;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: rgba(255,255,255,0.6);
white-space: nowrap;
}
.mode-badges {
position: absolute;
top: var(--space-3);
right: var(--space-3);
display: flex;
gap: var(--space-2);
}
.mode-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
backdrop-filter: blur(10px);
}
.mode-badge.wide-badge {
background: rgba(0,122,255,0.2);
color: #007AFF;
border: 1px solid rgba(0,122,255,0.3);
}
.mode-badge.narrow-badge {
background: rgba(255,149,0,0.2);
color: #FF9500;
border: 1px solid rgba(255,149,0,0.3);
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--text-tertiary); border-radius: 3px; }
</style>
</head>
<body data-theme="light">
<div class="page-layout">
<div class="control-panel">
<div class="control-title">📱 窄屏过渡</div>
<div class="control-subtitle">模拟从宽屏到窄屏的过渡动画效果,观察右侧面板的滑入滑出行为</div>
<div class="control-section">
<div class="control-label">设备旋转</div>
<button class="rotate-btn" onclick="rotateDevice()">
🔄 旋转设备
</button>
<div class="state-indicator">
<div class="state-dot" id="stateDot"></div>
<span class="state-text" id="stateText">宽屏模式 · 横向</span>
</div>
</div>
<div class="control-section">
<div class="control-label">动画速度</div>
<div class="speed-control">
<input class="speed-slider" type="range" min="200" max="1200" value="600" id="speedSlider" oninput="updateSpeed(this.value)">
<div class="speed-label">
<span>快 0.2s</span>
<span id="speedValue">0.6s</span>
<span>慢 1.2s</span>
</div>
</div>
</div>
<div class="control-section">
<div class="control-label">动画步骤</div>
<div class="animation-steps">
<div class="step-item active" id="step1">
<div class="step-number">1</div>
<span>检测屏幕宽度变化</span>
</div>
<div class="step-item" id="step2">
<div class="step-number">2</div>
<span>右侧面板开始滑出</span>
</div>
<div class="step-item" id="step3">
<div class="step-number">3</div>
<span>左侧面板扩展至全宽</span>
</div>
<div class="step-item" id="step4">
<div class="step-number">4</div>
<span>导航栏切换到底部</span>
</div>
<div class="step-item" id="step5">
<div class="step-number">5</div>
<span>过渡完成 · 窄屏模式</span>
</div>
</div>
</div>
<div class="control-section">
<div class="control-label">外观</div>
<div style="display:flex;gap:8px">
<button style="flex:1;padding:10px;border-radius:12px;border:2px solid var(--primary);background:var(--primary-light);color:var(--primary);font-family:var(--font-family);cursor:pointer;font-weight:600;font-size:13px" onclick="toggleTheme()">🌙 切换主题</button>
<button style="flex:1;padding:10px;border-radius:12px;border:1px solid var(--separator);background:var(--surface);color:var(--text-secondary);font-family:var(--font-family);cursor:pointer;font-size:13px" onclick="autoPlay()">▶️ 自动演示</button>
</div>
</div>
<div class="info-card">
<div class="info-card-title">📐 过渡规则</div>
<div class="info-card-text">
宽屏 → 窄屏:右侧面板向右滑出,左侧面板扩展,导航栏从左侧移至底部<br><br>
窄屏 → 宽屏:导航栏从底部移至左侧,右侧面板从右侧滑入<br><br>
触发条件:视口宽度 < 768px
</div>
</div>
</div>
<div class="preview-area">
<div class="mode-badges">
<div class="mode-badge wide-badge" id="wideBadge">🖥️ 宽屏 900×560</div>
<div class="mode-badge narrow-badge" id="narrowBadge" style="display:none">📱 窄屏 390×700</div>
</div>
<div class="device-container" id="deviceContainer">
<div class="device-frame wide" id="deviceFrame">
<div class="device-notch"></div>
<div class="device-screen">
<div class="nav-sidebar">
<div class="nav-logo">💬</div>
<a class="nav-item active">
<span class="nav-icon">💬</span>
<span class="nav-label">闲言</span>
</a>
<a class="nav-item">
<span class="nav-icon">🧭</span>
<span class="nav-label">发现</span>
</a>
<a class="nav-item">
<span class="nav-icon">👤</span>
<span class="nav-label">我的</span>
</a>
<div class="nav-spacer"></div>
</div>
<div class="content-body">
<div class="master-panel">
<div class="master-header">
<div class="master-title">闲言</div>
</div>
<div class="item-list">
<div class="list-item selected" onclick="showDetail(this)">
<div class="list-item-icon" style="background:linear-gradient(135deg,#007AFF,#5856D6)">🌙</div>
<div class="list-item-text">
<div class="list-item-title">长风破浪会有时</div>
<div class="list-item-subtitle">李白 · 3分钟前</div>
</div>
</div>
<div class="list-item" onclick="showDetail(this)">
<div class="list-item-icon" style="background:linear-gradient(135deg,#FF9500,#FF3B30)">🍂</div>
<div class="list-item-text">
<div class="list-item-title">一蓑烟雨任平生</div>
<div class="list-item-subtitle">苏轼 · 15分钟前</div>
</div>
</div>
<div class="list-item" onclick="showDetail(this)">
<div class="list-item-icon" style="background:linear-gradient(135deg,#34C759,#007AFF)">🌿</div>
<div class="list-item-text">
<div class="list-item-title">行到水穷处</div>
<div class="list-item-subtitle">王维 · 1小时前</div>
</div>
</div>
<div class="list-item" onclick="showDetail(this)">
<div class="list-item-icon" style="background:linear-gradient(135deg,#5856D6,#FF2D55)">🌸</div>
<div class="list-item-text">
<div class="list-item-title">寻寻觅觅</div>
<div class="list-item-subtitle">李清照 · 2小时前</div>
</div>
</div>
</div>
</div>
<div class="detail-panel" id="detailPanel">
<div class="detail-header">
<button class="back-btn" onclick="hideDetail()"></button>
<span class="detail-title">句子详情</span>
</div>
<div class="detail-content">
<div class="detail-sentence">长风破浪会有时,直挂云帆济沧海。人生不如意十之八九,但那又如何?风浪越大,我越强。</div>
<div class="detail-author">—— 李白《行路难》</div>
<div class="detail-actions">
<button class="action-btn">❤️ 2.3k</button>
<button class="action-btn">💬 186</button>
<button class="action-btn">🔗 分享</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="size-label" id="sizeLabel">900 × 560 · 宽屏模式</div>
</div>
</div>
</div>
<script>
let isWide = true;
let isAnimating = false;
let animDuration = 600;
function updateSpeed(val) {
animDuration = parseInt(val);
document.getElementById('speedValue').textContent = (animDuration / 1000).toFixed(1) + 's';
document.documentElement.style.setProperty('--transition-duration', animDuration + 'ms');
}
function rotateDevice() {
if (isAnimating) return;
isAnimating = true;
const frame = document.getElementById('deviceFrame');
const detailPanel = document.getElementById('detailPanel');
const steps = ['step1', 'step2', 'step3', 'step4', 'step5'];
steps.forEach(s => document.getElementById(s).classList.remove('active'));
document.getElementById('step1').classList.add('active');
if (isWide) {
setTimeout(() => document.getElementById('step2').classList.add('active'), animDuration * 0.2);
setTimeout(() => document.getElementById('step3').classList.add('active'), animDuration * 0.4);
setTimeout(() => document.getElementById('step4').classList.add('active'), animDuration * 0.6);
setTimeout(() => document.getElementById('step5').classList.add('active'), animDuration * 0.8);
frame.style.transition = `all ${animDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
frame.classList.remove('wide');
frame.classList.add('narrow');
document.getElementById('stateText').textContent = '窄屏模式 · 纵向';
document.getElementById('stateDot').style.background = 'var(--orange)';
document.getElementById('sizeLabel').textContent = '390 × 700 · 窄屏模式';
document.getElementById('wideBadge').style.display = 'none';
document.getElementById('narrowBadge').style.display = 'block';
isWide = false;
} else {
setTimeout(() => document.getElementById('step2').classList.add('active'), animDuration * 0.2);
setTimeout(() => document.getElementById('step3').classList.add('active'), animDuration * 0.4);
setTimeout(() => document.getElementById('step4').classList.add('active'), animDuration * 0.6);
setTimeout(() => document.getElementById('step5').classList.add('active'), animDuration * 0.8);
frame.style.transition = `all ${animDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
frame.classList.remove('narrow');
frame.classList.add('wide');
detailPanel.classList.remove('slide-in');
document.getElementById('stateText').textContent = '宽屏模式 · 横向';
document.getElementById('stateDot').style.background = 'var(--green)';
document.getElementById('sizeLabel').textContent = '900 × 560 · 宽屏模式';
document.getElementById('wideBadge').style.display = 'block';
document.getElementById('narrowBadge').style.display = 'none';
isWide = true;
}
setTimeout(() => {
isAnimating = false;
}, animDuration + 100);
}
function showDetail(el) {
document.querySelectorAll('.list-item').forEach(i => i.classList.remove('selected'));
el.classList.add('selected');
const frame = document.getElementById('deviceFrame');
if (frame.classList.contains('narrow')) {
const detailPanel = document.getElementById('detailPanel');
detailPanel.classList.add('slide-in');
}
}
function hideDetail() {
document.getElementById('detailPanel').classList.remove('slide-in');
}
function toggleTheme() {
const body = document.body;
const panel = document.querySelector('.control-panel');
if (body.dataset.theme === 'light') {
body.dataset.theme = 'dark';
panel.style.background = 'var(--surface)';
} else {
body.dataset.theme = 'light';
panel.style.background = 'var(--surface)';
}
}
let autoInterval = null;
function autoPlay() {
if (autoInterval) {
clearInterval(autoInterval);
autoInterval = null;
return;
}
rotateDevice();
autoInterval = setInterval(() => {
rotateDevice();
}, animDuration + 1500);
}
</script>
</body>
</html>