mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
feat: 顶栏登录信息
This commit is contained in:
+64
-1
@@ -24,7 +24,7 @@ import {
|
|||||||
HolderOutlined,
|
HolderOutlined,
|
||||||
CrownOutlined,
|
CrownOutlined,
|
||||||
} from "@ant-design/icons"
|
} from "@ant-design/icons"
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { HashRouter, useLocation, useNavigate, Routes, Route } from "react-router-dom"
|
import { HashRouter, useLocation, useNavigate, Routes, Route } from "react-router-dom"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Sidebar } from "./components/Sidebar"
|
import { Sidebar } from "./components/Sidebar"
|
||||||
@@ -115,6 +115,7 @@ function MainContent(): React.JSX.Element {
|
|||||||
const [authPassword, setAuthPassword] = useState("")
|
const [authPassword, setAuthPassword] = useState("")
|
||||||
const [authLoading, setAuthLoading] = useState(false)
|
const [authLoading, setAuthLoading] = useState(false)
|
||||||
const [oauthVisible, setOAuthVisible] = useState(false)
|
const [oauthVisible, setOAuthVisible] = useState(false)
|
||||||
|
const [oauthUserName, setOAuthUserName] = useState<string | null>(null)
|
||||||
const [mobileBottomNavItems, setMobileBottomNavItems] = useState<MobileNavKey[]>(
|
const [mobileBottomNavItems, setMobileBottomNavItems] = useState<MobileNavKey[]>(
|
||||||
DEFAULT_MOBILE_BOTTOM_NAV_ITEMS
|
DEFAULT_MOBILE_BOTTOM_NAV_ITEMS
|
||||||
)
|
)
|
||||||
@@ -140,6 +141,18 @@ function MainContent(): React.JSX.Element {
|
|||||||
const syncApplyLoadingRef = useRef(false)
|
const syncApplyLoadingRef = useRef(false)
|
||||||
const lastLocalMutationAtRef = useRef(0)
|
const lastLocalMutationAtRef = useRef(0)
|
||||||
const pluginRuntimeRef = useRef(getPluginRuntime())
|
const pluginRuntimeRef = useRef(getPluginRuntime())
|
||||||
|
const refreshPermissionFromAuth = useCallback(async () => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api) return
|
||||||
|
|
||||||
|
const authRes = await api.authGetStatus()
|
||||||
|
if (authRes?.success && authRes.data) {
|
||||||
|
const anyPwd = Boolean(authRes.data.hasAdminPassword || authRes.data.hasPointsPassword)
|
||||||
|
setHasAnyPassword(anyPwd)
|
||||||
|
setPermission(authRes.data.permission)
|
||||||
|
setAuthVisible(anyPwd && authRes.data.permission === "view")
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const activeMenu = useMemo(() => {
|
const activeMenu = useMemo(() => {
|
||||||
const p = location.pathname
|
const p = location.pathname
|
||||||
@@ -209,6 +222,11 @@ function MainContent(): React.JSX.Element {
|
|||||||
oauthRes?.success && oauthRes.data
|
oauthRes?.success && oauthRes.data
|
||||||
? mapOAuthPermissionToAppPermission(oauthRes.data.permission)
|
? mapOAuthPermissionToAppPermission(oauthRes.data.permission)
|
||||||
: null
|
: null
|
||||||
|
if (oauthRes?.success && oauthRes.data?.name) {
|
||||||
|
setOAuthUserName(String(oauthRes.data.name))
|
||||||
|
} else {
|
||||||
|
setOAuthUserName(null)
|
||||||
|
}
|
||||||
|
|
||||||
if (authRes?.success && authRes.data) {
|
if (authRes?.success && authRes.data) {
|
||||||
const anyPwd = Boolean(authRes.data.hasAdminPassword || authRes.data.hasPointsPassword)
|
const anyPwd = Boolean(authRes.data.hasAdminPassword || authRes.data.hasPointsPassword)
|
||||||
@@ -231,6 +249,31 @@ function MainContent(): React.JSX.Element {
|
|||||||
loadAuthAndSettings()
|
loadAuthAndSettings()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOAuthUserUpdated = (
|
||||||
|
event: Event
|
||||||
|
) => {
|
||||||
|
const customEvent = event as CustomEvent<{
|
||||||
|
user?: { name?: string } | null
|
||||||
|
}>
|
||||||
|
const userName = customEvent?.detail?.user?.name
|
||||||
|
if (typeof userName === "string" && userName.trim()) {
|
||||||
|
setOAuthUserName(userName)
|
||||||
|
} else {
|
||||||
|
setOAuthUserName(null)
|
||||||
|
void refreshPermissionFromAuth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("ss:oauth-user-updated", handleOAuthUserUpdated as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
"ss:oauth-user-updated",
|
||||||
|
handleOAuthUserUpdated as EventListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [refreshPermissionFromAuth])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = (window as any).api
|
const api = (window as any).api
|
||||||
if (!api || typeof api.onSettingChanged !== "function") return
|
if (!api || typeof api.onSettingChanged !== "function") return
|
||||||
@@ -456,6 +499,8 @@ function MainContent(): React.JSX.Element {
|
|||||||
// 退出时同时清理 OAuth 持久化状态,避免刷新后又自动恢复权限
|
// 退出时同时清理 OAuth 持久化状态,避免刷新后又自动恢复权限
|
||||||
try {
|
try {
|
||||||
await api.oauthClearLoginState()
|
await api.oauthClearLoginState()
|
||||||
|
setOAuthUserName(null)
|
||||||
|
window.dispatchEvent(new CustomEvent("ss:oauth-user-updated", { detail: { user: null } }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to clear OAuth login state:", error)
|
console.error("Failed to clear OAuth login state:", error)
|
||||||
}
|
}
|
||||||
@@ -467,6 +512,21 @@ function MainContent(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoutOAuthFromHeader = useCallback(async () => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.oauthClearLoginState()
|
||||||
|
setOAuthUserName(null)
|
||||||
|
window.dispatchEvent(new CustomEvent("ss:oauth-user-updated", { detail: { user: null } }))
|
||||||
|
await refreshPermissionFromAuth()
|
||||||
|
messageApi.success("已退出云账号")
|
||||||
|
} catch (error: any) {
|
||||||
|
messageApi.error(error?.message || t("common.error"))
|
||||||
|
}
|
||||||
|
}, [messageApi, refreshPermissionFromAuth, t])
|
||||||
|
|
||||||
const handleOAuthSuccess = (userInfo: {
|
const handleOAuthSuccess = (userInfo: {
|
||||||
user_id: string
|
user_id: string
|
||||||
email: string
|
email: string
|
||||||
@@ -476,6 +536,7 @@ function MainContent(): React.JSX.Element {
|
|||||||
}) => {
|
}) => {
|
||||||
setPermission(mapOAuthPermissionToAppPermission(userInfo.permission))
|
setPermission(mapOAuthPermissionToAppPermission(userInfo.permission))
|
||||||
setAuthVisible(false)
|
setAuthVisible(false)
|
||||||
|
setOAuthUserName(userInfo.name || null)
|
||||||
messageApi.success(t("auth.oauthSuccess", "登录成功"))
|
messageApi.success(t("auth.oauthSuccess", "登录成功"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +732,8 @@ function MainContent(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
<ContentArea
|
<ContentArea
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
oauthUserName={oauthUserName}
|
||||||
|
onOAuthLogout={logoutOAuthFromHeader}
|
||||||
hasAnyPassword={hasAnyPassword}
|
hasAnyPassword={hasAnyPassword}
|
||||||
onAuthClick={() => setAuthVisible(true)}
|
onAuthClick={() => setAuthVisible(true)}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Suspense, lazy, useEffect } from "react"
|
import React, { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Layout, Space, Button, Tag, Spin } from "antd"
|
import { Layout, Space, Button, Tag, Spin, Avatar, Popover, Progress } from "antd"
|
||||||
import {
|
import {
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
@@ -57,6 +57,8 @@ const { Content } = Layout
|
|||||||
|
|
||||||
interface ContentAreaProps {
|
interface ContentAreaProps {
|
||||||
permission: "admin" | "points" | "view"
|
permission: "admin" | "points" | "view"
|
||||||
|
oauthUserName?: string | null
|
||||||
|
onOAuthLogout?: () => Promise<void> | void
|
||||||
hasAnyPassword: boolean
|
hasAnyPassword: boolean
|
||||||
onAuthClick: () => void
|
onAuthClick: () => void
|
||||||
onLogout: () => void
|
onLogout: () => void
|
||||||
@@ -74,8 +76,17 @@ interface ContentAreaProps {
|
|||||||
bottomInset?: number
|
bottomInset?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HeaderStorageUsage {
|
||||||
|
used_storage_formatted: string
|
||||||
|
total_storage_formatted: string
|
||||||
|
percentage: number
|
||||||
|
file_count: number
|
||||||
|
}
|
||||||
|
|
||||||
export function ContentArea({
|
export function ContentArea({
|
||||||
permission,
|
permission,
|
||||||
|
oauthUserName,
|
||||||
|
onOAuthLogout,
|
||||||
hasAnyPassword,
|
hasAnyPassword,
|
||||||
onAuthClick,
|
onAuthClick,
|
||||||
onLogout,
|
onLogout,
|
||||||
@@ -158,6 +169,247 @@ export function ContentArea({
|
|||||||
: t("permissions.view")}
|
: t("permissions.view")}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
|
const fallbackDisplayName =
|
||||||
|
permission === "admin"
|
||||||
|
? t("permissions.admin")
|
||||||
|
: permission === "points"
|
||||||
|
? t("permissions.points")
|
||||||
|
: t("permissions.view")
|
||||||
|
const userDisplayName = (oauthUserName || fallbackDisplayName).trim()
|
||||||
|
const avatarText = userDisplayName.slice(0, 1).toUpperCase()
|
||||||
|
const [profilePopoverOpen, setProfilePopoverOpen] = useState(false)
|
||||||
|
const [storageUsageLoading, setStorageUsageLoading] = useState(false)
|
||||||
|
const [storageUsageError, setStorageUsageError] = useState<string | null>(null)
|
||||||
|
const [storageUsage, setStorageUsage] = useState<HeaderStorageUsage | null>(null)
|
||||||
|
const [oauthUserId, setOAuthUserId] = useState<string | null>(null)
|
||||||
|
const [lastSyncTime, setLastSyncTime] = useState<string | null>(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem("ss_last_sync_time")
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [copiedUserId, setCopiedUserId] = useState(false)
|
||||||
|
const [logoutLoading, setLogoutLoading] = useState(false)
|
||||||
|
const copyResetTimerRef = useRef<number | null>(null)
|
||||||
|
const hasOAuthSession = Boolean(oauthUserName && oauthUserName.trim())
|
||||||
|
const formattedLastSyncTime = (() => {
|
||||||
|
if (!lastSyncTime) return "暂无"
|
||||||
|
const date = new Date(lastSyncTime)
|
||||||
|
if (Number.isNaN(date.getTime())) return "暂无"
|
||||||
|
return date.toLocaleString("zh-CN", { hour12: false })
|
||||||
|
})()
|
||||||
|
|
||||||
|
const loadStorageUsage = useCallback(async () => {
|
||||||
|
setStorageUsageLoading(true)
|
||||||
|
setStorageUsageError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api?.oauthLoadLoginState) {
|
||||||
|
setStorageUsage(null)
|
||||||
|
setStorageUsageError("当前环境不支持云用量查询")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthStateRes = await api.oauthLoadLoginState()
|
||||||
|
const oauthState = oauthStateRes?.success ? oauthStateRes.data : null
|
||||||
|
if (!oauthState?.access_token || !oauthState?.user_id) {
|
||||||
|
setOAuthUserId(null)
|
||||||
|
setStorageUsage(null)
|
||||||
|
setStorageUsageError("当前未登录云账号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOAuthUserId(String(oauthState.user_id))
|
||||||
|
|
||||||
|
const platformId = import.meta.env.VITE_OAUTH_PLATFORM_ID
|
||||||
|
if (!platformId) {
|
||||||
|
setStorageUsage(null)
|
||||||
|
setStorageUsageError("未配置平台 ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: platformId,
|
||||||
|
user_id: oauthState.user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://appwrite.sectl.top/api/cloud/storage/usage?${params.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${oauthState.access_token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
let payload: any = {}
|
||||||
|
try {
|
||||||
|
payload = responseText ? JSON.parse(responseText) : {}
|
||||||
|
} catch {
|
||||||
|
payload = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload?.error_description || payload?.message || "获取云空间用量失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
setStorageUsage({
|
||||||
|
used_storage_formatted: String(payload?.used_storage_formatted || "0 B"),
|
||||||
|
total_storage_formatted: String(payload?.total_storage_formatted || "0 B"),
|
||||||
|
percentage: Number.isFinite(payload?.percentage) ? Number(payload.percentage) : 0,
|
||||||
|
file_count: Number.isFinite(payload?.file_count) ? Number(payload.file_count) : 0,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
setStorageUsage(null)
|
||||||
|
setStorageUsageError(error?.message || "获取云空间用量失败")
|
||||||
|
} finally {
|
||||||
|
setStorageUsageLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!profilePopoverOpen) return
|
||||||
|
void loadStorageUsage()
|
||||||
|
}, [loadStorageUsage, profilePopoverOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDataUpdated = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<{ source?: string }>
|
||||||
|
if (customEvent?.detail?.source !== "sync") return
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
setLastSyncTime(now)
|
||||||
|
try {
|
||||||
|
localStorage.setItem("ss_last_sync_time", now)
|
||||||
|
} catch {
|
||||||
|
void 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("ss:data-updated", handleDataUpdated as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("ss:data-updated", handleDataUpdated as EventListener)
|
||||||
|
if (copyResetTimerRef.current) {
|
||||||
|
window.clearTimeout(copyResetTimerRef.current)
|
||||||
|
copyResetTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCopyUserId = async () => {
|
||||||
|
if (!oauthUserId) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(oauthUserId)
|
||||||
|
setCopiedUserId(true)
|
||||||
|
if (copyResetTimerRef.current) {
|
||||||
|
window.clearTimeout(copyResetTimerRef.current)
|
||||||
|
}
|
||||||
|
copyResetTimerRef.current = window.setTimeout(() => {
|
||||||
|
setCopiedUserId(false)
|
||||||
|
copyResetTimerRef.current = null
|
||||||
|
}, 1500)
|
||||||
|
} catch {
|
||||||
|
setCopiedUserId(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOAuthLogoutClick = async () => {
|
||||||
|
if (logoutLoading) return
|
||||||
|
setLogoutLoading(true)
|
||||||
|
try {
|
||||||
|
if (onOAuthLogout) {
|
||||||
|
await onOAuthLogout()
|
||||||
|
} else {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (api?.oauthClearLoginState) {
|
||||||
|
await api.oauthClearLoginState()
|
||||||
|
window.dispatchEvent(new CustomEvent("ss:oauth-user-updated", { detail: { user: null } }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProfilePopoverOpen(false)
|
||||||
|
} finally {
|
||||||
|
setLogoutLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const profilePopoverContent = (
|
||||||
|
<div style={{ width: "260px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>账号 ID</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "var(--ss-text-main)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
title={oauthUserId || ""}
|
||||||
|
>
|
||||||
|
{oauthUserId || "未登录"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="small" onClick={handleCopyUserId} disabled={!oauthUserId}>
|
||||||
|
{copiedUserId ? "已复制" : "复制"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>最近同步时间</div>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ss-text-main)" }}>{formattedLastSyncTime}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--ss-text-secondary)" }}>云空间用量</div>
|
||||||
|
{storageUsageLoading ? (
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", padding: "10px 0" }}>
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
) : storageUsage ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--ss-text-main)" }}>
|
||||||
|
已用:{storageUsage.used_storage_formatted} / {storageUsage.total_storage_formatted}
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={Math.max(0, Math.min(100, Math.round(storageUsage.percentage)))}
|
||||||
|
size="small"
|
||||||
|
status={storageUsage.percentage > 90 ? "exception" : "active"}
|
||||||
|
/>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||||
|
文件数量:{storageUsage.file_count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ant-color-error, #ff4d4f)" }}>
|
||||||
|
{storageUsageError || "暂无云空间数据"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
block
|
||||||
|
size="small"
|
||||||
|
onClick={handleOAuthLogoutClick}
|
||||||
|
loading={logoutLoading}
|
||||||
|
disabled={!hasOAuthSession}
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
style={{
|
style={{
|
||||||
@@ -307,6 +559,55 @@ export function ContentArea({
|
|||||||
title={immersiveMode ? "退出沉浸模式" : "进入沉浸模式"}
|
title={immersiveMode ? "退出沉浸模式" : "进入沉浸模式"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="bottomRight"
|
||||||
|
open={profilePopoverOpen}
|
||||||
|
onOpenChange={setProfilePopoverOpen}
|
||||||
|
content={profilePopoverContent}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
borderRadius: "999px",
|
||||||
|
padding: "2px 10px 2px 2px",
|
||||||
|
background: "var(--ss-bg-color)",
|
||||||
|
minWidth: "92px",
|
||||||
|
maxWidth: "180px",
|
||||||
|
height: "30px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
title={userDisplayName}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size={24}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--ant-color-primary, #1677ff)",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "12px",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{avatarText || "U"}
|
||||||
|
</Avatar>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: "var(--ss-text-main)",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userDisplayName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
{permissionTag}
|
{permissionTag}
|
||||||
{hasAnyPassword && (
|
{hasAnyPassword && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
|||||||
console.error("[OAuth] 保存登录状态失败:", saveError)
|
console.error("[OAuth] 保存登录状态失败:", saveError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("ss:oauth-user-updated", {
|
||||||
|
detail: { user: userRes.data },
|
||||||
|
})
|
||||||
|
)
|
||||||
console.log("[OAuth] 调用 onSuccess...")
|
console.log("[OAuth] 调用 onSuccess...")
|
||||||
onSuccess(userRes.data)
|
onSuccess(userRes.data)
|
||||||
console.log("[OAuth] 调用 onClose...")
|
console.log("[OAuth] 调用 onClose...")
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ export const Settings: React.FC<{
|
|||||||
try {
|
try {
|
||||||
await api.oauthClearLoginState()
|
await api.oauthClearLoginState()
|
||||||
setOAuthUserInfo(null)
|
setOAuthUserInfo(null)
|
||||||
|
window.dispatchEvent(new CustomEvent("ss:oauth-user-updated", { detail: { user: null } }))
|
||||||
messageApi.success(t("auth.logout"))
|
messageApi.success(t("auth.logout"))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
messageApi.error(error?.message || t("common.error"))
|
messageApi.error(error?.message || t("common.error"))
|
||||||
@@ -412,6 +413,42 @@ export const Settings: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOAuthUserUpdated = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<{
|
||||||
|
user?:
|
||||||
|
| {
|
||||||
|
user_id?: string
|
||||||
|
email?: string
|
||||||
|
name?: string
|
||||||
|
github_username?: string
|
||||||
|
permission?: number
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
}>
|
||||||
|
const user = customEvent?.detail?.user
|
||||||
|
if (user?.user_id && user.email && user.name && typeof user.permission === "number") {
|
||||||
|
setOAuthUserInfo({
|
||||||
|
user_id: user.user_id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
github_username: user.github_username,
|
||||||
|
permission: user.permission,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setOAuthUserInfo(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("ss:oauth-user-updated", handleOAuthUserUpdated as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
"ss:oauth-user-updated",
|
||||||
|
handleOAuthUserUpdated as EventListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const showLogs = async () => {
|
const showLogs = async () => {
|
||||||
if (!(window as any).api) return
|
if (!(window as any).api) return
|
||||||
setLogsLoading(true)
|
setLogsLoading(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user