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

282 lines
16 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 id="dev-list" style="max-height:300px;overflow-y:auto;"><div class="msg">加载设备列表...</div></div>
<div class="refresh" onclick="loadDevices()">刷新设备列表</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, headers: {'Content-Type': 'application/json'} };
if (token) opts.headers['Authorization'] = 'Bearer ' + token;
if (body) opts.body = JSON.stringify(body);
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': '管理员',
'event.backup': '备份事件',
'sensor.backup': '备份状态',
'sensor.backup_2': '备份详情2',
'sensor.backup_3': '备份详情3',
'sensor.backup_4': '备份详情4',
};
try {
var r = await api('GET', '/api/states');
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">暂无设备 — 请接入小米 MIoT 或 HomeKit 硬件</div>';
} catch(e) {
document.getElementById('sys-status').textContent = e.message;
document.getElementById('dev-list').innerHTML = '<div class="msg error">' + e.message + '</div>';
}
}
init();
</script>
</body>
</html>