From 5276112e48aecd37a1d6aad69cdf5f95b07a1372 Mon Sep 17 00:00:00 2001 From: Kerwin Date: Wed, 19 Nov 2025 13:40:13 +0800 Subject: [PATCH] update: copyright-claims --- db/converter/copyright_claim_converter.go | 59 ++- db/dto/copyright_claim.go | 29 +- handlers/copyright_claim_handler.go | 48 ++- main.go | 2 +- web/pages/admin/copyright-claims.vue | 435 +++++++++++++++++++--- 5 files changed, 489 insertions(+), 84 deletions(-) diff --git a/db/converter/copyright_claim_converter.go b/db/converter/copyright_claim_converter.go index 6a457cb..34712c7 100644 --- a/db/converter/copyright_claim_converter.go +++ b/db/converter/copyright_claim_converter.go @@ -1,12 +1,66 @@ package converter import ( + "time" "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" - "time" ) -// CopyrightClaimToResponse 将版权申述实体转换为响应对象 +// CopyrightClaimToResponseWithResources 将版权申述实体和关联资源转换为响应对象 +func CopyrightClaimToResponseWithResources(claim *entity.CopyrightClaim, resources []*entity.Resource) *dto.CopyrightClaimResponse { + if claim == nil { + return nil + } + + // 转换关联的资源信息 + var resourceInfos []dto.ResourceInfo + for _, resource := range resources { + categoryName := "" + if resource.Category.ID != 0 { + categoryName = resource.Category.Name + } + + panName := "" + if resource.Pan.ID != 0 { + panName = resource.Pan.Name + } + + resourceInfo := dto.ResourceInfo{ + ID: resource.ID, + Title: resource.Title, + Description: resource.Description, + URL: resource.URL, + SaveURL: resource.SaveURL, + FileSize: resource.FileSize, + Category: categoryName, + PanName: panName, + ViewCount: resource.ViewCount, + IsValid: resource.IsValid, + CreatedAt: resource.CreatedAt.Format(time.RFC3339), + } + resourceInfos = append(resourceInfos, resourceInfo) + } + + return &dto.CopyrightClaimResponse{ + ID: claim.ID, + ResourceKey: claim.ResourceKey, + Identity: claim.Identity, + ProofType: claim.ProofType, + Reason: claim.Reason, + ContactInfo: claim.ContactInfo, + ClaimantName: claim.ClaimantName, + ProofFiles: claim.ProofFiles, + UserAgent: claim.UserAgent, + IPAddress: claim.IPAddress, + Status: claim.Status, + Note: claim.Note, + CreatedAt: claim.CreatedAt.Format(time.RFC3339), + UpdatedAt: claim.UpdatedAt.Format(time.RFC3339), + Resources: resourceInfos, + } +} + +// CopyrightClaimToResponse 将版权申述实体转换为响应对象(不包含资源详情) func CopyrightClaimToResponse(claim *entity.CopyrightClaim) *dto.CopyrightClaimResponse { if claim == nil { return nil @@ -27,6 +81,7 @@ func CopyrightClaimToResponse(claim *entity.CopyrightClaim) *dto.CopyrightClaimR Note: claim.Note, CreatedAt: claim.CreatedAt.Format(time.RFC3339), UpdatedAt: claim.UpdatedAt.Format(time.RFC3339), + Resources: []dto.ResourceInfo{}, // 空的资源列表 } } diff --git a/db/dto/copyright_claim.go b/db/dto/copyright_claim.go index 19f28be..db3af47 100644 --- a/db/dto/copyright_claim.go +++ b/db/dto/copyright_claim.go @@ -21,20 +21,21 @@ type CopyrightClaimUpdateRequest struct { // CopyrightClaimResponse 版权申述响应 type CopyrightClaimResponse struct { - ID uint `json:"id"` - ResourceKey string `json:"resource_key"` - Identity string `json:"identity"` - ProofType string `json:"proof_type"` - Reason string `json:"reason"` - ContactInfo string `json:"contact_info"` - ClaimantName string `json:"claimant_name"` - ProofFiles string `json:"proof_files"` - UserAgent string `json:"user_agent"` - IPAddress string `json:"ip_address"` - Status string `json:"status"` - Note string `json:"note"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID uint `json:"id"` + ResourceKey string `json:"resource_key"` + Identity string `json:"identity"` + ProofType string `json:"proof_type"` + Reason string `json:"reason"` + ContactInfo string `json:"contact_info"` + ClaimantName string `json:"claimant_name"` + ProofFiles string `json:"proof_files"` + UserAgent string `json:"user_agent"` + IPAddress string `json:"ip_address"` + Status string `json:"status"` + Note string `json:"note"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Resources []ResourceInfo `json:"resources"` } // CopyrightClaimListRequest 版权申述列表请求 diff --git a/handlers/copyright_claim_handler.go b/handlers/copyright_claim_handler.go index 6fc6b7c..cd1c707 100644 --- a/handlers/copyright_claim_handler.go +++ b/handlers/copyright_claim_handler.go @@ -7,6 +7,7 @@ import ( "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/repo" + "github.com/ctwj/urldb/middleware" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" @@ -15,12 +16,14 @@ import ( type CopyrightClaimHandler struct { copyrightClaimRepo repo.CopyrightClaimRepository + resourceRepo repo.ResourceRepository validate *validator.Validate } -func NewCopyrightClaimHandler(copyrightClaimRepo repo.CopyrightClaimRepository) *CopyrightClaimHandler { +func NewCopyrightClaimHandler(copyrightClaimRepo repo.CopyrightClaimRepository, resourceRepo repo.ResourceRepository) *CopyrightClaimHandler { return &CopyrightClaimHandler{ copyrightClaimRepo: copyrightClaimRepo, + resourceRepo: resourceRepo, validate: validator.New(), } } @@ -144,7 +147,38 @@ func (h *CopyrightClaimHandler) ListCopyrightClaims(c *gin.Context) { return } - PageResponse(c, converter.CopyrightClaimsToResponse(claims), total, req.Page, req.PageSize) + // 转换为包含资源信息的响应 + var responses []*dto.CopyrightClaimResponse + for _, claim := range claims { + // 查询关联的资源信息 + resources, err := h.getResourcesByResourceKey(claim.ResourceKey) + if err != nil { + // 如果查询资源失败,使用空资源列表 + responses = append(responses, converter.CopyrightClaimToResponse(claim)) + } else { + // 使用包含资源详情的转换函数 + responses = append(responses, converter.CopyrightClaimToResponseWithResources(claim, resources)) + } + } + + PageResponse(c, responses, total, req.Page, req.PageSize) +} + +// getResourcesByResourceKey 根据资源key获取关联的资源列表 +func (h *CopyrightClaimHandler) getResourcesByResourceKey(resourceKey string) ([]*entity.Resource, error) { + // 从资源仓库获取与key关联的所有资源 + resources, err := h.resourceRepo.FindByResourceKey(resourceKey) + if err != nil { + return nil, err + } + + // 将 []entity.Resource 转换为 []*entity.Resource + var resourcePointers []*entity.Resource + for i := range resources { + resourcePointers = append(resourcePointers, &resources[i]) + } + + return resourcePointers, nil } // UpdateCopyrightClaim 更新版权申述状态 @@ -263,16 +297,16 @@ func (h *CopyrightClaimHandler) GetCopyrightClaimByResource(c *gin.Context) { } // RegisterCopyrightClaimRoutes 注册版权申述相关路由 -func RegisterCopyrightClaimRoutes(router *gin.RouterGroup, copyrightClaimRepo repo.CopyrightClaimRepository) { - handler := NewCopyrightClaimHandler(copyrightClaimRepo) +func RegisterCopyrightClaimRoutes(router *gin.RouterGroup, copyrightClaimRepo repo.CopyrightClaimRepository, resourceRepo repo.ResourceRepository) { + handler := NewCopyrightClaimHandler(copyrightClaimRepo, resourceRepo) claims := router.Group("/copyright-claims") { claims.POST("", handler.CreateCopyrightClaim) // 创建版权申述 claims.GET("/:id", handler.GetCopyrightClaim) // 获取版权申述详情 - claims.GET("", handler.ListCopyrightClaims) // 获取版权申述列表 - claims.PUT("/:id", handler.UpdateCopyrightClaim) // 更新版权申述状态 - claims.DELETE("/:id", handler.DeleteCopyrightClaim) // 删除版权申述 + claims.GET("", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.ListCopyrightClaims) // 获取版权申述列表 + claims.PUT("/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.UpdateCopyrightClaim) // 更新版权申述状态 + claims.DELETE("/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.DeleteCopyrightClaim) // 删除版权申述 claims.GET("/resource/:resource_key", handler.GetCopyrightClaimByResource) // 获取资源版权申述列表 } } \ No newline at end of file diff --git a/main.go b/main.go index 2cc6795..c2e555a 100644 --- a/main.go +++ b/main.go @@ -213,7 +213,7 @@ func main() { // 创建举报和版权申述处理器 reportHandler := handlers.NewReportHandler(repoManager.ReportRepository, repoManager.ResourceRepository) - copyrightClaimHandler := handlers.NewCopyrightClaimHandler(repoManager.CopyrightClaimRepository) + copyrightClaimHandler := handlers.NewCopyrightClaimHandler(repoManager.CopyrightClaimRepository, repoManager.ResourceRepository) // API路由 api := r.Group("/api") diff --git a/web/pages/admin/copyright-claims.vue b/web/pages/admin/copyright-claims.vue index 4a3003c..b57bf4e 100644 --- a/web/pages/admin/copyright-claims.vue +++ b/web/pages/admin/copyright-claims.vue @@ -85,7 +85,7 @@ :bordered="false" :single-line="false" :loading="loading" - :scroll-x="1200" + :scroll-x="1020" class="h-full" /> @@ -143,7 +143,20 @@

