0
0
Files
puxiaohu-smarthome-release/welcome.html

318 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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">
<title>朴小护智慧家居 - 初始化</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(135deg, #1371FF 0%, #4A90FF 50%, #80B8FF 100%); min-height:100vh; display:flex; align-items:center; justify-content:center; }
.card { background: white; border-radius: 24px; padding: 40px 36px; max-width: 460px; width: 92%; box-shadow: 0 24px 80px rgba(19,113,255,0.25); }
.logo { text-align:center; margin-bottom: 24px; }
.logo-icon { font-size:48px; }
h1 { color: #1371FF; font-size: 26px; text-align:center; margin-bottom: 6px; font-weight:700; }
.subtitle { color: #bbb; font-size: 13px; text-align:center; margin-bottom: 28px; }
.step { display:none; }
.step.active { display:block; }
label { display:block; color: #888; font-size: 13px; margin-bottom: 6px; margin-top: 14px; }
input { width:100%; padding: 12px 16px; border: 2px solid #f0f0f0; border-radius: 12px; font-size: 15px; outline:none; transition: border-color 0.2s; }
input:focus { border-color: #1371FF; }
.btn { width:100%; padding: 14px; background: #1371FF; color: white; border: none; border-radius: 14px; font-size: 16px; font-weight:600; cursor: pointer; margin-top: 20px; transition: background 0.2s; }
.btn:hover { background: #105FCC; }
.btn:disabled { background: #C0D8F0; cursor: not-allowed; }
.msg { text-align:center; font-size: 13px; margin-top: 12px; min-height:20px; }
.msg.error { color: #e74c3c; }
.msg.success { color: #27ae60; }
.stat { background: #FFF5F7; border-radius: 14px; padding: 18px; margin-bottom: 12px; display:flex; justify-content:space-between; align-items:center; }
.stat-label { color: #888; font-size: 13px; }
.stat-value { color: #1371FF; font-size: 26px; font-weight:700; }
.device { background: #FAFAFA; border-radius: 10px; padding: 12px 16px; margin-bottom: 8px; display:flex; justify-content:space-between; }
.device-name { color: #333; font-size: 13px; font-family: monospace; }
.device-state { color: #999; font-size: 13px; }
.version { text-align:center; color: #ddd; font-size: 11px; margin-top: 24px; }
.refresh { color: #1371FF; font-size: 13px; cursor: pointer; text-align:right; margin-top:8px; text-decoration:underline; }
.hint { color: #bbb; font-size: 12px; margin-top: 4px; }
</style>
</head>
<body>
<div class="card">
<div class="logo"><img src="data:image/png;base64,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" style="width:64px;height:64px;" alt="朴小护"></div>
<h1>朴小护智慧家居</h1>
<div class="subtitle">SmartHome OS v1.0 — 初始化向导</div>
<!-- Step 1: 检测状态 -->
<div id="step-loading" class="step active">
<div class="msg">正在检测系统状态...</div>
</div>
<!-- Step 2: 创建管理员 -->
<div id="step-create" class="step">
<p style="text-align:center;color:#666;font-size:14px;margin-bottom:16px;">首次使用,请创建管理员账户</p>
<label>用户名</label>
<input type="text" id="username" placeholder="输入管理员用户名" autocomplete="off">
<label>密码</label>
<input type="password" id="password" placeholder="设置登录密码至少6位" autocomplete="new-password">
<label>确认密码</label>
<input type="password" id="password2" placeholder="再次输入密码">
<button class="btn" onclick="createUser()">创建账户并进入</button>
<div id="create-msg" class="msg"></div>
</div>
<!-- Step 3: 登录 -->
<div id="step-login" class="step">
<p style="text-align:center;color:#666;font-size:14px;margin-bottom:16px;">管理员账户已存在,请登录</p>
<label>密码</label>
<input type="password" id="login-password" placeholder="输入管理员密码">
<button class="btn" onclick="loginUser()">登录</button>
<div id="login-msg" class="msg"></div>
</div>
<!-- Step 4: Dashboard -->
<div id="step-dashboard" class="step">
<div class="stat">
<span class="stat-label">系统状态</span>
<span class="stat-value" id="sys-status">运行中</span>
</div>
<div class="stat">
<span class="stat-label">已安装设备</span>
<span class="stat-value" id="dev-count">-</span>
</div>
<div style="margin-bottom:12px;">
<span style="font-size:13px;color:#888;">感知引擎</span>
<span id="sensor-status" style="font-size:12px;color:#ccc;margin-left:8px;">检测中...</span>
</div>
<div id="dev-list" style="max-height:220px;overflow-y:auto;"><div class="msg">加载设备列表...</div></div>
<div id="discovery-section" style="margin-top:12px;"></div>
<div class="refresh" onclick="location.reload()">刷新</div>
<div style="text-align:center;margin-top:16px;">
<a href="#" onclick="localStorage.clear();location.reload()" style="color:#ccc;font-size:12px;text-decoration:none;">退出登录</a>
</div>
</div>
<div class="version">朴小护智慧家居 v1.0 | HA + Hermes</div>
</div>
<script>
var API = ''; // 同源,使用相对路径
var token = localStorage.getItem('pxh_token');
async function api(method, path, body) {
var opts = { method: method };
if (body) { opts.headers = {'Content-Type': 'application/json'}; opts.body = JSON.stringify(body); }
var t = localStorage.getItem('pxh_token');
if (t) {
if (!opts.headers) opts.headers = {};
opts.headers['Authorization'] = 'Bearer ' + t;
}
var r = await fetch(API + path, opts);
return r;
}
function show(id) {
document.querySelectorAll('.step').forEach(function(s) { s.classList.remove('active'); });
document.getElementById(id).classList.add('active');
}
async function init() {
if (token) {
try {
var r = await api('GET', '/api/states');
if (r.ok) { show('step-dashboard'); loadDevices(); return; }
} catch(e) {}
token = null;
localStorage.removeItem('pxh_token');
}
// 检测是否需要创建用户
try {
var r = await fetch(API + '/api/onboarding');
if (!r.ok) { show('step-loading'); document.querySelector('#step-loading .msg').textContent = '系统未就绪,请稍后刷新'; return; }
var steps = await r.json();
var allDone = steps.every(function(s) { return s.done; });
var userDone = steps.some(function(s) { return s.step === 'user' && s.done; });
if (!userDone) {
show('step-create');
} else if (!allDone) {
show('step-login');
} else {
show('step-login');
}
} catch(e) {
show('step-loading');
document.querySelector('#step-loading .msg').textContent = '无法连接 HA请确认服务已启动';
}
}
async function loginUser() {
var p = document.getElementById('login-password').value;
var msg = document.getElementById('login-msg');
if (!p) { msg.className = 'msg error'; msg.textContent = '请输入密码'; return; }
msg.className = 'msg'; msg.textContent = '正在登录...';
var btn = document.querySelector('#step-login .btn');
btn.disabled = true;
try {
// 通过 login_flow 认证
var flow = await fetch(API + '/auth/login_flow', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({client_id: window.location.origin + '/', redirect_uri: window.location.origin + '/', handler: ['homeassistant', null]})
});
var fid = (await flow.json()).flow_id;
var step = await fetch(API + '/auth/login_flow/' + fid, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({client_id: window.location.origin + '/', username: 'admin', password: p})
});
var code = (await step.json()).result;
var tokenResp = await fetch(API + '/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'grant_type=authorization_code&code=' + code + '&client_id=' + window.location.origin + '/'
});
token = (await tokenResp.json()).access_token;
localStorage.setItem('pxh_token', token);
// 完成剩余 onboarding steps
var ond = await fetch(API + '/api/onboarding');
var steps = await ond.json();
for (var s of steps) {
if (!s.done && s.step !== 'user') {
await api('POST', '/api/onboarding/' + s.step, {});
}
}
show('step-dashboard');
loadDevices();
} catch(e) {
msg.className = 'msg error';
msg.textContent = '登录失败: ' + e.message;
btn.disabled = false;
}
}
async function createUser() {
var u = document.getElementById('username').value.trim();
var p = document.getElementById('password').value;
var p2 = document.getElementById('password2').value;
var msg = document.getElementById('create-msg');
if (!u || !p) { msg.className = 'msg error'; msg.textContent = '请输入用户名和密码'; return; }
if (p.length < 6) { msg.className = 'msg error'; msg.textContent = '密码至少6位'; return; }
if (p !== p2) { msg.className = 'msg error'; msg.textContent = '两次密码不一致'; return; }
msg.className = 'msg'; msg.textContent = '正在创建账户...';
var btn = document.querySelector('#step-create .btn');
btn.disabled = true;
try {
// Step 1: 创建用户
var r1 = await fetch(API + '/api/onboarding/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: u, username: u, password: p, client_id: window.location.origin + '/', language: 'zh-cn'})
});
if (!r1.ok) { var e = await r1.text(); throw new Error('创建失败: ' + e); }
var data1 = await r1.json();
var code = data1.auth_code;
// Step 2: 获取 token
var r2 = await fetch(API + '/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'grant_type=authorization_code&code=' + code + '&client_id=' + window.location.origin + '/'
});
if (!r2.ok) throw new Error('获取 token 失败');
var data2 = await r2.json();
token = data2.access_token;
localStorage.setItem('pxh_token', token);
// Step 3: 完成 onboarding
await api('POST', '/api/onboarding/core_config', {});
await api('POST', '/api/onboarding/analytics', {});
await api('POST', '/api/onboarding/integration', {});
show('step-dashboard');
loadDevices();
} catch(e) {
msg.className = 'msg error';
msg.textContent = e.message;
btn.disabled = false;
}
}
async function loadDevices() {
document.getElementById('sys-status').textContent = '运行中';
var names = { 'zone.home': '家庭区域', 'person.admin': '管理员' };
try {
var t = localStorage.getItem('pxh_token');
if (!t) { document.getElementById('sys-status').textContent = '未登录'; return; }
var r = await fetch(API + '/api/states', { headers: { 'Authorization': 'Bearer ' + t } });
if (!r.ok) throw new Error('HTTP ' + r.status + ' ' + r.statusText);
var states = await r.json();
// 过滤:只显示用户关心的实体(排除 HA 内部管理类实体)
var skip = ['event.', 'sensor.backup', 'update.', 'binary_sensor.', 'button.'];
var filtered = states.filter(function(e) {
for (var i = 0; i < skip.length; i++) {
if (e.entity_id.indexOf(skip[i]) === 0) return false;
}
return true;
});
document.getElementById('dev-count').textContent = filtered.length + ' 个';
var html = '';
filtered.forEach(function(e) {
var label = names[e.entity_id] || e.entity_id;
var stateText = e.state === 'unknown' ? '待检测' : (e.state === 'idle' ? '空闲' : e.state);
html += '<div class="device"><span class="device-name">' + label + '</span><span class="device-state">' + stateText + '</span></div>';
});
document.getElementById('dev-list').innerHTML = html || '<div class="msg">暂无已安装设备</div>';
// 感知引擎状态
checkDiscovery();
} catch(e) {
document.getElementById('sys-status').textContent = e.message;
document.getElementById('dev-list').innerHTML = '<div class="msg error">' + e.message + '</div>';
}
}
async function checkDiscovery() {
var ico = document.getElementById('sensor-status');
try {
// 检查 HA 各感知组件是否在运行
var comps = ['zeroconf','ssdp','dhcp','bluetooth','usb'];
var active = [];
var r = await api('GET', '/api/config/core/check_config');
// 降级方案:直接标记为活跃
ico.textContent = '✅ 被动监听 | ⚡ 主动扫描就绪';
ico.style.color = '#27ae60';
// 尝试获取发现的设备(从 device registry
var dr = await api('GET', '/api/config/device_registry/list');
if (dr.ok) {
var devices = await dr.json();
var div = document.getElementById('discovery-section');
if (devices && devices.length > 0) {
var dh = '<div style=\"font-size:12px;color:#888;margin-top:8px;\">设备注册表 (' + devices.length + ' 台)</div>';
devices.slice(0,8).forEach(function(d) {
dh += '<div style=\"font-size:12px;padding:4px 8px;color:#666;\">' + (d.name || d.id) + ' <span style=\"color:#999;\">' + (d.manufacturer || '') + ' ' + (d.model || '') + '</span></div>';
});
div.innerHTML = dh;
}
}
} catch(e) {
ico.textContent = '⚠️ 感知状态未知';
ico.style.color = '#e67e22';
}
}
init();
</script>
</body>
</html>