便签条与贴纸系统扩展 — 设计文档 v0.3
v0.3 文档为 NoteArchive 项目便签贴纸与贴纸系统扩展的完整设计规格。涵盖 L1 抽象层(协议+泛型+注册表)、便签条、日期贴纸、天气贴纸、表情收藏与抽屉共五类贴纸的实现方案。32 项关键决策全数闭环,配套 ~9.5h 实施步骤。
便签条与贴纸系统扩展 — 设计文档 v0.3
文档版本:v0.3(完整决策版,待主上审阅) 创建时间:2026-06-02 12:12 GMT+8 修订时间:2026-06-02 14:46 GMT+8(主上 30+ 项决策全数闭环) 隶属项目:NoteArchive(XCode/NoteArchive) 涵盖:抽象贴纸系统 L1 + 便签条 + 日期贴纸 + 天气贴纸 + 表情收藏与抽屉 当前进度:v0.3 文档定稿 / 实施未启动(分支 feature/sticker-system-v0.3 待创建)
0. 重大设计转向(v0.1 → v0.2 → v0.3)
v0.1 仅规划「便签条」单一功能。v0.2 经主上多次裁定后,升级为完整的「贴纸系统扩展」设计。v0.3 在 v0.2 基础上进一步细化便签贴纸 UX(模态创建 sheet + 贴上后只读 + 富文本)与天气贴纸(自选极简 + 双模式创建)。
| 范围 | v0.1 | v0.2 | v0.3 |
|---|---|---|---|
| 贴纸类型 | 仅便签条 | 图片/便签/日期/天气/表情 | 同 v0.2 |
| 便签创建 | 工具栏按钮(U1 直入文字编辑) | 同 v0.2 | U_new 模态创建 sheet(颠覆 U1) |
| 便签编辑 | 贴上后可双态编辑(E1) | 同 v0.2 | E0 贴上后只读(颠覆 E1) |
| 便签文本 | 纯文本 | 同 v0.2 | DD_new 富文本(颠覆 DD1) |
| 天气创建 | 调 API 单模式 | 同 v0.2 | 双模式(点击→有定位自动/无定位 sheet;长按→sheet 自选) |
| 自选天气 | — | 含温度/城市输入 | R 极简:只选图标(颠覆 R 旧版) |
| 定位失败 | 弹提示无兜底 | 同 v0.2 | Q 改弹 sheet 自选(颠覆 Q 旧版) |
| 天气字段 | 全有 | 同 v0.2 | GG1 自选字段为 nil(颠覆旧版固定字段) |
全文档贯穿 32 项已闭环决策(见附录 B),吾之推荐项均已被主上采纳。
1. 目标与背景
主上欲在 NoteArchive 笔记页面上扩展统一贴纸系统,使各类内容(便签、日期、天气、表情)皆可像图片贴纸一样贴在页面上,旋转/缩放/移动,操作逻辑与现有图片贴纸一致。
1.1 具体目标
- 新四类贴纸: 便签条 / 日期贴纸 / 天气贴纸 / 表情包(收藏)
- 统一抽象层: 标准化扩展点,未来加新类型(动图、Logo 等)零侵入
- 资产/实例分离: 表情包可收藏、可重复添加,互不干扰
- 数据全快照: 贴纸一旦添加即定格,不刷新、不联网、不重试
- 跨区域兼容: 商店区域自动定 provider(CN→Open-Meteo,其他→WeatherKit)
1.2 核心思路
不另起炉灶,在现有 StickerStore/StickerView 架构之上抽象出 L1 协议+泛型层,让所有贴纸类型在 BookPageView 的 ZStack 中并列渲染;新增类型只需写 Content struct + Renderer + 注册一行。
2. 现有贴纸系统基线(仅简述,详见 v0.1 §2)
- 数据层:
StickerData(id/imageData/center/rotation/scale/entityID);StickerStore(@Published pages 二维数组) - 视图层:
StickerOverlay(叠加在 BookPageView)→IdleStickerView/EditingStickerView - 状态机:
idle ↔ editing(点击进编辑,确认/删除出编辑) - 手势:
DragGesture+MagnificationGesture.simultaneously(with: RotationGesture()),编辑结束写回 - 持久化: 防抖 800ms 全删全建;退出时
viewContext.save() - 关键防御: 编辑结束 →
refreshPageView()→pageRefreshID = UUID()重建 UIPageViewController
3. 设计原则(v0.2 核心)
3.1 原则 1:全快照语义(决策 D)
所有贴纸添加时定格,添加后永不改动。
| 贴纸类型 | 快照内容 |
|---|---|
| 图片 | 选择的图片(本来就静态) |
| 便签条 | 用户输入文字(本来就静态) |
| 日期 | 创建瞬间的日期时间 + 当日农历 |
| 天气 | 创建瞬间的天气数据(温度/天气/湿度/风速) |
| 表情 | 收藏时定格图片数据(本来就是静态) |
架构影响:
- ❌
RefreshableContent协议作废——所有内容静态 - ❌ 无需缓存策略、无需离线降级、无需后台任务
- ❌ 无需网络重试、错误处理降级
- ✅ CoreData 存什么就显示什么
3.2 原则 2:资产/实例分离(决策 F+K)
表情包(收藏)= 资产;贴纸 = 实例。
FavoriteSticker(资产 / 全局) ImageSticker(实例 / 笔记内)
┌──────────────────────┐ ┌──────────────────────┐
│ id: UUID │ │ id: UUID │
│ imageData: Data │──复制数据──→ │ imageData: Data(副本)│
│ label: String? │ 添加贴纸 │ centerX, Y, ... │
│ createdAt: Date │ │ (page, scale 等) │
└──────────────────────┘ └──────────────────────┘
↑ ↑
全局共享(用户级) 隶属于具体笔记
删除收藏不影响已有贴纸 删除贴纸不影响收藏
1 资产可对应 N 实例(允许重复添加)
3.3 原则 3:标准化扩展点(决策 A)
未来加新贴纸类型(动图、Logo、模板等)需零侵入:
- 写一个 Content Swift struct(实现
StickerContent协议) - 写一个 Renderer 视图(实现
StickerRenderer协议) - 在
StickerRegistry注册一行 - CoreData 加新实体
无需修改: StickerOverlay、StickerStore、BookPageView、SimpleDrawingView 任何现有代码。
4. 架构总览
┌──────────────────────────────┐
│ BookPageView (ZStack) │
├──────────────────────────────┤
已有图层(不动) │ CanvasView (PKCanvasView) │
│ StickerOverlay(泛型) │ ← L1 重构后
│ │
│ ▼ 各类型 Renderer 并列 │
│ - ImageRenderer │
│ - NoteRenderer │
│ - DateRenderer │
│ - WeatherRenderer │
│ - FavoriteRenderer │ ← 未来 emoji
└──────────────────────────────┘
↑ ↑
│ │
┌───────────┘ └────────────┐
│ │
┌─────▼─────────────┐ ┌───────▼────────────┐
│ StickerStore<T> │ │ FavoriteStickerStore│
│ (泛型,<T: Content>)│ │ (资产库,全局) │
└─────┬─────────────┘ └───────┬────────────┘
│ │
┌─────▼─────────────┐ ┌───────▼────────────┐
│ 各 StickerEntity │ │ FavoriteSticker │
│ (多实体) │ │ Entity │
│ - ImageSticker │ └───────┬────────────┘
│ - NoteSticker │ │
│ - DateSticker │ └─ 全局共享
│ - WeatherSticker │ (与 PageEntity 无强关联)
└─────┬─────────────┘
│
└──────────── PageEntity ────────────┘
5. 抽象层 L1 设计(决策 A+G)
5.1 协议层
// MARK: - StickerContent(所有贴纸实现)
protocol StickerContent: Identifiable, Equatable {
var id: UUID { get }
var geometry: StickerGeometry { get set }
}
// 几何信息(统一字段,所有类型共用)
struct StickerGeometry: Equatable {
var centerX: CGFloat // 归一化 0~1
var centerY: CGFloat
var rotation: CGFloat // 弧度
var scale: CGFloat // 0.15 ~ 8.0
}
// MARK: - StickerRenderer(所有 Renderer 实现)
protocol StickerRenderer {
associatedtype Content: StickerContent
@ViewBuilder
func render(_ content: Content, isEditing: Bool) -> AnyView
}
// MARK: - 类型擦除(注册表用)
struct AnyStickerRenderer {
let renderFn: (Any, Bool) -> AnyView
// ... type-erased 包装
}
5.2 Store 泛型化
// 泛型 Store,承载任何 StickerContent
final class StickerStore<T: StickerContent>: ObservableObject {
@Published var pages: [[T]] = []
func items(forPage index: Int) -> [T] { ... }
func add(_ item: T, toPage index: Int) { ... }
func update(_ item: T, onPage index: Int) { ... }
func delete(itemID: UUID, fromPage index: Int) { ... }
func deleteAll(fromPage index: Int) { ... }
func moveItems(from: Int, to: Int) { ... }
func removePage(at: Int) { ... }
func appendEmptyPage() { ... }
func binding(forItemID id: UUID, onPage index: Int) -> Binding<T> { ... }
}
5.3 注册表(标准化扩展点)
// 全局注册表:未来加新类型 1 行即可
enum StickerRegistry {
static let renderers: [String: AnyStickerRenderer] = [
"image": ImageRenderer().erase(),
"note": NoteRenderer().erase(),
"date": DateRenderer().erase(),
"weather": WeatherRenderer().erase(),
"favorite": FavoriteRenderer().erase(), // 未来 emoji/动图/Logo
]
static func renderer(for kind: String) -> AnyStickerRenderer? {
renderers[kind]
}
}
5.4 视图层泛型化
// 泛型 Overlay,所有类型共用
struct StickerOverlay<T: StickerContent>: View {
@ObservedObject var store: StickerStore<T>
let pageIndex: Int
let contentKind: String // 决定调哪个 Renderer
var pageSize: CGSize
var onItemChange: () -> Void
var onEditingChange: (Bool) -> Void = { _ in }
var onEditingEnded: () -> Void = {}
@State private var selectedID: UUID?
var body: some View {
ZStack {
if let editingID = selectedID, ... {
EditingItemView<T>(...)
} else {
ForEach(items) { item in
if let renderer = StickerRegistry.renderer(for: contentKind) {
renderer.render(item, isEditing: false)
}
}
}
}
}
}
5.5 添加新类型的标准流程(零侵入示例)
假设未来要加「动图贴纸 GIFContent」:
// 1. 写 Content struct
struct GIFContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
var gifData: Data
// ... Equatable
}
// 2. 写 Renderer
struct GIFRenderer: StickerRenderer {
func render(_ content: GIFContent, isEditing: Bool) -> AnyView {
AnyView(GIFImageView(data: content.gifData))
}
}
// 3. 注册一行
StickerRegistry.renderers["gif"] = GIFRenderer().erase()
// 4. CoreData 加新实体 GIFStickerEntity
// 完成!不动任何现有代码
6. 数据模型(各 Content Swift Struct)
6.1 现有 ImageContent(保持现状,重命名以对齐)
struct ImageContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
var imageData: Data
var entityID: NSManagedObjectID?
// Equatable
}
6.2 NoteContent(便签贴纸,决策 A1+E0+U_new+X+Y+Z+W+CC+DD+KK)
struct NoteContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
// ——快照字段(创建时定,贴上后不可再改)——
var attributedText: Data // NSAttributedString 序列化(DD_new 富文本)
var color: NoteColor // 预设枚举:yellow/pink/blue/green
var createdAt: Date // 创建时间戳
var entityID: NSManagedObjectID?
// Equatable
}
enum NoteColor: String, Codable, CaseIterable {
case yellow, pink, blue, green
var swiftUIColor: Color {
switch self {
case .yellow: return Color(red: 1.0, green: 0.95, blue: 0.6)
case .pink: return Color(red: 1.0, green: 0.8, blue: 0.85)
case .blue: return Color(red: 0.75, green: 0.88, blue: 1.0)
case .green: return Color(red: 0.78, green: 0.95, blue: 0.78)
}
}
}
默认尺寸: 200×200pt(scale 1.0)
默认字体: .system(size: 16),多行换行(CC 决策)
默认限制: 500 字符(V 决策,超出截断)
6.2.1 创建流程(U_new 决策——模态 sheet)
颠覆 U1:从「直接编辑」改为「模态创建 sheet」
[用户点击工具栏"便签"按钮]
↓
[弹出 NoteStickerEditor sheet]
↓
[sheet 结构]
┌──────────────────────────┐
│ [关闭 X] │ ← 右上角 II 决策:取消
├──────────────────────────┤
│ ? 颜色:? ? ? ? │ ← 顶部 II 决策:4 色按钮横排
├──────────────────────────┤
│ ┌────────────────────┐ │
│ │ │ │
│ │ 多行文本输入区 │ │ ← 下方 II 决策:大文本框
│ │ (UITextView) │ │
│ │ │ │
│ └────────────────────┘ │
└──────────────────────────┘
↓
[用户输入文本/选颜色 / 点击其他区域]
↓
[校验:空文本不创建 (JJ 决策)]
↓ 有文本 → 创建 NoteContent(快照)→ 保存 CoreData
↓ 空文本 → 不做任何事(点击其他区域也不创建)
实现要点:
NoteStickerEditor为独立 SwiftUI 视图- 颜色选择:sheet 顶部 4 个圆形按钮,当前选中色加边框
- 文本输入:UITextView(UIViewRepresentable 包裹),
dataDetectorTypes = [.link, .phoneNumber, .address] - 键盘处理:
.ignoresSafeArea(.keyboard)适配 - 取消:sheet 右上角"关闭"按钮
- 确认:点击 sheet 外部(模态背景)触发创建逻辑
6.2.2 贴上后只读(E0 决策——颠覆 E1 双态编辑)
核心原则:便签一旦贴上即定格,与全快照语义 D 完全契合。
| 操作 | 是否允许 | 触发方式 |
|---|---|---|
| 拖动位置 | ✅ | 拖拽手势 |
| 旋转角度 | ✅ | 旋转手势 |
| 缩放大小 | ✅ | 捏合手势 |
| 删除便签 | ✅ | 编辑态操作栏"删除"按钮 |
| 复制文本 | ✅ | 长按便签(HH 决策) |
| 重新编辑文字 | ❌ | — |
| 修改颜色 | ❌ | — |
| 撤销操作 | ❌ | FF 决策:不实现 |
长按复制行为(HH 决策):
- 长按便签 → 复制 attributedText 的纯文本到系统剪贴板(
UIPasteboard.general.string) - 弹出 Toast 提示「已复制」(自动消失,2-3 秒)
- 不进入任何编辑态,不创建新便签
Toast 实现:
- 使用 SwiftUI 自定义 toast modifier
- 全局单例
ToastCenter.shared.show("已复制") - 显示在屏幕底部,自动消失
6.2.3 富文本支持(DD_new + KK1 决策)
范围:仅链接识别(KK1)——不提供粗体/斜体/字号编辑工具栏
UITextView 配置:
UITextView 配置:
- dataDetectorTypes = [.link, .phoneNumber, .address]
- isEditable = true
- isScrollEnabled = true
- font = .systemFont(ofSize: 16)
- textColor = .label(链接自动变蓝下划线)
支持能力:
- ✅ 用户可粘贴带链接/电话/邮箱的富文本,链接自动识别为蓝色可点击
- ✅ 点击链接自动跳转系统浏览器 / 拨号 / 邮件
- ❌ 不提供粗体/斜体/字号/字体等富文本编辑工具栏
- ❌ 不支持图片/附件
序列化:
attributedText: Data字段存储 NSAttributedString 的归档数据- CoreData 轻量迁移自动支持 Data 字段
6.2.4 视觉风格(Z1 决策——纸质便签感)
RoundedRectangle(cornerRadius: 8)
.fill(note.color.swiftUIColor)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color.black.opacity(0.1), lineWidth: 1)
)
.shadow(color: .black.opacity(0.15), radius: 2, x: 0, y: 1)
- 圆角 8pt
- 1px 浅灰边框
- 轻微阴影(向下 1pt,半径 2pt)
- 背景填充预设 4 色之一
6.2.5 容量与边界
- 字符限制:500 字(V 决策,超出截断)
- 多行换行:支持(CC 决策)
- 缩放范围:0.15 ~ 8.0(沿用 StickerView)
- 旋转:任意角度(沿用 StickerView)
- 位置:归一化坐标 0~1,边界夹紧(沿用 StickerView)
6.3 DateContent(日期贴纸,决策 C+H+I)
struct DateContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
// 快照字段:创建时定格
var capturedAt: Date // 创建瞬间的精确时间
var lunarText: String? // 农历字符串(设备区域 CN 时填入)
var entityID: NSManagedObjectID?
// Equatable
}
显示内容:
- 主行:
2026-06-02 12:30(年月日时分秒,本地化格式) - 副行(仅设备区域 CN):
农历 五月十五 丙午年 庚寅月 壬午日
农历判断:
let isChinaRegion = Locale.current.region?.identifier == "CN"
农历算法: 使用 Swift Package LunarSwift 或自实现 lunar-core 算法(公历→农历转换表)。
6.4 WeatherContent(天气贴纸,决策 E+R+O+Q+T+GG)
struct WeatherContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
// ——快照字段(创建时定格)——
var weatherCode: Int // WMO 标准代码(必有)
var temperature: Double? // 摄氏度(GG1 决策:自选时为 nil)
var humidity: Int? // 百分比(GG1 决策:自选时为 nil)
var windSpeed: Double? // km/h(GG1 决策:自选时为 nil)
var locationName: String? // 地区名(GG1 决策:自选时为 nil)
var provider: String // "weatherkit" / "openmeteo" / "manual"
var capturedAt: Date // 数据观测时间
var entityID: NSManagedObjectID?
// Equatable
}
GG1 关键决策:temperature/locationName/humidity/windSpeed 在自选模式下为 nil,UI 自动判断隐藏。
显示内容(按字段填充度自动适配):
| 模式 | 有字段 | 显示效果 |
|---|---|---|
| 实时模式 | 全部 | 24° ☀️ 上海 · 湿度 68% · 风力 12km/h |
| 自选模式 | 仅 weatherCode | ☀️ 晴(仅图标 + 描述) |
天气代码→中文+图标映射:
| WMO | 中文 | SF Symbol |
|-----|------|-----------|
| 0 | 晴 | sun.max.fill |
| 1-3 | 多云 | cloud.sun.fill / cloud.fill |
| 45-48 | 雾 | cloud.fog.fill |
| 51-55 | 毛毛雨 | cloud.drizzle.fill |
| 61-65 | 雨 | cloud.rain.fill |
| 71-75 | 雪 | cloud.snow.fill |
| 80-82 | 阵雨 | cloud.heavyrain.fill |
| 95-99 | 雷阵雨 | cloud.bolt.rain.fill |
新增 WMO 代码 95-99: 雷阵雨
6.5 FavoriteContent(未来 emoji/动图/Logo,决策 K1)
struct FavoriteContent: StickerContent {
let id: UUID
var geometry: StickerGeometry
var imageData: Data
var sourceFavoriteID: UUID? // 关联的 FavoriteSticker(用于追踪用频次,可选)
var entityID: NSManagedObjectID?
// Equatable
}
7. 持久化层(决策 B1:多实体方案)
7.1 CoreData 实体清单
| 实体 | 字段 | 关联 |
|---|---|---|
ImageStickerEntity(已有) |
id/imageData/centerX/centerY/rotation/scale | PageEntity.stickers |
NoteStickerEntity(新建) |
id/attributedText: Data/colorRaw/createdAt/centerX/centerY/rotation/scale | PageEntity.notes |
DateStickerEntity(新建) |
id/capturedAt/lunarText/centerX/centerY/rotation/scale | PageEntity.dates |
WeatherStickerEntity(新建) |
id/weatherCode/temperature: Double?/humidity: Int?/windSpeed: Double?/locationName: String?可选/provider/capturedAt/centerX/centerY/rotation/scale | PageEntity.weathers |
FavoriteStickerEntity(新建,全局共享,不与 PageEntity 关联) |
id/imageData/label/createdAt | (独立) |
v0.3 字段变化说明:
- NoteStickerEntity.text:
String→attributedText: Data(DD_new 富文本决策) - NoteStickerEntity.新增
createdAt: Date(用于显示/排序) - WeatherStickerEntity.多个字段改可选(GG1 决策,自选模式为 nil)
7.2 PageEntity 关系扩展
PageEntity
├── existing: stickers → Set<ImageStickerEntity> (Cascade)
├── new: notes → Set<NoteStickerEntity> (Cascade)
├── new: dates → Set<DateStickerEntity> (Cascade)
└── new: weathers → Set<WeatherStickerEntity> (Cascade)
FavoriteStickerEntity(独立,无 PageEntity 关联)
7.3 模型版本与迁移
- 新增轻量迁移版本
NoteArchive.xcdatamodeld/NoteArchive v2.momd/ - 4 个新实体 + 3 个新关系(PageEntity 新增 notes/dates/weathers)
Mapping Model自动生成(仅新增实体/关系,无需手工映射)- 旧笔记无新数据,无需迁移逻辑
7.4 持久化策略(全快照,与现有一致)
- 防抖 800ms → 全删全建
- 退出 →
viewContext.saveOrReport - 收藏(FavoriteStickerEntity)独立保存,与贴纸实例无强关联
8. 视图层
8.1 各 Renderer 实现概要
// ImageRenderer(保持现有 StickerView 逻辑)
struct ImageRenderer: StickerRenderer {
func render(_ content: ImageContent, isEditing: Bool) -> AnyView {
AnyView(ImageStickerView(content: content, isEditing: isEditing))
}
}
// NoteRenderer
struct NoteRenderer: StickerRenderer {
func render(_ content: NoteContent, isEditing: Bool) -> AnyView {
AnyView(NoteStickerView(content: content, isEditing: isEditing))
}
}
// DateRenderer
struct DateRenderer: StickerRenderer {
func render(_ content: DateContent, isEditing: Bool) -> AnyView {
AnyView(DateStickerView(content: content, isEditing: isEditing))
}
}
// WeatherRenderer
struct WeatherRenderer: StickerRenderer {
func render(_ content: WeatherContent, isEditing: Bool) -> AnyView {
AnyView(WeatherStickerView(content: content, isEditing: isEditing))
}
}
8.2 几何编辑态(EditingItemView<T>)
完全沿用现有 EditingStickerView 逻辑,泛型化:
- 暗色遮罩
DragGesture+MagnificationGesture.simultaneously(with: RotationGesture())- 边界夹紧:
min(0, max(1, center));缩放:max(0.15, min(scale, 8.0)) - 操作栏(重置 ✓ ?)
- 编辑结束 →
refreshPageView()
8.3 便签条 UX(E0 贴上后只读 + U_new 模态创建)
颠覆 E1 双态编辑:便签贴上后只读,与全快照语义 D 完全契合。
创建态: 模态 NoteStickerEditor sheet(详见 §6.2.1)
- 顶部:4 色选择(II 决策)
- 下方:大文本框 UITextView
- 右上角:关闭按钮
- 点击 sheet 外部:确认创建(空文本不创建,JJ 决策)
贴上后状态:
idle(默认)↔editing(geometric)(点击进入)- 无文字编辑态(贴上后只读)
- 长按 → 复制纯文本到剪贴板 + Toast(HH 决策)
- 编辑态操作栏:重置 / 确认 / 删除
- 几何手势完全沿用 StickerView(拖动 / 缩放 / 旋转)
便签条不提供 E1 双态编辑(几何 ↔ 文字),与 D 决策一致。
9. 天气贴纸——双 Provider 路由 + 双模式创建(决策 E+I+O+Q+R+T+GG)
9.1 Provider 抽象
protocol WeatherProvider {
func fetchCurrent(at location: CLLocation) async throws -> WeatherSnapshot
}
struct WeatherSnapshot {
let temperature: Double
let weatherCode: Int
let humidity: Int?
let windSpeed: Double?
let locationName: String
let observedAt: Date
}
9.2 两个 Provider 实现
// WeatherKit(海外 + 港澳台)
struct WeatherKitProvider: WeatherProvider {
func fetchCurrent(at location: CLLocation) async throws -> WeatherSnapshot {
let service = WeatherService.shared
let weather = try await service.weather(for: location).currentWeather
return WeatherSnapshot(
temperature: weather.temperature.value,
weatherCode: mapAppleToWMO(weather.condition),
humidity: Int(weather.humidity * 100),
windSpeed: weather.wind.speed.value,
locationName: weather.locationMetadata?.name ?? "Unknown",
observedAt: weather.date
)
}
}
// Open-Meteo(CN 大陆)
struct OpenMeteoProvider: WeatherProvider {
func fetchCurrent(at location: CLLocation) async throws -> WeatherSnapshot {
let lat = location.coordinate.latitude
let lon = location.coordinate.longitude
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(lat)&longitude=\(lon)¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&timezone=auto")!
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(OpenMeteoResponse.self, from: data)
// 反向地理:Open-Meteo geocoding API(免费免 key)
let name = try await reverseGeocode(lat: lat, lon: lon)
return WeatherSnapshot(
temperature: decoded.current.temperature_2m,
weatherCode: decoded.current.weather_code,
humidity: decoded.current.relative_humidity_2m,
windSpeed: decoded.current.wind_speed_10m,
locationName: name,
observedAt: ISO8601DateFormatter().date(from: decoded.current.time) ?? Date()
)
}
}
9.3 区域路由(WeatherRouter)
struct WeatherRouter: WeatherProvider {
func fetchCurrent(at location: CLLocation) async throws -> WeatherSnapshot {
// 决策 E:按 App Store 区域选 provider
let regionCode = await currentStorefrontRegion()
let provider: WeatherProvider = (regionCode == "CN")
? OpenMeteoProvider()
: WeatherKitProvider()
return try await provider.fetchCurrent(at: location)
}
private func currentStorefrontRegion() async -> String {
if let storefront = await Storefront.current {
return storefront.countryCode
}
return "US" // 兑底
}
}
9.4 双模式创建流程(决策 O+Q+R+T)
Q 决策修订:定位失败 → 弹自选 sheet(不是弹提示无兑底) R 决策修订:自选 sheet 只含天气图标(无温度无城市) T 决策:长按工具栏 → 弹自选 sheet(无论有无定位)
[用户点击工具栏"天气"按钮]
↓
[检查定位缓存 (UserDefaults)]
↓ 有 → 调 API → 成功创建 / 失败 → 弹自选 sheet
↓ 无 → 请求 CoreLocation 定位
↓ ├─ 用户授权 → 获取经纬度 → 缓存 → 调 API
↓ │ ↓ 成功 → 创建实时贴纸
↓ │ ↓ 失败 → 弹自选 sheet
↓ └─ 用户拒绝/失败 → 弹自选 sheet
[用户长按工具栏"天气"按钮]
↓
[不论有无定位 → 直接弹自选 sheet]
[自选 sheet 内]
↓
[点击某个天气图标]
↓
[创建 WeatherContent(snapshot, provider="manual")]
→ temperature/locationName/humidity/windSpeed 为 nil
→ 保存 CoreData
→ 关闭 sheet
自选 sheet UI(M1 半屏 sheet):
┌──────────────────────────┐
│ ⌄ 自选天气 │
├──────────────────────────┤
│ │
│ ☀️ ?️ ☁️ ?️ │
│ 晴 少云 多云 雾 │ ← 11 类天气图标(不含温度/城市)
│ │
│ ?️ ?️ ⛈️ ❄️ │
│ 毛毛雨 雨 雷雨 雪 │ (WMO 0/1-3/45-48/51-55/61-65/71-75/80-82/95-99)
│ │
│ ?️ ⛈️ ?️ │
│ 阵雪 强雷雨 雷暴 │
│ │
└──────────────────────────┘
[点击图标] → 立即创建贴纸 + 关闭 sheet
11 类 WMO 代码 + 中文 + SF Symbol:
| WMO | 中文 | SF Symbol |
|---|---|---|
| 0 | 晴 | sun.max.fill |
| 1 | 少云 | cloud.sun.fill |
| 2 | 多云 | cloud.fill |
| 3 | 阴 | cloud.fill |
| 45 | 雾 | cloud.fog.fill |
| 51 | 毛毛雨 | cloud.drizzle.fill |
| 61 | 雨 | cloud.rain.fill |
| 71 | 雪 | cloud.snow.fill |
| 80 | 阵雨 | cloud.heavyrain.fill |
| 95 | 雷阵雨 | cloud.bolt.rain.fill |
| 99 | 强雷暴 | cloud.bolt.fill |
9.5 GG1 字段处理
自选模式创建的 WeatherContent:
weatherCode: 选中的 WMO 代码temperature/humidity/windSpeed/locationName: 均为 nilprovider: "manual"capturedAt: 创建时刻
实时模式创建的 WeatherContent:
- 所有字段均有值
provider: "weatherkit" 或 "openmeteo"capturedAt: 天气 API 返回的观测时间
UI 渲染逻辑(Renderer 内部判断):
func render(_ content: WeatherContent, isEditing: Bool) -> AnyView {
AnyView(WeatherStickerView(content: content))
}
struct WeatherStickerView: View {
let content: WeatherContent
var body: some View {
VStack {
HStack {
Image(systemName: wmoToSFSymbol(content.weatherCode))
if let temp = content.temperature {
Text("\(Int(temp))°")
}
}
Text(wmoToChinese(content.weatherCode))
if let name = content.locationName {
Text(name)
}
// 可选第三行:湿度 + 风力(仅实时模式)
if let humidity = content.humidity, let wind = content.windSpeed {
Text("湿度 \(humidity)% · 风力 \(Int(wind)) km/h")
}
}
}
}
9.5 Storefront 区域获取
// iOS 15+ StoreKit 2
let storefront = await Storefront.current
let regionCode = storefront?.countryCode ?? "US"
Info.plist 需新增:
NSLocationWhenInUseUsageDescription(使用位置获取本地天气)
10. 表情收藏与抽屉(决策 F+K+L+M+N+P)
10.1 FavoriteStickerStore(全局资产库)
final class FavoriteStickerStore: ObservableObject {
@Published var favorites: [FavoriteStickerData] = []
func add(imageData: Data, label: String? = nil) { ... }
func delete(id: UUID) { ... }
func moveItem(from: Int, to: Int) { ... }
func load() { ... } // 从 CoreData 加载
func save() { ... } // 全删全建
}
struct FavoriteStickerData: Identifiable, Equatable {
let id: UUID
var imageData: Data
var label: String?
var createdAt: Date
}
10.2 收藏入口(L1:图片选择器中加星标)
修改 PhotosPicker 的选择确认回调,在选完图片后增加「收藏为表情包」选项:
PhotosPicker(...) { /* 选图后 */ }
.confirmationDialog("图片操作", isPresented: $showAction) {
Button("添加为贴纸") { addAsSticker() }
Button("收藏为表情包") { addAsFavorite() } // ← 新增
Button("添加并收藏") { addAndFavorite() } // ← 快捷
}
10.3 抽屉 UI(M1:半屏 sheet)
struct FavoriteDrawer: View {
@ObservedObject var store: FavoriteStickerStore
@Environment(\.dismiss) var dismiss
@State private var pendingDelete: FavoriteStickerData?
let columns = [GridItem(.adaptive(minimum: 80), spacing: 12)]
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(store.favorites) { fav in
FavoriteThumbnail(data: fav)
.onTapGesture {
addAsSticker(fav) // P 决策:点击立即添加
dismiss()
}
.contextMenu {
Button(role: .destructive) {
pendingDelete = fav // P 决策:长按二级确认
} label: {
Label("删除", systemImage: "trash")
}
}
}
}
.padding()
}
.navigationTitle("收藏的表情")
.confirmationDialog(
"确认删除?",
isPresented: Binding(
get: { pendingDelete != nil },
set: { if !$0 { pendingDelete = nil } }
),
presenting: pendingDelete
) { fav in
Button("删除", role: .destructive) {
store.delete(id: fav.id) // P 决策:确认后删除
pendingDelete = nil
}
Button("取消", role: .cancel) {
pendingDelete = nil
}
}
}
.presentationDetents([.medium]) // M1 决策:半屏 sheet
.presentationDragIndicator(.visible) // 下拉手柄
.interactiveDismissDisabled(false) // P 决策:允许下拉关闭
}
}
交互细则(决策 P):
- ✅ 点击表情 → 立即添加为新贴纸 → 关闭抽屉
- ✅ 长按表情 → 弹出删除二级确认 → 确认后删除
- ✅ 点击空白区域 / 下拉手柄 → 关闭抽屉
10.4 工具栏入口(N1)
SimpleDrawingView 工具栏新增独立「收藏」按钮(与其他添加按钮并列):
HStack {
Button { /* 添加便签 */ } label: { Image(systemName: "note.text") }
Button { /* 添加图片 */ } label: { Image(systemName: "photo") }
Button { /* 添加日期 */ } label: { Image(systemName: "calendar") }
Button { /* 添加天气 */ } label: { Image(systemName: "cloud.sun") }
Button { // ← N1 决策:独立收藏按钮
showFavoriteDrawer = true
} label: { Image(systemName: "face.smiling") }
}
.sheet(isPresented: $showFavoriteDrawer) {
FavoriteDrawer(store: favoriteStore)
.presentationDetents([.medium])
}
11. 集成点
11.1 BookPageView.swift 修改
ZStack 叠加多个泛型 Overlay(每个类型一个 StickerOverlay 实例):
ZStack {
CanvasView(...)
StickerOverlay<ImageContent>(store: imageStore, contentKind: "image", ...)
StickerOverlay<NoteContent>(store: noteStore, contentKind: "note", ...)
StickerOverlay<DateContent>(store: dateStore, contentKind: "date", ...)
StickerOverlay<WeatherContent>(store: weatherStore, contentKind: "weather", ...)
}
11.2 SimpleDrawingView.swift 修改
- 持有 4 个
@StateObject:imageStore / noteStore / dateStore / weatherStore - 持有 1 个
@StateObject:favoriteStore(全局资产库) - 工具栏:5 个添加按钮(便签/图片/日期/天气/收藏)
- 便签 按钮 → 弹 NoteStickerEditor sheet(U_new 决策)
- 图片 按钮 → 调 PhotosPicker(现有逻辑)
- 日期 按钮 → 直接创建 DateContent(快照定格)
- 天气 按钮(点击)→ 有定位调 API / 无定位弹自选 sheet(Q+R 决策)
- 天气 按钮(长按)→ 直接弹自选 sheet(T 决策)
- 收藏 按钮 → 弹 FavoriteDrawer(N1 决策)
- 编辑态互斥:编辑某贴纸时禁用其他类型添加按钮
onAppear:加载所有 storeonDisappear:保存所有 storedeleteCurrentPage/movePage:同步所有 store 的 page 操作- 天气贴纸特殊处理:经纬度缓存 + CoreLocation 首次请求
- Toast 集成:
ToastCenter.shared.show("已复制")全局提示 - 长按便签复制:
onLongPressGesture触发UIPasteboard.general.string = noteText
天气按钮点击/长按手势实现:
Button {
addWeatherSticker() // 点击 → 有定位调 API / 无定位弹自选 sheet
} label: {
Image(systemName: "cloud.sun")
}
.simultaneousGesture(
LongPressGesture(minimumDuration: 0.5)
.onEnded { _ in
showWeatherPickerSheet = true // 长按 → 直接弹自选 sheet
}
)
11.3 NoteArchive.xcdatamodeld 修改
- 新增 v2 版本
- 4 个新实体(NoteStickerEntity / DateStickerEntity / WeatherStickerEntity / FavoriteStickerEntity)
- PageEntity 新增 3 个 to-many 关系
- 启用轻量迁移
11.4 Info.plist 新增
NSLocationWhenInUseUsageDescription:「用于获取本地天气数据」
12. iOS 26 / Swift 6 / Liquid Glass 适配
- 抽象层协议加
@MainActor标注(Swift 6 严格并发) Storefront.current异步调用自动适配WeatherService(WeatherKit) 异步- Liquid Glass:便签条
RoundedRectangle+.ultraThinMaterial与现有贴纸一致 - 颜色枚举用
Color(uiColor:)适配 iOS 26 颜色系统
13. 测试要点
13.1 单元测试
StickerStoreTests<T>:泛型化测试,add/update/delete/move/appendEmptyPageWeatherRouterTests:Mock Storefront + Mock WeatherProviderLunarConversionTests:公历→农历转换(边界:闰月、立春切换)WMOWeatherCodeTests:所有代码→中文+图标映射
13.2 集成测试(手动)
- 工具栏点击各类型 → 贴纸出现
- 几何编辑:拖动/缩放/旋转/重置/删除
- 便签条文字编辑:键盘弹起/收起/文本写回
- 日期贴纸:创建瞬间定格(可对比系统时间验证)
- 天气贴纸:CN/海外自动路由
- 收藏抽屉:点击添加/长按删除/下拉关闭
- 重复添加:同一图片可加 N 次
- CoreData:删除笔记→所有相关实体一并删除
- CoreData:删除收藏→已有贴纸不受影响
13.3 回归
- 现有图片贴纸所有功能不破
- 翻页/手势/编辑态无污染
14. 风险与权衡
| 风险 | 等级 | 缓解 |
|---|---|---|
| L1 抽象过深影响现有图片贴纸 | 中 | 现有 StickerView 改 Renderer 时充分测试;分步重构 |
| 泛型 StickerStore 与多类型 Overlay 性能 | 低 | 每类型独立实例,类型擦除仅在 Renderer 注册表 |
| 农历算法实现复杂度 | 中 | 用成熟 Swift Package(如 LunarSwift)或自实现标准 lunar-core 表 |
| CoreLocation 权限拒绝 | 低 | 弹提示 J 决策(无兜底) |
| Storefront.current 异步 + 启动时序 | 低 | 启动时缓存到 UserDefaults;首次进入笔记时校正 |
| CloudKit 同步 Data 字段(FavoriteStickerEntity) | 中 | 限制单张图片大小(建议 < 5MB);测试同步耗时 |
| iCloud 同步新 CoreData 实体 | 低 | NSPersistentCloudKitContainer 对轻量迁移友好 |
| iOS 17+ 农历边缘 case(闰月/节气) | 低 | 使用标准农历库;测试春节/闰月/立春切换 |
| 天气 API 失败/超时 | 低 | 弹提示 J 决策(无兜底) |
| 收藏夹容量无限制 | 低 | 文档建议 < 100 项,UI 分页(可选) |
15. 文件改动清单
15.1 新增文件
| 路径 | 用途 |
|---|---|
NoteArchive/Stickers/StickerContent.swift |
StickerContent / StickerGeometry 协议 |
NoteArchive/Stickers/StickerRenderer.swift |
StickerRenderer / AnyStickerRenderer |
NoteArchive/Stickers/StickerRegistry.swift |
全局注册表 |
NoteArchive/Stickers/Stickers/ImageContent.swift |
现有 StickerData 重命名 |
NoteArchive/Stickers/Stickers/NoteContent.swift |
便签贴纸(v0.3:attributedText 富文本) |
NoteArchive/Stickers/Stickers/DateContent.swift |
日期贴纸 |
NoteArchive/Stickers/Stickers/WeatherContent.swift |
天气贴纸(v0.3:可选字段) |
NoteArchive/Stickers/Stickers/FavoriteContent.swift |
表情(未来用) |
NoteArchive/Stickers/StickerStore.swift |
泛型化 StickerStore |
NoteArchive/Stickers/Renderers/ImageRenderer.swift |
现有 StickerView 改造 |
NoteArchive/Stickers/Renderers/NoteRenderer.swift |
便签视图(E0 贴上后只读 + HH 长按复制) |
NoteArchive/Stickers/Renderers/DateRenderer.swift |
日期视图 |
NoteArchive/Stickers/Renderers/WeatherRenderer.swift |
天气视图(v0.3:GG1 字段判断显示) |
NoteArchive/Stickers/Renderers/FavoriteRenderer.swift |
表情视图(未来) |
NoteArchive/Stickers/StickerOverlay.swift |
泛型化 Overlay |
NoteArchive/Stickers/EditingItemView.swift |
泛型化编辑态 |
NoteArchive/Views/Drawing/NoteStickerEditor.swift |
v0.3 新增:便签创建 sheet(U_new) |
NoteArchive/Views/Drawing/WeatherPickerSheet.swift |
v0.3 新增:天气自选 sheet(R) |
NoteArchive/Views/Drawing/FavoriteDrawer.swift |
抽屉 UI |
NoteArchive/Views/Drawing/FavoriteStickerStore.swift |
收藏 Store |
NoteArchive/UI/ToastCenter.swift |
v0.3 新增:全局 Toast 提示(HH 决策) |
NoteArchive/UI/ToastModifier.swift |
v0.3 新增:Toast SwiftUI modifier |
NoteArchive/Weather/WeatherProvider.swift |
Provider 协议 |
NoteArchive/Weather/WeatherKitProvider.swift |
WeatherKit 实现 |
NoteArchive/Weather/OpenMeteoProvider.swift |
Open-Meteo 实现 |
NoteArchive/Weather/WeatherRouter.swift |
区域路由 |
NoteArchive/Weather/StorefrontRegion.swift |
Storefront API 包装 |
NoteArchive/Weather/LocationManager.swift |
v0.3 新增:CoreLocation 封装 + 缓存 |
NoteArchive/Lunar/LunarConverter.swift |
农历转换 |
NoteArchive/Weather/WMOWeatherCode.swift |
WMO 代码映射 + 中文+SF Symbol 映射 |
15.2 修改文件
| 路径 | 改动 |
|---|---|
NoteArchive/NoteArchive.xcdatamodeld/NoteArchive v2.xcdatamodel/contents |
4 新实体 + 3 新关系(v0.3:NoteStickerEntity.text 改 Data) |
NoteArchive/Info.plist |
NSLocationWhenInUseUsageDescription |
NoteArchive/Views/Drawing/BookPageView.swift |
4 个泛型 StickerOverlay 叠加 |
NoteArchive/Views/Drawing/SimpleDrawingView.swift |
4 个 store + 工具栏 5 按钮(v0.3:含长按手势)+ 抽屉 + 天气定位 + Toast 集成 |
15.3 不动文件
StickerView.swift(逻辑迁移至EditingItemView.swift与ImageRenderer.swift)StickerStore.swift(旧版,迁移至泛型StickerStore<T>,旧文件可删除)ContentView.swift、CategoryDetailView.swiftPersistence.swift(除非需要 iCloud 兼容调整)
15.4 单元测试文件
| 路径 | 用途 |
|---|---|
NoteArchiveTests/Stickers/StickerStoreTests.swift |
泛型 store 测试 |
NoteArchiveTests/Weather/WeatherRouterTests.swift |
路由测试 |
NoteArchiveTests/Lunar/LunarConverterTests.swift |
农历测试 |
NoteArchiveTests/Weather/WMOWeatherCodeTests.swift |
WMO 映射测试 |
16. 实施步骤(估时 ~9.5h)
| 阶段 | 内容 | 估时 | 前置 |
|---|---|---|---|
| 0 | 主上审阅 v0.2 文档 | — | — |
| 1 | L1 抽象层:StickerContent / StickerRegistry / 泛型 StickerStore<T> / 泛型 StickerOverlay<T> |
2.0h | — |
| 2 | 现有 ImageContent / ImageRenderer 迁移至抽象层 |
0.5h | 1 |
| 3 | CoreData v2:4 新实体 + 3 新关系 + 轻量迁移 | 0.5h | — |
| 4 | NoteContent + NoteRenderer(便签条) |
1.5h | 1, 2 |
| 5 | DateContent + DateRenderer + LunarConverter |
1.0h | 1, 2 |
| 6 | WeatherContent + WeatherRenderer + 双 provider + 路由 |
1.5h | 1, 2 |
| 7 | FavoriteStickerStore + FavoriteDrawer |
1.0h | 1, 2 |
| 8 | BookPageView 集成 4 泛型 Overlay |
0.5h | 1-7 |
| 9 | SimpleDrawingView 工具栏 5 按钮 + 抽屉 + 天气定位流程 |
1.0h | 1-8 |
| 10 | Info.plist + CloudKit 同步测试 | 0.3h | 3 |
| 11 | 单元测试 + 集成手动测试 | 1.0h | 1-10 |
| 12 | 提交 + 推送 Gitea | 0.1h | 11 |
| 合计 | ~9.5h |
附录 A. 实施前置检查
- ✅ Apple Developer 账号已开通 WeatherKit capability
- ✅ Xcode 项目 Info.plist 准备新增
NSLocationWhenInUseUsageDescription - ✅ 当前 git 分支稳定(基线 commit 7c870e5 之后)
- ✅ CoreData v1 模型已稳定(Phase 1-4 已合入)
- ⚠️ Phase 5 单元测试失败问题已取消(主上 2026-06-02 决策),不阻塞实施
附录 B. 17 项决策记录(全部已闭环)
| # | 决策点 | 决策项 | 备选 |
|---|---|---|---|
| A | 抽象范围 | A1 全纳入 + 标准化扩展点 | A2 仅新三类 |
| B | 持久化 | B1 多实体 | B2 字段 / B3 统一实体 |
| C | 日期内容 | 纯本地 + 农历(年月日时分秒) | 原 EventKit 日程作废 |
| D | 数据时效 | 全快照语义 | 原 RefreshableContent 作废 |
| E | 商店区域 | Storefront.current | Locale/手动 |
| F | 表情 | 静态 + 收藏 + 抽屉 | 动图(可推迟) |
| G | 刷新 | D 闭环 | — |
| H | 农历区域 | H3 设备区域 | H1 语言 / H2 商店 |
| I | 农历格式 | I1 完整(干支年) | I2-I4 简略 |
| J | 失败兜底 | 弹提示「暂时无法获取」 | 兜底方案 |
| K | 收藏模型 | K1 独立 FavoriteStickerEntity | K2 复用 ImageSticker |
| L | 收藏入口 | L1 图片选择器星标 | L2 长按 / L3 两者 |
| M | 抽屉形态 | M1 半屏 sheet | M2 popover / M3 键盘栏 |
| N | 抽屉入口 | N1 工具栏独立按钮 | N2 长按菜单 |
| O | 天气定位 | 首次添加请求 CoreLocation 一次 | 城市选择 / 缓存 |
| P | 收藏交互 | 点击添加关闭 / 长按二级确认删除 / 点击空白或下拉关闭 | — |
| Q | 定位失败 | v0.3 修订:弹自选 sheet(颠覆 v0.2 弹提示无兜底) | 引导设置 / 城市兜底 |
| R | 自选 sheet 内容 | v0.3 修订:只含天气图标(颠覆 v0.2 含温度/城市) | 含温度 / 含城市 |
| T | 长按工具栏 | v0.3 新增:弹自选 sheet(无论有无定位) | 其他 |
| GG | 自选字段处理 | v0.3 新增 GG1:温度/位置/湿度/风速设为 nil,UI 自动隐藏 | GG2 固定默认 / GG3 按图标给默认 |
| U | v0.3 颠覆 创建流程 | U_new 模态 NoteStickerEditor sheet(颜色顶 + 文本底) | U1 直接编辑 / U2 先创建空便签 / U3 提前选颜色 |
| E_new | v0.3 颠覆 编辑能力 | E0 贴上后只读(颠覆 E1 双态编辑) | 原 E1 |
| V | 文字超长 | 500 字截断 | V1 滚动 / V3 无限制 |
| W | 复制行为 | 长按复制(HH 详) | 不支持 |
| X | 颜色切换 | 创建时可改,贴上后不可改 | 创建后也可改 |
| Y | 编辑能力 | 只能创建时编辑文字+选颜色 | 贴上后可编辑 |
| Z | 视觉 | Z1 圆角 + 阴影 + 边框 | Z2 胶带 / Z3 极简 |
| HH | 长按复制具体 | 长按 → 复制纯文本到剪贴板 + Toast「已复制」 | HH2 新便签 / HH3 菜单 |
| II | 颜色选择 UI 位置 | 模态 sheet 顶部(4 色按钮横排) | II2 便签右上 / II3 提前选 |
| JJ | 空便签校验 | 空文本不创建(点击其他区域也不创建) | 保留占位文字 |
| CC | 多行换行 | 支持多行 + 大文本框 + 500 字限制 | 单行 |
| DD | v0.3 颠覆 文本格式 | DD_new 富文本(颠覆 DD1 纯文本) | 原 DD1 |
| FF | 撤销/重做 | 不实现 | FF1 摇晃撤销 / FF3 按钮 |
| KK | 富文本范围 | KK1 仅链接识别(不提供粗体/斜体/字号工具栏) | KK2/KK3 |
主上 32 项决策全部闭环。
附录 C. 后续可扩展(v0.3 不含,留作未来)
- 动图贴纸(GIFContent):仅需 Content struct + Renderer + 注册一行
- Logo 贴纸:同上
- 任务清单贴纸:交互式 checkbox,需要状态管理
- 模板贴纸:复合贴纸(多个子贴纸组合)
- 贴纸分组/文件夹:抽屉升级版
- 贴纸使用频次追踪:从 FavoriteStickerEntity 反查 sourceFavoriteID
附录 D. v0.1 → v0.2 → v0.3 关键差异
| 维度 | v0.1 | v0.2 | v0.3 |
|---|---|---|---|
| 范围 | 仅便签条 | 完整贴纸系统扩展 | 同 v0.2 |
| 抽象 | 无 | L1 协议+泛型+注册表 | 同 v0.2 |
| 数据语义 | 实时 | 全快照 | 同 v0.2 |
| 持久化 | 单 NoteStickerEntity | 4 个新实体 | text 改 Data 字段(富文本) |
| 视图 | 独立 NoteStickerOverlay | 泛型 StickerOverlay |
新增便签编辑器 sheet / 天气选择 sheet / Toast |
| 扩展性 | 不可扩 | 标准扩展点,零侵入 | 同 v0.2 |
| 收藏功能 | 无 | 资产/实例分离 + 抽屉 | 同 v0.2 |
| 便签创建 | — | 工具栏点击 | 模态 sheet(U_new) |
| 便签编辑 | — | 双态可编辑 | 贴上后只读(E0) |
| 便签文本 | — | 纯文本 | 富文本 + 链接识别(DD_new/KK1) |
| 天气创建 | — | 调 API 单模式 | 双模式(点击 + 长按 sheet) |
| 自选天气 | — | 含温度/城市 | 只选图标(R) |
| 定位失败 | — | 弹提示无兑底 | 弹自选 sheet(Q 修订) |
文档完。下一步:创建分支 feature/sticker-system-v0.3,按附录 B 之 32 项决策及第 16 节之 12 阶段实施步骤落实施工。
最后核对:2026-06-02 14:46 GMT+8
所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。
