From e4d9ee0c851582a905a3c2f5e8a6844cd5bab185 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Sun, 29 Mar 2026 18:17:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=97=B4=E9=9A=94=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=8A=A0=E5=88=86=E5=8F=AF=E4=BB=A5=E6=8C=89=E7=85=A7=E5=A4=9A?= =?UTF-8?q?=E5=B0=91=E5=A4=A9=E3=80=81=E6=9C=88=E3=80=81=E5=88=86=E9=92=9F?= =?UTF-8?q?=E5=90=8E=E5=8A=A0=E5=88=86=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=8B=A5=E5=B9=B2=E9=97=AE=E9=A2=98=20=E4=B8=8B=E9=9B=A8?= =?UTF-8?q?=E4=BA=86=E5=A5=BD=E5=AE=B3=E6=80=95=E6=88=91=E9=9D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/board.rs | 7 +- src-tauri/src/commands/database.rs | 25 +-- src-tauri/src/commands/student.rs | 19 ++- src-tauri/src/lib.rs | 7 +- src-tauri/src/services/auto_score.rs | 80 ++++++++-- src-tauri/src/services/settings.rs | 4 +- src/assets/main.css | 3 + src/components/AutoScoreManager.tsx | 50 ++++-- .../AutoScoreManagerPage/ActionEditor.tsx | 47 +++++- .../AutoScoreManagerPage/AutoScoreUtils.ts | 143 ++++++++++++++---- .../IntervalValueCodec.ts | 79 ++++++++++ .../IntervalValueWidget.tsx | 118 +++++++++++++++ src/i18n/locales/en-US.json | 4 + src/i18n/locales/zh-CN.json | 4 + 14 files changed, 507 insertions(+), 83 deletions(-) create mode 100644 src/components/AutoScoreManagerPage/IntervalValueCodec.ts create mode 100644 src/components/AutoScoreManagerPage/IntervalValueWidget.tsx diff --git a/src-tauri/src/commands/board.rs b/src-tauri/src/commands/board.rs index 6459962..43f387b 100644 --- a/src-tauri/src/commands/board.rs +++ b/src-tauri/src/commands/board.rs @@ -1,6 +1,6 @@ use parking_lot::RwLock; -use serde::Deserialize; use sea_orm::{ConnectionTrait, DatabaseConnection, Statement}; +use serde::Deserialize; use serde_json::{Map, Value as JsonValue}; use sqlx::{Column, Row, SqlitePool}; use std::collections::HashSet; @@ -155,7 +155,10 @@ async fn get_board_configs_raw(conn: &DatabaseConnection) -> Result Result<(), String> { +async fn upsert_board_configs_raw( + conn: &DatabaseConnection, + config_json: &str, +) -> Result<(), String> { let config_escaped = escape_sql(config_json); let now_escaped = escape_sql(&now_iso()); let sql = format!( diff --git a/src-tauri/src/commands/database.rs b/src-tauri/src/commands/database.rs index dd2f8bb..ec92403 100644 --- a/src-tauri/src/commands/database.rs +++ b/src-tauri/src/commands/database.rs @@ -1150,7 +1150,10 @@ pub async fn db_switch_connection( let logger = state_guard.logger.read(); logger.log( LogLevel::Info, - &format!("Database switch requested, connection_string length: {}", connection_string.len()), + &format!( + "Database switch requested, connection_string length: {}", + connection_string.len() + ), Some("database"), None, ); @@ -1225,17 +1228,15 @@ pub async fn db_switch_connection( connection_string }; - let conn = create_sqlite_connection(&path) - .await - .map_err(|e| { - logger.log( - LogLevel::Error, - &format!("SQLite connection failed: {}", e), - Some("database"), - None, - ); - e.to_string() - })?; + let conn = create_sqlite_connection(&path).await.map_err(|e| { + logger.log( + LogLevel::Error, + &format!("SQLite connection failed: {}", e), + Some("database"), + None, + ); + e.to_string() + })?; run_migration(&conn, DatabaseType::SQLite) .await .map_err(|e| { diff --git a/src-tauri/src/commands/student.rs b/src-tauri/src/commands/student.rs index 48d3027..65539a4 100644 --- a/src-tauri/src/commands/student.rs +++ b/src-tauri/src/commands/student.rs @@ -313,7 +313,9 @@ async fn post_banyou_action( params: serde_json::Value, ) -> Result { let timestamp = chrono::Utc::now().timestamp_millis(); - let url = format!("https://care.seewo.com/app/apis.json?action={action}×tamp={timestamp}&isAjax=1"); + let url = format!( + "https://care.seewo.com/app/apis.json?action={action}×tamp={timestamp}&isAjax=1" + ); let payload = serde_json::json!({ "action": action, "params": params, @@ -371,7 +373,11 @@ async fn post_banyou_action( "body_preview": first_n_chars(&body, 500), })), ); - return Err(format!("班优接口请求失败({} HTTP {})", action, status.as_u16())); + return Err(format!( + "班优接口请求失败({} HTTP {})", + action, + status.as_u16() + )); } let parsed: BanYouApiResponse = serde_json::from_str(&body).map_err(|e| { @@ -993,7 +999,9 @@ pub async fn student_fetch_banyou_classroom_detail( (Vec::new(), None) }; - let final_team_plan_id = params.team_plan_id.or_else(|| team_plans.first().map(|p| p.team_plan_id)); + let final_team_plan_id = params + .team_plan_id + .or_else(|| team_plans.first().map(|p| p.team_plan_id)); let group_data = if let Some(team_plan_id) = final_team_plan_id { match post_banyou_action::( @@ -1032,7 +1040,10 @@ pub async fn student_fetch_banyou_classroom_detail( let detail = BanYouClassroomDetailData { medals, students: students_data.students, - teams: group_data.as_ref().map(|d| d.teams.clone()).unwrap_or_default(), + teams: group_data + .as_ref() + .map(|d| d.teams.clone()) + .unwrap_or_default(), ungrouped_students: group_data .as_ref() .map(|d| d.students.clone()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 15b3e06..68f2407 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -292,9 +292,10 @@ fn setup_database(app: &mut App) -> Result<(), Box> { *db_guard = Some(active_conn); } - state_guard.initialize().await.map_err(|e| { - format!("Failed to initialize app state: {}", e) - })?; + state_guard + .initialize() + .await + .map_err(|e| format!("Failed to initialize app state: {}", e))?; Ok::<_, Box>(()) }); diff --git a/src-tauri/src/services/auto_score.rs b/src-tauri/src/services/auto_score.rs index 5cf4e03..bfaefc9 100644 --- a/src-tauri/src/services/auto_score.rs +++ b/src-tauri/src/services/auto_score.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Months, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::{AppHandle, Emitter}; @@ -15,6 +15,20 @@ pub struct AutoScoreAction { pub value: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum IntervalUnit { + Minute, + Day, + Month, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct IntervalTriggerValue { + amount: i64, + unit: IntervalUnit, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoScoreRule { pub id: i32, @@ -171,23 +185,17 @@ impl AutoScoreService { for trigger in &rule.triggers { if trigger.event == "interval_time_passed" { - let minutes = trigger - .value + let interval = parse_interval_trigger_value(trigger.value.as_ref()); + let base_time = rule + .last_executed .as_ref() - .and_then(|v| v.parse::().ok()) - .unwrap_or(30); - let interval_ms = minutes * 60 * 1000; + .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) + .map(|value| value.with_timezone(&Utc)) + .unwrap_or(now); - if let Some(last_executed_str) = &rule.last_executed { - if let Ok(last_executed) = DateTime::parse_from_rfc3339(last_executed_str) { - let last_executed_utc: DateTime = last_executed.with_timezone(&Utc); - let next_execute_time = - last_executed_utc + chrono::Duration::milliseconds(interval_ms); - let delay_ms = (next_execute_time - now).num_milliseconds(); - return Some(delay_ms.max(0)); - } - } - return Some(interval_ms); + let next_execute_time = add_interval_to_time(base_time, &interval)?; + let delay_ms = (next_execute_time - now).num_milliseconds(); + return Some(delay_ms.max(0)); } } None @@ -197,3 +205,43 @@ impl AutoScoreService { let _ = app_handle.emit("auto-score:rulesChanged", &self.rules); } } + +fn parse_interval_trigger_value(value: Option<&String>) -> IntervalTriggerValue { + if let Some(raw_value) = value { + if let Ok(minutes) = raw_value.parse::() { + if minutes > 0 { + return IntervalTriggerValue { + amount: minutes, + unit: IntervalUnit::Minute, + }; + } + } + + if let Ok(parsed_value) = serde_json::from_str::(raw_value) { + if parsed_value.amount > 0 { + return parsed_value; + } + } + } + + IntervalTriggerValue { + amount: 30, + unit: IntervalUnit::Minute, + } +} + +fn add_interval_to_time( + base_time: DateTime, + interval: &IntervalTriggerValue, +) -> Option> { + match interval.unit { + IntervalUnit::Minute => { + base_time.checked_add_signed(chrono::Duration::minutes(interval.amount)) + } + IntervalUnit::Day => base_time.checked_add_signed(chrono::Duration::days(interval.amount)), + IntervalUnit::Month => { + let months = u32::try_from(interval.amount).ok()?; + base_time.checked_add_months(Months::new(months)) + } + } +} diff --git a/src-tauri/src/services/settings.rs b/src-tauri/src/services/settings.rs index b41660f..41ea8c2 100644 --- a/src-tauri/src/services/settings.rs +++ b/src-tauri/src/services/settings.rs @@ -431,9 +431,7 @@ impl SettingsService { "settings", ]; return items.iter().all(|item| { - item.as_str() - .map(|s| allowed.contains(&s)) - .unwrap_or(false) + item.as_str().map(|s| allowed.contains(&s)).unwrap_or(false) }); } false diff --git a/src/assets/main.css b/src/assets/main.css index f517b87..ac0acb0 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -92,6 +92,9 @@ select { .ss-sidebar .ant-menu-item { color: var(--ss-sidebar-text, var(--ss-text-main)); + height: 40px !important; + line-height: 42px !important; + border-radius: 6px !important; } .ss-sidebar .ant-menu-item-selected { diff --git a/src/components/AutoScoreManager.tsx b/src/components/AutoScoreManager.tsx index 5eeda21..cdf527a 100644 --- a/src/components/AutoScoreManager.tsx +++ b/src/components/AutoScoreManager.tsx @@ -14,9 +14,10 @@ import { Tag, message, } from "antd" -import { Utils as QbUtils, type ImmutableTree } from "@react-awesome-query-builder/antd" +import { type ImmutableTree } from "@react-awesome-query-builder/antd" import type { ColumnsType } from "antd/es/table" import { useTranslation } from "react-i18next" +import { fetchAllTags } from "./TagEditorDialog" import { ActionEditor } from "./AutoScoreManagerPage/ActionEditor" import { TriggerRuleBuilder } from "./AutoScoreManagerPage/TriggerRuleBuilder" import { @@ -30,6 +31,7 @@ import { triggersToQueryTree, type ActionDraft, type AutoScoreRule, + type AutoScoreTagOption, } from "./AutoScoreManagerPage/AutoScoreUtils" interface StudentItem { @@ -37,6 +39,11 @@ interface StudentItem { name: string } +interface TagItem { + id: number + name: string +} + interface RuleFormValues { name?: string studentNames?: string[] @@ -50,10 +57,20 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element const { t, i18n } = useTranslation() const [form] = Form.useForm() const [messageApi, contextHolder] = message.useMessage() + const [tags, setTags] = useState([]) + + const tagOptions = useMemo( + () => + tags.map((tag) => ({ + label: tag.name, + value: tag.name, + })), + [tags] + ) const triggerConfig = useMemo( - () => createTriggerQueryConfig(t), - [i18n.resolvedLanguage, i18n.language] + () => createTriggerQueryConfig(t, tagOptions), + [i18n.resolvedLanguage, i18n.language, tagOptions] ) const [triggerTree, setTriggerTree] = useState(() => createEmptyTriggerTree(triggerConfig) @@ -67,10 +84,6 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) - useEffect(() => { - setTriggerTree((prevTree) => QbUtils.checkTree(prevTree, triggerConfig)) - }, [triggerConfig]) - useEffect(() => { const maxPage = Math.max(1, Math.ceil(rules.length / pageSize)) if (currentPage > maxPage) { @@ -103,6 +116,19 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element } } + const fetchTags = async () => { + if (!canEdit) return + + try { + const tagList = await fetchAllTags() + if (Array.isArray(tagList)) { + setTags(tagList) + } + } catch { + void 0 + } + } + const fetchRules = async () => { const api = (window as any).api if (!api || !canEdit) return @@ -124,6 +150,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element useEffect(() => { if (!canEdit) return + fetchTags().catch(() => void 0) fetchStudents().catch(() => void 0) fetchRules().catch(() => void 0) }, [canEdit]) @@ -387,10 +414,15 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element config={triggerConfig} value={triggerTree} canEdit={canEdit} - onChange={(nextTree, config) => setTriggerTree(QbUtils.checkTree(nextTree, config))} + onChange={(nextTree) => setTriggerTree(nextTree)} /> - +