mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
feat(响应式): 添加移动端适配支持
- 在Sidebar、Settings、OOBE组件中添加响应式布局处理 - 为移动端优化CSS样式,包括模态框、表格和选择器 - 在数据库切换命令中添加详细的日志记录 - 改进OOBE组件在移动设备上的显示效果
This commit is contained in:
@@ -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 }))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user