任务tab

This commit is contained in:
zhx 2026-05-08 22:50:04 +08:00
parent 720ddc36ad
commit 1d67b3aa97

View File

@ -16,8 +16,8 @@ import {
Spin,
Switch,
Table,
Tabs,
TabPane,
Tabs,
Tag,
} from 'antdv-next';
@ -32,20 +32,25 @@ import { getAllowedSysOrigins } from '#/views/system/shared';
defineOptions({ name: 'ResidentDailyTaskConfigPage' });
let localRowId = 0;
let localConditionJumpRowId = 0;
const rechargeJumpPage = '/main/me/wallet/recharge';
function createRowKey(seed?: number | string) {
localRowId += 1;
return `daily-task-${seed || localRowId}-${localRowId}`;
}
function createConditionJumpRowKey(seed?: number | string) {
localConditionJumpRowId += 1;
return `condition-jump-${seed || localConditionJumpRowId}-${localConditionJumpRowId}`;
}
function createTask(sortOrder = 10) {
return {
conditionType: 'MIC_DURATION_SECONDS',
cover: '',
enabled: true,
id: null,
jumpPage: '',
jumpType: 'ROOM_RANDOM_WITH_MIC',
rewardGold: 0,
rowKey: createRowKey(),
sortOrder,
@ -62,6 +67,22 @@ function defaultTasks() {
return [createTask(10)];
}
function defaultJumpType(conditionType: string) {
return conditionType === 'RECHARGE_GOLD' ? 'RECHARGE_IN_APP' : 'ROOM_RANDOM_WITH_MIC';
}
function createConditionJump(sortOrder = 10, conditionType = 'MIC_DURATION_SECONDS') {
return {
conditionType,
id: null,
jumpPage: '',
jumpType: defaultJumpType(conditionType),
rowKey: createConditionJumpRowKey(conditionType),
sortOrder,
taskCategory: '',
};
}
const conditionTypeOptions = [
{ label: '麦克风时长(秒)', value: 'MIC_DURATION_SECONDS' },
{ label: '游戏消耗金币', value: 'GAME_CONSUME_GOLD' },
@ -77,6 +98,7 @@ const taskCategoryOptions = [
const jumpTypeOptions = [
{ label: '随机上麦房间', value: 'ROOM_RANDOM_WITH_MIC' },
{ label: '随机房间', value: 'ROOM_RANDOM' },
{ label: '充值页', value: 'RECHARGE_IN_APP' },
{ label: '端内路由', value: 'APP_ROUTE' },
{ label: 'H5 链接', value: 'H5_URL' },
{ label: '无跳转', value: 'NONE' },
@ -97,12 +119,18 @@ const taskColumns = [
{ dataIndex: 'targetValue', key: 'targetValue', title: '目标', width: 130 },
{ dataIndex: 'targetUnit', key: 'targetUnit', title: '单位', width: 110 },
{ dataIndex: 'rewardGold', key: 'rewardGold', title: '奖励金币', width: 140 },
{ dataIndex: 'jumpType', key: 'jumpType', title: '跳转', width: 180 },
{ dataIndex: 'jumpPage', key: 'jumpPage', title: '跳转参数', width: 220 },
{ dataIndex: 'sortOrder', key: 'sortOrder', title: '排序', width: 110 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 90 },
];
const conditionJumpColumns = [
{ dataIndex: 'conditionType', key: 'conditionType', title: '条件', width: 220 },
{ dataIndex: 'jumpType', key: 'jumpType', title: '跳转', width: 190 },
{ dataIndex: 'jumpPage', key: 'jumpPage', title: '跳转参数', width: 320 },
{ dataIndex: 'sortOrder', key: 'sortOrder', title: '排序', width: 120 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 90 },
];
const claimColumns = [
{ dataIndex: 'userId', key: 'userId', title: '用户ID', width: 140 },
{ dataIndex: 'taskCode', key: 'taskCode', title: '任务编码', width: 220 },
@ -141,6 +169,7 @@ const claimLoading = ref(false);
const eventLoading = ref(false);
const form = reactive<Record<string, any>>({
conditionJumps: [],
configured: false,
enabled: false,
id: 0,
@ -195,6 +224,42 @@ const activeTaskCategoryLabel = computed(() => {
return taskCategoryOptions.find((item) => item.value === activeTaskCategory.value)?.label || '每日任务';
});
function normalizeConditionJumpRows(
rows: Array<Record<string, any>>,
tasks: Array<Record<string, any>>,
) {
const result = rows.map((item: Record<string, any>, index: number) => {
const conditionType = String(item.conditionType || 'MIC_DURATION_SECONDS');
return {
conditionType,
id: item.id ?? null,
jumpPage: String(item.jumpPage || ''),
jumpType: String(item.jumpType || defaultJumpType(conditionType)),
rowKey: createConditionJumpRowKey(item.id || conditionType || index),
sortOrder: Number(item.sortOrder || (index + 1) * 10),
taskCategory: String(item.taskCategory || activeTaskCategory.value),
};
});
const seenConditions = new Set(result.map((item) => String(item.conditionType || '')));
for (const task of tasks) {
const conditionType = String(task.conditionType || '').trim();
if (!conditionType || seenConditions.has(conditionType)) {
continue;
}
result.push(createConditionJump((result.length + 1) * 10, conditionType));
seenConditions.add(conditionType);
}
if (result.length === 0) {
result.push(createConditionJump(10));
}
return result.toSorted((left: Record<string, any>, right: Record<string, any>) =>
Number(left.sortOrder || 0) - Number(right.sortOrder || 0),
);
}
function normalizeConfig(result: null | Record<string, any> | undefined) {
const value = result || {};
form.configured = Boolean(value.configured);
@ -207,14 +272,12 @@ function normalizeConfig(result: null | Record<string, any> | undefined) {
const rows = Array.isArray(value.tasks) && value.tasks.length > 0
? value.tasks
: defaultTasks();
form.tasks = rows
const normalizedTasks = rows
.map((item: Record<string, any>, index: number) => ({
conditionType: String(item.conditionType || 'MIC_DURATION_SECONDS'),
cover: String(item.cover || ''),
enabled: item.enabled !== false,
id: item.id ?? null,
jumpPage: String(item.jumpPage || ''),
jumpType: String(item.jumpType || 'ROOM_RANDOM_WITH_MIC'),
rewardGold: Number(item.rewardGold || 0),
rowKey: createRowKey(item.id || index),
sortOrder: Number(item.sortOrder || (index + 1) * 10),
@ -225,9 +288,14 @@ function normalizeConfig(result: null | Record<string, any> | undefined) {
taskIcon: String(item.taskIcon || ''),
taskName: String(item.taskName || ''),
}))
.sort((left: Record<string, any>, right: Record<string, any>) =>
.toSorted((left: Record<string, any>, right: Record<string, any>) =>
Number(left.sortOrder || 0) - Number(right.sortOrder || 0),
);
form.tasks = normalizedTasks;
form.conditionJumps = normalizeConditionJumpRows(
Array.isArray(value.conditionJumps) ? value.conditionJumps : [],
normalizedTasks,
);
}
async function loadConfig() {
@ -256,6 +324,108 @@ function removeTask(index: number) {
(form.tasks as Array<Record<string, any>>).splice(index, 1);
}
function addConditionJump() {
const rows = form.conditionJumps as Array<Record<string, any>>;
const usedConditions = new Set(rows.map((item) => String(item.conditionType || '')));
const option = conditionTypeOptions.find((item) => !usedConditions.has(item.value));
if (!option) {
message.warning('可配置的条件都已添加');
return;
}
const maxSort = Math.max(0, ...rows.map((item) => Number(item.sortOrder || 0)));
rows.push(createConditionJump(maxSort + 10, option.value));
}
function removeConditionJump(index: number) {
(form.conditionJumps as Array<Record<string, any>>).splice(index, 1);
}
function shouldReplaceJumpPage(value: unknown) {
const jumpPage = String(value || '').trim();
return !jumpPage || jumpPage === '/room' || jumpPage === rechargeJumpPage;
}
function jumpTypeRequiresPage(jumpType: string) {
return jumpType === 'APP_ROUTE' || jumpType === 'H5_URL';
}
function taskAt(index: number) {
return (form.tasks as Array<Record<string, any>>)[index];
}
function conditionJumpAt(index: number) {
return (form.conditionJumps as Array<Record<string, any>>)[index];
}
function jumpPagePlaceholder(record: Record<string, any>) {
const jumpType = String(record.jumpType || '').trim();
if (jumpType === 'RECHARGE_IN_APP') {
return rechargeJumpPage;
}
if (jumpType === 'H5_URL') {
return 'https://example.com';
}
if (jumpType === 'APP_ROUTE') {
return '/main/me/wallet/recharge';
}
if (jumpType === 'ROOM_RANDOM' || jumpType === 'ROOM_RANDOM_WITH_MIC') {
return '/room';
}
return '';
}
function ensureConditionJumpForCondition(conditionType: string) {
const normalizedCondition = String(conditionType || '').trim();
if (!normalizedCondition) {
return;
}
const rows = form.conditionJumps as Array<Record<string, any>>;
if (rows.some((item) => String(item.conditionType || '') === normalizedCondition)) {
return;
}
const maxSort = Math.max(0, ...rows.map((item) => Number(item.sortOrder || 0)));
rows.push(createConditionJump(maxSort + 10, normalizedCondition));
}
function handleConditionTypeChange(index: number, value: unknown) {
const record = taskAt(index);
if (!record) {
return;
}
record.conditionType = String(value || record.conditionType || '').trim();
ensureConditionJumpForCondition(record.conditionType);
if (
record.conditionType === 'RECHARGE_GOLD' &&
(!record.targetUnit || record.targetUnit === 'second')
) {
record.targetUnit = 'gold';
}
}
function handleConditionJumpTypeChange(index: number, value: unknown) {
const record = conditionJumpAt(index);
if (!record) {
return;
}
record.jumpType = String(value || record.jumpType || '').trim();
if (shouldReplaceJumpPage(record.jumpPage)) {
record.jumpPage = '';
}
}
function handleConditionJumpConditionChange(index: number, value: unknown) {
const record = conditionJumpAt(index);
if (!record) {
return;
}
const conditionType = String(value || record.conditionType || '').trim();
record.conditionType = conditionType;
record.jumpType = defaultJumpType(conditionType);
if (shouldReplaceJumpPage(record.jumpPage)) {
record.jumpPage = '';
}
}
function confirmRemoveTask(index: number) {
Modal.confirm({
onOk: () => removeTask(index),
@ -273,6 +443,7 @@ function validateBeforeSave() {
for (const item of tasks) {
const taskCode = String(item.taskCode || '').trim();
const taskName = String(item.taskName || '').trim();
const conditionType = String(item.conditionType || '').trim();
if (!taskCode) {
message.warning('任务编码不能为空');
return false;
@ -281,6 +452,10 @@ function validateBeforeSave() {
message.warning('任务名称不能为空');
return false;
}
if (!conditionType) {
message.warning('任务条件不能为空');
return false;
}
if (Number(item.targetValue || 0) <= 0) {
message.warning('任务目标必须大于 0');
return false;
@ -295,6 +470,39 @@ function validateBeforeSave() {
}
seenCodes.add(taskCode);
}
const conditionJumps = form.conditionJumps as Array<Record<string, any>>;
const seenConditions = new Set<string>();
for (const item of conditionJumps) {
const conditionType = String(item.conditionType || '').trim();
const jumpType = String(item.jumpType || '').trim();
const jumpPage = String(item.jumpPage || '').trim();
if (!conditionType) {
message.warning('条件跳转的条件不能为空');
return false;
}
if (seenConditions.has(conditionType)) {
message.warning('条件跳转的条件不能重复');
return false;
}
if (!jumpType) {
message.warning('条件跳转的跳转类型不能为空');
return false;
}
if (jumpTypeRequiresPage(jumpType) && !jumpPage) {
message.warning('端内路由和 H5 链接需要填写跳转参数');
return false;
}
seenConditions.add(conditionType);
}
for (const item of tasks) {
const conditionType = String(item.conditionType || '').trim();
if (!seenConditions.has(conditionType)) {
message.warning('请先配置任务条件对应的跳转类型');
return false;
}
}
return true;
}
@ -314,8 +522,6 @@ async function submitForm() {
cover: String(item.cover || '').trim(),
enabled: item.enabled !== false,
id: item.id || 0,
jumpPage: String(item.jumpPage || '').trim(),
jumpType: String(item.jumpType || '').trim(),
rewardGold: Number(item.rewardGold || 0),
sortOrder: Number(item.sortOrder || 0),
targetUnit: String(item.targetUnit || '').trim(),
@ -325,6 +531,17 @@ async function submitForm() {
taskIcon: String(item.taskIcon || '').trim(),
taskName: String(item.taskName || '').trim(),
})),
conditionJumps: (form.conditionJumps || []).map((item: Record<string, any>) => {
const jumpType = String(item.jumpType || '').trim();
return {
conditionType: String(item.conditionType || '').trim(),
id: item.id || 0,
jumpPage: String(item.jumpPage || '').trim(),
jumpType,
sortOrder: Number(item.sortOrder || 0),
taskCategory: activeTaskCategory.value,
};
}),
timezone: String(form.timezone || '').trim(),
});
message.success('保存成功');
@ -457,7 +674,7 @@ watch(activeTaskCategory, () => {
<Button :loading="loading" @click="loadConfig">刷新配置</Button>
</div>
<Tabs v-model:activeKey="activeTaskCategory" class="category-tabs">
<Tabs v-model:active-key="activeTaskCategory" class="category-tabs">
<TabPane
v-for="item in taskCategoryOptions"
:key="item.value"
@ -465,7 +682,7 @@ watch(activeTaskCategory, () => {
/>
</Tabs>
<Tabs v-model:activeKey="activeTab">
<Tabs v-model:active-key="activeTab">
<TabPane key="config" tab="任务配置">
<div class="config-row">
<Space align="center" wrap>
@ -482,7 +699,7 @@ watch(activeTaskCategory, () => {
:data-source="form.tasks"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 1850 }"
:scroll="{ x: 1350 }"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'enabled'">
@ -505,6 +722,7 @@ watch(activeTaskCategory, () => {
v-model:value="record.conditionType"
:options="conditionTypeOptions"
style="width: 170px"
@change="(value) => handleConditionTypeChange(index, value)"
/>
</template>
<template v-else-if="column.key === 'targetValue'">
@ -530,16 +748,6 @@ watch(activeTaskCategory, () => {
style="width: 110px"
/>
</template>
<template v-else-if="column.key === 'jumpType'">
<Select
v-model:value="record.jumpType"
:options="jumpTypeOptions"
style="width: 160px"
/>
</template>
<template v-else-if="column.key === 'jumpPage'">
<Input v-model:value="record.jumpPage" placeholder="/room" />
</template>
<template v-else-if="column.key === 'sortOrder'">
<InputNumber
v-model:value="record.sortOrder"
@ -561,6 +769,60 @@ watch(activeTaskCategory, () => {
</div>
</TabPane>
<TabPane key="conditionJumps" tab="条件跳转类型">
<Table
:columns="conditionJumpColumns"
:data-source="form.conditionJumps"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 940 }"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'conditionType'">
<Select
v-model:value="record.conditionType"
:options="conditionTypeOptions"
style="width: 190px"
@change="(value) => handleConditionJumpConditionChange(index, value)"
/>
</template>
<template v-else-if="column.key === 'jumpType'">
<Select
v-model:value="record.jumpType"
:options="jumpTypeOptions"
style="width: 160px"
@change="(value) => handleConditionJumpTypeChange(index, value)"
/>
</template>
<template v-else-if="column.key === 'jumpPage'">
<Input
v-model:value="record.jumpPage"
:placeholder="jumpPagePlaceholder(record)"
/>
</template>
<template v-else-if="column.key === 'sortOrder'">
<InputNumber
v-model:value="record.sortOrder"
:precision="0"
style="width: 80px"
/>
</template>
<template v-else-if="column.key === 'actions'">
<Button danger type="link" @click="removeConditionJump(index)">
删除
</Button>
</template>
</template>
</Table>
<div class="actions">
<Button @click="addConditionJump">新增条件</Button>
<Button :loading="saving" type="primary" @click="submitForm">
保存配置
</Button>
</div>
</TabPane>
<TabPane key="claims" tab="领取记录">
<div class="toolbar">
<Input