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

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

View File

@@ -63,7 +63,7 @@ import {
useComposeRef,
useInsertStyles,
warning2 as warning
} from "./chunk-5Q2RTODE.js";
} from "./chunk-MBOWMX2L.js";
import {
require_react
} from "./chunk-NZP3G7XT.js";

View File

@@ -1,85 +1,85 @@
{
"hash": "3570225d",
"hash": "f188899e",
"configHash": "632304f4",
"lockfileHash": "80049c3e",
"browserHash": "517aea31",
"lockfileHash": "ee2683e5",
"browserHash": "11bb468f",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "2ffb555a",
"fileHash": "82b88bac",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "497e92e9",
"fileHash": "f147bcf0",
"needsInterop": true
},
"@reduxjs/toolkit": {
"src": "../../@reduxjs/toolkit/dist/redux-toolkit.modern.mjs",
"file": "@reduxjs_toolkit.js",
"fileHash": "326be4c8",
"fileHash": "e2e532ed",
"needsInterop": false
},
"react-redux": {
"src": "../../react-redux/dist/react-redux.mjs",
"file": "react-redux.js",
"fileHash": "c9e96044",
"fileHash": "49e0dcfd",
"needsInterop": false
},
"antd": {
"src": "../../antd/es/index.js",
"file": "antd.js",
"fileHash": "fbadf007",
"fileHash": "1c827006",
"needsInterop": false
},
"socket.io-client": {
"src": "../../socket.io-client/build/esm/index.js",
"file": "socket__io-client.js",
"fileHash": "3a7d980f",
"fileHash": "f1f126a4",
"needsInterop": false
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "771481b3",
"fileHash": "92554eaf",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "fba82193",
"fileHash": "c9146743",
"needsInterop": true
},
"@ant-design/icons": {
"src": "../../@ant-design/icons/es/index.js",
"file": "@ant-design_icons.js",
"fileHash": "6de612e4",
"fileHash": "5c57aafc",
"needsInterop": false
},
"antd/locale/zh_CN": {
"src": "../../antd/locale/zh_CN.js",
"file": "antd_locale_zh_CN.js",
"fileHash": "335584cf",
"fileHash": "731388b8",
"needsInterop": true
},
"dayjs": {
"src": "../../dayjs/dayjs.min.js",
"file": "dayjs.js",
"fileHash": "7f4736e7",
"fileHash": "581df681",
"needsInterop": true
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "f5462f19",
"fileHash": "f34fbc85",
"needsInterop": true
}
},
"chunks": {
"chunk-5Q2RTODE": {
"file": "chunk-5Q2RTODE.js"
"chunk-MBOWMX2L": {
"file": "chunk-MBOWMX2L.js"
},
"chunk-624ZMHH6": {
"file": "chunk-624ZMHH6.js"

2
node_modules/.vite/deps/antd.js generated vendored
View File

@@ -86,7 +86,7 @@ import {
useMemo,
warning,
warning_default
} from "./chunk-5Q2RTODE.js";
} from "./chunk-MBOWMX2L.js";
import {
require_dayjs_min
} from "./chunk-624ZMHH6.js";

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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