From 0112303b88d2a150daf46f94c302fccdcf557677 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 26 Jun 2026 12:58:27 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8F=91=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/pages/ResourceGrantListPage.jsx | 203 ++++++++++++++---- .../pages/ResourceGrantListPage.test.jsx | 85 ++++++++ src/features/resources/resources.module.css | 69 ++++++ 3 files changed, 318 insertions(+), 39 deletions(-) diff --git a/src/features/resources/pages/ResourceGrantListPage.jsx b/src/features/resources/pages/ResourceGrantListPage.jsx index 47b3a4f..5f247c7 100644 --- a/src/features/resources/pages/ResourceGrantListPage.jsx +++ b/src/features/resources/pages/ResourceGrantListPage.jsx @@ -1,4 +1,5 @@ import Add from "@mui/icons-material/Add"; +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; import UndoOutlined from "@mui/icons-material/UndoOutlined"; import MenuItem from "@mui/material/MenuItem"; 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 { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx"; import { + resourceGroupAssetLabels, resourceGrantStatusFilters, resourceGrantStatusLabels, resourceGrantSubjectLabels, + resourceTypeLabels, } from "@/features/resources/constants.js"; import { useResourceGrantListPage } from "@/features/resources/hooks/useResourcePages.js"; import styles from "@/features/resources/resources.module.css"; @@ -27,12 +30,11 @@ import styles from "@/features/resources/resources.module.css"; const grantColumns = [ { key: "grant", - label: "赠送记录", - width: "minmax(240px, 1.2fr)", + label: "来源", + width: "minmax(220px, 1fr)", render: (grant) => (
- {grant.grantId || "-"} - {grant.commandId || "-"} + {grantSourceLabel(grant.grantSource)}
), }, @@ -45,24 +47,19 @@ const grantColumns = [ { key: "operator", label: "操作人", - width: "minmax(180px, 1fr)", + width: "minmax(190px, 0.9fr)", render: (grant) => , }, { key: "subject", - label: "对象", - width: "minmax(160px, 0.8fr)", - render: (grant) => ( -
- {resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "-"} - {grant.grantSubjectId || "-"} -
- ), + label: "发放方式", + width: "minmax(170px, 0.75fr)", + render: (grant) => , }, { key: "items", label: "发放内容", - width: "minmax(180px, 1fr)", + width: "minmax(320px, 1.5fr)", render: (grant) => , }, { @@ -79,8 +76,8 @@ const grantColumns = [ { key: "reason", label: "原因", - width: "minmax(160px, 1fr)", - render: (grant) => grant.reason || "-", + width: "minmax(190px, 0.95fr)", + render: (grant) => , }, { key: "time", @@ -143,7 +140,7 @@ export function ResourceGrantListPage() { 0 ? { @@ -347,14 +344,53 @@ function GrantItems({ grant }) { return "-"; } return ( -
- {items.slice(0, 2).map((item) => ( - - {item.resourceId || "-"} / {item.quantity || 0} - {item.durationMs ? ` / ${formatDuration(item.durationMs)}` : ""} - +
+ {items.map((item) => ( + ))} - {items.length > 2 ? +{items.length - 2} : null} +
+ ); +} + +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 {`${label} ${formatNumber(amount || item.quantity)}`}; + } + + const label = grantItemLabel(item, snapshot); + const imageUrl = grantItemImageUrl(snapshot); + return ( + + + {imageUrl ? : } + + {label} + + ); +} + +function GrantSubject({ grant }) { + const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "发放"; + const sourceLabel = grantSourceLabel(grant.grantSource); + return ( +
+ {sourceLabel === "-" ? subjectLabel : `${subjectLabel} · ${sourceLabel}`} + {subjectMeta(grant)} +
+ ); +} + +function GrantReason({ grant }) { + const label = reasonLabel(grant.reason); + const raw = String(grant.reason || "").trim(); + return ( +
+ {label} + {raw && raw !== label ? {raw} : null}
); } @@ -384,26 +420,115 @@ function GrantOperator({ grant }) { /> ); } - if (source === "admin") { - const name = operator.name || operator.username || (operatorUserId ? `Admin ${operatorUserId}` : "-"); - return ( -
- {name} - {operator.username || operatorUserId || "-"} -
- ); + return "-"; +} + +const grantSourceLabels = { + achievement: "成就奖励", + admin: "后台发放", + 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 ( -
- {operatorUserId} - {source || "-"} -
+ return parts.join(" · "); +} + +function grantItemImageUrl(snapshot) { + 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) { const days = Number(durationMs || 0) / (24 * 60 * 60 * 1000); return Number.isInteger(days) ? `${days}天` : `${days.toFixed(1)}天`; diff --git a/src/features/resources/pages/ResourceGrantListPage.test.jsx b/src/features/resources/pages/ResourceGrantListPage.test.jsx index 4df88ca..f5142e8 100644 --- a/src/features/resources/pages/ResourceGrantListPage.test.jsx +++ b/src/features/resources/pages/ResourceGrantListPage.test.jsx @@ -65,6 +65,91 @@ test("resource grant list hides revoke without permission", () => { 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(); + + 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(); + + expect(screen.getByText("Manager A")).toBeInTheDocument(); +}); + function pageFixture(patch = {}) { return { abilities: { diff --git a/src/features/resources/resources.module.css b/src/features/resources/resources.module.css index a24f80a..0f27230 100644 --- a/src/features/resources/resources.module.css +++ b/src/features/resources/resources.module.css @@ -100,6 +100,75 @@ 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 { display: inline-flex; min-height: var(--control-height);