Sichern Sie sich heute 20%. Verwenden Sie beim Bezahlvorgang den Code WELCOME. WILLKOMMEN

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

  1. Ein laufender FiveM-Server mit txAdmin und MySQL (oxmysql oder mysql-async).
  2. Node.js 18+ und pnpm oder npm auf Ihrem Entwicklungs-PC.
  3. Ein Framework installiert: QBCore oder ESX.
  4. Empfohlene Bibliotheken: ox_lib (Rückrufe, Benachrichtigungen), ox_inventory (optional für Telefonartikel), ox_ziel (optional für Weltinteraktionen).
  5. Grundlegende React-Kenntnisse.

Dokumente

Interne Ablesung (FiveMX)


Architektur

  1. Ressource mein_Telefon mit fxmanifest.lua, Kunde, Server, Und Benutzeroberfläche bündeln.
  2. Benutzeroberfläche: React-App mit Vite integriert in /ui/dist. NUI kommuniziert mit Lua über PostMessage + RegisterNUICallback.
  3. Daten: MySQL-Tabellen für Telefonkontakte, Telefonnachrichten, Telefonanrufe.
  4. Gerüstkleber: QBCore oder Der verwendbare ESX-Elementhandler schaltet das Telefon um und Server-Rückrufe laden/speichern Daten.

Ereignisfluss

  1. Spieler drückt Taste oder nutzt das Telefonelement → 2) SetNuiFocus(true, true) Und SendNUIMessage({ Aktion = 'öffnen' }) → 3) React zeigt UI → 4) UI fordert Daten an über fetch('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. Offen http://localhost:13172 in 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

  1. UI-Aufrufe Kontakte:Liste → Server gibt Zeilen zurück.
  2. Formular „Kontakt hinzufügen“ hinzufügen → Anruf Kontakt hinzufügen.
  3. Hinzufügen „Kontakt entfernen“ → Server löscht durch Ausweis mit Bürgereigentumsprüfung.

Nachrichten (SMS)

  1. Tisch Telefonnachrichten speichert Eigentümer, Peer, Körper.
  2. UI öffnet einen Chat, ruft an Nachrichten:Liste Und Nachrichten: senden.
  3. 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

  1. Vertrauen Sie niemals NUI-Eingaben. Überprüfen Sie Typen und Länge auf dem Server.
  2. Überprüfen Sie bei jeder Abfrage den Besitz mit Bürger-ID oder Kennung.
  3. Vermeiden Sie die Offenlegung von Kennungen gegenüber anderen Clients. Verwenden Sie Server-Relays.

UX

  1. Deaktivieren Sie das Telefon, während Sie am Boden liegen, gefesselt sind oder fahren, wenn die Serverregeln dies erfordern.
  2. Sorgen Sie für eine schnelle Benutzeroberfläche. Verwenden Sie optimistische Updates und führen Sie eine Abstimmung bei Serverbestätigung durch.

Leistung

  1. NUI im Leerlauf halten. SetInterval-Schleifen in React vermeiden. Effekte und Ereignisse verwenden.
  2. Halten Sie die Pakete klein. Laden Sie schwere Bildschirme verzögert. Versenden Sie komprimierte Assets.
  3. 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

  1. Ressource starten in server.cfg vor abhängigen Skripten.
Stellen Sie sicher, dass mein Telefon
  1. Drücken Sie im Spiel F8 → Ausführen nui_devTools → öffnen http://localhost:13172 und wählen Sie Ihre NUI-Seite aus.
  2. Überprüfen Sie die Registerkarte Netzwerk. Jeder NUI → Lua-Aufruf trifft https://my_phone/ Endpunkte.
  3. Verwenden /meinTelefon Befehl und Bestätigung des Fokuswechsels.
  4. 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

  1. Begehen Benutzeroberfläche/ Quelle und ui/dist/ bauen.
  2. Führen Sie in CI Folgendes aus: pnpm --filter ui build und versenden nur Entfernung in Veröffentlichungen.
  3. Versionieren Sie Ihre SQL-Migrationen. Verlieren Sie niemals Benutzerdaten ohne Backups.

Schritt 10 – Erweiterungen, die Sie als Nächstes hinzufügen können

  1. Bankwesen: Link zur Bankressource Ihres Servers; Kontostand und Überweisungen offenlegen.
  2. Tweets/Anzeigen: globaler Feed mit Ratenbegrenzungen und Moderation.
  3. Marktplatz: Inserate mit Treuhandkonto.
  4. Job-Apps: Polizei-/EMS-MDT-Haken.
  5. Fotos: Screenshot-Integration über Server-Endpunkt, nicht über Daten-URLs.
  6. 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_version Ist himmelblau Und ui_page weist auf ui/dist/index.html.
  • Überprüfen Sie die F8-Konsole auf CORS- oder JSON-Fehler.

Nicht verwendbare Artikel

  • QBCore: bestätigen QBCore.Functions.CreateUseableItem läuft und Telefon ist 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


Meta

  • Zielresmon: ≤0,02 ms durchschnittlich im Leerlauf.
  • Bundle-Budget: ≤250 KB, gzippt für MVP.
  • UI-FPS: 60.
Lukas
Lukas

Ich bin Luke, ein Gamer und schreibe gerne über FiveM, GTA und Rollenspiele. Ich betreibe eine Rollenspiel-Community und habe etwa 10 Jahre Erfahrung in der Verwaltung von Servern.

Artikel570