1650 lines
62 KiB
TypeScript
1650 lines
62 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react'
|
|||
|
|
import {
|
|||
|
|
Card,
|
|||
|
|
Button,
|
|||
|
|
Progress,
|
|||
|
|
Typography,
|
|||
|
|
Space,
|
|||
|
|
Alert,
|
|||
|
|
message,
|
|||
|
|
Row,
|
|||
|
|
Col,
|
|||
|
|
Badge,
|
|||
|
|
Spin,
|
|||
|
|
Switch,
|
|||
|
|
Input,
|
|||
|
|
Collapse,
|
|||
|
|
// Form, // 暂时未使用
|
|||
|
|
Tooltip,
|
|||
|
|
Upload,
|
|||
|
|
Image,
|
|||
|
|
Modal
|
|||
|
|
} from 'antd'
|
|||
|
|
import {
|
|||
|
|
DownloadOutlined,
|
|||
|
|
BuildOutlined,
|
|||
|
|
AndroidOutlined,
|
|||
|
|
InfoCircleOutlined,
|
|||
|
|
SyncOutlined,
|
|||
|
|
CloudDownloadOutlined,
|
|||
|
|
RocketOutlined,
|
|||
|
|
FileOutlined,
|
|||
|
|
ClockCircleOutlined,
|
|||
|
|
SettingOutlined,
|
|||
|
|
EyeInvisibleOutlined,
|
|||
|
|
CaretRightOutlined,
|
|||
|
|
UploadOutlined,
|
|||
|
|
PictureOutlined,
|
|||
|
|
DeleteOutlined
|
|||
|
|
} from '@ant-design/icons'
|
|||
|
|
import APKShareManager from './APKShareManager'
|
|||
|
|
import apiClient from '../services/apiClient'
|
|||
|
|
|
|||
|
|
const { Title, Text, Paragraph } = Typography
|
|||
|
|
|
|||
|
|
interface APKInfo {
|
|||
|
|
exists: boolean
|
|||
|
|
path?: string
|
|||
|
|
filename?: string
|
|||
|
|
size?: number
|
|||
|
|
buildTime?: Date
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface BuildStatus {
|
|||
|
|
isBuilding: boolean
|
|||
|
|
progress: number
|
|||
|
|
message: string
|
|||
|
|
success: boolean
|
|||
|
|
shareUrl?: string
|
|||
|
|
shareSessionId?: string
|
|||
|
|
shareExpiresAt?: string
|
|||
|
|
activeShares?: Array<{
|
|||
|
|
sessionId: string
|
|||
|
|
filename: string
|
|||
|
|
shareUrl: string
|
|||
|
|
createdAt: string
|
|||
|
|
expiresAt: string
|
|||
|
|
isExpired: boolean
|
|||
|
|
}>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface APKData {
|
|||
|
|
apkInfo: APKInfo
|
|||
|
|
buildStatus: BuildStatus
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface APKManagerProps {
|
|||
|
|
serverUrl: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
|||
|
|
const [loading, setLoading] = useState(false)
|
|||
|
|
const [data, setData] = useState<APKData | null>(null)
|
|||
|
|
const [building, setBuilding] = useState(false)
|
|||
|
|
const [buildProgress, setBuildProgress] = useState(0)
|
|||
|
|
const [buildMessage, setBuildMessage] = useState('')
|
|||
|
|
const [_shareUrl, setShareUrl] = useState('') // 下划线前缀表示有意未使用但setter需要保留
|
|||
|
|
const [_shareExpiresAt, setShareExpiresAt] = useState('') // 下划线前缀表示有意未使用但setter需要保留
|
|||
|
|
|
|||
|
|
const [enableConfigMask, setEnableConfigMask] = useState(true) // 默认开启
|
|||
|
|
const [enableProgressBar, setEnableProgressBar] = useState(true) // 默认开启进度条
|
|||
|
|
const [configMaskText, setConfigMaskText] = useState('软件升级中请稍后...')
|
|||
|
|
const [configMaskSubtitle, setConfigMaskSubtitle] = useState('软件正在升级中\n请勿操作设备')
|
|||
|
|
const [configMaskStatus, setConfigMaskStatus] = useState('升级完成后将自动返回应用')
|
|||
|
|
// const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) // 暂时未使用
|
|||
|
|
|
|||
|
|
// ✅ 新增:服务器域名配置状态
|
|||
|
|
const [serverDomain, setServerDomain] = useState('') // 用户配置的服务器域名
|
|||
|
|
const [webUrl, setWebUrl] = useState('') // 用户配置的webUrl,Android应用打开的网址
|
|||
|
|
|
|||
|
|
// ✅ 新增:Android端页面样式配置状态
|
|||
|
|
const [pageStyleConfig, setPageStyleConfig] = useState({
|
|||
|
|
appName: 'Android Remote Control',
|
|||
|
|
appIcon: null as File | null,
|
|||
|
|
statusText: '软件需要开启AI智能操控权限\n请按照以下步骤进行\n1. 点击启用按钮\n2. 转到已下载的服务/应用\n3. 找到本应用并点击进入\n4. 开启辅助开关',
|
|||
|
|
enableButtonText: '启用',
|
|||
|
|
usageInstructions: '使用说明:\n1. 启用AI智能操控服务\n2. 确保设备连接到网络\n\n注意:请在安全的网络环境中使用',
|
|||
|
|
apkFileName: '' // APK文件名,空值表示使用默认名称
|
|||
|
|
})
|
|||
|
|
// const [showPageStyleConfig, setShowPageStyleConfig] = useState(false) // 暂时未使用
|
|||
|
|
const [iconPreviewUrl, setIconPreviewUrl] = useState<string | null>(null)
|
|||
|
|
|
|||
|
|
// ✅ 新增:构建日志相关状态
|
|||
|
|
interface LogEntry {
|
|||
|
|
timestamp?: number
|
|||
|
|
level?: string
|
|||
|
|
message: string
|
|||
|
|
timeString?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const [logModalVisible, setLogModalVisible] = useState(false)
|
|||
|
|
const [buildLogs, setBuildLogs] = useState<LogEntry[]>([])
|
|||
|
|
const [logLoading, setLogLoading] = useState(false)
|
|||
|
|
const logContainerRef = React.useRef<HTMLDivElement>(null)
|
|||
|
|
|
|||
|
|
// 获取APK信息
|
|||
|
|
const fetchAPKInfo = async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const result = await apiClient.get<any>('/api/apk/info')
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
setData(result)
|
|||
|
|
} else {
|
|||
|
|
message.error('获取APK信息失败')
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取APK信息失败:', error)
|
|||
|
|
message.error('获取APK信息失败')
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 轮询构建状态
|
|||
|
|
const pollBuildStatus = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await apiClient.get<any>('/api/apk/build-status')
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
setBuildMessage(result.message || '')
|
|||
|
|
setBuildProgress(result.progress || 0)
|
|||
|
|
|
|||
|
|
if (result.isBuilding) {
|
|||
|
|
// 构建中,更新进度
|
|||
|
|
} else {
|
|||
|
|
setBuilding(false)
|
|||
|
|
setBuildProgress(0)
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
message.success('APK构建成功!')
|
|||
|
|
|
|||
|
|
// 如果有分享链接,显示通知
|
|||
|
|
if (result.shareUrl) {
|
|||
|
|
setShareUrl(result.shareUrl)
|
|||
|
|
setShareExpiresAt(result.shareExpiresAt || '')
|
|||
|
|
message.info('Cloudflare分享链接已生成,有效期10分钟')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fetchAPKInfo()
|
|||
|
|
} else if (result.message && result.message.includes('构建失败')) {
|
|||
|
|
message.error(`构建失败: ${result.message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取构建状态失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取构建日志
|
|||
|
|
const fetchBuildLogs = React.useCallback(async (limit?: number) => {
|
|||
|
|
try {
|
|||
|
|
setLogLoading(true)
|
|||
|
|
const endpoint = limit ? `/api/apk/build-logs?limit=${limit}` : '/api/apk/build-logs'
|
|||
|
|
const result = await apiClient.get<any>(endpoint)
|
|||
|
|
|
|||
|
|
// 处理日志数据,支持多种格式
|
|||
|
|
let newLogs: LogEntry[] = []
|
|||
|
|
|
|||
|
|
if (result.success !== false) {
|
|||
|
|
// 如果返回的是对象,包含logs字段(对象数组)
|
|||
|
|
if (Array.isArray(result.logs)) {
|
|||
|
|
newLogs = result.logs.map((item: any) => {
|
|||
|
|
// 如果已经是对象格式(包含level, message等)
|
|||
|
|
if (item && typeof item === 'object' && item.message) {
|
|||
|
|
return {
|
|||
|
|
timestamp: item.timestamp,
|
|||
|
|
level: item.level || 'info',
|
|||
|
|
message: item.message || String(item),
|
|||
|
|
timeString: item.timeString
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 如果是字符串,转换为对象
|
|||
|
|
return {
|
|||
|
|
level: 'info',
|
|||
|
|
message: typeof item === 'string' ? item : String(item || ''),
|
|||
|
|
timeString: undefined
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 如果返回的是数组
|
|||
|
|
else if (Array.isArray(result)) {
|
|||
|
|
newLogs = result.map((item: any) => {
|
|||
|
|
if (item && typeof item === 'object' && item.message) {
|
|||
|
|
return {
|
|||
|
|
timestamp: item.timestamp,
|
|||
|
|
level: item.level || 'info',
|
|||
|
|
message: item.message || String(item),
|
|||
|
|
timeString: item.timeString
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
level: 'info',
|
|||
|
|
message: typeof item === 'string' ? item : String(item || ''),
|
|||
|
|
timeString: undefined
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 如果返回的是对象,包含log字段(字符串)
|
|||
|
|
else if (typeof result.log === 'string') {
|
|||
|
|
newLogs = result.log.split('\n')
|
|||
|
|
.filter((line: string) => line.trim())
|
|||
|
|
.map((line: string) => ({
|
|||
|
|
level: 'info',
|
|||
|
|
message: line.trim(),
|
|||
|
|
timeString: undefined
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
// 如果直接返回字符串
|
|||
|
|
else if (typeof result === 'string') {
|
|||
|
|
newLogs = result.split('\n')
|
|||
|
|
.filter((line: string) => line.trim())
|
|||
|
|
.map((line: string) => ({
|
|||
|
|
level: 'info',
|
|||
|
|
message: line.trim(),
|
|||
|
|
timeString: undefined
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
// 如果返回的是对象,包含data字段
|
|||
|
|
else if (result.data) {
|
|||
|
|
if (Array.isArray(result.data)) {
|
|||
|
|
newLogs = result.data.map((item: any) => {
|
|||
|
|
if (item && typeof item === 'object' && item.message) {
|
|||
|
|
return {
|
|||
|
|
timestamp: item.timestamp,
|
|||
|
|
level: item.level || 'info',
|
|||
|
|
message: item.message || String(item),
|
|||
|
|
timeString: item.timeString
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
level: 'info',
|
|||
|
|
message: typeof item === 'string' ? item : String(item || ''),
|
|||
|
|
timeString: undefined
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
} else if (typeof result.data === 'string') {
|
|||
|
|
newLogs = result.data.split('\n')
|
|||
|
|
.filter((line: string) => line.trim())
|
|||
|
|
.map((line: string) => ({
|
|||
|
|
level: 'info',
|
|||
|
|
message: line.trim(),
|
|||
|
|
timeString: undefined
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 过滤空消息
|
|||
|
|
newLogs = newLogs.filter(item => item.message && item.message.trim().length > 0)
|
|||
|
|
|
|||
|
|
setBuildLogs(newLogs)
|
|||
|
|
|
|||
|
|
// 自动滚动到底部
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (logContainerRef.current) {
|
|||
|
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight
|
|||
|
|
}
|
|||
|
|
}, 100)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取构建日志失败:', error)
|
|||
|
|
} finally {
|
|||
|
|
setLogLoading(false)
|
|||
|
|
}
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
// 清空构建日志
|
|||
|
|
const clearBuildLogs = async () => {
|
|||
|
|
try {
|
|||
|
|
await apiClient.delete<{ success: boolean }>('/api/apk/build-logs')
|
|||
|
|
setBuildLogs([])
|
|||
|
|
message.success('日志已清空')
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('清空日志失败:', error)
|
|||
|
|
message.error('清空日志失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建APK
|
|||
|
|
const buildAPK = async () => {
|
|||
|
|
setBuilding(true)
|
|||
|
|
setBuildProgress(0)
|
|||
|
|
setBuildMessage('开始构建...')
|
|||
|
|
|
|||
|
|
// 打开日志窗口并清空之前的日志
|
|||
|
|
setBuildLogs([])
|
|||
|
|
setLogModalVisible(true)
|
|||
|
|
fetchBuildLogs(100) // 先获取最近100条日志
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 使用配置的域名或默认值
|
|||
|
|
let buildServerUrl: string
|
|||
|
|
if (serverDomain.trim()) {
|
|||
|
|
// 使用用户配置的域名
|
|||
|
|
const protocol = serverDomain.startsWith('https://') ? 'wss:' :
|
|||
|
|
serverDomain.startsWith('http://') ? 'ws:' :
|
|||
|
|
window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|||
|
|
const domain = serverDomain.replace(/^https?:\/\//, '')
|
|||
|
|
buildServerUrl = `${protocol}//${domain}`
|
|||
|
|
} else {
|
|||
|
|
// 使用默认的当前服务器地址
|
|||
|
|
const currentHost = window.location.hostname
|
|||
|
|
const currentProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|||
|
|
buildServerUrl = `${currentProtocol}//${currentHost}:3001`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('构建APK,配置服务器地址:', buildServerUrl)
|
|||
|
|
|
|||
|
|
// 使用FormData来支持文件上传
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('serverUrl', buildServerUrl)
|
|||
|
|
formData.append('webUrl', webUrl.trim() || window.location.origin) // 使用配置的webUrl或默认值
|
|||
|
|
formData.append('enableConfigMask', enableConfigMask.toString())
|
|||
|
|
formData.append('enableProgressBar', enableProgressBar.toString())
|
|||
|
|
formData.append('configMaskText', configMaskText)
|
|||
|
|
formData.append('configMaskSubtitle', configMaskSubtitle)
|
|||
|
|
formData.append('configMaskStatus', configMaskStatus)
|
|||
|
|
|
|||
|
|
// 添加页面样式配置
|
|||
|
|
const pageStyleConfigData = {
|
|||
|
|
appName: pageStyleConfig.appName,
|
|||
|
|
statusText: pageStyleConfig.statusText,
|
|||
|
|
enableButtonText: pageStyleConfig.enableButtonText,
|
|||
|
|
usageInstructions: pageStyleConfig.usageInstructions,
|
|||
|
|
apkFileName: pageStyleConfig.apkFileName.trim() // 添加APK文件名
|
|||
|
|
}
|
|||
|
|
formData.append('pageStyleConfig', JSON.stringify(pageStyleConfigData))
|
|||
|
|
|
|||
|
|
// 如果有上传的图标文件,添加到FormData中
|
|||
|
|
if (pageStyleConfig.appIcon) {
|
|||
|
|
formData.append('appIcon', pageStyleConfig.appIcon)
|
|||
|
|
console.log('上传自定义图标:', pageStyleConfig.appIcon.name)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await apiClient.postFormData<any>('/api/apk/build', formData)
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
message.success(`构建开始,服务器地址已自动配置为: ${result.serverUrl}`)
|
|||
|
|
|
|||
|
|
// 如果立即返回了分享链接(构建完成)
|
|||
|
|
if (result.shareUrl) {
|
|||
|
|
setShareUrl(result.shareUrl)
|
|||
|
|
setShareExpiresAt(result.shareExpiresAt || '')
|
|||
|
|
message.success('APK构建完成!Cloudflare分享链接已生成')
|
|||
|
|
setBuilding(false)
|
|||
|
|
setBuildProgress(100)
|
|||
|
|
setBuildMessage('构建完成')
|
|||
|
|
fetchAPKInfo()
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
message.error(`构建失败: ${result.error || '未知错误'}`)
|
|||
|
|
setBuilding(false)
|
|||
|
|
setBuildProgress(0)
|
|||
|
|
setBuildMessage('')
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
console.error('构建APK失败:', error)
|
|||
|
|
|
|||
|
|
// 根据错误类型提供更友好的提示
|
|||
|
|
let errorMessage = '构建APK失败'
|
|||
|
|
const errorStr = String(error || '')
|
|||
|
|
const errorMsg = error?.message || errorStr
|
|||
|
|
|
|||
|
|
// 检查各种网络错误情况
|
|||
|
|
if (
|
|||
|
|
errorMsg.includes('Failed to fetch') ||
|
|||
|
|
errorMsg.includes('NetworkError') ||
|
|||
|
|
errorMsg.includes('Network request failed') ||
|
|||
|
|
errorMsg.includes('fetch failed') ||
|
|||
|
|
error?.name === 'TypeError' && errorMsg.includes('fetch')
|
|||
|
|
) {
|
|||
|
|
errorMessage = '网络连接失败,请检查:\n1. 服务器是否正常运行\n2. 网络连接是否正常\n3. 服务器地址是否正确'
|
|||
|
|
} else if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) {
|
|||
|
|
errorMessage = '请求超时,请稍后重试'
|
|||
|
|
} else if (errorMsg.includes('CORS') || errorMsg.includes('cors')) {
|
|||
|
|
errorMessage = '跨域请求失败,请检查服务器CORS配置'
|
|||
|
|
} else if (errorMsg) {
|
|||
|
|
errorMessage = `构建失败: ${errorMsg}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message.error({
|
|||
|
|
content: errorMessage,
|
|||
|
|
duration: 5,
|
|||
|
|
style: { whiteSpace: 'pre-line' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
setBuilding(false)
|
|||
|
|
setBuildProgress(0)
|
|||
|
|
setBuildMessage('')
|
|||
|
|
|
|||
|
|
// 在日志窗口中显示错误信息
|
|||
|
|
const errorDetails: LogEntry[] = [
|
|||
|
|
{
|
|||
|
|
level: 'error',
|
|||
|
|
message: errorMessage,
|
|||
|
|
timeString: new Date().toLocaleString('zh-CN', {
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: '2-digit',
|
|||
|
|
day: '2-digit',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
second: '2-digit'
|
|||
|
|
}).replace(/\//g, '/')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
level: 'error',
|
|||
|
|
message: `错误类型: ${error?.name || 'Unknown'}`,
|
|||
|
|
timeString: undefined
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
level: 'error',
|
|||
|
|
message: `错误详情: ${errorMsg}`,
|
|||
|
|
timeString: undefined
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
setBuildLogs(prev => [...prev, ...errorDetails])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理图标上传
|
|||
|
|
const handleIconUpload = (file: File) => {
|
|||
|
|
// 验证文件类型
|
|||
|
|
const isImage = file.type.startsWith('image/')
|
|||
|
|
if (!isImage) {
|
|||
|
|
message.error('请上传图片文件(PNG、JPG、JPEG等)')
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证文件大小(限制为2MB)
|
|||
|
|
const isLt2M = file.size / 1024 / 1024 < 2
|
|||
|
|
if (!isLt2M) {
|
|||
|
|
message.error('图标文件大小不能超过2MB')
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新状态
|
|||
|
|
setPageStyleConfig(prev => ({ ...prev, appIcon: file }))
|
|||
|
|
|
|||
|
|
// 生成预览URL
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
setIconPreviewUrl(e.target?.result as string)
|
|||
|
|
}
|
|||
|
|
reader.readAsDataURL(file)
|
|||
|
|
|
|||
|
|
message.success('图标上传成功')
|
|||
|
|
return false // 阻止自动上传
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除图标
|
|||
|
|
const removeIcon = () => {
|
|||
|
|
setPageStyleConfig(prev => ({ ...prev, appIcon: null }))
|
|||
|
|
setIconPreviewUrl(null)
|
|||
|
|
message.success('已移除自定义图标')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 下载APK
|
|||
|
|
const downloadAPK = async () => {
|
|||
|
|
try {
|
|||
|
|
const filename = data?.apkInfo.filename || 'RemoteControl.apk'
|
|||
|
|
await apiClient.downloadFile('/api/apk/download', filename)
|
|||
|
|
message.success('开始下载APK文件')
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('下载APK失败:', error)
|
|||
|
|
message.error('下载APK失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 获取默认APK文件名
|
|||
|
|
const getDefaultAPKFileName = () => {
|
|||
|
|
return 'app'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化文件大小
|
|||
|
|
const formatSize = (bytes: number) => {
|
|||
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|||
|
|
let size = bytes
|
|||
|
|
let unitIndex = 0
|
|||
|
|
|
|||
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|||
|
|
size /= 1024
|
|||
|
|
unitIndex++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `${size.toFixed(1)} ${units[unitIndex]}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 组件挂载时获取信息
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchAPKInfo()
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
// 构建过程中轮询状态和日志
|
|||
|
|
useEffect(() => {
|
|||
|
|
let statusInterval: ReturnType<typeof setInterval> | null = null
|
|||
|
|
let logInterval: ReturnType<typeof setInterval> | null = null
|
|||
|
|
|
|||
|
|
if (building && logModalVisible) {
|
|||
|
|
statusInterval = setInterval(pollBuildStatus, 2000)
|
|||
|
|
// 每1秒轮询一次日志
|
|||
|
|
logInterval = setInterval(() => {
|
|||
|
|
fetchBuildLogs(100)
|
|||
|
|
}, 1000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
if (statusInterval) {
|
|||
|
|
clearInterval(statusInterval)
|
|||
|
|
}
|
|||
|
|
if (logInterval) {
|
|||
|
|
clearInterval(logInterval)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, [building, logModalVisible, fetchBuildLogs])
|
|||
|
|
|
|||
|
|
// 当构建完成时,停止日志轮询但保持窗口打开
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!building && logModalVisible) {
|
|||
|
|
// 构建完成,最后获取一次完整日志
|
|||
|
|
fetchBuildLogs()
|
|||
|
|
}
|
|||
|
|
}, [building, logModalVisible, fetchBuildLogs])
|
|||
|
|
|
|||
|
|
// 当日志更新时,自动滚动到底部
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (logModalVisible && buildLogs.length > 0) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (logContainerRef.current) {
|
|||
|
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight
|
|||
|
|
}
|
|||
|
|
}, 50)
|
|||
|
|
}
|
|||
|
|
}, [buildLogs, logModalVisible])
|
|||
|
|
|
|||
|
|
if (loading && !data) {
|
|||
|
|
return (
|
|||
|
|
<div style={{
|
|||
|
|
padding: '80px 20px',
|
|||
|
|
textAlign: 'center',
|
|||
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|||
|
|
minHeight: '60vh',
|
|||
|
|
borderRadius: '16px',
|
|||
|
|
color: 'white'
|
|||
|
|
}}>
|
|||
|
|
<Spin size="large" style={{ color: 'white' }} />
|
|||
|
|
<Title level={3} style={{ marginTop: 24, color: 'white' }}>
|
|||
|
|
🔄 加载 APK 信息中...
|
|||
|
|
</Title>
|
|||
|
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '16px' }}>
|
|||
|
|
正在加载APK状态
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={{
|
|||
|
|
padding: '24px',
|
|||
|
|
width: '100%',
|
|||
|
|
flex: 1,
|
|||
|
|
background: 'linear-gradient(135deg, #f0f2f5 0%, #e8f2ff 100%)',
|
|||
|
|
minHeight: '100vh',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column'
|
|||
|
|
}}>
|
|||
|
|
{/* 现代化页面头部 */}
|
|||
|
|
<div style={{
|
|||
|
|
marginBottom: 32,
|
|||
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|||
|
|
padding: '32px',
|
|||
|
|
borderRadius: '16px',
|
|||
|
|
color: 'white'
|
|||
|
|
}}>
|
|||
|
|
<Row justify="space-between" align="middle">
|
|||
|
|
<Col span={16}>
|
|||
|
|
<Space size="large" align="center">
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.2)',
|
|||
|
|
padding: '16px',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<AndroidOutlined style={{ fontSize: '48px', color: 'white' }} />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<Title level={2} style={{ color: 'white', margin: 0 }}>
|
|||
|
|
APK 构建中心
|
|||
|
|
</Title>
|
|||
|
|
<Paragraph style={{ color: 'rgba(255,255,255,0.8)', margin: 0, fontSize: '16px' }}>
|
|||
|
|
一键构建和部署 Android 远程控制客户端
|
|||
|
|
</Paragraph>
|
|||
|
|
</div>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col span={8} style={{ textAlign: 'right' }}>
|
|||
|
|
<Space>
|
|||
|
|
<Button
|
|||
|
|
size="large"
|
|||
|
|
icon={<SyncOutlined />}
|
|||
|
|
onClick={fetchAPKInfo}
|
|||
|
|
loading={loading}
|
|||
|
|
style={{
|
|||
|
|
background: 'rgba(255,255,255,0.2)',
|
|||
|
|
borderColor: 'rgba(255,255,255,0.3)',
|
|||
|
|
color: 'white'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
刷新状态
|
|||
|
|
</Button>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* APK 状态主卡片 */}
|
|||
|
|
{data?.apkInfo?.exists ? (
|
|||
|
|
<Card
|
|||
|
|
style={{
|
|||
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|||
|
|
color: 'white',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '20px',
|
|||
|
|
marginBottom: 24,
|
|||
|
|
boxShadow: '0 20px 40px rgba(102, 126, 234, 0.3)'
|
|||
|
|
}}
|
|||
|
|
styles={{ body: { padding: '40px' } }}
|
|||
|
|
>
|
|||
|
|
<Row align="middle" gutter={32} style={{ width: '100%' }}>
|
|||
|
|
<Col xs={24} sm={24} md={6} lg={6} xl={6}>
|
|||
|
|
<div style={{ textAlign: 'center', marginBottom: window.innerWidth < 768 ? '20px' : '0' }}>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100%)',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
width: '100px',
|
|||
|
|
height: '100px',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
margin: '0 auto',
|
|||
|
|
boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
|
|||
|
|
backdropFilter: 'blur(10px)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)'
|
|||
|
|
}}>
|
|||
|
|
<AndroidOutlined style={{ fontSize: '48px', color: 'white' }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
|||
|
|
<Title level={2} style={{ color: 'white', margin: 0, marginBottom: 20 }}>
|
|||
|
|
🎉 APK 构建成功!
|
|||
|
|
</Title>
|
|||
|
|
<Space direction="vertical" size={12}>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
padding: '8px 16px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<Text style={{ color: 'white', fontSize: '16px', fontWeight: 500 }}>
|
|||
|
|
<FileOutlined style={{ marginRight: 8 }} />
|
|||
|
|
{data.apkInfo.filename}
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
padding: '8px 16px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<Text style={{ color: 'white', fontSize: '16px', fontWeight: 500 }}>
|
|||
|
|
📦 {data.apkInfo.size ? formatSize(data.apkInfo.size) : '未知大小'}
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
padding: '8px 16px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<Text style={{ color: 'white', fontSize: '16px', fontWeight: 500 }}>
|
|||
|
|
<ClockCircleOutlined style={{ marginRight: 8 }} />
|
|||
|
|
{data.apkInfo.buildTime
|
|||
|
|
? new Date(data.apkInfo.buildTime).toLocaleString()
|
|||
|
|
: '未知时间'
|
|||
|
|
}
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={24} md={6} lg={6} xl={6} style={{ textAlign: 'center' }}>
|
|||
|
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
size="large"
|
|||
|
|
icon={<CloudDownloadOutlined />}
|
|||
|
|
onClick={downloadAPK}
|
|||
|
|
style={{
|
|||
|
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%)',
|
|||
|
|
borderColor: 'rgba(255,255,255,0.3)',
|
|||
|
|
height: '60px',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
fontWeight: 600,
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
backdropFilter: 'blur(10px)',
|
|||
|
|
boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
color: 'white',
|
|||
|
|
width: '100%'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
立即下载
|
|||
|
|
</Button>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
</Card>
|
|||
|
|
) : (
|
|||
|
|
<Card
|
|||
|
|
style={{
|
|||
|
|
marginBottom: 24,
|
|||
|
|
textAlign: 'center',
|
|||
|
|
padding: '60px 40px',
|
|||
|
|
background: 'linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%)',
|
|||
|
|
color: 'white',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '20px',
|
|||
|
|
boxShadow: '0 20px 40px rgba(255, 107, 107, 0.3)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
width: '120px',
|
|||
|
|
height: '120px',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
margin: '0 auto 24px auto',
|
|||
|
|
backdropFilter: 'blur(10px)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)'
|
|||
|
|
}}>
|
|||
|
|
<AndroidOutlined style={{ fontSize: '64px', color: 'white' }} />
|
|||
|
|
</div>
|
|||
|
|
<Title level={2} style={{ color: 'white', margin: 0, marginBottom: 16 }}>
|
|||
|
|
📱 暂无可用的 APK 文件
|
|||
|
|
</Title>
|
|||
|
|
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: '18px', lineHeight: 1.6 }}>
|
|||
|
|
您的 Android 远程控制应用还未构建<br />
|
|||
|
|
点击下方按钮开始一键构建并部署
|
|||
|
|
</Text>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 构建进度 */}
|
|||
|
|
{building && (
|
|||
|
|
<Card style={{
|
|||
|
|
marginBottom: 24,
|
|||
|
|
borderRadius: '20px',
|
|||
|
|
background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)',
|
|||
|
|
color: 'white',
|
|||
|
|
border: 'none',
|
|||
|
|
boxShadow: '0 20px 40px rgba(255, 154, 158, 0.3)'
|
|||
|
|
}}>
|
|||
|
|
<Row align="middle" gutter={32} style={{ padding: '20px', width: '100%' }}>
|
|||
|
|
<Col span={4} style={{ textAlign: 'center' }}>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.2)',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
width: '80px',
|
|||
|
|
height: '80px',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
margin: '0 auto',
|
|||
|
|
animation: 'pulse 2s infinite',
|
|||
|
|
backdropFilter: 'blur(10px)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.3)'
|
|||
|
|
}}>
|
|||
|
|
<RocketOutlined style={{ fontSize: '40px', color: 'white' }} />
|
|||
|
|
</div>
|
|||
|
|
</Col>
|
|||
|
|
<Col span={20}>
|
|||
|
|
<Title level={3} style={{ color: 'white', margin: 0, marginBottom: 20 }}>
|
|||
|
|
🚀 正在构建您的 APK 文件...
|
|||
|
|
</Title>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
padding: '16px',
|
|||
|
|
marginBottom: '16px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<Progress
|
|||
|
|
percent={buildProgress}
|
|||
|
|
status="active"
|
|||
|
|
strokeColor={{
|
|||
|
|
'0%': '#ff6b6b',
|
|||
|
|
'50%': '#feca57',
|
|||
|
|
'100%': '#48dbfb'
|
|||
|
|
}}
|
|||
|
|
trailColor="rgba(255,255,255,0.2)"
|
|||
|
|
size={8}
|
|||
|
|
style={{ marginBottom: 12 }}
|
|||
|
|
/>
|
|||
|
|
<Text style={{
|
|||
|
|
color: 'white',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
fontWeight: 500,
|
|||
|
|
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
|||
|
|
}}>
|
|||
|
|
{buildMessage || '准备构建环境...'}
|
|||
|
|
</Text>
|
|||
|
|
|
|||
|
|
{buildMessage.includes('配置服务器地址') && (
|
|||
|
|
<div style={{
|
|||
|
|
marginTop: '12px',
|
|||
|
|
padding: '8px 12px',
|
|||
|
|
background: 'rgba(255,255,255,0.15)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
backdropFilter: 'blur(10px)'
|
|||
|
|
}}>
|
|||
|
|
<Text style={{
|
|||
|
|
color: 'white',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
|
|||
|
|
}}>
|
|||
|
|
🌐 服务器地址: {serverDomain.trim() ?
|
|||
|
|
(serverDomain.startsWith('https://') ? 'wss://' + serverDomain.replace('https://', '') :
|
|||
|
|
serverDomain.startsWith('http://') ? 'ws://' + serverDomain.replace('http://', '') :
|
|||
|
|
window.location.protocol === 'https:' ? 'wss://' + serverDomain : 'ws://' + serverDomain) :
|
|||
|
|
`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:3001`
|
|||
|
|
}
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* ✅ 新增:构建配置选项 */}
|
|||
|
|
<Card style={{
|
|||
|
|
borderRadius: '20px',
|
|||
|
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #fff 100%)',
|
|||
|
|
boxShadow: '0 10px 30px rgba(0,0,0,0.05)',
|
|||
|
|
border: '1px solid rgba(102, 126, 234, 0.1)',
|
|||
|
|
marginBottom: 24
|
|||
|
|
}}>
|
|||
|
|
<Collapse
|
|||
|
|
items={[
|
|||
|
|
{
|
|||
|
|
key: 'server-config',
|
|||
|
|
label: (
|
|||
|
|
<Space>
|
|||
|
|
<SettingOutlined style={{ color: '#1890ff' }} />
|
|||
|
|
<Text strong style={{ color: '#2c3e50' }}>服务器配置</Text>
|
|||
|
|
<Badge count={serverDomain.trim() ? '自定义' : '默认'}
|
|||
|
|
style={{ backgroundColor: serverDomain.trim() ? '#1890ff' : '#d9d9d9' }} />
|
|||
|
|
</Space>
|
|||
|
|
),
|
|||
|
|
children: (
|
|||
|
|
<div style={{ padding: '16px 0' }}>
|
|||
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<CloudDownloadOutlined style={{ color: '#1890ff', fontSize: '20px' }} />
|
|||
|
|
<Text strong>服务器域名</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
<Input
|
|||
|
|
value={serverDomain}
|
|||
|
|
onChange={(e) => setServerDomain(e.target.value)}
|
|||
|
|
placeholder="例如: example.com 或 https://example.com"
|
|||
|
|
maxLength={100}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
suffix={
|
|||
|
|
<Tooltip title="清空使用默认域名">
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
size="small"
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={() => setServerDomain('')}
|
|||
|
|
style={{ border: 'none', padding: '0 4px' }}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
留空则使用默认域名: {window.location.hostname}:3001
|
|||
|
|
</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<FileOutlined style={{ color: '#1890ff', fontSize: '20px' }} />
|
|||
|
|
<Text strong>Web网址</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
<Input
|
|||
|
|
value={webUrl}
|
|||
|
|
onChange={(e) => setWebUrl(e.target.value)}
|
|||
|
|
placeholder="例如: https://example.com 或 http://192.168.1.100:8080"
|
|||
|
|
maxLength={200}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
suffix={
|
|||
|
|
<Tooltip title="清空使用默认网址">
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
size="small"
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={() => setWebUrl('')}
|
|||
|
|
style={{ border: 'none', padding: '0 4px' }}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
留空则使用默认网址: {window.location.origin}
|
|||
|
|
</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Alert
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
message="服务器配置说明"
|
|||
|
|
description={
|
|||
|
|
<div>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>服务器域名</strong>:输入完整的服务器域名,不需要带https,如果是IP 需要输入ws://ip:port
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>Web网址</strong>:Android应用启动时打开的网页地址 需要带https
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
style={{
|
|||
|
|
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)',
|
|||
|
|
border: '1px solid #91d5ff',
|
|||
|
|
borderRadius: '8px'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Space>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: '1',
|
|||
|
|
label: (
|
|||
|
|
<Space>
|
|||
|
|
<SettingOutlined style={{ color: '#667eea' }} />
|
|||
|
|
<Text strong style={{ color: '#2c3e50' }}>构建配置选项</Text>
|
|||
|
|
<Badge count={enableConfigMask ? '已启用' : '已禁用'}
|
|||
|
|
style={{ backgroundColor: enableConfigMask ? '#52c41a' : '#d9d9d9' }} />
|
|||
|
|
</Space>
|
|||
|
|
),
|
|||
|
|
children: (
|
|||
|
|
<div style={{ padding: '16px 0' }}>
|
|||
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|||
|
|
<Row gutter={[16, 16]} align="middle">
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<EyeInvisibleOutlined style={{ color: '#667eea' }} />
|
|||
|
|
<Text strong>配置期间遮盖</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
<Row align="middle" gutter={16}>
|
|||
|
|
<Col>
|
|||
|
|
<Switch
|
|||
|
|
checked={enableConfigMask}
|
|||
|
|
onChange={setEnableConfigMask}
|
|||
|
|
checkedChildren="开启"
|
|||
|
|
unCheckedChildren="关闭"
|
|||
|
|
// disabled={true}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
<Col>
|
|||
|
|
<Tooltip title="开启后,设备在配置权限期间会显示黑色遮盖,防止用户误操作">
|
|||
|
|
<Text type="secondary">
|
|||
|
|
开启无障碍服务后显示遮盖,防止误点击
|
|||
|
|
</Text>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
{enableConfigMask && (
|
|||
|
|
<Space direction="vertical" size={16} style={{ width: '100%', marginTop: 12 }}>
|
|||
|
|
{/* 进度条开关 */}
|
|||
|
|
<Row align="middle" gutter={16}>
|
|||
|
|
<Col>
|
|||
|
|
<Switch
|
|||
|
|
checked={enableProgressBar}
|
|||
|
|
onChange={setEnableProgressBar}
|
|||
|
|
checkedChildren="开启"
|
|||
|
|
unCheckedChildren="关闭"
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
<Col>
|
|||
|
|
<Tooltip title="显示配置进度条,让用户了解当前配置状态">
|
|||
|
|
<Text type="secondary">
|
|||
|
|
显示配置进度条
|
|||
|
|
</Text>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={16}>
|
|||
|
|
<Col xs={24} sm={6}>
|
|||
|
|
<Text strong>主标题文字:</Text>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={18}>
|
|||
|
|
<Input
|
|||
|
|
value={configMaskText}
|
|||
|
|
onChange={(e) => setConfigMaskText(e.target.value)}
|
|||
|
|
placeholder="请输入遮盖时显示的主标题文字"
|
|||
|
|
maxLength={50}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={16}>
|
|||
|
|
<Col xs={24} sm={6}>
|
|||
|
|
<Text strong>副标题文字:</Text>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={18}>
|
|||
|
|
<Input.TextArea
|
|||
|
|
value={configMaskSubtitle}
|
|||
|
|
onChange={(e) => setConfigMaskSubtitle(e.target.value)}
|
|||
|
|
placeholder="请输入遮盖时显示的副标题文字"
|
|||
|
|
rows={2}
|
|||
|
|
maxLength={100}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={16}>
|
|||
|
|
<Col xs={24} sm={6}>
|
|||
|
|
<Text strong>状态提示文字:</Text>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={18}>
|
|||
|
|
<Input
|
|||
|
|
value={configMaskStatus}
|
|||
|
|
onChange={(e) => setConfigMaskStatus(e.target.value)}
|
|||
|
|
placeholder="请输入遮盖时显示的状态提示文字"
|
|||
|
|
maxLength={80}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
</Space>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Alert
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
message="配置说明"
|
|||
|
|
description={
|
|||
|
|
<div>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>遮盖功能</strong>:开启后,设备在获取权限配置期间会显示黑色遮盖层,防止用户误操作
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>自动移除</strong>:所有权限配置完成后,遮盖会自动消失
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>主标题</strong>:遮盖界面顶部显示的大标题文字
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>副标题</strong>:主标题下方的详细说明文字,支持换行
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0 }}>
|
|||
|
|
• <strong>状态提示</strong>:界面底部显示的状态信息文字
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
style={{
|
|||
|
|
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)',
|
|||
|
|
border: '1px solid #91d5ff',
|
|||
|
|
borderRadius: '8px'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Space>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'page-style',
|
|||
|
|
label: (
|
|||
|
|
<Space>
|
|||
|
|
<SettingOutlined style={{ color: '#722ed1' }} />
|
|||
|
|
<Text strong>Android端页面样式</Text>
|
|||
|
|
</Space>
|
|||
|
|
),
|
|||
|
|
children: (
|
|||
|
|
<div style={{ padding: '16px 0' }}>
|
|||
|
|
<Space direction="vertical" size={24} style={{ width: '100%' }}>
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<PictureOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>应用图标</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|||
|
|
<Space align="start" size={16}>
|
|||
|
|
<Upload
|
|||
|
|
beforeUpload={handleIconUpload}
|
|||
|
|
showUploadList={false}
|
|||
|
|
accept="image/*"
|
|||
|
|
>
|
|||
|
|
<Button
|
|||
|
|
icon={<UploadOutlined />}
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
>
|
|||
|
|
上传图标
|
|||
|
|
</Button>
|
|||
|
|
</Upload>
|
|||
|
|
{pageStyleConfig.appIcon && (
|
|||
|
|
<Button
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={removeIcon}
|
|||
|
|
danger
|
|||
|
|
type="text"
|
|||
|
|
size="small"
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
>
|
|||
|
|
移除
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
{iconPreviewUrl ? (
|
|||
|
|
<div style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: '12px',
|
|||
|
|
padding: '12px',
|
|||
|
|
background: '#f8f9fa',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid #e9ecef'
|
|||
|
|
}}>
|
|||
|
|
<Image
|
|||
|
|
src={iconPreviewUrl}
|
|||
|
|
alt="图标预览"
|
|||
|
|
width={48}
|
|||
|
|
height={48}
|
|||
|
|
preview={false}
|
|||
|
|
style={{
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid #d1d5db'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<div>
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
{pageStyleConfig.appIcon?.name}
|
|||
|
|
</Text>
|
|||
|
|
<br />
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
{pageStyleConfig.appIcon && (pageStyleConfig.appIcon.size / 1024).toFixed(1)} KB
|
|||
|
|
</Text>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<Alert
|
|||
|
|
message="使用默认图标"
|
|||
|
|
description="未上传自定义图标时,将使用应用默认图标"
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
style={{
|
|||
|
|
fontSize: '12px',
|
|||
|
|
borderRadius: '8px'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<AndroidOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>应用名称</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Input
|
|||
|
|
value={pageStyleConfig.appName}
|
|||
|
|
onChange={(e) => setPageStyleConfig(prev => ({ ...prev, appName: e.target.value }))}
|
|||
|
|
placeholder="请输入应用名称"
|
|||
|
|
maxLength={30}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<FileOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>状态文本</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Input.TextArea
|
|||
|
|
value={pageStyleConfig.statusText}
|
|||
|
|
onChange={(e) => setPageStyleConfig(prev => ({ ...prev, statusText: e.target.value }))}
|
|||
|
|
placeholder="请输入主页状态文本内容"
|
|||
|
|
rows={4}
|
|||
|
|
maxLength={200}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<RocketOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>启用按钮文字</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Input
|
|||
|
|
value={pageStyleConfig.enableButtonText}
|
|||
|
|
onChange={(e) => setPageStyleConfig(prev => ({ ...prev, enableButtonText: e.target.value }))}
|
|||
|
|
placeholder="请输入启用按钮文字"
|
|||
|
|
maxLength={20}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<InfoCircleOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>使用说明</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Input.TextArea
|
|||
|
|
value={pageStyleConfig.usageInstructions}
|
|||
|
|
onChange={(e) => setPageStyleConfig(prev => ({ ...prev, usageInstructions: e.target.value }))}
|
|||
|
|
placeholder="请输入使用说明文本"
|
|||
|
|
rows={6}
|
|||
|
|
maxLength={500}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
<Col xs={24} sm={8}>
|
|||
|
|
<Space>
|
|||
|
|
<FileOutlined style={{ color: '#722ed1', fontSize: '20px' }} />
|
|||
|
|
<Text strong>APK文件名</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} sm={16}>
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
<Input
|
|||
|
|
value={pageStyleConfig.apkFileName}
|
|||
|
|
onChange={(e) => setPageStyleConfig(prev => ({ ...prev, apkFileName: e.target.value }))}
|
|||
|
|
placeholder={`默认: ${getDefaultAPKFileName()}`}
|
|||
|
|
maxLength={50}
|
|||
|
|
showCount
|
|||
|
|
style={{ borderRadius: '8px' }}
|
|||
|
|
suffix={
|
|||
|
|
<Tooltip title="清空使用默认名称">
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
size="small"
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={() => setPageStyleConfig(prev => ({ ...prev, apkFileName: '' }))}
|
|||
|
|
style={{ border: 'none', padding: '0 4px' }}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|||
|
|
留空则使用默认名称: {getDefaultAPKFileName()}.apk
|
|||
|
|
</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
<Alert
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
message="页面样式说明"
|
|||
|
|
description={
|
|||
|
|
<div>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>应用名称</strong>:将显示在Android应用主页的标题位置
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>状态文本</strong>:显示在页面中央的提示文字,支持换行符 \\n
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>按钮文字</strong>:可以自定义启用按钮的显示文字
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0, marginBottom: 8 }}>
|
|||
|
|
• <strong>使用说明</strong>:显示在页面底部的详细说明文字
|
|||
|
|
</p>
|
|||
|
|
<p style={{ margin: 0 }}>
|
|||
|
|
• <strong>APK文件名</strong>:自定义构建完成后的APK文件名,留空则使用默认名称
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
style={{
|
|||
|
|
background: 'linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%)',
|
|||
|
|
border: '1px solid #b7eb8f',
|
|||
|
|
borderRadius: '8px'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Space>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
]}
|
|||
|
|
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
|
|||
|
|
style={{
|
|||
|
|
background: 'transparent',
|
|||
|
|
border: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 操作按钮区域 */}
|
|||
|
|
<Card style={{
|
|||
|
|
textAlign: 'center',
|
|||
|
|
padding: '40px',
|
|||
|
|
borderRadius: '20px',
|
|||
|
|
background: 'linear-gradient(135deg, #fff 0%, #f8f9ff 100%)',
|
|||
|
|
boxShadow: '0 20px 40px rgba(0,0,0,0.08)',
|
|||
|
|
border: '1px solid rgba(102, 126, 234, 0.1)'
|
|||
|
|
}}>
|
|||
|
|
<Title level={3} style={{ marginBottom: 32, color: '#2c3e50' }}>
|
|||
|
|
🛠️ 操作中心
|
|||
|
|
</Title>
|
|||
|
|
|
|||
|
|
<Space size="large">
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
size="large"
|
|||
|
|
icon={<BuildOutlined />}
|
|||
|
|
onClick={buildAPK}
|
|||
|
|
loading={building}
|
|||
|
|
disabled={building}
|
|||
|
|
style={{
|
|||
|
|
minWidth: 160,
|
|||
|
|
height: '60px',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
fontWeight: 600,
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|||
|
|
border: 'none',
|
|||
|
|
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)',
|
|||
|
|
transition: 'all 0.3s ease'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{building ? '🔄 构建中...' : '🚀 开始构建'}
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
{data?.apkInfo?.exists && (
|
|||
|
|
<Space direction="vertical" size={8}>
|
|||
|
|
<Button
|
|||
|
|
size="large"
|
|||
|
|
icon={<DownloadOutlined />}
|
|||
|
|
onClick={downloadAPK}
|
|||
|
|
style={{
|
|||
|
|
minWidth: 160,
|
|||
|
|
height: '60px',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
fontWeight: 600,
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
background: 'linear-gradient(135deg, #48dbfb 0%, #0abde3 100%)',
|
|||
|
|
border: 'none',
|
|||
|
|
color: 'white',
|
|||
|
|
boxShadow: '0 8px 32px rgba(72, 219, 251, 0.3)',
|
|||
|
|
transition: 'all 0.3s ease'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
📥 下载 APK
|
|||
|
|
</Button>
|
|||
|
|
</Space>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* APK 分享管理器 */}
|
|||
|
|
<div style={{ marginTop: 24 }}>
|
|||
|
|
<APKShareManager
|
|||
|
|
serverUrl={serverUrl}
|
|||
|
|
onShareUrlGenerated={(url) => {
|
|||
|
|
setShareUrl(url)
|
|||
|
|
// 可以在这里添加额外的处理逻辑
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 构建日志窗口 */}
|
|||
|
|
<Modal
|
|||
|
|
title={
|
|||
|
|
<Space>
|
|||
|
|
<FileOutlined />
|
|||
|
|
<span>构建日志</span>
|
|||
|
|
{building && (
|
|||
|
|
<Badge status="processing" text="构建中..." />
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
}
|
|||
|
|
open={logModalVisible}
|
|||
|
|
onCancel={() => setLogModalVisible(false)}
|
|||
|
|
width={900}
|
|||
|
|
footer={[
|
|||
|
|
<Button key="refresh" icon={<SyncOutlined />} onClick={() => fetchBuildLogs(100)} loading={logLoading}>
|
|||
|
|
刷新
|
|||
|
|
</Button>,
|
|||
|
|
<Button key="clear" danger icon={<DeleteOutlined />} onClick={clearBuildLogs}>
|
|||
|
|
清空日志
|
|||
|
|
</Button>,
|
|||
|
|
<Button key="close" onClick={() => setLogModalVisible(false)}>
|
|||
|
|
关闭
|
|||
|
|
</Button>
|
|||
|
|
]}
|
|||
|
|
styles={{
|
|||
|
|
body: {
|
|||
|
|
padding: 0,
|
|||
|
|
maxHeight: '70vh',
|
|||
|
|
overflow: 'hidden'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
ref={logContainerRef}
|
|||
|
|
style={{
|
|||
|
|
background: '#1e1e1e',
|
|||
|
|
color: '#d4d4d4',
|
|||
|
|
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
|||
|
|
fontSize: '13px',
|
|||
|
|
lineHeight: '1.6',
|
|||
|
|
padding: '16px',
|
|||
|
|
height: '60vh',
|
|||
|
|
overflow: 'auto',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
position: 'relative'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{logLoading && buildLogs.length === 0 ? (
|
|||
|
|
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
|
|||
|
|
<Spin size="large" />
|
|||
|
|
<div style={{ marginTop: 16 }}>正在加载日志...</div>
|
|||
|
|
</div>
|
|||
|
|
) : buildLogs.length === 0 ? (
|
|||
|
|
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
|
|||
|
|
<FileOutlined style={{ fontSize: '48px', marginBottom: 16 }} />
|
|||
|
|
<div>暂无日志内容</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div style={{
|
|||
|
|
whiteSpace: 'pre-wrap',
|
|||
|
|
wordBreak: 'break-word'
|
|||
|
|
}}>
|
|||
|
|
{buildLogs.map((log, index) => {
|
|||
|
|
// 根据日志级别和内容设置颜色
|
|||
|
|
let logColor = '#d4d4d4'
|
|||
|
|
const message = log.message || ''
|
|||
|
|
|
|||
|
|
// 优先使用日志级别
|
|||
|
|
if (log.level === 'error' || log.level === 'ERROR') {
|
|||
|
|
logColor = '#f48771'
|
|||
|
|
} else if (log.level === 'warning' || log.level === 'WARNING' || log.level === 'warn') {
|
|||
|
|
logColor = '#dcdcaa'
|
|||
|
|
} else if (log.level === 'success' || log.level === 'SUCCESS') {
|
|||
|
|
logColor = '#4ec9b0'
|
|||
|
|
} else if (log.level === 'info' || log.level === 'INFO') {
|
|||
|
|
logColor = '#569cd6'
|
|||
|
|
} else {
|
|||
|
|
// 如果没有级别,根据消息内容判断
|
|||
|
|
if (message.includes('错误') || message.includes('error') || message.includes('Error') || message.includes('ERROR') || message.includes('失败') || message.includes('FAIL')) {
|
|||
|
|
logColor = '#f48771'
|
|||
|
|
} else if (message.includes('警告') || message.includes('warning') || message.includes('Warning') || message.includes('WARNING')) {
|
|||
|
|
logColor = '#dcdcaa'
|
|||
|
|
} else if (message.includes('成功') || message.includes('success') || message.includes('Success') || message.includes('SUCCESS') || message.includes('完成')) {
|
|||
|
|
logColor = '#4ec9b0'
|
|||
|
|
} else if (message.includes('信息') || message.includes('info') || message.includes('Info') || message.includes('INFO')) {
|
|||
|
|
logColor = '#569cd6'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={index}
|
|||
|
|
style={{
|
|||
|
|
color: logColor,
|
|||
|
|
marginBottom: '2px',
|
|||
|
|
padding: '2px 0',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'flex-start',
|
|||
|
|
lineHeight: '1.5'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{log.timeString && (
|
|||
|
|
<span style={{
|
|||
|
|
color: '#666',
|
|||
|
|
marginRight: '8px',
|
|||
|
|
fontSize: '11px',
|
|||
|
|
minWidth: '140px',
|
|||
|
|
flexShrink: 0,
|
|||
|
|
fontFamily: 'monospace'
|
|||
|
|
}}>
|
|||
|
|
{log.timeString}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
<span style={{
|
|||
|
|
color: '#888',
|
|||
|
|
marginRight: '8px',
|
|||
|
|
fontSize: '11px',
|
|||
|
|
minWidth: '70px',
|
|||
|
|
flexShrink: 0,
|
|||
|
|
fontWeight: 'bold'
|
|||
|
|
}}>
|
|||
|
|
{log.level ? `[${log.level.toUpperCase()}]` : '[INFO]'}
|
|||
|
|
</span>
|
|||
|
|
<span style={{ flex: 1, wordBreak: 'break-word' }}>
|
|||
|
|
{message}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
|
|||
|
|
<style>{`
|
|||
|
|
@keyframes spin {
|
|||
|
|
from { transform: rotate(0deg); }
|
|||
|
|
to { transform: rotate(360deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0% { transform: scale(1); opacity: 1; }
|
|||
|
|
50% { transform: scale(1.05); opacity: 0.8; }
|
|||
|
|
100% { transform: scale(1); opacity: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fadeIn {
|
|||
|
|
from { opacity: 0; transform: translateY(20px); }
|
|||
|
|
to { opacity: 1; transform: translateY(0); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hover-effect:hover {
|
|||
|
|
transform: translateY(-4px);
|
|||
|
|
box-shadow: 0 16px 64px rgba(0,0,0,0.15) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ant-card {
|
|||
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.ant-col {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mobile-stack {
|
|||
|
|
flex-direction: column !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mobile-center {
|
|||
|
|
text-align: center !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1200px) {
|
|||
|
|
.large-screen-spacing {
|
|||
|
|
padding: 16px !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
`}</style>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default APKManager
|