Show app logos across admin
This commit is contained in:
parent
6d237766f9
commit
cada5d8f5e
@ -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"],
|
||||
|
||||
@ -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}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -1005,6 +1006,7 @@ export function FinanceApp() {
|
||||
</Dialog>
|
||||
<FinanceAuditDialog
|
||||
application={auditTarget}
|
||||
apps={optionsState.data.apps}
|
||||
loading={auditLoading}
|
||||
operations={optionsState.data.operations}
|
||||
open={Boolean(auditTarget)}
|
||||
@ -1013,6 +1015,7 @@ export function FinanceApp() {
|
||||
/>
|
||||
<FinanceAuditDialog
|
||||
application={withdrawalAuditTarget}
|
||||
apps={optionsState.data.apps}
|
||||
loading={withdrawalAuditLoading}
|
||||
operations={optionsState.data.operations}
|
||||
open={Boolean(withdrawalAuditTarget)}
|
||||
|
||||
@ -380,7 +380,8 @@ function normalizeApps(data) {
|
||||
}
|
||||
return {
|
||||
appCode,
|
||||
appName: stringValue(item.appName ?? item.app_name ?? item.name) || appCode
|
||||
appName: stringValue(item.appName ?? item.app_name ?? item.name) || appCode,
|
||||
logoUrl: stringValue(item.logoUrl ?? item.logo_url)
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
@ -33,7 +33,13 @@ test("finance application APIs use generated endpoint paths", async () => {
|
||||
"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([
|
||||
"增加用户金币",
|
||||
"减少用户金币",
|
||||
|
||||
@ -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) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
@ -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,
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@ -93,7 +94,14 @@ export function FinanceApplicationList({ applications, error, filters, loading,
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.appName || item.appCode || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<AppIdentity
|
||||
app={options.apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
appName={item.appName}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={operationLabel(item.operation, operations)} secondary={walletIdentityLabel(item.walletIdentity)} />
|
||||
</TableCell>
|
||||
|
||||
@ -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
|
||||
<DialogTitle>{decision === "approved" ? "通过申请" : "拒绝申请"}</DialogTitle>
|
||||
<DialogContent className="finance-audit-dialog">
|
||||
<div className="finance-audit-summary">
|
||||
<span>{application?.appName || application?.appCode || "-"}</span>
|
||||
<AppIdentity
|
||||
app={apps.find((app) => app.appCode === application?.appCode)}
|
||||
appCode={application?.appCode}
|
||||
appName={application?.appName}
|
||||
size="small"
|
||||
/>
|
||||
<strong>{operationLabel(application?.operation, operations)}</strong>
|
||||
<span>{application?.targetUserId || "-"}</span>
|
||||
</div>
|
||||
|
||||
@ -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) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
@ -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 (
|
||||
<Dialog
|
||||
@ -77,7 +78,7 @@ export function FinanceCoinSellerRechargeCreateDialog({
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@ -242,7 +243,7 @@ export function FinanceCoinSellerRechargeCreateDialog({
|
||||
</div>
|
||||
|
||||
<OrderPreview
|
||||
appName={selectedAppName}
|
||||
app={selectedApp}
|
||||
form={form}
|
||||
quote={quote}
|
||||
quoteLoading={quoteLoading}
|
||||
@ -296,7 +297,7 @@ function CoinQuoteResult({ form, quote, quoteError, quoteLoading }) {
|
||||
);
|
||||
}
|
||||
|
||||
function OrderPreview({ appName, form, quote, quoteLoading, receiptVerification }) {
|
||||
function OrderPreview({ app, form, quote, quoteLoading, receiptVerification }) {
|
||||
const usdAmount = Number(form.usdAmount || 0);
|
||||
const coinAmount = form.isMakeup ? 0 : Number(quote?.coinAmount || 0);
|
||||
const providerLabel = COIN_SELLER_RECHARGE_PROVIDERS.find(([value]) => value === form.providerCode)?.[1] || "待选择";
|
||||
@ -316,7 +317,7 @@ function OrderPreview({ appName, form, quote, quoteLoading, receiptVerification
|
||||
<strong>{quoteLoading ? "计算中" : formatAmount(coinAmount)}</strong>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>APP</dt><dd>{appName}</dd></div>
|
||||
<div><dt>APP</dt><dd><AppIdentity app={app} appCode={form.appCode} size="small" /></dd></div>
|
||||
<div><dt>目标用户</dt><dd>{form.targetUserId || "待填写"}</dd></div>
|
||||
<div><dt>订单时间</dt><dd>{formatTime(form.orderDateMs) || "待选择"}</dd></div>
|
||||
<div><dt>支付渠道</dt><dd>{providerLabel}</dd></div>
|
||||
|
||||
@ -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({
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@ -323,7 +324,13 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
items.map((item) => (
|
||||
<tr className="finance-flat-table__row" key={item.id}>
|
||||
<td className="finance-order-cell">{item.id || "-"}</td>
|
||||
<td>{appName(item.appCode, apps)}</td>
|
||||
<td>
|
||||
<AppIdentity
|
||||
app={apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
size="small"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Stacked
|
||||
primary={item.targetDisplayUserId || item.targetUserId}
|
||||
@ -509,10 +516,6 @@ function Stacked({ primary, secondary }) {
|
||||
);
|
||||
}
|
||||
|
||||
function appName(appCode, apps) {
|
||||
return apps.find((app) => app.appCode === appCode)?.appName || appCode || "-";
|
||||
}
|
||||
|
||||
function usdText(item) {
|
||||
if (item.usdAmount !== undefined && item.usdAmount !== null && item.usdAmount !== "") {
|
||||
return `USD ${item.usdAmount}`;
|
||||
|
||||
@ -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
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@ -77,7 +78,14 @@ export function FinanceMyApplicationList({ applications, error, filters, loading
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.appName || item.appCode || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<AppIdentity
|
||||
app={options.apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
appName={item.appName}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={operationLabel(item.operation, operations)} secondary={walletIdentityLabel(item.walletIdentity)} />
|
||||
</TableCell>
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
<td>
|
||||
<b>{app.appName || app.appCode}</b>
|
||||
<AppIdentity app={app} size="small" />
|
||||
</td>
|
||||
<td className="r num">{formatUsdMinor(appTotal, "USD")}</td>
|
||||
<td className="r num">{formatAmount(app.summary?.total?.billCount || 0)}</td>
|
||||
|
||||
@ -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: <AppIdentity app={selectedApp} appCode={filters.appCode} size="small" />,
|
||||
reset: { appCode: "", regionId: "" },
|
||||
}
|
||||
: null,
|
||||
filters.regionId
|
||||
? {
|
||||
@ -200,7 +206,13 @@ export function FinanceRechargeDetailList({
|
||||
onClick={() => setExpandedId(expanded ? "" : rowId)}
|
||||
>
|
||||
<td className="num">{formatTime(item.createdAtMs)}</td>
|
||||
<td>{appName(item.appCode, apps)}</td>
|
||||
<td>
|
||||
<AppIdentity
|
||||
app={(apps || []).find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
size="small"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`finance-badge finance-badge--${badgeTone(item.rechargeType)}`}
|
||||
|
||||
@ -8,6 +8,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 { formatAmount, formatTime, statusLabel } from "../format.js";
|
||||
|
||||
export function FinanceWithdrawalApplicationList({ applications, canAudit, error, filters, loading, onAudit, onFiltersChange, onReload, options }) {
|
||||
@ -29,7 +30,7 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@ -60,7 +61,14 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.appName || item.appCode || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<AppIdentity
|
||||
app={options.apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
appName={item.appName}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{item.userId || "-"}</TableCell>
|
||||
<TableCell align="right">$ {formatAmount(item.withdrawAmount)}</TableCell>
|
||||
<TableCell>{item.withdrawMethod || "-"}</TableCell>
|
||||
|
||||
@ -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) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -67,7 +67,7 @@ export function exportUsers(query?: PageQuery): Promise<Response> {
|
||||
|
||||
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[];
|
||||
|
||||
@ -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 (
|
||||
<div className="permission-scope-group">
|
||||
<strong>{app.appName || appCode}</strong>
|
||||
<strong><AppIdentity app={app} size="small" /></strong>
|
||||
<label className="permission-check" key={`${appCode}:0`}>
|
||||
<input checked={isChecked(0)} onChange={(event) => onChecked(appCode, 0, event.target.checked)} type="checkbox" />
|
||||
<span>全部区域</span>
|
||||
|
||||
@ -13,7 +13,7 @@ const baseForm = {
|
||||
};
|
||||
|
||||
const baseFinanceScopeCatalog = {
|
||||
apps: [{ appCode: "Huwaa", appName: "Huwaa" }],
|
||||
apps: [{ appCode: "Huwaa", appName: "Huwaa", logoUrl: "https://media.example.com/huwaa.png" }],
|
||||
error: null,
|
||||
loading: false,
|
||||
regions: [{ appCode: "Huwaa", name: "菲律宾", regionCode: "PHILIPPINES", regionId: 8 }]
|
||||
|
||||
59
src/shared/api/generated/schema.d.ts
vendored
59
src/shared/api/generated/schema.d.ts
vendored
@ -4336,6 +4336,12 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
ApiResponseAdminAppList: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["AdminAppList"];
|
||||
};
|
||||
ApiResponseFinanceScopeCatalog: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["FinanceScopeCatalog"];
|
||||
};
|
||||
ApiResponseH5LinkList: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["H5LinkList"];
|
||||
};
|
||||
@ -5570,6 +5576,35 @@ export interface components {
|
||||
createdAtMs?: number;
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
AdminApp: {
|
||||
appId?: number;
|
||||
appCode: string;
|
||||
appName: string;
|
||||
/** Format: uri */
|
||||
logoUrl: string;
|
||||
packageName?: string;
|
||||
platform?: string;
|
||||
status?: string;
|
||||
/** Format: int64 */
|
||||
createdAtMs?: number;
|
||||
/** Format: int64 */
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
AdminAppList: {
|
||||
items: components["schemas"]["AdminApp"][];
|
||||
total: number;
|
||||
};
|
||||
FinanceScopeCatalog: {
|
||||
all: boolean;
|
||||
apps: components["schemas"]["AdminApp"][];
|
||||
countries: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
regions: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
scopes: components["schemas"]["UserFinanceScope"][];
|
||||
};
|
||||
Country: {
|
||||
countryCode: string;
|
||||
countryDisplayName?: string;
|
||||
@ -5903,6 +5938,24 @@ export interface components {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
AppListResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseAdminAppList"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
FinanceScopeCatalogResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseFinanceScopeCatalog"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
H5LinkListResponse: {
|
||||
headers: {
|
||||
@ -7996,7 +8049,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["AppListResponse"];
|
||||
};
|
||||
};
|
||||
listBDLeaders: {
|
||||
@ -9474,7 +9527,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["AppListResponse"];
|
||||
};
|
||||
};
|
||||
listRechargeProducts: {
|
||||
@ -9630,7 +9683,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["FinanceScopeCatalogResponse"];
|
||||
};
|
||||
};
|
||||
listFinanceApplications: {
|
||||
|
||||
@ -1646,6 +1646,7 @@ export interface AdminAppDto {
|
||||
appId: number;
|
||||
appName: string;
|
||||
createdAtMs?: number;
|
||||
logoUrl: string;
|
||||
packageName?: string;
|
||||
platform?: string;
|
||||
status?: string;
|
||||
|
||||
45
src/shared/ui/AppIdentity.jsx
Normal file
45
src/shared/ui/AppIdentity.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import styles from "./AppIdentity.module.css";
|
||||
|
||||
// AppIdentity 是后台中 App 的唯一展示形式:业务页只传目录数据,避免各个下拉、表格和弹窗分别维护 Logo 规则。
|
||||
export function AppIdentity({ app, appCode, appName, className = "", logoUrl, size = "medium" }) {
|
||||
const resolvedCode = String(app?.appCode || appCode || "").trim();
|
||||
const resolvedName = String(app?.appName || appName || resolvedCode || "-").trim();
|
||||
const resolvedLogoUrl = String(app?.logoUrl || logoUrl || "").trim();
|
||||
|
||||
return (
|
||||
<span className={[styles.root, styles[size] || styles.medium, className].filter(Boolean).join(" ")}>
|
||||
<AppLogo appCode={resolvedCode} appName={resolvedName} logoUrl={resolvedLogoUrl} />
|
||||
<span className={styles.name}>{resolvedName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AppLogo({ appCode, appName, logoUrl }) {
|
||||
const [failedLogoUrl, setFailedLogoUrl] = useState("");
|
||||
const canRenderImage = Boolean(logoUrl) && failedLogoUrl !== logoUrl;
|
||||
|
||||
if (canRenderImage) {
|
||||
return (
|
||||
<img
|
||||
alt=""
|
||||
className={styles.logo}
|
||||
loading="lazy"
|
||||
src={logoUrl}
|
||||
onError={() => setFailedLogoUrl(logoUrl)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 网络失败或历史数据暂无 Logo 时保留稳定占位,不让选择项高度和表格对齐抖动。
|
||||
const initial =
|
||||
String(appName || appCode || "?")
|
||||
.trim()
|
||||
.slice(0, 1)
|
||||
.toUpperCase() || "?";
|
||||
return (
|
||||
<span aria-hidden="true" className={styles.fallback}>
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
52
src/shared/ui/AppIdentity.module.css
Normal file
52
src/shared/ui/AppIdentity.module.css
Normal file
@ -0,0 +1,52 @@
|
||||
.root {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.logo,
|
||||
.fallback {
|
||||
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 10%);
|
||||
flex: 0 0 auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.fallback {
|
||||
align-items: center;
|
||||
background: linear-gradient(145deg, #e8f1ff, #f7faff);
|
||||
color: var(--primary);
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.small .logo,
|
||||
.small .fallback {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.medium .logo,
|
||||
.medium .fallback {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.large .logo,
|
||||
.large .fallback {
|
||||
border-radius: 9px;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
22
src/shared/ui/AppIdentity.test.jsx
Normal file
22
src/shared/ui/AppIdentity.test.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { AppIdentity } from "./AppIdentity.jsx";
|
||||
|
||||
test("renders the server-provided app logo beside the app name", () => {
|
||||
const { container } = render(
|
||||
<AppIdentity app={{ appCode: "lalu", appName: "Lalu", logoUrl: "https://media.example.com/lalu.png" }} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Lalu")).toBeInTheDocument();
|
||||
expect(container.querySelector("img")).toHaveAttribute("src", "https://media.example.com/lalu.png");
|
||||
});
|
||||
|
||||
test("falls back to a stable initial when the logo cannot load", () => {
|
||||
const { container } = render(
|
||||
<AppIdentity app={{ appCode: "lalu", appName: "Lalu", logoUrl: "https://media.example.com/missing.png" }} />,
|
||||
);
|
||||
|
||||
fireEvent.error(container.querySelector("img"));
|
||||
expect(container.querySelector("img")).toBeNull();
|
||||
expect(screen.getByText("L", { selector: "span" })).toBeInTheDocument();
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user