返回博客
Tutorial3 分钟阅读

FiveM通知系统:Toast、警报与ox_lib

构建精致的 FiveM 通知系统。吐司通知、进度条、环形菜单,以及 ox_lib 和顶级脚本如何处理客户端 UI 反馈。

Agency Scripts

Agency Scripts 创始人兼首席开发者

为什么要打造自定义通知系统?

默认 FiveM 通知功能齐全,但缺乏视觉精致和灵活性。大多数角色扮演服务器依赖通用文本弹窗,外观完全相同,玩家难以区分错误、成功确认或信息提醒。使用 NUI 构建的自定义通知系统让你完全控制样式、动画、位置、音效和排队行为。它将基础 UI 元素转变为符合你服务器品牌形象的内容,显著提升玩家体验。本教程将从零开始构建完整的吐司通知系统,服务器和客户端使用 Lua,前端使用 HTML、CSS 和 JavaScript 驱动 NUI。

设置 NUI 层

任何FiveM自定义通知系统的基础是NUI(新UI)层。NUI允许您将HTML内容渲染为游戏上的覆盖层,这意味着您可以使用现代网页技术的全部功能。首先创建您的资源结构,使用 fxmanifest.lua,一个客户端 Lua 脚本,和一个 html 包含您的 UI 文件的文件夹。清单需要声明您的 NUI 页面并注册消息处理程序,以便您的 Lua 脚本能与前端通信。

-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

ui_page 'html/index.html'

files {
    'html/index.html',
    'html/style.css',
    'html/script.js',
    'html/sounds/*.ogg'
}

client_script 'client.lua'
server_script 'server.lua'

您的 html/index.html 文件应尽量简洁。只需一个容器div,通知将由JavaScript动态注入。保持HTML精简,因为每个通知元素均由程序创建和销毁。链接你的样式表和脚本,确保body背景透明,以便游戏世界在通知后可见。

<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="notification-container"></div>
    <script src="script.js"></script>
</body>
</html>

设计吐司通知 CSS

Toast通知应根据类型视觉上区分。为 成功, 错误, 信息,和 警告 通知。使用固定定位将容器放置在屏幕右上角,并垂直堆叠通知,间隔较小。每个提示应具有细腻的玻璃拟态效果和背景模糊,左侧带有彩色边框以指示类型,并带有平滑的进入和退出动画。使用CSS关键帧实现从右侧滑入和通知过期时的淡出效果。

