-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
512 lines (471 loc) · 20.4 KB
/
index.html
File metadata and controls
512 lines (471 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>野球スコア 完全版</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body{ -webkit-font-smoothing:antialiased; }
.hidden{ display:none; }
.btn{ cursor:pointer; padding:.5rem 1rem; border-radius:.375rem; color:white; margin:.2rem; position:relative; z-index:1; }
.btn-blue{ background:#3b82f6; } .btn-green{ background:#10b981; } .btn-red{ background:#ef4444; } .btn-yellow{ background:#facc15; color:#000; }
table{ border-collapse:collapse; width:100%; }
th,td{ border:1px solid #ddd; padding:6px; text-align:center; font-size:14px; }
.small { font-size:12px; color:#666; }
.panel{ background:#fff; padding:12px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.06); }
.current-inning{ background:#fde68a; }
@media (max-width:720px){
.grid-2 { display:block; }
}
</style>
</head>
<body class="bg-slate-50 p-4">
<div class="max-w-5xl mx-auto">
<header class="flex justify-between items-center mb-4">
<div>
<h1 class="text-2xl font-bold">⚾ 野球スコア(完全版)</h1>
<div class="small">右上に日時・試合作成・個人成績(ここ簡潔)</div>
</div>
<div class="text-right">
<div id="todayDate" class="small"></div>
<div id="todayGames" class="small mt-2">今日の試合: なし</div>
</div>
</header>
<!-- HOME -->
<section id="home" class="panel mb-4">
<div class="flex justify-between items-start">
<div>
<div class="mb-2">
<label class="small">ホーム: <input id="homeTeam" class="border rounded px-2 py-1" value="Home"></label>
<label class="small ml-3">ビジター: <input id="awayTeam" class="border rounded px-2 py-1" value="Away"></label>
</div>
<div class="small">直近の試合(クリックで開く)</div>
<div id="recentGames" class="mt-2"></div>
</div>
<div class="flex flex-col items-end">
<div class="flex gap-2 mb-2">
<button id="btnCreate" class="btn btn-blue">➕ 試合作成</button>
<button id="btnStats" class="btn btn-green">📊 個人成績</button>
</div>
<div class="small">(作成は右のボタン。作成時に球場名・イニング数を入力します)</div>
</div>
</div>
</section>
<!-- CREATE -->
<section id="createPanel" class="panel hidden mb-4">
<h2 class="font-medium mb-2">試合作成</h2>
<div class="grid grid-2 gap-2">
<div>
<label class="small">ホームチーム: <input id="create_homeTeam" class="border rounded px-2 py-1" value="Home"></label>
</div>
<div>
<label class="small">ビジターチーム: <input id="create_awayTeam" class="border rounded px-2 py-1" value="Away"></label>
</div>
<div>
<label class="small">イニング数: <input id="create_innings" type="number" min="1" max="18" value="9" class="border rounded px-2 py-1 w-20"></label>
</div>
<div>
<label class="small">球場名: <input id="create_stadium" class="border rounded px-2 py-1" placeholder="球場名"></label>
</div>
</div>
<div class="mt-3">
<button id="btnCreateOk" class="btn btn-blue">完了(作成してスコア画面へ)</button>
<button id="btnCreateCancel" class="btn btn-red">キャンセル</button>
</div>
</section>
<!-- GAME -->
<section id="gamePanel" class="panel hidden mb-4">
<div class="flex justify-between items-start mb-2">
<div>
<h2 id="gameTitle" class="text-xl font-semibold"></h2>
<div id="gameMeta" class="small"></div>
</div>
<div class="text-right">
<button id="btnBackHome" class="small text-slate-600">← ホームに戻る</button>
</div>
</div>
<!-- Scoreboard -->
<div id="scoreboardPanel" class="mb-3"></div>
<!-- Input area -->
<div class="mb-3">
<label class="small">攻撃チーム:
<select id="input_team" class="border rounded px-2 py-1">
<option value="away">Away</option>
<option value="home">Home</option>
</select>
</label>
<label class="small ml-3">打者: <input id="input_batter" class="border rounded px-2 py-1" placeholder="打者名"></label>
</div>
<div id="resultButtons" class="flex flex-wrap gap-2 mb-3">
<button data-result="OUT" class="btn btn-red">アウト</button>
<button data-result="1B" class="btn btn-yellow">単打</button>
<button data-result="2B" class="btn btn-green">二塁打</button>
<button data-result="3B" class="btn btn-blue">三塁打</button>
<button data-result="HR" class="btn btn-red">本塁打</button>
<button data-result="BB" class="btn btn-green">四球</button>
<button data-result="K" class="btn btn-red">三振</button>
<button id="btnUndo" class="btn btn-yellow">取り消し</button>
</div>
<div id="eventsList" class="border rounded p-2 bg-slate-50 max-h-60 overflow-auto"></div>
</section>
<!-- STATS -->
<section id="statsPanel" class="panel hidden mb-4">
<div class="flex justify-between items-center mb-2">
<h2 class="font-medium">個人成績</h2>
<button id="btnBackFromStats" class="small text-slate-600">← ホームに戻る</button>
</div>
<div class="flex gap-2 mb-3">
<button id="tab_team" class="btn btn-blue">チーム内</button>
<button id="tab_all" class="btn btn-green">全体</button>
<button id="tab_month" class="btn btn-yellow">月間</button>
</div>
<div id="statsInfo" class="small mb-2">※「チーム内」は現在開いている試合のチーム内成績です。ホーム画面や試合を選んでからご利用ください。</div>
<div id="statsTable" class="overflow-auto"></div>
</section>
<footer class="small text-slate-600 mt-6">※データはブラウザ(localStorage)に保存されます。</footer>
</div>
<script>
/* -------------------------
Complete single-file app
------------------------- */
(function(){
const STORAGE_KEY = 'bb_score_v3';
let state = { games: {}, order: [] }; // games[id] = {...}
let currentGameId = null;
let undoStack = [];
// DOM refs
const todayDateEl = document.getElementById('todayDate');
const btnCreate = document.getElementById('btnCreate');
const btnStats = document.getElementById('btnStats');
const recentGamesEl = document.getElementById('recentGames');
const todayGamesEl = document.getElementById('todayGames');
const createPanel = document.getElementById('createPanel');
const create_homeTeam = document.getElementById('create_homeTeam');
const create_awayTeam = document.getElementById('create_awayTeam');
const create_innings = document.getElementById('create_innings');
const create_stadium = document.getElementById('create_stadium');
const btnCreateOk = document.getElementById('btnCreateOk');
const btnCreateCancel = document.getElementById('btnCreateCancel');
const gamePanel = document.getElementById('gamePanel');
const gameTitle = document.getElementById('gameTitle');
const gameMeta = document.getElementById('gameMeta');
const scoreboardPanel = document.getElementById('scoreboardPanel');
const input_team = document.getElementById('input_team');
const input_batter = document.getElementById('input_batter');
const resultButtons = document.getElementById('resultButtons');
const eventsList = document.getElementById('eventsList');
const btnBackHome = document.getElementById('btnBackHome');
const btnUndo = document.getElementById('btnUndo');
const statsPanel = document.getElementById('statsPanel');
const btnBackFromStats = document.getElementById('btnBackFromStats');
const tab_team = document.getElementById('tab_team');
const tab_all = document.getElementById('tab_all');
const tab_month = document.getElementById('tab_month');
const statsTable = document.getElementById('statsTable');
const statsInfo = document.getElementById('statsInfo');
// clock
function refreshClock(){
todayDateEl.textContent = new Date().toLocaleString();
}
setInterval(refreshClock,1000); refreshClock();
// storage
function loadState(){
try{
const s = localStorage.getItem(STORAGE_KEY);
if(s) state = JSON.parse(s);
}catch(e){ console.error('loadState', e); }
}
function saveState(){
try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }catch(e){ console.error('saveState', e); }
}
// helpers
function shortId(){ return Math.random().toString(36).slice(2,9); }
function sum(arr){ return arr.reduce((a,b)=>a+b,0); }
function nowStr(){ return new Date().toLocaleString(); }
function monthKey(date){ // YYYY-MM
const d = new Date(date);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
}
// init
loadState();
renderRecentGames();
// UI: show/hide helpers
function showHome(){
createPanel.classList.add('hidden');
gamePanel.classList.add('hidden');
statsPanel.classList.add('hidden');
document.getElementById('home').classList.remove('hidden');
renderRecentGames();
}
function showCreate(){
document.getElementById('home').classList.add('hidden');
createPanel.classList.remove('hidden');
gamePanel.classList.add('hidden');
statsPanel.classList.add('hidden');
// preload team names from header if user edited
create_homeTeam.value = create_homeTeam.value || 'Home';
create_awayTeam.value = create_awayTeam.value || 'Away';
}
function showGame(){
document.getElementById('home').classList.add('hidden');
createPanel.classList.add('hidden');
statsPanel.classList.add('hidden');
gamePanel.classList.remove('hidden');
}
function showStats(){
document.getElementById('home').classList.add('hidden');
createPanel.classList.add('hidden');
gamePanel.classList.add('hidden');
statsPanel.classList.remove('hidden');
}
// recent games list
function renderRecentGames(){
recentGamesEl.innerHTML = '';
todayGamesEl.textContent = state.order.length ? `今日の試合: ${state.order.length}件` : '今日の試合: なし';
state.order.forEach(id=>{
const g = state.games[id];
if(!g) return;
const btn = document.createElement('button');
btn.className = 'btn btn-blue m-1';
btn.textContent = `${g.away} vs ${g.home} (${g.createdAt})`;
btn.addEventListener('click', ()=> openGame(id) );
recentGamesEl.appendChild(btn);
});
}
// create flow
btnCreate.addEventListener('click', ()=> { showCreate(); });
btnCreateCancel.addEventListener('click', ()=> { showHome(); });
btnCreateOk.addEventListener('click', ()=>{
const homeName = (create_homeTeam.value || 'Home').trim();
const awayName = (create_awayTeam.value || 'Away').trim();
const innings = Math.max(1, Math.min(18, parseInt(create_innings.value) || 9));
const stadium = (create_stadium.value || '').trim();
const id = shortId();
const createdAt = nowStr();
state.games[id] = {
id, home: homeName, away: awayName, innings, stadium, createdAt,
homeInnings: Array(innings).fill(0),
awayInnings: Array(innings).fill(0),
events: [], outs:0, currentInning:1, top:true
};
state.order.unshift(id);
saveState();
renderRecentGames();
openGame(id);
});
// open game
function openGame(id){
if(!state.games[id]) return alert('試合が見つかりません');
currentGameId = id;
undoStack = [];
renderGameScreen();
showGame();
bindResultDelegation(); // ensure events work
}
// render scoreboard (table) and meta
function renderGameScreen(){
const g = state.games[currentGameId];
if(!g) return;
gameTitle.textContent = `${g.away} vs ${g.home}`;
gameMeta.textContent = `球場: ${g.stadium || '未設定'} | 作成: ${g.createdAt} | 現在: ${g.currentInning}回 ${g.top ? '表' : '裏'} (outs:${g.outs||0})`;
// build table
let html = '<table><thead><tr><th>チーム</th>';
for(let i=1;i<=g.innings;i++) html += `<th>${i}</th>`;
html += '<th>R</th></tr></thead><tbody>';
html += `<tr><td>${g.away}</td>`;
for(let i=0;i<g.innings;i++) html += `<td ${ (i+1)===g.currentInning && g.top ? 'class="current-inning"' : '' }>${g.awayInnings[i]}</td>`;
html += `<td>${sum(g.awayInnings)}</td></tr>`;
html += `<tr><td>${g.home}</td>`;
for(let i=0;i<g.innings;i++) html += `<td ${ (i+1)===g.currentInning && !g.top ? 'class="current-inning"' : '' }>${g.homeInnings[i]}</td>`;
html += `<td>${sum(g.homeInnings)}</td></tr>`;
html += '</tbody></table>';
scoreboardPanel.innerHTML = html;
renderEvents();
}
// events list
function renderEvents(){
const g = state.games[currentGameId];
eventsList.innerHTML = '';
if(!g || !g.events.length){ eventsList.innerHTML = '<div class="small">イベントなし</div>'; return; }
// show chronological
g.events.slice().reverse().forEach(ev=>{
const d = document.createElement('div');
d.className = 'small py-1 border-b';
d.textContent = `[${ev.time}] ${ev.team === 'home' ? g.home : g.away } - ${ev.batter}: ${ev.result}${ev.runs ? ' (得点:' + ev.runs + ')' : ''}`;
eventsList.appendChild(d);
});
}
// result handling: delegation
function bindResultDelegation(){
// remove previous to avoid double-binding
resultButtons.onclick = null;
resultButtons.addEventListener('click', onResultClick);
// undo button
btnUndo.onclick = ()=>{ undoLast(); };
}
function onResultClick(e){
const btn = e.target.closest('button');
if(!btn) return;
const res = btn.dataset.result;
if(!res) return;
handleResult(res);
}
function handleResult(result){
if(!currentGameId) return alert('先に試合を開いてください');
const g = state.games[currentGameId];
const batter = (input_batter.value || '名無し').trim();
const team = input_team.value; // 'home' or 'away'
// basic runs calculation:
// HR -> 1 run (simple), others -> 0 unless we expand.
let runs = 0;
if(result === 'HR') runs = 1; // simple auto
// push event
const ev = { id: shortId(), team, batter, result, runs, time: nowStr(), inning: g.currentInning, top: g.top };
g.events.push(ev);
undoStack.push({ gameId: currentGameId, eventId: ev.id });
// update inning runs
if(runs){
if(team === 'home') g.homeInnings[g.currentInning - 1] += runs;
else g.awayInnings[g.currentInning - 1] += runs;
}
// outs management
if(result === 'OUT' || result === 'K'){
g.outs = (g.outs || 0) + 1;
if(g.outs >= 3){
g.outs = 0;
// switch top/bottom
if(g.top){
g.top = false;
} else {
g.top = true;
g.currentInning++;
}
}
}
// after each event, clear batter input -> next batter
input_batter.value = '';
saveState();
renderGameScreen();
// update stats display proactively if stats panel visible
if(!statsPanel.classList.contains('hidden')) renderStats(currentStatsMode);
}
// undo
function undoLast(){
const last = undoStack.pop();
if(!last) return alert('取り消し対象がありません');
const g = state.games[last.gameId];
if(!g) return;
const idx = g.events.findIndex(e=>e.id===last.eventId);
if(idx === -1) return alert('イベントが見つかりません');
const ev = g.events.splice(idx,1)[0];
// revert runs simplistic (only HR handled)
if(ev.runs){
if(ev.team === 'home') g.homeInnings[ev.inning -1] = Math.max(0, g.homeInnings[ev.inning -1] - ev.runs);
else g.awayInnings[ev.inning -1] = Math.max(0, g.awayInnings[ev.inning -1] - ev.runs);
}
// revert outs logic (best-effort): if event was OUT or K, decrement outs and potentially rewind top/bottom and inning
if(ev.result === 'OUT' || ev.result === 'K'){
// If current outs == 0, we probably switched the side; best-effort: set outs to 2 and reverse side if needed
if((g.outs || 0) === 0){
// reverse last side change
if(g.top){
// we are at top now, so previous must have been bottom -> move to previous inning bottom
if(g.currentInning > 1){ g.currentInning = Math.max(1, g.currentInning - 1); g.top = false; g.outs = 2; }
else { g.outs = 0; }
} else {
// we are at bottom now, previous was top
g.top = true; g.outs = 2;
}
} else {
g.outs = Math.max(0, g.outs - 1);
}
}
saveState();
renderGameScreen();
if(!statsPanel.classList.contains('hidden')) renderStats(currentStatsMode);
}
// back buttons
btnBackHome.addEventListener('click', ()=>{ showHome(); });
btnBackFromStats.addEventListener('click', ()=>{ showHome(); });
// stats
let currentStatsMode = 'team'; // 'team'|'all'|'month'
tab_team.addEventListener('click', ()=>{ currentStatsMode='team'; renderStats('team'); });
tab_all.addEventListener('click', ()=>{ currentStatsMode='all'; renderStats('all'); });
tab_month.addEventListener('click', ()=>{ currentStatsMode='month'; renderStats('month'); });
function renderStats(mode){
currentStatsMode = mode || currentStatsMode;
// aggregate across games
const playerStats = {}; // map name -> {AB,H,HR,BB,K}
const monthNow = new Date();
const mKeyNow = `${monthNow.getFullYear()}-${String(monthNow.getMonth()+1).padStart(2,'0')}`;
const gamesToScan = Object.values(state.games);
gamesToScan.forEach(game=>{
game.events.forEach(ev=>{
// filter by mode
if(currentStatsMode === 'team'){
if(currentGameId == null){ /* no game open */ return; }
if(game.id !== currentGameId) return;
} else if(currentStatsMode === 'month'){
// event time compare month
if(monthKey(ev.time) !== mKeyNow) return;
}
const name = ev.batter || '名無し';
if(!playerStats[name]) playerStats[name] = { AB:0, H:0, HR:0, BB:0, K:0 };
// count AB/BB/K/H/HR
if(ev.result === 'BB'){
playerStats[name].BB++;
} else {
// treat everything except BB as AB (simplified)
playerStats[name].AB++;
if(ev.result === '1B' || ev.result === '2B' || ev.result === '3B' || ev.result === 'HR'){
playerStats[name].H++;
}
if(ev.result === 'HR') playerStats[name].HR++;
if(ev.result === 'K') playerStats[name].K++;
}
});
});
// build table
let html = '<table><thead><tr><th>選手</th><th>打数</th><th>安打</th><th>本塁打</th><th>四球</th><th>三振</th><th>打率</th></tr></thead><tbody>';
const rows = Object.entries(playerStats).map(([name,s])=>{
const avg = s.AB > 0 ? (s.H / s.AB).toFixed(3) : '0.000';
return { name, ...s, avg };
});
// sort by AB desc then H desc
rows.sort((a,b)=> (b.AB - a.AB) || (b.H - a.H) || a.name.localeCompare(b.name) );
rows.forEach(r=>{
html += `<tr><td>${r.name}</td><td>${r.AB}</td><td>${r.H}</td><td>${r.HR}</td><td>${r.BB}</td><td>${r.K}</td><td>${r.avg}</td></tr>`;
});
if(rows.length === 0) html += '<tr><td colspan="7" class="small">データがありません</td></tr>';
html += '</tbody></table>';
statsTable.innerHTML = html;
// extra info
if(currentStatsMode === 'team'){
statsInfo.textContent = 'チーム内成績:現在開いている試合の全選手の成績を表示しています。';
if(!currentGameId) statsInfo.textContent = 'チーム内成績を表示するには、ホーム画面で試合を開いてください。';
} else if(currentStatsMode === 'all'){
statsInfo.textContent = '全体成績:全試合の記録から集計しています。';
} else {
statsInfo.textContent = '月間成績:今月のイベントから集計しています。';
}
}
// Utility: monthKey for events stored in nowStr format -> convert reliably
function monthKey(timeStr){
const d = new Date(timeStr);
if(isNaN(d)) return '';
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
}
// Make sure stats button opens with fresh data
btnStats.addEventListener('click', ()=>{
showStats();
renderStats(currentStatsMode);
});
// initial UI: show home
showHome();
// expose for debugging (optional)
window._bb_state = state;
})(); // end IIFE
</script>
</body>
</html>