Files
website/html-webproject/MahjongWeb/control-panel.html
2026-01-18 20:49:14 +08:00

555 lines
24 KiB
HTML
Raw Permalink 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">
<head>
<meta charset="UTF-8">
<title>麻将直播控制面板</title>
<style>
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #228B22, #556B2F);
color: #fff;
padding: 20px;
margin: 0;
min-height: 100vh;
}
.container { max-width: 800px; margin: 0 auto; }
h1 { text-align: center; color: #FFD700; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
.card {
background: rgba(255,255,255,0.1);
border-radius: 15px;
padding: 20px;
margin: 15px 0;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.card h3 { color: #FFD700; margin-top: 0; border-bottom: 2px solid #FFD700; padding-bottom: 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
button {
background: linear-gradient(45deg, #FF6B35, #F7931E);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.3); }
input, select {
padding: 8px;
border-radius: 5px;
border: 1px solid #FFD700;
background: rgba(255,255,255,0.9);
margin: 5px;
width: 60px;
}
.hu-type { display: flex; gap: 10px; align-items: center; }
.hu-type input[type="radio"] { width: auto; }
label { font-size: 14px; }
.rotate-btns { display: flex; justify-content: space-around; }
#log { background: rgba(0,0,0,0.5); padding: 10px; border-radius: 5px; font-size: 12px; max-height: 100px; overflow-y: auto; }
.yaku-list { max-height: 200px; overflow-y: auto; }
.yaku-item { margin: 5px 0; }
.yaku-checkbox { margin-right: 10px; }
details { cursor: pointer; }
summary { font-weight: bold; color: #FFD700; }
#yaku-select { max-height: 150px; overflow-y: auto; border: 1px solid #FFD700; padding: 10px; border-radius: 5px; background: rgba(255,255,255,0.1); }
</style>
</head>
<body>
<div class="container">
<h1>🀄 麻将直播控制面板</h1>
<!-- 全局设置 -->
<div class="card">
<h3>🌟 全局设置</h3>
<div class="grid">
<label>初始分数: <input id="initial-score" type="number" value="25000"></label>
<button onclick="sendResetScores()">重置所有分数</button>
<br />
<!-- 新增:直播链接输入和按钮 -->
<label>直播链接: <input id="video-src" type="text" value="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4" style="width: 300px;"></label>
<button onclick="sendUpdateVideo()">更新视频</button>
<!-- 结束新增 -->
</div>
</div>
<!-- 局况 -->
<div class="card">
<h3>📅 局况设置</h3>
<div class="grid">
<label>当前局: <select id="round-select" onchange="sendUpdateRound()">
<option value="东1局">东1局</option>
<option value="东2局">东2局</option>
<option value="东3局">东3局</option>
<option value="东4局">东4局</option>
<option value="南1局">南1局</option>
<option value="南2局">南2局</option>
<option value="南3局">南3局</option>
<option value="南4局">南4局</option>
<option value="西1局">西1局</option>
<option value="西2局">西2局</option>
<option value="西3局">西3局</option>
<option value="西4局">西4局</option>
</select></label>
<label>宝牌: <input id="dora-input" type="text" value="?" onchange="sendUpdateDora()"></label>
<button onclick="advanceRound()">下一局 ➡️</button>
</div>
</div>
<!-- 玩家信息 -->
<div class="card">
<h3>👥 玩家信息 (东庄)</h3>
<div class="grid">
<div>
<label>东: <input id="name-east" value="玩家1" onchange="sendUpdateName('east')"></label>
<input id="score-east" type="number" value="25000" onchange="sendUpdateScore('east')">
</div>
<div>
<label>南: <input id="name-south" value="玩家2" onchange="sendUpdateName('south')"></label>
<input id="score-south" type="number" value="25000" onchange="sendUpdateScore('south')">
</div>
<div>
<label>西: <input id="name-west" value="玩家3" onchange="sendUpdateName('west')"></label>
<input id="score-west" type="number" value="25000" onchange="sendUpdateScore('west')">
</div>
<div>
<label>北: <input id="name-north" value="玩家4" onchange="sendUpdateName('north')"></label>
<input id="score-north" type="number" value="25000" onchange="sendUpdateScore('north')">
</div>
</div>
<div class="rotate-btns">
<button onclick="rotatePlayers('prev')">⬅️ 上一局</button>
<button onclick="rotatePlayers('next')">下一局 ➡️</button>
</div>
</div>
<!--
<div class="card">
<h3>🀄 番种参考 (日本麻将 37 种役)</h3>
<details>
<summary>点击展开所有番种</summary>
<div class="yaku-list">
<div class="yaku-item"><strong>1番役</strong>立直、一发、门前清自摸和、平和、役牌 (自风/场风/三元牌)、断幺九 (副露)、一气通贯 (副露)、对倒、混一色 (副露)、三色同顺 (副露)、七对子、赤宝牌</div>
<div class="yaku-item"><strong>2番役</strong>断幺九 (门清)、一气通贯 (门清)、混一色 (门清)、三色同顺 (门清)、对对和、混全带幺、三暗刻 (副露)、三暗刻 (门清,无显杠)</div>
<div class="yaku-item"><strong>3番役</strong>平胡 (门清)、一发 (门清? 配役)、混全带 (门清)、三暗刻 (门清,有显杠)</div>
<div class="yaku-item"><strong>役满:</strong>国士无双、大三元、小四喜、四暗刻单骑、大四喜、清老头、九莲宝灯、绿一色、清一色、四槓子、字一色、天和、地和</div>
</div>
</details>
<p><em>选役后自动累加番数(门清/副露已考虑减番逻辑简化,实际手动调整总番)。</em></p>
</div>番种参考 -->
<!-- 胡牌 -->
<div class="card">
<h3>🎉 触发胡牌</h3>
<div class="grid">
<div>
<label>赢家: <select id="winner-select">
<option value="east"></option><option value="south"></option><option value="west">西</option><option value="north"></option>
</select></label>
<label>类型: <div class="hu-type">
<label><input type="radio" name="hu-type" value="ron" checked> 荣和</label>
<label><input type="radio" name="hu-type" value="tsumo"> 自摸</label>
</div></label>
<label id="loser-label" style="display:block;">放铳: <select id="loser-select">
<option value="south"></option><option value="west">西</option><option value="north"></option><option value="east"></option>
</select></label>
<label>役牌: <div id="yaku-select">
<label class="yaku-checkbox"><input type="checkbox" value="场风东" data-han="1"></label><br />
<label class="yaku-checkbox"><input type="checkbox" value="双东" data-han="2">双东</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="场凤南" data-han="1"></label><br />
<label class="yaku-checkbox"><input type="checkbox" value="双南" data-han="2">双南</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="场风西" data-han="1">西</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="双西" data-han="2">双西</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="场风北" data-han="1"></label><br />
<label class="yaku-checkbox"><input type="checkbox" value="役牌白" data-han="1"></label><br />
<label class="yaku-checkbox"><input type="checkbox" value="役牌发" data-han="1"></label><br />
<label class="yaku-checkbox"><input type="checkbox" value="役牌中" data-han="1"></label><br />
</div></label>
<label>1番: <div id="yaku-select">
<label class="yaku-checkbox"><input type="checkbox" value="立直" data-han="1">立直</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="一发" data-han="1">一发</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="门前清自摸和" data-han="1">门前清自摸和</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="平和" data-han="1">平和</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="断幺九" data-han="1">断幺九</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="一盃口" data-han="1">一盃口</label><br />
</div></label>
<label>2番: <div id="yaku-select">
<label class="yaku-checkbox"><input type="checkbox" value="一气通贯" data-han="2">一气通贯</label><label class="yaku-checkbox"><input type="checkbox" value="一气通贯" data-han="1">非门清</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="混一色" data-han="3">混一色</label><label class="yaku-checkbox"><input type="checkbox" value="混一色" data-han="2">非门清</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="三色同顺" data-han="2">三色同顺</label><label class="yaku-checkbox"><input type="checkbox" value="三色同顺" data-han="1">非门清</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="三色同刻" data-han="2">三色同刻</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="七对子" data-han="2" data-fu="25">七对子</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="对对和" data-han="2">对对和</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="三暗刻" data-han="2">三暗刻</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="混全带幺九" data-han="2">混全</label><label class="yaku-checkbox"><input type="checkbox" value="混全带幺九" data-han="1">非门清</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="纯全带幺九" data-han="3">纯全</label><label class="yaku-checkbox"><input type="checkbox" value="纯全带幺九" data-han="2">非门清</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="清一色" data-han="6">清一色</label><br />
</div></label>
<label>役满: <div id="yaku-select">
<label class="yaku-checkbox"><input type="checkbox" value="国士无双" data-han="13">国士无双</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="大三元" data-han="13">大三元</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="小四喜" data-han="13">小四喜</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="大四喜" data-han="13">大四喜</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="四暗刻" data-han="13">四暗刻</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="绿一色" data-han="13">绿一色</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="天和" data-han="13">天和</label><br />
<label class="yaku-checkbox"><input type="checkbox" value="地和" data-han="13">地和</label><br />
</div></label>
<label>总番: <input id="fan-input" type="number" value="0" readonly></label>
<label>符: <input id="fu-input" type="number" value="30"></label>
<button onclick="sendHuPai()">执行胡牌</button>
</div>
</div>
</div>
<!-- 场况 -->
<div class="card">
<h3>📊 场况</h3>
<div class="grid">
<label>本场: <input id="honba-input" type="number" value="0"></label>
<label>立直棒: <input id="riichi-input" type="number" value="0"></label>
<button onclick="sendUpdateField()">更新</button>
</div>
</div>
<!-- 听牌 & 样式 -->
<div class="card">
<h3>🎯 听牌提示</h3>
<div class="grid">
<label>玩家: <select id="tenpai-player">
<option value="east"></option><option value="south"></option><option value="west">西</option><option value="north"></option>
</select></label>
<button onclick="sendTenpai(true)">置入听牌</button>
<button onclick="sendTenpai(false)">取消</button>
</div>
<h3>🎨 调整样式</h3>
<div class="grid">
<label>玩家: <select id="style-player">
<option value="east"></option><option value="south"></option><option value="west">西</option><option value="north"></option>
</select></label>
<label>颜色: <input id="color-input" type="color" value="#ff0000"></label>
<label>透明度: <input id="opacity-input" type="number" min="0" max="1" step="0.1" value="0.8"></label>
<button onclick="sendUpdateStyle()">更新</button>
</div>
</div>
<div id="log" class="card">日志: 准备就绪...</div>
</div>
<script>
const channel = 'mahjong-channel';
let bc = null;
let pos_names = {east: '玩家1', south: '玩家2', west: '玩家3', north: '玩家4'};
let pos_scores = {east: 25000, south: 25000, west: 25000, north: 25000};
const rounds = ['东1局', '东2局', '东3局', '东4局', '南1局', '南2局', '南3局', '南4局'];
let currentRoundIndex = 0;
try {
bc = new BroadcastChannel(channel);
log('BroadcastChannel 支持,通信就绪');
} catch (e) {
log('使用 localStorage 备用通信');
function postMessage(msg) { localStorage.setItem(channel, JSON.stringify(msg)); }
window.postMessage = postMessage;
}
function sendEvent(type, data) {
const msg = { type, data };
try {
if (bc) {
bc.postMessage(JSON.stringify(msg));
} else {
localStorage.setItem(channel, JSON.stringify(msg));
}
log(`发送: ${type} ${JSON.stringify(data)}`);
} catch (e) {
log(`发送失败: ${e.message}`);
}
}
function log(text) {
const logEl = document.getElementById('log');
logEl.innerHTML += `<br>${new Date().toLocaleTimeString()}: ${text}`;
logEl.scrollTop = logEl.scrollHeight;
}
function sendResetScores() {
try {
const initial = parseInt(document.getElementById('initial-score').value);
['east','south','west','north'].forEach(p => {
pos_scores[p] = initial;
document.getElementById(`score-${p}`).value = initial;
sendUpdateScore(p);
});
sendEvent('reset_scores', {initial});
log('重置分数到 ' + initial);
} catch (e) {
log('重置失败: ' + e.message);
}
}
// 新增:发送视频源更新事件
function sendUpdateVideo() {
try {
const src = document.getElementById('video-src').value.trim();
if (!src) {
log('错误:直播链接不能为空');
return;
}
sendEvent('update_video_src', {src});
log(`更新视频源: ${src}`);
} catch (e) {
log('视频源更新失败: ' + e.message);
}
}
// 结束新增
function sendUpdateName(player) {
const name = document.getElementById(`name-${player}`).value;
pos_names[player] = name;
sendEvent('update_name', {player, name});
log(`更新 ${player} 名字: ${name}`);
}
function sendUpdateScore(player) {
const score = parseInt(document.getElementById(`score-${player}`).value);
pos_scores[player] = score;
sendEvent('update_score', {player, score});
log(`更新 ${player} 分数: ${score}`);
}
function rotatePlayers(direction) {
try {
let old_e_name = pos_names.east, old_e_score = pos_scores.east;
let old_s_name = pos_names.south, old_s_score = pos_scores.south;
let old_w_name = pos_names.west, old_w_score = pos_scores.west;
let old_n_name = pos_names.north, old_n_score = pos_scores.north;
if (direction === 'next') {
pos_names.east = old_s_name; pos_scores.east = old_s_score;
pos_names.south = old_w_name; pos_scores.south = old_w_score;
pos_names.west = old_n_name; pos_scores.west = old_n_score;
pos_names.north = old_e_name; pos_scores.north = old_e_score;
} else {
pos_names.east = old_n_name; pos_scores.east = old_n_score;
pos_names.north = old_w_name; pos_scores.north = old_w_score;
pos_names.west = old_s_name; pos_scores.west = old_s_score;
pos_names.south = old_e_name; pos_scores.south = old_e_score;
}
['east','south','west','north'].forEach(p => {
document.getElementById(`name-${p}`).value = pos_names[p];
document.getElementById(`score-${p}`).value = pos_scores[p];
sendUpdateName(p);
sendUpdateScore(p);
});
log(`${direction === 'next' ? '下一局' : '上一局'} 轮换完成`);
} catch (e) {
log('轮换失败: ' + e.message);
}
}
function advanceRound() {
currentRoundIndex = (currentRoundIndex + 1) % rounds.length;
document.getElementById('round-select').value = rounds[currentRoundIndex];
sendUpdateRound();
// 如果进入南场
if (rounds[currentRoundIndex].startsWith('南')) {
sendUpdateField();
log('进入南场');
}
}
function sendUpdateRound() {
const round = document.getElementById('round-select').value;
sendEvent('update_round', {round});
log(`更新局况: ${round}`);
}
function sendUpdateDora() {
const dora = document.getElementById('dora-input').value || '?';
sendEvent('update_dora', {dora});
log(`更新宝牌: ${dora}`);
}
function calculateBasic(han, fu) {
let bp;
if (han >= 13) {
bp = 8000;
} else if (han >= 11) {
bp = 6000;
} else if (han >= 8) {
bp = 4000;
} else if (han >= 6) {
bp = 3000;
} else if (han >= 5) {
bp = 2000;
} else {
bp = fu * Math.pow(2, han + 2);
if (bp > 2000) bp = 2000;
}
return bp;
}
function calculatePayment(bp, mult) {
let pmt = bp * mult;
return Math.ceil(pmt / 100) * 100;
}
function sendHuPai() {
try {
const winner = document.getElementById('winner-select').value;
const type = document.querySelector('input[name="hu-type"]:checked').value;
const fu = parseInt(document.getElementById('fu-input').value) || 30;
let honba = parseInt(document.getElementById('honba-input').value) || 0;
let riichi_sticks = parseInt(document.getElementById('riichi-input').value) || 0;
const loser = type === 'ron' ? document.getElementById('loser-select').value : null;
const isDealer = winner === 'east';
if (type === 'ron' && winner === loser) {
log('错误:赢家和放铳者不能相同');
return;
}
// --- 1. 计算番种 ---
const selectedYaku = [];
let totalHan = 0;
document.querySelectorAll('#yaku-select input:checked').forEach(cb => {
selectedYaku.push(cb.value);
totalHan += parseInt(cb.dataset.han);
});
document.getElementById('fan-input').value = totalHan;
// --- 2. 按当前本场数计算分数 ---
const bp = calculateBasic(totalHan, fu);
const riichiExtra = riichi_sticks * 1000;
const honbaPer = honba * 100;
const honbaRon = honba * 300;
let totalGain = riichiExtra;
if (type === 'ron') {
const mult = isDealer ? 6 : 4;
const payment = calculatePayment(bp, mult);
const totalPay = payment + honbaRon;
pos_scores[loser] -= totalPay;
document.getElementById(`score-${loser}`).value = pos_scores[loser];
sendUpdateScore(loser);
totalGain += totalPay;
log(`放铳者 ${pos_names[loser]} 扣除 ${totalPay}`);
} else { // tsumo
if (isDealer) {
['south','west','north'].forEach(p => {
const pmt = calculatePayment(bp, 2);
const totalPmt = pmt + honbaPer;
pos_scores[p] -= totalPmt;
document.getElementById(`score-${p}`).value = pos_scores[p];
sendUpdateScore(p);
totalGain += totalPmt;
});
} else {
const dealerPmt = calculatePayment(bp, 2);
const totalDealerPmt = dealerPmt + honbaPer;
pos_scores.east -= totalDealerPmt;
document.getElementById('score-east').value = pos_scores.east;
sendUpdateScore('east');
totalGain += totalDealerPmt;
const others = ['south','west','north'].filter(p => p !== winner);
others.forEach(p => {
const pmt = calculatePayment(bp, 1);
const totalPmt = pmt + honbaPer;
pos_scores[p] -= totalPmt;
document.getElementById(`score-${p}`).value = pos_scores[p];
sendUpdateScore(p);
totalGain += totalPmt;
});
}
}
// --- 3. 赢家加分 ---
pos_scores[winner] += totalGain;
document.getElementById(`score-${winner}`).value = pos_scores[winner];
sendUpdateScore(winner);
// --- 4. 胡牌后才更新本场和立直棒 ---
if (isDealer) {
honba += 1; // 庄家连庄
riichi_sticks = 0;
} else {
honba = 0; // 非庄家胡,重置本场
riichi_sticks = 0;
}
document.getElementById('honba-input').value = honba;
document.getElementById('riichi-input').value = riichi_sticks;
sendUpdateField();
// --- 5. 输出事件日志 ---
sendEvent('hu_pai', {type, winner, loser, yaku: selectedYaku, fan: totalHan, fu, honba, riichi_sticks});
log(`胡牌执行: ${type} ${pos_names[winner]} ${selectedYaku.join(', ')} ${totalHan}${fu}符,总获 ${totalGain}${type === 'ron' ? `,放铳者 ${pos_names[loser]}` : ''}${isDealer ? ' (庄家连庄)' : ' (重置本场/立直棒)'}`);
} catch (e) {
log('胡牌执行失败: ' + e.message);
}
}
function sendUpdateField() {
try {
const honba = parseInt(document.getElementById('honba-input').value);
const riichi_sticks = parseInt(document.getElementById('riichi-input').value);
sendEvent('update_field', {honba, riichi_sticks});
log('场况更新');
} catch (e) {
log('场况更新失败: ' + e.message);
}
}
function sendTenpai(status) {
try {
const player = document.getElementById('tenpai-player').value;
sendEvent('tenpai', {player, status});
log(`${status ? '置入' : '取消'} ${player} 听牌`);
} catch (e) {
log('听牌更新失败: ' + e.message);
}
}
function sendUpdateStyle() {
try {
const player = document.getElementById('style-player').value;
const color = document.getElementById('color-input').value;
const opacity = parseFloat(document.getElementById('opacity-input').value);
sendEvent('update_style', {player, color, opacity});
log(`样式更新: ${player} ${color} ${opacity}`);
} catch (e) {
log('样式更新失败: ' + e.message);
}
}
// 事件监听
document.querySelectorAll('input[name="hu-type"]').forEach(r => {
r.addEventListener('change', (e) => {
document.getElementById('loser-label').style.display = e.target.value === 'ron' ? 'block' : 'none';
});
});
document.querySelectorAll('#yaku-select input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => {
let total = 0;
document.querySelectorAll('#yaku-select input:checked').forEach(checked => total += parseInt(checked.dataset.han));
document.getElementById('fan-input').value = total;
});
});
// 初始化当前局
document.getElementById('round-select').value = rounds[currentRoundIndex];
sendUpdateRound();
sendUpdateDora();
</script>
</body>
</html>