证明文件

-

{{ selectedClaim.proof_files }}

+
+
+
+ + {{ getFileName(file) }} +
+ +
+

提交时间

@@ -204,102 +217,180 @@ const columns = [ { title: 'ID', key: 'id', - width: 80, + width: 60, render: (row: any) => { - return h('span', { class: 'font-medium' }, row.id) + return h('div', { class: 'space-y-1' }, [ + h('div', { class: 'font-medium text-sm' }, row.id), + h('div', { + class: 'text-xs text-gray-400', + title: `IP: ${row.ip_address || '未知'}` + }, row.ip_address ? `IP: ${row.ip_address.slice(0, 8)}...` : 'IP:未知') + ]) } }, { - title: '资源Key', + title: '资源', key: 'resource_key', + width: 200, + render: (row: any) => { + const resourceInfo = getResourceInfo(row); + return h('div', { class: 'space-y-1' }, [ + // 第一行:标题(单行,省略号) + h('div', { + class: 'font-medium text-sm truncate max-w-[200px]', + style: { maxWidth: '200px' }, + title: resourceInfo.title // 鼠标hover显示完整标题 + }, resourceInfo.title), + // 第二行:详情(单行,省略号) + h('div', { + class: 'text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]', + style: { maxWidth: '200px' }, + title: resourceInfo.description // 鼠标hover显示完整描述 + }, resourceInfo.description), + // 第三行:分类图片和链接数 + h('div', { class: 'flex items-center gap-1' }, [ + h('i', { + class: `fas fa-${getCategoryIcon(resourceInfo.category)} text-blue-500 text-xs`, + // 鼠标hover显示第一个资源的链接地址 + title: resourceInfo.resources.length > 0 ? `链接地址: ${resourceInfo.resources[0].save_url || resourceInfo.resources[0].url}` : `资源链接地址: ${row.resource_key}` + }), + h('span', { class: 'text-xs text-gray-400' }, `链接数: ${resourceInfo.resources.length}`) + ]) + ]) + } + }, + { + title: '申述人信息', + key: 'claimant_info', width: 180, render: (row: any) => { - return h('n-tag', { - type: 'info', - size: 'small', - class: 'truncate max-w-xs' - }, { default: () => row.resource_key }) + return h('div', { class: 'space-y-1' }, [ + // 第一行:姓名和身份 + h('div', { class: 'font-medium text-sm' }, [ + h('i', { class: 'fas fa-user text-green-500 mr-1 text-xs' }), + row.claimant_name || '未知' + ]), + h('div', { + class: 'text-xs text-blue-600 dark:text-blue-400 truncate max-w-[180px]', + title: getIdentityLabel(row.identity) + }, getIdentityLabel(row.identity)), + // 第二行:联系方式 + h('div', { + class: 'text-xs text-gray-500 dark:text-gray-400 truncate max-w-[180px]', + title: row.contact_info + }, [ + h('i', { class: 'fas fa-phone text-purple-500 mr-1' }), + row.contact_info || '未提供' + ]), + // 第三行:证明类型 + h('div', { + class: 'text-xs text-orange-600 dark:text-orange-400 truncate max-w-[180px]', + title: getProofTypeLabel(row.proof_type) + }, [ + h('i', { class: 'fas fa-certificate text-orange-500 mr-1' }), + getProofTypeLabel(row.proof_type) + ]) + ]) } }, { - title: '申述人身份', - key: 'identity', - width: 120, + title: '申述详情', + key: 'claim_details', + width: 280, render: (row: any) => { - return h('span', null, getIdentityLabel(row.identity)) - } - }, - { - title: '证明类型', - key: 'proof_type', - width: 140, - render: (row: any) => { - return h('span', null, getProofTypeLabel(row.proof_type)) - } - }, - { - title: '申述人姓名', - key: 'claimant_name', - width: 120, - render: (row: any) => { - return h('span', null, row.claimant_name) - } - }, - { - title: '联系方式', - key: 'contact_info', - width: 150, - render: (row: any) => { - return h('span', null, row.contact_info) + return h('div', { class: 'space-y-1' }, [ + // 第一行:申述理由和提交时间 + h('div', { class: 'space-y-1' }, [ + h('div', { class: 'text-xs text-gray-500 dark:text-gray-400' }, '申述理由:'), + h('div', { + class: 'text-sm text-gray-700 dark:text-gray-300 line-clamp-2 max-h-10', + title: row.reason + }, row.reason || '无'), + h('div', { class: 'text-xs text-gray-400' }, [ + h('i', { class: 'fas fa-clock mr-1' }), + `提交时间: ${formatDateTime(row.created_at)}` + ]) + ]), + // 第二行:证明文件 + row.proof_files ? + h('div', { class: 'space-y-1' }, [ + h('div', { class: 'text-xs text-gray-500 dark:text-gray-400' }, '证明文件:'), + ...getProofFiles(row.proof_files).slice(0, 2).map((file, index) => + h('div', { + class: 'text-xs text-blue-600 dark:text-blue-400 truncate max-w-[280px] cursor-pointer hover:text-blue-500 hover:underline', + title: `点击下载: ${file}`, + onClick: () => downloadFile(file) + }, [ + h('i', { class: 'fas fa-download text-blue-500 mr-1' }), + getFileName(file) + ]) + ), + getProofFiles(row.proof_files).length > 2 ? + h('div', { class: 'text-xs text-gray-400' }, `还有 ${getProofFiles(row.proof_files).length - 2} 个文件...`) : null + ]) : + h('div', { class: 'text-xs text-gray-400' }, '无证明文件'), + // 第三行:处理备注(如果有) + row.note ? + h('div', { class: 'space-y-1' }, [ + h('div', { class: 'text-xs text-gray-500 dark:text-gray-400' }, '处理备注:'), + h('div', { + class: 'text-xs text-yellow-600 dark:text-yellow-400 truncate max-w-[280px]', + title: row.note + }, [ + h('i', { class: 'fas fa-sticky-note text-yellow-500 mr-1' }), + row.note.length > 30 ? `${row.note.slice(0, 30)}...` : row.note + ]) + ]) : null + ].filter(Boolean)) } }, { title: '状态', key: 'status', - width: 120, + width: 100, render: (row: any) => { const type = getStatusType(row.status) - return h('n-tag', { - type: type, - size: 'small', - bordered: false - }, { default: () => getStatusLabel(row.status) }) - } - }, - { - title: '提交时间', - key: 'created_at', - width: 180, - render: (row: any) => { - return h('span', null, formatDateTime(row.created_at)) + return h('div', { class: 'space-y-1' }, [ + h('n-tag', { + type: type, + size: 'small', + bordered: false + }, { default: () => getStatusLabel(row.status) }), + // 显示处理时间(如果已处理) + (row.status !== 'pending' && row.updated_at) ? + h('div', { + class: 'text-xs text-gray-400', + title: `处理时间: ${formatDateTime(row.updated_at)}` + }, `更新: ${new Date(row.updated_at).toLocaleDateString()}`) : null + ].filter(Boolean)) } }, { title: '操作', key: 'actions', - width: 180, + width: 160, render: (row: any) => { const buttons = [ h('button', { - class: 'px-2 py-1 text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400 rounded transition-colors mr-1', + class: 'px-2 py-1 text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400 rounded transition-colors mb-1 w-full', onClick: () => viewClaim(row) }, [ h('i', { class: 'fas fa-eye mr-1 text-xs' }), - '查看' + '查看详情' ]) ] if (row.status === 'pending') { buttons.push( h('button', { - class: 'px-2 py-1 text-xs bg-green-100 hover:bg-green-200 text-green-700 dark:bg-green-900/20 dark:text-green-400 rounded transition-colors mr-1', + class: 'px-2 py-1 text-xs bg-green-100 hover:bg-green-200 text-green-700 dark:bg-green-900/20 dark:text-green-400 rounded transition-colors mb-1 w-full', onClick: () => updateClaimStatus(row, 'approved') }, [ h('i', { class: 'fas fa-check mr-1 text-xs' }), '批准' ]), h('button', { - class: 'px-2 py-1 text-xs bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-900/20 dark:text-red-400 rounded transition-colors', + class: 'px-2 py-1 text-xs bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-900/20 dark:text-red-400 rounded transition-colors w-full', onClick: () => updateClaimStatus(row, 'rejected') }, [ h('i', { class: 'fas fa-times mr-1 text-xs' }), @@ -308,7 +399,7 @@ const columns = [ ) } - return h('div', { class: 'flex items-center gap-1' }, buttons) + return h('div', { class: 'flex flex-col gap-1' }, buttons) } } ] @@ -338,8 +429,18 @@ const fetchClaims = async () => { if (filters.value.resourceKey) params.resource_key = filters.value.resourceKey const response = await resourceApi.getCopyrightClaims(params) - claims.value = response.items || [] - pagination.value.total = response.total || 0 + console.log(response) + + // 检查响应格式并处理 + if (response && response.data && response.data.list !== undefined) { + // 如果后端返回了分页格式,使用正确的字段 + claims.value = response.data.list || [] + pagination.value.total = response.data.total || 0 + } else { + // 如果是其他格式,尝试直接使用响应 + claims.value = response || [] + pagination.value.total = response.length || 0 + } } catch (error) { console.error('获取版权申述列表失败:', error) // 显示错误提示 @@ -503,6 +604,220 @@ const formatDateTime = (dateString: string) => { return date.toLocaleString('zh-CN') } +// 获取分类图标 +const getCategoryIcon = (category: string) => { + if (!category) return 'folder'; + + // 根据分类名称返回对应的图标 + const categoryMap: Record = { + '文档': 'file-alt', + '文档资料': 'file-alt', + '压缩包': 'file-archive', + '图片': 'images', + '视频': 'film', + '音乐': 'music', + '电子书': 'book', + '软件': 'cogs', + '应用': 'mobile-alt', + '游戏': 'gamepad', + '资料': 'folder', + '其他': 'file', + 'folder': 'folder', + 'file': 'file' + }; + + return categoryMap[category] || 'folder'; +} + +// 获取资源信息显示 +const getResourceInfo = (row: any) => { + // 从后端返回的资源列表中获取信息 + const resources = row.resources || []; + + if (resources.length > 0) { + // 如果有多个资源,可以选择第一个或合并信息 + const resource = resources[0]; + return { + title: resource.title || `资源: ${row.resource_key}`, + description: resource.description || `资源详情: ${row.resource_key}`, + category: resource.category || 'folder', + resources: resources // 返回所有资源用于显示链接数量等 + } + } else { + // 如果没有关联资源,使用默认值 + return { + title: `资源: ${row.resource_key}`, + description: `资源详情: ${row.resource_key}`, + category: 'folder', + resources: [] + } + } +} + +// 解析证明文件字符串 +const getProofFiles = (proofFiles: string) => { + if (!proofFiles) return [] + + console.log('原始证明文件数据:', proofFiles) + + try { + // 尝试解析为JSON格式 + const parsed = JSON.parse(proofFiles) + console.log('JSON解析结果:', parsed) + + if (Array.isArray(parsed)) { + // 处理对象数组格式:[{id: "xxx", name: "文件名.pdf", status: "pending"}] + const fileObjects = parsed.filter(item => item && typeof item === 'object') + if (fileObjects.length > 0) { + // 返回原始对象,包含完整信息 + console.log('解析出文件对象数组:', fileObjects) + return fileObjects + } + + // 如果不是对象数组,尝试作为字符串数组处理 + const files = parsed.filter(file => file && typeof file === 'string' && file.trim()).map(file => file.trim()) + if (files.length > 0) { + console.log('解析出的文件字符串数组:', files) + return files + } + } else if (typeof parsed === 'object' && parsed.url) { + console.log('解析出的单个文件:', parsed.url) + return [parsed.url] + } else if (typeof parsed === 'object' && parsed.files) { + // 处理 {files: ["url1", "url2"]} 格式 + if (Array.isArray(parsed.files)) { + const files = parsed.files.filter(file => file && file.trim()).map(file => file.trim()) + console.log('解析出的files数组:', files) + return files + } + } + } catch (e) { + console.log('JSON解析失败,尝试分隔符解析:', e.message) + // 如果不是JSON格式,按分隔符解析 + // 假设文件URL以逗号、分号或换行符分隔 + const files = proofFiles.split(/[,;\n\r]+/).filter(file => file.trim()).map(file => file.trim()) + console.log('分隔符解析结果:', files) + return files + } + + console.log('未解析出任何文件') + return [] +} + +// 获取文件名 +const getFileName = (fileInfo: any) => { + if (!fileInfo) return '未知文件' + + // 如果是对象,优先使用name字段 + if (typeof fileInfo === 'object') { + return fileInfo.name || fileInfo.id || '未知文件' + } + + // 如果是字符串,从URL中提取文件名 + const fileName = fileInfo.split('/').pop() || fileInfo.split('\\').pop() || fileInfo + + // 如果URL太长,截断显示 + return fileName.length > 50 ? fileName.substring(0, 47) + '...' : fileName +} + +// 下载文件 +const downloadFile = async (fileInfo: any) => { + console.log('尝试下载文件:', fileInfo) + + if (!fileInfo) { + console.error('文件信息为空') + if (process.client) { + notification.warning({ + content: '文件信息无效', + duration: 3000 + }) + } + return + } + + try { + let downloadUrl = '' + let fileName = '' + + // 处理文件对象格式:{id: "xxx", name: "文件名.pdf", status: "pending"} + if (typeof fileInfo === 'object' && fileInfo.id) { + fileName = fileInfo.name || fileInfo.id + // 构建下载API URL,假设有 /api/files/{id} 端点 + downloadUrl = `/api/files/${fileInfo.id}` + console.log('文件对象下载:', { id: fileInfo.id, name: fileName, url: downloadUrl }) + } + // 处理字符串格式(直接是URL) + else if (typeof fileInfo === 'string') { + downloadUrl = fileInfo + fileName = getFileName(fileInfo) + + // 检查是否是文件名(不包含http://或https://或/开头) + if (!fileInfo.match(/^https?:\/\//) && !fileInfo.startsWith('/')) { + console.log('检测到纯文件名,需要通过API下载:', fileName) + + if (process.client) { + notification.info({ + content: `文件 "${fileName}" 需要通过API下载,功能开发中...`, + duration: 3000 + }) + } + return + } + + // 处理相对路径URL + if (fileInfo.startsWith('/uploads/')) { + downloadUrl = `${window.location.origin}${fileInfo}` + console.log('处理本地文件URL:', downloadUrl) + } + } + + if (!downloadUrl) { + console.error('无法确定下载URL') + if (process.client) { + notification.warning({ + content: '无法确定下载地址', + duration: 3000 + }) + } + return + } + + // 创建下载链接 + const link = document.createElement('a') + link.href = downloadUrl + link.target = '_blank' // 在新标签页打开,避免跨域问题 + + // 设置下载文件名 + link.download = fileName.includes('.') ? fileName : fileName + '.file' + + console.log('下载参数:', { + originalInfo: fileInfo, + downloadUrl: downloadUrl, + fileName: fileName + }) + + // 添加到页面并触发点击 + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + if (process.client) { + notification.success({ + content: `开始下载: ${fileName}`, + duration: 2000 + }) + } + } catch (error) { + console.error('下载文件失败:', error) + if (process.client) { + notification.error({ + content: `下载失败: ${error.message}`, + duration: 3000 + }) + } + } +} + // 初始化数据 onMounted(() => { fetchClaims()