285 lines
7.1 KiB
TypeScript
285 lines
7.1 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react'
|
|||
|
|
import {
|
|||
|
|
Card,
|
|||
|
|
Button,
|
|||
|
|
Table,
|
|||
|
|
Typography,
|
|||
|
|
Space,
|
|||
|
|
message,
|
|||
|
|
Tag,
|
|||
|
|
Modal,
|
|||
|
|
QRCode,
|
|||
|
|
Tooltip,
|
|||
|
|
Alert,
|
|||
|
|
Row,
|
|||
|
|
Col
|
|||
|
|
} from 'antd'
|
|||
|
|
import {
|
|||
|
|
LinkOutlined,
|
|||
|
|
CopyOutlined,
|
|||
|
|
QrcodeOutlined,
|
|||
|
|
CheckCircleOutlined,
|
|||
|
|
ClockCircleOutlined
|
|||
|
|
} from '@ant-design/icons'
|
|||
|
|
import type { ColumnsType } from 'antd/es/table'
|
|||
|
|
import apiClient from '../services/apiClient'
|
|||
|
|
|
|||
|
|
const { Text, Paragraph } = Typography
|
|||
|
|
|
|||
|
|
interface ShareInfo {
|
|||
|
|
sessionId: string
|
|||
|
|
filename: string
|
|||
|
|
shareUrl: string
|
|||
|
|
createdAt: string
|
|||
|
|
expiresAt: string
|
|||
|
|
isExpired: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface APKShareManagerProps {
|
|||
|
|
serverUrl: string
|
|||
|
|
onShareUrlGenerated?: (shareUrl: string) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const APKShareManager: React.FC<APKShareManagerProps> = ({
|
|||
|
|
serverUrl,
|
|||
|
|
onShareUrlGenerated
|
|||
|
|
}) => {
|
|||
|
|
const [shares, setShares] = useState<ShareInfo[]>([])
|
|||
|
|
const [loading, setLoading] = useState(false)
|
|||
|
|
const [qrModalVisible, setQrModalVisible] = useState(false)
|
|||
|
|
const [currentShareUrl, setCurrentShareUrl] = useState('')
|
|||
|
|
const [currentFilename, setCurrentFilename] = useState('')
|
|||
|
|
|
|||
|
|
// 获取分享链接列表
|
|||
|
|
const fetchShares = async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const result = await apiClient.get<any>('/api/apk/shares')
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
setShares(result.shares || [])
|
|||
|
|
|
|||
|
|
// 如果有新的分享链接,回调通知
|
|||
|
|
if (result.shares && result.shares.length > 0 && onShareUrlGenerated) {
|
|||
|
|
const latestShare = result.shares[result.shares.length - 1]
|
|||
|
|
onShareUrlGenerated(latestShare.shareUrl)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取分享链接失败:', error)
|
|||
|
|
message.error('获取分享链接失败')
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 复制链接
|
|||
|
|
const copyShareUrl = async (shareUrl: string) => {
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(shareUrl)
|
|||
|
|
message.success('分享链接已复制到剪贴板')
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error('复制失败,请手动复制')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示二维码
|
|||
|
|
const showQRCode = (shareUrl: string, filename: string) => {
|
|||
|
|
setCurrentShareUrl(shareUrl)
|
|||
|
|
setCurrentFilename(filename)
|
|||
|
|
setQrModalVisible(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化时间
|
|||
|
|
const formatTime = (timeStr: string) => {
|
|||
|
|
const date = new Date(timeStr)
|
|||
|
|
return date.toLocaleString('zh-CN')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算剩余时间
|
|||
|
|
const getRemainingTime = (expiresAt: string) => {
|
|||
|
|
const now = Date.now()
|
|||
|
|
const expiry = new Date(expiresAt).getTime()
|
|||
|
|
const remaining = expiry - now
|
|||
|
|
|
|||
|
|
if (remaining <= 0) {
|
|||
|
|
return '已过期'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const minutes = Math.floor(remaining / 60000)
|
|||
|
|
const seconds = Math.floor((remaining % 60000) / 1000)
|
|||
|
|
|
|||
|
|
if (minutes > 0) {
|
|||
|
|
return `${minutes}分${seconds}秒`
|
|||
|
|
} else {
|
|||
|
|
return `${seconds}秒`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 表格列定义
|
|||
|
|
const columns: ColumnsType<ShareInfo> = [
|
|||
|
|
{
|
|||
|
|
title: '文件名',
|
|||
|
|
dataIndex: 'filename',
|
|||
|
|
key: 'filename',
|
|||
|
|
render: (filename) => (
|
|||
|
|
<Space>
|
|||
|
|
<Text strong>{filename}</Text>
|
|||
|
|
</Space>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '状态',
|
|||
|
|
key: 'status',
|
|||
|
|
render: (_, record) => (
|
|||
|
|
<Tag
|
|||
|
|
icon={record.isExpired ? <ClockCircleOutlined /> : <CheckCircleOutlined />}
|
|||
|
|
color={record.isExpired ? 'red' : 'green'}
|
|||
|
|
>
|
|||
|
|
{record.isExpired ? '已过期' : '活跃'}
|
|||
|
|
</Tag>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '剩余时间',
|
|||
|
|
key: 'remaining',
|
|||
|
|
render: (_, record) => (
|
|||
|
|
<Text type={record.isExpired ? 'danger' : 'success'}>
|
|||
|
|
{getRemainingTime(record.expiresAt)}
|
|||
|
|
</Text>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '创建时间',
|
|||
|
|
dataIndex: 'createdAt',
|
|||
|
|
key: 'createdAt',
|
|||
|
|
render: (createdAt) => formatTime(createdAt)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '过期时间',
|
|||
|
|
dataIndex: 'expiresAt',
|
|||
|
|
key: 'expiresAt',
|
|||
|
|
render: (expiresAt) => formatTime(expiresAt)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '操作',
|
|||
|
|
key: 'actions',
|
|||
|
|
render: (_, record) => (
|
|||
|
|
<Space>
|
|||
|
|
<Tooltip title="显示二维码">
|
|||
|
|
<Button
|
|||
|
|
type="default"
|
|||
|
|
size="small"
|
|||
|
|
icon={<QrcodeOutlined />}
|
|||
|
|
onClick={() => showQRCode(record.shareUrl, record.filename)}
|
|||
|
|
disabled={record.isExpired}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Space>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 组件挂载时获取数据
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchShares()
|
|||
|
|
|
|||
|
|
// 定时刷新分享列表
|
|||
|
|
const interval = setInterval(fetchShares, 10000) // 每10秒刷新一次
|
|||
|
|
|
|||
|
|
return () => clearInterval(interval)
|
|||
|
|
}, [serverUrl])
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card
|
|||
|
|
title={
|
|||
|
|
<Space>
|
|||
|
|
<LinkOutlined />
|
|||
|
|
<span>Cloudflare 分享链接</span>
|
|||
|
|
</Space>
|
|||
|
|
}
|
|||
|
|
extra={
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
onClick={fetchShares}
|
|||
|
|
loading={loading}
|
|||
|
|
>
|
|||
|
|
刷新
|
|||
|
|
</Button>
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
{shares.length === 0 ? (
|
|||
|
|
<Alert
|
|||
|
|
message="暂无分享链接"
|
|||
|
|
description="构建APK后将自动生成Cloudflare分享链接,有效期10分钟"
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
|||
|
|
<Col span={24}>
|
|||
|
|
<Alert
|
|||
|
|
message="分享链接说明"
|
|||
|
|
description={
|
|||
|
|
<div>
|
|||
|
|
<p>• 每次构建APK后会自动生成Cloudflare临时分享链接</p>
|
|||
|
|
<p>• 分享链接有效期为10分钟,过期后自动失效</p>
|
|||
|
|
<p>• 可以通过二维码或链接分享给他人下载</p>
|
|||
|
|
<p>• 建议及时下载,避免链接过期</p>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Table
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={shares || []}
|
|||
|
|
rowKey="sessionId"
|
|||
|
|
loading={loading}
|
|||
|
|
pagination={false}
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 二维码模态框 */}
|
|||
|
|
<Modal
|
|||
|
|
title={`${currentFilename} - 分享二维码`}
|
|||
|
|
open={qrModalVisible}
|
|||
|
|
onCancel={() => setQrModalVisible(false)}
|
|||
|
|
footer={[
|
|||
|
|
<Button key="copy" onClick={() => copyShareUrl(currentShareUrl)}>
|
|||
|
|
<CopyOutlined /> 复制链接
|
|||
|
|
</Button>,
|
|||
|
|
<Button key="close" onClick={() => setQrModalVisible(false)}>
|
|||
|
|
关闭
|
|||
|
|
</Button>
|
|||
|
|
]}
|
|||
|
|
width={400}
|
|||
|
|
>
|
|||
|
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
|||
|
|
<QRCode value={currentShareUrl} size={200} />
|
|||
|
|
<Paragraph
|
|||
|
|
copyable
|
|||
|
|
style={{
|
|||
|
|
marginTop: 16,
|
|||
|
|
wordBreak: 'break-all',
|
|||
|
|
fontSize: '12px'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{currentShareUrl}
|
|||
|
|
</Paragraph>
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
使用手机扫描二维码或点击链接下载APK
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
</Card>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default APKShareManager
|