上传更改

This commit is contained in:
wdvipa
2026-02-25 00:48:34 +08:00
parent caf11b406a
commit 148b9e815e
20 changed files with 806 additions and 683 deletions

View File

@@ -7,37 +7,38 @@
flex-direction: column; flex-direction: column;
} }
/* Main layout */ /* ========== 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 var(--md-spacing-xl);
background: var(--md-surface-container-lowest); background: var(--md-surface-container-lowest);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
box-shadow: var(--md-elevation-1); box-shadow: var(--md-elevation-1);
flex-shrink: 0; flex-shrink: 0;
height: 64px; height: 56px;
line-height: 64px; line-height: 56px;
z-index: 10; z-index: 10;
border-bottom: 1px solid var(--md-outline-variant); border-bottom: 1px solid var(--md-outline-variant);
backdrop-filter: blur(8px);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.app-header { .app-header {
padding: 0 12px; padding: 0 var(--md-spacing-md);
height: 56px; height: 48px;
line-height: 56px; line-height: 48px;
} }
} }
/* Sidebar */ /* ========== Sidebar ========== */
.app-sider { .app-sider {
background: var(--md-surface-container-low) !important; background: var(--md-surface-container-low) !important;
border-right: 1px solid var(--md-outline-variant); border-right: 1px solid var(--md-outline-variant);
@@ -48,7 +49,23 @@
background: transparent; background: transparent;
} }
/* Content area */ .app-sider .ant-menu-item {
margin: var(--md-spacing-xs) var(--md-spacing-sm);
border-radius: var(--md-shape-md);
transition: all var(--md-transition-normal);
}
.app-sider .ant-menu-item:hover {
background: var(--md-primary-container);
}
.app-sider .ant-menu-item-selected {
background: var(--md-primary-container) !important;
color: var(--md-on-primary-container) !important;
font-weight: 500;
}
/* ========== Content Area ========== */
.app-content { .app-content {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -59,9 +76,9 @@
flex: 1; flex: 1;
} }
/* Device list page */ /* ========== Device List Page ========== */
.device-list-page { .device-list-page {
padding: 20px; padding: var(--md-spacing-lg);
background: var(--md-surface); background: var(--md-surface);
flex: 1; flex: 1;
display: flex; display: flex;
@@ -71,14 +88,14 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.device-list-page { .device-list-page {
padding: 12px; padding: var(--md-spacing-sm);
} }
} }
.device-list-card { .device-list-card {
background: var(--md-surface-container-lowest); background: var(--md-surface-container-lowest);
border-radius: var(--md-shape-lg); border-radius: var(--md-shape-lg);
padding: 20px; padding: var(--md-spacing-lg);
box-shadow: var(--md-elevation-1); box-shadow: var(--md-elevation-1);
flex: 1; flex: 1;
display: flex; display: flex;
@@ -87,36 +104,47 @@
border: 1px solid var(--md-outline-variant); border: 1px solid var(--md-outline-variant);
} }
/* Filter bar */ /* ========== Filter Bar ========== */
.device-filter-bar { .device-filter-bar {
background: var(--md-surface-container-low); background: var(--md-surface-container-low);
border-radius: var(--md-shape-sm); border-radius: var(--md-shape-md);
padding: 12px 16px; padding: var(--md-spacing-sm) var(--md-spacing-lg);
margin-bottom: 16px; margin-bottom: var(--md-spacing-md);
border: 1px solid var(--md-outline-variant); border: 1px solid var(--md-outline-variant);
transition: box-shadow var(--md-transition-normal);
}
.device-filter-bar:hover {
box-shadow: var(--md-elevation-1);
} }
.device-filter-bar .ant-form-inline { .device-filter-bar .ant-form-inline {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: var(--md-spacing-sm);
flex-wrap: wrap; flex-wrap: wrap;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.device-filter-bar .ant-form-inline .ant-form-item { .device-filter-bar .ant-form-inline .ant-form-item {
margin-bottom: 8px; margin-bottom: var(--md-spacing-sm);
} }
} }
/* Empty state */ @media (max-width: 768px) {
.device-filter-bar {
padding: var(--md-spacing-sm);
}
}
/* ========== Empty State ========== */
.device-empty-state { .device-empty-state {
text-align: center; text-align: center;
padding: 60px 20px; padding: var(--md-spacing-3xl) var(--md-spacing-xl);
color: var(--md-on-surface-variant); color: var(--md-on-surface-variant);
} }
/* Standalone control page */ /* ========== Standalone Control Page ========== */
.standalone-control-page { .standalone-control-page {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@@ -126,7 +154,7 @@
overflow: hidden; overflow: hidden;
} }
/* Control screen area */ /* ========== Control Screen Area ========== */
.control-screen-area { .control-screen-area {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -137,35 +165,36 @@
overflow: hidden; overflow: hidden;
} }
/* Toolbar */ /* ========== Toolbar ========== */
.control-toolbar { .control-toolbar {
padding: 6px 12px; padding: var(--md-spacing-xs) var(--md-spacing-md);
border-bottom: 1px solid var(--md-outline-variant); border-bottom: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low); background: var(--md-surface-container-low);
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: var(--md-spacing-sm);
flex-wrap: wrap; flex-wrap: wrap;
min-height: 40px; min-height: 38px;
} }
.control-toolbar .ant-btn { .control-toolbar .ant-btn {
font-size: 12px; font-size: var(--md-font-sm);
border-radius: var(--md-shape-sm);
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.control-toolbar { .control-toolbar {
padding: 4px 8px; padding: var(--md-spacing-xs) var(--md-spacing-sm);
} }
.control-toolbar .ant-btn { .control-toolbar .ant-btn {
font-size: 11px; font-size: var(--md-font-xs);
padding: 0 6px; padding: 0 6px;
} }
} }
/* Screen + reader horizontal layout */ /* ========== Screen + Reader Layout ========== */
.screen-reader-row { .screen-reader-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -193,29 +222,29 @@
flex-direction: column; flex-direction: column;
} }
/* Text input bar */ /* ========== Text Input Bar ========== */
.text-input-bar { .text-input-bar {
height: 44px; height: 40px;
border-top: 1px solid var(--md-outline-variant); border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-lowest); background: var(--md-surface-container-lowest);
padding: 6px 12px; padding: var(--md-spacing-xs) var(--md-spacing-md);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: var(--md-spacing-sm);
flex-shrink: 0; flex-shrink: 0;
} }
.text-input-bar input { .text-input-bar input {
flex: 1; flex: 1;
height: 32px; height: 30px;
padding: 0 12px; padding: 0 var(--md-spacing-md);
border: 1px solid var(--md-outline-variant); border: 1px solid var(--md-outline-variant);
border-radius: var(--md-shape-full); border-radius: var(--md-shape-full);
font-size: 13px; font-size: var(--md-font-base);
outline: none; outline: none;
background: var(--md-surface-container-low); background: var(--md-surface-container-low);
color: var(--md-on-surface); color: var(--md-on-surface);
transition: border-color 0.2s, box-shadow 0.2s; transition: border-color var(--md-transition-fast), box-shadow var(--md-transition-fast);
} }
.text-input-bar input:focus { .text-input-bar input:focus {
@@ -225,19 +254,20 @@
.text-input-bar input::placeholder { .text-input-bar input::placeholder {
color: var(--md-on-surface-variant); color: var(--md-on-surface-variant);
opacity: 0.6;
} }
.text-input-bar button { .text-input-bar button {
height: 32px; height: 30px;
padding: 0 16px; padding: 0 var(--md-spacing-lg);
border: none; border: none;
border-radius: var(--md-shape-full); border-radius: var(--md-shape-full);
background: var(--md-primary); background: var(--md-primary);
color: var(--md-on-primary); color: var(--md-on-primary);
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: var(--md-font-base);
font-weight: 500; font-weight: 500;
transition: background 0.2s, box-shadow 0.2s; transition: background var(--md-transition-fast), box-shadow var(--md-transition-fast);
} }
.text-input-bar button:hover:not(:disabled) { .text-input-bar button:hover:not(:disabled) {
@@ -251,25 +281,33 @@
cursor: not-allowed; cursor: not-allowed;
} }
/* System keys bar */ /* ========== System Keys Bar ========== */
.system-keys-bar { .system-keys-bar {
padding: 8px 12px; padding: var(--md-spacing-sm) var(--md-spacing-md);
border-top: 1px solid var(--md-outline-variant); border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-lowest); background: var(--md-surface-container-lowest);
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 10px; gap: var(--md-spacing-sm);
} }
.system-keys-bar .ant-btn { .system-keys-bar .ant-btn {
min-width: 72px; min-width: 68px;
height: 34px; height: 32px;
border-radius: var(--md-shape-full); border-radius: var(--md-shape-full);
font-size: 13px; font-size: var(--md-font-base);
border: 1px solid var(--md-outline-variant);
transition: all var(--md-transition-fast);
} }
/* Right control panel */ .system-keys-bar .ant-btn:hover {
border-color: var(--md-primary);
color: var(--md-primary);
background: var(--md-primary-container);
}
/* ========== Right Control Panel ========== */
.control-panel-area { .control-panel-area {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -280,15 +318,15 @@
} }
.control-panel-header { .control-panel-header {
padding: 10px 16px; padding: var(--md-spacing-sm) var(--md-spacing-lg);
border-bottom: 1px solid var(--md-outline-variant); border-bottom: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low); 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: var(--md-spacing-sm);
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: var(--md-font-md);
color: var(--md-on-surface); color: var(--md-on-surface);
} }
@@ -298,18 +336,18 @@
padding: 0; padding: 0;
} }
/* Bottom status bar */ /* ========== Status Bar ========== */
.screen-reader-status-bar { .screen-reader-status-bar {
padding: 3px 12px; padding: 2px var(--md-spacing-md);
border-top: 1px solid var(--md-outline-variant); border-top: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low); background: var(--md-surface-container-low);
font-size: 11px; font-size: var(--md-font-xs);
color: var(--md-on-surface-variant); color: var(--md-on-surface-variant);
text-align: center; text-align: center;
flex-shrink: 0; flex-shrink: 0;
} }
/* Mobile overlay */ /* ========== Mobile Overlay ========== */
.mobile-overlay { .mobile-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -319,9 +357,10 @@
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.25);
z-index: 999; z-index: 999;
animation: fadeIn 0.2s ease; animation: fadeIn 0.2s ease;
backdrop-filter: blur(2px);
} }
/* Mobile sidebar */ /* ========== Mobile Responsive ========== */
@media (max-width: 768px) { @media (max-width: 768px) {
.ant-layout-sider { .ant-layout-sider {
position: fixed !important; position: fixed !important;
@@ -329,15 +368,57 @@
z-index: 1000 !important; z-index: 1000 !important;
} }
.ant-layout-header { .ant-layout-header {
padding: 0 12px !important; padding: 0 var(--md-spacing-md) !important;
}
.system-keys-bar .ant-btn {
min-width: 56px;
height: 28px;
font-size: var(--md-font-sm);
}
.control-toolbar {
min-height: 34px;
} }
} }
/* Table responsive */ /* ========== Table Responsive ========== */
.ant-table-wrapper { .ant-table-wrapper {
overflow-x: auto; overflow-x: auto;
} }
.ant-table { .ant-table {
font-size: 13px; font-size: var(--md-font-base);
}
.ant-table-thead > tr > th {
font-weight: 500;
color: var(--md-on-surface-variant);
font-size: var(--md-font-sm);
text-transform: none;
letter-spacing: 0.02em;
}
.ant-table-tbody > tr > td {
padding: var(--md-spacing-sm) var(--md-spacing-md);
}
/* ========== Connection Status Dot ========== */
.connection-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.connection-status-dot.connected {
background: var(--md-success);
}
.connection-status-dot.connecting {
background: var(--md-primary);
animation: pulse 1.5s infinite;
}
.connection-status-dot.disconnected,
.connection-status-dot.error {
background: var(--md-error);
} }

