diff --git a/apps/src/views/resident-activity/daily-task-config.vue b/apps/src/views/resident-activity/daily-task-config.vue index d3092fa..8ef8921 100644 --- a/apps/src/views/resident-activity/daily-task-config.vue +++ b/apps/src/views/resident-activity/daily-task-config.vue @@ -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>({ + 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>, + tasks: Array>, +) { + const result = rows.map((item: Record, 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, right: Record) => + Number(left.sortOrder || 0) - Number(right.sortOrder || 0), + ); +} + function normalizeConfig(result: null | Record | undefined) { const value = result || {}; form.configured = Boolean(value.configured); @@ -207,14 +272,12 @@ function normalizeConfig(result: null | Record | undefined) { const rows = Array.isArray(value.tasks) && value.tasks.length > 0 ? value.tasks : defaultTasks(); - form.tasks = rows + const normalizedTasks = rows .map((item: Record, 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 | undefined) { taskIcon: String(item.taskIcon || ''), taskName: String(item.taskName || ''), })) - .sort((left: Record, right: Record) => + .toSorted((left: Record, right: Record) => 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>).splice(index, 1); } +function addConditionJump() { + const rows = form.conditionJumps as Array>; + 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>).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>)[index]; +} + +function conditionJumpAt(index: number) { + return (form.conditionJumps as Array>)[index]; +} + +function jumpPagePlaceholder(record: Record) { + 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>; + 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>; + const seenConditions = new Set(); + 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) => { + 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, () => { - + { /> - +
@@ -482,7 +699,7 @@ watch(activeTaskCategory, () => { :data-source="form.tasks" :pagination="false" row-key="rowKey" - :scroll="{ x: 1850 }" + :scroll="{ x: 1350 }" >