便签条与贴纸系统扩展 — 设计文档 v0.3

v0.3 文档为 NoteArchive 项目便签贴纸与贴纸系统扩展的完整设计规格。涵盖 L1 抽象层(协议+泛型+注册表)、便签条、日期贴纸、天气贴纸、表情收藏与抽屉共五类贴纸的实现方案。32 项关键决策全数闭环,配套 ~9.5h 实施步骤。

最后编辑时间:BenDJoaner

便签条与贴纸系统扩展 — 设计文档 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: StringattributedText: 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)&current=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: 均为 nil
  • provider: "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:加载所有 store
  • onDisappear:保存所有 store
  • deleteCurrentPage / 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/appendEmptyPage
  • WeatherRouterTests:Mock Storefront + Mock WeatherProvider
  • LunarConversionTests:公历→农历转换(边界:闰月、立春切换)
  • 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.swiftImageRenderer.swift
  • StickerStore.swift(旧版,迁移至泛型 StickerStore<T>,旧文件可删除)
  • ContentView.swiftCategoryDetailView.swift
  • Persistence.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 + 4 Renderer 新增便签编辑器 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

  • 留下精彩的评论吧~(0条)

所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。