Files
xiaozhi-esp32/managed_components/78__esp-wifi-connect/assets/wifi_configuration.html
2025-09-05 13:25:11 +08:00

596 lines
22 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>
<head>
<title>Network Configuration</title>
<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">
<style type="text/css">
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 5px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 3px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
padding: 10px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
input[type="submit"]:disabled {
background-color: #ccc;
cursor: not-allowed;
}
#ap_list {
margin-top: 20px;
border-top: 1px solid #ccc;
padding-top: 10px;
}
#ap_list a {
display: block;
margin-top: 5px;
color: #007bff;
text-decoration: none;
}
#ap_list a:hover {
text-decoration: underline;
}
.language-switch {
position: absolute;
top: 20px;
right: 20px;
}
.language-switch select {
padding: 5px;
border-radius: 3px;
border: 1px solid #ccc;
}
/* 标签页样式 */
.tabs {
display: flex;
margin-top: 20px;
margin-bottom: 0;
padding-left: 20px;
border-bottom: 1px solid #ccc;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid #ccc;
border-bottom: none;
border-radius: 5px 5px 0 0;
margin-right: 5px;
background-color: #f0f0f0;
position: relative;
top: 1px;
}
.tab.active {
background-color: #fff;
}
.tab-content {
display: none;
border-top: none;
padding: 20px;
background-color: #fff;
}
.tab-content.active {
display: block;
}
#remember_bssid, #sleep_mode {
width: auto;
}
/* 提示框样式 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 30px;
border-radius: 8px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease-in-out;
min-width: 200px;
text-align: center;
}
.toast.show {
opacity: 1;
}
</style>
</head>
<body>
<div class="toast" id="toast"></div>
<div class="language-switch">
<select id="language" onchange="changeLanguage()">
<option value="en-US">English</option>
<option value="zh-CN">简体中文</option>
<option value="zh-TW">繁體中文</option>
<option value="ja-JP">日本語</option>
</select>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('wifi')" data-lang="wifi_tab">WiFi 配置</div>
<div class="tab" onclick="switchTab('advanced')" data-lang="advanced_tab">高级选项</div>
</div>
<div id="wifi-tab" class="tab-content active">
<form action="/submit" method="post" onsubmit="submitForm(event)">
<div id="saved_list_container" style="display: none;">
<h3 data-lang="saved_wifi">已保存的 WiFi</h3>
<ul id="saved_list">
<li>
<span>SSID</span>
</li>
</ul>
</div>
<div>
<h3 data-lang="new_wifi">新的 WiFi</h3>
<p class="error" style="color: red; text-align: center;" id="error">
</p>
<p>
<label for="ssid">SSID:</label>
<input type="text" id="ssid" name="ssid" required>
</p>
<p>
<label for="password" data-lang="password">密码:</label>
<input type="password" id="password" name="password">
</p>
<p style="text-align: center;">
<input type="submit" value="连接" id="button" data-lang-value="connect">
</p>
<p id="ap_list">
</p>
</div>
</form>
</div>
<div id="advanced-tab" class="tab-content">
<form action="/advanced/submit" method="post" onsubmit="submitAdvancedForm(event)">
<div>
<h3 data-lang="advanced_tab">高级选项</h3>
<p class="error" style="color: red; text-align: center;" id="advanced_error"></p>
<p>
<label for="ota_url" data-lang="ota_url">OTA服务器地址:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="text" id="ota_url" name="ota_url" style="flex: 1;">
<button type="button" onclick="clearOtaUrl()" style="padding: 5px 10px; border: 1px solid #ccc; border-radius: 3px; background-color: #f0f0f0; cursor: pointer;"></button>
</div>
</p>
<p style="display: flex; align-items: center; gap: 10px;">
<label for="max_tx_power" data-lang="max_tx_power" style="white-space: nowrap;">Wi-Fi发射功率:</label>
<select id="max_tx_power" name="max_tx_power">
<option value="8">2 dBm</option>
<option value="20">5 dBm</option>
<option value="28">7 dBm</option>
<option value="34">8 dBm</option>
<option value="44">11 dBm</option>
<option value="52">13 dBm</option>
<option value="56">14 dBm</option>
<option value="60">15 dBm</option>
<option value="66">16 dBm</option>
<option value="72">18 dBm</option>
<option value="80">20 dBm</option>
</select>
</p>
<p style="display: flex; align-items: center; gap: 10px;">
<label for="remember_bssid" style="margin: 0;">
<span data-lang="remember_bssid">连接 Wi-Fi 时记住 BSSID</span>
</label>
<input type="checkbox" id="remember_bssid" name="remember_bssid">
</p>
<p style="display: flex; align-items: center; gap: 10px;">
<label for="sleep_mode" style="margin: 0;">
<span data-lang="sleep_mode">启用睡眠模式</span>
</label>
<input type="checkbox" id="sleep_mode" name="sleep_mode">
</p>
<p style="text-align: center;">
<input type="submit" value="保存" id="advanced_button" data-lang-value="save">
</p>
</div>
</form>
</div>
<script type="text/javascript">
const button = document.getElementById('button');
const error = document.getElementById('error');
const ssid = document.getElementById('ssid');
// Add language translations
const translations = {
'zh-CN': {
title: '网络配置',
saved_wifi: '已保存的 Wi-Fi',
new_wifi: '新的 Wi-Fi',
password: '密码:',
connect: '连接',
select_wifi: '从下面列表选择 2.4G Wi-Fi:',
wifi_tab: 'Wi-Fi 配置',
advanced_tab: '高级选项',
ota_url: '自定义 OTA 地址:',
max_tx_power: 'Wi-Fi 最大发射功率:',
remember_bssid: '连接 Wi-Fi 时记住 BSSID',
sleep_mode: '启用睡眠模式',
save: '保存'
},
'zh-TW': {
title: '網路設定',
saved_wifi: '已儲存的 Wi-Fi',
new_wifi: '新的 Wi-Fi',
password: '密碼:',
connect: '連接',
select_wifi: '從下方列表選擇 2.4G Wi-Fi:',
wifi_tab: 'Wi-Fi 設定',
advanced_tab: '進階選項',
ota_url: '自定義 OTA 位址:',
max_tx_power: 'Wi-Fi 最大發射功率:',
remember_bssid: '連接 Wi-Fi 時記住 BSSID',
sleep_mode: '啟用睡眠模式',
save: '儲存'
},
'en-US': {
title: 'Network Configuration',
saved_wifi: 'Saved Wi-Fi',
new_wifi: 'New Wi-Fi',
password: 'Password:',
connect: 'Connect',
select_wifi: 'Select an 2.4G Wi-Fi from the list below:',
wifi_tab: 'Wi-Fi Config',
advanced_tab: 'Advanced',
ota_url: 'Custom OTA URL:',
max_tx_power: 'Wi-Fi Max TX Power:',
remember_bssid: 'Remember BSSID when connecting to Wi-Fi',
sleep_mode: 'Enable Sleep Mode',
save: 'Save'
},
'ja-JP': {
title: 'ネットワーク設定',
saved_wifi: '保存済みのWi-Fi',
new_wifi: '新しいWi-Fi',
password: 'パスワード:',
connect: '接続',
select_wifi: '以下のリストから2.4G Wi-Fiを選択してください:',
wifi_tab: 'Wi-Fi設定',
advanced_tab: '詳細設定',
ota_url: '自定義 OTA 位址:',
max_tx_power: 'Wi-Fi最大送信電力:',
remember_bssid: 'Wi-Fi接続時にBSSIDを記憶する',
sleep_mode: 'スリープモードを有効にする',
save: '保存'
}
};
function changeLanguage() {
const lang = document.getElementById('language').value;
// 检查语言值是否合法
if (!translations[lang]) {
console.warn(`不支持的语言: ${lang},默认使用中文`);
document.getElementById('language').value = 'zh-CN';
return changeLanguage();
}
// Set page title
document.title = translations[lang].title;
document.querySelectorAll('[data-lang]').forEach(element => {
const key = element.getAttribute('data-lang');
element.textContent = translations[lang][key];
});
document.querySelectorAll('[data-lang-value]').forEach(element => {
const key = element.getAttribute('data-lang-value');
element.value = translations[lang][key];
});
// Update AP list text
const apList = document.getElementById('ap_list');
if (apList.firstChild) {
apList.firstChild.textContent = translations[lang].select_wifi;
}
// Save language preference
localStorage.setItem('preferred_language', lang);
}
function renderSavedList(data) {
const savedListContainer = document.getElementById('saved_list_container');
const savedList = document.getElementById('saved_list');
savedList.innerHTML = '';
data.forEach((ssid, index) => {
const li = document.createElement('li');
let html = `<span>${ssid}</span>`;
// Only add priority and delete buttons after the first item
if (index > 0) {
html += ` <span>
<button type="button" onclick="setDefaultItem(this, ${index})">⬆️</button>
<button type="button" onclick="deleteItem(this, ${index})">❌</button>
</span>`;
} else {
html += ` <span><button type="button" onclick="deleteItem(this, ${index})">❌</button></span>`;
}
li.innerHTML = html;
savedList.appendChild(li);
});
if (data.length > 0) {
savedListContainer.style.display = 'block';
} else {
savedListContainer.style.display = 'none';
}
}
// Delete one item from the list
function deleteItem(item, index) {
// disable the button
item.disabled = true;
// /saved/delete?index=INDEX
fetch('/saved/delete?index=' + index)
.then(response => response.json())
.then(data => {
loadSavedList();
});
}
function setDefaultItem(item, index) {
item.disabled = true;
fetch('/saved/set_default?index=' + index)
.then(response => response.json())
.then(data => {
loadSavedList();
});
}
// Load saved ssid and password list
function loadSavedList() {
fetch('/saved/list')
.then(response => response.json())
.then(data => {
renderSavedList(data);
});
}
// Load AP list from /scan
function loadAPList() {
if (button.disabled) {
return;
}
fetch('/scan')
.then(response => response.json())
.then(data => {
const lang = document.getElementById('language').value;
const apList = document.getElementById('ap_list');
apList.innerHTML = '<p>' + translations[lang].select_wifi + '</p>';
data.forEach(ap => {
// Create a link for each AP
const link = document.createElement('a');
link.href = '#';
link.textContent = ap.ssid + ' (' + ap.rssi + ' dBm)';
if (ap.authmode === 0) {
link.textContent += ' 🌐';
} else {
link.textContent += ' 🔒';
}
link.addEventListener('click', () => {
ssid.value = ap.ssid;
});
apList.appendChild(link);
});
setTimeout(loadAPList, 5000);
})
.catch(error => {
console.error('Error:', error);
});
}
// Set initial language
document.addEventListener('DOMContentLoaded', () => {
// 从 URL 参数中获取语言设置
const urlParams = new URLSearchParams(window.location.search);
const langParam = urlParams.get('lang');
// 获取浏览器语言
const browserLang = navigator.language || navigator.userLanguage;
// 语言映射表,将浏览器语言代码映射到支持的语言代码
const languageMap = {
'zh': 'zh-CN', // 简体中文
'zh-CN': 'zh-CN', // 简体中文
'zh-TW': 'zh-TW', // 繁体中文
'zh-HK': 'zh-TW', // 繁体中文(香港)
'ja': 'ja-JP', // 日语
'ja-JP': 'ja-JP', // 日语
'en': 'en-US', // 英语
'en-US': 'en-US', // 英语
'en-GB': 'en-US', // 英语(英国)
'en-CA': 'en-US', // 英语(加拿大)
'en-AU': 'en-US' // 英语(澳大利亚)
};
// 获取支持的语言代码
const getSupportedLanguage = (lang) => {
// 首先尝试完全匹配
if (languageMap[lang]) {
return languageMap[lang];
}
// 然后尝试只匹配主语言代码(例如 'zh' 而不是 'zh-CN'
const mainLang = lang.split('-')[0];
if (languageMap[mainLang]) {
return languageMap[mainLang];
}
// 如果都不匹配,返回英语
return 'en-US';
};
// 优先使用 URL 参数的语言设置,其次是本地存储的设置,最后是浏览器语言设置
const savedLang = langParam ||
localStorage.getItem('preferred_language') ||
getSupportedLanguage(browserLang);
document.getElementById('language').value = savedLang;
changeLanguage();
loadSavedList();
loadAPList();
loadAdvancedConfig();
});
// 监听 pageshow 事件以处理浏览器返回键
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
loadSavedList();
} else {
// 正常加载时已处理
}
});
async function submitForm(event) {
event.preventDefault();
button.disabled = true;
error.textContent = '';
const ssidValue = ssid.value;
const passwordValue = document.getElementById('password').value;
const payload = {
ssid: ssidValue,
password: passwordValue
};
try {
const response = await fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || '连接失败');
}
// 连接成功,跳转到完成页面
button.disabled = false;
window.location.href = '/done.html';
} catch (err) {
error.textContent = err.message;
button.disabled = false;
}
}
function switchTab(tabName) {
// 隐藏所有标签页内容
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 移除所有标签页的active类
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示选中的标签页内容
document.getElementById(tabName + '-tab').classList.add('active');
// 激活选中的标签
document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`).classList.add('active');
}
async function submitAdvancedForm(event) {
event.preventDefault();
const advancedButton = document.getElementById('advanced_button');
const advancedError = document.getElementById('advanced_error');
advancedButton.disabled = true;
advancedError.textContent = '';
const config = {
ota_url: document.getElementById('ota_url').value,
max_tx_power: parseInt(document.getElementById('max_tx_power').value),
remember_bssid: document.getElementById('remember_bssid').checked,
sleep_mode: document.getElementById('sleep_mode').checked
};
try {
const response = await fetch('/advanced/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || '保存失败');
}
advancedButton.disabled = false;
showToast('配置已保存');
} catch (err) {
advancedError.textContent = err.message;
advancedButton.disabled = false;
}
}
// 显示提示框
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 加载高级配置
async function loadAdvancedConfig() {
try {
const response = await fetch('/advanced/config');
const data = await response.json();
if (data.ota_url) {
document.getElementById('ota_url').value = data.ota_url;
}
if (data.max_tx_power) {
document.getElementById('max_tx_power').value = data.max_tx_power;
}
if (data.remember_bssid !== undefined) {
document.getElementById('remember_bssid').checked = data.remember_bssid;
}
if (data.sleep_mode !== undefined) {
document.getElementById('sleep_mode').checked = data.sleep_mode;
}
} catch (error) {
console.error('Error loading advanced config:', error);
}
}
// 清空OTA地址
function clearOtaUrl() {
document.getElementById('ota_url').value = '';
}
</script>
</body>
</html>