From cada5d8f5e5f4321f9c3440e21bdacc773a28eaf Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 14 Jul 2026 11:29:05 +0800 Subject: [PATCH] Show app logos across admin --- contracts/admin-openapi.json | 142 +++++++++++++++++- finance/src/FinanceApp.jsx | 5 +- finance/src/api.js | 3 +- finance/src/api.test.js | 12 +- .../src/components/FinanceApplicationForm.jsx | 3 +- .../src/components/FinanceApplicationList.jsx | 12 +- finance/src/components/FinanceAuditDialog.jsx | 10 +- .../FinanceCoinSellerExchangeRateDialog.jsx | 3 +- .../FinanceCoinSellerRechargeCreateDialog.jsx | 11 +- .../FinanceCoinSellerRechargeOrderList.jsx | 15 +- .../components/FinanceMyApplicationList.jsx | 12 +- finance/src/components/FinanceOverview.jsx | 3 +- .../components/FinanceRechargeDetailList.jsx | 16 +- .../FinanceWithdrawalApplicationList.jsx | 12 +- src/app/layout/Header.jsx | 3 +- src/features/users/api.ts | 2 +- .../users/components/UserFormDrawer.jsx | 3 +- .../users/components/UserFormDrawer.test.jsx | 2 +- src/shared/api/generated/schema.d.ts | 59 +++++++- src/shared/api/types.ts | 1 + src/shared/ui/AppIdentity.jsx | 45 ++++++ src/shared/ui/AppIdentity.module.css | 52 +++++++ src/shared/ui/AppIdentity.test.jsx | 22 +++ 23 files changed, 410 insertions(+), 38 deletions(-) create mode 100644 src/shared/ui/AppIdentity.jsx create mode 100644 src/shared/ui/AppIdentity.module.css create mode 100644 src/shared/ui/AppIdentity.test.jsx diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index b17e634..4363abe 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -1618,7 +1618,7 @@ "operationId": "listApps", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/AppListResponse" } } } @@ -3945,7 +3945,7 @@ "operationId": "listRechargeApps", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/AppListResponse" } }, "x-permission": "payment-bill:view", @@ -4146,7 +4146,7 @@ "operationId": "getFinanceScope", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/FinanceScopeCatalogResponse" } }, "x-permission": "finance:view", @@ -7636,6 +7636,26 @@ } }, "responses": { + "AppListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseAdminAppList" + } + } + } + }, + "FinanceScopeCatalogResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceScopeCatalog" + } + } + } + }, "H5LinkListResponse": { "description": "OK", "content": { @@ -8214,6 +8234,36 @@ } }, "schemas": { + "ApiResponseAdminAppList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AdminAppList" + } + } + } + ] + }, + "ApiResponseFinanceScopeCatalog": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinanceScopeCatalog" + } + } + } + ] + }, "ApiResponseH5LinkList": { "allOf": [ { @@ -11552,6 +11602,92 @@ } } }, + "AdminApp": { + "type": "object", + "required": ["appCode", "appName", "logoUrl"], + "properties": { + "appId": { + "type": "integer" + }, + "appCode": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "logoUrl": { + "type": "string", + "format": "uri" + }, + "packageName": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "status": { + "type": "string" + }, + "createdAtMs": { + "type": "integer", + "format": "int64" + }, + "updatedAtMs": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminAppList": { + "type": "object", + "required": ["items", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminApp" + } + }, + "total": { + "type": "integer" + } + } + }, + "FinanceScopeCatalog": { + "type": "object", + "required": ["all", "apps", "countries", "regions", "scopes"], + "properties": { + "all": { + "type": "boolean" + }, + "apps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminApp" + } + }, + "countries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "regions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserFinanceScope" + } + } + } + }, "Country": { "type": "object", "required": ["countryCode", "countryId"], diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index 51b25d3..391b309 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -49,6 +49,7 @@ import { } from "./format.js"; import { downloadCsv } from "@/shared/api/download"; import { buildLoginRedirectPath } from "@/features/auth/loginRedirect.js"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; @@ -842,7 +843,7 @@ export function FinanceApp() { type="button" onClick={() => updateRechargeDetailFilters({ appCode: app.appCode, regionId: "" })} > - {app.appName || app.appCode} + ))} @@ -1005,6 +1006,7 @@ export function FinanceApp() { { "fetch", vi.fn(async (url) => { if (String(url).includes("/admin/apps")) { - return new Response(JSON.stringify({ code: 0, data: { items: [{ appCode: "lalu", appName: "lalu" }], total: 1 } })); + return new Response(JSON.stringify({ + code: 0, + data: { + items: [{ appCode: "lalu", appName: "Lalu", logoUrl: "https://media.example.com/lalu.png" }], + total: 1 + } + })); } if (String(url).includes("/applications?")) { return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } })); @@ -65,7 +71,9 @@ test("finance application APIs use generated endpoint paths", async () => { await rejectFinanceWithdrawalApplication(15, { auditRemark: "bad" }); const calls = vi.mocked(fetch).mock.calls; - expect(options.apps).toEqual([{ appCode: "lalu", appName: "lalu" }]); + expect(options.apps).toEqual([ + { appCode: "lalu", appName: "Lalu", logoUrl: "https://media.example.com/lalu.png" } + ]); expect(options.operations.map((item) => item.label)).toEqual([ "增加用户金币", "减少用户金币", diff --git a/finance/src/components/FinanceApplicationForm.jsx b/finance/src/components/FinanceApplicationForm.jsx index eb90bb0..cca1463 100644 --- a/finance/src/components/FinanceApplicationForm.jsx +++ b/finance/src/components/FinanceApplicationForm.jsx @@ -4,6 +4,7 @@ import Button from "@mui/material/Button"; import InputAdornment from "@mui/material/InputAdornment"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx"; export function FinanceApplicationForm({ @@ -47,7 +48,7 @@ export function FinanceApplicationForm({ > {apps.map((app) => ( - {app.appName || app.appCode} + ))} diff --git a/finance/src/components/FinanceApplicationList.jsx b/finance/src/components/FinanceApplicationList.jsx index 16402d5..96ed6d6 100644 --- a/finance/src/components/FinanceApplicationList.jsx +++ b/finance/src/components/FinanceApplicationList.jsx @@ -11,6 +11,7 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import TextField from "@mui/material/TextField"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import Tooltip from "@mui/material/Tooltip"; import { APPLICATION_STATUS } from "../constants.js"; import { formatAmount, formatTime, operationLabel, statusLabel, walletIdentityLabel } from "../format.js"; @@ -34,7 +35,7 @@ export function FinanceApplicationList({ applications, error, filters, loading, 全部 APP {options.apps.map((app) => ( - {app.appName || app.appCode} + ))} @@ -93,7 +94,14 @@ export function FinanceApplicationList({ applications, error, filters, loading, {items.length ? ( items.map((item) => ( - {item.appName || item.appCode || "-"} + + app.appCode === item.appCode)} + appCode={item.appCode} + appName={item.appName} + size="small" + /> + diff --git a/finance/src/components/FinanceAuditDialog.jsx b/finance/src/components/FinanceAuditDialog.jsx index 2924857..07ccb6f 100644 --- a/finance/src/components/FinanceAuditDialog.jsx +++ b/finance/src/components/FinanceAuditDialog.jsx @@ -5,10 +5,11 @@ import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; import TextField from "@mui/material/TextField"; import { useEffect, useState } from "react"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { operationLabel } from "../format.js"; -export function FinanceAuditDialog({ application, loading, onClose, onSubmit, open, operations, requireApprovalImage = false, requireRejectRemark = false }) { +export function FinanceAuditDialog({ application, apps = [], loading, onClose, onSubmit, open, operations, requireApprovalImage = false, requireRejectRemark = false }) { const [remark, setRemark] = useState(""); const [auditImageUrl, setAuditImageUrl] = useState(""); const decision = application?.decision || "approved"; @@ -32,7 +33,12 @@ export function FinanceAuditDialog({ application, loading, onClose, onSubmit, op {decision === "approved" ? "通过申请" : "拒绝申请"}
- {application?.appName || application?.appCode || "-"} + app.appCode === application?.appCode)} + appCode={application?.appCode} + appName={application?.appName} + size="small" + /> {operationLabel(application?.operation, operations)} {application?.targetUserId || "-"}
diff --git a/finance/src/components/FinanceCoinSellerExchangeRateDialog.jsx b/finance/src/components/FinanceCoinSellerExchangeRateDialog.jsx index b229dd1..6e644c2 100644 --- a/finance/src/components/FinanceCoinSellerExchangeRateDialog.jsx +++ b/finance/src/components/FinanceCoinSellerExchangeRateDialog.jsx @@ -8,6 +8,7 @@ import DialogTitle from "@mui/material/DialogTitle"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { useEffect, useMemo, useRef, useState } from "react"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { buildCoinSellerRechargeExchangeRatePayload, validateCoinSellerRechargeExchangeRatePayload, @@ -123,7 +124,7 @@ export function FinanceCoinSellerExchangeRateDialog({ apps, initialAppCode, open > {apps.map((app) => ( - {app.appName || app.appCode} + ))} diff --git a/finance/src/components/FinanceCoinSellerRechargeCreateDialog.jsx b/finance/src/components/FinanceCoinSellerRechargeCreateDialog.jsx index c144b79..8709392 100644 --- a/finance/src/components/FinanceCoinSellerRechargeCreateDialog.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeCreateDialog.jsx @@ -13,6 +13,7 @@ import DialogTitle from "@mui/material/DialogTitle"; import InputAdornment from "@mui/material/InputAdornment"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { DateTimePicker } from "@/shared/ui/DateTimePicker.jsx"; import { COIN_SELLER_RECHARGE_CHAINS, @@ -40,7 +41,7 @@ export function FinanceCoinSellerRechargeCreateDialog({ const disabled = loading || receiptLoading; // 财务录单的主按钮只在“金额结果已确定 + 三方凭证已校验”时开放,避免界面优化后弱化原有风控门槛。 const canCreate = receiptVerification?.verified === true && (form.isMakeup || Boolean(quote?.coinAmount)) && !disabled; - const selectedAppName = apps.find((app) => app.appCode === form.appCode)?.appName || form.appCode || "待选择"; + const selectedApp = apps.find((app) => app.appCode === form.appCode); return ( {apps.map((app) => ( - {app.appName || app.appCode} + ))} @@ -242,7 +243,7 @@ export function FinanceCoinSellerRechargeCreateDialog({ value === form.providerCode)?.[1] || "待选择"; @@ -316,7 +317,7 @@ function OrderPreview({ appName, form, quote, quoteLoading, receiptVerification {quoteLoading ? "计算中" : formatAmount(coinAmount)}
-
APP
{appName}
+
APP
目标用户
{form.targetUserId || "待填写"}
订单时间
{formatTime(form.orderDateMs) || "待选择"}
支付渠道
{providerLabel}
diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx index ab24cef..0478152 100644 --- a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx @@ -7,6 +7,7 @@ import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { useEffect, useState } from "react"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { COIN_SELLER_RECHARGE_PROVIDERS, COIN_SELLER_RECHARGE_STATUS, @@ -237,7 +238,7 @@ export function FinanceCoinSellerRechargeOrderList({ 全部 APP {apps.map((app) => ( - {app.appName || app.appCode} + ))} @@ -323,7 +324,13 @@ export function FinanceCoinSellerRechargeOrderList({ items.map((item) => ( {item.id || "-"} - {appName(item.appCode, apps)} + + app.appCode === item.appCode)} + appCode={item.appCode} + size="small" + /> + app.appCode === appCode)?.appName || appCode || "-"; -} - function usdText(item) { if (item.usdAmount !== undefined && item.usdAmount !== null && item.usdAmount !== "") { return `USD ${item.usdAmount}`; diff --git a/finance/src/components/FinanceMyApplicationList.jsx b/finance/src/components/FinanceMyApplicationList.jsx index a65cf7d..9de5b4f 100644 --- a/finance/src/components/FinanceMyApplicationList.jsx +++ b/finance/src/components/FinanceMyApplicationList.jsx @@ -9,6 +9,7 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import TextField from "@mui/material/TextField"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { APPLICATION_STATUS } from "../constants.js"; import { formatAmount, formatTime, operationLabel, statusLabel, walletIdentityLabel } from "../format.js"; @@ -26,7 +27,7 @@ export function FinanceMyApplicationList({ applications, error, filters, loading 全部 APP {options.apps.map((app) => ( - {app.appName || app.appCode} + ))} @@ -77,7 +78,14 @@ export function FinanceMyApplicationList({ applications, error, filters, loading {items.length ? ( items.map((item) => ( - {item.appName || item.appCode || "-"} + + app.appCode === item.appCode)} + appCode={item.appCode} + appName={item.appName} + size="small" + /> + diff --git a/finance/src/components/FinanceOverview.jsx b/finance/src/components/FinanceOverview.jsx index 4300bd2..e8119eb 100644 --- a/finance/src/components/FinanceOverview.jsx +++ b/finance/src/components/FinanceOverview.jsx @@ -1,3 +1,4 @@ +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { EMPTY_RECHARGE_OVERVIEW, EMPTY_RECHARGE_SUMMARY, formatAmount, formatUsdMinor } from "../format.js"; const KPI_CARDS = [ @@ -166,7 +167,7 @@ export function FinanceOverview({ onClick={() => onOpenApp(app.appCode)} > - {app.appName || app.appCode} + {formatUsdMinor(appTotal, "USD")} {formatAmount(app.summary?.total?.billCount || 0)} diff --git a/finance/src/components/FinanceRechargeDetailList.jsx b/finance/src/components/FinanceRechargeDetailList.jsx index 96f65af..af835a1 100644 --- a/finance/src/components/FinanceRechargeDetailList.jsx +++ b/finance/src/components/FinanceRechargeDetailList.jsx @@ -5,6 +5,7 @@ import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { Fragment, useState } from "react"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { formatAmount, formatMicroMoney, @@ -60,10 +61,15 @@ export function FinanceRechargeDetailList({ .map((item) => item.transactionId) .filter(Boolean); const columnCount = showCoins ? 9 : 8; + const selectedApp = (apps || []).find((app) => app.appCode === filters.appCode); const chips = [ filters.appCode - ? { key: "appCode", label: `App:${appName(filters.appCode, apps)}`, reset: { appCode: "", regionId: "" } } + ? { + key: "appCode", + label: , + reset: { appCode: "", regionId: "" }, + } : null, filters.regionId ? { @@ -200,7 +206,13 @@ export function FinanceRechargeDetailList({ onClick={() => setExpandedId(expanded ? "" : rowId)} > {formatTime(item.createdAtMs)} - {appName(item.appCode, apps)} + + app.appCode === item.appCode)} + appCode={item.appCode} + size="small" + /> + 全部 APP {options.apps.map((app) => ( - {app.appName || app.appCode} + ))} @@ -60,7 +61,14 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error {items.length ? ( items.map((item) => ( - {item.appName || item.appCode || "-"} + + app.appCode === item.appCode)} + appCode={item.appCode} + appName={item.appName} + size="small" + /> + {item.userId || "-"} $ {formatAmount(item.withdrawAmount)} {item.withdrawMethod || "-"} diff --git a/src/app/layout/Header.jsx b/src/app/layout/Header.jsx index 3e02f5f..f2d083a 100644 --- a/src/app/layout/Header.jsx +++ b/src/app/layout/Header.jsx @@ -16,6 +16,7 @@ import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx"; import { useAuth } from "@/app/auth/AuthProvider.jsx"; import { useTimeZone } from "@/app/timezone/TimeZoneProvider.jsx"; import { changePassword } from "@/features/auth/api"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; @@ -164,7 +165,7 @@ export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefr {appScope.apps.length ? ( appScope.apps.map((app) => ( - {app.appName || app.appCode} + )) ) : ( diff --git a/src/features/users/api.ts b/src/features/users/api.ts index f28583a..5adb20a 100644 --- a/src/features/users/api.ts +++ b/src/features/users/api.ts @@ -67,7 +67,7 @@ export function exportUsers(query?: PageQuery): Promise { export interface FinanceScopeCatalogDto { all?: boolean; - apps?: Array<{ appCode: string; appName?: string }>; + apps?: Array<{ appCode: string; appName?: string; logoUrl?: string }>; regions?: Array<{ appCode: string; countries?: string[]; name?: string; regionCode?: string; regionId: number }>; countries?: Array<{ appCode: string; countryCode: string; countryDisplayName?: string; countryName?: string; regionId?: number }>; scopes?: UserFinanceScopeDto[]; diff --git a/src/features/users/components/UserFormDrawer.jsx b/src/features/users/components/UserFormDrawer.jsx index 2cb7380..ea19dda 100644 --- a/src/features/users/components/UserFormDrawer.jsx +++ b/src/features/users/components/UserFormDrawer.jsx @@ -3,6 +3,7 @@ import MenuItem from "@mui/material/MenuItem"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import TextField from "@mui/material/TextField"; import { Button } from "@/shared/ui/Button.jsx"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; export function UserFormDrawer({ editingUser, @@ -115,7 +116,7 @@ function FinanceScopeGroup({ app, checkedScopes, onChecked, regions }) { return (
- {app.appName || appCode} +