From c015ff37688437a425cf9b092bf25a7a444dd9c6 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Tue, 31 Mar 2026 19:28:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AD=97=E4=BD=93=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8A=A0=E8=BD=BD=E4=B8=8D=E4=BA=86=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20feat:=20=E6=95=B4=E7=90=86=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/data.sql | Bin 110592 -> 110592 bytes src-tauri/src/commands/settings.rs | 134 +++++++++++++++ src-tauri/src/lib.rs | 7 +- src-tauri/src/services/settings.rs | 25 +++ src/App.tsx | 31 +++- .../ActionEditor.tsx | 0 .../AutoScoreUtils.ts | 0 .../IntervalValueCodec.ts | 0 .../IntervalValueWidget.tsx | 0 .../TriggerRuleBuilder.tsx | 0 src/components/AutoScoreManager.tsx | 6 +- src/components/{ => OAuth}/OAuthCallback.tsx | 0 src/components/{ => OAuth}/OAuthLogin.tsx | 0 src/components/Settings.tsx | 153 +++++++++++++----- src/preload/types.ts | 4 + 15 files changed, 316 insertions(+), 44 deletions(-) rename src/components/{AutoScoreManagerPage => AutoScore}/ActionEditor.tsx (100%) rename src/components/{AutoScoreManagerPage => AutoScore}/AutoScoreUtils.ts (100%) rename src/components/{AutoScoreManagerPage => AutoScore}/IntervalValueCodec.ts (100%) rename src/components/{AutoScoreManagerPage => AutoScore}/IntervalValueWidget.tsx (100%) rename src/components/{AutoScoreManagerPage => AutoScore}/TriggerRuleBuilder.tsx (100%) rename src/components/{ => OAuth}/OAuthCallback.tsx (100%) rename src/components/{ => OAuth}/OAuthLogin.tsx (100%) diff --git a/src-tauri/data.sql b/src-tauri/data.sql index c8b719d7e76cc8be4136486613277bd871ad7fd4..f82cd1f89141a3aacb5b907f760312a03ffcc2c4 100644 GIT binary patch delta 770 zcma)(!D|yi6vk(EVoXxa%Qh91Rw;y{9wyzHOg6hIOKm-RP{DX;3GMD?1AtZ5Fw-JybO^NT*X75UvX9W7?}pG#;V*(z zJ}wzTuz}z^?7$b;klx2RF+xsbzcyEDzk;3N{=tX>;|P{v zT+ZmQSd=h@B*PJGA?7%qBeEIlFhfw`m>xCk9H%8_aL3Rc)8;t|+**IpdiQ+m^`oy3 z8m$k{wpZ(&2KGUV^4$OAJxyqu<}#|6M45`Rtx@xmp_$ZngtG&8{ Gr+)#oKE)&e delta 150 zcmV;H0BQe#;0A!;29O&8IFTGf0XVT>p%1eGpaT#H0uSi`5AzS{vk`FJ53}hn8z8d; z(EKlwfCK>$lLdeyvk?$~3bUt*dITOJC1Z7Ra%FCGUvy|?ZDn6+WMpA-Yb|7DW?^+~ zbdx`i#SRb-hX4 Vec { + vec![ + "系统默认".to_string(), + "Microsoft YaHei UI".to_string(), + "Microsoft YaHei".to_string(), + "PingFang SC".to_string(), + "Noto Sans CJK SC".to_string(), + "Source Han Sans SC".to_string(), + "Segoe UI".to_string(), + "Arial".to_string(), + "Helvetica Neue".to_string(), + ] +} + +fn normalize_font_name(value: &str) -> String { + value + .trim() + .trim_start_matches('@') + .replace(" (TrueType)", "") + .replace(" (OpenType)", "") + .replace(" (All res)", "") + .trim() + .to_string() +} + +#[cfg(target_os = "windows")] +fn query_system_fonts() -> Vec { + let output = Command::new("reg") + .args([ + "query", + r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", + ]) + .output(); + + let Ok(output) = output else { + return Vec::new(); + }; + if !output.status.success() { + return Vec::new(); + } + + let mut families = BTreeSet::new(); + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if !line.contains("REG_") { + continue; + } + let Some((left, _)) = line.split_once("REG_") else { + continue; + }; + let name = normalize_font_name(left); + if !name.is_empty() { + families.insert(name); + } + } + + families.into_iter().collect() +} + +#[cfg(target_os = "linux")] +fn query_system_fonts() -> Vec { + let output = Command::new("fc-list").args([":", "family"]).output(); + let Ok(output) = output else { + return Vec::new(); + }; + if !output.status.success() { + return Vec::new(); + } + + let mut families = BTreeSet::new(); + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + for part in line.split(',') { + let name = normalize_font_name(part); + if !name.is_empty() { + families.insert(name); + } + } + } + families.into_iter().collect() +} + +#[cfg(target_os = "macos")] +fn query_system_fonts() -> Vec { + let output = Command::new("system_profiler") + .args(["SPFontsDataType", "-detailLevel", "mini"]) + .output(); + let Ok(output) = output else { + return Vec::new(); + }; + if !output.status.success() { + return Vec::new(); + } + + let mut families = BTreeSet::new(); + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let trimmed = line.trim(); + if let Some(name) = trimmed.strip_prefix("Full Name:") { + let normalized = normalize_font_name(name); + if !normalized.is_empty() { + families.insert(normalized); + } + } + } + families.into_iter().collect() +} + +#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] +fn query_system_fonts() -> Vec { + Vec::new() +} + +#[tauri::command] +pub async fn settings_get_system_fonts() -> Result>, String> { + let mut fonts = query_system_fonts(); + if fonts.is_empty() { + fonts = fallback_font_families(); + } else { + let mut merged = BTreeSet::new(); + for item in fallback_font_families() { + merged.insert(item); + } + for item in fonts { + merged.insert(item); + } + fonts = merged.into_iter().collect(); + } + + Ok(IpcResponse::success(fonts)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 791873a..2ab7ea0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -68,6 +68,7 @@ pub fn run() { settings_get_all, settings_get, settings_set, + settings_get_system_fonts, auth_get_status, auth_login, auth_logout, @@ -155,7 +156,11 @@ fn setup_deep_link(app: &mut App) -> Result<(), Box> { use tauri_plugin_deep_link::DeepLinkExt; app.deep_link().on_open_url(move |event| { - let url = event.urls().first().map(|u| u.to_string()).unwrap_or_default(); + let url = event + .urls() + .first() + .map(|u| u.to_string()) + .unwrap_or_default(); if !url.is_empty() { let _ = handle.emit("deep-link://new-url", url); } diff --git a/src-tauri/src/services/settings.rs b/src-tauri/src/services/settings.rs index 41ea8c2..775d749 100644 --- a/src-tauri/src/services/settings.rs +++ b/src-tauri/src/services/settings.rs @@ -10,6 +10,7 @@ pub struct SettingsSpec { pub window_zoom: f64, pub search_keyboard_layout: String, pub disable_search_keyboard: bool, + pub font_family: String, pub themes_custom: JsonValue, pub auto_score_enabled: bool, pub auto_score_rules: JsonValue, @@ -28,6 +29,7 @@ impl Default for SettingsSpec { window_zoom: 1.0, search_keyboard_layout: "qwerty26".to_string(), disable_search_keyboard: false, + font_family: "system".to_string(), themes_custom: JsonValue::Array(vec![]), auto_score_enabled: false, auto_score_rules: JsonValue::Array(vec![]), @@ -58,6 +60,7 @@ pub enum SettingsKey { WindowZoom, SearchKeyboardLayout, DisableSearchKeyboard, + FontFamily, ThemesCustom, AutoScoreEnabled, AutoScoreRules, @@ -76,6 +79,7 @@ impl SettingsKey { SettingsKey::WindowZoom => "window_zoom", SettingsKey::SearchKeyboardLayout => "search_keyboard_layout", SettingsKey::DisableSearchKeyboard => "disable_search_keyboard", + SettingsKey::FontFamily => "font_family", SettingsKey::ThemesCustom => "themes_custom", SettingsKey::AutoScoreEnabled => "auto_score_enabled", SettingsKey::AutoScoreRules => "auto_score_rules", @@ -94,6 +98,7 @@ impl SettingsKey { "window_zoom" => Some(SettingsKey::WindowZoom), "search_keyboard_layout" => Some(SettingsKey::SearchKeyboardLayout), "disable_search_keyboard" => Some(SettingsKey::DisableSearchKeyboard), + "font_family" => Some(SettingsKey::FontFamily), "themes_custom" => Some(SettingsKey::ThemesCustom), "auto_score_enabled" => Some(SettingsKey::AutoScoreEnabled), "auto_score_rules" => Some(SettingsKey::AutoScoreRules), @@ -327,6 +332,22 @@ impl SettingsService { }, ); + defs.insert( + SettingsKey::FontFamily, + SettingDefinition { + kind: SettingValueKind::String, + default_value: SettingsValue::String("system".to_string()), + write_permission: PermissionRequirement::Admin, + validate: Some(|v| { + if let SettingsValue::String(s) = v { + !s.trim().is_empty() + } else { + false + } + }), + }, + ); + defs.insert( SettingsKey::ThemesCustom, SettingDefinition { @@ -549,6 +570,10 @@ impl SettingsService { SettingsValue::Boolean(b) => b, _ => false, }, + font_family: match self.get_value(SettingsKey::FontFamily) { + SettingsValue::String(s) => s, + _ => "system".to_string(), + }, themes_custom: match self.get_value(SettingsKey::ThemesCustom) { SettingsValue::Json(j) => j, _ => JsonValue::Array(vec![]), diff --git a/src/App.tsx b/src/App.tsx index 47d6779..6f6d5b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,8 +29,8 @@ import { useTranslation } from "react-i18next" import { Sidebar } from "./components/Sidebar" import { ContentArea } from "./components/ContentArea" import { OOBE } from "./components/OOBE/OOBE" -import { OAuthLogin } from "./components/OAuthLogin" -import { OAuthCallback } from "./components/OAuthCallback" +import { OAuthLogin } from "./components/OAuth/OAuthLogin" +import { OAuthCallback } from "./components/OAuth/OAuthCallback" import { ThemeProvider, useTheme } from "./contexts/ThemeContext" import { MOBILE_NAV_ITEMS, MobileNavKey, sanitizeMobileNavKeys } from "./shared/mobileNavigation" @@ -39,6 +39,23 @@ const DEFAULT_MOBILE_BOTTOM_PRIMARY_KEYS: MobileNavKey[] = DEFAULT_MOBILE_BOTTOM 0, 4 ) +const SYSTEM_FONT_STACK = + '"PingFang SC", "PingFangTC-Regular", "Hiragino Sans GB", "Hiragino Sans", "STHeiti", "Heiti SC", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif' + +const resolveFontFamily = (fontValue?: string): string => { + if (!fontValue || fontValue === "system") return SYSTEM_FONT_STACK + if (fontValue.startsWith("system-")) { + const family = fontValue.slice("system-".length).trim() + if (family) return `"${family}", ${SYSTEM_FONT_STACK}` + } + return SYSTEM_FONT_STACK +} + +const applyGlobalFontFamily = (fontValue?: string) => { + const fontFamily = resolveFontFamily(fontValue) + document.documentElement.style.setProperty("--ss-font-family", fontFamily) + document.body.style.fontFamily = fontFamily +} function MainContent(): React.JSX.Element { const { t } = useTranslation() @@ -163,6 +180,7 @@ function MainContent(): React.JSX.Element { const settingsRes = await (window as any).api.getAllSettings() if (settingsRes?.success && settingsRes.data) { setMobileBottomNavItems(normalizeStoredBottomKeys(settingsRes.data.mobile_bottom_nav_items)) + applyGlobalFontFamily(settingsRes.data.font_family) } } @@ -178,8 +196,13 @@ function MainContent(): React.JSX.Element { api .onSettingChanged((change: { key?: string; value?: unknown }) => { - if (change?.key !== "mobile_bottom_nav_items") return - setMobileBottomNavItems(normalizeStoredBottomKeys(change.value)) + if (change?.key === "mobile_bottom_nav_items") { + setMobileBottomNavItems(normalizeStoredBottomKeys(change.value)) + return + } + if (change?.key === "font_family") { + applyGlobalFontFamily(String(change.value || "system")) + } }) .then((fn: () => void) => { if (disposed) { diff --git a/src/components/AutoScoreManagerPage/ActionEditor.tsx b/src/components/AutoScore/ActionEditor.tsx similarity index 100% rename from src/components/AutoScoreManagerPage/ActionEditor.tsx rename to src/components/AutoScore/ActionEditor.tsx diff --git a/src/components/AutoScoreManagerPage/AutoScoreUtils.ts b/src/components/AutoScore/AutoScoreUtils.ts similarity index 100% rename from src/components/AutoScoreManagerPage/AutoScoreUtils.ts rename to src/components/AutoScore/AutoScoreUtils.ts diff --git a/src/components/AutoScoreManagerPage/IntervalValueCodec.ts b/src/components/AutoScore/IntervalValueCodec.ts similarity index 100% rename from src/components/AutoScoreManagerPage/IntervalValueCodec.ts rename to src/components/AutoScore/IntervalValueCodec.ts diff --git a/src/components/AutoScoreManagerPage/IntervalValueWidget.tsx b/src/components/AutoScore/IntervalValueWidget.tsx similarity index 100% rename from src/components/AutoScoreManagerPage/IntervalValueWidget.tsx rename to src/components/AutoScore/IntervalValueWidget.tsx diff --git a/src/components/AutoScoreManagerPage/TriggerRuleBuilder.tsx b/src/components/AutoScore/TriggerRuleBuilder.tsx similarity index 100% rename from src/components/AutoScoreManagerPage/TriggerRuleBuilder.tsx rename to src/components/AutoScore/TriggerRuleBuilder.tsx diff --git a/src/components/AutoScoreManager.tsx b/src/components/AutoScoreManager.tsx index 2fd4f22..1dfdddb 100644 --- a/src/components/AutoScoreManager.tsx +++ b/src/components/AutoScoreManager.tsx @@ -18,8 +18,8 @@ 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 { ActionEditor } from "./AutoScore/ActionEditor" +import { TriggerRuleBuilder } from "./AutoScore/TriggerRuleBuilder" import { actionDraftsToPayload, actionsToDrafts, @@ -33,7 +33,7 @@ import { type ActionDraft, type AutoScoreRule, type AutoScoreTagOption, -} from "./AutoScoreManagerPage/AutoScoreUtils" +} from "./AutoScore/AutoScoreUtils" interface StudentItem { id: number diff --git a/src/components/OAuthCallback.tsx b/src/components/OAuth/OAuthCallback.tsx similarity index 100% rename from src/components/OAuthCallback.tsx rename to src/components/OAuth/OAuthCallback.tsx diff --git a/src/components/OAuthLogin.tsx b/src/components/OAuth/OAuthLogin.tsx similarity index 100% rename from src/components/OAuthLogin.tsx rename to src/components/OAuth/OAuthLogin.tsx diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 67b91a7..de49893 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -16,7 +16,7 @@ import { Typography, } from "antd" import { ThemeQuickSettings } from "./ThemeQuickSettings" -import { OAuthLogin } from "./OAuthLogin" +import { OAuthLogin } from "./OAuth/OAuthLogin" import { useTranslation } from "react-i18next" import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n" import { useResponsive } from "../hooks/useResponsive" @@ -39,6 +39,62 @@ interface FontOption { fontFamily: string } +const SYSTEM_FONT_STACK = + '"PingFang SC", "PingFangTC-Regular", "Hiragino Sans GB", "Hiragino Sans", "STHeiti", "Heiti SC", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif' + +const defaultFontOptions: FontOption[] = [ + { + value: "system", + label: "系统默认", + fontFamily: SYSTEM_FONT_STACK, + }, + { + value: "system-Microsoft YaHei UI", + label: "Microsoft YaHei UI", + fontFamily: '"Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", sans-serif', + }, + { + value: "system-Microsoft YaHei", + label: "Microsoft YaHei", + fontFamily: '"Microsoft YaHei", "微软雅黑", sans-serif', + }, + { + value: "system-PingFang SC", + label: "PingFang SC", + fontFamily: '"PingFang SC", "Hiragino Sans GB", sans-serif', + }, + { + value: "system-Noto Sans CJK SC", + label: "Noto Sans CJK SC", + fontFamily: '"Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", sans-serif', + }, + { + value: "system-Segoe UI", + label: "Segoe UI", + fontFamily: '"Segoe UI", sans-serif', + }, + { + value: "system-Arial", + label: "Arial", + fontFamily: "Arial, sans-serif", + }, +] + +const mergeFontOptions = (options: FontOption[]): FontOption[] => { + const map = new Map() + for (const option of options) { + if (!map.has(option.value)) { + map.set(option.value, option) + } + } + return Array.from(map.values()) +} + +const findFontOption = (options: FontOption[], value?: string): FontOption | undefined => { + if (!value) return options.find((item) => item.value === "system") || options[0] + return options.find((item) => item.value === value) || options.find((item) => item.value === "system") +} + const applyFontFamily = (fontFamily: string) => { document.documentElement.style.setProperty("--ss-font-family", fontFamily) document.body.style.fontFamily = fontFamily @@ -76,8 +132,9 @@ export const Settings: React.FC<{ search_keyboard_layout: "qwerty26", disable_search_keyboard: false, }) - const [fontOptions, setFontOptions] = useState([]) - const [isLoadingFonts, setIsLoadingFonts] = useState(true) + const [fontOptions, setFontOptions] = useState(defaultFontOptions) + const [isLoadingFonts, setIsLoadingFonts] = useState(false) + const fontOptionsRef = useRef(defaultFontOptions) const [securityStatus, setSecurityStatus] = useState<{ permission: permissionLevel @@ -168,6 +225,10 @@ export const Settings: React.FC<{ ) }, [permission, t]) + useEffect(() => { + fontOptionsRef.current = fontOptions + }, [fontOptions]) + const emitDataUpdated = (category: "events" | "students" | "reasons" | "all") => { window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } })) } @@ -187,54 +248,73 @@ export const Settings: React.FC<{ } } - const loadSystemFonts = async () => { - if (!(window as any).api?.getSystemFonts) return - + const loadSystemFonts = async (selectedFontValue?: string) => { setIsLoadingFonts(true) + + const applySelectedFont = (options: FontOption[]) => { + const current = findFontOption(options, selectedFontValue || settings.font_family) + if (current) applyFontFamily(current.fontFamily) + } + + const api = (window as any).api + if (!api?.getSystemFonts) { + setFontOptions(defaultFontOptions) + applySelectedFont(defaultFontOptions) + setIsLoadingFonts(false) + return + } + try { - const res = await (window as any).api.getSystemFonts() + const res = await api.getSystemFonts() if (res.success && Array.isArray(res.data)) { - const fontList = res.data - .map((name: string) => ({ - value: `system-${name}`, - label: name, - fontFamily: `"${name}"`, - })) as FontOption[] + const remoteOptions = res.data + .map((name: string) => String(name || "").trim()) + .filter((name: string) => Boolean(name)) + .map( + (name: string) => + ({ + value: `system-${name}`, + label: name, + fontFamily: `"${name}", ${SYSTEM_FONT_STACK}`, + }) satisfies FontOption + ) - setFontOptions(fontList) - - if (settings.font_family) { - const currentFontOpt = fontList.find((f) => f.value === settings.font_family) - if (currentFontOpt) { - applyFontFamily(currentFontOpt.fontFamily) - } - } + const mergedOptions = mergeFontOptions([...defaultFontOptions, ...remoteOptions]) + setFontOptions(mergedOptions) + applySelectedFont(mergedOptions) + } else { + setFontOptions(defaultFontOptions) + applySelectedFont(defaultFontOptions) } } catch (error) { console.error("Failed to load system fonts:", error) + setFontOptions(defaultFontOptions) + applySelectedFont(defaultFontOptions) } finally { setIsLoadingFonts(false) } } const loadAll = async () => { - if (!(window as any).api) return - const res = await (window as any).api.getAllSettings() + const api = (window as any).api + if (!api) { + setFontOptions(defaultFontOptions) + setIsLoadingFonts(false) + return + } + + let savedFontFamily = settings.font_family || "system" + const res = await api.getAllSettings() if (res.success && res.data) { setSettings(res.data) setPgConnectionString(res.data.pg_connection_string || "") setPgConnectionStatus(res.data.pg_connection_status || { connected: true, type: "sqlite" }) - if (res.data.font_family) { - const fontOpt = fontOptions.find((f) => f.value === res.data.font_family) - if (fontOpt) { - applyFontFamily(fontOpt.fontFamily) - } - } + savedFontFamily = res.data.font_family || "system" } - const authRes = await (window as any).api.authGetStatus() + const authRes = await api.authGetStatus() if (authRes.success && authRes.data) setSecurityStatus(authRes.data) await loadMcpStatus() - await loadSystemFonts() + await loadSystemFonts(savedFontFamily) } const loadAboutContent = async () => { @@ -272,11 +352,12 @@ export const Settings: React.FC<{ if (change?.key === "auto_score_enabled") return { ...prev, auto_score_enabled: change.value } if (change?.key === "font_family") { - const fontOpt = fontOptions.find((f) => f.value === change.value) + const value = String(change.value || "system") + const fontOpt = findFontOption(fontOptionsRef.current, value) if (fontOpt) { applyFontFamily(fontOpt.fontFamily) } - return { ...prev, font_family: change.value } + return { ...prev, font_family: value } } return prev }) @@ -726,12 +807,12 @@ export const Settings: React.FC<{