Files
AI_Printer/components/platform/assets/wifi_configuration.html

625 lines
21 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 http-equiv="Content-Security-Policy" content="referrer no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>TalkingQ Network Configuration</title>
<style>
:root {
--bg-a: #031f4c;
--bg-b: #0a66ff;
--bg-c: #2bc7ff;
--text-primary: #0b1c35;
--text-secondary: #4f5f86;
--btn-a: #0a61ff;
--btn-b: #24befc;
--ok-bg: #dcfce7;
--ok-text: #166534;
--err-bg: #fee2e2;
--err-text: #991b1b;
--muted-bg: #eef4ff;
--muted-text: #2a4b95;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
color: var(--text-primary);
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-a) 0%, var(--bg-b) 45%, var(--bg-c) 100%);
padding: 28px 14px 40px;
}
.page-shell {
width: 100%;
max-width: 860px;
margin: 0 auto;
}
.card {
background: rgba(255, 255, 255, 0.96);
border-radius: 26px;
overflow: hidden;
box-shadow: 0 28px 65px rgba(5, 29, 82, 0.35);
backdrop-filter: blur(8px);
}
.brand-header {
display: flex;
align-items: center;
gap: 14px;
padding: 24px 26px;
color: #fff;
background: linear-gradient(120deg, #0a54ff, #1f95ff 60%, #66e0ff);
}
.brand-logo {
width: 56px;
height: 56px;
border-radius: 18px;
border: 2px solid rgba(255, 255, 255, 0.55);
background: rgba(255, 255, 255, 0.12);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
letter-spacing: 1px;
box-shadow: 0 8px 22px rgba(3, 18, 66, 0.25);
flex-shrink: 0;
}
.brand-copy {
min-width: 0;
flex: 1;
}
.brand-copy h1 {
margin: 0;
font-size: 24px;
letter-spacing: 0.4px;
}
.brand-copy p {
margin: 4px 0 0;
opacity: 0.86;
font-size: 13px;
}
.language-switch {
width: 156px;
}
.language-switch select {
width: 100%;
padding: 10px 12px;
border-radius: 12px;
border: none;
color: #fff;
font-weight: 600;
background: rgba(255, 255, 255, 0.2);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.language-switch select:focus {
outline: 2px solid rgba(255, 255, 255, 0.85);
}
.content {
padding: 28px 24px 30px;
}
.intro {
margin: 0 0 18px;
}
.intro h2 {
margin: 0;
color: #0a2d72;
font-size: 26px;
}
.intro p {
margin: 8px 0 0;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.5;
}
.panel {
border: 1px solid #e0e8ff;
border-radius: 18px;
background: #f8fbff;
padding: 18px;
}
.field {
margin-top: 14px;
}
.field:first-child {
margin-top: 0;
}
label {
display: block;
margin-bottom: 8px;
color: #1a2f5c;
font-weight: 600;
font-size: 14px;
}
input,
select,
button {
width: 100%;
border-radius: 12px;
border: 1px solid #d6def0;
font-size: 15px;
}
input,
select {
padding: 12px;
background: #fff;
}
input:focus,
select:focus {
outline: none;
border-color: #2b8cff;
box-shadow: 0 0 0 3px rgba(43, 140, 255, 0.2);
}
.button {
border: none;
color: #fff;
cursor: pointer;
padding: 12px;
font-weight: 700;
letter-spacing: 0.2px;
background: linear-gradient(120deg, var(--btn-a), var(--btn-b));
box-shadow: 0 12px 30px rgba(9, 71, 161, 0.35);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 16px 34px rgba(8, 64, 150, 0.35);
}
.button:disabled {
background: #b9c6e4;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
.secondary {
color: var(--muted-text);
background: var(--muted-bg);
border: 1px solid #c9d8ff;
box-shadow: none;
}
.status {
margin-top: 16px;
min-height: 24px;
padding: 11px 12px;
border-radius: 12px;
border: 1px solid #dce5ff;
background: #f4f7ff;
color: #334c7d;
font-size: 14px;
line-height: 1.45;
}
.status.ok {
background: var(--ok-bg);
color: var(--ok-text);
border-color: #b7e9c4;
}
.status.err {
background: var(--err-bg);
color: var(--err-text);
border-color: #fecaca;
}
@media (max-width: 720px) {
.brand-header {
padding: 20px;
flex-wrap: wrap;
}
.language-switch {
width: 100%;
}
.content {
padding: 22px 18px 24px;
}
.intro h2 {
font-size: 22px;
}
}
</style>
</head>
<body>
<div class="page-shell">
<div class="card">
<div class="brand-header">
<div class="brand-logo">TQ</div>
<div class="brand-copy">
<h1>TalkingQ</h1>
<p data-i18n="tagline">Draw smarter. Connect faster.</p>
</div>
<div class="language-switch">
<select id="language" aria-label="Language">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
</div>
<div class="content">
<div class="intro">
<h2 data-i18n="title">Wi-Fi Configuration</h2>
<p data-i18n="subtitle">Select a scanned network or enter SSID manually. Credentials are saved only after connection succeeds.</p>
</div>
<div class="panel">
<div class="field">
<button id="scan-btn" class="button secondary" type="button" data-i18n="scan_button">Scan Nearby Wi-Fi</button>
</div>
<div class="field">
<label for="network-list" data-i18n="nearby_label">Nearby Wi-Fi</label>
<select id="network-list">
<option value="" data-i18n="manual_option">Manual input</option>
</select>
</div>
<form id="wifi-form">
<div class="field">
<label for="ssid" data-i18n="ssid_label">SSID</label>
<input id="ssid" name="ssid" maxlength="32" required>
</div>
<div class="field">
<label for="password" data-i18n="password_label">Password</label>
<input id="password" name="password" maxlength="64" type="password" autocomplete="off">
</div>
<div class="field">
<button id="submit-btn" class="button" type="submit" data-i18n="connect_button">Connect</button>
</div>
</form>
<div id="status" class="status"></div>
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById('wifi-form');
const statusBox = document.getElementById('status');
const submitBtn = document.getElementById('submit-btn');
const scanBtn = document.getElementById('scan-btn');
const networkList = document.getElementById('network-list');
const ssidInput = document.getElementById('ssid');
const languageSelect = document.getElementById('language');
const i18n = {
'zh-CN': {
title: 'Wi-Fi 配网',
subtitle: '可先从扫描结果选择网络,也可以手动输入 SSID。仅在连接成功后才会保存密码。',
tagline: 'Draw smarter. Connect faster.',
scan_button: '扫描附近 Wi-Fi',
nearby_label: '附近 Wi-Fi',
manual_option: '手动输入',
ssid_label: 'SSID',
password_label: '密码',
connect_button: '连接',
status_ready: '准备就绪。',
status_scanning: '正在扫描附近 Wi-Fi...',
status_scan_ok: '扫描完成,请选择网络后连接。',
status_scan_empty: '未扫描到 Wi-Fi请手动输入 SSID。',
status_scan_failed: '扫描失败,请手动输入 SSID。',
status_ssid_required: 'SSID 不能为空。',
status_connecting: '正在连接,请稍候...',
status_connect_ok: '连接成功,正在关闭配网页面...',
status_connect_failed: '连接失败:{error}。请重试。',
status_request_failed: '请求失败,请重试。',
error_invalid_payload: '提交内容无效',
error_read_failed: '设备读取请求失败',
error_ssid_required: 'SSID 不能为空',
error_password_too_short: '密码至少需要 8 位',
error_wrong_password: '密码错误',
error_no_ap_found: '未找到该 Wi-Fi',
error_signal_too_weak: '信号太弱,请靠近路由器',
error_ap_not_responding: '路由器无响应',
error_association_failed: '连接被路由器拒绝',
error_ap_security_unsupported: '路由器加密方式暂不支持',
error_connect_timeout: '连接超时',
error_wifi_unavailable: '设备 Wi-Fi 暂不可用',
error_save_failed: '连接成功但保存失败,请重试',
error_connect_failed: '无法连接到该 Wi-Fi',
error_unknown: '未知错误',
signal_unit: 'dBm'
},
'en-US': {
title: 'Wi-Fi Configuration',
subtitle: 'Select a scanned network or enter SSID manually. Credentials are saved only after connection succeeds.',
tagline: 'Draw smarter. Connect faster.',
scan_button: 'Scan Nearby Wi-Fi',
nearby_label: 'Nearby Wi-Fi',
manual_option: 'Manual input',
ssid_label: 'SSID',
password_label: 'Password',
connect_button: 'Connect',
status_ready: 'Ready.',
status_scanning: 'Scanning nearby Wi-Fi...',
status_scan_ok: 'Scan complete. Select a network and connect.',
status_scan_empty: 'No Wi-Fi found. Please input SSID manually.',
status_scan_failed: 'Scan failed. Please input SSID manually.',
status_ssid_required: 'SSID is required.',
status_connecting: 'Connecting, please wait...',
status_connect_ok: 'Connected successfully. Closing this provisioning page...',
status_connect_failed: 'Connection failed: {error}. Please try again.',
status_request_failed: 'Request failed. Please try again.',
error_invalid_payload: 'Invalid request payload',
error_read_failed: 'Device failed to read request',
error_ssid_required: 'SSID is required',
error_password_too_short: 'Password must be at least 8 characters',
error_wrong_password: 'Wrong password',
error_no_ap_found: 'Wi-Fi network not found',
error_signal_too_weak: 'Signal is too weak',
error_ap_not_responding: 'Router is not responding',
error_association_failed: 'Router rejected the connection',
error_ap_security_unsupported: 'Wi-Fi security mode is unsupported',
error_connect_timeout: 'Connection timed out',
error_wifi_unavailable: 'Device Wi-Fi is unavailable',
error_save_failed: 'Connected, but failed to save credentials',
error_connect_failed: 'Unable to connect to this Wi-Fi',
error_unknown: 'Unknown error',
signal_unit: 'dBm'
}
};
const languageMap = {
'zh': 'zh-CN',
'zh-CN': 'zh-CN',
'zh-SG': 'zh-CN',
'en': 'en-US',
'en-US': 'en-US',
'en-GB': 'en-US',
'en-CA': 'en-US',
'en-AU': 'en-US'
};
let currentLang = 'zh-CN';
function tr(key) {
const langPack = i18n[currentLang] || i18n['en-US'];
return langPack[key] || i18n['en-US'][key] || key;
}
function trWithParams(key, params) {
let text = tr(key);
if (!params) {
return text;
}
Object.keys(params).forEach((k) => {
text = text.split('{' + k + '}').join(String(params[k]));
});
return text;
}
function setStatusText(text, type) {
statusBox.textContent = text;
statusBox.className = type ? ('status ' + type) : 'status';
}
function setStatusKey(key, type, params) {
setStatusText(trWithParams(key, params), type);
}
function mapProvisionError(code) {
if (!code) {
return tr('error_unknown');
}
const key = 'error_' + String(code);
const mapped = tr(key);
if (mapped === key) {
return tr('error_connect_failed');
}
return mapped;
}
function decodeSsid(value) {
try {
return decodeURIComponent(value);
} catch (_err) {
return value;
}
}
function closeProvisionPage() {
window.open('', '_self');
window.close();
setTimeout(() => {
window.location.replace('about:blank');
}, 250);
}
function applyTranslations() {
document.documentElement.lang = currentLang;
document.title = 'TalkingQ Network Configuration';
document.querySelectorAll('[data-i18n]').forEach((el) => {
const key = el.getAttribute('data-i18n');
const text = tr(key);
if (text) {
el.textContent = text;
}
});
const manualOption = networkList.querySelector('option[value=\"\"]');
if (manualOption) {
manualOption.textContent = tr('manual_option');
}
if (!statusBox.textContent) {
setStatusKey('status_ready', '');
}
}
function normalizeLanguage(input) {
if (!input) {
return 'zh-CN';
}
if (languageMap[input]) {
return languageMap[input];
}
const main = input.split('-')[0];
return languageMap[main] || 'zh-CN';
}
function initLanguage() {
const saved = localStorage.getItem('provision_lang');
const browser = (navigator.language || 'zh-CN').trim();
currentLang = normalizeLanguage(saved || browser);
languageSelect.value = currentLang;
applyTranslations();
}
languageSelect.addEventListener('change', () => {
currentLang = normalizeLanguage(languageSelect.value);
localStorage.setItem('provision_lang', currentLang);
applyTranslations();
});
function rebuildNetworkList(networks) {
networkList.innerHTML = '';
const manualOption = document.createElement('option');
manualOption.value = '';
manualOption.textContent = tr('manual_option');
networkList.appendChild(manualOption);
networks.forEach((item) => {
const ssid = decodeSsid(item.ssid || '');
if (!ssid) {
return;
}
const option = document.createElement('option');
const signal = typeof item.rssi === 'number' ? (item.rssi + ' ' + tr('signal_unit')) : ('? ' + tr('signal_unit'));
const auth = item.auth || 'unknown';
option.value = ssid;
option.textContent = ssid + ' (' + signal + ', ' + auth + ')';
networkList.appendChild(option);
});
}
async function scanNetworks() {
scanBtn.disabled = true;
setStatusKey('status_scanning', '');
try {
const response = await fetch('/scan', { cache: 'no-store' });
const data = await response.json();
if (!data.ok) {
throw new Error(data.error || 'scan_failed');
}
const networks = Array.isArray(data.networks) ? data.networks : [];
rebuildNetworkList(networks);
if (networks.length > 0) {
setStatusKey('status_scan_ok', '');
} else {
setStatusKey('status_scan_empty', 'err');
}
} catch (_err) {
rebuildNetworkList([]);
setStatusKey('status_scan_failed', 'err');
}
scanBtn.disabled = false;
}
networkList.addEventListener('change', () => {
if (networkList.value) {
ssidInput.value = networkList.value;
}
});
scanBtn.addEventListener('click', () => {
scanNetworks();
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
let ssid = ssidInput.value.trim();
if (!ssid && networkList.value) {
ssid = networkList.value;
ssidInput.value = ssid;
}
const password = document.getElementById('password').value;
if (!ssid) {
setStatusKey('status_ssid_required', 'err');
return;
}
submitBtn.disabled = true;
setStatusKey('status_connecting', '');
try {
const payload = 'ssid=' + encodeURIComponent(ssid) + '&password=' + encodeURIComponent(password);
const response = await fetch('/provision', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: payload
});
const data = await response.json();
if (data.ok) {
setStatusKey('status_connect_ok', 'ok');
setTimeout(() => {
closeProvisionPage();
}, 1200);
return;
}
setStatusKey('status_connect_failed', 'err', {
error: mapProvisionError(data.error)
});
} catch (_err) {
setStatusKey('status_request_failed', 'err');
}
submitBtn.disabled = false;
});
window.addEventListener('load', () => {
initLanguage();
setStatusKey('status_ready', '');
scanNetworks();
});
</script>
</body>
</html>