任务
This commit is contained in:
parent
7a6f0db859
commit
103b70f5e5
@ -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", "幸运礼物消耗金币"],
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 })}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
className={styles.flutterJumpField}
|
||||
disabled={disabled}
|
||||
multiline
|
||||
minRows={5}
|
||||
label="Flutter 跳转参数"
|
||||
value={form.flutterJumpParam}
|
||||
onChange={(event) => page.setForm({ ...form, flutterJumpParam: event.target.value })}
|
||||
/>
|
||||
{form.actionType === "none" ? null : (
|
||||
<AppJumpConfigField
|
||||
allowCustom={false}
|
||||
allowH5={false}
|
||||
appParamLabel="APP 公共跳转 JSON"
|
||||
className={styles.flutterJumpField}
|
||||
disabled={disabled}
|
||||
paramValue={form.appJumpParam}
|
||||
required
|
||||
typeLabel="跳转协议"
|
||||
typeValue="app"
|
||||
onChange={({ param }) =>
|
||||
page.setForm({
|
||||
...form,
|
||||
actionType: page.appJumpTypeFromParam(param),
|
||||
appJumpParam: param,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="有效期">
|
||||
|
||||
@ -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"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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"}');
|
||||
});
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
<MenuItem value="h5">H5</MenuItem>
|
||||
{allowH5 ? <MenuItem value="h5">H5</MenuItem> : null}
|
||||
<MenuItem value="app">APP</MenuItem>
|
||||
</TextField>
|
||||
{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]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user