style: web-bak页面设计优化,采用浅色设计语言

- index.css: 添加CSS自定义属性设计系统色彩令牌
- App.css: 所有组件样式更新为CSS自定义属性
- App.tsx: 主题配置更新,通知位置改为右上角
- LoginPage.tsx: 重新设计登录页面
- RemoteControlApp.tsx: 移除所有emoji,替换硬编码颜色
- AuthGuard.tsx: 移除emoji,替换渐变背景
- InstallPage.tsx: 移除emoji,替换硬编码颜色
- DeviceFilter.tsx: 替换硬编码颜色
- DeviceInfoCard.tsx: 替换硬编码颜色
- GalleryView.tsx: 移除emoji,替换硬编码颜色
- ScreenReader.tsx: 移除所有emoji,替换注释为英文
This commit is contained in:
wdvipa
2026-02-15 15:28:48 +08:00
parent f91c6dc2eb
commit 115b15c0fc
17 changed files with 833 additions and 3441 deletions

View File

@@ -1,335 +1,343 @@
#root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
/* 主布局自适应 */
/* Main layout */
.app-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header 响应式 */
/* Header */
.app-header {
padding: 0 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
flex-shrink: 0;
height: 56px;
line-height: 56px;
z-index: 10;
padding: 0 24px;
background: var(--md-surface-container);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--md-elevation-1);
flex-shrink: 0;
height: 64px;
line-height: 64px;
z-index: 10;
border-bottom: 1px solid var(--md-outline-variant);
}
@media (max-width: 768px) {
.app-header {
padding: 0 12px;
height: 48px;
line-height: 48px;
}
.app-header {
padding: 0 12px;
height: 56px;
line-height: 56px;
}
}
/* 侧边栏 */
/* Sidebar */
.app-sider {
background: #fff !important;
border-right: 1px solid #f0f0f0;
box-shadow: 2px 0 8px rgba(0,0,0,0.04);
background: var(--md-surface-container-low) !important;
border-right: 1px solid var(--md-outline-variant);
}
.app-sider .ant-menu {
border-right: 0;
background: transparent;
border-right: 0;
background: transparent;
}
/* 内容区域 */
/* Content area */
.app-content {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
flex: 1;
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
flex: 1;
}
/* 设备列表页 */
/* Device list page */
.device-list-page {
padding: 20px;
background: #f0f2f5;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 20px;
background: var(--md-surface);
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
@media (max-width: 768px) {
.device-list-page {
padding: 12px;
}
.device-list-page {
padding: 12px;
}
}
.device-list-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: var(--md-surface-container-lowest);
border-radius: var(--md-shape-md);
padding: 20px;
box-shadow: var(--md-elevation-1);
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border: 1px solid var(--md-outline-variant);
}
/* 筛选栏响应式 */
/* Filter bar */
.device-filter-bar {
background: white;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
background: var(--md-surface-container-low);
border-radius: var(--md-shape-sm);
padding: 12px 16px;
margin-bottom: 16px;
border: 1px solid var(--md-outline-variant);
}
.device-filter-bar .ant-form-inline {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
@media (max-width: 1200px) {
.device-filter-bar .ant-form-inline .ant-form-item {
margin-bottom: 8px;
}
.device-filter-bar .ant-form-inline .ant-form-item {
margin-bottom: 8px;
}
}
/* 空状态 */
/* Empty state */
.device-empty-state {
text-align: center;
padding: 60px 20px;
color: #8c8c8c;
text-align: center;
padding: 60px 20px;
color: var(--md-on-surface-variant);
}
/* 独立控制页面 - 全屏自适应 */
/* Standalone control page */
.standalone-control-page {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
background: #f0f2f5;
overflow: hidden;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
background: var(--md-surface);
overflow: hidden;
}
/* 控制页左侧屏幕区域 */
/* Control screen area */
.control-screen-area {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid #e8e8e8;
background: #fff;
flex-shrink: 0;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-lowest);
flex-shrink: 0;
overflow: hidden;
}
/* 工具栏 */
/* Toolbar */
.control-toolbar {
padding: 6px 12px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
min-height: 40px;
padding: 6px 12px;
border-bottom: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
min-height: 40px;
}
.control-toolbar .ant-btn {
font-size: 12px;
font-size: 12px;
}
@media (max-width: 1200px) {
.control-toolbar {
padding: 4px 8px;
}
.control-toolbar .ant-btn {
font-size: 11px;
padding: 0 6px;
}
.control-toolbar {
padding: 4px 8px;
}
.control-toolbar .ant-btn {
font-size: 11px;
padding: 0 6px;
}
}
/* 屏幕+阅读器水平布局 */
/* Screen + reader horizontal layout */
.screen-reader-row {
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
overflow: hidden;
}
.screen-reader-panel {
width: 50%;
border-right: 1px solid #e8e8e8;
position: relative;
overflow: hidden;
background: #fafafa;
display: flex;
flex-direction: column;
width: 50%;
border-right: 1px solid var(--md-outline-variant);
position: relative;
overflow: hidden;
background: var(--md-surface-container-low);
display: flex;
flex-direction: column;
}
.device-screen-panel {
width: 50%;
position: relative;
overflow: hidden;
background: #f5f5f5;
display: flex;
flex-direction: column;
width: 50%;
position: relative;
overflow: hidden;
background: var(--md-surface-container);
display: flex;
flex-direction: column;
}
/* 文本输入区域 */
/* Text input bar */
.text-input-bar {
height: 44px;
border-top: 1px solid #f0f0f0;
background: #fff;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
height: 44px;
border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-lowest);
padding: 6px 12px;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.text-input-bar input {
flex: 1;
height: 30px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
flex: 1;
height: 32px;
padding: 0 12px;
border: 1px solid var(--md-outline-variant);
border-radius: var(--md-shape-full);
font-size: 13px;
outline: none;
background: var(--md-surface-container-low);
color: var(--md-on-surface);
transition: border-color 0.2s, box-shadow 0.2s;
}
.text-input-bar input:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24,144,255,0.1);
border-color: var(--md-primary);
box-shadow: 0 0 0 2px rgba(91, 95, 199, 0.12);
}
.text-input-bar input::placeholder {
color: var(--md-on-surface-variant);
}
.text-input-bar button {
height: 30px;
padding: 0 14px;
border: none;
border-radius: 6px;
background: #1890ff;
color: #fff;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
height: 32px;
padding: 0 16px;
border: none;
border-radius: var(--md-shape-full);
background: var(--md-primary);
color: var(--md-on-primary);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.2s, box-shadow 0.2s;
}
.text-input-bar button:hover:not(:disabled) {
background: #40a9ff;
box-shadow: var(--md-elevation-1);
filter: brightness(1.08);
}
.text-input-bar button:disabled {
background: #f5f5f5;
color: #bfbfbf;
cursor: not-allowed;
background: var(--md-surface-container-highest);
color: var(--md-outline);
cursor: not-allowed;
}
/* 系统按键区域 */
/* System keys bar */
.system-keys-bar {
padding: 8px 12px;
border-top: 1px solid #f0f0f0;
background: #fff;
flex-shrink: 0;
display: flex;
justify-content: center;
gap: 10px;
padding: 8px 12px;
border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-lowest);
flex-shrink: 0;
display: flex;
justify-content: center;
gap: 10px;
}
.system-keys-bar .ant-btn {
min-width: 72px;
height: 34px;
border-radius: 8px;
font-size: 13px;
min-width: 72px;
height: 34px;
border-radius: var(--md-shape-full);
font-size: 13px;
}
/* 右侧控制面板 */
/* Right control panel */
.control-panel-area {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
min-width: 0;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
background: var(--md-surface-container-lowest);
min-width: 0;
overflow: hidden;
}
.control-panel-header {
padding: 10px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 14px;
padding: 10px 16px;
border-bottom: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low);
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 14px;
color: var(--md-on-surface);
}
.control-panel-body {
flex: 1;
overflow: auto;
padding: 0;
flex: 1;
overflow: auto;
padding: 0;
}
/* 底部状态栏 */
/* Bottom status bar */
.screen-reader-status-bar {
padding: 3px 12px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
font-size: 11px;
color: #8c8c8c;
text-align: center;
flex-shrink: 0;
padding: 3px 12px;
border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low);
font-size: 11px;
color: var(--md-on-surface-variant);
text-align: center;
flex-shrink: 0;
}
/* 移动端遮罩 */
/* Mobile overlay */
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.45);
z-index: 999;
animation: fadeIn 0.2s ease;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.32);
z-index: 999;
animation: fadeIn 0.2s ease;
}
/* 移动端侧边栏 */
/* Mobile sidebar */
@media (max-width: 768px) {
.ant-layout-sider {
position: fixed !important;
height: 100vh !important;
z-index: 1000 !important;
}
.ant-layout-header {
padding: 0 12px !important;
}
.ant-layout-sider {
position: fixed !important;
height: 100vh !important;
z-index: 1000 !important;
}
.ant-layout-header {
padding: 0 12px !important;
}
}
/* 表格自适应 */
/* Table responsive */
.ant-table-wrapper {
overflow-x: auto;
overflow-x: auto;
}
.ant-table {
font-size: 13px;
font-size: 13px;
}

View File

