mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
feat: 添加SECTL云服务集成功能
实现SECTL云服务的全面集成,包括: 1. 添加OAuth认证服务,支持PKCE流程 2. 实现云存储管理功能 3. 添加积分数据同步服务 4. 实现通知服务和KV存储服务 5. 添加多语言支持 6. 创建相关React组件和上下文 7. 更新依赖项添加crypto-js 8. 优化HTTP客户端重用
This commit is contained in:
@@ -73,7 +73,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
if (result.code) {
|
||||
console.log("[OAuth] 授权码:", result.code)
|
||||
console.log("[OAuth] State:", result.state, "期望:", getExpectedState())
|
||||
|
||||
|
||||
// 验证 state 防止 CSRF
|
||||
const expectedState = getExpectedState()
|
||||
if (expectedState && result.state !== expectedState) {
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* SECTL 积分数据同步组件
|
||||
* 提供云端数据同步功能
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Card, Button, Space, Alert, Descriptions, message, Modal, Typography, Divider } from "antd"
|
||||
import {
|
||||
CloudSyncOutlined,
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { scoreSyncService } from "../services/scoreSyncService"
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface SyncPanelProps {
|
||||
// 从父组件获取数据的方法
|
||||
onGetLocalData?: () => {
|
||||
students: any[]
|
||||
events: any[]
|
||||
settings: { reasons: any[]; tags: any[] }
|
||||
}
|
||||
// 从云端恢复数据到本地的方法
|
||||
onRestoreData?: (data: {
|
||||
students: any[]
|
||||
events: any[]
|
||||
settings: { reasons: any[]; tags: any[] }
|
||||
}) => void
|
||||
}
|
||||
|
||||
export const ScoreSyncPanel: React.FC<SyncPanelProps> = ({ onGetLocalData, onRestoreData }) => {
|
||||
const { isAuthenticated } = useSectl()
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [metadata, setMetadata] = useState<any>(null)
|
||||
const [hasData, setHasData] = useState(false)
|
||||
|
||||
// 检查云端数据状态
|
||||
const checkCloudStatus = async () => {
|
||||
if (!isAuthenticated) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const [meta, hasCloudData] = await Promise.all([
|
||||
scoreSyncService.getSyncMetadata(),
|
||||
scoreSyncService.hasCloudData(),
|
||||
])
|
||||
|
||||
setMetadata(meta)
|
||||
setHasData(hasCloudData)
|
||||
} catch (error: any) {
|
||||
console.error("检查云端状态失败:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
checkCloudStatus()
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
// 上传数据到云端
|
||||
const handleSyncToCloud = async () => {
|
||||
if (!onGetLocalData) {
|
||||
message.error("未配置数据获取方法")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSyncing(true)
|
||||
const localData = onGetLocalData()
|
||||
await scoreSyncService.fullSync(localData)
|
||||
await checkCloudStatus()
|
||||
message.success("数据已同步到云端")
|
||||
} catch (error: any) {
|
||||
message.error(`同步失败:${error.message}`)
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 从云端下载数据
|
||||
const handleRestoreFromCloud = async () => {
|
||||
Modal.confirm({
|
||||
title: "确认恢复云端数据",
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: "此操作将用云端数据覆盖本地数据,确定继续吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
if (!onRestoreData) {
|
||||
message.error("未配置数据恢复方法")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setRestoring(true)
|
||||
const cloudData = await scoreSyncService.restoreFromCloud()
|
||||
onRestoreData({
|
||||
students: cloudData.students as any[],
|
||||
events: cloudData.events as any[],
|
||||
settings: cloudData.settings,
|
||||
})
|
||||
message.success("数据已从云端恢复")
|
||||
} catch (error: any) {
|
||||
message.error(`恢复失败:${error.message}`)
|
||||
} finally {
|
||||
setRestoring(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 清除云端数据
|
||||
const handleClearCloudData = async () => {
|
||||
Modal.confirm({
|
||||
title: "确认清除云端数据",
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: "此操作将永久删除云端的所有数据,且不可恢复!确定继续吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
setClearing(true)
|
||||
await scoreSyncService.clearCloudData()
|
||||
await checkCloudStatus()
|
||||
message.success("云端数据已清除")
|
||||
} catch (error: any) {
|
||||
message.error(`清除失败:${error.message}`)
|
||||
} finally {
|
||||
setClearing(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Card>
|
||||
<Alert
|
||||
message="请先登录 SECTL"
|
||||
description="登录后可使用云端数据同步功能"
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<CloudSyncOutlined />
|
||||
<span>云端数据同步</span>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
{/* 云端状态 */}
|
||||
<Alert
|
||||
message={
|
||||
hasData ? (
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: "#52c41a" }} />
|
||||
<span>云端有数据</span>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<ExclamationCircleOutlined style={{ color: "#faad14" }} />
|
||||
<span>云端暂无数据</span>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
description={
|
||||
metadata ? (
|
||||
<div>
|
||||
<Text>
|
||||
最后同步时间:{new Date(metadata.lastSyncTime).toLocaleString("zh-CN")}
|
||||
</Text>
|
||||
<br />
|
||||
<Text>数据版本:{metadata.version}</Text>
|
||||
</div>
|
||||
) : (
|
||||
"暂无同步记录"
|
||||
)
|
||||
}
|
||||
type={hasData ? "success" : "warning"}
|
||||
showIcon
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 同步操作 */}
|
||||
<div>
|
||||
<Title level={5}>同步操作</Title>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Alert
|
||||
message="数据同步说明"
|
||||
description={
|
||||
<div>
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
<li>上传:将本地积分数据同步到云端</li>
|
||||
<li>下载:从云端恢复数据到本地(会覆盖本地数据)</li>
|
||||
<li>清除:删除云端的所有数据(不可恢复)</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
onClick={handleSyncToCloud}
|
||||
loading={syncing}
|
||||
disabled={!onGetLocalData}
|
||||
>
|
||||
上传到云端
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleRestoreFromCloud}
|
||||
loading={restoring}
|
||||
disabled={!hasData || !onRestoreData}
|
||||
>
|
||||
从云端恢复
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleClearCloudData}
|
||||
loading={clearing}
|
||||
disabled={!hasData}
|
||||
>
|
||||
清除云端数据
|
||||
</Button>
|
||||
|
||||
<Button icon={<CloudSyncOutlined />} onClick={checkCloudStatus} loading={loading}>
|
||||
刷新状态
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 同步记录 */}
|
||||
<div>
|
||||
<Title level={5}>同步信息</Title>
|
||||
{metadata ? (
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="最后同步时间">
|
||||
{new Date(metadata.lastSyncTime).toLocaleString("zh-CN")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数据版本">{metadata.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="平台">
|
||||
{metadata.platform || "SecScore"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Text type="secondary">暂无同步记录</Text>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* SECTL 云存储管理组件
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from "react"
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Upload,
|
||||
Space,
|
||||
Progress,
|
||||
Modal,
|
||||
Input,
|
||||
message,
|
||||
Popconfirm,
|
||||
Typography,
|
||||
} from "antd"
|
||||
import {
|
||||
CloudOutlined,
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
ShareAltOutlined,
|
||||
ReloadOutlined,
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import type { UploadFile } from "antd"
|
||||
import { sectlCloudStorage, CloudFile, StorageUsage } from "../services/sectlCloudStorage"
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
export const SectlCloudStorageManager: React.FC = () => {
|
||||
const { isAuthenticated } = useSectl()
|
||||
const [files, setFiles] = useState<CloudFile[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [storageUsage, setStorageUsage] = useState<StorageUsage | null>(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState<{
|
||||
visible: boolean
|
||||
fileId?: string
|
||||
filename?: string
|
||||
}>({ visible: false })
|
||||
const [newFilename, setNewFilename] = useState("")
|
||||
|
||||
// 加载文件列表
|
||||
const loadFiles = async () => {
|
||||
if (!isAuthenticated) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await sectlCloudStorage.listFiles({ limit: 100 })
|
||||
setFiles(result.files)
|
||||
} catch (error: any) {
|
||||
message.error(`加载文件列表失败:${error.message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载存储使用情况
|
||||
const loadStorageUsage = async () => {
|
||||
if (!isAuthenticated) return
|
||||
|
||||
try {
|
||||
const usage = await sectlCloudStorage.getStorageUsage()
|
||||
setStorageUsage(usage)
|
||||
} catch (error: any) {
|
||||
console.error("加载存储使用情况失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadFiles()
|
||||
loadStorageUsage()
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
// 处理文件上传
|
||||
const handleUpload = async (file: UploadFile): Promise<void> => {
|
||||
if (!file.originFileObj) return
|
||||
|
||||
try {
|
||||
setUploading(true)
|
||||
await sectlCloudStorage.uploadFile(file.originFileObj)
|
||||
message.success(`文件 ${file.name} 上传成功`)
|
||||
loadFiles()
|
||||
loadStorageUsage()
|
||||
} catch (error: any) {
|
||||
message.error(`上传失败:${error.message}`)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件下载
|
||||
const handleDownload = async (file: CloudFile) => {
|
||||
try {
|
||||
const { download_url } = await sectlCloudStorage.downloadFile(file.file_id)
|
||||
window.open(download_url, "_blank")
|
||||
message.success("开始下载")
|
||||
} catch (error: any) {
|
||||
message.error(`下载失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件删除
|
||||
const handleDelete = async (file: CloudFile) => {
|
||||
try {
|
||||
await sectlCloudStorage.deleteFile(file.file_id)
|
||||
message.success("文件已删除")
|
||||
loadFiles()
|
||||
loadStorageUsage()
|
||||
} catch (error: any) {
|
||||
message.error(`删除失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件重命名
|
||||
const handleRename = async () => {
|
||||
if (!renameModal.fileId || !newFilename) return
|
||||
|
||||
try {
|
||||
await sectlCloudStorage.renameFile(renameModal.fileId, newFilename)
|
||||
message.success("重命名成功")
|
||||
setRenameModal({ visible: false })
|
||||
setNewFilename("")
|
||||
loadFiles()
|
||||
} catch (error: any) {
|
||||
message.error(`重命名失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分享链接
|
||||
const handleCreateShare = async (file: CloudFile) => {
|
||||
try {
|
||||
const share = await sectlCloudStorage.createShare(file.file_id, {
|
||||
expires_in: 86400, // 1 天
|
||||
})
|
||||
|
||||
// 复制到剪贴板
|
||||
await navigator.clipboard.writeText(share.share_url)
|
||||
message.success("分享链接已创建并复制到剪贴板")
|
||||
} catch (error: any) {
|
||||
message.error(`创建分享失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "filename",
|
||||
key: "filename",
|
||||
render: (text: string, record: CloudFile) => (
|
||||
<Space>
|
||||
<FileOutlined />
|
||||
<span>{text}</span>
|
||||
<Typography.Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
.{record.extension}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size_formatted",
|
||||
key: "size_formatted",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 180,
|
||||
render: (text: string) => new Date(text).toLocaleString("zh-CN"),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 250,
|
||||
render: (_: any, record: CloudFile) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<DownloadOutlined />} onClick={() => handleDownload(record)}>
|
||||
下载
|
||||
</Button>
|
||||
<Button type="link" icon={<ShareAltOutlined />} onClick={() => handleCreateShare(record)}>
|
||||
分享
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
setRenameModal({
|
||||
visible: true,
|
||||
fileId: record.file_id,
|
||||
filename: record.filename,
|
||||
})
|
||||
}
|
||||
>
|
||||
重命名
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个文件吗?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<CloudOutlined style={{ fontSize: 48, color: "#999" }} />
|
||||
<Title level={4} style={{ marginTop: 16 }}>
|
||||
请先登录 SECTL 以使用云存储服务
|
||||
</Title>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<CloudOutlined />
|
||||
<span>云存储管理</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadFiles} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Upload
|
||||
accept="*/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={handleUpload}
|
||||
disabled={uploading}
|
||||
>
|
||||
<Button type="primary" icon={<UploadOutlined />} loading={uploading}>
|
||||
上传文件
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 存储使用情况 */}
|
||||
{storageUsage && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="small">
|
||||
<div>
|
||||
已用:{storageUsage.used_storage_formatted} / {storageUsage.total_storage_formatted}
|
||||
</div>
|
||||
<Progress
|
||||
percent={storageUsage.percentage}
|
||||
status={storageUsage.percentage > 90 ? "exception" : "active"}
|
||||
/>
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
文件数量:{storageUsage.file_count}
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件列表 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={files}
|
||||
rowKey="file_id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 个文件`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 重命名对话框 */}
|
||||
<Modal
|
||||
title="重命名文件"
|
||||
open={renameModal.visible}
|
||||
onOk={handleRename}
|
||||
onCancel={() => {
|
||||
setRenameModal({ visible: false })
|
||||
setNewFilename("")
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入新文件名"
|
||||
value={newFilename}
|
||||
onChange={(e) => setNewFilename(e.target.value)}
|
||||
onPressEnter={handleRename}
|
||||
defaultValue={renameModal.filename}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* SECTL 登录按钮组件
|
||||
*/
|
||||
|
||||
import React, { useState } from "react"
|
||||
import { Button, message } from "antd"
|
||||
import { LoginOutlined, UserOutlined } from "@ant-design/icons"
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
|
||||
interface SectlLoginButtonProps {
|
||||
onLoginSuccess?: () => void
|
||||
onLogoutSuccess?: () => void
|
||||
scope?: string[]
|
||||
}
|
||||
|
||||
export const SectlLoginButton: React.FC<SectlLoginButtonProps> = ({
|
||||
onLoginSuccess,
|
||||
onLogoutSuccess,
|
||||
scope = ["user:read", "cloud:read", "cloud:write"],
|
||||
}) => {
|
||||
const { isAuthenticated, isLoading, userInfo, login, logout } = useSectl()
|
||||
const [isLogging, setIsLogging] = useState(false)
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
setIsLogging(true)
|
||||
await login(scope)
|
||||
message.success("登录成功!")
|
||||
onLoginSuccess?.()
|
||||
} catch (error: any) {
|
||||
message.error(`登录失败:${error.message}`)
|
||||
} finally {
|
||||
setIsLogging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setIsLogging(true)
|
||||
await logout()
|
||||
message.success("已退出登录")
|
||||
onLogoutSuccess?.()
|
||||
} catch (error: any) {
|
||||
message.error(`登出失败:${error.message}`)
|
||||
} finally {
|
||||
setIsLogging(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Button loading>加载中...</Button>
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<Button icon={<UserOutlined />} onClick={handleLogout} loading={isLogging} danger>
|
||||
{userInfo?.name || "退出登录"}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="primary" icon={<LoginOutlined />} onClick={handleLogin} loading={isLogging}>
|
||||
使用 SECTL 登录
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* SECTL 设置面板组件
|
||||
*/
|
||||
|
||||
import React, { useState } from "react"
|
||||
import { Card, Form, Input, Button, Space, Alert, Tag, Descriptions, message } from "antd"
|
||||
import {
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
import { SectlLoginButton } from "./SectlLoginButton"
|
||||
import { ScoreSyncPanel } from "./ScoreSyncPanel"
|
||||
|
||||
export const SectlSettingsPanel: React.FC = () => {
|
||||
const { isAuthenticated, isLoading, userInfo, platformId, setPlatformId, refreshUserInfo } =
|
||||
useSectl()
|
||||
const [form] = Form.useForm()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 保存平台 ID 配置
|
||||
const handleSaveConfig = async (values: any) => {
|
||||
try {
|
||||
setIsSaving(true)
|
||||
setPlatformId(values.platformId)
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem("sectl_platform_id", values.platformId)
|
||||
|
||||
message.success("配置已保存")
|
||||
} catch (error: any) {
|
||||
message.error(`保存失败:${error.message}`)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载已保存的配置
|
||||
React.useEffect(() => {
|
||||
const savedPlatformId = localStorage.getItem("sectl_platform_id")
|
||||
if (savedPlatformId) {
|
||||
form.setFieldsValue({ platformId: savedPlatformId })
|
||||
}
|
||||
}, [form])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 认证状态卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
<span>SECTL 认证状态</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div>
|
||||
<Space>
|
||||
<span>当前状态:</span>
|
||||
{isLoading ? (
|
||||
<Tag color="processing">加载中...</Tag>
|
||||
) : isAuthenticated ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
已登录
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<CloseCircleOutlined />} color="default">
|
||||
未登录
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<SectlLoginButton onLoginSuccess={refreshUserInfo} onLogoutSuccess={() => {}} />
|
||||
</div>
|
||||
|
||||
{isAuthenticated && userInfo && (
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="用户 ID">{userInfo.user_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户名">{userInfo.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="邮箱">{userInfo.email}</Descriptions.Item>
|
||||
<Descriptions.Item label="权限">{userInfo.permission}</Descriptions.Item>
|
||||
<Descriptions.Item label="角色">{userInfo.role}</Descriptions.Item>
|
||||
{userInfo.github_username && (
|
||||
<Descriptions.Item label="GitHub">{userInfo.github_username}</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 平台配置卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<SettingOutlined />
|
||||
<span>平台配置</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSaveConfig}
|
||||
initialValues={{ platformId }}
|
||||
>
|
||||
<Alert
|
||||
message="平台 ID 配置"
|
||||
description="请在 SECTL 控制台创建应用后填写平台 ID(Client ID)"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
label="平台 ID (Client ID)"
|
||||
name="platformId"
|
||||
rules={[
|
||||
{ required: true, message: "请输入平台 ID" },
|
||||
{ pattern: /^pf_[a-zA-Z0-9]+$/, message: "平台 ID 格式不正确" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:pf_xxxxxxxxxxxx" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isSaving}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.resetFields()
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 数据同步面板 */}
|
||||
<ScoreSyncPanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user