feat(admin+api+player): 站内信手动发送、媒体库选择与发送记录详情

新增管理员手动发送站内信(三语富文本),支持发送记录查看与删除;修复全站「从媒体库选择」因分类过滤导致列表为空的问题;玩家端支持渲染管理员自定义消息;并优化后台列表页双层卡片布局。
This commit is contained in:
2026-06-22 14:01:31 +08:00
parent 648c314e23
commit 1210142a33
21 changed files with 1355 additions and 40 deletions

View File

@@ -820,6 +820,28 @@ export const adminPagesMs: Record<string, string> = {
'content.inbox_notify.manual_title': 'Tanda "Notifikasi peti mesej" semasa mencipta:',
'content.inbox_notify.banner_note': 'Promosi laman utama',
'content.inbox_notify.announcement_note': 'Notifikasi / ticker',
'content.inbox_broadcast.title': 'Hantar manual',
'content.inbox_broadcast.hint': 'Hantar mesej peti secara manual kepada pemain. Padam rekod juga membuang mesej di peti pemain.',
'content.inbox_broadcast.field_title': 'Tajuk',
'content.inbox_broadcast.field_body': 'Kandungan',
'content.inbox_broadcast.field_target': 'Penerima',
'content.inbox_broadcast.field_username': 'Nama pengguna pemain',
'content.inbox_broadcast.target_all': 'Semua pemain',
'content.inbox_broadcast.target_user': 'Pemain tertentu',
'content.inbox_broadcast.username_placeholder': 'Masukkan nama log masuk pemain',
'content.inbox_broadcast.send': 'Hantar',
'content.inbox_broadcast.send_success': 'Dihantar kepada {n} pemain',
'content.inbox_broadcast.form_invalid': 'Sila isi tajuk atau kandungan sekurang-kurangnya satu bahasa',
'content.inbox_broadcast.target_user_required': 'Nama pengguna pemain diperlukan',
'content.inbox_broadcast.history_title': 'Sejarah penghantaran',
'content.inbox_broadcast.view_title': 'Butiran penghantaran',
'content.inbox_broadcast.col_title': 'Tajuk',
'content.inbox_broadcast.col_target': 'Sasaran',
'content.inbox_broadcast.col_recipients': 'Bilangan',
'content.inbox_broadcast.col_sender': 'Penghantar',
'content.inbox_broadcast.col_time': 'Masa',
'content.inbox_broadcast.delete_confirm': 'Padam "{title}"? Salinan di peti pemain juga akan dibuang.',
'content.inbox_broadcast.locale_fallback_hint': 'Locale yang kosong akan guna susunan: bahasa pemain → Inggeris → Cina Ringkas → Melayu.',
'content.hint.banner': 'Dipaparkan dalam karusel laman utama; ketik untuk halaman butiran. Muat naik imej muka depan dan kandungan kaya seperti pengumuman laman rasmi.',
'content.hint.announcement': 'Dipaparkan sebagai teks bergulir di bahagian atas aplikasi pemain. Isi tajuk dan teks bergulir setiap bahasa — teks biasa sahaja.',
'content.section.publish': 'Tetapan terbitan',
@@ -837,6 +859,7 @@ export const adminPagesMs: Record<string, string> = {
'content.upload.cover_size_hint': 'Lebar disyorkan 860px+. Imej muka depan dan dalam kandungan akan menyesuaikan lebar pada pemain.',
'content.upload.pick_media_title': 'Pilih imej',
'content.upload.no_media': 'Tiada imej dalam pustaka — muat naik dahulu',
'content.upload.load_media_failed': 'Gagal memuat pustaka media',
'content.status.DRAFT': 'Draf',
'content.status.ACTIVE': 'Aktif',
'content.status.INACTIVE': 'Tidak aktif',

View File

@@ -888,6 +888,7 @@ export const adminPagesZh: Record<string, string> = {
'content.upload.pick_media': '从媒体库选择',
'content.upload.pick_media_title': '选择图片',
'content.upload.no_media': '媒体库中暂无图片,请先上传',
'content.upload.load_media_failed': '加载媒体库失败',
'content.upload.url_placeholder': '或手动粘贴图片 URL',
'content.upload.recommended_size': '建议尺寸860 x 360 px或 43:18 同比例图片;前台会完整显示并自动填充不合比例区域。',
'content.link.none': '无跳转',
@@ -1987,6 +1988,7 @@ export const adminPagesEn: Record<string, string> = {
'content.upload.pick_media': 'Pick from library',
'content.upload.pick_media_title': 'Select image',
'content.upload.no_media': 'No images in library — upload one first',
'content.upload.load_media_failed': 'Failed to load media library',
'content.upload.url_placeholder': 'Or paste image URL',
'content.upload.recommended_size': 'Recommended size: 860 x 360 px, or any 43:18 image. The player carousel keeps the full image visible and fills extra space.',
'content.link.none': 'No link',

View File

@@ -829,6 +829,7 @@ const adminPages: Record<string, string> = {
'content.upload.pick_media_title': 'Select image',
'content.upload.no_media': 'No images in library — upload one first',
'content.upload.load_media_failed': 'Failed to load media library',
'content.btn.create': 'New content',
'content.btn.enable': 'Enable',
'content.btn.disable': 'Disable',
@@ -849,6 +850,28 @@ const adminPages: Record<string, string> = {
'content.inbox_notify.manual_title': 'Check "Inbox notify" when creating:',
'content.inbox_notify.banner_note': 'Homepage promo',
'content.inbox_notify.announcement_note': 'Announcements / ticker',
'content.inbox_broadcast.title': 'Manual send',
'content.inbox_broadcast.hint': 'Manually send inbox messages to players. Deleting a record also removes the message from player inboxes.',
'content.inbox_broadcast.field_title': 'Title',
'content.inbox_broadcast.field_body': 'Body',
'content.inbox_broadcast.field_target': 'Recipients',
'content.inbox_broadcast.field_username': 'Player username',
'content.inbox_broadcast.target_all': 'All players',
'content.inbox_broadcast.target_user': 'Single player',
'content.inbox_broadcast.username_placeholder': 'Enter player login username',
'content.inbox_broadcast.send': 'Send',
'content.inbox_broadcast.send_success': 'Sent to {n} player(s)',
'content.inbox_broadcast.form_invalid': 'Provide a title or body in at least one language',
'content.inbox_broadcast.target_user_required': 'Player username is required',
'content.inbox_broadcast.history_title': 'Send history',
'content.inbox_broadcast.view_title': 'Send details',
'content.inbox_broadcast.col_title': 'Title',
'content.inbox_broadcast.col_target': 'Target',
'content.inbox_broadcast.col_recipients': 'Count',
'content.inbox_broadcast.col_sender': 'Sender',
'content.inbox_broadcast.col_time': 'Sent at',
'content.inbox_broadcast.delete_confirm': 'Delete "{title}"? Player inbox copies will be removed too.',
'content.inbox_broadcast.locale_fallback_hint': 'Missing locales fall back in order: player language → English → Simplified Chinese → Malay.',
'content.hint.banner': 'Shown in the home carousel; tapping opens the detail page. Add a cover image and rich body like an official site announcement.',
'content.hint.announcement': 'Shows as a scrolling marquee at the top of the player app. Enter title and marquee text per language — plain text only.',
'content.section.publish': 'Publish settings',

View File

@@ -847,6 +847,28 @@ const adminPages: Record<string, string> = {
'content.inbox_notify.manual_title': '以下类型在新建时勾选「邮箱通知」',
'content.inbox_notify.banner_note': '首页推广',
'content.inbox_notify.announcement_note': '通知公告 / 跑马灯',
'content.inbox_broadcast.title': '手动发送',
'content.inbox_broadcast.hint': '手动向玩家发送站内信;删除记录将同时撤回玩家端对应消息。',
'content.inbox_broadcast.field_title': '标题',
'content.inbox_broadcast.field_body': '正文',
'content.inbox_broadcast.field_target': '发送对象',
'content.inbox_broadcast.field_username': '玩家账号',
'content.inbox_broadcast.target_all': '全部玩家',
'content.inbox_broadcast.target_user': '指定玩家',
'content.inbox_broadcast.username_placeholder': '输入玩家登录账号',
'content.inbox_broadcast.send': '发送',
'content.inbox_broadcast.send_success': '已发送给 {n} 位玩家',
'content.inbox_broadcast.form_invalid': '请至少填写一种语言的标题或正文',
'content.inbox_broadcast.target_user_required': '请填写玩家账号',
'content.inbox_broadcast.history_title': '发送记录',
'content.inbox_broadcast.view_title': '发送详情',
'content.inbox_broadcast.col_title': '标题',
'content.inbox_broadcast.col_target': '对象',
'content.inbox_broadcast.col_recipients': '人数',
'content.inbox_broadcast.col_sender': '发送人',
'content.inbox_broadcast.col_time': '发送时间',
'content.inbox_broadcast.delete_confirm': '确定删除「{title}」?玩家端对应消息将一并删除。',
'content.inbox_broadcast.locale_fallback_hint': '未填写的语言将按玩家语言 → 英文 → 简体中文 → 马来语顺序回退使用已有内容。',
'content.hint.banner': '用于首页轮播展示,点击后进入通知详情页;请填写封面图与正文,像官网发布活动通知一样编辑。',
'content.hint.announcement': '在玩家端顶部显示滚动跑马灯文字;填写各语言标题与滚动文案即可,纯文本无富文本。',
'content.section.publish': '发布设置',
@@ -864,6 +886,7 @@ const adminPages: Record<string, string> = {
'content.upload.cover_size_hint': '建议宽度 860px 以上;玩家端详情页会完整显示封面,正文内图片也会自适应宽度。',
'content.upload.pick_media_title': '选择图片',
'content.upload.no_media': '媒体库中暂无图片,请先上传',
'content.upload.load_media_failed': '加载媒体库失败',
'content.status.DRAFT': '草稿',
'content.status.ACTIVE': '已启用',
'content.status.INACTIVE': '已停用',