diff --git a/external-admin/index.html b/external-admin/index.html
index 60bbbf8..a83fee8 100644
--- a/external-admin/index.html
+++ b/external-admin/index.html
@@ -4,7 +4,7 @@
-
HYApp External Admin
+ Fami
diff --git a/external-admin/src/api/auth.js b/external-admin/src/api/auth.js
index 9371100..f6dcae6 100644
--- a/external-admin/src/api/auth.js
+++ b/external-admin/src/api/auth.js
@@ -66,6 +66,7 @@ export function normalizeSession(data) {
account: textValue(accountRecord.username, accountRecord.account, legacyAccount.username, legacyAccount.account, root.accountName, typeof root.account === "string" ? root.account : "", root.username),
appCode: textValue(root.appCode, root.app_code, app.appCode, app.app_code, accountRecord.appCode, accountRecord.app_code, linkedUser.appCode, linkedUser.app_code),
appName: textValue(app.name, app.appName, app.app_name, root.appName, root.app_name),
+ logoUrl: textValue(app.logoUrl, app.logo_url, root.logoUrl, root.logo_url),
capabilities: [...new Set(capabilities)],
displayName: textValue(linkedUser.displayName, linkedUser.display_name, linkedUser.name, linkedUser.username, accountRecord.username, legacyAccount.username, root.displayName),
id: textValue(accountRecord.id, accountRecord.externalAdminUserId, accountRecord.external_admin_user_id, legacyAccount.id, root.accountId, root.account_id),
diff --git a/external-admin/src/api/auth.test.js b/external-admin/src/api/auth.test.js
index 1c3584e..f732676 100644
--- a/external-admin/src/api/auth.test.js
+++ b/external-admin/src/api/auth.test.js
@@ -9,13 +9,14 @@ describe("normalizeSession", () => {
test("normalizes account objects, fixed app and capabilities without stringifying objects", () => {
expect(normalizeSession({
account: { id: "ext-1", username: "fami-manager" },
- app: { appCode: "fami", appName: "Fami" },
+ app: { appCode: "fami", appName: "Fami", logoUrl: "https://media.example.com/fami.png" },
capabilities: ["user:list"],
passwordChangeRequired: true
})).toEqual(expect.objectContaining({
account: "fami-manager",
appCode: "fami",
appName: "Fami",
+ logoUrl: "https://media.example.com/fami.png",
capabilities: ["user:list"],
passwordChangeRequired: true
}));
diff --git a/external-admin/src/components/BannerCreateDialog.jsx b/external-admin/src/components/BannerCreateDialog.jsx
index 486fec1..340ecc2 100644
--- a/external-admin/src/components/BannerCreateDialog.jsx
+++ b/external-admin/src/components/BannerCreateDialog.jsx
@@ -57,6 +57,7 @@ export function BannerCreateDialog({ loading, onClose, onSubmit, open }) {
messages={appJumpMessages(t)}
paramValue={form.param}
required
+ showHint={false}
typeLabel={t("banners.jumpType")}
typeValue={form.bannerType}
onChange={({ param, type }) => setForm({ ...form, bannerType: type, param })}
@@ -119,7 +120,6 @@ function appJumpMessages(t) {
appTarget: t("jump.appTarget"),
gameId: t("jump.gameId"),
giftId: t("jump.giftId"),
- hint: t("jump.hint"),
redPacketNo: t("jump.redPacketNo"),
roomId: t("jump.roomId"),
targets: Object.fromEntries(["wallet", "room", "room_random", "room_window", "room_random_window", "room_game", "room_random_game", "explore_game", "custom"].map((value) => [value, t(`jump.target.${value}`)])),
diff --git a/external-admin/src/components/BannerCreateDialog.test.jsx b/external-admin/src/components/BannerCreateDialog.test.jsx
index bafad3e..f07ecc1 100644
--- a/external-admin/src/components/BannerCreateDialog.test.jsx
+++ b/external-admin/src/components/BannerCreateDialog.test.jsx
@@ -16,5 +16,6 @@ describe("BannerCreateDialog", () => {
expect(screen.getByRole("combobox", { name: "App destination" })).toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Parameter (App navigation JSON)" })).toHaveValue(JSON.stringify({ type: "wallet" }));
+ expect(screen.queryByText("App navigation parameters are generated as JSON. You can edit the JSON directly for custom parameters.")).not.toBeInTheDocument();
});
});
diff --git a/external-admin/src/components/GrantDialogs.jsx b/external-admin/src/components/GrantDialogs.jsx
index 6fce0f1..7f986ee 100644
--- a/external-admin/src/components/GrantDialogs.jsx
+++ b/external-admin/src/components/GrantDialogs.jsx
@@ -1,4 +1,3 @@
-import Alert from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
@@ -16,7 +15,6 @@ export function ResourceGrantDialog({ loading, onClose, onSubmit, open, resource
useEffect(() => setForm(resourceGrantInitial), [open]);
return (
onSubmit(form)}>
-
setForm({ ...form, target: event.target.value.trim() })} />
setForm({ ...form, resourceId: event.target.value })}>
{resources.map((item) => )}
@@ -34,7 +32,6 @@ export function PrettyIdGrantDialog({ loading, onClose, onSubmit, open }) {
useEffect(() => setForm(prettyGrantInitial), [open]);
return (
onSubmit(form)}>
-
setForm({ ...form, target: event.target.value.trim() })} />
setForm({ ...form, prettyId: event.target.value.trim() })} />
setForm({ ...form, durationDays: event.target.value })} />
@@ -43,11 +40,6 @@ export function PrettyIdGrantDialog({ loading, onClose, onSubmit, open }) {
);
}
-function TargetNotice() {
- const { t } = useExternalI18n();
- return {t("grants.notice")};
-}
-
function GrantDialog({ children, loading, onClose, onSubmit, open, title }) {
const { t } = useExternalI18n();
return (
diff --git a/external-admin/src/components/GrantDialogs.test.jsx b/external-admin/src/components/GrantDialogs.test.jsx
new file mode 100644
index 0000000..3c72d63
--- /dev/null
+++ b/external-admin/src/components/GrantDialogs.test.jsx
@@ -0,0 +1,17 @@
+import { render, screen } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+import { ExternalI18nProvider } from "../i18n/ExternalI18nProvider.jsx";
+import { ResourceGrantDialog } from "./GrantDialogs.jsx";
+
+test("shows only actionable grant fields without a descriptive notice", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("dialog", { name: "Grant privilege item" })).toBeInTheDocument();
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ expect(screen.queryByText("Before submission, the user is verified in the current App and the server-returned user ID is used for the grant.")).not.toBeInTheDocument();
+ expect(screen.getByText("Enter 0 for permanent validity")).toBeInTheDocument();
+});
diff --git a/external-admin/src/i18n/ExternalI18nProvider.jsx b/external-admin/src/i18n/ExternalI18nProvider.jsx
index b5fe1ee..3c244c4 100644
--- a/external-admin/src/i18n/ExternalI18nProvider.jsx
+++ b/external-admin/src/i18n/ExternalI18nProvider.jsx
@@ -41,7 +41,6 @@ export function ExternalI18nProvider({ children }) {
root.lang = locale;
root.dir = direction;
globalThis.document.body?.setAttribute("dir", direction);
- globalThis.document.title = t("app.title");
}, [direction, locale, t]);
const value = useMemo(
diff --git a/external-admin/src/i18n/ExternalI18nProvider.test.jsx b/external-admin/src/i18n/ExternalI18nProvider.test.jsx
index abcb047..86af77f 100644
--- a/external-admin/src/i18n/ExternalI18nProvider.test.jsx
+++ b/external-admin/src/i18n/ExternalI18nProvider.test.jsx
@@ -16,7 +16,7 @@ describe("ExternalI18nProvider", () => {
render();
expect(screen.getByTestId("locale")).toHaveTextContent("en");
- expect(screen.getByTestId("title")).toHaveTextContent("HYApp External Admin");
+ expect(screen.getByTestId("title")).toHaveTextContent("Fami");
expect(document.documentElement).toHaveAttribute("lang", "en");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
});
@@ -26,25 +26,25 @@ describe("ExternalI18nProvider", () => {
const firstRender = render();
await user.click(screen.getByRole("button", { name: "ar" }));
- expect(screen.getByTestId("title")).toHaveTextContent("إدارة HYApp الخارجية");
+ expect(screen.getByTestId("title")).toHaveTextContent("Fami");
expect(document.documentElement).toHaveAttribute("lang", "ar");
expect(document.documentElement).toHaveAttribute("dir", "rtl");
expect(document.body).toHaveAttribute("dir", "rtl");
expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("ar");
await user.click(screen.getByRole("button", { name: "tr" }));
- expect(screen.getByTestId("title")).toHaveTextContent("HYApp Harici Yönetim");
+ expect(screen.getByTestId("title")).toHaveTextContent("Fami");
expect(document.documentElement).toHaveAttribute("lang", "tr");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
await user.click(screen.getByRole("button", { name: "zh-CN" }));
- expect(screen.getByTestId("title")).toHaveTextContent("HYApp 外管后台");
+ expect(screen.getByTestId("title")).toHaveTextContent("Fami");
expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("zh-CN");
firstRender.unmount();
render();
expect(screen.getByTestId("locale")).toHaveTextContent("zh-CN");
- expect(screen.getByTestId("title")).toHaveTextContent("HYApp 外管后台");
+ expect(screen.getByTestId("title")).toHaveTextContent("Fami");
});
});
diff --git a/external-admin/src/i18n/messages.js b/external-admin/src/i18n/messages.js
index c442286..cb399f3 100644
--- a/external-admin/src/i18n/messages.js
+++ b/external-admin/src/i18n/messages.js
@@ -10,8 +10,8 @@ export const EXTERNAL_LANGUAGE_OPTIONS = Object.freeze([
]);
const en = {
- "app.title": "HYApp External Admin",
- "app.shortTitle": "HYApp External Admin",
+ "app.title": "Fami",
+ "app.shortTitle": "Fami",
"language.label": "Language",
"common.actions": "Actions",
"common.active": "Active",
@@ -191,7 +191,6 @@ const en = {
"grants.validDays": "Validity (days)",
"grants.permanentHint": "Enter 0 for permanent validity",
"grants.reason": "Grant reason",
- "grants.notice": "Before submission, the user is verified in the current App and the server-returned user ID is used for the grant.",
"grants.confirm": "Confirm grant",
"banners.title": "Banner management",
"banners.banner": "Banner",
@@ -257,7 +256,6 @@ const en = {
"jump.gameId": "Game ID",
"jump.giftId": "Gift ID",
"jump.redPacketNo": "Red packet number",
- "jump.hint": "App navigation parameters are generated as JSON. You can edit the JSON directly for custom parameters.",
"time.unlimited": "Any time",
"time.quickHour": "Last hour",
"time.quickDay": "Last 24 hours",
@@ -323,8 +321,8 @@ const en = {
};
const zhCN = {
- "app.title": "HYApp 外管后台",
- "app.shortTitle": "HYApp 外管",
+ "app.title": "Fami",
+ "app.shortTitle": "Fami",
"language.label": "语言",
"common.actions": "操作", "common.active": "正常", "common.all": "全部", "common.allStatuses": "全部状态", "common.banned": "已封禁", "common.cancel": "取消", "common.confirm": "确认", "common.create": "创建", "common.disabled": "禁用", "common.edit": "编辑", "common.enabled": "启用", "common.expired": "过期", "common.loading": "正在加载", "common.noData": "当前无数据", "common.normal": "正常", "common.query": "查询", "common.reason": "原因", "common.reload": "重新加载", "common.retry": "重试", "common.save": "保存", "common.status": "状态", "common.user": "用户",
"auth.account": "外管账号", "auth.changePassword": "修改外管密码", "auth.checkingSession": "正在校验外管会话", "auth.confirmNewPassword": "确认新密码", "auth.currentPassword": "当前密码", "auth.fillAccountPassword": "请填写账号和密码", "auth.firstLoginChange": "首次登录必须修改密码", "auth.hidePassword": "隐藏密码", "auth.invalidCredentials": "账号或密码错误", "auth.login": "登录", "auth.loginFailed": "登录失败", "auth.logout": "退出登录", "auth.newPassword": "新密码", "auth.noPermission": "当前外管账号没有访问此功能的权限。", "auth.password": "密码", "auth.passwordChangedFailed": "密码修改失败", "auth.passwordMismatch": "两次输入的新密码不一致", "auth.passwordMin": "至少 8 个字符", "auth.passwordMinError": "新密码至少 8 个字符", "auth.passwordWhitespace": "新密码不能为空或全为空白字符", "auth.recheck": "重新校验", "auth.returnOverview": "返回权限总览", "auth.savePassword": "保存密码", "auth.sessionFailed": "会话校验失败", "auth.showPassword": "显示密码",
@@ -335,19 +333,19 @@ const zhCN = {
"bans.title": "账号封禁列表", "bans.reason": "封禁原因", "bans.unban": "解除封禁", "bans.unbanned": "用户已解除封禁", "bans.unbanFailed": "解除封禁失败", "bans.confirmTitle": "解除封禁", "bans.confirmContent": "确认解除用户 {user} 的封禁?",
"organization.title": "组织管理", "organization.agencies": "公会列表", "organization.bds": "BD 列表", "organization.bdManagers": "BD Manager 列表", "organization.hosts": "主播列表", "organization.managers": "管理员列表", "organization.superAdmins": "Super Admin 列表", "organization.team": "我的团队", "organization.agency": "公会", "organization.owner": "负责人", "organization.parentBd": "上级 BD", "organization.member": "成员", "organization.country": "国家/地区", "organization.userReference": "用户 {id}", "organization.agencyReference": "公会 {id}", "organization.teamSearch": "公会 ID / 负责人用户 ID / 上级 BD ID", "organization.noPermission": "当前外管账号没有访问该组织列表的权限。", "organization.bdLeaderCount": "BD Leader:{count}", "organization.bdCount": "BD:{count}", "organization.agencyCount": "公会:{count}", "organization.truncated": "团队规模超过接口上限,当前结果不完整",
"rooms.title": "房间管理", "rooms.room": "房间", "rooms.owner": "房主", "rooms.visibleRegion": "可见区域", "rooms.search": "房间 ID / 名称 / 房主", "rooms.updated": "房间信息已更新", "rooms.updateFailed": "房间更新失败", "rooms.editTitle": "编辑房间", "rooms.background": "房间背景图", "rooms.name": "房间名称", "rooms.description": "房间介绍", "rooms.visibleRegionId": "可见区域 ID", "rooms.allRegions": "全部",
- "grants.title": "资源与靓号", "grants.grantResource": "发放道具", "grants.grantPrettyId": "下发靓号", "grants.availableResources": "可发放资源", "grants.targetUser": "目标用户", "grants.resource": "资源", "grants.quantity": "数量", "grants.resourceGranted": "特权道具已发放", "grants.prettyIdGranted": "靓号已下发", "grants.failed": "发放失败", "grants.resourceTitle": "特权道具发放", "grants.prettyIdTitle": "靓号下发", "grants.target": "用户 ID / 短 ID(靓号)", "grants.currentTarget": "用户 ID / 当前短 ID", "grants.prettyId": "下发靓号", "grants.validDays": "有效天数", "grants.permanentHint": "填 0 表示永久有效", "grants.reason": "发放原因", "grants.notice": "提交前会校验当前 App 内的用户,并使用服务端返回的用户 ID 执行发放。", "grants.confirm": "确认发放",
+ "grants.title": "资源与靓号", "grants.grantResource": "发放道具", "grants.grantPrettyId": "下发靓号", "grants.availableResources": "可发放资源", "grants.targetUser": "目标用户", "grants.resource": "资源", "grants.quantity": "数量", "grants.resourceGranted": "特权道具已发放", "grants.prettyIdGranted": "靓号已下发", "grants.failed": "发放失败", "grants.resourceTitle": "特权道具发放", "grants.prettyIdTitle": "靓号下发", "grants.target": "用户 ID / 短 ID(靓号)", "grants.currentTarget": "用户 ID / 当前短 ID", "grants.prettyId": "下发靓号", "grants.validDays": "有效天数", "grants.permanentHint": "填 0 表示永久有效", "grants.reason": "发放原因", "grants.confirm": "确认发放",
"banners.title": "Banner 管理", "banners.banner": "Banner", "banners.type": "类型", "banners.platform": "平台", "banners.sort": "排序", "banners.create": "创建 Banner", "banners.created": "Banner 已创建", "banners.createFailed": "Banner 创建失败", "banners.image": "Banner 图片", "banners.roomImage": "房间内小屏图", "banners.description": "描述", "banners.jumpType": "跳转类型", "banners.h5Param": "参数(H5 链接)", "banners.appParam": "参数(APP 公共跳转 JSON)", "banners.displayScope": "展示范围", "banners.scopeHome": "首页", "banners.scopeRoom": "房间", "banners.scopeRecharge": "充值", "banners.scopeMe": "我的", "banners.regionId": "区域 ID", "banners.countryCode": "国家码", "banners.countryCodeHint": "2-3 位字母,留空为全部", "banners.schedule": "投放时间",
"upload.preview": "{label}预览", "upload.empty": "暂无图片", "upload.uploading": "上传中", "upload.replace": "更换图片", "upload.select": "上传图片", "upload.imageOnly": "请选择图片文件", "upload.sizeLimit": "图片不能超过 10MB", "upload.missingUrl": "上传结果缺少图片地址", "upload.failed": "图片上传失败",
"pagination.total": "共 {count} 条", "pagination.previous": "上一页", "pagination.next": "下一页",
"status.active": "正常", "status.enabled": "启用", "status.running": "运行中", "status.normal": "正常", "status.unbanned": "未封禁", "status.banned": "已封禁", "status.disabled": "禁用", "status.failed": "失败", "status.blocked": "已拦截", "status.expired": "过期",
- "jump.appTarget": "APP 目标", "jump.target.wallet": "钱包", "jump.target.room": "指定房间", "jump.target.room_random": "随机房间", "jump.target.room_window": "指定房内拉起窗口", "jump.target.room_random_window": "随机房内拉起窗口", "jump.target.room_game": "指定房内拉起游戏", "jump.target.room_random_game": "随机房内拉起游戏", "jump.target.explore_game": "Explore 游戏 H5", "jump.target.custom": "自定义 JSON", "jump.roomId": "房间 ID", "jump.window": "房内窗口", "jump.window.game_window": "游戏窗口", "jump.window.gift_panel": "礼物面板", "jump.window.red_packet_claim": "领红包弹窗", "jump.window.wheel": "转盘窗口", "jump.gameId": "游戏 ID", "jump.giftId": "礼物 ID", "jump.redPacketNo": "红包编号", "jump.hint": "APP 参数会按公共跳转方案生成 JSON;需要特殊参数时可直接编辑 JSON。",
+ "jump.appTarget": "APP 目标", "jump.target.wallet": "钱包", "jump.target.room": "指定房间", "jump.target.room_random": "随机房间", "jump.target.room_window": "指定房内拉起窗口", "jump.target.room_random_window": "随机房内拉起窗口", "jump.target.room_game": "指定房内拉起游戏", "jump.target.room_random_game": "随机房内拉起游戏", "jump.target.explore_game": "Explore 游戏 H5", "jump.target.custom": "自定义 JSON", "jump.roomId": "房间 ID", "jump.window": "房内窗口", "jump.window.game_window": "游戏窗口", "jump.window.gift_panel": "礼物面板", "jump.window.red_packet_claim": "领红包弹窗", "jump.window.wheel": "转盘窗口", "jump.gameId": "游戏 ID", "jump.giftId": "礼物 ID", "jump.redPacketNo": "红包编号",
"time.unlimited": "不限", "time.quickHour": "近 1 小时", "time.quickDay": "近 24 小时", "time.quickWeek": "近 7 天", "time.start": "开始时间", "time.end": "结束时间", "time.startDate": "开始日期", "time.endDate": "结束日期", "time.pending": "待选择", "time.startMark": "始", "time.endMark": "末", "time.previousMonth": "上个月", "time.nextMonth": "下个月", "time.hour": "时", "time.minute": "分", "time.clear": "清空", "time.apply": "确定", "time.endAfterStart": "结束时间必须晚于开始时间", "time.endDateAfterStart": "结束日期不能早于开始日期", "time.completeRange": "选择完整区间", "time.endLaterShort": "结束需晚于开始", "time.endDateLaterShort": "结束不能早于开始", "time.durationDays": "{count} 天", "time.durationHours": "{count} 小时", "time.durationMinutes": "{count} 分钟", "time.duration": "间隔 {value}", "time.sameDay": "同一天", "time.lessMinute": "间隔少于 1 分钟",
"errors.loadFailed": "数据加载失败", "errors.requestFailed": "请求失败", "errors.unauthorized": "会话已失效,请重新登录。", "errors.forbidden": "你没有执行此操作的权限。", "errors.notFound": "未找到请求的资源。", "errors.conflict": "当前数据状态存在冲突,请刷新后重试。", "errors.tooLarge": "请求内容过大。", "errors.rateLimited": "请求过于频繁,请稍后重试。", "errors.server": "服务暂时不可用,请稍后重试。", "errors.accountLocked": "账号已临时锁定,请稍后重试。", "errors.appInactive": "该账号关联的 App 已停用。", "errors.vipDisabled": "当前 App 的 VIP 体系已停用", "errors.vipLevelInvalid": "请选择当前 App 已配置的 VIP 等级", "errors.organizationRoute": "该组织列表尚无安全的数据接口", "errors.regionId": "可见区域 ID 必须是安全的非负整数", "errors.grantTarget": "未找到当前 App 内的目标用户", "errors.bannerType": "Banner 类型不正确", "errors.bannerPlatform": "Banner 平台不正确", "errors.bannerStatus": "Banner 状态不正确", "errors.bannerScope": "请至少选择一个展示范围", "errors.bannerImage": "请上传 Banner 图片", "errors.h5Url": "请填写合法的 H5 链接", "errors.h5Protocol": "H5 链接仅支持 http 或 https", "errors.appParamRequired": "请填写 APP 跳转参数", "errors.appParamObject": "APP 跳转参数必须是 JSON 对象", "errors.appParamType": "APP 跳转 type 不在公共跳转方案内", "errors.appParamRoom": "该 APP 跳转需要填写 room_id", "errors.appParamGame": "该 APP 跳转需要填写 game_id", "errors.appParamWindow": "该 APP 跳转需要填写有效 window", "errors.appParamGameCode": "Explore 游戏 H5 需要选择有效 game_code", "errors.roomBannerImage": "房间范围必须上传房间内小屏图", "errors.countryCode": "国家码必须是 2-3 位字母", "errors.bannerRegion": "区域 ID 不正确", "errors.bannerSchedule": "投放结束时间必须晚于开始时间"
};
const ar = {
...en,
- "app.title": "إدارة HYApp الخارجية", "app.shortTitle": "إدارة HYApp الخارجية", "language.label": "اللغة",
+ "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "اللغة",
"common.actions": "الإجراءات", "common.active": "نشط", "common.all": "الكل", "common.allStatuses": "كل الحالات", "common.banned": "محظور", "common.cancel": "إلغاء", "common.confirm": "تأكيد", "common.create": "إنشاء", "common.disabled": "معطّل", "common.edit": "تعديل", "common.enabled": "مفعّل", "common.expired": "منتهي", "common.loading": "جارٍ التحميل", "common.noData": "لا توجد بيانات", "common.normal": "طبيعي", "common.query": "بحث", "common.reason": "السبب", "common.reload": "إعادة التحميل", "common.retry": "إعادة المحاولة", "common.save": "حفظ", "common.status": "الحالة", "common.user": "المستخدم",
"auth.account": "حساب الإدارة الخارجية", "auth.changePassword": "تغيير كلمة مرور الإدارة الخارجية", "auth.checkingSession": "جارٍ التحقق من الجلسة", "auth.confirmNewPassword": "تأكيد كلمة المرور الجديدة", "auth.currentPassword": "كلمة المرور الحالية", "auth.fillAccountPassword": "أدخل الحساب وكلمة المرور", "auth.firstLoginChange": "يجب تغيير كلمة المرور عند تسجيل الدخول لأول مرة", "auth.hidePassword": "إخفاء كلمة المرور", "auth.invalidCredentials": "الحساب أو كلمة المرور غير صحيحة", "auth.login": "تسجيل الدخول", "auth.loginFailed": "فشل تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.newPassword": "كلمة المرور الجديدة", "auth.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى هذه الميزة.", "auth.password": "كلمة المرور", "auth.passwordChangedFailed": "فشل تغيير كلمة المرور", "auth.passwordMismatch": "كلمتا المرور الجديدتان غير متطابقتين", "auth.passwordMin": "8 أحرف على الأقل", "auth.passwordMinError": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل", "auth.passwordWhitespace": "لا يمكن أن تكون كلمة المرور الجديدة فارغة أو مسافات فقط", "auth.recheck": "إعادة التحقق", "auth.returnOverview": "العودة إلى ملخص الصلاحيات", "auth.savePassword": "حفظ كلمة المرور", "auth.sessionFailed": "فشل التحقق من الجلسة", "auth.showPassword": "إظهار كلمة المرور",
"nav.expand": "فتح القائمة", "nav.overview": "ملخص الصلاحيات", "nav.users": "إدارة المستخدمين", "nav.bans": "إدارة الحظر", "nav.hosts": "المضيفون", "nav.agencies": "الوكالات", "nav.bds": "قائمة BD", "nav.bdManagers": "مديرو BD", "nav.superAdmins": "المشرفون العامون", "nav.rooms": "إدارة الغرف", "nav.grants": "الموارد والمعرّفات المميزة", "nav.banners": "إدارة اللافتات", "nav.team": "فريقي",
@@ -357,19 +355,19 @@ const ar = {
"bans.title": "قائمة الحسابات المحظورة", "bans.reason": "سبب الحظر", "bans.unban": "إلغاء الحظر", "bans.unbanned": "تم إلغاء حظر المستخدم", "bans.unbanFailed": "فشل إلغاء حظر المستخدم", "bans.confirmTitle": "إلغاء حظر المستخدم", "bans.confirmContent": "هل تريد إلغاء حظر {user}؟",
"organization.title": "المؤسسة", "organization.agencies": "قائمة الوكالات", "organization.bds": "قائمة BD", "organization.bdManagers": "قائمة مديري BD", "organization.hosts": "قائمة المضيفين", "organization.managers": "قائمة المديرين", "organization.superAdmins": "قائمة المشرفين العامين", "organization.team": "فريقي", "organization.agency": "الوكالة", "organization.owner": "المالك", "organization.parentBd": "BD الأعلى", "organization.member": "العضو", "organization.country": "الدولة / المنطقة", "organization.userReference": "المستخدم {id}", "organization.agencyReference": "الوكالة {id}", "organization.teamSearch": "معرّف الوكالة / معرّف المالك / معرّف BD الأعلى", "organization.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى قائمة المؤسسة هذه.", "organization.bdLeaderCount": "قادة BD: {count}", "organization.bdCount": "BD: {count}", "organization.agencyCount": "الوكالات: {count}", "organization.truncated": "يتجاوز حجم الفريق حد الواجهة. النتائج الحالية غير مكتملة.",
"rooms.title": "إدارة الغرف", "rooms.room": "الغرفة", "rooms.owner": "المالك", "rooms.visibleRegion": "المنطقة المرئية", "rooms.search": "معرّف الغرفة / الاسم / المالك", "rooms.updated": "تم تحديث بيانات الغرفة", "rooms.updateFailed": "فشل تحديث الغرفة", "rooms.editTitle": "تعديل الغرفة", "rooms.background": "خلفية الغرفة", "rooms.name": "اسم الغرفة", "rooms.description": "وصف الغرفة", "rooms.visibleRegionId": "معرّف المنطقة المرئية", "rooms.allRegions": "الكل",
- "grants.title": "الموارد والمعرّفات المميزة", "grants.grantResource": "منح عنصر", "grants.grantPrettyId": "منح معرّف مميز", "grants.availableResources": "الموارد المتاحة", "grants.targetUser": "المستخدم المستهدف", "grants.resource": "المورد", "grants.quantity": "الكمية", "grants.resourceGranted": "تم منح عنصر الامتياز", "grants.prettyIdGranted": "تم منح المعرّف المميز", "grants.failed": "فشل المنح", "grants.resourceTitle": "منح عنصر امتياز", "grants.prettyIdTitle": "منح معرّف مميز", "grants.target": "معرّف المستخدم / المعرّف المميز", "grants.currentTarget": "معرّف المستخدم / المعرّف المميز الحالي", "grants.prettyId": "المعرّف المميز المراد منحه", "grants.validDays": "الصلاحية (بالأيام)", "grants.permanentHint": "أدخل 0 للصلاحية الدائمة", "grants.reason": "سبب المنح", "grants.notice": "قبل الإرسال، يتم التحقق من المستخدم داخل التطبيق الحالي ويُستخدم معرّف المستخدم الذي يعيده الخادم.", "grants.confirm": "تأكيد المنح",
+ "grants.title": "الموارد والمعرّفات المميزة", "grants.grantResource": "منح عنصر", "grants.grantPrettyId": "منح معرّف مميز", "grants.availableResources": "الموارد المتاحة", "grants.targetUser": "المستخدم المستهدف", "grants.resource": "المورد", "grants.quantity": "الكمية", "grants.resourceGranted": "تم منح عنصر الامتياز", "grants.prettyIdGranted": "تم منح المعرّف المميز", "grants.failed": "فشل المنح", "grants.resourceTitle": "منح عنصر امتياز", "grants.prettyIdTitle": "منح معرّف مميز", "grants.target": "معرّف المستخدم / المعرّف المميز", "grants.currentTarget": "معرّف المستخدم / المعرّف المميز الحالي", "grants.prettyId": "المعرّف المميز المراد منحه", "grants.validDays": "الصلاحية (بالأيام)", "grants.permanentHint": "أدخل 0 للصلاحية الدائمة", "grants.reason": "سبب المنح", "grants.confirm": "تأكيد المنح",
"banners.title": "إدارة اللافتات", "banners.banner": "اللافتة", "banners.type": "النوع", "banners.platform": "المنصة", "banners.sort": "الترتيب", "banners.create": "إنشاء لافتة", "banners.created": "تم إنشاء اللافتة", "banners.createFailed": "فشل إنشاء اللافتة", "banners.image": "صورة اللافتة", "banners.roomImage": "الصورة المصغرة داخل الغرفة", "banners.description": "الوصف", "banners.jumpType": "نوع الوجهة", "banners.h5Param": "المعامل (رابط H5)", "banners.appParam": "المعامل (JSON للتنقل داخل التطبيق)", "banners.displayScope": "نطاق العرض", "banners.scopeHome": "الرئيسية", "banners.scopeRoom": "الغرفة", "banners.scopeRecharge": "الشحن", "banners.scopeMe": "حسابي", "banners.regionId": "معرّف المنطقة", "banners.countryCode": "رمز الدولة", "banners.countryCodeHint": "حرفان أو 3؛ اتركه فارغًا للكل", "banners.schedule": "جدول العرض",
"upload.preview": "معاينة {label}", "upload.empty": "لا توجد صورة", "upload.uploading": "جارٍ الرفع", "upload.replace": "استبدال الصورة", "upload.select": "رفع صورة", "upload.imageOnly": "اختر ملف صورة", "upload.sizeLimit": "يجب ألا يتجاوز حجم الصورة 10 ميغابايت", "upload.missingUrl": "لا تحتوي استجابة الرفع على رابط للصورة", "upload.failed": "فشل رفع الصورة",
"pagination.total": "{count} عنصر", "pagination.previous": "السابق", "pagination.next": "التالي",
"status.active": "نشط", "status.enabled": "مفعّل", "status.running": "قيد التشغيل", "status.normal": "طبيعي", "status.unbanned": "غير محظور", "status.banned": "محظور", "status.disabled": "معطّل", "status.failed": "فشل", "status.blocked": "محجوب", "status.expired": "منتهي",
- "jump.appTarget": "وجهة التطبيق", "jump.target.wallet": "المحفظة", "jump.target.room": "غرفة محددة", "jump.target.room_random": "غرفة عشوائية", "jump.target.room_window": "نافذة في غرفة محددة", "jump.target.room_random_window": "نافذة في غرفة عشوائية", "jump.target.room_game": "لعبة في غرفة محددة", "jump.target.room_random_game": "لعبة في غرفة عشوائية", "jump.target.explore_game": "لعبة Explore H5", "jump.target.custom": "JSON مخصص", "jump.roomId": "معرّف الغرفة", "jump.window": "نافذة داخل الغرفة", "jump.window.game_window": "نافذة اللعبة", "jump.window.gift_panel": "لوحة الهدايا", "jump.window.red_packet_claim": "نافذة الظرف الأحمر", "jump.window.wheel": "نافذة العجلة", "jump.gameId": "معرّف اللعبة", "jump.giftId": "معرّف الهدية", "jump.redPacketNo": "رقم الظرف الأحمر", "jump.hint": "يتم إنشاء معاملات التنقل داخل التطبيق بصيغة JSON، ويمكنك تعديلها مباشرة للمعاملات المخصصة.",
+ "jump.appTarget": "وجهة التطبيق", "jump.target.wallet": "المحفظة", "jump.target.room": "غرفة محددة", "jump.target.room_random": "غرفة عشوائية", "jump.target.room_window": "نافذة في غرفة محددة", "jump.target.room_random_window": "نافذة في غرفة عشوائية", "jump.target.room_game": "لعبة في غرفة محددة", "jump.target.room_random_game": "لعبة في غرفة عشوائية", "jump.target.explore_game": "لعبة Explore H5", "jump.target.custom": "JSON مخصص", "jump.roomId": "معرّف الغرفة", "jump.window": "نافذة داخل الغرفة", "jump.window.game_window": "نافذة اللعبة", "jump.window.gift_panel": "لوحة الهدايا", "jump.window.red_packet_claim": "نافذة الظرف الأحمر", "jump.window.wheel": "نافذة العجلة", "jump.gameId": "معرّف اللعبة", "jump.giftId": "معرّف الهدية", "jump.redPacketNo": "رقم الظرف الأحمر",
"time.unlimited": "أي وقت", "time.quickHour": "آخر ساعة", "time.quickDay": "آخر 24 ساعة", "time.quickWeek": "آخر 7 أيام", "time.start": "وقت البدء", "time.end": "وقت الانتهاء", "time.startDate": "تاريخ البدء", "time.endDate": "تاريخ الانتهاء", "time.pending": "غير محدد", "time.startMark": "ب", "time.endMark": "ن", "time.previousMonth": "الشهر السابق", "time.nextMonth": "الشهر التالي", "time.hour": "الساعة", "time.minute": "الدقيقة", "time.clear": "مسح", "time.apply": "تطبيق", "time.endAfterStart": "يجب أن يكون وقت الانتهاء بعد وقت البدء", "time.endDateAfterStart": "لا يمكن أن يسبق تاريخ الانتهاء تاريخ البدء", "time.completeRange": "حدد النطاق الكامل", "time.endLaterShort": "يجب أن تكون النهاية بعد البداية", "time.endDateLaterShort": "لا يمكن أن تسبق النهاية البداية", "time.durationDays": "{count} يوم", "time.durationHours": "{count} ساعة", "time.durationMinutes": "{count} دقيقة", "time.duration": "{value}", "time.sameDay": "اليوم نفسه", "time.lessMinute": "أقل من دقيقة واحدة",
"errors.loadFailed": "فشل تحميل البيانات", "errors.requestFailed": "فشل الطلب", "errors.unauthorized": "انتهت الجلسة. سجّل الدخول مرة أخرى.", "errors.forbidden": "ليس لديك إذن لتنفيذ هذه العملية.", "errors.notFound": "لم يتم العثور على المورد المطلوب.", "errors.conflict": "تتعارض العملية مع البيانات الحالية. حدّث الصفحة وحاول مجددًا.", "errors.tooLarge": "حجم الطلب كبير جدًا.", "errors.rateLimited": "طلبات كثيرة جدًا. حاول لاحقًا.", "errors.server": "الخدمة غير متاحة مؤقتًا. حاول لاحقًا.", "errors.accountLocked": "الحساب مقفل مؤقتًا. حاول لاحقًا.", "errors.appInactive": "التطبيق المرتبط بهذا الحساب غير نشط.", "errors.vipDisabled": "نظام VIP معطّل في التطبيق الحالي", "errors.vipLevelInvalid": "اختر مستوى VIP معدًا للتطبيق الحالي", "errors.organizationRoute": "لا توجد نقطة بيانات آمنة لقائمة المؤسسة هذه", "errors.regionId": "يجب أن يكون معرّف المنطقة عددًا صحيحًا آمنًا وغير سالب", "errors.grantTarget": "لم يتم العثور على المستخدم المستهدف في التطبيق الحالي", "errors.bannerType": "نوع اللافتة غير صالح", "errors.bannerPlatform": "منصة اللافتة غير صالحة", "errors.bannerStatus": "حالة اللافتة غير صالحة", "errors.bannerScope": "اختر نطاق عرض واحدًا على الأقل", "errors.bannerImage": "ارفع صورة للافتة", "errors.h5Url": "أدخل رابط H5 صالحًا", "errors.h5Protocol": "يجب أن يستخدم رابط H5 بروتوكول http أو https", "errors.appParamRequired": "أدخل معاملات التنقل داخل التطبيق", "errors.appParamObject": "يجب أن تكون معاملات التطبيق كائن JSON", "errors.appParamType": "نوع التنقل داخل التطبيق غير مدعوم", "errors.appParamRoom": "تتطلب هذه الوجهة room_id", "errors.appParamGame": "تتطلب هذه الوجهة game_id", "errors.appParamWindow": "تتطلب هذه الوجهة نافذة صالحة", "errors.appParamGameCode": "تتطلب لعبة Explore H5 قيمة game_code صالحة", "errors.roomBannerImage": "ارفع صورة مصغرة لنطاق الغرفة", "errors.countryCode": "يجب أن يتكون رمز الدولة من حرفين أو 3", "errors.bannerRegion": "معرّف المنطقة غير صالح", "errors.bannerSchedule": "يجب أن يكون وقت انتهاء العرض بعد وقت البدء"
};
const tr = {
...en,
- "app.title": "HYApp Harici Yönetim", "app.shortTitle": "HYApp Harici Yönetim", "language.label": "Dil",
+ "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "Dil",
"common.actions": "İşlemler", "common.active": "Aktif", "common.all": "Tümü", "common.allStatuses": "Tüm durumlar", "common.banned": "Yasaklı", "common.cancel": "İptal", "common.confirm": "Onayla", "common.create": "Oluştur", "common.disabled": "Devre dışı", "common.edit": "Düzenle", "common.enabled": "Etkin", "common.expired": "Süresi dolmuş", "common.loading": "Yükleniyor", "common.noData": "Veri yok", "common.normal": "Normal", "common.query": "Ara", "common.reason": "Neden", "common.reload": "Yeniden yükle", "common.retry": "Tekrar dene", "common.save": "Kaydet", "common.status": "Durum", "common.user": "Kullanıcı",
"auth.account": "Harici yönetim hesabı", "auth.changePassword": "Harici yönetim parolasını değiştir", "auth.checkingSession": "Oturum kontrol ediliyor", "auth.confirmNewPassword": "Yeni parolayı doğrula", "auth.currentPassword": "Mevcut parola", "auth.fillAccountPassword": "Hesabınızı ve parolanızı girin", "auth.firstLoginChange": "İlk girişte parolanızı değiştirmeniz gerekir", "auth.hidePassword": "Parolayı gizle", "auth.invalidCredentials": "Hesap veya parola hatalı", "auth.login": "Giriş yap", "auth.loginFailed": "Giriş başarısız", "auth.logout": "Çıkış yap", "auth.newPassword": "Yeni parola", "auth.noPermission": "Bu harici yönetim hesabının bu özelliğe erişimi yok.", "auth.password": "Parola", "auth.passwordChangedFailed": "Parola değiştirilemedi", "auth.passwordMismatch": "Yeni parolalar eşleşmiyor", "auth.passwordMin": "En az 8 karakter", "auth.passwordMinError": "Yeni parola en az 8 karakter olmalıdır", "auth.passwordWhitespace": "Yeni parola boş veya yalnızca boşluk olamaz", "auth.recheck": "Tekrar kontrol et", "auth.returnOverview": "Yetki özetine dön", "auth.savePassword": "Parolayı kaydet", "auth.sessionFailed": "Oturum doğrulanamadı", "auth.showPassword": "Parolayı göster",
"nav.expand": "Menüyü aç", "nav.overview": "Yetki özeti", "nav.users": "Kullanıcı yönetimi", "nav.bans": "Yasak yönetimi", "nav.hosts": "Yayıncılar", "nav.agencies": "Ajanslar", "nav.bds": "BD listesi", "nav.bdManagers": "BD Yöneticileri", "nav.superAdmins": "Süper Yöneticiler", "nav.rooms": "Oda yönetimi", "nav.grants": "Kaynaklar ve özel ID'ler", "nav.banners": "Banner yönetimi", "nav.team": "Ekibim",
@@ -379,12 +377,12 @@ const tr = {
"bans.title": "Yasaklı hesap listesi", "bans.reason": "Yasak nedeni", "bans.unban": "Yasağı kaldır", "bans.unbanned": "Kullanıcı yasağı kaldırıldı", "bans.unbanFailed": "Kullanıcı yasağı kaldırılamadı", "bans.confirmTitle": "Kullanıcı yasağını kaldır", "bans.confirmContent": "{user} kullanıcısının yasağı kaldırılsın mı?",
"organization.title": "Organizasyon", "organization.agencies": "Ajans listesi", "organization.bds": "BD listesi", "organization.bdManagers": "BD Yöneticisi listesi", "organization.hosts": "Yayıncı listesi", "organization.managers": "Yönetici listesi", "organization.superAdmins": "Süper Yönetici listesi", "organization.team": "Ekibim", "organization.agency": "Ajans", "organization.owner": "Sahip", "organization.parentBd": "Üst BD", "organization.member": "Üye", "organization.country": "Ülke / bölge", "organization.userReference": "Kullanıcı {id}", "organization.agencyReference": "Ajans {id}", "organization.teamSearch": "Ajans ID / sahip kullanıcı ID / üst BD ID", "organization.noPermission": "Bu hesabın bu organizasyon listesine erişimi yok.", "organization.bdLeaderCount": "BD Liderleri: {count}", "organization.bdCount": "BD: {count}", "organization.agencyCount": "Ajanslar: {count}", "organization.truncated": "Ekip API sınırını aşıyor. Mevcut sonuçlar eksik.",
"rooms.title": "Oda yönetimi", "rooms.room": "Oda", "rooms.owner": "Sahip", "rooms.visibleRegion": "Görünür bölge", "rooms.search": "Oda ID / ad / sahip", "rooms.updated": "Oda bilgileri güncellendi", "rooms.updateFailed": "Oda güncellenemedi", "rooms.editTitle": "Odayı düzenle", "rooms.background": "Oda arka planı", "rooms.name": "Oda adı", "rooms.description": "Oda açıklaması", "rooms.visibleRegionId": "Görünür bölge ID", "rooms.allRegions": "Tümü",
- "grants.title": "Kaynaklar ve özel ID'ler", "grants.grantResource": "Öğe ver", "grants.grantPrettyId": "Özel ID ver", "grants.availableResources": "Kullanılabilir kaynaklar", "grants.targetUser": "Hedef kullanıcı", "grants.resource": "Kaynak", "grants.quantity": "Miktar", "grants.resourceGranted": "Ayrıcalık öğesi verildi", "grants.prettyIdGranted": "Özel ID verildi", "grants.failed": "Verme işlemi başarısız", "grants.resourceTitle": "Ayrıcalık öğesi ver", "grants.prettyIdTitle": "Özel ID ver", "grants.target": "Kullanıcı ID / özel ID", "grants.currentTarget": "Kullanıcı ID / mevcut özel ID", "grants.prettyId": "Verilecek özel ID", "grants.validDays": "Geçerlilik (gün)", "grants.permanentHint": "Kalıcı geçerlilik için 0 girin", "grants.reason": "Verme nedeni", "grants.notice": "Göndermeden önce kullanıcı geçerli uygulamada doğrulanır ve sunucunun döndürdüğü kullanıcı ID kullanılır.", "grants.confirm": "Vermeyi onayla",
+ "grants.title": "Kaynaklar ve özel ID'ler", "grants.grantResource": "Öğe ver", "grants.grantPrettyId": "Özel ID ver", "grants.availableResources": "Kullanılabilir kaynaklar", "grants.targetUser": "Hedef kullanıcı", "grants.resource": "Kaynak", "grants.quantity": "Miktar", "grants.resourceGranted": "Ayrıcalık öğesi verildi", "grants.prettyIdGranted": "Özel ID verildi", "grants.failed": "Verme işlemi başarısız", "grants.resourceTitle": "Ayrıcalık öğesi ver", "grants.prettyIdTitle": "Özel ID ver", "grants.target": "Kullanıcı ID / özel ID", "grants.currentTarget": "Kullanıcı ID / mevcut özel ID", "grants.prettyId": "Verilecek özel ID", "grants.validDays": "Geçerlilik (gün)", "grants.permanentHint": "Kalıcı geçerlilik için 0 girin", "grants.reason": "Verme nedeni", "grants.confirm": "Vermeyi onayla",
"banners.title": "Banner yönetimi", "banners.banner": "Banner", "banners.type": "Tür", "banners.platform": "Platform", "banners.sort": "Sıralama", "banners.create": "Banner oluştur", "banners.created": "Banner oluşturuldu", "banners.createFailed": "Banner oluşturulamadı", "banners.image": "Banner görseli", "banners.roomImage": "Oda içi küçük görsel", "banners.description": "Açıklama", "banners.jumpType": "Hedef türü", "banners.h5Param": "Parametre (H5 bağlantısı)", "banners.appParam": "Parametre (uygulama yönlendirme JSON'u)", "banners.displayScope": "Gösterim kapsamı", "banners.scopeHome": "Ana sayfa", "banners.scopeRoom": "Oda", "banners.scopeRecharge": "Yükleme", "banners.scopeMe": "Ben", "banners.regionId": "Bölge ID", "banners.countryCode": "Ülke kodu", "banners.countryCodeHint": "2–3 harf; tümü için boş bırakın", "banners.schedule": "Gösterim zamanı",
"upload.preview": "{label} önizlemesi", "upload.empty": "Görsel yok", "upload.uploading": "Yükleniyor", "upload.replace": "Görseli değiştir", "upload.select": "Görsel yükle", "upload.imageOnly": "Bir görsel dosyası seçin", "upload.sizeLimit": "Görseller 10 MB'ı aşamaz", "upload.missingUrl": "Yükleme yanıtında görsel URL'si yok", "upload.failed": "Görsel yüklenemedi",
"pagination.total": "{count} öğe", "pagination.previous": "Önceki", "pagination.next": "Sonraki",
"status.active": "Aktif", "status.enabled": "Etkin", "status.running": "Çalışıyor", "status.normal": "Normal", "status.unbanned": "Yasaksız", "status.banned": "Yasaklı", "status.disabled": "Devre dışı", "status.failed": "Başarısız", "status.blocked": "Engelli", "status.expired": "Süresi dolmuş",
- "jump.appTarget": "Uygulama hedefi", "jump.target.wallet": "Cüzdan", "jump.target.room": "Belirli oda", "jump.target.room_random": "Rastgele oda", "jump.target.room_window": "Belirli odada pencere", "jump.target.room_random_window": "Rastgele odada pencere", "jump.target.room_game": "Belirli odada oyun", "jump.target.room_random_game": "Rastgele odada oyun", "jump.target.explore_game": "Explore oyunu H5", "jump.target.custom": "Özel JSON", "jump.roomId": "Oda ID", "jump.window": "Oda içi pencere", "jump.window.game_window": "Oyun penceresi", "jump.window.gift_panel": "Hediye paneli", "jump.window.red_packet_claim": "Kırmızı paket penceresi", "jump.window.wheel": "Çark penceresi", "jump.gameId": "Oyun ID", "jump.giftId": "Hediye ID", "jump.redPacketNo": "Kırmızı paket numarası", "jump.hint": "Uygulama yönlendirme parametreleri JSON olarak oluşturulur; özel parametreler için JSON'u doğrudan düzenleyebilirsiniz.",
+ "jump.appTarget": "Uygulama hedefi", "jump.target.wallet": "Cüzdan", "jump.target.room": "Belirli oda", "jump.target.room_random": "Rastgele oda", "jump.target.room_window": "Belirli odada pencere", "jump.target.room_random_window": "Rastgele odada pencere", "jump.target.room_game": "Belirli odada oyun", "jump.target.room_random_game": "Rastgele odada oyun", "jump.target.explore_game": "Explore oyunu H5", "jump.target.custom": "Özel JSON", "jump.roomId": "Oda ID", "jump.window": "Oda içi pencere", "jump.window.game_window": "Oyun penceresi", "jump.window.gift_panel": "Hediye paneli", "jump.window.red_packet_claim": "Kırmızı paket penceresi", "jump.window.wheel": "Çark penceresi", "jump.gameId": "Oyun ID", "jump.giftId": "Hediye ID", "jump.redPacketNo": "Kırmızı paket numarası",
"time.unlimited": "Sınırsız", "time.quickHour": "Son 1 saat", "time.quickDay": "Son 24 saat", "time.quickWeek": "Son 7 gün", "time.start": "Başlangıç zamanı", "time.end": "Bitiş zamanı", "time.startDate": "Başlangıç tarihi", "time.endDate": "Bitiş tarihi", "time.pending": "Seçilmedi", "time.startMark": "B", "time.endMark": "S", "time.previousMonth": "Önceki ay", "time.nextMonth": "Sonraki ay", "time.hour": "Saat", "time.minute": "Dakika", "time.clear": "Temizle", "time.apply": "Uygula", "time.endAfterStart": "Bitiş zamanı başlangıçtan sonra olmalıdır", "time.endDateAfterStart": "Bitiş tarihi başlangıçtan önce olamaz", "time.completeRange": "Tam aralığı seçin", "time.endLaterShort": "Bitiş başlangıçtan sonra olmalı", "time.endDateLaterShort": "Bitiş başlangıçtan önce olamaz", "time.durationDays": "{count} gün", "time.durationHours": "{count} saat", "time.durationMinutes": "{count} dakika", "time.duration": "{value}", "time.sameDay": "Aynı gün", "time.lessMinute": "1 dakikadan az",
"errors.loadFailed": "Veriler yüklenemedi", "errors.requestFailed": "İstek başarısız", "errors.unauthorized": "Oturumunuz sona erdi. Tekrar giriş yapın.", "errors.forbidden": "Bu işlemi yapma yetkiniz yok.", "errors.notFound": "İstenen kaynak bulunamadı.", "errors.conflict": "İşlem mevcut verilerle çakışıyor. Yenileyip tekrar deneyin.", "errors.tooLarge": "İstek çok büyük.", "errors.rateLimited": "Çok fazla istek gönderildi. Daha sonra tekrar deneyin.", "errors.server": "Hizmet geçici olarak kullanılamıyor. Daha sonra tekrar deneyin.", "errors.accountLocked": "Hesap geçici olarak kilitlendi. Daha sonra tekrar deneyin.", "errors.appInactive": "Bu hesaba bağlı uygulama etkin değil.", "errors.vipDisabled": "Geçerli uygulamanın VIP programı devre dışı", "errors.vipLevelInvalid": "Geçerli uygulama için yapılandırılmış bir VIP seviyesi seçin", "errors.organizationRoute": "Bu organizasyon listesi için güvenli veri uç noktası yok", "errors.regionId": "Görünür bölge ID güvenli ve negatif olmayan bir tam sayı olmalıdır", "errors.grantTarget": "Geçerli uygulamada hedef kullanıcı bulunamadı", "errors.bannerType": "Geçersiz banner türü", "errors.bannerPlatform": "Geçersiz banner platformu", "errors.bannerStatus": "Geçersiz banner durumu", "errors.bannerScope": "En az bir gösterim kapsamı seçin", "errors.bannerImage": "Bir banner görseli yükleyin", "errors.h5Url": "Geçerli bir H5 bağlantısı girin", "errors.h5Protocol": "H5 bağlantıları http veya https kullanmalıdır", "errors.appParamRequired": "Uygulama yönlendirme parametrelerini girin", "errors.appParamObject": "Uygulama parametreleri bir JSON nesnesi olmalıdır", "errors.appParamType": "Uygulama yönlendirme türü desteklenmiyor", "errors.appParamRoom": "Bu hedef room_id gerektirir", "errors.appParamGame": "Bu hedef game_id gerektirir", "errors.appParamWindow": "Bu hedef geçerli bir window gerektirir", "errors.appParamGameCode": "Explore oyunu H5 geçerli bir game_code gerektirir", "errors.roomBannerImage": "Oda kapsamı için oda içi küçük görsel yükleyin", "errors.countryCode": "Ülke kodu 2–3 harften oluşmalıdır", "errors.bannerRegion": "Geçersiz bölge ID", "errors.bannerSchedule": "Gösterim bitiş zamanı başlangıçtan sonra olmalıdır"
};
diff --git a/external-admin/src/i18n/messages.test.js b/external-admin/src/i18n/messages.test.js
index 7fa4dbb..08ee1cf 100644
--- a/external-admin/src/i18n/messages.test.js
+++ b/external-admin/src/i18n/messages.test.js
@@ -13,6 +13,10 @@ describe("external admin translations", () => {
test("uses native language names and Arabic RTL only", () => {
expect(EXTERNAL_LANGUAGE_OPTIONS.map((item) => item.label)).toEqual(["English", "简体中文", "العربية", "Türkçe"]);
+ for (const locale of ["en", "zh-CN", "ar", "tr"]) {
+ expect(externalMessage(locale, "app.title")).toBe("Fami");
+ expect(externalMessage(locale, "app.shortTitle")).toBe("Fami");
+ }
expect(externalDirection("ar")).toBe("rtl");
expect(externalDirection("en")).toBe("ltr");
expect(externalDirection("zh-CN")).toBe("ltr");
diff --git a/external-admin/src/layout/ExternalAdminLayout.jsx b/external-admin/src/layout/ExternalAdminLayout.jsx
index 3ff4361..5354b28 100644
--- a/external-admin/src/layout/ExternalAdminLayout.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.jsx
@@ -27,8 +27,9 @@ import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
+import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx";
import { hasAnyExternalCapability } from "../config/capabilities.js";
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
@@ -64,6 +65,19 @@ export function ExternalAdminLayout() {
);
const current = [...visibleNavigation].reverse().find((item) => location.pathname.startsWith(item.path));
+ useEffect(() => {
+ // Login resolves the account's App server-side. Both the visible brand and browser metadata must use that apps row rather than a bundled image or client-selected tenant.
+ const appName = String(session?.appName || session?.appCode || t("app.title")).trim();
+ const logoUrl = String(session?.logoUrl || "").trim();
+ globalThis.document.title = appName;
+ syncBrandFavicon(logoUrl);
+ return () => {
+ // Leaving an authenticated App session must not leak its brand into the next account's login screen.
+ globalThis.document.title = t("app.title");
+ syncBrandFavicon("");
+ };
+ }, [session?.appCode, session?.appName, session?.logoUrl, t]);
+
const handleLogout = async () => {
await logout();
navigate("/login", { replace: true });
@@ -72,11 +86,14 @@ export function ExternalAdminLayout() {
const drawer = (
- HY
-
- {t("app.shortTitle")}
- {session?.appName || session?.appCode}
-
+
@@ -140,3 +157,19 @@ export function ExternalAdminLayout() {
);
}
+
+function syncBrandFavicon(logoUrl) {
+ const selector = 'link[data-external-app-favicon="true"]';
+ const current = globalThis.document?.head?.querySelector(selector);
+ if (!logoUrl) {
+ current?.remove();
+ return;
+ }
+ const link = current || globalThis.document.createElement("link");
+ link.dataset.externalAppFavicon = "true";
+ link.rel = "icon";
+ link.href = logoUrl;
+ if (!current) {
+ globalThis.document.head.append(link);
+ }
+}
diff --git a/external-admin/src/layout/ExternalAdminLayout.test.jsx b/external-admin/src/layout/ExternalAdminLayout.test.jsx
index 525e67b..6f158b9 100644
--- a/external-admin/src/layout/ExternalAdminLayout.test.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.test.jsx
@@ -12,6 +12,7 @@ const auth = vi.hoisted(() => ({
account: "fami-manager",
appCode: "fami",
appName: "Fami",
+ logoUrl: "https://media.example.com/apps/fami.png",
capabilities: ["user:list", "room:list"]
}
}));
@@ -24,6 +25,13 @@ describe("ExternalAdminLayout RTL", () => {
beforeEach(() => {
localStorage.setItem(EXTERNAL_LOCALE_STORAGE_KEY, "ar");
viewport.mobile = false;
+ auth.session = {
+ account: "fami-manager",
+ appCode: "fami",
+ appName: "Fami",
+ logoUrl: "https://media.example.com/apps/fami.png",
+ capabilities: ["user:list", "room:list"]
+ };
});
test("opens the mobile navigation from the right in Arabic", async () => {
@@ -40,7 +48,7 @@ describe("ExternalAdminLayout RTL", () => {
});
test("places the desktop navigation drawer on the right for Arabic", () => {
- renderLayout();
+ const view = renderLayout();
expect(document.documentElement).toHaveAttribute("dir", "rtl");
expect(document.documentElement).toHaveAttribute("lang", "ar");
@@ -52,8 +60,37 @@ describe("ExternalAdminLayout RTL", () => {
.find((cssText) => cssText.includes(`.${generatedClass}`));
expect(drawerRule).toContain("right: 0px");
expect(drawerRule).toContain("border-left:");
+ expect(document.querySelector(".external-sidebar-brand img")).toHaveAttribute("src", "https://media.example.com/apps/fami.png");
+ expect(screen.getAllByText("Fami").length).toBeGreaterThan(0);
+ expect(screen.queryByText("إدارة HYApp الخارجية")).not.toBeInTheDocument();
+ expect(document.title).toBe("Fami");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/fami.png");
expect(screen.getAllByText("ملخص الصلاحيات").length).toBeGreaterThan(0);
expect(screen.getByRole("combobox", { name: "اللغة" })).toHaveTextContent("العربية");
+
+ view.unmount();
+ expect(document.title).toBe("Fami");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument();
+ });
+
+ test("uses the App name, logo and title resolved by the authenticated session", () => {
+ auth.session = {
+ ...auth.session,
+ account: "lalu-manager",
+ appCode: "lalu",
+ appName: "Lalu",
+ logoUrl: "https://media.example.com/apps/lalu.png"
+ };
+ const view = renderLayout();
+
+ expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0);
+ expect(document.querySelector(".external-sidebar-brand img")).toHaveAttribute("src", "https://media.example.com/apps/lalu.png");
+ expect(document.title).toBe("Lalu");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/lalu.png");
+
+ view.unmount();
+ expect(document.title).toBe("Fami");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument();
});
});
diff --git a/external-admin/src/pages/LoginPage.jsx b/external-admin/src/pages/LoginPage.jsx
index b58ff84..c9a2747 100644
--- a/external-admin/src/pages/LoginPage.jsx
+++ b/external-admin/src/pages/LoginPage.jsx
@@ -1,8 +1,6 @@
-import LockOutlined from "@mui/icons-material/LockOutlined";
import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined";
import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined";
import Alert from "@mui/material/Alert";
-import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
@@ -62,7 +60,6 @@ export default function LoginPage() {
-
{t("app.title")}
diff --git a/external-admin/src/pages/LoginPage.test.jsx b/external-admin/src/pages/LoginPage.test.jsx
index bac99f1..eb4ed36 100644
--- a/external-admin/src/pages/LoginPage.test.jsx
+++ b/external-admin/src/pages/LoginPage.test.jsx
@@ -20,7 +20,8 @@ describe("LoginPage", () => {
const user = userEvent.setup();
const { container } = renderLogin();
- expect(screen.getByRole("heading", { name: "HYApp External Admin" })).toBeInTheDocument();
+ expect(screen.getByRole("heading", { name: "Fami" })).toBeInTheDocument();
+ expect(container.querySelector("img")).toBeNull();
expect(screen.queryByRole("combobox", { name: "App" })).not.toBeInTheDocument();
await user.type(screen.getByRole("textbox", { name: "External admin account" }), "fami-manager");
await user.type(container.querySelector('input[autocomplete="current-password"]'), "secret-password");
diff --git a/external-admin/src/styles/index.css b/external-admin/src/styles/index.css
index df45aae..d0ed2ac 100644
--- a/external-admin/src/styles/index.css
+++ b/external-admin/src/styles/index.css
@@ -35,8 +35,7 @@ body {
min-width: 140px !important;
}
-.external-login-avatar,
-.external-logo-mark {
+.external-login-avatar {
background: var(--brand-gradient) !important;
color: #fff;
}
@@ -89,14 +88,9 @@ body {
padding: 12px 20px;
}
-.external-logo-mark {
- width: 38px;
- height: 38px;
- display: grid;
- place-items: center;
- border-radius: 10px;
- font-weight: 800;
- box-shadow: var(--brand-shadow);
+.external-sidebar-brand {
+ font-size: 16px;
+ font-weight: 750;
}
.external-nav {
diff --git a/src/shared/ui/AppIdentity.jsx b/src/shared/ui/AppIdentity.jsx
index 475f98a..702622e 100644
--- a/src/shared/ui/AppIdentity.jsx
+++ b/src/shared/ui/AppIdentity.jsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import styles from "./AppIdentity.module.css";
// AppIdentity 是后台中 App 的唯一展示形式:业务页只传目录数据,避免各个下拉、表格和弹窗分别维护 Logo 规则。
-export function AppIdentity({ app, appCode, appName, className = "", logoUrl, size = "medium" }) {
+export function AppIdentity({ app, appCode, appName, className = "", logoUrl, showLogoFallback = true, size = "medium" }) {
// 主后台使用 camelCase,Social BI 的服务端聚合契约使用 snake_case;组件在展示边界统一兼容,业务视图不复制字段转换。
const resolvedCode = String(app?.appCode || app?.app_code || appCode || "").trim();
const resolvedName = String(app?.appName || app?.app_name || appName || resolvedCode || "-").trim();
@@ -10,13 +10,13 @@ export function AppIdentity({ app, appCode, appName, className = "", logoUrl, si
return (
-
+
{resolvedName}
);
}
-function AppLogo({ appCode, appName, logoUrl }) {
+function AppLogo({ appCode, appName, logoUrl, showFallback }) {
const [failedLogoUrl, setFailedLogoUrl] = useState("");
const canRenderImage = Boolean(logoUrl) && failedLogoUrl !== logoUrl;
@@ -32,6 +32,11 @@ function AppLogo({ appCode, appName, logoUrl }) {
);
}
+ if (!showFallback) {
+ // 外管品牌只能展示 apps.logo_url 的真实图片;缺失或加载失败时不伪造一个看似品牌 Logo 的首字母图标。
+ return null;
+ }
+
// 网络失败或历史数据暂无 Logo 时保留稳定占位,不让选择项高度和表格对齐抖动。
const initial =
String(appName || appCode || "?")
diff --git a/src/shared/ui/AppIdentity.test.jsx b/src/shared/ui/AppIdentity.test.jsx
index 913afce..a4d3c88 100644
--- a/src/shared/ui/AppIdentity.test.jsx
+++ b/src/shared/ui/AppIdentity.test.jsx
@@ -29,3 +29,13 @@ test("supports the snake case Social BI app contract", () => {
expect(screen.getByText("Aslan")).toBeInTheDocument();
expect(container.querySelector("img")).toHaveAttribute("src", "https://media.example.com/aslan.png");
});
+
+test("can hide the fallback when a surface must only show a database logo", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector("img")).toBeNull();
+ expect(screen.getByText("Fami")).toBeInTheDocument();
+ expect(screen.queryByText("F", { selector: "span" })).not.toBeInTheDocument();
+});
diff --git a/src/shared/ui/AppJumpConfigField.jsx b/src/shared/ui/AppJumpConfigField.jsx
index 64a3121..f157b27 100644
--- a/src/shared/ui/AppJumpConfigField.jsx
+++ b/src/shared/ui/AppJumpConfigField.jsx
@@ -23,6 +23,7 @@ export function AppJumpConfigField({
onChange,
paramValue,
required = false,
+ showHint = true,
typeLabel = "跳转类型",
typeValue,
}) {
@@ -102,7 +103,7 @@ export function AppJumpConfigField({
>
)}
- {type === "app" ? (
+ {type === "app" && showHint ? (
{messages.hint || "APP 参数会按公共跳转方案生成 JSON;需要特殊参数时可直接编辑 JSON。"}
) : null}