View File

@@ -93,11 +93,11 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
const [configMaskStatus, setConfigMaskStatus] = useState('升级完成后将自动返回应用') const [configMaskStatus, setConfigMaskStatus] = useState('升级完成后将自动返回应用')
// const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) // 暂时未使用 // const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) // 暂时未使用
// ✅ 新增:服务器域名配置状态 // [NEW] 服务器域名配置状态
const [serverDomain, setServerDomain] = useState('') // 用户配置的服务器域名 const [serverDomain, setServerDomain] = useState('') // 用户配置的服务器域名
const [webUrl, setWebUrl] = useState('') // 用户配置的webUrlAndroid应用打开的网址 const [webUrl, setWebUrl] = useState('') // 用户配置的webUrlAndroid应用打开的网址
// ✅ 新增:Android端页面样式配置状态 // [NEW] Android端页面样式配置状态
const [pageStyleConfig, setPageStyleConfig] = useState({ const [pageStyleConfig, setPageStyleConfig] = useState({
appName: 'Android Remote Control', appName: 'Android Remote Control',
appIcon: null as File | null, appIcon: null as File | null,
@@ -109,7 +109,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
// const [showPageStyleConfig, setShowPageStyleConfig] = useState(false) // 暂时未使用 // const [showPageStyleConfig, setShowPageStyleConfig] = useState(false) // 暂时未使用
const [iconPreviewUrl, setIconPreviewUrl] = useState<string | null>(null) const [iconPreviewUrl, setIconPreviewUrl] = useState<string | null>(null)
// ✅ 新增:构建日志相关状态 // [NEW] 构建日志相关状态
interface LogEntry { interface LogEntry {
timestamp?: number timestamp?: number
level?: string level?: string
@@ -758,7 +758,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
<AndroidOutlined style={{ fontSize: '64px', color: 'white' }} /> <AndroidOutlined style={{ fontSize: '64px', color: 'white' }} />
</div> </div>
<Title level={2} style={{ color: 'white', margin: 0, marginBottom: 16 }}> <Title level={2} style={{ color: 'white', margin: 0, marginBottom: 16 }}>
📱 APK APK
</Title> </Title>
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: '18px', lineHeight: 1.6 }}> <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: '18px', lineHeight: 1.6 }}>
Android <br /> Android <br />
@@ -797,7 +797,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
</Col> </Col>
<Col span={20}> <Col span={20}>
<Title level={3} style={{ color: 'white', margin: 0, marginBottom: 20 }}> <Title level={3} style={{ color: 'white', margin: 0, marginBottom: 20 }}>
🚀 APK ... APK ...
</Title> </Title>
<div style={{ <div style={{
background: 'rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.1)',
@@ -840,7 +840,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
fontSize: '14px', fontSize: '14px',
textShadow: '0 1px 2px rgba(0,0,0,0.3)' textShadow: '0 1px 2px rgba(0,0,0,0.3)'
}}> }}>
🌐 : {serverDomain.trim() ? : {serverDomain.trim() ?
(serverDomain.startsWith('https://') ? 'wss://' + serverDomain.replace('https://', '') : (serverDomain.startsWith('https://') ? 'wss://' + serverDomain.replace('https://', '') :
serverDomain.startsWith('http://') ? 'ws://' + serverDomain.replace('http://', '') : serverDomain.startsWith('http://') ? 'ws://' + serverDomain.replace('http://', '') :
window.location.protocol === 'https:' ? 'wss://' + serverDomain : 'ws://' + serverDomain) : window.location.protocol === 'https:' ? 'wss://' + serverDomain : 'ws://' + serverDomain) :
@@ -855,7 +855,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
</Card> </Card>
)} )}
{/* ✅ 新增:构建配置选项 */} {/* [NEW] 构建配置选项 */}
<Card style={{ <Card style={{
borderRadius: '20px', borderRadius: '20px',
background: 'linear-gradient(135deg, #f8f9ff 0%, #fff 100%)', background: 'linear-gradient(135deg, #f8f9ff 0%, #fff 100%)',
@@ -1384,7 +1384,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
border: '1px solid rgba(102, 126, 234, 0.1)' border: '1px solid rgba(102, 126, 234, 0.1)'
}}> }}>
<Title level={3} style={{ marginBottom: 32, color: '#2c3e50' }}> <Title level={3} style={{ marginBottom: 32, color: '#2c3e50' }}>
🛠
</Title> </Title>
<Space size="large"> <Space size="large">
@@ -1407,7 +1407,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
> >
{building ? '🔄 构建中...' : '🚀 开始构建'} {building ? '构建中...' : '开始构建'}
</Button> </Button>
{data?.apkInfo?.exists && ( {data?.apkInfo?.exists && (
@@ -1429,7 +1429,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
> >
📥 APK APK
</Button> </Button>
</Space> </Space>
)} )}

View File

@@ -125,7 +125,7 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
</Form.Item> </Form.Item>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div style={{ marginBottom: '8px', color: '#666', fontSize: '14px' }}> <div style={{ marginBottom: '8px', color: 'var(--md-on-surface-variant)', fontSize: '14px' }}>
</div> </div>
<Space wrap> <Space wrap>

File diff suppressed because it is too large Load Diff

View File

