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:
452
src/App.css
452
src/App.css
@@ -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;
|
||||
}
|
||||
|
||||
119
src/App.tsx
119
src/App.tsx
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
// 🔧 修复:使用容器的clientWidth和clientHeight,排除边框和滚动条
|
||||
// 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>
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
|
||||
@@ -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' }}>
|
||||
系统已创建初始化锁文件,防止重复初始化。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
147
src/index.css
147
src/index.css
@@ -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); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user