2 Commits

Author SHA1 Message Date
Kerwin
11a3204c18 update: check 2025-11-19 16:50:47 +08:00
Kerwin
5276112e48 update: copyright-claims 2025-11-19 13:40:13 +08:00
9 changed files with 965 additions and 134 deletions

View File

@@ -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{}, // 空的资源列表
}
}

View File

@@ -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 版权申述列表请求

View File

@@ -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) // 获取资源版权申述列表
}
}

View File

@@ -8,6 +8,7 @@ import (
"time"
pan "github.com/ctwj/urldb/common"
panutils "github.com/ctwj/urldb/common"
commonutils "github.com/ctwj/urldb/common/utils"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
@@ -1212,6 +1213,282 @@ func GetRelatedResources(c *gin.Context) {
SuccessResponse(c, responseData)
}
// CheckResourceValidity 检查资源链接有效性
func CheckResourceValidity(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
return
}
// 查询资源信息
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "资源不存在", http.StatusNotFound)
return
}
utils.Info("开始检测资源有效性 - ID: %d, URL: %s", resource.ID, resource.URL)
// 检查缓存
cacheKey := fmt.Sprintf("resource_validity_%d", resource.ID)
cacheManager := utils.GetResourceValidityCache()
ttl := 5 * time.Minute // 5分钟缓存
if cachedData, found := cacheManager.Get(cacheKey, ttl); found {
if result, ok := cachedData.(gin.H); ok {
utils.Info("使用资源有效性缓存 - ID: %d", resource.ID)
result["cached"] = true
SuccessResponse(c, result)
return
}
}
// 执行检测:只使用深度检测实现
isValid, detectionMethod, err := performAdvancedValidityCheck(resource)
if err != nil {
utils.Error("深度检测资源链接失败 - ID: %d, Error: %v", resource.ID, err)
// 深度检测失败,但不标记为无效(用户可自行验证)
result := gin.H{
"resource_id": resource.ID,
"url": resource.URL,
"is_valid": resource.IsValid, // 保持原始状态
"last_checked": time.Now().Format(time.RFC3339),
"error": err.Error(),
"detection_method": detectionMethod,
"cached": false,
"note": "当前网盘暂不支持自动检测,建议用户自行验证",
}
cacheManager.Set(cacheKey, result)
SuccessResponse(c, result)
return
}
// 只有明确检测出无效的资源才更新数据库状态
// 如果检测成功且结果与数据库状态不同,则更新
if detectionMethod == "quark_deep" && isValid != resource.IsValid {
resource.IsValid = isValid
updateErr := repoManager.ResourceRepository.Update(resource)
if updateErr != nil {
utils.Error("更新资源有效性状态失败 - ID: %d, Error: %v", resource.ID, updateErr)
} else {
utils.Info("更新资源有效性状态 - ID: %d, Status: %v, Method: %s", resource.ID, isValid, detectionMethod)
}
}
// 构建检测结果
result := gin.H{
"resource_id": resource.ID,
"url": resource.URL,
"is_valid": isValid,
"last_checked": time.Now().Format(time.RFC3339),
"detection_method": detectionMethod,
"cached": false,
}
// 缓存检测结果
cacheManager.Set(cacheKey, result)
utils.Info("资源有效性检测完成 - ID: %d, Valid: %v, Method: %s", resource.ID, isValid, detectionMethod)
SuccessResponse(c, result)
}
// performAdvancedValidityCheck 执行深度检测(只使用具体网盘服务)
func performAdvancedValidityCheck(resource *entity.Resource) (bool, string, error) {
// 提取分享ID和服务类型
shareID, serviceType := panutils.ExtractShareId(resource.URL)
if serviceType == panutils.NotFound {
return false, "unsupported", fmt.Errorf("不支持的网盘服务: %s", resource.URL)
}
utils.Info("开始深度检测 - Service: %s, ShareID: %s", serviceType.String(), shareID)
// 根据服务类型选择检测策略
switch serviceType {
case panutils.Quark:
return performQuarkValidityCheck(resource, shareID)
case panutils.Alipan:
return performAlipanValidityCheck(resource, shareID)
case panutils.BaiduPan, panutils.UC, panutils.Xunlei, panutils.Tianyi, panutils.Pan123, panutils.Pan115:
// 这些网盘暂未实现深度检测,返回不支持提示
return false, "unsupported", fmt.Errorf("当前网盘类型 %s 暂不支持深度检测,请等待后续更新", serviceType.String())
default:
return false, "unsupported", fmt.Errorf("未知的网盘服务类型: %s", serviceType.String())
}
}
// performQuarkValidityCheck 夸克网盘深度检测
func performQuarkValidityCheck(resource *entity.Resource, shareID string) (bool, string, error) {
// 获取夸克网盘账号
panID, err := getQuarkPanID()
if err != nil {
return false, "quark_failed", fmt.Errorf("获取夸克平台ID失败: %v", err)
}
accounts, err := repoManager.CksRepository.FindByPanID(panID)
if err != nil {
return false, "quark_failed", fmt.Errorf("获取夸克网盘账号失败: %v", err)
}
if len(accounts) == 0 {
return false, "quark_failed", fmt.Errorf("没有可用的夸克网盘账号")
}
// 选择第一个有效账号
var selectedAccount *entity.Cks
for _, account := range accounts {
if account.IsValid {
selectedAccount = &account
break
}
}
if selectedAccount == nil {
return false, "quark_failed", fmt.Errorf("没有有效的夸克网盘账号")
}
// 创建网盘服务配置
config := &pan.PanConfig{
URL: resource.URL,
Code: "",
IsType: 1, // 只获取基本信息,不转存
ExpiredType: 1,
AdFid: "",
Stoken: "",
Cookie: selectedAccount.Ck,
}
// 创建夸克网盘服务
factory := pan.NewPanFactory()
panService, err := factory.CreatePanService(resource.URL, config)
if err != nil {
return false, "quark_failed", fmt.Errorf("创建夸克网盘服务失败: %v", err)
}
// 执行深度检测Transfer方法
utils.Info("执行夸克网盘深度检测 - ShareID: %s", shareID)
result, err := panService.Transfer(shareID)
if err != nil {
return false, "quark_failed", fmt.Errorf("夸克网盘检测失败: %v", err)
}
if !result.Success {
return false, "quark_failed", fmt.Errorf("夸克网盘链接无效: %s", result.Message)
}
utils.Info("夸克网盘深度检测成功 - ShareID: %s", shareID)
return true, "quark_deep", nil
}
// performAlipanValidityCheck 阿里云盘深度检测
func performAlipanValidityCheck(resource *entity.Resource, shareID string) (bool, string, error) {
// 阿里云盘深度检测暂未实现
utils.Info("阿里云盘暂不支持深度检测 - ShareID: %s", shareID)
return false, "unsupported", fmt.Errorf("阿里云盘暂不支持深度检测,请等待后续更新")
}
// BatchCheckResourceValidity 批量检查资源链接有效性
func BatchCheckResourceValidity(c *gin.Context) {
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.IDs) == 0 {
ErrorResponse(c, "ID列表不能为空", http.StatusBadRequest)
return
}
if len(req.IDs) > 20 {
ErrorResponse(c, "单次最多检测20个资源", http.StatusBadRequest)
return
}
utils.Info("开始批量检测资源有效性 - Count: %d", len(req.IDs))
cacheManager := utils.GetResourceValidityCache()
ttl := 5 * time.Minute
results := make([]gin.H, 0, len(req.IDs))
for _, id := range req.IDs {
// 查询资源信息
resource, err := repoManager.ResourceRepository.FindByID(id)
if err != nil {
results = append(results, gin.H{
"resource_id": id,
"is_valid": false,
"error": "资源不存在",
"cached": false,
})
continue
}
// 检查缓存
cacheKey := fmt.Sprintf("resource_validity_%d", id)
if cachedData, found := cacheManager.Get(cacheKey, ttl); found {
if result, ok := cachedData.(gin.H); ok {
result["cached"] = true
results = append(results, result)
continue
}
}
// 执行深度检测
isValid, detectionMethod, err := performAdvancedValidityCheck(resource)
if err != nil {
// 深度检测失败,但不标记为无效(用户可自行验证)
result := gin.H{
"resource_id": id,
"url": resource.URL,
"is_valid": resource.IsValid, // 保持原始状态
"last_checked": time.Now().Format(time.RFC3339),
"error": err.Error(),
"detection_method": detectionMethod,
"cached": false,
"note": "当前网盘暂不支持自动检测,建议用户自行验证",
}
cacheManager.Set(cacheKey, result)
results = append(results, result)
continue
}
// 只有明确检测出无效的资源才更新数据库状态
if detectionMethod == "quark_deep" && isValid != resource.IsValid {
resource.IsValid = isValid
updateErr := repoManager.ResourceRepository.Update(resource)
if updateErr != nil {
utils.Error("更新资源有效性状态失败 - ID: %d, Error: %v", id, updateErr)
}
}
result := gin.H{
"resource_id": id,
"url": resource.URL,
"is_valid": isValid,
"last_checked": time.Now().Format(time.RFC3339),
"detection_method": detectionMethod,
"cached": false,
}
cacheManager.Set(cacheKey, result)
results = append(results, result)
}
utils.Info("批量检测资源有效性完成 - Count: %d", len(results))
SuccessResponse(c, gin.H{
"results": results,
"total": len(results),
})
}
// getQuarkPanID 获取夸克网盘ID
func getQuarkPanID() (uint, error) {
// 通过FindAll方法查找所有平台然后过滤出quark平台

View File

@@ -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")
@@ -247,6 +247,8 @@ func main() {
api.GET("/resources/related", handlers.GetRelatedResources)
api.POST("/resources/:id/view", handlers.IncrementResourceViewCount)
api.GET("/resources/:id/link", handlers.GetResourceLink)
api.GET("/resources/:id/validity", handlers.CheckResourceValidity)
api.POST("/resources/validity/batch", handlers.BatchCheckResourceValidity)
api.DELETE("/resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchDeleteResources)
// 分类管理

View File

@@ -143,6 +143,9 @@ var (
// 标签缓存
TagsCache = NewCacheManager()
// 资源有效性检测缓存
ResourceValidityCache = NewCacheManager()
)
// GetHotResourcesCache 获取热门资源缓存管理器
@@ -170,6 +173,11 @@ func GetTagsCache() *CacheManager {
return TagsCache
}
// GetResourceValidityCache 获取资源有效性检测缓存管理器
func GetResourceValidityCache() *CacheManager {
return ResourceValidityCache
}
// ClearAllCaches 清空所有全局缓存
func ClearAllCaches() {
HotResourcesCache.Clear()
@@ -177,6 +185,7 @@ func ClearAllCaches() {
SystemConfigCache.Clear()
CategoriesCache.Clear()
TagsCache.Clear()
ResourceValidityCache.Clear()
}
// CleanAllExpiredCaches 清理所有过期缓存
@@ -187,6 +196,7 @@ func CleanAllExpiredCaches(ttl time.Duration) {
totalCleaned += SystemConfigCache.CleanExpired(ttl)
totalCleaned += CategoriesCache.CleanExpired(ttl)
totalCleaned += TagsCache.CleanExpired(ttl)
totalCleaned += ResourceValidityCache.CleanExpired(ttl)
if totalCleaned > 0 {
Info("清理过期缓存完成,共清理 %d 个缓存项", totalCleaned)

View File

@@ -63,6 +63,10 @@ export const useResourceApi = () => {
const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse)
// 新增:获取相关资源
const getRelatedResources = (params?: any) => useApiFetch('/resources/related', { params }).then(parseApiResponse)
// 新增:检查资源有效性
const checkResourceValidity = (id: number) => useApiFetch(`/resources/${id}/validity`).then(parseApiResponse)
// 新增:批量检查资源有效性
const batchCheckResourceValidity = (ids: number[]) => useApiFetch('/resources/validity/batch', { method: 'POST', body: { ids } }).then(parseApiResponse)
// 新增:提交举报
const submitReport = (data: any) => useApiFetch('/reports', { method: 'POST', body: data }).then(parseApiResponse)
// 新增:提交版权申述
@@ -82,7 +86,7 @@ export const useResourceApi = () => {
const deleteCopyrightClaim = (id: number) => useApiFetch(`/copyright-claims/${id}`, { method: 'DELETE' }).then(parseApiResponse)
return {
getResources, getHotResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources,
getResources, getHotResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources, checkResourceValidity, batchCheckResourceValidity,
submitReport, submitCopyrightClaim,
getReports, getReport, updateReport, deleteReport, getReportsRaw,
getCopyrightClaims, getCopyrightClaim, updateCopyrightClaim, deleteCopyrightClaim

View File

@@ -85,7 +85,7 @@
:bordered="false"
:single-line="false"
:loading="loading"
:scroll-x="1200"
:scroll-x="1020"
class="h-full"
/>
</div>
@@ -143,7 +143,20 @@
</div>
<div v-if="selectedClaim.proof_files">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">证明文件</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100 break-all">{{ selectedClaim.proof_files }}</p>
<div class="mt-1 space-y-2">
<div
v-for="(file, index) in getProofFiles(selectedClaim.proof_files)"
:key="index"
class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="downloadFile(file)"
>
<div class="flex items-center space-x-2">
<i class="fas fa-file-download text-blue-500"></i>
<span class="text-sm text-gray-900 dark:text-gray-100">{{ getFileName(file) }}</span>
</div>
<i class="fas fa-download text-gray-400 hover:text-blue-500 transition-colors"></i>
</div>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">提交时间</h3>
@@ -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<string, string> = {
'文档': '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()

View File

@@ -128,10 +128,19 @@
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<i class="fas fa-cloud-download-alt text-blue-500"></i>
网盘资源 ({{ resourcesData?.resources?.length || 0 }})
</h3>
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<i class="fas fa-cloud-download-alt text-blue-500"></i>
网盘资源 ({{ resourcesData?.resources?.length || 0 }})
</h3>
<!-- 检测状态总览 -->
<div v-if="resourcesData?.resources?.length > 0" class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium" :class="detectionStatus.text + ' bg-opacity-10 ' + detectionStatus.text.replace('text', 'bg')">
<i :class="detectionStatus.icon"></i>
<span>{{ detectionStatus.label }}</span>
<span v-if="detectionStatus.detectedCount > 0" class="ml-1 opacity-75">({{ detectionStatus.detectedCount }}已检测)</span>
</div>
</div>
<!-- 分享按钮 -->
<ShareButtons
@@ -175,16 +184,45 @@
</div>
</div>
<!-- 右侧操作按钮 -->
<!-- 右侧检测状态和操作按钮 -->
<div class="flex items-center gap-2">
<!-- 检测状态标签放在最前面 -->
<div v-if="detectionMethods[resource.id]" class="flex items-center gap-1">
<!-- 检测方法标识 -->
<span
class="px-1.5 py-0.5 rounded text-xs font-medium"
:class="getDetectionMethodClass(detectionMethods[resource.id])"
:title="getDetectionMethodTitle(detectionMethods[resource.id], resource)"
>
{{ getDetectionMethodLabel(detectionMethods[resource.id]) }}
</span>
<!-- 不支持检测的三角感叹号提示 -->
<span v-if="detectionMethods[resource.id] === 'unsupported'" class="text-amber-600 dark:text-amber-400" title="当前网盘暂不支持自动检测,建议您点击链接自行验证">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<!-- 检测中状态 -->
<div v-if="isDetecting && !detectionResults[resource.id]" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
<div v-if="isDetecting && !detectionResults[resource.id]" class="flex items-center gap-2 px-3 py-2 text-sm text-blue-600 dark:text-blue-400">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<span>检测中</span>
<span>检测中... ({{ Object.keys(detectionResults).length }}/{{ resourcesData?.resources?.length }})</span>
</div>
<!-- 检测完成后的按钮 -->
<template v-else>
<!-- 重新检测按钮 -->
<button
class="px-3 py-1.5 text-xs font-medium rounded-lg border border-green-200 dark:border-green-400/30 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-500/20 transition-colors flex items-center gap-1 disabled:opacity-50"
:disabled="isDetecting"
@click="smartDetectResourceValidity(true)"
title="重新检测链接有效性"
>
<i class="fas" :class="isDetecting ? 'fa-spinner fa-spin' : 'fa-sync-alt'"></i>
<span class="hidden sm:inline">{{ isDetecting ? '检测中' : '重测' }}</span>
</button>
<!-- 获取链接按钮 -->
<button
class="px-4 py-2 text-sm font-medium rounded-lg border border-blue-200 dark:border-blue-400/30 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors flex items-center gap-2 disabled:opacity-50"
:disabled="resource.forbidden || loadingStates[resource.id]"
@@ -194,6 +232,7 @@
{{ resource.forbidden ? '受限' : (loadingStates[resource.id] ? '获取中' : '获取链接') }}
</button>
<!-- 复制转存链接按钮 -->
<button
v-if="resource.save_url && !resource.forbidden"
class="p-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-slate-600 transition-colors"
@@ -453,6 +492,9 @@ const selectedResource = ref<any>(null)
const loadingStates = ref<Record<number, boolean>>({})
const isDetecting = ref(false)
const detectionResults = ref<Record<number, boolean>>({})
const detectionErrors = ref<Record<number, string>>({})
const detectionMethods = ref<Record<number, string>>({})
const detectionNotes = ref<Record<number, string>>({})
const relatedResources = ref<any[]>([])
const relatedResourcesLoading = ref(true)
const hotResources = ref<any[]>([])
@@ -496,23 +538,16 @@ const mainResource = computed(() => {
// 生成完整的资源URL
const getResourceUrl = computed(() => {
const config = useRuntimeConfig()
const key = mainResource.value?.key
if (!key) return ''
// 优先使用配置中的站点URL如果未设置则使用当前页面的origin
let siteUrl = config.public.siteUrl
if (!siteUrl || siteUrl === 'https://yourdomain.com') {
// 在客户端使用当前页面的origin
if (typeof window !== 'undefined') {
siteUrl = window.location.origin
} else {
// 在服务端渲染时,使用默认值(这应该在部署时被环境变量覆盖)
siteUrl = process.env.NUXT_PUBLIC_SITE_URL || 'https://yourdomain.com'
}
// 在客户端直接使用当前页面的origin
if (typeof window !== 'undefined') {
return `${window.location.origin}/r/${key}`
}
return `${siteUrl}/r/${key}`
// 在服务端渲染时返回相对路径(客户端会自动补全)
return `/r/${key}`
})
// 服务端相关资源处理(去重)
@@ -555,7 +590,8 @@ const detectionStatus = computed(() => {
return {
icon: 'fas fa-spinner fa-spin text-blue-600',
text: 'text-blue-600',
label: '检测中'
label: '检测中',
detectedCount: 0
}
}
@@ -564,30 +600,49 @@ const detectionStatus = computed(() => {
return {
icon: 'fas fa-question-circle text-gray-400',
text: 'text-gray-400',
label: '未知状态'
label: '未知状态',
detectedCount: 0
}
}
const validCount = resources.filter(r => detectionResults.value[r.id] !== false && (detectionResults.value[r.id] || r.is_valid)).length
const totalCount = resources.length
// 统计有效和已检测的资源(只统计真正有检测结果的)
const detectedResources = resources.filter(r => detectionResults.value[r.id] !== undefined)
const validCount = detectedResources.filter(r => detectionResults.value[r.id] === true).length
const totalCount = detectedResources.length
const undetectedCount = resources.length - detectedResources.length
// 如果没有检测任何资源,显示未检测状态
if (detectedResources.length === 0) {
return {
icon: 'fas fa-question-circle text-gray-400',
text: 'text-gray-400',
label: '未检测',
detectedCount: 0
}
}
// 基于已检测的资源显示状态
if (validCount === totalCount) {
return {
icon: 'fas fa-check-circle text-green-600',
text: 'text-green-600',
label: '全部有效'
label: '全部有效',
detectedCount: detectedResources.length
}
} else if (validCount === 0) {
return {
icon: 'fas fa-times-circle text-red-600',
text: 'text-red-600',
label: '全部无效'
label: '全部无效',
detectedCount: detectedResources.length
}
} else {
return {
icon: 'fas fa-exclamation-triangle text-orange-600',
text: 'text-orange-600',
label: `${validCount}/${totalCount} 有效`
label: `${validCount}/${totalCount} 有效`,
detectedCount: detectedResources.length
}
}
})
@@ -643,28 +698,77 @@ const detectResourceValidity = async () => {
isDetecting.value = true
detectionResults.value = {} // 重置检测结果
detectionErrors.value = {} // 重置错误信息
detectionMethods.value = {} // 重置检测方法
detectionNotes.value = {} // 重置检测提示
try {
// 逐个检测每个资源
for (const resource of resourcesData.value.resources) {
try {
// 这里可以添加实际的检测逻辑
// const result = await resourceApi.checkResourceValidity(resource.id)
// detectionResults.value[resource.id] = result.isValid
// 提取所有资源ID
const resourceIds = resourcesData.value.resources.map(r => r.id)
// 暂时使用现有的is_valid字段但添加随机延迟模拟真实检测
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 700))
detectionResults.value[resource.id] = resource.is_valid
} catch (error) {
console.error(`检测资源 ${resource.id} 失败:`, error)
detectionResults.value[resource.id] = false
}
// 批量检测所有资源
const response = await resourceApi.batchCheckResourceValidity(resourceIds) as any
// 处理检测结果
if (response && response.results) {
response.results.forEach((result: any) => {
// 只有真正进行了检测的资源才设置检测结果
if (result.detection_method !== 'unsupported') {
detectionResults.value[result.resource_id] = result.is_valid
}
detectionMethods.value[result.resource_id] = result.detection_method || 'unknown'
// 保存错误信息
if (result.error) {
detectionErrors.value[result.resource_id] = result.error
}
// 保存检测提示
if (result.note) {
detectionNotes.value[result.resource_id] = result.note
}
// 显示缓存状态
if (result.cached) {
console.log(`资源 ${result.resource_id} 使用缓存检测结果`)
}
})
}
} finally {
isDetecting.value = false
}
}
// 智能检测:避免频繁重复检测
const smartDetectResourceValidity = async (force = false) => {
if (!resourcesData.value?.resources) return
// 如果正在检测,不重复执行
if (isDetecting.value) {
console.log('检测正在进行中,跳过重复请求')
return
}
// 如果不是强制检测且已有检测结果,跳过
if (!force && Object.keys(detectionResults.value).length > 0) {
const lastDetectionTime = lastDetectionTimestamp.value
const now = Date.now()
const timeSinceLastDetection = now - lastDetectionTime
// 5分钟内不重复检测
if (timeSinceLastDetection < 5 * 60 * 1000) {
console.log('距离上次检测时间不足5分钟跳过重复检测')
return
}
}
await detectResourceValidity()
lastDetectionTimestamp.value = Date.now()
}
// 添加最后检测时间戳
const lastDetectionTimestamp = ref(0)
// 切换链接显示
const toggleLink = async (resource: any) => {
if (resource.forbidden) {
@@ -833,15 +937,39 @@ const navigateToResource = (key: string) => {
navigateTo(`/r/${key}`)
}
// 页面加载完成后开始检测
onMounted(() => {
// 开始检测资源有效性
nextTick(() => {
if (resourcesData.value?.resources) {
detectResourceValidity()
}
})
// 检测方法相关函数
const getDetectionMethodLabel = (method: string) => {
const labels: Record<string, string> = {
'quark_deep': '深度检测',
'unsupported': '未检测',
'unknown': '未知方法',
'error': '检测错误'
}
return labels[method] || '未知'
}
const getDetectionMethodClass = (method: string) => {
const classes: Record<string, string> = {
'quark_deep': 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-100',
'unsupported': 'bg-gray-100 text-gray-600 dark:bg-gray-500/20 dark:text-gray-300',
'unknown': 'bg-gray-100 text-gray-700 dark:bg-gray-500/20 dark:text-gray-100',
'error': 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-100'
}
return classes[method] || 'bg-gray-100 text-gray-700 dark:bg-gray-500/20 dark:text-gray-100'
}
const getDetectionMethodTitle = (method: string, resource: any) => {
const titles: Record<string, string> = {
'quark_deep': '使用深度检测(通过实际网盘操作验证)',
'unsupported': `${resource.pan?.remark || '未知网盘'} 暂不支持深度检测`,
'unknown': '检测方法未知',
'error': '检测过程中发生错误'
}
return titles[method] || '未知检测方法'
}
// 页面加载完成后
onMounted(() => {
// 获取相关资源
nextTick(() => {
fetchRelatedResources()
@@ -891,8 +1019,13 @@ const updatePageSeo = () => {
})
// 设置更详细的HTML元数据
const baseUrl = config.public.siteUrl || 'https://yourdomain.com'
const canonicalUrl = `${baseUrl}/r/${resourceKey.value}`
let canonicalUrl
if (typeof window !== 'undefined') {
canonicalUrl = `${window.location.origin}/r/${resourceKey.value}`
} else {
// 在服务端渲染时使用相对路径
canonicalUrl = `/r/${resourceKey.value}`
}
// 生成动态OG图片URL使用新的key参数格式
const ogImageUrl = generateOgImageUrl(resourceKey.value, '', 'blue')