mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
326 lines
8.5 KiB
TypeScript
326 lines
8.5 KiB
TypeScript
import { Layout, Menu, Card, Tag, Button, Space, message } from "antd"
|
|
import {
|
|
UserOutlined,
|
|
SettingOutlined,
|
|
HistoryOutlined,
|
|
UnorderedListOutlined,
|
|
HomeOutlined,
|
|
SyncOutlined,
|
|
FileTextOutlined,
|
|
CloudOutlined,
|
|
UploadOutlined,
|
|
MenuFoldOutlined,
|
|
MenuUnfoldOutlined,
|
|
} from "@ant-design/icons"
|
|
import { useState, useEffect } from "react"
|
|
import { useTranslation } from "react-i18next"
|
|
import appLogo from "../assets/logoHD.svg"
|
|
|
|
const { Sider } = Layout
|
|
|
|
interface SidebarProps {
|
|
activeMenu: string
|
|
permission: "admin" | "points" | "view"
|
|
onMenuChange: (value: string) => void
|
|
}
|
|
|
|
interface DbStatus {
|
|
type: "sqlite" | "postgresql"
|
|
connected: boolean
|
|
error?: string
|
|
}
|
|
|
|
export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps): React.JSX.Element {
|
|
const { t } = useTranslation()
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
const [dbStatus, setDbStatus] = useState<DbStatus>({ type: "sqlite", connected: true })
|
|
const [syncLoading, setSyncLoading] = useState(false)
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
|
|
useEffect(() => {
|
|
loadDbStatus()
|
|
const handleStatusChange = () => {
|
|
loadDbStatus()
|
|
}
|
|
const api = (window as any).api
|
|
if (!api) return
|
|
|
|
let disposed = false
|
|
let unlisten: (() => void) | null = null
|
|
|
|
api
|
|
.onSettingChanged((change: { key: string; value: any }) => {
|
|
if (change.key === "pg_connection_status") {
|
|
handleStatusChange()
|
|
}
|
|
})
|
|
.then((fn: () => void) => {
|
|
if (disposed) {
|
|
fn()
|
|
return
|
|
}
|
|
unlisten = fn
|
|
})
|
|
.catch(() => void 0)
|
|
|
|
return () => {
|
|
disposed = true
|
|
if (unlisten) unlisten()
|
|
}
|
|
}, [])
|
|
|
|
const loadDbStatus = async () => {
|
|
if (!(window as any).api) return
|
|
try {
|
|
const res = await (window as any).api.dbGetStatus()
|
|
if (res.success && res.data) {
|
|
setDbStatus(res.data)
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load database status:", e)
|
|
}
|
|
}
|
|
|
|
const handleSync = async () => {
|
|
if (!(window as any).api) return
|
|
setSyncLoading(true)
|
|
try {
|
|
await loadDbStatus()
|
|
} catch (e) {
|
|
console.error("Failed to sync database status:", e)
|
|
} finally {
|
|
setSyncLoading(false)
|
|
}
|
|
}
|
|
|
|
const [forceSyncLoading, setForceSyncLoading] = useState(false)
|
|
|
|
const handleForceSync = async () => {
|
|
if (!(window as any).api) return
|
|
|
|
const statusRes = await (window as any).api.dbGetStatus()
|
|
if (!statusRes.success || !statusRes.data) {
|
|
messageApi.error(t("sidebar.getDbStatusFailed"))
|
|
return
|
|
}
|
|
|
|
if (statusRes.data.type !== "postgresql") {
|
|
messageApi.error(t("sidebar.notRemoteMode"))
|
|
return
|
|
}
|
|
|
|
if (!statusRes.data.connected) {
|
|
messageApi.error(t("sidebar.dbNotConnected"))
|
|
return
|
|
}
|
|
|
|
setForceSyncLoading(true)
|
|
try {
|
|
const res = await (window as any).api.dbSync()
|
|
if (res.success && res.data?.success) {
|
|
messageApi.success(t("sidebar.syncSuccess"))
|
|
} else {
|
|
messageApi.error(res.data?.message || res.message || t("sidebar.syncFailed"))
|
|
}
|
|
} catch (e: any) {
|
|
messageApi.error(e?.message || t("sidebar.syncFailed"))
|
|
} finally {
|
|
setForceSyncLoading(false)
|
|
}
|
|
}
|
|
|
|
const menuItems = [
|
|
{
|
|
key: "home",
|
|
icon: <HomeOutlined />,
|
|
label: t("sidebar.home"),
|
|
},
|
|
{
|
|
key: "students",
|
|
icon: <UserOutlined />,
|
|
label: t("sidebar.students"),
|
|
disabled: permission !== "admin",
|
|
},
|
|
{
|
|
key: "score",
|
|
icon: <HistoryOutlined />,
|
|
label: t("sidebar.score"),
|
|
},
|
|
{
|
|
key: "auto-score",
|
|
icon: <SyncOutlined />,
|
|
label: t("sidebar.autoScore"),
|
|
},
|
|
{
|
|
key: "leaderboard",
|
|
icon: <UnorderedListOutlined />,
|
|
label: t("sidebar.leaderboard"),
|
|
},
|
|
{
|
|
key: "settlements",
|
|
icon: <FileTextOutlined />,
|
|
label: t("sidebar.settlements"),
|
|
},
|
|
{
|
|
key: "reasons",
|
|
icon: <UnorderedListOutlined />,
|
|
label: t("sidebar.reasons"),
|
|
disabled: permission !== "admin",
|
|
},
|
|
{
|
|
key: "settings",
|
|
icon: <SettingOutlined />,
|
|
label: t("sidebar.settings"),
|
|
disabled: permission !== "admin",
|
|
},
|
|
]
|
|
|
|
return (
|
|
<Sider
|
|
className="ss-sidebar"
|
|
width={200}
|
|
collapsed={collapsed}
|
|
collapsedWidth={80}
|
|
style={{
|
|
background: "var(--ss-sidebar-bg)",
|
|
borderRight: "1px solid var(--ss-border-color)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
theme="light"
|
|
>
|
|
<div
|
|
data-tauri-drag-region
|
|
style={
|
|
{
|
|
padding: collapsed ? "24px 8px 12px" : "32px 24px 16px",
|
|
textAlign: "center",
|
|
WebkitAppRegion: "drag",
|
|
userSelect: "none",
|
|
flexShrink: 0,
|
|
position: "relative",
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
onClick={() => setCollapsed((prev) => !prev)}
|
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
style={{
|
|
position: "absolute",
|
|
top: "8px",
|
|
right: "8px",
|
|
WebkitAppRegion: "no-drag",
|
|
}}
|
|
/>
|
|
<img
|
|
src={appLogo}
|
|
style={{ width: "48px", height: "48px", marginBottom: collapsed ? "0" : "12px" }}
|
|
alt="logo"
|
|
/>
|
|
{!collapsed && (
|
|
<>
|
|
<h2
|
|
style={{
|
|
color: "var(--ss-sidebar-text, var(--ss-text-main))",
|
|
margin: 0,
|
|
fontSize: "20px",
|
|
}}
|
|
>
|
|
SecScore
|
|
</h2>
|
|
<div
|
|
style={{
|
|
fontSize: "12px",
|
|
color: "var(--ss-sidebar-text, var(--ss-text-main))",
|
|
opacity: 0.8,
|
|
marginTop: "4px",
|
|
}}
|
|
>
|
|
{t("settings.about.appName")}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }}>
|
|
<Menu
|
|
mode="inline"
|
|
inlineCollapsed={collapsed}
|
|
selectedKeys={[activeMenu]}
|
|
onClick={({ key }) => onMenuChange(key)}
|
|
style={{
|
|
width: "100%",
|
|
border: "none",
|
|
backgroundColor: "transparent",
|
|
}}
|
|
items={menuItems}
|
|
/>
|
|
</div>
|
|
|
|
{!collapsed && dbStatus.type === "postgresql" && (
|
|
<Card
|
|
size="small"
|
|
style={{
|
|
margin: "8px",
|
|
backgroundColor: "var(--ss-card-bg)",
|
|
border: "1px solid var(--ss-border-color)",
|
|
}}
|
|
styles={{ body: { padding: "12px" } }}
|
|
>
|
|
{contextHolder}
|
|
<Space orientation="vertical" size={4} style={{ width: "100%" }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
<Space size={4}>
|
|
<CloudOutlined style={{ fontSize: "12px", color: "#1890ff" }} />
|
|
<span style={{ fontSize: "12px", fontWeight: 500 }}>{t("sidebar.remoteDb")}</span>
|
|
</Space>
|
|
<Tag
|
|
color={dbStatus.connected ? "success" : "error"}
|
|
style={{ margin: 0, fontSize: "10px" }}
|
|
>
|
|
{dbStatus.connected
|
|
? t("settings.database.connected")
|
|
: t("settings.database.disconnected")}
|
|
</Tag>
|
|
</div>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<SyncOutlined spin={syncLoading} />}
|
|
onClick={handleSync}
|
|
loading={syncLoading}
|
|
style={{
|
|
width: "100%",
|
|
height: "24px",
|
|
fontSize: "12px",
|
|
padding: "0 8px",
|
|
color: "var(--ss-text-secondary)",
|
|
}}
|
|
>
|
|
{t("sidebar.refreshStatus")}
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<UploadOutlined />}
|
|
onClick={handleForceSync}
|
|
loading={forceSyncLoading}
|
|
disabled={!dbStatus.connected}
|
|
style={{
|
|
width: "100%",
|
|
height: "24px",
|
|
fontSize: "12px",
|
|
padding: "0 8px",
|
|
}}
|
|
>
|
|
{t("sidebar.syncNow")}
|
|
</Button>
|
|
</Space>
|
|
</Card>
|
|
)}
|
|
</Sider>
|
|
)
|
|
}
|