feat(settings): add quick messages management with drag-and-drop sorting

Adds a new "Quick Messages" settings page below Experts for managing reusable title/content snippets, backed by SQLite via SeaORM and exposed through both Tauri commands and the Axum web router. The list supports drag-to-reorder using the same motion/react Reorder pattern as the agent list, with translations provided across all 10 supported locales.
This commit is contained in:
xintaofei
2026-04-24 10:46:33 +08:00
parent fbe272de4f
commit 61778f152b
30 changed files with 1434 additions and 11 deletions

View File

@@ -9,3 +9,4 @@ pub mod folder_command;
pub mod model_provider;
pub mod opened_tab;
pub mod prelude;
pub mod quick_message;

View File

@@ -10,3 +10,4 @@ pub use super::folder::Entity as Folder;
pub use super::folder_command::Entity as FolderCommand;
pub use super::model_provider::Entity as ModelProvider;
pub use super::opened_tab::Entity as OpenedTab;
pub use super::quick_message::Entity as QuickMessage;

View File

@@ -0,0 +1,19 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "quick_message")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
#[sea_orm(column_type = "Text")]
pub content: String,
pub sort_order: i32,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,70 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(QuickMessage::Table)
.if_not_exists()
.col(
ColumnDef::new(QuickMessage::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(QuickMessage::Title).string().not_null())
.col(ColumnDef::new(QuickMessage::Content).text().not_null())
.col(
ColumnDef::new(QuickMessage::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(QuickMessage::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(QuickMessage::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_quick_message_sort_order")
.table(QuickMessage::Table)
.col(QuickMessage::SortOrder)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(QuickMessage::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum QuickMessage {
Table,
Id,
Title,
Content,
SortOrder,
CreatedAt,
UpdatedAt,
}

View File

@@ -13,6 +13,7 @@ mod m20260420_000001_opened_tabs;
mod m20260422_000001_folder_sort_order;
mod m20260423_000001_drop_folder_parent_branch;
mod m20260424_000001_folder_color;
mod m20260424_000002_quick_message;
pub struct Migrator;
#[async_trait::async_trait]
@@ -32,6 +33,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260422_000001_folder_sort_order::Migration),
Box::new(m20260423_000001_drop_folder_parent_branch::Migration),
Box::new(m20260424_000001_folder_color::Migration),
Box::new(m20260424_000002_quick_message::Migration),
]
}
}

View File

@@ -7,5 +7,6 @@ pub mod folder_command_service;
pub mod folder_service;
pub mod import_service;
pub mod model_provider_service;
pub mod quick_message_service;
pub mod sender_context_service;
pub mod tab_service;

View File

@@ -0,0 +1,114 @@
use chrono::Utc;
use sea_orm::DatabaseConnection;
use sea_orm::{
ActiveModelTrait, ActiveValue::NotSet, ConnectionTrait, DbBackend, EntityTrait,
IntoActiveModel, QueryOrder, Set, Statement,
};
use crate::db::entities::quick_message;
use crate::db::error::DbError;
use crate::models::QuickMessageInfo;
fn to_info(m: quick_message::Model) -> QuickMessageInfo {
QuickMessageInfo {
id: m.id,
title: m.title,
content: m.content,
sort_order: m.sort_order,
created_at: m.created_at,
updated_at: m.updated_at,
}
}
pub async fn list(conn: &DatabaseConnection) -> Result<Vec<QuickMessageInfo>, DbError> {
let rows = quick_message::Entity::find()
.order_by_asc(quick_message::Column::SortOrder)
.all(conn)
.await?;
Ok(rows.into_iter().map(to_info).collect())
}
pub async fn create(
conn: &DatabaseConnection,
title: &str,
content: &str,
) -> Result<QuickMessageInfo, DbError> {
let now = Utc::now();
let max_order = quick_message::Entity::find()
.order_by_desc(quick_message::Column::SortOrder)
.one(conn)
.await?
.map(|m| m.sort_order)
.unwrap_or(-1);
let active = quick_message::ActiveModel {
id: NotSet,
title: Set(title.to_string()),
content: Set(content.to_string()),
sort_order: Set(max_order + 1),
created_at: Set(now),
updated_at: Set(now),
};
let model = active.insert(conn).await?;
Ok(to_info(model))
}
pub async fn update(
conn: &DatabaseConnection,
id: i32,
title: Option<String>,
content: Option<String>,
) -> Result<QuickMessageInfo, DbError> {
let row = quick_message::Entity::find_by_id(id)
.one(conn)
.await?
.ok_or_else(|| DbError::Migration(format!("QuickMessage {} not found", id)))?;
let mut active = row.into_active_model();
if let Some(t) = title {
active.title = Set(t);
}
if let Some(c) = content {
active.content = Set(c);
}
active.updated_at = Set(Utc::now());
let model = active.update(conn).await?;
Ok(to_info(model))
}
pub async fn delete(conn: &DatabaseConnection, id: i32) -> Result<(), DbError> {
quick_message::Entity::delete_by_id(id).exec(conn).await?;
Ok(())
}
pub async fn reorder(conn: &DatabaseConnection, ids: Vec<i32>) -> Result<(), DbError> {
if ids.is_empty() {
return Ok(());
}
let now = Utc::now();
let now_str = now.format("%Y-%m-%d %H:%M:%S %:z").to_string();
let case_expr = ids
.iter()
.enumerate()
.map(|(idx, id)| format!("WHEN {} THEN {}", id, idx))
.collect::<Vec<_>>()
.join(" ");
let id_list = ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ");
let sql = format!(
"UPDATE quick_message SET sort_order = CASE id {case_expr} END, updated_at = '{now_str}' WHERE id IN ({id_list})"
);
conn.execute(Statement::from_string(DbBackend::Sqlite, sql))
.await?;
Ok(())
}