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

581 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 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: '请求失败,请重试。',
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.',
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 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: data.error || 'unknown' });
} catch (_err) {
setStatusKey('status_request_failed', 'err');
}
submitBtn.disabled = false;
});
window.addEventListener('load', () => {
initLanguage();
setStatusKey('status_ready', '');
scanNetworks();
});
</script>
</body>
</html>