Erstellen Sie eine benutzerdefinierte Telefon-App (NUI + React) für QBCore/ESX…
Ziel
Erstellen Sie mit NUI + React ein produktionsreifes In-Game-Smartphone für FiveM. Sie erstellen ein Ressourcen-Gerüst, verdrahten QBCore/ESX-Ereignisse, speichern Daten in MySQL und liefern eine reibungslose Benutzeroberfläche, die Leistungsbudgets berücksichtigt.
Voraussetzungen
- Ein laufender FiveM-Server mit txAdmin und MySQL (oxmysql oder mysql-async).
- Node.js 18+ und pnpm oder npm auf Ihrem Entwicklungs-PC.
- Ein Framework installiert: QBCore oder ESX.
- Empfohlene Bibliotheken: ox_lib (Rückrufe, Benachrichtigungen), ox_inventory (optional für Telefonartikel), ox_ziel (optional für Weltinteraktionen).
- Grundlegende React-Kenntnisse.
Dokumente
- Cfx.re NUI-Übersicht – https://docs.fivem.net/docs/scripting-manual/nui-development/
- NUI-Rückrufe – https://docs.fivem.net/docs/scripting-manual/nui-development/nui-callbacks/
- SendNUIMessage – https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/SendNUIMessage/
- Debuggen von NUI-Entwicklertools – https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/
- QBCore-Serverfunktionen – https://docs.qbcore.org/qbcore-documentation/qb-core/server-function-reference
- ESX
RegisterUsableItem– https://docs.esx-framework.org/en/esx_core/es_extended/server/functions - ox_lib-Rückrufe – https://overextended.dev/ox_lib
Interne Ablesung (FiveMX)
- Resmon & Leistung – https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/
- Leistungszentrum – https://fivemx.com/performance
- Marktübersicht Telefonskripte – https://fivemx.com/phone-scripts
Architektur
- Ressource
mein_Telefonmitfxmanifest.lua,Kunde,Server, UndBenutzeroberflächebündeln. - Benutzeroberfläche: React-App mit Vite integriert in
/ui/dist. NUI kommuniziert mit Lua überPostMessage+RegisterNUICallback. - Daten: MySQL-Tabellen für
Telefonkontakte,Telefonnachrichten,Telefonanrufe. - Gerüstkleber: QBCore oder Der verwendbare ESX-Elementhandler schaltet das Telefon um und Server-Rückrufe laden/speichern Daten.
Ereignisfluss
- Spieler drückt Taste oder nutzt das Telefonelement → 2)
SetNuiFocus(true, true)UndSendNUIMessage({ Aktion = 'öffnen' })→ 3) React zeigt UI → 4) UI fordert Daten an überfetch('https://my_phone/xyz')(NUI) → 5)RegisterNUICallback('xyz', ...)läuft auf Client/Server → 6) Server liest/schreibt DB → 7) Antwort kehrt zur Benutzeroberfläche zurück → 8) Telefon schließen und Fokus freigeben.
Schritt 1 – Gerüst der Ressource
Ordnerlayout
Ressourcen/ [lokal]/ mein_Telefon/ fxmanifest.lua Client/ main.lua Server/ main.lua ui/ index.html src/ main.tsx App.tsx api.ts styles.css
fxmanifest.lua
fx_version „cerulean“ Spiel „gta5“ ui_page „ui/dist/index.html“ Dateien { „ui/dist/**“ } Client-Skripte { „client/main.lua“ } Server-Skripte { „@oxmysql/lib/MySQL.lua“, „server/main.lua“ } lua54 „ja“
Schritt 2 – Erstellen Sie die React NUI
Initialisieren Sie eine Vite React-App im Inneren mein_Telefon/UI und bauen zu ui/dist.
cd my_phone/ui pnpm create vite@latest . --template react-ts pnpm i
Vite-Konfiguration (sicherstellen, dass Vermögenswerte landen in Entfernung)
// vite.config.ts importiere { defineConfig } von 'vite' importiere react von '@vitejs/plugin-react' exportiere default defineConfig({ plugins: [react()], build: { outDir: 'dist', emptyOutDir: true }, base: '' })
NUI-Brücke
// src/api.ts export async function nui (Ereignis: Zeichenkette, Daten?: unbekannt): Promise { const res = await fetch(`https://my_phone/${event}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data ?? {}) }) return await res.json() }
Mounten Sie React + Nachrichtenlistener
// src/main.tsx import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' const root = ReactDOM.createRoot(document.getElementById('root')!) root.render( window.addEventListener('message', (e) => { if (e.data?.action === 'open') document.body.classList.add('open') if (e.data?.action === 'close') document.body.classList.remove('open') })
Grundlegende Benutzeroberfläche
// src/App.tsx
import { useEffect, useState } from 'react'
import { nui } from './api'
type Contact = { id: number; name: string; number: string }
export default function App() {
const [contacts, setContacts] = useState<Contact[]>([])
const [visible, setVisible] = useState(false)
useEffect(() => {
const handler = (e: MessageEvent) => {
if (e.data?.action === 'open') {
setVisible(true)
nui<Contact[]>('contacts:list').then(setContacts)
}
if (e.data?.action === 'close') setVisible(false)
}
window.addEventListener('message', handler)
return () => window.removeEventListener('message', handler)
}, [])
if (!visible) return null
return (
<div className="phone">
<header>Phone</header>
<section>
{contacts.map(c => (
<div key={c.id} className="row">
<div>{c.name}</div>
<div>{c.number}</div>
</div>
))}
</section>
<footer>
<button onClick={() => nui('ui:close')}>Close</button>
</footer>
</div>
)
}
Hauptseite
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mein_Telefon</title>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Erstellen Sie die Benutzeroberfläche:
pnpm-Build
Schritt 3 – Client: Öffnen/Schließen, NUI-Fokus, Rückrufe
-- Client/main.lua lokal öffnen = false lokale Funktion openPhone() wenn geöffnet, dann returniere end open = true SetNuiFocus(true, true) SendNUIMessage({ Aktion = 'öffnen' }) end lokale Funktion closePhone() wenn nicht geöffnet, dann returniere end open = false SetNuiFocus(false, false) SendNUIMessage({ Aktion = 'schließen' }) end -- Tastenkombination (F1-Beispiel) RegisterCommand('meinTelefon', Funktion() wenn geöffnet, dann closePhone() sonst openPhone() end end) RegisterKeyMapping('meinTelefon', 'Telefon umschalten', 'Tastatur', 'F1') -- NUI → Spiel-Rückrufe RegisterNUICallback('ui:schließen', Funktion(_, cb) closePhone() cb({ ok = true }) end) -- Kontakte auflisten fragt den Server RegisterNUICallback('Kontakte:Liste', Funktion(_, cb) lib.callback('my_phone:server:getContacts', false, Funktion(Zeilen) cb(Zeilen) Ende) Ende)
Tipp: Aktivieren Sie NUI Devtools in der Spielkonsole mit
nui_devTools. Offenhttp://localhost:13172in Ihrem Chromium-Browser, um die Benutzeroberfläche zu überprüfen.
Schritt 4 – Server: DB-Schema + Rückrufe
SQL (MySQL)
TABELLE ERSTELLEN, WENN NICHT VORHANDEN: phone_contacts (id INT AUTO_INCREMENT PRIMARY KEY, citizenid VARCHAR(64) NOT NULL, name VARCHAR(64) NOT NULL, number VARCHAR(32) NOT NULL, INDEX(citizenid)); TABELLE ERSTELLEN, WENN NICHT VORHANDEN: phone_messages (id BIGINT AUTO_INCREMENT PRIMARY KEY, owner VARCHAR(64) NOT NULL, peer VARCHAR(64) NOT NULL, body TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX(owner), INDEX(peer));
Server mit oxmysql + ox_lib
-- server/main.lua local QBCore = exports['qb-core'] und exports['qb-core']:GetCoreObject() ESX = ESX oder nil wenn nicht QBCore dann TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) end -- Kontakte für den angemeldeten Charakter laden lib.callback.register('my_phone:server:getContacts', function(source) local citizenid wenn QBCore dann local Player = QBCore.Functions.GetPlayer(source) citizenid = Player und Player.PlayerData.citizenid sonst local xPlayer = ESX.GetPlayerFromId(source) citizenid = xPlayer und xPlayer.identifier end wenn nicht citizenid dann return {} end local rows = MySQL.query.await('SELECT id, name, number FROM phone_contacts WHERE citizenid = ?', { citizenid }) return rows oder {} end) -- Einen Kontakt speichern lib.callback.register('my_phone:server:addContact', Funktion (Quelle, Kontakt) wenn Typ (Kontakt) ~= 'Tabelle', dann return { ok = false } Ende lokaler Name, Nummer = Kontakt.Name, Kontakt.Nummer wenn nicht Name oder nicht Nummer, dann return { ok = false } Ende lokale Bürger-ID wenn QBCore dann lokaler Player = QBCore.Functions.GetPlayer(Quelle) Bürger-ID = Player und Player.PlayerData.Bürger-ID sonst lokaler xPlayer = ESX.GetPlayerFromId(Quelle) Bürger-ID = xPlayer und xPlayer.Kennung Ende wenn nicht Bürger-ID, dann return { ok = false } Ende MySQL.insert.await('INSERT INTO phone_contacts (Bürger-ID, Name, Nummer) VALUES (?, ?, ?)', { Bürger-ID, Name, Nummer }) return { ok = true } Ende)
Schritt 5 – Framework-Integration (Element + Berechtigungen)
QBCore
Fügen Sie ein verwendbares Telefonelement hinzu und schalten Sie die Benutzeroberfläche bei Verwendung um.
-- server/main.lua (nur QBCore) wenn QBCore dann QBCore.Functions.CreateUseableItem('phone', function(src, item) TriggerClientEvent('my_phone:client:toggle', src) end) end
-- Client/main.lua RegisterNetEvent('my_phone:client:toggle', function() if IsPauseMenuActive() then return end if IsPedInAnyVehicle(PlayerPedId(), false) then -- optionale Regel -- zeige eine Benachrichtigung über ox_lib lib.notify({ title = 'Phone', description = 'Kein Telefon während der Fahrt.', type = 'error' }) return end if IsNuiFocused() then ExecuteCommand('myphone') else ExecuteCommand('myphone') end end)
ESX
-- server/main.lua (nur ESX), wenn ESX und nicht QBCore, dann ESX.RegisterUsableItem('phone', function(playerId) TriggerClientEvent('my_phone:client:toggle', playerId) end) end
Wenn Sie ox_inventory, erstellen Sie das Element dort und verlassen Sie sich auf seine verwendbaren Handler. Sie können weiterhin dasselbe Client-Ereignis auslösen.
Schritt 6 – Kernfunktionen
Implementieren Sie kleine Abschnitte und liefern Sie diese schrittweise aus.
Kontakte
- UI-Aufrufe
Kontakte:Liste→ Server gibt Zeilen zurück. - Formular „Kontakt hinzufügen“ hinzufügen → Anruf
Kontakt hinzufügen. - Hinzufügen „Kontakt entfernen“ → Server löscht durch
Ausweismit Bürgereigentumsprüfung.
Nachrichten (SMS)
- Tisch
Telefonnachrichtenspeichert Eigentümer, Peer, Körper. - UI öffnet einen Chat, ruft an
Nachrichten:ListeUndNachrichten: senden. - Der Server fügt eine Nachricht ein und sendet optional ein Client-Ereignis an den Peer, wenn er online ist.
Serverskizze
lib.callback.register('my_phone:server:messages:list', function(source, peer)
local cid = GetCitizenId(source)
return MySQL.query.await('SELECT * FROM phone_messages WHERE owner=? AND peer=? ORDER BY id DESC LIMIT 200', { cid, peer }) or {}
end)
RegisterNetEvent('my_phone:server:messages:send', function(peer, body)
local src = source
local cid = GetCitizenId(src)
if type(body) ~= 'string' or #body == 0 or #body > 500 then return end
MySQL.insert.await('INSERT INTO phone_messages (owner, peer, body) VALUES (?, ?, ?)', { cid, peer, body })
TriggerClientEvent('my_phone:client:messages:push', src, peer, body)
-- optional: find target player by phone number and push live event
end)
Client erhält
RegisterNetEvent('mein_Telefon:Client:Nachrichten:Push', Funktion(Peer, Text) SendNUIMessage({ Aktion = 'Nachricht:Neu', Peer = Peer, Text = Text }) Ende)
Anrufe (optional MVP)
- Speichern Sie nur Anrufprotokolle. Echtes Audio verwendet Ihr Sprach-Plugin (pma-voice, mumble, SaltyChat) und liegt außerhalb dieses MVP.
- UI-Tastatur hinzufügen → Beim Wählen wird ein ausgehender Anruf protokolliert; beim Antworten wird ein eingehender Anruf protokolliert. Sie können später die API eines Sprach-Plugins integrieren.
Schritt 7 – Sicherheit, UX, Leistung
Sicherheit
- Vertrauen Sie niemals NUI-Eingaben. Überprüfen Sie Typen und Länge auf dem Server.
- Überprüfen Sie bei jeder Abfrage den Besitz mit
Bürger-IDoderKennung. - Vermeiden Sie die Offenlegung von Kennungen gegenüber anderen Clients. Verwenden Sie Server-Relays.
UX
- Deaktivieren Sie das Telefon, während Sie am Boden liegen, gefesselt sind oder fahren, wenn die Serverregeln dies erfordern.
- Sorgen Sie für eine schnelle Benutzeroberfläche. Verwenden Sie optimistische Updates und führen Sie eine Abstimmung bei Serverbestätigung durch.
Leistung
- NUI im Leerlauf halten. SetInterval-Schleifen in React vermeiden. Effekte und Ereignisse verwenden.
- Halten Sie die Pakete klein. Laden Sie schwere Bildschirme verzögert. Versenden Sie komprimierte Assets.
- Verwenden Sie Resmon, um im Durchschnitt unter 0,01–0,02 ms zu budgetieren. Siehe die oben verlinkte FiveMX-Anleitung.
Schritt 8 – Testen und Debuggen
- Ressource starten in
server.cfgvor abhängigen Skripten.
Stellen Sie sicher, dass mein Telefon
- Drücken Sie im Spiel F8 → Ausführen
nui_devTools→ öffnenhttp://localhost:13172und wählen Sie Ihre NUI-Seite aus. - Überprüfen Sie die Registerkarte Netzwerk. Jeder NUI → Lua-Aufruf trifft
https://my_phone/Endpunkte. - Verwenden
/meinTelefonBefehl und Bestätigung des Fokuswechsels. - Führen Sie Resmon aus und überprüfen Sie, ob die CPU-Leistung niedrig bleibt, während das Telefon geöffnet und geschlossen ist.
Schritt 9 – Verpackung und Updates
- Begehen
Benutzeroberfläche/Quelle undui/dist/bauen. - Führen Sie in CI Folgendes aus:
pnpm --filter ui buildund versenden nurEntfernungin Veröffentlichungen. - Versionieren Sie Ihre SQL-Migrationen. Verlieren Sie niemals Benutzerdaten ohne Backups.
Schritt 10 – Erweiterungen, die Sie als Nächstes hinzufügen können
- Bankwesen: Link zur Bankressource Ihres Servers; Kontostand und Überweisungen offenlegen.
- Tweets/Anzeigen: globaler Feed mit Ratenbegrenzungen und Moderation.
- Marktplatz: Inserate mit Treuhandkonto.
- Job-Apps: Polizei-/EMS-MDT-Haken.
- Fotos: Screenshot-Integration über Server-Endpunkt, nicht über Daten-URLs.
- Einstellungen: dynamische Themen, Klingeltöne, Hintergründe.
Fehlerbehebung
Telefon öffnet sich hinter Pausenmenü
Während der Pausenprüfungen deaktivieren und erneut öffnen, wenn die Aktivität zurückkehrt.
NUI-Rückruf wird nicht ausgelöst
- Sicherstellen
RegisterNUICallback('Ereignis', ...)Namen stimmen mit dem UI-Abrufpfad überein. - Bestätigen
fx_versionIsthimmelblauUndui_pageweist aufui/dist/index.html. - Überprüfen Sie die F8-Konsole auf CORS- oder JSON-Fehler.
Nicht verwendbare Artikel
- QBCore: bestätigen
QBCore.Functions.CreateUseableItemläuft undTelefonist in Ihrer Artikelliste vorhanden. - ESX: Bestätigen
ESX.RegisterUsableItem('Telefon', ...)Register nach Inventarladungen.
Datenbankfehler
- Stellen Sie sicher, dass Oxmysql vor dieser Ressource gestartet wurde.
- Überprüfen Sie Spaltengrößen und Kodierungen für Unicode-Namen.
Referenzausschnitte (Kopieren und Einfügen)
Helfer
Funktion GetCitizenId(Quelle) wenn QBCore dann lokal P = QBCore.Functions.GetPlayer(Quelle) gibt P und P.PlayerData.citizenid zurück, sonst lokal xP = ESX.GetPlayerFromId(Quelle) gibt xP und xP.identifier zurück Ende Ende
Kontakt über die Benutzeroberfläche hinzufügen
// UI async function addContact(name: string, number: string) { const res = await nui<{ ok: boolean }>('contacts:add', { name, number }) if (res.ok) { const next = await nui ('contacts:list') // Status aktualisieren } }
-- Client RegisterNUICallback('Kontakte:hinzufügen', Funktion(Daten, cb) lib.callback('mein_Telefon:Server:Kontakt hinzufügen', falsch, Funktion(resp) cb(resp) Ende, Daten) Ende)
Was du gebaut hast
- Ein fokussiertes Telefon-MVP mit Kontakten und Nachrichten.
- Eine saubere NUI-Brücke, die auf beiden Frameworks funktioniert.
- Eine DB-Schicht, die Sie sicher erweitern können.
Versenden Sie es, messen Sie die Leistung und wiederholen Sie die Schritte.
Weiterführende Literatur
- FiveMX Resmon & Optimierung: https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/
- Stimmenvergleich (wählen Sie Ihren Stapel): https://fivemx.com/fivem-voice-mumble-saltychat-pma-voice-guide
- Vorhandene Telefone zur Inspiration:
- lb‑phone v2: https://fivemx.com/lb-phone-v2
- Quasar Smartphone: https://fivemx.com/quasar-smartphone
- GCPhone: https://fivemx.com/gcphone
- Z-Phone: https://fivemx.com/z-phone
Meta
- Zielresmon: ≤0,02 ms durchschnittlich im Leerlauf.
- Bundle-Budget: ≤250 KB, gzippt für MVP.
- UI-FPS: 60.






