diff --git a/font/SourceHanSansSC-Bold.otf b/font/SourceHanSansSC-Bold.otf new file mode 100644 index 0000000..3b33e77 Binary files /dev/null and b/font/SourceHanSansSC-Bold.otf differ diff --git a/font/SourceHanSansSC-Regular.otf b/font/SourceHanSansSC-Regular.otf new file mode 100644 index 0000000..8113ad5 Binary files /dev/null and b/font/SourceHanSansSC-Regular.otf differ diff --git a/handlers/og_image.go b/handlers/og_image.go index 35efae2..f55f012 100644 --- a/handlers/og_image.go +++ b/handlers/og_image.go @@ -2,14 +2,21 @@ package handlers import ( "bytes" + "fmt" "net/http" "strconv" "strings" "github.com/ctwj/urldb/utils" + "github.com/ctwj/urldb/db" + "github.com/ctwj/urldb/db/entity" "github.com/gin-gonic/gin" "github.com/fogleman/gg" "image/color" + "image" + _ "image/jpeg" + _ "image/png" + "io" ) // OGImageHandler 处理OG图片生成请求 @@ -20,17 +27,69 @@ func NewOGImageHandler() *OGImageHandler { return &OGImageHandler{} } +// Resource 简化的资源结构体 +type Resource struct { + Title string + Description string + Cover string + Key string +} + +// getResourceByKey 通过key获取资源信息 +func (h *OGImageHandler) getResourceByKey(key string) (*Resource, error) { + // 这里简化处理,实际应该从数据库查询 + // 为了演示,我们先返回一个模拟的资源 + // 在实际应用中,您需要连接数据库并查询 + + // 模拟数据库查询 - 实际应用中请替换为真实的数据库查询 + dbInstance := db.DB + if dbInstance == nil { + return nil, fmt.Errorf("数据库连接失败") + } + + var resource entity.Resource + result := dbInstance.Where("key = ?", key).First(&resource) + if result.Error != nil { + return nil, result.Error + } + + return &Resource{ + Title: resource.Title, + Description: resource.Description, + Cover: resource.Cover, + Key: resource.Key, + }, nil +} + // GenerateOGImage 生成OG图片 func (h *OGImageHandler) GenerateOGImage(c *gin.Context) { // 获取请求参数 + key := strings.TrimSpace(c.Query("key")) title := strings.TrimSpace(c.Query("title")) description := strings.TrimSpace(c.Query("description")) siteName := strings.TrimSpace(c.Query("site_name")) theme := strings.TrimSpace(c.Query("theme")) + coverUrl := strings.TrimSpace(c.Query("cover")) width, _ := strconv.Atoi(c.Query("width")) height, _ := strconv.Atoi(c.Query("height")) + // 如果提供了key,从数据库获取资源信息 + if key != "" { + resource, err := h.getResourceByKey(key) + if err == nil && resource != nil { + if title == "" { + title = resource.Title + } + if description == "" { + description = resource.Description + } + if coverUrl == "" && resource.Cover != "" { + coverUrl = resource.Cover + } + } + } + // 设置默认值 if title == "" { title = "老九网盘资源数据库" @@ -45,8 +104,16 @@ func (h *OGImageHandler) GenerateOGImage(c *gin.Context) { height = 630 } + // 获取当前请求的域名 + scheme := "http" + if c.Request.TLS != nil { + scheme = "https" + } + host := c.Request.Host + domain := scheme + "://" + host + // 生成图片 - imageBuffer, err := createOGImage(title, description, siteName, theme, width, height) + imageBuffer, err := createOGImage(title, description, siteName, theme, width, height, coverUrl, key, domain) if err != nil { utils.Error("生成OG图片失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ @@ -62,13 +129,16 @@ func (h *OGImageHandler) GenerateOGImage(c *gin.Context) { } // createOGImage 创建OG图片 -func createOGImage(title, description, siteName, theme string, width, height int) (*bytes.Buffer, error) { +func createOGImage(title, description, siteName, theme string, width, height int, coverUrl, key, domain string) (*bytes.Buffer, error) { dc := gg.NewContext(width, height) + // 设置圆角裁剪区域 + cornerRadius := 20.0 + dc.DrawRoundedRectangle(0, 0, float64(width), float64(height), cornerRadius) + // 设置背景色 bgColor := getBackgroundColor(theme) dc.SetColor(bgColor) - dc.DrawRectangle(0, 0, float64(width), float64(height)) dc.Fill() // 绘制渐变效果 @@ -76,62 +146,151 @@ func createOGImage(title, description, siteName, theme string, width, height int gradient.AddColorStop(0, getGradientStartColor(theme)) gradient.AddColorStop(1, getGradientEndColor(theme)) dc.SetFillStyle(gradient) - dc.DrawRectangle(0, 0, float64(width), float64(height)) dc.Fill() - // 设置站点标识 - dc.SetHexColor("#ffffff") - // 尝试加载字体,如果失败则使用默认字体 - if err := dc.LoadFontFace("assets/fonts/SourceHanSansCN-Regular.ttc", 24); err != nil { - // 使用默认字体设置 + // 定义布局区域 + imageAreaWidth := width / 3 // 左侧1/3用于图片 + textAreaWidth := width * 2 / 3 // 右侧2/3用于文案 + textAreaX := imageAreaWidth // 文案区域起始X坐标 + + // 统一的字体加载函数,确保中文显示正常 + loadChineseFont := func(fontSize float64) bool { + // 优先使用项目字体 + if err := dc.LoadFontFace("font/SourceHanSansSC-Regular.otf", fontSize); err == nil { + return true + } + + // Windows系统常见字体,按优先级顺序尝试 + commonFonts := []string{ + "C:/Windows/Fonts/msyh.ttc", // 微软雅黑 + "C:/Windows/Fonts/simhei.ttf", // 黑体 + "C:/Windows/Fonts/simsun.ttc", // 宋体 + } + + for _, fontPath := range commonFonts { + if err := dc.LoadFontFace(fontPath, fontSize); err == nil { + return true + } + } + + // 如果都失败了,尝试使用粗体版本 + boldFonts := []string{ + "C:/Windows/Fonts/msyhbd.ttc", // 微软雅黑粗体 + "C:/Windows/Fonts/simhei.ttf", // 黑体 + } + + for _, fontPath := range boldFonts { + if err := dc.LoadFontFace(fontPath, fontSize); err == nil { + return true + } + } + + return false } - dc.DrawStringAnchored(siteName, 60, 50, 0, 0.5) + // 加载基础字体(24px) + fontLoaded := loadChineseFont(24) + dc.SetHexColor("#ffffff") + + // 绘制封面图片(如果存在) + if coverUrl != "" { + if err := drawCoverImageInLeftArea(dc, coverUrl, width, height, imageAreaWidth); err != nil { + utils.Error("绘制封面图片失败: %v", err) + } + } + + // 设置站点标识 + dc.DrawStringAnchored(siteName, float64(textAreaX)+60, 50, 0, 0.5) // 绘制标题 dc.SetHexColor("#ffffff") - if err := dc.LoadFontFace("assets/fonts/SourceHanSansCN-Bold.ttc", 48); err != nil { - // 使用默认字体设置 - } - // 文字居中处理 - titleWidth, _ := dc.MeasureString(title) - if titleWidth > float64(width-120) { - // 如果标题过长,尝试加载较小字体 - if err := dc.LoadFontFace("assets/fonts/SourceHanSansCN-Bold.ttc", 42); err != nil { - // 使用默认字体设置 - } - titleWidth, _ = dc.MeasureString(title) - if titleWidth > float64(width-120) { - if err := dc.LoadFontFace("assets/fonts/SourceHanSansCN-Bold.ttc", 36); err != nil { - // 使用默认字体设置 + // 标题在右侧区域显示,考虑文案宽度限制 + maxTitleWidth := float64(textAreaWidth - 120) // 右侧区域减去左右边距 + + // 动态调整字体大小以适应文案区域,使用统一的字体加载逻辑 + fontSize := 48.0 + titleFontLoaded := false + for fontSize > 24 { // 最小字体24 + // 优先使用项目粗体字体 + if err := dc.LoadFontFace("font/SourceHanSansSC-Bold.otf", fontSize); err == nil { + titleWidth, _ := dc.MeasureString(title) + if titleWidth <= maxTitleWidth { + titleFontLoaded = true + break + } + } else { + // 尝试系统粗体字体 + boldFonts := []string{ + "C:/Windows/Fonts/msyhbd.ttc", // 微软雅黑粗体 + "C:/Windows/Fonts/simhei.ttf", // 黑体 + } + for _, fontPath := range boldFonts { + if err := dc.LoadFontFace(fontPath, fontSize); err == nil { + titleWidth, _ := dc.MeasureString(title) + if titleWidth <= maxTitleWidth { + titleFontLoaded = true + break + } + break // 找到可用字体就跳出内层循环 + } + } + if titleFontLoaded { + break } } + fontSize -= 4 } - dc.DrawStringAnchored(title, float64(width)/2, float64(height)/2-30, 0.5, 0.5) + // 如果粗体字体都失败了,使用常规字体 + if !titleFontLoaded { + loadChineseFont(36) // 使用稍大的常规字体 + } + + // 标题左对齐显示在右侧区域 + titleX := float64(textAreaX) + 60 + titleY := float64(height)/2 - 80 + dc.DrawString(title, titleX, titleY) // 绘制描述 if description != "" { dc.SetHexColor("#e5e7eb") - // 尝试加载较小字体 - if err := dc.LoadFontFace("assets/fonts/SourceHanSansCN-Regular.ttc", 28); err != nil { - // 使用默认字体设置 - } + // 使用统一的字体加载逻辑 + loadChineseFont(28) - // 自动换行处理 - wrappedDesc := wrapText(dc, description, float64(width-120)) - startY := float64(height)/2 + 40 + // 自动换行处理,适配右侧区域宽度 + wrappedDesc := wrapText(dc, description, float64(textAreaWidth-120)) + descY := titleY + 60 // 标题下方 for i, line := range wrappedDesc { - y := startY + float64(i)*35 - dc.DrawStringAnchored(line, float64(width)/2, y, 0.5, 0.5) + y := descY + float64(i)*30 // 行高30像素 + dc.DrawString(line, titleX, y) } } // 添加装饰性元素 drawDecorativeElements(dc, width, height, theme) + // 绘制底部URL访问地址 + if key != "" && domain != "" { + resourceURL := domain + "/r/" + key + dc.SetHexColor("#d1d5db") // 浅灰色 + + // 使用统一的字体加载逻辑 + loadChineseFont(20) + + // URL位置:底部居中,距离底部边缘40像素,给更多空间 + urlY := float64(height) - 40 + + dc.DrawStringAnchored(resourceURL, float64(width)/2, urlY, 0.5, 0.5) + } + + // 添加调试信息(仅在开发环境) + if title == "DEBUG" { + dc.SetHexColor("#ff0000") + dc.DrawString("Font loaded: "+strconv.FormatBool(fontLoaded), 50, float64(height)-80) + } + // 生成图片 buf := &bytes.Buffer{} err := dc.EncodePNG(buf) @@ -240,4 +399,167 @@ func drawDecorativeElements(dc *gg.Context, width, height int, theme string) { // 绘制底部装饰线 dc.DrawLine(60, float64(height-80), float64(width-60), float64(height-80)) dc.Stroke() +} + +// drawCoverImageInLeftArea 在左侧1/3区域绘制封面图片 +func drawCoverImageInLeftArea(dc *gg.Context, coverUrl string, width, height int, imageAreaWidth int) error { + // 下载封面图片 + resp, err := http.Get(coverUrl) + if err != nil { + return err + } + defer resp.Body.Close() + + // 读取图片数据 + imgData, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // 解码图片 + img, _, err := image.Decode(bytes.NewReader(imgData)) + if err != nil { + return err + } + + // 获取图片尺寸和宽高比 + bounds := img.Bounds() + imgWidth := bounds.Dx() + imgHeight := bounds.Dy() + aspectRatio := float64(imgWidth) / float64(imgHeight) + + // 计算图片区域的可显示尺寸,留出边距 + padding := 40 + maxImageWidth := imageAreaWidth - padding*2 + maxImageHeight := height - padding*2 + + var scaledImg image.Image + var drawWidth, drawHeight, drawX, drawY int + + // 判断是竖图还是横图,采用不同的缩放策略 + if aspectRatio < 1.0 { + // 竖图:充满整个左侧区域(去掉边距) + drawHeight = height - padding*2 // 留上下边距 + drawWidth = int(float64(drawHeight) * aspectRatio) + + // 如果宽度超出左侧区域,则以宽度为准充满整个区域宽度 + if drawWidth > imageAreaWidth - padding*2 { + drawWidth = imageAreaWidth - padding*2 + drawHeight = int(float64(drawWidth) / aspectRatio) + } + + // 缩放图片 + scaledImg = scaleImage(img, drawWidth, drawHeight) + + // 垂直居中,水平居左 + drawX = padding + drawY = (height - drawHeight) / 2 + } else { + // 横图:优先占满宽度 + drawWidth = maxImageWidth + drawHeight = int(float64(drawWidth) / aspectRatio) + + // 如果高度超出限制,则以高度为准 + if drawHeight > maxImageHeight { + drawHeight = maxImageHeight + drawWidth = int(float64(drawHeight) * aspectRatio) + } + + // 缩放图片 + scaledImg = scaleImage(img, drawWidth, drawHeight) + + // 水平居中,垂直居中 + drawX = (imageAreaWidth - drawWidth) / 2 + drawY = (height - drawHeight) / 2 + } + + // 绘制图片 + dc.DrawImage(scaledImg, drawX, drawY) + + // 添加半透明遮罩效果,让文字更清晰(仅在有图片时添加) + maskColor := color.RGBA{0, 0, 0, 80} // 半透明黑色,透明度稍低 + dc.SetColor(maskColor) + dc.DrawRectangle(float64(drawX), float64(drawY), float64(drawWidth), float64(drawHeight)) + dc.Fill() + + return nil +} + +// scaleImage 图片缩放函数 +func scaleImage(img image.Image, width, height int) image.Image { + // 使用 gg 库的 Scale 变换来实现缩放 + srcWidth := img.Bounds().Dx() + srcHeight := img.Bounds().Dy() + + // 创建目标尺寸的画布 + dc := gg.NewContext(width, height) + + // 计算缩放比例 + scaleX := float64(width) / float64(srcWidth) + scaleY := float64(height) / float64(srcHeight) + + // 应用缩放变换并绘制图片 + dc.Scale(scaleX, scaleY) + dc.DrawImage(img, 0, 0) + + return dc.Image() +} + +// drawCoverImage 绘制封面图片(保留原函数作为备用) +func drawCoverImage(dc *gg.Context, coverUrl string, width, height int) error { + // 下载封面图片 + resp, err := http.Get(coverUrl) + if err != nil { + return err + } + defer resp.Body.Close() + + // 读取图片数据 + imgData, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // 解码图片 + img, _, err := image.Decode(bytes.NewReader(imgData)) + if err != nil { + return err + } + + // 计算封面图片的位置和大小,放置在左侧 + coverWidth := 200 // 封面图宽度 + coverHeight := 280 // 封面图高度 + coverX := 50 + coverY := (height - coverHeight) / 2 + + // 绘制封面图片(按比例缩放) + bounds := img.Bounds() + imgWidth := bounds.Dx() + imgHeight := bounds.Dy() + + // 计算缩放比例,保持宽高比 + scaleX := float64(coverWidth) / float64(imgWidth) + scaleY := float64(coverHeight) / float64(imgHeight) + scale := scaleX + if scaleY < scaleX { + scale = scaleY + } + + // 计算缩放后的尺寸 + newWidth := int(float64(imgWidth) * scale) + newHeight := int(float64(imgHeight) * scale) + + // 居中绘制 + offsetX := coverX + (coverWidth-newWidth)/2 + offsetY := coverY + (coverHeight-newHeight)/2 + + dc.DrawImage(img, offsetX, offsetY) + + // 添加半透明遮罩效果,让文字更清晰 + maskColor := color.RGBA{0, 0, 0, 120} // 半透明黑色 + dc.SetColor(maskColor) + dc.DrawRectangle(float64(coverX), float64(coverY), float64(coverWidth), float64(coverHeight)) + dc.Fill() + + return nil } \ No newline at end of file diff --git a/handlers/resource_handler.go b/handlers/resource_handler.go index 7e5635c..ed5a4f8 100644 --- a/handlers/resource_handler.go +++ b/handlers/resource_handler.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "strings" pan "github.com/ctwj/urldb/common" commonutils "github.com/ctwj/urldb/common/utils" @@ -901,6 +902,203 @@ func transferSingleResource(resource *entity.Resource, account entity.Cks, facto } } +// GetRelatedResources 获取相关资源 +func GetRelatedResources(c *gin.Context) { + // 获取查询参数 + key := c.Query("key") // 当前资源的key + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "8")) + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + + utils.Info("获取相关资源请求 - key: %s, limit: %d", key, limit) + + if key == "" { + ErrorResponse(c, "缺少资源key参数", http.StatusBadRequest) + return + } + + // 首先通过key获取当前资源信息 + currentResources, err := repoManager.ResourceRepository.FindByKey(key) + if err != nil { + utils.Error("获取当前资源失败: %v", err) + ErrorResponse(c, "资源不存在", http.StatusNotFound) + return + } + + if len(currentResources) == 0 { + ErrorResponse(c, "资源不存在", http.StatusNotFound) + return + } + + currentResource := ¤tResources[0] // 取第一个资源作为当前资源 + + var resources []entity.Resource + var total int64 + + // 获取当前资源的标签ID列表 + var tagIDsList []string + if currentResource.Tags != nil { + for _, tag := range currentResource.Tags { + tagIDsList = append(tagIDsList, strconv.Itoa(int(tag.ID))) + } + } + + utils.Info("当前资源标签: %v", tagIDsList) + + // 1. 优先使用Meilisearch进行标签搜索 + if meilisearchManager != nil && meilisearchManager.IsEnabled() && len(tagIDsList) > 0 { + service := meilisearchManager.GetService() + if service != nil { + // 使用标签进行搜索 + filters := make(map[string]interface{}) + filters["tag_ids"] = tagIDsList + + // 使用当前资源的标题作为搜索关键词,提高相关性 + searchQuery := currentResource.Title + if searchQuery == "" { + searchQuery = strings.Join(tagIDsList, " ") // 如果没有标题,使用标签作为搜索词 + } + + docs, docTotal, err := service.Search(searchQuery, filters, page, limit) + if err == nil && len(docs) > 0 { + // 转换为Resource实体 + for _, doc := range docs { + // 排除当前资源 + if doc.Key == key { + continue + } + resource := entity.Resource{ + ID: doc.ID, + Title: doc.Title, + Description: doc.Description, + URL: doc.URL, + SaveURL: doc.SaveURL, + FileSize: doc.FileSize, + Key: doc.Key, + PanID: doc.PanID, + ViewCount: 0, // Meilisearch文档中没有ViewCount字段,设为默认值 + CreatedAt: doc.CreatedAt, + UpdatedAt: doc.UpdatedAt, + Cover: doc.Cover, + Author: doc.Author, + } + resources = append(resources, resource) + } + total = docTotal + utils.Info("Meilisearch搜索到 %d 个相关资源", len(resources)) + } else { + utils.Error("Meilisearch搜索失败,回退到标签搜索: %v", err) + } + } + } + + // 2. 如果Meilisearch未启用、搜索失败或没有结果,使用数据库标签搜索 + if len(resources) == 0 { + params := map[string]interface{}{ + "page": page, + "page_size": limit, + "is_public": true, + "order_by": "updated_at", + "order_dir": "desc", + } + + // 使用当前资源的标签进行搜索 + if len(tagIDsList) > 0 { + params["tag_ids"] = strings.Join(tagIDsList, ",") + } else { + // 如果没有标签,使用当前资源的分类作为搜索条件 + if currentResource.CategoryID != nil && *currentResource.CategoryID > 0 { + params["category_id"] = *currentResource.CategoryID + } + } + + var err error + resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params) + if err != nil { + utils.Error("搜索相关资源失败: %v", err) + ErrorResponse(c, "搜索相关资源失败", http.StatusInternalServerError) + return + } + + // 排除当前资源 + var filteredResources []entity.Resource + for _, resource := range resources { + if resource.Key != key { + filteredResources = append(filteredResources, resource) + } + } + resources = filteredResources + total = int64(len(filteredResources)) + } + + utils.Info("标签搜索到 %d 个相关资源", len(resources)) + + // 获取违禁词配置 + cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) { + return repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyForbiddenWords) + }) + if err != nil { + utils.Error("获取违禁词配置失败: %v", err) + cleanWords = []string{} + } + + // 处理违禁词并转换为响应格式 + var resourceResponses []gin.H + for _, resource := range resources { + // 检查违禁词 + forbiddenInfo := utils.CheckResourceForbiddenWords(resource.Title, resource.Description, cleanWords) + + resourceResponse := gin.H{ + "id": resource.ID, + "key": resource.Key, + "title": forbiddenInfo.ProcessedTitle, + "url": resource.URL, + "description": forbiddenInfo.ProcessedDesc, + "pan_id": resource.PanID, + "view_count": resource.ViewCount, + "created_at": resource.CreatedAt.Format("2006-01-02 15:04:05"), + "updated_at": resource.UpdatedAt.Format("2006-01-02 15:04:05"), + "cover": resource.Cover, + "author": resource.Author, + "file_size": resource.FileSize, + } + + // 添加违禁词标记 + resourceResponse["has_forbidden_words"] = forbiddenInfo.HasForbiddenWords + resourceResponse["forbidden_words"] = forbiddenInfo.ForbiddenWords + + // 添加标签信息 + var tagResponses []gin.H + if len(resource.Tags) > 0 { + for _, tag := range resource.Tags { + tagResponse := gin.H{ + "id": tag.ID, + "name": tag.Name, + "description": tag.Description, + } + tagResponses = append(tagResponses, tagResponse) + } + } + resourceResponse["tags"] = tagResponses + + resourceResponses = append(resourceResponses, resourceResponse) + } + + // 构建响应数据 + responseData := gin.H{ + "data": resourceResponses, + "total": total, + "page": page, + "page_size": limit, + "source": "database", + } + + if meilisearchManager != nil && meilisearchManager.IsEnabled() && len(tagIDsList) > 0 { + responseData["source"] = "meilisearch" + } + + SuccessResponse(c, responseData) +} + // getQuarkPanID 获取夸克网盘ID func getQuarkPanID() (uint, error) { // 通过FindAll方法查找所有平台,然后过滤出quark平台 diff --git a/main.go b/main.go index 66ae09a..34e1e2b 100644 --- a/main.go +++ b/main.go @@ -239,6 +239,7 @@ func main() { api.GET("/resources/:id", handlers.GetResourceByID) api.GET("/resources/key/:key", handlers.GetResourcesByKey) api.GET("/resources/check-exists", handlers.CheckResourceExists) + api.GET("/resources/related", handlers.GetRelatedResources) api.POST("/resources/:id/view", handlers.IncrementResourceViewCount) api.GET("/resources/:id/link", handlers.GetResourceLink) api.DELETE("/resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchDeleteResources) diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index d23333f..942813c 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -60,7 +60,9 @@ export const useResourceApi = () => { const batchDeleteResources = (ids: number[]) => useApiFetch('/resources/batch', { method: 'DELETE', body: { ids } }).then(parseApiResponse) // 新增:获取资源链接(智能转存) const getResourceLink = (id: number) => useApiFetch(`/resources/${id}/link`).then(parseApiResponse) - return { getResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink } + // 新增:获取相关资源 + const getRelatedResources = (params?: any) => useApiFetch('/resources/related', { params }).then(parseApiResponse) + return { getResources, getResource, getResourcesByKey, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources, getResourceLink, getRelatedResources } } export const useAuthApi = () => { diff --git a/web/composables/useSeo.ts b/web/composables/useSeo.ts index 0090461..0428efb 100644 --- a/web/composables/useSeo.ts +++ b/web/composables/useSeo.ts @@ -1,5 +1,6 @@ import { ref } from 'vue' import { useRoute } from '#imports' +import { usePublicSystemConfigApi } from './useApi' interface SystemConfig { id: number @@ -60,19 +61,28 @@ export const useSeo = () => { } // 生成动态OG图片URL - const generateOgImageUrl = (title: string, description?: string, theme: string = 'default') => { + const generateOgImageUrl = (keyOrTitle: string, descriptionOrEmpty: string = '', theme: string = 'default') => { // 获取运行时配置 const config = useRuntimeConfig() const ogApiUrl = config.public.ogApiUrl || '/api/og-image' // 构建URL参数 const params = new URLSearchParams() - params.set('title', title) - if (description) { - // 限制描述长度 - const trimmedDesc = description.length > 200 ? description.substring(0, 200) + '...' : description - params.set('description', trimmedDesc) + // 检测第一个参数是key还是title(通过长度和格式判断) + // 如果是较短的字符串且符合key格式(通常是字母数字组合),则当作key处理 + if (keyOrTitle.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(keyOrTitle)) { + // 作为key参数使用 + params.set('key', keyOrTitle) + } else { + // 作为title参数使用 + params.set('title', keyOrTitle) + + if (descriptionOrEmpty) { + // 限制描述长度 + const trimmedDesc = descriptionOrEmpty.length > 200 ? descriptionOrEmpty.substring(0, 200) + '...' : descriptionOrEmpty + params.set('description', trimmedDesc) + } } params.set('site_name', (systemConfig.value && systemConfig.value.site_title) || '老九网盘资源数据库') diff --git a/web/pages/r/[key].vue b/web/pages/r/[key].vue index 4fca390..f904634 100644 --- a/web/pages/r/[key].vue +++ b/web/pages/r/[key].vue @@ -221,53 +221,46 @@ -
+
{{ resource.description }}
-