160 lines
5.9 KiB
TypeScript
160 lines
5.9 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import {
|
||
analyzeMatchConditions,
|
||
type MatchConditionRequestPayload,
|
||
type MatchConditionResponse,
|
||
} from '@/lib/analytics-api';
|
||
import { MatchConditionsCard } from '@/components/MatchConditionsCard';
|
||
|
||
export default function MatchConditionsPage() {
|
||
const [matchId, setMatchId] = useState('MEX-2026-GRP-D01');
|
||
const [avgYellow, setAvgYellow] = useState(5.2);
|
||
const [penaltiesPerGame, setPenaltiesPerGame] = useState(0.35);
|
||
const [cardsLine, setCardsLine] = useState(4.5);
|
||
const [temperature, setTemperature] = useState(34);
|
||
const [humidity, setHumidity] = useState(71);
|
||
const [altitude, setAltitude] = useState(2240);
|
||
const [homeAttack, setHomeAttack] = useState(1.85);
|
||
const [awayAttack] = useState(1.72);
|
||
const [loading, setLoading] = useState(false);
|
||
const [errorMessage, setErrorMessage] = useState('');
|
||
const [result, setResult] = useState<MatchConditionResponse | null>(null);
|
||
|
||
async function runConditionAnalysis() {
|
||
setLoading(true);
|
||
setErrorMessage('');
|
||
try {
|
||
const payload: MatchConditionRequestPayload = {
|
||
match_id: matchId,
|
||
avg_yellow_cards: avgYellow,
|
||
penalties_per_game: penaltiesPerGame,
|
||
cards_ou_line: cardsLine,
|
||
temp_c: temperature,
|
||
humidity_pct: humidity,
|
||
venue_altitude_meters: altitude,
|
||
home_second_half_attack: homeAttack,
|
||
away_second_half_attack: awayAttack,
|
||
};
|
||
const data = await analyzeMatchConditions(payload);
|
||
setResult(data);
|
||
} catch (error) {
|
||
setErrorMessage(error instanceof Error ? error.message : '比賽條件分析暫時中斷');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<h2 className="dot-matrix text-2xl text-[#7d2a15]">裁判與天候條件量化</h2>
|
||
<section className="panel-glow rounded-2xl p-4">
|
||
<h3 className="dot-matrix text-lg text-[#7d2a15]">條件輸入</h3>
|
||
<div className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||
<label className="text-sm text-[#7a5b46]">
|
||
場次 ID
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
value={matchId}
|
||
onChange={(event) => setMatchId(event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
每場平均黃牌
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
step={0.1}
|
||
value={avgYellow}
|
||
onChange={(event) => setAvgYellow(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
每場平均點球
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
step={0.05}
|
||
value={penaltiesPerGame}
|
||
onChange={(event) => setPenaltiesPerGame(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
Cards O/U 盤口
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
step={0.1}
|
||
value={cardsLine}
|
||
onChange={(event) => setCardsLine(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
溫度(攝氏)
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
value={temperature}
|
||
onChange={(event) => setTemperature(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
濕度(%)
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
value={humidity}
|
||
onChange={(event) => setHumidity(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
海拔(m)
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
value={altitude}
|
||
onChange={(event) => setAltitude(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
<label className="text-sm text-[#7a5b46]">
|
||
主隊下半場攻擊
|
||
<input
|
||
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
|
||
type="number"
|
||
step={0.01}
|
||
value={homeAttack}
|
||
onChange={(event) => setHomeAttack(Number(event.target.value))}
|
||
/>
|
||
</label>
|
||
</div>
|
||
<div className="mt-4 flex gap-3">
|
||
<button
|
||
className="rounded-lg bg-[#7d2a15] px-4 py-2 text-white"
|
||
type="button"
|
||
onClick={runConditionAnalysis}
|
||
disabled={loading}
|
||
>
|
||
{loading ? '分析中…' : '執行環境條件分析'}
|
||
</button>
|
||
</div>
|
||
{errorMessage ? <p className="mt-3 text-xs text-[#8c2f2f]">{errorMessage}</p> : null}
|
||
</section>
|
||
|
||
{result ? (
|
||
<MatchConditionsCard
|
||
matchId={result.match_id}
|
||
strictnessIndex={result.strictness_index}
|
||
heatIndex={result.heat_index}
|
||
cardsPressureAlert={result.cards_pressure_alert}
|
||
secondHalfHomeAttack={result.second_half_home_attack}
|
||
secondHalfAwayAttack={result.second_half_away_attack}
|
||
secondHalfUnderRecommendation={result.second_half_under_recommendation}
|
||
attackerDirection={result.attacker_direction}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|