feat: configure daily check-in rewards

This commit is contained in:
Codex
2026-06-30 14:52:10 +08:00
parent 7d2b8fd610
commit a5a2caa2e9
12 changed files with 610 additions and 94 deletions

View File

@@ -94,6 +94,20 @@ function intText(value: unknown, fallback = '') {
return String(value);
}
function metadataNumberText(metadata: Record<string, unknown> | undefined, key: string) {
const value = metadata?.[key];
if (value === null || value === undefined || value === '') return '';
const parsed = Number(value);
return Number.isFinite(parsed) ? String(Math.trunc(parsed)) : '';
}
function optionalPositiveIntText(value: string) {
if (!value.trim()) return undefined;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return undefined;
return Math.max(1, Math.trunc(parsed));
}
function taskTypeLabel(type?: string | null) {
if (type === 'daily_check_in') return '每日签到';
if (type === 'feedback_submit') return '提交反馈';
@@ -207,6 +221,10 @@ interface PointTaskFormState {
validFrom: string;
validTo: string;
order: string;
requiredStreak: string;
minStreak: string;
maxStreak: string;
streakMultipleOf: string;
}
function defaultPointTaskForm(): PointTaskFormState {
@@ -223,6 +241,10 @@ function defaultPointTaskForm(): PointTaskFormState {
validFrom: '',
validTo: '',
order: '0',
requiredStreak: '',
minStreak: '',
maxStreak: '',
streakMultipleOf: '',
};
}
@@ -500,6 +522,10 @@ export default function TenantMarketingPage() {
validFrom: item.validFrom ? item.validFrom.slice(0, 10) : '',
validTo: item.validTo ? item.validTo.slice(0, 10) : '',
order: intText(item.order, '0'),
requiredStreak: metadataNumberText(item.metadata, 'requiredStreak'),
minStreak: metadataNumberText(item.metadata, 'minStreak'),
maxStreak: metadataNumberText(item.metadata, 'maxStreak'),
streakMultipleOf: metadataNumberText(item.metadata, 'streakMultipleOf'),
});
setPointFilter(prev => ({ ...prev, selectedTaskId: item.id }));
}
@@ -612,6 +638,17 @@ export default function TenantMarketingPage() {
setBusy('point-task-save');
setError('');
try {
const metadata: Record<string, unknown> = { source: 'taro-tenant-admin' };
if (pointTaskForm.taskType === 'daily_check_in') {
const requiredStreak = optionalPositiveIntText(pointTaskForm.requiredStreak);
const minStreak = optionalPositiveIntText(pointTaskForm.minStreak);
const maxStreak = optionalPositiveIntText(pointTaskForm.maxStreak);
const streakMultipleOf = optionalPositiveIntText(pointTaskForm.streakMultipleOf);
if (requiredStreak !== undefined) metadata.requiredStreak = requiredStreak;
if (minStreak !== undefined) metadata.minStreak = minStreak;
if (maxStreak !== undefined) metadata.maxStreak = maxStreak;
if (streakMultipleOf !== undefined) metadata.streakMultipleOf = streakMultipleOf;
}
const result = await upsertPointActivityTask({
id: pointTaskForm.id || undefined,
code: pointTaskForm.code.trim(),
@@ -625,7 +662,7 @@ export default function TenantMarketingPage() {
validFrom: pointTaskForm.validFrom || null,
validTo: pointTaskForm.validTo || null,
order: Math.trunc(Number(pointTaskForm.order || 0)),
metadata: { source: 'taro-tenant-admin' },
metadata,
});
Taro.showToast({ title: '积分任务已保存', icon: 'success' });
if (result.item?.id) setPointFilter(prev => ({ ...prev, selectedTaskId: result.item?.id || prev.selectedTaskId }));
@@ -978,6 +1015,14 @@ export default function TenantMarketingPage() {
<Input className='admin-input' type='number' placeholder='奖励积分' value={pointTaskForm.rewardPoints} onInput={event => setPointTaskForm(prev => ({ ...prev, rewardPoints: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='单用户领取上限' value={pointTaskForm.claimLimitPerUser} onInput={event => setPointTaskForm(prev => ({ ...prev, claimLimitPerUser: String(event.detail.value || '') }))} />
</View>
{pointTaskForm.taskType === 'daily_check_in' ? (
<View className='admin-form-grid'>
<Input className='admin-input' type='number' placeholder='仅第 N 天奖励' value={pointTaskForm.requiredStreak} onInput={event => setPointTaskForm(prev => ({ ...prev, requiredStreak: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='最小连续天数' value={pointTaskForm.minStreak} onInput={event => setPointTaskForm(prev => ({ ...prev, minStreak: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='最大连续天数' value={pointTaskForm.maxStreak} onInput={event => setPointTaskForm(prev => ({ ...prev, maxStreak: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='每 N 天循环奖励' value={pointTaskForm.streakMultipleOf} onInput={event => setPointTaskForm(prev => ({ ...prev, streakMultipleOf: String(event.detail.value || '') }))} />
</View>
) : null}
<View className='admin-actions compact'>
{(['manual', 'practice_complete', 'vocabulary_review', 'mock_exam_submit', 'daily_check_in', 'feedback_submit', 'feedback_resolved'] as const).map(type => (
<Button key={type} className={`admin-button ${pointTaskForm.taskType === type ? 'active' : ''}`} onClick={() => setPointTaskForm(prev => ({ ...prev, taskType: type }))}>{taskTypeLabel(type)}</Button>