feat(leisure): 新增闲情逸致模块与多项功能优化

本次提交完成多项核心更新:
1. 新增闲情逸致功能模块,包含时间线、收藏标注、季节主题等基础框架
2. 替换hive为社区维护的hive_ce包,修复依赖兼容问题
3. 统一替换"开发中"提示为"当前设备不支持",优化用户提示文案
4. 新增多项功能开关与特性标志,统一管理不可用功能提示
5. 完善用户账户洞察系统,新增头像审核中状态检测
6. 优化TTS语音朗读服务,修复Android端引擎初始化问题
7. 重构知识图谱缩放手势逻辑,解决缩放不跟手问题
8. 新增精灵头像组件,替换默认聊天头像样式
9. 新增外部链接跳转确认弹窗,提升使用安全性
10. 升级后端API接口,新增签到配置获取与补签积分规则动态读取
11. 完善多语言翻译覆盖率限制,非中文语言仅显示最高50%进度
12. 新增HTTP缓存拦截器,优化网络请求性能
13. 新增恢复出厂设置选项,完善数据管理功能

同时修复了多处代码细节问题:简化字符串拼接、优化布局代码、移除多余代码等。
This commit is contained in:
Developer
2026-05-27 08:06:54 +08:00
parent c44457f94c
commit 355191aaf6
144 changed files with 23600 additions and 1464 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,714 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>闲情逸致 — 设置页面</title>
<style>
:root {
--primary: #6C63FF;
--primary-light: #8B83FF;
--accent: #4ECDC4;
--secondary: #FF6B6B;
--success: #10B981;
--warning: #F59E0B;
--error: #EF4444;
--info: #3B82F6;
--bg-primary: #FAFAFA;
--bg-secondary: #F5F5F5;
--bg-card: #FFFFFF;
--bg-elevated: #FFFFFF;
--text-primary: #1A1A2E;
--text-secondary: #6B7280;
--text-hint: #9CA3AF;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
.phone-frame {
max-width: 430px;
margin: 0 auto;
min-height: 100vh;
background: var(--bg-primary);
}
.appbar {
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: rgba(250,250,250,0.78);
border-bottom: 0.5px solid rgba(0,0,0,0.08);
padding: 12px var(--space-md);
display: flex;
align-items: center;
gap: 8px;
}
.appbar-back {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
border: none;
color: var(--text-primary);
}
.appbar-title {
font-size: 17px;
font-weight: 600;
}
.content {
padding: var(--space-md);
}
.section {
margin-bottom: var(--space-lg);
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-hint);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: var(--space-sm);
padding-left: var(--space-md);
}
.card-group {
background: var(--bg-card);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
border: 0.5px solid rgba(0,0,0,0.04);
}
.setting-row {
padding: 14px var(--space-md);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 0.5px solid rgba(0,0,0,0.04);
cursor: pointer;
transition: background 0.15s;
}
.setting-row:last-child {
border-bottom: none;
}
.setting-row:active {
background: rgba(0,0,0,0.03);
}
.setting-left {
display: flex;
align-items: center;
gap: 12px;
}
.setting-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.setting-icon.purple { background: rgba(108,99,255,0.12); }
.setting-icon.teal { background: rgba(78,205,196,0.12); }
.setting-icon.orange { background: rgba(245,158,11,0.12); }
.setting-icon.blue { background: rgba(59,130,246,0.12); }
.setting-icon.red { background: rgba(239,68,68,0.12); }
.setting-icon.green { background: rgba(16,185,129,0.12); }
.setting-icon.pink { background: rgba(255,107,107,0.12); }
.setting-icon.indigo { background: rgba(99,102,241,0.12); }
.setting-label {
font-size: 15px;
font-weight: 500;
}
.setting-desc {
font-size: 12px;
color: var(--text-hint);
margin-top: 2px;
}
.setting-right {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-hint);
font-size: 14px;
}
.setting-right .chevron {
font-size: 12px;
opacity: 0.4;
}
/* Choice Group (3-option selector) */
.choice-group {
padding: 10px var(--space-md);
border-bottom: 0.5px solid rgba(0,0,0,0.04);
}
.choice-group:last-child {
border-bottom: none;
}
.choice-label {
font-size: 15px;
font-weight: 500;
margin-bottom: 4px;
}
.choice-desc {
font-size: 12px;
color: var(--text-hint);
margin-bottom: 10px;
}
.choice-options {
display: flex;
gap: 8px;
}
.choice-option {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 6px;
border-radius: var(--radius-lg);
background: var(--bg-secondary);
cursor: pointer;
transition: all 0.2s;
border: 1.5px solid transparent;
}
.choice-option:active {
transform: scale(0.97);
}
.choice-option.selected {
background: rgba(108,99,255,0.08);
border-color: rgba(108,99,255,0.4);
}
.choice-option-icon {
font-size: 24px;
}
.choice-option-text {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
}
.choice-option.selected .choice-option-text {
color: var(--primary);
font-weight: 600;
}
.choice-preview {
width: 60px;
height: 40px;
border-radius: 6px;
overflow: hidden;
}
.preview-gradient {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4ECDC4, #44B09E);
}
.preview-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea, #764ba2);
}
.preview-minimal {
width: 100%;
height: 100%;
background: var(--bg-card);
border: 1px solid rgba(0,0,0,0.06);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: var(--text-hint);
}
/* Toggle Switch */
.toggle {
width: 51px;
height: 31px;
border-radius: 16px;
background: #E5E5EA;
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.toggle.active {
background: var(--primary);
}
.toggle::after {
content: '';
position: absolute;
width: 27px;
height: 27px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.toggle.active::after {
transform: translateX(20px);
}
/* Slider */
.slider-row {
padding: 14px var(--space-md);
border-bottom: 0.5px solid rgba(0,0,0,0.04);
}
.slider-label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.slider-label span:first-child {
font-size: 15px;
font-weight: 500;
}
.slider-label span:last-child {
font-size: 13px;
color: var(--primary);
font-weight: 600;
}
input[type="range"] {
width: 100%;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: var(--bg-secondary);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
box-shadow: 0 2px 6px rgba(108,99,255,0.3);
}
/* Submit Button */
.submit-section {
margin-top: var(--space-xl);
padding: 0 var(--space-md);
}
.submit-btn {
width: 100%;
padding: 14px;
border-radius: var(--radius-lg);
background: var(--primary);
color: white;
font-size: 16px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(108,99,255,0.3);
}
.submit-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.submit-note {
text-align: center;
font-size: 12px;
color: var(--text-hint);
margin-top: 8px;
line-height: 1.5;
}
/* Data Stats */
.data-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: var(--space-lg);
}
.stat-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
padding: var(--space-md);
text-align: center;
box-shadow: var(--shadow-sm);
border: 0.5px solid rgba(0,0,0,0.04);
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--primary);
}
.stat-label {
font-size: 11px;
color: var(--text-hint);
margin-top: 4px;
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1A1A2E;
--bg-secondary: #16213E;
--bg-card: #2D2D44;
--bg-elevated: #333355;
--text-primary: #E5E5E5;
--text-secondary: #9CA3AF;
--text-hint: #6B7280;
}
.appbar {
background: rgba(26,26,46,0.78);
border-bottom-color: rgba(255,255,255,0.06);
}
.toggle { background: #3A3A5C; }
}
</style>
</head>
<body>
<div class="phone-frame">
<div class="appbar">
<button class="appbar-back" onclick="window.location.href='leisure_timeline.html'"></button>
<div class="appbar-title">⚙️ 闲情逸致设置</div>
</div>
<div class="content">
<!-- Data Stats -->
<div class="data-stats">
<div class="stat-card">
<div class="stat-value">365</div>
<div class="stat-label">总节点</div>
</div>
<div class="stat-card">
<div class="stat-value">728</div>
<div class="stat-label">卡片数</div>
</div>
<div class="stat-card">
<div class="stat-value">12</div>
<div class="stat-label">已收藏</div>
</div>
</div>
<!-- Display Settings -->
<div class="section">
<div class="section-title">显示设置</div>
<div class="card-group">
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon purple">📐</div>
<div>
<div class="setting-label">卡片大小</div>
<div class="setting-desc">调整时间线卡片的显示大小</div>
</div>
</div>
<div class="setting-right">
<span>标准</span>
<span class="chevron"></span>
</div>
</div>
<div class="slider-row">
<div class="slider-label">
<span>卡片圆角</span>
<span>12px</span>
</div>
<input type="range" min="0" max="24" value="12">
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon teal">🎨</div>
<div>
<div class="setting-label">卡片样式</div>
<div class="setting-desc">精致卡片 / 扁平简约 / 图文卡片</div>
</div>
</div>
<div class="setting-right">
<span>精致卡片</span>
<span class="chevron"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon blue">🌓</div>
<div>
<div class="setting-label">显示季节标签</div>
<div class="setting-desc">在日期节点显示季节颜色标签</div>
</div>
</div>
<div class="setting-right">
<div class="toggle active" onclick="this.classList.toggle('active')"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon orange">🌅</div>
<div>
<div class="setting-label">显示日出日落</div>
<div class="setting-desc">在卡片中显示当日日出日落时间</div>
</div>
</div>
<div class="setting-right">
<div class="toggle active" onclick="this.classList.toggle('active')"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon red">⚠️</div>
<div>
<div class="setting-label">风险提示</div>
<div class="setting-desc">高海拔/边境/高危地区风险标注</div>
</div>
</div>
<div class="setting-right">
<div class="toggle active" onclick="this.classList.toggle('active')"></div>
</div>
</div>
</div>
</div>
<!-- Decision Options -->
<div class="section">
<div class="section-title">决策选项</div>
<div class="card-group">
<div class="choice-group">
<div class="choice-label">📤 分享卡片样式</div>
<div class="choice-desc">选择分享时生成的卡片风格</div>
<div class="choice-options">
<div class="choice-option selected" onclick="selectChoice(this)">
<div class="choice-preview"><div class="preview-gradient"></div></div>
<span class="choice-option-text">渐变头图</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<div class="choice-preview"><div class="preview-image"></div></div>
<span class="choice-option-text">图片卡片</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<div class="choice-preview"><div class="preview-minimal"></div></div>
<span class="choice-option-text">极简诗意</span>
</div>
</div>
</div>
<div class="choice-group">
<div class="choice-label">📍 默认打开位置</div>
<div class="choice-desc">打开时间线时默认定位到哪个位置</div>
<div class="choice-options">
<div class="choice-option selected" onclick="selectChoice(this)">
<span class="choice-option-icon">📅</span>
<span class="choice-option-text">今日</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">📊</span>
<span class="choice-option-text">最近有数据</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">🔖</span>
<span class="choice-option-text">上次浏览</span>
</div>
</div>
</div>
<div class="choice-group">
<div class="choice-label">📐 卡片信息密度</div>
<div class="choice-desc">控制卡片中显示的信息量</div>
<div class="choice-options">
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">压缩</span>
<span class="choice-option-text">紧凑</span>
</div>
<div class="choice-option selected" onclick="selectChoice(this)">
<span class="choice-option-icon">标准</span>
<span class="choice-option-text">标准</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">宽松</span>
<span class="choice-option-text">宽松</span>
</div>
</div>
</div>
<div class="choice-group">
<div class="choice-label">⚠️ 风险提示级别</div>
<div class="choice-desc">控制风险提示的显示范围</div>
<div class="choice-options">
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">🔴</span>
<span class="choice-option-text">仅高危</span>
</div>
<div class="choice-option selected" onclick="selectChoice(this)">
<span class="choice-option-icon">🟡</span>
<span class="choice-option-text">全部</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon"></span>
<span class="choice-option-text">关闭</span>
</div>
</div>
</div>
<div class="choice-group">
<div class="choice-label">🔄 数据更新频率</div>
<div class="choice-desc">控制时间线数据的自动更新策略</div>
<div class="choice-options">
<div class="choice-option selected" onclick="selectChoice(this)">
<span class="choice-option-icon">📲</span>
<span class="choice-option-text">每次打开</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon">📅</span>
<span class="choice-option-text">每日</span>
</div>
<div class="choice-option" onclick="selectChoice(this)">
<span class="choice-option-icon"></span>
<span class="choice-option-text">手动</span>
</div>
</div>
</div>
</div>
</div>
<!-- Data Settings -->
<div class="section">
<div class="section-title">数据管理</div>
<div class="card-group">
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon green">📥</div>
<div>
<div class="setting-label">更新数据</div>
<div class="setting-desc">从服务器获取最新花期/美食/景点数据</div>
</div>
</div>
<div class="setting-right">
<span class="chevron"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon blue">🗑️</div>
<div>
<div class="setting-label">清除缓存</div>
<div class="setting-desc">清除已缓存的图片和临时数据</div>
</div>
</div>
<div class="setting-right">
<span>2.3 MB</span>
<span class="chevron"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon purple">📤</div>
<div>
<div class="setting-label">导出数据</div>
<div class="setting-desc">导出收藏和标注数据为JSON</div>
</div>
</div>
<div class="setting-right">
<span class="chevron"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-left">
<div class="setting-icon orange">📊</div>
<div>
<div class="setting-label">数据源管理</div>
<div class="setting-desc">管理花期/美食/景点的数据来源</div>
</div>
</div>
<div class="setting-right">
<span>3 个源</span>
<span class="chevron"></span>
</div>
</div>
</div>
</div>
<!-- Submit Section -->
<div class="submit-section">
<button class="submit-btn" onclick="alert('提交收录功能即将上线,敬请期待 ✨')">
📮 提交收录
</button>
<div class="submit-note">
发现好的花期/美食/景点?提交收录后经审核可加入时间线<br>
此功能后续开发,敬请期待
</div>
</div>
</div>
</div>
<script>
function selectChoice(el) {
el.parentElement.querySelectorAll('.choice-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,476 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>闲情逸致 — 分享卡片</title>
<style>
:root {
--primary: #6C63FF;
--accent: #4ECDC4;
--secondary: #FF6B6B;
--success: #10B981;
--warning: #F59E0B;
--info: #3B82F6;
--bg-primary: #FAFAFA;
--bg-card: #FFFFFF;
--text-primary: #1A1A2E;
--text-secondary: #6B7280;
--text-hint: #9CA3AF;
--radius-lg: 12px;
--radius-xl: 16px;
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.page-title {
font-size: 17px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.page-desc {
font-size: 13px;
color: var(--text-hint);
margin-bottom: 24px;
text-align: center;
line-height: 1.5;
}
/* ===== Share Card Styles ===== */
.share-card {
width: 340px;
border-radius: 20px;
overflow: hidden;
box-shadow: var(--shadow-lg);
margin-bottom: 24px;
position: relative;
}
/* Style A: Gradient Header */
.share-card-a {
background: var(--bg-card);
}
.share-card-a .card-gradient {
height: 120px;
background: linear-gradient(135deg, #4ECDC4 0%, #44B09E 50%, #2D8B7A 100%);
padding: 20px;
display: flex;
flex-direction: column;
justify-content: flex-end;
position: relative;
overflow: hidden;
}
.share-card-a .card-gradient::after {
content: '';
position: absolute;
top: -20px;
right: -20px;
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
}
.share-card-a .gradient-date {
font-size: 32px;
font-weight: 700;
color: white;
line-height: 1;
}
.share-card-a .gradient-info {
font-size: 13px;
color: rgba(255,255,255,0.85);
margin-top: 4px;
}
.share-card-a .card-content {
padding: 16px 20px;
}
.share-card-a .card-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.share-card-a .card-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.share-card-a .card-meta {
display: flex;
gap: 12px;
margin-top: 12px;
font-size: 12px;
color: var(--text-hint);
}
.share-card-a .card-footer {
padding: 12px 20px;
border-top: 0.5px solid rgba(0,0,0,0.06);
display: flex;
justify-content: space-between;
align-items: center;
}
.share-card-a .card-brand {
font-size: 12px;
color: var(--text-hint);
display: flex;
align-items: center;
gap: 4px;
}
.share-card-a .card-brand-name {
font-weight: 600;
color: var(--primary);
}
/* Style B: Photo Card */
.share-card-b {
background: var(--bg-card);
}
.share-card-b .card-photo {
height: 180px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
position: relative;
}
.share-card-b .card-photo-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(transparent, rgba(0,0,0,0.4));
}
.share-card-b .card-photo-date {
position: absolute;
top: 16px;
right: 16px;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
padding: 4px 12px;
border-radius: 12px;
color: white;
font-size: 13px;
font-weight: 600;
}
.share-card-b .card-content {
padding: 16px 20px;
}
.share-card-b .card-season-badge {
display: inline-block;
font-size: 11px;
padding: 2px 10px;
border-radius: 10px;
background: rgba(78,205,196,0.12);
color: var(--accent);
font-weight: 600;
margin-bottom: 8px;
}
.share-card-b .card-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.share-card-b .card-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.share-card-b .card-tags {
display: flex;
gap: 6px;
margin-top: 10px;
flex-wrap: wrap;
}
.share-card-b .card-tag {
font-size: 11px;
padding: 3px 8px;
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-secondary);
}
.share-card-b .card-footer {
padding: 12px 20px;
border-top: 0.5px solid rgba(0,0,0,0.06);
display: flex;
justify-content: space-between;
align-items: center;
}
.share-card-b .card-brand {
font-size: 12px;
color: var(--text-hint);
display: flex;
align-items: center;
gap: 4px;
}
.share-card-b .card-brand-name {
font-weight: 600;
color: var(--primary);
}
/* Style C: Minimal */
.share-card-c {
background: var(--bg-card);
padding: 24px;
}
.share-card-c .card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.share-card-c .card-date-block {
text-align: center;
}
.share-card-c .card-date-day {
font-size: 42px;
font-weight: 800;
color: var(--primary);
line-height: 1;
}
.share-card-c .card-date-month {
font-size: 14px;
color: var(--text-hint);
font-weight: 500;
}
.share-card-c .card-season-icon {
font-size: 36px;
}
.share-card-c .card-divider {
height: 2px;
background: linear-gradient(to right, var(--primary), var(--accent));
border-radius: 1px;
margin-bottom: 16px;
}
.share-card-c .card-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
}
.share-card-c .card-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
.share-card-c .card-poem {
font-size: 13px;
color: var(--text-hint);
font-style: italic;
margin-top: 12px;
padding-left: 12px;
border-left: 2px solid var(--accent);
line-height: 1.6;
}
.share-card-c .card-footer {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.share-card-c .card-brand {
font-size: 12px;
color: var(--text-hint);
display: flex;
align-items: center;
gap: 4px;
}
.share-card-c .card-brand-name {
font-weight: 600;
color: var(--primary);
}
/* Action Buttons */
.action-bar {
display: flex;
gap: 12px;
margin-top: 8px;
width: 340px;
}
.action-btn {
flex: 1;
padding: 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.action-btn:active {
transform: scale(0.97);
}
.action-btn.primary {
background: var(--primary);
color: white;
box-shadow: 0 4px 12px rgba(108,99,255,0.3);
}
.action-btn.secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid rgba(0,0,0,0.08);
}
.style-label {
font-size: 13px;
color: var(--text-hint);
font-weight: 500;
margin-bottom: 8px;
margin-top: 12px;
width: 340px;
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1A1A2E;
--bg-card: #2D2D44;
--text-primary: #E5E5E5;
--text-secondary: #9CA3AF;
--text-hint: #6B7280;
}
}
</style>
</head>
<body>
<div class="page-title">📤 分享卡片预览</div>
<div class="page-desc">点击卡片上的「分享」按钮后,生成以下样式的卡片<br>呼出系统分享面板</div>
<!-- Style A -->
<div class="style-label">方案A: 渐变头图卡片</div>
<div class="share-card share-card-a">
<div class="card-gradient">
<div class="gradient-date">5月27日</div>
<div class="gradient-info">星期三 · 夏 · ☀️</div>
</div>
<div class="card-content">
<div class="card-title">🌸 杨梅季</div>
<div class="card-desc">仙居杨梅·东魁·荸荠种,酸甜多汁,初夏限定美味</div>
<div class="card-meta">
<span>📍 浙江·仙居</span>
<span>🌅 05:12</span>
<span>🌇 19:02</span>
</div>
</div>
<div class="card-footer">
<div class="card-brand">🌸 <span class="card-brand-name">闲情逸致</span></div>
<div style="font-size:11px;color:var(--text-hint)">闲时与你立黄昏</div>
</div>
</div>
<!-- Style B -->
<div class="style-label">方案B: 图片卡片</div>
<div class="share-card share-card-b">
<div class="card-photo">
🏔️
<div class="card-photo-overlay"></div>
<div class="card-photo-date">5月28日 星期四</div>
</div>
<div class="card-content">
<div class="card-season-badge">☀️ 夏季</div>
<div class="card-title">纳木错观星 ✨</div>
<div class="card-desc">银河拱桥·星空露营·日出金山,世界屋脊上的星空盛宴</div>
<div class="card-tags">
<span class="card-tag">🏔️ 海拔4718m</span>
<span class="card-tag">⚠️ 高原反应</span>
<span class="card-tag">🌌 观星</span>
</div>
</div>
<div class="card-footer">
<div class="card-brand">🌸 <span class="card-brand-name">闲情逸致</span></div>
<div style="font-size:11px;color:var(--text-hint)">灶前笑问粥可温</div>
</div>
</div>
<!-- Style C -->
<div class="style-label">方案C: 极简诗意卡片</div>
<div class="share-card share-card-c">
<div class="card-top">
<div class="card-date-block">
<div class="card-date-day">27</div>
<div class="card-date-month">5月·星期三</div>
</div>
<div class="card-season-icon">☀️</div>
</div>
<div class="card-divider"></div>
<div class="card-title">杨梅季 🍇</div>
<div class="card-desc">仙居杨梅·东魁·荸荠种<br>📍 浙江·仙居 · 🌅 05:12 · 🌇 19:02</div>
<div class="card-poem">
「日啖荔枝三百颗,不辞长作岭南人」<br>
—— 苏轼
</div>
<div class="card-footer">
<div class="card-brand">🌸 <span class="card-brand-name">闲情逸致</span></div>
<div style="font-size:11px;color:var(--text-hint)">闲时与你立黄昏</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-bar">
<button class="action-btn secondary" onclick="window.location.href='leisure_timeline.html'">返回</button>
<button class="action-btn primary" onclick="alert('呼出系统分享')">📤 分享卡片</button>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
# 闲情逸致 — 设计文档
> 创建时间: 2026-05-27
> 状态: 待确认
---
## 一、功能概述
**名称**: 闲情逸致
**描述**: 闲时与你立黄昏,灶前笑问粥可温
**入口**: InspirationPage灵感页会话列表新增会话条目
**核心**: 时间线布局,每日节点,左吃右玩,季节性花期/美食/景点推荐
---
## 二、页面结构
### 2.1 入口 — InspirationPage 会话条目
`ChatSessionNotifier._buildSessions()` 中新增系统会话:
```
id: 'leisure'
type: ChatSessionType.custom
emoji: '🌸'
name: '闲情逸致'
lastMessage: '闲时与你立黄昏,灶前笑问粥可温'
tag: 'NEW'
route: '/leisure'
```
### 2.2 主页面 — LeisureTimelinePage
**路由**: `/leisure`
**布局**: CupertinoPageScaffold + CustomScrollView
```
┌─────────────────────────────┐
│ AppBar │
│ ← 🌸 闲情逸致 🔍 ⚙️ │
│ 闲时与你立黄昏... │
├─────────────────────────────┤
│ [⇄] 固定交换按钮 │
├─────────────────────────────┤
│ │ │
│ ●────┼────● │
│ 日期 │ 节点 │
│ │ │
│ ┌─────┼─────┐ │
│ │ 吃 │ 玩 │ │
│ │ 卡片 │ 卡片 │ │
│ └─────┼─────┘ │
│ │ │
│ ═══ 今天 · 5月27日 ═══ │
│ │ │
│ ●今日节点 │
│ ┌─────┼─────┐ │
│ │ 吃 │ 玩 │ │
│ └─────┼─────┘ │
│ ┌─────┼─────┐ │
│ │ 吃 │ 玩 │ │
│ └─────┼─────┘ │
│ │ │
├─────────────────────────────┤
│ 底部标注筛选栏 │
│ 🌸花期 🦞美食 🏔️高海拔 ... │
└─────────────────────────────┘
```
**组件拆分**:
- `LeisureTimelinePage` — 主页面
- `LeisureTimelineAppBar` — 顶部导航栏
- `LeisureSwapButton` — 固定交换按钮
- `LeisureDateNode` — 日期节点组件
- `LeisureTodayDivider` — 今日分割线
- `LeisureCardRow` — 卡片行(左吃右玩)
- `LeisureCard` — 单张卡片
- `LeisureBottomFilterBar` — 底部筛选栏
- `LeisureSearchFab` — 外部搜索浮动按钮
### 2.3 设置页面 — LeisureSettingsPage
**路由**: `/leisure/settings`
```
┌─────────────────────────────┐
│ ← ⚙️ 闲情逸致设置 │
├─────────────────────────────┤
│ 数据统计卡片 (3列) │
│ 总节点 | 卡片数 | 已收藏 │
├─────────────────────────────┤
│ 显示设置 │
│ · 卡片大小 │
│ · 卡片圆角 (Slider) │
│ · 卡片样式 │
│ · 显示季节标签 (Toggle) │
│ · 显示日出日落 (Toggle) │
│ · 风险提示 (Toggle) │
├─────────────────────────────┤
│ 数据管理 │
│ · 更新数据 │
│ · 清除缓存 │
│ · 导出数据 │
│ · 数据源管理 │
├─────────────────────────────┤
│ [📮 提交收录] │
│ 此功能后续开发 │
└─────────────────────────────┘
```
### 2.4 卡片详情 — LeisureCardDetailSheet
点击时间线卡片后弹出可拖拽Sheet:
- 初始1/3屏可上拖至90%
- 顶部拖拽手柄 + 毛玻璃背景
- Hero区: 渐变背景 + 大emoji + 标题 + 季节标签
- 信息网格: 地点、海拔、日出日落、价格类型
- 风险提示横幅
- 操作按钮: 收藏笔记、分享卡片、外部搜索、添加标注
- 多选标注区: checkbox勾选后显示在卡片上
- 外部搜索: 百度/高德/大众点评/小红书/抖音
- 相关推荐: 同季节/同地区的其他卡片
### 2.5 分享卡片 — LeisureShareSheet
点击卡片「分享」按钮后:
1. 生成精美卡片图片3种样式可选
2. 呼出系统分享面板
**卡片样式**:
- 方案A: 渐变头图卡片(默认)
- 方案B: 图片卡片
- 方案C: 极简诗意卡片
---
## 三、数据模型
### 3.1 时间线节点
```dart
class LeisureNode {
final String date; // "2026-05-27"
final int month;
final int day;
final String weekday; // "星期三"
final bool isWeekend;
final String season; // spring/summer/autumn/winter
final String? sunrise; // "05:12"
final String? sunset; // "19:12"
final LeisureCards cards;
}
class LeisureCards {
final List<LeisureCard> food;
final List<LeisureCard> play;
}
```
### 3.2 卡片
```dart
class LeisureCard {
final String id;
final String title;
final String emoji;
final String description;
final String location;
final String province;
final int? altitude; // 海拔(米)
final String priceType; // free/paid/commercial/unknown
final String? priceNote;
final List<String> tags;
final List<String> riskWarnings;
final bool isFood; // true=吃, false=玩
}
```
### 3.3 用户标注
```dart
class LeisureBookmark {
final String cardId;
final bool isBookmarked;
final List<String> checkedItems; // 多选标注
final String? noteId; // 关联笔记ID
}
```
### 3.4 设置
```dart
class LeisureSettings {
final String cardSize; // small/standard/large
final double cardRadius; // 4-24
final String cardStyle; // exquisite/flat/photo
final bool showSeasonLabel;
final bool showSunInfo;
final bool showRiskWarning;
final bool isSwapped; // 左右是否交换
final List<String> activeFilters; // 激活的筛选标签
final String shareCardStyle; // gradient/photo/minimal
final String defaultOpenPosition; // today/nearest/lastViewed
final String infoDensity; // compact/standard/spacious
final String riskLevel; // highOnly/all/off
final String updateFrequency; // always/daily/manual
}
```
---
## 四、状态管理
### Provider 设计
```dart
// 时间线数据
leisureTimelineProvider NotifierProvider<LeisureTimelineNotifier, LeisureTimelineState>
// 设置
leisureSettingsProvider NotifierProvider<LeisureSettingsNotifier, LeisureSettingsState>
// 收藏/标注
leisureBookmarkProvider NotifierProvider<LeisureBookmarkNotifier, LeisureBookmarkState>
```
### 数据流
```
JSON本地文件 → LeisureTimelineService.parse() → LeisureTimelineNotifier
LeisureTimelineState
LeisureTimelinePage (ConsumerWidget)
```
---
## 五、关键交互逻辑
### 5.1 打开页面定位今日
每次打开时间线页面,自动滚动到今日节点位置。使用 `ScrollController` + `GlobalKey` 定位。
### 5.2 交换左右位置
顶部固定按钮,点击后交换吃/玩卡片位置。通过 `isSwapped` 状态控制 `FlexDirection`
### 5.3 收藏到笔记
点击卡片「收藏」按钮 → 写入 `LeisureBookmark` → 可选关联到笔记模块。
### 5.4 分享卡片
点击「分享」→ 渲染 `RepaintBoundary` 生成图片 → 呼出 `Share.shareFiles()`
### 5.5 外部搜索
点击搜索按钮 → 弹出 BottomSheet → 列出可跳转的应用(百度/高德/大众点评/小红书等)。
### 5.6 底部多选标注
底部筛选栏勾选后,卡片内显示对应勾选项目。勾选状态持久化到本地。
### 5.7 风险提示
云南边境、西部新疆/西藏/内蒙古等地区自动标注:
- 高海拔 (>3000m): 🔴 高海拔·高原反应
- 边境地区: ⚠️ 边境地区·注意安全
- 极寒地区: ❄️ 极寒·防寒装备
---
## 六、主题适配
### 6.1 动态主题
所有颜色使用 `AppTheme.ext(context)` 令牌,不硬编码。
### 6.2 季节色
| 季节 | 颜色 | 使用场景 |
|------|------|---------|
| 春 | #4ECDC4 (teal) | 日期节点、季节标签 |
| 夏 | #FF6B6B (red) | 日期节点、季节标签 |
| 秋 | #F59E0B (amber) | 日期节点、季节标签 |
| 冬 | #3B82F6 (blue) | 日期节点、季节标签 |
### 6.3 周末强调色
周六/周日的星期文字使用 `ext.accent` 强调色。
### 6.4 动态圆角
卡片圆角跟随 `AppRadius.of(context)` 动态调整。
---
## 七、文件结构
```
features/tool_center/leisure/
├── models/
│ ├── leisure_node.dart # 时间线节点模型
│ ├── leisure_card.dart # 卡片模型
│ └── leisure_settings.dart # 设置模型
├── presentation/
│ ├── pages/
│ │ ├── leisure_timeline_page.dart # 主时间线页面
│ │ └── leisure_settings_page.dart # 设置页面
│ └── widgets/
│ ├── leisure_app_bar.dart # 顶部导航栏
│ ├── leisure_swap_button.dart # 交换按钮
│ ├── leisure_date_node.dart # 日期节点
│ ├── leisure_today_divider.dart # 今日分割线
│ ├── leisure_card_row.dart # 卡片行
│ ├── leisure_card.dart # 单张卡片
│ ├── leisure_bottom_filter.dart # 底部筛选栏
│ ├── leisure_search_fab.dart # 外部搜索FAB
│ └── leisure_share_sheet.dart # 分享卡片Sheet
├── providers/
│ ├── leisure_timeline_provider.dart # 时间线状态
│ ├── leisure_settings_provider.dart # 设置状态
│ └── leisure_bookmark_provider.dart # 收藏/标注状态
└── services/
└── leisure_data_service.dart # JSON解析+数据服务
```
---
## 八、路由注册
`app_routes.dart` 新增:
```dart
static const String leisure = '/leisure';
static const String leisureSettings = '/leisure/settings';
```
`content_routes.dart` 或新建 `leisure_routes.dart` 注册路由。
---
## 九、数据来源
数据架构: 按月拆分 + 增量合并 + Supabase服务器推送
本地数据:
- assets/data/leisure/index.json — 索引文件(可用年月、季节颜色、风险等级等)
- assets/data/leisure/2026_01.json ~ 2026_12.json — 12个月份数据文件
- assets/data/leisure/patch_*.json — 增量补丁文件
增量机制:
- patch文件支持 add新增节点和 update更新卡片字段操作
- 服务器通过Supabase推送增量JSON客户端合并到本地月度文件
- 支持多年复用2027年数据只需创建 2027_01.json ~ 2027_12.json
当前数据:
- 67个核心节点覆盖全年花期/美食/景点/观星/观鸥/温泉/冰雪
- 2个增量补丁示例文件
---
## 十、待确认事项
1. ✅ 时间线布局: 中轴线对称
2. ✅ 卡片风格: 精致卡片(圆角+阴影+毛玻璃)
3. ✅ 数据来源: 预置JSON
4. ❓ 分享卡片样式偏好: A渐变 / B图片 / C极简
5. ❓ 外部搜索支持哪些应用?
6. ❓ 是否需要离线缓存支持?

View File

@@ -0,0 +1,156 @@
# 闲情逸致 — 开发文档 & 归档清单
> 创建时间: 2026-05-27
> 状态: 开发中
> 负责人: AI Coder
> 审计要求: 每完成一项必须更新状态,禁止偷工减料
---
## 一、归档文件清单
### 1.1 HTML 原型文件4个
| 文件 | 路径 | 行数 | 状态 | 对应Flutter页面 |
|------|------|------|------|----------------|
| 主时间线 | `docs/mockups/leisure_timeline.html` | 1371 | ✅ 已确认 | LeisureTimelinePage |
| 设置页面 | `docs/mockups/leisure_settings.html` | 714 | ✅ 已确认 | LeisureSettingsPage |
| 分享卡片 | `docs/mockups/leisure_share_card.html` | 476 | ✅ 已确认 | LeisureShareSheet |
| 卡片详情Sheet | `docs/mockups/leisure_card_detail.html` | 1518 | ✅ 已确认 | LeisureCardDetailSheet |
### 1.2 JSON 数据文件15个
| 文件 | 路径 | 节点数 | 状态 |
|------|------|--------|------|
| 索引文件 | `assets/data/leisure/index.json` | - | ✅ |
| 1月数据 | `assets/data/leisure/2026_01.json` | 4 | ✅ |
| 2月数据 | `assets/data/leisure/2026_02.json` | 5 | ✅ |
| 3月数据 | `assets/data/leisure/2026_03.json` | 5 | ✅ |
| 4月数据 | `assets/data/leisure/2026_04.json` | 5 | ✅ |
| 5月数据 | `assets/data/leisure/2026_05.json` | 6 | ✅ |
| 6月数据 | `assets/data/leisure/2026_06.json` | 5 | ✅ |
| 7月数据 | `assets/data/leisure/2026_07.json` | 6 | ✅ |
| 8月数据 | `assets/data/leisure/2026_08.json` | 5 | ✅ |
| 9月数据 | `assets/data/leisure/2026_09.json` | 6 | ✅ |
| 10月数据 | `assets/data/leisure/2026_10.json` | 6 | ✅ |
| 11月数据 | `assets/data/leisure/2026_11.json` | 6 | ✅ |
| 12月数据 | `assets/data/leisure/2026_12.json` | 7 | ✅ |
| 增量补丁示例 | `assets/data/leisure/patch_2026_05_27.json` | - | ✅ |
| 服务器推送示例 | `assets/data/leisure/patch_2026_06_server.json` | - | ✅ |
### 1.3 设计文档1个
| 文件 | 路径 | 状态 |
|------|------|------|
| 设计文档 | `docs/superpowers/specs/2026-05-27-leisure-design.md` | ✅ |
---
## 二、开发任务清单
### Phase 1: 数据层
| # | 任务 | 文件 | 验收标准 | 状态 |
|---|------|------|---------|------|
| 1.1 | LeisureNode 模型 | `models/leisure_node.dart` | freezed模型含date/month/day/weekday/isWeekend/season/sunrise/sunset/cards | ⬜ |
| 1.2 | LeisureCard 模型 | `models/leisure_card.dart` | freezed模型含id/title/emoji/description/location/province/altitude/priceType/tags/riskWarnings/isFood | ⬜ |
| 1.3 | LeisureSettings 模型 | `models/leisure_settings.dart` | 含cardSize/cardRadius/cardStyle/showSeasonLabel/showSunInfo/showRiskWarning/isSwapped/activeFilters/shareCardStyle/defaultOpenPosition/infoDensity/riskLevel/updateFrequency | ⬜ |
| 1.4 | LeisureBookmark 模型 | `models/leisure_bookmark.dart` | 含cardId/isBookmarked/checkedItems/noteId | ⬜ |
| 1.5 | LeisureIndex 模型 | `models/leisure_index.dart` | 含availableYears/availableMonths/seasonColors/riskLevels/priceTypes/serverSync | ⬜ |
| 1.6 | LeisurePatch 模型 | `models/leisure_patch.dart` | 含type/operations(op=add/update)/targetYear/targetMonth | ⬜ |
| 1.7 | 数据服务 | `services/leisure_data_service.dart` | 加载index.json+月度JSON+增量合并+Supabase拉取 | ⬜ |
### Phase 2: 状态层
| # | 任务 | 文件 | 验收标准 | 状态 |
|---|------|------|---------|------|
| 2.1 | 时间线Provider | `providers/leisure_timeline_provider.dart` | NotifierProvider加载月度数据支持按月懒加载 | ⬜ |
| 2.2 | 设置Provider | `providers/leisure_settings_provider.dart` | NotifierProvider持久化到KvStorage | ⬜ |
| 2.3 | 收藏Provider | `providers/leisure_bookmark_provider.dart` | NotifierProvider持久化到Hive | ⬜ |
### Phase 3: 页面层
| # | 任务 | 文件 | 验收标准 | 状态 |
|---|------|------|---------|------|
| 3.1 | 主时间线页面 | `pages/leisure_timeline_page.dart` | CupertinoPageScaffold+CustomScrollView自动滚动到今日 | ⬜ |
| 3.2 | AppBar组件 | `widgets/leisure_app_bar.dart` | 返回按钮+标题+副标题+搜索按钮+设置按钮 | ⬜ |
| 3.3 | 交换按钮 | `widgets/leisure_swap_button.dart` | 固定位置,点击交换左右卡片 | ⬜ |
| 3.4 | 日期节点 | `widgets/leisure_date_node.dart` | 中轴线圆点+日期+星期+季节标签,今日高亮脉冲 | ⬜ |
| 3.5 | 今日分割线 | `widgets/leisure_today_divider.dart` | 渐变线+今日文字 | ⬜ |
| 3.6 | 卡片行 | `widgets/leisure_card_row.dart` | 左吃右玩双列,支持交换 | ⬜ |
| 3.7 | 单张卡片 | `widgets/leisure_card.dart` | 精致卡片风格,圆角+阴影+季节色+风险提示+价格标签+操作按钮 | ⬜ |
| 3.8 | 底部筛选栏 | `widgets/leisure_bottom_filter.dart` | 多选Chip筛选标注 | ⬜ |
| 3.9 | 外部搜索FAB | `widgets/leisure_search_fab.dart` | 浮动按钮,弹出搜索面板 | ⬜ |
| 3.10 | 卡片详情Sheet | `widgets/leisure_card_detail_sheet.dart` | 可拖拽Sheet1/3→90%Hero区+信息网格+操作按钮+标注+外部搜索+相关推荐 | ⬜ |
| 3.11 | 设置页面 | `pages/leisure_settings_page.dart` | 统计卡片+显示设置+数据管理+提交收录 | ⬜ |
| 3.12 | 分享卡片 | `widgets/leisure_share_sheet.dart` | 渐变头图卡片+系统分享 | ⬜ |
### Phase 4: 集成层
| # | 任务 | 文件 | 验收标准 | 状态 |
|---|------|------|---------|------|
| 4.1 | 路由注册 | `core/router/app_routes.dart` + `content_routes.dart` | /leisure + /leisure/settings | ⬜ |
| 4.2 | 入口会话 | `chat_session_provider.dart` | 新增leisure系统会话条目 | ⬜ |
| 4.3 | Asset注册 | `pubspec.yaml` | assets/data/leisure/ 目录 | ⬜ |
| 4.4 | CHANGELOG | `CHANGELOG.md` | 记录功能新增 | ⬜ |
---
## 三、验收标准
### 3.1 功能验收
| 功能 | 验收方法 | 状态 |
|------|---------|------|
| 灵感页新增"闲情逸致"会话条目 | 可见且可点击 | ⬜ |
| 点击跳转到时间线页面 | 路由正确跳转 | ⬜ |
| 时间线显示今日+明天+后天节点 | 自动定位到今日 | ⬜ |
| 今日分割线 | 可见且样式正确 | ⬜ |
| 左吃右玩双列卡片 | 布局正确 | ⬜ |
| 交换按钮切换左右位置 | 功能正常 | ⬜ |
| 周末星期显示强调色 | 颜色正确 | ⬜ |
| 季节色标签 | 每季不同颜色 | ⬜ |
| 风险提示 | 高海拔/边境/极寒正确标注 | ⬜ |
| 价格标签 | 免费/付费/商业化/未知 | ⬜ |
| 卡片点击弹出详情Sheet | 可拖拽Sheet | ⬜ |
| 收藏到笔记 | 功能正常 | ⬜ |
| 分享卡片 | 生成图片+系统分享 | ⬜ |
| 外部搜索 | 跳转百度/高德/大众点评等 | ⬜ |
| 底部多选标注 | 勾选后卡片内显示 | ⬜ |
| 设置页面 | 所有选项可用 | ⬜ |
| 动态主题 | 日间/夜间/AMOLED正确 | ⬜ |
| 动态圆角 | 跟随系统设置 | ⬜ |
| 增量数据合并 | patch文件正确合并 | ⬜ |
| 服务器数据拉取 | Supabase拉取+合并 | ⬜ |
### 3.2 代码质量验收
| 检查项 | 标准 | 状态 |
|--------|------|------|
| 空指针安全 | 所有可空字段做空判断 | ⬜ |
| 主题令牌 | 无硬编码颜色/圆角/间距 | ⬜ |
| 文件头注释 | 创建时间/更新时间/名称/作用 | ⬜ |
| 方法注释 | 关键方法有注释 | ⬜ |
| iOS风格 | 优先Cupertino组件 | ⬜ |
| 文件行数 | 不超过1000行 | ⬜ |
| flutter analyze | 无error | ⬜ |
---
## 四、开发日志
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 2026-05-27 | 设计阶段完成 | ✅ | HTML原型+JSON数据+设计文档 |
| 2026-05-27 | 开发文档创建 | ✅ | 本文档 |
| 2026-05-27 | Phase 1 数据层 | 🔄 | 进行中 |
---
## 五、审计检查点
- [ ] Phase 1 完成后审计: 模型是否完整服务是否可加载JSON
- [ ] Phase 2 完成后审计: Provider是否正确管理状态
- [ ] Phase 3 完成后审计: 页面是否与HTML原型一致
- [ ] Phase 4 完成后审计: 集成是否完整,路由是否正确
- [ ] 最终审计: 全部验收标准通过

View File

@@ -14,7 +14,7 @@ use think\Config;
*/
class UserCenter extends Api
{
protected $noNeedLogin = ['public_profile'];
protected $noNeedLogin = ['public_profile', 'signin_config'];
protected $noNeedRight = '*';
private static $secQuestions = [
@@ -402,6 +402,17 @@ class UserCenter extends Api
]);
}
/**
* @name 签到配置
* @desc 获取补签消耗积分等配置信息
*/
public function signin_config()
{
$makeupRule = db('coin_rule')->where('action', 'signin_makeup')->where('status', 'normal')->find();
$makeupCost = $makeupRule ? intval($makeupRule['amount']) : 10;
$this->success('获取成功', ['makeup_cost' => $makeupCost]);
}
/**
* @name 补签
* @desc 消耗积分补签过去30天内的日期
@@ -428,7 +439,8 @@ class UserCenter extends Api
$this->error('该日期已签到');
}
$makeupCost = 10;
$makeupRule = db('coin_rule')->where('action', 'signin_makeup')->where('status', 'normal')->find();
$makeupCost = $makeupRule ? intval($makeupRule['amount']) : 10;
$user = db('user')->where('id', $userId)->find();
if ($user['score'] < $makeupCost) {
$this->error('积分不足,补签需要' . $makeupCost . '积分');

View File

@@ -15,7 +15,7 @@ use think\Validate;
* @time 2026-04-29
* @name UserSecurity
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码/回执登录/二维码登录/密保问题等
* @lastUpdate v10.1.0 新增密保问题(secQuestion/changeSecQuestion); changepwd/changeemail/changemobile支持多验证方式; register支持可选密保
* @lastUpdate v10.2.0 register注册赠送50积分+50金币; 补签积分不足提醒增强
*/
class UserSecurity extends Api
{
@@ -381,8 +381,8 @@ class UserSecurity extends Api
/**
* @name 用户注册
* @desc 注册新用户,需回执验证(客户端已验证邮箱),可选填密保问题
* @lastUpdate v10.1.0 新增可选参数sec_question/sec_answer
* @desc 注册新用户,需回执验证(客户端已验证邮箱),可选填密保问题注册赠送50积分+50金币
* @lastUpdate v10.2.0 新增注册赠送50积分+50金币
*/
public function register()
{
@@ -443,6 +443,40 @@ class UserSecurity extends Api
$verification = $verification ? json_decode($verification, true) : [];
$verification['email'] = 1;
db('user')->where('id', $userId)->update(['verification' => json_encode($verification)]);
// 注册赠送50积分和50金币
$registerScore = 50;
$registerGold = 50;
$userBefore = db('user')->where('id', $userId)->find();
$scoreBefore = isset($userBefore['score']) ? intval($userBefore['score']) : 0;
$goldBefore = isset($userBefore['gold']) ? intval($userBefore['gold']) : 0;
db('user')->where('id', $userId)->setInc('score', $registerScore);
db('user')->where('id', $userId)->setInc('gold', $registerGold);
try {
db('coin_log')->insert([
'user_id' => $userId,
'coin_type' => 'score',
'amount' => $registerScore,
'before' => $scoreBefore,
'after' => $scoreBefore + $registerScore,
'action' => 'register_reward',
'remark' => '新用户注册赠送积分',
'createtime' => time(),
]);
} catch (\Exception $e) {}
try {
db('coin_log')->insert([
'user_id' => $userId,
'coin_type' => 'gold',
'amount' => $registerGold,
'before' => $goldBefore,
'after' => $goldBefore + $registerGold,
'action' => 'register_reward',
'remark' => '新用户注册赠送金币',
'createtime' => time(),
]);
} catch (\Exception $e) {}
$token = $this->auth->getToken();
$data = ['userinfo' => $this->auth->getUserinfo(), 'token' => $token];
$this->success(__('Sign up successful'), $data);

View File

@@ -1,8 +1,8 @@
# 闲言APP — 深度链接Deep Link技术文档
> 创建时间: 2026-05-23
> 更新时间: 2026-05-23
> 版本: v1.0
> 更新时间: 2026-05-27
> 版本: v2.0
---
@@ -16,6 +16,15 @@
| Universal Link | `https://xianyan.app/path` | iOS 无缝跳转(不经过浏览器确认) |
| App Link | `https://xianyan.app/path` | Android 无缝跳转(需验证域名) |
### 1.1 app_links 库集成
v2.0 架构引入 [`app_links`](https://pub.dev/packages/app_links) 库,统一处理 Universal LinkiOS和 App LinkAndroid的自动捕获
- **自动监听**:无需手动注册 `Intent``NSUserActivity` 回调,`app_links` 自动接收系统分发的链接
- **冷启动支持**:通过 `getInitialLink()` 获取应用启动时携带的链接
- **热恢复支持**:通过 `uriLinkStream` 监听应用运行期间收到的链接
- **平台兼容**iOS / Android / macOS / Windows / Linux 全平台支持
---
## 二、URL 格式与参数说明
@@ -29,7 +38,7 @@ https://xianyan.app/<path>[?<query_params>]
### 2.2 支持的路径映射
以下为 `_resolveDeepLinkPath()` 中注册的所有深链接路径:
以下为 `AppRouter.resolveDeepLinkUri()` 中注册的所有深链接路径:
| 深链接路径 | 应用内路由 | 说明 |
|-----------|-----------|------|
@@ -179,30 +188,137 @@ OhosNavBridge.push(context, path);
---
## 五、路由重定向流程
## 五、DeepLinkService 架构说明
### 5.1 架构总览
v2.0 将深度链接处理拆分为两层:
```
外部 URL
┌─────────────────────────────────────────────────────────┐
│ 外部链接来源 │
│ (推送通知 / 浏览器 / 分享 / 其他应用) │
└─────────────┬───────────────────────┬───────────────────┘
│ │
┌─────────▼─────────┐ ┌────────▼────────┐
│ GoRouter redirect │ │ app_links 库 │
│ (内部路由重定向) │ │ (系统链接捕获) │
└─────────┬─────────┘ └────────┬────────┘
│ │
│ ┌────────▼────────┐
│ │ DeepLinkService │
│ │ (统一入口服务) │
│ └────────┬────────┘
│ │
┌────────▼───────────────────────▼────────┐
│ AppRouter.resolveDeepLinkUri() │
│ (公共静态方法,统一路径映射) │
└────────────────────┬────────────────────┘
┌──────────▼──────────┐
│ GoRouter 导航执行 │
│ context.appGo() │
└─────────────────────┘
```
### 5.2 核心组件
| 组件 | 文件路径 | 职责 |
|------|---------|------|
| `DeepLinkService` | `lib/core/services/network/deep_link_service.dart` | 使用 app_links 捕获系统链接,调度导航 |
| `AppRouter.resolveDeepLinkUri()` | `lib/core/router/app_router.dart` | URI → 内部路由路径的统一映射(公共静态方法) |
| `_handleDeepLinkRedirect()` | `lib/core/router/app_router.dart` | GoRouter redirect 回调,处理内部路由重定向 |
### 5.3 初始化流程
`DeepLinkService.init()` 在应用启动时调用,执行以下步骤:
```
App 启动
├─ xianyan://fortune
│ └─ _handleDeepLinkRedirect()
│ └─ scheme == 'xianyan' → _resolveDeepLinkPath('/fortune')
│ └─ 返回 '/daily-fortune'
│ └─ GoRouter 导航到 DailyFortunePage
├─ 1. 创建 AppLinks 实例
├─ https://xianyan.app/article/42
│ └─ _handleDeepLinkRedirect()
│ └─ host == 'xianyan.app' → _resolveDeepLinkPath('/article/42')
│ └─ 返回 '/article/42'
│ └─ GoRouter 导航到 ArticleDetailPage(id: 42)
├─ 2. 冷启动处理getInitialLink()
│ └─ 获取 App 启动前收到的链接
│ └─ 如果存在 → _handleLink(uri)
└─ https://other.com/path
└─ host 不匹配 → 不处理(正常网页浏览)
└─ 3. 热恢复监听uriLinkStream.listen()
└─ App 运行期间收到新链接
└─ _handleLink(uri)
```
### 5.4 冷启动 vs 热恢复
| 场景 | 触发方式 | 处理方法 | 说明 |
|------|---------|---------|------|
| **冷启动** | 点击链接时 App 未运行 | `getInitialLink()` | App 被链接唤醒,启动后立即获取链接 |
| **热恢复** | 点击链接时 App 已在后台 | `uriLinkStream` | App 从后台恢复,通过流接收链接 |
### 5.5 延迟导航机制_pendingLink
`_handleLink()` 执行时,可能遇到 `rootNavigatorKey.currentContext` 不可用的情况(如 Widget 树尚未构建完成)。此时使用 `_pendingLink` 机制:
```
_handleLink(uri)
├─ context 可用且 mounted
│ └─ 立即导航: context.appGo(resolved)
└─ context 不可用或未挂载
└─ 暂存链接: _pendingLink = resolved
└─ 500ms 后重试: _schedulePendingNavigation()
├─ context 就绪 → 导航并清除 _pendingLink
└─ context 仍不可用 → 放弃(日志警告)
```
---
## 六、完整路由常量参考
## 六、路由重定向流程
### 6.1 外部链接处理流程DeepLinkService 路径)
```
外部 URL系统分发
├─ xianyan://fortune
│ └─ app_links 捕获
│ └─ DeepLinkService._handleLink(uri)
│ └─ AppRouter.resolveDeepLinkUri(uri)
│ └─ scheme == 'xianyan' → _resolveCustomScheme(uri)
│ └─ host == 'fortune' → 返回 '/daily-fortune'
│ └─ context.appGo('/daily-fortune')
│ └─ GoRouter 导航到 DailyFortunePage
├─ https://xianyan.app/article/42
│ └─ app_links 捕获
│ └─ DeepLinkService._handleLink(uri)
│ └─ AppRouter.resolveDeepLinkUri(uri)
│ └─ scheme == 'https' && host == 'xianyan.app'
│ └─ _resolveHttps(uri)
│ └─ segments[0] == 'article' → 返回 '/article/42'
│ └─ context.appGo('/article/42')
│ └─ GoRouter 导航到 ArticleDetailPage(id: 42)
└─ https://other.com/path
└─ host 不匹配 → resolveDeepLinkUri 返回 null → 不处理
```
### 6.2 内部路由重定向流程GoRouter redirect 路径)
```
GoRouter 导航请求
└─ _handleDeepLinkRedirect(context, state)
└─ AppRouter.resolveDeepLinkUri(uri)
└─ 解析成功且与当前路径不同 → 返回重定向路径
└─ GoRouter 自动导航到目标页面
```
> **说明**`AppRouter.resolveDeepLinkUri()` 是两条路径的统一入口,确保路径映射逻辑只维护一份。
---
## 七、完整路由常量参考
所有路由常量定义在 `lib/core/router/app_routes.dart``AppRoutes` 类中:
@@ -245,22 +361,35 @@ OhosNavBridge.push(context, path);
---
## 、可扩展功能
## 、可扩展功能
### 7.1 新增深链接路径
### 8.1 新增深链接路径
`_resolveDeepLinkPath()` 中添加新的 case 分支:
`AppRouter` 类的 `_resolveCustomScheme()``_resolveHttps()` 中添加新的 case 分支:
```dart
String? _resolveDeepLinkPath(String path) {
// ...
return switch (segments.first) {
// 在 _resolveCustomScheme() 中添加:
static String? _resolveCustomScheme(Uri uri) {
final host = uri.host;
return switch (host) {
'fortune' => AppRoutes.dailyFortune,
'article' => path,
// ↓ 新增深链接
'pomodoro' => AppRoutes.pomodoro,
'countdown' => AppRoutes.countdown,
_ => path,
_ => _resolvePathFallback(uri.path),
};
}
// 在 _resolveHttps() 中同步添加:
static String? _resolveHttps(Uri uri) {
final segments = uri.pathSegments;
final first = segments.first;
return switch (first) {
'fortune' => AppRoutes.dailyFortune,
// ↓ 新增深链接
'pomodoro' => AppRoutes.pomodoro,
'countdown' => AppRoutes.countdown,
_ => _resolvePathFallback('/${segments.join('/')}'),
};
}
```
@@ -268,10 +397,13 @@ String? _resolveDeepLinkPath(String path) {
同时在 `AppRoutes` 中添加常量:
```dart
static const String deepPomodoro = '/pomodoro';
static const String pomodoro = '/pomodoro';
static const String countdown = '/countdown';
```
### 7.2 新增鸿蒙端路由
> **重要**:由于 `AppRouter.resolveDeepLinkUri()` 是 `DeepLinkService` 和 `GoRouter redirect` 的统一入口,只需在此处添加映射,两条路径均自动生效。
### 8.2 新增鸿蒙端路由
`OhosNavBridge._routes` 中添加注册条目:
@@ -284,11 +416,12 @@ OhosRouteEntry(
> ⚠️ **同步提醒**:新增路由时,必须同时更新以下文件:
> 1. `app_routes.dart` — 路由路径常量
> 2. 对应的模块路由文件settings_routes / tool_routes / editor_router 等)
> 3. `ohos_nav_bridge.dart` — 鸿蒙端路由注册表
> 4. `CHANGELOG.md` — 变更日志
> 2. `app_router.dart` — `AppRouter._resolveCustomScheme()` 和 `_resolveHttps()` 映射
> 3. 对应的模块路由文件settings_routes / tool_routes / editor_router 等)
> 4. `ohos_nav_bridge.dart` — 鸿蒙端路由注册表
> 5. `CHANGELOG.md` — 变更日志
### 7.3 自定义中间件
### 8.3 自定义中间件
可扩展 `OhosNavMiddleware` 创建新的路由守卫:
@@ -308,7 +441,7 @@ class FeatureFlagMiddleware extends OhosNavMiddleware {
}
```
### 7.4 延迟加载路由
### 8.4 延迟加载路由
对于大型页面,可使用懒加载减少启动时间:
@@ -320,26 +453,36 @@ OhosRouteEntry(
),
```
### 7.5 深链接统计分析
### 8.5 深链接统计分析
可在 `_handleDeepLinkRedirect` 中添加埋点:
可在 `DeepLinkService._handleLink()` 中添加埋点:
```dart
if (scheme == 'xianyan') {
static void _handleLink(Uri uri) {
final resolved = AppRouter.resolveDeepLinkUri(uri);
// 埋点统计
AnalyticsService.track('deep_link_received', {
'url': uri.toString(),
'path': uri.path,
'source': 'custom_scheme',
'scheme': uri.scheme,
'resolved': resolved,
'source': uri.scheme == 'xianyan' ? 'custom_scheme' : 'universal_link',
});
// ...
if (resolved == null) {
Log.w('🔗 [DeepLink] 无法解析: $uri');
return;
}
// ... 导航逻辑
}
```
---
## 、测试验证
## 、测试验证
### 8.1 命令行测试iOS 模拟器)
### 9.1 命令行测试iOS 模拟器)
```bash
xcrun simctl openurl booted "xianyan://fortune"
@@ -347,17 +490,24 @@ xcrun simctl openurl booted "xianyan://article/42"
xcrun simctl openurl booted "https://xianyan.app/rank"
```
### 8.2 命令行测试Android
### 9.2 命令行测试Android
```bash
adb shell am start -a android.intent.action.VIEW -d "xianyan://fortune"
adb shell am start -a android.intent.action.VIEW -d "https://xianyan.app/rank"
```
### 8.3 Flutter 测试代码
### 9.3 Flutter 测试代码
```dart
testWidgets('deep link redirect', (tester) async {
testWidgets('deep link resolve', (tester) async {
// 测试 AppRouter.resolveDeepLinkUri 统一解析
final uri = Uri.parse('xianyan://fortune');
final resolved = AppRouter.resolveDeepLinkUri(uri);
expect(resolved, equals('/daily-fortune'));
});
testWidgets('deep link redirect via GoRouter', (tester) async {
final router = appRouter;
router.go('/fortune');
await tester.pumpAndSettle();
@@ -368,10 +518,25 @@ testWidgets('deep link redirect', (tester) async {
---
## 、注意事项
## 、注意事项
1. **登录态路由**`/notes``/achievement``/chat-settings/:id` 等路由需要登录态,未登录时鸿蒙端通过 `AuthMiddleware` 自动重定向到 `/login`
2. **参数类型**:路径参数 `:id``:uid` 为整数类型,传入非数字会导致页面异常
3. **URL编码**:查询参数中的中文和特殊字符需要 URL 编码(如 `text=%E4%BD%A0%E5%A5%BD`
4. **鸿蒙端限制**:鸿蒙端不使用 GoRouter通过 `OhosNavBridge.push()` 跳转,所有路由必须在 `_routes` 注册表中注册
5. **首次安装**:首次安装用户会先进入引导页 `/onboarding`,深链接会在引导完成后生效
6. **冷启动时序**`DeepLinkService.init()` 必须在 `rootNavigatorKey` 绑定到 `MaterialApp` 之后调用,否则 `_pendingLink` 延迟导航机制会介入
7. **路径映射唯一入口**:所有路径映射逻辑统一在 `AppRouter.resolveDeepLinkUri()` 中维护,禁止在其他位置重复定义映射关系
---
## 附录v1.0 → v2.0 变更记录
| 变更项 | v1.0 | v2.0 |
|--------|------|------|
| 链接捕获方式 | GoRouter redirect 内联处理 | `app_links` 库 + `DeepLinkService` |
| 路径映射方法 | `_resolveDeepLinkPath()` (私有) | `AppRouter.resolveDeepLinkUri()` (公共静态) |
| 冷启动支持 | 依赖 GoRouter 初始化 | `getInitialLink()` 独立获取 |
| 热恢复支持 | 无 | `uriLinkStream` 实时监听 |
| Context 不可用处理 | 无 | `_pendingLink` 延迟导航机制 |
| 映射逻辑复用 | GoRouter redirect 独占 | GoRouter + DeepLinkService 共享 |