@@ -127,7 +127,7 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
borderColor: screenReaderEnabled ? '#52c41a' : undefined borderColor: screenReaderEnabled ? '#52c41a' : undefined
}} }}
> >
📱 {screenReaderEnabled ? '增强版屏幕阅读器已启用' : '启用增强版屏幕阅读器'} {screenReaderEnabled ? '增强版屏幕阅读器已启用' : '启用增强版屏幕阅读器'}
</Button> </Button>
</Col> </Col>
<Col span={24}> <Col span={24}>
@@ -143,7 +143,7 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
borderColor: virtualKeyboardEnabled ? '#1890ff' : undefined borderColor: virtualKeyboardEnabled ? '#1890ff' : undefined
}} }}
> >
{virtualKeyboardEnabled ? '虚拟按键已显示' : '显示虚拟按键'} {virtualKeyboardEnabled ? '虚拟按键已显示' : '显示虚拟按键'}
</Button> </Button>
</Col> </Col>
</Row> </Row>
@@ -281,10 +281,10 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
<Col span={12}><Button block onClick={onSwipeRight}> </Button></Col> <Col span={12}><Button block onClick={onSwipeRight}> </Button></Col>
)} )}
{onPullDownLeft && ( {onPullDownLeft && (
<Col span={12}><Button block type="primary" onClick={onPullDownLeft}> </Button></Col> <Col span={12}><Button block type="primary" onClick={onPullDownLeft}></Button></Col>
)} )}
{onPullDownRight && ( {onPullDownRight && (
<Col span={12}><Button block type="primary" onClick={onPullDownRight}> </Button></Col> <Col span={12}><Button block type="primary" onClick={onPullDownRight}></Button></Col>
)} )}
</Row> </Row>
)} )}

View File

@@ -115,11 +115,11 @@ const DeviceFilterComponent: React.FC<DeviceFilterProps> = ({ onFilterChange, st
gap: '8px', gap: '8px',
marginRight: '16px', marginRight: '16px',
fontWeight: 500, fontWeight: 500,
color: '#262626' color: 'var(--md-on-surface)'
}}> }}>
<FilterOutlined style={{ color: 'var(--md-primary)' }} /> <FilterOutlined style={{ color: 'var(--md-primary)' }} />
<span style={{ fontSize: '12px', color: '#666' }}> <span style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}>
({getFilteredCount()}) ({getFilteredCount()})
</span> </span>
</div> </div>

View File

@@ -23,7 +23,7 @@ export const LogsCard: React.FC<LogsCardProps> = ({ onView, onClear }) => {
</Col> </Col>
</Row> </Row>
<div style={{ marginTop: 8, fontSize: 12, textAlign: 'center', color: '#666' }}> <div style={{ marginTop: 8, fontSize: 12, textAlign: 'center', color: '#666' }}>
💡
</div> </div>
</Card> </Card>
) )

View File

@@ -69,7 +69,7 @@ export const SmsControlCard: React.FC<SmsControlCardProps> = ({
return ( return (
<Card> <Card>
{/* 🆕 读取条数设置 */} {/* [NEW] 读取条数设置 */}
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}> <Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
<Col span={8}> <Col span={8}>
<span style={{ lineHeight: '32px', marginRight: 8 }}></span> <span style={{ lineHeight: '32px', marginRight: 8 }}></span>

View File

@@ -55,7 +55,7 @@ const CoordinateMappingStatus: React.FC<CoordinateMappingStatusProps> = ({
fontFamily: 'monospace' fontFamily: 'monospace'
}}> }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}> <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
📊
</div> </div>
<pre style={{ margin: 0, fontSize: '11px', whiteSpace: 'pre-wrap' }}> <pre style={{ margin: 0, fontSize: '11px', whiteSpace: 'pre-wrap' }}>
{performanceReport || '正在加载...'} {performanceReport || '正在加载...'}

View File

@@ -66,7 +66,7 @@ const DeviceCamera: React.FC<DeviceCameraProps> = ({ deviceId, onActiveChange })
} }
setImageSize({ width: img.width, height: img.height }) setImageSize({ width: img.width, height: img.height })
console.debug(` 成功绘制摄像头帧: ${img.width}x${img.height}, 格式: ${frameData.format}`) console.debug(`[OK] 成功绘制摄像头帧: ${img.width}x${img.height}, 格式: ${frameData.format}`)
} catch (drawError) { } catch (drawError) {
console.error('绘制摄像头图像失败:', drawError) console.error('绘制摄像头图像失败:', drawError)
} }

View File

