新增: 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:
39
src-tauri/Cargo.lock
generated
39
src-tauri/Cargo.lock
generated
@@ -1589,6 +1589,12 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.9.0"
|
||||
@@ -3149,6 +3155,15 @@ dependencies = [
|
||||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -3483,6 +3498,29 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "10.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"thiserror 2.0.18",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "10.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -3503,6 +3541,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -17,3 +17,8 @@ tauri-build = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ts-rs = { version = "10", features = ["no-serde-warnings"] }
|
||||
|
||||
[features]
|
||||
# 默认不导出类型(避免 CI 构建 fail)
|
||||
default = []
|
||||
|
||||
33
src-tauri/bindings/types.ts
Normal file
33
src-tauri/bindings/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AiCfg = { preset: string, protocol: string, base: string, key: string, model: string, proxy: string, img_base: string | null, img_key: string | null, img_model: string | null, };
|
||||
|
||||
export type AnimType = "fade-up" | "fade" | "scale" | "zoom" | "slide-l" | "slide-r" | "pop" | "rotate" | "bounce" | "flip" | "blur";
|
||||
|
||||
export type ChartItem = { label: string | null, value: number | null, };
|
||||
|
||||
export type ChartType = "bar" | "line" | "pie" | "area" | "doughnut" | "radar" | "hbar" | "progress";
|
||||
|
||||
export type ChatMessage = { role: string, content: string, };
|
||||
|
||||
export type Deck = { v: number, theme: string, slides: Array<Slide>, chat_id: string | null, };
|
||||
|
||||
export type ElementStyle = { font_size: number | null, color: string | null, align: string | null, bold: boolean | null, italic: boolean | null, anim: string | null, label: string | null, label_color: string | null, label_size: number | null, fit: string | null, shape_type: ShapeType | null, fill: string | null, gradient: boolean | null, opacity: number | null, radius: number | null, max: number | null, chart_type: ChartType | null, legend: boolean | null, stack: boolean | null, grid: boolean | null, header: boolean | null, lang: string | null, inline: boolean | null, icon: string | null, accent: string | null, };
|
||||
|
||||
export type ElementType = "title" | "text" | "list" | "stat" | "quote" | "image" | "shape" | "chart" | "card" | "table" | "code" | "formula";
|
||||
|
||||
export type LibItem = { id: string, name: string, deck: Deck, created_at: number, updated_at: number, };
|
||||
|
||||
export type PageTemplate = { id: string, name: string, category: string, background: string, elements: Array<SlideElement>, created_at: number | null, };
|
||||
|
||||
export type RichLine = { segments: Array<RichSegment>, };
|
||||
|
||||
export type RichSegment = { text: string, bold: boolean | null, italic: boolean | null, underline: boolean | null, strike: boolean | null, color: string | null, highlight: boolean | null, code: boolean | null, sup: boolean | null, sub: boolean | null, font_size: number | null, link: string | null, };
|
||||
|
||||
export type ShapeType = "rect" | "circle" | "triangle";
|
||||
|
||||
export type Slide = { id: string, background: string, elements: Array<SlideElement>, };
|
||||
|
||||
export type SlideElement = { id: string, type: string, x: number, y: number, w: number, h: number, content: string, style: ElementStyle, segments: Array<RichLine> | null, };
|
||||
|
||||
export type Theme = { name: string, primary: string, accent: string, bg: string, panel: string, text: string, muted: string, };
|
||||
212
src-tauri/src/color.rs
Normal file
212
src-tauri/src/color.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
/* =====================================================================
|
||||
* color.rs — 颜色/背景解析纯函数(对应 store.ts 的颜色函数)
|
||||
*
|
||||
* 这些函数供 Rust 端的渲染/序列化使用(B2 store 迁移时调用)。
|
||||
* 当前阶段保持与 TS 版本完全一致的逻辑。
|
||||
* ===================================================================== */
|
||||
|
||||
pub fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
|
||||
let c = hex.trim_start_matches('#');
|
||||
let full = if c.len() == 3 {
|
||||
format!(
|
||||
"{}{}{}{}{}{}",
|
||||
&c[0..1],
|
||||
&c[0..1],
|
||||
&c[1..2],
|
||||
&c[1..2],
|
||||
&c[2..3],
|
||||
&c[2..3]
|
||||
)
|
||||
} else {
|
||||
c.to_string()
|
||||
};
|
||||
if full.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&full[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&full[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&full[4..6], 16).ok()?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
pub fn is_dark_hex(hex: &str) -> bool {
|
||||
match hex_to_rgb(hex) {
|
||||
Some((r, g, b)) => (0.299 * r as f64 + 0.587 * g as f64 + 0.114 * b as f64) < 145.0,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shade(hex: &str, pct: f64) -> String {
|
||||
let (r, g, b) = match hex_to_rgb(hex) {
|
||||
Some(rgb) => rgb,
|
||||
None => return hex.to_string(),
|
||||
};
|
||||
let f = if pct < 0.0 { 0.0 } else { 255.0 };
|
||||
let p = pct.abs() / 100.0;
|
||||
let nr = ((f - r as f64) * p + r as f64).round() as u8;
|
||||
let ng = ((f - g as f64) * p + g as f64).round() as u8;
|
||||
let nb = ((f - b as f64) * p + b as f64).round() as u8;
|
||||
format!("#{:02x}{:02x}{:02x}", nr, ng, nb)
|
||||
}
|
||||
|
||||
pub fn is_dark_bg(bg: &str) -> bool {
|
||||
if bg.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if bg.starts_with('#') {
|
||||
return is_dark_hex(bg);
|
||||
}
|
||||
if bg == "primary" || bg == "accent" {
|
||||
return true;
|
||||
}
|
||||
if bg.starts_with("g-") {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_valid_hex(s: &str) -> bool {
|
||||
s.starts_with('#')
|
||||
&& s.len() >= 4
|
||||
&& s[1..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
/// 颜色解析:dark=true 时深色文字键反相为浅色
|
||||
pub fn resolve_color(key: &str, dark: bool, theme: &crate::model::Theme) -> String {
|
||||
if key.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if key.starts_with('#') {
|
||||
if !is_valid_hex(key) {
|
||||
return if dark {
|
||||
"#ffffff".into()
|
||||
} else {
|
||||
theme.text.clone()
|
||||
};
|
||||
}
|
||||
if dark && is_dark_hex(key) {
|
||||
return "#ffffff".into();
|
||||
}
|
||||
return key.into();
|
||||
}
|
||||
if dark {
|
||||
match key {
|
||||
"text" => "#ffffff".into(),
|
||||
"muted" => "rgba(255,255,255,0.72)".into(),
|
||||
"primary" => "#ffffff".into(),
|
||||
"accent" => theme.accent.clone(),
|
||||
_ => "#ffffff".into(),
|
||||
}
|
||||
} else {
|
||||
match key {
|
||||
"primary" => theme.primary.clone(),
|
||||
"accent" => theme.accent.clone(),
|
||||
"text" => theme.text.clone(),
|
||||
"muted" => theme.muted.clone(),
|
||||
_ => theme.text.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 背景解析:渐变键 → CSS gradient
|
||||
pub fn resolve_bg(key: &str, theme: &crate::model::Theme) -> String {
|
||||
if key.is_empty() {
|
||||
return "#ffffff".into();
|
||||
}
|
||||
if key.starts_with('#') {
|
||||
return if is_valid_hex(key) {
|
||||
key.into()
|
||||
} else {
|
||||
"#ffffff".into()
|
||||
};
|
||||
}
|
||||
match key {
|
||||
"g-primary" => format!(
|
||||
"linear-gradient(135deg, {} 0%, {} 100%)",
|
||||
theme.primary, theme.accent
|
||||
),
|
||||
"g-deep" => format!(
|
||||
"linear-gradient(160deg, {} 0%, {} 100%)",
|
||||
shade(&theme.primary, -28.0),
|
||||
theme.primary
|
||||
),
|
||||
"g-soft" => format!(
|
||||
"linear-gradient(135deg, {} 0%, {} 100%)",
|
||||
theme.panel.clone(),
|
||||
theme.bg.clone()
|
||||
),
|
||||
"primary" => theme.primary.clone(),
|
||||
"accent" => theme.accent.clone(),
|
||||
"panel" => theme.panel.clone(),
|
||||
"bg" => theme.bg.clone(),
|
||||
_ => "#ffffff".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_rgb() {
|
||||
assert_eq!(hex_to_rgb("#4f46e5"), Some((79, 70, 229)));
|
||||
assert_eq!(hex_to_rgb("#fff"), Some((255, 255, 255)));
|
||||
assert_eq!(hex_to_rgb("invalid"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_dark_hex() {
|
||||
assert!(is_dark_hex("#0f172a")); // 深色
|
||||
assert!(is_dark_hex("#4f46e5")); // 青蓝偏深
|
||||
assert!(!is_dark_hex("#ffffff")); // 白色
|
||||
assert!(!is_dark_hex("#f59e0b")); // 琥珀亮色
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_dark_bg() {
|
||||
assert!(is_dark_bg("primary"));
|
||||
assert!(is_dark_bg("g-deep"));
|
||||
assert!(!is_dark_bg("bg"));
|
||||
assert!(!is_dark_bg("panel"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shade() {
|
||||
let result = shade("#000000", 50.0);
|
||||
assert_eq!(result, "#808080"); // 黑色提亮 50% → 灰色
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_color_light() {
|
||||
use crate::model::Theme;
|
||||
let theme = Theme {
|
||||
name: "test".into(),
|
||||
primary: "#4f46e5".into(),
|
||||
accent: "#06b6d4".into(),
|
||||
bg: "#ffffff".into(),
|
||||
panel: "#f8fafc".into(),
|
||||
text: "#1e293b".into(),
|
||||
muted: "#64748b".into(),
|
||||
};
|
||||
assert_eq!(resolve_color("primary", false, &theme), "#4f46e5");
|
||||
assert_eq!(resolve_color("text", false, &theme), "#1e293b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_color_dark() {
|
||||
use crate::model::Theme;
|
||||
let theme = Theme {
|
||||
name: "test".into(),
|
||||
primary: "#4f46e5".into(),
|
||||
accent: "#06b6d4".into(),
|
||||
bg: "#ffffff".into(),
|
||||
panel: "#f8fafc".into(),
|
||||
text: "#1e293b".into(),
|
||||
muted: "#64748b".into(),
|
||||
};
|
||||
// 深色背景下 text 键 → 白色
|
||||
assert_eq!(resolve_color("text", true, &theme), "#ffffff");
|
||||
// primary 键在深色背景下也 → 白色
|
||||
assert_eq!(resolve_color("primary", true, &theme), "#ffffff");
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,44 @@
|
||||
/* =====================================================================
|
||||
* lib.rs — Tauri 应用入口(B0 脚手架)
|
||||
* lib.rs — Tauri 应用入口
|
||||
*
|
||||
* 当前阶段:Vue 前端原样跑在 WebView 里,Rust 端只有 ping 验证 IPC。
|
||||
* 后续 B1-B5 逐步迁移核心逻辑到 Rust。
|
||||
* B0: 脚手架 + ping IPC
|
||||
* B1: 数据模型(model.rs)+ 颜色纯函数(color.rs)
|
||||
* B2: store 迁移(待实现)
|
||||
* B3: AI 引擎迁移(待实现)
|
||||
* ===================================================================== */
|
||||
|
||||
/// IPC 验证命令:前端 invoke('ping') → 返回 'pong'
|
||||
mod color;
|
||||
mod model;
|
||||
|
||||
/// IPC 验证命令
|
||||
#[tauri::command]
|
||||
fn ping() -> String {
|
||||
"pong".to_string()
|
||||
}
|
||||
|
||||
/// 返回应用版本信息
|
||||
#[tauri::command]
|
||||
fn app_info() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"name": "u-ppt",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"platform": std::env::consts::OS,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![ping])
|
||||
.invoke_handler(tauri::generate_handler![ping, app_info])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ping() {
|
||||
assert_eq!(ping(), "pong");
|
||||
}
|
||||
}
|
||||
|
||||
307
src-tauri/src/model.rs
Normal file
307
src-tauri/src/model.rs
Normal 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>,
|
||||
}
|
||||
Reference in New Issue
Block a user