礼物资源
This commit is contained in:
parent
216031f776
commit
08872009e1
@ -817,6 +817,38 @@
|
||||
"x-permissions": ["bd:create"]
|
||||
}
|
||||
},
|
||||
"/admin/managers/{user_id}": {
|
||||
"put": {
|
||||
"operationId": "updateManager",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "user_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateManagerPayload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/ManagerResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "bd:update",
|
||||
"x-permissions": ["bd:update"]
|
||||
}
|
||||
},
|
||||
"/admin/agencies": {
|
||||
"get": {
|
||||
"operationId": "listAgencies",
|
||||
@ -7189,6 +7221,36 @@
|
||||
"bdLeaderCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"canAddAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddBdLeader": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddSuperadmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canBlockUser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantAvatarFrame": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantBadge": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVehicle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVip": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canTransferUserCountry": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canUpdateUserLevel": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
@ -7226,6 +7288,36 @@
|
||||
"type": "object",
|
||||
"required": ["targetUserId"],
|
||||
"properties": {
|
||||
"canAddAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddBdLeader": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddSuperadmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canBlockUser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantAvatarFrame": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantBadge": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVehicle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVip": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canTransferUserCountry": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canUpdateUserLevel": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contact": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
@ -7235,6 +7327,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateManagerPayload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canAddAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddBdLeader": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddSuperadmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canBlockUser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantAvatarFrame": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantBadge": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVehicle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canGrantVip": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canTransferUserCountry": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canUpdateUserLevel": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contact": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "disabled"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"BDProfile": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
|
||||
@ -62,10 +62,11 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
region_id: 7,
|
||||
status: "active",
|
||||
});
|
||||
await createManager({ canBlockUser: false, canGrantBadge: false, contact: "+63", targetUserId: "1003" });
|
||||
await createManager({ canBlockUser: false, canGrantBadge: false, canGrantVip: true, contact: "+63", targetUserId: "1003" });
|
||||
await updateManager("1003", {
|
||||
canBlockUser: false,
|
||||
canGrantVehicle: true,
|
||||
canGrantVip: false,
|
||||
canTransferUserCountry: false,
|
||||
contact: "+63",
|
||||
status: "disabled",
|
||||
@ -147,6 +148,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({
|
||||
canBlockUser: false,
|
||||
canGrantBadge: false,
|
||||
canGrantVip: true,
|
||||
contact: "+63",
|
||||
targetUserId: "1003",
|
||||
});
|
||||
@ -155,6 +157,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({
|
||||
canBlockUser: false,
|
||||
canGrantVehicle: true,
|
||||
canGrantVip: false,
|
||||
canTransferUserCountry: false,
|
||||
contact: "+63",
|
||||
status: "disabled",
|
||||
|
||||
@ -191,9 +191,10 @@ export function createManager(payload: CreateManagerPayload): Promise<ManagerDto
|
||||
}
|
||||
|
||||
export function updateManager(userId: EntityId, payload: UpdateManagerPayload): Promise<ManagerDto> {
|
||||
return apiRequest<ManagerDto, UpdateManagerPayload>(`/v1/admin/managers/${encodeURIComponent(String(userId))}`, {
|
||||
const endpoint = API_ENDPOINTS.updateManager;
|
||||
return apiRequest<ManagerDto, UpdateManagerPayload>(apiEndpointPath(API_OPERATIONS.updateManager, { user_id: userId }), {
|
||||
body: payload,
|
||||
method: "PUT",
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ export const managerPermissionFields = [
|
||||
"canGrantAvatarFrame",
|
||||
"canGrantVehicle",
|
||||
"canGrantBadge",
|
||||
"canGrantVip",
|
||||
"canUpdateUserLevel",
|
||||
"canAddBdLeader",
|
||||
"canAddAdmin",
|
||||
|
||||
@ -19,6 +19,7 @@ const managerPermissionOptions = [
|
||||
{ key: "canGrantAvatarFrame", label: "头像框" },
|
||||
{ key: "canGrantVehicle", label: "Car" },
|
||||
{ key: "canGrantBadge", label: "徽章" },
|
||||
{ key: "canGrantVip", label: "VIP" },
|
||||
{ key: "canUpdateUserLevel", label: "升级等级" },
|
||||
{ key: "canAddBdLeader", label: "BD Leader" },
|
||||
{ key: "canAddAdmin", label: "Admin" },
|
||||
|
||||
@ -21,6 +21,8 @@ test("manager status column uses a switch and toggles through the page hook", ()
|
||||
|
||||
render(<HostManagersPage />);
|
||||
|
||||
expect(screen.getByText("VIP")).toBeInTheDocument();
|
||||
|
||||
const statusSwitch = screen.getByRole("switch", { name: "停用经理" });
|
||||
expect(statusSwitch).toBeChecked();
|
||||
|
||||
@ -55,6 +57,7 @@ function pageFixture(patch = {}) {
|
||||
canGrantAvatarFrame: true,
|
||||
canGrantBadge: true,
|
||||
canGrantVehicle: true,
|
||||
canGrantVip: true,
|
||||
canTransferUserCountry: true,
|
||||
canUpdateUserLevel: true,
|
||||
contact: "",
|
||||
@ -88,6 +91,7 @@ function managerFixture() {
|
||||
canGrantAvatarFrame: true,
|
||||
canGrantBadge: true,
|
||||
canGrantVehicle: true,
|
||||
canGrantVip: true,
|
||||
canTransferUserCountry: true,
|
||||
canUpdateUserLevel: true,
|
||||
contact: "+63 910 323 3670",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { agencyCloseSchema, agencyJoinEnabledSchema, coinSellerStatusSchema } from "./schema";
|
||||
import { agencyCloseSchema, agencyJoinEnabledSchema, coinSellerStatusSchema, createManagerSchema } from "./schema";
|
||||
|
||||
test("coin seller contact accepts plus sign with large internal user id", () => {
|
||||
const result = coinSellerStatusSchema.safeParse({
|
||||
@ -40,3 +40,17 @@ test("agency action schemas preserve large agency ids as strings", () => {
|
||||
expect(joinResult.data.agencyId).toBe(agencyId);
|
||||
}
|
||||
});
|
||||
|
||||
test("manager schema defaults vip grant permission on and preserves explicit false", () => {
|
||||
const defaultResult = createManagerSchema.safeParse({ targetUserId: "1003" });
|
||||
const disabledResult = createManagerSchema.safeParse({ canGrantVip: false, targetUserId: "1003" });
|
||||
|
||||
expect(defaultResult.success).toBe(true);
|
||||
expect(disabledResult.success).toBe(true);
|
||||
if (defaultResult.success) {
|
||||
expect(defaultResult.data.canGrantVip).toBe(true);
|
||||
}
|
||||
if (disabledResult.success) {
|
||||
expect(disabledResult.data.canGrantVip).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -116,6 +116,7 @@ export const createManagerSchema = z.object({
|
||||
canGrantAvatarFrame: z.boolean().optional().default(true),
|
||||
canGrantBadge: z.boolean().optional().default(true),
|
||||
canGrantVehicle: z.boolean().optional().default(true),
|
||||
canGrantVip: z.boolean().optional().default(true),
|
||||
canTransferUserCountry: z.boolean().optional().default(true),
|
||||
canUpdateUserLevel: z.boolean().optional().default(true),
|
||||
contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""),
|
||||
|
||||
@ -21,6 +21,7 @@ const drawerZIndex = 1600;
|
||||
export function GiftResourceSelectField({
|
||||
disabled = false,
|
||||
drawerTitle = "选择资源",
|
||||
excludedResourceIds = [],
|
||||
giftTypes = [],
|
||||
gifts = [],
|
||||
label = "资源",
|
||||
@ -36,6 +37,8 @@ export function GiftResourceSelectField({
|
||||
const [activeType, setActiveType] = useState("");
|
||||
const selectedIds = useMemo(() => selectedResourceIds.map((id) => String(id)), [selectedResourceIds]);
|
||||
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||||
const excludedIds = useMemo(() => excludedResourceIds.map((id) => String(id)), [excludedResourceIds]);
|
||||
const excludedIdSet = useMemo(() => new Set(excludedIds), [excludedIds]);
|
||||
const giftsByResourceId = useMemo(() => {
|
||||
const items = new Map();
|
||||
gifts.forEach((gift) => {
|
||||
@ -49,10 +52,19 @@ export function GiftResourceSelectField({
|
||||
() => buildResourceItems(resources, giftsByResourceId),
|
||||
[giftsByResourceId, resources],
|
||||
);
|
||||
const itemCounts = useMemo(() => countResourceItems(resourceItems), [resourceItems]);
|
||||
const visibleResourceItems = useMemo(
|
||||
() =>
|
||||
resourceItems.filter((item) => {
|
||||
const resourceId = String(item.resource.resourceId);
|
||||
return !excludedIdSet.has(resourceId) || selectedIdSet.has(resourceId);
|
||||
}),
|
||||
[excludedIdSet, resourceItems, selectedIdSet],
|
||||
);
|
||||
const visibleResources = useMemo(() => visibleResourceItems.map((item) => item.resource), [visibleResourceItems]);
|
||||
const itemCounts = useMemo(() => countResourceItems(visibleResourceItems), [visibleResourceItems]);
|
||||
const categoryOptions = useMemo(
|
||||
() => buildCategoryOptions(giftTypes, resources, resourceItems),
|
||||
[giftTypes, resourceItems, resources],
|
||||
() => buildCategoryOptions(giftTypes, visibleResources, visibleResourceItems),
|
||||
[giftTypes, visibleResourceItems, visibleResources],
|
||||
);
|
||||
const activeCategory = categoryOptions.some((item) => item.value === activeType)
|
||||
? activeType
|
||||
@ -76,7 +88,7 @@ export function GiftResourceSelectField({
|
||||
}, [resourcesById, selectedIds]);
|
||||
const filteredResources = useMemo(() => {
|
||||
const keyword = query.trim().toLowerCase();
|
||||
return resourceItems.filter((item) => {
|
||||
return visibleResourceItems.filter((item) => {
|
||||
if (activeCategory && item.categoryValue !== activeCategory) {
|
||||
return false;
|
||||
}
|
||||
@ -99,7 +111,7 @@ export function GiftResourceSelectField({
|
||||
.toLowerCase();
|
||||
return haystack.includes(keyword);
|
||||
});
|
||||
}, [activeCategory, query, resourceItems]);
|
||||
}, [activeCategory, query, visibleResourceItems]);
|
||||
|
||||
const openDrawer = () => {
|
||||
if (!disabled && !loading) {
|
||||
|
||||
@ -117,3 +117,71 @@ test("gift resource select drawer filters by gift type and supports multiple sel
|
||||
expect(changes.at(-1)).toEqual(["11", "12"]);
|
||||
expect(screen.getByDisplayValue("Rose Gift,CP Ring")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("gift resource select drawer hides excluded gift resources from candidates", () => {
|
||||
function Harness() {
|
||||
const [value, setValue] = useState([]);
|
||||
return (
|
||||
<GiftResourceSelectField
|
||||
excludedResourceIds={[11]}
|
||||
giftTypes={giftTypes}
|
||||
gifts={gifts}
|
||||
resources={resources}
|
||||
selectedResourceIds={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
fireEvent.click(screen.getByPlaceholderText("请选择资源"));
|
||||
fireEvent.click(screen.getByRole("tab", { name: "普通礼物" }));
|
||||
|
||||
expect(screen.queryByText("Rose Gift")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("当前无可选资源")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "CP礼物" }));
|
||||
expect(screen.getByText("CP Ring")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("gift resource select drawer hides excluded resources but keeps selected resources removable", () => {
|
||||
const changes = [];
|
||||
|
||||
function Harness() {
|
||||
const [value, setValue] = useState(["11"]);
|
||||
return (
|
||||
<GiftResourceSelectField
|
||||
excludedResourceIds={[11, 21]}
|
||||
giftTypes={giftTypes}
|
||||
gifts={gifts}
|
||||
resources={resources}
|
||||
selectedResourceIds={value}
|
||||
onChange={(nextValue) => {
|
||||
changes.push(nextValue);
|
||||
setValue(nextValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
fireEvent.click(screen.getByPlaceholderText("请选择资源"));
|
||||
|
||||
expect(screen.getByText("Rose Gift")).toBeInTheDocument();
|
||||
expect(screen.queryByText("VIP Vehicle")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "CP礼物" }));
|
||||
fireEvent.click(screen.getByText("CP Ring"));
|
||||
|
||||
expect(changes.at(-1)).toEqual(["11", "12"]);
|
||||
expect(screen.getByDisplayValue("Rose Gift,CP Ring")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "普通礼物" }));
|
||||
fireEvent.click(screen.getByText("Rose Gift"));
|
||||
|
||||
expect(changes.at(-1)).toEqual(["12"]);
|
||||
expect(screen.queryByText("Rose Gift")).not.toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("CP Ring")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -163,6 +163,7 @@ const emptyGiftForm = (gift = {}) => {
|
||||
priceVersion: gift.priceVersion || "default",
|
||||
regionIds: (gift.regionIds?.length ? gift.regionIds : [0]).map(String),
|
||||
resourceId: gift.resourceId ? String(gift.resourceId) : "",
|
||||
items: [],
|
||||
sortOrder: gift.sortOrder === 0 || gift.sortOrder ? String(gift.sortOrder) : "0",
|
||||
};
|
||||
};
|
||||
@ -180,6 +181,52 @@ export function applyGiftResourceSelection(form, resource, fillIdentity = true)
|
||||
return applyGiftResourcePrice(nextForm, resource);
|
||||
}
|
||||
|
||||
export function buildGiftResourceDrafts(form, resourceIds = [], resources = []) {
|
||||
const resourcesById = new Map(resources.map((resource) => [String(resource.resourceId), resource]));
|
||||
const existingDraftsByResourceId = new Map(
|
||||
(form.items || [])
|
||||
.filter((item) => item?.resourceId)
|
||||
.map((item) => [String(item.resourceId), item]),
|
||||
);
|
||||
|
||||
return resourceIds
|
||||
.map((resourceId) => String(resourceId || "").trim())
|
||||
.filter(Boolean)
|
||||
.map((resourceId) => {
|
||||
const existingDraft = existingDraftsByResourceId.get(resourceId);
|
||||
if (existingDraft) {
|
||||
return existingDraft;
|
||||
}
|
||||
const resource = resourcesById.get(resourceId) || { resourceId };
|
||||
return {
|
||||
...applyGiftResourceSelection(giftDraftDefaultsFromForm(form), resource, true),
|
||||
resource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function giftDraftDefaultsFromForm(form) {
|
||||
const giftTypeCode = form.giftTypeCode || "normal";
|
||||
const cpRelationType = giftTypeCode === cpGiftTypeCode ? form.cpRelationType || defaultCPRelationType : "";
|
||||
return {
|
||||
chargeAssetType: form.chargeAssetType || "COIN",
|
||||
coinPrice: "",
|
||||
cpRelationType,
|
||||
effectTypes: form.effectTypes || [],
|
||||
effectiveFrom: form.effectiveFrom || "",
|
||||
effectiveTo: form.effectiveTo || "",
|
||||
enabled: form.enabled !== false,
|
||||
giftId: "",
|
||||
giftTypeCode,
|
||||
name: "",
|
||||
presentationJson: normalizeGiftPresentationJson(form.presentationJson || "{}", giftTypeCode, cpRelationType),
|
||||
priceVersion: form.priceVersion || "default",
|
||||
regionIds: form.regionIds?.length ? form.regionIds : ["0"],
|
||||
resourceId: "",
|
||||
sortOrder: form.sortOrder || "0",
|
||||
};
|
||||
}
|
||||
|
||||
export function applyGiftPriceDefaults(form, coinPrice) {
|
||||
const priceValue = String(coinPrice ?? "");
|
||||
return {
|
||||
@ -234,12 +281,12 @@ export function normalizeGiftPresentationJson(presentationJson, giftTypeCode, cp
|
||||
return isCPGift ? cpPresentationJson(nextPresentation, cpRelationType) : JSON.stringify(nextPresentation);
|
||||
}
|
||||
|
||||
export async function fetchAllOptionPages(fetcher, query = {}, pageSize = optionPageSize) {
|
||||
export async function fetchAllOptionPages(fetcher, query = {}, pageSize = optionPageSize, maxPages = optionMaxPages) {
|
||||
const items = [];
|
||||
let total = Number.POSITIVE_INFINITY;
|
||||
|
||||
// 后台会把选项接口的 page_size 截断到 100;抽屉候选项必须按真实分页拉全,避免只展示第一页资源。
|
||||
for (let page = 1; page <= optionMaxPages; page += 1) {
|
||||
for (let page = 1; page <= maxPages; page += 1) {
|
||||
const data = await fetcher({ ...query, page, page_size: pageSize });
|
||||
const pageItems = Array.isArray(data?.items) ? data.items : [];
|
||||
const responsePageSize = Number(data?.pageSize || data?.page_size || pageSize) || pageSize;
|
||||
@ -1049,6 +1096,8 @@ export function useGiftListPage() {
|
||||
const [giftTypeDialogOpen, setGiftTypeDialogOpen] = useState(false);
|
||||
const [giftTypesLoading, setGiftTypesLoading] = useState(false);
|
||||
const [resourceOptions, setResourceOptions] = useState([]);
|
||||
const [giftResourceGiftOptions, setGiftResourceGiftOptions] = useState([]);
|
||||
const [existingGiftResourceIds, setExistingGiftResourceIds] = useState([]);
|
||||
const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false);
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const filters = useMemo(
|
||||
@ -1113,10 +1162,25 @@ export function useGiftListPage() {
|
||||
}
|
||||
let ignore = false;
|
||||
setResourceOptionsLoading(true);
|
||||
fetchAllOptionPages(listResources, { resource_type: "gift" })
|
||||
.then((data) => {
|
||||
const optionRequests =
|
||||
activeAction === "create"
|
||||
? [
|
||||
fetchAllOptionPages(listResources, { resource_type: "gift" }),
|
||||
fetchAllOptionPages(listGifts, {}, optionPageSize, 1000),
|
||||
]
|
||||
: [fetchAllOptionPages(listResources, { resource_type: "gift" }), Promise.resolve({ items: [] })];
|
||||
Promise.all(optionRequests)
|
||||
.then(([resourceData, giftOptionsData]) => {
|
||||
if (!ignore) {
|
||||
setResourceOptions(toGiftResourceOptions(data.items || [], selectedGift));
|
||||
const giftOptions = giftOptionsData.items || [];
|
||||
setGiftResourceGiftOptions(giftOptions);
|
||||
setExistingGiftResourceIds(
|
||||
giftOptions
|
||||
.map((gift) => gift?.resourceId)
|
||||
.filter((resourceId) => resourceId || resourceId === 0)
|
||||
.map(String),
|
||||
);
|
||||
setResourceOptions(toGiftResourceOptions(resourceData.items || [], selectedGift));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -1165,6 +1229,66 @@ export function useGiftListPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const selectGiftResources = (resourceIds) => {
|
||||
setForm((currentForm) => {
|
||||
const items = buildGiftResourceDrafts(currentForm, resourceIds, resourceOptions);
|
||||
return {
|
||||
...currentForm,
|
||||
items,
|
||||
resourceId: items[0]?.resourceId || "",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const updateGiftDraft = (resourceId, patch) => {
|
||||
setForm((currentForm) => ({
|
||||
...currentForm,
|
||||
items: (currentForm.items || []).map((item) => {
|
||||
if (String(item.resourceId) !== String(resourceId)) {
|
||||
return item;
|
||||
}
|
||||
const nextItem = { ...item, ...patch };
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "giftTypeCode")) {
|
||||
const cpRelationType =
|
||||
nextItem.giftTypeCode === cpGiftTypeCode
|
||||
? nextItem.cpRelationType || defaultCPRelationType
|
||||
: "";
|
||||
return {
|
||||
...nextItem,
|
||||
cpRelationType,
|
||||
presentationJson: normalizeGiftPresentationJson(
|
||||
nextItem.presentationJson,
|
||||
nextItem.giftTypeCode,
|
||||
cpRelationType,
|
||||
),
|
||||
};
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "cpRelationType")) {
|
||||
return {
|
||||
...nextItem,
|
||||
presentationJson: normalizeGiftPresentationJson(
|
||||
nextItem.presentationJson,
|
||||
nextItem.giftTypeCode,
|
||||
nextItem.cpRelationType,
|
||||
),
|
||||
};
|
||||
}
|
||||
return nextItem;
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
const removeGiftDraft = (resourceId) => {
|
||||
setForm((currentForm) => {
|
||||
const items = (currentForm.items || []).filter((item) => String(item.resourceId) !== String(resourceId));
|
||||
return {
|
||||
...currentForm,
|
||||
items,
|
||||
resourceId: items[0]?.resourceId || "",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const changeGiftPrice = (coinPrice) => {
|
||||
setForm((currentForm) => applyGiftPriceDefaults(currentForm, coinPrice));
|
||||
};
|
||||
@ -1249,21 +1373,55 @@ export function useGiftListPage() {
|
||||
return;
|
||||
}
|
||||
setLoadingAction(editing ? "gift-edit" : "gift-create");
|
||||
const createdDrafts = [];
|
||||
let shouldReload = false;
|
||||
try {
|
||||
const payload = buildGiftPayload(parseForm(giftFormSchema, form));
|
||||
const createDrafts = !editing && Array.isArray(form.items) && form.items.length ? form.items : [form];
|
||||
if (editing) {
|
||||
const payload = buildGiftPayload(parseForm(giftFormSchema, form));
|
||||
await updateGift(selectedGift.giftId, payload);
|
||||
} else {
|
||||
await createGift(payload);
|
||||
const payloads = createDrafts.map((draft) => ({
|
||||
draft,
|
||||
payload: buildGiftPayload(parseForm(giftFormSchema, draft)),
|
||||
}));
|
||||
for (const { draft, payload } of payloads) {
|
||||
await createGift(payload);
|
||||
createdDrafts.push(draft);
|
||||
}
|
||||
}
|
||||
showToast(editing ? "礼物已更新" : "礼物已创建", "success");
|
||||
showToast(editing ? "礼物已更新" : `已创建 ${createDrafts.length} 个礼物`, "success");
|
||||
closeAction();
|
||||
await result.reload();
|
||||
shouldReload = true;
|
||||
} catch (err) {
|
||||
showToast(err.message || "操作失败", "error");
|
||||
if (!editing && createdDrafts.length) {
|
||||
const createdResourceIds = new Set(createdDrafts.map((draft) => String(draft.resourceId)));
|
||||
setExistingGiftResourceIds((current) => Array.from(new Set([...current, ...createdResourceIds])));
|
||||
setForm((currentForm) => {
|
||||
const items = (currentForm.items || []).filter(
|
||||
(item) => !createdResourceIds.has(String(item.resourceId)),
|
||||
);
|
||||
return {
|
||||
...currentForm,
|
||||
items,
|
||||
resourceId: items[0]?.resourceId || "",
|
||||
};
|
||||
});
|
||||
shouldReload = true;
|
||||
showToast(`已创建 ${createdDrafts.length} 个礼物,剩余礼物创建失败:${err.message || "操作失败"}`, "error");
|
||||
} else {
|
||||
showToast(err.message || "操作失败", "error");
|
||||
}
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
if (shouldReload) {
|
||||
try {
|
||||
await result.reload();
|
||||
} catch (err) {
|
||||
showToast(err.message || "刷新礼物列表失败", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGift = async (gift, nextEnabled = gift.status !== "active") => {
|
||||
@ -1389,6 +1547,8 @@ export function useGiftListPage() {
|
||||
giftTypeForm,
|
||||
giftTypeOptions: giftTypes,
|
||||
giftTypesLoading,
|
||||
giftResourceGiftOptions,
|
||||
existingGiftResourceIds,
|
||||
loadingAction,
|
||||
loadingRegions,
|
||||
openCreateGift,
|
||||
@ -1401,12 +1561,15 @@ export function useGiftListPage() {
|
||||
selectedGift,
|
||||
resetFilters,
|
||||
selectGiftResource,
|
||||
selectGiftResources,
|
||||
removeGiftDraft,
|
||||
setForm,
|
||||
saveGiftSortOrder,
|
||||
submitGiftTypes,
|
||||
status,
|
||||
submitGift,
|
||||
toggleGift,
|
||||
updateGiftDraft,
|
||||
updateGiftTypeFormItem,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { expect, test, vi } from "vitest";
|
||||
import {
|
||||
applyGiftPriceDefaults,
|
||||
applyGiftResourceSelection,
|
||||
buildGiftResourceDrafts,
|
||||
cpRelationTypeFromGift,
|
||||
cpRelationTypeFromPresentationJson,
|
||||
cpRelationTypeShortLabel,
|
||||
@ -47,6 +48,76 @@ test("fills gift price without gift points", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("builds gift drafts from multiple selected resources and preserves edits", () => {
|
||||
const form = {
|
||||
chargeAssetType: "COIN",
|
||||
cpRelationType: "",
|
||||
effectTypes: ["animation"],
|
||||
effectiveFrom: "",
|
||||
effectiveTo: "",
|
||||
enabled: true,
|
||||
giftTypeCode: "normal",
|
||||
items: [
|
||||
{
|
||||
chargeAssetType: "COIN",
|
||||
coinPrice: "88",
|
||||
effectTypes: [],
|
||||
effectiveFrom: "",
|
||||
effectiveTo: "",
|
||||
enabled: false,
|
||||
giftId: "custom_rose",
|
||||
giftTypeCode: "normal",
|
||||
name: "Custom Rose",
|
||||
presentationJson: "{}",
|
||||
priceVersion: "default",
|
||||
regionIds: ["0"],
|
||||
resourceId: "11",
|
||||
sortOrder: "7",
|
||||
},
|
||||
],
|
||||
presentationJson: "{}",
|
||||
priceVersion: "default",
|
||||
regionIds: ["0", "1001"],
|
||||
sortOrder: "3",
|
||||
};
|
||||
|
||||
const drafts = buildGiftResourceDrafts(form, ["11", "12"], [
|
||||
{
|
||||
coinPrice: 10,
|
||||
name: "Rose",
|
||||
priceType: "coin",
|
||||
resourceCode: "gift_rose",
|
||||
resourceId: 11,
|
||||
},
|
||||
{
|
||||
coinPrice: 20,
|
||||
name: "Ring",
|
||||
priceType: "coin",
|
||||
resourceCode: "gift_ring",
|
||||
resourceId: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(drafts).toHaveLength(2);
|
||||
expect(drafts[0]).toMatchObject({
|
||||
coinPrice: "88",
|
||||
giftId: "custom_rose",
|
||||
name: "Custom Rose",
|
||||
resourceId: "11",
|
||||
sortOrder: "7",
|
||||
});
|
||||
expect(drafts[1]).toMatchObject({
|
||||
coinPrice: "20",
|
||||
effectTypes: ["animation"],
|
||||
giftId: "12",
|
||||
giftTypeCode: "normal",
|
||||
name: "Ring",
|
||||
regionIds: ["0", "1001"],
|
||||
resourceId: "12",
|
||||
sortOrder: "3",
|
||||
});
|
||||
});
|
||||
|
||||
test("fetches all option pages instead of only the first resource page", async () => {
|
||||
const fetcher = vi
|
||||
.fn()
|
||||
|
||||
@ -43,6 +43,7 @@ import {
|
||||
resourceStatusFilters,
|
||||
} from "@/features/resources/constants.js";
|
||||
import { GiftTypeConfigDialog } from "@/features/resources/components/GiftTypeConfigDialog.jsx";
|
||||
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
|
||||
import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
||||
import {
|
||||
cpRelationTypeFromGift,
|
||||
@ -137,7 +138,7 @@ const baseColumns = (giftTypeOptions) => [
|
||||
|
||||
export function GiftListPage() {
|
||||
const page = useGiftListPage();
|
||||
const items = page.data.items || [];
|
||||
const items = useMemo(() => page.data.items || [], [page.data.items]);
|
||||
const total = page.data.total || 0;
|
||||
const [selectedGiftIds, setSelectedGiftIds] = useState([]);
|
||||
const isEditing = page.activeAction === "edit";
|
||||
@ -419,7 +420,14 @@ function GiftSortOrderCell({ gift, page }) {
|
||||
function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, page, regionOptions }) {
|
||||
const submitDisabled =
|
||||
disabled || loading || page.resourceOptionsLoading || page.loadingRegions || page.giftTypesLoading;
|
||||
const showGiftIdentityFields = mode === "edit" || Boolean(form.resourceId);
|
||||
const isCreateMode = mode === "create";
|
||||
const giftDrafts = Array.isArray(form.items) ? form.items : [];
|
||||
const selectedResourceIds = isCreateMode
|
||||
? giftDrafts.map((item) => item.resourceId).filter(Boolean)
|
||||
: form.resourceId
|
||||
? [form.resourceId]
|
||||
: [];
|
||||
const showGiftIdentityFields = mode === "edit" && Boolean(form.resourceId);
|
||||
const isCPGiftType = form.giftTypeCode === "cp";
|
||||
return (
|
||||
<AdminFormDialog
|
||||
@ -433,15 +441,30 @@ function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
|
||||
>
|
||||
<AdminFormSection title="资源信息">
|
||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||
<ResourceSelectField
|
||||
disabled={disabled || page.resourceOptionsLoading}
|
||||
drawerTitle="选择礼物资源"
|
||||
label="礼物资源"
|
||||
loading={page.resourceOptionsLoading}
|
||||
resources={page.resourceOptions}
|
||||
value={form.resourceId}
|
||||
onChange={(resourceId) => page.selectGiftResource(resourceId)}
|
||||
/>
|
||||
{isCreateMode ? (
|
||||
<GiftResourceSelectField
|
||||
disabled={disabled || page.resourceOptionsLoading}
|
||||
drawerTitle="选择礼物资源"
|
||||
excludedResourceIds={page.existingGiftResourceIds}
|
||||
giftTypes={page.giftTypeOptions}
|
||||
gifts={page.giftResourceGiftOptions}
|
||||
label="礼物资源"
|
||||
loading={page.resourceOptionsLoading}
|
||||
resources={page.resourceOptions}
|
||||
selectedResourceIds={selectedResourceIds}
|
||||
onChange={page.selectGiftResources}
|
||||
/>
|
||||
) : (
|
||||
<ResourceSelectField
|
||||
disabled={disabled || page.resourceOptionsLoading}
|
||||
drawerTitle="选择礼物资源"
|
||||
label="礼物资源"
|
||||
loading={page.resourceOptionsLoading}
|
||||
resources={page.resourceOptions}
|
||||
value={form.resourceId}
|
||||
onChange={(resourceId) => page.selectGiftResource(resourceId)}
|
||||
/>
|
||||
)}
|
||||
{showGiftIdentityFields ? (
|
||||
<>
|
||||
<TextField
|
||||
@ -460,21 +483,23 @@ function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<TextField
|
||||
disabled={disabled || page.giftTypesLoading}
|
||||
label="礼物类型"
|
||||
required
|
||||
select
|
||||
value={form.giftTypeCode}
|
||||
onChange={(event) => page.changeGiftFormTypeCode(event.target.value)}
|
||||
>
|
||||
{page.giftTypeOptions.map((item) => (
|
||||
<MenuItem key={item.tabKey} value={item.tabKey}>
|
||||
{item.displayName || item.tabKey}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{isCPGiftType ? (
|
||||
{!isCreateMode ? (
|
||||
<TextField
|
||||
disabled={disabled || page.giftTypesLoading}
|
||||
label="礼物类型"
|
||||
required
|
||||
select
|
||||
value={form.giftTypeCode}
|
||||
onChange={(event) => page.changeGiftFormTypeCode(event.target.value)}
|
||||
>
|
||||
{page.giftTypeOptions.map((item) => (
|
||||
<MenuItem key={item.tabKey} value={item.tabKey}>
|
||||
{item.displayName || item.tabKey}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
) : null}
|
||||
{!isCreateMode && isCPGiftType ? (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="关系类型"
|
||||
@ -492,56 +517,216 @@ function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
|
||||
) : null}
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="价格与排序">
|
||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||
<TextField disabled label="价格" required type="number" value={form.coinPrice} />
|
||||
{isCreateMode ? (
|
||||
<GiftDraftSection
|
||||
disabled={disabled}
|
||||
drafts={giftDrafts}
|
||||
page={page}
|
||||
regionOptions={regionOptions}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AdminFormSection title="价格与排序">
|
||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||
<TextField disabled label="价格" required type="number" value={form.coinPrice} />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="排序"
|
||||
type="number"
|
||||
value={form.sortOrder}
|
||||
onChange={(event) => page.setForm({ ...form, sortOrder: event.target.value })}
|
||||
/>
|
||||
<AdminFormSwitchField
|
||||
checked={form.enabled}
|
||||
checkedLabel="启用"
|
||||
className={styles.giftSwitch}
|
||||
disabled={disabled}
|
||||
label="礼物状态"
|
||||
uncheckedLabel="禁用"
|
||||
onChange={(checked) => page.setForm({ ...form, enabled: checked })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="投放与时间">
|
||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||
<GiftRegionSelect
|
||||
disabled={disabled || page.loadingRegions}
|
||||
labels={buildRegionLabelMap(regionOptions)}
|
||||
options={regionOptions}
|
||||
value={form.regionIds}
|
||||
onChange={(regionIds) => page.setForm({ ...form, regionIds })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效开始时间"
|
||||
value={form.effectiveFrom}
|
||||
onChange={(effectiveFrom) => page.setForm({ ...form, effectiveFrom })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效结束时间"
|
||||
value={form.effectiveTo}
|
||||
onChange={(effectiveTo) => page.setForm({ ...form, effectiveTo })}
|
||||
/>
|
||||
<GiftEffectSelect
|
||||
disabled={disabled}
|
||||
value={form.effectTypes}
|
||||
onChange={(effectTypes) => page.setForm({ ...form, effectTypes })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
</>
|
||||
)}
|
||||
</AdminFormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function GiftDraftSection({ disabled, drafts, page, regionOptions }) {
|
||||
const regionLabels = buildRegionLabelMap(regionOptions);
|
||||
if (!drafts.length) {
|
||||
return (
|
||||
<AdminFormSection title="礼物配置">
|
||||
<div className={styles.giftDraftEmpty}>当前无数据</div>
|
||||
</AdminFormSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminFormSection title={`礼物配置(${drafts.length})`}>
|
||||
<div className={styles.giftDraftList}>
|
||||
{drafts.map((draft, index) => (
|
||||
<GiftDraftEditor
|
||||
disabled={disabled}
|
||||
draft={draft}
|
||||
index={index}
|
||||
key={draft.resourceId || index}
|
||||
page={page}
|
||||
regionLabels={regionLabels}
|
||||
regionOptions={regionOptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AdminFormSection>
|
||||
);
|
||||
}
|
||||
|
||||
function GiftDraftEditor({ disabled, draft, index, page, regionLabels, regionOptions }) {
|
||||
const isCPGiftType = draft.giftTypeCode === "cp";
|
||||
const updateDraft = (patch) => page.updateGiftDraft(draft.resourceId, patch);
|
||||
return (
|
||||
<div className={styles.giftDraftItem}>
|
||||
<div className={styles.giftDraftHeader}>
|
||||
<GiftDraftThumb draft={draft} />
|
||||
<div className={styles.giftDraftTitle}>
|
||||
<span>{draft.name || draft.giftId || `礼物 ${index + 1}`}</span>
|
||||
<span>资源 {draft.resourceId || "-"}</span>
|
||||
</div>
|
||||
<AdminActionIconButton
|
||||
disabled={disabled}
|
||||
label="移除礼物"
|
||||
onClick={() => page.removeGiftDraft(draft.resourceId)}
|
||||
>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
</div>
|
||||
<AdminFormFieldGrid columns="repeat(4, minmax(0, 1fr))">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="礼物名称"
|
||||
required
|
||||
value={draft.name}
|
||||
onChange={(event) => updateDraft({ name: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="礼物 ID"
|
||||
required
|
||||
value={draft.giftId}
|
||||
onChange={(event) => updateDraft({ giftId: event.target.value })}
|
||||
/>
|
||||
<TextField disabled label="价格" required type="number" value={draft.coinPrice} />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="排序"
|
||||
type="number"
|
||||
value={draft.sortOrder}
|
||||
onChange={(event) => updateDraft({ sortOrder: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled || page.giftTypesLoading}
|
||||
label="礼物类型"
|
||||
required
|
||||
select
|
||||
value={draft.giftTypeCode}
|
||||
onChange={(event) => updateDraft({ giftTypeCode: event.target.value })}
|
||||
>
|
||||
{page.giftTypeOptions.map((item) => (
|
||||
<MenuItem key={item.tabKey} value={item.tabKey}>
|
||||
{item.displayName || item.tabKey}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{isCPGiftType ? (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="排序"
|
||||
type="number"
|
||||
value={form.sortOrder}
|
||||
onChange={(event) => page.setForm({ ...form, sortOrder: event.target.value })}
|
||||
/>
|
||||
<AdminFormSwitchField
|
||||
checked={form.enabled}
|
||||
checkedLabel="启用"
|
||||
className={styles.giftSwitch}
|
||||
disabled={disabled}
|
||||
label="礼物状态"
|
||||
uncheckedLabel="禁用"
|
||||
onChange={(checked) => page.setForm({ ...form, enabled: checked })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="投放与时间">
|
||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||
<GiftRegionSelect
|
||||
disabled={disabled || page.loadingRegions}
|
||||
labels={buildRegionLabelMap(regionOptions)}
|
||||
options={regionOptions}
|
||||
value={form.regionIds}
|
||||
onChange={(regionIds) => page.setForm({ ...form, regionIds })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效开始时间"
|
||||
value={form.effectiveFrom}
|
||||
onChange={(effectiveFrom) => page.setForm({ ...form, effectiveFrom })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效结束时间"
|
||||
value={form.effectiveTo}
|
||||
onChange={(effectiveTo) => page.setForm({ ...form, effectiveTo })}
|
||||
/>
|
||||
<GiftEffectSelect
|
||||
disabled={disabled}
|
||||
value={form.effectTypes}
|
||||
onChange={(effectTypes) => page.setForm({ ...form, effectTypes })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
</AdminFormDialog>
|
||||
label="关系类型"
|
||||
required
|
||||
select
|
||||
value={draft.cpRelationType || "cp"}
|
||||
onChange={(event) => updateDraft({ cpRelationType: event.target.value })}
|
||||
>
|
||||
{cpRelationTypeOptions.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
) : null}
|
||||
<GiftRegionSelect
|
||||
disabled={disabled || page.loadingRegions}
|
||||
labels={regionLabels}
|
||||
options={regionOptions}
|
||||
value={draft.regionIds}
|
||||
onChange={(regionIds) => updateDraft({ regionIds })}
|
||||
/>
|
||||
<AdminFormSwitchField
|
||||
checked={draft.enabled}
|
||||
checkedLabel="启用"
|
||||
className={styles.giftSwitch}
|
||||
disabled={disabled}
|
||||
label="礼物状态"
|
||||
uncheckedLabel="禁用"
|
||||
onChange={(checked) => updateDraft({ enabled: checked })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效开始时间"
|
||||
value={draft.effectiveFrom}
|
||||
onChange={(effectiveFrom) => updateDraft({ effectiveFrom })}
|
||||
/>
|
||||
<DateTimeField
|
||||
disabled={disabled}
|
||||
label="有效结束时间"
|
||||
value={draft.effectiveTo}
|
||||
onChange={(effectiveTo) => updateDraft({ effectiveTo })}
|
||||
/>
|
||||
<GiftEffectSelect
|
||||
disabled={disabled}
|
||||
value={draft.effectTypes}
|
||||
onChange={(effectTypes) => updateDraft({ effectTypes })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GiftDraftThumb({ draft }) {
|
||||
const resource = draft.resource || {};
|
||||
const imageUrl = imageURL(resource.previewUrl || resource.assetUrl || resource.animationUrl);
|
||||
return (
|
||||
<span className={styles.giftDraftThumb}>
|
||||
{imageUrl ? <img alt="" src={imageUrl} /> : <CardGiftcardOutlined fontSize="small" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -308,6 +308,82 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.giftDraftList {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.giftDraftItem {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.giftDraftHeader {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
grid-template-columns: 44px minmax(0, 1fr) var(--control-height);
|
||||
}
|
||||
|
||||
.giftDraftThumb {
|
||||
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);
|
||||
}
|
||||
|
||||
.giftDraftThumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.giftDraftTitle {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.giftDraftTitle span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.giftDraftTitle span:first-child {
|
||||
color: var(--text-primary);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.giftDraftTitle span:last-child {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.giftDraftEmpty {
|
||||
display: flex;
|
||||
min-height: 120px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card-strong);
|
||||
color: var(--text-tertiary);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.batchInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -299,6 +299,7 @@ export const API_OPERATIONS = {
|
||||
updateHostAgencyPolicy: "updateHostAgencyPolicy",
|
||||
updateHumanRoomRobotConfig: "updateHumanRoomRobotConfig",
|
||||
updateInviteActivityRewardConfig: "updateInviteActivityRewardConfig",
|
||||
updateManager: "updateManager",
|
||||
updateMenu: "updateMenu",
|
||||
updateMenuVisible: "updateMenuVisible",
|
||||
updatePermission: "updatePermission",
|
||||
@ -2329,6 +2330,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "invite-activity-reward:update",
|
||||
permissions: ["invite-activity-reward:update"]
|
||||
},
|
||||
updateManager: {
|
||||
method: "PUT",
|
||||
operationId: API_OPERATIONS.updateManager,
|
||||
path: "/v1/admin/managers/{user_id}",
|
||||
permission: "bd:update",
|
||||
permissions: ["bd:update"]
|
||||
},
|
||||
updateMenu: {
|
||||
method: "PATCH",
|
||||
operationId: API_OPERATIONS.updateMenu,
|
||||
|
||||
69
src/shared/api/generated/schema.d.ts
vendored
69
src/shared/api/generated/schema.d.ts
vendored
@ -676,6 +676,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/managers/{user_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put: operations["updateManager"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/agencies": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -3997,6 +4013,16 @@ export interface components {
|
||||
Manager: {
|
||||
avatar?: string;
|
||||
bdLeaderCount?: number;
|
||||
canAddAdmin?: boolean;
|
||||
canAddBdLeader?: boolean;
|
||||
canAddSuperadmin?: boolean;
|
||||
canBlockUser?: boolean;
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
createdAtMs?: number;
|
||||
contact?: string;
|
||||
displayUserId?: string;
|
||||
@ -4010,9 +4036,34 @@ export interface components {
|
||||
username?: string;
|
||||
};
|
||||
CreateManagerPayload: {
|
||||
canAddAdmin?: boolean;
|
||||
canAddBdLeader?: boolean;
|
||||
canAddSuperadmin?: boolean;
|
||||
canBlockUser?: boolean;
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
contact?: string;
|
||||
targetUserId: string;
|
||||
};
|
||||
UpdateManagerPayload: {
|
||||
canAddAdmin?: boolean;
|
||||
canAddBdLeader?: boolean;
|
||||
canAddSuperadmin?: boolean;
|
||||
canBlockUser?: boolean;
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
contact?: string;
|
||||
/** @enum {string} */
|
||||
status?: "active" | "disabled";
|
||||
};
|
||||
BDProfile: {
|
||||
agencyCount?: number;
|
||||
createdAtMs?: number;
|
||||
@ -5416,6 +5467,24 @@ export interface operations {
|
||||
201: components["responses"]["ManagerResponse"];
|
||||
};
|
||||
};
|
||||
updateManager: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
user_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateManagerPayload"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["ManagerResponse"];
|
||||
};
|
||||
};
|
||||
listAgencies: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
@ -681,6 +681,7 @@ export interface ManagerDto {
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
contact?: string;
|
||||
@ -703,6 +704,7 @@ export interface CreateManagerPayload {
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
contact?: string;
|
||||
@ -717,6 +719,7 @@ export interface UpdateManagerPayload {
|
||||
canGrantAvatarFrame?: boolean;
|
||||
canGrantBadge?: boolean;
|
||||
canGrantVehicle?: boolean;
|
||||
canGrantVip?: boolean;
|
||||
canTransferUserCountry?: boolean;
|
||||
canUpdateUserLevel?: boolean;
|
||||
contact?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user