refactor: deriva punt/set/servHome dalla striscia, estrai mixin WebSocket
La striscia è ora l'unica source of truth: punt, set vinti e servizio vengono calcolati via helper puri (punteggio/servizio/setVinti) invece di essere mantenuti come campi ridondanti nello stato. Il boilerplate WebSocket (~150 righe identiche in entrambi i componenti) è estratto in src/wsMixin.js con createWsMixin(role); i componenti diventano solo logica specifica del loro ruolo.
This commit is contained in:
@@ -10,15 +10,15 @@
|
||||
<div class="score-preview">
|
||||
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
||||
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.home }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.home }}</div>
|
||||
<img v-show="state.sp.servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
|
||||
<div class="team-pts">{{ punt.home }}</div>
|
||||
<div class="team-set">SET {{ set.home }}</div>
|
||||
<img v-show="servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
|
||||
</div>
|
||||
<div class="team-score guest-bg" @click="sendAction({ type: 'incPunt', team: 'guest' })">
|
||||
<div class="team-name">{{ state.sp.nomi.guest }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.guest }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.guest }}</div>
|
||||
<img v-show="!state.sp.servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
|
||||
<div class="team-pts">{{ punt.guest }}</div>
|
||||
<div class="team-set">SET {{ set.guest }}</div>
|
||||
<img v-show="!servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -190,16 +190,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createWsMixin } from '../wsMixin.js'
|
||||
|
||||
export default {
|
||||
name: "ControllerPage",
|
||||
mixins: [createWsMixin('controller')],
|
||||
data() {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
isConnecting: false,
|
||||
reconnectTimeout: null,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectDelay: 30000,
|
||||
confirmReset: false,
|
||||
showSetVinto: false,
|
||||
setVintoTeam: null,
|
||||
@@ -207,44 +204,23 @@ export default {
|
||||
showCambiTeam: false,
|
||||
showCambi: false,
|
||||
cambiTeam: "home",
|
||||
cambiData: [
|
||||
{ in: "", out: "" },
|
||||
{ in: "", out: "" },
|
||||
],
|
||||
cambiData: [{ in: "", out: "" }, { in: "", out: "" }],
|
||||
cambiError: "",
|
||||
configData: {
|
||||
nomeHome: "",
|
||||
nomeGuest: "",
|
||||
modalita: "3/5",
|
||||
nomeHome: "", nomeGuest: "", modalita: "3/5",
|
||||
formHome: ["1", "2", "3", "4", "5", "6"],
|
||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
state: {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: [{ serv: 'h', ris: '' }],
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPunteggioZeroZero() {
|
||||
return this.state.sp.punt.home === 0 && this.state.sp.punt.guest === 0
|
||||
return this.state.sp.striscia.at(-1).ris === ''
|
||||
},
|
||||
squadraVincente() {
|
||||
const { home, guest } = this.state.sp.punt
|
||||
const totSet = this.state.sp.set.home + this.state.sp.set.guest
|
||||
const { home, guest } = this.punt
|
||||
const sv = this.set
|
||||
const totSet = sv.home + sv.guest
|
||||
const isSetDecisivo = this.state.modalitaPartita === '2/3' ? totSet >= 2 : totSet >= 4
|
||||
const soglia = isSetDecisivo ? 15 : 25
|
||||
if (home >= soglia && home - guest >= 2) return 'home'
|
||||
@@ -254,20 +230,18 @@ export default {
|
||||
isPartitaFinita() {
|
||||
if (!this.setVintoTeam) return false
|
||||
const setsToWin = this.state.modalitaPartita === '2/3' ? 2 : 3
|
||||
return this.state.sp.set[this.setVintoTeam] + 1 >= setsToWin
|
||||
return this.set[this.setVintoTeam] + 1 >= setsToWin
|
||||
},
|
||||
cambiValid() {
|
||||
let hasComplete = false
|
||||
let allValid = true
|
||||
this.cambiData.forEach((c) => {
|
||||
const cin = (c.in || "").trim()
|
||||
const cout = (c.out || "").trim()
|
||||
if (!cin && !cout) return
|
||||
if (!cin || !cout) { allValid = false; return }
|
||||
for (const c of this.cambiData) {
|
||||
const cin = (c.in || "").trim(), cout = (c.out || "").trim()
|
||||
if (!cin && !cout) continue
|
||||
if (!cin || !cout) return false
|
||||
hasComplete = true
|
||||
})
|
||||
return allValid && hasComplete
|
||||
}
|
||||
}
|
||||
return hasComplete
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
squadraVincente(val) {
|
||||
@@ -277,231 +251,47 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
|
||||
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||
// Annulla eventuali tentativi di riconnessione pianificati.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Pulisce il timeout di riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
|
||||
// Chiude il WebSocket con il codice di chiusura appropriato.
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
|
||||
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Component unmounting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Controller] Error closing WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
// Evita connessioni simultanee multiple.
|
||||
if (this.isConnecting) {
|
||||
console.log('[Controller] Already connecting, skipping...')
|
||||
return
|
||||
}
|
||||
|
||||
// Chiude la connessione precedente, se presente.
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Reconnecting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Controller] Error closing previous WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
|
||||
// Permette di specificare un host WebSocket alternativo via query parameter
|
||||
// Utile per scenari WSL2 o development remoto: ?wsHost=192.168.1.100:3001
|
||||
const params = new URLSearchParams(location.search)
|
||||
const defaultHost = (
|
||||
location.hostname === 'localhost' || location.hostname === '::1'
|
||||
)
|
||||
? `127.0.0.1${location.port ? `:${location.port}` : ''}`
|
||||
: location.host
|
||||
const wsHost = params.get('wsHost') || defaultHost
|
||||
const wsUrl = `${protocol}//${wsHost}/ws`
|
||||
|
||||
console.log('[Controller] Connecting to WebSocket:', wsUrl)
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to create WebSocket:', err)
|
||||
this.isConnecting = false
|
||||
this.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = true
|
||||
this.reconnectAttempts = 0
|
||||
console.log('[Controller] Connected to server')
|
||||
|
||||
// Invia la registrazione solo se la connessione e realmente aperta.
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to register:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
} else if (msg.type === 'error') {
|
||||
console.error('[Controller] Server error:', msg.message)
|
||||
// Fornisce feedback di errore all'utente.
|
||||
this.showErrorFeedback(msg.message)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Controller] Parse error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
console.log('[Controller] Disconnected from server', event.code, event.reason)
|
||||
|
||||
// Non riconnette durante HMR (codice 1001, "going away")
|
||||
// ne in caso di chiusura pulita (codice 1000).
|
||||
if (event.code === 1000 || event.code === 1001) {
|
||||
console.log('[Controller] Clean close, not reconnecting')
|
||||
return
|
||||
}
|
||||
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('[Controller] WebSocket error:', err)
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
}
|
||||
onWsMessage(msg) {
|
||||
if (msg.type === 'error') this.showErrorFeedback(msg.message)
|
||||
},
|
||||
|
||||
scheduleReconnect() {
|
||||
// Evita pianificazioni multiple della riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
||||
const delay = Math.min(
|
||||
1000 * Math.pow(2, this.reconnectAttempts),
|
||||
this.maxReconnectDelay
|
||||
)
|
||||
this.reconnectAttempts++
|
||||
|
||||
console.log(`[Controller] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.reconnectTimeout = null
|
||||
this.connectWebSocket()
|
||||
}, delay)
|
||||
},
|
||||
|
||||
sendAction(action) {
|
||||
if (!this.wsConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[Controller] Cannot send action: not connected')
|
||||
this.showErrorFeedback('Non connesso al server')
|
||||
return
|
||||
}
|
||||
|
||||
// Valida l'azione prima dell'invio.
|
||||
if (!action || !action.type) {
|
||||
console.error('[Controller] Invalid action format:', action)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to send action:', err)
|
||||
this.showErrorFeedback('Errore invio comando')
|
||||
}
|
||||
if (!action?.type) return
|
||||
if (!this.sendWs({ type: 'action', action })) this.showErrorFeedback('Non connesso al server')
|
||||
},
|
||||
|
||||
showErrorFeedback(message) {
|
||||
// Feedback visivo degli errori: attualmente solo log su console.
|
||||
// In futuro puo essere esteso con notifiche a comparsa (toast).
|
||||
console.error('[Controller] Error:', message)
|
||||
},
|
||||
|
||||
doReset() {
|
||||
this.sendAction({ type: 'resetta' })
|
||||
this.confirmReset = false
|
||||
},
|
||||
|
||||
undoUltimoPoint() {
|
||||
this.sendAction({ type: 'decPunt' })
|
||||
this.showSetVinto = false
|
||||
},
|
||||
|
||||
doNuovoSet() {
|
||||
this.sendAction({ type: 'nuovoSet', team: this.setVintoTeam })
|
||||
this.showSetVinto = false
|
||||
this.configData.nomeHome = this.state.sp.nomi.home
|
||||
this.configData.nomeGuest = this.state.sp.nomi.guest
|
||||
this.configData.modalita = this.state.modalitaPartita
|
||||
this.configData.formHome = ["1", "2", "3", "4", "5", "6"]
|
||||
this.configData.formGuest = ["1", "2", "3", "4", "5", "6"]
|
||||
this.configData = {
|
||||
nomeHome: this.state.sp.nomi.home,
|
||||
nomeGuest: this.state.sp.nomi.guest,
|
||||
modalita: this.state.modalitaPartita,
|
||||
formHome: ["1", "2", "3", "4", "5", "6"],
|
||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
this.showConfig = true
|
||||
},
|
||||
|
||||
openConfig() {
|
||||
this.configData.nomeHome = this.state.sp.nomi.home
|
||||
this.configData.nomeGuest = this.state.sp.nomi.guest
|
||||
this.configData.modalita = this.state.modalitaPartita
|
||||
this.configData.formHome = [...this.state.sp.form.home]
|
||||
this.configData.formGuest = [...this.state.sp.form.guest]
|
||||
this.configData = {
|
||||
nomeHome: this.state.sp.nomi.home,
|
||||
nomeGuest: this.state.sp.nomi.guest,
|
||||
modalita: this.state.modalitaPartita,
|
||||
formHome: [...this.state.sp.form.home],
|
||||
formGuest: [...this.state.sp.form.guest],
|
||||
}
|
||||
this.showConfig = true
|
||||
},
|
||||
|
||||
saveConfig() {
|
||||
this.sendAction({ type: 'setNomi', home: this.configData.nomeHome, guest: this.configData.nomeGuest })
|
||||
this.sendAction({ type: 'setModalita', modalita: this.configData.modalita })
|
||||
@@ -509,11 +299,7 @@ export default {
|
||||
this.sendAction({ type: 'setFormazione', team: 'guest', form: this.configData.formGuest })
|
||||
this.showConfig = false
|
||||
},
|
||||
|
||||
openCambiTeam() {
|
||||
this.showCambiTeam = true
|
||||
},
|
||||
|
||||
openCambiTeam() { this.showCambiTeam = true },
|
||||
openCambi(team) {
|
||||
this.showCambiTeam = false
|
||||
this.cambiTeam = team
|
||||
@@ -521,76 +307,44 @@ export default {
|
||||
this.cambiError = ""
|
||||
this.showCambi = true
|
||||
},
|
||||
|
||||
closeCambi() {
|
||||
this.showCambi = false
|
||||
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
|
||||
this.cambiError = ""
|
||||
},
|
||||
|
||||
confermaCambi() {
|
||||
if (!this.cambiValid) return
|
||||
this.cambiError = ""
|
||||
|
||||
const cambi = this.cambiData
|
||||
.filter(c => (c.in || "").trim() && (c.out || "").trim())
|
||||
.map(c => ({ in: c.in.trim(), out: c.out.trim() }))
|
||||
|
||||
// Simula i cambi in sequenza per validare
|
||||
const formCorrente = this.state.sp.form[this.cambiTeam].map(v => String(v).trim())
|
||||
const formSimulata = [...formCorrente]
|
||||
|
||||
const formSimulata = this.state.sp.form[this.cambiTeam].map(v => String(v).trim())
|
||||
for (const cambio of cambi) {
|
||||
if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
|
||||
this.cambiError = "I numeri dei giocatori devono essere cifre"
|
||||
return
|
||||
this.cambiError = "I numeri dei giocatori devono essere cifre"; return
|
||||
}
|
||||
if (cambio.in === cambio.out) {
|
||||
this.cambiError = `Il giocatore ${cambio.in} non può sostituire sé stesso`
|
||||
return
|
||||
this.cambiError = `Il giocatore ${cambio.in} non può sostituire sé stesso`; return
|
||||
}
|
||||
if (formSimulata.includes(cambio.in)) {
|
||||
this.cambiError = `Il giocatore ${cambio.in} è già in formazione`
|
||||
return
|
||||
this.cambiError = `Il giocatore ${cambio.in} è già in formazione`; return
|
||||
}
|
||||
if (!formSimulata.includes(cambio.out)) {
|
||||
this.cambiError = `Il giocatore ${cambio.out} non è in formazione`
|
||||
return
|
||||
this.cambiError = `Il giocatore ${cambio.out} non è in formazione`; return
|
||||
}
|
||||
const idx = formSimulata.indexOf(cambio.out)
|
||||
formSimulata.splice(idx, 1, cambio.in)
|
||||
formSimulata.splice(formSimulata.indexOf(cambio.out), 1, cambio.in)
|
||||
}
|
||||
|
||||
this.sendAction({ type: 'confermaCambi', team: this.cambiTeam, cambi })
|
||||
this.closeCambi()
|
||||
},
|
||||
|
||||
speak() {
|
||||
let text = ''
|
||||
if (this.state.sp.punt.home + this.state.sp.punt.guest === 0) {
|
||||
text = "zero a zero"
|
||||
} else if (this.state.sp.punt.home === this.state.sp.punt.guest) {
|
||||
text = this.state.sp.punt.home + " pari"
|
||||
} else {
|
||||
if (this.state.sp.servHome) {
|
||||
text = this.state.sp.punt.home + " a " + this.state.sp.punt.guest
|
||||
} else {
|
||||
text = this.state.sp.punt.guest + " a " + this.state.sp.punt.home
|
||||
}
|
||||
}
|
||||
if (!this.wsConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.showErrorFeedback('Non connesso al server')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'speak', text }))
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to send speak command:', err)
|
||||
this.showErrorFeedback('Errore invio comando voce')
|
||||
}
|
||||
}
|
||||
}
|
||||
const { home, guest } = this.punt
|
||||
const text = home + guest === 0 ? "zero a zero"
|
||||
: home === guest ? `${home} pari`
|
||||
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
||||
this.sendWs({ type: 'speak', text })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user