mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
feat(URL协议): 增强URL协议注册健壮性
- 新增check_url_protocol_status命令检测注册状态 - 新增unregister_url_protocol命令取消注册 - 改进register_url_protocol命令增加验证逻辑 - 前端增加状态检测、自动重试和操作日志功能 - 更新多语言翻译支持新功能
This commit is contained in:
+523
-139
@@ -14,9 +14,14 @@ import {
|
||||
message,
|
||||
Avatar,
|
||||
Typography,
|
||||
Radio,
|
||||
Collapse,
|
||||
} from "antd"
|
||||
import { CloudOutlined, DatabaseOutlined } from "@ant-design/icons"
|
||||
import { ThemeQuickSettings } from "./ThemeQuickSettings"
|
||||
import { OAuthLogin } from "./OAuth/OAuthLogin"
|
||||
import { SectlCloudSyncPanel } from "./SectlCloudSyncPanel"
|
||||
import { SectlProvider } from "../contexts/SectlContext"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { pinyin } from "pinyin-pro"
|
||||
import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n"
|
||||
@@ -168,6 +173,21 @@ export const Settings: React.FC<{
|
||||
const [settleDialogVisible, setSettleDialogVisible] = useState(false)
|
||||
|
||||
const [urlRegisterLoading, setUrlRegisterLoading] = useState(false)
|
||||
const [elevationStatus, setElevationStatus] = useState<{
|
||||
is_elevated: boolean
|
||||
platform: string
|
||||
can_request_elevation: boolean
|
||||
} | null>(null)
|
||||
const [elevationLoading, setElevationLoading] = useState(false)
|
||||
const [urlStatus, setUrlStatus] = useState<{
|
||||
registered: boolean
|
||||
protocol: string
|
||||
platform: string
|
||||
details: string
|
||||
} | null>(null)
|
||||
const [urlStatusLoading, setUrlStatusLoading] = useState(false)
|
||||
const [urlUnregisterLoading, setUrlUnregisterLoading] = useState(false)
|
||||
const [urlOperationLogs, setUrlOperationLogs] = useState<string[]>([])
|
||||
const [appQuitLoading, setAppQuitLoading] = useState(false)
|
||||
const [appRestartLoading, setAppRestartLoading] = useState(false)
|
||||
const [mcpLoading, setMcpLoading] = useState(false)
|
||||
@@ -187,6 +207,8 @@ export const Settings: React.FC<{
|
||||
const canAdmin = permission === "admin"
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const [syncMethod, setSyncMethod] = useState<"postgresql" | "sectl_cloud">("postgresql")
|
||||
|
||||
const [pgConnectionString, setPgConnectionString] = useState("")
|
||||
const [pgConnectionStatus, setPgConnectionStatus] = useState<{
|
||||
connected: boolean
|
||||
@@ -307,6 +329,11 @@ export const Settings: React.FC<{
|
||||
setSettings(res.data)
|
||||
setPgConnectionString(res.data.pg_connection_string || "")
|
||||
setPgConnectionStatus(res.data.pg_connection_status || { connected: true, type: "sqlite" })
|
||||
if (res.data.sync_method === "sectl_cloud") {
|
||||
setSyncMethod("sectl_cloud")
|
||||
} else {
|
||||
setSyncMethod("postgresql")
|
||||
}
|
||||
savedFontFamily = res.data.font_family || "system"
|
||||
}
|
||||
const authRes = await api.authGetStatus()
|
||||
@@ -326,6 +353,28 @@ export const Settings: React.FC<{
|
||||
|
||||
await loadMcpStatus()
|
||||
await loadSystemFonts(savedFontFamily)
|
||||
|
||||
if (api.checkElevation) {
|
||||
try {
|
||||
const elevRes = await api.checkElevation()
|
||||
if (elevRes?.success && elevRes.data) {
|
||||
setElevationStatus(elevRes.data)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (api.checkUrlProtocolStatus) {
|
||||
try {
|
||||
const urlRes = await api.checkUrlProtocolStatus()
|
||||
if (urlRes?.success && urlRes.data) {
|
||||
setUrlStatus(urlRes.data)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleOAuthLogout = async () => {
|
||||
@@ -345,6 +394,118 @@ export const Settings: React.FC<{
|
||||
}
|
||||
}
|
||||
|
||||
const handleRequestElevation = async () => {
|
||||
const api = (window as any).api
|
||||
if (!api?.requestElevation) return
|
||||
setElevationLoading(true)
|
||||
try {
|
||||
const res = await api.requestElevation()
|
||||
if (res?.success) {
|
||||
messageApi.success(t("settings.elevation.requestSuccess"))
|
||||
} else {
|
||||
messageApi.error(res?.message || t("settings.elevation.requestFailed"))
|
||||
}
|
||||
} catch (error: any) {
|
||||
messageApi.error(error?.message || t("settings.elevation.requestFailed"))
|
||||
} finally {
|
||||
setElevationLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const appendUrlLog = (msg: string) => {
|
||||
const ts = new Date().toLocaleTimeString()
|
||||
setUrlOperationLogs((prev) => [...prev.slice(-19), `[${ts}] ${msg}`])
|
||||
}
|
||||
|
||||
const loadUrlStatus = async () => {
|
||||
const api = (window as any).api
|
||||
if (!api?.checkUrlProtocolStatus) return
|
||||
setUrlStatusLoading(true)
|
||||
try {
|
||||
const res = await api.checkUrlProtocolStatus()
|
||||
if (res?.success && res.data) {
|
||||
setUrlStatus(res.data)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setUrlStatusLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUrlRegister = async () => {
|
||||
const api = (window as any).api
|
||||
if (!api?.registerUrlProtocol || !api?.checkUrlProtocolStatus) return
|
||||
setUrlRegisterLoading(true)
|
||||
appendUrlLog(t("settings.url.logRegisterStart"))
|
||||
|
||||
const maxRetries = 7
|
||||
let success = false
|
||||
|
||||
const doRegister = async () => {
|
||||
await api.registerUrlProtocol()
|
||||
appendUrlLog(t("settings.url.logRegisterVerify"))
|
||||
const statusRes = await api.checkUrlProtocolStatus()
|
||||
if (statusRes?.success && statusRes.data?.registered) {
|
||||
setUrlStatus(statusRes.data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
success = await doRegister()
|
||||
|
||||
if (!success) {
|
||||
for (let retry = 1; retry <= maxRetries; retry++) {
|
||||
appendUrlLog(t("settings.url.retrying", { current: retry, max: maxRetries }))
|
||||
await new Promise((r) => setTimeout(r, retry * 1000))
|
||||
success = await doRegister()
|
||||
if (success) {
|
||||
appendUrlLog(t("settings.url.retrySuccess", { n: retry }))
|
||||
break
|
||||
}
|
||||
appendUrlLog(t("settings.url.logRegisterFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
setUrlRegisterLoading(false)
|
||||
|
||||
if (success) {
|
||||
appendUrlLog(t("settings.url.logRegisterSuccess"))
|
||||
messageApi.success(t("settings.url.registered"))
|
||||
} else {
|
||||
appendUrlLog(t("settings.url.logMaxRetries"))
|
||||
messageApi.error(t("settings.url.retryFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUrlUnregister = async () => {
|
||||
const api = (window as any).api
|
||||
if (!api?.unregisterUrlProtocol || !api?.checkUrlProtocolStatus) return
|
||||
setUrlUnregisterLoading(true)
|
||||
appendUrlLog(t("settings.url.logUnregisterStart"))
|
||||
|
||||
try {
|
||||
const res = await api.unregisterUrlProtocol()
|
||||
if (res?.success) {
|
||||
const statusRes = await api.checkUrlProtocolStatus()
|
||||
if (statusRes?.success && statusRes.data) {
|
||||
setUrlStatus(statusRes.data)
|
||||
}
|
||||
appendUrlLog(t("settings.url.logUnregisterSuccess"))
|
||||
messageApi.success(t("settings.url.unregistered"))
|
||||
} else {
|
||||
appendUrlLog(t("settings.url.logUnregisterFailed"))
|
||||
messageApi.error(res?.message || t("settings.url.unregisterFailed"))
|
||||
}
|
||||
} catch (error: any) {
|
||||
appendUrlLog(t("settings.url.logUnregisterFailed"))
|
||||
messageApi.error(error?.message || t("settings.url.unregisterFailed"))
|
||||
} finally {
|
||||
setUrlUnregisterLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadAboutContent = async () => {
|
||||
try {
|
||||
const res = await fetch("/about-content.json")
|
||||
@@ -1178,7 +1339,7 @@ export const Settings: React.FC<{
|
||||
},
|
||||
{
|
||||
key: "database",
|
||||
label: t("settings.database.title"),
|
||||
label: t("settings.cloudSync.title"),
|
||||
children: (
|
||||
<>
|
||||
<Card
|
||||
@@ -1189,110 +1350,220 @@ export const Settings: React.FC<{
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.currentStatus")}
|
||||
</div>
|
||||
<Space>
|
||||
<Tag color={pgConnectionStatus.type === "postgresql" ? "blue" : "green"}>
|
||||
{pgConnectionStatus.type === "postgresql"
|
||||
? t("settings.database.postgresqlRemote")
|
||||
: t("settings.database.sqliteLocal")}
|
||||
</Tag>
|
||||
<Tag color={pgConnectionStatus.connected ? "success" : "error"}>
|
||||
{pgConnectionStatus.connected
|
||||
? t("settings.database.connected")
|
||||
: t("settings.database.disconnected")}
|
||||
</Tag>
|
||||
{pgConnectionStatus.error && (
|
||||
<span style={{ color: "var(--ant-color-error, #ff4d4f)", fontSize: "12px" }}>
|
||||
{pgConnectionStatus.error}
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: "var(--ss-card-bg)",
|
||||
color: "var(--ss-text-main)",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.postgresqlConnection")}
|
||||
{t("settings.cloudSync.selectMethod")}
|
||||
</div>
|
||||
<div
|
||||
style={{ marginBottom: "12px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
|
||||
style={{ marginBottom: "16px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.database.connectionHint")}
|
||||
{t("settings.cloudSync.selectMethodHint")}
|
||||
</div>
|
||||
<Space.Compact style={{ width: "100%", marginBottom: "12px" }}>
|
||||
<Input
|
||||
value={pgConnectionString}
|
||||
onChange={(e) => setPgConnectionString(e.target.value)}
|
||||
placeholder="postgresql://user:password@host:port/database?sslmode=require"
|
||||
style={{ flex: 1 }}
|
||||
disabled={!canAdmin}
|
||||
/>
|
||||
<Button
|
||||
onClick={testPgConnection}
|
||||
loading={pgTestLoading}
|
||||
disabled={!canAdmin || !pgConnectionString}
|
||||
>
|
||||
{t("settings.database.testConnection")}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<div
|
||||
style={{ marginBottom: "12px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.database.connectionExample")}
|
||||
</div>
|
||||
<Space
|
||||
direction={isMobile ? "vertical" : "horizontal"}
|
||||
size={isMobile ? 8 : "small"}
|
||||
<Radio.Group
|
||||
value={syncMethod}
|
||||
onChange={async (e) => {
|
||||
const next = e.target.value as "postgresql" | "sectl_cloud"
|
||||
setSyncMethod(next)
|
||||
if ((window as any).api) {
|
||||
await (window as any).api.setSetting("sync_method", next)
|
||||
}
|
||||
}}
|
||||
disabled={!canAdmin}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={switchToPg}
|
||||
loading={pgSwitchLoading}
|
||||
disabled={!canAdmin || !pgConnectionString}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.switchToPostgreSQL")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={switchToSQLite}
|
||||
loading={pgSwitchLoading}
|
||||
disabled={!canAdmin || pgConnectionStatus.type === "sqlite"}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.switchToSQLite")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={uploadLocalToRemote}
|
||||
loading={pgUploadLoading}
|
||||
disabled={!canAdmin || pgConnectionStatus.type !== "postgresql"}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.uploadButton")}
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||
{t("settings.database.uploadHint")}
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size={12}>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
style={{
|
||||
borderColor:
|
||||
syncMethod === "postgresql" ? "var(--ant-color-primary)" : undefined,
|
||||
backgroundColor:
|
||||
syncMethod === "postgresql" ? "var(--ant-color-primary-bg)" : undefined,
|
||||
}}
|
||||
>
|
||||
<Radio value="postgresql">
|
||||
<Space>
|
||||
<DatabaseOutlined />
|
||||
<span style={{ fontWeight: 500 }}>{t("settings.cloudSync.postgresql")}</span>
|
||||
</Space>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
marginTop: 4,
|
||||
marginLeft: 22,
|
||||
}}
|
||||
>
|
||||
{t("settings.cloudSync.postgresqlDesc")}
|
||||
</div>
|
||||
</Radio>
|
||||
</Card>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
style={{
|
||||
borderColor:
|
||||
syncMethod === "sectl_cloud" ? "var(--ant-color-primary)" : undefined,
|
||||
backgroundColor:
|
||||
syncMethod === "sectl_cloud" ? "var(--ant-color-primary-bg)" : undefined,
|
||||
}}
|
||||
>
|
||||
<Radio value="sectl_cloud">
|
||||
<Space>
|
||||
<CloudOutlined />
|
||||
<span style={{ fontWeight: 500 }}>{t("settings.cloudSync.sectlCloud")}</span>
|
||||
</Space>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
marginTop: 4,
|
||||
marginLeft: 22,
|
||||
}}
|
||||
>
|
||||
{t("settings.cloudSync.sectlCloudDesc")}
|
||||
</div>
|
||||
</Radio>
|
||||
</Card>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Card>
|
||||
|
||||
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.syncDescription")}
|
||||
</div>
|
||||
<div style={{ fontSize: "13px", color: "var(--ss-text-secondary)", lineHeight: "1.8" }}>
|
||||
<p>{t("settings.database.syncPoint1")}</p>
|
||||
<p>{t("settings.database.syncPoint2")}</p>
|
||||
<p>{t("settings.database.syncPoint3")}</p>
|
||||
<p>{t("settings.database.syncPoint4")}</p>
|
||||
</div>
|
||||
</Card>
|
||||
{syncMethod === "postgresql" ? (
|
||||
<>
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: "var(--ss-card-bg)",
|
||||
color: "var(--ss-text-main)",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.currentStatus")}
|
||||
</div>
|
||||
<Space>
|
||||
<Tag color={pgConnectionStatus.type === "postgresql" ? "blue" : "green"}>
|
||||
{pgConnectionStatus.type === "postgresql"
|
||||
? t("settings.database.postgresqlRemote")
|
||||
: t("settings.database.sqliteLocal")}
|
||||
</Tag>
|
||||
<Tag color={pgConnectionStatus.connected ? "success" : "error"}>
|
||||
{pgConnectionStatus.connected
|
||||
? t("settings.database.connected")
|
||||
: t("settings.database.disconnected")}
|
||||
</Tag>
|
||||
{pgConnectionStatus.error && (
|
||||
<span style={{ color: "var(--ant-color-error, #ff4d4f)", fontSize: "12px" }}>
|
||||
{pgConnectionStatus.error}
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: "var(--ss-card-bg)",
|
||||
color: "var(--ss-text-main)",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.postgresqlConnection")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
fontSize: "12px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{t("settings.database.connectionHint")}
|
||||
</div>
|
||||
<Space.Compact style={{ width: "100%", marginBottom: "12px" }}>
|
||||
<Input
|
||||
value={pgConnectionString}
|
||||
onChange={(e) => setPgConnectionString(e.target.value)}
|
||||
placeholder="postgresql://user:password@host:port/database?sslmode=require"
|
||||
style={{ flex: 1 }}
|
||||
disabled={!canAdmin}
|
||||
/>
|
||||
<Button
|
||||
onClick={testPgConnection}
|
||||
loading={pgTestLoading}
|
||||
disabled={!canAdmin || !pgConnectionString}
|
||||
>
|
||||
{t("settings.database.testConnection")}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
fontSize: "12px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{t("settings.database.connectionExample")}
|
||||
</div>
|
||||
<Space
|
||||
direction={isMobile ? "vertical" : "horizontal"}
|
||||
size={isMobile ? 8 : "small"}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={switchToPg}
|
||||
loading={pgSwitchLoading}
|
||||
disabled={!canAdmin || !pgConnectionString}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.switchToPostgreSQL")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={switchToSQLite}
|
||||
loading={pgSwitchLoading}
|
||||
disabled={!canAdmin || pgConnectionStatus.type === "sqlite"}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.switchToSQLite")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={uploadLocalToRemote}
|
||||
loading={pgUploadLoading}
|
||||
disabled={!canAdmin || pgConnectionStatus.type !== "postgresql"}
|
||||
style={{ width: isMobile ? "100%" : "auto" }}
|
||||
>
|
||||
{t("settings.database.uploadButton")}
|
||||
</Button>
|
||||
</Space>
|
||||
<div
|
||||
style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.database.uploadHint")}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px" }}>
|
||||
{t("settings.database.syncDescription")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
lineHeight: "1.8",
|
||||
}}
|
||||
>
|
||||
<p>{t("settings.database.syncPoint1")}</p>
|
||||
<p>{t("settings.database.syncPoint2")}</p>
|
||||
<p>{t("settings.database.syncPoint3")}</p>
|
||||
<p>{t("settings.database.syncPoint4")}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<SectlProvider>
|
||||
<SectlCloudSyncPanel />
|
||||
</SectlProvider>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -1412,56 +1683,169 @@ export const Settings: React.FC<{
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
label: t("settings.url.title"),
|
||||
label: t("settings.tabs.urlProtocol"),
|
||||
children: (
|
||||
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
|
||||
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
||||
{t("settings.url.protocol")}
|
||||
</div>
|
||||
<Divider />
|
||||
<div
|
||||
style={{ marginBottom: "12px", fontSize: "13px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.url.description")}
|
||||
</div>
|
||||
<div
|
||||
<>
|
||||
<Card
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
fontSize: "12px",
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", monospace',
|
||||
backgroundColor: "var(--ss-card-bg)",
|
||||
color: "var(--ss-text-main)",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<div>secscore://settings</div>
|
||||
<div>secscore://score</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Space>
|
||||
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
||||
{t("settings.elevation.title")}
|
||||
</div>
|
||||
<Divider />
|
||||
<div
|
||||
style={{ marginBottom: "12px", fontSize: "13px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.elevation.description")}
|
||||
</div>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Tag color={elevationStatus?.is_elevated ? "success" : "warning"}>
|
||||
{elevationStatus?.is_elevated
|
||||
? t("settings.elevation.elevated")
|
||||
: t("settings.elevation.notElevated")}
|
||||
</Tag>
|
||||
{elevationStatus?.platform && <Tag color="blue">{elevationStatus.platform}</Tag>}
|
||||
</Space>
|
||||
<Divider />
|
||||
<Button
|
||||
type="primary"
|
||||
loading={urlRegisterLoading}
|
||||
disabled={!canAdmin}
|
||||
onClick={async () => {
|
||||
if (!(window as any).api) return
|
||||
setUrlRegisterLoading(true)
|
||||
const res = await (window as any).api.registerUrlProtocol()
|
||||
setUrlRegisterLoading(false)
|
||||
if (res && res.success) {
|
||||
messageApi.success(t("settings.url.registered"))
|
||||
} else {
|
||||
messageApi.error(res?.message || t("settings.url.registerFailed"))
|
||||
}
|
||||
loading={elevationLoading}
|
||||
disabled={
|
||||
!canAdmin || !elevationStatus?.can_request_elevation || elevationStatus?.is_elevated
|
||||
}
|
||||
onClick={handleRequestElevation}
|
||||
>
|
||||
{t("settings.elevation.request")}
|
||||
</Button>
|
||||
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||
{elevationStatus?.platform === "windows"
|
||||
? t("settings.elevation.windowsHint")
|
||||
: elevationStatus?.platform === "macos"
|
||||
? t("settings.elevation.macosHint")
|
||||
: t("settings.elevation.linuxHint")}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}>
|
||||
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
||||
{t("settings.url.protocol")}
|
||||
</div>
|
||||
<Divider />
|
||||
<div
|
||||
style={{ marginBottom: "12px", fontSize: "13px", color: "var(--ss-text-secondary)" }}
|
||||
>
|
||||
{t("settings.url.description")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
fontSize: "12px",
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", monospace',
|
||||
}}
|
||||
>
|
||||
{t("settings.url.register")}
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||
{t("settings.url.installerRequired")}
|
||||
</div>
|
||||
</Card>
|
||||
<div>secscore://settings</div>
|
||||
<div>secscore://score</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Tag color={urlStatus?.registered ? "success" : "error"}>
|
||||
{urlStatus?.registered
|
||||
? t("settings.url.statusRegistered")
|
||||
: t("settings.url.statusNotRegistered")}
|
||||
</Tag>
|
||||
<Button size="small" loading={urlStatusLoading} onClick={loadUrlStatus}>
|
||||
{t("settings.url.checkStatus")}
|
||||
</Button>
|
||||
</Space>
|
||||
{urlStatus?.details && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
fontSize: "12px",
|
||||
color: "var(--ss-text-secondary)",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{urlStatus.details}
|
||||
</div>
|
||||
)}
|
||||
<Divider />
|
||||
<Space>
|
||||
{!urlStatus?.registered ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={urlRegisterLoading}
|
||||
disabled={!canAdmin}
|
||||
onClick={handleUrlRegister}
|
||||
>
|
||||
{t("settings.url.register")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
loading={urlRegisterLoading}
|
||||
disabled={!canAdmin}
|
||||
onClick={handleUrlRegister}
|
||||
>
|
||||
{t("settings.url.reRegister")}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
loading={urlUnregisterLoading}
|
||||
disabled={!canAdmin}
|
||||
onClick={handleUrlUnregister}
|
||||
>
|
||||
{t("settings.url.unregister")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<div style={{ marginTop: "8px", fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||
{t("settings.url.installerRequired")}
|
||||
</div>
|
||||
{urlOperationLogs.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "logs",
|
||||
label: (
|
||||
<span style={{ fontSize: "13px" }}>
|
||||
{t("settings.url.operationLog")} ({urlOperationLogs.length})
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
maxHeight: "200px",
|
||||
overflowY: "auto",
|
||||
lineHeight: "1.8",
|
||||
}}
|
||||
>
|
||||
{urlOperationLogs.map((log, i) => (
|
||||
<div key={i}>{log}</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user