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:
1518
docs/mockups/leisure_card_detail.html
Normal file
1518
docs/mockups/leisure_card_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
714
docs/mockups/leisure_settings.html
Normal file
714
docs/mockups/leisure_settings.html
Normal 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>
|
||||
476
docs/mockups/leisure_share_card.html
Normal file
476
docs/mockups/leisure_share_card.html
Normal 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>
|
||||
1371
docs/mockups/leisure_timeline.html
Normal file
1371
docs/mockups/leisure_timeline.html
Normal file
File diff suppressed because it is too large
Load Diff
371
docs/superpowers/specs/2026-05-27-leisure-design.md
Normal file
371
docs/superpowers/specs/2026-05-27-leisure-design.md
Normal 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. ❓ 是否需要离线缓存支持?
|
||||
156
docs/superpowers/specs/2026-05-27-leisure-devlog.md
Normal file
156
docs/superpowers/specs/2026-05-27-leisure-devlog.md
Normal 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` | 可拖拽Sheet,1/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 完成后审计: 集成是否完整,路由是否正确
|
||||
- [ ] 最终审计: 全部验收标准通过
|
||||
@@ -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 . '积分');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 Link(iOS)和 App Link(Android)的自动捕获:
|
||||
|
||||
- **自动监听**:无需手动注册 `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 共享 |
|
||||
|
||||
Reference in New Issue
Block a user