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
-56
View File
@@ -1,63 +1,7 @@
import { Context } from "./shared/kernel"
import { appRegistry } from "./di/AppServiceRegistry"
import { PluginRuntimeContext } from "./di"
export class ClientContext extends Context {
private diInitialized = false
constructor() {
super()
}
/**
* 初始化 DI 系统
*/
initializeDI(): void {
if (this.diInitialized) return
const runtimeContext: PluginRuntimeContext = {
app: "renderer",
logger: {
info: (...args: any[]) => console.log("[Renderer]", ...args),
warn: (...args: any[]) => console.warn("[Renderer]", ...args),
error: (...args: any[]) => console.error("[Renderer]", ...args),
debug: (...args: any[]) => console.debug("[Renderer]", ...args),
},
config: {},
settings: {
all: () => ({}),
get: <T = unknown>(_key?: string, _def?: T) => ({}) as T,
set: async () => {},
patch: async () => {},
reset: async () => {},
onChange: () => () => {},
},
effect: (fn: () => void | (() => void) | Promise<void | (() => void)>) => {
const disposer = fn()
if (typeof disposer === "function") {
this.effect(disposer)
} else if (disposer && typeof (disposer as any).then === "function") {
;(disposer as Promise<void>).then((d) => {
if (typeof d === "function") this.effect(d)
})
}
},
services: {
provide: () => () => {},
inject: <T = unknown>(_name: string, _owner?: string) => ({}) as T,
has: () => false,
},
desktopApi: (window as any).api,
}
appRegistry.initialize(runtimeContext)
this.diInitialized = true
}
/**
* 获取服务注册表
*/
getRegistry() {
return appRegistry
}
}
+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)
-183
View File
@@ -1,183 +0,0 @@
import type {
ConfigureHostDelegate,
HostBuilderContext,
HostExposure,
HostedService,
PluginHostApplicationContext,
PluginMiddleware,
ServiceToken,
} from "./types"
import { ServiceProvider } from "./serviceCollection"
// 插件Host应用类,管理应用的生命周期和托管服务
export class PluginHostApplication {
private hostedInstances: HostedService[] = [] // 已启动的托管服务实例
private started = false // 是否已启动
constructor(
private readonly context: HostBuilderContext, // Host构建器上下文
private readonly provider: ServiceProvider, // 服务提供者
private readonly configureDelegates: ConfigureHostDelegate[], // 配置委托
private readonly middleware: PluginMiddleware[], // 中间件
private readonly hostedTokens: ServiceToken<HostedService>[] // 托管服务令牌
) {}
// 包装应用,添加服务暴露功能
withExposures(exposures: HostExposure[]): PluginHostApplicationWithExposures {
return new PluginHostApplicationWithExposures(this, exposures)
}
// 启动应用
async start(): Promise<void> {
if (this.started) return
const appCtx = this.createApplicationContext() // 创建应用上下文
// 执行所有配置委托
for (const configure of this.configureDelegates) {
await configure(this.context, appCtx)
}
// 通过中间件管道执行终端逻辑(启动托管服务)
await this.dispatch(0, appCtx, async () => {
await this.bootstrapHostedServices()
})
this.started = true
await this.context.lifetime.notifyStarted() // 通知生命周期已启动
}
// 停止应用
async stop(): Promise<void> {
if (!this.started) return
await this.context.lifetime.notifyStopping() // 通知正在停止
await this.disposeHostedServices() // 停止托管服务
await this.context.lifetime.notifyStopped() // 通知已停止
this.started = false
}
// 释放资源
async dispose(): Promise<void> {
await this.stop()
await this.provider.dispose() // 释放服务提供者
}
// 获取服务提供者
get services(): ServiceProvider {
return this.provider
}
// 获取Host上下文
get hostContext(): HostBuilderContext {
return this.context
}
// 创建应用上下文
private createApplicationContext(): PluginHostApplicationContext {
return {
ctx: this.context.ctx,
services: this.provider,
host: this.context,
}
}
// 启动所有托管服务
private async bootstrapHostedServices() {
for (const token of this.hostedTokens) {
const service = this.provider.get(token) // 从提供者获取服务实例
this.hostedInstances.push(service)
if (typeof service.start === "function") {
await service.start() // 调用启动方法
}
}
}
// 停止所有托管服务
private async disposeHostedServices() {
while (this.hostedInstances.length) {
const service = this.hostedInstances.pop()
if (!service) continue
if (typeof service.stop === "function") {
await service.stop() // 调用停止方法
}
}
}
// 执行中间件管道
private async dispatch(
index: number,
appCtx: PluginHostApplicationContext,
terminal: () => Promise<void>
) {
const middleware = this.middleware[index]
if (!middleware) {
await terminal() // 执行终端逻辑
return
}
// 调用中间件,传入下一个中间件的执行函数
await middleware(appCtx, () => this.dispatch(index + 1, appCtx, terminal))
}
}
// 带服务暴露的Host应用类
export class PluginHostApplicationWithExposures {
private readonly exposureDisposers: Array<() => void> = [] // 暴露服务的清理函数
constructor(
private readonly app: PluginHostApplication,
private readonly exposures: HostExposure[]
) {}
// 启动应用,包括注册暴露服务
async start(): Promise<void> {
try {
await this.registerExposures() // 先注册暴露服务
await this.app.start() // 再启动应用
} catch (error) {
await this.disposeExposures() // 出错时清理暴露服务
throw error
}
}
// 停止应用,包括清理暴露服务
async stop(): Promise<void> {
await this.app.stop()
await this.disposeExposures()
}
// 释放资源
async dispose(): Promise<void> {
await this.app.dispose()
await this.disposeExposures()
}
// 获取服务提供者
get services() {
return this.app.services
}
// 获取Host上下文
get hostContext() {
return this.app.hostContext
}
// 注册暴露的服务到插件上下文
private async registerExposures() {
const ctx = this.app.hostContext.ctx
if (!ctx.services) return
for (const exposure of this.exposures) {
const value = exposure.resolver(this.app.services) // 解析服务值
const disposer = ctx.services.provide(exposure.name, value) // 注册到服务API
this.exposureDisposers.push(disposer)
}
}
// 清理暴露的服务
private async disposeExposures() {
while (this.exposureDisposers.length) {
const dispose = this.exposureDisposers.pop()
if (!dispose) continue
await dispose() // 调用清理函数
}
}
}
-164
View File
@@ -1,164 +0,0 @@
import { ServiceCollection, ServiceProvider } from "./serviceCollection"
import { PluginHostApplication, PluginHostApplicationWithExposures } from "./hostApplication"
import {
type ConfigureHostDelegate,
type ConfigureServicesDelegate,
type HostBuilderContext,
type HostBuilderSettings,
type HostExposure,
type HostExposureResolver,
type HostedService,
type PluginMiddleware,
type PluginRuntimeContext,
type ServiceToken,
} from "./types"
// ExamAware Host构建器,类似.NET的HostBuilder,用于配置插件的服务和生命周期
export class ExamAwareHostBuilder {
private readonly serviceCollection: ServiceCollection // 服务集合,管理所有注册的服务
private readonly configureServicesDelegates: ConfigureServicesDelegate[] = [] // 配置服务的委托列表
private readonly configureDelegates: ConfigureHostDelegate[] = [] // 配置应用的委托列表
private readonly middleware: PluginMiddleware[] = [] // 中间件列表
private readonly hostedServices: ServiceToken<HostedService>[] = [] // 托管服务的令牌列表
private readonly exposures: HostExposure[] = [] // 要暴露的服务列表
private readonly builderContext: HostBuilderContext // 构建器上下文
constructor(runtimeCtx: PluginRuntimeContext, settings: HostBuilderSettings = {}) {
this.serviceCollection = new ServiceCollection(runtimeCtx)
this.builderContext = {
ctx: runtimeCtx,
environmentName: settings.environment ?? process.env.EXAMAWARE_ENV ?? "Production",
properties: new Map(Object.entries(settings.properties ?? {})),
lifetime: this.serviceCollection.getLifetime(), // 获取应用生命周期管理器
}
}
// 获取构建器上下文
get context(): HostBuilderContext {
return this.builderContext
}
// 添加服务配置委托
configureServices(callback: ConfigureServicesDelegate): this {
this.configureServicesDelegates.push(callback)
return this
}
// 添加应用配置委托
configure(callback: ConfigureHostDelegate): this {
this.configureDelegates.push(callback)
return this
}
// 添加中间件
use(middleware: PluginMiddleware): this {
this.middleware.push(middleware)
return this
}
// 添加托管服务
addHostedService(token: ServiceToken<HostedService>): this {
this.hostedServices.push(token)
return this
}
// 暴露服务到插件上下文
exposeHostService(name: string, resolver: HostExposureResolver): this {
const normalized = this.normalizeResolver(resolver)
if (normalized) {
this.exposures.push({ name, resolver: normalized })
}
return this
}
// 构建Host应用
async build(): Promise<PluginHost> {
// 执行所有服务配置委托
for (const configure of this.configureServicesDelegates) {
await configure(this.builderContext, this.serviceCollection)
}
const provider = this.serviceCollection.buildServiceProvider() // 构建服务提供者
const application = new PluginHostApplication(
this.builderContext,
provider,
this.configureDelegates,
this.middleware,
this.hostedServices
)
return new PluginHost(application.withExposures(this.exposures)) // 返回包装了暴露服务的Host
}
// 标准化暴露解析器
private normalizeResolver(
resolver: HostExposureResolver
): ((provider: ServiceProvider) => unknown) | null {
if (resolver.token) {
return (provider) => provider.get(resolver.token as ServiceToken) // 通过令牌获取服务
}
if (resolver.factory) {
return resolver.factory // 使用工厂函数
}
return null
}
}
// 插件Host类,管理应用的启动和停止
export class PluginHost {
constructor(private readonly app: PluginHostApplicationWithExposures) {}
// 获取服务提供者
get services() {
return this.app.services
}
// 获取Host上下文
get hostContext() {
return this.app.hostContext
}
// 启动应用
async start() {
await this.app.start()
}
// 停止应用
async stop() {
await this.app.stop()
}
// 释放资源
async dispose() {
await this.app.dispose()
}
// 运行应用,返回停止函数
async run() {
await this.start()
return async () => {
await this.dispose()
}
}
}
// 创建Host构建器的工厂函数
export function createPluginHostBuilder(ctx: PluginRuntimeContext, settings?: HostBuilderSettings) {
return new ExamAwareHostBuilder(ctx, settings)
}
// Host工具类,提供创建构建器的静态方法
export const Host = {
createApplicationBuilder: createPluginHostBuilder,
}
// 定义插件的辅助函数,使用Host构建器配置插件
export function defineExamAwarePlugin(
setup: (builder: ExamAwareHostBuilder) => Promise<void> | void
) {
return async function examAwarePlugin(ctx: PluginRuntimeContext) {
const builder = Host.createApplicationBuilder(ctx)
await setup(builder)
const host = await builder.build()
return host.run() // 返回运行函数,插件加载时调用
}
}
-30
View File
@@ -1,30 +0,0 @@
export {
ExamAwareHostBuilder,
PluginHost,
createPluginHostBuilder,
Host,
defineExamAwarePlugin,
} from "./hostBuilder"
export { ServiceCollection, ServiceProvider } from "./serviceCollection"
export {
PluginContextToken,
PluginLoggerToken,
PluginSettingsToken,
DesktopApiToken,
HostApplicationLifetimeToken,
} from "./tokens"
export type {
PluginRuntimeContext,
PluginLogger,
PluginSettingsAPI,
ServiceAPI,
HostedService,
PluginMiddleware,
HostBuilderSettings,
HostBuilderContext,
PluginHostApplicationLifetime,
ConfigureServicesDelegate,
ConfigureHostDelegate,
PluginHostApplicationContext,
ServiceToken,
} from "./types"
-61
View File
@@ -1,61 +0,0 @@
import type { Awaitable, Disposer, PluginHostApplicationLifetime } from "./types"
type LoggerLike = { error: (...args: any[]) => void }
// 默认的应用生命周期实现,管理启动/停止事件
export class DefaultHostApplicationLifetime implements PluginHostApplicationLifetime {
constructor(private readonly logger: LoggerLike = console) {}
private readonly started = new Set<() => Awaitable<void>>() // 启动事件处理器
private readonly stopping = new Set<() => Awaitable<void>>() // 停止事件处理器
private readonly stopped = new Set<() => Awaitable<void>>() // 已停止事件处理器
// 注册启动事件处理器
onStarted(handler: () => Awaitable<void>): Disposer {
this.started.add(handler)
return () => {
this.started.delete(handler)
} // 返回清理函数
}
// 注册停止事件处理器
onStopping(handler: () => Awaitable<void>): Disposer {
this.stopping.add(handler)
return () => {
this.stopping.delete(handler)
}
}
// 注册已停止事件处理器
onStopped(handler: () => Awaitable<void>): Disposer {
this.stopped.add(handler)
return () => {
this.stopped.delete(handler)
}
}
// 通知所有启动事件处理器
async notifyStarted() {
await this.dispatch(this.started)
}
// 通知所有停止事件处理器
async notifyStopping() {
await this.dispatch(this.stopping)
}
// 通知所有已停止事件处理器
async notifyStopped() {
await this.dispatch(this.stopped)
}
// 执行事件处理器列表
private async dispatch(targets: Set<() => Awaitable<void>>) {
for (const handler of Array.from(targets)) {
try {
await handler()
} catch (error) {
this.logger.error("[PluginHostLifetime] handler failed", error as Error) // 记录错误但不中断
}
}
}
}
-237
View File
@@ -1,237 +0,0 @@
import {
DesktopApiToken,
HostApplicationLifetimeToken,
PluginContextToken,
PluginLoggerToken,
PluginSettingsToken,
} from "./tokens"
import { DefaultHostApplicationLifetime } from "./lifetime"
import type {
Disposer,
InjectableClass,
PluginRuntimeContext,
ServiceDescriptor,
ServiceFactory,
ServiceFactoryOrValue,
ServiceLifetime,
ServiceToken,
} from "./types"
// 检查值是否为构造函数
function isConstructor<T>(value: ServiceFactoryOrValue<T>): value is InjectableClass<T> {
return (
typeof value === "function" &&
!!(value as any).prototype &&
(value as any).prototype.constructor === value
)
}
// 服务集合类,管理所有注册的服务描述符
export class ServiceCollection {
private readonly descriptors = new Map<ServiceToken, ServiceDescriptor>() // 服务描述符映射
private readonly hostLifetime: DefaultHostApplicationLifetime // 应用生命周期管理器
constructor(private readonly ctx: PluginRuntimeContext) {
this.hostLifetime = new DefaultHostApplicationLifetime((ctx as any).logger ?? console)
// 预注册常用服务
this.addSingleton(PluginContextToken, () => ctx)
this.addSingleton(PluginLoggerToken, () => ctx.logger)
this.addSingleton(PluginSettingsToken, () => ctx.settings)
this.addSingleton(DesktopApiToken, () => ctx.desktopApi)
this.addSingleton(HostApplicationLifetimeToken, () => this.hostLifetime)
}
// 获取生命周期管理器
getLifetime() {
return this.hostLifetime
}
// 添加单例服务
addSingleton<T>(token: ServiceToken<T>, impl: ServiceFactoryOrValue<T>): this {
return this.register(token, "singleton", impl)
}
// 添加作用域服务
addScoped<T>(token: ServiceToken<T>, impl: ServiceFactoryOrValue<T>): this {
return this.register(token, "scoped", impl)
}
// 添加瞬时服务
addTransient<T>(token: ServiceToken<T>, impl: ServiceFactoryOrValue<T>): this {
return this.register(token, "transient", impl)
}
// 尝试添加单例服务(如果不存在)
tryAddSingleton<T>(token: ServiceToken<T>, impl: ServiceFactoryOrValue<T>): this {
if (!this.descriptors.has(token)) {
this.addSingleton(token, impl)
}
return this
}
// 检查服务是否已注册
has(token: ServiceToken): boolean {
return this.descriptors.has(token)
}
// 清空所有服务
clear(): void {
this.descriptors.clear()
}
// 构建服务提供者
buildServiceProvider(): ServiceProvider {
return new ServiceProvider(this.ctx, new Map(this.descriptors))
}
// 注册服务
private register<T>(
token: ServiceToken<T>,
lifetime: ServiceLifetime,
impl: ServiceFactoryOrValue<T>
): this {
const descriptor: ServiceDescriptor = {
token,
lifetime,
factory: this.normalizeFactory(impl), // 标准化工厂函数
}
this.descriptors.set(token, descriptor)
return this
}
// 标准化工厂函数
private normalizeFactory<T>(impl: ServiceFactoryOrValue<T>): ServiceFactory<T> {
if (isConstructor(impl)) {
return (provider) => this.instantiateClass(impl, provider) // 构造函数,实例化类
}
if (typeof impl === "function") {
return impl as ServiceFactory<T> // 已经是工厂函数
}
return () => impl // 直接值,返回常量
}
// 实例化类,注入依赖
private instantiateClass<T>(Ctor: InjectableClass<T>, provider: ServiceProvider): T {
const deps = [...(Ctor.inject ?? [])].map((token) => provider.get(token)) // 获取依赖
return new Ctor(...deps) // 构造实例
}
}
// 服务提供者类,负责解析和提供服务实例
export class ServiceProvider {
private readonly singletonCache: Map<ServiceToken, unknown> // 单例缓存
private readonly scopedCache = new Map<ServiceToken, unknown>() // 作用域缓存
private readonly singletonCleanup: Disposer[] // 单例清理函数
private readonly scopedCleanup: Disposer[] = [] // 作用域清理函数
private readonly root: ServiceProvider | null // 根提供者
constructor(
private readonly ctx: PluginRuntimeContext,
private readonly descriptors: Map<ServiceToken, ServiceDescriptor>,
root: ServiceProvider | null = null,
private readonly isScope = false // 是否为作用域提供者
) {
this.root = root
this.singletonCache = root ? root.singletonCache : new Map() // 共享单例缓存
this.singletonCleanup = root ? root.singletonCleanup : []
}
// 获取服务实例
get<T>(token: ServiceToken<T>): T {
const descriptor = this.descriptors.get(token) as ServiceDescriptor<T> | undefined
if (!descriptor) {
const hostValue = this.resolveFromHost<T>(token) // 尝试从宿主获取
if (hostValue !== undefined) return hostValue
throw new Error(`Service not registered for token: ${token.toString?.() ?? String(token)}`)
}
switch (descriptor.lifetime) {
case "singleton":
return this.resolveSingleton(descriptor)
case "scoped":
return this.resolveScoped(descriptor)
case "transient":
default:
return this.instantiate(descriptor) // 每次都新实例
}
}
// 创建作用域提供者
createScope(): ServiceProvider {
return new ServiceProvider(this.ctx, this.descriptors, this.root ?? this, true)
}
// 释放资源
async dispose(): Promise<void> {
await this.flushCleanup(this.scopedCleanup) // 先清理作用域
if (!this.isScope) {
await this.flushCleanup(this.singletonCleanup) // 再清理单例
this.singletonCache.clear()
}
this.scopedCache.clear()
}
// 解析单例服务
private resolveSingleton<T>(descriptor: ServiceDescriptor<T>): T {
const cacheOwner = this.root ?? this
if (cacheOwner.singletonCache.has(descriptor.token)) {
return cacheOwner.singletonCache.get(descriptor.token) as T
}
const instance = this.instantiate(descriptor)
cacheOwner.singletonCache.set(descriptor.token, instance)
const disposer = this.extractDisposer(instance) // 提取清理函数
if (disposer) cacheOwner.singletonCleanup.push(disposer)
return instance
}
// 解析作用域服务
private resolveScoped<T>(descriptor: ServiceDescriptor<T>): T {
if (this.scopedCache.has(descriptor.token)) {
return this.scopedCache.get(descriptor.token) as T
}
const instance = this.instantiate(descriptor)
this.scopedCache.set(descriptor.token, instance)
const disposer = this.extractDisposer(instance)
if (disposer) this.scopedCleanup.push(disposer)
return instance
}
// 实例化服务
private instantiate<T>(descriptor: ServiceDescriptor<T>): T {
return descriptor.factory(this)
}
// 从宿主服务API解析
private resolveFromHost<T>(token: ServiceToken<T>): T | undefined {
if (typeof token === "string" && this.ctx.services?.has?.(token)) {
return this.ctx.services.inject<T>(token)
}
return undefined
}
// 提取实例的清理函数
private extractDisposer(instance: unknown): Disposer | null {
if (!instance) return null
if (typeof (instance as any).dispose === "function") {
return () => (instance as any).dispose()
}
if (typeof (instance as any).destroy === "function") {
return () => (instance as any).destroy()
}
if (typeof (instance as any).stop === "function") {
return () => (instance as any).stop()
}
return null
}
// 执行清理函数列表
private async flushCleanup(cleanup: Disposer[]) {
while (cleanup.length) {
const disposer = cleanup.pop()
if (!disposer) continue
await disposer()
}
}
}
-17
View File
@@ -1,17 +0,0 @@
// 服务令牌定义,用于依赖注入
// 使用Symbol确保唯一性,避免字符串冲突
// 插件运行时上下文令牌
export const PluginContextToken = Symbol.for("examaware.hosting.ctx")
// 插件日志器令牌
export const PluginLoggerToken = Symbol.for("examaware.hosting.logger")
// 插件设置API令牌
export const PluginSettingsToken = Symbol.for("examaware.hosting.settings")
// Desktop API令牌
export const DesktopApiToken = Symbol.for("examaware.hosting.desktopApi")
// 应用生命周期管理器令牌
export const HostApplicationLifetimeToken = Symbol.for("examaware.hosting.lifetime")
-150
View File
@@ -1,150 +0,0 @@
import type { ServiceProvideOptions, ServiceWatcherMeta } from "../../shared/services/registry"
import type { ServiceCollection, ServiceProvider } from "./serviceCollection"
// 基础类型定义
export type Awaitable<T> = T | Promise<T> // 可以是同步值或Promise
export type Disposer = () => Awaitable<void> // 清理函数,返回void或Promise<void>
// 服务令牌,可以是字符串、符号或构造函数
export type ServiceToken<T = unknown> = string | symbol | (new (...args: any[]) => T)
// 可注入的类,带可选的inject属性声明依赖
export interface InjectableClass<T = unknown> {
new (...args: any[]): T
inject?: readonly ServiceToken[] // 依赖的令牌列表
}
// 服务工厂函数,从provider获取实例
export type ServiceFactory<T> = (provider: ServiceProvider) => T
// 服务实现,可以是工厂函数、构造函数或直接值
export type ServiceFactoryOrValue<T> = ServiceFactory<T> | InjectableClass<T> | T
// 服务生命周期:单例、作用域内单例、每次都新实例
export type ServiceLifetime = "singleton" | "scoped" | "transient"
// 服务描述符,定义如何创建服务
export interface ServiceDescriptor<T = unknown> {
token: ServiceToken<T>
lifetime: ServiceLifetime
factory: ServiceFactory<T> // 创建实例的工厂函数
}
// 托管服务接口,有启动和停止方法
export interface HostedService {
start(): Awaitable<void>
stop(): Awaitable<void>
}
// Host构建器设置
export interface HostBuilderSettings {
environment?: string // 环境名,如'development'
properties?: Record<string, unknown> // 额外属性
}
// Host构建器上下文,包含运行时上下文和配置
export interface HostBuilderContext {
ctx: PluginRuntimeContext // 插件运行时上下文
environmentName: string // 当前环境
properties: Map<string | symbol, unknown> // 属性映射
lifetime: PluginHostApplicationLifetime // 应用生命周期管理
}
// 插件应用上下文,包含服务提供者和上下文
export interface PluginHostApplicationContext {
ctx: PluginRuntimeContext
services: ServiceProvider
host: HostBuilderContext
}
// 配置服务委托,在构建时注册服务
export type ConfigureServicesDelegate = (
context: HostBuilderContext,
services: ServiceCollection
) => Awaitable<void>
// 配置Host委托,在应用启动时执行
export type ConfigureHostDelegate = (
context: HostBuilderContext,
app: PluginHostApplicationContext
) => Awaitable<void>
// 中间件函数,包装应用逻辑
export type PluginMiddleware = (
app: PluginHostApplicationContext,
next: () => Promise<void>
) => Awaitable<void>
// 暴露服务解析器,可以通过令牌或工厂函数
export interface HostExposureResolver<T = unknown> {
token?: ServiceToken<T> // 通过令牌暴露
factory?: (provider: ServiceProvider) => T // 通过工厂函数暴露
}
// 服务API接口,插件用来注册和获取服务
export interface ServiceAPI {
provide: (name: string, value: unknown, options?: ServiceProvideOptions) => Disposer
inject: <T = unknown>(name: string, owner?: string) => T
injectAsync?: <T = unknown>(name: string, owner?: string) => Promise<T>
when?: <T = unknown>(
name: string,
cb: (svc: T, owner: string, meta: ServiceWatcherMeta) => void | (() => void)
) => Disposer
has: (name: string, owner?: string) => boolean
}
// 插件日志接口
export interface PluginLogger {
info: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
debug?: (...args: any[]) => void
}
// 插件设置API,读写配置
export interface PluginSettingsAPI {
all(): Record<string, any> // 获取所有配置
get<T = unknown>(key?: string, def?: T): T // 获取单个配置项
set<T = unknown>(key: string, value: T): Promise<void> // 设置配置项
patch(partial: Record<string, any>): Promise<void> // 批量更新配置
reset(): Promise<void> // 重置配置
onChange(listener: (config: Record<string, any>) => void): Disposer // 监听配置变化
}
// 插件运行时上下文,插件的核心接口
export interface PluginRuntimeContext {
app: "main" | "renderer" // 运行在主进程还是渲染进程
logger: PluginLogger // 日志器
config: Record<string, any> // 插件配置
settings: PluginSettingsAPI // 设置API
effect: (fn: () => void | Disposer | Promise<void | Disposer>) => void // 注册副作用清理
services: ServiceAPI // 服务API
windows?: {
// 窗口操作(主进程)
broadcast: (channel: string, payload?: any) => void
}
ipc?: {
// IPC通信(主进程)
registerChannel: (channel: string, handler: (event: unknown, ...args: any[]) => any) => Disposer
invokeRenderer?: (channel: string, payload?: any) => void
}
desktopApi?: unknown // Desktop API(渲染进程)
}
// 插件应用生命周期接口,管理启动/停止事件
export interface PluginHostApplicationLifetime {
onStarted(handler: () => Awaitable<void>): Disposer // 监听启动事件
onStopping(handler: () => Awaitable<void>): Disposer // 监听停止事件
onStopped(handler: () => Awaitable<void>): Disposer // 监听已停止事件
notifyStarted(): Promise<void> // 触发启动通知
notifyStopping(): Promise<void> // 触发停止通知
notifyStopped(): Promise<void> // 触发已停止通知
}
// 暴露的服务定义
export type HostExposure = {
name: string // 服务名
resolver: (provider: ServiceProvider) => unknown // 解析函数
}
// 重新导出类型,便于使用
export type { ServiceCollection, ServiceProvider }
-109
View File
@@ -1,109 +0,0 @@
import { ServiceCollection, ServiceProvider } from "../copying/hosting/serviceCollection"
import { PluginRuntimeContext, ServiceToken } from "../copying/hosting/types"
import { ConfigService } from "../services/ConfigService"
/**
* SecScore 应用服务注册表
* 统一管理所有应用级服务的注册和访问
*/
export class AppServiceRegistry {
private static instance: AppServiceRegistry
private serviceCollection: ServiceCollection | null = null
private serviceProvider: ServiceProvider | null = null
private initialized = false
private constructor() {}
static getInstance(): AppServiceRegistry {
if (!AppServiceRegistry.instance) {
AppServiceRegistry.instance = new AppServiceRegistry()
}
return AppServiceRegistry.instance
}
/**
* 初始化服务集合
* @param ctx 运行时上下文
*/
initialize(ctx: PluginRuntimeContext): void {
if (this.initialized) return
this.serviceCollection = new ServiceCollection(ctx)
this.registerCoreServices(ctx)
this.serviceProvider = this.serviceCollection.buildServiceProvider()
this.initialized = true
}
/**
* 注册核心服务
*/
private registerCoreServices(ctx: PluginRuntimeContext): void {
if (!this.serviceCollection) return
// 注册配置服务
this.serviceCollection.addSingleton("ConfigService", ConfigService)
// 注册运行时上下文
this.serviceCollection.addSingleton("PluginContext", ctx)
}
/**
* 获取服务提供者
*/
getServiceProvider(): ServiceProvider {
if (!this.serviceProvider) {
throw new Error("Service registry not initialized. Call initialize() first.")
}
return this.serviceProvider
}
/**
* 获取服务实例
*/
getService<T>(token: ServiceToken<T>): T {
return this.getServiceProvider().get(token)
}
/**
* 检查服务是否已注册
*/
hasService(token: ServiceToken): boolean {
if (!this.serviceCollection) return false
return this.serviceCollection.has(token)
}
/**
* 注册服务
*/
registerService<T>(
token: ServiceToken<T>,
impl: any,
lifetime: "singleton" | "scoped" | "transient" = "singleton"
): void {
if (!this.serviceCollection) {
throw new Error("Service collection not initialized")
}
switch (lifetime) {
case "singleton":
this.serviceCollection.addSingleton(token, impl)
break
case "scoped":
this.serviceCollection.addScoped(token, impl)
break
case "transient":
this.serviceCollection.addTransient(token, impl)
break
}
}
/**
* 是否已初始化
*/
isInitialized(): boolean {
return this.initialized
}
}
// 导出单例实例
export const appRegistry = AppServiceRegistry.getInstance()
-251
View File
@@ -1,251 +0,0 @@
import { Context, disposer } from "../shared/kernel"
/**
* 可逆插件系统基础类
* 基于 Koishi 的 Disposable 设计理念
*
* 特性:
* - 可组合性:插件可拆卸
* - 可靠性:资源安全,可追踪
* - 可访问性:热重载支持
*/
export abstract class DisposablePlugin extends Context {
private _pluginName: string
private _pluginDisposables: Set<disposer> = new Set()
private _initialized = false
constructor(name: string) {
super()
this._pluginName = name
}
/**
* 插件名称
*/
get name(): string {
return this._pluginName
}
/**
* 是否已初始化
*/
get initialized(): boolean {
return this._initialized
}
/**
* 注册可逆操作
* @param fn 清理函数
*/
protected registerDisposer(fn: disposer): void {
this._pluginDisposables.add(fn)
this.effect(() => {
this._pluginDisposables.delete(fn)
})
}
/**
* 初始化插件
* 子类应重写此方法实现具体逻辑
*/
async initialize(): Promise<void> {
if (this._initialized) return
try {
await this.onInitialize()
this._initialized = true
this.emit("initialized")
} catch (error) {
this.emit("error", error)
throw error
}
}
/**
* 插件初始化逻辑
* 子类应重写此方法
*/
protected abstract onInitialize(): Promise<void>
/**
* 卸载插件
*/
async dispose(): Promise<void> {
this.emit("disposing")
// 执行所有清理函数
const disposers = [...this._pluginDisposables]
this._pluginDisposables.clear()
for (const disposer of disposers) {
try {
await disposer()
} catch (error) {
this.emit("error", error)
}
}
// 调用父类的 dispose
super.dispose()
this._initialized = false
this.emit("disposed")
}
/**
* 热重载插件
*/
async reload(): Promise<void> {
await this.dispose()
this._pluginDisposables.clear()
await this.initialize()
}
/**
* 监听初始化完成
*/
onInitialized(fn: () => void): void {
this.once("initialized", fn)
}
/**
* 监听卸载开始
*/
onDisposing(fn: () => void): void {
this.once("disposing", fn)
}
/**
* 监听卸载完成
*/
onDisposed(fn: () => void): void {
this.once("disposed", fn)
}
/**
* 监听错误
*/
onError(fn: (error: Error) => void): void {
this.on("error", fn)
}
}
/**
* 插件工厂函数类型
*/
export type PluginFactory<T extends DisposablePlugin = DisposablePlugin> = () => T | Promise<T>
/**
* 创建插件
* @param factory 插件工厂函数
*/
export function createPlugin<T extends DisposablePlugin>(
factory: PluginFactory<T>
): PluginFactory<T> {
return factory
}
/**
* 插件管理器
* 管理所有插件的生命周期
*/
export class PluginManager extends Context {
private plugins: Map<string, DisposablePlugin> = new Map()
/**
* 注册插件
*/
async registerPlugin(plugin: DisposablePlugin): Promise<void> {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin "${plugin.name}" already registered`)
}
this.plugins.set(plugin.name, plugin)
plugin.on("error", (error) => {
this.emit("plugin-error", { plugin: plugin.name, error })
})
try {
await plugin.initialize()
this.emit("plugin-registered", plugin.name)
} catch (error) {
this.plugins.delete(plugin.name)
throw error
}
}
/**
* 卸载插件
*/
async unregisterPlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name)
if (!plugin) return
await plugin.dispose()
this.plugins.delete(name)
this.emit("plugin-unregistered", name)
}
/**
* 获取插件
*/
getPlugin<T extends DisposablePlugin>(name: string): T | undefined {
return this.plugins.get(name) as T
}
/**
* 检查插件是否存在
*/
hasPlugin(name: string): boolean {
return this.plugins.has(name)
}
/**
* 获取所有插件
*/
getAllPlugins(): Map<string, DisposablePlugin> {
return new Map(this.plugins)
}
/**
* 热重载插件
*/
async reloadPlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name)
if (!plugin) throw new Error(`Plugin "${name}" not found`)
await plugin.reload()
this.emit("plugin-reloaded", name)
}
/**
* 监听插件注册
*/
onPluginRegistered(fn: (name: string) => void): void {
this.on("plugin-registered", fn)
}
/**
* 监听插件卸载
*/
onPluginUnregistered(fn: (name: string) => void): void {
this.on("plugin-unregistered", fn)
}
/**
* 监听插件重载
*/
onPluginReloaded(fn: (name: string) => void): void {
this.on("plugin-reloaded", fn)
}
/**
* 监听插件错误
*/
onPluginError(fn: (data: { plugin: string; error: Error }) => void): void {
this.on("plugin-error", fn)
}
}
// 导出全局插件管理器实例
export const globalPluginManager = new PluginManager()
-225
View File
@@ -1,225 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
import { UnlistenFn } from "@tauri-apps/api/event"
export interface WindowConfig {
label: string
title: string
width?: number
height?: number
resizable?: boolean
maximized?: boolean
decorations?: boolean
alwaysOnTop?: boolean
center?: boolean
}
export interface WindowState {
label: string
visible: boolean
maximized: boolean
minimized: boolean
focused: boolean
}
/**
* SecScore 窗口管理器
* 统一管理所有应用窗口
*/
class WindowManagerClass {
private windows: Map<string, WindowConfig> = new Map()
private currentWindow: string | null = null
private unlistenFns: UnlistenFn[] = []
constructor() {
this.setupWindowListeners()
}
/**
* 注册窗口配置
*/
registerWindow(config: WindowConfig): void {
this.windows.set(config.label, config)
}
/**
* 获取窗口配置
*/
getWindowConfig(label: string): WindowConfig | undefined {
return this.windows.get(label)
}
/**
* 设置当前窗口
*/
setCurrentWindow(label: string): void {
this.currentWindow = label
}
/**
* 获取当前窗口标签
*/
getCurrentWindow(): string | null {
return this.currentWindow
}
/**
* 最小化窗口
*/
async minimizeWindow(): Promise<void> {
const api = (window as any).api
if (api?.windowMinimize) {
await api.windowMinimize()
} else {
await invoke("window_minimize")
}
}
/**
* 最大化/还原窗口
*/
async toggleMaximize(): Promise<void> {
const api = (window as any).api
if (api?.windowMaximize) {
const isMaximized = await api.windowIsMaximized()
if (isMaximized) {
// 还原窗口逻辑需要 Rust 端支持
await this.minimizeWindow()
} else {
await api.windowMaximize()
}
} else {
await invoke("window_maximize")
}
}
/**
* 关闭窗口
*/
async closeWindow(): Promise<void> {
const api = (window as any).api
if (api?.windowClose) {
await api.windowClose()
} else {
await invoke("window_close")
}
}
/**
* 检查窗口是否最大化
*/
async isMaximized(): Promise<boolean> {
const api = (window as any).api
if (api?.windowIsMaximized) {
return await api.windowIsMaximized()
}
return await invoke("window_is_maximized")
}
/**
* 开始拖动窗口
*/
async startDragging(): Promise<void> {
const api = (window as any).api
if (api?.startDraggingWindow) {
await api.startDraggingWindow()
}
}
/**
* 切换开发者工具
*/
async toggleDevTools(): Promise<void> {
const api = (window as any).api
if (api?.toggleDevTools) {
await api.toggleDevTools()
} else {
await invoke("toggle_devtools")
}
}
/**
* 调整窗口大小
*/
async resizeWindow(width: number, height: number): Promise<void> {
const api = (window as any).api
if (api?.windowResize) {
await api.windowResize(width, height)
} else {
await invoke("window_resize", { width, height })
}
}
/**
* 设置窗口是否可调整大小
*/
async setResizable(resizable: boolean): Promise<void> {
const api = (window as any).api
if (api?.windowSetResizable) {
await api.windowSetResizable(resizable)
} else {
await invoke("window_set_resizable", { resizable })
}
}
/**
* 监听窗口最大化变化
*/
onWindowMaximizedChanged(callback: (maximized: boolean) => void): void {
const api = (window as any).api
if (api?.onWindowMaximizedChanged) {
api.onWindowMaximizedChanged(callback).then((unlisten: UnlistenFn) => {
this.unlistenFns.push(unlisten)
})
}
}
/**
* 监听导航事件
*/
onNavigate(callback: (route: string) => void): void {
const api = (window as any).api
if (api?.onNavigate) {
api.onNavigate(callback).then((unlisten: UnlistenFn) => {
this.unlistenFns.push(unlisten)
})
}
}
/**
* 设置窗口标题
*/
async setTitle(title: string): Promise<void> {
document.title = title
}
/**
* 获取所有注册窗口
*/
getAllWindows(): Map<string, WindowConfig> {
return new Map(this.windows)
}
/**
* 清理监听器
*/
dispose(): void {
this.unlistenFns.forEach((unlisten) => unlisten())
this.unlistenFns = []
this.windows.clear()
this.currentWindow = null
}
private setupWindowListeners(): void {
// 监听最大化变化
this.onWindowMaximizedChanged((maximized) => {
console.log("Window maximized changed:", maximized)
})
// 监听导航事件
this.onNavigate((route) => {
console.log("Navigate to:", route)
})
}
}
export const WindowManager = new WindowManagerClass()
-60
View File
@@ -1,60 +0,0 @@
/**
* SecScore 依赖注入系统
* 基于 ExamAware 项目的 DI 实现进行适配
* 参照 Koishi 的 Disposable 设计理念
*/
// DI 核心
export {
ExamAwareHostBuilder,
PluginHost,
createPluginHostBuilder,
Host,
defineExamAwarePlugin,
} from "../copying/hosting/hostBuilder"
export { ServiceCollection, ServiceProvider } from "../copying/hosting/serviceCollection"
export {
PluginContextToken,
PluginLoggerToken,
PluginSettingsToken,
DesktopApiToken,
HostApplicationLifetimeToken,
} from "../copying/hosting/tokens"
export type {
PluginRuntimeContext,
PluginLogger,
PluginSettingsAPI,
ServiceAPI,
HostedService,
PluginMiddleware,
HostBuilderSettings,
HostBuilderContext,
PluginHostApplicationLifetime,
ConfigureServicesDelegate,
ConfigureHostDelegate,
PluginHostApplicationContext,
ServiceToken,
} from "../copying/hosting/types"
// 可逆插件系统
export {
DisposablePlugin,
PluginManager,
globalPluginManager,
createPlugin,
type PluginFactory,
} from "./DisposablePlugin"
// 应用服务注册表
export { AppServiceRegistry, appRegistry } from "./AppServiceRegistry"
// 窗口管理器
export { WindowManager } from "./WindowManager"
// 内置插件
export {
WikiPlugin,
NotificationPlugin,
DataExportPlugin,
type WikiPluginConfig,
} from "./plugins/builtin"
-181
View File
@@ -1,181 +0,0 @@
/**
* 内置插件示例:Wiki 插件
* 演示如何使用 DisposablePlugin 创建可逆插件
*/
import { DisposablePlugin } from "../DisposablePlugin"
import { ConfigService } from "../../services/ConfigService"
export interface WikiPluginConfig {
enabled: boolean
wikiUrl?: string
cacheEnabled: boolean
}
export class WikiPlugin extends DisposablePlugin {
private config: WikiPluginConfig = {
enabled: true,
wikiUrl: undefined,
cacheEnabled: true,
}
constructor() {
super("wiki")
}
protected async onInitialize(): Promise<void> {
console.log("[WikiPlugin] Initializing...")
// 加载配置
try {
const enabled = await ConfigService.get("auto_score_enabled")
this.config.enabled = enabled as boolean
} catch (error) {
console.error("[WikiPlugin] Failed to load config:", error)
}
// 注册配置变更监听
const unsubscribe = ConfigService.onChange((event: { key: string; value: any }) => {
if (event.key === "auto_score_enabled") {
this.config.enabled = event.value
console.log("[WikiPlugin] Config updated:", this.config.enabled)
}
})
// 注册清理函数
this.registerDisposer(() => {
unsubscribe()
console.log("[WikiPlugin] Disposed")
})
console.log("[WikiPlugin] Initialized successfully")
}
/**
* 获取 Wiki URL
*/
async setWikiUrl(url: string): Promise<void> {
this.config.wikiUrl = url
console.log("[WikiPlugin] Wiki URL set to:", url)
}
/**
* 获取当前配置
*/
getConfig(): WikiPluginConfig {
return { ...this.config }
}
}
/**
* 内置插件示例:通知插件
*/
export class NotificationPlugin extends DisposablePlugin {
private notifications: Array<{ id: string; message: string; timestamp: number }> = []
constructor() {
super("notification")
}
protected async onInitialize(): Promise<void> {
console.log("[NotificationPlugin] Initializing...")
// 可以在这里初始化通知系统
this.registerDisposer(() => {
this.notifications = []
console.log("[NotificationPlugin] Disposed")
})
console.log("[NotificationPlugin] Initialized successfully")
}
/**
* 发送通知
*/
notify(message: string): string {
const id = `notif_${Date.now()}`
this.notifications.push({
id,
message,
timestamp: Date.now(),
})
// 触发通知事件
window.dispatchEvent(
new CustomEvent("ss:notification", {
detail: { id, message, timestamp: Date.now() },
})
)
return id
}
/**
* 获取所有通知
*/
getNotifications(): typeof this.notifications {
return [...this.notifications]
}
/**
* 清除通知
*/
clear(id?: string): void {
if (id) {
this.notifications = this.notifications.filter((n) => n.id !== id)
} else {
this.notifications = []
}
}
}
/**
* 内置插件示例:数据导出插件
*/
export class DataExportPlugin extends DisposablePlugin {
private exportHistory: Array<{ id: string; type: string; timestamp: number; filename: string }> =
[]
constructor() {
super("data-export")
}
protected async onInitialize(): Promise<void> {
console.log("[DataExportPlugin] Initializing...")
this.registerDisposer(() => {
this.exportHistory = []
console.log("[DataExportPlugin] Disposed")
})
console.log("[DataExportPlugin] Initialized successfully")
}
/**
* 导出数据
*/
async exportData<T>(type: string, data: T, filename: string): Promise<void> {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
this.exportHistory.push({
id: `export_${Date.now()}`,
type,
timestamp: Date.now(),
filename,
})
console.log(`[DataExportPlugin] Exported ${type} to ${filename}`)
}
/**
* 获取导出历史
*/
getHistory(): typeof this.exportHistory {
return [...this.exportHistory]
}
}
-164
View File
@@ -1,164 +0,0 @@
import { useEffect, useState, useCallback } from "react"
import {
BuiltinPluginMeta,
getAvailablePlugins,
getInstalledPlugins,
installPlugin,
uninstallPlugin,
enablePlugin,
disablePlugin,
isPluginEnabled,
} from "../plugins/builtin"
/**
* React Hook: 使用内置插件系统
*/
export function useBuiltinPlugins() {
const [plugins, setPlugins] = useState<
Array<BuiltinPluginMeta & { installed: boolean; enabled: boolean }>
>([])
const [installedPlugins, setInstalledPlugins] = useState<
Array<BuiltinPluginMeta & { enabled: boolean; installedAt?: number }>
>([])
const [loading, setLoading] = useState(true)
const refreshPlugins = useCallback(async () => {
setLoading(true)
try {
const [available, installed] = await Promise.all([
getAvailablePlugins(),
getInstalledPlugins(),
])
setPlugins(available)
setInstalledPlugins(installed)
} catch (error) {
console.error("Failed to refresh plugins:", error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
refreshPlugins()
// 监听插件状态变化
const handleStateChanged = () => {
refreshPlugins()
}
window.addEventListener("ss:plugins-state-changed", handleStateChanged)
window.addEventListener("ss:plugin-installed", handleStateChanged)
window.addEventListener("ss:plugin-uninstalled", handleStateChanged)
window.addEventListener("ss:plugin-enabled", handleStateChanged)
window.addEventListener("ss:plugin-disabled", handleStateChanged)
return () => {
window.removeEventListener("ss:plugins-state-changed", handleStateChanged)
window.removeEventListener("ss:plugin-installed", handleStateChanged)
window.removeEventListener("ss:plugin-uninstalled", handleStateChanged)
window.removeEventListener("ss:plugin-enabled", handleStateChanged)
window.removeEventListener("ss:plugin-disabled", handleStateChanged)
}
}, [refreshPlugins])
const install = useCallback(
async (pluginId: string) => {
const result = await installPlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const uninstall = useCallback(
async (pluginId: string) => {
const result = await uninstallPlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const enable = useCallback(
async (pluginId: string) => {
const result = await enablePlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const disable = useCallback(
async (pluginId: string) => {
const result = await disablePlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const isEnabled = useCallback(async (pluginId: string) => {
return await isPluginEnabled(pluginId)
}, [])
return {
plugins,
installedPlugins,
loading,
refreshPlugins,
install,
uninstall,
enable,
disable,
isEnabled,
}
}
/**
* React Hook: 检查特定插件是否启用
*/
export function usePluginEnabled(pluginId: string): boolean {
const [enabled, setEnabled] = useState(false)
useEffect(() => {
let mounted = true
const checkEnabled = async () => {
try {
const result = await isPluginEnabled(pluginId)
if (mounted) {
setEnabled(result)
}
} catch (error) {
console.error(`Failed to check plugin ${pluginId} state:`, error)
}
}
checkEnabled()
const handleStateChanged = () => {
checkEnabled()
}
window.addEventListener("ss:plugins-state-changed", handleStateChanged)
window.addEventListener("ss:plugin-enabled", handleStateChanged)
window.addEventListener("ss:plugin-disabled", handleStateChanged)
return () => {
mounted = false
window.removeEventListener("ss:plugins-state-changed", handleStateChanged)
window.removeEventListener("ss:plugin-enabled", handleStateChanged)
window.removeEventListener("ss:plugin-disabled", handleStateChanged)
}
}, [pluginId])
return enabled
}
-99
View File
@@ -1,99 +0,0 @@
import { useEffect, useState, useCallback } from "react"
import { ConfigService, ConfigSpec } from "../services/ConfigService"
export function useConfig<K extends keyof ConfigSpec>(
key: K,
defaultValue?: ConfigSpec[K]
): [ConfigSpec[K], (value: ConfigSpec[K]) => Promise<void>, boolean] {
const [value, setValue] = useState<ConfigSpec[K]>(
ConfigService.has(key) ? ConfigService.getAll()[key] : (defaultValue as ConfigSpec[K])
)
const [loading, setLoading] = useState(!ConfigService.has(key))
useEffect(() => {
let mounted = true
const init = async () => {
try {
await ConfigService.initialize()
const currentValue = ConfigService.getAll()[key]
if (mounted) {
setValue(currentValue ?? defaultValue)
setLoading(false)
}
} catch (error) {
console.error("Failed to load config:", error)
if (mounted) {
setValue(defaultValue as ConfigSpec[K])
setLoading(false)
}
}
}
init()
const unsubscribe = ConfigService.onChange((event) => {
if (event.key === key && mounted) {
setValue(event.value)
}
})
return () => {
mounted = false
unsubscribe()
}
}, [key, defaultValue])
const updateValue = useCallback(
async (newValue: ConfigSpec[K]) => {
try {
await ConfigService.set(key, newValue)
setValue(newValue)
} catch (error) {
console.error("Failed to update config:", error)
throw error
}
},
[key]
)
return [value, updateValue, loading]
}
export function useConfigAll(): [ConfigSpec, (key: keyof ConfigSpec, value: any) => Promise<void>] {
const [config, setConfig] = useState<ConfigSpec>(ConfigService.getAll())
useEffect(() => {
let mounted = true
const init = async () => {
try {
await ConfigService.initialize()
if (mounted) {
setConfig(ConfigService.getAll())
}
} catch (error) {
console.error("Failed to load config:", error)
}
}
init()
const unsubscribe = ConfigService.onChange(() => {
if (mounted) {
setConfig(ConfigService.getAll())
}
})
return () => {
mounted = false
unsubscribe()
}
}, [])
const updateConfig = useCallback(async (key: keyof ConfigSpec, value: any) => {
await ConfigService.set(key, value)
}, [])
return [config, updateConfig]
}
-55
View File
@@ -1,55 +0,0 @@
import { useCallback } from "react"
import { logger, LogLevel } from "../services/LoggerService"
/**
* React Hook: 使用日志服务
*/
export function useLogger(source?: string) {
const log = useCallback(
(_level: LogLevel, message: string, meta?: any) => {
if (source) {
const childLogger = logger.createChild(source)
childLogger.debug(message, meta)
} else {
logger.debug(message, meta)
}
},
[source]
)
const debug = useCallback(
(message: string, meta?: any) => {
log("debug", message, meta)
},
[log]
)
const info = useCallback(
(message: string, meta?: any) => {
log("info", message, meta)
},
[log]
)
const warn = useCallback(
(message: string, meta?: any) => {
log("warn", message, meta)
},
[log]
)
const error = useCallback(
(message: string, meta?: any) => {
log("error", message, meta)
},
[log]
)
return {
debug,
info,
warn,
error,
log,
}
}
-7
View File
@@ -8,21 +8,14 @@ import { ClientContext } from "./ClientContext"
import { StudentService } from "./services/StudentService"
import { ServiceProvider } from "./contexts/ServiceContext"
import { api } from "./preload/types"
import { initBuiltinPlugins } from "./plugins/builtin"
if (!(window as any).api) {
;(window as any).api = api
}
const ctx = new ClientContext()
ctx.initializeDI()
new StudentService(ctx)
// 初始化内置插件系统
initBuiltinPlugins().catch((error) => {
console.error("Failed to initialize builtin plugins:", error)
})
const safeWriteLog = (payload: {
level: "debug" | "info" | "warn" | "error"
message: string
-288
View File
@@ -1,288 +0,0 @@
/**
* SecScore 内置插件系统
* 将非核心功能作为内置插件提供,默认不安装
*/
import { DisposablePlugin } from "../../di/DisposablePlugin"
// 插件元数据接口
export interface BuiltinPluginMeta {
id: string
name: string
description: string
version: string
author: string
icon?: string
category: "automation" | "visualization" | "management" | "integration"
defaultEnabled: boolean
requiresAdmin: boolean
}
// 内置插件注册表
export const BUILTIN_PLUGINS: BuiltinPluginMeta[] = [
{
id: "auto-score",
name: "自动评分",
description: "基于规则的自动评分系统,支持定时任务和条件触发",
version: "1.0.0",
author: "SecScore Team",
category: "automation",
defaultEnabled: false,
requiresAdmin: true,
},
{
id: "boards",
name: "看板管理",
description: "可视化数据看板,支持自定义布局和实时数据展示",
version: "1.0.0",
author: "SecScore Team",
category: "visualization",
defaultEnabled: false,
requiresAdmin: true,
},
{
id: "settlements",
name: "结算历史",
description: "积分结算记录管理,支持导出和统计分析",
version: "1.0.0",
author: "SecScore Team",
category: "management",
defaultEnabled: false,
requiresAdmin: false,
},
{
id: "reward-settings",
name: "奖励设置",
description: "奖励兑换系统配置,管理奖励项目和兑换规则",
version: "1.0.0",
author: "SecScore Team",
category: "management",
defaultEnabled: false,
requiresAdmin: true,
},
{
id: "cloud-sync",
name: "云同步",
description: "SECTL 云服务集成,支持数据云同步和跨设备访问",
version: "1.0.0",
author: "SecScore Team",
category: "integration",
defaultEnabled: false,
requiresAdmin: true,
},
]
// 插件状态存储键(用于 future use)
// const PLUGIN_STATE_KEY = "builtin_plugins_state"
// 插件状态接口
export interface PluginState {
[pluginId: string]: {
enabled: boolean
installed: boolean
installedAt?: number
config?: Record<string, unknown>
}
}
const PLUGIN_STATE_STORAGE_KEY = "secscore_builtin_plugins_state"
/**
* 获取插件状态
*/
export async function getPluginState(): Promise<PluginState> {
try {
const stored = localStorage.getItem(PLUGIN_STATE_STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
} catch {
return {}
}
}
/**
* 保存插件状态
*/
export async function savePluginState(state: PluginState): Promise<void> {
try {
localStorage.setItem(PLUGIN_STATE_STORAGE_KEY, JSON.stringify(state))
} catch (error) {
console.error("Failed to save plugin state:", error)
}
}
/**
* 检查插件是否已安装
*/
export async function isPluginInstalled(pluginId: string): Promise<boolean> {
const state = await getPluginState()
return state[pluginId]?.installed || false
}
/**
* 检查插件是否已启用
*/
export async function isPluginEnabled(pluginId: string): Promise<boolean> {
const state = await getPluginState()
return state[pluginId]?.enabled || false
}
/**
* 安装插件
*/
export async function installPlugin(pluginId: string): Promise<boolean> {
const meta = BUILTIN_PLUGINS.find((p) => p.id === pluginId)
if (!meta) return false
const state = await getPluginState()
state[pluginId] = {
enabled: meta.defaultEnabled,
installed: true,
installedAt: Date.now(),
config: {},
}
await savePluginState(state)
// 触发插件安装事件
window.dispatchEvent(new CustomEvent("ss:plugin-installed", { detail: { pluginId } }))
return true
}
/**
* 卸载插件
*/
export async function uninstallPlugin(pluginId: string): Promise<boolean> {
const state = await getPluginState()
if (state[pluginId]) {
state[pluginId].installed = false
state[pluginId].enabled = false
await savePluginState(state)
// 触发插件卸载事件
window.dispatchEvent(new CustomEvent("ss:plugin-uninstalled", { detail: { pluginId } }))
}
return true
}
/**
* 启用插件
*/
export async function enablePlugin(pluginId: string): Promise<boolean> {
const state = await getPluginState()
if (!state[pluginId]?.installed) {
// 如果未安装,先安装
await installPlugin(pluginId)
}
state[pluginId].enabled = true
await savePluginState(state)
// 触发插件启用事件
window.dispatchEvent(new CustomEvent("ss:plugin-enabled", { detail: { pluginId } }))
return true
}
/**
* 禁用插件
*/
export async function disablePlugin(pluginId: string): Promise<boolean> {
const state = await getPluginState()
if (state[pluginId]) {
state[pluginId].enabled = false
await savePluginState(state)
// 触发插件禁用事件
window.dispatchEvent(new CustomEvent("ss:plugin-disabled", { detail: { pluginId } }))
}
return true
}
/**
* 获取已安装的内置插件列表
*/
export async function getInstalledPlugins(): Promise<
Array<BuiltinPluginMeta & { enabled: boolean; installedAt?: number }>
> {
const state = await getPluginState()
return BUILTIN_PLUGINS.filter((p) => state[p.id]?.installed).map((p) => ({
...p,
enabled: state[p.id]?.enabled || false,
installedAt: state[p.id]?.installedAt,
}))
}
/**
* 获取可用的内置插件列表
*/
export async function getAvailablePlugins(): Promise<
Array<BuiltinPluginMeta & { installed: boolean; enabled: boolean }>
> {
const state = await getPluginState()
return BUILTIN_PLUGINS.map((p) => ({
...p,
installed: state[p.id]?.installed || false,
enabled: state[p.id]?.enabled || false,
}))
}
/**
* 初始化内置插件系统
* 在应用启动时调用
*/
export async function initBuiltinPlugins(): Promise<void> {
// 确保状态存在
const state = await getPluginState()
// 为每个插件初始化默认状态
let hasChanges = false
for (const plugin of BUILTIN_PLUGINS) {
if (!state[plugin.id]) {
state[plugin.id] = {
enabled: false,
installed: false,
config: {},
}
hasChanges = true
}
}
if (hasChanges) {
await savePluginState(state)
}
console.log("[BuiltinPlugins] Initialized")
}
/**
* 内置插件管理器类
*/
export class BuiltinPluginManager extends DisposablePlugin {
constructor() {
super("builtin-plugin-manager")
}
protected async onInitialize(): Promise<void> {
console.log("[BuiltinPluginManager] Initializing...")
await initBuiltinPlugins()
// 监听 localStorage 变化
const handleStorageChange = (event: StorageEvent) => {
if (event.key === PLUGIN_STATE_STORAGE_KEY) {
console.log("[BuiltinPluginManager] Plugin state changed")
window.dispatchEvent(new CustomEvent("ss:plugins-state-changed"))
}
}
window.addEventListener("storage", handleStorageChange)
this.registerDisposer(() => {
window.removeEventListener("storage", handleStorageChange)
})
console.log("[BuiltinPluginManager] Initialized")
}
}
// 导出全局实例
export const builtinPluginManager = new BuiltinPluginManager()
+1 -6
View File
@@ -852,12 +852,6 @@ const api = {
message?: string
}> => invoke("plugin_get_runtime_modules"),
// Settings Window
openSettingsWindow: (): Promise<{ success: boolean; message?: string }> =>
invoke("open_settings_window"),
closeSettingsWindow: (): Promise<{ success: boolean; message?: string }> =>
invoke("close_settings_window"),
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
invoke: async (channel: string): Promise<any> => {
switch (channel) {
@@ -869,3 +863,4 @@ const api = {
export default api
export { api }
-150
View File
@@ -1,150 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
export interface ConfigChangeEvent {
key: string
value: any
timestamp: number
}
export type ConfigChangeListener = (event: ConfigChangeEvent) => void
export interface ConfigSpec {
// 应用设置
is_wizard_completed: boolean
log_level: "debug" | "info" | "warn" | "error"
window_zoom: number
search_keyboard_layout: "t9" | "qwerty26"
disable_search_keyboard: boolean
font_family: string
current_theme_id: string
themes_custom: any[]
dashboards_config: any[]
mobile_bottom_nav_items: string[]
// 自动评分
auto_score_enabled: boolean
auto_score_rules: any[]
auto_score_batches: any[]
// 数据库
pg_connection_string: string
pg_connection_status: {
connected: boolean
type: "sqlite" | "postgresql"
error?: string
}
}
class ConfigServiceClass {
private cache: Map<string, any> = new Map()
private listeners: Set<ConfigChangeListener> = new Set()
private initialized = false
private initPromise: Promise<void> | null = null
async initialize(): Promise<void> {
if (this.initialized) return
if (this.initPromise) return this.initPromise
this.initPromise = (async () => {
try {
const result = await this.invokeApi("getAllSettings")
if (result.success && result.data) {
Object.entries(result.data).forEach(([key, value]) => {
this.cache.set(key, value)
})
}
this.setupListener()
this.initialized = true
} catch (error) {
console.error("Failed to initialize config service:", error)
throw error
}
})()
return this.initPromise
}
async get<K extends keyof ConfigSpec>(key: K): Promise<ConfigSpec[K]> {
await this.initialize()
return this.cache.get(key)
}
async set<K extends keyof ConfigSpec>(key: K, value: ConfigSpec[K]): Promise<void> {
await this.initialize()
const result = await this.invokeApi("setSetting", [key, value])
if (!result.success) {
throw new Error(result.message || "Failed to set config")
}
this.cache.set(key, value)
this.notifyListeners({ key, value, timestamp: Date.now() })
}
async patch(partial: Partial<ConfigSpec>): Promise<void> {
await this.initialize()
for (const [key, value] of Object.entries(partial)) {
await this.set(key as keyof ConfigSpec, value)
}
}
getAll(): ConfigSpec {
const entries = Array.from(this.cache.entries())
return Object.fromEntries(entries) as ConfigSpec
}
has(key: string): boolean {
return this.cache.has(key)
}
onChange(listener: ConfigChangeListener): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
private notifyListeners(event: ConfigChangeEvent): void {
this.listeners.forEach((listener) => {
try {
listener(event)
} catch (error) {
console.error("Config listener error:", error)
}
})
}
private setupListener(): void {
const api = (window as any).api
if (!api?.onSettingChanged) return
api
.onSettingChanged((change: any) => {
if (change?.key && change?.value) {
this.cache.set(change.key, change.value)
this.notifyListeners({
key: change.key,
value: change.value,
timestamp: Date.now(),
})
}
})
.catch(() => void 0)
}
private async invokeApi<T = any>(
command: string,
args?: any[]
): Promise<{ success: boolean; data?: T; message?: string }> {
const api = (window as any).api
if (api?.[command]) {
return await api[command](...(args || []))
}
// Fallback to invoke if API not available
try {
const result = await invoke(command, args ? { args } : {})
return { success: true, data: result as T }
} catch (error: any) {
return { success: false, message: error.message || "Unknown error" }
}
}
}
export const ConfigService = new ConfigServiceClass()
-230
View File
@@ -1,230 +0,0 @@
/**
* SecScore 日志服务
* 支持分级日志、多色打印、文件存储
*/
export type LogLevel = "debug" | "info" | "warn" | "error"
export interface LogEntry {
level: LogLevel
message: string
timestamp: number
meta?: any
source?: string
}
export interface LogWriter {
write(entry: LogEntry): void
}
/**
* 控制台日志写入器
* 支持多色打印
*/
class ConsoleLogWriter implements LogWriter {
private colors: Record<LogLevel, string> = {
debug: "\x1b[36m", // Cyan
info: "\x1b[32m", // Green
warn: "\x1b[33m", // Yellow
error: "\x1b[31m", // Red
}
private reset = "\x1b[0m"
write(entry: LogEntry): void {
const { level, message, timestamp, meta, source } = entry
const color = this.colors[level]
const time = new Date(timestamp).toISOString()
const prefix = source ? `[${source}]` : "[SecScore]"
const logMessage = `${color}${time} ${prefix} [${level.toUpperCase()}]${this.reset} ${message}`
switch (level) {
case "debug":
console.debug(logMessage, meta || "")
break
case "info":
console.info(logMessage, meta || "")
break
case "warn":
console.warn(logMessage, meta || "")
break
case "error":
console.error(logMessage, meta || "")
break
}
}
}
/**
* 远程日志写入器
* 将日志发送到 Rust 后端
*/
class RemoteLogWriter implements LogWriter {
write(entry: LogEntry): void {
const api = (window as any).api
if (!api?.writeLog) return
const payload = {
level: entry.level,
message: entry.message,
meta: entry.meta,
}
Promise.resolve(api.writeLog(payload)).catch(() => void 0)
}
}
/**
* 日志服务类
*/
class LoggerServiceClass {
private writers: LogWriter[] = []
private minLevel: LogLevel = "info"
private levelPriority: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
}
private source: string = "App"
private buffer: LogEntry[] = []
private bufferSize = 1000
constructor() {
// 默认添加控制台和远程写入器
this.addWriter(new ConsoleLogWriter())
this.addWriter(new RemoteLogWriter())
}
/**
* 设置日志源
*/
setSource(source: string): void {
this.source = source
}
/**
* 添加日志写入器
*/
addWriter(writer: LogWriter): void {
this.writers.push(writer)
}
/**
* 设置最低日志级别
*/
setLevel(level: LogLevel): void {
this.minLevel = level
}
/**
* 获取当前日志级别
*/
getLevel(): LogLevel {
return this.minLevel
}
/**
* 写入日志
*/
private log(level: LogLevel, message: string, meta?: any): void {
if (this.levelPriority[level] < this.levelPriority[this.minLevel]) {
return
}
const entry: LogEntry = {
level,
message,
timestamp: Date.now(),
meta,
source: this.source,
}
// 添加到缓冲区
this.buffer.push(entry)
if (this.buffer.length > this.bufferSize) {
this.buffer.shift()
}
// 写入所有写入器
this.writers.forEach((writer) => {
try {
writer.write(entry)
} catch (error) {
console.error("Log writer error:", error)
}
})
}
/**
* Debug 日志
*/
debug(message: string, meta?: any): void {
this.log("debug", message, meta)
}
/**
* Info 日志
*/
info(message: string, meta?: any): void {
this.log("info", message, meta)
}
/**
* Warning 日志
*/
warn(message: string, meta?: any): void {
this.log("warn", message, meta)
}
/**
* Error 日志
*/
error(message: string, meta?: any): void {
this.log("error", message, meta)
}
/**
* 创建带指定源的日志记录器
*/
createChild(source: string): LoggerServiceClass {
const child = new LoggerServiceClass()
child.writers = [...this.writers]
child.minLevel = this.minLevel
child.source = source
return child
}
/**
* 获取最近的日志
*/
getRecentLogs(count: number = 100): LogEntry[] {
return this.buffer.slice(-count)
}
/**
* 清空日志缓冲
*/
clear(): void {
this.buffer = []
}
}
// 导出全局日志服务实例
export const LoggerService = new LoggerServiceClass()
/**
* 便捷日志函数
*/
export const logger = {
debug: (message: string, meta?: any) => LoggerService.debug(message, meta),
info: (message: string, meta?: any) => LoggerService.info(message, meta),
warn: (message: string, meta?: any) => LoggerService.warn(message, meta),
error: (message: string, meta?: any) => LoggerService.error(message, meta),
createChild: (source: string) => LoggerService.createChild(source),
setLevel: (level: LogLevel) => LoggerService.setLevel(level),
getLevel: () => LoggerService.getLevel(),
getRecentLogs: (count?: number) => LoggerService.getRecentLogs(count),
clear: () => LoggerService.clear(),
}
-176
View File
@@ -1,176 +0,0 @@
import "./assets/main.css"
import "./i18n"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { ConfigProvider, theme as antdTheme } from "antd"
import { Settings } from "./components/Settings"
import { ThemeProvider } from "./contexts/ThemeContext"
import { ServiceProvider } from "./contexts/ServiceContext"
import { ClientContext } from "./ClientContext"
import { api } from "./preload/types"
// 确保 API 可用
if (!(window as any).api) {
;(window as any).api = api
}
// 创建 ClientContext 实例
const clientContext = new ClientContext()
clientContext.initializeDI()
// 设置窗口应用
const SettingsWindowApp = () => {
return (
<ServiceProvider value={clientContext}>
<ThemeProvider>
<ConfigProvider
theme={{
algorithm: antdTheme.defaultAlgorithm,
}}
>
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
{/* 自定义标题栏 */}
<div
data-tauri-drag-region
style={{
height: "40px",
background: "var(--ss-card-bg, #fff)",
borderBottom: "1px solid var(--ss-border, #e8e8e8)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 16px",
// @ts-ignore - Tauri specific property
WebkitAppRegion: "drag",
userSelect: "none",
flexShrink: 0,
}}
>
<div style={{ fontWeight: 500, color: "var(--ss-text-main, #000)" }}>
SecScore
</div>
<div
style={{
display: "flex",
gap: "4px",
// @ts-ignore - Tauri specific property
WebkitAppRegion: "no-drag",
}}
>
<button
onClick={async () => {
const api = (window as any).api
if (api?.windowMinimize) {
await api.windowMinimize()
}
}}
style={{
width: "32px",
height: "32px",
border: "none",
background: "transparent",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--ss-hover-bg, #f5f5f5)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
}}
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<rect x="0" y="5" width="12" height="2" />
</svg>
</button>
<button
onClick={async () => {
const api = (window as any).api
if (api?.windowMaximize) {
await api.windowMaximize()
}
}}
style={{
width: "32px",
height: "32px",
border: "none",
background: "transparent",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--ss-hover-bg, #f5f5f5)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
}}
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<rect
x="1"
y="1"
width="10"
height="10"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
/>
</svg>
</button>
<button
onClick={async () => {
const api = (window as any).api
if (api?.closeSettingsWindow) {
await api.closeSettingsWindow()
}
}}
style={{
width: "32px",
height: "32px",
border: "none",
background: "transparent",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "#ff4d4f"
e.currentTarget.style.color = "#fff"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
e.currentTarget.style.color = "inherit"
}}
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<path d="M1 1L11 11M1 11L11 1" stroke="currentColor" strokeWidth="1.5" />
</svg>
</button>
</div>
</div>
{/* 设置内容 */}
<div style={{ flex: 1, overflow: "auto" }}>
<Settings permission="admin" />
</div>
</div>
</ConfigProvider>
</ThemeProvider>
</ServiceProvider>
)
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<SettingsWindowApp />
</StrictMode>
)
-13
View File
@@ -1,13 +0,0 @@
/**
* 服务注册表类型定义
*/
export interface ServiceProvideOptions {
overwrite?: boolean
immediate?: boolean
}
export interface ServiceWatcherMeta {
name: string
owner?: string
}