From 103b70f5e5f6e2de1431ba2892405ca2766b87ed Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 11 Jun 2026 18:40:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/daily-tasks/constants.js | 14 ++- .../daily-tasks/hooks/useDailyTasksPage.js | 94 ++++++++++++------- .../daily-tasks/pages/DailyTaskListPage.jsx | 32 +++++-- src/features/daily-tasks/schema.ts | 35 ++++++- src/features/daily-tasks/taskPayload.test.js | 28 +++--- src/shared/ui/AppJumpConfigField.jsx | 13 ++- 6 files changed, 143 insertions(+), 73 deletions(-) diff --git a/src/features/daily-tasks/constants.js b/src/features/daily-tasks/constants.js index 8e8ddf8..32406a5 100644 --- a/src/features/daily-tasks/constants.js +++ b/src/features/daily-tasks/constants.js @@ -42,17 +42,19 @@ export const taskAudienceLabels = { export const taskActionOptions = [ ["none", "无跳转"], - ["room_random", "随机房间"], - ["room_random_game", "随机房间并打开游戏"], - ["wallet", "钱包"], - ["gift_panel", "礼物面板"], - ["gift_panel_specific", "指定礼物面板"], + ["app", "APP 公共跳转"], ]; export const taskActionLabels = { + app: "APP 公共跳转", + explore_game: "Explore 游戏 H5", gift_panel: "礼物面板", gift_panel_specific: "指定礼物面板", + room: "指定房间", + room_game: "指定房内拉起游戏", none: "无跳转", + room_random_window: "随机房内拉起窗口", + room_window: "指定房内拉起窗口", room_random: "随机房间", room_random_game: "随机房间并打开游戏", wallet: "钱包", @@ -60,6 +62,8 @@ export const taskActionLabels = { export const taskMetricOptions = [ ["game_spend_coin", "游戏消耗金币"], + ["game_dice_play_count", "玩 Dice Battle 局数"], + ["game_rock_play_count", "玩 Rock Paper Scissors 局数"], ["gift_spend_coin", "普通礼物消耗金币"], ["gift_send_count", "赠送礼物次数"], ["lucky_gift_spend_coin", "幸运礼物消耗金币"], diff --git a/src/features/daily-tasks/hooks/useDailyTasksPage.js b/src/features/daily-tasks/hooks/useDailyTasksPage.js index b7c36c2..6ffd076 100644 --- a/src/features/daily-tasks/hooks/useDailyTasksPage.js +++ b/src/features/daily-tasks/hooks/useDailyTasksPage.js @@ -10,21 +10,22 @@ import { } from "@/features/daily-tasks/api"; import { useDailyTaskAbilities } from "@/features/daily-tasks/permissions.js"; import { dailyTaskFormSchema } from "@/features/daily-tasks/schema"; +import { buildAppJumpParam } from "@/shared/app-jump/appJumpConfig.js"; const pageSize = 50; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyForm = (task = {}) => { const actionType = task.actionType || "none"; - const flutterJumpParam = taskFlutterJumpParam(actionType, task.actionParam, task.actionPayloadJson); + const appJumpParam = taskAppJumpParam(actionType, task.actionParam, task.actionPayloadJson); return { + appJumpParam, actionParam: task.actionParam || "", - actionType, + actionType: actionType === "none" ? "none" : appJumpTypeFromParam(appJumpParam), category: task.category || "", description: task.description || "", effectiveFrom: msToDatetimeLocal(task.effectiveFromMs), effectiveTo: msToDatetimeLocal(task.effectiveToMs), - flutterJumpParam, iconUrl: task.iconUrl || "", metricType: task.metricType || "", rewardCoinAmount: task.rewardCoinAmount === 0 || task.rewardCoinAmount ? String(task.rewardCoinAmount) : "", @@ -102,11 +103,12 @@ export function useDailyTasksPage() { }; const changeFormActionType = (actionType) => { + const nextAppJumpParam = actionType === "app" ? buildAppJumpParam({ target: "wallet" }) : ""; setForm((current) => ({ ...current, actionParam: "", - actionType, - flutterJumpParam: defaultFlutterJumpParam(actionType), + actionType: actionType === "app" ? "wallet" : "none", + appJumpParam: nextAppJumpParam, })); }; @@ -158,6 +160,7 @@ export function useDailyTasksPage() { return { abilities, activeAction, + appJumpTypeFromParam, changeFormActionType, changeQuery, changeStatus, @@ -185,16 +188,15 @@ export function useDailyTasksPage() { } export function buildTaskPayload(form) { - // 表单不再暴露“跳转扩展 JSON”,但后端和 App 仍读取 action_payload_json。 - // 这里把可编辑的 Flutter 跳转参数压缩成对象 JSON,既保留后台配置能力,也避免 UI 暴露底层字段名。 - const actionPayloadJSON = normalizedJSONObject(form.flutterJumpParam || defaultFlutterJumpParam(form.actionType)); + // 每日任务现在和 banner/弹窗共用公共 APP 跳转 JSON;action_type/action_param 继续回填短字段,保证旧 H5 或旧 App 只读短字段时仍能打开。 + const appJumpEnabled = form.actionType !== "none"; + const actionPayloadJSON = appJumpEnabled ? normalizedJSONObject(form.appJumpParam || defaultAppJumpParam()) : "{}"; + const actionType = appJumpEnabled ? appJumpTypeFromParam(actionPayloadJSON) : "none"; return { - // room_random_game / gift_panel_specific 的旧 App 合约仍从 action_param 读取 game_id/gift_id。 - // 新表单只让运营维护一份 Flutter 参数,所以提交前从对象里反抽标量,保证新旧字段保持同一含义。 - action_param: actionParamFromFlutterJump(form.actionType, actionPayloadJSON, form.actionParam), + action_param: appJumpEnabled ? actionParamFromAppJump(actionPayloadJSON, form.actionParam) : "", action_payload_json: actionPayloadJSON, - action_type: form.actionType, + action_type: actionType, // 人群、维度过滤、图标键已从表单移除:任务默认全量用户可见,不做事件维度限制,图标只使用上传 URL。 audience_type: "all", category: hiddenTaskCategory(form), @@ -215,42 +217,62 @@ export function buildTaskPayload(form) { }; } -export function defaultFlutterJumpParam(actionType) { - const template = - { - gift_panel: { route: "gift_panel" }, - gift_panel_specific: { gift_id: "", route: "gift_panel" }, - none: {}, - room_random: { route: "room_random" }, - room_random_game: { game_id: "", route: "room_random", show_game: true }, - wallet: { route: "wallet" }, - }[actionType] || {}; - - return JSON.stringify(template, null, 2); +export function defaultAppJumpParam() { + return buildAppJumpParam({ target: "wallet" }); } -function taskFlutterJumpParam(actionType, actionParam, actionPayloadJSON) { +function taskAppJumpParam(actionType, actionParam, actionPayloadJSON) { const parsed = parseObjectJSON(actionPayloadJSON); - const payload = Object.keys(parsed).length > 0 ? parsed : parseObjectJSON(defaultFlutterJumpParam(actionType)); + if (Object.keys(parsed).length > 0 && String(parsed.type || "").trim()) { + return JSON.stringify(parsed); + } const scalarParam = String(actionParam || "").trim(); - if (actionType === "room_random_game" && scalarParam && !payload.game_id) { - payload.game_id = scalarParam; + switch (String(actionType || "").trim()) { + case "wallet": + return buildAppJumpParam({ target: "wallet" }); + case "room_random": + return buildAppJumpParam({ target: "room_random" }); + case "room_random_game": + return buildAppJumpParam({ gameId: scalarParam, target: "room_random_game" }); + case "gift_panel": + return buildAppJumpParam({ target: "room_random_window", window: "gift_panel" }); + case "gift_panel_specific": + return buildAppJumpParam({ giftId: scalarParam, target: "room_random_window", window: "gift_panel" }); + case "explore_game": + return buildAppJumpParam({ gameCode: scalarParam, target: "explore_game" }); + case "room": + case "room_window": + case "room_random_window": + case "room_game": + return buildAppJumpParam({ target: actionType }); + default: + return ""; } - if (actionType === "gift_panel_specific" && scalarParam && !payload.gift_id) { - payload.gift_id = scalarParam; - } - - return JSON.stringify(payload, null, 2); } -function actionParamFromFlutterJump(actionType, actionPayloadJSON, fallback = "") { +export function appJumpTypeFromParam(actionPayloadJSON) { const payload = parseObjectJSON(actionPayloadJSON); + return String(payload.type || "").trim() || "wallet"; +} - if (actionType === "room_random_game") { +function actionParamFromAppJump(actionPayloadJSON, fallback = "") { + const payload = parseObjectJSON(actionPayloadJSON); + const target = String(payload.type || "").trim(); + + if (target === "explore_game") { + return String(payload.game_code || payload.gameCode || fallback || "").trim(); + } + if (target === "room_game" || target === "room_random_game") { return String(payload.game_id || payload.gameId || fallback || "").trim(); } - if (actionType === "gift_panel_specific") { + if (target === "room" || target === "room_window") { + return String(payload.room_id || payload.roomId || fallback || "").trim(); + } + if (target === "room_window" || target === "room_random_window") { + if (String(payload.window || "").trim() === "game_window") { + return String(payload.game_id || payload.gameId || fallback || "").trim(); + } return String(payload.gift_id || payload.giftId || fallback || "").trim(); } return String(fallback || "").trim(); diff --git a/src/features/daily-tasks/pages/DailyTaskListPage.jsx b/src/features/daily-tasks/pages/DailyTaskListPage.jsx index f35e5a3..531c443 100644 --- a/src/features/daily-tasks/pages/DailyTaskListPage.jsx +++ b/src/features/daily-tasks/pages/DailyTaskListPage.jsx @@ -21,6 +21,7 @@ import { AdminRowActions, } from "@/shared/ui/AdminListLayout.jsx"; import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { AppJumpConfigField } from "@/shared/ui/AppJumpConfigField.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { formatMillis } from "@/shared/utils/time.js"; import { @@ -384,7 +385,7 @@ function TaskFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open label="跳转类型" required select - value={form.actionType} + value={form.actionType === "none" ? "none" : "app"} onChange={(event) => page.changeFormActionType(event.target.value)} > {taskActionOptions.map(([value, label]) => ( @@ -402,15 +403,26 @@ function TaskFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open onChange={(iconUrl) => page.setForm({ ...form, iconUrl })} /> - page.setForm({ ...form, flutterJumpParam: event.target.value })} - /> + {form.actionType === "none" ? null : ( + + page.setForm({ + ...form, + actionType: page.appJumpTypeFromParam(param), + appJumpParam: param, + }) + } + /> + )} diff --git a/src/features/daily-tasks/schema.ts b/src/features/daily-tasks/schema.ts index 95908f4..ddd84e3 100644 --- a/src/features/daily-tasks/schema.ts +++ b/src/features/daily-tasks/schema.ts @@ -1,14 +1,29 @@ import { z } from "zod"; +import { validatePublicAppJumpParam } from "@/shared/app-jump/appJumpConfig.js"; + +const taskActionTypes = [ + "none", + "wallet", + "room", + "room_random", + "room_window", + "room_random_window", + "room_game", + "room_random_game", + "explore_game", + "gift_panel", + "gift_panel_specific", +] as const; export const dailyTaskFormSchema = z .object({ + appJumpParam: z.string().trim().max(2048, "APP 跳转参数不能超过 2048 个字符").optional(), actionParam: z.string().trim().max(512, "跳转参数不能超过 512 个字符").optional(), - actionType: z.enum(["none", "room_random", "room_random_game", "wallet", "gift_panel", "gift_panel_specific"]), + actionType: z.enum(taskActionTypes), category: z.string().trim().max(32, "任务分类不能超过 32 个字符").optional(), description: z.string().trim().max(512, "描述不能超过 512 个字符"), effectiveFrom: z.string().optional(), effectiveTo: z.string().optional(), - flutterJumpParam: z.string().trim().optional(), iconUrl: z.string().trim().max(512, "图标 URL 不能超过 512 个字符"), metricType: z.string().trim().min(1, "请输入指标类型").max(64, "指标类型不能超过 64 个字符"), rewardCoinAmount: z.union([z.string(), z.number()]), @@ -44,11 +59,21 @@ export const dailyTaskFormSchema = z if (effectiveFromMs > 0 && effectiveToMs > 0 && effectiveToMs <= effectiveFromMs) { context.addIssue({ code: "custom", message: "结束时间必须晚于开始时间", path: ["effectiveTo"] }); } - if (!isJSONObject(value.flutterJumpParam)) { + if (value.actionType !== "none") { + const validation = validatePublicAppJumpParam(value.appJumpParam || ""); + if (!validation.valid) { + context.addIssue({ + code: "custom", + message: validation.message, + path: ["appJumpParam"], + }); + } + } + if (!isJSONObject(value.appJumpParam)) { context.addIssue({ code: "custom", - message: "Flutter 跳转参数必须是对象格式", - path: ["flutterJumpParam"], + message: "APP 跳转参数必须是对象格式", + path: ["appJumpParam"], }); } }); diff --git a/src/features/daily-tasks/taskPayload.test.js b/src/features/daily-tasks/taskPayload.test.js index 2676615..ee8ba83 100644 --- a/src/features/daily-tasks/taskPayload.test.js +++ b/src/features/daily-tasks/taskPayload.test.js @@ -1,38 +1,38 @@ import { expect, test } from "vitest"; import { parseForm } from "@/shared/forms/validation"; -import { buildTaskPayload, defaultFlutterJumpParam } from "./hooks/useDailyTasksPage.js"; +import { buildTaskPayload, defaultAppJumpParam } from "./hooks/useDailyTasksPage.js"; import { dailyTaskFormSchema } from "./schema"; -test("daily task payload hides removed admin fields and keeps Flutter jump params editable", () => { +test("daily task payload stores public app jump params for explore game", () => { const form = parseForm(dailyTaskFormSchema, { - actionType: "room_random_game", + actionType: "explore_game", + appJumpParam: '{"type":"explore_game","game_code":"game_dice"}', description: "", effectiveFrom: "", effectiveTo: "", - flutterJumpParam: JSON.stringify({ game_id: "dice", route: "room_random", show_game: true }, null, 2), iconUrl: "https://media.haiyihy.com/admin/images/task-game.png", - metricType: "game_spend_coin", + metricType: "game_dice_play_count", rewardCoinAmount: "20", sortOrder: "3", status: "active", - targetUnit: "coin", - targetValue: "100", + targetUnit: "count", + targetValue: "1", taskType: "daily", - title: "玩游戏消耗金币", + title: "玩 Dice Battle 1 局", }); const payload = buildTaskPayload(form); - expect(payload.category).toBe("game_spend_coin"); + expect(payload.category).toBe("game_dice_play_count"); expect(payload.audience_type).toBe("all"); expect(payload.dimension_filter_json).toBe("{}"); expect(payload.icon_key).toBe(""); expect(payload.icon_url).toBe("https://media.haiyihy.com/admin/images/task-game.png"); - expect(payload.action_param).toBe("dice"); - expect(payload.action_payload_json).toBe('{"game_id":"dice","route":"room_random","show_game":true}'); + expect(payload.action_type).toBe("explore_game"); + expect(payload.action_param).toBe("game_dice"); + expect(payload.action_payload_json).toBe('{"type":"explore_game","game_code":"game_dice"}'); }); -test("daily task action type selection provides concrete Flutter jump template", () => { - expect(defaultFlutterJumpParam("gift_panel_specific")).toContain('"gift_id": ""'); - expect(defaultFlutterJumpParam("wallet")).toContain('"route": "wallet"'); +test("daily task action type selection provides public app jump template", () => { + expect(defaultAppJumpParam()).toBe('{"type":"wallet"}'); }); diff --git a/src/shared/ui/AppJumpConfigField.jsx b/src/shared/ui/AppJumpConfigField.jsx index 7b7458e..5a240df 100644 --- a/src/shared/ui/AppJumpConfigField.jsx +++ b/src/shared/ui/AppJumpConfigField.jsx @@ -13,6 +13,8 @@ import { AdminFormFieldGrid } from "@/shared/ui/AdminFormDialog.jsx"; import styles from "@/shared/ui/AppJumpConfigField.module.css"; export function AppJumpConfigField({ + allowCustom = true, + allowH5 = true, appParamLabel = "APP 参数 JSON", className = "", disabled = false, @@ -25,6 +27,11 @@ export function AppJumpConfigField({ }) { const type = normalizeJumpConfigType(typeValue); const appJump = useMemo(() => parseAppJumpParam(paramValue), [paramValue]); + const appTargetOptions = useMemo( + () => (allowCustom ? appJumpTargetOptions : appJumpTargetOptions.filter(([value]) => value !== "custom")), + [allowCustom], + ); + const appTargetValue = !allowCustom && appJump.target === "custom" ? "wallet" : appJump.target; const setType = (nextType) => { if (nextType === "app") { onChange({ param: buildAppJumpParam({ target: "wallet" }), type: "app" }); @@ -52,7 +59,7 @@ export function AppJumpConfigField({ value={type} onChange={(event) => setType(event.target.value)} > - H5 + {allowH5 ? H5 : null} APP {type === "h5" ? ( @@ -71,10 +78,10 @@ export function AppJumpConfigField({ label="APP 目标" required select - value={appJump.target} + value={appTargetValue} onChange={(event) => setAppJump({ target: event.target.value })} > - {appJumpTargetOptions.map(([value, label]) => ( + {appTargetOptions.map(([value, label]) => ( {label}