941 lines
30 KiB
Vue
941 lines
30 KiB
Vue
<script lang="ts" setup>
|
||
import type { Dayjs } from 'dayjs';
|
||
|
||
import { computed, reactive, ref, watch } from 'vue';
|
||
|
||
import { Page } from '@vben/common-ui';
|
||
import { useAccessStore } from '@vben/stores';
|
||
|
||
import {
|
||
Button,
|
||
Card,
|
||
DatePicker,
|
||
Input,
|
||
InputNumber,
|
||
message,
|
||
Modal,
|
||
Select,
|
||
Space,
|
||
Spin,
|
||
Switch,
|
||
Table,
|
||
TabPane,
|
||
Tabs,
|
||
Tag,
|
||
} from 'antdv-next';
|
||
import dayjs from 'dayjs';
|
||
|
||
import {
|
||
getResidentDailyTaskConfig,
|
||
pageResidentDailyTaskClaimRecords,
|
||
saveResidentDailyTaskConfig,
|
||
} from '#/api/legacy/resident-activity';
|
||
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,
|
||
rewardGold: 0,
|
||
rowKey: createRowKey(),
|
||
sortOrder,
|
||
targetUnit: 'second',
|
||
targetValue: 60,
|
||
taskCode: '',
|
||
taskDesc: '进度 / 目标',
|
||
taskIcon: '',
|
||
taskName: '',
|
||
};
|
||
}
|
||
|
||
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' },
|
||
{ label: '礼物消耗金币', value: 'GIFT_CONSUME_GOLD' },
|
||
{ label: '充值金币', value: 'RECHARGE_GOLD' },
|
||
];
|
||
|
||
const taskCategoryOptions = [
|
||
{ label: '每日任务', value: 'DAILY' },
|
||
{ label: '专属任务', value: 'NEWBIE' },
|
||
];
|
||
|
||
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' },
|
||
];
|
||
|
||
const targetUnitOptions = [
|
||
{ label: '秒', value: 'second' },
|
||
{ label: '次', value: 'count' },
|
||
{ label: '金币', value: 'gold' },
|
||
];
|
||
|
||
const taskColumns = [
|
||
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 80 },
|
||
{ dataIndex: 'taskCode', key: 'taskCode', title: '任务编码', width: 210 },
|
||
{ dataIndex: 'taskName', key: 'taskName', title: '任务名称', width: 220 },
|
||
{ dataIndex: 'taskDesc', key: 'taskDesc', title: '说明', width: 160 },
|
||
{ dataIndex: 'conditionType', key: 'conditionType', title: '条件', width: 190 },
|
||
{ dataIndex: 'targetValue', key: 'targetValue', title: '目标', width: 130 },
|
||
{ dataIndex: 'targetUnit', key: 'targetUnit', title: '单位', width: 110 },
|
||
{ dataIndex: 'rewardGold', key: 'rewardGold', title: '奖励金币', width: 140 },
|
||
{ 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 },
|
||
{ dataIndex: 'taskName', key: 'taskName', title: '任务名称', width: 220 },
|
||
{ dataIndex: 'cycleKey', key: 'cycleKey', title: '周期', width: 130 },
|
||
{ dataIndex: 'rewardGold', key: 'rewardGold', title: '金币', width: 110 },
|
||
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
||
{ dataIndex: 'walletEventId', key: 'walletEventId', title: '钱包事件ID', width: 260 },
|
||
{ dataIndex: 'failureReason', key: 'failureReason', title: '失败原因', width: 220 },
|
||
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
||
];
|
||
|
||
const accessStore = useAccessStore();
|
||
const sysOriginOptions = computed(() => {
|
||
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||
});
|
||
|
||
const selectedSysOrigin = ref('');
|
||
const activeTaskCategory = ref('DAILY');
|
||
const activeTab = ref('config');
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const claimLoading = ref(false);
|
||
|
||
const form = reactive<Record<string, any>>({
|
||
conditionJumps: [],
|
||
configured: false,
|
||
enabled: false,
|
||
id: 0,
|
||
sysOrigin: '',
|
||
taskCategory: 'DAILY',
|
||
tasks: defaultTasks(),
|
||
timezone: 'Asia/Riyadh',
|
||
updateTime: '',
|
||
});
|
||
|
||
const claimQuery = reactive<Record<string, any>>({
|
||
cursor: 1,
|
||
limit: 20,
|
||
status: '',
|
||
taskCode: '',
|
||
userId: '',
|
||
});
|
||
|
||
function defaultClaimDateRange(): [Dayjs, Dayjs] {
|
||
return [dayjs().startOf('day'), dayjs().endOf('day')];
|
||
}
|
||
|
||
const claimDateRange = ref<[Dayjs, Dayjs] | null>(defaultClaimDateRange());
|
||
|
||
const claimPage = reactive<Record<string, any>>({
|
||
current: 1,
|
||
records: [],
|
||
size: 20,
|
||
total: 0,
|
||
});
|
||
|
||
const claimSummary = reactive<Record<string, any>>({
|
||
selectedTaskRewardGold: 0,
|
||
taskCode: '',
|
||
totalRewardGold: 0,
|
||
});
|
||
|
||
const statusMeta = computed(() => {
|
||
if (!form.configured) {
|
||
return { color: 'processing', text: '未配置' };
|
||
}
|
||
if (form.enabled) {
|
||
return { color: 'success', text: '已启用' };
|
||
}
|
||
return { color: 'default', text: '已关闭' };
|
||
});
|
||
|
||
const activeTaskCategoryLabel = computed(() => {
|
||
return taskCategoryOptions.find((item) => item.value === activeTaskCategory.value)?.label || '每日任务';
|
||
});
|
||
|
||
const taskCodeOptions = computed(() => {
|
||
const seen = new Set<string>();
|
||
return ((form.tasks || []) as Array<Record<string, any>>)
|
||
.map((item) => {
|
||
const value = String(item.taskCode || '').trim();
|
||
if (!value || seen.has(value)) {
|
||
return null;
|
||
}
|
||
seen.add(value);
|
||
const taskName = String(item.taskName || '').trim();
|
||
return {
|
||
label: taskName ? `${value} / ${taskName}` : value,
|
||
value,
|
||
};
|
||
})
|
||
.filter(Boolean) as Array<{ label: string; value: string }>;
|
||
});
|
||
|
||
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);
|
||
form.enabled = Boolean(value.enabled);
|
||
form.id = value.id || 0;
|
||
form.sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
|
||
form.taskCategory = activeTaskCategory.value;
|
||
form.timezone = String(value.timezone || 'Asia/Riyadh');
|
||
form.updateTime = String(value.updateTime || '');
|
||
const rows = Array.isArray(value.tasks) && value.tasks.length > 0
|
||
? value.tasks
|
||
: defaultTasks();
|
||
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,
|
||
rewardGold: Number(item.rewardGold || 0),
|
||
rowKey: createRowKey(item.id || index),
|
||
sortOrder: Number(item.sortOrder || (index + 1) * 10),
|
||
targetUnit: String(item.targetUnit || 'second'),
|
||
targetValue: Number(item.targetValue || 0),
|
||
taskCode: String(item.taskCode || ''),
|
||
taskDesc: String(item.taskDesc || '进度 / 目标'),
|
||
taskIcon: String(item.taskIcon || ''),
|
||
taskName: String(item.taskName || ''),
|
||
}))
|
||
.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() {
|
||
if (!selectedSysOrigin.value) {
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try {
|
||
const result = await getResidentDailyTaskConfig(
|
||
selectedSysOrigin.value,
|
||
activeTaskCategory.value,
|
||
);
|
||
normalizeConfig(result);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function addTask() {
|
||
const tasks = form.tasks as Array<Record<string, any>>;
|
||
const maxSort = Math.max(0, ...tasks.map((item) => Number(item.sortOrder || 0)));
|
||
tasks.push(createTask(maxSort + 10));
|
||
}
|
||
|
||
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),
|
||
title: `确认删除该${activeTaskCategoryLabel.value}?`,
|
||
});
|
||
}
|
||
|
||
function validateBeforeSave() {
|
||
const tasks = form.tasks as Array<Record<string, any>>;
|
||
if (tasks.length === 0) {
|
||
message.warning(`请至少配置一个${activeTaskCategoryLabel.value}`);
|
||
return false;
|
||
}
|
||
const seenCodes = new Set<string>();
|
||
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;
|
||
}
|
||
if (!taskName) {
|
||
message.warning('任务名称不能为空');
|
||
return false;
|
||
}
|
||
if (!conditionType) {
|
||
message.warning('任务条件不能为空');
|
||
return false;
|
||
}
|
||
if (Number(item.targetValue || 0) <= 0) {
|
||
message.warning('任务目标必须大于 0');
|
||
return false;
|
||
}
|
||
if (Number(item.rewardGold || 0) <= 0) {
|
||
message.warning('奖励金币必须大于 0');
|
||
return false;
|
||
}
|
||
if (seenCodes.has(taskCode)) {
|
||
message.warning('任务编码不能重复');
|
||
return false;
|
||
}
|
||
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;
|
||
}
|
||
|
||
async function submitForm() {
|
||
if (!validateBeforeSave()) {
|
||
return;
|
||
}
|
||
saving.value = true;
|
||
try {
|
||
const conditionJumpPayloads: Array<Record<string, any>> = (
|
||
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,
|
||
};
|
||
});
|
||
const jumpByCondition = new Map<string, Record<string, any>>(
|
||
conditionJumpPayloads.map((item: Record<string, any>) => [
|
||
String(item.conditionType || ''),
|
||
item,
|
||
]),
|
||
);
|
||
await saveResidentDailyTaskConfig({
|
||
enabled: Boolean(form.enabled),
|
||
id: form.id || 0,
|
||
sysOrigin: selectedSysOrigin.value,
|
||
taskCategory: activeTaskCategory.value,
|
||
tasks: (form.tasks || []).map((item: Record<string, any>) => {
|
||
const conditionType = String(item.conditionType || '').trim();
|
||
const conditionJump = jumpByCondition.get(conditionType);
|
||
return {
|
||
conditionType,
|
||
cover: String(item.cover || '').trim(),
|
||
enabled: item.enabled !== false,
|
||
id: item.id || 0,
|
||
jumpPage: conditionJump?.jumpPage || '',
|
||
jumpType: conditionJump?.jumpType || '',
|
||
rewardGold: Number(item.rewardGold || 0),
|
||
sortOrder: Number(item.sortOrder || 0),
|
||
targetUnit: String(item.targetUnit || '').trim(),
|
||
targetValue: Number(item.targetValue || 0),
|
||
taskCode: String(item.taskCode || '').trim(),
|
||
taskDesc: String(item.taskDesc || '').trim(),
|
||
taskIcon: String(item.taskIcon || '').trim(),
|
||
taskName: String(item.taskName || '').trim(),
|
||
};
|
||
}),
|
||
conditionJumps: conditionJumpPayloads,
|
||
timezone: String(form.timezone || '').trim(),
|
||
});
|
||
message.success('保存成功');
|
||
await loadConfig();
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
function normalizePage(target: Record<string, any>, result: Record<string, any>) {
|
||
target.records = Array.isArray(result?.records) ? result.records : [];
|
||
target.total = Number(result?.total || 0);
|
||
target.current = Number(result?.current || 1);
|
||
target.size = Number(result?.size || 20);
|
||
const summary = result?.summary || {};
|
||
claimSummary.totalRewardGold = Number(summary.totalRewardGold || 0);
|
||
claimSummary.selectedTaskRewardGold = Number(summary.selectedTaskRewardGold || 0);
|
||
claimSummary.taskCode = String(summary.taskCode || '');
|
||
}
|
||
|
||
function claimTimeParams() {
|
||
if (!claimDateRange.value?.[0] || !claimDateRange.value?.[1]) {
|
||
return {};
|
||
}
|
||
return {
|
||
endTime: String(claimDateRange.value[1].valueOf()),
|
||
startTime: String(claimDateRange.value[0].valueOf()),
|
||
};
|
||
}
|
||
|
||
async function loadClaimRecords() {
|
||
if (!selectedSysOrigin.value) {
|
||
return;
|
||
}
|
||
claimLoading.value = true;
|
||
try {
|
||
const result = await pageResidentDailyTaskClaimRecords({
|
||
...claimQuery,
|
||
...claimTimeParams(),
|
||
sysOrigin: selectedSysOrigin.value,
|
||
taskCategory: activeTaskCategory.value,
|
||
});
|
||
normalizePage(claimPage, result || {});
|
||
} finally {
|
||
claimLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function searchClaimRecords() {
|
||
claimQuery.cursor = 1;
|
||
void loadClaimRecords();
|
||
}
|
||
|
||
function resetClaimDateRange() {
|
||
claimDateRange.value = defaultClaimDateRange();
|
||
searchClaimRecords();
|
||
}
|
||
|
||
function handleClaimTableChange(pagination: Record<string, any>) {
|
||
claimQuery.cursor = Number(pagination.current || 1);
|
||
claimQuery.limit = Number(pagination.pageSize || 20);
|
||
void loadClaimRecords();
|
||
}
|
||
|
||
watch(
|
||
sysOriginOptions,
|
||
(options) => {
|
||
if (!selectedSysOrigin.value) {
|
||
selectedSysOrigin.value = String(options[0]?.value || '');
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
watch(
|
||
selectedSysOrigin,
|
||
(value) => {
|
||
if (value) {
|
||
claimQuery.cursor = 1;
|
||
claimQuery.taskCode = '';
|
||
void loadConfig();
|
||
void loadClaimRecords();
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
watch(activeTaskCategory, () => {
|
||
if (!selectedSysOrigin.value) {
|
||
return;
|
||
}
|
||
claimQuery.cursor = 1;
|
||
claimQuery.taskCode = '';
|
||
void loadConfig();
|
||
void loadClaimRecords();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Page auto-content-height>
|
||
<Spin :spinning="loading">
|
||
<Card :bordered="false">
|
||
<template #title>
|
||
<Space align="center" wrap>
|
||
<span>每日任务</span>
|
||
<Tag :color="statusMeta.color">{{ statusMeta.text }}</Tag>
|
||
</Space>
|
||
</template>
|
||
|
||
<div class="toolbar">
|
||
<Select
|
||
v-model:value="selectedSysOrigin"
|
||
:options="sysOriginOptions"
|
||
option-filter-prop="label"
|
||
option-label-prop="label"
|
||
placeholder="请选择系统"
|
||
show-search
|
||
style="width: 180px"
|
||
/>
|
||
<Button :loading="loading" @click="loadConfig">刷新配置</Button>
|
||
</div>
|
||
|
||
<Tabs v-model:active-key="activeTaskCategory" class="category-tabs">
|
||
<TabPane
|
||
v-for="item in taskCategoryOptions"
|
||
:key="item.value"
|
||
:tab="item.label"
|
||
/>
|
||
</Tabs>
|
||
|
||
<Tabs v-model:active-key="activeTab">
|
||
<TabPane key="config" tab="任务配置">
|
||
<div class="config-row">
|
||
<Space align="center" wrap>
|
||
<span class="field-label">启用</span>
|
||
<Switch v-model:checked="form.enabled" />
|
||
<span class="field-label">任务时区</span>
|
||
<Input v-model:value="form.timezone" style="width: 180px" />
|
||
<span class="sub-text">更新时间:{{ form.updateTime || '-' }}</span>
|
||
</Space>
|
||
</div>
|
||
|
||
<Table
|
||
:columns="taskColumns"
|
||
:data-source="form.tasks"
|
||
:pagination="false"
|
||
row-key="rowKey"
|
||
:scroll="{ x: 1350 }"
|
||
>
|
||
<template #bodyCell="{ column, index, record }">
|
||
<template v-if="column.key === 'enabled'">
|
||
<Switch v-model:checked="record.enabled" />
|
||
</template>
|
||
<template v-else-if="column.key === 'taskCode'">
|
||
<Input
|
||
v-model:value="record.taskCode"
|
||
:placeholder="activeTaskCategory === 'DAILY' ? 'DAILY_MIC_DURATION' : 'EXCLUSIVE_RECHARGE'"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'taskName'">
|
||
<Input v-model:value="record.taskName" placeholder="使用麦克风 %d 分钟。" />
|
||
</template>
|
||
<template v-else-if="column.key === 'taskDesc'">
|
||
<Input v-model:value="record.taskDesc" placeholder="进度 / 目标" />
|
||
</template>
|
||
<template v-else-if="column.key === 'conditionType'">
|
||
<Select
|
||
v-model:value="record.conditionType"
|
||
:options="conditionTypeOptions"
|
||
style="width: 170px"
|
||
@change="(value) => handleConditionTypeChange(index, value)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'targetValue'">
|
||
<InputNumber
|
||
v-model:value="record.targetValue"
|
||
:min="1"
|
||
:precision="0"
|
||
style="width: 100px"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'targetUnit'">
|
||
<Select
|
||
v-model:value="record.targetUnit"
|
||
:options="targetUnitOptions"
|
||
style="width: 90px"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'rewardGold'">
|
||
<InputNumber
|
||
v-model:value="record.rewardGold"
|
||
:min="1"
|
||
:precision="0"
|
||
style="width: 110px"
|
||
/>
|
||
</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="confirmRemoveTask(index)">删除</Button>
|
||
</template>
|
||
</template>
|
||
</Table>
|
||
|
||
<div class="actions">
|
||
<Button @click="addTask">新增任务</Button>
|
||
<Button :loading="saving" type="primary" @click="submitForm">
|
||
保存配置
|
||
</Button>
|
||
</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
|
||
v-model:value="claimQuery.userId"
|
||
allow-clear
|
||
placeholder="用户ID"
|
||
style="width: 160px"
|
||
/>
|
||
<Select
|
||
v-model:value="claimQuery.taskCode"
|
||
allow-clear
|
||
:options="taskCodeOptions"
|
||
option-filter-prop="label"
|
||
placeholder="任务编码"
|
||
show-search
|
||
style="width: 280px"
|
||
/>
|
||
<DatePicker.RangePicker
|
||
v-model:value="claimDateRange"
|
||
allow-clear
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
show-time
|
||
style="width: 390px"
|
||
/>
|
||
<Select
|
||
v-model:value="claimQuery.status"
|
||
allow-clear
|
||
:options="[
|
||
{ label: '成功', value: 'SUCCESS' },
|
||
{ label: '失败', value: 'FAILED' },
|
||
]"
|
||
placeholder="状态"
|
||
style="width: 140px"
|
||
/>
|
||
<Button :loading="claimLoading" type="primary" @click="searchClaimRecords">
|
||
查询
|
||
</Button>
|
||
<Button @click="resetClaimDateRange">今天</Button>
|
||
</div>
|
||
<div class="claim-summary">
|
||
<Tag color="blue">
|
||
当前时间总发放金币:{{ claimSummary.totalRewardGold }}
|
||
</Tag>
|
||
<Tag v-if="claimQuery.taskCode" color="green">
|
||
{{ claimQuery.taskCode }} 发放金币:{{
|
||
claimSummary.selectedTaskRewardGold
|
||
}}
|
||
</Tag>
|
||
</div>
|
||
<Table
|
||
:columns="claimColumns"
|
||
:data-source="claimPage.records"
|
||
:loading="claimLoading"
|
||
:pagination="{
|
||
current: claimPage.current,
|
||
pageSize: claimPage.size,
|
||
showSizeChanger: true,
|
||
total: claimPage.total,
|
||
}"
|
||
row-key="id"
|
||
:scroll="{ x: 1580 }"
|
||
@change="handleClaimTableChange"
|
||
/>
|
||
</TabPane>
|
||
</Tabs>
|
||
</Card>
|
||
</Spin>
|
||
</Page>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.toolbar {
|
||
align-items: center;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.config-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.field-label {
|
||
color: rgb(71 85 105);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.sub-text {
|
||
color: rgb(100 116 139);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.claim-summary {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
}
|
||
</style>
|