@@ -6,9 +6,9 @@ import type { RootState } from '../../store/store'
/** 画质档位参考billd-desk的参数化控制 */ /** 画质档位参考billd-desk的参数化控制 */
const QUALITY_PROFILES = [ const QUALITY_PROFILES = [
{ key: 'low', label: '低画质', fps: 5, quality: 30, resolution: '360P' }, { key: 'low', label: '低画质', fps: 5, quality: 30, resolution: '360P' },
{ key: 'medium', label: '中画质', fps: 10, quality: 45, resolution: '480P' }, { key: 'medium', label: '中画质', fps: 15, quality: 45, resolution: '480P' },
{ key: 'high', label: '高画质', fps: 15, quality: 60, resolution: '720P' }, { key: 'high', label: '高画质', fps: 20, quality: 55, resolution: '720P' },
{ key: 'ultra', label: '超高画质', fps: 20, quality: 75, resolution: '1080P' }, { key: 'ultra', label: '超高画质', fps: 25, quality: 70, resolution: '1080P' },
] ]
interface DeviceScreenProps { interface DeviceScreenProps {
@@ -23,10 +23,14 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
// const dispatch = useDispatch<AppDispatch>() // const dispatch = useDispatch<AppDispatch>()
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
const fullscreenContainerRef = useRef<HTMLDivElement>(null) const fullscreenContainerRef = useRef<HTMLDivElement>(null)
const screenContainerRef = useRef<HTMLDivElement>(null)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [imageSize, setImageSize] = useState<{ width: number, height: number } | null>(null) const [imageSize, setImageSize] = useState<{ width: number, height: number } | null>(null)
const [isFullscreen, setIsFullscreen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false)
// 容器实际可用尺寸(通过 ResizeObserver 实时跟踪)
const [availableSize, setAvailableSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 })
// FPS 计算:使用滑动窗口统计真实帧率 // FPS 计算:使用滑动窗口统计真实帧率
const fpsFrameTimesRef = useRef<number[]>([]) const fpsFrameTimesRef = useRef<number[]>([])
const [displayFps, setDisplayFps] = useState(0) const [displayFps, setDisplayFps] = useState(0)
@@ -45,6 +49,12 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
// Base64预解码缓存将base64字符串提前转为Blob避免渲染时阻塞主线程 // Base64预解码缓存将base64字符串提前转为Blob避免渲染时阻塞主线程
const pendingBlobRef = useRef<Blob | null>(null) const pendingBlobRef = useRef<Blob | null>(null)
// 双缓冲离屏canvas避免直接修改可见canvas尺寸导致闪屏
const offscreenCanvasRef = useRef<HTMLCanvasElement | null>(null)
// 帧序号:防止异步解码导致旧帧覆盖新帧
const frameSeqRef = useRef(0)
// 添加控制权状态跟踪,避免重复申请 // 添加控制权状态跟踪,避免重复申请
const [isControlRequested, setIsControlRequested] = useState(false) const [isControlRequested, setIsControlRequested] = useState(false)
const [currentWebSocket, setCurrentWebSocket] = useState<any>(null) const [currentWebSocket, setCurrentWebSocket] = useState<any>(null)
@@ -150,30 +160,35 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
console.log(`[屏幕数据] 已收到 ${frameCountRef.current} 帧, 数据大小: ${dataLen}, 渲染中: ${isRenderingRef.current}, 解码中: ${decodingRef.current}`) console.log(`[屏幕数据] 已收到 ${frameCountRef.current} 帧, 数据大小: ${dataLen}, 渲染中: ${isRenderingRef.current}, 解码中: ${decodingRef.current}`)
} }
// 提前将Base64转为Blob避免渲染循环中阻塞主线程 // 高性能Base64解码直接atob+Uint8Array避免fetch(dataUri)的字符串拼接和网络栈开销
// fetch+data URI方式比atob+逐字节复制快得多,且不阻塞主线程
const format = data?.format ?? 'JPEG' const format = data?.format ?? 'JPEG'
const mimeType = `image/${format.toLowerCase()}` const mimeType = `image/${format.toLowerCase()}`
// 分配帧序号,防止异步解码乱序
const seq = ++frameSeqRef.current
if (typeof data.data === 'string') { if (typeof data.data === 'string') {
try { try {
const dataUri = `data:${mimeType};base64,${data.data}` // 直接二进制解码比fetch(dataUri)快避免拼接巨大的data URI字符串
fetch(dataUri) const binaryStr = atob(data.data)
.then(res => res.blob()) const len = binaryStr.length
.then(blob => { const bytes = new Uint8Array(len)
pendingBlobRef.current = blob for (let i = 0; i < len; i++) {
latestFrameRef.current = data bytes[i] = binaryStr.charCodeAt(i)
}
if (!isRenderingRef.current) { const blob = new Blob([bytes], { type: mimeType })
isRenderingRef.current = true
renderLatestFrame() // 只接受最新帧,丢弃已过时的帧
} if (seq < frameSeqRef.current) return
}) pendingBlobRef.current = blob
.catch(err => { latestFrameRef.current = data
console.error('Base64 pre-decode failed:', err)
}) if (!isRenderingRef.current) {
isRenderingRef.current = true
renderLatestFrame()
}
} catch (err) { } catch (err) {
console.error('Frame pre-decode error:', err) console.error('Frame decode error:', err)
} }
} else { } else {
pendingBlobRef.current = new Blob([data.data], { type: mimeType }) pendingBlobRef.current = new Blob([data.data], { type: mimeType })
@@ -200,8 +215,9 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
rafIdRef.current = 0 rafIdRef.current = 0
} }
isRenderingRef.current = false isRenderingRef.current = false
// 设备切换或断开时重置锁定尺寸,下次连接重新锁定 // 设备切换或断开时重置锁定尺寸和离屏canvas,下次连接重新锁定
lockedCanvasSizeRef.current = null lockedCanvasSizeRef.current = null
offscreenCanvasRef.current = null
} }
}, [webSocket, deviceId]) }, [webSocket, deviceId])
@@ -359,40 +375,58 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
.then(bitmap => { .then(bitmap => {
decodingRef.current = false decodingRef.current = false
const ctx = canvas.getContext('2d') // 双缓冲先在离屏canvas上绘制完整帧再一次性拷贝到可见canvas
if (!ctx) { // 避免直接修改可见canvas尺寸导致清空画布产生闪屏
// canvas尺寸锁定策略
const locked = lockedCanvasSizeRef.current
let targetW: number, targetH: number
if (!locked) {
targetW = bitmap.width
targetH = bitmap.height
lockedCanvasSizeRef.current = { width: targetW, height: targetH }
} else {
const needUpdate = bitmap.width > locked.width || bitmap.height > locked.height
if (needUpdate) {
targetW = Math.max(locked.width, bitmap.width)
targetH = Math.max(locked.height, bitmap.height)
lockedCanvasSizeRef.current = { width: targetW, height: targetH }
} else {
targetW = locked.width
targetH = locked.height
}
}
// 初始化或调整离屏canvas尺寸离屏canvas清空不影响用户可见画面
if (!offscreenCanvasRef.current) {
offscreenCanvasRef.current = document.createElement('canvas')
}
const offscreen = offscreenCanvasRef.current
if (offscreen.width !== targetW || offscreen.height !== targetH) {
offscreen.width = targetW
offscreen.height = targetH
}
// 在离屏canvas上绘制完整帧
const offCtx = offscreen.getContext('2d')
if (!offCtx) {
bitmap.close() bitmap.close()
rafIdRef.current = requestAnimationFrame(doRender) rafIdRef.current = requestAnimationFrame(doRender)
return return
} }
offCtx.drawImage(bitmap, 0, 0, targetW, targetH)
// canvas尺寸锁定策略(增强版): // 仅在尺寸真正变化时才修改可见canvas尺寸
// 取历史最大帧尺寸锁定canvas避免小帧导致画面缩小闪烁 if (canvas.width !== targetW || canvas.height !== targetH) {
// 当新帧比锁定尺寸大时,更新锁定尺寸(设备旋转等场景) canvas.width = targetW
const locked = lockedCanvasSizeRef.current canvas.height = targetH
if (!locked) {
// 首次帧锁定canvas尺寸
lockedCanvasSizeRef.current = { width: bitmap.width, height: bitmap.height }
canvas.width = bitmap.width
canvas.height = bitmap.height
} else {
// 后续帧:只在新帧更大时更新锁定尺寸,防止缩小闪烁
const needUpdate = bitmap.width > locked.width || bitmap.height > locked.height
if (needUpdate) {
const newW = Math.max(locked.width, bitmap.width)
const newH = Math.max(locked.height, bitmap.height)
lockedCanvasSizeRef.current = { width: newW, height: newH }
canvas.width = newW
canvas.height = newH
} else if (canvas.width !== locked.width || canvas.height !== locked.height) {
// canvas被外部重置了恢复锁定尺寸
canvas.width = locked.width
canvas.height = locked.height
}
} }
// 始终将bitmap绘制到整个canvas区域浏览器自动缩放 // 一次性将离屏canvas内容拷贝到可见canvas无闪烁
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height) const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(offscreen, 0, 0)
}
// 使用锁定的canvas尺寸上报保持稳定 // 使用锁定的canvas尺寸上报保持稳定
const reportSize = lockedCanvasSizeRef.current const reportSize = lockedCanvasSizeRef.current
@@ -428,36 +462,6 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
doRender() doRender()
}, []) }, [])
const drawFitMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => {
const scale = Math.min(canvas.width / img.width, canvas.height / img.height)
const w = img.width * scale
const h = img.height * scale
const x = (canvas.width - w) / 2
const y = (canvas.height - h) / 2
// 只清除图像未覆盖的边缘区域避免全画布clearRect导致闪烁
if (x > 0 || y > 0) {
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
ctx.drawImage(img, x, y, w, h)
}
const drawFillMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => {
const scale = Math.max(canvas.width / img.width, canvas.height / img.height)
const x = (canvas.width - img.width * scale) / 2
const y = (canvas.height - img.height * scale) / 2
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
}
const drawStretchMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
const drawOriginalMode = (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, canvas: HTMLCanvasElement) => {
const x = (canvas.width - img.width) / 2
const y = (canvas.height - img.height) / 2
ctx.drawImage(img, x, y)
}
// 坐标转换函数将canvas坐标转换为设备坐标 // 坐标转换函数将canvas坐标转换为设备坐标
const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, canvas: HTMLCanvasElement, device: any) => { const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, canvas: HTMLCanvasElement, device: any) => {
@@ -1100,39 +1104,53 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
} }
}, []) }, [])
// 全屏模式下计算 canvas 的 CSS 尺寸(保持宽高比,适配屏幕) // ResizeObserver实时跟踪屏幕容器的可用尺寸
// 使用 state 存储全屏容器尺寸确保全屏切换和窗口resize时触发重渲染 useEffect(() => {
const [containerSize, setContainerSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 }) const container = screenContainerRef.current
if (!container) return
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
setAvailableSize({ w: width, h: height })
}
}
})
ro.observe(container)
return () => ro.disconnect()
}, [])
// 全屏时额外监听 window resize部分浏览器全屏动画后尺寸才稳定
useEffect(() => { useEffect(() => {
if (!isFullscreen) return if (!isFullscreen) return
const updateSize = () => { const sync = () => setAvailableSize({ w: window.innerWidth, h: window.innerHeight })
setContainerSize({ w: window.innerWidth, h: window.innerHeight }) sync()
} const t = setTimeout(sync, 120)
// 全屏后立即更新 + 延迟更新(部分浏览器全屏动画完成后尺寸才稳定) window.addEventListener('resize', sync)
updateSize() return () => { clearTimeout(t); window.removeEventListener('resize', sync) }
const timer = setTimeout(updateSize, 100)
window.addEventListener('resize', updateSize)
return () => {
clearTimeout(timer)
window.removeEventListener('resize', updateSize)
}
}, [isFullscreen]) }, [isFullscreen])
// 计算 canvas CSS 尺寸:始终保持宽高比,自适应容器
const getCanvasStyle = useCallback((): React.CSSProperties => { const getCanvasStyle = useCallback((): React.CSSProperties => {
if (!isFullscreen || !imageSize || containerSize.w === 0) { if (!imageSize) {
return { return { width: '100%', height: '100%' }
width: imageSize ? `${imageSize.width}px` : '100%',
height: imageSize ? `${imageSize.height}px` : 'auto',
}
} }
// 全屏时canvas 按宽高比缩放填满屏幕
const scale = Math.min(containerSize.w / imageSize.width, containerSize.h / imageSize.height) // 使用全屏尺寸或容器尺寸
const cw = isFullscreen ? (availableSize.w || window.innerWidth) : availableSize.w
const ch = isFullscreen ? (availableSize.h || window.innerHeight) : availableSize.h
if (cw <= 0 || ch <= 0) {
return { width: '100%', height: 'auto' }
}
const scale = Math.min(cw / imageSize.width, ch / imageSize.height)
return { return {
width: `${Math.round(imageSize.width * scale)}px`, width: `${Math.round(imageSize.width * scale)}px`,
height: `${Math.round(imageSize.height * scale)}px`, height: `${Math.round(imageSize.height * scale)}px`,
} }
}, [isFullscreen, imageSize, containerSize]) }, [isFullscreen, imageSize, availableSize])
@@ -1160,73 +1178,77 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
</style> </style>
<div <div
ref={fullscreenContainerRef} ref={(el) => {
style={{ // 同时绑定两个 ref
position: 'relative', (fullscreenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
width: '100%', ;(screenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
}}
style={{
position: 'relative',
width: '100%',
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: isFullscreen ? '#000' : undefined, backgroundColor: isFullscreen ? '#000' : '#1a1a2e',
}} }}
> >
{/* 顶部信息栏 - 屏幕阅读器模式下隐藏 */}
{/* 操作状态指示器 */} {/* 操作状态指示器 */}
{!operationEnabled && ( {!operationEnabled && (
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: '20px', top: '12px',
right: '20px', right: '12px',
zIndex: 20, zIndex: 20,
color: '#fff', color: '#fff',
backgroundColor: 'rgba(255, 77, 79, 0.9)', backgroundColor: 'rgba(255, 77, 79, 0.85)',
padding: '8px 16px', padding: '6px 14px',
borderRadius: '4px', borderRadius: '6px',
fontSize: '14px', fontSize: '12px',
fontWeight: 'bold', fontWeight: 600,
border: '2px solid #ff4d4f', border: '1px solid rgba(255, 77, 79, 0.6)',
animation: 'pulse 2s infinite' animation: 'pulse 2s infinite',
backdropFilter: 'blur(4px)',
}}> }}>
[LOCKED]
</div> </div>
)} )}
{isLoading && ( {isLoading && (
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 10 zIndex: 10,
textAlign: 'center',
}}> }}>
<Spin size="large" /> <Spin size="large" />
<div style={{ color: '#fff', marginTop: '16px' }}> <div style={{ color: 'rgba(255,255,255,0.7)', marginTop: '12px', fontSize: '13px' }}>
... ...
</div> </div>
</div> </div>
)} )}
{/* Canvas wrapper - position:relative so touch/swipe indicators are positioned relative to canvas */} {/* Canvas wrapper */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative', lineHeight: 0 }}>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
style={{ style={{
...getCanvasStyle(), ...getCanvasStyle(),
cursor: 'pointer', cursor: 'pointer',
display: 'block', display: 'block',
}} borderRadius: isFullscreen ? 0 : '4px',
onMouseDown={handleMouseDown} boxShadow: isFullscreen ? 'none' : '0 2px 16px rgba(0,0,0,0.3)',
onMouseUp={handleMouseUp} }}
onMouseMove={handleMouseMove} onMouseDown={handleMouseDown}
onMouseLeave={handleMouseLeave} onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()} onMouseMove={handleMouseMove}
/> onMouseLeave={handleMouseLeave}
onContextMenu={(e) => e.preventDefault()}
/>
</div> </div>
{/* 画质控制面板 + 全屏按钮 */} {/* 画质控制面板 + 全屏按钮 */}

