Spare 20 % mit WELCOMEAngebote ansehen
Baue eine benutzerdefinierte FiveM-Handy-App

Baue eine benutzerdefinierte FiveM-Handy-App

Ziel
Erstelle ein produktionsbereites Smartphone im Spiel für FiveM unter Verwendung von NUI + React. Du wirst ein Resource-Gerüst erstellen, QBCore/ESX-Ereignisse verbinden, Daten in MySQL speichern und eine flüssige UI ausliefern, die Leistungsbudgets einhält.


Voraussetzungen

  1. Ein laufender FiveM-Server mit txAdmin und MySQL (oxmysql oder mysql-async).
  2. Node.js 18+ und pnpm oder npm auf deinem Entwicklungs-PC.
  3. Ein Framework installiert: Abonnieren 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

resources/
 [local]/
 my_phone/
 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'
game 'gta5'

ui_page 'ui/dist/index.html'

dateien { 'ui/dist/**' }

client_scripts { 'client/main.lua' }
server_scripts {
 '@oxmysql/lib/MySQL.lua',
 'server/main.lua'
}

lua54 'yes'

Schritt 2 — Erstelle das React NUI

Initialisiere eine Vite React-App innerhalb 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
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
 plugins: [react()],
 build: { outDir: 'dist', emptyOutDir: true },
 base: ''
})

NUI-Brücke

// src/api.ts
export async function nui(event: string, data?: unknown): Promise {
 const res = await fetch(`https://my_phone/${event}`, {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify(data ?? {})
 })
 return await res.json()
}

Mount React + Nachrichtenzuhörer

// 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>

Baue die UI:

pnpm-Build

Schritt 3 – Client: Öffnen/Schließen, NUI-Fokus, Rückrufe

-- client/main.lua
local open = false

local function openPhone()
 if open then return end
 open = true
 SetNuiFocus(true, true)
 SendNUIMessage({ action = 'open' })
end

local function closePhone()
 if not open then return end
 open = false
 SetNuiFocus(false, false)
 SendNUIMessage({ action = 'close' })
end

-- Tastenbelegung (F1 Beispiel)
RegisterCommand('myphone', function()
 if open then closePhone() else openPhone() end
end)
RegisterKeyMapping('myphone', 'Telefon umschalten', 'keyboard', 'F1')

-- NUI → Spiel-Rückrufe
RegisterNUICallback('ui:close', function(_, cb)
 closePhone()
 cb({ ok = true })
end)

-- Kontakte auflisten, fragt den Server
RegisterNUICallback('contacts:list', function(_, cb)
 lib.callback('my_phone:server:getContacts', false, function(rows)
 cb(rows)
 end)
end)

Tipp: Aktiviere NUI-Entwicklungswerkzeuge in der Spielkonsole mit nui_devTools. Offen http://localhost:13172 in deinem Chromium-Browser, um die UI zu inspizieren.


Schritt 4 – Server: DB-Schema + Rückrufe

SQL (MySQL)

CREATE TABLE IF NOT EXISTS 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)
);

CREATE TABLE IF NOT EXISTS 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'] and exports['qb-core']:GetCoreObject()
ESX = ESX or nil

if not QBCore then
 TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
end

-- Kontakte für den eingeloggten Charakter laden
lib.callback.register('my_phone:server:getContacts', function(source)
 local citizenid
 if QBCore then
 local Player = QBCore.Functions.GetPlayer(source)
 citizenid = Player and Player.PlayerData.citizenid
 else
 local xPlayer = ESX.GetPlayerFromId(source)
 citizenid = xPlayer and xPlayer.identifier
 end
 if not citizenid then return {} end

 local rows = MySQL.query.await('SELECT id, name, number FROM phone_contacts WHERE citizenid = ?', { citizenid })
 return rows or {}
end)

-- Einen Kontakt speichern
lib.callback.register('my_phone:server:addContact', function(source, contact)
 if type(contact) ~= 'table' then return { ok = false } end
 local name, number = contact.name, contact.number
 if not name or not number then return { ok = false } end

 local citizenid
 if QBCore then
 local Player = QBCore.Functions.GetPlayer(source)
 citizenid = Player and Player.PlayerData.citizenid
 else
 local xPlayer = ESX.GetPlayerFromId(source)
 citizenid = xPlayer and xPlayer.identifier
 end
 if not citizenid then return { ok = false } end

 MySQL.insert.await('INSERT INTO phone_contacts (citizenid, name, number) VALUES (?, ?, ?)', { citizenid, name, number })
 return { ok = true }
end)

Schritt 5 – Framework-Integration (Element + Berechtigungen)

Abonnieren

Füge ein nutzbares Telefon-Item hinzu und schalte die UI ein, wenn es verwendet wird.

