Skip to content
Merged
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
21 changes: 20 additions & 1 deletion docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,29 @@ const docTemplate = `{
"parameters": [
{
"type": "string",
"description": "项目ID",
"description": "项目ID (Project ID)",
"name": "id",
"in": "path",
"required": true
},
{
"minimum": 1,
"type": "integer",
"name": "current",
"in": "query"
},
{
"maxLength": 1024,
"type": "string",
"name": "search",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "size",
"in": "query"
}
],
"responses": {
Expand Down
21 changes: 20 additions & 1 deletion docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -591,10 +591,29 @@
"parameters": [
{
"type": "string",
"description": "项目ID",
"description": "项目ID (Project ID)",
"name": "id",
"in": "path",
"required": true
},
{
"minimum": 1,
"type": "integer",
"name": "current",
"in": "query"
},
{
"maxLength": 1024,
"type": "string",
"name": "search",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "size",
"in": "query"
}
],
"responses": {
Expand Down
15 changes: 14 additions & 1 deletion docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -764,11 +764,24 @@ paths:
consumes:
- application/json
parameters:
- description: 项目ID
- description: 项目ID (Project ID)
in: path
name: id
required: true
type: string
- in: query
minimum: 1
name: current
type: integer
- in: query
maxLength: 1024
name: search
type: string
- in: query
maximum: 100
minimum: 1
name: size
type: integer
produces:
- application/json
responses:
Expand Down
53 changes: 16 additions & 37 deletions frontend/components/common/project/ReceiverDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {useState, useEffect, useCallback} from 'react';
import {useIsMobile} from '@/hooks/use-mobile';
import {useDebounce} from '@/hooks/use-debounce';
import {toast} from 'sonner';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
Expand Down Expand Up @@ -29,9 +30,9 @@ export function ReceiverDialog({
const isMobile = useIsMobile();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [receivers, setReceivers] = useState<ProjectReceiver[]>([]);
const [filteredReceivers, setFilteredReceivers] = useState<ProjectReceiver[]>([]);
const [searchKeyword, setSearchKeyword] = useState('');
const debouncedSearchKeyword = useDebounce(searchKeyword, 500);
const [error, setError] = useState<string | null>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);

Expand All @@ -43,10 +44,9 @@ export function ReceiverDialog({
setError(null);

try {
const result = await services.project.getProjectReceiversSafe(projectId);
const result = await services.project.getProjectReceiversSafe(projectId, 1, 100, debouncedSearchKeyword);

if (result.success) {
setReceivers(result.data || []);
setFilteredReceivers(result.data || []);
} else {
setError(result.error || '获取领取人列表失败');
Expand All @@ -56,27 +56,20 @@ export function ReceiverDialog({
} finally {
setLoading(false);
}
}, [projectId]);

/**
* 搜索功能
*/
const handleSearch = useCallback((keyword: string) => {
const filtered = receivers.filter((receiver) =>
receiver.username.toLowerCase().includes(keyword.toLowerCase()) ||
receiver.nickname.toLowerCase().includes(keyword.toLowerCase()) ||
receiver.content.toLowerCase().includes(keyword.toLowerCase()),
);

setFilteredReceivers(filtered);
}, [receivers]);
}, [projectId, debouncedSearchKeyword]);

/**
* 复制内容到剪贴板
*/
const handleCopy = async (content: string, index: number) => {
try {
await navigator.clipboard.writeText(content);
// 拆分内容并整理成多行格式
const contentItems = content.split('$\n*');
const cleanedItems = contentItems.map((item) =>
item.replace(/^[\u4e00-\u9fa5\w]+\d*:\s*/, ''),
);
const formattedContent = cleanedItems.join('\n');
await navigator.clipboard.writeText(formattedContent);
setCopiedIndex(index);
toast.success('已复制到剪贴板');

Expand All @@ -102,17 +95,9 @@ export function ReceiverDialog({
useEffect(() => {
if (open) {
fetchReceivers();
setSearchKeyword('');
}
}, [open, fetchReceivers]);

/**
* 搜索关键词变化时过滤数据
*/
useEffect(() => {
handleSearch(searchKeyword);
}, [searchKeyword, handleSearch]);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
Expand Down Expand Up @@ -146,16 +131,6 @@ export function ReceiverDialog({
/>
</div>

{/* 统计信息 */}
{!loading && !error && (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
共 {receivers.length} 人领取
{searchKeyword && ` · 筛选出 ${filteredReceivers.length} 条结果`}
</span>
</div>
)}

{/* 内容区域 */}
<div className="min-h-[300px]">
{loading ? (
Expand Down Expand Up @@ -194,7 +169,11 @@ export function ReceiverDialog({
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{receiver.username} ({receiver.nickname})</span>
<span className="text-xs text-muted-foreground">-</span>
<span className="text-xs font-mono truncate flex-1 min-w-0">{receiver.content}</span>
<div className="text-xs font-mono flex-1 min-w-0 max-h-24 overflow-y-auto">
{receiver.content.split('$\n*').map((item, itemIndex) => (
<div key={itemIndex} className="truncate">{item}</div>
))}
</div>
<Button
variant="secondary"
size="sm"
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/common/project/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const DEFAULT_FORM_VALUES = {
*/
export const DISTRIBUTION_MODE_NAMES: Record<number, string> = {
0: '一码一用',
1: '邀请制',
1: '抽奖分发',
2: '邀请制',
};

/**
Expand Down
37 changes: 22 additions & 15 deletions frontend/components/common/receive/ReceiveContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,31 @@ const ReceiveButton = ({
onReceive,
}: ReceiveButtonProps) => {
if (hasReceived && receivedContent) {
const contentItems = receivedContent.split('$\n*');

return (
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg -mt-4">
<div className="text-xs text-muted-foreground mb-2">分发内容</div>
<div className="flex-1 min-w-0 flex items-center justify-between gap-2">
<code className="block text-sm font-bold text-gray-900 dark:text-gray-100 break-all">
{receivedContent}
</code>
<Button
variant="ghost"
size="sm"
className="ml-3 flex-shrink-0 h-7 w-7 p-0"
onClick={() => {
copyToClipboard(receivedContent);
toast.success('复制成功');
}}
>
<Copy className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</Button>
<div className="space-y-2">
{contentItems.map((item, index) => (
<div key={index} className="flex-1 min-w-0 flex items-center justify-between gap-2">
<code className="block text-sm font-bold text-gray-900 dark:text-gray-100 break-all">
{item}
</code>
<Button
variant="ghost"
size="sm"
className="ml-3 flex-shrink-0 h-7 w-7 p-0"
onClick={() => {
const cleanContent = item.replace(/^[\u4e00-\u9fa5\w]+\d*:\s*/, '');
copyToClipboard(cleanContent);
toast.success('复制成功');
}}
>
<Copy className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</Button>
</div>
))}
</div>
</div>
);
Expand Down
25 changes: 25 additions & 0 deletions frontend/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import {useEffect, useState} from 'react';

/**
* 防抖钩子函数
* @param value - 需要防抖的值
* @param delay - 延迟时间,单位毫秒,默认为300ms
* @returns 防抖后的值
*/
export function useDebounce<T>(value: T, delay = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
}
30 changes: 26 additions & 4 deletions frontend/lib/services/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,24 @@ export class ProjectService extends BaseService {
/**
* 获取项目领取者列表(仅项目创建者可访问)
* @param projectId - 项目ID
* @param current - 当前页码,默认为1
* @param size - 每页大小,默认为10
* @param search - 搜索关键词,可选
* @returns 项目领取者列表
*/
static async getProjectReceivers(projectId: string): Promise<ProjectReceiver[]> {
const response = await apiClient.get<ProjectReceiversResponse>(`${this.basePath}/${projectId}/receivers`);
static async getProjectReceivers(
projectId: string,
current: number = 1,
size: number = 10,
search: string = '',
): Promise<ProjectReceiver[]> {
const response = await apiClient.get<ProjectReceiversResponse>(`${this.basePath}/${projectId}/receivers`, {
params: {
current,
size,
search,
},
});
if (response.data.error_msg) {
throw new Error(response.data.error_msg);
}
Expand Down Expand Up @@ -478,15 +492,23 @@ export class ProjectService extends BaseService {
/**
* 获取项目领取者列表(带错误处理,仅项目创建者可访问)
* @param projectId - 项目ID
* @param current - 当前页码,默认为1
* @param size - 每页大小,默认为10
* @param search - 搜索关键词,可选
* @returns 获取结果,包含成功状态、领取者列表和错误信息
*/
static async getProjectReceiversSafe(projectId: string): Promise<{
static async getProjectReceiversSafe(
projectId: string,
current: number = 1,
size: number = 10,
search: string = '',
): Promise<{
success: boolean;
data?: ProjectReceiver[];
error?: string;
}> {
try {
const data = await this.getProjectReceivers(projectId);
const data = await this.getProjectReceivers(projectId, current, size, search);
return {
success: true,
data,
Expand Down
2 changes: 1 addition & 1 deletion internal/apps/project/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ type ProjectItem struct {
Content string `json:"content" gorm:"size:1024"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ReceivedAt *time.Time `json:"received_at"`
ReceivedAt *time.Time `json:"received_at" gorm:"index"`
}

func (p *ProjectItem) Exact(tx *gorm.DB, id uint64) error {
Expand Down
Loading