主播相关页面改动
This commit is contained in:
parent
0f92efea87
commit
58be420ae9
@ -178,7 +178,6 @@ export function HostAgenciesPage() {
|
|||||||
<TextField
|
<TextField
|
||||||
disabled={createDisabled}
|
disabled={createDisabled}
|
||||||
label="上级 BD 短 ID"
|
label="上级 BD 短 ID"
|
||||||
required
|
|
||||||
type="number"
|
type="number"
|
||||||
value={page.agencyForm.parentBdUserId}
|
value={page.agencyForm.parentBdUserId}
|
||||||
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })}
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })}
|
||||||
|
|||||||
@ -153,7 +153,6 @@ export function HostBdsPage() {
|
|||||||
<TextField
|
<TextField
|
||||||
disabled={createDisabled}
|
disabled={createDisabled}
|
||||||
label="上级 Leader 短 ID"
|
label="上级 Leader 短 ID"
|
||||||
required
|
|
||||||
type="number"
|
type="number"
|
||||||
value={page.bdForm.parentLeaderUserId}
|
value={page.bdForm.parentLeaderUserId}
|
||||||
onChange={(event) => page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })}
|
onChange={(event) => page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
||||||
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
@ -361,7 +361,7 @@ export function HostCoinSellersPage() {
|
|||||||
label="删除区间"
|
label="删除区间"
|
||||||
onClick={() => page.removeRateTier(index)}
|
onClick={() => page.removeRateTier(index)}
|
||||||
>
|
>
|
||||||
<DeleteOutline fontSize="small" />
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -21,6 +21,12 @@ const hostColumns = [
|
|||||||
render: (item) => <HostStatusSwitch item={item} />,
|
render: (item) => <HostStatusSwitch item={item} />,
|
||||||
width: "minmax(90px, 0.7fr)",
|
width: "minmax(90px, 0.7fr)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "diamond",
|
||||||
|
label: "钻石",
|
||||||
|
render: (item) => formatNumber(item.diamond),
|
||||||
|
width: "minmax(110px, 0.7fr)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "regionId",
|
key: "regionId",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
@ -114,7 +120,7 @@ export function HostHostsPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
context={{ regionOptions: page.regionOptions }}
|
context={{ regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1240px"
|
minWidth="1350px"
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
? {
|
? {
|
||||||
@ -134,6 +140,10 @@ export function HostHostsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
return Number(value || 0).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
function HostUser({ item }) {
|
function HostUser({ item }) {
|
||||||
return (
|
return (
|
||||||
<HostOrgPerson
|
<HostOrgPerson
|
||||||
|
|||||||
@ -12,6 +12,15 @@ const commandContactSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
||||||
|
const optionalUserIdSchema = z.preprocess((value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.trim() === "") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}, z.coerce.number().int().min(0, "请输入有效用户短 ID"));
|
||||||
const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
|
const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
|
||||||
const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0);
|
const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0);
|
||||||
const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
||||||
@ -62,7 +71,7 @@ export const createBDLeaderSchema = commandContactSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createBDSchema = commandBaseSchema.extend({
|
export const createBDSchema = commandBaseSchema.extend({
|
||||||
parentLeaderUserId: userIdSchema,
|
parentLeaderUserId: optionalUserIdSchema,
|
||||||
targetUserId: userIdSchema,
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +115,7 @@ export const createAgencySchema = commandBaseSchema.extend({
|
|||||||
maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0),
|
maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0),
|
||||||
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
||||||
ownerUserId: userIdSchema,
|
ownerUserId: userIdSchema,
|
||||||
parentBdUserId: userIdSchema,
|
parentBdUserId: optionalUserIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const agencyCloseSchema = commandBaseSchema.extend({
|
export const agencyCloseSchema = commandBaseSchema.extend({
|
||||||
|
|||||||
@ -1,16 +1,28 @@
|
|||||||
import { useMemo, useState } from "react";
|
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
|
||||||
|
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
|
||||||
|
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { listRechargeBills } from "@/features/payment/api";
|
import { listRechargeBills } from "@/features/payment/api";
|
||||||
import {
|
import {
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
|
AdminListToolbar,
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
|
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||||
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
import styles from "@/features/payment/payment.module.css";
|
||||||
|
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
|
const exportPageSize = 100;
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
["", "全部状态"],
|
["", "全部状态"],
|
||||||
["succeeded", "成功"],
|
["succeeded", "成功"],
|
||||||
@ -24,20 +36,20 @@ const columns = [
|
|||||||
{
|
{
|
||||||
key: "bill",
|
key: "bill",
|
||||||
label: "账单",
|
label: "账单",
|
||||||
width: "minmax(240px, 1.4fr)",
|
width: "minmax(82px, 0.35fr)",
|
||||||
render: (bill) => <Stack primary={bill.transactionId} secondary={bill.commandId || bill.externalRef || "-"} />,
|
render: (bill) => <BillCopyButton value={bill.transactionId} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "userId",
|
key: "userId",
|
||||||
label: "用户 ID",
|
label: "用户 ID",
|
||||||
width: "minmax(120px, 0.65fr)",
|
width: "minmax(190px, 1fr)",
|
||||||
render: (bill) => bill.userId || "-",
|
render: (bill) => <BillUser bill={bill} type="user" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "sellerUserId",
|
key: "sellerUserId",
|
||||||
label: "币商 ID",
|
label: "币商 ID",
|
||||||
width: "minmax(120px, 0.65fr)",
|
width: "minmax(190px, 1fr)",
|
||||||
render: (bill) => bill.sellerUserId || "-",
|
render: (bill) => <BillUser bill={bill} type="seller" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
@ -65,10 +77,10 @@ const columns = [
|
|||||||
key: "region",
|
key: "region",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
width: "minmax(110px, 0.55fr)",
|
width: "minmax(110px, 0.55fr)",
|
||||||
render: (bill) => (
|
render: (bill, _index, context) => (
|
||||||
<Stack
|
<Stack
|
||||||
primary={bill.targetRegionId ? `区域 ${bill.targetRegionId}` : "-"}
|
primary={regionName(bill.targetRegionId, context?.regionNames)}
|
||||||
secondary={bill.sellerRegionId ? `来源 ${bill.sellerRegionId}` : "-"}
|
secondary={bill.sellerRegionId ? `来源 ${regionName(bill.sellerRegionId, context?.regionNames)}` : "-"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -99,16 +111,23 @@ export function PaymentBillListPage() {
|
|||||||
const [rechargeType, setRechargeType] = useState("");
|
const [rechargeType, setRechargeType] = useState("");
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [sellerUserId, setSellerUserId] = useState("");
|
const [sellerUserId, setSellerUserId] = useState("");
|
||||||
|
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const { regionOptions } = useRegionOptions();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const regionNames = useMemo(() => regionNameMap(regionOptions), [regionOptions]);
|
||||||
|
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
end_at_ms: timeRange.endMs,
|
||||||
keyword,
|
keyword,
|
||||||
recharge_type: rechargeType,
|
recharge_type: rechargeType,
|
||||||
seller_user_id: sellerUserId,
|
seller_user_id: sellerUserId,
|
||||||
|
start_at_ms: timeRange.startMs,
|
||||||
status,
|
status,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
}),
|
}),
|
||||||
[keyword, rechargeType, sellerUserId, status, userId],
|
[keyword, rechargeType, sellerUserId, status, timeRange.endMs, timeRange.startMs, userId],
|
||||||
);
|
);
|
||||||
const query = usePaginatedQuery({
|
const query = usePaginatedQuery({
|
||||||
errorMessage: "获取账单列表失败",
|
errorMessage: "获取账单列表失败",
|
||||||
@ -126,6 +145,22 @@ export function PaymentBillListPage() {
|
|||||||
setter(value);
|
setter(value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
const changeTimeRange = (value) => {
|
||||||
|
setTimeRange(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
const downloadBills = async () => {
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const bills = await fetchAllRechargeBills(filters);
|
||||||
|
downloadTextFile(createBillsCsv(bills, regionNames), `hyapp-payment-bills-${filenameTimestamp()}.csv`);
|
||||||
|
showToast(`已导出 ${bills.length} 条账单`, "success");
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err?.message || "导出账单失败", "error");
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tableColumns = columns.map((column) => {
|
const tableColumns = columns.map((column) => {
|
||||||
if (column.key === "bill") {
|
if (column.key === "bill") {
|
||||||
@ -185,12 +220,32 @@ export function PaymentBillListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
|
<AdminListToolbar
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
disabled={exporting}
|
||||||
|
startIcon={<DownloadOutlined fontSize="small" />}
|
||||||
|
onClick={downloadBills}
|
||||||
|
>
|
||||||
|
{exporting ? "导出中" : "导出"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
filters={
|
||||||
|
<TimeRangeFilter
|
||||||
|
disabled={exporting}
|
||||||
|
label="创建时间"
|
||||||
|
value={timeRange}
|
||||||
|
onChange={changeTimeRange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
|
context={{ regionNames }}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1260px"
|
minWidth="1380px"
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
? {
|
? {
|
||||||
@ -218,6 +273,58 @@ function Stack({ primary, secondary }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BillCopyButton({ value }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!copied) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const timer = window.setTimeout(() => setCopied(false), 1200);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
const copyBill = async () => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await copyText(String(value));
|
||||||
|
setCopied(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip arrow title={copied ? "已复制" : "复制账单"}>
|
||||||
|
<span className={styles.copyWrap}>
|
||||||
|
<IconButton disabled={!value} label="复制账单" onClick={copyBill}>
|
||||||
|
<ContentCopyOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BillUser({ bill, type }) {
|
||||||
|
const fallbackId = type === "seller" ? bill.sellerUserId : bill.userId;
|
||||||
|
const profile = normalizeBillUser(type === "seller" ? bill.seller : bill.user, fallbackId);
|
||||||
|
if (!profile.userId && !profile.displayUserId && !profile.username) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortId = profile.displayUserId || profile.userId || "-";
|
||||||
|
const name = profile.username || "-";
|
||||||
|
return (
|
||||||
|
<div className={styles.identity}>
|
||||||
|
<span className={styles.avatar}>
|
||||||
|
{profile.avatar ? <img alt="" src={profile.avatar} /> : <PersonOutlineOutlined fontSize="small" />}
|
||||||
|
</span>
|
||||||
|
<span className={styles.identityText}>
|
||||||
|
<span className={styles.identityId}>{shortId}</span>
|
||||||
|
<span className={styles.identityName}>{name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StatusBadge({ status }) {
|
function StatusBadge({ status }) {
|
||||||
const tone = status === "succeeded" ? "succeeded" : status === "failed" ? "danger" : "stopped";
|
const tone = status === "succeeded" ? "succeeded" : status === "failed" ? "danger" : "stopped";
|
||||||
return (
|
return (
|
||||||
@ -259,6 +366,151 @@ function formatMoney(value, currency = "USD") {
|
|||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBillUser(profile, fallbackId) {
|
||||||
|
const source = profile || {};
|
||||||
|
return {
|
||||||
|
avatar: source.avatar || "",
|
||||||
|
displayUserId: source.displayUserId || source.display_user_id || "",
|
||||||
|
userId: String(source.userId || source.user_id || fallbackId || ""),
|
||||||
|
username: source.username || source.name || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionNameMap(regionOptions = []) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
regionOptions.map((region) => [String(region.regionId || region.value), region.name || region.label]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionName(regionId, regionNames = {}) {
|
||||||
|
if (!regionId) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return regionNames[String(regionId)] || `区域 ${regionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllRechargeBills(filters) {
|
||||||
|
const items = [];
|
||||||
|
let page = 1;
|
||||||
|
while (true) {
|
||||||
|
const data = await listRechargeBills({ ...filters, page, page_size: exportPageSize });
|
||||||
|
const batch = Array.isArray(data?.items) ? data.items : [];
|
||||||
|
items.push(...batch);
|
||||||
|
|
||||||
|
const total = Number(data?.total || 0);
|
||||||
|
if (!batch.length || (total > 0 && items.length >= total) || batch.length < exportPageSize) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBillsCsv(bills, regionNames) {
|
||||||
|
const headers = [
|
||||||
|
"账单",
|
||||||
|
"命令号",
|
||||||
|
"外部单号",
|
||||||
|
"用户长ID",
|
||||||
|
"用户短ID",
|
||||||
|
"用户名称",
|
||||||
|
"币商长ID",
|
||||||
|
"币商短ID",
|
||||||
|
"币商名称",
|
||||||
|
"金币",
|
||||||
|
"金额",
|
||||||
|
"兑换政策",
|
||||||
|
"兑换金币",
|
||||||
|
"兑换金额",
|
||||||
|
"区域",
|
||||||
|
"来源区域",
|
||||||
|
"充值途径",
|
||||||
|
"状态",
|
||||||
|
"创建时间",
|
||||||
|
];
|
||||||
|
const rows = bills.map((bill) => {
|
||||||
|
const user = normalizeBillUser(bill.user, bill.userId);
|
||||||
|
const seller = normalizeBillUser(bill.seller, bill.sellerUserId);
|
||||||
|
return [
|
||||||
|
bill.transactionId,
|
||||||
|
bill.commandId,
|
||||||
|
bill.externalRef,
|
||||||
|
user.userId,
|
||||||
|
user.displayUserId,
|
||||||
|
user.username,
|
||||||
|
seller.userId,
|
||||||
|
seller.displayUserId,
|
||||||
|
seller.username,
|
||||||
|
formatNumber(bill.coinAmount),
|
||||||
|
formatMoney(bill.usdMinorAmount, bill.currencyCode),
|
||||||
|
bill.policyVersion,
|
||||||
|
formatNumber(bill.exchangeCoinAmount),
|
||||||
|
formatMoney(bill.exchangeUsdMinorAmount, bill.currencyCode),
|
||||||
|
regionName(bill.targetRegionId, regionNames),
|
||||||
|
bill.sellerRegionId ? regionName(bill.sellerRegionId, regionNames) : "",
|
||||||
|
rechargeTypeLabel(bill.rechargeType),
|
||||||
|
statusLabel(bill.status),
|
||||||
|
formatMillis(bill.createdAtMs),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return [headers, ...rows].map((row) => row.map(csvCell).join(",")).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvCell(value) {
|
||||||
|
const text = value === 0 || value ? String(value) : "";
|
||||||
|
if (/[",\n\r]/.test(text)) {
|
||||||
|
return `"${text.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTextFile(content, filename) {
|
||||||
|
const blob = new Blob([`\uFEFF${content}`], { type: "text/csv;charset=utf-8" });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyText(value) {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// 浏览器可能因权限或非安全上下文拒绝 Clipboard API,这里退回同步复制。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.createElement("textarea");
|
||||||
|
input.value = value;
|
||||||
|
input.setAttribute("readonly", "");
|
||||||
|
input.style.position = "fixed";
|
||||||
|
input.style.opacity = "0";
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
input.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filenameTimestamp() {
|
||||||
|
const now = new Date();
|
||||||
|
return [
|
||||||
|
now.getFullYear(),
|
||||||
|
pad2(now.getMonth() + 1),
|
||||||
|
pad2(now.getDate()),
|
||||||
|
"-",
|
||||||
|
pad2(now.getHours()),
|
||||||
|
pad2(now.getMinutes()),
|
||||||
|
pad2(now.getSeconds()),
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad2(value) {
|
||||||
|
return String(value).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
function digitsOnly(value) {
|
function digitsOnly(value) {
|
||||||
return value.replace(/\D/g, "");
|
return value.replace(/\D/g, "");
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/features/payment/payment.module.css
Normal file
56
src/features/payment/payment.module.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
.copyWrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityText {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityId,
|
||||||
|
.identityName {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityId {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityName {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
}
|
||||||
@ -300,7 +300,16 @@ export interface RechargeBillDto {
|
|||||||
targetRegionId?: number;
|
targetRegionId?: number;
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
usdMinorAmount: number;
|
usdMinorAmount: number;
|
||||||
|
user?: RechargeBillUserDto;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
seller?: RechargeBillUserDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RechargeBillUserDto {
|
||||||
|
avatar?: string;
|
||||||
|
displayUserId?: string;
|
||||||
|
userId?: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RechargeProductDto {
|
export interface RechargeProductDto {
|
||||||
@ -410,6 +419,7 @@ export interface HostProfileDto {
|
|||||||
currentAgencyId?: number;
|
currentAgencyId?: number;
|
||||||
currentAgencyName?: string;
|
currentAgencyName?: string;
|
||||||
currentMembershipId?: number;
|
currentMembershipId?: number;
|
||||||
|
diamond?: number;
|
||||||
displayUserId?: string;
|
displayUserId?: string;
|
||||||
firstBecameHostAtMs?: number;
|
firstBecameHostAtMs?: number;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user