资源发放
This commit is contained in:
parent
c33fb469d9
commit
0112303b88
@ -1,4 +1,5 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||||||
import UndoOutlined from "@mui/icons-material/UndoOutlined";
|
import UndoOutlined from "@mui/icons-material/UndoOutlined";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
@ -17,9 +18,11 @@ import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/t
|
|||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
|
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
|
||||||
import {
|
import {
|
||||||
|
resourceGroupAssetLabels,
|
||||||
resourceGrantStatusFilters,
|
resourceGrantStatusFilters,
|
||||||
resourceGrantStatusLabels,
|
resourceGrantStatusLabels,
|
||||||
resourceGrantSubjectLabels,
|
resourceGrantSubjectLabels,
|
||||||
|
resourceTypeLabels,
|
||||||
} from "@/features/resources/constants.js";
|
} from "@/features/resources/constants.js";
|
||||||
import { useResourceGrantListPage } from "@/features/resources/hooks/useResourcePages.js";
|
import { useResourceGrantListPage } from "@/features/resources/hooks/useResourcePages.js";
|
||||||
import styles from "@/features/resources/resources.module.css";
|
import styles from "@/features/resources/resources.module.css";
|
||||||
@ -27,12 +30,11 @@ import styles from "@/features/resources/resources.module.css";
|
|||||||
const grantColumns = [
|
const grantColumns = [
|
||||||
{
|
{
|
||||||
key: "grant",
|
key: "grant",
|
||||||
label: "赠送记录",
|
label: "来源",
|
||||||
width: "minmax(240px, 1.2fr)",
|
width: "minmax(220px, 1fr)",
|
||||||
render: (grant) => (
|
render: (grant) => (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span className={styles.name}>{grant.grantId || "-"}</span>
|
<span className={styles.name}>{grantSourceLabel(grant.grantSource)}</span>
|
||||||
<span className={styles.meta}>{grant.commandId || "-"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -45,24 +47,19 @@ const grantColumns = [
|
|||||||
{
|
{
|
||||||
key: "operator",
|
key: "operator",
|
||||||
label: "操作人",
|
label: "操作人",
|
||||||
width: "minmax(180px, 1fr)",
|
width: "minmax(190px, 0.9fr)",
|
||||||
render: (grant) => <GrantOperator grant={grant} />,
|
render: (grant) => <GrantOperator grant={grant} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "subject",
|
key: "subject",
|
||||||
label: "对象",
|
label: "发放方式",
|
||||||
width: "minmax(160px, 0.8fr)",
|
width: "minmax(170px, 0.75fr)",
|
||||||
render: (grant) => (
|
render: (grant) => <GrantSubject grant={grant} />,
|
||||||
<div className={styles.stack}>
|
|
||||||
<span>{resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "-"}</span>
|
|
||||||
<span className={styles.meta}>{grant.grantSubjectId || "-"}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "items",
|
key: "items",
|
||||||
label: "发放内容",
|
label: "发放内容",
|
||||||
width: "minmax(180px, 1fr)",
|
width: "minmax(320px, 1.5fr)",
|
||||||
render: (grant) => <GrantItems grant={grant} />,
|
render: (grant) => <GrantItems grant={grant} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -79,8 +76,8 @@ const grantColumns = [
|
|||||||
{
|
{
|
||||||
key: "reason",
|
key: "reason",
|
||||||
label: "原因",
|
label: "原因",
|
||||||
width: "minmax(160px, 1fr)",
|
width: "minmax(190px, 0.95fr)",
|
||||||
render: (grant) => grant.reason || "-",
|
render: (grant) => <GrantReason grant={grant} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "time",
|
key: "time",
|
||||||
@ -143,7 +140,7 @@ export function ResourceGrantListPage() {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1460px"
|
minWidth="1560px"
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
? {
|
? {
|
||||||
@ -347,14 +344,53 @@ function GrantItems({ grant }) {
|
|||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.stack}>
|
<div className={styles.grantMediaScroller}>
|
||||||
{items.slice(0, 2).map((item) => (
|
{items.map((item) => (
|
||||||
<span key={item.grantItemId || `${grant.grantId}-${item.resourceId}`}>
|
<GrantItemMedia item={item} key={item.grantItemId || `${grant.grantId}-${item.resourceId}`} />
|
||||||
{item.resourceId || "-"} / {item.quantity || 0}
|
|
||||||
{item.durationMs ? ` / ${formatDuration(item.durationMs)}` : ""}
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
{items.length > 2 ? <span className={styles.meta}>+{items.length - 2}</span> : null}
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrantItemMedia({ item }) {
|
||||||
|
const snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson);
|
||||||
|
const walletAssetType = snapshotValue(snapshot, "walletAssetType", "wallet_asset_type", "WalletAssetType");
|
||||||
|
if (walletAssetType) {
|
||||||
|
const label = resourceGroupAssetLabels[walletAssetType] || walletAssetType;
|
||||||
|
const amount = snapshotValue(snapshot, "walletAssetAmount", "wallet_asset_amount", "WalletAssetAmount", "quantity");
|
||||||
|
return <span className={styles.grantAssetPill}>{`${label} ${formatNumber(amount || item.quantity)}`}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = grantItemLabel(item, snapshot);
|
||||||
|
const imageUrl = grantItemImageUrl(snapshot);
|
||||||
|
return (
|
||||||
|
<span className={styles.grantMediaCard} title={label}>
|
||||||
|
<span className={styles.grantMediaThumb}>
|
||||||
|
{imageUrl ? <img alt="" src={imageUrl} /> : <Inventory2Outlined fontSize="small" />}
|
||||||
|
</span>
|
||||||
|
<span className={styles.grantMediaName}>{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrantSubject({ grant }) {
|
||||||
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "发放";
|
||||||
|
const sourceLabel = grantSourceLabel(grant.grantSource);
|
||||||
|
return (
|
||||||
|
<div className={styles.stack}>
|
||||||
|
<span className={styles.name}>{sourceLabel === "-" ? subjectLabel : `${subjectLabel} · ${sourceLabel}`}</span>
|
||||||
|
<span className={styles.meta}>{subjectMeta(grant)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrantReason({ grant }) {
|
||||||
|
const label = reasonLabel(grant.reason);
|
||||||
|
const raw = String(grant.reason || "").trim();
|
||||||
|
return (
|
||||||
|
<div className={styles.stack}>
|
||||||
|
<span className={styles.name}>{label}</span>
|
||||||
|
{raw && raw !== label ? <span className={styles.meta}>{raw}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -384,26 +420,115 @@ function GrantOperator({ grant }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (source === "admin") {
|
return "-";
|
||||||
const name = operator.name || operator.username || (operatorUserId ? `Admin ${operatorUserId}` : "-");
|
}
|
||||||
return (
|
|
||||||
<div className={styles.stack}>
|
const grantSourceLabels = {
|
||||||
<span className={styles.name}>{name}</span>
|
achievement: "成就奖励",
|
||||||
<span className={styles.meta}>{operator.username || operatorUserId || "-"}</span>
|
admin: "后台发放",
|
||||||
</div>
|
agency_opening: "Agency 开通",
|
||||||
);
|
cp_weekly_rank: "CP 周榜奖励",
|
||||||
|
cumulative_recharge_reward: "累充奖励",
|
||||||
|
first_recharge_reward: "首充奖励",
|
||||||
|
game_robot_init: "机器人初始化",
|
||||||
|
growth_level: "成长等级奖励",
|
||||||
|
manager_center: "经理中心",
|
||||||
|
registration_reward: "注册奖励",
|
||||||
|
resource_shop: "道具商店",
|
||||||
|
room_rocket: "房间火箭",
|
||||||
|
seven_day_checkin: "七日签到",
|
||||||
|
weekly_star: "周星奖励",
|
||||||
|
wheel_reward: "转盘奖励",
|
||||||
|
};
|
||||||
|
|
||||||
|
const grantReasonLabels = {
|
||||||
|
...grantSourceLabels,
|
||||||
|
growth_level_registration_badge: "注册成长等级徽章",
|
||||||
|
"resource shop purchase": "道具商店购买",
|
||||||
|
};
|
||||||
|
|
||||||
|
function grantSourceLabel(source) {
|
||||||
|
const key = String(source || "").trim();
|
||||||
|
return grantSourceLabels[key] || key || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasonLabel(reason) {
|
||||||
|
const key = String(reason || "").trim();
|
||||||
|
return grantReasonLabels[key] || key || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function subjectMeta(grant) {
|
||||||
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "对象";
|
||||||
|
return grant.grantSubjectId ? `${subjectLabel} ID ${grant.grantSubjectId}` : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function grantItemLabel(item, snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson)) {
|
||||||
|
const walletAssetType = snapshotValue(snapshot, "walletAssetType", "wallet_asset_type", "WalletAssetType");
|
||||||
|
if (walletAssetType) {
|
||||||
|
const label = resourceGroupAssetLabels[walletAssetType] || walletAssetType;
|
||||||
|
const amount = snapshotValue(snapshot, "walletAssetAmount", "wallet_asset_amount", "WalletAssetAmount", "quantity");
|
||||||
|
return `${label} ${formatNumber(amount || item.quantity)}`;
|
||||||
}
|
}
|
||||||
if (!operatorUserId) {
|
|
||||||
return "-";
|
const resourceName = snapshotValue(snapshot, "name", "Name", "resourceName", "resource_name") || `资源 ${item.resourceId || "-"}`;
|
||||||
|
const resourceType = snapshotValue(snapshot, "resourceType", "resource_type", "ResourceType");
|
||||||
|
const typeLabel = resourceTypeLabels[resourceType] || resourceType || "资源";
|
||||||
|
const quantity = Number(item.quantity || 0);
|
||||||
|
const parts = [`${resourceName} x${formatNumber(quantity)}`, typeLabel];
|
||||||
|
if (item.durationMs) {
|
||||||
|
parts.push(formatDuration(item.durationMs));
|
||||||
}
|
}
|
||||||
return (
|
return parts.join(" · ");
|
||||||
<div className={styles.stack}>
|
}
|
||||||
<span className={styles.name}>{operatorUserId}</span>
|
|
||||||
<span className={styles.meta}>{source || "-"}</span>
|
function grantItemImageUrl(snapshot) {
|
||||||
</div>
|
return imageURL(
|
||||||
|
snapshotValue(
|
||||||
|
snapshot,
|
||||||
|
"previewUrl",
|
||||||
|
"preview_url",
|
||||||
|
"PreviewURL",
|
||||||
|
"assetUrl",
|
||||||
|
"asset_url",
|
||||||
|
"AssetURL",
|
||||||
|
"animationUrl",
|
||||||
|
"animation_url",
|
||||||
|
"AnimationURL",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseGrantItemSnapshot(value) {
|
||||||
|
if (!value || typeof value !== "string") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return parsed && typeof parsed === "object" ? parsed : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotValue(snapshot, ...keys) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = snapshot?.[key];
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
return Number(value || 0).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageURL(value) {
|
||||||
|
const url = String(value || "").trim();
|
||||||
|
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
|
||||||
|
}
|
||||||
|
|
||||||
function formatDuration(durationMs) {
|
function formatDuration(durationMs) {
|
||||||
const days = Number(durationMs || 0) / (24 * 60 * 60 * 1000);
|
const days = Number(durationMs || 0) / (24 * 60 * 60 * 1000);
|
||||||
return Number.isInteger(days) ? `${days}天` : `${days.toFixed(1)}天`;
|
return Number.isInteger(days) ? `${days}天` : `${days.toFixed(1)}天`;
|
||||||
|
|||||||
@ -65,6 +65,91 @@ test("resource grant list hides revoke without permission", () => {
|
|||||||
expect(screen.queryByLabelText("撤销")).not.toBeInTheDocument();
|
expect(screen.queryByLabelText("撤销")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resource grant list renders readable source, reason, and granted items", () => {
|
||||||
|
vi.mocked(useResourceGrantListPage).mockReturnValue(
|
||||||
|
pageFixture({
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
grantFixture({
|
||||||
|
grantId: "rgr_1234567890abcdef",
|
||||||
|
commandId: "seven_day_checkin_1782448934991_9b39c38",
|
||||||
|
grantSource: "seven_day_checkin",
|
||||||
|
grantSubjectId: "18",
|
||||||
|
grantSubjectType: "resource_group",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
grantItemId: 1,
|
||||||
|
quantity: 100,
|
||||||
|
resourceId: 0,
|
||||||
|
resourceSnapshotJson: JSON.stringify({
|
||||||
|
wallet_asset_type: "COIN",
|
||||||
|
wallet_asset_amount: 100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
durationMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
grantItemId: 2,
|
||||||
|
quantity: 1,
|
||||||
|
resourceId: 385,
|
||||||
|
resourceSnapshotJson: JSON.stringify({
|
||||||
|
Name: "星光头像框",
|
||||||
|
PreviewURL: "https://media.haiyihy.com/star-frame.png",
|
||||||
|
ResourceType: "avatar_frame",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
operator: { name: "后台用户", source: "admin", userId: "7" },
|
||||||
|
reason: "seven_day_checkin",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<ResourceGrantListPage />);
|
||||||
|
|
||||||
|
expect(screen.getAllByText("七日签到")).toHaveLength(2);
|
||||||
|
expect(screen.getByText("资源组 · 七日签到")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("资源组 ID 18")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("金币 100")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("星光头像框 x1 · 头像框 · 7天")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("0 / 100")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("rgr_1234567890abcdef")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("seven_day_checkin_1782448934991_9b39c38")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("后台用户")).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('img[src="https://media.haiyihy.com/star-frame.png"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resource grant list keeps operator only for user initiated manager grants", () => {
|
||||||
|
vi.mocked(useResourceGrantListPage).mockReturnValue(
|
||||||
|
pageFixture({
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
grantFixture({
|
||||||
|
grantId: "rgr_manager",
|
||||||
|
grantSource: "manager_center",
|
||||||
|
operator: {
|
||||||
|
source: "manager_center",
|
||||||
|
userId: "328151620022083584",
|
||||||
|
username: "Manager A",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<ResourceGrantListPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Manager A")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
function pageFixture(patch = {}) {
|
function pageFixture(patch = {}) {
|
||||||
return {
|
return {
|
||||||
abilities: {
|
abilities: {
|
||||||
|
|||||||
@ -100,6 +100,75 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grantMediaScroller {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-2);
|
||||||
|
overflow-x: auto;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grantMediaCard {
|
||||||
|
display: grid;
|
||||||
|
width: 72px;
|
||||||
|
flex: 0 0 72px;
|
||||||
|
min-width: 0;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grantMediaThumb {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grantMediaThumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grantMediaName {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grantAssetPill {
|
||||||
|
display: inline-flex;
|
||||||
|
height: var(--admin-tag-height);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--admin-tag-border);
|
||||||
|
border-radius: var(--admin-tag-radius);
|
||||||
|
background: var(--admin-tag-bg);
|
||||||
|
color: var(--admin-tag-color);
|
||||||
|
font-size: var(--admin-tag-font-size);
|
||||||
|
font-weight: var(--admin-tag-font-weight);
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 var(--admin-tag-padding-x);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.identityGrantLabel {
|
.identityGrantLabel {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-height: var(--control-height);
|
min-height: var(--control-height);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user