diff --git a/.gitignore b/.gitignore
index 894a840f..ce82f3d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,9 @@ frontend/node_modules/*
frontend/.next/*
frontend/next-env.d.ts
frontend/package-lock.json
-frontend/.env
+.env.*
+!.env.example
+*.tsbuildinfo
# os
.DS_Store
diff --git a/frontend/components/common/receive/ReceiveContent.tsx b/frontend/components/common/receive/ReceiveContent.tsx
index ee5c3887..8ad2dfcc 100644
--- a/frontend/components/common/receive/ReceiveContent.tsx
+++ b/frontend/components/common/receive/ReceiveContent.tsx
@@ -8,6 +8,7 @@ import {Badge} from '@/components/ui/badge';
import {TRUST_LEVEL_OPTIONS} from '@/components/common/project';
import {ArrowLeftIcon, Copy, Tag, Gift, Clock, AlertCircle, Package} from 'lucide-react';
import ContentRender from '@/components/common/markdown/ContentRender';
+import {ReportButton} from '@/components/common/receive/ReportButton';
import services from '@/lib/services';
import {BasicUserInfo} from '@/lib/services/core';
import {GetProjectResponseData} from '@/lib/services/project';
@@ -331,6 +332,18 @@ export function ReceiveContent({data}: ReceiveContentProps) {
/>
+
+
+
+
+
+
+
);
}
diff --git a/frontend/components/common/receive/ReportButton.tsx b/frontend/components/common/receive/ReportButton.tsx
new file mode 100644
index 00000000..7ee5ef62
--- /dev/null
+++ b/frontend/components/common/receive/ReportButton.tsx
@@ -0,0 +1,160 @@
+'use client';
+
+import {useState} from 'react';
+import {toast} from 'sonner';
+import {Button} from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {Textarea} from '@/components/ui/textarea';
+import {Label} from '@/components/ui/label';
+import {Flag} from 'lucide-react';
+import services from '@/lib/services';
+import {BasicUserInfo} from '@/lib/services/core';
+
+/**
+ * 举报按钮组件 Props
+ */
+interface ReportButtonProps {
+ /** 项目ID */
+ projectId: string;
+ /** 当前用户信息 */
+ user: BasicUserInfo | null;
+ /** 是否已经举报过 */
+ hasReported?: boolean;
+ /** 按钮样式变体 */
+ variant?: 'default' | 'text';
+}
+
+/**
+ * 举报按钮组件
+ * 提供项目举报功能,通过弹窗让用户填写举报理由
+ */
+export function ReportButton({projectId, user, hasReported = false, variant = 'default'}: ReportButtonProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [reason, setReason] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [hasReportedLocal, setHasReportedLocal] = useState(hasReported);
+
+ // 计算 trimmed reason,用于多个地方的验证
+ const trimmedReason = reason.trim();
+
+ /**
+ * 处理举报提交
+ */
+ const handleSubmit = async () => {
+ if (!trimmedReason) {
+ toast.error('请填写举报理由');
+ return;
+ }
+
+ if (trimmedReason.length > 255) {
+ toast.error('举报理由不能超过255个字符');
+ return;
+ }
+
+ try {
+ setIsSubmitting(true);
+
+ const result = await services.project.reportProjectSafe(projectId, trimmedReason);
+
+ if (result.success) {
+ toast.success('举报提交成功,感谢您的反馈');
+ setHasReportedLocal(true);
+ setIsOpen(false);
+ setReason('');
+ } else {
+ // 检查是否是重复举报的错误
+ if (result.error && result.error.includes('已举报过当前项目')) {
+ setHasReportedLocal(true);
+ }
+ toast.error(result.error || '举报失败,请稍后重试');
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '举报失败,请稍后重试';
+ // 检查是否是重复举报的错误
+ if (errorMessage.includes('已举报过当前项目')) {
+ setHasReportedLocal(true);
+ }
+ toast.error(errorMessage);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ /**
+ * 处理对话框关闭
+ */
+ const handleClose = () => {
+ if (!isSubmitting) {
+ setIsOpen(false);
+ setReason('');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/components/common/receive/index.ts b/frontend/components/common/receive/index.ts
index 5905c137..12693125 100644
--- a/frontend/components/common/receive/index.ts
+++ b/frontend/components/common/receive/index.ts
@@ -1,2 +1,3 @@
export {ReceiveMain} from './ReceiveMain';
export {ReceiveContent} from './ReceiveContent';
+export {ReportButton} from './ReportButton';
diff --git a/frontend/lib/services/project/project.service.ts b/frontend/lib/services/project/project.service.ts
index df140b21..3e8c220e 100644
--- a/frontend/lib/services/project/project.service.ts
+++ b/frontend/lib/services/project/project.service.ts
@@ -17,6 +17,7 @@ import {
ProjectListResponse,
ApiRequestParams,
ReceiveProjectData,
+ ReportProjectResponse,
} from './types';
import apiClient from '../core/api-client';
@@ -195,6 +196,22 @@ export class ProjectService extends BaseService {
return response.data.data;
}
+ /**
+ * 举报项目
+ * @param projectId - 项目ID
+ * @param reason - 举报理由
+ * @returns 举报结果
+ */
+ static async reportProject(projectId: string, reason: string): Promise {
+ const response = await apiClient.post(`${this.basePath}/${projectId}/report`, {
+ reason,
+ });
+
+ if (response.data.error_msg) {
+ throw new Error(response.data.error_msg);
+ }
+ }
+
/**
* 获取项目详情(带错误处理)
* @param projectId - 项目ID
@@ -412,4 +429,26 @@ export class ProjectService extends BaseService {
};
}
}
+
+ /**
+ * 举报项目(带错误处理)
+ * @param projectId - 项目ID
+ * @param reason - 举报理由
+ * @returns 举报结果,包含成功状态和错误信息
+ */
+ static async reportProjectSafe(projectId: string, reason: string): Promise<{
+ success: boolean;
+ error?: string;
+ }> {
+ try {
+ await this.reportProject(projectId, reason);
+ return {success: true};
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '举报失败';
+ return {
+ success: false,
+ error: errorMessage,
+ };
+ }
+ }
}
diff --git a/frontend/lib/services/project/types.ts b/frontend/lib/services/project/types.ts
index 06b4a9b5..275b263e 100644
--- a/frontend/lib/services/project/types.ts
+++ b/frontend/lib/services/project/types.ts
@@ -278,3 +278,16 @@ export interface ProjectListData {
* 项目列表响应类型
*/
export type ProjectListResponse = BackendResponse;
+
+/**
+ * 举报项目请求参数
+ */
+export interface ReportProjectRequest {
+ /** 举报理由 */
+ reason: string;
+}
+
+/**
+ * 举报项目响应类型
+ */
+export type ReportProjectResponse = BackendResponse;