fix: 字体选择列表加载不了的问题

feat: 整理项目文件
This commit is contained in:
NanGua-QWQ
2026-03-31 19:28:34 +08:00
parent 9a022f3b31
commit c015ff3768
15 changed files with 316 additions and 44 deletions
Binary file not shown.
+134
View File
@@ -1,6 +1,8 @@
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
use std::process::Command;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
@@ -158,3 +160,135 @@ pub async fn settings_set(
Ok(IpcResponse::success(()))
}
fn fallback_font_families() -> Vec<String> {
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<String> {
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<String> {
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<String> {
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<String> {
Vec::new()
}
#[tauri::command]
pub async fn settings_get_system_fonts() -> Result<IpcResponse<Vec<String>>, 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))
}
+6 -1
View File
@@ -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<dyn std::error::Error>> {
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);
}
+25
View File
@@ -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![]),
+26 -3
View File
@@ -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
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) {
+3 -3
View File
@@ -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
+115 -34
View File
@@ -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<string, FontOption>()
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<FontOption[]>([])
const [isLoadingFonts, setIsLoadingFonts] = useState(true)
const [fontOptions, setFontOptions] = useState<FontOption[]>(defaultFontOptions)
const [isLoadingFonts, setIsLoadingFonts] = useState(false)
const fontOptionsRef = useRef<FontOption[]>(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) => ({
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}"`,
})) as FontOption[]
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<{
<Select
value={settings.font_family || "system"}
onChange={async (v) => {
const fontOpt = fontOptions.find((f) => f.value === v)
const fontOpt = findFontOption(fontOptions, String(v))
if (!fontOpt) return
applyFontFamily(fontOpt.fontFamily)
setSettings((prev) => ({ ...prev, font_family: v }))
setSettings((prev) => ({ ...prev, font_family: String(v) }))
if ((window as any).api) {
const res = await (window as any).api.setSetting("font_family", v)
const res = await (window as any).api.setSetting("font_family", String(v))
if (res.success) {
messageApi.success(t("settings.general.saved"))
} else {
+4
View File
@@ -48,6 +48,7 @@ export type settingsKey =
| "window_zoom"
| "search_keyboard_layout"
| "disable_search_keyboard"
| "font_family"
| "themes_custom"
| "auto_score_enabled"
| "auto_score_rules"
@@ -63,6 +64,7 @@ export interface settingsSpec {
window_zoom: number
search_keyboard_layout: "t9" | "qwerty26"
disable_search_keyboard: boolean
font_family: string
themes_custom: themeConfig[]
auto_score_enabled: boolean
auto_score_rules: autoScoreRule[]
@@ -311,6 +313,8 @@ const api = {
key: K,
value: settingsSpec[K]
): Promise<{ success: boolean }> => invoke("settings_set", { key, value }),
getSystemFonts: (): Promise<{ success: boolean; data: string[]; message?: string }> =>
invoke("settings_get_system_fonts"),
onSettingChanged: (callback: (change: settingChange) => void): Promise<UnlistenFn> => {
return listen<settingChange>("settings:changed", (event) => {
callback(event.payload)