新增: Rust 数据模型 + 颜色纯函数 + ts-rs 类型同步(B1)

- model.rs: 全部数据结构(Deck/Slide/Element/Style/Theme/AiCfg/LibItem/PageTemplate/ChatMessage/Op/RichSegment)
  所有结构体 derive Serialize/Deserialize/TS,自动生成 TS 类型到 bindings/types.ts
- color.rs: 颜色解析纯函数(resolve_color/resolve_bg/is_dark_bg/shade/hex_to_rgb)
  对应 store.ts 的颜色逻辑,23 个单元测试全部通过
- lib.rs: 新增 app_info IPC 命令
- ts-rs 自动生成的 bindings/types.ts 与手写 types.ts 类型一致
This commit is contained in:
2026-07-12 18:20:49 +08:00
parent b9a388b570
commit 1430e9e061
6 changed files with 626 additions and 5 deletions

307
src-tauri/src/model.rs Normal file
View File

@@ -0,0 +1,307 @@
/* =====================================================================
* 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")]
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>,
}