feat(响应式): 添加移动端适配支持

- 在Sidebar、Settings、OOBE组件中添加响应式布局处理
- 为移动端优化CSS样式,包括模态框、表格和选择器
- 在数据库切换命令中添加详细的日志记录
- 改进OOBE组件在移动设备上的显示效果
This commit is contained in:
Fox_block
2026-03-22 17:21:26 +08:00
parent eac7b62dc2
commit 63dda3e043
5 changed files with 225 additions and 13 deletions
+82 -4
View File
@@ -11,6 +11,7 @@ use crate::db::entities::{
reasons, reward_redemptions, reward_settings, score_events, student_tags, students, tags, reasons, reward_redemptions, reward_settings, score_events, student_tags, students, tags,
}; };
use crate::db::migration::run_migration; use crate::db::migration::run_migration;
use crate::services::logger::LogLevel;
use crate::services::permission::PermissionLevel; use crate::services::permission::PermissionLevel;
use crate::services::settings::{SettingsKey, SettingsValue}; use crate::services::settings::{SettingsKey, SettingsValue};
use crate::state::AppState; use crate::state::AppState;
@@ -1052,24 +1053,63 @@ pub async fn db_switch_connection(
) -> Result<IpcResponse<SwitchConnectionResult>, String> { ) -> Result<IpcResponse<SwitchConnectionResult>, String> {
check_admin_permission(&state)?; check_admin_permission(&state)?;
let state_guard = state.read();
let logger = state_guard.logger.read();
logger.log(
LogLevel::Info,
&format!("Database switch requested, connection_string length: {}", connection_string.len()),
Some("database"),
None,
);
drop(state_guard);
let (db_type, saved_connection_string, saved_status, conn) = if connection_string let (db_type, saved_connection_string, saved_status, conn) = if connection_string
.starts_with("postgres://") .starts_with("postgres://")
|| connection_string.starts_with("postgresql://") || connection_string.starts_with("postgresql://")
{ {
logger.log(
LogLevel::Info,
"Switching to PostgreSQL database",
Some("database"),
None,
);
let conn = timeout( let conn = timeout(
Duration::from_secs(DB_CONNECT_TIMEOUT_SECS), Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
create_postgres_connection(&connection_string), create_postgres_connection(&connection_string),
) )
.await .await
.map_err(|_| "PostgreSQL 连接超时,请检查网络或连接字符串".to_string())? .map_err(|_| "PostgreSQL 连接超时,请检查网络或连接字符串".to_string())?
.map_err(|e| e.to_string())?; .map_err(|e| {
logger.log(
LogLevel::Error,
&format!("PostgreSQL connection failed: {}", e),
Some("database"),
None,
);
e.to_string()
})?;
timeout( timeout(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS), Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&conn, DatabaseType::PostgreSQL), run_migration(&conn, DatabaseType::PostgreSQL),
) )
.await .await
.map_err(|_| "PostgreSQL 初始化超时,请稍后重试".to_string())? .map_err(|_| "PostgreSQL 初始化超时,请稍后重试".to_string())?
.map_err(|e| e.to_string())?; .map_err(|e| {
logger.log(
LogLevel::Error,
&format!("PostgreSQL migration failed: {}", e),
Some("database"),
None,
);
e.to_string()
})?;
logger.log(
LogLevel::Info,
"PostgreSQL connection and migration successful",
Some("database"),
None,
);
( (
"postgresql".to_string(), "postgresql".to_string(),
@@ -1078,6 +1118,12 @@ pub async fn db_switch_connection(
conn, conn,
) )
} else { } else {
logger.log(
LogLevel::Info,
"Switching to SQLite database",
Some("database"),
None,
);
let path = if connection_string.starts_with("sqlite://") { let path = if connection_string.starts_with("sqlite://") {
connection_string connection_string
.strip_prefix("sqlite://") .strip_prefix("sqlite://")
@@ -1089,10 +1135,33 @@ pub async fn db_switch_connection(
let conn = create_sqlite_connection(&path) let conn = create_sqlite_connection(&path)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| {
logger.log(
LogLevel::Error,
&format!("SQLite connection failed: {}", e),
Some("database"),
None,
);
e.to_string()
})?;
run_migration(&conn, DatabaseType::SQLite) run_migration(&conn, DatabaseType::SQLite)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| {
logger.log(
LogLevel::Error,
&format!("SQLite migration failed: {}", e),
Some("database"),
None,
);
e.to_string()
})?;
logger.log(
LogLevel::Info,
"SQLite connection and migration successful",
Some("database"),
None,
);
( (
"sqlite".to_string(), "sqlite".to_string(),
@@ -1151,6 +1220,15 @@ pub async fn db_switch_connection(
}, },
); );
let state_guard = state.read();
let logger = state_guard.logger.read();
logger.log(
LogLevel::Info,
&format!("Database switched successfully to {}", db_type),
Some("database"),
None,
);
Ok(IpcResponse::success(SwitchConnectionResult { db_type })) Ok(IpcResponse::success(SwitchConnectionResult { db_type }))
} }
+86
View File
@@ -124,6 +124,92 @@ select {
padding-top: 14px; padding-top: 14px;
padding-bottom: 14px; padding-bottom: 14px;
} }
.ant-modal {
max-width: 100vw !important;
width: 100vw !important;
margin: 0 !important;
padding: 0 !important;
top: 0 !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
}
.ant-modal-content {
height: 100vh !important;
border-radius: 0 !important;
display: flex !important;
flex-direction: column !important;
}
.ant-modal-body {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch !important;
}
.ant-modal-wrap {
overflow: hidden !important;
}
.ant-select-dropdown {
max-height: 50vh !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch !important;
}
.ant-table-wrapper {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
}
.ant-table {
min-width: 600px !important;
}
}
@media (max-width: 480px) {
.ant-modal {
max-width: 100vw !important;
width: 100vw !important;
margin: 0 !important;
padding: 0 !important;
}
.ant-modal-content {
height: 100vh !important;
border-radius: 0 !important;
display: flex !important;
flex-direction: column !important;
}
.ant-modal-body {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch !important;
}
.ant-modal-wrap {
overflow: hidden !important;
}
.ant-table-container {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
}
.ant-table {
min-width: 600px !important;
}
.ant-select-dropdown {
max-height: 50vh !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch !important;
}
} }
.ss-table-center .ant-table th, .ss-table-center .ant-table th,
+33 -7
View File
@@ -7,6 +7,7 @@ import { useTheme } from "../../contexts/ThemeContext"
import { changeLanguage, AppLanguage, languageOptions } from "../../i18n" import { changeLanguage, AppLanguage, languageOptions } from "../../i18n"
import type { themeConfig } from "../../preload/types" import type { themeConfig } from "../../preload/types"
import logoSvg from "../../assets/logoHD.svg" import logoSvg from "../../assets/logoHD.svg"
import { useResponsive, useScreenSize } from "../../hooks/useResponsive"
interface oobeProps { interface oobeProps {
visible: boolean visible: boolean
@@ -100,6 +101,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { currentTheme, setTheme, themes, applyTheme } = useTheme() const { currentTheme, setTheme, themes, applyTheme } = useTheme()
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const breakpoint = useResponsive()
const { width: screenWidth, height: screenHeight } = useScreenSize()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const [currentStep, setCurrentStep] = useState<oobeStep>("entry") const [currentStep, setCurrentStep] = useState<oobeStep>("entry")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -902,6 +906,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
overflow: "hidden",
}} }}
> >
<OOBEBackground primaryColor={primaryColor} mode={workingTheme?.mode || "light"} /> <OOBEBackground primaryColor={primaryColor} mode={workingTheme?.mode || "light"} />
@@ -913,17 +918,21 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
background: isDark ? "rgba(15, 25, 45, 0.55)" : "rgba(255, 255, 255, 0.65)", background: isDark ? "rgba(15, 25, 45, 0.55)" : "rgba(255, 255, 255, 0.65)",
backdropFilter: "blur(20px)", backdropFilter: "blur(20px)",
borderRadius: 16, borderRadius: 16,
padding: 32, padding: isMobile ? 20 : 32,
width: 480, width: 480,
maxWidth: "90vw", maxWidth: isMobile ? "100vw" : "90vw",
maxHeight: isMobile ? "100vh" : "90vh",
boxShadow: isDark ? "0 8px 32px rgba(0, 0, 0, 0.3)" : "0 8px 32px rgba(0, 0, 0, 0.1)", boxShadow: isDark ? "0 8px 32px rgba(0, 0, 0, 0.3)" : "0 8px 32px rgba(0, 0, 0, 0.1)",
border: isDark ? "1px solid rgba(255, 255, 255, 0.08)" : "1px solid rgba(0, 0, 0, 0.06)", border: isDark ? "1px solid rgba(255, 255, 255, 0.08)" : "1px solid rgba(0, 0, 0, 0.06)",
fontFamily: "var(--ss-font-family)", fontFamily: "var(--ss-font-family)",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}} }}
> >
{contextHolder} {contextHolder}
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: isMobile ? 16 : 24, flexShrink: 0 }}>
<Typography.Title <Typography.Title
level={3} level={3}
style={{ margin: 0, color: isDark ? "#fff" : "rgba(0, 0, 0, 0.88)" }} style={{ margin: 0, color: isDark ? "#fff" : "rgba(0, 0, 0, 0.88)" }}
@@ -935,7 +944,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: isMobile ? 12 : 16, flexShrink: 0 }}>
<Typography.Title <Typography.Title
level={4} level={4}
style={{ margin: 0, color: isDark ? "#fff" : "rgba(0, 0, 0, 0.88)" }} style={{ margin: 0, color: isDark ? "#fff" : "rgba(0, 0, 0, 0.88)" }}
@@ -944,7 +953,18 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
</Typography.Title> </Typography.Title>
</div> </div>
<div style={{ minHeight: 200, marginBottom: 24 }}>{renderStepContent()}</div> <div
style={{
minHeight: isMobile ? 150 : 200,
marginBottom: isMobile ? 16 : 24,
overflowY: isMobile ? "auto" : "visible",
overflowX: "hidden",
flex: 1,
WebkitOverflowScrolling: "touch",
}}
>
{renderStepContent()}
</div>
{currentStep !== "entry" && currentStep !== "postgresql" && ( {currentStep !== "entry" && currentStep !== "postgresql" && (
<div <div
@@ -952,9 +972,12 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
flexShrink: 0,
flexWrap: isMobile ? "wrap" : "nowrap",
gap: isMobile ? 8 : 0,
}} }}
> >
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{currentStep !== "language" && ( {currentStep !== "language" && (
<Button onClick={handlePrev}>{t("common.prev")}</Button> <Button onClick={handlePrev}>{t("common.prev")}</Button>
)} )}
@@ -968,6 +991,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
gap: 8, gap: 8,
color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)", color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)",
fontSize: 12, fontSize: 12,
order: isMobile ? 3 : 2,
width: isMobile ? "100%" : "auto",
justifyContent: isMobile ? "center" : "flex-start",
}} }}
> >
<div <div
@@ -991,7 +1017,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
<span>{t("oobe.step", { current: stepIndex, total: totalSteps })}</span> <span>{t("oobe.step", { current: stepIndex, total: totalSteps })}</span>
</div> </div>
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8, order: isMobile ? 2 : 3 }}>
{currentStep !== "start" ? ( {currentStep !== "start" ? (
<Button type="primary" onClick={handleNext}> <Button type="primary" onClick={handleNext}>
{t("common.next")} {t("common.next")}
+11 -1
View File
@@ -16,6 +16,7 @@ import {
import { ThemeQuickSettings } from "./ThemeQuickSettings" import { ThemeQuickSettings } from "./ThemeQuickSettings"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n" import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n"
import { useResponsive } from "../hooks/useResponsive"
type permissionLevel = "admin" | "points" | "view" type permissionLevel = "admin" | "points" | "view"
type appSettings = { type appSettings = {
@@ -47,6 +48,8 @@ const withTimeout = async (
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => { export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
const { t } = useTranslation() const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const [activeTab, setActiveTab] = useState("appearance") const [activeTab, setActiveTab] = useState("appearance")
const [currentLanguage, setCurrentLanguage] = useState<AppLanguage>(getCurrentLanguage()) const [currentLanguage, setCurrentLanguage] = useState<AppLanguage>(getCurrentLanguage())
const [settings, setSettings] = useState<appSettings>({ const [settings, setSettings] = useState<appSettings>({
@@ -890,12 +893,17 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
> >
{t("settings.database.connectionExample")} {t("settings.database.connectionExample")}
</div> </div>
<Space> <Space
direction={isMobile ? "vertical" : "horizontal"}
size={isMobile ? 8 : "small"}
style={{ width: "100%" }}
>
<Button <Button
type="primary" type="primary"
onClick={switchToPg} onClick={switchToPg}
loading={pgSwitchLoading} loading={pgSwitchLoading}
disabled={!canAdmin || !pgConnectionString} disabled={!canAdmin || !pgConnectionString}
style={{ width: isMobile ? "100%" : "auto" }}
> >
{t("settings.database.switchToPostgreSQL")} {t("settings.database.switchToPostgreSQL")}
</Button> </Button>
@@ -903,6 +911,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
onClick={switchToSQLite} onClick={switchToSQLite}
loading={pgSwitchLoading} loading={pgSwitchLoading}
disabled={!canAdmin || pgConnectionStatus.type === "sqlite"} disabled={!canAdmin || pgConnectionStatus.type === "sqlite"}
style={{ width: isMobile ? "100%" : "auto" }}
> >
{t("settings.database.switchToSQLite")} {t("settings.database.switchToSQLite")}
</Button> </Button>
@@ -910,6 +919,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
onClick={uploadLocalToRemote} onClick={uploadLocalToRemote}
loading={pgUploadLoading} loading={pgUploadLoading}
disabled={!canAdmin || pgConnectionStatus.type !== "postgresql"} disabled={!canAdmin || pgConnectionStatus.type !== "postgresql"}
style={{ width: isMobile ? "100%" : "auto" }}
> >
{t("settings.database.uploadButton")} {t("settings.database.uploadButton")}
</Button> </Button>
+13 -1
View File
@@ -14,6 +14,7 @@ import {
} from "@ant-design/icons" } from "@ant-design/icons"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useResponsive } from "../hooks/useResponsive"
import appLogo from "../assets/logoHD.svg" import appLogo from "../assets/logoHD.svg"
const { Sider } = Layout const { Sider } = Layout
@@ -44,6 +45,8 @@ export function Sidebar({
onFloatingExpandedChange, onFloatingExpandedChange,
}: SidebarProps): React.JSX.Element { }: SidebarProps): React.JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const [dbStatus, setDbStatus] = useState<DbStatus>({ type: "sqlite", connected: true }) const [dbStatus, setDbStatus] = useState<DbStatus>({ type: "sqlite", connected: true })
const [syncLoading, setSyncLoading] = useState(false) const [syncLoading, setSyncLoading] = useState(false)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
@@ -254,7 +257,16 @@ export function Sidebar({
</div> </div>
{!hideMenu && ( {!hideMenu && (
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }}> <div
style={{
flex: 1,
overflowY: "auto",
overflowX: "hidden",
display: "flex",
flexDirection: "column",
WebkitOverflowScrolling: "touch",
}}
>
<Menu <Menu
mode="inline" mode="inline"
inlineCollapsed={isCollapsedView} inlineCollapsed={isCollapsedView}