Skip to content

Commit fc9a524

Browse files
authored
Merge pull request #128 from linux-do/receiver
feat: add get receivers
2 parents 93f5cbe + 875d6d4 commit fc9a524

File tree

8 files changed

+308
-2
lines changed

8 files changed

+308
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
## 📖 项目简介
2424

25-
LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具包,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。
25+
LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具平台,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。
2626

2727
### ✨ 主要特性
2828

frontend/components/common/layout/ManagementBar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,16 @@ export function ManagementBar() {
238238
</Link>
239239
</div>
240240
</div>
241+
242+
<div>
243+
<h4 className="text-sm font-semibold mb-3 text-muted-foreground">关于 LINUX DO CDK</h4>
244+
<div className='space-y-2'>
245+
<div className="text-xs text-muted-foreground font-light">版本: 1.1.0</div>
246+
<div className="text-xs text-muted-foreground font-light">构建时间: 2025-09-27</div>
247+
<div className="text-xs text-muted-foreground font-light">LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具平台,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。</div>
248+
</div>
249+
</div>
250+
241251
{!isLoading && !user && (
242252
<div className="text-center text-muted-foreground">
243253
未登录用户

frontend/components/common/project/MineProject.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import {
2424
Pencil,
2525
Filter,
2626
X,
27+
Users,
2728
} from 'lucide-react';
2829
import {EditDialog, ProjectCard} from '@/components/common/project';
30+
import {ReceiverDialog} from '@/components/common/project/ReceiverDialog';
2931
import {EmptyState} from '@/components/common/layout/EmptyState';
3032
import {TagFilterPopover} from '@/components/ui/tag-filter-popover';
3133
import services from '@/lib/services';
@@ -215,7 +217,20 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) {
215217
e.preventDefault();
216218
e.stopPropagation();
217219
}}
220+
className="flex gap-1"
218221
>
222+
<ReceiverDialog
223+
projectId={project.id}
224+
projectName={project.name}
225+
>
226+
<Button
227+
variant="ghost"
228+
size="sm"
229+
className="h-6 w-6 sm:h-7 sm:w-7 p-0 bg-white/20 hover:bg-white/30 text-white"
230+
>
231+
<Users className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
232+
</Button>
233+
</ReceiverDialog>
219234
<EditDialog
220235
project={project}
221236
onProjectUpdated={handleProjectUpdated}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
'use client';
2+
3+
import {useState, useEffect, useCallback} from 'react';
4+
import {useIsMobile} from '@/hooks/use-mobile';
5+
import {toast} from 'sonner';
6+
import {Button} from '@/components/ui/button';
7+
import {Input} from '@/components/ui/input';
8+
import {ScrollArea} from '@/components/ui/scroll-area';
9+
import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription} from '@/components/animate-ui/radix/dialog';
10+
import {EmptyState} from '@/components/common/layout/EmptyState';
11+
import {Users, Search, Copy, CheckCircle, AlertCircle, Loader2} from 'lucide-react';
12+
import services from '@/lib/services';
13+
import {ProjectReceiver} from '@/lib/services/project/types';
14+
15+
interface ReceiverDialogProps {
16+
projectId: string;
17+
projectName: string;
18+
children?: React.ReactNode;
19+
}
20+
21+
/**
22+
* 项目领取人对话框组件
23+
*/
24+
export function ReceiverDialog({
25+
projectId,
26+
projectName,
27+
children,
28+
}: ReceiverDialogProps) {
29+
const isMobile = useIsMobile();
30+
const [open, setOpen] = useState(false);
31+
const [loading, setLoading] = useState(false);
32+
const [receivers, setReceivers] = useState<ProjectReceiver[]>([]);
33+
const [filteredReceivers, setFilteredReceivers] = useState<ProjectReceiver[]>([]);
34+
const [searchKeyword, setSearchKeyword] = useState('');
35+
const [error, setError] = useState<string | null>(null);
36+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
37+
38+
/**
39+
* 获取项目领取人列表
40+
*/
41+
const fetchReceivers = useCallback(async () => {
42+
setLoading(true);
43+
setError(null);
44+
45+
try {
46+
const result = await services.project.getProjectReceiversSafe(projectId);
47+
48+
if (result.success) {
49+
setReceivers(result.data || []);
50+
setFilteredReceivers(result.data || []);
51+
} else {
52+
setError(result.error || '获取领取人列表失败');
53+
}
54+
} catch {
55+
setError('获取领取人列表失败');
56+
} finally {
57+
setLoading(false);
58+
}
59+
}, [projectId]);
60+
61+
/**
62+
* 搜索功能
63+
*/
64+
const handleSearch = useCallback((keyword: string) => {
65+
const filtered = receivers.filter((receiver) =>
66+
receiver.username.toLowerCase().includes(keyword.toLowerCase()) ||
67+
receiver.nickname.toLowerCase().includes(keyword.toLowerCase()) ||
68+
receiver.content.toLowerCase().includes(keyword.toLowerCase()),
69+
);
70+
71+
setFilteredReceivers(filtered);
72+
}, [receivers]);
73+
74+
/**
75+
* 复制内容到剪贴板
76+
*/
77+
const handleCopy = async (content: string, index: number) => {
78+
try {
79+
await navigator.clipboard.writeText(content);
80+
setCopiedIndex(index);
81+
toast.success('已复制到剪贴板');
82+
83+
// 2秒后重置复制状态
84+
setTimeout(() => {
85+
setCopiedIndex(null);
86+
}, 2000);
87+
} catch {
88+
toast.error('复制失败');
89+
}
90+
};
91+
92+
/**
93+
* 重试获取数据
94+
*/
95+
const handleRetry = () => {
96+
fetchReceivers();
97+
};
98+
99+
/**
100+
* 对话框打开时获取数据
101+
*/
102+
useEffect(() => {
103+
if (open) {
104+
fetchReceivers();
105+
setSearchKeyword('');
106+
}
107+
}, [open, fetchReceivers]);
108+
109+
/**
110+
* 搜索关键词变化时过滤数据
111+
*/
112+
useEffect(() => {
113+
handleSearch(searchKeyword);
114+
}, [searchKeyword, handleSearch]);
115+
116+
return (
117+
<Dialog open={open} onOpenChange={setOpen}>
118+
<DialogTrigger asChild>
119+
{children || (
120+
<Button size="sm" variant="ghost">
121+
<Users className="h-4 w-4" />
122+
</Button>
123+
)}
124+
</DialogTrigger>
125+
<DialogContent
126+
className={`${isMobile ? 'max-w-[95vw] max-h-[85vh]' : 'max-w-3xl max-h-[80vh]'} overflow-hidden`}
127+
>
128+
<DialogHeader>
129+
<DialogTitle className="flex items-center gap-2">
130+
项目领取人
131+
</DialogTitle>
132+
<DialogDescription>
133+
查看项目 &ldquo;{projectName}&rdquo; 的所有领取人信息
134+
</DialogDescription>
135+
</DialogHeader>
136+
137+
<div className="space-y-3">
138+
{/* 搜索栏 */}
139+
<div className="relative">
140+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
141+
<Input
142+
placeholder="搜索用户名、昵称或内容..."
143+
value={searchKeyword}
144+
onChange={(e) => setSearchKeyword(e.target.value)}
145+
className="pl-10"
146+
/>
147+
</div>
148+
149+
{/* 统计信息 */}
150+
{!loading && !error && (
151+
<div className="flex items-center justify-between text-sm text-muted-foreground">
152+
<span>
153+
{receivers.length} 人领取
154+
{searchKeyword && ` · 筛选出 ${filteredReceivers.length} 条结果`}
155+
</span>
156+
</div>
157+
)}
158+
159+
{/* 内容区域 */}
160+
<div className="min-h-[300px]">
161+
{loading ? (
162+
<div className="flex items-center justify-center h-[300px]">
163+
<div className="flex flex-col items-center gap-2">
164+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
165+
<p className="text-sm text-muted-foreground">正在加载领取人列表...</p>
166+
</div>
167+
</div>
168+
) : error ? (
169+
<EmptyState
170+
icon={AlertCircle}
171+
title="加载失败"
172+
description={error}
173+
className="h-[300px] flex flex-col items-center justify-center"
174+
>
175+
<Button onClick={handleRetry} variant="outline" size="sm" className="mt-3">
176+
重试
177+
</Button>
178+
</EmptyState>
179+
) : filteredReceivers.length === 0 ? (
180+
<EmptyState
181+
icon={Users}
182+
title={searchKeyword ? '未找到匹配的领取人' : '暂无领取人'}
183+
description={searchKeyword ? '尝试调整搜索关键词' : '还没有人领取此项目'}
184+
className="h-[300px] flex flex-col items-center justify-center"
185+
/>
186+
) : (
187+
<ScrollArea className="h-[300px] pr-4">
188+
<div className="space-y-2">
189+
{filteredReceivers.map((receiver, index) => (
190+
<div
191+
key={`${receiver.username}-${index}`}
192+
className="p-2 border rounded-md hover:bg-muted/50 transition-colors"
193+
>
194+
<div className="flex items-center gap-2">
195+
<span className="font-medium text-sm">{receiver.username} ({receiver.nickname})</span>
196+
<span className="text-xs text-muted-foreground">-</span>
197+
<span className="text-xs font-mono truncate flex-1 min-w-0">{receiver.content}</span>
198+
<Button
199+
variant="secondary"
200+
size="sm"
201+
onClick={() => handleCopy(receiver.content, index)}
202+
className="flex-shrink-0 h-6 w-6 p-0"
203+
>
204+
{copiedIndex === index ? (
205+
<CheckCircle className="h-3 w-3 text-green-600" />
206+
) : (
207+
<Copy className="h-3 w-3" />
208+
)}
209+
</Button>
210+
</div>
211+
</div>
212+
))}
213+
</div>
214+
</ScrollArea>
215+
)}
216+
</div>
217+
</div>
218+
</DialogContent>
219+
</Dialog>
220+
);
221+
}

