Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/components/common/project/CreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,10 @@ export function CreateDialog({
project_tags: tags.length > 0 ? tags : undefined,
start_time: formData.startTime.toISOString(),
end_time: formData.endTime.toISOString(),
trust_level_limit_type: formData.trustLevelLimitType,
minimum_trust_level: formData.minimumTrustLevel,
allow_same_ip: formData.allowSameIP,
score_limit_type: formData.scoreLimitType,
risk_level: formData.riskLevel,
distribution_type: formData.distributionType,
topic_id: formData.topicId,
Expand Down Expand Up @@ -214,11 +216,14 @@ export function CreateDialog({
tags: tags,
start_time: formData.startTime.toISOString(),
end_time: formData.endTime.toISOString(),
trust_level_limit_type: formData.trustLevelLimitType,
minimum_trust_level: formData.minimumTrustLevel,
allow_same_ip: formData.allowSameIP,
score_limit_type: formData.scoreLimitType,
risk_level: formData.riskLevel,
distribution_type: formData.distributionType,
total_items: items.length,
price: formData.price || '0',
created_at: new Date().toISOString(),
};

Expand Down
5 changes: 5 additions & 0 deletions frontend/components/common/project/EditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ export function EditDialog({
project_tags: tags.length > 0 ? tags : undefined,
start_time: formData.startTime.toISOString(),
end_time: formData.endTime.toISOString(),
trust_level_limit_type: formData.trustLevelLimitType,
minimum_trust_level: formData.minimumTrustLevel,
allow_same_ip: formData.allowSameIP,
score_limit_type: formData.scoreLimitType,
risk_level: formData.riskLevel,
price: formData.price || '0',
// 只有非抽奖项目才允许更新项目内容
Expand All @@ -184,9 +186,12 @@ export function EditDialog({
tags: tags,
start_time: formData.startTime.toISOString(),
end_time: formData.endTime.toISOString(),
trust_level_limit_type: formData.trustLevelLimitType,
minimum_trust_level: formData.minimumTrustLevel,
allow_same_ip: formData.allowSameIP,
score_limit_type: formData.scoreLimitType,
risk_level: formData.riskLevel,
price: formData.price || '0',
total_items: project.distribution_type === DistributionType.LOTTERY ?
project.total_items :
project.total_items + newItems.length,
Expand Down
118 changes: 84 additions & 34 deletions frontend/components/common/project/ProjectBasicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {TagSelector} from '@/components/ui/tag-selector';
import {DateTimePicker} from '@/components/ui/DateTimePicker';
import MarkdownEditor from '@/components/common/markdown/Editor';
import {HelpCircle, Wallet} from 'lucide-react';
import {FORM_LIMITS, TRUST_LEVEL_OPTIONS} from '@/components/common/project';
import {FORM_LIMITS, REQUIREMENT_LIMIT_TYPE_OPTIONS, TRUST_LEVEL_OPTIONS} from '@/components/common/project';
import {TrustLevel} from '@/lib/services/core/types';
import {DistributionType} from '@/lib/services/project/types';
import {DistributionType, RequirementLimitType} from '@/lib/services/project/types';
import {ProjectFormData} from '@/hooks/use-project-form';
import services from '@/lib/services';

Expand Down Expand Up @@ -153,27 +153,50 @@ export function ProjectBasicForm({

<div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-2'} gap-4`}>
<div className="space-y-2">
<Label className="text-xs">最低社区等级</Label>
<Select
value={formData.minimumTrustLevel.toString()}
onValueChange={(value) => updateField('minimumTrustLevel', parseInt(value) as TrustLevel)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TRUST_LEVEL_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Label className="text-xs">社区等级限制</Label>
<div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-[120px_minmax(0,1fr)]'} gap-2`}>
<Select
value={formData.trustLevelLimitType.toString()}
onValueChange={(value) => updateField('trustLevelLimitType', parseInt(value) as RequirementLimitType)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{REQUIREMENT_LIMIT_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>

<Select
value={formData.minimumTrustLevel.toString()}
onValueChange={(value) => updateField('minimumTrustLevel', parseInt(value) as TrustLevel)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TRUST_LEVEL_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className="text-xs text-muted-foreground">
{formData.trustLevelLimitType === RequirementLimitType.MINIMUM ?
'仅允许达到该等级及以上的用户领取' :
'仅允许达到该等级及以下的用户领取'}
</p>
</div>

<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-xs">最低社区分数</Label>
<Label className="text-xs">社区分数限制</Label>
<TooltipProvider>
<Tooltip open={showTooltip} onOpenChange={setShowTooltip}>
<TooltipTrigger asChild>
Expand All @@ -182,25 +205,52 @@ export function ProjectBasicForm({
</div>
</TooltipTrigger>
<TooltipContent>
<p>注:此功能已于 2025/07/22 更新 <br /> 低于此分数的用户无法领取项目内容!</p>
<p>
注:你可以设置最低分或最高分。
<br />
最低分适合提高门槛,最高分适合更偏向新人。
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Input
id="riskLevel"
type="number"
min={0}
max={100}
value={100 - formData.riskLevel}
onChange={(e) => {
const userInput = parseInt(e.target.value) || 0;
const clampedInput = Math.max(0, Math.min(100, userInput));
const riskLevel = 100 - clampedInput;
updateField('riskLevel', riskLevel);
}}
placeholder="输入0-100的用户分数"
/>
<div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-[120px_minmax(0,1fr)]'} gap-2`}>
<Select
value={formData.scoreLimitType.toString()}
onValueChange={(value) => updateField('scoreLimitType', parseInt(value) as RequirementLimitType)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{REQUIREMENT_LIMIT_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>

<Input
id="riskLevel"
type="number"
min={0}
max={100}
value={100 - formData.riskLevel}
onChange={(e) => {
const userInput = parseInt(e.target.value) || 0;
const clampedInput = Math.max(0, Math.min(100, userInput));
const riskLevel = 100 - clampedInput;
updateField('riskLevel', riskLevel);
}}
placeholder="输入 0-100 的用户分数"
/>
</div>
<p className="text-xs text-muted-foreground">
{formData.scoreLimitType === RequirementLimitType.MINIMUM ?
'仅允许达到该分数及以上的用户领取' :
'仅允许达到该分数及以下的用户领取'}
</p>
</div>
</div>

Expand Down
20 changes: 10 additions & 10 deletions frontend/components/common/project/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import {Button} from '@/components/ui/button';
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip';
import {MotionEffect} from '@/components/animate-ui/effects/motion-effect';
import {CURRENCY_LABEL, DISTRIBUTION_MODE_NAMES} from '@/components/common/project';
import {CURRENCY_LABEL, DISTRIBUTION_MODE_NAMES, getRequirementLimitTypeLabel} from '@/components/common/project';
import {BadgeCheck, Boxes, Coins, Gauge, Pencil, ShieldCheck, Trash2, Waypoints} from 'lucide-react';
import {formatDateTimeWithSeconds} from '@/lib/utils';
import {ProjectListItem} from '@/lib/services/project/types';

const TRUST_LEVEL_SHORT_LABELS: Partial<Record<number, string>> = {
0: 'TL0',
1: 'TL1',
2: 'TL2',
3: 'TL3',
Expand Down Expand Up @@ -63,11 +64,12 @@ export function ProjectCard({
const modeText = DISTRIBUTION_MODE_NAMES[project.distribution_type];
const description = project.description?.trim();
const trustText = TRUST_LEVEL_SHORT_LABELS[project.minimum_trust_level];
const riskText = `${String(project.risk_level).padStart(2, '0')}`;
const showRisk = project.risk_level > 0;
const riskThreshold = String(100 - project.risk_level).padStart(2, '0');
const showPrice = priceNum > 0;
const showIpLimit = !project.allow_same_ip;
const showTrustLevel = Boolean(trustText);
const trustPrefix = getRequirementLimitTypeLabel(project.trust_level_limit_type);
const scorePrefix = getRequirementLimitTypeLabel(project.score_limit_type);

return (
<TooltipProvider>
Expand Down Expand Up @@ -154,12 +156,10 @@ export function ProjectCard({
<Boxes className="h-3.5 w-3.5 text-foreground/50" />
{itemText}
</span>
{showRisk && (
<span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap">
<Gauge className="h-3.5 w-3.5 text-foreground/50" />
{riskText}
</span>
)}
<span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap">
<Gauge className="h-3.5 w-3.5 text-foreground/50" />
{scorePrefix}{riskThreshold}
</span>
{showPrice && (
<Tooltip>
<TooltipTrigger asChild>
Expand All @@ -178,7 +178,7 @@ export function ProjectCard({
{showTrustLevel && (
<span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap">
<BadgeCheck className="h-3.5 w-3.5 text-foreground/50" />
{trustText}
{trustPrefix}{trustText}
</span>
)}
{showIpLimit && (
Expand Down
9 changes: 9 additions & 0 deletions frontend/components/common/project/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {TrustLevel} from '@/lib/services/core/types';
import {RequirementLimitType} from '@/lib/services/project/types';

/**
* 表单验证常量
Expand Down Expand Up @@ -63,6 +64,14 @@ export const TRUST_LEVEL_OPTIONS = [
{value: TrustLevel.LEADER, label: '领导者'},
];

export const REQUIREMENT_LIMIT_TYPE_OPTIONS = [
{value: RequirementLimitType.MINIMUM, label: '最低'},
{value: RequirementLimitType.MAXIMUM, label: '最高'},
];

export const getRequirementLimitTypeLabel = (value: RequirementLimitType): string =>
REQUIREMENT_LIMIT_TYPE_OPTIONS.find((option) => option.value === value)?.label || '最低';

/**
* 信任等级配置 - 用于卡片样式和显示
*/
Expand Down
34 changes: 28 additions & 6 deletions frontend/components/common/receive/ReceiveContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import {Button} from '@/components/ui/button';
import {Badge} from '@/components/ui/badge';
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar';
import {AvatarGroup, AvatarGroupTooltip} from '@/components/animate-ui/components/animate/avatar-group';
import {CURRENCY_LABEL, DISTRIBUTION_MODE_NAMES, TRUST_LEVEL_OPTIONS} from '@/components/common/project';
import {CURRENCY_LABEL, DISTRIBUTION_MODE_NAMES, getRequirementLimitTypeLabel, TRUST_LEVEL_OPTIONS} from '@/components/common/project';
import {ArrowLeftIcon, Copy, Gift, Clock, AlertCircle, Package, Coins, Loader2, CalendarRange, Hash} from 'lucide-react';
import ContentRender from '@/components/common/markdown/ContentRender';
import {ReportButton} from '@/components/common/receive/ReportButton';
import {ReceiveVerify, ReceiveVerifyRef} from '@/components/common/receive/ReceiveVerify';
import services from '@/lib/services';
import {BasicUserInfo} from '@/lib/services/core';
import {GetProjectResponseData} from '@/lib/services/project';
import {GetProjectResponseData, RequirementLimitType} from '@/lib/services/project';
import {formatDate, formatDateTimeWithSeconds, copyToClipboard} from '@/lib/utils';
import {motion} from 'motion/react';
import {Separator} from '@/components/ui/separator';
Expand Down Expand Up @@ -91,11 +91,30 @@ const ReceiveButton = ({
);
}

if (!user || user.trust_level < project.minimum_trust_level) {
const trustLevelBlocked = !user ||
(project.trust_level_limit_type === RequirementLimitType.MINIMUM ?
user.trust_level < project.minimum_trust_level :
user.trust_level > project.minimum_trust_level);
const scoreThreshold = 100 - project.risk_level;
const scoreBlocked = !user ||
(project.score_limit_type === RequirementLimitType.MINIMUM ?
user.score < scoreThreshold :
user.score > scoreThreshold);

if (trustLevelBlocked) {
return (
<Button disabled className="h-9 w-full cursor-not-allowed rounded-full bg-muted text-muted-foreground shadow-none">
<AlertCircle className="w-4 h-4 mr-2" />
信任等级不足
{project.trust_level_limit_type === RequirementLimitType.MINIMUM ? '信任等级不足' : '信任等级过高'}
</Button>
);
}

if (scoreBlocked) {
return (
<Button disabled className="h-9 w-full cursor-not-allowed rounded-full bg-muted text-muted-foreground shadow-none">
<AlertCircle className="w-4 h-4 mr-2" />
{project.score_limit_type === RequirementLimitType.MINIMUM ? '社区分数不足' : '社区分数过高'}
</Button>
);
}
Expand Down Expand Up @@ -396,6 +415,9 @@ export function ReceiveContent({data}: ReceiveContentProps) {

const trustLevelConfig = TRUST_LEVEL_OPTIONS.find((option) => option.value === currentProject.minimum_trust_level);
const distributionModeName = DISTRIBUTION_MODE_NAMES[currentProject.distribution_type] || '分发项目';
const scoreThreshold = 100 - currentProject.risk_level;
const trustLabel = `${getRequirementLimitTypeLabel(currentProject.trust_level_limit_type)}${trustLevelConfig?.label || '无限制'}`;
const scoreLabel = `${getRequirementLimitTypeLabel(currentProject.score_limit_type)} ${scoreThreshold}`;
const startTime = new Date(currentProject.start_time);
const endTime = new Date(currentProject.end_time);
const receivedCount = Math.max(currentProject.total_items - currentProject.available_items_count, 0);
Expand Down Expand Up @@ -424,11 +446,11 @@ export function ReceiveContent({data}: ReceiveContentProps) {
},
{
label: '社区分数',
value: `${100 - currentProject.risk_level}`,
value: scoreLabel,
},
{
label: '信任等级',
value: trustLevelConfig?.label || '无限制',
value: trustLabel,
},
{
label: '消耗 LDC',
Expand Down
8 changes: 7 additions & 1 deletion frontend/hooks/use-project-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

import {useState, useCallback} from 'react';
import {TrustLevel} from '@/lib/services/core/types';
import {DistributionType, ProjectListItem} from '@/lib/services/project/types';
import {DistributionType, ProjectListItem, RequirementLimitType} from '@/lib/services/project/types';
import {DEFAULT_FORM_VALUES} from '@/components/common/project';

export interface ProjectFormData {
name: string;
description: string;
startTime: Date;
endTime: Date;
trustLevelLimitType: RequirementLimitType;
minimumTrustLevel: TrustLevel;
allowSameIP: boolean;
scoreLimitType: RequirementLimitType;
riskLevel: number;
distributionType: DistributionType;
topicId?: number;
Expand All @@ -35,8 +37,10 @@ export function useProjectForm(options: UseProjectFormOptions) {
description: project.description || '',
startTime: new Date(project.start_time),
endTime: new Date(project.end_time),
trustLevelLimitType: project.trust_level_limit_type ?? RequirementLimitType.MINIMUM,
minimumTrustLevel: project.minimum_trust_level,
allowSameIP: project.allow_same_ip,
scoreLimitType: project.score_limit_type ?? RequirementLimitType.MINIMUM,
riskLevel: project.risk_level,
distributionType: project.distribution_type,
price: project.price ?? '0',
Expand All @@ -48,8 +52,10 @@ export function useProjectForm(options: UseProjectFormOptions) {
description: '',
startTime: new Date(),
endTime: new Date(Date.now() + DEFAULT_FORM_VALUES.TIME_OFFSET_24H),
trustLevelLimitType: RequirementLimitType.MINIMUM,
minimumTrustLevel: TrustLevel.BASIC_USER,
allowSameIP: false,
scoreLimitType: RequirementLimitType.MINIMUM,
riskLevel: DEFAULT_FORM_VALUES.RISK_LEVEL,
distributionType: DistributionType.ONE_FOR_EACH,
price: '0',
Expand Down
Loading