chip-game/Assets/Scripts/Server/HtmlBuilder.cs
_sega a8b26773e8 FEAT: Рилтайм обновление настроек на сайте
1. Настройки на сайте обновляются без кнопки отправить
2. Добавлена кнопка для начисления монет через сайт(пока заглушка)
2026-01-28 13:05:48 +03:00

311 lines
9.2 KiB
C#
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.

using System.Collections.Generic;
using System.Globalization;
using System.Text;
public class HtmlBuilder
{
public static string GetSettingsPage(List<Setting> settings)
{
return Decorate(GetSettingsString(settings), title: "Настройки Фишки");
}
public static string GetLoginPage()
{
string content = @"
<div class='login-root'>
<h1>Вход</h1>
<div class='form-group'>
<label for='pass'>Пароль</label>
<input id='pass' type='password' autocomplete='current-password' />
</div>
<button id='loginBtn' type='button'>Войти</button>
<p id='loginStatus' style='margin-top:12px;'></p>
</div>
<script>
(function(){
const pass = document.getElementById('pass');
const btn = document.getElementById('loginBtn');
const st = document.getElementById('loginStatus');
async function doLogin(){
st.textContent = '';
try{
const r = await fetch('/api/login', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password: pass.value || '' })
});
const t = await r.text();
if(!r.ok){
st.textContent = 'Ошибка входа';
try{
const j = JSON.parse(t);
if(j && j.message) st.textContent = j.message;
}catch(e){}
return;
}
window.location.href = '/';
}catch(e){
st.textContent = 'Сервер недоступен';
}
}
btn.addEventListener('click', doLogin);
pass.addEventListener('keydown', (e)=>{ if(e.key === 'Enter') doLogin(); });
})();
</script>
";
return Decorate(content, title: "Вход");
}
public static string GetSettingsString(List<Setting> settings)
{
var sb = new StringBuilder();
sb.AppendLine("<h1>Настройки фишки</h1>");
// блок монет
sb.AppendLine(@"
<div class='panel'>
<h2>Монеты</h2>
<div class='coins-row'>
<input id='coinsAmount' type='number' value='10' min='1' step='1' class='coins-input'/>
<button id='addCoinsBtn' type='button' class='coins-btn'>Добавить монеты</button>
</div>
<div id='coinsStatus' class='status'></div>
</div>
");
sb.AppendLine("<div class='panel'>");
sb.AppendLine("<h2>Параметры</h2>");
foreach (Setting setting in settings)
{
string view = setting.Type switch
{
SettingType.Bool => GetBoolView(setting),
SettingType.Float => GetFloatView(setting),
SettingType.String => GetStringView(setting),
SettingType.Int => GetIntView(setting),
_ => $"<p>{Escape(setting.Alias)}: (unknown)</p>"
};
sb.AppendLine(view);
}
sb.AppendLine("</div>");
// JS для realtime отправки
sb.AppendLine(@"
<script>
(function(){
const status = (txt)=>{
const el = document.getElementById('globalStatus');
if(!el) return;
el.textContent = txt || '';
if(txt){ setTimeout(()=>{ el.textContent=''; }, 1200); }
};
const debounceMap = new Map();
function debounce(key, fn, ms){
if(debounceMap.has(key)) clearTimeout(debounceMap.get(key));
debounceMap.set(key, setTimeout(fn, ms));
}
async function sendSetting(id, value){
try{
const r = await fetch('/api/setting', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id, value })
});
if(!r.ok){
status('Ошибка применения');
return;
}
status('Сохранено');
}catch(e){
status('Сервер недоступен');
}
}
// Все элементы настроек
document.querySelectorAll('[data-setting-id]').forEach(el=>{
const id = el.getAttribute('data-setting-id');
if(el.type === 'checkbox'){
el.addEventListener('change', ()=>{
sendSetting(id, el.checked ? 'on' : 'off');
});
return;
}
if(el.type === 'range'){
const valueSpanId = el.getAttribute('data-value-span');
const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null;
const apply = ()=>{
if(valueSpan) valueSpan.textContent = el.value;
// range шлём с небольшой задержкой, чтобы не заспамить
debounce('rng_'+id, ()=>sendSetting(id, el.value), 120);
};
el.addEventListener('input', apply);
el.addEventListener('change', apply);
return;
}
if(el.type === 'radio'){
el.addEventListener('change', ()=>{
if(el.checked) sendSetting(id, el.value);
});
return;
}
// number/text (int)
el.addEventListener('change', ()=>{
sendSetting(id, el.value);
});
});
// Монеты
const coinsAmount = document.getElementById('coinsAmount');
const addBtn = document.getElementById('addCoinsBtn');
const coinsStatus = document.getElementById('coinsStatus');
async function addCoins(){
coinsStatus.textContent = '';
const v = parseInt(coinsAmount.value || '0', 10);
if(!Number.isFinite(v) || v <= 0){
coinsStatus.textContent = 'Введи число > 0';
return;
}
try{
const r = await fetch('/api/coins', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ amount: v })
});
if(!r.ok){
coinsStatus.textContent = 'Ошибка запроса';
return;
}
coinsStatus.textContent = 'Ок';
setTimeout(()=>{ coinsStatus.textContent=''; }, 1200);
}catch(e){
coinsStatus.textContent = 'Сервер недоступен';
}
}
addBtn.addEventListener('click', addCoins);
})();
</script>
<div id='globalStatus' class='status'></div>
");
return sb.ToString();
}
private static string GetBoolView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
string isChecked = setting.BoolValue ? "checked" : "";
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}</label>
<input type='checkbox' id='{id}' data-setting-id='{EscapeAttr(name)}' {isChecked}>
</div>";
}
private static string GetFloatView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
string spanId = id + "_VAL";
string min = setting.FloatMin.ToString(CultureInfo.InvariantCulture);
string max = setting.FloatMax.ToString(CultureInfo.InvariantCulture);
string value = setting.FloatValue.ToString(CultureInfo.InvariantCulture);
string step = setting.FloatStep.ToString(CultureInfo.InvariantCulture);
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}: <span id='{spanId}'>{value}</span></label>
<input id='{id}' type='range' min='{min}' max='{max}' value='{value}' step='{step}'
data-setting-id='{EscapeAttr(name)}' data-value-span='{spanId}'>
</div>";
}
private static string GetStringView(Setting setting)
{
var sb = new StringBuilder();
sb.AppendLine("<div class='form-group'>");
sb.AppendLine($"<p>{Escape(setting.Alias)}</p>");
for (int i = 0; i < setting.AllowedValues.Count; i++)
{
string stringValue = setting.AllowedValues[i];
string name = setting.Id;
string id = name + "ID" + i;
bool isChecked = setting.StringValue == stringValue;
string Checked = isChecked ? "checked" : "";
sb.AppendLine("<div class='radio-row'>");
sb.AppendLine($"<label for='{EscapeAttr(id)}'>{Escape(stringValue)}</label>");
sb.AppendLine(
$"<input type='radio' id='{EscapeAttr(id)}' " +
$"name='{EscapeAttr(name)}' value='{EscapeAttr(stringValue)}' {Checked} data-setting-id='{EscapeAttr(name)}'>"
);
sb.AppendLine("</div>");
}
sb.AppendLine("</div>");
return sb.ToString();
}
private static string GetIntView(Setting setting)
{
string name = setting.Id;
string id = name + "ID";
return $@"
<div class='form-group'>
<label for='{id}'>{Escape(setting.Alias)}</label>
<input id='{id}' type='number' value='{setting.IntValue}' step='1'
data-setting-id='{EscapeAttr(name)}'>
</div>";
}
private static string Decorate(string content, string title)
{
string res = "<!DOCTYPE html>"
+ "<html lang='ru'>"
+ "<head>"
+ "<meta charset='utf-8'>"
+ "<meta name='viewport' content='width=device-width, initial-scale=1'>"
+ "<link rel='stylesheet' href='style.css'>"
+ $"<title>{Escape(title)}</title>"
+ "</head>"
+ "<body>";
res += content;
res += "</body></html>";
return res;
}
// минимальная защита от мусора в html
private static string Escape(string s)
{
if (string.IsNullOrEmpty(s)) return "";
return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
}
private static string EscapeAttr(string s) => Escape(s).Replace("'", "&#39;");
}