先提交上去

This commit is contained in:
Fox_block
2026-03-16 20:49:46 +08:00
parent 86abcb5602
commit 192b48cab0
303 changed files with 30019 additions and 5846 deletions
+957
View File
@@ -0,0 +1,957 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import { Tabs, Card, Form, Select, Input, Button, Space, Divider, Tag, Modal, message } from "antd"
import { ThemeQuickSettings } from "./ThemeQuickSettings"
import { useTranslation } from "react-i18next"
import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n"
type permissionLevel = "admin" | "points" | "view"
type appSettings = {
is_wizard_completed: boolean
log_level: "debug" | "info" | "warn" | "error"
window_zoom?: string
auto_score_enabled?: boolean
}
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState("appearance")
const [currentLanguage, setCurrentLanguage] = useState<AppLanguage>(getCurrentLanguage())
const [settings, setSettings] = useState<appSettings>({
is_wizard_completed: false,
log_level: "info",
window_zoom: "1.0",
})
const [securityStatus, setSecurityStatus] = useState<{
permission: permissionLevel
hasAdminPassword: boolean
hasPointsPassword: boolean
hasRecoveryString: boolean
} | null>(null)
const [adminPassword, setAdminPassword] = useState("")
const [pointsPassword, setPointsPassword] = useState("")
const [recoveryToReset, setRecoveryToReset] = useState("")
const [recoveryDialogVisible, setRecoveryDialogVisible] = useState(false)
const [recoveryDialogHeader, setRecoveryDialogHeader] = useState("")
const [recoveryDialogString, setRecoveryDialogString] = useState("")
const [recoveryDialogFilename, setRecoveryDialogFilename] = useState("")
const [logsDialogVisible, setLogsDialogVisible] = useState(false)
const [logsText, setLogsText] = useState("")
const [logsLoading, setLogsLoading] = useState(false)
const [clearDialogVisible, setClearDialogVisible] = useState(false)
const [clearLoading, setClearLoading] = useState(false)
const importInputRef = useRef<HTMLInputElement | null>(null)
const [settleLoading, setSettleLoading] = useState(false)
const [settleDialogVisible, setSettleDialogVisible] = useState(false)
const [urlRegisterLoading, setUrlRegisterLoading] = useState(false)
const canAdmin = permission === "admin"
const [messageApi, contextHolder] = message.useMessage()
const [pgConnectionString, setPgConnectionString] = useState("")
const [pgConnectionStatus, setPgConnectionStatus] = useState<{
connected: boolean
type: "sqlite" | "postgresql"
error?: string
}>({ connected: true, type: "sqlite" })
const [pgTestLoading, setPgTestLoading] = useState(false)
const [pgSwitchLoading, setPgSwitchLoading] = useState(false)
const permissionTag = useMemo(() => {
return (
<Tag
color={permission === "admin" ? "success" : permission === "points" ? "warning" : "default"}
>
{permission === "admin"
? t("permissions.admin")
: permission === "points"
? t("permissions.points")
: t("permissions.view")}
</Tag>
)
}, [permission, t])
const emitDataUpdated = (category: "events" | "students" | "reasons" | "all") => {
window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } }))
}
const loadAll = async () => {
if (!(window as any).api) return
const res = await (window as any).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" })
}
const authRes = await (window as any).api.authGetStatus()
if (authRes.success && authRes.data) setSecurityStatus(authRes.data)
}
useEffect(() => {
loadAll()
if (!(window as any).api) return
const unsubscribe = (window as any).api.onSettingChanged((change: any) => {
setSettings((prev) => {
if (change?.key === "log_level") return { ...prev, log_level: change.value }
if (change?.key === "is_wizard_completed")
return { ...prev, is_wizard_completed: change.value }
if (change?.key === "window_zoom") return { ...prev, window_zoom: change.value }
if (change?.key === "auto_score_enabled")
return { ...prev, auto_score_enabled: change.value }
return prev
})
})
return () => {
if (typeof unsubscribe === "function") unsubscribe()
}
}, [])
const showLogs = async () => {
if (!(window as any).api) return
setLogsLoading(true)
const res = await (window as any).api.queryLogs(200)
setLogsLoading(false)
if (!res.success) {
messageApi.error(res.message || t("settings.data.readLogsFailed"))
return
}
setLogsText((res.data || []).join("\n"))
setLogsDialogVisible(true)
}
const exportLogs = async () => {
if (!(window as any).api) return
const res = await (window as any).api.queryLogs(5000)
if (!res.success) {
messageApi.error(res.message || t("settings.data.readLogsFailed"))
return
}
const dateTime = new Date().toISOString().replace(/[:.]/g, "-")
downloadTextFile(`secscore_logs_${dateTime}.txt`, `${(res.data || []).join("\n")}\n`)
messageApi.success(t("settings.data.logsExported"))
}
const downloadTextFile = (filename: string, text: string) => {
const blob = new Blob(["\ufeff" + text], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
const showRecoveryDialog = (header: string, recoveryString: string) => {
const date = new Date().toISOString().slice(0, 10)
const filename = `secscore_recovery_${date}.txt`
setRecoveryDialogHeader(header)
setRecoveryDialogString(recoveryString)
setRecoveryDialogFilename(filename)
setRecoveryDialogVisible(true)
}
const exportJson = async () => {
if (!(window as any).api) return
const res = await (window as any).api.exportDataJson()
if (!res.success || !res.data) {
messageApi.error(res.message || t("settings.data.exportFailed"))
return
}
const blob = new Blob([res.data], { type: "application/json;charset=utf-8" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `secscore_export_${new Date().toISOString().slice(0, 10)}.json`
a.click()
URL.revokeObjectURL(url)
messageApi.success(t("settings.data.exportSuccess"))
}
const importJson = async (file: File) => {
if (!(window as any).api) return
const text = await file.text()
const res = await (window as any).api.importDataJson(text)
if (res.success) {
messageApi.success(t("settings.data.importSuccess"))
setTimeout(() => window.location.reload(), 300)
} else {
messageApi.error(res.message || t("settings.data.importFailed"))
}
}
const savePasswords = async () => {
if (!(window as any).api) return
const res = await (window as any).api.authSetPasswords({
adminPassword: adminPassword ? adminPassword : undefined,
pointsPassword: pointsPassword ? pointsPassword : undefined,
})
if (res.success) {
setAdminPassword("")
setPointsPassword("")
await loadAll()
if (res.data?.recoveryString) {
showRecoveryDialog(
t("settings.security.recoveryString") + ` (${t("recovery.hint")})`,
res.data.recoveryString
)
} else {
messageApi.success(t("settings.security.saved"))
}
} else {
messageApi.error(res.message || t("settings.general.saveFailed"))
}
}
const generateRecovery = async () => {
if (!(window as any).api) return
const res = await (window as any).api.authGenerateRecovery()
if (!res.success || !res.data?.recoveryString) {
messageApi.error(res.message || t("settings.security.generateFailed"))
return
}
await loadAll()
showRecoveryDialog(
t("settings.security.newRecoveryString", "New recovery string (please save it)"),
res.data.recoveryString
)
}
const resetByRecovery = async () => {
if (!(window as any).api) return
const res = await (window as any).api.authResetByRecovery(recoveryToReset)
if (!res.success || !res.data?.recoveryString) {
messageApi.error(res.message || t("settings.security.resetFailed"))
return
}
setRecoveryToReset("")
await loadAll()
showRecoveryDialog(
t("settings.security.passwordClearedNewRecovery", "Password cleared, new recovery string"),
res.data.recoveryString
)
}
const clearAllPasswords = () => {
setClearDialogVisible(true)
}
const handleConfirmClearAll = async () => {
if (!(window as any).api) return
setClearLoading(true)
const res = await (window as any).api.authClearAll()
setClearLoading(false)
if (res.success) {
messageApi.success(t("settings.security.cleared"))
await loadAll()
setClearDialogVisible(false)
} else {
messageApi.error(res.message || t("settings.security.clearFailed"))
}
}
const confirmSettlement = () => {
setSettleDialogVisible(true)
}
const testPgConnection = async () => {
if (!(window as any).api) return
if (!pgConnectionString) {
messageApi.warning(t("settings.database.enterConnectionString"))
return
}
setPgTestLoading(true)
try {
const res = await (window as any).api.dbTestConnection(pgConnectionString)
if (res.success && res.data?.success) {
messageApi.success(t("settings.database.connectionTestSuccess"))
} else {
messageApi.error(
res.data?.error || res.message || t("settings.database.connectionTestFailed")
)
}
} catch (e: any) {
messageApi.error(e?.message || t("settings.database.connectionTestFailed"))
} finally {
setPgTestLoading(false)
}
}
const switchToPg = async () => {
if (!(window as any).api) return
setPgSwitchLoading(true)
try {
const res = await (window as any).api.dbSwitchConnection(pgConnectionString)
if (res.success) {
messageApi.success(
t("settings.database.switchedTo", {
type: res.data?.type === "postgresql" ? "PostgreSQL" : "SQLite",
})
)
await loadAll()
} else {
messageApi.error(res.message || t("settings.database.switchFailed"))
}
} catch (e: any) {
messageApi.error(e?.message || t("settings.database.switchFailed"))
} finally {
setPgSwitchLoading(false)
}
}
const switchToSQLite = async () => {
if (!(window as any).api) return
setPgSwitchLoading(true)
try {
const res = await (window as any).api.dbSwitchConnection("")
if (res.success) {
messageApi.success(t("settings.database.switchedToSQLite"))
setPgConnectionString("")
await loadAll()
} else {
messageApi.error(res.message || t("settings.database.switchFailed"))
}
} catch (e: any) {
messageApi.error(e?.message || t("settings.database.switchFailed"))
} finally {
setPgSwitchLoading(false)
}
}
const currentYear = new Date().getFullYear()
const tabItems = [
{
key: "appearance",
label: t("settings.tabs.appearance"),
children: (
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label={t("settings.language")}>
<Select
value={currentLanguage}
onChange={async (v: AppLanguage) => {
await changeLanguage(v)
setCurrentLanguage(v)
messageApi.success(t("common.success"))
}}
style={{ width: "320px" }}
options={languageOptions.map((opt) => ({
value: opt.value,
label: opt.label,
}))}
/>
<div
style={{ marginTop: "4px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
>
{t("settings.languageHint")}
</div>
</Form.Item>
</Form>
<Divider />
<ThemeQuickSettings />
<Divider />
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label={t("settings.interfaceZoom")}>
<Select
value={settings.window_zoom || "1.0"}
onChange={async (v) => {
if (!(window as any).api) return
const next = String(v)
const res = await (window as any).api.setSetting("window_zoom", next)
if (res.success) {
setSettings((prev) => ({ ...prev, window_zoom: next }))
messageApi.success(t("settings.general.saved"))
} else {
messageApi.error(res.message || t("settings.general.saveFailed"))
}
}}
style={{ width: "320px" }}
disabled={!canAdmin}
options={[
{ value: "0.7", label: t("settings.zoomOptions.small70") },
{ value: "0.8", label: "80%" },
{ value: "0.9", label: "90%" },
{ value: "1.0", label: t("settings.zoomOptions.default100") },
{ value: "1.1", label: "110%" },
{ value: "1.2", label: "120%" },
{ value: "1.3", label: "130%" },
{ value: "1.5", label: t("settings.zoomOptions.large150") },
]}
/>
<div
style={{ marginTop: "4px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
>
{t("settings.zoomHint")}
</div>
</Form.Item>
</Form>
</Card>
),
},
{
key: "security",
label: t("settings.tabs.security"),
children: (
<>
<Card
style={{
backgroundColor: "var(--ss-card-bg)",
color: "var(--ss-text-main)",
marginBottom: "16px",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ fontWeight: 600 }}>{t("settings.security.title")}</div>
<Space>
<Tag color={securityStatus?.hasAdminPassword ? "success" : "default"}>
{t("settings.security.adminPassword")}{" "}
{securityStatus?.hasAdminPassword
? t("settings.security.set")
: t("settings.security.notSet")}
</Tag>
<Tag color={securityStatus?.hasPointsPassword ? "success" : "default"}>
{t("settings.security.pointsPassword")}{" "}
{securityStatus?.hasPointsPassword
? t("settings.security.set")
: t("settings.security.notSet")}
</Tag>
<Tag color={securityStatus?.hasRecoveryString ? "success" : "default"}>
{t("settings.security.recoveryString")}{" "}
{securityStatus?.hasRecoveryString
? t("settings.security.generated")
: t("settings.security.notGenerated")}
</Tag>
</Space>
</div>
<Divider />
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label={t("settings.security.adminPassword")}>
<Input
value={adminPassword}
onChange={(e) => setAdminPassword(e.target.value)}
placeholder={t("settings.security.adminPasswordPlaceholder")}
maxLength={6}
disabled={!canAdmin && Boolean(securityStatus?.hasAdminPassword)}
/>
</Form.Item>
<Form.Item label={t("settings.security.pointsPassword")}>
<Input
value={pointsPassword}
onChange={(e) => setPointsPassword(e.target.value)}
placeholder={t("settings.security.pointsPasswordPlaceholder")}
maxLength={6}
disabled={!canAdmin && Boolean(securityStatus?.hasAdminPassword)}
/>
</Form.Item>
<Form.Item label={t("common.operation")}>
<Space>
<Button
type="primary"
onClick={savePasswords}
disabled={!canAdmin && Boolean(securityStatus?.hasAdminPassword)}
>
{t("settings.security.savePassword")}
</Button>
<Button
onClick={generateRecovery}
disabled={!canAdmin && Boolean(securityStatus?.hasAdminPassword)}
>
{t("settings.security.generateRecovery")}
</Button>
<Button danger onClick={clearAllPasswords} disabled={!canAdmin}>
{t("settings.security.clearAllPasswords")}
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
{t("settings.security.recoveryReset")}
</div>
<Space>
<Input
value={recoveryToReset}
onChange={(e) => setRecoveryToReset(e.target.value)}
placeholder={t("settings.security.recoveryPlaceholder")}
style={{ width: "420px" }}
/>
<Button type="primary" onClick={resetByRecovery}>
{t("settings.security.resetPassword")}
</Button>
</Space>
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
{t("settings.security.resetHint")}
</div>
</Card>
</>
),
},
{
key: "database",
label: t("settings.database.title"),
children: (
<>
<Card
style={{
backgroundColor: "var(--ss-card-bg)",
color: "var(--ss-text-main)",
marginBottom: "16px",
}}
>
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
{t("settings.database.currentStatus")}
</div>
<Space>
<Tag color={pgConnectionStatus.type === "postgresql" ? "blue" : "green"}>
{pgConnectionStatus.type === "postgresql"
? t("settings.database.postgresqlRemote")
: t("settings.database.sqliteLocal")}
</Tag>
<Tag color={pgConnectionStatus.connected ? "success" : "error"}>
{pgConnectionStatus.connected
? t("settings.database.connected")
: t("settings.database.disconnected")}
</Tag>
{pgConnectionStatus.error && (
<span style={{ color: "var(--ant-color-error, #ff4d4f)", fontSize: "12px" }}>
{pgConnectionStatus.error}
</span>
)}
</Space>
</Card>
<Card
style={{
backgroundColor: "var(--ss-card-bg)",
color: "var(--ss-text-main)",
marginBottom: "16px",
}}
>
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
{t("settings.database.postgresqlConnection")}
</div>
<div
style={{ marginBottom: "12px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
>
{t("settings.database.connectionHint")}
</div>
<Space.Compact style={{ width: "100%", marginBottom: "12px" }}>
<Input
value={pgConnectionString}
onChange={(e) => setPgConnectionString(e.target.value)}
placeholder="postgresql://user:password@host:port/database?sslmode=require"
style={{ flex: 1 }}
disabled={!canAdmin}
/>
<Button
onClick={testPgConnection}
loading={pgTestLoading}
disabled={!canAdmin || !pgConnectionString}
>
{t("settings.database.testConnection")}
</Button>
</Space.Compact>
<div
style={{ marginBottom: "12px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
>
{t("settings.database.connectionExample")}
</div>
<Space>
<Button
type="primary"
onClick={switchToPg}
loading={pgSwitchLoading}
disabled={!canAdmin || !pgConnectionString}
>
{t("settings.database.switchToPostgreSQL")}
</Button>
<Button
onClick={switchToSQLite}
loading={pgSwitchLoading}
disabled={!canAdmin || pgConnectionStatus.type === "sqlite"}
>
{t("settings.database.switchToSQLite")}
</Button>
</Space>
</Card>
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
{t("settings.database.syncDescription")}
</div>
<div style={{ fontSize: "13px", color: "var(--ss-text-secondary)", lineHeight: "1.8" }}>
<p>{t("settings.database.syncPoint1")}</p>
<p>{t("settings.database.syncPoint2")}</p>
<p>{t("settings.database.syncPoint3")}</p>
<p>{t("settings.database.syncPoint4")}</p>
</div>
</Card>
</>
),
},
{
key: "data",
label: t("settings.data.title"),
children: (
<>
<Card
title={t("settings.data.settlement")}
style={{
backgroundColor: "var(--ss-card-bg)",
color: "var(--ss-text-main)",
marginBottom: "16px",
}}
>
<Space align="center">
<Button
danger
disabled={!canAdmin}
loading={settleLoading}
onClick={confirmSettlement}
>
{t("settings.data.settlementAndRestart")}
</Button>
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>
{t("settings.data.settlementHint")}
</div>
</Space>
</Card>
<Card
style={{
backgroundColor: "var(--ss-card-bg)",
color: "var(--ss-text-main)",
marginBottom: "16px",
}}
>
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
{t("settings.data.importExport")}
</div>
<Space>
<Button type="primary" onClick={exportJson}>
{t("settings.data.exportJson")}
</Button>
<Button onClick={() => importInputRef.current?.click()}>
{t("settings.data.importJson")}
</Button>
<input
ref={importInputRef}
type="file"
accept=".json,application/json"
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0]
if (file) importJson(file)
if (importInputRef.current) importInputRef.current.value = ""
}}
/>
</Space>
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
{t("settings.data.importHint")}
</div>
</Card>
<Card
title={t("settings.data.logs")}
style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}
>
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label={t("settings.data.logLevel")}>
<Select
value={settings.log_level}
onChange={async (v) => {
if (!(window as any).api) return
const next = String(v) as any
const res = await (window as any).api.setSetting("log_level", next)
if (res.success) {
setSettings((prev) => ({ ...prev, log_level: next }))
messageApi.success(t("settings.general.saved"))
} else {
messageApi.error(res.message || "更新失败")
}
}}
style={{ width: "320px" }}
options={[
{ value: "debug", label: t("settings.data.logLevels.debug") },
{ value: "info", label: t("settings.data.logLevels.info") },
{ value: "warn", label: t("settings.data.logLevels.warn") },
{ value: "error", label: t("settings.data.logLevels.error") },
]}
/>
</Form.Item>
<Form.Item label={t("settings.data.logOperation")}>
<Space>
<Button loading={logsLoading} onClick={showLogs}>
{t("settings.data.viewLogs")}
</Button>
<Button onClick={exportLogs}>{t("settings.data.exportLogs")}</Button>
<Button
danger
onClick={async () => {
if (!(window as any).api) return
const res = await (window as any).api.clearLogs()
if (res.success) messageApi.success(t("settings.data.logsCleared"))
else messageApi.error(res.message || t("settings.data.clearFailed"))
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
</>
),
},
{
key: "url",
label: t("settings.url.title"),
children: (
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
{t("settings.url.protocol")}
</div>
<Divider />
<div
style={{ marginBottom: "12px", fontSize: "13px", color: "var(--ss-text-secondary)" }}
>
{t("settings.url.description")}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
fontSize: "12px",
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", monospace',
}}
>
<div>secscore://settings</div>
<div>secscore://score</div>
</div>
<Divider />
<Space>
<Button
type="primary"
loading={urlRegisterLoading}
disabled={!canAdmin}
onClick={async () => {
if (!(window as any).api) return
setUrlRegisterLoading(true)
const res = await (window as any).api.registerUrlProtocol()
setUrlRegisterLoading(false)
if (res && res.success) {
messageApi.success(t("settings.url.registered"))
} else {
messageApi.error(res?.message || t("settings.url.registerFailed"))
}
}}
>
{t("settings.url.register")}
</Button>
</Space>
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
{t("settings.url.installerRequired")}
</div>
</Card>
),
},
{
key: "about",
label: t("settings.about.title"),
children: (
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<div style={{ fontSize: "16px", fontWeight: 700, marginBottom: "8px" }}>SecScore</div>
<div style={{ color: "var(--ss-text-secondary)", marginBottom: "16px" }}>
{t("settings.about.appName")}
</div>
<Divider />
<div style={{ display: "grid", gridTemplateColumns: "160px 1fr", rowGap: "10px" }}>
<div style={{ color: "var(--ss-text-secondary)" }}>{t("settings.about.version")}</div>
<div>v1.0.0</div>
<div style={{ color: "var(--ss-text-secondary)" }}>{t("settings.about.copyright")}</div>
<div>{"CopyRight © 2025-" + currentYear + " SECTL"}</div>
<div style={{ color: "var(--ss-text-secondary)" }}>Electron</div>
<div>{(window as any).electron?.process?.versions?.electron || "-"}</div>
<div style={{ color: "var(--ss-text-secondary)" }}>Chromium</div>
<div>{(window as any).electron?.process?.versions?.chrome || "-"}</div>
<div style={{ color: "var(--ss-text-secondary)" }}>Node</div>
<div>{(window as any).electron?.process?.versions?.node || "-"}</div>
<div style={{ color: "var(--ss-text-secondary)" }}>{t("settings.about.ipcStatus")}</div>
<div>
<Tag color={(window as any).api ? "success" : "error"}>
{(window as any).api
? t("settings.about.ipcConnected")
: t("settings.about.ipcDisconnected")}
</Tag>
</div>
<div style={{ color: "var(--ss-text-secondary)" }}>
{t("settings.about.environment")}
</div>
<div>
<Tag>{import.meta.env.DEV ? "Development" : "Production"}</Tag>
</div>
</div>
<Divider />
<div style={{ marginTop: "16px" }}>
<Button
onClick={() => {
;(window as any).api?.toggleDevTools()
}}
>
{t("settings.about.toggleDevTools")}
</Button>
</div>
</Card>
),
},
]
return (
<div
style={{
padding: "24px",
maxWidth: "900px",
margin: "0 auto",
color: "var(--ss-text-main)",
}}
>
{contextHolder}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "16px",
}}
>
<h2 style={{ margin: 0 }}>{t("settings.title")}</h2>
{permissionTag}
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
<Modal
title={recoveryDialogHeader}
open={recoveryDialogVisible}
onCancel={() => setRecoveryDialogVisible(false)}
footer={
<Button type="primary" onClick={() => setRecoveryDialogVisible(false)}>
{t("recovery.saved")}
</Button>
}
width="70%"
>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<div
style={{
wordBreak: "break-all",
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", monospace',
}}
>
{recoveryDialogString}
</div>
<Space>
<Button
type="primary"
onClick={() =>
downloadTextFile(
recoveryDialogFilename ||
`secscore_recovery_${new Date().toISOString().slice(0, 10)}.txt`,
`SecScore 找回字符串: ${recoveryDialogString}\n`
)
}
>
</Button>
</Space>
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>
{t("recovery.exportHint")}
</div>
</div>
</Modal>
<Modal
title={t("settings.data.systemLogs")}
open={logsDialogVisible}
onCancel={() => setLogsDialogVisible(false)}
footer={<Button onClick={() => setLogsDialogVisible(false)}>{t("common.close")}</Button>}
width="80%"
>
<div
style={{
maxHeight: "400px",
overflowY: "auto",
fontSize: "12px",
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", monospace',
whiteSpace: "pre-wrap",
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
padding: "10px",
}}
>
{logsText || t("settings.data.noLogs")}
</div>
</Modal>
<Modal
title={t("settings.data.confirmSettlement")}
open={settleDialogVisible}
onCancel={() => !settleLoading && setSettleDialogVisible(false)}
onOk={async () => {
if (!(window as any).api) return
setSettleLoading(true)
const res = await (window as any).api.createSettlement()
setSettleLoading(false)
if (res.success && res.data) {
messageApi.success(t("settings.data.settlementSuccess"))
emitDataUpdated("all")
setSettleDialogVisible(false)
} else {
messageApi.error(res.message || t("settings.data.settlementFailed"))
}
}}
confirmLoading={settleLoading}
okText={t("settings.data.settle")}
>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div>{t("settings.data.settlementConfirm1")}</div>
<div style={{ color: "var(--ss-text-secondary)", fontSize: "12px" }}>
{t("settings.data.settlementConfirm2")}
</div>
</div>
</Modal>
<Modal
title="确认清空所有密码?"
open={clearDialogVisible}
onCancel={() => !clearLoading && setClearDialogVisible(false)}
onOk={handleConfirmClearAll}
confirmLoading={clearLoading}
okText="确认清空"
okButtonProps={{ danger: true }}
>
</Modal>
</div>
)
}