- {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);