Revert "feat: 实现依赖注入系统、插件架构和窗口管理"

This reverts commit 18eada8ff3.
This commit is contained in:
Yukino_fox
2026-05-02 14:08:44 +08:00
parent 8c7aaadabc
commit 3e85c32c9c
47 changed files with 180 additions and 5347 deletions
+6 -6
View File
@@ -630,9 +630,9 @@ export const normalizeActionDrafts = (drafts: ActionDraft[] | null | undefined):
? stringifyRewardActionValue(parsedValue.rewardId, parsedValue.rewardName) || ""
: ""
})()
: draft.event === "settle_score"
? ""
: toStringValue(Array.isArray(draft.value) ? draft.value[0] : draft.value),
: draft.event === "settle_score"
? ""
: toStringValue(Array.isArray(draft.value) ? draft.value[0] : draft.value),
} satisfies ActionDraft
})
.filter((item): item is ActionDraft => Boolean(item))
@@ -664,9 +664,9 @@ export const actionsToDrafts = (actions: AutoScoreAction[]): ActionDraft[] => {
? stringifyRewardActionValue(parsedValue.rewardId, parsedValue.rewardName) || ""
: ""
})()
: action.event === "settle_score"
? ""
: toStringValue(action.value),
: action.event === "settle_score"
? ""
: toStringValue(action.value),
} satisfies ActionDraft
})
.filter((item): item is ActionDraft => Boolean(item))
-193
View File
@@ -1,193 +0,0 @@
import React from "react"
import { Card, List, Button, Tag, Space, Typography, Spin, Empty, message } from "antd"
import {
ThunderboltOutlined,
DashboardOutlined,
HistoryOutlined,
CloudOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
DownloadOutlined,
DeleteOutlined,
} from "@ant-design/icons"
import { useBuiltinPlugins } from "../hooks/useBuiltinPlugins"
import { BuiltinPluginMeta } from "../plugins/builtin"
const { Title, Text, Paragraph } = Typography
const categoryIcons: Record<string, React.ReactNode> = {
automation: <ThunderboltOutlined />,
visualization: <DashboardOutlined />,
management: <HistoryOutlined />,
integration: <CloudOutlined />,
}
const categoryLabels: Record<string, string> = {
automation: "自动化",
visualization: "可视化",
management: "管理",
integration: "集成",
}
interface BuiltinPluginManagerProps {
canEdit: boolean
}
export const BuiltinPluginManager: React.FC<BuiltinPluginManagerProps> = ({ canEdit }) => {
const { plugins, loading, install, uninstall, enable, disable } = useBuiltinPlugins()
const [messageApi, contextHolder] = message.useMessage()
const handleInstall = async (pluginId: string) => {
try {
await install(pluginId)
messageApi.success("插件安装成功")
} catch (error) {
messageApi.error("插件安装失败")
}
}
const handleUninstall = async (pluginId: string) => {
try {
await uninstall(pluginId)
messageApi.success("插件已卸载")
} catch (error) {
messageApi.error("插件卸载失败")
}
}
const handleEnable = async (pluginId: string) => {
try {
await enable(pluginId)
messageApi.success("插件已启用")
} catch (error) {
messageApi.error("插件启用失败")
}
}
const handleDisable = async (pluginId: string) => {
try {
await disable(pluginId)
messageApi.success("插件已禁用")
} catch (error) {
messageApi.error("插件禁用失败")
}
}
const renderPluginActions = (
plugin: BuiltinPluginMeta & { installed: boolean; enabled: boolean }
) => {
if (!plugin.installed) {
return (
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => handleInstall(plugin.id)}
disabled={!canEdit}
>
</Button>
)
}
return (
<Space>
{plugin.enabled ? (
<Button
type="default"
icon={<CloseCircleOutlined />}
onClick={() => handleDisable(plugin.id)}
disabled={!canEdit}
>
</Button>
) : (
<Button
type="primary"
icon={<CheckCircleOutlined />}
onClick={() => handleEnable(plugin.id)}
disabled={!canEdit}
>
</Button>
)}
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleUninstall(plugin.id)}
disabled={!canEdit}
>
</Button>
</Space>
)
}
return (
<div style={{ padding: "24px" }}>
{contextHolder}
<Title level={4}></Title>
<Paragraph type="secondary">
SecScore 访
</Paragraph>
{loading ? (
<div style={{ textAlign: "center", padding: "40px" }}>
<Spin size="large" />
</div>
) : plugins.length === 0 ? (
<Empty description="暂无可用插件" />
) : (
<List
grid={{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 2, xl: 3, xxl: 3 }}
dataSource={plugins}
renderItem={(plugin) => (
<List.Item>
<Card
hoverable
style={{
backgroundColor: "var(--ss-card-bg)",
borderColor: plugin.enabled
? "var(--ant-color-primary)"
: "var(--ss-border-color)",
}}
title={
<Space>
{categoryIcons[plugin.category]}
<span>{plugin.name}</span>
</Space>
}
extra={
<Tag color={plugin.enabled ? "success" : "default"}>
{plugin.enabled ? "已启用" : plugin.installed ? "已安装" : "未安装"}
</Tag>
}
>
<div style={{ marginBottom: "12px" }}>
<Text type="secondary">{plugin.description}</Text>
</div>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<div>
<Tag>{categoryLabels[plugin.category]}</Tag>
{plugin.requiresAdmin && <Tag color="warning"></Tag>}
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text type="secondary" style={{ fontSize: "12px" }}>
{plugin.version}
</Text>
{renderPluginActions(plugin)}
</div>
</Space>
</Card>
</List.Item>
)}
/>
)}
</div>
)
}
+15 -27
View File
@@ -11,10 +11,10 @@ import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-
import { useTranslation } from "react-i18next"
import { WindowControls } from "./WindowControls"
import appLogo from "../assets/logoHD.svg"
import { usePluginEnabled } from "../hooks/useBuiltinPlugins"
const loadHome = () => import("./Home")
const loadStudentManager = () => import("./StudentManager")
const loadSettings = () => import("./Settings")
const loadReasonManager = () => import("./ReasonManager")
const loadScoreManager = () => import("./ScoreManager")
const loadAutoScoreManager = () => import("./AutoScoreManager")
@@ -26,6 +26,7 @@ const loadPluginManager = () => import("./PluginManager")
const Home = lazy(() => loadHome().then((m) => ({ default: m.Home })))
const StudentManager = lazy(() => loadStudentManager().then((m) => ({ default: m.StudentManager })))
const Settings = lazy(() => loadSettings().then((m) => ({ default: m.Settings })))
const ReasonManager = lazy(() => loadReasonManager().then((m) => ({ default: m.ReasonManager })))
const ScoreManager = lazy(() => loadScoreManager().then((m) => ({ default: m.ScoreManager })))
const AutoScoreManager = lazy(loadAutoScoreManager)
@@ -41,6 +42,7 @@ const warmupRouteChunks = () =>
Promise.allSettled([
loadHome(),
loadStudentManager(),
loadSettings(),
loadReasonManager(),
loadScoreManager(),
loadAutoScoreManager(),
@@ -112,13 +114,6 @@ export function ContentArea({
const isPrimaryMobilePage =
normalizedPath.startsWith("/home") || normalizedPath.startsWith("/settings")
const showMobileBack = isMobileHeaderMode && !isPrimaryMobilePage
// 检查插件启用状态
const autoScoreEnabled = usePluginEnabled("auto-score")
const boardsEnabled = usePluginEnabled("boards")
const settlementsEnabled = usePluginEnabled("settlements")
const rewardSettingsEnabled = usePluginEnabled("reward-settings")
const mobilePageTitle = (() => {
if (normalizedPath.startsWith("/home")) return t("sidebar.home")
if (normalizedPath.startsWith("/students")) return t("sidebar.students")
@@ -669,28 +664,20 @@ export function ContentArea({
<ScoreManager canEdit={permission === "admin" || permission === "points"} />
}
/>
{autoScoreEnabled && (
<Route
path="/auto-score"
element={<AutoScoreManager canEdit={permission === "admin"} />}
/>
)}
{boardsEnabled && (
<Route
path="/boards"
element={<BoardManager canManage={permission === "admin"} />}
/>
)}
<Route
path="/auto-score"
element={<AutoScoreManager canEdit={permission === "admin"} />}
/>
<Route path="/boards" element={<BoardManager canManage={permission === "admin"} />} />
<Route path="/leaderboard" element={<Leaderboard />} />
{settlementsEnabled && <Route path="/settlements" element={<SettlementHistory />} />}
<Route path="/settlements" element={<SettlementHistory />} />
<Route path="/reasons" element={<ReasonManager canEdit={permission === "admin"} />} />
{rewardSettingsEnabled && (
<Route
path="/reward-settings"
element={<RewardSettings canEdit={permission === "admin"} />}
/>
)}
<Route
path="/reward-settings"
element={<RewardSettings canEdit={permission === "admin"} />}
/>
<Route path="/plugins" element={<PluginManager canEdit={permission === "admin"} />} />
<Route path="/settings" element={<Settings permission={permission} />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
@@ -699,3 +686,4 @@ export function ContentArea({
</Layout>
)
}
+34 -55
View File
@@ -14,12 +14,10 @@ import {
Descriptions,
Upload,
Tooltip,
Tabs,
} from "antd"
import type { ColumnsType } from "antd/es/table"
import { useTranslation } from "react-i18next"
import { UploadOutlined, DeleteOutlined, FolderOpenOutlined } from "@ant-design/icons"
import { BuiltinPluginManager } from "./BuiltinPluginManager"
interface Plugin {
id: string
@@ -51,8 +49,6 @@ export const PluginManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const [installLoading, setInstallLoading] = useState(false)
const [selectedPath, setSelectedPath] = useState<string>("")
const [messageApi, contextHolder] = message.useMessage()
const [activeTab, setActiveTab] = useState("external")
const emitPluginsUpdated = (action: "install" | "uninstall" | "toggle", pluginId?: string) => {
window.dispatchEvent(
new CustomEvent("ss:plugins-updated", {
@@ -236,61 +232,44 @@ export const PluginManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
},
]
const tabItems = [
{
key: "external",
label: "外部插件",
children: (
<>
<Card size="small" style={{ marginBottom: 16 }}>
<Descriptions size="small" column={3}>
<Descriptions.Item label={t("plugin.totalPlugins")}>
<Tag color="blue">{stats.total_plugins}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t("plugin.enabledPlugins")}>
<Tag color="success">{stats.enabled_plugins}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t("plugin.disabledPlugins")}>
<Tag color="default">{stats.disabled_plugins}</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
<div style={{ marginBottom: 16, display: "flex", justifyContent: "space-between" }}>
<h2 style={{ margin: 0, color: "var(--ss-text-main)" }}>{t("plugin.title")}</h2>
<Button
type="primary"
icon={<FolderOpenOutlined />}
disabled={!canEdit}
onClick={() => setInstallModalVisible(true)}
>
{t("plugin.install")}
</Button>
</div>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
locale={{ emptyText: t("plugin.noPlugins") }}
pagination={{ pageSize: 10, showSizeChanger: false }}
/>
</>
),
},
{
key: "builtin",
label: "内置插件",
children: <BuiltinPluginManager canEdit={canEdit} />,
},
]
return (
<div style={{ padding: "24px" }}>
{contextHolder}
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
<Card size="small" style={{ marginBottom: 16 }}>
<Descriptions size="small" column={3}>
<Descriptions.Item label={t("plugin.totalPlugins")}>
<Tag color="blue">{stats.total_plugins}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t("plugin.enabledPlugins")}>
<Tag color="success">{stats.enabled_plugins}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t("plugin.disabledPlugins")}>
<Tag color="default">{stats.disabled_plugins}</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
<div style={{ marginBottom: 16, display: "flex", justifyContent: "space-between" }}>
<h2 style={{ margin: 0, color: "var(--ss-text-main)" }}>{t("plugin.title")}</h2>
<Button
type="primary"
icon={<FolderOpenOutlined />}
disabled={!canEdit}
onClick={() => setInstallModalVisible(true)}
>
{t("plugin.install")}
</Button>
</div>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
locale={{ emptyText: t("plugin.noPlugins") }}
pagination={{ pageSize: 10, showSizeChanger: false }}
/>
<Modal
title={t("plugin.install")}
+1 -7
View File
@@ -855,13 +855,7 @@ export const Settings: React.FC<{
<Divider />
{/* 主题设置 - 预留区域 */}
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: 600, marginBottom: "12px", fontSize: "16px" }}>
{t("settings.theme.title", "主题设置")}
</div>
<ThemeQuickSettings />
</div>
<ThemeQuickSettings />
<Divider />
-475
View File
@@ -1,475 +0,0 @@
import React, { useEffect, useRef, useState } from "react"
import { Menu, Card, Form, Select, Switch, message, Layout, Button, Space } from "antd"
import {
SettingOutlined,
SafetyOutlined,
UserOutlined,
DatabaseOutlined,
FileTextOutlined,
LinkOutlined,
InfoCircleOutlined,
ApiOutlined,
CloseOutlined,
MinusOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
} from "@ant-design/icons"
import { ThemeQuickSettings } from "./ThemeQuickSettings"
import { useTranslation } from "react-i18next"
import { pinyin } from "pinyin-pro"
import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n"
import { useResponsive } from "../hooks/useResponsive"
import {
buildSystemFontFamily,
buildSystemFontValue,
SYSTEM_FONT_STACK,
} from "../shared/fontFamily"
import { useConfig } from "../hooks/useConfig"
const { Content } = Layout
type permissionLevel = "admin" | "points" | "view"
interface FontOption {
value: string
label: string
fontFamily: string
searchText: string
}
const CHINESE_FONT_CHAR_PATTERN = /[\u3400-\u9fff]/
const CHINESE_FONT_SEGMENT_PATTERN = /[\u3400-\u9fff]+/g
const toPascalCasePinyin = (value: string): string =>
pinyin(value, { toneType: "none" })
.split(/\s+/)
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())
.join("")
const formatFontDisplayName = (name: string): string => {
if (!CHINESE_FONT_CHAR_PATTERN.test(name)) return name
return name.replace(CHINESE_FONT_SEGMENT_PATTERN, (segment) => toPascalCasePinyin(segment))
}
const buildFontSearchText = (name: string, label: string): string =>
`${name} ${label}`.toLowerCase()
const defaultFontOptions: FontOption[] = [
{
value: "system",
label: "系统默认",
fontFamily: SYSTEM_FONT_STACK,
searchText: "系统默认 system default",
},
]
const mergeFontOptions = (options: FontOption[]): FontOption[] => {
const map = new Map<string, FontOption>()
for (const option of options) {
if (!map.has(option.value)) {
map.set(option.value, option)
}
}
return Array.from(map.values())
}
const findFontOption = (options: FontOption[], value?: string): FontOption | undefined => {
if (!value) return options.find((item) => item.value === "system") || options[0]
return (
options.find((item) => item.value === value) || options.find((item) => item.value === "system")
)
}
const applyFontFamily = (fontFamily: string) => {
document.documentElement.style.setProperty("--ss-font-family", fontFamily)
document.body.style.fontFamily = fontFamily
}
export const SettingsWindow: 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 [fontFamily, setFontFamily, fontLoading] = useConfig("font_family", "system")
const [windowZoom, setWindowZoom] = useConfig("window_zoom", 1.0)
const [searchKeyboardLayout, setSearchKeyboardLayout] = useConfig(
"search_keyboard_layout",
"qwerty26" as any
)
const [disableSearchKeyboard, setDisableSearchKeyboard] = useConfig(
"disable_search_keyboard",
false
)
const [fontOptions, setFontOptions] = useState<FontOption[]>(defaultFontOptions)
const [isLoadingFonts, setIsLoadingFonts] = useState(false)
const fontOptionsRef = useRef<FontOption[]>(defaultFontOptions)
const canAdmin = permission === "admin"
const [messageApi, contextHolder] = message.useMessage()
// 窗口控制状态
const [isMaximized, setIsMaximized] = useState(false)
// 窗口控制函数
const handleMinimize = async () => {
const api = (window as any).api
if (api?.windowMinimize) {
await api.windowMinimize()
}
}
const handleMaximize = async () => {
const api = (window as any).api
if (api?.windowMaximize) {
const result = await api.windowMaximize()
setIsMaximized(result)
}
}
const handleClose = async () => {
const api = (window as any).api
if (api?.closeSettingsWindow) {
await api.closeSettingsWindow()
}
}
// 监听最大化状态变化
useEffect(() => {
const api = (window as any).api
if (api?.onWindowMaximizedChanged) {
api.onWindowMaximizedChanged((maximized: boolean) => {
setIsMaximized(maximized)
})
}
}, [])
const loadSystemFonts = async (selectedFontValue?: string) => {
setIsLoadingFonts(true)
const applySelectedFont = (options: FontOption[]) => {
const current = findFontOption(options, selectedFontValue || fontFamily)
if (current) applyFontFamily(current.fontFamily)
}
const api = (window as any).api
if (!api?.getSystemFonts) {
setFontOptions(defaultFontOptions)
applySelectedFont(defaultFontOptions)
setIsLoadingFonts(false)
return
}
try {
const res = await api.getSystemFonts()
if (res.success && Array.isArray(res.data)) {
const remoteOptions = res.data
.map((name: string) => String(name || "").trim())
.filter((name: string) => Boolean(name))
.map((name: string) => {
const label = formatFontDisplayName(name)
return {
value: buildSystemFontValue(name),
label,
fontFamily: buildSystemFontFamily(name),
searchText: buildFontSearchText(name, label),
} satisfies FontOption
})
.sort((a: FontOption, b: FontOption) =>
a.label.localeCompare(b.label, "zh-Hans-CN-u-co-pinyin")
)
const mergedOptions = mergeFontOptions([...defaultFontOptions, ...remoteOptions])
setFontOptions(mergedOptions)
applySelectedFont(mergedOptions)
} else {
setFontOptions(defaultFontOptions)
applySelectedFont(defaultFontOptions)
}
} catch (error) {
console.error("Failed to load system fonts:", error)
setFontOptions(defaultFontOptions)
applySelectedFont(defaultFontOptions)
} finally {
setIsLoadingFonts(false)
}
}
useEffect(() => {
fontOptionsRef.current = fontOptions
}, [fontOptions])
useEffect(() => {
loadSystemFonts(fontFamily)
}, [])
const menuItems = [
{
key: "appearance",
icon: <SettingOutlined />,
label: t("settings.tabs.appearance"),
},
{
key: "security",
icon: <SafetyOutlined />,
label: t("settings.tabs.security"),
},
{
key: "account",
icon: <UserOutlined />,
label: t("settings.tabs.account"),
},
{
key: "database",
icon: <DatabaseOutlined />,
label: t("settings.database.title"),
},
{
key: "data",
icon: <FileTextOutlined />,
label: t("settings.data.title"),
},
{
key: "url",
icon: <LinkOutlined />,
label: t("settings.url.title"),
},
{
key: "about",
icon: <InfoCircleOutlined />,
label: t("settings.about.title"),
},
{
key: "api",
icon: <ApiOutlined />,
label: t("settings.mcp.title"),
},
]
const renderContent = () => {
switch (activeTab) {
case "appearance":
return (
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<Form layout="vertical">
<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,
}))}
/>
</Form.Item>
<ThemeQuickSettings />
<Form.Item label={t("settings.fontFamily")}>
<Select
value={fontFamily}
onChange={async (v) => {
const fontOpt = findFontOption(fontOptions, String(v))
if (!fontOpt) return
applyFontFamily(fontOpt.fontFamily)
await setFontFamily(String(v))
messageApi.success(t("settings.general.saved"))
}}
style={{ width: "320px" }}
loading={isLoadingFonts || fontLoading}
options={fontOptions.map((opt) => ({
value: opt.value,
label: opt.label,
searchText: opt.searchText,
}))}
showSearch
filterOption={(input, option) =>
String(option?.searchText ?? option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
/>
</Form.Item>
{!isMobile && (
<>
<Form.Item label={t("settings.searchKeyboard.title")}>
<Select
value={searchKeyboardLayout}
onChange={async (v) => {
await setSearchKeyboardLayout(String(v) as any)
messageApi.success(t("settings.general.saved"))
}}
style={{ width: "320px" }}
disabled={!canAdmin}
options={[
{ value: "qwerty26", label: t("settings.searchKeyboard.options.qwerty26") },
{ value: "t9", label: t("settings.searchKeyboard.options.t9") },
]}
/>
</Form.Item>
<Form.Item label={t("settings.searchKeyboard.disableToggle")}>
<Switch
checked={disableSearchKeyboard}
onChange={async (checked) => {
await setDisableSearchKeyboard(checked)
messageApi.success(t("settings.general.saved"))
}}
disabled={!canAdmin}
/>
</Form.Item>
</>
)}
<Form.Item label={t("settings.interfaceZoom")}>
<Select
value={String(windowZoom)}
onChange={async (v) => {
await setWindowZoom(Number(v))
messageApi.success(t("settings.general.saved"))
}}
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") },
]}
/>
</Form.Item>
</Form>
</Card>
)
default:
return (
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
<div
style={{ textAlign: "center", padding: "40px", color: "var(--ss-text-secondary)" }}
>
{t("common.comingSoon")}
</div>
</Card>
)
}
}
return (
<Layout style={{ minHeight: "100vh", background: "var(--ss-bg)" }}>
{contextHolder}
{!isMobile && (
<Layout.Sider
width={240}
style={{
background: "var(--ss-card-bg)",
borderRight: "1px solid var(--ss-border)",
}}
>
<div style={{ padding: "16px", color: "var(--ss-text-main)", fontWeight: 600 }}>
{t("settings.title")}
</div>
<Menu
mode="inline"
selectedKeys={[activeTab]}
items={menuItems}
onClick={({ key }) => setActiveTab(key)}
style={{ borderRight: 0 }}
/>
</Layout.Sider>
)}
<Layout>
{/* 窗口标题栏 */}
<div
data-tauri-drag-region
style={{
height: "40px",
background: "var(--ss-card-bg)",
borderBottom: "1px solid var(--ss-border)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 16px",
userSelect: "none",
// @ts-ignore - Tauri specific property
WebkitAppRegion: "drag",
}}
>
<div style={{ fontWeight: 500, color: "var(--ss-text-main)" }}>{t("settings.title")}</div>
<Space
size={4}
style={{
// @ts-ignore - Tauri specific property
WebkitAppRegion: "no-drag",
}}
>
<Button type="text" size="small" icon={<MinusOutlined />} onClick={handleMinimize} />
<Button
type="text"
size="small"
icon={isMaximized ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={handleMaximize}
/>
<Button
type="text"
size="small"
danger
icon={<CloseOutlined />}
onClick={handleClose}
/>
</Space>
</div>
<Content style={{ padding: "24px", overflow: "auto" }}>
<div style={{ maxWidth: "900px", margin: "0 auto" }}>{renderContent()}</div>
</Content>
</Layout>
{/* Mobile bottom navigation */}
{isMobile && (
<div
style={{
position: "fixed",
bottom: 0,
left: 0,
right: 0,
background: "var(--ss-card-bg)",
borderTop: "1px solid var(--ss-border)",
display: "flex",
justifyContent: "space-around",
padding: "8px 0",
}}
>
{menuItems.map((item) => (
<div
key={item.key}
onClick={() => setActiveTab(item.key)}
style={{
textAlign: "center",
color:
activeTab === item.key ? "var(--ant-color-primary)" : "var(--ss-text-secondary)",
cursor: "pointer",
}}
>
{item.icon}
<div style={{ fontSize: "12px", marginTop: "4px" }}>{item.label}</div>
</div>
))}
</div>
)}
</Layout>
)
}
+60 -121
View File
@@ -16,7 +16,6 @@ import {
import { useState, useEffect } from "react"
import { useTranslation } from "react-i18next"
import appLogo from "../assets/logoHD.svg"
import { usePluginEnabled } from "../hooks/useBuiltinPlugins"
const { Sider } = Layout
@@ -36,25 +35,6 @@ interface DbStatus {
error?: string
}
// 插件菜单项配置
const PLUGIN_MENU_ITEMS: Record<
string,
{ icon: React.ReactElement; labelKey: string; requiresAdmin: boolean }
> = {
"auto-score": { icon: <SyncOutlined />, labelKey: "sidebar.autoScore", requiresAdmin: true },
boards: { icon: <ApartmentOutlined />, labelKey: "sidebar.boards", requiresAdmin: false },
settlements: {
icon: <FileTextOutlined />,
labelKey: "sidebar.settlements",
requiresAdmin: false,
},
"reward-settings": {
icon: <AppstoreAddOutlined />,
labelKey: "sidebar.rewardSettings",
requiresAdmin: true,
},
}
export function Sidebar({
activeMenu,
permission,
@@ -69,12 +49,6 @@ export function Sidebar({
const [syncLoading, setSyncLoading] = useState(false)
const [messageApi, contextHolder] = message.useMessage()
// 检查插件启用状态
const autoScoreEnabled = usePluginEnabled("auto-score")
const boardsEnabled = usePluginEnabled("boards")
const settlementsEnabled = usePluginEnabled("settlements")
const rewardSettingsEnabled = usePluginEnabled("reward-settings")
useEffect(() => {
loadDbStatus()
const handleStatusChange = () => {
@@ -173,95 +147,70 @@ export function Sidebar({
}
}
// 构建菜单项
const buildMenuItems = () => {
const items = [
{
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"),
},
]
// 根据插件状态添加菜单项
if (autoScoreEnabled) {
items.push({
key: "auto-score",
icon: PLUGIN_MENU_ITEMS["auto-score"].icon,
label: t(PLUGIN_MENU_ITEMS["auto-score"].labelKey),
disabled: permission !== "admin" && PLUGIN_MENU_ITEMS["auto-score"].requiresAdmin,
})
}
if (rewardSettingsEnabled) {
items.push({
key: "reward-settings",
icon: PLUGIN_MENU_ITEMS["reward-settings"].icon,
label: t(PLUGIN_MENU_ITEMS["reward-settings"].labelKey),
disabled: permission !== "admin" && PLUGIN_MENU_ITEMS["reward-settings"].requiresAdmin,
})
}
if (boardsEnabled) {
items.push({
key: "boards",
icon: PLUGIN_MENU_ITEMS["boards"].icon,
label: t(PLUGIN_MENU_ITEMS["boards"].labelKey),
disabled: permission !== "admin" && PLUGIN_MENU_ITEMS["boards"].requiresAdmin,
})
}
items.push({
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"),
disabled: permission !== "admin",
},
{
key: "reward-settings",
icon: <AppstoreAddOutlined />,
label: t("sidebar.rewardSettings"),
disabled: permission !== "admin",
},
{
key: "boards",
icon: <ApartmentOutlined />,
label: t("sidebar.boards"),
},
{
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: "plugins",
icon: <CrownOutlined />,
label: t("sidebar.plugins"),
disabled: permission !== "admin",
},
{
key: "settings",
icon: <SettingOutlined />,
label: t("sidebar.settings"),
disabled: permission !== "admin",
},
]
if (settlementsEnabled) {
items.push({
key: "settlements",
icon: PLUGIN_MENU_ITEMS["settlements"].icon,
label: t(PLUGIN_MENU_ITEMS["settlements"].labelKey),
disabled: permission !== "admin" && PLUGIN_MENU_ITEMS["settlements"].requiresAdmin,
})
}
items.push(
{
key: "reasons",
icon: <UnorderedListOutlined />,
label: t("sidebar.reasons"),
disabled: permission !== "admin",
},
{
key: "plugins",
icon: <CrownOutlined />,
label: t("sidebar.plugins"),
disabled: permission !== "admin",
},
{
key: "settings",
icon: <SettingOutlined />,
label: t("sidebar.settings"),
disabled: permission !== "admin",
}
)
return items
}
const menuItems = buildMenuItems()
const showFloatingPanel = floatingExpand && collapsed && floatingExpanded
const renderSidebarBody = (isCollapsedView: boolean, hideMenu = false) => (
@@ -328,16 +277,6 @@ export function Sidebar({
inlineCollapsed={isCollapsedView}
selectedKeys={[activeMenu]}
onClick={({ key }) => {
if (key === "settings") {
// 打开设置窗口
const api = (window as any).api
if (api?.openSettingsWindow) {
api.openSettingsWindow().catch((err: any) => {
console.error("Failed to open settings window:", err)
})
}
return
}
onMenuChange(key)
if (floatingExpand && collapsed) {
onFloatingExpandedChange(false)