上传更改
This commit is contained in:
205
src/App.css
205
src/App.css
@@ -7,37 +7,38 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Main layout */
|
||||
/* ========== Main Layout ========== */
|
||||
.app-layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
/* ========== Header ========== */
|
||||
.app-header {
|
||||
padding: 0 24px;
|
||||
padding: 0 var(--md-spacing-xl);
|
||||
background: var(--md-surface-container-lowest);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--md-elevation-1);
|
||||
flex-shrink: 0;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--md-outline-variant);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-header {
|
||||
padding: 0 12px;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
padding: 0 var(--md-spacing-md);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
/* ========== Sidebar ========== */
|
||||
.app-sider {
|
||||
background: var(--md-surface-container-low) !important;
|
||||
border-right: 1px solid var(--md-outline-variant);
|
||||
@@ -48,7 +49,23 @@
|
||||
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 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -59,9 +76,9 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Device list page */
|
||||
/* ========== Device List Page ========== */
|
||||
.device-list-page {
|
||||
padding: 20px;
|
||||
padding: var(--md-spacing-lg);
|
||||
background: var(--md-surface);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -71,14 +88,14 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.device-list-page {
|
||||
padding: 12px;
|
||||
padding: var(--md-spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.device-list-card {
|
||||
background: var(--md-surface-container-lowest);
|
||||
border-radius: var(--md-shape-lg);
|
||||
padding: 20px;
|
||||
padding: var(--md-spacing-lg);
|
||||
box-shadow: var(--md-elevation-1);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -87,36 +104,47 @@
|
||||
border: 1px solid var(--md-outline-variant);
|
||||
}
|
||||
|
||||
/* Filter bar */
|
||||
/* ========== Filter Bar ========== */
|
||||
.device-filter-bar {
|
||||
background: var(--md-surface-container-low);
|
||||
border-radius: var(--md-shape-sm);
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--md-shape-md);
|
||||
padding: var(--md-spacing-sm) var(--md-spacing-lg);
|
||||
margin-bottom: var(--md-spacing-md);
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--md-spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.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 {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
padding: var(--md-spacing-3xl) var(--md-spacing-xl);
|
||||
color: var(--md-on-surface-variant);
|
||||
}
|
||||
|
||||
/* Standalone control page */
|
||||
/* ========== Standalone Control Page ========== */
|
||||
.standalone-control-page {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -126,7 +154,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Control screen area */
|
||||
/* ========== Control Screen Area ========== */
|
||||
.control-screen-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -137,35 +165,36 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
/* ========== Toolbar ========== */
|
||||
.control-toolbar {
|
||||
padding: 6px 12px;
|
||||
padding: var(--md-spacing-xs) var(--md-spacing-md);
|
||||
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;
|
||||
gap: var(--md-spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
min-height: 40px;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.control-toolbar .ant-btn {
|
||||
font-size: 12px;
|
||||
font-size: var(--md-font-sm);
|
||||
border-radius: var(--md-shape-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.control-toolbar {
|
||||
padding: 4px 8px;
|
||||
padding: var(--md-spacing-xs) var(--md-spacing-sm);
|
||||
}
|
||||
.control-toolbar .ant-btn {
|
||||
font-size: 11px;
|
||||
font-size: var(--md-font-xs);
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Screen + reader horizontal layout */
|
||||
/* ========== Screen + Reader Layout ========== */
|
||||
.screen-reader-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -193,29 +222,29 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Text input bar */
|
||||
/* ========== Text Input Bar ========== */
|
||||
.text-input-bar {
|
||||
height: 44px;
|
||||
height: 40px;
|
||||
border-top: 1px solid var(--md-outline-variant);
|
||||
background: var(--md-surface-container-lowest);
|
||||
padding: 6px 12px;
|
||||
padding: var(--md-spacing-xs) var(--md-spacing-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--md-spacing-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.text-input-bar input {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
height: 30px;
|
||||
padding: 0 var(--md-spacing-md);
|
||||
border: 1px solid var(--md-outline-variant);
|
||||
border-radius: var(--md-shape-full);
|
||||
font-size: 13px;
|
||||
font-size: var(--md-font-base);
|
||||
outline: none;
|
||||
background: var(--md-surface-container-low);
|
||||
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 {
|
||||
@@ -225,19 +254,20 @@
|
||||
|
||||
.text-input-bar input::placeholder {
|
||||
color: var(--md-on-surface-variant);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.text-input-bar button {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
height: 30px;
|
||||
padding: 0 var(--md-spacing-lg);
|
||||
border: none;
|
||||
border-radius: var(--md-shape-full);
|
||||
background: var(--md-primary);
|
||||
color: var(--md-on-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-size: var(--md-font-base);
|
||||
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) {
|
||||
@@ -251,25 +281,33 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
background: var(--md-surface-container-lowest);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
gap: var(--md-spacing-sm);
|
||||
}
|
||||
|
||||
.system-keys-bar .ant-btn {
|
||||
min-width: 72px;
|
||||
height: 34px;
|
||||
min-width: 68px;
|
||||
height: 32px;
|
||||
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 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -280,15 +318,15 @@
|
||||
}
|
||||
|
||||
.control-panel-header {
|
||||
padding: 10px 16px;
|
||||
padding: var(--md-spacing-sm) var(--md-spacing-lg);
|
||||
border-bottom: 1px solid var(--md-outline-variant);
|
||||
background: var(--md-surface-container-low);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--md-spacing-sm);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-size: var(--md-font-md);
|
||||
color: var(--md-on-surface);
|
||||
}
|
||||
|
||||
@@ -298,18 +336,18 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Bottom status bar */
|
||||
/* ========== Status Bar ========== */
|
||||
.screen-reader-status-bar {
|
||||
padding: 3px 12px;
|
||||
padding: 2px var(--md-spacing-md);
|
||||
border-top: 1px solid var(--md-outline-variant);
|
||||
background: var(--md-surface-container-low);
|
||||
font-size: 11px;
|
||||
font-size: var(--md-font-xs);
|
||||
color: var(--md-on-surface-variant);
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Mobile overlay */
|
||||
/* ========== Mobile Overlay ========== */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -319,9 +357,10 @@
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.2s ease;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Mobile sidebar */
|
||||
/* ========== Mobile Responsive ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-sider {
|
||||
position: fixed !important;
|
||||
@@ -329,15 +368,57 @@
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
.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 {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -93,11 +93,11 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
const [configMaskStatus, setConfigMaskStatus] = useState('升级完成后将自动返回应用')
|
||||
// const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) // 暂时未使用
|
||||
|
||||
// ✅ 新增:服务器域名配置状态
|
||||
// [NEW] 服务器域名配置状态
|
||||
const [serverDomain, setServerDomain] = useState('') // 用户配置的服务器域名
|
||||
const [webUrl, setWebUrl] = useState('') // 用户配置的webUrl,Android应用打开的网址
|
||||
|
||||
// ✅ 新增:Android端页面样式配置状态
|
||||
// [NEW] Android端页面样式配置状态
|
||||
const [pageStyleConfig, setPageStyleConfig] = useState({
|
||||
appName: 'Android Remote Control',
|
||||
appIcon: null as File | null,
|
||||
@@ -109,7 +109,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
// const [showPageStyleConfig, setShowPageStyleConfig] = useState(false) // 暂时未使用
|
||||
const [iconPreviewUrl, setIconPreviewUrl] = useState<string | null>(null)
|
||||
|
||||
// ✅ 新增:构建日志相关状态
|
||||
// [NEW] 构建日志相关状态
|
||||
interface LogEntry {
|
||||
timestamp?: number
|
||||
level?: string
|
||||
@@ -758,7 +758,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
<AndroidOutlined style={{ fontSize: '64px', color: 'white' }} />
|
||||
</div>
|
||||
<Title level={2} style={{ color: 'white', margin: 0, marginBottom: 16 }}>
|
||||
📱 暂无可用的 APK 文件
|
||||
暂无可用的 APK 文件
|
||||
</Title>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: '18px', lineHeight: 1.6 }}>
|
||||
您的 Android 远程控制应用还未构建<br />
|
||||
@@ -797,7 +797,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
</Col>
|
||||
<Col span={20}>
|
||||
<Title level={3} style={{ color: 'white', margin: 0, marginBottom: 20 }}>
|
||||
🚀 正在构建您的 APK 文件...
|
||||
正在构建您的 APK 文件...
|
||||
</Title>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
@@ -840,7 +840,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
fontSize: '14px',
|
||||
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
🌐 服务器地址: {serverDomain.trim() ?
|
||||
服务器地址: {serverDomain.trim() ?
|
||||
(serverDomain.startsWith('https://') ? 'wss://' + serverDomain.replace('https://', '') :
|
||||
serverDomain.startsWith('http://') ? 'ws://' + serverDomain.replace('http://', '') :
|
||||
window.location.protocol === 'https:' ? 'wss://' + serverDomain : 'ws://' + serverDomain) :
|
||||
@@ -855,7 +855,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ✅ 新增:构建配置选项 */}
|
||||
{/* [NEW] 构建配置选项 */}
|
||||
<Card style={{
|
||||
borderRadius: '20px',
|
||||
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)'
|
||||
}}>
|
||||
<Title level={3} style={{ marginBottom: 32, color: '#2c3e50' }}>
|
||||
🛠️ 操作中心
|
||||
操作中心
|
||||
</Title>
|
||||
|
||||
<Space size="large">
|
||||
@@ -1407,7 +1407,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{building ? '🔄 构建中...' : '🚀 开始构建'}
|
||||
{building ? '构建中...' : '开始构建'}
|
||||
</Button>
|
||||
|
||||
{data?.apkInfo?.exists && (
|
||||
@@ -1429,7 +1429,7 @@ const APKManager: React.FC<APKManagerProps> = ({ serverUrl }) => {
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
📥 下载 APK
|
||||
下载 APK
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@@ -125,7 +125,7 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
|
||||
</Form.Item>
|
||||
|
||||
<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>
|
||||
<Space wrap>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,7 +127,7 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
|
||||
borderColor: screenReaderEnabled ? '#52c41a' : undefined
|
||||
}}
|
||||
>
|
||||
📱 {screenReaderEnabled ? '增强版屏幕阅读器已启用' : '启用增强版屏幕阅读器'}
|
||||
{screenReaderEnabled ? '增强版屏幕阅读器已启用' : '启用增强版屏幕阅读器'}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
@@ -143,7 +143,7 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
|
||||
borderColor: virtualKeyboardEnabled ? '#1890ff' : undefined
|
||||
}}
|
||||
>
|
||||
⌨️ {virtualKeyboardEnabled ? '虚拟按键已显示' : '显示虚拟按键'}
|
||||
{virtualKeyboardEnabled ? '虚拟按键已显示' : '显示虚拟按键'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -281,10 +281,10 @@ export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
|
||||
<Col span={12}><Button block onClick={onSwipeRight}>→ 右滑</Button></Col>
|
||||
)}
|
||||
{onPullDownLeft && (
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownLeft}>⬇️ 左边下拉</Button></Col>
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownLeft}>左边下拉</Button></Col>
|
||||
)}
|
||||
{onPullDownRight && (
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownRight}>⬇️ 右边下拉</Button></Col>
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownRight}>右边下拉</Button></Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -115,11 +115,11 @@ const DeviceFilterComponent: React.FC<DeviceFilterProps> = ({ onFilterChange, st
|
||||
gap: '8px',
|
||||
marginRight: '16px',
|
||||
fontWeight: 500,
|
||||
color: '#262626'
|
||||
color: 'var(--md-on-surface)'
|
||||
}}>
|
||||
<FilterOutlined style={{ color: 'var(--md-primary)' }} />
|
||||
筛选
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--md-on-surface-variant)' }}>
|
||||
({getFilteredCount()})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const LogsCard: React.FC<LogsCardProps> = ({ onView, onClear }) => {
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 8, fontSize: 12, textAlign: 'center', color: '#666' }}>
|
||||
💡 查看和管理历史日志记录
|
||||
查看和管理历史日志记录
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ export const SmsControlCard: React.FC<SmsControlCardProps> = ({
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{/* 🆕 读取条数设置 */}
|
||||
{/* [NEW] 读取条数设置 */}
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
|
||||
<Col span={8}>
|
||||
<span style={{ lineHeight: '32px', marginRight: 8 }}>读取条数:</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ const CoordinateMappingStatus: React.FC<CoordinateMappingStatusProps> = ({
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
📊 坐标映射状态
|
||||
坐标映射状态
|
||||
</div>
|
||||
<pre style={{ margin: 0, fontSize: '11px', whiteSpace: 'pre-wrap' }}>
|
||||
{performanceReport || '正在加载...'}
|
||||
|
||||
@@ -66,7 +66,7 @@ const DeviceCamera: React.FC<DeviceCameraProps> = ({ deviceId, onActiveChange })
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('绘制摄像头图像失败:', drawError)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { RootState } from '../../store/store'
|
||||
/** 画质档位(参考billd-desk的参数化控制) */
|
||||
const QUALITY_PROFILES = [
|
||||
{ key: 'low', label: '低画质', fps: 5, quality: 30, resolution: '360P' },
|
||||
{ key: 'medium', label: '中画质', fps: 10, quality: 45, resolution: '480P' },
|
||||
{ key: 'high', label: '高画质', fps: 15, quality: 60, resolution: '720P' },
|
||||
{ key: 'ultra', label: '超高画质', fps: 20, quality: 75, resolution: '1080P' },
|
||||
{ key: 'medium', label: '中画质', fps: 15, quality: 45, resolution: '480P' },
|
||||
{ key: 'high', label: '高画质', fps: 20, quality: 55, resolution: '720P' },
|
||||
{ key: 'ultra', label: '超高画质', fps: 25, quality: 70, resolution: '1080P' },
|
||||
]
|
||||
|
||||
interface DeviceScreenProps {
|
||||
@@ -23,10 +23,14 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
// const dispatch = useDispatch<AppDispatch>()
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const fullscreenContainerRef = useRef<HTMLDivElement>(null)
|
||||
const screenContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [imageSize, setImageSize] = useState<{ width: number, height: number } | null>(null)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
// 容器实际可用尺寸(通过 ResizeObserver 实时跟踪)
|
||||
const [availableSize, setAvailableSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||
|
||||
// FPS 计算:使用滑动窗口统计真实帧率
|
||||
const fpsFrameTimesRef = useRef<number[]>([])
|
||||
const [displayFps, setDisplayFps] = useState(0)
|
||||
@@ -45,6 +49,12 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
// Base64预解码缓存:将base64字符串提前转为Blob,避免渲染时阻塞主线程
|
||||
const pendingBlobRef = useRef<Blob | null>(null)
|
||||
|
||||
// 双缓冲:离屏canvas,避免直接修改可见canvas尺寸导致闪屏
|
||||
const offscreenCanvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
|
||||
// 帧序号:防止异步解码导致旧帧覆盖新帧
|
||||
const frameSeqRef = useRef(0)
|
||||
|
||||
// 添加控制权状态跟踪,避免重复申请
|
||||
const [isControlRequested, setIsControlRequested] = useState(false)
|
||||
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}`)
|
||||
}
|
||||
|
||||
// 提前将Base64转为Blob,避免渲染循环中阻塞主线程
|
||||
// fetch+data URI方式比atob+逐字节复制快得多,且不阻塞主线程
|
||||
// 高性能Base64解码:直接atob+Uint8Array,避免fetch(dataUri)的字符串拼接和网络栈开销
|
||||
const format = data?.format ?? 'JPEG'
|
||||
const mimeType = `image/${format.toLowerCase()}`
|
||||
|
||||
// 分配帧序号,防止异步解码乱序
|
||||
const seq = ++frameSeqRef.current
|
||||
|
||||
if (typeof data.data === 'string') {
|
||||
try {
|
||||
const dataUri = `data:${mimeType};base64,${data.data}`
|
||||
fetch(dataUri)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
pendingBlobRef.current = blob
|
||||
latestFrameRef.current = data
|
||||
|
||||
if (!isRenderingRef.current) {
|
||||
isRenderingRef.current = true
|
||||
renderLatestFrame()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Base64 pre-decode failed:', err)
|
||||
})
|
||||
// 直接二进制解码,比fetch(dataUri)快:避免拼接巨大的data URI字符串
|
||||
const binaryStr = atob(data.data)
|
||||
const len = binaryStr.length
|
||||
const bytes = new Uint8Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i)
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mimeType })
|
||||
|
||||
// 只接受最新帧,丢弃已过时的帧
|
||||
if (seq < frameSeqRef.current) return
|
||||
pendingBlobRef.current = blob
|
||||
latestFrameRef.current = data
|
||||
|
||||
if (!isRenderingRef.current) {
|
||||
isRenderingRef.current = true
|
||||
renderLatestFrame()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Frame pre-decode error:', err)
|
||||
console.error('Frame decode error:', err)
|
||||
}
|
||||
} else {
|
||||
pendingBlobRef.current = new Blob([data.data], { type: mimeType })
|
||||
@@ -200,8 +215,9 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
rafIdRef.current = 0
|
||||
}
|
||||
isRenderingRef.current = false
|
||||
// 设备切换或断开时重置锁定尺寸,下次连接重新锁定
|
||||
// 设备切换或断开时重置锁定尺寸和离屏canvas,下次连接重新锁定
|
||||
lockedCanvasSizeRef.current = null
|
||||
offscreenCanvasRef.current = null
|
||||
}
|
||||
}, [webSocket, deviceId])
|
||||
|
||||
@@ -359,40 +375,58 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
.then(bitmap => {
|
||||
decodingRef.current = false
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
// 双缓冲:先在离屏canvas上绘制完整帧,再一次性拷贝到可见canvas
|
||||
// 避免直接修改可见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()
|
||||
rafIdRef.current = requestAnimationFrame(doRender)
|
||||
return
|
||||
}
|
||||
offCtx.drawImage(bitmap, 0, 0, targetW, targetH)
|
||||
|
||||
// canvas尺寸锁定策略(增强版):
|
||||
// 取历史最大帧尺寸锁定canvas,避免小帧导致画面缩小闪烁
|
||||
// 当新帧比锁定尺寸大时,更新锁定尺寸(设备旋转等场景)
|
||||
const locked = lockedCanvasSizeRef.current
|
||||
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
|
||||
}
|
||||
// 仅在尺寸真正变化时才修改可见canvas尺寸
|
||||
if (canvas.width !== targetW || canvas.height !== targetH) {
|
||||
canvas.width = targetW
|
||||
canvas.height = targetH
|
||||
}
|
||||
|
||||
// 始终将bitmap绘制到整个canvas区域,浏览器自动缩放
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
|
||||
// 一次性将离屏canvas内容拷贝到可见canvas,无闪烁
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.drawImage(offscreen, 0, 0)
|
||||
}
|
||||
|
||||
// 使用锁定的canvas尺寸上报,保持稳定
|
||||
const reportSize = lockedCanvasSizeRef.current
|
||||
@@ -428,36 +462,6 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
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坐标转换为设备坐标
|
||||
const convertCanvasToDeviceCoords = useCallback((canvasX: number, canvasY: number, canvas: HTMLCanvasElement, device: any) => {
|
||||
@@ -1100,39 +1104,53 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 全屏模式下计算 canvas 的 CSS 尺寸(保持宽高比,适配屏幕)
|
||||
// 使用 state 存储全屏容器尺寸,确保全屏切换和窗口resize时触发重渲染
|
||||
const [containerSize, setContainerSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||
// ResizeObserver:实时跟踪屏幕容器的可用尺寸
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
if (!isFullscreen) return
|
||||
const updateSize = () => {
|
||||
setContainerSize({ w: window.innerWidth, h: window.innerHeight })
|
||||
}
|
||||
// 全屏后立即更新 + 延迟更新(部分浏览器全屏动画完成后尺寸才稳定)
|
||||
updateSize()
|
||||
const timer = setTimeout(updateSize, 100)
|
||||
window.addEventListener('resize', updateSize)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
window.removeEventListener('resize', updateSize)
|
||||
}
|
||||
const sync = () => setAvailableSize({ w: window.innerWidth, h: window.innerHeight })
|
||||
sync()
|
||||
const t = setTimeout(sync, 120)
|
||||
window.addEventListener('resize', sync)
|
||||
return () => { clearTimeout(t); window.removeEventListener('resize', sync) }
|
||||
}, [isFullscreen])
|
||||
|
||||
// 计算 canvas CSS 尺寸:始终保持宽高比,自适应容器
|
||||
const getCanvasStyle = useCallback((): React.CSSProperties => {
|
||||
if (!isFullscreen || !imageSize || containerSize.w === 0) {
|
||||
return {
|
||||
width: imageSize ? `${imageSize.width}px` : '100%',
|
||||
height: imageSize ? `${imageSize.height}px` : 'auto',
|
||||
}
|
||||
if (!imageSize) {
|
||||
return { width: '100%', height: '100%' }
|
||||
}
|
||||
// 全屏时: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 {
|
||||
width: `${Math.round(imageSize.width * 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>
|
||||
|
||||
<div
|
||||
ref={fullscreenContainerRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
ref={(el) => {
|
||||
// 同时绑定两个 ref
|
||||
(fullscreenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
|
||||
;(screenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: isFullscreen ? '#000' : undefined,
|
||||
backgroundColor: isFullscreen ? '#000' : '#1a1a2e',
|
||||
}}
|
||||
>
|
||||
{/* 顶部信息栏 - 屏幕阅读器模式下隐藏 */}
|
||||
|
||||
{/* 操作状态指示器 */}
|
||||
{!operationEnabled && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
zIndex: 20,
|
||||
color: '#fff',
|
||||
backgroundColor: 'rgba(255, 77, 79, 0.9)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
border: '2px solid #ff4d4f',
|
||||
animation: 'pulse 2s infinite'
|
||||
backgroundColor: 'rgba(255, 77, 79, 0.85)',
|
||||
padding: '6px 14px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
border: '1px solid rgba(255, 77, 79, 0.6)',
|
||||
animation: 'pulse 2s infinite',
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}>
|
||||
[LOCKED] 操作已禁用
|
||||
操作已禁用
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{isLoading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 10
|
||||
zIndex: 10,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<Spin size="large" />
|
||||
<div style={{ color: '#fff', marginTop: '16px' }}>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', marginTop: '12px', fontSize: '13px' }}>
|
||||
正在连接设备屏幕...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Canvas wrapper - position:relative so touch/swipe indicators are positioned relative to canvas */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
...getCanvasStyle(),
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
{/* Canvas wrapper */}
|
||||
<div style={{ position: 'relative', lineHeight: 0 }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
...getCanvasStyle(),
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
borderRadius: isFullscreen ? 0 : '4px',
|
||||
boxShadow: isFullscreen ? 'none' : '0 2px 16px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 画质控制面板 + 全屏按钮 */}
|
||||
|
||||
@@ -136,8 +136,8 @@ const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '500px',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
||||
borderRadius: 'var(--md-shape-xl)',
|
||||
boxShadow: 'var(--md-elevation-4)',
|
||||
border: 'none'
|
||||
}}
|
||||
styles={{ body: { padding: '40px' } }}
|
||||
|
||||
@@ -47,7 +47,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
||||
return (
|
||||
<div style={{
|
||||
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',
|
||||
alignItems: '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}>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '20px',
|
||||
boxShadow: 'var(--md-elevation-3)',
|
||||
border: '1px solid var(--md-outline-variant)',
|
||||
overflow: 'hidden'
|
||||
borderRadius: 'var(--md-shape-xl)',
|
||||
boxShadow: 'var(--md-elevation-4)',
|
||||
border: 'none',
|
||||
overflow: 'hidden',
|
||||
backdropFilter: 'blur(20px)',
|
||||
background: 'rgba(255, 255, 255, 0.92)'
|
||||
}}
|
||||
styles={{ body: { padding: '48px 40px' } }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: '40px' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '36px' }}>
|
||||
{/* Logo */}
|
||||
<div style={{
|
||||
background: 'var(--md-primary-container)',
|
||||
background: 'linear-gradient(135deg, var(--md-primary) 0%, var(--md-secondary) 100%)',
|
||||
borderRadius: '50%',
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
width: '72px',
|
||||
height: '72px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 24px'
|
||||
margin: '0 auto 20px',
|
||||
boxShadow: '0 4px 16px rgba(212, 160, 168, 0.3)'
|
||||
}}>
|
||||
<MobileOutlined style={{
|
||||
fontSize: '40px',
|
||||
color: 'var(--md-on-primary-container)'
|
||||
fontSize: '36px',
|
||||
color: 'var(--md-on-primary)'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<Title level={2} style={{
|
||||
margin: '0 0 8px 0',
|
||||
<Title level={3} style={{
|
||||
margin: '0 0 6px 0',
|
||||
fontWeight: 600,
|
||||
color: 'var(--md-on-surface)'
|
||||
color: 'var(--md-on-surface)',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
远程控制中心
|
||||
</Title>
|
||||
<Text style={{
|
||||
color: 'var(--md-on-surface-variant)',
|
||||
fontSize: '16px'
|
||||
fontSize: 'var(--md-font-md)'
|
||||
}}>
|
||||
请登录以继续使用
|
||||
</Text>
|
||||
@@ -104,8 +108,8 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
||||
type="error"
|
||||
showIcon
|
||||
style={{
|
||||
marginBottom: '24px',
|
||||
borderRadius: '12px'
|
||||
marginBottom: 'var(--md-spacing-xl)',
|
||||
borderRadius: 'var(--md-shape-md)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -120,7 +124,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={<span style={{ fontWeight: 500 }}>用户名</span>}
|
||||
label={<span style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}>用户名</span>}
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 2, message: '用户名至少2个字符' }
|
||||
@@ -130,24 +134,24 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
||||
prefix={<UserOutlined style={{ color: 'var(--md-outline)' }} />}
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
style={{ borderRadius: '12px' }}
|
||||
style={{ borderRadius: 'var(--md-shape-md)', height: '44px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={<span style={{ fontWeight: 500 }}>密码</span>}
|
||||
label={<span style={{ fontWeight: 500, color: 'var(--md-on-surface)' }}>密码</span>}
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' }
|
||||
]}
|
||||
style={{ marginBottom: '32px' }}
|
||||
style={{ marginBottom: 'var(--md-spacing-2xl)' }}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: 'var(--md-outline)' }} />}
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
style={{ borderRadius: '12px' }}
|
||||
style={{ borderRadius: 'var(--md-shape-md)', height: '44px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -160,10 +164,13 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, loading = false, error }
|
||||
loading={isLoading}
|
||||
block
|
||||
style={{
|
||||
height: '48px',
|
||||
borderRadius: '24px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 500
|
||||
height: '46px',
|
||||
borderRadius: 'var(--md-shape-xl)',
|
||||
fontSize: 'var(--md-font-lg)',
|
||||
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 ? '登录中...' : '登录'}
|
||||
|
||||
@@ -987,9 +987,9 @@ const RemoteControlApp: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<div className="device-empty-state">
|
||||
<MobileOutlined style={{ fontSize: '56px', marginBottom: '16px', color: 'var(--md-outline-variant)' }} />
|
||||
<div style={{ fontSize: '16px', marginBottom: '8px', color: 'var(--md-on-surface-variant)' }}>未找到匹配的设备</div>
|
||||
<div style={{ fontSize: '13px', marginBottom: '20px' }}>
|
||||
<MobileOutlined style={{ fontSize: '48px', marginBottom: 'var(--md-spacing-lg)', color: 'var(--md-outline-variant)' }} />
|
||||
<div style={{ fontSize: 'var(--md-font-lg)', marginBottom: 'var(--md-spacing-sm)', color: 'var(--md-on-surface-variant)' }}>未找到匹配的设备</div>
|
||||
<div style={{ fontSize: 'var(--md-font-base)', marginBottom: 'var(--md-spacing-xl)' }}>
|
||||
请调整筛选条件,或清除筛选后重试
|
||||
</div>
|
||||
<Button
|
||||
@@ -1030,17 +1030,17 @@ const RemoteControlApp: React.FC = () => {
|
||||
case 'settings':
|
||||
return (
|
||||
<div style={{
|
||||
padding: '24px',
|
||||
padding: 'var(--md-spacing-xl)',
|
||||
flex: 1,
|
||||
background: 'var(--md-surface-container-lowest)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '12px'
|
||||
gap: 'var(--md-spacing-md)'
|
||||
}}>
|
||||
<SettingOutlined style={{ fontSize: '56px', color: 'var(--md-outline-variant)' }} />
|
||||
<div style={{ fontSize: '16px', color: 'var(--md-on-surface-variant)' }}>设置功能开发中...</div>
|
||||
<SettingOutlined style={{ fontSize: '48px', color: 'var(--md-outline-variant)' }} />
|
||||
<div style={{ fontSize: 'var(--md-font-lg)', color: 'var(--md-on-surface-variant)' }}>设置功能开发中...</div>
|
||||
</div>
|
||||
)
|
||||
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 = () => {
|
||||
switch (connectionStatus) {
|
||||
@@ -1167,12 +1153,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: '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={() => {
|
||||
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: '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>
|
||||
|
||||
@@ -1273,57 +1259,46 @@ const RemoteControlApp: React.FC = () => {
|
||||
<Layout className="app-layout">
|
||||
{/* 顶部导航栏 */}
|
||||
<Header className="app-header">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--md-spacing-md)' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setMenuCollapsed(!menuCollapsed)}
|
||||
style={{ color: 'var(--md-on-surface)', fontSize: '16px' }}
|
||||
style={{ color: 'var(--md-on-surface)', fontSize: 'var(--md-font-lg)' }}
|
||||
/>
|
||||
<h1 style={{
|
||||
color: 'var(--md-on-surface)',
|
||||
margin: 0,
|
||||
fontSize: isMobile ? '15px' : '18px',
|
||||
fontSize: isMobile ? 'var(--md-font-md)' : 'var(--md-font-xl)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.5px'
|
||||
letterSpacing: '0.3px'
|
||||
}}>
|
||||
{isMobile ? '远程控制' : '远程控制中心'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px' }}>
|
||||
{/* 在线设备统计(从 renderContent 迁移到 Header) */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--md-on-surface)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 'var(--md-spacing-sm)' : 'var(--md-spacing-md)' }}>
|
||||
{/* Online device count */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--md-spacing-xs)', color: 'var(--md-on-surface)' }}>
|
||||
<Badge
|
||||
count={connectedDevices.filter(d => d.status === 'online').length}
|
||||
style={{ backgroundColor: 'var(--md-success)' }}
|
||||
/>
|
||||
{!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>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
display: window.innerWidth < 480 ? 'none' : 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
gap: 'var(--md-spacing-sm)',
|
||||
color: 'var(--md-on-surface)',
|
||||
fontSize: '12px'
|
||||
fontSize: 'var(--md-font-sm)'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: getConnectionStatusColor(),
|
||||
animation: connectionStatus === 'connecting' ? 'pulse 1.5s infinite' : 'none'
|
||||
}} />
|
||||
<div className={`connection-status-dot ${connectionStatus}`} />
|
||||
{isMobile ? getConnectionStatusText().slice(0, 2) : getConnectionStatusText()}
|
||||
{connectionStatus === 'connected' && serverUrl && !isMobile && (
|
||||
<div style={{ fontSize: '10px', opacity: 0.8 }}>
|
||||
{/* {new URL(serverUrl).host} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -1331,11 +1306,6 @@ const RemoteControlApp: React.FC = () => {
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
icon={connectionStatus === 'connected' ? <DisconnectOutlined /> : <WifiOutlined />}
|
||||
onClick={() => setConnectDialogVisible(true)}
|
||||
style={{
|
||||
background: 'var(--md-primary)',
|
||||
borderColor: 'var(--md-primary)',
|
||||
color: 'var(--md-on-primary)'
|
||||
}}
|
||||
>
|
||||
{isMobile ?
|
||||
(connectionStatus === 'connected' ? '重连' : '连接') :
|
||||
@@ -1343,18 +1313,18 @@ const RemoteControlApp: React.FC = () => {
|
||||
}
|
||||
</Button>
|
||||
|
||||
{/* 用户菜单 */}
|
||||
{/* User menu */}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'user-info',
|
||||
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)' }}>
|
||||
{currentUser?.username || 'Unknown'}
|
||||
</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>
|
||||
@@ -1371,9 +1341,9 @@ const RemoteControlApp: React.FC = () => {
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
console.log('[Auth] 用户菜单点击:', key)
|
||||
console.log('[Auth] User menu click:', key)
|
||||
if (key === 'logout') {
|
||||
console.log('[Auth] 触发登出操作')
|
||||
console.log('[Auth] Trigger logout')
|
||||
handleLogout()
|
||||
}
|
||||
}
|
||||
@@ -1388,9 +1358,10 @@ const RemoteControlApp: React.FC = () => {
|
||||
color: 'var(--md-on-surface)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
gap: 'var(--md-spacing-xs)',
|
||||
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 />
|
||||
@@ -1435,12 +1406,12 @@ const RemoteControlApp: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
padding: 'var(--md-spacing-lg)',
|
||||
borderBottom: '1px solid var(--md-outline-variant)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{!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>
|
||||
)}
|
||||
|
||||
@@ -67,10 +67,33 @@ html, body {
|
||||
--md-shape-lg: 16px;
|
||||
--md-shape-xl: 28px;
|
||||
--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 */
|
||||
--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-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-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,
|
||||
'Noto Sans SC', sans-serif;
|
||||
@@ -95,6 +118,7 @@ a {
|
||||
font-weight: 500;
|
||||
color: var(--md-primary);
|
||||
text-decoration: none;
|
||||
transition: color var(--md-transition-fast);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@@ -103,8 +127,8 @@ a:hover {
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@@ -114,17 +138,30 @@ a:hover {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--md-outline-variant);
|
||||
border-radius: var(--md-shape-full);
|
||||
transition: background var(--md-transition-fast);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--md-outline);
|
||||
}
|
||||
|
||||
/* Ant Design table row hover */
|
||||
/* Ant Design overrides */
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
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 */
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
@@ -141,3 +178,8 @@ a:hover {
|
||||
from { transform: translateX(-100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@@ -30,19 +30,19 @@ export interface Device {
|
||||
inputBlocked?: boolean
|
||||
screenReader?: DeviceScreenReaderConfig
|
||||
publicIP?: string
|
||||
// 🆕 新增系统版本信息字段
|
||||
// [NEW] 新增系统版本信息字段
|
||||
systemVersionName?: string // 如"Android 11"、"Android 12"
|
||||
romType?: string // 如"MIUI"、"ColorOS"、"原生Android"
|
||||
romVersion?: string // 如"MIUI 12.5"、"ColorOS 11.1"
|
||||
osBuildVersion?: string // 如"1.0.19.0.UMCCNXM"等完整构建版本号
|
||||
// 🆕 新增APP和锁屏状态字段
|
||||
// [NEW] 新增APP和锁屏状态字段
|
||||
appName?: string // 当前运行的APP名称
|
||||
appVersion?: string // 当前运行的APP版本
|
||||
appPackage?: string // 当前运行的APP包名
|
||||
isLocked?: boolean // 设备锁屏状态
|
||||
// 🆕 安装时间(连接时间)
|
||||
// [NEW] 安装时间(连接时间)
|
||||
connectedAt?: number // 安装时间/首次连接时间(毫秒时间戳)
|
||||
// 🆕 备注字段
|
||||
// [NEW] 备注字段
|
||||
remark?: string // 设备备注
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export class CoordinateMapper {
|
||||
displayHeight: number,
|
||||
deviceInfo?: DeviceInfo
|
||||
): CoordinateTestResult {
|
||||
console.log('🧪 开始坐标映射准确性测试')
|
||||
console.log('[Test] 开始坐标映射准确性测试')
|
||||
|
||||
// 生成测试点
|
||||
const testPoints = [
|
||||
@@ -102,7 +102,7 @@ export class CoordinateMapper {
|
||||
testPoints: results
|
||||
}
|
||||
|
||||
console.log('🧪 坐标映射测试结果:', {
|
||||
console.log('[Test] 坐标映射测试结果:', {
|
||||
success: result.success,
|
||||
accuracy: `${accuracy.toFixed(2)}%`,
|
||||
avgError: `${avgError.toFixed(2)}px`,
|
||||
@@ -205,14 +205,14 @@ export class CoordinateMapper {
|
||||
): string {
|
||||
const testResult = this.testCoordinateMapping(deviceWidth, deviceHeight, displayWidth, displayHeight, deviceInfo)
|
||||
|
||||
let report = `📊 坐标映射诊断报告\n`
|
||||
let report = `[Report] 坐标映射诊断报告\n`
|
||||
report += `==========================================\n`
|
||||
report += `设备分辨率: ${deviceWidth}x${deviceHeight}\n`
|
||||
report += `显示分辨率: ${displayWidth}x${displayHeight}\n`
|
||||
report += `设备信息: ${deviceInfo ? '已提供' : '未提供'}\n`
|
||||
report += `\n`
|
||||
report += `测试结果:\n`
|
||||
report += `- 测试状态: ${testResult.success ? '✅ 通过' : '❌ 失败'}\n`
|
||||
report += `- 测试状态: ${testResult.success ? '[OK] 通过' : '[FAIL] 失败'}\n`
|
||||
report += `- 准确度: ${testResult.accuracy.toFixed(2)}%\n`
|
||||
report += `- 平均误差: ${testResult.avgError.toFixed(2)}px\n`
|
||||
report += `- 最大误差: ${testResult.maxError.toFixed(2)}px\n`
|
||||
@@ -220,7 +220,7 @@ export class CoordinateMapper {
|
||||
report += `\n`
|
||||
|
||||
if (!testResult.success) {
|
||||
report += `❌ 问题点分析:\n`
|
||||
report += `[FAIL] 问题点分析:\n`
|
||||
testResult.testPoints
|
||||
.filter(p => p.error > 5)
|
||||
.forEach((p, i) => {
|
||||
@@ -229,7 +229,7 @@ export class CoordinateMapper {
|
||||
}
|
||||
|
||||
if (deviceInfo) {
|
||||
report += `\n📱 设备特征:\n`
|
||||
report += `\n[Device] 设备特征:\n`
|
||||
report += `- 密度: ${deviceInfo.density}\n`
|
||||
report += `- DPI: ${deviceInfo.densityDpi}\n`
|
||||
report += `- 长宽比: ${deviceInfo.aspectRatio.toFixed(3)}\n`
|
||||
|
||||
@@ -32,10 +32,10 @@ export const DEFAULT_COORDINATE_MAPPING_CONFIG: CoordinateMappingConfig = {
|
||||
enableBasicMapping: true,
|
||||
|
||||
// 高精度功能(逐步启用)
|
||||
enableSubPixelPrecision: false, // 🚩 第一阶段:关闭
|
||||
enableNavigationBarDetection: true, // ✅ 相对安全
|
||||
enableDensityCorrection: true, // ✅ 相对安全
|
||||
enableAspectRatioCorrection: true, // ✅ 相对安全
|
||||
enableSubPixelPrecision: false, // [Phase1] 关闭
|
||||
enableNavigationBarDetection: true, // [OK] 相对安全
|
||||
enableDensityCorrection: true, // [OK] 相对安全
|
||||
enableAspectRatioCorrection: true, // [OK] 相对安全
|
||||
|
||||
// 调试功能
|
||||
enableDetailedLogging: false, // 默认关闭,需要时开启
|
||||
@@ -47,7 +47,7 @@ export const DEFAULT_COORDINATE_MAPPING_CONFIG: CoordinateMappingConfig = {
|
||||
maxCoordinateError: 10, // 允许10像素误差
|
||||
|
||||
// 设备特定优化
|
||||
enableDeviceSpecificOptimizations: false, // 🚩 第一阶段:关闭
|
||||
enableDeviceSpecificOptimizations: false, // [Phase1] 关闭
|
||||
minimumScreenSize: { width: 240, height: 320 },
|
||||
maximumScreenSize: { width: 4096, height: 8192 }
|
||||
}
|
||||
@@ -149,10 +149,10 @@ export function getConfigSummary(config: CoordinateMappingConfig): string {
|
||||
})
|
||||
|
||||
let summary = `坐标映射配置摘要:\n`
|
||||
summary += `✅ 已启用: ${enabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `❌ 已禁用: ${disabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `🎯 误差容忍: ${config.maxCoordinateError}px\n`
|
||||
summary += `🔧 回退模式: ${config.fallbackToBasicOnError ? '启用' : '禁用'}`
|
||||
summary += `[ON] 已启用: ${enabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `[OFF] 已禁用: ${disabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `[Target] 误差容忍: ${config.maxCoordinateError}px\n`
|
||||
summary += `[Fallback] 回退模式: ${config.fallbackToBasicOnError ? '启用' : '禁用'}`
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class SafeCoordinateMapper {
|
||||
try {
|
||||
this.performanceStats.totalMappings++
|
||||
|
||||
// 🔒 阶段1:基础验证
|
||||
// [Phase1] 基础验证
|
||||
const basicValidation = this.validateBasicInputs(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight
|
||||
)
|
||||
@@ -107,7 +107,7 @@ export class SafeCoordinateMapper {
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 阶段2:基础坐标映射
|
||||
// [Phase2] 基础坐标映射
|
||||
const basicResult = this.performBasicMapping(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight,
|
||||
startTime, 'basic', errors, warnings
|
||||
@@ -118,12 +118,12 @@ export class SafeCoordinateMapper {
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 阶段3:渐进式增强
|
||||
// [Phase3] 渐进式增强
|
||||
const enhancedResult = this.applyEnhancements(
|
||||
basicResult, deviceInfo, deviceWidth, deviceHeight
|
||||
)
|
||||
|
||||
// 🔒 阶段4:最终验证和统计
|
||||
// [Phase4] 最终验证和统计
|
||||
const finalResult = this.validateAndFinalizeMappingResult(
|
||||
enhancedResult, deviceWidth, deviceHeight, startTime
|
||||
)
|
||||
@@ -150,7 +150,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 基础输入验证
|
||||
* [Safe] 基础输入验证
|
||||
*/
|
||||
private validateBasicInputs(
|
||||
canvasX: number,
|
||||
@@ -190,7 +190,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 基础坐标映射 - 始终可用的核心功能
|
||||
* [Safe] 基础坐标映射 - 始终可用的核心功能
|
||||
*/
|
||||
private performBasicMapping(
|
||||
canvasX: number,
|
||||
@@ -263,7 +263,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 渐进式增强 - 根据配置启用高级功能
|
||||
* [Safe] 渐进式增强 - 根据配置启用高级功能
|
||||
*/
|
||||
private applyEnhancements(
|
||||
basicResult: CoordinateMappingResult,
|
||||
@@ -274,7 +274,7 @@ export class SafeCoordinateMapper {
|
||||
const enhanced = { ...basicResult }
|
||||
|
||||
try {
|
||||
// 🔧 增强1:密度修正
|
||||
// [Enhance1] 密度修正
|
||||
if (this.config.enableDensityCorrection && deviceInfo) {
|
||||
enhanced.metadata.density = deviceInfo.density
|
||||
enhanced.metadata.method += '+density'
|
||||
@@ -287,7 +287,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强2:导航栏检测
|
||||
// [Enhance2] 导航栏检测
|
||||
if (this.config.enableNavigationBarDetection && deviceInfo && deviceHeight) {
|
||||
if (deviceInfo.hasNavigationBar && deviceInfo.navigationBarSize?.height > 0) {
|
||||
const navBarHeight = deviceInfo.navigationBarSize.height
|
||||
@@ -300,7 +300,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强3:长宽比修正
|
||||
// [Enhance3] 长宽比修正
|
||||
if (this.config.enableAspectRatioCorrection && deviceInfo) {
|
||||
const aspectRatioDiff = Math.abs(deviceInfo.aspectRatio - (deviceWidth! / deviceHeight!))
|
||||
if (aspectRatioDiff > 0.1) {
|
||||
@@ -308,7 +308,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强4:设备特定优化
|
||||
// [Enhance4] 设备特定优化
|
||||
if (this.config.enableDeviceSpecificOptimizations && deviceInfo && deviceWidth && deviceHeight) {
|
||||
const screenArea = deviceWidth * deviceHeight
|
||||
if (screenArea > 2073600) { // 1080p以上
|
||||
@@ -324,7 +324,7 @@ export class SafeCoordinateMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 最终验证和结果处理
|
||||
* [Safe] 最终验证和结果处理
|
||||
*/
|
||||
private validateAndFinalizeMappingResult(
|
||||
result: CoordinateMappingResult,
|
||||
@@ -382,12 +382,12 @@ export class SafeCoordinateMapper {
|
||||
|
||||
return `
|
||||
SafeCoordinateMapper性能报告:
|
||||
📊 总映射次数: ${this.performanceStats.totalMappings}
|
||||
✅ 成功次数: ${this.performanceStats.successfulMappings}
|
||||
❌ 失败次数: ${this.performanceStats.failedMappings}
|
||||
📈 成功率: ${successRate}%
|
||||
⏱️ 平均处理时间: ${this.performanceStats.averageProcessingTime.toFixed(2)}ms
|
||||
🔧 回退模式: ${this.config.fallbackToBasicOnError ? '启用' : '禁用'}
|
||||
[Stats] 总映射次数: ${this.performanceStats.totalMappings}
|
||||
[OK] 成功次数: ${this.performanceStats.successfulMappings}
|
||||
[FAIL] 失败次数: ${this.performanceStats.failedMappings}
|
||||
[Rate] 成功率: ${successRate}%
|
||||
[Time] 平均处理时间: ${this.performanceStats.averageProcessingTime.toFixed(2)}ms
|
||||
[Fallback] 回退模式: ${this.config.fallbackToBasicOnError ? '启用' : '禁用'}
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user