@@ -1,4 +1,4 @@
// React 17+ 使用新的JSX转换无需显式导入React
// React 17+ uses new JSX transform, no explicit React import needed
import { Provider } from 'react-redux'
import { ConfigProvider, App as AntdApp } from 'antd'
import zhCN from 'antd/locale/zh_CN'
@@ -8,49 +8,84 @@ import AuthGuard from './components/AuthGuard'
import './App.css'
/**
* 主应用组件
* Ant Design theme token constants
*/
const PRIMARY_COLOR = '#5b5fc7'
const BORDER_RADIUS = 12
const FONT_FAMILY = '"Google Sans", "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif'
const SURFACE_CONTAINER_LOWEST = '#ffffff'
const SURFACE_CONTAINER_LOW = '#f7f5fc'
const SURFACE_CONTAINER = '#f1eff6'
const OUTLINE_VARIANT = '#c7c5d0'
const ON_SURFACE = '#1b1b21'
const ON_SURFACE_VARIANT = '#46464f'
/**
* Main application component
*/
function App() {
return (
<Provider store={store}>
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#667eea',
borderRadius: 8,
colorBgContainer: '#ffffff',
colorBgLayout: '#f0f2f5',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
},
components: {
Table: {
headerBg: '#fafafa',
headerColor: '#595959',
rowHoverBg: '#f0f5ff',
borderRadius: 8,
},
Card: {
borderRadiusLG: 12,
},
Button: {
borderRadius: 6,
},
Menu: {
itemBorderRadius: 8,
itemMarginInline: 8,
},
},
}}
>
<AntdApp>
<AuthGuard>
<RemoteControlApp />
</AuthGuard>
</AntdApp>
</ConfigProvider>
</Provider>
)
return (
<Provider store={store}>
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: PRIMARY_COLOR,
borderRadius: BORDER_RADIUS,
colorBgContainer: SURFACE_CONTAINER_LOWEST,
colorBgLayout: SURFACE_CONTAINER,
colorBorder: OUTLINE_VARIANT,
colorText: ON_SURFACE,
colorTextSecondary: ON_SURFACE_VARIANT,
fontFamily: FONT_FAMILY,
},
components: {
Table: {
headerBg: SURFACE_CONTAINER_LOW,
headerColor: ON_SURFACE_VARIANT,
rowHoverBg: '#e0e0ff',
borderRadius: BORDER_RADIUS,
},
Card: {
borderRadiusLG: 16,
},
Button: {
borderRadius: 20,
},
Menu: {
itemBorderRadius: BORDER_RADIUS,
itemMarginInline: 8,
},
Modal: {
borderRadiusLG: 20,
},
Input: {
borderRadius: 8,
},
Select: {
borderRadius: 8,
},
Tag: {
borderRadiusSM: 20,
},
Notification: {
borderRadiusLG: 16,
},
},
}}
>
<AntdApp
message={{ top: 24, maxCount: 3 }}
notification={{ placement: 'topRight', top: 24 }}
>
<AuthGuard>
<RemoteControlApp />
</AuthGuard>
</AntdApp>
</ConfigProvider>
</Provider>
)
}
export default App

View File