-- server/main.lua (nur QBCore)
if QBCore then
 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
 -- Benachrichtigung über ox_lib anzeigen
 lib.notify({ title = 'Telefon', 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)
if ESX and not QBCore then
 ESX.RegisterUsableItem('phone', function(playerId)
 TriggerClientEvent('my_phone:client:toggle', playerId)
 end)
end

Wenn du ox_inventory, verwendest, erstelle das Item dort und verlass dich auf dessen nutzbare Handler. Du kannst trotzdem dasselbe Client-Ereignis auslösen.


Schritt 6 – Kernfunktionen

Implementiere kleine Teile und liefere sie 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('my_phone:client:messages:push', function(peer, body)
 SendNUIMessage({ action = 'message:new', peer = peer, body = body })
end)

Anrufe (optional MVP)

  • Speichere nur Anrufprotokolle. Echte Audioaufnahmen verwenden dein Sprach-Plugin (pma-voice, mumble, SaltyChat) und sind außerhalb dieses MVPs.
  • Füge eine UI-Tastatur hinzu → beim Wählen einen abgehenden Anruf protokollieren; beim Antworten einen eingehenden Anruf protokollieren. Du kannst später mit der API eines Sprach-Plugins integrieren.

Schritt 7 – Sicherheit, UX, Leistung

Sicherheit

  1. Vertraue niemals NUI-Eingaben. Überprüfe Typen und Länge auf dem Server.
  2. Überprüfe den Besitz bei jeder Abfrage mit Bürger-ID oder Kennung.
  3. Vermeide das Freilegen von Identifikatoren für andere Clients. Verwende Server-Relays.

UX

  1. Breche das Telefon ab, wenn du niedergeschlagen, gefesselt oder fährst, falls deine Serverregeln dies erfordern.
  2. Halte die UI schnell. Verwende optimistische Aktualisierungen und stimme sie mit der Server-Bestätigung ab.

Leistung

  1. NUI im Leerlauf halten. SetInterval-Schleifen in React vermeiden. Effekte und Ereignisse verwenden.
  2. Halte die Bündel klein. Lade schwere Bildschirme nach. Liefere komprimierte Assets aus.
  3. Verwende Resmon, um unter 0,01–0,02 ms im Durchschnitt zu bleiben. Siehe FiveMX-Leitfaden, der oben verlinkt ist.

Schritt 8 – Testen und Debuggen

  1. Ressource starten in server.cfg vor abhängigen Skripten.
stelle sicher, dass my_phone
  1. Im Spiel, drücke F8 → führe nui_devTools → öffnen http://localhost:13172 aus und wähle deine NUI-Seite aus.
  2. Inspektiere den Netzwerk-Tab. Jeder NUI → Lua-Aufruf trifft zu. https://my_phone/ Endpunkte.
  3. Verwenden /meinTelefon Befehl und Bestätigung des Fokuswechsels.
  4. Führe Resmon aus und überprüfe, ob die CPU 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. In CI ausführen pnpm --filter ui build und versenden nur Entfernung in Veröffentlichungen.
  3. Versioniere deine SQL-Migrationen. Lösche niemals Benutzerdaten ohne Backups.

Schritt 10 — Erweiterungen, die du als Nächstes hinzufügen kannst

  1. Bankwesen: Link zu deiner Server-Banking-Ressource; zeige Kontostand und Überweisungen.
  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üfe die F8-Konsole auf CORS- oder JSON-Fehler.

Nicht verwendbare Artikel

  • QBCore: bestätigen QBCore.Functions.CreateUseableItem läuft und Telefon existiert in deiner Artikel-Liste.
  • ESX: Bestätigen ESX.RegisterUsableItem('Telefon', ...) Register nach Inventarladungen.

Datenbankfehler

  • Stelle sicher, dass oxmysql vor diesem Ressourcen gestartet wird.
  • Überprüfe die Spaltengrößen und Kodierungen für Unicode-Namen.

Referenzausschnitte (Kopieren und Einfügen)

Helfer

function GetCitizenId(source)
 if QBCore then
 local P = QBCore.Functions.GetPlayer(source)
 return P and P.PlayerData.citizenid
 else
 local xP = ESX.GetPlayerFromId(source)
 return xP and xP.identifier
 end
end

Kontakt über die Benutzeroberfläche hinzufügen

// UI
async function addContact(name: string, number: string) {
 const res = await nui('contacts:add', { name, number })
 if (res.ok) {
 const next = await nui('contacts:list')
 // Zustand aktualisieren
 }
}
-- client
RegisterNUICallback('contacts:add', function(data, cb)
 lib.callback('my_phone:server:addContact', false, function(resp)
 cb(resp)
 end, data)
end)

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 du sicher erweitern kannst.

Versende es, messe die Leistung und iteriere.


Weiterführende Literatur


Meta

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