Files
xianyan/docs/preview/my_devices_preview.html
Developer 228095f80a chore: 完成v6.7.0版本迭代更新
本次更新涵盖多个功能模块的优化与新增:
1. 新增Wi-Fi直连配对方式与协作画布模块
2. 完成设备管理重命名API与前端适配
3. 优化日签卡片空数据保护与UI细节
4. 新增剪贴板工具与每日运势会话
5. 修复应用锁恢复、语音消息录制等已知问题
6. 完善文件传输统计与配对逻辑
7. 更新安卓权限配置与iOS隐私描述
8. 新增自动化测试脚本与文档
9. 清理旧版审计报告与测试文件
2026-05-14 05:35:18 +08:00

882 lines
22 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>闲言 — 我的设备页面设计稿</title>
<style>
:root {
--primary: #007AFF;
--primary-light: #4DA2FF;
--primary-dark: #0056CC;
--primary-bg: rgba(0,122,255,0.08);
--success: #34C759;
--success-bg: rgba(52,199,89,0.10);
--warning: #FF9500;
--danger: #FF3B30;
--danger-bg: rgba(255,59,48,0.08);
--bg: #F2F2F7;
--bg-card: #FFFFFF;
--bg-secondary: #E5E5EA;
--bg-grouped: #F2F2F7;
--text: #1C1C1E;
--text-secondary: #8E8E93;
--text-hint: #AEAEB2;
--text-disabled: #C7C7CC;
--text-inverse: #FFFFFF;
--separator: rgba(60,60,67,0.12);
--border: rgba(60,60,67,0.08);
--glass-bg: rgba(255,255,255,0.72);
--glass-border: rgba(255,255,255,0.3);
--glass-blur: 20px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 22px;
--radius-pill: 9999px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 2px 8px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
--safe-bottom: 34px;
}
[data-theme="dark"] {
--bg: #000000;
--bg-card: #1C1C1E;
--bg-secondary: #2C2C2E;
--bg-grouped: #000000;
--text: #F5F5F7;
--text-secondary: #98989D;
--text-hint: #636366;
--text-disabled: #48484A;
--text-inverse: #000000;
--separator: rgba(84,84,88,0.36);
--border: rgba(84,84,88,0.25);
--glass-bg: rgba(28,28,30,0.78);
--glass-border: rgba(255,255,255,0.06);
--primary-bg: rgba(0,122,255,0.18);
--success-bg: rgba(52,199,89,0.18);
--danger-bg: rgba(255,59,48,0.18);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
--shadow-md: 0 2px 8px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.4);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-family);
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 20px;
gap: 40px;
flex-wrap: wrap;
}
.page-header {
width: 100%;
text-align: center;
padding: 20px 0 10px;
}
.page-header h1 {
font-size: 28px;
color: #fff;
font-weight: 700;
letter-spacing: -0.5px;
}
.page-header p {
font-size: 14px;
color: rgba(255,255,255,0.5);
margin-top: 6px;
}
.theme-switcher {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.theme-btn {
padding: 8px 18px;
border-radius: var(--radius-pill);
border: 1px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.6);
font-size: 13px;
cursor: pointer;
transition: all 0.25s;
font-family: var(--font-family);
}
.theme-btn:hover { background: rgba(255,255,255,0.1); }
.theme-btn.active {
background: var(--primary);
color: #fff;
border-color: var(--primary);
box-shadow: 0 2px 12px rgba(0,122,255,0.4);
}
.phone-frame {
width: 390px;
height: 844px;
border-radius: 44px;
overflow: hidden;
position: relative;
background: var(--bg);
box-shadow: 0 0 0 8px #2a2a4a, 0 20px 60px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
}
.phone-notch {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 126px;
height: 34px;
background: #000;
border-radius: 0 0 20px 20px;
z-index: 100;
}
.phone-status-bar {
height: 54px;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 28px 6px;
font-size: 14px;
font-weight: 600;
color: var(--text);
background: transparent;
z-index: 50;
flex-shrink: 0;
}
.nav-bar {
height: 52px;
display: flex;
align-items: center;
padding: 0 16px;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border-bottom: 0.5px solid var(--separator);
flex-shrink: 0;
z-index: 40;
}
.nav-bar .back {
font-size: 20px;
color: var(--primary);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
gap: 2px;
}
.nav-bar .back svg { width: 20px; height: 20px; }
.nav-bar .title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 600;
color: var(--text);
}
.nav-bar .action {
font-size: 14px;
color: var(--danger);
cursor: pointer;
padding: 4px 8px;
font-weight: 500;
border: none;
background: none;
font-family: var(--font-family);
transition: opacity 0.2s;
}
.nav-bar .action:hover { opacity: 0.7; }
.nav-bar .action:active { opacity: 0.5; }
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 0 16px 20px;
-webkit-overflow-scrolling: touch;
}
.scroll-area::-webkit-scrollbar { display: none; }
.section-label {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 20px 4px 8px;
}
.overview-card {
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border-radius: var(--radius-lg);
padding: 20px;
margin-top: 12px;
box-shadow: var(--shadow-md);
border: 0.5px solid var(--border);
display: flex;
gap: 16px;
}
.overview-item {
flex: 1;
text-align: center;
position: relative;
}
.overview-item:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 0.5px;
height: 36px;
background: var(--separator);
}
.overview-value {
font-size: 28px;
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
.overview-value .unit {
font-size: 16px;
font-weight: 500;
color: var(--text-secondary);
}
.overview-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
.overview-value.online { color: var(--success); }
.overview-value.ip { color: var(--primary); font-size: 20px; }
.device-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
margin-top: 10px;
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
border: 0.5px solid var(--border);
}
.device-card:active {
transform: scale(0.98);
box-shadow: var(--shadow-md);
}
.device-main {
display: flex;
align-items: center;
padding: 16px;
gap: 14px;
}
.device-icon-wrap {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
position: relative;
}
.device-icon-wrap.ios {
background: linear-gradient(135deg, #007AFF, #5856D6);
color: #fff;
}
.device-icon-wrap.android {
background: linear-gradient(135deg, #3DDC84, #00C853);
color: #fff;
}
.device-icon-wrap.web {
background: linear-gradient(135deg, #FF9500, #FF6B00);
color: #fff;
}
.device-icon-wrap.unknown {
background: linear-gradient(135deg, #8E8E93, #636366);
color: #fff;
}
.device-info {
flex: 1;
min-width: 0;
}
.device-name-row {
display: flex;
align-items: center;
gap: 8px;
}
.device-name {
font-size: 16px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.online {
background: var(--success);
box-shadow: 0 0 6px rgba(52,199,89,0.5);
animation: pulse-green 2s ease-in-out infinite;
}
.status-dot.offline {
background: var(--text-disabled);
}
@keyframes pulse-green {
0%, 100% { box-shadow: 0 0 4px rgba(52,199,89,0.4); }
50% { box-shadow: 0 0 10px rgba(52,199,89,0.7); }
}
.device-model {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
.device-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
flex-wrap: wrap;
}
.meta-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: var(--radius-pill);
background: var(--primary-bg);
color: var(--primary);
font-weight: 500;
}
.meta-tag.os {
background: var(--bg-secondary);
color: var(--text-secondary);
}
.meta-tag.offline-tag {
background: var(--danger-bg);
color: var(--danger);
}
.device-more {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: var(--text-hint);
cursor: pointer;
flex-shrink: 0;
transition: background 0.2s;
border: none;
background: none;
font-size: 18px;
font-family: var(--font-family);
}
.device-more:hover { background: var(--bg-secondary); }
.device-detail {
border-top: 0.5px solid var(--separator);
padding: 12px 16px;
display: none;
}
.device-detail.show { display: block; }
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
}
.detail-row:not(:last-child) {
border-bottom: 0.5px solid var(--separator);
}
.detail-label {
font-size: 13px;
color: var(--text-secondary);
}
.detail-value {
font-size: 13px;
color: var(--text);
font-weight: 500;
text-align: right;
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dedup-notice {
background: var(--primary-bg);
border-radius: var(--radius-md);
padding: 12px 16px;
margin-top: 16px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.dedup-notice .icon {
font-size: 18px;
flex-shrink: 0;
margin-top: 1px;
}
.dedup-notice .text {
font-size: 12px;
color: var(--primary);
line-height: 1.5;
}
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-state.show { display: flex; }
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.6;
}
.empty-title {
font-size: 20px;
font-weight: 600;
color: var(--text);
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
max-width: 260px;
}
.action-sheet-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 200;
display: none;
align-items: flex-end;
justify-content: center;
}
.action-sheet-overlay.show { display: flex; }
.action-sheet {
width: 100%;
background: var(--bg-card);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
padding: 8px 0 var(--safe-bottom);
animation: slide-up 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
@keyframes slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.action-sheet-handle {
width: 36px;
height: 5px;
border-radius: 3px;
background: var(--bg-secondary);
margin: 6px auto 12px;
}
.action-sheet-title {
font-size: 13px;
color: var(--text-secondary);
text-align: center;
padding: 4px 16px 12px;
}
.action-sheet-btn {
width: 100%;
padding: 16px;
font-size: 20px;
text-align: center;
border: none;
background: none;
font-family: var(--font-family);
cursor: pointer;
color: var(--primary);
transition: background 0.15s;
}
.action-sheet-btn:hover { background: var(--bg-secondary); }
.action-sheet-btn.danger { color: var(--danger); }
.action-sheet-btn:not(:last-child) {
border-bottom: 0.5px solid var(--separator);
}
.action-sheet-cancel {
margin-top: 8px;
background: var(--bg-grouped);
font-weight: 600;
}
.toast {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--bg-card);
color: var(--text);
padding: 12px 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
font-size: 14px;
font-weight: 500;
z-index: 300;
opacity: 0;
transition: all 0.3s cubic-bezier(0.32, 0.72, 0, 1);
pointer-events: none;
white-space: nowrap;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.home-indicator {
height: 34px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: transparent;
}
.home-indicator-bar {
width: 134px;
height: 5px;
border-radius: 3px;
background: var(--text-disabled);
opacity: 0.3;
}
@media (max-width: 420px) {
body { padding: 0; }
.page-header, .theme-switcher { display: none; }
.phone-frame {
width: 100%;
height: 100vh;
border-radius: 0;
box-shadow: none;
}
.phone-notch { display: none; }
}
</style>
</head>
<body>
<div class="page-header">
<h1>📱 我的设备 — 设计方案</h1>
<p>iOS 26 风格 · 毛玻璃效果 · 圆角卡片 · 响应式</p>
</div>
<div class="theme-switcher">
<button class="theme-btn active" onclick="switchTheme('light')">☀️ 浅色</button>
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色</button>
</div>
<div class="phone-frame" id="phoneFrame">
<div class="phone-notch"></div>
<div class="phone-status-bar">
<span>9:41</span>
<span style="letter-spacing: 2px;">●●●● ◗ ▮</span>
</div>
<nav class="nav-bar">
<div class="back">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
</div>
<div class="title">我的设备</div>
<button class="action" onclick="handleOfflineAll()">全部下线</button>
</nav>
<div class="scroll-area" id="scrollArea">
<div class="section-label">设备概览</div>
<div class="overview-card">
<div class="overview-item">
<div class="overview-value online" id="onlineCount">1</div>
<div class="overview-label">在线设备</div>
</div>
<div class="overview-item">
<div class="overview-value" id="totalCount">2<span class="unit"></span></div>
<div class="overview-label">总设备数</div>
</div>
<div class="overview-item">
<div class="overview-value ip" id="ipLocation">北京</div>
<div class="overview-label">当前IP归属地</div>
</div>
</div>
<div class="section-label">我的设备</div>
<div id="deviceList"></div>
<div class="dedup-notice">
<span class="icon"></span>
<span class="text">同一设备仅显示最新一条记录,按 device_id 自动去重。若同一设备多次登录,仅保留最近活跃的会话。</span>
</div>
<div class="empty-state" id="emptyState">
<div class="empty-icon">📱</div>
<div class="empty-title">暂无设备</div>
<div class="empty-desc">登录后设备将自动出现在这里,你可以管理所有已登录的设备。</div>
</div>
</div>
<div class="home-indicator">
<div class="home-indicator-bar"></div>
</div>
<div class="action-sheet-overlay" id="actionSheetOverlay" onclick="closeActionSheet(event)">
<div class="action-sheet" id="actionSheet">
<div class="action-sheet-handle"></div>
<div class="action-sheet-title" id="actionSheetTitle">设备操作</div>
<button class="action-sheet-btn" onclick="handleAction('rename')">✏️ 重命名设备</button>
<button class="action-sheet-btn" onclick="handleAction('detail')">📋 查看详情</button>
<button class="action-sheet-btn danger" onclick="handleAction('offline')">🔴 强制下线</button>
<button class="action-sheet-btn action-sheet-cancel" onclick="closeActionSheet()">取消</button>
</div>
</div>
<div class="toast" id="toast"></div>
</div>
<script>
var devices = [
{
device_id: 'ios_a1b2c3d4',
name: '我的 iPhone',
model: 'iPhone 16 Pro',
platform: 'ios',
os_version: 'iOS 18.5',
is_online: true,
ip_location: '北京 · 朝阳',
last_active: '刚刚',
ip: '116.24.xxx.xxx',
login_time: '2026-05-12 09:30',
app_version: 'v2.4.0'
},
{
device_id: 'android_e5f6g7h8',
name: '备用安卓机',
model: 'Xiaomi 15',
platform: 'android',
os_version: 'Android 15',
is_online: false,
ip_location: '上海 · 浦东',
last_active: '2小时前',
ip: '139.186.xxx.xxx',
login_time: '2026-05-10 18:45',
app_version: 'v2.3.8'
}
];
var currentDeviceId = null;
function getPlatformIcon(platform) {
var icons = {
ios: '',
android: '',
web: '',
unknown: ''
};
return icons[platform] || icons.unknown;
}
function getPlatformClass(platform) {
var classes = { ios: 'ios', android: 'android', web: 'web' };
return classes[platform] || 'unknown';
}
function getPlatformEmoji(platform) {
var emojis = { ios: '', android: '', web: '🌐', unknown: '📟' };
return emojis[platform] || emojis.unknown;
}
function deduplicateDevices(list) {
var map = {};
list.forEach(function(d) {
if (!map[d.device_id]) {
map[d.device_id] = d;
} else {
if (d.last_active > map[d.device_id].last_active) {
map[d.device_id] = d;
}
}
});
return Object.values(map);
}
function renderDevices() {
var deduped = deduplicateDevices(devices);
var container = document.getElementById('deviceList');
var onlineCount = deduped.filter(function(d) { return d.is_online; }).length;
var totalCount = deduped.length;
document.getElementById('onlineCount').textContent = onlineCount;
document.getElementById('totalCount').innerHTML = totalCount + '<span class="unit"> 台</span>';
if (deduped.length === 0) {
container.innerHTML = '';
document.getElementById('emptyState').classList.add('show');
return;
}
document.getElementById('emptyState').classList.remove('show');
var html = '';
deduped.forEach(function(device) {
var statusClass = device.is_online ? 'online' : 'offline';
var statusTag = device.is_online
? '<span class="meta-tag">在线</span>'
: '<span class="meta-tag offline-tag">离线</span>';
html += '<div class="device-card" data-id="' + device.device_id + '">';
html += ' <div class="device-main" onclick="toggleDetail(\'' + device.device_id + '\')">';
html += ' <div class="device-icon-wrap ' + getPlatformClass(device.platform) + '">';
html += ' ' + getPlatformEmoji(device.platform);
html += ' </div>';
html += ' <div class="device-info">';
html += ' <div class="device-name-row">';
html += ' <span class="device-name">' + device.name + '</span>';
html += ' <span class="status-dot ' + statusClass + '"></span>';
html += ' </div>';
html += ' <div class="device-model">' + device.model + '</div>';
html += ' <div class="device-meta">';
html += ' <span class="meta-tag os">' + device.os_version + '</span>';
html += ' ' + statusTag;
html += ' <span class="meta-tag">📍 ' + device.ip_location + '</span>';
html += ' </div>';
html += ' </div>';
html += ' <button class="device-more" onclick="event.stopPropagation(); openActionSheet(\'' + device.device_id + '\')">⋯</button>';
html += ' </div>';
html += ' <div class="device-detail" id="detail-' + device.device_id + '">';
html += ' <div class="detail-row"><span class="detail-label">设备ID</span><span class="detail-value">' + device.device_id + '</span></div>';
html += ' <div class="detail-row"><span class="detail-label">IP地址</span><span class="detail-value">' + device.ip + '</span></div>';
html += ' <div class="detail-row"><span class="detail-label">IP归属地</span><span class="detail-value">' + device.ip_location + '</span></div>';
html += ' <div class="detail-row"><span class="detail-label">登录时间</span><span class="detail-value">' + device.login_time + '</span></div>';
html += ' <div class="detail-row"><span class="detail-label">最后活跃</span><span class="detail-value">' + device.last_active + '</span></div>';
html += ' <div class="detail-row"><span class="detail-label">App版本</span><span class="detail-value">' + device.app_version + '</span></div>';
html += ' </div>';
html += '</div>';
});
container.innerHTML = html;
}
function toggleDetail(deviceId) {
var el = document.getElementById('detail-' + deviceId);
if (el) {
el.classList.toggle('show');
}
}
function openActionSheet(deviceId) {
currentDeviceId = deviceId;
var device = devices.find(function(d) { return d.device_id === deviceId; });
if (device) {
document.getElementById('actionSheetTitle').textContent = device.name;
}
document.getElementById('actionSheetOverlay').classList.add('show');
}
function closeActionSheet(e) {
if (e && e.target !== document.getElementById('actionSheetOverlay')) return;
document.getElementById('actionSheetOverlay').classList.remove('show');
currentDeviceId = null;
}
function handleAction(action) {
var device = devices.find(function(d) { return d.device_id === currentDeviceId; });
if (!device) return;
document.getElementById('actionSheetOverlay').classList.remove('show');
if (action === 'offline') {
device.is_online = false;
renderDevices();
showToast('🔴 已将「' + device.name + '」强制下线');
} else if (action === 'rename') {
showToast('✏️ 重命名功能开发中');
} else if (action === 'detail') {
toggleDetail(device.device_id);
}
currentDeviceId = null;
}
function handleOfflineAll() {
var onlineDevices = devices.filter(function(d) { return d.is_online; });
if (onlineDevices.length === 0) {
showToast('没有在线设备');
return;
}
devices.forEach(function(d) { d.is_online = false; });
renderDevices();
showToast('🔴 已将所有设备下线');
}
function showToast(msg) {
var toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(function() {
toast.classList.remove('show');
}, 2200);
}
function switchTheme(theme) {
var frame = document.getElementById('phoneFrame');
if (theme === 'dark') {
frame.setAttribute('data-theme', 'dark');
} else {
frame.removeAttribute('data-theme');
}
document.querySelectorAll('.theme-btn').forEach(function(btn) {
btn.classList.remove('active');
});
event.target.classList.add('active');
}
renderDevices();
</script>
</body>
</html>