tvvj.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Fursuit Pro V3 控制器</title>
<style>
:root {
--primary: #4a90e2;
--success: #2ecc71;
--danger: #e74c3c;
--dark: #34495e;
--bg: #f4f7f6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
margin: 0; padding: 15px; color: #333;
display: flex; flex-direction: column; align-items: center;
}
.container { width: 100%; max-width: 500px; }
.header {
background: white; padding: 20px; border-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05); margin-bottom: 20px; text-align: center;
}
h1 { margin: 0 0 10px 0; font-size: 1.4rem; color: var(--dark); }
#status { font-size: 0.9rem; color: #7f8c8d; margin-bottom: 15px; font-weight: bold;}
.card {
background: white; padding: 20px; border-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05); margin-bottom: 20px;
}
h2 { font-size: 1.1rem; border-left: 4px solid var(--primary); padding-left: 10px; margin: 0 0 15px 0; color: var(--dark); }
button {
width: 100%; padding: 14px; border: none; border-radius: 10px;
font-size: 1rem; font-weight: 600; cursor: pointer;
transition: transform 0.1s;
margin-bottom: 10px; color: white; background: var(--primary);
display: flex; align-items: center; justify-content: center; gap: 8px;
}
button:active { transform: scale(0.96); }
.btn-red { background: var(--danger); }
.btn-green { background: var(--success); }
.btn-outline { background: white; border: 2px solid var(--primary); color: var(--primary); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.slider-container { margin: 15px 0; }
input[type=range] { width: 100%; height: 8px; background: #ddd; border-radius: 5px; -webkit-appearance: none; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: var(--primary); border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.preset-row { display: flex; gap: 8px; margin-bottom: 10px; align-items: center; }
.preset-input-name { flex: 2; padding: 8px; border: 1px solid #ddd; border-radius: 6px; }
.preset-input-val { width: 55px; padding: 8px; border: 1px solid #ddd; border-radius: 6px; text-align: center; }
.preset-btn-save { width: auto; background: #95a5a6; padding: 8px 12px; margin: 0; }
.switch-row { display: flex; justify-content: space-between; align-items: center; }
.switch { position: relative; width: 50px; height: 28px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider-toggle { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px; transition: .4s; }
.slider-toggle:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; border-radius: 50%; transition: .4s; }
input:checked + .slider-toggle { background-color: var(--success); }
input:checked + .slider-toggle:before { transform: translateX(22px); }
.lang-btn { position: fixed; top: 15px; right: 15px; width: auto; padding: 5px 10px; background: white; border: 1px solid #ccc; font-size: 0.8rem; z-index: 100; }
</style>
</head>
<body>
<button class="lang-btn" onclick="toggleLang()">中/EN</button>
<div class="container">
<div class="header">
<h1 data-zh="🦊 Fursuit 智能控制 V3" data-en="🦊 Fursuit Control V3">🦊 Fursuit 智能控制 V3</h1>
<div id="status">🔴 未连接</div>
<button id="connectBtn" onclick="connectBLE()" style="background: var(--dark);">🔌 点击搜索设备</button>
</div>
<div id="panel" style="opacity: 0.4; pointer-events: none;">
<div class="card">
<h2 data-zh="👁️ 实时调节" data-en="👁️ Real-time">👁️ 实时调节</h2>
<div class="slider-container">
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<span data-zh="双眼开合度" data-en="Aperture">双眼开合度</span>
<span id="angVal">90°</span>
</div>
<input type="range" min="20" max="100" value="90"
oninput="updateVal('angVal', this.value)"
onchange="send('ANG', this.value)">
</div>
<div class="grid-3">
<button class="btn-outline" onclick="send('BLINK', 1)" data-zh="左快" data-en="L-F">左快</button>
<button onclick="send('BLINK', 0)" data-zh="双快" data-en="Both">双快</button>
<button class="btn-outline" onclick="send('BLINK', 2)" data-zh="右快" data-en="R-F">右快</button>
</div>
</div>
<div class="card">
<h2 data-zh="⏳ 慢动作眨眼" data-en="⏳ Slow Blink">⏳ 慢动作眨眼</h2>
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<span data-zh="眨眼耗时" data-en="Duration">眨眼耗时</span>
<span id="durVal" style="color:var(--primary); font-weight:bold;">1.0s</span>
</div>
<input type="range" min="500" max="5000" step="100" value="1000"
oninput="updateDurDisplay(this.value)"
onchange="send('DUR', this.value)">
<div class="grid-3" style="margin-top:15px">
<button class="btn-outline" onclick="send('SLOW', 1)" data-zh="左慢" data-en="L-S">左慢</button>
<button class="btn-outline" onclick="send('SLOW', 0)" data-zh="双慢" data-en="Both">双慢</button>
<button class="btn-outline" onclick="send('SLOW', 2)" data-zh="右慢" data-en="R-S">右慢</button>
</div>
</div>
<div class="card">
<h2 data-zh="🤖 预设与自动化" data-en="🤖 Presets">🤖 预设与自动化</h2>
<div class="switch-row" style="margin-bottom: 15px;">
<span data-zh="自动随机眨眼" data-en="Auto Blink">自动随机眨眼</span>
<label class="switch"><input type="checkbox" onchange="send('AUTO', this.checked?1:0)"><span class="slider-toggle"></span></label>
</div>
<div class="preset-row">
<input type="text" id="p1_name" class="preset-input-name" placeholder="预设1">
<input type="number" id="p1_val" class="preset-input-val" value="90">
<button class="preset-btn-save" onclick="savePreset(1)">💾</button>
<button style="width:auto; flex:1; margin:0;" onclick="loadPreset(1)">应用</button>
</div>
<div class="preset-row">
<input type="text" id="p2_name" class="preset-input-name" placeholder="预设2">
<input type="number" id="p2_val" class="preset-input-val" value="60">
<button class="preset-btn-save" onclick="savePreset(2)">💾</button>
<button style="width:auto; flex:1; margin:0;" onclick="loadPreset(2)">应用</button>
</div>
</div>
<div class="card">
<h2 data-zh="🔒 锁定控制" data-en="🔒 Locking">🔒 锁定控制</h2>
<div class="grid-2">
<button class="btn-red" onclick="send('CLOSE', 0)" data-zh="全闭锁定" data-en="Lock Close">全闭锁定</button>
<button class="btn-green" onclick="send('OPEN', 0)" data-zh="全开/释放" data-en="Release">全开/释放</button>
</div>
</div>
</div>
</div>
<script>
const SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
const CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
let bleChar;
let currentLang = 'zh';
window.onload = () => {
for(let i=1; i<=2; i++){
if(localStorage.getItem('v3_p'+i+'n')) document.getElementById('p'+i+'_name').value = localStorage.getItem('v3_p'+i+'n');
if(localStorage.getItem('v3_p'+i+'v')) document.getElementById('p'+i+'_val').value = localStorage.getItem('v3_p'+i+'v');
}
};
function updateVal(id, val) { document.getElementById(id).innerText = val + '°'; }
function updateDurDisplay(val) { document.getElementById('durVal').innerText = (val/1000).toFixed(1) + 's'; }
async function connectBLE() {
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'Fursuit-Pro-V3' }],
optionalServices: [SERVICE_UUID]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SERVICE_UUID);
bleChar = await service.getCharacteristic(CHAR_UUID);
document.getElementById('status').innerText = "✅ 已连接 V3";
document.getElementById('status').style.color = "#2ecc71";
document.getElementById('panel').style.opacity = "1";
document.getElementById('panel').style.pointerEvents = "auto";
document.getElementById('connectBtn').style.display = "none";
device.addEventListener('gattserverdisconnected', () => { location.reload(); });
} catch (e) { console.log(e); }
}
// 发送指令函数
function send(cmd, val) {
if (!bleChar) return;
const encoder = new TextEncoder();
const data = cmd + ":" + val;
bleChar.writeValue(encoder.encode(data)).catch(err => console.log(err));
}
function savePreset(id) {
const name = document.getElementById('p'+id+'_name').value;
const val = document.getElementById('p'+id+'_val').value;
localStorage.setItem('v3_p'+id+'n', name);
localStorage.setItem('v3_p'+id+'v', val);
alert("已保存");
}
function loadPreset(id) {
send('PRESET_LOAD', id);
const val = document.getElementById('p'+id+'_val').value;
document.getElementById('angVal').innerText = val + '°';
document.querySelector('input[type=range]').value = val;
}
function toggleLang() {
currentLang = currentLang === 'zh' ? 'en' : 'zh';
document.querySelectorAll('[data-zh]').forEach(el => {
el.innerText = el.getAttribute('data-' + currentLang);
});
}
</script>
</body>
</html>
Download
pasted.sh