View File

@@ -136,8 +136,8 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
style={{ style={{
width: '100%', width: '100%',
maxWidth: '500px', maxWidth: '500px',
borderRadius: '16px', borderRadius: 'var(--md-shape-xl)',
boxShadow: '0 8px 32px rgba(0,0,0,0.12)', boxShadow: 'var(--md-elevation-4)',
border: 'none' border: 'none'
}} }}
styles={{ body: { padding: '40px' } }} styles={{ body: { padding: '40px' } }}

View File

@@ -47,7 +47,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100vh',
background: 'var(--md-surface)', background: 'linear-gradient(135deg, var(--md-surface) 0%, var(--md-primary-container) 50%, var(--md-secondary-container) 100%)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -57,41 +57,45 @@ 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: '20px', borderRadius: 'var(--md-shape-xl)',
boxShadow: 'var(--md-elevation-3)', boxShadow: 'var(--md-elevation-4)',
border: '1px solid var(--md-outline-variant)', border: 'none',
overflow: 'hidden' overflow: 'hidden',
backdropFilter: 'blur(20px)',
background: 'rgba(255, 255, 255, 0.92)'
}} }}
styles={{ body: { padding: '48px 40px' } }} styles={{ body: { padding: '48px 40px' } }}
> >
<div style={{ textAlign: 'center', marginBottom: '40px' }}> <div style={{ textAlign: 'center', marginBottom: '36px' }}>
{/* Logo */} {/* Logo */}
<div style={{ <div style={{
background: 'var(--md-primary-container)', background: 'linear-gradient(135deg, var(--md-primary) 0%, var(--md-secondary) 100%)',
borderRadius: '50%', borderRadius: '50%',
width: '80px', width: '72px',
height: '80px', height: '72px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
margin: '0 auto 24px' margin: '0 auto 20px',
boxShadow: '0 4px 16px rgba(212, 160, 168, 0.3)'
}}> }}>
<MobileOutlined style={{ <MobileOutlined style={{
fontSize: '40px', fontSize: '36px',
color: 'var(--md-on-primary-container)' color: 'var(--md-on-primary)'
}} /> }} />
</div> </div>
<Title level={2} style={{ <Title level={3} style={{
margin: '0 0 8px 0', margin: '0 0 6px 0',
fontWeight: 600, fontWeight: 600,
color: 'var(--md-on-surface)' color: 'var(--md-on-surface)',
letterSpacing: '0.5px'
}}> }}>
</Title> </Title>
<Text style={{ <Text style={{
color: 'var(--md-on-surface-variant)', color: 'var(--md-on-surface-variant)',
fontSize: '16px' fontSize: 'var(--md-font-md)'
}}> }}>
使 使
</Text> </Text>
@@ -104,8 +108,8 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
type="error" type="error"
showIcon showIcon
style={{ style={{
marginBottom: '24px', marginBottom: 'var(--md-spacing-xl)',
borderRadius: '12px' borderRadius: 'var(--md-shape-md)'
}} }}
/> />
)} )}
@@ -120,7 +124,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
> >
<Form.Item <Form.Item
name="username" name="username"
label={<span style={{ fontWeight: 500 }}></span>} label={<span style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}></span>}
rules={[ rules={[
{ required: true, message: '请输入用户名' }, { required: true, message: '请输入用户名' },
{ min: 2, message: '用户名至少2个字符' } { min: 2, message: '用户名至少2个字符' }
@@ -130,24 +134,24 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
prefix={<UserOutlined style={{ color: 'var(--md-outline)' }} />} prefix={<UserOutlined style={{ color: 'var(--md-outline)' }} />}
placeholder="请输入用户名" placeholder="请输入用户名"
size="large" size="large"
style={{ borderRadius: '12px' }} style={{ borderRadius: 'var(--md-shape-md)', height: '44px' }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="password" name="password"
label={<span style={{ fontWeight: 500 }}></span>} label={<span style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}></span>}
rules={[ rules={[
{ required: true, message: '请输入密码' }, { required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' } { min: 6, message: '密码至少6个字符' }
]} ]}
style={{ marginBottom: '32px' }} style={{ marginBottom: 'var(--md-spacing-2xl)' }}
> >
<Input.Password <Input.Password
prefix={<LockOutlined style={{ color: 'var(--md-outline)' }} />} prefix={<LockOutlined style={{ color: 'var(--md-outline)' }} />}
placeholder="请输入密码" placeholder="请输入密码"
size="large" size="large"
style={{ borderRadius: '12px' }} style={{ borderRadius: 'var(--md-shape-md)', height: '44px' }}
/> />
</Form.Item> </Form.Item>
@@ -160,10 +164,13 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
loading={isLoading} loading={isLoading}
block block
style={{ style={{
height: '48px', height: '46px',
borderRadius: '24px', borderRadius: 'var(--md-shape-xl)',
fontSize: '16px', fontSize: 'var(--md-font-lg)',
fontWeight: 500 fontWeight: 500,
background: 'linear-gradient(135deg, var(--md-primary) 0%, #c48e96 100%)',
border: 'none',
boxShadow: '0 4px 12px rgba(212, 160, 168, 0.3)'
}} }}
> >
{isLoading ? '登录中...' : '登录'} {isLoading ? '登录中...' : '登录'}

View File

@@ -987,9 +987,9 @@ const RemoteControlApp: React.FC = () => {
/> />
) : ( ) : (
<div className="device-empty-state"> <div className="device-empty-state">
<MobileOutlined style={{ fontSize: '56px', marginBottom: '16px', color: 'var(--md-outline-variant)' }} /> <MobileOutlined style={{ fontSize: '48px', marginBottom: 'var(--md-spacing-lg)', color: 'var(--md-outline-variant)' }} />
<div style={{ fontSize: '16px', marginBottom: '8px', color: 'var(--md-on-surface-variant)' }}></div> <div style={{ fontSize: 'var(--md-font-lg)', marginBottom: 'var(--md-spacing-sm)', color: 'var(--md-on-surface-variant)' }}></div>
<div style={{ fontSize: '13px', marginBottom: '20px' }}> <div style={{ fontSize: 'var(--md-font-base)', marginBottom: 'var(--md-spacing-xl)' }}>
</div> </div>
<Button <Button
@@ -1030,17 +1030,17 @@ const RemoteControlApp: React.FC = () => {
case 'settings': case 'settings':
return ( return (
<div style={{ <div style={{
padding: '24px', padding: 'var(--md-spacing-xl)',
flex: 1, flex: 1,
background: 'var(--md-surface-container-lowest)', background: 'var(--md-surface-container-lowest)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexDirection: 'column', flexDirection: 'column',
gap: '12px' gap: 'var(--md-spacing-md)'
}}> }}>
<SettingOutlined style={{ fontSize: '56px', color: 'var(--md-outline-variant)' }} /> <SettingOutlined style={{ fontSize: '48px', color: 'var(--md-outline-variant)' }} />
<div style={{ fontSize: '16px', color: 'var(--md-on-surface-variant)' }}>...</div> <div style={{ fontSize: 'var(--md-font-lg)', color: 'var(--md-on-surface-variant)' }}>...</div>
</div> </div>
) )
default: default:
@@ -1048,20 +1048,6 @@ const RemoteControlApp: React.FC = () => {
} }
} }
const getConnectionStatusColor = () => {
switch (connectionStatus) {
case 'connected':
return 'var(--md-success)'
case 'connecting':
return 'var(--md-primary)'
case 'disconnected':
return 'var(--md-error)'
case 'error':
return 'var(--md-error)'
default:
return 'var(--md-outline)'
}
}
const getConnectionStatusText = () => { const getConnectionStatusText = () => {
switch (connectionStatus) { switch (connectionStatus) {
@@ -1167,12 +1153,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: 'var(--md-success)', borderColor: 'var(--md-success)', color: 'var(--md-on-primary)' }}></Button> }} style={{ background: 'var(--md-success)', borderColor: 'var(--md-success)', color: '#fff' }}></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: 'var(--md-warning)', borderColor: 'var(--md-warning)', color: 'var(--md-on-primary)' }}></Button> }} style={{ background: 'var(--md-warning)', borderColor: 'var(--md-warning)', color: '#fff' }}></Button>
</div> </div>
</div> </div>
@@ -1273,57 +1259,46 @@ const RemoteControlApp: React.FC = () => {
<Layout className="app-layout"> <Layout className="app-layout">
{/* 顶部导航栏 */} {/* 顶部导航栏 */}
<Header className="app-header"> <Header className="app-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--md-spacing-md)' }}>
<Button <Button
type="text" type="text"
icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setMenuCollapsed(!menuCollapsed)} onClick={() => setMenuCollapsed(!menuCollapsed)}
style={{ color: 'var(--md-on-surface)', fontSize: '16px' }} style={{ color: 'var(--md-on-surface)', fontSize: 'var(--md-font-lg)' }}
/> />
<h1 style={{ <h1 style={{
color: 'var(--md-on-surface)', color: 'var(--md-on-surface)',
margin: 0, margin: 0,
fontSize: isMobile ? '15px' : '18px', fontSize: isMobile ? 'var(--md-font-md)' : 'var(--md-font-xl)',
fontWeight: 600, fontWeight: 600,
letterSpacing: '0.5px' letterSpacing: '0.3px'
}}> }}>
{isMobile ? '远程控制' : '远程控制中心'} {isMobile ? '远程控制' : '远程控制中心'}
</h1> </h1>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 'var(--md-spacing-sm)' : 'var(--md-spacing-md)' }}>
{/* 在线设备统计(从 renderContent 迁移到 Header */} {/* Online device count */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--md-on-surface)' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--md-spacing-xs)', 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: 'var(--md-success)' }} style={{ backgroundColor: 'var(--md-success)' }}
/> />
{!isMobile && ( {!isMobile && (
<span style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}> <span style={{ fontSize: 'var(--md-font-sm)', color: 'var(--md-on-surface-variant)' }}>
线 线
</span> </span>
)} )}
</div> </div>
<div style={{ <div style={{
display: window.innerWidth < 480 ? 'none' : 'flex', display: window.innerWidth < 480 ? 'none' : 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: 'var(--md-spacing-sm)',
color: 'var(--md-on-surface)', color: 'var(--md-on-surface)',
fontSize: '12px' fontSize: 'var(--md-font-sm)'
}}> }}>
<div style={{ <div className={`connection-status-dot ${connectionStatus}`} />
width: '8px',
height: '8px',
borderRadius: '50%',
background: getConnectionStatusColor(),
animation: connectionStatus === 'connecting' ? 'pulse 1.5s infinite' : 'none'
}} />
{isMobile ? getConnectionStatusText().slice(0, 2) : getConnectionStatusText()} {isMobile ? getConnectionStatusText().slice(0, 2) : getConnectionStatusText()}
{connectionStatus === 'connected' && serverUrl && !isMobile && (
<div style={{ fontSize: '10px', opacity: 0.8 }}>
{/* {new URL(serverUrl).host} */}
</div>
)}
</div> </div>
<Button <Button
@@ -1331,11 +1306,6 @@ const RemoteControlApp: React.FC = () => {
size={isMobile ? 'small' : 'middle'} size={isMobile ? 'small' : 'middle'}
icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />} icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />}
onClick={() => setConnectDialogVisible(true)} onClick={() => setConnectDialogVisible(true)}
style={{
background: 'var(--md-primary)',
borderColor: 'var(--md-primary)',
color: 'var(--md-on-primary)'
}}
> >
{isMobile ? {isMobile ?
(connectionStatus === 'connected' ? '重连' : '连接') : (connectionStatus === 'connected' ? '重连' : '连接') :
@@ -1343,18 +1313,18 @@ const RemoteControlApp: React.FC = () => {
} }
</Button> </Button>
{/* 用户菜单 */} {/* User menu */}
<Dropdown <Dropdown
menu={{ menu={{
items: [ items: [
{ {
key: 'user-info', key: 'user-info',
label: ( label: (
<div style={{ padding: '8px 0', borderBottom: '1px solid var(--md-outline-variant)' }}> <div style={{ padding: 'var(--md-spacing-sm) 0', borderBottom: '1px solid var(--md-outline-variant)' }}>
<div style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}> <div style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}>
{currentUser?.username || 'Unknown'} {currentUser?.username || 'Unknown'}
</div> </div>
<div style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}> <div style={{ fontSize: 'var(--md-font-sm)', color: 'var(--md-on-surface-variant)' }}>
</div> </div>
</div> </div>
@@ -1371,9 +1341,9 @@ const RemoteControlApp: React.FC = () => {
} }
], ],
onClick: ({ key }) => { onClick: ({ key }) => {
console.log('[Auth] 用户菜单点击:', key) console.log('[Auth] User menu click:', key)
if (key === 'logout') { if (key === 'logout') {
console.log('[Auth] 触发登出操作') console.log('[Auth] Trigger logout')
handleLogout() handleLogout()
} }
} }
@@ -1388,9 +1358,10 @@ const RemoteControlApp: React.FC = () => {
color: 'var(--md-on-surface)', color: 'var(--md-on-surface)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', gap: 'var(--md-spacing-xs)',
background: 'var(--md-surface-container)', background: 'var(--md-surface-container)',
border: '1px solid var(--md-outline-variant)' border: '1px solid var(--md-outline-variant)',
borderRadius: 'var(--md-shape-full)'
}} }}
> >
<UserOutlined /> <UserOutlined />
@@ -1435,12 +1406,12 @@ const RemoteControlApp: React.FC = () => {
}} }}
> >
<div style={{ <div style={{
padding: '16px', padding: 'var(--md-spacing-lg)',
borderBottom: '1px solid var(--md-outline-variant)', borderBottom: '1px solid var(--md-outline-variant)',
textAlign: 'center' textAlign: 'center'
}}> }}>
{!menuCollapsed && ( {!menuCollapsed && (
<div style={{ color: 'var(--md-on-surface-variant)', fontSize: '14px' }}> <div style={{ color: 'var(--md-on-surface-variant)', fontSize: 'var(--md-font-md)' }}>
</div> </div>
)} )}

