Files
SecScore/src/components/Sidebar.tsx
T

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>
)
}