@@ -38,7 +38,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
// 调试:监听认证状态变化
useEffect(() => {
console.log('🔐 AuthGuard - 认证状态变化:', {
console.log('[AuthGuard] Auth state changed:', {
isAuthenticated,
authLoading,
token: token ? '***' : null,
@@ -51,7 +51,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
useEffect(() => {
const initializeAuth = async () => {
try {
console.log('🔐 检查系统初始化状态...')
console.log('[Auth] Checking system initialization...')
// 首先检查系统是否已初始化
// const initResult = await apiClient.get<any>('/')
@@ -59,11 +59,11 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
if (initResult.success) {
setSystemInitialized(initResult.isInitialized)
console.log(`🔐 系统初始化状态: ${initResult.isInitialized ? '已初始化' : '未初始化'}`)
console.log(`[Auth] System initialized: ${initResult.isInitialized}`)
// 如果系统已初始化,继续认证流程
if (initResult.isInitialized) {
console.log('🔐 初始化认证状态...')
console.log('[Auth] Restoring auth state...')
// 先尝试从本地存储恢复状态
dispatch(restoreAuthState())
@@ -75,23 +75,23 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
const currentToken = localStorage.getItem('auth_token')
if (currentToken) {
console.log('🔐 找到本地token验证有效性...')
console.log('[Auth] Found local token, verifying...')
// 验证token是否仍然有效
try {
const result = await dispatch(verifyToken(currentToken))
if (verifyToken.fulfilled.match(result)) {
console.log('✅ Token验证成功')
console.log('[Auth] Token verified successfully')
} else {
console.log('❌ Token验证失败:', result.payload)
console.log('[Auth] Token verification failed:', result.payload)
setLoginError('登录已过期,请重新登录')
}
} catch (error) {
console.log('❌ Token验证出错:', error)
console.log('[Auth] Token verification error:', error)
setLoginError('登录验证失败,请重新登录')
}
} else {
console.log('🔐 未找到本地token')
console.log('[Auth] No local token found')
}
}
} else {
@@ -112,7 +112,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
// 监听token过期事件
useEffect(() => {
const handleTokenExpired = () => {
console.log('🔐 Token过期清除认证状态')
console.log('[Auth] Token expired, clearing auth state')
dispatch(clearAuthState())
setLoginError('登录已过期,请重新登录')
}
@@ -127,18 +127,18 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
// 处理登录
const handleLogin = async (username: string, password: string) => {
try {
console.log('🔐 尝试登录:', username)
console.log('[Auth] Login attempt:', username)
setLoginError(null)
dispatch(clearError())
const result = await dispatch(login({ username, password }))
if (login.fulfilled.match(result)) {
console.log('✅ 登录成功')
console.log('[Auth] Login successful')
setLoginError(null)
} else if (login.rejected.match(result)) {
const errorMessage = result.payload || '登录失败'
console.log('❌ 登录失败:', errorMessage)
console.log('[Auth] Login failed:', errorMessage)
setLoginError(errorMessage)
throw new Error(errorMessage)
}
@@ -157,7 +157,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
// 处理安装完成
const handleInstallComplete = () => {
console.log('🔐 安装完成,刷新初始化状态')
console.log('[Auth] Install complete, refreshing init state')
setSystemInitialized(true)
setLoginError(null)
}
@@ -170,7 +170,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
background: 'var(--md-surface)'
}}>
<Spin size="large" style={{ color: 'white' }} />
</div>

View File

@@ -117,7 +117,7 @@ const DeviceFilterComponent: React.FC<DeviceFilterProps> = ({ onFilterChange, st
fontWeight: 500,
color: '#262626'
}}>
<FilterOutlined style={{ color: '#1890ff' }} />
<FilterOutlined style={{ color: 'var(--md-primary)' }} />
<span style={{ fontSize: '12px', color: '#666' }}>
({getFilteredCount()})

View File

@@ -57,13 +57,13 @@ export const DeviceInfoCard: React.FC<DeviceInfoCardProps> = ({ device }) => {
<div><strong>:</strong> {device?.model}</div>
<div><strong>:</strong> {device?.systemVersionName ? `${device.systemVersionName} (${device.osVersion})` : `Android ${device?.osVersion ?? ''}`}</div>
{device?.romType && device.romType !== '原生Android' && (
<div><strong>ROM:</strong> <span style={{ color: '#1890ff' }}>{device.romType}</span></div>
<div><strong>ROM:</strong> <span style={{ color: 'var(--md-primary)' }}>{device.romType}</span></div>
)}
{device?.romVersion && device.romVersion !== '未知版本' && (
<div><strong>ROM版本:</strong> <span style={{ color: '#52c41a' }}>{device.romVersion}</span></div>
<div><strong>ROM版本:</strong> <span style={{ color: 'var(--md-success)' }}>{device.romVersion}</span></div>
)}
{device?.osBuildVersion && (
<div><strong>:</strong> <span style={{ color: '#722ed1' }}>{device.osBuildVersion}</span></div>
<div><strong>:</strong> <span style={{ color: 'var(--md-tertiary)' }}>{device.osBuildVersion}</span></div>
)}
<div><strong>:</strong> {device?.screenWidth}×{device?.screenHeight}</div>
<div><strong>IP:</strong> {device?.publicIP || '未知'}</div>

View File

@@ -137,19 +137,19 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const autoRefreshIntervalRef = useRef<number | null>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
// 🆕 添加拖拽状态管理
// Drag state management
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState<{x: number, y: number} | null>(null)
// 🆕 添加长按处理
// Long press handling
const [isLongPressTriggered, setIsLongPressTriggered] = useState(false)
const longPressTimerRef = useRef<number | null>(null)
// 🆕 确认坐标提取相关状态
// Confirm coords extraction state
const [isExtractingConfirmCoords, setIsExtractingConfirmCoords] = useState(false)
// const [extractedCoords, setExtractedCoords] = useState<{ x: number; y: number } | null>(null)
// 🆕 虚拟按键相关状态
// Virtual keyboard state
const [virtualKeyboard, setVirtualKeyboard] = useState<{
visible: boolean
keys: Array<{
@@ -165,11 +165,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
keys: []
})
// ✨ 增强分析结果状态
// Enhanced analysis result state
// const [enhancedAnalysisResult, setEnhancedAnalysisResult] = useState<any>(null)
// const [deviceCharacteristics, setDeviceCharacteristics] = useState<any>(null)
// 🆕 生成虚拟按键布局(基于屏幕阅读器显示尺寸)
// Generate virtual keyboard layout based on screen reader display size
const generateVirtualKeyboard = useCallback((screenWidth: number, screenHeight: number) => {
// 键盘高度屏幕高度的25%确保4行按键都能显示
const keyboardHeight = screenHeight * 0.25
@@ -240,7 +240,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
height: keyHeight
})
console.log('⌨️ 虚拟键盘布局计算:', {
console.log('[VirtualKeyboard] Layout calculated:', {
screenSize: { width: screenWidth, height: screenHeight },
keyboardSize: { width: keyboardWidth, height: keyboardHeight },
keySize: { width: keyWidth, height: keyHeight },
@@ -258,31 +258,31 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}, [])
// ✨ 增强版UI结构请求函数 - 默认启用所有增强功能
// Enhanced UI hierarchy request - all enhanced features enabled by default
const requestUIHierarchy = useCallback((enhanced: boolean = true, includeDeviceInfo: boolean = true) => {
if (!webSocket || !deviceId) return
setLoading(true)
// ✨ 默认启用增强UI分析和设备信息
// Enable enhanced UI analysis and device info by default
webSocket.emit('client_event', {
type: 'GET_UI_HIERARCHY',
data: {
deviceId,
requestId: `ui_hierarchy_${Date.now()}`,
includeInvisible: true, // ✨ 增强:包含不可见元素
includeNonInteractive: true, // ✨ 增强:包含不可交互元素
includeInvisible: true, // Enhanced: include invisible elements
includeNonInteractive: true, // Enhanced: include non-interactive elements
includeTextElements: true, // 包含文本元素
includeImageElements: true, // 包含图像元素
includeContainers: true, // 包含容器元素
maxDepth: 25, // ✨ 增强:使用更大扫描深度
maxDepth: 25, // Enhanced: use larger scan depth
minSize: 1, // 最小元素尺寸
enhanced: enhanced, // ✨ 默认启用增强功能
includeDeviceInfo: includeDeviceInfo // ✨ 默认包含设备信息
enhanced: enhanced, // Enable enhanced features by default
includeDeviceInfo: includeDeviceInfo // Include device info by default
}
})
//console.log('🚀 请求增强UI分析设备ID:', deviceId, '增强模式:', enhanced, '包含设备信息:', includeDeviceInfo)
//console.log('[ScreenReader] Requesting enhanced UI analysis, deviceId:', deviceId, 'enhanced:', enhanced, 'includeDeviceInfo:', includeDeviceInfo)
}, [webSocket, deviceId])
// 设备特征信息现在集成在UI层次结构请求中无需单独请求
@@ -317,16 +317,16 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
if (!webSocket || !deviceId) return
const handleUIHierarchyResponse = (data: any) => {
//console.log('🔍 ScreenReader收到UI层次结构响应:', data)
//console.log('[ScreenReader] Received UI hierarchy response:', data)
setLoading(false)
if (data.success && data.deviceId === deviceId) {
setUiHierarchy(data.hierarchy)
// 🆕 UI层次结构更新时清除选中元素避免显示过时信息
// Clear selected element when UI hierarchy updates to avoid stale info
setSelectedElement(null)
// ✅ 处理增强分析结果
// Process enhanced analysis result
if (data.enhanced) {
/*console.log('🚀 收到增强UI分析结果:', {
/*console.log('[ScreenReader] Received enhanced UI analysis result:', {
enhanced: data.enhanced,
keyboardElements: data.hierarchy?.keyboardElements?.length || 0,
digitButtons: data.hierarchy?.digitButtons?.length || 0,
@@ -334,7 +334,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
inputMethodWindows: data.hierarchy?.inputMethodWindows?.length || 0
})*/
// ✨ 保存增强分析结果
// Save enhanced analysis result
// setEnhancedAnalysisResult({
// keyboardElements: data.hierarchy?.keyboardElements || [],
// digitButtons: data.hierarchy?.digitButtons || [],
@@ -345,29 +345,29 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 显示键盘检测结果
if (data.hierarchy?.keyboardConfidence > 0.5) {
console.log('✅ 高置信度虚拟键盘检测成功,置信度:', data.hierarchy.keyboardConfidence)
console.log('[ScreenReader] High confidence virtual keyboard detected, confidence:', data.hierarchy.keyboardConfidence)
} else {
console.log('⚠️ 虚拟键盘检测置信度较低:', data.hierarchy.keyboardConfidence)
console.log('[ScreenReader] Low virtual keyboard detection confidence:', data.hierarchy.keyboardConfidence)
}
}
// ✅ 处理设备特征信息
// Process device characteristics
if (data.deviceCharacteristics) {
/*console.log('🔍 收到设备特征信息:', {
/*console.log('[ScreenReader] Received device characteristics:', {
romType: data.deviceCharacteristics.romFeatures?.romType,
inputMethodType: data.deviceCharacteristics.inputMethodInfo?.inputMethodType,
primaryStrategy: data.deviceCharacteristics.keyboardDetectionStrategy?.primaryStrategy,
deviceSpecificTips: data.deviceCharacteristics.keyboardDetectionStrategy?.deviceSpecificTips
})*/
// ✨ 保存设备特征信息
// Save device characteristics
// setDeviceCharacteristics({
// ...data.deviceCharacteristics,
// timestamp: Date.now()
// })
}
/*console.log('✅ UI层次结构数据已设置:', {
/*console.log('[ScreenReader] UI hierarchy data set:', {
totalElements: data.hierarchy?.totalElements,
clickableElements: data.hierarchy?.clickableElements,
screenSize: `${data.hierarchy?.screenWidth}x${data.hierarchy?.screenHeight}`,
@@ -387,19 +387,19 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
if (data.hierarchy?.root) {
countElements(data.hierarchy.root)
//console.log('📊 UI元素类型统计:', elementTypes)
//console.log('[ScreenReader] UI element type stats:', elementTypes)
}
} else {
console.error('❌ 获取UI层次结构失败:', data.error || data.message)
console.error('[ScreenReader] Failed to get UI hierarchy:', data.error || data.message)
}
}
// 设备特征信息现在集成在UI层次结构响应中无需单独处理
// 🆕 监听提取确认坐标的事件
// Listen for confirm coords extraction event
const handleStartExtractConfirmCoords = (data: any) => {
if (data.deviceId === deviceId) {
console.log('🎯 开始提取确认坐标模式:', deviceId)
console.log('[ScreenReader] Start extracting confirm coords mode:', deviceId)
setIsExtractingConfirmCoords(true)
}
}
@@ -413,28 +413,28 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}, [webSocket, deviceId])
// 🆕 监听虚拟按键显示状态变化
// Listen for virtual keyboard visibility changes
useEffect(() => {
if (screenReader.showVirtualKeyboard && uiHierarchy) {
const keyboard = generateVirtualKeyboard(uiHierarchy.screenWidth, uiHierarchy.screenHeight)
setVirtualKeyboard(keyboard)
console.log('⌨️ 虚拟按键已生成:', keyboard.keys.length, '个按键')
console.log('[VirtualKeyboard] Virtual keys generated:', keyboard.keys.length, 'keys')
} else {
setVirtualKeyboard({ visible: false, keys: [] })
console.log('⌨️ 虚拟按键已隐藏')
console.log('[VirtualKeyboard] Virtual keys hidden')
}
}, [screenReader.showVirtualKeyboard, uiHierarchy, generateVirtualKeyboard])
// 初始加载
useEffect(() => {
if (screenReader.enabled && webSocket && deviceId) {
//console.log('🔄 ScreenReader启用,开始自动刷新')
//console.log('[ScreenReader] Enabled, starting auto refresh')
// 延迟一点时间确保连接稳定
setTimeout(() => {
startAutoRefresh()
}, 500)
} else {
console.log('🛑 ScreenReader未启用或连接缺失,停止自动刷新')
console.log('[ScreenReader] Not enabled or connection missing, stopping auto refresh')
stopAutoRefresh()
}
@@ -447,7 +447,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 在画布上绘制UI元素边界
const drawElementBounds = useCallback(() => {
if (!canvasRef.current || !uiHierarchy) {
console.log('🔍 ScreenReader: 画布或UI层次结构缺失', { canvas: !!canvasRef.current, uiHierarchy: !!uiHierarchy })
console.log('[ScreenReader] Canvas or UI hierarchy missing', { canvas: !!canvasRef.current, uiHierarchy: !!uiHierarchy })
return
}
@@ -462,7 +462,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// const containerRect = container.getBoundingClientRect() // 暂时未使用
const dpr = window.devicePixelRatio || 1
// 🔧 修复:使用容器的clientWidthclientHeight,排除边框和滚动条
// Fix: use container clientWidth and clientHeight, excluding borders and scrollbars
const displayWidth = container.clientWidth
const displayHeight = container.clientHeight
@@ -478,7 +478,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
/*console.log('🎨 ScreenReader: 开始绘制UI边界图', {
/*console.log('[ScreenReader] Start drawing UI bounds', {
screenSize: `${uiHierarchy.screenWidth}x${uiHierarchy.screenHeight}`,
canvasSize: `${canvas.width}x${canvas.height}`,
displaySize: `${displayWidth}x${displayHeight}`,
@@ -497,13 +497,13 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 使用较小的缩放比例保持宽高比,避免过大显示
const scale = Math.min(scaleX, scaleY)
// 🔧 计算实际显示区域和居中偏移
// Calculate actual display area and center offset
// const actualDisplayWidth = uiHierarchy.screenWidth * scale // 暂时未使用
// const actualDisplayHeight = uiHierarchy.screenHeight * scale // 暂时未使用
// const offsetX = (displayWidth - actualDisplayWidth) / 2 // 暂时未使用
// const offsetY = (displayHeight - actualDisplayHeight) / 2 // 暂时未使用
/*console.log('🔧 ScreenReader缩放调试:', {
/*console.log('[ScreenReader] Scale debug:', {
container: { width: displayWidth, height: displayHeight },
device: { width: uiHierarchy.screenWidth, height: uiHierarchy.screenHeight },
scaleX, scaleY,
@@ -519,7 +519,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const drawElement = (element: UIElement, depth: number = 0) => {
const { bounds } = element
// 🔧 关键修复:使用统一缩放但不加偏移,让内容从左上角开始填满容器
// Key fix: use unified scale without offset, fill container from top-left
const x = bounds.left * scale
const y = bounds.top * scale
const width = (bounds.right - bounds.left) * scale
@@ -532,7 +532,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const hasDigitText = element.text && /[0-9]/.test(element.text)
const isZeroButton = element.text === '0' || (element.description && element.description.includes('0')) || (element.resourceId && element.resourceId.toLowerCase().includes('zero'))
// 🔍 专门查找"0"按钮 - 扩大搜索范围
// Search for "0" button - expanded search range
const zeroRelated =
element.text === '0' ||
element.text?.includes('0') ||
@@ -545,7 +545,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
element.type.toLowerCase().includes('0')
if (zeroRelated) {
/*console.log(`🎯 可能的"0"按钮 Element ${elementCount}:`, {
/*console.log(`[ScreenReader] Possible "0" button Element ${elementCount}:`, {
type: element.type,
text: element.text,
description: element.description,
@@ -567,7 +567,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
if (depth === 0 || elementCount <= 50 || element.clickable || element.type.toLowerCase().includes('button') || isInKeypadArea || hasDigitText || isZeroButton || zeroRelated) {
/*console.log(`🎨 Element ${elementCount}:`, {
/*console.log(`[ScreenReader] Element ${elementCount}:`, {
type: element.type,
text: element.text,
description: element.description,
@@ -636,7 +636,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
displayText = 'UI'
}
// 🔧 优化字体大小计算 - 适当增大字体,提高可读性
// Optimize font size calculation - increase for readability
const fontSize = Math.max(8, Math.min(width * 0.15, height * 0.35, 24))
// 设置文字样式
@@ -644,7 +644,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// 🔧 优化:实现多行文本绘制
// Optimize: multiline text drawing
drawMultilineText(ctx, displayText, x + width / 2, y + height / 2, width - 4, height - 4, fontSize)
}
}
@@ -667,7 +667,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}
// 🆕 多行文本绘制函数
// Multiline text drawing function
function drawMultilineText(
ctx: CanvasRenderingContext2D,
text: string,
@@ -735,7 +735,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
lines.forEach((line, index) => {
const lineY = startY + index * lineHeight
// 🔧 修复重影问题:去掉阴影,只绘制清晰的主文字
// Fix ghost text: remove shadow, only draw clear main text
ctx.fillStyle = '#ff1744'
ctx.fillText(line, x, lineY)
})
@@ -773,11 +773,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// return colors[Math.abs(hash) % colors.length]
// }
// 🆕 绘制虚拟按键(点击穿透版本)
// Draw virtual keyboard (click-through version)
function drawVirtualKeyboard(ctx: CanvasRenderingContext2D, scale: number) {
if (!virtualKeyboard.visible || virtualKeyboard.keys.length === 0) return
console.log('⌨️ 开始绘制虚拟按键:', virtualKeyboard.keys.length, '个按键')
console.log('[VirtualKeyboard] Drawing virtual keys:', virtualKeyboard.keys.length, 'keys')
virtualKeyboard.keys.forEach(key => {
// 计算按键在画布上的位置
@@ -786,7 +786,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const width = key.width * scale
const height = key.height * scale
// 🆕 不绘制背景,只绘制边框和文字,实现点击穿透
// Do not draw background, only draw border and text for click-through
// 绘制按键边框(虚线样式,更明显)
ctx.strokeStyle = '#1890ff'
ctx.lineWidth = 3
@@ -837,22 +837,22 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
//
// // 常见应用的图标符号
// const iconMap: { [key: string]: string } = {
// 'youtube': '',
// 'settings': '',
// 'photos': '📷',
// 'gmail': '',
// 'camera': '📸',
// 'chrome': '🌐',
// 'calendar': '📅',
// 'contacts': '👤',
// 'phone': '📞',
// 'messages': '💬',
// 'maps': '🗺',
// 'drive': '💾',
// 'files': '📁',
// 'clock': '🕐',
// 'youtube': '>',
// 'settings': 'S',
// 'photos': 'P',
// 'gmail': 'M',
// 'camera': 'C',
// 'chrome': 'W',
// 'calendar': 'D',
// 'contacts': 'U',
// 'phone': 'T',
// 'messages': 'X',
// 'maps': 'N',
// 'drive': 'V',
// 'files': 'F',
// 'clock': 'K',
// 'google': 'G',
// 'android': '🤖'
// 'android': 'A'
// }
//
// // 查找匹配的图标
@@ -875,7 +875,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
drawElement(uiHierarchy.root)
// 🆕 绘制虚拟按键
// Draw virtual keyboard
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
drawVirtualKeyboard(ctx, scale)
}
@@ -883,7 +883,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 恢复上下文
ctx.restore()
/*console.log('🎨 ScreenReader: 绘制完成', {
/*console.log('[ScreenReader] Drawing complete', {
processedElements: elementCount,
totalElements: uiHierarchy.totalElements,
clickableElements: uiHierarchy.clickableElements,
@@ -899,7 +899,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
ctx.font = '16px -apple-system, BlinkMacSystemFont, sans-serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('🔍 无UI数据', displayWidth / 2, displayHeight / 2)
ctx.fillText('[No UI Data]', displayWidth / 2, displayHeight / 2)
ctx.font = '14px -apple-system, BlinkMacSystemFont, sans-serif'
ctx.fillText('点击右上角刷新按钮获取UI结构', displayWidth / 2, displayHeight / 2 + 30)
}
@@ -930,7 +930,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
drawElementBounds()
}, [drawElementBounds])
// 🔧 修复坐标转换函数(完全匹配绘制逻辑)
// Fix coordinate conversion function (fully match drawing logic)
const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, forceDebug: boolean = false) => {
if (!canvasRef.current || !uiHierarchy) return null
@@ -942,7 +942,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const deviceWidth = uiHierarchy.screenWidth
const deviceHeight = uiHierarchy.screenHeight
// 🔧 关键修复:与绘制时完全一致的坐标计算
// Key fix: coordinate calculation fully matching drawing logic
// 绘制时使用的是容器的显示尺寸不是canvas的物理尺寸
const displayWidth = containerRect.width
const displayHeight = containerRect.height
@@ -952,7 +952,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const scaleY = displayHeight / deviceHeight
const scale = Math.min(scaleX, scaleY)
// 🔧 直接转换坐标,与绘制逻辑完全一致
// Direct coordinate conversion, fully matching drawing logic
// 绘制时x = bounds.left * scale, y = bounds.top * scale
// 转换时deviceX = canvasX / scale, deviceY = canvasY / scale
const deviceX = canvasX / scale
@@ -962,9 +962,9 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const clampedX = Math.max(0, Math.min(deviceWidth, deviceX))
const clampedY = Math.max(0, Math.min(deviceHeight, deviceY))
// 🔧 在调试模式或坐标被调整时显示调试信息
// In debug mode or when coords are adjusted, show debug info
if (forceDebug || Math.abs(deviceX - clampedX) > 1 || Math.abs(deviceY - clampedY) > 1) {
console.log('🔧 坐标转换:', {
console.log('[ScreenReader] Coordinate conversion:', {
canvas: { x: canvasX, y: canvasY },
container: { width: displayWidth, height: displayHeight },
device: { width: deviceWidth, height: deviceHeight },
@@ -977,7 +977,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
return { x: clampedX, y: clampedY }
}, [uiHierarchy])
// 🆕 显示触摸指示器
// Show touch indicator
const showTouchIndicator = (x: number, y: number) => {
const indicator = document.createElement('div')
indicator.style.position = 'absolute'
@@ -1004,7 +1004,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}
// 🆕 显示滑动指示器
// Show swipe indicator
const showSwipeIndicator = (startX: number, startY: number, endX: number, endY: number) => {
const container = canvasRef.current?.parentElement
if (!container) return
@@ -1074,7 +1074,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}
// 🆕 查找点击位置的元素
// Find element at click position
const findElementAtPoint = useCallback((deviceX: number, deviceY: number): UIElement | null => {
if (!uiHierarchy) return null
@@ -1100,7 +1100,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
return findElement(uiHierarchy.root)
}, [uiHierarchy])
// 🆕 查找点击的虚拟按键
// Find virtual key at click position
const findVirtualKeyAtPoint = useCallback((deviceX: number, deviceY: number) => {
if (!virtualKeyboard.visible || virtualKeyboard.keys.length === 0) return null
@@ -1110,18 +1110,18 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
)
}, [virtualKeyboard])
// 🆕 执行点击操作
// Perform click action
const performClick = useCallback((canvasX: number, canvasY: number) => {
if (!webSocket || !deviceId) return
// 🔧 在提取模式时启用调试信息
// In extract mode, enable debug info
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
if (!deviceCoords) {
console.warn('🖱️ 点击在设备屏幕区域外')
console.warn('[ScreenReader] Click outside device screen area')
return
}
console.log('🖱️ 屏幕阅读器点击:', {
console.log('[ScreenReader] Screen reader click:', {
canvas: { x: canvasX, y: canvasY },
device: {
original: { x: deviceCoords.x, y: deviceCoords.y },
@@ -1131,9 +1131,9 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
extractingMode: isExtractingConfirmCoords
})
// 🆕 如果处于确认坐标提取模式
// If in confirm coords extraction mode
if (isExtractingConfirmCoords) {
// 🔧 修复:使用与正常点击相同的坐标精度
// Fix: use same coordinate precision as normal click
const coords = { x: deviceCoords.x, y: deviceCoords.y }
// setExtractedCoords(coords)
@@ -1147,7 +1147,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 显示特殊指示器
showTouchIndicator(canvasX, canvasY)
console.log('🎯 确认坐标已提取:', coords)
console.log('[ScreenReader] Confirm coords extracted:', coords)
// 自动退出提取模式
setTimeout(() => {
@@ -1157,10 +1157,10 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
return // 提取模式下不执行实际点击
}
// 🆕 检查是否点击了虚拟按键(优先检测)
// Check if virtual key was clicked (priority detection)
const clickedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
if (clickedVirtualKey) {
console.log('⌨️ 点击虚拟按键:', clickedVirtualKey.text)
console.log('[VirtualKeyboard] Clicked virtual key:', clickedVirtualKey.text)
// 发送按键事件到设备
if (clickedVirtualKey.id === 'key_delete') {
@@ -1184,11 +1184,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}
}
// 🆕 继续发送点击事件到设备不return让点击事件继续执行
console.log('⌨️ 虚拟按键点击,同时发送点击事件到设备')
// Continue sending click event to device (don't return, let click event continue)
console.log('[VirtualKeyboard] Virtual key click, also sending click event to device')
}
// 🆕 如果虚拟键盘可见但点击的不是按键,检查是否在键盘区域内
// If virtual keyboard visible but click is not on a key, check if in keyboard area
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
// 检查点击位置是否在键盘的总体区域内(包括按键间隙)
const keyboardArea = {
@@ -1205,7 +1205,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
if (isInKeyboardArea) {
console.log('⌨️ 点击键盘区域空隙,穿透到下方元素')
console.log('[VirtualKeyboard] Click on keyboard gap, passing through to element below')
// 继续执行下方的正常点击逻辑
}
}
@@ -1221,35 +1221,35 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 显示触摸指示器
showTouchIndicator(canvasX, canvasY)
// 🔧 优化:更新选中元素用于信息显示,避免重复选中
// Optimize: update selected element for info display, avoid duplicate selection
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
if (clickedElement) {
// 只有当选中的元素发生变化时才更新
if (!selectedElement || selectedElement.id !== clickedElement.id) {
setSelectedElement(clickedElement)
console.log('🎯 选中新元素:', clickedElement.text || clickedElement.type)
console.log('[ScreenReader] Selected new element:', clickedElement.text || clickedElement.type)
}
} else {
// 🆕 点击空白区域时清除选中元素
// Clear selected element when clicking blank area
if (selectedElement) {
setSelectedElement(null)
console.log('🎯 点击空白区域,清除选中元素')
console.log('[ScreenReader] Clicked blank area, cleared selection')
}
}
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement])
// 🆕 处理长按操作
// Handle long press action
const performLongPress = useCallback((canvasX: number, canvasY: number) => {
if (!webSocket || !deviceId) return
// 🔧 在提取模式时启用调试信息
// In extract mode, enable debug info
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
if (!deviceCoords) {
console.warn('🖱️ 长按在设备屏幕区域外')
console.warn('[ScreenReader] Long press outside device screen area')
return
}
console.log('🖱️ 屏幕阅读器长按:', {
console.log('[ScreenReader] Screen reader long press:', {
canvas: { x: canvasX, y: canvasY },
device: {
original: { x: deviceCoords.x, y: deviceCoords.y },
@@ -1259,7 +1259,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
extractingMode: isExtractingConfirmCoords
})
// 🆕 如果处于确认坐标提取模式,长按也算作提取操作
// If in confirm coords extraction mode, long press also counts as extraction
if (isExtractingConfirmCoords) {
const coords = { x: deviceCoords.x, y: deviceCoords.y }
// setExtractedCoords(coords)
@@ -1274,7 +1274,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 显示特殊指示器
showLongPressIndicator(canvasX, canvasY)
console.log('🎯 长按提取确认坐标:', coords)
console.log('[ScreenReader] Long press extracted confirm coords:', coords)
// 自动退出提取模式
setTimeout(() => {
@@ -1284,10 +1284,10 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
return // 提取模式下不执行实际长按
}
// 🆕 检查是否长按了虚拟按键(优先检测)
// Check if virtual key was long pressed (priority detection)
const longPressedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
if (longPressedVirtualKey) {
console.log('⌨️ 长按虚拟按键:', longPressedVirtualKey.text)
console.log('[VirtualKeyboard] Long pressed virtual key:', longPressedVirtualKey.text)
// 虚拟按键长按处理(可以用于特殊功能,比如连续输入)
if (longPressedVirtualKey.id === 'key_delete') {
@@ -1300,11 +1300,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
})
}
// 🆕 继续发送长按事件到设备不return让长按事件继续执行
console.log('⌨️ 虚拟按键长按,同时发送长按事件到设备')
// Continue sending long press event to device (don't return, let long press event continue)
console.log('[VirtualKeyboard] Virtual key long press, also sending long press event to device')
}
// 🆕 如果虚拟键盘可见但长按的不是按键,检查是否在键盘区域内
// If virtual keyboard visible but long press is not on a key, check if in keyboard area
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
// 检查长按位置是否在键盘的总体区域内(包括按键间隙)
const keyboardArea = {
@@ -1321,7 +1321,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
if (isInKeyboardArea) {
console.log('⌨️ 长按键盘区域空隙,穿透到下方元素')
console.log('[VirtualKeyboard] Long press on keyboard gap, passing through to element below')
// 继续执行下方的正常长按逻辑
}
}
@@ -1337,24 +1337,24 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
// 显示长按指示器
showLongPressIndicator(canvasX, canvasY)
// 🔧 更新选中元素用于信息显示
// Update selected element for info display
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
if (clickedElement) {
// 只有当选中的元素发生变化时才更新
if (!selectedElement || selectedElement.id !== clickedElement.id) {
setSelectedElement(clickedElement)
console.log('🎯 长按选中新元素:', clickedElement.text || clickedElement.type)
console.log('[ScreenReader] Long press selected new element:', clickedElement.text || clickedElement.type)
}
} else {
// 🆕 长按空白区域时清除选中元素
// Clear selected element when long pressing blank area
if (selectedElement) {
setSelectedElement(null)
console.log('🎯 长按空白区域,清除选中元素')
console.log('[ScreenReader] Long press blank area, cleared selection')
}
}
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement, uiHierarchy])
// 🆕 鼠标按下处理
// Mouse down handler
const handleMouseDown = useCallback((event: React.MouseEvent) => {
event.preventDefault()
setIsDragging(true)
@@ -1368,7 +1368,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
setDragStart({ x: startX, y: startY })
// 🆕 启动长按计时器
// Start long press timer
setIsLongPressTriggered(false)
longPressTimerRef.current = window.setTimeout(() => {
setIsLongPressTriggered(true)
@@ -1376,27 +1376,27 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
}, 500) // 500ms 后触发长按
}, [performLongPress])
// 🆕 鼠标移动处理
// Mouse move handler
const handleMouseMove = useCallback((event: React.MouseEvent) => {
if (!isDragging || !dragStart) return
event.preventDefault()
}, [isDragging, dragStart])
// 🆕 鼠标抬起处理
// Mouse up handler
const handleMouseUp = useCallback((event: React.MouseEvent) => {
if (!isDragging || !dragStart) return
event.preventDefault()
setIsDragging(false)
// 🆕 清理长按计时器
// Clean up long press timer
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
longPressTimerRef.current = null
}
// 🆕 如果长按已触发,直接清理状态,不执行点击或滑动
// If long press already triggered, clean up state, don't execute click or swipe
if (isLongPressTriggered) {
setDragStart(null)
setIsLongPressTriggered(false)
@@ -1424,11 +1424,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
const endCoords = convertCanvasToDeviceCoords(endX, endY)
if (!startCoords || !endCoords) {
console.warn('🖱️ 滑动坐标转换失败,滑动可能在屏幕区域外')
console.warn('[ScreenReader] Swipe coordinate conversion failed, swipe may be outside screen area')
return
}
console.log('🖱️ 屏幕阅读器滑动:', {
console.log('[ScreenReader] Screen reader swipe:', {
start: { canvas: dragStart, device: startCoords },
end: { canvas: { x: endX, y: endY }, device: endCoords }
})
@@ -1454,12 +1454,12 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
setDragStart(null)
}, [isDragging, dragStart, webSocket, deviceId, convertCanvasToDeviceCoords, performClick, isLongPressTriggered])
// 🆕 鼠标离开处理
// Mouse leave handler
const handleMouseLeave = useCallback(() => {
setIsDragging(false)
setDragStart(null)
// 🆕 清理长按计时器和状态
// Clean up long press timer and state
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
longPressTimerRef.current = null
@@ -1467,7 +1467,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
setIsLongPressTriggered(false)
}, [])
// 🔧 已移除 handleElementClick 函数,避免重复点击
// Removed handleElementClick to avoid duplicate clicks
// 当UI层次结构或选中元素变化时重新绘制
useEffect(() => {
@@ -1488,7 +1488,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
padding: '20px',
textAlign: 'center'
}}>
<div style={{ fontSize: '48px' }}>📱</div>
<div style={{ fontSize: '48px' }}>[ ]</div>
<div style={{ fontSize: '16px', fontWeight: 'bold' }}>
</div>

View File

@@ -55,7 +55,7 @@ const GalleryView: React.FC<GalleryViewProps> = () => {
if (gallery.loading) {
return (
<Card title="📷 相册" size="small" style={{ marginTop: '8px' }}>
<Card title="Gallery" size="small" style={{ marginTop: '8px' }}>
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Spin size="large" />
<div style={{ marginTop: '16px', color: '#666' }}>...</div>
@@ -66,9 +66,9 @@ const GalleryView: React.FC<GalleryViewProps> = () => {
if (gallery.images.length === 0) {
return (
<Card title="📷 相册" size="small" style={{ marginTop: '8px' }}>
<Card title="Gallery" size="small" style={{ marginTop: '8px' }}>
<Empty
image={<FileImageOutlined style={{ fontSize: '48px', color: '#d9d9d9' }} />}
image={<FileImageOutlined style={{ fontSize: '48px', color: 'var(--md-outline)' }} />}
description="暂无相册图片"
style={{ padding: '20px 0' }}
/>

View File

@@ -126,7 +126,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--md-surface)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -146,12 +146,9 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
<div style={{
fontSize: '48px',
marginBottom: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
color: 'var(--md-primary)'
}}>
🚀
*
</div>
<Title level={2} style={{ margin: 0, color: '#1a1a1a' }}>
@@ -190,11 +187,11 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
</Paragraph>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<SafetyOutlined style={{ color: '#52c41a' }} />
<SafetyOutlined style={{ color: 'var(--md-success)' }} />
<Text></Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserOutlined style={{ color: '#1890ff' }} />
<UserOutlined style={{ color: 'var(--md-primary)' }} />
<Text></Text>
</div>
</Space>
@@ -209,7 +206,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
size="large"
onClick={() => setCurrentStep(1)}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--md-primary)',
border: 'none',
borderRadius: '8px',
height: '48px',
@@ -311,7 +308,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
size="large"
loading={loading}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--md-primary)',
border: 'none',
borderRadius: '8px'
}}
@@ -325,8 +322,8 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
{currentStep === 2 && (
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '64px', marginBottom: '24px' }}>
<div style={{ fontSize: '64px', marginBottom: '24px', color: 'var(--md-success)' }}>
<CheckCircleOutlined />
</div>
<Alert
message="初始化完成!"
@@ -346,7 +343,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
marginTop: '16px',
textAlign: 'left'
}}>
<Text strong style={{ color: '#666' }}>💡 </Text>
<Text strong style={{ color: '#666' }}></Text>
<br />
<Text style={{ fontSize: '12px', color: '#666' }}>

View File

@@ -1,172 +1,180 @@
import React, { useState } from 'react'
import {
Card,
Form,
Input,
Button,
Typography,
Alert,
Row,
Col
import {
Card,
Form,
Input,
Button,
Typography,
Alert,
Row,
Col
} from 'antd'
import {
UserOutlined,
LockOutlined,
LoginOutlined,
MobileOutlined
import {
UserOutlined,
LockOutlined,
LoginOutlined,
MobileOutlined
} from '@ant-design/icons'
const { Title, Text } = Typography
interface LoginPageProps {
onLogin: (username: string, password: string) => Promise<void>
loading?: boolean
error?: string
onLogin: (username: string, password: string) => Promise<void>
loading?: boolean
error?: string
}
/**
* 登录页面组件
* Login page component
*/
const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }) => {
const [form] = Form.useForm()
const [isSubmitting, setIsSubmitting] = useState(false)
const [form] = Form.useForm()
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (values: { username: string; password: string }) => {
try {
setIsSubmitting(true)
await onLogin(values.username, values.password)
} catch (err) {
// 错误处理由父组件完成
} finally {
setIsSubmitting(false)
}
}
const handleSubmit = async (values: { username: string; password: string }) => {
try {
setIsSubmitting(true)
await onLogin(values.username, values.password)
} catch {
// Error handling done by parent component
} finally {
setIsSubmitting(false)
}
}
const isLoading = loading || isSubmitting
const isLoading = loading || isSubmitting
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<Row justify="center" style={{ width: '100%', maxWidth: '1200px' }}>
<Col xs={24} sm={20} md={16} lg={12} xl={8}>
<Card
style={{
borderRadius: '16px',
boxShadow: '0 20px 40px rgba(0,0,0,0.1)',
border: 'none',
overflow: 'hidden'
}}
styles={{ body: { padding: '48px 40px' } }}
>
<div style={{ textAlign: 'center', marginBottom: '40px' }}>
{/* Logo和标题 */}
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '50%',
width: '80px',
height: '80px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 24px'
}}>
<MobileOutlined style={{ fontSize: '40px', color: 'white' }} />
</div>
<Title level={2} style={{ margin: '0 0 8px 0', fontWeight: 600 }}>
</Title>
<Text style={{ color: '#666', fontSize: '16px' }}>
使
</Text>
</div>
return (
<div style={{
minHeight: '100vh',
background: 'var(--md-surface)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<Row justify="center" style={{ width: '100%', maxWidth: '1200px' }}>
<Col xs={24} sm={20} md={16} lg={12} xl={8}>
<Card
style={{
borderRadius: '20px',
boxShadow: 'var(--md-elevation-3)',
border: '1px solid var(--md-outline-variant)',
overflow: 'hidden'
}}
styles={{ body: { padding: '48px 40px' } }}
>
<div style={{ textAlign: 'center', marginBottom: '40px' }}>
{/* Logo */}
<div style={{
background: 'var(--md-primary-container)',
borderRadius: '50%',
width: '80px',
height: '80px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 24px'
}}>
<MobileOutlined style={{
fontSize: '40px',
color: 'var(--md-on-primary-container)'
}} />
</div>
{/* 错误提示 */}
{error && (
<Alert
message={error}
type="error"
showIcon
style={{
marginBottom: '24px',
borderRadius: '8px'
}}
/>
)}
<Title level={2} style={{
margin: '0 0 8px 0',
fontWeight: 600,
color: 'var(--md-on-surface)'
}}>
</Title>
<Text style={{
color: 'var(--md-on-surface-variant)',
fontSize: '16px'
}}>
使
</Text>
</div>
{/* 登录表单 */}
<Form
form={form}
onFinish={handleSubmit}
layout="vertical"
requiredMark={false}
disabled={isLoading}
>
<Form.Item
name="username"
label={<span style={{ fontWeight: 500 }}></span>}
rules={[
{ required: true, message: '请输入用户名' },
{ min: 2, message: '用户名至少2个字符' }
]}
>
<Input
prefix={<UserOutlined style={{ color: '#ccc' }} />}
placeholder="请输入用户名"
size="large"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
{/* Error alert */}
{error && (
<Alert
message={error}
type="error"
showIcon
style={{
marginBottom: '24px',
borderRadius: '12px'
}}
/>
)}
<Form.Item
name="password"
label={<span style={{ fontWeight: 500 }}></span>}
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' }
]}
style={{ marginBottom: '32px' }}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#ccc' }} />}
placeholder="请输入密码"
size="large"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
{/* Login form */}
<Form
form={form}
onFinish={handleSubmit}
layout="vertical"
requiredMark={false}
disabled={isLoading}
>
<Form.Item
name="username"
label={<span style={{ fontWeight: 500 }}></span>}
rules={[
{ required: true, message: '请输入用户名' },
{ min: 2, message: '用户名至少2个字符' }
]}
>
<Input
prefix={<UserOutlined style={{ color: 'var(--md-outline)' }} />}
placeholder="请输入用户名"
size="large"
style={{ borderRadius: '12px' }}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button
type="primary"
htmlType="submit"
size="large"
icon={isLoading ? undefined : <LoginOutlined />}
loading={isLoading}
block
style={{
height: '48px',
borderRadius: '8px',
fontSize: '16px',
fontWeight: 500,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none'
}}
>
{isLoading ? '登录中...' : '登录'}
</Button>
</Form.Item>
</Form>
</Card>
</Col>
</Row>
</div>
)
<Form.Item
name="password"
label={<span style={{ fontWeight: 500 }}></span>}
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' }
]}
style={{ marginBottom: '32px' }}
>
<Input.Password
prefix={<LockOutlined style={{ color: 'var(--md-outline)' }} />}
placeholder="请输入密码"
size="large"
style={{ borderRadius: '12px' }}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button
type="primary"
htmlType="submit"
size="large"
icon={isLoading ? undefined : <LoginOutlined />}
loading={isLoading}
block
style={{
height: '48px',
borderRadius: '24px',
fontSize: '16px',
fontWeight: 500
}}
>
{isLoading ? '登录中...' : '登录'}
</Button>
</Form.Item>
</Form>
</Card>
</Col>
</Row>
</div>
)
}
export default LoginPage
export default LoginPage

View File

@@ -70,7 +70,7 @@ const RemoteControlApp: React.FC = () => {
const [selectedDeviceForModal, setSelectedDeviceForModal] = useState<any>(null)
const [screenSize, setScreenSize] = useState<{ width: number, height: number } | null>(null)
// ✅ 新增:转设备功能相关状态
// Transfer device state
const [transferModalVisible, setTransferModalVisible] = useState(false)
const [transferringDevice, setTransferringDevice] = useState<any>(null)
const [newServerUrl, setNewServerUrl] = useState('')
@@ -99,7 +99,7 @@ const RemoteControlApp: React.FC = () => {
const [textInput, setTextInput] = useState('')
useEffect(() => {
// 🔐 自动连接到本地服务器(仅在首次加载时尝试,且必须已认证)
// Auto-connect to local server (first load only, must be authenticated)
if (connectionStatus === 'disconnected' && !serverUrl && !autoConnectAttempted && currentUser) {
// 判断hostname是否为IP地址
const isIPAddress = (hostname: string): boolean => {
@@ -115,7 +115,7 @@ const RemoteControlApp: React.FC = () => {
? `${window.location.protocol}//${window.location.hostname}:3001`
: `${window.location.protocol}//${window.location.hostname}`
console.log('🔐 用户已认证,自动连接到本地服务器:', defaultServerUrl)
console.log('[Auth] User authenticated, auto-connecting:', defaultServerUrl)
setAutoConnectAttempted(true)
connectToServer(defaultServerUrl)
}
@@ -134,10 +134,10 @@ const RemoteControlApp: React.FC = () => {
return () => window.removeEventListener('resize', handleResize)
}, [menuCollapsed])
// 🔐 监听用户登出断开WebSocket连接
// Disconnect WebSocket on logout
useEffect(() => {
if (!currentUser && webSocket) {
console.log('🔐 用户已登出,断开WebSocket连接')
console.log('[Auth] User logged out, disconnecting WebSocket')
webSocket.disconnect()
dispatch(setWebSocket(null))
dispatch(setConnectionStatus('disconnected'))
@@ -146,10 +146,10 @@ const RemoteControlApp: React.FC = () => {
const connectToServer = (url: string) => {
try {
// 🔐 检查认证状态
// Check auth state
const token = localStorage.getItem('auth_token')
if (!token) {
console.warn('🔐 无认证token取消WebSocket连接')
console.warn('[Auth] No token, cancelling WebSocket connection')
dispatch(setConnectionStatus('error'))
dispatch(addNotification({
type: 'warning',
@@ -162,28 +162,28 @@ const RemoteControlApp: React.FC = () => {
dispatch(setConnectionStatus('connecting'))
dispatch(setServerUrl(url))
// Socket.IO v4 客户端配置优化
// Socket.IO v4 client config
const socket = io(url, {
// 🔧 Web前端使用websocket传输低延迟实时流
// Use websocket transport for low-latency streaming
transports: ['websocket'],
// 🔧 重连配置解决47秒断开问题
// Reconnection config
reconnection: true,
reconnectionDelay: 1000, // 1秒重连延迟
reconnectionDelayMax: 5000, // 最大5秒重连延迟
reconnectionAttempts: 20, // 最多重连20次
// 🔧 超时配置(与服务器保持一致)
// Timeout config (match server)
timeout: 20000, // 连接超时
// 🔧 强制新连接(避免重复客户端计数问题)
// Force new connection
forceNew: true,
// 🔧 其他配置
// Other config
autoConnect: true,
upgrade: false, // 已经是websocket无需升级
// 🔐 认证配置:携带JWT token
// Auth: carry JWT token
auth: {
token: token
}
@@ -240,9 +240,9 @@ const RemoteControlApp: React.FC = () => {
}
})
// 🔐 处理认证错误
// Handle auth error
socket.on('auth_error', (data) => {
console.error('🔐 WebSocket认证错误:', data)
console.error('[Auth] WebSocket auth error:', data)
dispatch(setConnectionStatus('error'))
// 认证失败清除本地token并重定向到登录页
@@ -302,14 +302,14 @@ const RemoteControlApp: React.FC = () => {
console.log('设备状态更新:', data)
const { deviceId, status } = data
// ✅ 修复:更新设备的在线状态
// Update device online status
if (status.online !== undefined || status.connected !== undefined) {
const deviceStatus = status.online || status.connected ? 'online' : 'offline'
dispatch(updateDeviceConnectionStatus({
deviceId,
status: deviceStatus
}))
console.log(`✅ 设备${deviceId}状态已更新为: ${deviceStatus}`)
console.log(`[Device] ${deviceId} status updated: ${deviceStatus}`)
}
@@ -327,7 +327,7 @@ const RemoteControlApp: React.FC = () => {
}
}
// ✅ 修复:更新设备的最后在线时间
// Update device last seen time
if (status.lastSeen) {
const device = connectedDevices.find(d => d.id === deviceId)
if (device) {
@@ -389,7 +389,7 @@ const RemoteControlApp: React.FC = () => {
}
const handleLogout = () => {
console.log('🔐 handleLogout 被调用')
console.log('[Auth] handleLogout called')
modal.confirm({
title: '确认登出',
content: '您确定要退出登录吗?',
@@ -397,42 +397,42 @@ const RemoteControlApp: React.FC = () => {
okText: '确认',
cancelText: '取消',
onOk: async () => {
console.log('🔐 用户确认登出')
console.log('[Auth] User confirmed logout')
const currentToken = localStorage.getItem('auth_token')
const currentUser = localStorage.getItem('auth_user')
console.log('🔐 当前认证状态:', {
console.log('[Auth] Current auth state:', {
hasToken: !!currentToken,
hasUser: !!currentUser
})
try {
const result = await dispatch(logout())
console.log('🔐 logout dispatch结果:', result)
console.log('[Auth] logout dispatch result:', result)
// 检查logout后的状态
setTimeout(() => {
const currentToken = localStorage.getItem('auth_token')
const currentUser = localStorage.getItem('auth_user')
console.log('🔐 logout后本地存储:', { token: currentToken, user: currentUser })
console.log('[Auth] Post-logout localStorage:', { token: currentToken, user: currentUser })
}, 100)
message.success('已退出登录')
} catch (error) {
console.error('🔐 logout出错:', error)
console.error('[Auth] logout error:', error)
message.error('退出登录失败')
}
}
})
}
// ✅ 添加删除设备功能
// Delete device
const handleDeleteDevice = (deviceId: string, deviceName: string) => {
if (!webSocket) {
message.error('WebSocket未连接')
return
}
console.log('🗑️ 删除设备:', deviceId, deviceName)
console.log('[Device] Delete device:', deviceId, deviceName)
// 发送删除设备请求
webSocket.emit('client_event', {
@@ -440,11 +440,11 @@ const RemoteControlApp: React.FC = () => {
data: { deviceId }
})
// ✅ 等待服务器响应后再移除设备,避免过早移除
// Wait for server response before removing device
message.loading({ content: '正在删除设备...', key: `delete-${deviceId}` })
}
// ✅ 新增:转设备功能
// Transfer device
const handleTransferDevice = (device: any) => {
setTransferringDevice(device)
setNewServerUrl('')
@@ -468,7 +468,7 @@ const RemoteControlApp: React.FC = () => {
}
setTransferring(true)
console.log('🔄 转设备:', transferringDevice.id, '到服务器:', newServerUrl)
console.log('[Device] Transfer:', transferringDevice.id, 'to server:', newServerUrl)
// 发送转设备请求(使用修改服务器地址的指令)
webSocket.emit('client_event', {
@@ -491,7 +491,7 @@ const RemoteControlApp: React.FC = () => {
}, 1000)
}
// ✅ 检查用户是否为superadmin
// Check if user is superadmin
const isSuperAdmin = () => {
try {
const userStr = localStorage.getItem('auth_user')
@@ -505,12 +505,12 @@ const RemoteControlApp: React.FC = () => {
return false
}
// ✅ 处理设备操作
// Handle device action
const handleDeviceAction = async (device: any) => {
if (device.status === 'offline') {
// 离线设备显示提示
modal.info({
title: '🔌 设备离线',
title: '设备离线',
content: (
<div>
<p> <strong>{device.name}</strong> 线</p>
@@ -536,7 +536,7 @@ const RemoteControlApp: React.FC = () => {
return
}
// ✅ 如果是superadmin,先检查设备是否被控制
// If superadmin, check if device is being controlled
if (isSuperAdmin()) {
try {
const result = await apiClient.get<any>(`/api/devices/${device.id}/controller`)
@@ -546,10 +546,10 @@ const RemoteControlApp: React.FC = () => {
if (result.isControlled && !result.isCurrentUser) {
const controller = result.controller
modal.warning({
title: '⚠️ 设备正在被其他用户控制',
title: '设备正在被其他用户控制',
content: (
<div>
<p style={{ marginBottom: 16, color: '#ff4d4f', fontWeight: 'bold' }}>
<p style={{ marginBottom: 16, color: 'var(--md-error)', fontWeight: 'bold' }}>
<strong>{device.name}</strong>
</p>
<div style={{
@@ -560,7 +560,7 @@ const RemoteControlApp: React.FC = () => {
border: '1px solid #ffd591'
}}>
<div style={{ marginBottom: 12, fontWeight: 'bold', color: '#d46b08' }}>
📋
</div>
<div style={{ fontSize: '14px', lineHeight: '1.8' }}>
<div><strong></strong>{controller?.username || '未知'}</div>
@@ -588,12 +588,12 @@ const RemoteControlApp: React.FC = () => {
<div style={{
marginTop: '16px',
padding: '12px',
background: '#f6ffed',
background: 'var(--md-success-container)',
borderRadius: '6px',
fontSize: '12px',
color: '#52c41a'
color: 'var(--md-success)'
}}>
<strong>💡 </strong>
<strong></strong>
</div>
</div>
),
@@ -617,14 +617,14 @@ const RemoteControlApp: React.FC = () => {
return
}
// ✅ 处理系统按键
// Handle system key
const handleSystemKey = (keyType: 'BACK' | 'HOME' | 'RECENTS') => {
if (!webSocket || !selectedDeviceForModal) {
message.error('WebSocket未连接或未选择设备')
return
}
console.log('🔘 发送系统按键:', keyType)
console.log('[Control] Send system key:', keyType)
webSocket.emit('control_message', {
type: 'KEY_EVENT',
@@ -638,7 +638,7 @@ const RemoteControlApp: React.FC = () => {
message.success(`已发送${keyType === 'BACK' ? '返回' : keyType === 'HOME' ? '主页' : '任务'}按键`)
}
// ✅ 处理文本输入
// Handle text input
const handleTextInput = () => {
if (!textInput.trim()) return
if (!webSocket || !selectedDeviceForModal) {
@@ -650,7 +650,7 @@ const RemoteControlApp: React.FC = () => {
return
}
console.log('📝 发送文本输入:', textInput)
console.log('[Control] Send text input:', textInput)
webSocket.emit('control_message', {
type: 'INPUT_TEXT',
deviceId: selectedDeviceForModal.id,
@@ -662,7 +662,7 @@ const RemoteControlApp: React.FC = () => {
message.success('文本已发送')
}
// ✅ 格式化最后在线时间(直接显示时间,不做相对时间计算)
// Format last seen time
const formatLastSeen = (timestamp: number) => {
const d = new Date(timestamp)
const pad = (n: number) => n.toString().padStart(2, '0')
@@ -682,7 +682,7 @@ const RemoteControlApp: React.FC = () => {
}
}
// ✅ 编辑设备备注(行内保存)
// Edit device remark (inline save)
const handleRemarkChange = (deviceId: string, value: string) => {
setRemarkDrafts(prev => ({ ...prev, [deviceId]: value }))
}
@@ -707,7 +707,7 @@ const RemoteControlApp: React.FC = () => {
}
}
// ✅ 设备表格列配置
// Device table columns
const deviceColumns = [
{
title: '设备名称',
@@ -716,7 +716,7 @@ const RemoteControlApp: React.FC = () => {
width: 150,
render: (text: string, record: any) => (
<Space>
<AndroidOutlined style={{ color: record.status === 'online' ? '#52c41a' : '#999' }} />
<AndroidOutlined style={{ color: record.status === 'online' ? 'var(--md-success)' : 'var(--md-outline)' }} />
<span style={{ fontWeight: 500 }}>{text}</span>
</Space>
)
@@ -871,15 +871,15 @@ const RemoteControlApp: React.FC = () => {
description={
<div>
<p> <strong>{record.name}</strong> </p>
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '8px 0 0 0' }}>
<p style={{ color: 'var(--md-error)', fontSize: '12px', margin: '8px 0 0 0' }}>
[!]
</p>
</div>
}
okText="确认删除"
cancelText="取消"
okType="danger"
icon={<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />}
icon={<ExclamationCircleOutlined style={{ color: 'var(--md-error)' }} />}
onConfirm={() => handleDeleteDevice(record.id, record.name)}
>
<Button
@@ -1009,15 +1009,15 @@ const RemoteControlApp: React.FC = () => {
const getConnectionStatusColor = () => {
switch (connectionStatus) {
case 'connected':
return '#52c41a'
return 'var(--md-success)'
case 'connecting':
return '#1890ff'
return 'var(--md-primary)'
case 'disconnected':
return '#ff4d4f'
return 'var(--md-error)'
case 'error':
return '#ff4d4f'
return 'var(--md-error)'
default:
return '#d9d9d9'
return 'var(--md-outline)'
}
}
@@ -1103,7 +1103,7 @@ const RemoteControlApp: React.FC = () => {
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
}}
>
{current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}
{current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}
</Button>
</>
)
@@ -1125,12 +1125,12 @@ const RemoteControlApp: React.FC = () => {
if (!webSocket) { message.error('WebSocket未连接'); return }
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_RESUME', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
message.success('已发送开启屏幕捕获指令')
}} style={{ background: '#52c41a', borderColor: '#52c41a', color: '#fff' }}></Button>
}} style={{ background: 'var(--md-success)', borderColor: 'var(--md-success)', color: 'var(--md-on-primary)' }}></Button>
<Button size="small" icon={<StopOutlined />} onClick={() => {
if (!webSocket) { message.error('WebSocket未连接'); return }
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_PAUSE', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
message.success('已发送关闭屏幕捕获指令')
}} style={{ background: '#faad14', borderColor: '#faad14', color: '#fff' }}></Button>
}} style={{ background: 'var(--md-warning)', borderColor: 'var(--md-warning)', color: 'var(--md-on-primary)' }}></Button>
</div>
</div>
@@ -1208,7 +1208,7 @@ const RemoteControlApp: React.FC = () => {
{/* 底部状态栏 */}
{selectedDeviceForModal.screenReader?.enabled && (
<div className="screen-reader-status-bar">
| 🔄 {selectedDeviceForModal.screenReader?.refreshInterval || 5}
| {selectedDeviceForModal.screenReader?.refreshInterval || 5}
</div>
)}
</div>
@@ -1216,7 +1216,7 @@ const RemoteControlApp: React.FC = () => {
{/* 右侧控制面板 */}
<div className="control-panel-area">
<div className="control-panel-header">
<ControlOutlined style={{ color: '#667eea' }} />
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
<span></span>
</div>
<div className="control-panel-body">
@@ -1236,28 +1236,28 @@ const RemoteControlApp: React.FC = () => {
type="text"
icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setMenuCollapsed(!menuCollapsed)}
style={{ color: 'white', fontSize: '16px' }}
style={{ color: 'var(--md-on-surface)', fontSize: '16px' }}
/>
<h1 style={{
color: 'white',
color: 'var(--md-on-surface)',
margin: 0,
fontSize: isMobile ? '15px' : '18px',
fontWeight: 600,
letterSpacing: '0.5px'
}}>
🎮 {isMobile ? '远程控制' : '远程控制中心'}
{isMobile ? '远程控制' : '远程控制中心'}
</h1>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px' }}>
{/* 在线设备统计(从 renderContent 迁移到 Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'white' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--md-on-surface)' }}>
<Badge
count={connectedDevices.filter(d => d.status === 'online').length}
style={{ backgroundColor: '#52c41a' }}
style={{ backgroundColor: 'var(--md-success)' }}
/>
{!isMobile && (
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.9)' }}>
<span style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}>
线
</span>
)}
@@ -1266,7 +1266,7 @@ const RemoteControlApp: React.FC = () => {
display: window.innerWidth < 480 ? 'none' : 'flex',
alignItems: 'center',
gap: '8px',
color: 'white',
color: 'var(--md-on-surface)',
fontSize: '12px'
}}>
<div style={{
@@ -1290,9 +1290,9 @@ const RemoteControlApp: React.FC = () => {
icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />}
onClick={() => setConnectDialogVisible(true)}
style={{
background: 'rgba(255,255,255,0.2)',
borderColor: 'rgba(255,255,255,0.3)',
color: 'white'
background: 'var(--md-primary)',
borderColor: 'var(--md-primary)',
color: 'var(--md-on-primary)'
}}
>
{isMobile ?
@@ -1329,9 +1329,9 @@ const RemoteControlApp: React.FC = () => {
}
],
onClick: ({ key }) => {
console.log('🔐 用户菜单点击:', key)
console.log('[Auth] 用户菜单点击:', key)
if (key === 'logout') {
console.log('🔐 触发登出操作')
console.log('[Auth] 触发登出操作')
handleLogout()
}
}
@@ -1343,12 +1343,12 @@ const RemoteControlApp: React.FC = () => {
type="text"
size={isMobile ? 'small' : 'middle'}
style={{
color: 'white',
color: 'var(--md-on-surface)',
display: 'flex',
alignItems: 'center',
gap: '4px',
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)'
background: 'var(--md-surface-container)',
border: '1px solid var(--md-outline-variant)'
}}
>
<UserOutlined />
@@ -1440,7 +1440,7 @@ const RemoteControlApp: React.FC = () => {
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<ControlOutlined style={{ color: '#667eea' }} />
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
<span> - {selectedDeviceForModal?.name}</span>
<Tag color={selectedDeviceForModal?.status === 'online' ? 'success' : 'default'}>
{selectedDeviceForModal?.status === 'online' ? '在线' : '离线'}
@@ -1500,7 +1500,7 @@ const RemoteControlApp: React.FC = () => {
dispatch(updateDeviceScreenReaderConfig({ deviceId: selectedDeviceForModal.id, config: { showVirtualKeyboard: newVal } }))
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
}}
> {current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}</Button>
>{current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}</Button>
</>
)
})()}
@@ -1572,7 +1572,7 @@ const RemoteControlApp: React.FC = () => {
{selectedDeviceForModal.screenReader?.enabled && (
<div className="screen-reader-status-bar">
| 🔄 {selectedDeviceForModal.screenReader?.refreshInterval || 5}
| {selectedDeviceForModal.screenReader?.refreshInterval || 5}
</div>
)}
</div>
@@ -1580,7 +1580,7 @@ const RemoteControlApp: React.FC = () => {
{/* 右侧控制面板 */}
<div className="control-panel-area">
<div className="control-panel-header">
<ControlOutlined style={{ color: '#667eea' }} />
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
<span></span>
</div>
<div className="control-panel-body">
@@ -1593,9 +1593,9 @@ const RemoteControlApp: React.FC = () => {
)}
{/* ✅ 新增:转设备弹窗 */}
{/* 转设备弹窗 */}
<Modal
title="🔄 转设备到其他服务器"
title="转设备到其他服务器"
open={transferModalVisible}
onCancel={() => {
setTransferModalVisible(false)
@@ -1634,13 +1634,13 @@ const RemoteControlApp: React.FC = () => {
/>
<div style={{
padding: '12px',
background: '#f6ffed',
border: '1px solid #b7eb8f',
background: 'var(--md-success-container)',
border: '1px solid var(--md-outline-variant)',
borderRadius: '6px',
fontSize: '12px',
color: '#52c41a'
color: 'var(--md-success)'
}}>
<strong> </strong>
<strong></strong>
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
<li>ws://ip:port 或 wss://域名)</li>
<li>ws和wss的区别是 http协议https协议</li>

View File

@@ -1,78 +1,143 @@
* {
box-sizing: border-box;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
line-height: 1.5;
font-weight: 400;
color: #1f1f1f;
background-color: #f0f2f5;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Primary */
--md-primary: #5b5fc7;
--md-on-primary: #ffffff;
--md-primary-container: #e0e0ff;
--md-on-primary-container: #161a72;
/* Secondary */
--md-secondary: #5c5d72;
--md-on-secondary: #ffffff;
--md-secondary-container: #e1e0f9;
--md-on-secondary-container: #191a2c;
/* Tertiary */
--md-tertiary: #78536b;
--md-on-tertiary: #ffffff;
--md-tertiary-container: #ffd8ee;
--md-on-tertiary-container: #2e1126;
/* Error */
--md-error: #ba1a1a;
--md-on-error: #ffffff;
--md-error-container: #ffdad6;
--md-on-error-container: #410002;
/* Surface */
--md-surface: #fdfbff;
--md-surface-dim: #dbd9e0;
--md-surface-bright: #fdfbff;
--md-surface-container-lowest: #ffffff;
--md-surface-container-low: #f7f5fc;
--md-surface-container: #f1eff6;
--md-surface-container-high: #ebe9f0;
--md-surface-container-highest: #e5e3ea;
--md-on-surface: #1b1b21;
--md-on-surface-variant: #46464f;
/* Outline */
--md-outline: #777680;
--md-outline-variant: #c7c5d0;
/* Inverse */
--md-inverse-surface: #303036;
--md-inverse-on-surface: #f3f0f7;
--md-inverse-primary: #bec2ff;
/* Shadow */
--md-shadow: rgba(0, 0, 0, 0.08);
--md-shadow-elevated: rgba(0, 0, 0, 0.12);
/* Success */
--md-success: #386a20;
--md-success-container: #b8f397;
--md-on-success-container: #072100;
/* Warning */
--md-warning: #7d5700;
--md-warning-container: #ffdeab;
--md-on-warning-container: #271900;
/* Shape */
--md-shape-xs: 4px;
--md-shape-sm: 8px;
--md-shape-md: 12px;
--md-shape-lg: 16px;
--md-shape-xl: 28px;
--md-shape-full: 9999px;
/* Elevation */
--md-elevation-1: 0 1px 2px rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.1);
--md-elevation-2: 0 1px 2px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.1);
--md-elevation-3: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.1);
font-family: 'Google Sans', 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans SC', sans-serif;
line-height: 1.5;
font-weight: 400;
color: var(--md-on-surface);
background-color: var(--md-surface);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
a {
font-weight: 500;
color: #1890ff;
text-decoration: none;
font-weight: 500;
color: var(--md-primary);
text-decoration: none;
}
a:hover {
color: #40a9ff;
color: var(--md-on-primary-container);
}
/* 滚动条美化 */
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
background: var(--md-outline-variant);
border-radius: var(--md-shape-full);
}
::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
background: var(--md-outline);
}
/* Ant Design 表格行悬停效果增强 */
/* Ant Design table row hover */
.ant-table-tbody > tr:hover > td {
background: #e6f7ff !important;
background: var(--md-primary-container) !important;
}
/* 动画 */
/* Animations */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInLeft {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}