mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 09:39:03 +08:00
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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() // 调用清理函数
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() // 返回运行函数,插件加载时调用
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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) // 记录错误但不中断
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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 }
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* 服务注册表类型定义
|
||||
*/
|
||||
|
||||
export interface ServiceProvideOptions {
|
||||
overwrite?: boolean
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export interface ServiceWatcherMeta {
|
||||
name: string
|
||||
owner?: string
|
||||
}
|
||||
Reference in New Issue
Block a user