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,
};
use crate::db::migration::run_migration;
use crate::services::logger::LogLevel;
use crate::services::permission::PermissionLevel;
use crate::services::settings::{SettingsKey, SettingsValue};
use crate::state::AppState;
@@ -1052,24 +1053,63 @@ pub async fn db_switch_connection(
) -> Result<IpcResponse<SwitchConnectionResult>, String> {
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
.starts_with("postgres://")
|| connection_string.starts_with("postgresql://")
{
logger.log(
LogLevel::Info,
"Switching to PostgreSQL database",
Some("database"),
None,
);
let conn = timeout(
Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
create_postgres_connection(&connection_string),
)
.await
.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(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&conn, DatabaseType::PostgreSQL),
)
.await
.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(),
@@ -1078,6 +1118,12 @@ pub async fn db_switch_connection(
conn,
)
} else {
logger.log(
LogLevel::Info,
"Switching to SQLite database",
Some("database"),
None,
);
let path = if connection_string.starts_with("sqlite://") {
connection_string
.strip_prefix("sqlite://")
@@ -1089,10 +1135,33 @@ pub async fn db_switch_connection(
let conn = create_sqlite_connection(&path)
.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)
.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(),
@@ -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 }))
}
+86
View File
@@ -124,6 +124,92 @@ select {
padding-top: 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,
+33 -7
View File
@@ -7,6 +7,7 @@ import { useTheme } from "../../contexts/ThemeContext"
import { changeLanguage, AppLanguage, languageOptions } from "../../i18n"
import type { themeConfig } from "../../preload/types"
import logoSvg from "../../assets/logoHD.svg"
import { useResponsive, useScreenSize } from "../../hooks/useResponsive"
interface oobeProps {
visible: boolean
@@ -100,6 +101,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
const { t } = useTranslation()
const { currentTheme, setTheme, themes, applyTheme } = useTheme()
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 [loading, setLoading] = useState(false)
@@ -902,6 +906,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
<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)",
backdropFilter: "blur(20px)",
borderRadius: 16,
padding: 32,
padding: isMobile ? 20 : 32,
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)",
border: isDark ? "1px solid rgba(255, 255, 255, 0.08)" : "1px solid rgba(0, 0, 0, 0.06)",
fontFamily: "var(--ss-font-family)",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{contextHolder}
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: isMobile ? 16 : 24, flexShrink: 0 }}>
<Typography.Title
level={3}
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>
</div>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: isMobile ? 12 : 16, flexShrink: 0 }}>
<Typography.Title
level={4}
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>
</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" && (
<div
@@ -952,9 +972,12 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
display: "flex",
justifyContent: "space-between",
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" && (
<Button onClick={handlePrev}>{t("common.prev")}</Button>
)}
@@ -968,6 +991,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
gap: 8,
color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)",
fontSize: 12,
order: isMobile ? 3 : 2,
width: isMobile ? "100%" : "auto",
justifyContent: isMobile ? "center" : "flex-start",
}}
>
<div
@@ -991,7 +1017,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
<span>{t("oobe.step", { current: stepIndex, total: totalSteps })}</span>
</div>
<div style={{ display: "flex", gap: 8 }}>
<div style={{ display: "flex", gap: 8, order: isMobile ? 2 : 3 }}>
{currentStep !== "start" ? (
<Button type="primary" onClick={handleNext}>
{t("common.next")}
+11 -1
View File
@@ -16,6 +16,7 @@ import {
import { ThemeQuickSettings } from "./ThemeQuickSettings"
import { useTranslation } from "react-i18next"
import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n"
import { useResponsive } from "../hooks/useResponsive"
type permissionLevel = "admin" | "points" | "view"
type appSettings = {
@@ -47,6 +48,8 @@ const withTimeout = async (
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const [activeTab, setActiveTab] = useState("appearance")
const [currentLanguage, setCurrentLanguage] = useState<AppLanguage>(getCurrentLanguage())
const [settings, setSettings] = useState<appSettings>({
@@ -890,12 +893,17 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
>
{t("settings.database.connectionExample")}
</div>
<Space>
<Space
direction={isMobile ? "vertical" : "horizontal"}
size={isMobile ? 8 : "small"}
style={{ width: "100%" }}
>
<Button
type="primary"
onClick={switchToPg}
loading={pgSwitchLoading}
disabled={!canAdmin || !pgConnectionString}
style={{ width: isMobile ? "100%" : "auto" }}
>
{t("settings.database.switchToPostgreSQL")}
</Button>
@@ -903,6 +911,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
onClick={switchToSQLite}
loading={pgSwitchLoading}
disabled={!canAdmin || pgConnectionStatus.type === "sqlite"}
style={{ width: isMobile ? "100%" : "auto" }}
>
{t("settings.database.switchToSQLite")}
</Button>
@@ -910,6 +919,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
onClick={uploadLocalToRemote}
loading={pgUploadLoading}
disabled={!canAdmin || pgConnectionStatus.type !== "postgresql"}
style={{ width: isMobile ? "100%" : "auto" }}
>
{t("settings.database.uploadButton")}
</Button>
+13 -1
View File
@@ -14,6 +14,7 @@ import {
} from "@ant-design/icons"
import { useState, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { useResponsive } from "../hooks/useResponsive"
import appLogo from "../assets/logoHD.svg"
const { Sider } = Layout
@@ -44,6 +45,8 @@ export function Sidebar({
onFloatingExpandedChange,
}: SidebarProps): React.JSX.Element {
const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const [dbStatus, setDbStatus] = useState<DbStatus>({ type: "sqlite", connected: true })
const [syncLoading, setSyncLoading] = useState(false)
const [messageApi, contextHolder] = message.useMessage()
@@ -254,7 +257,16 @@ export function Sidebar({
</div>
{!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
mode="inline"
inlineCollapsed={isCollapsedView}