Improve: Web UI - iOS safe-area, icon toolbar buttons, logout confirm
This commit is contained in:
@@ -9,6 +9,8 @@
|
|||||||
//
|
//
|
||||||
// 编码要求:此文件必须保存为 UTF-8 with BOM(MSVC 要求)
|
// 编码要求:此文件必须保存为 UTF-8 with BOM(MSVC 要求)
|
||||||
// 注意:此文件中的配置会编译到程序中,运行时无法修改。
|
// 注意:此文件中的配置会编译到程序中,运行时无法修改。
|
||||||
|
// 修改原则:最小化原则。如果是UI文案修改,通常没有问题
|
||||||
|
// 如果修改项涉及数据存储、程序配置、代码逻辑,可能导致程序异常。
|
||||||
//
|
//
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
|
|
||||||
// 注册表键名 [仅ASCII,无空格]
|
// 注册表键名 [仅ASCII,无空格]
|
||||||
// 存储位置:HKCU\Software\{此键名}
|
// 存储位置:HKCU\Software\{此键名}
|
||||||
|
// 不能修改,修改会隐藏授权
|
||||||
#define BRAND_REGISTRY_KEY "YAMA"
|
#define BRAND_REGISTRY_KEY "YAMA"
|
||||||
|
|
||||||
// 网络通信前缀 [仅ASCII,无空格]
|
// 网络通信前缀 [仅ASCII,无空格]
|
||||||
@@ -262,7 +265,7 @@
|
|||||||
//
|
//
|
||||||
// 如果配置值以 "http" 开头,则作为 URL 打开浏览器。
|
// 如果配置值以 "http" 开头,则作为 URL 打开浏览器。
|
||||||
// 否则直接显示为文本消息。
|
// 否则直接显示为文本消息。
|
||||||
//
|
// 链接从上级同步而来,修改无效;建议设置这些菜单为隐藏
|
||||||
|
|
||||||
// 反馈链接(帮助菜单 → 反馈)
|
// 反馈链接(帮助菜单 → 反馈)
|
||||||
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
||||||
|
|||||||
@@ -32,7 +32,17 @@ inline std::string GetWebPageHTML() {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
.page { display: none !important; padding: 20px; min-height: 100vh; }
|
.page {
|
||||||
|
display: none !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
/* iOS notch / Dynamic Island: viewport-fit=cover 让 viewport 顶到物理边缘,
|
||||||
|
这里用 env(safe-area-inset-*) 把内容推回到安全区内,避免被前摄/底部 Home 条遮挡。
|
||||||
|
旧设备 / 非全屏环境下 env() 解析为 0,等价于纯 20px 内边距。 */
|
||||||
|
padding: calc(20px + env(safe-area-inset-top))
|
||||||
|
calc(20px + env(safe-area-inset-right))
|
||||||
|
calc(20px + env(safe-area-inset-bottom))
|
||||||
|
calc(20px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
.page.active { display: block !important; }
|
.page.active { display: block !important; }
|
||||||
#login-page.active {
|
#login-page.active {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
@@ -252,44 +262,32 @@ inline std::string GetWebPageHTML() {
|
|||||||
.view-btn:first-child { border-right: 1px solid rgba(255,255,255,0.1); }
|
.view-btn:first-child { border-right: 1px solid rgba(255,255,255,0.1); }
|
||||||
.view-btn:hover { color: #fff; }
|
.view-btn:hover { color: #fff; }
|
||||||
.view-btn.active { background: rgba(233, 69, 96, 0.3); color: #e94560; }
|
.view-btn.active { background: rgba(233, 69, 96, 0.3); color: #e94560; }
|
||||||
.refresh-btn {
|
/* Unified icon button (Refresh / Users / Logout). 40x40 方块更紧凑,留位置给未来按钮。
|
||||||
padding: 10px 20px;
|
颜色身份通过修饰类(.refresh / .users / .logout)保留,hover 高光与原来一致。 */
|
||||||
|
.icon-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: linear-gradient(135deg, #0f3460 0%, #1a4a7a 100%);
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.refresh-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(15, 52, 96, 0.4); }
|
.icon-btn svg { display: block; width: 20px; height: 20px; }
|
||||||
.logout-btn {
|
.icon-btn:hover { transform: translateY(-1px); }
|
||||||
padding: 10px 20px;
|
.icon-btn:active { transform: translateY(0); }
|
||||||
border: none;
|
.icon-btn.refresh { background: linear-gradient(135deg, #0f3460 0%, #1a4a7a 100%); }
|
||||||
border-radius: 8px;
|
.icon-btn.refresh:hover { box-shadow: 0 4px 12px rgba(15, 52, 96, 0.4); }
|
||||||
background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);
|
.icon-btn.users { background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); display: none; }
|
||||||
color: #fff;
|
.icon-btn.users.visible { display: inline-flex; }
|
||||||
font-size: 14px;
|
.icon-btn.users:hover { box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
|
||||||
cursor: pointer;
|
.icon-btn.logout { background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%); }
|
||||||
transition: all 0.3s;
|
.icon-btn.logout:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
|
|
||||||
.users-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
margin-left: 10px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.users-btn.visible { display: inline-block; }
|
|
||||||
.users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
|
|
||||||
/* User Management Modal */
|
/* User Management Modal */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -387,6 +385,54 @@ inline std::string GetWebPageHTML() {
|
|||||||
.user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
|
.user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
|
||||||
.user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
|
.user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
|
||||||
.user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
|
.user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
|
||||||
|
|
||||||
|
/* Generic confirmation modal (compact yes/no dialog, e.g. logout) —— 复用 .modal-overlay
|
||||||
|
做遮罩,自带一套紧凑布局 + 危险/取消双按钮风格。 */
|
||||||
|
.confirm-modal-content {
|
||||||
|
background: rgba(22, 33, 62, 0.98);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 28px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 380px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||||
|
border: 1px solid rgba(231, 76, 60, 0.25);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.confirm-modal-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(231, 76, 60, 0.15);
|
||||||
|
color: #e74c3c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.confirm-modal-icon svg { width: 28px; height: 28px; }
|
||||||
|
.confirm-modal-content h3 { color: #fff; margin: 0 0 8px; font-size: 18px; }
|
||||||
|
.confirm-modal-content p { color: #aaa; margin: 0 0 24px; font-size: 14px; line-height: 1.5; }
|
||||||
|
.confirm-modal-actions { display: flex; gap: 12px; justify-content: center; }
|
||||||
|
.confirm-modal-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.confirm-modal-actions .cancel-btn {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.confirm-modal-actions .cancel-btn:hover { background: rgba(255,255,255,0.15); }
|
||||||
|
.confirm-modal-actions .danger-btn {
|
||||||
|
background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); }
|
||||||
)HTML";
|
)HTML";
|
||||||
|
|
||||||
// Part 3: Device card styles
|
// Part 3: Device card styles
|
||||||
@@ -558,7 +604,11 @@ inline std::string GetWebPageHTML() {
|
|||||||
#screen-canvas { max-width: 100%; max-height: 100%; }
|
#screen-canvas { max-width: 100%; max-height: 100%; }
|
||||||
.screen-toolbar {
|
.screen-toolbar {
|
||||||
background: linear-gradient(180deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.85) 100%);
|
background: linear-gradient(180deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.85) 100%);
|
||||||
padding: 12px 20px;
|
/* 远程桌面顶部工具栏也避开 notch / Dynamic Island —— 顶 padding + 安全区 */
|
||||||
|
padding: calc(12px + env(safe-area-inset-top))
|
||||||
|
calc(20px + env(safe-area-inset-right))
|
||||||
|
12px
|
||||||
|
calc(20px + env(safe-area-inset-left));
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -633,13 +683,14 @@ inline std::string GetWebPageHTML() {
|
|||||||
height: 100dvh !important;
|
height: 100dvh !important;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
/* Portrait fullscreen: align canvas to top */
|
/* Portrait fullscreen: align canvas to top. 56px 是给浮动工具栏留出的空间;
|
||||||
|
竖屏 iPhone 全屏时再加上 safe-area-inset-top 把内容推到 notch / 灵动岛之下。 */
|
||||||
@media (orientation: portrait) {
|
@media (orientation: portrait) {
|
||||||
#screen-page:fullscreen .canvas-container,
|
#screen-page:fullscreen .canvas-container,
|
||||||
#screen-page:-webkit-full-screen .canvas-container,
|
#screen-page:-webkit-full-screen .canvas-container,
|
||||||
#screen-page.pseudo-fullscreen .canvas-container {
|
#screen-page.pseudo-fullscreen .canvas-container {
|
||||||
align-items: flex-start !important;
|
align-items: flex-start !important;
|
||||||
padding-top: 56px !important;
|
padding-top: calc(56px + env(safe-area-inset-top)) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#screen-page.pseudo-fullscreen #screen-canvas {
|
#screen-page.pseudo-fullscreen #screen-canvas {
|
||||||
@@ -666,7 +717,8 @@ inline std::string GetWebPageHTML() {
|
|||||||
/* Floating toolbar menu - minimal icon style */
|
/* Floating toolbar menu - minimal icon style */
|
||||||
.floating-toolbar {
|
.floating-toolbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 8px;
|
/* 8px 基础留白 + safe-area-inset-top 避开 iPhone 刘海/灵动岛 */
|
||||||
|
top: calc(8px + env(safe-area-inset-top));
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
@@ -701,7 +753,8 @@ inline std::string GetWebPageHTML() {
|
|||||||
.toolbar-btn:disabled:active { transform: none; }
|
.toolbar-btn:disabled:active { transform: none; }
|
||||||
.toolbar-toggle {
|
.toolbar-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 8px;
|
/* 同 .floating-toolbar:8px 基础 + 安全区 inset 避开刘海/灵动岛 */
|
||||||
|
top: calc(8px + env(safe-area-inset-top));
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -779,6 +832,10 @@ inline std::string GetWebPageHTML() {
|
|||||||
/* Portrait: horizontal at top center */
|
/* Portrait: horizontal at top center */
|
||||||
@media (orientation: portrait) {
|
@media (orientation: portrait) {
|
||||||
.quick-controls {
|
.quick-controls {
|
||||||
|
/* 非全屏:screen-toolbar 已经吃了一份 safe-area,这里再叠加会让快捷按钮
|
||||||
|
离 title 远到 ~2x 图标高度的空白,且 canvas-container 因此被推得过低,
|
||||||
|
底部 input-shortcuts 在键盘弹出时会被遮挡。
|
||||||
|
全屏:另在 fullscreen 选择器里补回 safe-area-inset-top 避开前置摄像头。 */
|
||||||
top: 4px;
|
top: 4px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -791,12 +848,23 @@ inline std::string GetWebPageHTML() {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
/* Portrait: align canvas to top, leave space for controls */
|
/* Portrait: align canvas to top, leave space for controls.
|
||||||
|
同上:非全屏 screen-toolbar 已避开刘海,这里只留固定 56px 给快捷按钮,
|
||||||
|
全屏路径在下方有独立 !important 覆写补回 safe-area。 */
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding-top: 56px;
|
padding-top: 56px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 全屏(无 screen-toolbar 兜底)才需要 quick-controls 自己加 safe-area
|
||||||
|
避开 iPhone 刘海/灵动岛/前置摄像头。 */
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
#screen-page:fullscreen .quick-controls,
|
||||||
|
#screen-page:-webkit-full-screen .quick-controls,
|
||||||
|
#screen-page.pseudo-fullscreen .quick-controls {
|
||||||
|
top: calc(4px + env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
}
|
||||||
/* Landscape: vertical at right center */
|
/* Landscape: vertical at right center */
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
.quick-controls {
|
.quick-controls {
|
||||||
@@ -852,7 +920,12 @@ inline std::string GetWebPageHTML() {
|
|||||||
.input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); }
|
.input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); }
|
||||||
/* Mobile responsive */
|
/* Mobile responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page { padding: 10px; }
|
.page {
|
||||||
|
padding: calc(10px + env(safe-area-inset-top))
|
||||||
|
calc(10px + env(safe-area-inset-right))
|
||||||
|
calc(10px + env(safe-area-inset-bottom))
|
||||||
|
calc(10px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
.header { flex-direction: column; gap: 12px; padding: 12px; }
|
.header { flex-direction: column; gap: 12px; padding: 12px; }
|
||||||
.header h1 { font-size: 20px; }
|
.header h1 { font-size: 20px; }
|
||||||
.search-box { width: 100%; }
|
.search-box { width: 100%; }
|
||||||
@@ -863,7 +936,12 @@ inline std::string GetWebPageHTML() {
|
|||||||
.device-card .ip { font-size: 12px; }
|
.device-card .ip { font-size: 12px; }
|
||||||
.device-card .meta-row { flex-wrap: wrap; gap: 8px; }
|
.device-card .meta-row { flex-wrap: wrap; gap: 8px; }
|
||||||
.device-card .active-window { font-size: 11px; }
|
.device-card .active-window { font-size: 11px; }
|
||||||
.screen-toolbar { padding: 8px 12px; }
|
.screen-toolbar {
|
||||||
|
padding: calc(8px + env(safe-area-inset-top))
|
||||||
|
calc(12px + env(safe-area-inset-right))
|
||||||
|
8px
|
||||||
|
calc(12px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
.back-btn { padding: 6px 12px; font-size: 13px; }
|
.back-btn { padding: 6px 12px; font-size: 13px; }
|
||||||
.toolbar-info .device-name { font-size: 13px; }
|
.toolbar-info .device-name { font-size: 13px; }
|
||||||
.toolbar-info .conn-info { font-size: 11px; }
|
.toolbar-info .conn-info { font-size: 11px; }
|
||||||
@@ -925,9 +1003,28 @@ inline std::string GetWebPageHTML() {
|
|||||||
<button id="view-grid" class="view-btn active" onclick="setViewMode('grid')" title="Grid View">Grid</button>
|
<button id="view-grid" class="view-btn active" onclick="setViewMode('grid')" title="Grid View">Grid</button>
|
||||||
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
|
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="refresh-btn" onclick="getDevices()">Refresh</button>
|
<button class="icon-btn refresh" onclick="getDevices()" title="Refresh" aria-label="Refresh">
|
||||||
<button class="users-btn" id="users-btn" onclick="openUsersModal()">Users</button>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<polyline points="1 20 1 14 7 14"/>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn users" id="users-btn" onclick="openUsersModal()" title="User Management" aria-label="User Management">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="9" cy="7" r="4"/>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn logout" onclick="logout()" title="Logout" aria-label="Logout">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||||
|
<polyline points="16 17 21 12 16 7"/>
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="stats-bar" class="stats-bar">
|
<div id="stats-bar" class="stats-bar">
|
||||||
@@ -1013,6 +1110,25 @@ inline std::string GetWebPageHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Logout Confirmation Modal -->
|
||||||
|
<div class="modal-overlay" id="logout-confirm-modal" onclick="if(event.target===this)cancelLogout()">
|
||||||
|
<div class="confirm-modal-content">
|
||||||
|
<div class="confirm-modal-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||||
|
<polyline points="16 17 21 12 16 7"/>
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Confirm Logout</h3>
|
||||||
|
<p>You will be returned to the login screen.<br>Continue?</p>
|
||||||
|
<div class="confirm-modal-actions">
|
||||||
|
<button class="cancel-btn" onclick="cancelLogout()">Cancel</button>
|
||||||
|
<button class="danger-btn" onclick="confirmLogout()">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)HTML";
|
)HTML";
|
||||||
|
|
||||||
// Part 7: JavaScript - State and WebSocket
|
// Part 7: JavaScript - State and WebSocket
|
||||||
@@ -1648,7 +1764,15 @@ inline std::string GetWebPageHTML() {
|
|||||||
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
|
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout 二次确认:onclick="logout()" 仅打开确认弹窗;用户点 "Logout" 才真正退出。
|
||||||
function logout() {
|
function logout() {
|
||||||
|
document.getElementById('logout-confirm-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
function cancelLogout() {
|
||||||
|
document.getElementById('logout-confirm-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
function confirmLogout() {
|
||||||
|
document.getElementById('logout-confirm-modal').classList.remove('active');
|
||||||
// Close and reconnect WebSocket to get new challenge
|
// Close and reconnect WebSocket to get new challenge
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.onclose = null; // Prevent auto-reconnect delay
|
ws.onclose = null; // Prevent auto-reconnect delay
|
||||||
|
|||||||
Reference in New Issue
Block a user