上传更改
This commit is contained in:
205
src/App.css
205
src/App.css
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('') // 用户配置的webUrl,Android应用打开的网址
|
const [webUrl, setWebUrl] = useState('') // 用户配置的webUrl,Android应用打开的网址
|
||||||
|
|
||||||
// ✅ 新增: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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 || '正在加载...'}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +160,26 @@ 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)
|
||||||
|
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
|
pendingBlobRef.current = blob
|
||||||
latestFrameRef.current = data
|
latestFrameRef.current = data
|
||||||
|
|
||||||
@@ -168,12 +187,8 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
|||||||
isRenderingRef.current = true
|
isRenderingRef.current = true
|
||||||
renderLatestFrame()
|
renderLatestFrame()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Base64 pre-decode failed:', err)
|
|
||||||
})
|
|
||||||
} 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',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用全屏尺寸或容器尺寸
|
||||||
|
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' }
|
||||||
}
|
}
|
||||||
// 全屏时:canvas 按宽高比缩放填满屏幕
|
|
||||||
const scale = Math.min(containerSize.w / imageSize.width, containerSize.h / imageSize.height)
|
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,7 +1178,11 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={fullscreenContainerRef}
|
ref={(el) => {
|
||||||
|
// 同时绑定两个 ref
|
||||||
|
(fullscreenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
|
||||||
|
;(screenContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1170,56 +1192,56 @@ const DeviceScreen: React.FC<DeviceScreenProps> = ({ deviceId, onScreenSizeChang
|
|||||||
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',
|
||||||
|
boxShadow: isFullscreen ? 'none' : '0 2px 16px rgba(0,0,0,0.3)',
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
|
|||||||
@@ -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' } }}
|
||||||
|
|||||||
@@ -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 ? '登录中...' : '登录'}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 // 设备备注
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ? '启用' : '禁用'}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user