- op.rs: 纯函数 apply_op(deck,op) → new_deck,覆盖全部 11 种 Op 类型 - invert_op(deck,op) → 逆 Op,支持完整的 undo 链路 - 所有 reducer 不 mutate 输入,返回新 deck - 8 个单元测试: add_slide/del_slide/set_theme/update_element/del_element/ move_slide/undo_update/undo_add_slide/replace_deck_undo - ElementStyle 加 rename_all=camelCase 匹配 JS 协议 - 32 个测试全部通过(B1:23 + B2:9)
309 lines
7.5 KiB
Rust
309 lines
7.5 KiB
Rust
/* =====================================================================
|
||
* model.rs — 数据模型(对应 src/core/types.ts)
|
||
*
|
||
* 所有结构体 derive Serialize/Deserialize/TS,确保:
|
||
* - serde: JSON 序列化(Rust ↔ IPC ↔ JS)
|
||
* - ts-rs: 自动生成 TypeScript 类型定义(Rust ↔ TS 类型同步)
|
||
*
|
||
* 运行 `cargo test --features export-types` 会在 ../bindings/ 生成 .ts 文件。
|
||
* ===================================================================== */
|
||
use serde::{Deserialize, Serialize};
|
||
use ts_rs::TS;
|
||
|
||
/* ---------- 颜色 / 背景 / 动画 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub enum ChartType {
|
||
#[serde(rename = "bar")]
|
||
Bar,
|
||
#[serde(rename = "line")]
|
||
Line,
|
||
#[serde(rename = "pie")]
|
||
Pie,
|
||
#[serde(rename = "area")]
|
||
Area,
|
||
#[serde(rename = "doughnut")]
|
||
Doughnut,
|
||
#[serde(rename = "radar")]
|
||
Radar,
|
||
#[serde(rename = "hbar")]
|
||
Hbar,
|
||
#[serde(rename = "progress")]
|
||
Progress,
|
||
}
|
||
|
||
impl Default for ChartType {
|
||
fn default() -> Self {
|
||
ChartType::Bar
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub enum ShapeType {
|
||
#[serde(rename = "rect")]
|
||
Rect,
|
||
#[serde(rename = "circle")]
|
||
Circle,
|
||
#[serde(rename = "triangle")]
|
||
Triangle,
|
||
}
|
||
|
||
impl Default for ShapeType {
|
||
fn default() -> Self {
|
||
ShapeType::Rect
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub enum AnimType {
|
||
#[serde(rename = "fade-up")]
|
||
FadeUp,
|
||
#[serde(rename = "fade")]
|
||
Fade,
|
||
#[serde(rename = "scale")]
|
||
Scale,
|
||
#[serde(rename = "zoom")]
|
||
Zoom,
|
||
#[serde(rename = "slide-l")]
|
||
SlideL,
|
||
#[serde(rename = "slide-r")]
|
||
SlideR,
|
||
#[serde(rename = "pop")]
|
||
Pop,
|
||
#[serde(rename = "rotate")]
|
||
Rotate,
|
||
#[serde(rename = "bounce")]
|
||
Bounce,
|
||
#[serde(rename = "flip")]
|
||
Flip,
|
||
#[serde(rename = "blur")]
|
||
Blur,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub enum ElementType {
|
||
#[serde(rename = "title")]
|
||
Title,
|
||
#[serde(rename = "text")]
|
||
Text,
|
||
#[serde(rename = "list")]
|
||
List,
|
||
#[serde(rename = "stat")]
|
||
Stat,
|
||
#[serde(rename = "quote")]
|
||
Quote,
|
||
#[serde(rename = "image")]
|
||
Image,
|
||
#[serde(rename = "shape")]
|
||
Shape,
|
||
#[serde(rename = "chart")]
|
||
Chart,
|
||
#[serde(rename = "card")]
|
||
Card,
|
||
#[serde(rename = "table")]
|
||
Table,
|
||
#[serde(rename = "code")]
|
||
Code,
|
||
#[serde(rename = "formula")]
|
||
Formula,
|
||
}
|
||
|
||
/* ---------- 主题 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct Theme {
|
||
pub name: String,
|
||
pub primary: String,
|
||
pub accent: String,
|
||
pub bg: String,
|
||
pub panel: String,
|
||
pub text: String,
|
||
pub muted: String,
|
||
}
|
||
|
||
/* ---------- 富文本 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct RichSegment {
|
||
pub text: String,
|
||
pub bold: Option<bool>,
|
||
pub italic: Option<bool>,
|
||
pub underline: Option<bool>,
|
||
pub strike: Option<bool>,
|
||
pub color: Option<String>,
|
||
pub highlight: Option<bool>,
|
||
pub code: Option<bool>,
|
||
#[serde(rename = "sup")]
|
||
pub sup_: Option<bool>,
|
||
#[serde(rename = "sub")]
|
||
pub sub_: Option<bool>,
|
||
pub font_size: Option<f64>,
|
||
pub link: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct RichLine {
|
||
pub segments: Vec<RichSegment>,
|
||
}
|
||
|
||
/* ---------- 元素 ---------- */
|
||
|
||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ElementStyle {
|
||
pub font_size: Option<f64>,
|
||
pub color: Option<String>,
|
||
pub align: Option<String>,
|
||
pub bold: Option<bool>,
|
||
pub italic: Option<bool>,
|
||
pub anim: Option<String>,
|
||
// stat
|
||
pub label: Option<String>,
|
||
pub label_color: Option<String>,
|
||
pub label_size: Option<f64>,
|
||
// image
|
||
pub fit: Option<String>,
|
||
// shape
|
||
pub shape_type: Option<ShapeType>,
|
||
pub fill: Option<String>,
|
||
pub gradient: Option<bool>,
|
||
pub opacity: Option<f64>,
|
||
pub radius: Option<f64>,
|
||
// chart
|
||
pub max: Option<f64>,
|
||
pub chart_type: Option<ChartType>,
|
||
pub legend: Option<bool>,
|
||
pub stack: Option<bool>,
|
||
pub grid: Option<bool>,
|
||
// table
|
||
pub header: Option<bool>,
|
||
// code
|
||
pub lang: Option<String>,
|
||
// formula
|
||
#[serde(rename = "inline")]
|
||
pub inline_: Option<bool>,
|
||
// card
|
||
pub icon: Option<String>,
|
||
pub accent: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct SlideElement {
|
||
pub id: String,
|
||
#[serde(rename = "type")]
|
||
pub element_type: String,
|
||
pub x: f64,
|
||
pub y: f64,
|
||
pub w: f64,
|
||
pub h: f64,
|
||
pub content: String,
|
||
pub style: ElementStyle,
|
||
pub segments: Option<Vec<RichLine>>,
|
||
}
|
||
|
||
/* ---------- 幻灯片 / Deck ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct Slide {
|
||
pub id: String,
|
||
pub background: String,
|
||
pub elements: Vec<SlideElement>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct Deck {
|
||
pub v: i32,
|
||
pub theme: String,
|
||
pub slides: Vec<Slide>,
|
||
pub chat_id: Option<String>,
|
||
}
|
||
|
||
/* ---------- AI 配置 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct AiCfg {
|
||
pub preset: String,
|
||
pub protocol: String,
|
||
pub base: String,
|
||
pub key: String,
|
||
pub model: String,
|
||
pub proxy: String,
|
||
pub img_base: Option<String>,
|
||
pub img_key: Option<String>,
|
||
pub img_model: Option<String>,
|
||
}
|
||
|
||
/* ---------- 文库 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct LibItem {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub deck: Deck,
|
||
pub created_at: f64,
|
||
pub updated_at: f64,
|
||
}
|
||
|
||
/* ---------- 模板 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct PageTemplate {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub category: String,
|
||
pub background: String,
|
||
pub elements: Vec<SlideElement>,
|
||
pub created_at: Option<f64>,
|
||
}
|
||
|
||
/* ---------- 会话 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct ChatMessage {
|
||
pub role: String,
|
||
pub content: String,
|
||
}
|
||
|
||
/* ---------- Op Log(对应 src/core/op.ts) ---------- */
|
||
|
||
/// Op 的 payload 是任意 JSON(不导出 TS,因为 payload 是动态的)
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct Op {
|
||
#[serde(rename = "type")]
|
||
pub op_type: String,
|
||
pub client_id: Option<String>,
|
||
pub timestamp: Option<f64>,
|
||
#[serde(flatten)]
|
||
pub payload: serde_json::Value,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct HistoryEntry {
|
||
pub forward: Op,
|
||
pub backward: Op,
|
||
}
|
||
|
||
/* ---------- 图表数据 ---------- */
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||
#[ts(export, export_to = "../bindings/types.ts")]
|
||
pub struct ChartItem {
|
||
pub label: Option<String>,
|
||
pub value: Option<f64>,
|
||
}
|