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:
2
node_modules/.vite/deps/@ant-design_icons.js
generated
vendored
2
node_modules/.vite/deps/@ant-design_icons.js
generated
vendored
@@ -63,7 +63,7 @@ import {
|
|||||||
useComposeRef,
|
useComposeRef,
|
||||||
useInsertStyles,
|
useInsertStyles,
|
||||||
warning2 as warning
|
warning2 as warning
|
||||||
} from "./chunk-5Q2RTODE.js";
|
} from "./chunk-MBOWMX2L.js";
|
||||||
import {
|
import {
|
||||||
require_react
|
require_react
|
||||||
} from "./chunk-NZP3G7XT.js";
|
} from "./chunk-NZP3G7XT.js";
|
||||||
|
|||||||
34
node_modules/.vite/deps/_metadata.json
generated
vendored
34
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,85 +1,85 @@
|
|||||||
{
|
{
|
||||||
"hash": "3570225d",
|
"hash": "f188899e",
|
||||||
"configHash": "632304f4",
|
"configHash": "632304f4",
|
||||||
"lockfileHash": "80049c3e",
|
"lockfileHash": "ee2683e5",
|
||||||
"browserHash": "517aea31",
|
"browserHash": "11bb468f",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"react": {
|
"react": {
|
||||||
"src": "../../react/index.js",
|
"src": "../../react/index.js",
|
||||||
"file": "react.js",
|
"file": "react.js",
|
||||||
"fileHash": "2ffb555a",
|
"fileHash": "82b88bac",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"src": "../../react-dom/index.js",
|
"src": "../../react-dom/index.js",
|
||||||
"file": "react-dom.js",
|
"file": "react-dom.js",
|
||||||
"fileHash": "497e92e9",
|
"fileHash": "f147bcf0",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"@reduxjs/toolkit": {
|
"@reduxjs/toolkit": {
|
||||||
"src": "../../@reduxjs/toolkit/dist/redux-toolkit.modern.mjs",
|
"src": "../../@reduxjs/toolkit/dist/redux-toolkit.modern.mjs",
|
||||||
"file": "@reduxjs_toolkit.js",
|
"file": "@reduxjs_toolkit.js",
|
||||||
"fileHash": "326be4c8",
|
"fileHash": "e2e532ed",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-redux": {
|
"react-redux": {
|
||||||
"src": "../../react-redux/dist/react-redux.mjs",
|
"src": "../../react-redux/dist/react-redux.mjs",
|
||||||
"file": "react-redux.js",
|
"file": "react-redux.js",
|
||||||
"fileHash": "c9e96044",
|
"fileHash": "49e0dcfd",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"antd": {
|
"antd": {
|
||||||
"src": "../../antd/es/index.js",
|
"src": "../../antd/es/index.js",
|
||||||
"file": "antd.js",
|
"file": "antd.js",
|
||||||
"fileHash": "fbadf007",
|
"fileHash": "1c827006",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"socket.io-client": {
|
"socket.io-client": {
|
||||||
"src": "../../socket.io-client/build/esm/index.js",
|
"src": "../../socket.io-client/build/esm/index.js",
|
||||||
"file": "socket__io-client.js",
|
"file": "socket__io-client.js",
|
||||||
"fileHash": "3a7d980f",
|
"fileHash": "f1f126a4",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react/jsx-dev-runtime": {
|
"react/jsx-dev-runtime": {
|
||||||
"src": "../../react/jsx-dev-runtime.js",
|
"src": "../../react/jsx-dev-runtime.js",
|
||||||
"file": "react_jsx-dev-runtime.js",
|
"file": "react_jsx-dev-runtime.js",
|
||||||
"fileHash": "771481b3",
|
"fileHash": "92554eaf",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-runtime": {
|
"react/jsx-runtime": {
|
||||||
"src": "../../react/jsx-runtime.js",
|
"src": "../../react/jsx-runtime.js",
|
||||||
"file": "react_jsx-runtime.js",
|
"file": "react_jsx-runtime.js",
|
||||||
"fileHash": "fba82193",
|
"fileHash": "c9146743",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"@ant-design/icons": {
|
"@ant-design/icons": {
|
||||||
"src": "../../@ant-design/icons/es/index.js",
|
"src": "../../@ant-design/icons/es/index.js",
|
||||||
"file": "@ant-design_icons.js",
|
"file": "@ant-design_icons.js",
|
||||||
"fileHash": "6de612e4",
|
"fileHash": "5c57aafc",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"antd/locale/zh_CN": {
|
"antd/locale/zh_CN": {
|
||||||
"src": "../../antd/locale/zh_CN.js",
|
"src": "../../antd/locale/zh_CN.js",
|
||||||
"file": "antd_locale_zh_CN.js",
|
"file": "antd_locale_zh_CN.js",
|
||||||
"fileHash": "335584cf",
|
"fileHash": "731388b8",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"dayjs": {
|
"dayjs": {
|
||||||
"src": "../../dayjs/dayjs.min.js",
|
"src": "../../dayjs/dayjs.min.js",
|
||||||
"file": "dayjs.js",
|
"file": "dayjs.js",
|
||||||
"fileHash": "7f4736e7",
|
"fileHash": "581df681",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-dom/client": {
|
"react-dom/client": {
|
||||||
"src": "../../react-dom/client.js",
|
"src": "../../react-dom/client.js",
|
||||||
"file": "react-dom_client.js",
|
"file": "react-dom_client.js",
|
||||||
"fileHash": "f5462f19",
|
"fileHash": "f34fbc85",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chunks": {
|
"chunks": {
|
||||||
"chunk-5Q2RTODE": {
|
"chunk-MBOWMX2L": {
|
||||||
"file": "chunk-5Q2RTODE.js"
|
"file": "chunk-MBOWMX2L.js"
|
||||||
},
|
},
|
||||||
"chunk-624ZMHH6": {
|
"chunk-624ZMHH6": {
|
||||||
"file": "chunk-624ZMHH6.js"
|
"file": "chunk-624ZMHH6.js"
|
||||||
|
|||||||
2
node_modules/.vite/deps/antd.js
generated
vendored
2
node_modules/.vite/deps/antd.js
generated
vendored
@@ -86,7 +86,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
warning,
|
warning,
|
||||||
warning_default
|
warning_default
|
||||||
} from "./chunk-5Q2RTODE.js";
|
} from "./chunk-MBOWMX2L.js";
|
||||||
import {
|
import {
|
||||||
require_dayjs_min
|
require_dayjs_min
|
||||||
} from "./chunk-624ZMHH6.js";
|
} from "./chunk-624ZMHH6.js";
|
||||||
|
|||||||
2
node_modules/.vite/deps/antd.js.map
generated
vendored
2
node_modules/.vite/deps/antd.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2714
node_modules/.vite/deps/chunk-5Q2RTODE.js
generated
vendored
2714
node_modules/.vite/deps/chunk-5Q2RTODE.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
node_modules/.vite/deps/chunk-5Q2RTODE.js.map
generated
vendored
7
node_modules/.vite/deps/chunk-5Q2RTODE.js.map
generated
vendored
File diff suppressed because one or more lines are too long
154
src/App.css
154
src/App.css
@@ -7,40 +7,40 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主布局自适应 */
|
/* Main layout */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header 响应式 */
|
/* Header */
|
||||||
.app-header {
|
.app-header {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--md-surface-container);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
box-shadow: var(--md-elevation-1);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 56px;
|
height: 64px;
|
||||||
line-height: 56px;
|
line-height: 64px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
border-bottom: 1px solid var(--md-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app-header {
|
.app-header {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 48px;
|
height: 56px;
|
||||||
line-height: 48px;
|
line-height: 56px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边栏 */
|
/* Sidebar */
|
||||||
.app-sider {
|
.app-sider {
|
||||||
background: #fff !important;
|
background: var(--md-surface-container-low) !important;
|
||||||
border-right: 1px solid #f0f0f0;
|
border-right: 1px solid var(--md-outline-variant);
|
||||||
box-shadow: 2px 0 8px rgba(0,0,0,0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-sider .ant-menu {
|
.app-sider .ant-menu {
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域 */
|
/* Content area */
|
||||||
.app-content {
|
.app-content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 设备列表页 */
|
/* Device list page */
|
||||||
.device-list-page {
|
.device-list-page {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f0f2f5;
|
background: var(--md-surface);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -76,24 +76,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-list-card {
|
.device-list-card {
|
||||||
background: white;
|
background: var(--md-surface-container-lowest);
|
||||||
border-radius: 12px;
|
border-radius: var(--md-shape-md);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
box-shadow: var(--md-elevation-1);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
border: 1px solid var(--md-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 筛选栏响应式 */
|
/* Filter bar */
|
||||||
.device-filter-bar {
|
.device-filter-bar {
|
||||||
background: white;
|
background: var(--md-surface-container-low);
|
||||||
border-radius: 8px;
|
border-radius: var(--md-shape-sm);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
border: 1px solid var(--md-outline-variant);
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-filter-bar .ant-form-inline {
|
.device-filter-bar .ant-form-inline {
|
||||||
@@ -109,39 +109,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
/* Empty state */
|
||||||
.device-empty-state {
|
.device-empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #8c8c8c;
|
color: var(--md-on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 独立控制页面 - 全屏自适应 */
|
/* Standalone control page */
|
||||||
.standalone-control-page {
|
.standalone-control-page {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
background: #f0f2f5;
|
background: var(--md-surface);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 控制页左侧屏幕区域 */
|
/* Control screen area */
|
||||||
.control-screen-area {
|
.control-screen-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: 1px solid #e8e8e8;
|
border-right: 1px solid var(--md-outline-variant);
|
||||||
background: #fff;
|
background: var(--md-surface-container-lowest);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 工具栏 */
|
/* Toolbar */
|
||||||
.control-toolbar {
|
.control-toolbar {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--md-outline-variant);
|
||||||
background: #fafafa;
|
background: var(--md-surface-container-low);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 屏幕+阅读器水平布局 */
|
/* Screen + reader horizontal layout */
|
||||||
.screen-reader-row {
|
.screen-reader-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -176,10 +176,10 @@
|
|||||||
|
|
||||||
.screen-reader-panel {
|
.screen-reader-panel {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
border-right: 1px solid #e8e8e8;
|
border-right: 1px solid var(--md-outline-variant);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fafafa;
|
background: var(--md-surface-container-low);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -188,16 +188,16 @@
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f5f5f5;
|
background: var(--md-surface-container);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文本输入区域 */
|
/* Text input bar */
|
||||||
.text-input-bar {
|
.text-input-bar {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid var(--md-outline-variant);
|
||||||
background: #fff;
|
background: var(--md-surface-container-lowest);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -207,47 +207,55 @@
|
|||||||
|
|
||||||
.text-input-bar input {
|
.text-input-bar input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 30px;
|
height: 32px;
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid var(--md-outline-variant);
|
||||||
border-radius: 6px;
|
border-radius: var(--md-shape-full);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
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 {
|
.text-input-bar input:focus {
|
||||||
border-color: #1890ff;
|
border-color: var(--md-primary);
|
||||||
box-shadow: 0 0 0 2px rgba(24,144,255,0.1);
|
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 {
|
.text-input-bar button {
|
||||||
height: 30px;
|
height: 32px;
|
||||||
padding: 0 14px;
|
padding: 0 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: var(--md-shape-full);
|
||||||
background: #1890ff;
|
background: var(--md-primary);
|
||||||
color: #fff;
|
color: var(--md-on-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
transition: background 0.2s;
|
font-weight: 500;
|
||||||
|
transition: background 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input-bar button:hover:not(:disabled) {
|
.text-input-bar button:hover:not(:disabled) {
|
||||||
background: #40a9ff;
|
box-shadow: var(--md-elevation-1);
|
||||||
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input-bar button:disabled {
|
.text-input-bar button:disabled {
|
||||||
background: #f5f5f5;
|
background: var(--md-surface-container-highest);
|
||||||
color: #bfbfbf;
|
color: var(--md-outline);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 系统按键区域 */
|
/* System keys bar */
|
||||||
.system-keys-bar {
|
.system-keys-bar {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid var(--md-outline-variant);
|
||||||
background: #fff;
|
background: var(--md-surface-container-lowest);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -257,30 +265,31 @@
|
|||||||
.system-keys-bar .ant-btn {
|
.system-keys-bar .ant-btn {
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: var(--md-shape-full);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右侧控制面板 */
|
/* Right control panel */
|
||||||
.control-panel-area {
|
.control-panel-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #fff;
|
background: var(--md-surface-container-lowest);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel-header {
|
.control-panel-header {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--md-outline-variant);
|
||||||
background: #fafafa;
|
background: var(--md-surface-container-low);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: var(--md-on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel-body {
|
.control-panel-body {
|
||||||
@@ -289,43 +298,42 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部状态栏 */
|
/* Bottom status bar */
|
||||||
.screen-reader-status-bar {
|
.screen-reader-status-bar {
|
||||||
padding: 3px 12px;
|
padding: 3px 12px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid var(--md-outline-variant);
|
||||||
background: #fafafa;
|
background: var(--md-surface-container-low);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #8c8c8c;
|
color: var(--md-on-surface-variant);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端遮罩 */
|
/* Mobile overlay */
|
||||||
.mobile-overlay {
|
.mobile-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: rgba(0,0,0,0.45);
|
background: rgba(0, 0, 0, 0.32);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
animation: fadeIn 0.2s ease;
|
animation: fadeIn 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端侧边栏 */
|
/* Mobile sidebar */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.ant-layout-sider {
|
.ant-layout-sider {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
z-index: 1000 !important;
|
z-index: 1000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-header {
|
.ant-layout-header {
|
||||||
padding: 0 12px !important;
|
padding: 0 12px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格自适应 */
|
/* Table responsive */
|
||||||
.ant-table-wrapper {
|
.ant-table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/App.tsx
65
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 { Provider } from 'react-redux'
|
||||||
import { ConfigProvider, App as AntdApp } from 'antd'
|
import { ConfigProvider, App as AntdApp } from 'antd'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
@@ -8,7 +8,21 @@ import AuthGuard from './components/AuthGuard'
|
|||||||
import './App.css'
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -17,33 +31,54 @@ function App() {
|
|||||||
locale={zhCN}
|
locale={zhCN}
|
||||||
theme={{
|
theme={{
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#667eea',
|
colorPrimary: PRIMARY_COLOR,
|
||||||
borderRadius: 8,
|
borderRadius: BORDER_RADIUS,
|
||||||
colorBgContainer: '#ffffff',
|
colorBgContainer: SURFACE_CONTAINER_LOWEST,
|
||||||
colorBgLayout: '#f0f2f5',
|
colorBgLayout: SURFACE_CONTAINER,
|
||||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
colorBorder: OUTLINE_VARIANT,
|
||||||
|
colorText: ON_SURFACE,
|
||||||
|
colorTextSecondary: ON_SURFACE_VARIANT,
|
||||||
|
fontFamily: FONT_FAMILY,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Table: {
|
Table: {
|
||||||
headerBg: '#fafafa',
|
headerBg: SURFACE_CONTAINER_LOW,
|
||||||
headerColor: '#595959',
|
headerColor: ON_SURFACE_VARIANT,
|
||||||
rowHoverBg: '#f0f5ff',
|
rowHoverBg: '#e0e0ff',
|
||||||
borderRadius: 8,
|
borderRadius: BORDER_RADIUS,
|
||||||
},
|
},
|
||||||
Card: {
|
Card: {
|
||||||
borderRadiusLG: 12,
|
borderRadiusLG: 16,
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
borderRadius: 6,
|
borderRadius: 20,
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
itemBorderRadius: 8,
|
itemBorderRadius: BORDER_RADIUS,
|
||||||
itemMarginInline: 8,
|
itemMarginInline: 8,
|
||||||
},
|
},
|
||||||
|
Modal: {
|
||||||
|
borderRadiusLG: 20,
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
Tag: {
|
||||||
|
borderRadiusSM: 20,
|
||||||
|
},
|
||||||
|
Notification: {
|
||||||
|
borderRadiusLG: 16,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AntdApp>
|
<AntdApp
|
||||||
|
message={{ top: 24, maxCount: 3 }}
|
||||||
|
notification={{ placement: 'topRight', top: 24 }}
|
||||||
|
>
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<RemoteControlApp />
|
<RemoteControlApp />
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 调试:监听认证状态变化
|
// 调试:监听认证状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔐 AuthGuard - 认证状态变化:', {
|
console.log('[AuthGuard] Auth state changed:', {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
authLoading,
|
authLoading,
|
||||||
token: token ? '***' : null,
|
token: token ? '***' : null,
|
||||||
@@ -51,7 +51,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeAuth = async () => {
|
const initializeAuth = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🔐 检查系统初始化状态...')
|
console.log('[Auth] Checking system initialization...')
|
||||||
|
|
||||||
// 首先检查系统是否已初始化
|
// 首先检查系统是否已初始化
|
||||||
// const initResult = await apiClient.get<any>('/')
|
// const initResult = await apiClient.get<any>('/')
|
||||||
@@ -59,11 +59,11 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
|
|
||||||
if (initResult.success) {
|
if (initResult.success) {
|
||||||
setSystemInitialized(initResult.isInitialized)
|
setSystemInitialized(initResult.isInitialized)
|
||||||
console.log(`🔐 系统初始化状态: ${initResult.isInitialized ? '已初始化' : '未初始化'}`)
|
console.log(`[Auth] System initialized: ${initResult.isInitialized}`)
|
||||||
|
|
||||||
// 如果系统已初始化,继续认证流程
|
// 如果系统已初始化,继续认证流程
|
||||||
if (initResult.isInitialized) {
|
if (initResult.isInitialized) {
|
||||||
console.log('🔐 初始化认证状态...')
|
console.log('[Auth] Restoring auth state...')
|
||||||
|
|
||||||
// 先尝试从本地存储恢复状态
|
// 先尝试从本地存储恢复状态
|
||||||
dispatch(restoreAuthState())
|
dispatch(restoreAuthState())
|
||||||
@@ -75,23 +75,23 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
const currentToken = localStorage.getItem('auth_token')
|
const currentToken = localStorage.getItem('auth_token')
|
||||||
|
|
||||||
if (currentToken) {
|
if (currentToken) {
|
||||||
console.log('🔐 找到本地token,验证有效性...')
|
console.log('[Auth] Found local token, verifying...')
|
||||||
// 验证token是否仍然有效
|
// 验证token是否仍然有效
|
||||||
try {
|
try {
|
||||||
const result = await dispatch(verifyToken(currentToken))
|
const result = await dispatch(verifyToken(currentToken))
|
||||||
|
|
||||||
if (verifyToken.fulfilled.match(result)) {
|
if (verifyToken.fulfilled.match(result)) {
|
||||||
console.log('✅ Token验证成功')
|
console.log('[Auth] Token verified successfully')
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ Token验证失败:', result.payload)
|
console.log('[Auth] Token verification failed:', result.payload)
|
||||||
setLoginError('登录已过期,请重新登录')
|
setLoginError('登录已过期,请重新登录')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('❌ Token验证出错:', error)
|
console.log('[Auth] Token verification error:', error)
|
||||||
setLoginError('登录验证失败,请重新登录')
|
setLoginError('登录验证失败,请重新登录')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('🔐 未找到本地token')
|
console.log('[Auth] No local token found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -112,7 +112,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
// 监听token过期事件
|
// 监听token过期事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTokenExpired = () => {
|
const handleTokenExpired = () => {
|
||||||
console.log('🔐 Token过期,清除认证状态')
|
console.log('[Auth] Token expired, clearing auth state')
|
||||||
dispatch(clearAuthState())
|
dispatch(clearAuthState())
|
||||||
setLoginError('登录已过期,请重新登录')
|
setLoginError('登录已过期,请重新登录')
|
||||||
}
|
}
|
||||||
@@ -127,18 +127,18 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
// 处理登录
|
// 处理登录
|
||||||
const handleLogin = async (username: string, password: string) => {
|
const handleLogin = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('🔐 尝试登录:', username)
|
console.log('[Auth] Login attempt:', username)
|
||||||
setLoginError(null)
|
setLoginError(null)
|
||||||
dispatch(clearError())
|
dispatch(clearError())
|
||||||
|
|
||||||
const result = await dispatch(login({ username, password }))
|
const result = await dispatch(login({ username, password }))
|
||||||
|
|
||||||
if (login.fulfilled.match(result)) {
|
if (login.fulfilled.match(result)) {
|
||||||
console.log('✅ 登录成功')
|
console.log('[Auth] Login successful')
|
||||||
setLoginError(null)
|
setLoginError(null)
|
||||||
} else if (login.rejected.match(result)) {
|
} else if (login.rejected.match(result)) {
|
||||||
const errorMessage = result.payload || '登录失败'
|
const errorMessage = result.payload || '登录失败'
|
||||||
console.log('❌ 登录失败:', errorMessage)
|
console.log('[Auth] Login failed:', errorMessage)
|
||||||
setLoginError(errorMessage)
|
setLoginError(errorMessage)
|
||||||
throw new Error(errorMessage)
|
throw new Error(errorMessage)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 处理安装完成
|
// 处理安装完成
|
||||||
const handleInstallComplete = () => {
|
const handleInstallComplete = () => {
|
||||||
console.log('🔐 安装完成,刷新初始化状态')
|
console.log('[Auth] Install complete, refreshing init state')
|
||||||
setSystemInitialized(true)
|
setSystemInitialized(true)
|
||||||
setLoginError(null)
|
setLoginError(null)
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
background: 'var(--md-surface)'
|
||||||
}}>
|
}}>
|
||||||
<Spin size="large" style={{ color: 'white' }} />
|
<Spin size="large" style={{ color: 'white' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const DeviceFilterComponent: React.FC<DeviceFilterProps> = ({ onFilterChange, st
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: '#262626'
|
color: '#262626'
|
||||||
}}>
|
}}>
|
||||||
<FilterOutlined style={{ color: '#1890ff' }} />
|
<FilterOutlined style={{ color: 'var(--md-primary)' }} />
|
||||||
筛选
|
筛选
|
||||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
<span style={{ fontSize: '12px', color: '#666' }}>
|
||||||
({getFilteredCount()})
|
({getFilteredCount()})
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export const DeviceInfoCard: React.FC<DeviceInfoCardProps> = ({ device }) => {
|
|||||||
<div><strong>型号:</strong> {device?.model}</div>
|
<div><strong>型号:</strong> {device?.model}</div>
|
||||||
<div><strong>系统:</strong> {device?.systemVersionName ? `${device.systemVersionName} (${device.osVersion})` : `Android ${device?.osVersion ?? ''}`}</div>
|
<div><strong>系统:</strong> {device?.systemVersionName ? `${device.systemVersionName} (${device.osVersion})` : `Android ${device?.osVersion ?? ''}`}</div>
|
||||||
{device?.romType && device.romType !== '原生Android' && (
|
{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 !== '未知版本' && (
|
{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 && (
|
{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>分辨率:</strong> {device?.screenWidth}×{device?.screenHeight}</div>
|
||||||
<div><strong>公网IP:</strong> {device?.publicIP || '未知'}</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 autoRefreshIntervalRef = useRef<number | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
// 🆕 添加拖拽状态管理
|
// Drag state management
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [dragStart, setDragStart] = useState<{x: number, y: number} | null>(null)
|
const [dragStart, setDragStart] = useState<{x: number, y: number} | null>(null)
|
||||||
|
|
||||||
// 🆕 添加长按处理
|
// Long press handling
|
||||||
const [isLongPressTriggered, setIsLongPressTriggered] = useState(false)
|
const [isLongPressTriggered, setIsLongPressTriggered] = useState(false)
|
||||||
const longPressTimerRef = useRef<number | null>(null)
|
const longPressTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
// 🆕 确认坐标提取相关状态
|
// Confirm coords extraction state
|
||||||
const [isExtractingConfirmCoords, setIsExtractingConfirmCoords] = useState(false)
|
const [isExtractingConfirmCoords, setIsExtractingConfirmCoords] = useState(false)
|
||||||
// const [extractedCoords, setExtractedCoords] = useState<{ x: number; y: number } | null>(null)
|
// const [extractedCoords, setExtractedCoords] = useState<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
// 🆕 虚拟按键相关状态
|
// Virtual keyboard state
|
||||||
const [virtualKeyboard, setVirtualKeyboard] = useState<{
|
const [virtualKeyboard, setVirtualKeyboard] = useState<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
keys: Array<{
|
keys: Array<{
|
||||||
@@ -165,11 +165,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
keys: []
|
keys: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✨ 增强分析结果状态
|
// Enhanced analysis result state
|
||||||
// const [enhancedAnalysisResult, setEnhancedAnalysisResult] = useState<any>(null)
|
// const [enhancedAnalysisResult, setEnhancedAnalysisResult] = useState<any>(null)
|
||||||
// const [deviceCharacteristics, setDeviceCharacteristics] = 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) => {
|
const generateVirtualKeyboard = useCallback((screenWidth: number, screenHeight: number) => {
|
||||||
// 键盘高度:屏幕高度的25%(确保4行按键都能显示)
|
// 键盘高度:屏幕高度的25%(确保4行按键都能显示)
|
||||||
const keyboardHeight = screenHeight * 0.25
|
const keyboardHeight = screenHeight * 0.25
|
||||||
@@ -240,7 +240,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
height: keyHeight
|
height: keyHeight
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('⌨️ 虚拟键盘布局计算:', {
|
console.log('[VirtualKeyboard] Layout calculated:', {
|
||||||
screenSize: { width: screenWidth, height: screenHeight },
|
screenSize: { width: screenWidth, height: screenHeight },
|
||||||
keyboardSize: { width: keyboardWidth, height: keyboardHeight },
|
keyboardSize: { width: keyboardWidth, height: keyboardHeight },
|
||||||
keySize: { width: keyWidth, height: keyHeight },
|
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) => {
|
const requestUIHierarchy = useCallback((enhanced: boolean = true, includeDeviceInfo: boolean = true) => {
|
||||||
if (!webSocket || !deviceId) return
|
if (!webSocket || !deviceId) return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
// ✨ 默认启用增强UI分析和设备信息
|
// Enable enhanced UI analysis and device info by default
|
||||||
webSocket.emit('client_event', {
|
webSocket.emit('client_event', {
|
||||||
type: 'GET_UI_HIERARCHY',
|
type: 'GET_UI_HIERARCHY',
|
||||||
data: {
|
data: {
|
||||||
deviceId,
|
deviceId,
|
||||||
requestId: `ui_hierarchy_${Date.now()}`,
|
requestId: `ui_hierarchy_${Date.now()}`,
|
||||||
includeInvisible: true, // ✨ 增强:包含不可见元素
|
includeInvisible: true, // Enhanced: include invisible elements
|
||||||
includeNonInteractive: true, // ✨ 增强:包含不可交互元素
|
includeNonInteractive: true, // Enhanced: include non-interactive elements
|
||||||
includeTextElements: true, // 包含文本元素
|
includeTextElements: true, // 包含文本元素
|
||||||
includeImageElements: true, // 包含图像元素
|
includeImageElements: true, // 包含图像元素
|
||||||
includeContainers: true, // 包含容器元素
|
includeContainers: true, // 包含容器元素
|
||||||
maxDepth: 25, // ✨ 增强:使用更大扫描深度
|
maxDepth: 25, // Enhanced: use larger scan depth
|
||||||
minSize: 1, // 最小元素尺寸
|
minSize: 1, // 最小元素尺寸
|
||||||
enhanced: enhanced, // ✨ 默认启用增强功能
|
enhanced: enhanced, // Enable enhanced features by default
|
||||||
includeDeviceInfo: includeDeviceInfo // ✨ 默认包含设备信息
|
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])
|
}, [webSocket, deviceId])
|
||||||
|
|
||||||
// 设备特征信息现在集成在UI层次结构请求中,无需单独请求
|
// 设备特征信息现在集成在UI层次结构请求中,无需单独请求
|
||||||
@@ -317,16 +317,16 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
if (!webSocket || !deviceId) return
|
if (!webSocket || !deviceId) return
|
||||||
|
|
||||||
const handleUIHierarchyResponse = (data: any) => {
|
const handleUIHierarchyResponse = (data: any) => {
|
||||||
//console.log('🔍 ScreenReader收到UI层次结构响应:', data)
|
//console.log('[ScreenReader] Received UI hierarchy response:', data)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (data.success && data.deviceId === deviceId) {
|
if (data.success && data.deviceId === deviceId) {
|
||||||
setUiHierarchy(data.hierarchy)
|
setUiHierarchy(data.hierarchy)
|
||||||
// 🆕 UI层次结构更新时清除选中元素,避免显示过时信息
|
// Clear selected element when UI hierarchy updates to avoid stale info
|
||||||
setSelectedElement(null)
|
setSelectedElement(null)
|
||||||
|
|
||||||
// ✅ 处理增强分析结果
|
// Process enhanced analysis result
|
||||||
if (data.enhanced) {
|
if (data.enhanced) {
|
||||||
/*console.log('🚀 收到增强UI分析结果:', {
|
/*console.log('[ScreenReader] Received enhanced UI analysis result:', {
|
||||||
enhanced: data.enhanced,
|
enhanced: data.enhanced,
|
||||||
keyboardElements: data.hierarchy?.keyboardElements?.length || 0,
|
keyboardElements: data.hierarchy?.keyboardElements?.length || 0,
|
||||||
digitButtons: data.hierarchy?.digitButtons?.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
|
inputMethodWindows: data.hierarchy?.inputMethodWindows?.length || 0
|
||||||
})*/
|
})*/
|
||||||
|
|
||||||
// ✨ 保存增强分析结果
|
// Save enhanced analysis result
|
||||||
// setEnhancedAnalysisResult({
|
// setEnhancedAnalysisResult({
|
||||||
// keyboardElements: data.hierarchy?.keyboardElements || [],
|
// keyboardElements: data.hierarchy?.keyboardElements || [],
|
||||||
// digitButtons: data.hierarchy?.digitButtons || [],
|
// digitButtons: data.hierarchy?.digitButtons || [],
|
||||||
@@ -345,29 +345,29 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
// 显示键盘检测结果
|
// 显示键盘检测结果
|
||||||
if (data.hierarchy?.keyboardConfidence > 0.5) {
|
if (data.hierarchy?.keyboardConfidence > 0.5) {
|
||||||
console.log('✅ 高置信度虚拟键盘检测成功,置信度:', data.hierarchy.keyboardConfidence)
|
console.log('[ScreenReader] High confidence virtual keyboard detected, confidence:', data.hierarchy.keyboardConfidence)
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ 虚拟键盘检测置信度较低:', data.hierarchy.keyboardConfidence)
|
console.log('[ScreenReader] Low virtual keyboard detection confidence:', data.hierarchy.keyboardConfidence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 处理设备特征信息
|
// Process device characteristics
|
||||||
if (data.deviceCharacteristics) {
|
if (data.deviceCharacteristics) {
|
||||||
/*console.log('🔍 收到设备特征信息:', {
|
/*console.log('[ScreenReader] Received device characteristics:', {
|
||||||
romType: data.deviceCharacteristics.romFeatures?.romType,
|
romType: data.deviceCharacteristics.romFeatures?.romType,
|
||||||
inputMethodType: data.deviceCharacteristics.inputMethodInfo?.inputMethodType,
|
inputMethodType: data.deviceCharacteristics.inputMethodInfo?.inputMethodType,
|
||||||
primaryStrategy: data.deviceCharacteristics.keyboardDetectionStrategy?.primaryStrategy,
|
primaryStrategy: data.deviceCharacteristics.keyboardDetectionStrategy?.primaryStrategy,
|
||||||
deviceSpecificTips: data.deviceCharacteristics.keyboardDetectionStrategy?.deviceSpecificTips
|
deviceSpecificTips: data.deviceCharacteristics.keyboardDetectionStrategy?.deviceSpecificTips
|
||||||
})*/
|
})*/
|
||||||
|
|
||||||
// ✨ 保存设备特征信息
|
// Save device characteristics
|
||||||
// setDeviceCharacteristics({
|
// setDeviceCharacteristics({
|
||||||
// ...data.deviceCharacteristics,
|
// ...data.deviceCharacteristics,
|
||||||
// timestamp: Date.now()
|
// timestamp: Date.now()
|
||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
/*console.log('✅ UI层次结构数据已设置:', {
|
/*console.log('[ScreenReader] UI hierarchy data set:', {
|
||||||
totalElements: data.hierarchy?.totalElements,
|
totalElements: data.hierarchy?.totalElements,
|
||||||
clickableElements: data.hierarchy?.clickableElements,
|
clickableElements: data.hierarchy?.clickableElements,
|
||||||
screenSize: `${data.hierarchy?.screenWidth}x${data.hierarchy?.screenHeight}`,
|
screenSize: `${data.hierarchy?.screenWidth}x${data.hierarchy?.screenHeight}`,
|
||||||
@@ -387,19 +387,19 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
}
|
}
|
||||||
if (data.hierarchy?.root) {
|
if (data.hierarchy?.root) {
|
||||||
countElements(data.hierarchy.root)
|
countElements(data.hierarchy.root)
|
||||||
//console.log('📊 UI元素类型统计:', elementTypes)
|
//console.log('[ScreenReader] UI element type stats:', elementTypes)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 获取UI层次结构失败:', data.error || data.message)
|
console.error('[ScreenReader] Failed to get UI hierarchy:', data.error || data.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备特征信息现在集成在UI层次结构响应中,无需单独处理
|
// 设备特征信息现在集成在UI层次结构响应中,无需单独处理
|
||||||
|
|
||||||
// 🆕 监听提取确认坐标的事件
|
// Listen for confirm coords extraction event
|
||||||
const handleStartExtractConfirmCoords = (data: any) => {
|
const handleStartExtractConfirmCoords = (data: any) => {
|
||||||
if (data.deviceId === deviceId) {
|
if (data.deviceId === deviceId) {
|
||||||
console.log('🎯 开始提取确认坐标模式:', deviceId)
|
console.log('[ScreenReader] Start extracting confirm coords mode:', deviceId)
|
||||||
setIsExtractingConfirmCoords(true)
|
setIsExtractingConfirmCoords(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,28 +413,28 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
}
|
}
|
||||||
}, [webSocket, deviceId])
|
}, [webSocket, deviceId])
|
||||||
|
|
||||||
// 🆕 监听虚拟按键显示状态变化
|
// Listen for virtual keyboard visibility changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (screenReader.showVirtualKeyboard && uiHierarchy) {
|
if (screenReader.showVirtualKeyboard && uiHierarchy) {
|
||||||
const keyboard = generateVirtualKeyboard(uiHierarchy.screenWidth, uiHierarchy.screenHeight)
|
const keyboard = generateVirtualKeyboard(uiHierarchy.screenWidth, uiHierarchy.screenHeight)
|
||||||
setVirtualKeyboard(keyboard)
|
setVirtualKeyboard(keyboard)
|
||||||
console.log('⌨️ 虚拟按键已生成:', keyboard.keys.length, '个按键')
|
console.log('[VirtualKeyboard] Virtual keys generated:', keyboard.keys.length, 'keys')
|
||||||
} else {
|
} else {
|
||||||
setVirtualKeyboard({ visible: false, keys: [] })
|
setVirtualKeyboard({ visible: false, keys: [] })
|
||||||
console.log('⌨️ 虚拟按键已隐藏')
|
console.log('[VirtualKeyboard] Virtual keys hidden')
|
||||||
}
|
}
|
||||||
}, [screenReader.showVirtualKeyboard, uiHierarchy, generateVirtualKeyboard])
|
}, [screenReader.showVirtualKeyboard, uiHierarchy, generateVirtualKeyboard])
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (screenReader.enabled && webSocket && deviceId) {
|
if (screenReader.enabled && webSocket && deviceId) {
|
||||||
//console.log('🔄 ScreenReader启用,开始自动刷新')
|
//console.log('[ScreenReader] Enabled, starting auto refresh')
|
||||||
// 延迟一点时间确保连接稳定
|
// 延迟一点时间确保连接稳定
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startAutoRefresh()
|
startAutoRefresh()
|
||||||
}, 500)
|
}, 500)
|
||||||
} else {
|
} else {
|
||||||
console.log('🛑 ScreenReader未启用或连接缺失,停止自动刷新')
|
console.log('[ScreenReader] Not enabled or connection missing, stopping auto refresh')
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// 在画布上绘制UI元素边界
|
// 在画布上绘制UI元素边界
|
||||||
const drawElementBounds = useCallback(() => {
|
const drawElementBounds = useCallback(() => {
|
||||||
if (!canvasRef.current || !uiHierarchy) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// const containerRect = container.getBoundingClientRect() // 暂时未使用
|
// const containerRect = container.getBoundingClientRect() // 暂时未使用
|
||||||
const dpr = window.devicePixelRatio || 1
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
|
||||||
// 🔧 修复:使用容器的clientWidth和clientHeight,排除边框和滚动条
|
// Fix: use container clientWidth and clientHeight, excluding borders and scrollbars
|
||||||
const displayWidth = container.clientWidth
|
const displayWidth = container.clientWidth
|
||||||
const displayHeight = container.clientHeight
|
const displayHeight = container.clientHeight
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
ctx.imageSmoothingEnabled = true
|
ctx.imageSmoothingEnabled = true
|
||||||
ctx.imageSmoothingQuality = 'high'
|
ctx.imageSmoothingQuality = 'high'
|
||||||
|
|
||||||
/*console.log('🎨 ScreenReader: 开始绘制UI边界图', {
|
/*console.log('[ScreenReader] Start drawing UI bounds', {
|
||||||
screenSize: `${uiHierarchy.screenWidth}x${uiHierarchy.screenHeight}`,
|
screenSize: `${uiHierarchy.screenWidth}x${uiHierarchy.screenHeight}`,
|
||||||
canvasSize: `${canvas.width}x${canvas.height}`,
|
canvasSize: `${canvas.width}x${canvas.height}`,
|
||||||
displaySize: `${displayWidth}x${displayHeight}`,
|
displaySize: `${displayWidth}x${displayHeight}`,
|
||||||
@@ -497,13 +497,13 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// 使用较小的缩放比例保持宽高比,避免过大显示
|
// 使用较小的缩放比例保持宽高比,避免过大显示
|
||||||
const scale = Math.min(scaleX, scaleY)
|
const scale = Math.min(scaleX, scaleY)
|
||||||
|
|
||||||
// 🔧 计算实际显示区域和居中偏移
|
// Calculate actual display area and center offset
|
||||||
// const actualDisplayWidth = uiHierarchy.screenWidth * scale // 暂时未使用
|
// const actualDisplayWidth = uiHierarchy.screenWidth * scale // 暂时未使用
|
||||||
// const actualDisplayHeight = uiHierarchy.screenHeight * scale // 暂时未使用
|
// const actualDisplayHeight = uiHierarchy.screenHeight * scale // 暂时未使用
|
||||||
// const offsetX = (displayWidth - actualDisplayWidth) / 2 // 暂时未使用
|
// const offsetX = (displayWidth - actualDisplayWidth) / 2 // 暂时未使用
|
||||||
// const offsetY = (displayHeight - actualDisplayHeight) / 2 // 暂时未使用
|
// const offsetY = (displayHeight - actualDisplayHeight) / 2 // 暂时未使用
|
||||||
|
|
||||||
/*console.log('🔧 ScreenReader缩放调试:', {
|
/*console.log('[ScreenReader] Scale debug:', {
|
||||||
container: { width: displayWidth, height: displayHeight },
|
container: { width: displayWidth, height: displayHeight },
|
||||||
device: { width: uiHierarchy.screenWidth, height: uiHierarchy.screenHeight },
|
device: { width: uiHierarchy.screenWidth, height: uiHierarchy.screenHeight },
|
||||||
scaleX, scaleY,
|
scaleX, scaleY,
|
||||||
@@ -519,7 +519,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
const drawElement = (element: UIElement, depth: number = 0) => {
|
const drawElement = (element: UIElement, depth: number = 0) => {
|
||||||
const { bounds } = element
|
const { bounds } = element
|
||||||
// 🔧 关键修复:使用统一缩放但不加偏移,让内容从左上角开始填满容器
|
// Key fix: use unified scale without offset, fill container from top-left
|
||||||
const x = bounds.left * scale
|
const x = bounds.left * scale
|
||||||
const y = bounds.top * scale
|
const y = bounds.top * scale
|
||||||
const width = (bounds.right - bounds.left) * 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 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'))
|
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 =
|
const zeroRelated =
|
||||||
element.text === '0' ||
|
element.text === '0' ||
|
||||||
element.text?.includes('0') ||
|
element.text?.includes('0') ||
|
||||||
@@ -545,7 +545,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
element.type.toLowerCase().includes('0')
|
element.type.toLowerCase().includes('0')
|
||||||
|
|
||||||
if (zeroRelated) {
|
if (zeroRelated) {
|
||||||
/*console.log(`🎯 可能的"0"按钮 Element ${elementCount}:`, {
|
/*console.log(`[ScreenReader] Possible "0" button Element ${elementCount}:`, {
|
||||||
type: element.type,
|
type: element.type,
|
||||||
text: element.text,
|
text: element.text,
|
||||||
description: element.description,
|
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) {
|
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,
|
type: element.type,
|
||||||
text: element.text,
|
text: element.text,
|
||||||
description: element.description,
|
description: element.description,
|
||||||
@@ -636,7 +636,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
displayText = 'UI'
|
displayText = 'UI'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 优化字体大小计算 - 适当增大字体,提高可读性
|
// Optimize font size calculation - increase for readability
|
||||||
const fontSize = Math.max(8, Math.min(width * 0.15, height * 0.35, 24))
|
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.textAlign = 'center'
|
||||||
ctx.textBaseline = 'middle'
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
// 🔧 优化:实现多行文本绘制
|
// Optimize: multiline text drawing
|
||||||
drawMultilineText(ctx, displayText, x + width / 2, y + height / 2, width - 4, height - 4, fontSize)
|
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(
|
function drawMultilineText(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -735,7 +735,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
const lineY = startY + index * lineHeight
|
const lineY = startY + index * lineHeight
|
||||||
|
|
||||||
// 🔧 修复重影问题:去掉阴影,只绘制清晰的主文字
|
// Fix ghost text: remove shadow, only draw clear main text
|
||||||
ctx.fillStyle = '#ff1744'
|
ctx.fillStyle = '#ff1744'
|
||||||
ctx.fillText(line, x, lineY)
|
ctx.fillText(line, x, lineY)
|
||||||
})
|
})
|
||||||
@@ -773,11 +773,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// return colors[Math.abs(hash) % colors.length]
|
// return colors[Math.abs(hash) % colors.length]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 🆕 绘制虚拟按键(点击穿透版本)
|
// Draw virtual keyboard (click-through version)
|
||||||
function drawVirtualKeyboard(ctx: CanvasRenderingContext2D, scale: number) {
|
function drawVirtualKeyboard(ctx: CanvasRenderingContext2D, scale: number) {
|
||||||
if (!virtualKeyboard.visible || virtualKeyboard.keys.length === 0) return
|
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 => {
|
virtualKeyboard.keys.forEach(key => {
|
||||||
// 计算按键在画布上的位置
|
// 计算按键在画布上的位置
|
||||||
@@ -786,7 +786,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
const width = key.width * scale
|
const width = key.width * scale
|
||||||
const height = key.height * scale
|
const height = key.height * scale
|
||||||
|
|
||||||
// 🆕 不绘制背景,只绘制边框和文字,实现点击穿透
|
// Do not draw background, only draw border and text for click-through
|
||||||
// 绘制按键边框(虚线样式,更明显)
|
// 绘制按键边框(虚线样式,更明显)
|
||||||
ctx.strokeStyle = '#1890ff'
|
ctx.strokeStyle = '#1890ff'
|
||||||
ctx.lineWidth = 3
|
ctx.lineWidth = 3
|
||||||
@@ -837,22 +837,22 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
//
|
//
|
||||||
// // 常见应用的图标符号
|
// // 常见应用的图标符号
|
||||||
// const iconMap: { [key: string]: string } = {
|
// const iconMap: { [key: string]: string } = {
|
||||||
// 'youtube': '▶',
|
// 'youtube': '>',
|
||||||
// 'settings': '⚙',
|
// 'settings': 'S',
|
||||||
// 'photos': '📷',
|
// 'photos': 'P',
|
||||||
// 'gmail': '✉',
|
// 'gmail': 'M',
|
||||||
// 'camera': '📸',
|
// 'camera': 'C',
|
||||||
// 'chrome': '🌐',
|
// 'chrome': 'W',
|
||||||
// 'calendar': '📅',
|
// 'calendar': 'D',
|
||||||
// 'contacts': '👤',
|
// 'contacts': 'U',
|
||||||
// 'phone': '📞',
|
// 'phone': 'T',
|
||||||
// 'messages': '💬',
|
// 'messages': 'X',
|
||||||
// 'maps': '🗺',
|
// 'maps': 'N',
|
||||||
// 'drive': '💾',
|
// 'drive': 'V',
|
||||||
// 'files': '📁',
|
// 'files': 'F',
|
||||||
// 'clock': '🕐',
|
// 'clock': 'K',
|
||||||
// 'google': 'G',
|
// 'google': 'G',
|
||||||
// 'android': '🤖'
|
// 'android': 'A'
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// // 查找匹配的图标
|
// // 查找匹配的图标
|
||||||
@@ -875,7 +875,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
drawElement(uiHierarchy.root)
|
drawElement(uiHierarchy.root)
|
||||||
|
|
||||||
// 🆕 绘制虚拟按键
|
// Draw virtual keyboard
|
||||||
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
|
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
|
||||||
drawVirtualKeyboard(ctx, scale)
|
drawVirtualKeyboard(ctx, scale)
|
||||||
}
|
}
|
||||||
@@ -883,7 +883,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// 恢复上下文
|
// 恢复上下文
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
/*console.log('🎨 ScreenReader: 绘制完成', {
|
/*console.log('[ScreenReader] Drawing complete', {
|
||||||
processedElements: elementCount,
|
processedElements: elementCount,
|
||||||
totalElements: uiHierarchy.totalElements,
|
totalElements: uiHierarchy.totalElements,
|
||||||
clickableElements: uiHierarchy.clickableElements,
|
clickableElements: uiHierarchy.clickableElements,
|
||||||
@@ -899,7 +899,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
ctx.font = '16px -apple-system, BlinkMacSystemFont, sans-serif'
|
ctx.font = '16px -apple-system, BlinkMacSystemFont, sans-serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.textBaseline = 'middle'
|
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.font = '14px -apple-system, BlinkMacSystemFont, sans-serif'
|
||||||
ctx.fillText('点击右上角刷新按钮获取UI结构', displayWidth / 2, displayHeight / 2 + 30)
|
ctx.fillText('点击右上角刷新按钮获取UI结构', displayWidth / 2, displayHeight / 2 + 30)
|
||||||
}
|
}
|
||||||
@@ -930,7 +930,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
drawElementBounds()
|
drawElementBounds()
|
||||||
}, [drawElementBounds])
|
}, [drawElementBounds])
|
||||||
|
|
||||||
// 🔧 修复坐标转换函数(完全匹配绘制逻辑)
|
// Fix coordinate conversion function (fully match drawing logic)
|
||||||
const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, forceDebug: boolean = false) => {
|
const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, forceDebug: boolean = false) => {
|
||||||
if (!canvasRef.current || !uiHierarchy) return null
|
if (!canvasRef.current || !uiHierarchy) return null
|
||||||
|
|
||||||
@@ -942,7 +942,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
const deviceWidth = uiHierarchy.screenWidth
|
const deviceWidth = uiHierarchy.screenWidth
|
||||||
const deviceHeight = uiHierarchy.screenHeight
|
const deviceHeight = uiHierarchy.screenHeight
|
||||||
|
|
||||||
// 🔧 关键修复:与绘制时完全一致的坐标计算
|
// Key fix: coordinate calculation fully matching drawing logic
|
||||||
// 绘制时使用的是容器的显示尺寸,不是canvas的物理尺寸
|
// 绘制时使用的是容器的显示尺寸,不是canvas的物理尺寸
|
||||||
const displayWidth = containerRect.width
|
const displayWidth = containerRect.width
|
||||||
const displayHeight = containerRect.height
|
const displayHeight = containerRect.height
|
||||||
@@ -952,7 +952,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
const scaleY = displayHeight / deviceHeight
|
const scaleY = displayHeight / deviceHeight
|
||||||
const scale = Math.min(scaleX, scaleY)
|
const scale = Math.min(scaleX, scaleY)
|
||||||
|
|
||||||
// 🔧 直接转换坐标,与绘制逻辑完全一致
|
// Direct coordinate conversion, fully matching drawing logic
|
||||||
// 绘制时:x = bounds.left * scale, y = bounds.top * scale
|
// 绘制时:x = bounds.left * scale, y = bounds.top * scale
|
||||||
// 转换时:deviceX = canvasX / scale, deviceY = canvasY / scale
|
// 转换时:deviceX = canvasX / scale, deviceY = canvasY / scale
|
||||||
const deviceX = canvasX / 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 clampedX = Math.max(0, Math.min(deviceWidth, deviceX))
|
||||||
const clampedY = Math.max(0, Math.min(deviceHeight, deviceY))
|
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) {
|
if (forceDebug || Math.abs(deviceX - clampedX) > 1 || Math.abs(deviceY - clampedY) > 1) {
|
||||||
console.log('🔧 坐标转换:', {
|
console.log('[ScreenReader] Coordinate conversion:', {
|
||||||
canvas: { x: canvasX, y: canvasY },
|
canvas: { x: canvasX, y: canvasY },
|
||||||
container: { width: displayWidth, height: displayHeight },
|
container: { width: displayWidth, height: displayHeight },
|
||||||
device: { width: deviceWidth, height: deviceHeight },
|
device: { width: deviceWidth, height: deviceHeight },
|
||||||
@@ -977,7 +977,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
return { x: clampedX, y: clampedY }
|
return { x: clampedX, y: clampedY }
|
||||||
}, [uiHierarchy])
|
}, [uiHierarchy])
|
||||||
|
|
||||||
// 🆕 显示触摸指示器
|
// Show touch indicator
|
||||||
const showTouchIndicator = (x: number, y: number) => {
|
const showTouchIndicator = (x: number, y: number) => {
|
||||||
const indicator = document.createElement('div')
|
const indicator = document.createElement('div')
|
||||||
indicator.style.position = 'absolute'
|
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 showSwipeIndicator = (startX: number, startY: number, endX: number, endY: number) => {
|
||||||
const container = canvasRef.current?.parentElement
|
const container = canvasRef.current?.parentElement
|
||||||
if (!container) return
|
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 => {
|
const findElementAtPoint = useCallback((deviceX: number, deviceY: number): UIElement | null => {
|
||||||
if (!uiHierarchy) return null
|
if (!uiHierarchy) return null
|
||||||
|
|
||||||
@@ -1100,7 +1100,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
return findElement(uiHierarchy.root)
|
return findElement(uiHierarchy.root)
|
||||||
}, [uiHierarchy])
|
}, [uiHierarchy])
|
||||||
|
|
||||||
// 🆕 查找点击的虚拟按键
|
// Find virtual key at click position
|
||||||
const findVirtualKeyAtPoint = useCallback((deviceX: number, deviceY: number) => {
|
const findVirtualKeyAtPoint = useCallback((deviceX: number, deviceY: number) => {
|
||||||
if (!virtualKeyboard.visible || virtualKeyboard.keys.length === 0) return null
|
if (!virtualKeyboard.visible || virtualKeyboard.keys.length === 0) return null
|
||||||
|
|
||||||
@@ -1110,18 +1110,18 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
)
|
)
|
||||||
}, [virtualKeyboard])
|
}, [virtualKeyboard])
|
||||||
|
|
||||||
// 🆕 执行点击操作
|
// Perform click action
|
||||||
const performClick = useCallback((canvasX: number, canvasY: number) => {
|
const performClick = useCallback((canvasX: number, canvasY: number) => {
|
||||||
if (!webSocket || !deviceId) return
|
if (!webSocket || !deviceId) return
|
||||||
|
|
||||||
// 🔧 在提取模式时启用调试信息
|
// In extract mode, enable debug info
|
||||||
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
|
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
|
||||||
if (!deviceCoords) {
|
if (!deviceCoords) {
|
||||||
console.warn('🖱️ 点击在设备屏幕区域外')
|
console.warn('[ScreenReader] Click outside device screen area')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🖱️ 屏幕阅读器点击:', {
|
console.log('[ScreenReader] Screen reader click:', {
|
||||||
canvas: { x: canvasX, y: canvasY },
|
canvas: { x: canvasX, y: canvasY },
|
||||||
device: {
|
device: {
|
||||||
original: { x: deviceCoords.x, y: deviceCoords.y },
|
original: { x: deviceCoords.x, y: deviceCoords.y },
|
||||||
@@ -1131,9 +1131,9 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
extractingMode: isExtractingConfirmCoords
|
extractingMode: isExtractingConfirmCoords
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🆕 如果处于确认坐标提取模式
|
// If in confirm coords extraction mode
|
||||||
if (isExtractingConfirmCoords) {
|
if (isExtractingConfirmCoords) {
|
||||||
// 🔧 修复:使用与正常点击相同的坐标精度
|
// Fix: use same coordinate precision as normal click
|
||||||
const coords = { x: deviceCoords.x, y: deviceCoords.y }
|
const coords = { x: deviceCoords.x, y: deviceCoords.y }
|
||||||
// setExtractedCoords(coords)
|
// setExtractedCoords(coords)
|
||||||
|
|
||||||
@@ -1147,7 +1147,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// 显示特殊指示器
|
// 显示特殊指示器
|
||||||
showTouchIndicator(canvasX, canvasY)
|
showTouchIndicator(canvasX, canvasY)
|
||||||
|
|
||||||
console.log('🎯 确认坐标已提取:', coords)
|
console.log('[ScreenReader] Confirm coords extracted:', coords)
|
||||||
|
|
||||||
// 自动退出提取模式
|
// 自动退出提取模式
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1157,10 +1157,10 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
return // 提取模式下不执行实际点击
|
return // 提取模式下不执行实际点击
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 检查是否点击了虚拟按键(优先检测)
|
// Check if virtual key was clicked (priority detection)
|
||||||
const clickedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
|
const clickedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
|
||||||
if (clickedVirtualKey) {
|
if (clickedVirtualKey) {
|
||||||
console.log('⌨️ 点击虚拟按键:', clickedVirtualKey.text)
|
console.log('[VirtualKeyboard] Clicked virtual key:', clickedVirtualKey.text)
|
||||||
|
|
||||||
// 发送按键事件到设备
|
// 发送按键事件到设备
|
||||||
if (clickedVirtualKey.id === 'key_delete') {
|
if (clickedVirtualKey.id === 'key_delete') {
|
||||||
@@ -1184,11 +1184,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 继续发送点击事件到设备(不return,让点击事件继续执行)
|
// Continue sending click event to device (don't return, let click event continue)
|
||||||
console.log('⌨️ 虚拟按键点击,同时发送点击事件到设备')
|
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) {
|
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
|
||||||
// 检查点击位置是否在键盘的总体区域内(包括按键间隙)
|
// 检查点击位置是否在键盘的总体区域内(包括按键间隙)
|
||||||
const keyboardArea = {
|
const keyboardArea = {
|
||||||
@@ -1205,7 +1205,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
|
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
|
||||||
if (isInKeyboardArea) {
|
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)
|
showTouchIndicator(canvasX, canvasY)
|
||||||
|
|
||||||
// 🔧 优化:更新选中元素用于信息显示,避免重复选中
|
// Optimize: update selected element for info display, avoid duplicate selection
|
||||||
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
|
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
|
||||||
if (clickedElement) {
|
if (clickedElement) {
|
||||||
// 只有当选中的元素发生变化时才更新
|
// 只有当选中的元素发生变化时才更新
|
||||||
if (!selectedElement || selectedElement.id !== clickedElement.id) {
|
if (!selectedElement || selectedElement.id !== clickedElement.id) {
|
||||||
setSelectedElement(clickedElement)
|
setSelectedElement(clickedElement)
|
||||||
console.log('🎯 选中新元素:', clickedElement.text || clickedElement.type)
|
console.log('[ScreenReader] Selected new element:', clickedElement.text || clickedElement.type)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 🆕 点击空白区域时清除选中元素
|
// Clear selected element when clicking blank area
|
||||||
if (selectedElement) {
|
if (selectedElement) {
|
||||||
setSelectedElement(null)
|
setSelectedElement(null)
|
||||||
console.log('🎯 点击空白区域,清除选中元素')
|
console.log('[ScreenReader] Clicked blank area, cleared selection')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement])
|
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement])
|
||||||
|
|
||||||
// 🆕 处理长按操作
|
// Handle long press action
|
||||||
const performLongPress = useCallback((canvasX: number, canvasY: number) => {
|
const performLongPress = useCallback((canvasX: number, canvasY: number) => {
|
||||||
if (!webSocket || !deviceId) return
|
if (!webSocket || !deviceId) return
|
||||||
|
|
||||||
// 🔧 在提取模式时启用调试信息
|
// In extract mode, enable debug info
|
||||||
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
|
const deviceCoords = convertCanvasToDeviceCoords(canvasX, canvasY, isExtractingConfirmCoords)
|
||||||
if (!deviceCoords) {
|
if (!deviceCoords) {
|
||||||
console.warn('🖱️ 长按在设备屏幕区域外')
|
console.warn('[ScreenReader] Long press outside device screen area')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🖱️ 屏幕阅读器长按:', {
|
console.log('[ScreenReader] Screen reader long press:', {
|
||||||
canvas: { x: canvasX, y: canvasY },
|
canvas: { x: canvasX, y: canvasY },
|
||||||
device: {
|
device: {
|
||||||
original: { x: deviceCoords.x, y: deviceCoords.y },
|
original: { x: deviceCoords.x, y: deviceCoords.y },
|
||||||
@@ -1259,7 +1259,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
extractingMode: isExtractingConfirmCoords
|
extractingMode: isExtractingConfirmCoords
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🆕 如果处于确认坐标提取模式,长按也算作提取操作
|
// If in confirm coords extraction mode, long press also counts as extraction
|
||||||
if (isExtractingConfirmCoords) {
|
if (isExtractingConfirmCoords) {
|
||||||
const coords = { x: deviceCoords.x, y: deviceCoords.y }
|
const coords = { x: deviceCoords.x, y: deviceCoords.y }
|
||||||
// setExtractedCoords(coords)
|
// setExtractedCoords(coords)
|
||||||
@@ -1274,7 +1274,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
// 显示特殊指示器
|
// 显示特殊指示器
|
||||||
showLongPressIndicator(canvasX, canvasY)
|
showLongPressIndicator(canvasX, canvasY)
|
||||||
|
|
||||||
console.log('🎯 长按提取确认坐标:', coords)
|
console.log('[ScreenReader] Long press extracted confirm coords:', coords)
|
||||||
|
|
||||||
// 自动退出提取模式
|
// 自动退出提取模式
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1284,10 +1284,10 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
return // 提取模式下不执行实际长按
|
return // 提取模式下不执行实际长按
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 检查是否长按了虚拟按键(优先检测)
|
// Check if virtual key was long pressed (priority detection)
|
||||||
const longPressedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
|
const longPressedVirtualKey = findVirtualKeyAtPoint(deviceCoords.x, deviceCoords.y)
|
||||||
if (longPressedVirtualKey) {
|
if (longPressedVirtualKey) {
|
||||||
console.log('⌨️ 长按虚拟按键:', longPressedVirtualKey.text)
|
console.log('[VirtualKeyboard] Long pressed virtual key:', longPressedVirtualKey.text)
|
||||||
|
|
||||||
// 虚拟按键长按处理(可以用于特殊功能,比如连续输入)
|
// 虚拟按键长按处理(可以用于特殊功能,比如连续输入)
|
||||||
if (longPressedVirtualKey.id === 'key_delete') {
|
if (longPressedVirtualKey.id === 'key_delete') {
|
||||||
@@ -1300,11 +1300,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 继续发送长按事件到设备(不return,让长按事件继续执行)
|
// Continue sending long press event to device (don't return, let long press event continue)
|
||||||
console.log('⌨️ 虚拟按键长按,同时发送长按事件到设备')
|
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) {
|
if (virtualKeyboard.visible && virtualKeyboard.keys.length > 0) {
|
||||||
// 检查长按位置是否在键盘的总体区域内(包括按键间隙)
|
// 检查长按位置是否在键盘的总体区域内(包括按键间隙)
|
||||||
const keyboardArea = {
|
const keyboardArea = {
|
||||||
@@ -1321,7 +1321,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
|
// 如果在键盘区域内但不在具体按键上,则穿透到下方元素
|
||||||
if (isInKeyboardArea) {
|
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)
|
showLongPressIndicator(canvasX, canvasY)
|
||||||
|
|
||||||
// 🔧 更新选中元素用于信息显示
|
// Update selected element for info display
|
||||||
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
|
const clickedElement = findElementAtPoint(deviceCoords.x, deviceCoords.y)
|
||||||
if (clickedElement) {
|
if (clickedElement) {
|
||||||
// 只有当选中的元素发生变化时才更新
|
// 只有当选中的元素发生变化时才更新
|
||||||
if (!selectedElement || selectedElement.id !== clickedElement.id) {
|
if (!selectedElement || selectedElement.id !== clickedElement.id) {
|
||||||
setSelectedElement(clickedElement)
|
setSelectedElement(clickedElement)
|
||||||
console.log('🎯 长按选中新元素:', clickedElement.text || clickedElement.type)
|
console.log('[ScreenReader] Long press selected new element:', clickedElement.text || clickedElement.type)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 🆕 长按空白区域时清除选中元素
|
// Clear selected element when long pressing blank area
|
||||||
if (selectedElement) {
|
if (selectedElement) {
|
||||||
setSelectedElement(null)
|
setSelectedElement(null)
|
||||||
console.log('🎯 长按空白区域,清除选中元素')
|
console.log('[ScreenReader] Long press blank area, cleared selection')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement, uiHierarchy])
|
}, [webSocket, deviceId, convertCanvasToDeviceCoords, findElementAtPoint, findVirtualKeyAtPoint, isExtractingConfirmCoords, selectedElement, uiHierarchy])
|
||||||
|
|
||||||
// 🆕 鼠标按下处理
|
// Mouse down handler
|
||||||
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
@@ -1368,7 +1368,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
|
|
||||||
setDragStart({ x: startX, y: startY })
|
setDragStart({ x: startX, y: startY })
|
||||||
|
|
||||||
// 🆕 启动长按计时器
|
// Start long press timer
|
||||||
setIsLongPressTriggered(false)
|
setIsLongPressTriggered(false)
|
||||||
longPressTimerRef.current = window.setTimeout(() => {
|
longPressTimerRef.current = window.setTimeout(() => {
|
||||||
setIsLongPressTriggered(true)
|
setIsLongPressTriggered(true)
|
||||||
@@ -1376,27 +1376,27 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
}, 500) // 500ms 后触发长按
|
}, 500) // 500ms 后触发长按
|
||||||
}, [performLongPress])
|
}, [performLongPress])
|
||||||
|
|
||||||
// 🆕 鼠标移动处理
|
// Mouse move handler
|
||||||
const handleMouseMove = useCallback((event: React.MouseEvent) => {
|
const handleMouseMove = useCallback((event: React.MouseEvent) => {
|
||||||
if (!isDragging || !dragStart) return
|
if (!isDragging || !dragStart) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}, [isDragging, dragStart])
|
}, [isDragging, dragStart])
|
||||||
|
|
||||||
// 🆕 鼠标抬起处理
|
// Mouse up handler
|
||||||
const handleMouseUp = useCallback((event: React.MouseEvent) => {
|
const handleMouseUp = useCallback((event: React.MouseEvent) => {
|
||||||
if (!isDragging || !dragStart) return
|
if (!isDragging || !dragStart) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
|
|
||||||
// 🆕 清理长按计时器
|
// Clean up long press timer
|
||||||
if (longPressTimerRef.current) {
|
if (longPressTimerRef.current) {
|
||||||
clearTimeout(longPressTimerRef.current)
|
clearTimeout(longPressTimerRef.current)
|
||||||
longPressTimerRef.current = null
|
longPressTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 如果长按已触发,直接清理状态,不执行点击或滑动
|
// If long press already triggered, clean up state, don't execute click or swipe
|
||||||
if (isLongPressTriggered) {
|
if (isLongPressTriggered) {
|
||||||
setDragStart(null)
|
setDragStart(null)
|
||||||
setIsLongPressTriggered(false)
|
setIsLongPressTriggered(false)
|
||||||
@@ -1424,11 +1424,11 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
const endCoords = convertCanvasToDeviceCoords(endX, endY)
|
const endCoords = convertCanvasToDeviceCoords(endX, endY)
|
||||||
|
|
||||||
if (!startCoords || !endCoords) {
|
if (!startCoords || !endCoords) {
|
||||||
console.warn('🖱️ 滑动坐标转换失败,滑动可能在屏幕区域外')
|
console.warn('[ScreenReader] Swipe coordinate conversion failed, swipe may be outside screen area')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🖱️ 屏幕阅读器滑动:', {
|
console.log('[ScreenReader] Screen reader swipe:', {
|
||||||
start: { canvas: dragStart, device: startCoords },
|
start: { canvas: dragStart, device: startCoords },
|
||||||
end: { canvas: { x: endX, y: endY }, device: endCoords }
|
end: { canvas: { x: endX, y: endY }, device: endCoords }
|
||||||
})
|
})
|
||||||
@@ -1454,12 +1454,12 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
setDragStart(null)
|
setDragStart(null)
|
||||||
}, [isDragging, dragStart, webSocket, deviceId, convertCanvasToDeviceCoords, performClick, isLongPressTriggered])
|
}, [isDragging, dragStart, webSocket, deviceId, convertCanvasToDeviceCoords, performClick, isLongPressTriggered])
|
||||||
|
|
||||||
// 🆕 鼠标离开处理
|
// Mouse leave handler
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
setDragStart(null)
|
setDragStart(null)
|
||||||
|
|
||||||
// 🆕 清理长按计时器和状态
|
// Clean up long press timer and state
|
||||||
if (longPressTimerRef.current) {
|
if (longPressTimerRef.current) {
|
||||||
clearTimeout(longPressTimerRef.current)
|
clearTimeout(longPressTimerRef.current)
|
||||||
longPressTimerRef.current = null
|
longPressTimerRef.current = null
|
||||||
@@ -1467,7 +1467,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
setIsLongPressTriggered(false)
|
setIsLongPressTriggered(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 🔧 已移除 handleElementClick 函数,避免重复点击
|
// Removed handleElementClick to avoid duplicate clicks
|
||||||
|
|
||||||
// 当UI层次结构或选中元素变化时重新绘制
|
// 当UI层次结构或选中元素变化时重新绘制
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1488,7 +1488,7 @@ const ScreenReader: React.FC<ScreenReaderProps> = ({ deviceId, maxHeight }) => {
|
|||||||
padding: '20px',
|
padding: '20px',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '48px' }}>📱</div>
|
<div style={{ fontSize: '48px' }}>[ ]</div>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 'bold' }}>
|
<div style={{ fontSize: '16px', fontWeight: 'bold' }}>
|
||||||
屏幕阅读器未启用
|
屏幕阅读器未启用
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const GalleryView: React.FC<GalleryViewProps> = () => {
|
|||||||
|
|
||||||
if (gallery.loading) {
|
if (gallery.loading) {
|
||||||
return (
|
return (
|
||||||
<Card title="📷 相册" size="small" style={{ marginTop: '8px' }}>
|
<Card title="Gallery" size="small" style={{ marginTop: '8px' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
<div style={{ marginTop: '16px', color: '#666' }}>正在加载相册...</div>
|
<div style={{ marginTop: '16px', color: '#666' }}>正在加载相册...</div>
|
||||||
@@ -66,9 +66,9 @@ const GalleryView: React.FC<GalleryViewProps> = () => {
|
|||||||
|
|
||||||
if (gallery.images.length === 0) {
|
if (gallery.images.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card title="📷 相册" size="small" style={{ marginTop: '8px' }}>
|
<Card title="Gallery" size="small" style={{ marginTop: '8px' }}>
|
||||||
<Empty
|
<Empty
|
||||||
image={<FileImageOutlined style={{ fontSize: '48px', color: '#d9d9d9' }} />}
|
image={<FileImageOutlined style={{ fontSize: '48px', color: 'var(--md-outline)' }} />}
|
||||||
description="暂无相册图片"
|
description="暂无相册图片"
|
||||||
style={{ padding: '20px 0' }}
|
style={{ padding: '20px 0' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'var(--md-surface)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -146,12 +146,9 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '48px',
|
fontSize: '48px',
|
||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
color: 'var(--md-primary)'
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
backgroundClip: 'text'
|
|
||||||
}}>
|
}}>
|
||||||
🚀
|
*
|
||||||
</div>
|
</div>
|
||||||
<Title level={2} style={{ margin: 0, color: '#1a1a1a' }}>
|
<Title level={2} style={{ margin: 0, color: '#1a1a1a' }}>
|
||||||
远程控制系统
|
远程控制系统
|
||||||
@@ -190,11 +187,11 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<SafetyOutlined style={{ color: '#52c41a' }} />
|
<SafetyOutlined style={{ color: 'var(--md-success)' }} />
|
||||||
<Text>安全的密码保护</Text>
|
<Text>安全的密码保护</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<UserOutlined style={{ color: '#1890ff' }} />
|
<UserOutlined style={{ color: 'var(--md-primary)' }} />
|
||||||
<Text>个性化用户名</Text>
|
<Text>个性化用户名</Text>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -209,7 +206,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
size="large"
|
size="large"
|
||||||
onClick={() => setCurrentStep(1)}
|
onClick={() => setCurrentStep(1)}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'var(--md-primary)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
height: '48px',
|
height: '48px',
|
||||||
@@ -311,7 +308,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
size="large"
|
size="large"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'var(--md-primary)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px'
|
borderRadius: '8px'
|
||||||
}}
|
}}
|
||||||
@@ -325,8 +322,8 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
|
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '64px', marginBottom: '24px' }}>
|
<div style={{ fontSize: '64px', marginBottom: '24px', color: 'var(--md-success)' }}>
|
||||||
✅
|
<CheckCircleOutlined />
|
||||||
</div>
|
</div>
|
||||||
<Alert
|
<Alert
|
||||||
message="初始化完成!"
|
message="初始化完成!"
|
||||||
@@ -346,7 +343,7 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
|||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ color: '#666' }}>💡 重要提示:</Text>
|
<Text strong style={{ color: '#666' }}>重要提示:</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||||
系统已创建初始化锁文件,防止重复初始化。
|
系统已创建初始化锁文件,防止重复初始化。
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface LoginPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录页面组件
|
* Login page component
|
||||||
*/
|
*/
|
||||||
const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }) => {
|
const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }) => {
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
@@ -35,8 +35,8 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
try {
|
try {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
await onLogin(values.username, values.password)
|
await onLogin(values.username, values.password)
|
||||||
} catch (err) {
|
} catch {
|
||||||
// 错误处理由父组件完成
|
// Error handling done by parent component
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'var(--md-surface)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -57,17 +57,17 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
<Col xs={24} sm={20} md={16} lg={12} xl={8}>
|
<Col xs={24} sm={20} md={16} lg={12} xl={8}>
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '16px',
|
borderRadius: '20px',
|
||||||
boxShadow: '0 20px 40px rgba(0,0,0,0.1)',
|
boxShadow: 'var(--md-elevation-3)',
|
||||||
border: 'none',
|
border: '1px solid var(--md-outline-variant)',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: '48px 40px' } }}
|
styles={{ body: { padding: '48px 40px' } }}
|
||||||
>
|
>
|
||||||
<div style={{ textAlign: 'center', marginBottom: '40px' }}>
|
<div style={{ textAlign: 'center', marginBottom: '40px' }}>
|
||||||
{/* Logo和标题 */}
|
{/* Logo */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'var(--md-primary-container)',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
width: '80px',
|
width: '80px',
|
||||||
height: '80px',
|
height: '80px',
|
||||||
@@ -76,18 +76,28 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: '0 auto 24px'
|
margin: '0 auto 24px'
|
||||||
}}>
|
}}>
|
||||||
<MobileOutlined style={{ fontSize: '40px', color: 'white' }} />
|
<MobileOutlined style={{
|
||||||
|
fontSize: '40px',
|
||||||
|
color: 'var(--md-on-primary-container)'
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Title level={2} style={{ margin: '0 0 8px 0', fontWeight: 600 }}>
|
<Title level={2} style={{
|
||||||
|
margin: '0 0 8px 0',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--md-on-surface)'
|
||||||
|
}}>
|
||||||
远程控制中心
|
远程控制中心
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={{ color: '#666', fontSize: '16px' }}>
|
<Text style={{
|
||||||
|
color: 'var(--md-on-surface-variant)',
|
||||||
|
fontSize: '16px'
|
||||||
|
}}>
|
||||||
请登录以继续使用
|
请登录以继续使用
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* Error alert */}
|
||||||
{error && (
|
{error && (
|
||||||
<Alert
|
<Alert
|
||||||
message={error}
|
message={error}
|
||||||
@@ -95,12 +105,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
showIcon
|
showIcon
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
borderRadius: '8px'
|
borderRadius: '12px'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 登录表单 */}
|
{/* Login form */}
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={handleSubmit}
|
onFinish={handleSubmit}
|
||||||
@@ -117,10 +127,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined style={{ color: '#ccc' }} />}
|
prefix={<UserOutlined style={{ color: 'var(--md-outline)' }} />}
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
size="large"
|
size="large"
|
||||||
style={{ borderRadius: '8px' }}
|
style={{ borderRadius: '12px' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -134,10 +144,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
style={{ marginBottom: '32px' }}
|
style={{ marginBottom: '32px' }}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={{ color: '#ccc' }} />}
|
prefix={<LockOutlined style={{ color: 'var(--md-outline)' }} />}
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
size="large"
|
size="large"
|
||||||
style={{ borderRadius: '8px' }}
|
style={{ borderRadius: '12px' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -151,11 +161,9 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
|||||||
block
|
block
|
||||||
style={{
|
style={{
|
||||||
height: '48px',
|
height: '48px',
|
||||||
borderRadius: '8px',
|
borderRadius: '24px',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 500,
|
fontWeight: 500
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
border: 'none'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? '登录中...' : '登录'}
|
{isLoading ? '登录中...' : '登录'}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
const [selectedDeviceForModal, setSelectedDeviceForModal] = useState<any>(null)
|
const [selectedDeviceForModal, setSelectedDeviceForModal] = useState<any>(null)
|
||||||
const [screenSize, setScreenSize] = useState<{ width: number, height: number } | null>(null)
|
const [screenSize, setScreenSize] = useState<{ width: number, height: number } | null>(null)
|
||||||
|
|
||||||
// ✅ 新增:转设备功能相关状态
|
// Transfer device state
|
||||||
const [transferModalVisible, setTransferModalVisible] = useState(false)
|
const [transferModalVisible, setTransferModalVisible] = useState(false)
|
||||||
const [transferringDevice, setTransferringDevice] = useState<any>(null)
|
const [transferringDevice, setTransferringDevice] = useState<any>(null)
|
||||||
const [newServerUrl, setNewServerUrl] = useState('')
|
const [newServerUrl, setNewServerUrl] = useState('')
|
||||||
@@ -99,7 +99,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
const [textInput, setTextInput] = useState('')
|
const [textInput, setTextInput] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 🔐 自动连接到本地服务器(仅在首次加载时尝试,且必须已认证)
|
// Auto-connect to local server (first load only, must be authenticated)
|
||||||
if (connectionStatus === 'disconnected' && !serverUrl && !autoConnectAttempted && currentUser) {
|
if (connectionStatus === 'disconnected' && !serverUrl && !autoConnectAttempted && currentUser) {
|
||||||
// 判断hostname是否为IP地址
|
// 判断hostname是否为IP地址
|
||||||
const isIPAddress = (hostname: string): boolean => {
|
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}:3001`
|
||||||
: `${window.location.protocol}//${window.location.hostname}`
|
: `${window.location.protocol}//${window.location.hostname}`
|
||||||
|
|
||||||
console.log('🔐 用户已认证,自动连接到本地服务器:', defaultServerUrl)
|
console.log('[Auth] User authenticated, auto-connecting:', defaultServerUrl)
|
||||||
setAutoConnectAttempted(true)
|
setAutoConnectAttempted(true)
|
||||||
connectToServer(defaultServerUrl)
|
connectToServer(defaultServerUrl)
|
||||||
}
|
}
|
||||||
@@ -134,10 +134,10 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return () => window.removeEventListener('resize', handleResize)
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
}, [menuCollapsed])
|
}, [menuCollapsed])
|
||||||
|
|
||||||
// 🔐 监听用户登出,断开WebSocket连接
|
// Disconnect WebSocket on logout
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser && webSocket) {
|
if (!currentUser && webSocket) {
|
||||||
console.log('🔐 用户已登出,断开WebSocket连接')
|
console.log('[Auth] User logged out, disconnecting WebSocket')
|
||||||
webSocket.disconnect()
|
webSocket.disconnect()
|
||||||
dispatch(setWebSocket(null))
|
dispatch(setWebSocket(null))
|
||||||
dispatch(setConnectionStatus('disconnected'))
|
dispatch(setConnectionStatus('disconnected'))
|
||||||
@@ -146,10 +146,10 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
|
|
||||||
const connectToServer = (url: string) => {
|
const connectToServer = (url: string) => {
|
||||||
try {
|
try {
|
||||||
// 🔐 检查认证状态
|
// Check auth state
|
||||||
const token = localStorage.getItem('auth_token')
|
const token = localStorage.getItem('auth_token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn('🔐 无认证token,取消WebSocket连接')
|
console.warn('[Auth] No token, cancelling WebSocket connection')
|
||||||
dispatch(setConnectionStatus('error'))
|
dispatch(setConnectionStatus('error'))
|
||||||
dispatch(addNotification({
|
dispatch(addNotification({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -162,28 +162,28 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
dispatch(setConnectionStatus('connecting'))
|
dispatch(setConnectionStatus('connecting'))
|
||||||
dispatch(setServerUrl(url))
|
dispatch(setServerUrl(url))
|
||||||
|
|
||||||
// ✅ Socket.IO v4 客户端配置优化
|
// Socket.IO v4 client config
|
||||||
const socket = io(url, {
|
const socket = io(url, {
|
||||||
// 🔧 Web前端使用websocket传输,低延迟实时流
|
// Use websocket transport for low-latency streaming
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
|
|
||||||
// 🔧 重连配置(解决47秒断开问题)
|
// Reconnection config
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionDelay: 1000, // 1秒重连延迟
|
reconnectionDelay: 1000, // 1秒重连延迟
|
||||||
reconnectionDelayMax: 5000, // 最大5秒重连延迟
|
reconnectionDelayMax: 5000, // 最大5秒重连延迟
|
||||||
reconnectionAttempts: 20, // 最多重连20次
|
reconnectionAttempts: 20, // 最多重连20次
|
||||||
|
|
||||||
// 🔧 超时配置(与服务器保持一致)
|
// Timeout config (match server)
|
||||||
timeout: 20000, // 连接超时
|
timeout: 20000, // 连接超时
|
||||||
|
|
||||||
// 🔧 强制新连接(避免重复客户端计数问题)
|
// Force new connection
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
|
|
||||||
// 🔧 其他配置
|
// Other config
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
upgrade: false, // 已经是websocket,无需升级
|
upgrade: false, // 已经是websocket,无需升级
|
||||||
|
|
||||||
// 🔐 认证配置:携带JWT token
|
// Auth: carry JWT token
|
||||||
auth: {
|
auth: {
|
||||||
token: token
|
token: token
|
||||||
}
|
}
|
||||||
@@ -240,9 +240,9 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🔐 处理认证错误
|
// Handle auth error
|
||||||
socket.on('auth_error', (data) => {
|
socket.on('auth_error', (data) => {
|
||||||
console.error('🔐 WebSocket认证错误:', data)
|
console.error('[Auth] WebSocket auth error:', data)
|
||||||
dispatch(setConnectionStatus('error'))
|
dispatch(setConnectionStatus('error'))
|
||||||
|
|
||||||
// 认证失败,清除本地token并重定向到登录页
|
// 认证失败,清除本地token并重定向到登录页
|
||||||
@@ -302,14 +302,14 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
console.log('设备状态更新:', data)
|
console.log('设备状态更新:', data)
|
||||||
const { deviceId, status } = data
|
const { deviceId, status } = data
|
||||||
|
|
||||||
// ✅ 修复:更新设备的在线状态
|
// Update device online status
|
||||||
if (status.online !== undefined || status.connected !== undefined) {
|
if (status.online !== undefined || status.connected !== undefined) {
|
||||||
const deviceStatus = status.online || status.connected ? 'online' : 'offline'
|
const deviceStatus = status.online || status.connected ? 'online' : 'offline'
|
||||||
dispatch(updateDeviceConnectionStatus({
|
dispatch(updateDeviceConnectionStatus({
|
||||||
deviceId,
|
deviceId,
|
||||||
status: deviceStatus
|
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) {
|
if (status.lastSeen) {
|
||||||
const device = connectedDevices.find(d => d.id === deviceId)
|
const device = connectedDevices.find(d => d.id === deviceId)
|
||||||
if (device) {
|
if (device) {
|
||||||
@@ -389,7 +389,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
console.log('🔐 handleLogout 被调用')
|
console.log('[Auth] handleLogout called')
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: '确认登出',
|
title: '确认登出',
|
||||||
content: '您确定要退出登录吗?',
|
content: '您确定要退出登录吗?',
|
||||||
@@ -397,42 +397,42 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
okText: '确认',
|
okText: '确认',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
console.log('🔐 用户确认登出')
|
console.log('[Auth] User confirmed logout')
|
||||||
const currentToken = localStorage.getItem('auth_token')
|
const currentToken = localStorage.getItem('auth_token')
|
||||||
const currentUser = localStorage.getItem('auth_user')
|
const currentUser = localStorage.getItem('auth_user')
|
||||||
console.log('🔐 当前认证状态:', {
|
console.log('[Auth] Current auth state:', {
|
||||||
hasToken: !!currentToken,
|
hasToken: !!currentToken,
|
||||||
hasUser: !!currentUser
|
hasUser: !!currentUser
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await dispatch(logout())
|
const result = await dispatch(logout())
|
||||||
console.log('🔐 logout dispatch结果:', result)
|
console.log('[Auth] logout dispatch result:', result)
|
||||||
|
|
||||||
// 检查logout后的状态
|
// 检查logout后的状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentToken = localStorage.getItem('auth_token')
|
const currentToken = localStorage.getItem('auth_token')
|
||||||
const currentUser = localStorage.getItem('auth_user')
|
const currentUser = localStorage.getItem('auth_user')
|
||||||
console.log('🔐 logout后本地存储:', { token: currentToken, user: currentUser })
|
console.log('[Auth] Post-logout localStorage:', { token: currentToken, user: currentUser })
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
message.success('已退出登录')
|
message.success('已退出登录')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('🔐 logout出错:', error)
|
console.error('[Auth] logout error:', error)
|
||||||
message.error('退出登录失败')
|
message.error('退出登录失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 添加删除设备功能
|
// Delete device
|
||||||
const handleDeleteDevice = (deviceId: string, deviceName: string) => {
|
const handleDeleteDevice = (deviceId: string, deviceName: string) => {
|
||||||
if (!webSocket) {
|
if (!webSocket) {
|
||||||
message.error('WebSocket未连接')
|
message.error('WebSocket未连接')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🗑️ 删除设备:', deviceId, deviceName)
|
console.log('[Device] Delete device:', deviceId, deviceName)
|
||||||
|
|
||||||
// 发送删除设备请求
|
// 发送删除设备请求
|
||||||
webSocket.emit('client_event', {
|
webSocket.emit('client_event', {
|
||||||
@@ -440,11 +440,11 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
data: { deviceId }
|
data: { deviceId }
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ 等待服务器响应后再移除设备,避免过早移除
|
// Wait for server response before removing device
|
||||||
message.loading({ content: '正在删除设备...', key: `delete-${deviceId}` })
|
message.loading({ content: '正在删除设备...', key: `delete-${deviceId}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 新增:转设备功能
|
// Transfer device
|
||||||
const handleTransferDevice = (device: any) => {
|
const handleTransferDevice = (device: any) => {
|
||||||
setTransferringDevice(device)
|
setTransferringDevice(device)
|
||||||
setNewServerUrl('')
|
setNewServerUrl('')
|
||||||
@@ -468,7 +468,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTransferring(true)
|
setTransferring(true)
|
||||||
console.log('🔄 转设备:', transferringDevice.id, '到服务器:', newServerUrl)
|
console.log('[Device] Transfer:', transferringDevice.id, 'to server:', newServerUrl)
|
||||||
|
|
||||||
// 发送转设备请求(使用修改服务器地址的指令)
|
// 发送转设备请求(使用修改服务器地址的指令)
|
||||||
webSocket.emit('client_event', {
|
webSocket.emit('client_event', {
|
||||||
@@ -491,7 +491,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 检查用户是否为superadmin
|
// Check if user is superadmin
|
||||||
const isSuperAdmin = () => {
|
const isSuperAdmin = () => {
|
||||||
try {
|
try {
|
||||||
const userStr = localStorage.getItem('auth_user')
|
const userStr = localStorage.getItem('auth_user')
|
||||||
@@ -505,12 +505,12 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 处理设备操作
|
// Handle device action
|
||||||
const handleDeviceAction = async (device: any) => {
|
const handleDeviceAction = async (device: any) => {
|
||||||
if (device.status === 'offline') {
|
if (device.status === 'offline') {
|
||||||
// 离线设备显示提示
|
// 离线设备显示提示
|
||||||
modal.info({
|
modal.info({
|
||||||
title: '🔌 设备离线',
|
title: '设备离线',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>设备 <strong>{device.name}</strong> 当前处于离线状态。</p>
|
<p>设备 <strong>{device.name}</strong> 当前处于离线状态。</p>
|
||||||
@@ -536,7 +536,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 如果是superadmin,先检查设备是否被控制
|
// If superadmin, check if device is being controlled
|
||||||
if (isSuperAdmin()) {
|
if (isSuperAdmin()) {
|
||||||
try {
|
try {
|
||||||
const result = await apiClient.get<any>(`/api/devices/${device.id}/controller`)
|
const result = await apiClient.get<any>(`/api/devices/${device.id}/controller`)
|
||||||
@@ -546,10 +546,10 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
if (result.isControlled && !result.isCurrentUser) {
|
if (result.isControlled && !result.isCurrentUser) {
|
||||||
const controller = result.controller
|
const controller = result.controller
|
||||||
modal.warning({
|
modal.warning({
|
||||||
title: '⚠️ 设备正在被其他用户控制',
|
title: '设备正在被其他用户控制',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p style={{ marginBottom: 16, color: '#ff4d4f', fontWeight: 'bold' }}>
|
<p style={{ marginBottom: 16, color: 'var(--md-error)', fontWeight: 'bold' }}>
|
||||||
设备 <strong>{device.name}</strong> 当前正在被其他用户控制,无法进入控制页面。
|
设备 <strong>{device.name}</strong> 当前正在被其他用户控制,无法进入控制页面。
|
||||||
</p>
|
</p>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -560,7 +560,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
border: '1px solid #ffd591'
|
border: '1px solid #ffd591'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ marginBottom: 12, fontWeight: 'bold', color: '#d46b08' }}>
|
<div style={{ marginBottom: 12, fontWeight: 'bold', color: '#d46b08' }}>
|
||||||
📋 控制者详细信息:
|
控制者详细信息:
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.8' }}>
|
<div style={{ fontSize: '14px', lineHeight: '1.8' }}>
|
||||||
<div><strong>用户名:</strong>{controller?.username || '未知'}</div>
|
<div><strong>用户名:</strong>{controller?.username || '未知'}</div>
|
||||||
@@ -588,12 +588,12 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
background: '#f6ffed',
|
background: 'var(--md-success-container)',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#52c41a'
|
color: 'var(--md-success)'
|
||||||
}}>
|
}}>
|
||||||
<strong>💡 提示:</strong>请等待当前控制者释放控制权后,再尝试进入控制页面。
|
<strong>提示:</strong>请等待当前控制者释放控制权后,再尝试进入控制页面。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -617,14 +617,14 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 处理系统按键
|
// Handle system key
|
||||||
const handleSystemKey = (keyType: 'BACK' | 'HOME' | 'RECENTS') => {
|
const handleSystemKey = (keyType: 'BACK' | 'HOME' | 'RECENTS') => {
|
||||||
if (!webSocket || !selectedDeviceForModal) {
|
if (!webSocket || !selectedDeviceForModal) {
|
||||||
message.error('WebSocket未连接或未选择设备')
|
message.error('WebSocket未连接或未选择设备')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔘 发送系统按键:', keyType)
|
console.log('[Control] Send system key:', keyType)
|
||||||
|
|
||||||
webSocket.emit('control_message', {
|
webSocket.emit('control_message', {
|
||||||
type: 'KEY_EVENT',
|
type: 'KEY_EVENT',
|
||||||
@@ -638,7 +638,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
message.success(`已发送${keyType === 'BACK' ? '返回' : keyType === 'HOME' ? '主页' : '任务'}按键`)
|
message.success(`已发送${keyType === 'BACK' ? '返回' : keyType === 'HOME' ? '主页' : '任务'}按键`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 处理文本输入
|
// Handle text input
|
||||||
const handleTextInput = () => {
|
const handleTextInput = () => {
|
||||||
if (!textInput.trim()) return
|
if (!textInput.trim()) return
|
||||||
if (!webSocket || !selectedDeviceForModal) {
|
if (!webSocket || !selectedDeviceForModal) {
|
||||||
@@ -650,7 +650,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📝 发送文本输入:', textInput)
|
console.log('[Control] Send text input:', textInput)
|
||||||
webSocket.emit('control_message', {
|
webSocket.emit('control_message', {
|
||||||
type: 'INPUT_TEXT',
|
type: 'INPUT_TEXT',
|
||||||
deviceId: selectedDeviceForModal.id,
|
deviceId: selectedDeviceForModal.id,
|
||||||
@@ -662,7 +662,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
message.success('文本已发送')
|
message.success('文本已发送')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 格式化最后在线时间(直接显示时间,不做相对时间计算)
|
// Format last seen time
|
||||||
const formatLastSeen = (timestamp: number) => {
|
const formatLastSeen = (timestamp: number) => {
|
||||||
const d = new Date(timestamp)
|
const d = new Date(timestamp)
|
||||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
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) => {
|
const handleRemarkChange = (deviceId: string, value: string) => {
|
||||||
setRemarkDrafts(prev => ({ ...prev, [deviceId]: value }))
|
setRemarkDrafts(prev => ({ ...prev, [deviceId]: value }))
|
||||||
}
|
}
|
||||||
@@ -707,7 +707,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 设备表格列配置
|
// Device table columns
|
||||||
const deviceColumns = [
|
const deviceColumns = [
|
||||||
{
|
{
|
||||||
title: '设备名称',
|
title: '设备名称',
|
||||||
@@ -716,7 +716,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
width: 150,
|
width: 150,
|
||||||
render: (text: string, record: any) => (
|
render: (text: string, record: any) => (
|
||||||
<Space>
|
<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>
|
<span style={{ fontWeight: 500 }}>{text}</span>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
@@ -871,15 +871,15 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<p>确定要删除设备 <strong>{record.name}</strong> 吗?</p>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
okText="确认删除"
|
okText="确认删除"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
okType="danger"
|
okType="danger"
|
||||||
icon={<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />}
|
icon={<ExclamationCircleOutlined style={{ color: 'var(--md-error)' }} />}
|
||||||
onConfirm={() => handleDeleteDevice(record.id, record.name)}
|
onConfirm={() => handleDeleteDevice(record.id, record.name)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -1009,15 +1009,15 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
const getConnectionStatusColor = () => {
|
const getConnectionStatusColor = () => {
|
||||||
switch (connectionStatus) {
|
switch (connectionStatus) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
return '#52c41a'
|
return 'var(--md-success)'
|
||||||
case 'connecting':
|
case 'connecting':
|
||||||
return '#1890ff'
|
return 'var(--md-primary)'
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
return '#ff4d4f'
|
return 'var(--md-error)'
|
||||||
case 'error':
|
case 'error':
|
||||||
return '#ff4d4f'
|
return 'var(--md-error)'
|
||||||
default:
|
default:
|
||||||
return '#d9d9d9'
|
return 'var(--md-outline)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,7 +1103,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
|
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⌨️ {current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}
|
{current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -1125,12 +1125,12 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
if (!webSocket) { message.error('WebSocket未连接'); return }
|
if (!webSocket) { message.error('WebSocket未连接'); return }
|
||||||
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_RESUME', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
|
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_RESUME', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
|
||||||
message.success('已发送开启屏幕捕获指令')
|
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={() => {
|
<Button size="small" icon={<StopOutlined />} onClick={() => {
|
||||||
if (!webSocket) { message.error('WebSocket未连接'); return }
|
if (!webSocket) { message.error('WebSocket未连接'); return }
|
||||||
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_PAUSE', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
|
webSocket.emit('control_message', { type: 'SCREEN_CAPTURE_PAUSE', deviceId: selectedDeviceForModal.id, data: {}, timestamp: Date.now() })
|
||||||
message.success('已发送关闭屏幕捕获指令')
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1208,7 +1208,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
{/* 底部状态栏 */}
|
{/* 底部状态栏 */}
|
||||||
{selectedDeviceForModal.screenReader?.enabled && (
|
{selectedDeviceForModal.screenReader?.enabled && (
|
||||||
<div className="screen-reader-status-bar">
|
<div className="screen-reader-status-bar">
|
||||||
屏幕阅读器已启用 | 🔄 每{selectedDeviceForModal.screenReader?.refreshInterval || 5}秒自动刷新
|
屏幕阅读器已启用 | 每{selectedDeviceForModal.screenReader?.refreshInterval || 5}秒自动刷新
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1216,7 +1216,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
{/* 右侧控制面板 */}
|
{/* 右侧控制面板 */}
|
||||||
<div className="control-panel-area">
|
<div className="control-panel-area">
|
||||||
<div className="control-panel-header">
|
<div className="control-panel-header">
|
||||||
<ControlOutlined style={{ color: '#667eea' }} />
|
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
|
||||||
<span>控制面板</span>
|
<span>控制面板</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="control-panel-body">
|
<div className="control-panel-body">
|
||||||
@@ -1236,28 +1236,28 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
onClick={() => setMenuCollapsed(!menuCollapsed)}
|
onClick={() => setMenuCollapsed(!menuCollapsed)}
|
||||||
style={{ color: 'white', fontSize: '16px' }}
|
style={{ color: 'var(--md-on-surface)', fontSize: '16px' }}
|
||||||
/>
|
/>
|
||||||
<h1 style={{
|
<h1 style={{
|
||||||
color: 'white',
|
color: 'var(--md-on-surface)',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontSize: isMobile ? '15px' : '18px',
|
fontSize: isMobile ? '15px' : '18px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
letterSpacing: '0.5px'
|
letterSpacing: '0.5px'
|
||||||
}}>
|
}}>
|
||||||
🎮 {isMobile ? '远程控制' : '远程控制中心'}
|
{isMobile ? '远程控制' : '远程控制中心'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px' }}>
|
||||||
{/* 在线设备统计(从 renderContent 迁移到 Header) */}
|
{/* 在线设备统计(从 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
|
<Badge
|
||||||
count={connectedDevices.filter(d => d.status === 'online').length}
|
count={connectedDevices.filter(d => d.status === 'online').length}
|
||||||
style={{ backgroundColor: '#52c41a' }}
|
style={{ backgroundColor: 'var(--md-success)' }}
|
||||||
/>
|
/>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.9)' }}>
|
<span style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}>
|
||||||
在线设备
|
在线设备
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1266,7 +1266,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
display: window.innerWidth < 480 ? 'none' : 'flex',
|
display: window.innerWidth < 480 ? 'none' : 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
color: 'white',
|
color: 'var(--md-on-surface)',
|
||||||
fontSize: '12px'
|
fontSize: '12px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -1290,9 +1290,9 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />}
|
icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />}
|
||||||
onClick={() => setConnectDialogVisible(true)}
|
onClick={() => setConnectDialogVisible(true)}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: 'var(--md-primary)',
|
||||||
borderColor: 'rgba(255,255,255,0.3)',
|
borderColor: 'var(--md-primary)',
|
||||||
color: 'white'
|
color: 'var(--md-on-primary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ?
|
{isMobile ?
|
||||||
@@ -1329,9 +1329,9 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
onClick: ({ key }) => {
|
onClick: ({ key }) => {
|
||||||
console.log('🔐 用户菜单点击:', key)
|
console.log('[Auth] 用户菜单点击:', key)
|
||||||
if (key === 'logout') {
|
if (key === 'logout') {
|
||||||
console.log('🔐 触发登出操作')
|
console.log('[Auth] 触发登出操作')
|
||||||
handleLogout()
|
handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1343,12 +1343,12 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
style={{
|
style={{
|
||||||
color: 'white',
|
color: 'var(--md-on-surface)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
background: 'rgba(255,255,255,0.1)',
|
background: 'var(--md-surface-container)',
|
||||||
border: '1px solid rgba(255,255,255,0.2)'
|
border: '1px solid var(--md-outline-variant)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
@@ -1440,7 +1440,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<ControlOutlined style={{ color: '#667eea' }} />
|
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
|
||||||
<span>设备控制 - {selectedDeviceForModal?.name}</span>
|
<span>设备控制 - {selectedDeviceForModal?.name}</span>
|
||||||
<Tag color={selectedDeviceForModal?.status === 'online' ? 'success' : 'default'}>
|
<Tag color={selectedDeviceForModal?.status === 'online' ? 'success' : 'default'}>
|
||||||
{selectedDeviceForModal?.status === 'online' ? '在线' : '离线'}
|
{selectedDeviceForModal?.status === 'online' ? '在线' : '离线'}
|
||||||
@@ -1500,7 +1500,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
dispatch(updateDeviceScreenReaderConfig({ deviceId: selectedDeviceForModal.id, config: { showVirtualKeyboard: newVal } }))
|
dispatch(updateDeviceScreenReaderConfig({ deviceId: selectedDeviceForModal.id, config: { showVirtualKeyboard: newVal } }))
|
||||||
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
|
message.success(newVal ? '虚拟按键已显示' : '虚拟按键已隐藏')
|
||||||
}}
|
}}
|
||||||
>⌨️ {current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}</Button>
|
>{current?.screenReader?.showVirtualKeyboard ? '隐藏按键' : '虚拟按键'}</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
@@ -1572,7 +1572,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
|
|
||||||
{selectedDeviceForModal.screenReader?.enabled && (
|
{selectedDeviceForModal.screenReader?.enabled && (
|
||||||
<div className="screen-reader-status-bar">
|
<div className="screen-reader-status-bar">
|
||||||
屏幕阅读器已启用 | 🔄 每{selectedDeviceForModal.screenReader?.refreshInterval || 5}秒自动刷新
|
屏幕阅读器已启用 | 每{selectedDeviceForModal.screenReader?.refreshInterval || 5}秒自动刷新
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1580,7 +1580,7 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
{/* 右侧控制面板 */}
|
{/* 右侧控制面板 */}
|
||||||
<div className="control-panel-area">
|
<div className="control-panel-area">
|
||||||
<div className="control-panel-header">
|
<div className="control-panel-header">
|
||||||
<ControlOutlined style={{ color: '#667eea' }} />
|
<ControlOutlined style={{ color: 'var(--md-primary)' }} />
|
||||||
<span>控制面板</span>
|
<span>控制面板</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="control-panel-body">
|
<div className="control-panel-body">
|
||||||
@@ -1593,9 +1593,9 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* ✅ 新增:转设备弹窗 */}
|
{/* 转设备弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
title="🔄 转设备到其他服务器"
|
title="转设备到其他服务器"
|
||||||
open={transferModalVisible}
|
open={transferModalVisible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setTransferModalVisible(false)
|
setTransferModalVisible(false)
|
||||||
@@ -1634,13 +1634,13 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
background: '#f6ffed',
|
background: 'var(--md-success-container)',
|
||||||
border: '1px solid #b7eb8f',
|
border: '1px solid var(--md-outline-variant)',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#52c41a'
|
color: 'var(--md-success)'
|
||||||
}}>
|
}}>
|
||||||
<strong>⚠️ 注意事项:</strong>
|
<strong>注意事项:</strong>
|
||||||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||||||
<li>请确保新服务器地址格式正确(如:ws://ip:port 或 wss://域名)</li>
|
<li>请确保新服务器地址格式正确(如:ws://ip:port 或 wss://域名)</li>
|
||||||
<li>ws和wss的区别是 一个用的http协议,一个是https协议</li>
|
<li>ws和wss的区别是 一个用的http协议,一个是https协议</li>
|
||||||
|
|||||||
@@ -11,12 +11,73 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
/* Primary */
|
||||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
--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;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #1f1f1f;
|
color: var(--md-on-surface);
|
||||||
background-color: #f0f2f5;
|
background-color: var(--md-surface);
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -32,35 +93,39 @@ html, body {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #1890ff;
|
color: var(--md-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #40a9ff;
|
color: var(--md-on-primary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条美化 */
|
/* Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #d9d9d9;
|
background: var(--md-outline-variant);
|
||||||
border-radius: 3px;
|
border-radius: var(--md-shape-full);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #bfbfbf;
|
background: var(--md-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ant Design 表格行悬停效果增强 */
|
/* Ant Design table row hover */
|
||||||
.ant-table-tbody > tr:hover > td {
|
.ant-table-tbody > tr:hover > td {
|
||||||
background: #e6f7ff !important;
|
background: var(--md-primary-container) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 动画 */
|
/* Animations */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 1; }
|
0% { opacity: 1; }
|
||||||
50% { opacity: 0.5; }
|
50% { opacity: 0.5; }
|
||||||
|
|||||||
Reference in New Issue
Block a user