mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
feat: OOBE新增入口页与PostgreSQL自动同步分流
This commit is contained in:
+166
-59
@@ -13,7 +13,15 @@ interface oobeProps {
|
|||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type oobeStep = "language" | "theme" | "password" | "students" | "reasons" | "start"
|
type oobeStep =
|
||||||
|
| "entry"
|
||||||
|
| "postgresql"
|
||||||
|
| "language"
|
||||||
|
| "theme"
|
||||||
|
| "password"
|
||||||
|
| "students"
|
||||||
|
| "reasons"
|
||||||
|
| "start"
|
||||||
|
|
||||||
interface studentItem {
|
interface studentItem {
|
||||||
name: string
|
name: string
|
||||||
@@ -75,8 +83,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
const { currentTheme, setTheme, themes, applyTheme } = useTheme()
|
const { currentTheme, setTheme, themes, applyTheme } = useTheme()
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<oobeStep>("language")
|
const [currentStep, setCurrentStep] = useState<oobeStep>("entry")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [pgAutoLoading, setPgAutoLoading] = useState(false)
|
||||||
|
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<AppLanguage>("zh-CN")
|
const [selectedLanguage, setSelectedLanguage] = useState<AppLanguage>("zh-CN")
|
||||||
const [workingTheme, setWorkingTheme] = useState<themeConfig | null>(null)
|
const [workingTheme, setWorkingTheme] = useState<themeConfig | null>(null)
|
||||||
@@ -88,10 +97,11 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
const [newReasonDelta, setNewReasonDelta] = useState(1)
|
const [newReasonDelta, setNewReasonDelta] = useState(1)
|
||||||
const [adminPassword, setAdminPassword] = useState("")
|
const [adminPassword, setAdminPassword] = useState("")
|
||||||
const [pointsPassword, setPointsPassword] = useState("")
|
const [pointsPassword, setPointsPassword] = useState("")
|
||||||
|
const [pgConnectionString, setPgConnectionString] = useState("")
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const steps: oobeStep[] = ["language", "theme", "password", "students", "reasons", "start"]
|
const steps: oobeStep[] = ["entry", "language", "theme", "password", "students", "reasons", "start"]
|
||||||
const stepIndex = steps.indexOf(currentStep) + 1
|
const stepIndex = steps.indexOf(currentStep) + 1
|
||||||
const totalSteps = steps.length
|
const totalSteps = steps.length
|
||||||
|
|
||||||
@@ -325,6 +335,63 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
handleNext()
|
handleNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markWizardCompleted = async () => {
|
||||||
|
if (!(window as any).api) throw new Error("api not ready")
|
||||||
|
const res = await (window as any).api.setSetting("is_wizard_completed", true)
|
||||||
|
if (!res?.success) throw new Error("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSkipToApp = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await markWizardCompleted()
|
||||||
|
showOobeMessage("success", t("common.success"))
|
||||||
|
onComplete()
|
||||||
|
} catch (e: any) {
|
||||||
|
showOobeMessage("error", e?.message || t("common.error"))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConnectPgAndSync = async () => {
|
||||||
|
const connectionString = pgConnectionString.trim()
|
||||||
|
if (!connectionString) {
|
||||||
|
showOobeMessage("warning", t("settings.database.enterConnectionString"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPgAutoLoading(true)
|
||||||
|
try {
|
||||||
|
if (!(window as any).api) throw new Error("api not ready")
|
||||||
|
const switchRes = await (window as any).api.dbSwitchConnection(connectionString)
|
||||||
|
if (!switchRes?.success || switchRes?.data?.type !== "postgresql") {
|
||||||
|
throw new Error(switchRes?.message || t("settings.database.switchFailed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewRes = await (window as any).api.dbSyncPreview()
|
||||||
|
if (!previewRes?.success || !previewRes?.data?.can_sync) {
|
||||||
|
throw new Error(previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewRes?.data?.need_sync) {
|
||||||
|
const syncApplyRes = await (window as any).api.dbSyncApply("keep_local")
|
||||||
|
if (!syncApplyRes?.success || !syncApplyRes?.data?.success) {
|
||||||
|
throw new Error(
|
||||||
|
syncApplyRes?.data?.message || syncApplyRes?.message || t("settings.database.uploadFailed")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await markWizardCompleted()
|
||||||
|
showOobeMessage("success", t("settings.database.uploadSuccess"))
|
||||||
|
onComplete()
|
||||||
|
} catch (e: any) {
|
||||||
|
showOobeMessage("error", e?.message || t("settings.database.uploadFailed"))
|
||||||
|
} finally {
|
||||||
|
setPgAutoLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleFinish = async () => {
|
const handleFinish = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -374,8 +441,7 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
ensureSuccess(authRes, t("common.error"))
|
ensureSuccess(authRes, t("common.error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await (window as any).api.setSetting("is_wizard_completed", true)
|
await markWizardCompleted()
|
||||||
ensureSuccess(res, "failed")
|
|
||||||
|
|
||||||
showOobeMessage("success", t("common.success"))
|
showOobeMessage("success", t("common.success"))
|
||||||
onComplete()
|
onComplete()
|
||||||
@@ -392,6 +458,45 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
|
|
||||||
const renderStepContent = () => {
|
const renderStepContent = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
|
case "entry":
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
|
<Typography.Text type="secondary">{t("oobe.steps.entry.description")}</Typography.Text>
|
||||||
|
<Button type="primary" size="large" onClick={() => setCurrentStep("language")}>
|
||||||
|
{t("oobe.steps.entry.enterOobe")}
|
||||||
|
</Button>
|
||||||
|
<Button size="large" onClick={() => setCurrentStep("postgresql")}>
|
||||||
|
{t("oobe.steps.entry.connectPostgresAutoSync")}
|
||||||
|
</Button>
|
||||||
|
<Button size="large" onClick={handleSkipToApp} loading={loading}>
|
||||||
|
{t("oobe.steps.entry.skipDirect")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
case "postgresql":
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{t("oobe.steps.postgresql.description")}
|
||||||
|
</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={pgConnectionString}
|
||||||
|
onChange={(e) => setPgConnectionString(e.target.value)}
|
||||||
|
placeholder={t("oobe.steps.postgresql.connectionPlaceholder")}
|
||||||
|
/>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t("settings.database.connectionExample")}
|
||||||
|
</Typography.Text>
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
<Button onClick={() => setCurrentStep("entry")}>{t("common.prev")}</Button>
|
||||||
|
<Button type="primary" loading={pgAutoLoading} onClick={handleConnectPgAndSync}>
|
||||||
|
{t("oobe.steps.postgresql.autoSyncAndEnter")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
case "language":
|
case "language":
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
@@ -802,78 +907,80 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
|
|
||||||
<div style={{ minHeight: 200, marginBottom: 24 }}>{renderStepContent()}</div>
|
<div style={{ minHeight: 200, marginBottom: 24 }}>{renderStepContent()}</div>
|
||||||
|
|
||||||
<div
|
{currentStep !== "entry" && currentStep !== "postgresql" && (
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
|
||||||
{currentStep !== "language" && <Button onClick={handlePrev}>{t("common.prev")}</Button>}
|
|
||||||
{currentStep !== "start" && <Button onClick={handleSkip}>{t("oobe.skip")}</Button>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
|
||||||
color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)",
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
{currentStep !== "language" && <Button onClick={handlePrev}>{t("common.prev")}</Button>}
|
||||||
|
{currentStep !== "start" && <Button onClick={handleSkip}>{t("oobe.skip")}</Button>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 60,
|
display: "flex",
|
||||||
height: 4,
|
alignItems: "center",
|
||||||
background: isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)",
|
gap: 8,
|
||||||
borderRadius: 2,
|
color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.45)",
|
||||||
overflow: "hidden",
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: `${(stepIndex / totalSteps) * 100}%`,
|
width: 60,
|
||||||
height: "100%",
|
height: 4,
|
||||||
background: primaryColor,
|
background: isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)",
|
||||||
transition: "width 0.3s",
|
borderRadius: 2,
|
||||||
}}
|
overflow: "hidden",
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span>{t("oobe.step", { current: stepIndex, total: totalSteps })}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
|
||||||
{currentStep !== "start" ? (
|
|
||||||
<Button type="primary" onClick={handleNext}>
|
|
||||||
{t("common.next")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={handleFinish}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
src={logoSvg}
|
|
||||||
alt="SecScore"
|
|
||||||
style={{
|
style={{
|
||||||
width: 16,
|
width: `${(stepIndex / totalSteps) * 100}%`,
|
||||||
height: 16,
|
height: "100%",
|
||||||
filter: isDark ? "brightness(0) invert(1)" : "none",
|
background: primaryColor,
|
||||||
|
transition: "width 0.3s",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{t("oobe.steps.start.startButton")}
|
</div>
|
||||||
</Button>
|
<span>{t("oobe.step", { current: stepIndex, total: totalSteps })}</span>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
{currentStep !== "start" ? (
|
||||||
|
<Button type="primary" onClick={handleNext}>
|
||||||
|
{t("common.next")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleFinish}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={logoSvg}
|
||||||
|
alt="SecScore"
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
filter: isDark ? "brightness(0) invert(1)" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{t("oobe.steps.start.startButton")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,19 @@
|
|||||||
"step": "Step {{current}}/{{total}}",
|
"step": "Step {{current}}/{{total}}",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"steps": {
|
"steps": {
|
||||||
|
"entry": {
|
||||||
|
"title": "Get Started",
|
||||||
|
"description": "Choose how you want to proceed",
|
||||||
|
"enterOobe": "Enter OOBE",
|
||||||
|
"connectPostgresAutoSync": "Connect PostgreSQL with Auto Sync",
|
||||||
|
"skipDirect": "Skip OOBE and Enter"
|
||||||
|
},
|
||||||
|
"postgresql": {
|
||||||
|
"title": "Connect PostgreSQL",
|
||||||
|
"description": "Enter a PostgreSQL connection string. The app will connect and auto-sync before entering.",
|
||||||
|
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
|
||||||
|
"autoSyncAndEnter": "Connect, Auto Sync, and Enter"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "Select Language",
|
"title": "Select Language",
|
||||||
"description": "Choose your preferred interface language"
|
"description": "Choose your preferred interface language"
|
||||||
|
|||||||
@@ -42,6 +42,19 @@
|
|||||||
"step": "第 {{current}}/{{total}} 步",
|
"step": "第 {{current}}/{{total}} 步",
|
||||||
"skip": "跳过",
|
"skip": "跳过",
|
||||||
"steps": {
|
"steps": {
|
||||||
|
"entry": {
|
||||||
|
"title": "开始配置",
|
||||||
|
"description": "请选择接下来要进行的操作",
|
||||||
|
"enterOobe": "进入OOBE",
|
||||||
|
"connectPostgresAutoSync": "连接PostgreSQL自动同步",
|
||||||
|
"skipDirect": "跳过OOBE,直接进入"
|
||||||
|
},
|
||||||
|
"postgresql": {
|
||||||
|
"title": "连接 PostgreSQL",
|
||||||
|
"description": "输入 PostgreSQL 连接字符串,系统将自动连接并同步后进入应用",
|
||||||
|
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
|
||||||
|
"autoSyncAndEnter": "连接并自动同步后进入"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "选择语言",
|
"title": "选择语言",
|
||||||
"description": "请选择您偏好的界面语言"
|
"description": "请选择您偏好的界面语言"
|
||||||
|
|||||||
Reference in New Issue
Block a user