/* html/style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; font-family: 'Segoe UI', sans-serif; overflow: hidden; }

#notification-container {
    position: fixed;
    top: 20px;
    right: 20px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    z-index: 9999;
    max-width: 380px;
    width: 100%;
}

.toast {
    background: rgba(15, 15, 25, 0.85);
    backdrop-filter: blur(12px);
    border-radius: 10px;
    padding: 14px 18px;
    border-left: 4px solid #3b82f6;
    color: #e2e8f0;
    animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

.toast.success { border-left-color: #22c55e; }
.toast.error   { border-left-color: #ef4444; }
.toast.warning { border-left-color: #f59e0b; }
.toast.info    { border-left-color: #3b82f6; }

.toast-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
.toast-body  { flex: 1; }
.toast-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.toast-msg   { font-size: 13px; color: #94a3b8; line-height: 1.5; }

.toast-progress {
    position: absolute;
    bottom: 0; left: 0;
    height: 3px;
    background: currentColor;
    border-radius: 0 0 0 10px;
    animation: progress linear forwards;
}

@keyframes slideIn {
    from { opacity: 0; transform: translateX(100px); }
    to   { opacity: 1; transform: translateX(0); }
}

@keyframes slideOut {
    from { opacity: 1; transform: translateX(0); }
    to   { opacity: 0; transform: translateX(100px); }
}

@keyframes progress {
    from { width: 100%; }
    to   { width: 0%; }
}

构建 JavaScript 通知队列

JavaScript层处理来自Lua的消息,创建DOM元素,管理队列并处理自动关闭。通知队列至关重要,因为不希望屏幕同时堆叠十条通知。限制最大可见数为五条,溢出通知排队,待前面通知过期后显示。每条通知应有可配置持续时间,点击通知立即关闭。每条toast底部的进度条为玩家提供通知剩余显示时间的视觉指示。

// html/script.js
const container = document.getElementById('notification-container');
const MAX_VISIBLE = 5;
const queue = [];
let activeCount = 0;

const icons = {
    success: '✔',
    error:   '✖',
    warning: '⚠',
    info:    'ℹ'
};

const sounds = {
    success: new Audio('sounds/success.ogg'),
    error:   new Audio('sounds/error.ogg'),
    warning: new Audio('sounds/warning.ogg'),
    info:    new Audio('sounds/info.ogg')
};

window.addEventListener('message', (event) => {
    if (event.data.type === 'showNotification') {
        addNotification(event.data);
    }
});

function addNotification(data) {
    if (activeCount >= MAX_VISIBLE) {
        queue.push(data);
        return;
    }
    createToast(data);
}

function createToast(data) {
    activeCount++;
    const duration = data.duration || 5000;
    const toast = document.createElement('div');
    toast.className = `toast ${data.style || 'info'}`;
    toast.style.position = 'relative';
    toast.innerHTML = `
        <div class="toast-icon">${icons[data.style] || icons.info}</div>
        <div class="toast-body">
            <div class="toast-title">${data.title || ''}</div>
            <div class="toast-msg">${data.message}</div>
        </div>
        <div class="toast-progress" style="animation-duration:${duration}ms"></div>
    `;

    toast.addEventListener('click', () => dismissToast(toast));
    container.appendChild(toast);

    if (data.sound !== false && sounds[data.style]) {
        sounds[data.style].currentTime = 0;
        sounds[data.style].play().catch(() => {});
    }

    setTimeout(() => dismissToast(toast), duration);
}

function dismissToast(toast) {
    if (toast.dataset.dismissed) return;
    toast.dataset.dismissed = 'true';
    toast.style.animation = 'slideOut 0.3s ease forwards';
    setTimeout(() => {
        toast.remove();
        activeCount--;
        if (queue.length > 0) {
            createToast(queue.shift());
        }
    }, 300);
}

客户端Lua集成

客户端需要一个函数向HTML层发送NUI消息,并导出接口以便其他资源触发通知而不直接依赖脚本内部。 SendNUIMessage native 将数据推送到浏览器上下文,由你的 JavaScript 接收。将其封装在一个干净的 API 函数中,接受标题、消息、类型和可选持续时间。注册客户端事件和导出,以便从服务器端脚本和其他客户端资源触发通知。这种双重方式确保与任何框架的最大兼容性。

-- client.lua
local function ShowNotification(title, message, style, duration, sound)
    SendNUIMessage({
        type   = 'showNotification',
        title  = title or '',
        message = message or '',
        style  = style or 'info',
        duration = duration or 5000,
        sound  = sound ~= false
    })
end

-- Export for other resources
exports('ShowNotification', ShowNotification)

-- Event-based trigger from server
RegisterNetEvent('notifications:show', function(title, message, style, duration)
    ShowNotification(title, message, style, duration)
end)

-- Convenience commands for testing
RegisterCommand('testnotify', function()
    ShowNotification('Success', 'Your item has been saved.', 'success', 4000)
    Wait(500)
    ShowNotification('Error', 'Insufficient funds for this purchase.', 'error', 5000)
    Wait(500)
    ShowNotification('Warning', 'Your vehicle is low on fuel.', 'warning', 4000)
    Wait(500)
    ShowNotification('Info', 'Press E to interact with the NPC.', 'info', 3000)
end, false)

服务器端事件分发

服务器端脚本提供辅助函数,用于向特定玩家、所有玩家或玩家组发送通知。在这里处理广播公告、发送交易确认或提醒管理员可疑活动等用例。通过在服务器端集中调度逻辑,您可以保持对通知发送的单一控制点。您还可以在此添加速率限制,以防止恶意或有缺陷的客户端脚本发送通知垃圾邮件。服务器应在转发前验证通知参数,以防止通过精心制作的消息进行NUI注入。

-- server.lua
local function NotifyPlayer(source, title, message, style, duration)
    if not source or source <= 0 then return end
    title   = tostring(title or '')
    message = tostring(message or '')
    style   = style or 'info'
    TriggerClientEvent('notifications:show', source, title, message, style, duration)
end

local function NotifyAll(title, message, style, duration)
    TriggerClientEvent('notifications:show', -1, title, message, style, duration)
end

exports('NotifyPlayer', NotifyPlayer)
exports('NotifyAll', NotifyAll)

-- Example: welcome notification
AddEventHandler('playerJoining', function()
    local src = source
    Wait(3000)
    NotifyPlayer(src, 'Welcome', 'Welcome to the server! Have fun.', 'success', 6000)
end)

-- Admin broadcast command
RegisterCommand('broadcast', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.broadcast') then return end
    local msg = table.concat(args, ' ')
    NotifyAll('Announcement', msg, 'info', 8000)
end, true)

添加音效和优化

音效将通知从纯视觉元素提升为多感官反馈机制。玩家常在后台运行游戏或专注驾驶,可能错过无声提示。成功通知的短促细微铃声、错误的低沉嗡鸣和信息提示的轻柔叮当声确保玩家即使不看通知区也能接收重要信息。保持音频文件短小,低于500毫秒,使用OGG格式以兼容浏览器。音量设置约30%,以便与游戏音频融合而非压倒它。将音效文件存储在您的 html/sounds 目录,并在 JavaScript 中预加载它们以避免首次通知时的播放延迟。

高级功能与自定义

核心系统运行后,考虑添加高级功能以区分你的服务器。持续通知会一直显示,直到玩家明确点击,适用于重要提醒如未接电话或监狱计时器。通知中的操作按钮让玩家直接响应,例如接受交易请求而无需打开单独菜单。你还可以实现通知分组,将重复相同通知合并为带计数徽章的单一弹窗。为框架集成,创建桥接文件覆盖QBCore或ESX中的默认通知函数,使服务器上的所有资源自动使用你的自定义系统,无需修改代码。这种用自定义实现替换框架默认的模式,是一次性升级整个服务器UI的干净方法。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。