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)}
>
-
+ {allowH5 ? : null}
{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]) => (