View File

@@ -67,10 +67,33 @@ html, body {
--md-shape-lg: 16px; --md-shape-lg: 16px;
--md-shape-xl: 28px; --md-shape-xl: 28px;
--md-shape-full: 9999px; --md-shape-full: 9999px;
/* Spacing */
--md-spacing-xs: 4px;
--md-spacing-sm: 8px;
--md-spacing-md: 12px;
--md-spacing-lg: 16px;
--md-spacing-xl: 24px;
--md-spacing-2xl: 32px;
--md-spacing-3xl: 48px;
/* Typography */
--md-font-xs: 11px;
--md-font-sm: 12px;
--md-font-base: 13px;
--md-font-md: 14px;
--md-font-lg: 16px;
--md-font-xl: 18px;
--md-font-2xl: 24px;
--md-font-3xl: 32px;
/* Elevation */ /* Elevation */
--md-elevation-0: none;
--md-elevation-1: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.06); --md-elevation-1: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.06);
--md-elevation-2: 0 1px 2px rgba(0,0,0,0.05), 0 2px 6px rgba(0,0,0,0.08); --md-elevation-2: 0 1px 2px rgba(0,0,0,0.05), 0 2px 6px rgba(0,0,0,0.08);
--md-elevation-3: 0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.08); --md-elevation-3: 0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.08);
--md-elevation-4: 0 2px 4px rgba(0,0,0,0.06), 0 8px 24px rgba(0,0,0,0.10);
/* Transition */
--md-transition-fast: 0.15s ease;
--md-transition-normal: 0.2s ease;
--md-transition-slow: 0.3s ease;
font-family: 'Google Sans', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, font-family: 'Google Sans', 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans SC', sans-serif; 'Noto Sans SC', sans-serif;
@@ -95,6 +118,7 @@ a {
font-weight: 500; font-weight: 500;
color: var(--md-primary); color: var(--md-primary);
text-decoration: none; text-decoration: none;
transition: color var(--md-transition-fast);
} }
a:hover { a:hover {
@@ -103,8 +127,8 @@ a:hover {
/* Scrollbar */ /* Scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 5px;
height: 6px; height: 5px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@@ -114,17 +138,30 @@ a:hover {
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--md-outline-variant); background: var(--md-outline-variant);
border-radius: var(--md-shape-full); border-radius: var(--md-shape-full);
transition: background var(--md-transition-fast);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--md-outline); background: var(--md-outline);
} }
/* Ant Design table row hover */ /* Ant Design overrides */
.ant-table-tbody > tr:hover > td { .ant-table-tbody > tr:hover > td {
background: var(--md-primary-container) !important; background: var(--md-primary-container) !important;
} }
.ant-btn {
transition: all var(--md-transition-normal);
}
.ant-card {
transition: box-shadow var(--md-transition-normal);
}
.ant-tag {
border: none;
}
/* Animations */ /* Animations */
@keyframes pulse { @keyframes pulse {
0% { opacity: 1; } 0% { opacity: 1; }
@@ -141,3 +178,8 @@ a:hover {
from { transform: translateX(-100%); } from { transform: translateX(-100%); }
to { transform: translateX(0); } to { transform: translateX(0); }
} }
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}

View File

@@ -30,19 +30,19 @@ export interface Device {
inputBlocked?: boolean inputBlocked?: boolean
screenReader?: DeviceScreenReaderConfig screenReader?: DeviceScreenReaderConfig
publicIP?: string publicIP?: string
// 🆕 新增系统版本信息字段 // [NEW] 新增系统版本信息字段
systemVersionName?: string // 如"Android 11"、"Android 12" systemVersionName?: string // 如"Android 11"、"Android 12"
romType?: string // 如"MIUI"、"ColorOS"、"原生Android" romType?: string // 如"MIUI"、"ColorOS"、"原生Android"
romVersion?: string // 如"MIUI 12.5"、"ColorOS 11.1" romVersion?: string // 如"MIUI 12.5"、"ColorOS 11.1"
osBuildVersion?: string // 如"1.0.19.0.UMCCNXM"等完整构建版本号 osBuildVersion?: string // 如"1.0.19.0.UMCCNXM"等完整构建版本号
// 🆕 新增APP和锁屏状态字段 // [NEW] 新增APP和锁屏状态字段
appName?: string // 当前运行的APP名称 appName?: string // 当前运行的APP名称
appVersion?: string // 当前运行的APP版本 appVersion?: string // 当前运行的APP版本
appPackage?: string // 当前运行的APP包名 appPackage?: string // 当前运行的APP包名
isLocked?: boolean // 设备锁屏状态 isLocked?: boolean // 设备锁屏状态
// 🆕 安装时间(连接时间) // [NEW] 安装时间(连接时间)
connectedAt?: number // 安装时间/首次连接时间(毫秒时间戳) connectedAt?: number // 安装时间/首次连接时间(毫秒时间戳)
// 🆕 备注字段 // [NEW] 备注字段
remark?: string // 设备备注 remark?: string // 设备备注
} }

View File

@@ -36,7 +36,7 @@ export class CoordinateMapper {
displayHeight: number, displayHeight: number,
deviceInfo?: DeviceInfo deviceInfo?: DeviceInfo
): CoordinateTestResult { ): CoordinateTestResult {
console.log('🧪 开始坐标映射准确性测试') console.log('[Test] 开始坐标映射准确性测试')
// 生成测试点 // 生成测试点
const testPoints = [ const testPoints = [
@@ -102,7 +102,7 @@ export class CoordinateMapper {
testPoints: results testPoints: results
} }
console.log('🧪 坐标映射测试结果:', { console.log('[Test] 坐标映射测试结果:', {
success: result.success, success: result.success,
accuracy: `${accuracy.toFixed(2)}%`, accuracy: `${accuracy.toFixed(2)}%`,
avgError: `${avgError.toFixed(2)}px`, avgError: `${avgError.toFixed(2)}px`,
@@ -205,14 +205,14 @@ export class CoordinateMapper {
): string { ): string {
const testResult = this.testCoordinateMapping(deviceWidth, deviceHeight, displayWidth, displayHeight, deviceInfo) const testResult = this.testCoordinateMapping(deviceWidth, deviceHeight, displayWidth, displayHeight, deviceInfo)
let report = `📊 坐标映射诊断报告\n` let report = `[Report] 坐标映射诊断报告\n`
report += `==========================================\n` report += `==========================================\n`
report += `设备分辨率: ${deviceWidth}x${deviceHeight}\n` report += `设备分辨率: ${deviceWidth}x${deviceHeight}\n`
report += `显示分辨率: ${displayWidth}x${displayHeight}\n` report += `显示分辨率: ${displayWidth}x${displayHeight}\n`
report += `设备信息: ${deviceInfo ? '已提供' : '未提供'}\n` report += `设备信息: ${deviceInfo ? '已提供' : '未提供'}\n`
report += `\n` report += `\n`
report += `测试结果:\n` report += `测试结果:\n`
report += `- 测试状态: ${testResult.success ? ' 通过' : ' 失败'}\n` report += `- 测试状态: ${testResult.success ? '[OK] 通过' : '[FAIL] 失败'}\n`
report += `- 准确度: ${testResult.accuracy.toFixed(2)}%\n` report += `- 准确度: ${testResult.accuracy.toFixed(2)}%\n`
report += `- 平均误差: ${testResult.avgError.toFixed(2)}px\n` report += `- 平均误差: ${testResult.avgError.toFixed(2)}px\n`
report += `- 最大误差: ${testResult.maxError.toFixed(2)}px\n` report += `- 最大误差: ${testResult.maxError.toFixed(2)}px\n`
@@ -220,7 +220,7 @@ export class CoordinateMapper {
report += `\n` report += `\n`
if (!testResult.success) { if (!testResult.success) {
report += ` 问题点分析:\n` report += `[FAIL] 问题点分析:\n`
testResult.testPoints testResult.testPoints
.filter(p => p.error > 5) .filter(p => p.error > 5)
.forEach((p, i) => { .forEach((p, i) => {
@@ -229,7 +229,7 @@ export class CoordinateMapper {
} }
if (deviceInfo) { if (deviceInfo) {
report += `\n📱 设备特征:\n` report += `\n[Device] 设备特征:\n`
report += `- 密度: ${deviceInfo.density}\n` report += `- 密度: ${deviceInfo.density}\n`
report += `- DPI: ${deviceInfo.densityDpi}\n` report += `- DPI: ${deviceInfo.densityDpi}\n`
report += `- 长宽比: ${deviceInfo.aspectRatio.toFixed(3)}\n` report += `- 长宽比: ${deviceInfo.aspectRatio.toFixed(3)}\n`

View File

@@ -32,10 +32,10 @@ export const DEFAULT_COORDINATE_MAPPING_CONFIG: CoordinateMappingConfig = {
enableBasicMapping: true, enableBasicMapping: true,
// 高精度功能(逐步启用) // 高精度功能(逐步启用)
enableSubPixelPrecision: false, // 🚩 第一阶段:关闭 enableSubPixelPrecision: false, // [Phase1] 关闭
enableNavigationBarDetection: true, // 相对安全 enableNavigationBarDetection: true, // [OK] 相对安全
enableDensityCorrection: true, // 相对安全 enableDensityCorrection: true, // [OK] 相对安全
enableAspectRatioCorrection: true, // 相对安全 enableAspectRatioCorrection: true, // [OK] 相对安全
// 调试功能 // 调试功能
enableDetailedLogging: false, // 默认关闭,需要时开启 enableDetailedLogging: false, // 默认关闭,需要时开启
@@ -47,7 +47,7 @@ export const DEFAULT_COORDINATE_MAPPING_CONFIG: CoordinateMappingConfig = {
maxCoordinateError: 10, // 允许10像素误差 maxCoordinateError: 10, // 允许10像素误差
// 设备特定优化 // 设备特定优化
enableDeviceSpecificOptimizations: false, // 🚩 第一阶段:关闭 enableDeviceSpecificOptimizations: false, // [Phase1] 关闭
minimumScreenSize: { width: 240, height: 320 }, minimumScreenSize: { width: 240, height: 320 },
maximumScreenSize: { width: 4096, height: 8192 } maximumScreenSize: { width: 4096, height: 8192 }
} }
@@ -149,10 +149,10 @@ export function getConfigSummary(config: CoordinateMappingConfig): string {
}) })
let summary = `坐标映射配置摘要:\n` let summary = `坐标映射配置摘要:\n`
summary += ` 已启用: ${enabledFeatures.join(', ') || '无'}\n` summary += `[ON] 已启用: ${enabledFeatures.join(', ') || '无'}\n`
summary += ` 已禁用: ${disabledFeatures.join(', ') || '无'}\n` summary += `[OFF] 已禁用: ${disabledFeatures.join(', ') || '无'}\n`
summary += `🎯 误差容忍: ${config.maxCoordinateError}px\n` summary += `[Target] 误差容忍: ${config.maxCoordinateError}px\n`
summary += `🔧 回退模式: ${config.fallbackToBasicOnError ? '启用' : '禁用'}` summary += `[Fallback] 回退模式: ${config.fallbackToBasicOnError ? '启用' : '禁用'}`
return summary return summary
} }

View File

@@ -90,7 +90,7 @@ export class SafeCoordinateMapper {
try { try {
this.performanceStats.totalMappings++ this.performanceStats.totalMappings++
// 🔒 阶段1基础验证 // [Phase1] 基础验证
const basicValidation = this.validateBasicInputs( const basicValidation = this.validateBasicInputs(
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight canvasX, canvasY, canvasElement, deviceWidth, deviceHeight
) )
@@ -107,7 +107,7 @@ export class SafeCoordinateMapper {
return null return null
} }
// 🔒 阶段2基础坐标映射 // [Phase2] 基础坐标映射
const basicResult = this.performBasicMapping( const basicResult = this.performBasicMapping(
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight, canvasX, canvasY, canvasElement, deviceWidth, deviceHeight,
startTime, 'basic', errors, warnings startTime, 'basic', errors, warnings
@@ -118,12 +118,12 @@ export class SafeCoordinateMapper {
return null return null
} }
// 🔒 阶段3渐进式增强 // [Phase3] 渐进式增强
const enhancedResult = this.applyEnhancements( const enhancedResult = this.applyEnhancements(
basicResult, deviceInfo, deviceWidth, deviceHeight basicResult, deviceInfo, deviceWidth, deviceHeight
) )
// 🔒 阶段4最终验证和统计 // [Phase4] 最终验证和统计
const finalResult = this.validateAndFinalizeMappingResult( const finalResult = this.validateAndFinalizeMappingResult(
enhancedResult, deviceWidth, deviceHeight, startTime enhancedResult, deviceWidth, deviceHeight, startTime
) )
@@ -150,7 +150,7 @@ export class SafeCoordinateMapper {
} }
/** /**
* 🔒 基础输入验证 * [Safe] 基础输入验证
*/ */
private validateBasicInputs( private validateBasicInputs(
canvasX: number, canvasX: number,
@@ -190,7 +190,7 @@ export class SafeCoordinateMapper {
} }
/** /**
* 🔒 基础坐标映射 - 始终可用的核心功能 * [Safe] 基础坐标映射 - 始终可用的核心功能
*/ */
private performBasicMapping( private performBasicMapping(
canvasX: number, canvasX: number,
@@ -263,7 +263,7 @@ export class SafeCoordinateMapper {
} }
/** /**
* 🔒 渐进式增强 - 根据配置启用高级功能 * [Safe] 渐进式增强 - 根据配置启用高级功能
*/ */
private applyEnhancements( private applyEnhancements(
basicResult: CoordinateMappingResult, basicResult: CoordinateMappingResult,
@@ -274,7 +274,7 @@ export class SafeCoordinateMapper {
const enhanced = { ...basicResult } const enhanced = { ...basicResult }
try { try {
// 🔧 增强1密度修正 // [Enhance1] 密度修正
if (this.config.enableDensityCorrection && deviceInfo) { if (this.config.enableDensityCorrection && deviceInfo) {
enhanced.metadata.density = deviceInfo.density enhanced.metadata.density = deviceInfo.density
enhanced.metadata.method += '+density' enhanced.metadata.method += '+density'
@@ -287,7 +287,7 @@ export class SafeCoordinateMapper {
} }
} }
// 🔧 增强2导航栏检测 // [Enhance2] 导航栏检测
if (this.config.enableNavigationBarDetection && deviceInfo && deviceHeight) { if (this.config.enableNavigationBarDetection && deviceInfo && deviceHeight) {
if (deviceInfo.hasNavigationBar && deviceInfo.navigationBarSize?.height > 0) { if (deviceInfo.hasNavigationBar && deviceInfo.navigationBarSize?.height > 0) {
const navBarHeight = deviceInfo.navigationBarSize.height const navBarHeight = deviceInfo.navigationBarSize.height
@@ -300,7 +300,7 @@ export class SafeCoordinateMapper {
} }
} }
// 🔧 增强3长宽比修正 // [Enhance3] 长宽比修正
if (this.config.enableAspectRatioCorrection && deviceInfo) { if (this.config.enableAspectRatioCorrection && deviceInfo) {
const aspectRatioDiff = Math.abs(deviceInfo.aspectRatio - (deviceWidth! / deviceHeight!)) const aspectRatioDiff = Math.abs(deviceInfo.aspectRatio - (deviceWidth! / deviceHeight!))
if (aspectRatioDiff > 0.1) { if (aspectRatioDiff > 0.1) {
@@ -308,7 +308,7 @@ export class SafeCoordinateMapper {
} }
} }
// 🔧 增强4设备特定优化 // [Enhance4] 设备特定优化
if (this.config.enableDeviceSpecificOptimizations && deviceInfo && deviceWidth && deviceHeight) { if (this.config.enableDeviceSpecificOptimizations && deviceInfo && deviceWidth && deviceHeight) {
const screenArea = deviceWidth * deviceHeight const screenArea = deviceWidth * deviceHeight
if (screenArea > 2073600) { // 1080p以上 if (screenArea > 2073600) { // 1080p以上
@@ -324,7 +324,7 @@ export class SafeCoordinateMapper {
} }
/** /**
* 🔒 最终验证和结果处理 * [Safe] 最终验证和结果处理
*/ */
private validateAndFinalizeMappingResult( private validateAndFinalizeMappingResult(
result: CoordinateMappingResult, result: CoordinateMappingResult,
@@ -382,12 +382,12 @@ export class SafeCoordinateMapper {
return ` return `
SafeCoordinateMapper性能报告: SafeCoordinateMapper性能报告:
📊 总映射次数: ${this.performanceStats.totalMappings} [Stats] 总映射次数: ${this.performanceStats.totalMappings}
成功次数: ${this.performanceStats.successfulMappings} [OK] 成功次数: ${this.performanceStats.successfulMappings}
失败次数: ${this.performanceStats.failedMappings} [FAIL] 失败次数: ${this.performanceStats.failedMappings}
📈 成功率: ${successRate}% [Rate] 成功率: ${successRate}%
⏱️ 平均处理时间: ${this.performanceStats.averageProcessingTime.toFixed(2)}ms [Time] 平均处理时间: ${this.performanceStats.averageProcessingTime.toFixed(2)}ms
🔧 回退模式: ${this.config.fallbackToBasicOnError ? '启用' : '禁用'} [Fallback] 回退模式: ${this.config.fallbackToBasicOnError ? '启用' : '禁用'}
` `
} }

View File

@@ -15,11 +15,11 @@ export default defineConfig({
port: 5173, port: 5173,
}, },
build: { build: {
// 🔧 优化构建配置,解决大块警告 // 优化构建配置,解决大块警告
chunkSizeWarningLimit: 1000, // 提高块大小警告限制到1MB chunkSizeWarningLimit: 1000, // 提高块大小警告限制到1MB
rollupOptions: { rollupOptions: {
output: { output: {
// 📦 手动代码分割,优化加载性能 // 手动代码分割,优化加载性能
manualChunks: { manualChunks: {
// React相关库单独打包 // React相关库单独打包
'react-vendor': ['react', 'react-dom'], 'react-vendor': ['react', 'react-dom'],
@@ -30,13 +30,13 @@ export default defineConfig({
// Socket.IO单独打包 // Socket.IO单独打包
'socket-vendor': ['socket.io-client'], 'socket-vendor': ['socket.io-client'],
}, },
// 📁 优化文件命名 // 优化文件命名
chunkFileNames: 'assets/js/[name]-[hash].js', chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
} }
}, },
// 🚀 构建优化 // 构建优化
sourcemap: false, // 生产环境不生成sourcemap sourcemap: false, // 生产环境不生成sourcemap
minify: 'terser', // 使用terser压缩 minify: 'terser', // 使用terser压缩
terserOptions: { terserOptions: {
@@ -45,10 +45,10 @@ export default defineConfig({
drop_debugger: true, // 移除debugger drop_debugger: true, // 移除debugger
}, },
}, },
// 📊 资源优化 // 资源优化
assetsInlineLimit: 4096, // 小于4KB的资源内联为base64 assetsInlineLimit: 4096, // 小于4KB的资源内联为base64
}, },
// 🔧 依赖优化 // 依赖优化
optimizeDeps: { optimizeDeps: {
include: [ include: [
'react', 'react',