frontend/components/common/project/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {MineProject} from './MineProject';
33
export {ProjectCard} from './ProjectCard';
44
export {CreateDialog} from './CreateDialog';
55
export {EditDialog} from './EditDialog';
6+
export {ReceiverDialog} from './ReceiverDialog';
67
export {ProjectBasicForm} from './ProjectBasicForm';
78
export {BulkImportSection} from './BulkImportSection';
89
export {DistributionModeSelect} from './DistributionModeSelect';

frontend/lib/services/project/project.service.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
ApiRequestParams,
1919
ReceiveProjectData,
2020
ReportProjectResponse,
21+
ProjectReceiver,
22+
ProjectReceiversResponse,
2123
} from './types';
2224
import apiClient from '../core/api-client';
2325

@@ -86,6 +88,20 @@ export class ProjectService extends BaseService {
8688
}
8789
}
8890

91+
/**
92+
* 获取项目领取者列表(仅项目创建者可访问)
93+
* @param projectId - 项目ID
94+
* @returns 项目领取者列表
95+
*/
96+
static async getProjectReceivers(projectId: string): Promise<ProjectReceiver[]> {
97+
const response = await apiClient.get<ProjectReceiversResponse>(`${this.basePath}/${projectId}/receivers`);
98+
if (response.data.error_msg) {
99+
throw new Error(response.data.error_msg);
100+
}
101+
// 处理后端返回null的情况,确保返回空数组而不是null
102+
return response.data.data || [];
103+
}
104+
89105
/**
90106
* 领取项目内容(必须带验证码)
91107
* @param projectId - 项目ID
@@ -458,4 +474,30 @@ export class ProjectService extends BaseService {
458474
};
459475
}
460476
}
477+
478+
/**
479+
* 获取项目领取者列表(带错误处理,仅项目创建者可访问)
480+
* @param projectId - 项目ID
481+
* @returns 获取结果,包含成功状态、领取者列表和错误信息
482+
*/
483+
static async getProjectReceiversSafe(projectId: string): Promise<{
484+
success: boolean;
485+
data?: ProjectReceiver[];
486+
error?: string;
487+
}> {
488+
try {
489+
const data = await this.getProjectReceivers(projectId);
490+
return {
491+
success: true,
492+
data,
493+
};
494+
} catch (error) {
495+
const errorMessage = error instanceof Error ? error.message : '获取项目领取者列表失败';
496+
return {
497+
success: false,
498+
data: [],
499+
error: errorMessage,
500+
};
501+
}
502+
}
461503
}

frontend/lib/services/project/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,20 @@ export interface ReportProjectRequest {
297297
* 举报项目响应类型
298298
*/
299299
export type ReportProjectResponse = BackendResponse<null>;
300+
301+
/**
302+
* 项目领取者信息
303+
*/
304+
export interface ProjectReceiver {
305+
/** 用户名 */
306+
username: string;
307+
/** 用户昵称 */
308+
nickname: string;
309+
/** 领取到的内容 */
310+
content: string;
311+
}
312+
313+
/**
314+
* 项目领取者响应类型
315+
*/
316+
export type ProjectReceiversResponse = BackendResponse<ProjectReceiver[] | null>;

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "linux-do-cdk",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

0 commit comments

Comments
 (0)