mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: index
This commit is contained in:
@@ -51,6 +51,8 @@ type ResourceRepository interface {
|
||||
FindByResourceKey(key string) ([]entity.Resource, error)
|
||||
FindByKey(key string) ([]entity.Resource, error)
|
||||
GetHotResources(limit int) ([]entity.Resource, error)
|
||||
GetTotalCount() (int64, error)
|
||||
GetAllValidResources() ([]entity.Resource, error)
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -799,3 +801,18 @@ func (r *ResourceRepositoryImpl) FindByResourceKey(key string) ([]entity.Resourc
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetTotalCount 获取资源总数
|
||||
func (r *ResourceRepositoryImpl) GetTotalCount() (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB().Model(&entity.Resource{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetAllValidResources 获取所有有效的公开资源
|
||||
func (r *ResourceRepositoryImpl) GetAllValidResources() ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
err := r.GetDB().Where("is_valid = ? AND is_public = ?", true, true).
|
||||
Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type TaskItemRepository interface {
|
||||
UpdateStatus(id uint, status string) error
|
||||
UpdateStatusAndOutput(id uint, status, outputData string) error
|
||||
GetStatsByTaskID(taskID uint) (map[string]int, error)
|
||||
GetIndexStats() (map[string]int, error)
|
||||
ResetProcessingItems(taskID uint) error
|
||||
}
|
||||
|
||||
@@ -210,3 +211,30 @@ func (r *TaskItemRepositoryImpl) ResetProcessingItems(taskID uint) error {
|
||||
utils.Debug("ResetProcessingItems成功: 任务ID=%d, 更新耗时=%v", taskID, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIndexStats 获取索引统计信息
|
||||
func (r *TaskItemRepositoryImpl) GetIndexStats() (map[string]int, error) {
|
||||
stats := make(map[string]int)
|
||||
|
||||
// 统计各种状态的数量
|
||||
statuses := []string{"completed", "failed", "pending"}
|
||||
|
||||
for _, status := range statuses {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.TaskItem{}).Where("status = ?", status).Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "completed":
|
||||
stats["indexed"] = int(count)
|
||||
case "failed":
|
||||
stats["error"] = int(count)
|
||||
case "pending":
|
||||
stats["not_indexed"] = int(count)
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/ctwj/urldb/task"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
goauth "golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
// GoogleIndexHandler Google索引处理程序
|
||||
@@ -725,23 +727,17 @@ func (h *GoogleIndexHandler) makeSafeFileName(filename string) string {
|
||||
|
||||
// ValidateCredentials 验证Google索引凭据
|
||||
func (h *GoogleIndexHandler) ValidateCredentials(c *gin.Context) {
|
||||
var req struct {
|
||||
CredentialsFile string `json:"credentials_file" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// 使用固定的凭据文件路径
|
||||
credentialsFile := "data/google_credentials.json"
|
||||
|
||||
// 检查凭据文件是否存在
|
||||
if _, err := os.Stat(req.CredentialsFile); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(credentialsFile); os.IsNotExist(err) {
|
||||
ErrorResponse(c, "凭据文件不存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试创建Google客户端并验证凭据
|
||||
config, err := h.loadCredentials(req.CredentialsFile)
|
||||
config, err := h.loadCredentials(credentialsFile)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "凭据格式错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -792,17 +788,34 @@ func (h *GoogleIndexHandler) GetStatus(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息(从任务管理器或数据库获取相关统计)
|
||||
// 这里简化处理,返回一个基本的状态响应
|
||||
// 在实际实现中,可能需要查询数据库获取更详细的统计信息
|
||||
// 获取统计信息(从数据库查询实际的资源数量)
|
||||
totalURLs := 0
|
||||
indexedURLs := 0
|
||||
notIndexedURLs := 0
|
||||
errorURLs := 0
|
||||
|
||||
// 查询resources表获取总URL数
|
||||
totalResources, err := h.repoMgr.ResourceRepository.GetTotalCount()
|
||||
if err == nil {
|
||||
totalURLs = int(totalResources)
|
||||
}
|
||||
|
||||
// 查询任务项统计获取索引状态
|
||||
taskStats, err := h.repoMgr.TaskItemRepository.GetIndexStats()
|
||||
if err == nil {
|
||||
indexedURLs = taskStats["indexed"]
|
||||
notIndexedURLs = taskStats["not_indexed"]
|
||||
errorURLs = taskStats["error"]
|
||||
}
|
||||
|
||||
statusResponse := dto.GoogleIndexStatusResponse{
|
||||
Enabled: enabled,
|
||||
SiteURL: siteURL,
|
||||
LastCheckTime: time.Now(),
|
||||
TotalURLs: 0,
|
||||
IndexedURLs: 0,
|
||||
NotIndexedURLs: 0,
|
||||
ErrorURLs: 0,
|
||||
TotalURLs: totalURLs,
|
||||
IndexedURLs: indexedURLs,
|
||||
NotIndexedURLs: notIndexedURLs,
|
||||
ErrorURLs: errorURLs,
|
||||
LastSitemapSubmit: time.Time{},
|
||||
AuthValid: authValid,
|
||||
}
|
||||
@@ -812,10 +825,7 @@ func (h *GoogleIndexHandler) GetStatus(c *gin.Context) {
|
||||
|
||||
// loadCredentials 从文件加载凭据
|
||||
func (h *GoogleIndexHandler) loadCredentials(credentialsFile string) (*google.Config, error) {
|
||||
// 从pkg/google/client.go导入的Config
|
||||
// 注意:我们需要一个方法来安全地加载凭据
|
||||
// 为了简化,我们只是检查文件是否可以读取以及格式是否正确
|
||||
|
||||
// 读取凭据文件
|
||||
data, err := os.ReadFile(credentialsFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法读取凭据文件: %v", err)
|
||||
@@ -835,10 +845,32 @@ func (h *GoogleIndexHandler) loadCredentials(credentialsFile string) (*google.Co
|
||||
}
|
||||
}
|
||||
|
||||
// 返回配置(简化处理,实际实现可能需要更复杂的逻辑)
|
||||
return &google.Config{
|
||||
// 检查凭据类型
|
||||
if temp["type"] != "service_account" {
|
||||
return nil, fmt.Errorf("仅支持服务账号类型的凭据")
|
||||
}
|
||||
|
||||
// 尝试从JSON数据加载凭据,需要指定作用域
|
||||
scopes := []string{
|
||||
"https://www.googleapis.com/auth/webmasters",
|
||||
"https://www.googleapis.com/auth/indexing",
|
||||
}
|
||||
jwtConfig, err := goauth.JWTConfigFromJSON(data, scopes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建JWT配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建一个简单的配置对象,暂时只存储JWT配置
|
||||
config := &google.Config{
|
||||
CredentialsFile: credentialsFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 为了验证凭据,我们尝试获取token源
|
||||
ctx := context.Background()
|
||||
tokenSource := jwtConfig.TokenSource(ctx)
|
||||
_ = tokenSource // 实际验证在getValidToken中进行
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// getValidToken 获取有效的token
|
||||
@@ -972,20 +1004,26 @@ func (h *GoogleIndexHandler) UpdateGoogleIndexConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *GoogleIndexHandler) getValidToken(config *google.Config) error {
|
||||
// 这里应该使用Google的验证逻辑
|
||||
// 为了简化我们返回一个模拟的验证过程
|
||||
// 在实际实现中,应该使用Google API进行验证
|
||||
// 为了简单验证,我们只尝试读取凭据文件并确保JWT配置可以正常工作
|
||||
// 在实际实现中,这里应该尝试获取一个实际的token
|
||||
|
||||
// 尝试初始化Google客户端
|
||||
client, err := google.NewClient(config)
|
||||
// 重新读取凭据文件进行验证
|
||||
data, err := os.ReadFile(config.CredentialsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建Google客户端失败: %v", err)
|
||||
return fmt.Errorf("无法读取凭据文件: %v", err)
|
||||
}
|
||||
|
||||
// 简单的验证:尝试获取网站列表,如果成功说明凭据有效
|
||||
// 这里我们只检查客户端是否能成功初始化
|
||||
// 在实际实现中,应该尝试执行一个API调用以验证凭据
|
||||
_ = client // 使用client变量避免未使用警告
|
||||
// 尝试从JSON数据加载凭据,需要指定作用域
|
||||
scopes := []string{
|
||||
"https://www.googleapis.com/auth/webmasters",
|
||||
"https://www.googleapis.com/auth/indexing",
|
||||
}
|
||||
_, err = goauth.JWTConfigFromJSON(data, scopes...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建JWT配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果能成功创建JWT配置,我们认为凭据格式是正确的
|
||||
// 在实际环境中,这里应该尝试获取token来验证凭据的有效性
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -499,3 +499,36 @@ func (h *PublicAPIHandler) recordAPIAccessToDB(ip, userAgent, endpoint, method s
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// GetPublicSiteVerificationCode 获取网站验证代码(公开访问)
|
||||
func GetPublicSiteVerificationCode(c *gin.Context) {
|
||||
// 获取站点URL配置
|
||||
siteURL, err := repoManager.SystemConfigRepository.GetConfigValue("site_url")
|
||||
if err != nil || siteURL == "" {
|
||||
c.JSON(400, gin.H{
|
||||
"success": false,
|
||||
"message": "站点URL未配置",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成Google Search Console验证代码示例
|
||||
verificationCode := map[string]interface{}{
|
||||
"site_url": siteURL,
|
||||
"verification_methods": map[string]string{
|
||||
"html_tag": `<meta name="google-site-verification" content="your-verification-code">`,
|
||||
"dns_txt": `google-site-verification=your-verification-code`,
|
||||
"html_file": `google1234567890abcdef.html`,
|
||||
},
|
||||
"instructions": map[string]string{
|
||||
"html_tag": "请将以下meta标签添加到您网站的首页<head>部分中",
|
||||
"dns_txt": "请添加以下TXT记录到您的DNS配置中",
|
||||
"html_file": "请在网站根目录创建包含指定内容的HTML文件",
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": verificationCode,
|
||||
})
|
||||
}
|
||||
|
||||
16
main.go
16
main.go
@@ -198,6 +198,7 @@ func main() {
|
||||
autoProcessReadyResources, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
autoTransferEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
autoSitemapEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeySitemapAutoGenerateEnabled)
|
||||
autoGoogleIndexEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.GoogleIndexConfigKeyEnabled)
|
||||
|
||||
globalScheduler.UpdateSchedulerStatusWithAutoTransfer(
|
||||
autoFetchHotDrama,
|
||||
@@ -213,6 +214,14 @@ func main() {
|
||||
utils.Info("系统配置禁用Sitemap自动生成功能")
|
||||
}
|
||||
|
||||
// 根据系统配置启动Google索引调度器
|
||||
if autoGoogleIndexEnabled {
|
||||
globalScheduler.StartGoogleIndexScheduler()
|
||||
utils.Info("系统配置启用Google索引自动提交功能,启动定时任务")
|
||||
} else {
|
||||
utils.Info("系统配置禁用Google索引自动提交功能")
|
||||
}
|
||||
|
||||
utils.Info("调度器初始化完成")
|
||||
|
||||
// 设置公开API中间件的Repository管理器
|
||||
@@ -374,7 +383,7 @@ func main() {
|
||||
api.GET("/system/config/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetConfigStatus)
|
||||
api.POST("/system/config/toggle-auto-process", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ToggleAutoProcess)
|
||||
api.GET("/public/system-config", handlers.GetPublicSystemConfig)
|
||||
api.GET("/public/site-verification", handlers.GetSiteVerificationCode) // 网站验证代码(公开访问)
|
||||
api.GET("/public/site-verification", handlers.GetPublicSiteVerificationCode) // 网站验证代码(公开访问)
|
||||
|
||||
// 热播剧管理路由(查询接口无需认证)
|
||||
api.GET("/hot-dramas", handlers.GetHotDramaList)
|
||||
@@ -536,9 +545,10 @@ api.GET("/public/site-verification", handlers.GetSiteVerificationCode) // 网
|
||||
|
||||
// Google索引管理API
|
||||
api.GET("/google-index/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetConfig)
|
||||
api.GET("/google-index/config-all", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetAllConfig) // 获取所有配置
|
||||
api.POST("/google-index/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.UpdateConfig)
|
||||
api.GET("/google-index/config/:key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetConfigByKey) // 根据键获取配置
|
||||
api.POST("/google-index/config/update", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.UpdateGoogleIndexConfig) // 分组配置更新
|
||||
api.POST("/google-index/config/update", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.UpdateGoogleIndexConfig) // 分组配置更新
|
||||
api.GET("/google-index/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetStatus) // 获取状态
|
||||
api.POST("/google-index/tasks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.CreateTask)
|
||||
api.GET("/google-index/tasks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetTasks)
|
||||
api.GET("/google-index/tasks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), googleIndexHandler.GetTaskStatus)
|
||||
|
||||
@@ -201,3 +201,39 @@ func (gs *GlobalScheduler) TriggerSitemapGeneration() {
|
||||
|
||||
gs.manager.TriggerSitemapGeneration()
|
||||
}
|
||||
|
||||
// StartGoogleIndexScheduler 启动Google索引调度任务
|
||||
func (gs *GlobalScheduler) StartGoogleIndexScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if gs.manager.IsGoogleIndexRunning() {
|
||||
utils.Debug("Google索引调度任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StartGoogleIndexScheduler()
|
||||
utils.Info("Google索引调度任务已启动")
|
||||
}
|
||||
|
||||
// StopGoogleIndexScheduler 停止Google索引调度任务
|
||||
func (gs *GlobalScheduler) StopGoogleIndexScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if !gs.manager.IsGoogleIndexRunning() {
|
||||
utils.Debug("Google索引调度任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StopGoogleIndexScheduler()
|
||||
utils.Info("Google索引调度任务已停止")
|
||||
}
|
||||
|
||||
// IsGoogleIndexSchedulerRunning 检查Google索引调度任务是否在运行
|
||||
func (gs *GlobalScheduler) IsGoogleIndexSchedulerRunning() bool {
|
||||
gs.mutex.RLock()
|
||||
defer gs.mutex.RUnlock()
|
||||
|
||||
return gs.manager.IsGoogleIndexRunning()
|
||||
}
|
||||
|
||||
382
scheduler/google_index.go
Normal file
382
scheduler/google_index.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/pkg/google"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// GoogleIndexScheduler Google索引调度器
|
||||
type GoogleIndexScheduler struct {
|
||||
*BaseScheduler
|
||||
config entity.SystemConfig
|
||||
stopChan chan bool
|
||||
isRunning bool
|
||||
enabled bool
|
||||
checkInterval time.Duration
|
||||
googleClient *google.Client
|
||||
}
|
||||
|
||||
// NewGoogleIndexScheduler 创建Google索引调度器
|
||||
func NewGoogleIndexScheduler(baseScheduler *BaseScheduler) *GoogleIndexScheduler {
|
||||
return &GoogleIndexScheduler{
|
||||
BaseScheduler: baseScheduler,
|
||||
stopChan: make(chan bool),
|
||||
isRunning: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动Google索引调度任务
|
||||
func (s *GoogleIndexScheduler) Start() {
|
||||
if s.isRunning {
|
||||
utils.Debug("Google索引调度任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
if err := s.loadConfig(); err != nil {
|
||||
utils.Error("加载Google索引配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.enabled {
|
||||
utils.Debug("Google索引功能未启用,跳过调度任务")
|
||||
return
|
||||
}
|
||||
|
||||
s.isRunning = true
|
||||
utils.Info("开始启动Google索引调度任务,检查间隔: %v", s.checkInterval)
|
||||
|
||||
go s.run()
|
||||
}
|
||||
|
||||
// Stop 停止Google索引调度任务
|
||||
func (s *GoogleIndexScheduler) Stop() {
|
||||
if !s.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("正在停止Google索引调度任务...")
|
||||
s.stopChan <- true
|
||||
s.isRunning = false
|
||||
}
|
||||
|
||||
// IsRunning 检查调度器是否正在运行
|
||||
func (s *GoogleIndexScheduler) IsRunning() bool {
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// run 运行调度器主循环
|
||||
func (s *GoogleIndexScheduler) run() {
|
||||
ticker := time.NewTicker(s.checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 启动时立即执行一次
|
||||
s.performScheduledTasks()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
utils.Info("Google索引调度任务已停止")
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.performScheduledTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfig 加载配置
|
||||
func (s *GoogleIndexScheduler) loadConfig() error {
|
||||
// 获取启用状态
|
||||
enabledStr, err := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyEnabled)
|
||||
if err != nil {
|
||||
s.enabled = false
|
||||
} else {
|
||||
s.enabled = enabledStr == "true" || enabledStr == "1"
|
||||
}
|
||||
|
||||
// 获取检查间隔
|
||||
intervalStr, err := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyCheckInterval)
|
||||
if err != nil {
|
||||
s.checkInterval = 60 * time.Minute // 默认60分钟
|
||||
} else {
|
||||
if interval, parseErr := time.ParseDuration(intervalStr + "m"); parseErr == nil {
|
||||
s.checkInterval = interval
|
||||
} else {
|
||||
s.checkInterval = 60 * time.Minute
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Google客户端
|
||||
if s.enabled {
|
||||
if err := s.initGoogleClient(); err != nil {
|
||||
utils.Error("初始化Google客户端失败: %v", err)
|
||||
s.enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initGoogleClient 初始化Google客户端
|
||||
func (s *GoogleIndexScheduler) initGoogleClient() error {
|
||||
// 获取凭据文件路径
|
||||
credentialsFile, err := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyCredentialsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取凭据文件路径失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取站点URL
|
||||
siteURL, err := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeySiteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取站点URL失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建Google客户端配置
|
||||
config := &google.Config{
|
||||
CredentialsFile: credentialsFile,
|
||||
SiteURL: siteURL,
|
||||
}
|
||||
|
||||
client, err := google.NewClient(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建Google客户端失败: %v", err)
|
||||
}
|
||||
|
||||
s.googleClient = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// performScheduledTasks 执行调度任务
|
||||
func (s *GoogleIndexScheduler) performScheduledTasks() {
|
||||
if !s.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 任务1: 扫描未索引的URL并自动提交
|
||||
if err := s.scanAndSubmitUnindexedURLs(ctx); err != nil {
|
||||
utils.Error("扫描未索引URL失败: %v", err)
|
||||
}
|
||||
|
||||
// 任务2: 定期检查已索引URL的状态
|
||||
if err := s.checkIndexedURLsStatus(ctx); err != nil {
|
||||
utils.Error("检查索引状态失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Google索引调度任务执行完成")
|
||||
}
|
||||
|
||||
// scanAndSubmitUnindexedURLs 扫描并提交未索引的URL
|
||||
func (s *GoogleIndexScheduler) scanAndSubmitUnindexedURLs(ctx context.Context) error {
|
||||
utils.Info("开始扫描未索引的URL...")
|
||||
|
||||
// 1. 获取所有资源URL
|
||||
resources, err := s.resourceRepo.GetAllValidResources()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取资源列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 获取已索引的URL记录
|
||||
indexedURLs, err := s.getIndexedURLs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取已索引URL列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 找出未索引的URL
|
||||
var unindexedURLs []string
|
||||
indexedURLSet := make(map[string]bool)
|
||||
for _, url := range indexedURLs {
|
||||
indexedURLSet[url] = true
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
if resource.IsPublic && resource.IsValid && resource.URL != "" {
|
||||
if !indexedURLSet[resource.URL] {
|
||||
unindexedURLs = append(unindexedURLs, resource.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Info("发现 %d 个未索引的URL", len(unindexedURLs))
|
||||
|
||||
// 4. 批量提交未索引的URL
|
||||
if len(unindexedURLs) > 0 {
|
||||
if err := s.batchSubmitURLs(ctx, unindexedURLs); err != nil {
|
||||
return fmt.Errorf("批量提交URL失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getIndexedURLs 获取已索引的URL列表
|
||||
func (s *GoogleIndexScheduler) getIndexedURLs() ([]string, error) {
|
||||
// 这里需要通过TaskItemRepository获取已索引的URL
|
||||
// 由于BaseScheduler没有TaskItemRepository,我们暂时返回空列表
|
||||
// 后续可以通过扩展BaseScheduler或创建专门的方法来处理
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// batchSubmitURLs 批量提交URL
|
||||
func (s *GoogleIndexScheduler) batchSubmitURLs(ctx context.Context, urls []string) error {
|
||||
utils.Info("开始批量提交 %d 个URL到Google索引...", len(urls))
|
||||
|
||||
// 获取批量大小配置
|
||||
batchSizeStr, _ := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyBatchSize)
|
||||
batchSize := 100 // 默认值
|
||||
if batchSizeStr != "" {
|
||||
if size, err := strconv.Atoi(batchSizeStr); err == nil && size > 0 {
|
||||
batchSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// 获取并发数配置
|
||||
concurrencyStr, _ := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyConcurrency)
|
||||
concurrency := 5 // 默认值
|
||||
if concurrencyStr != "" {
|
||||
if conc, err := strconv.Atoi(concurrencyStr); err == nil && conc > 0 {
|
||||
concurrency = conc
|
||||
}
|
||||
}
|
||||
|
||||
// 分批处理
|
||||
for i := 0; i < len(urls); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(urls) {
|
||||
end = len(urls)
|
||||
}
|
||||
|
||||
batch := urls[i:end]
|
||||
if err := s.processBatch(ctx, batch, concurrency); err != nil {
|
||||
utils.Error("处理批次失败 (批次 %d-%d): %v", i+1, end, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 避免API限制,批次间稍作延迟
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
utils.Info("批量URL提交完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// processBatch 处理单个批次
|
||||
func (s *GoogleIndexScheduler) processBatch(ctx context.Context, urls []string, concurrency int) error {
|
||||
semaphore := make(chan struct{}, concurrency)
|
||||
errChan := make(chan error, len(urls))
|
||||
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// 检查URL索引状态
|
||||
result, err := s.googleClient.InspectURL(u)
|
||||
if err != nil {
|
||||
utils.Error("检查URL失败: %s, 错误: %v", u, err)
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// Google Search Console API 不直接支持URL提交
|
||||
// 这里只记录URL状态,实际的URL索引需要通过sitemap或其他方式
|
||||
if result.IndexStatusResult.IndexingState == "NOT_SUBMITTED" {
|
||||
utils.Debug("URL未提交,需要通过sitemap提交: %s", u)
|
||||
// TODO: 可以考虑将未提交的URL加入到sitemap中
|
||||
}
|
||||
|
||||
// 记录索引状态
|
||||
s.recordURLStatus(u, result)
|
||||
errChan <- nil
|
||||
}(url)
|
||||
}
|
||||
|
||||
// 等待所有goroutine完成
|
||||
for i := 0; i < len(urls); i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkIndexedURLsStatus 检查已索引URL的状态
|
||||
func (s *GoogleIndexScheduler) checkIndexedURLsStatus(ctx context.Context) error {
|
||||
utils.Info("开始检查已索引URL的状态...")
|
||||
|
||||
// 暂时跳过状态检查,因为需要TaskItemRepository访问权限
|
||||
// TODO: 后续通过扩展BaseScheduler来支持TaskItemRepository
|
||||
urlsToCheck := []string{}
|
||||
utils.Info("检查 %d 个已索引URL的状态", len(urlsToCheck))
|
||||
|
||||
// 并发检查状态
|
||||
concurrencyStr, _ := s.systemConfigRepo.GetConfigValue(entity.GoogleIndexConfigKeyConcurrency)
|
||||
concurrency := 3 // 状态检查使用较低并发
|
||||
if concurrencyStr != "" {
|
||||
if conc, err := strconv.Atoi(concurrencyStr); err == nil && conc > 0 {
|
||||
concurrency = conc / 2 // 状态检查并发减半
|
||||
if concurrency < 1 {
|
||||
concurrency = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 由于没有URL需要检查,跳过循环
|
||||
if len(urlsToCheck) == 0 {
|
||||
utils.Info("没有URL需要状态检查")
|
||||
return nil
|
||||
}
|
||||
|
||||
semaphore := make(chan struct{}, concurrency)
|
||||
for _, url := range urlsToCheck {
|
||||
go func(u string) {
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// 检查URL最新状态
|
||||
result, err := s.googleClient.InspectURL(u)
|
||||
if err != nil {
|
||||
utils.Error("检查URL状态失败: %s, 错误: %v", u, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录状态
|
||||
s.recordURLStatus(u, result)
|
||||
}(url)
|
||||
}
|
||||
|
||||
// 等待所有检查完成
|
||||
for i := 0; i < len(urlsToCheck); i++ {
|
||||
<-semaphore
|
||||
}
|
||||
|
||||
utils.Info("索引状态检查完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordURLStatus 记录URL索引状态
|
||||
func (s *GoogleIndexScheduler) recordURLStatus(url string, result *google.URLInspectionResult) {
|
||||
// 暂时只记录日志,不保存到数据库
|
||||
// TODO: 后续通过扩展BaseScheduler来支持TaskItemRepository以保存状态
|
||||
utils.Debug("记录URL状态: %s - %s", url, result.IndexStatusResult.IndexingState)
|
||||
}
|
||||
|
||||
// updateURLStatus 更新URL状态
|
||||
func (s *GoogleIndexScheduler) updateURLStatus(taskItem *entity.TaskItem, result *google.URLInspectionResult) {
|
||||
// 暂时只记录日志,不保存到数据库
|
||||
// TODO: 后续通过扩展BaseScheduler来支持TaskItemRepository以保存状态
|
||||
utils.Debug("更新URL状态: %s - %s", taskItem.URL, result.IndexStatusResult.IndexingState)
|
||||
}
|
||||
|
||||
// SetRunning 设置运行状态
|
||||
func (s *GoogleIndexScheduler) SetRunning(running bool) {
|
||||
s.isRunning = running
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type Manager struct {
|
||||
hotDramaScheduler *HotDramaScheduler
|
||||
readyResourceScheduler *ReadyResourceScheduler
|
||||
sitemapScheduler *SitemapScheduler
|
||||
googleIndexScheduler *GoogleIndexScheduler
|
||||
}
|
||||
|
||||
// NewManager 创建调度器管理器
|
||||
@@ -40,12 +41,14 @@ func NewManager(
|
||||
hotDramaScheduler := NewHotDramaScheduler(baseScheduler)
|
||||
readyResourceScheduler := NewReadyResourceScheduler(baseScheduler)
|
||||
sitemapScheduler := NewSitemapScheduler(baseScheduler)
|
||||
googleIndexScheduler := NewGoogleIndexScheduler(baseScheduler)
|
||||
|
||||
return &Manager{
|
||||
baseScheduler: baseScheduler,
|
||||
hotDramaScheduler: hotDramaScheduler,
|
||||
readyResourceScheduler: readyResourceScheduler,
|
||||
sitemapScheduler: sitemapScheduler,
|
||||
googleIndexScheduler: googleIndexScheduler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +62,9 @@ func (m *Manager) StartAll() {
|
||||
// 启动待处理资源调度任务
|
||||
m.readyResourceScheduler.Start()
|
||||
|
||||
// 启动Google索引调度任务
|
||||
m.googleIndexScheduler.Start()
|
||||
|
||||
utils.Debug("所有调度任务已启动")
|
||||
}
|
||||
|
||||
@@ -72,6 +78,9 @@ func (m *Manager) StopAll() {
|
||||
// 停止待处理资源调度任务
|
||||
m.readyResourceScheduler.Stop()
|
||||
|
||||
// 停止Google索引调度任务
|
||||
m.googleIndexScheduler.Stop()
|
||||
|
||||
utils.Debug("所有调度任务已停止")
|
||||
}
|
||||
|
||||
@@ -140,11 +149,27 @@ func (m *Manager) TriggerSitemapGeneration() {
|
||||
go m.sitemapScheduler.generateSitemap()
|
||||
}
|
||||
|
||||
// StartGoogleIndexScheduler 启动Google索引调度任务
|
||||
func (m *Manager) StartGoogleIndexScheduler() {
|
||||
m.googleIndexScheduler.Start()
|
||||
}
|
||||
|
||||
// StopGoogleIndexScheduler 停止Google索引调度任务
|
||||
func (m *Manager) StopGoogleIndexScheduler() {
|
||||
m.googleIndexScheduler.Stop()
|
||||
}
|
||||
|
||||
// IsGoogleIndexRunning 检查Google索引调度任务是否在运行
|
||||
func (m *Manager) IsGoogleIndexRunning() bool {
|
||||
return m.googleIndexScheduler.IsRunning()
|
||||
}
|
||||
|
||||
// GetStatus 获取所有调度任务的状态
|
||||
func (m *Manager) GetStatus() map[string]bool {
|
||||
return map[string]bool{
|
||||
"hot_drama": m.IsHotDramaRunning(),
|
||||
"ready_resource": m.IsReadyResourceRunning(),
|
||||
"sitemap": m.IsSitemapRunning(),
|
||||
"google_index": m.IsGoogleIndexRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
539
web/components/Admin/GoogleIndexTab.vue
Normal file
539
web/components/Admin/GoogleIndexTab.vue
Normal file
@@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<div class="tab-content-container">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<!-- Google索引配置 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Google索引配置</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">配置Google Search Console API和索引相关设置</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-2">Google索引功能</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
开启后系统将自动检查和提交URL到Google索引
|
||||
</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="googleIndexConfig.enabled"
|
||||
@update:value="updateGoogleIndexConfig"
|
||||
:loading="configLoading"
|
||||
size="large"
|
||||
>
|
||||
<template #checked>已开启</template>
|
||||
<template #unchecked>已关闭</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
|
||||
<!-- 配置详情 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">站点URL</label>
|
||||
<n-input
|
||||
:value="systemConfig?.site_url || '站点URL未配置'"
|
||||
:disabled="true"
|
||||
placeholder="请先在站点配置中设置站点URL"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="fas fa-globe text-gray-400"></i>
|
||||
</template>
|
||||
</n-input>
|
||||
<!-- 所有权验证按钮 -->
|
||||
<div class="mt-3">
|
||||
<n-button
|
||||
type="info"
|
||||
size="small"
|
||||
ghost
|
||||
@click="$emit('show-verification')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</template>
|
||||
所有权验证
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">凭据文件路径</label>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<n-input
|
||||
:value="credentialsFilePath"
|
||||
placeholder="点击上传按钮选择文件"
|
||||
:disabled="true"
|
||||
/>
|
||||
<div class="flex space-x-2">
|
||||
<!-- 申请凭据按钮 -->
|
||||
<n-button
|
||||
size="small"
|
||||
type="warning"
|
||||
ghost
|
||||
@click="$emit('show-credentials-guide')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</template>
|
||||
申请凭据
|
||||
</n-button>
|
||||
<!-- 上传按钮 -->
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="$emit('select-credentials-file')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-upload"></i>
|
||||
</template>
|
||||
上传凭据
|
||||
</n-button>
|
||||
<!-- 验证按钮 -->
|
||||
<n-button
|
||||
size="small"
|
||||
type="info"
|
||||
ghost
|
||||
@click="validateCredentials"
|
||||
:loading="validatingCredentials"
|
||||
:disabled="!credentialsFilePath"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</template>
|
||||
验证凭据
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">检查间隔(分钟)</label>
|
||||
<n-input-number
|
||||
v-model:value="googleIndexConfig.checkInterval"
|
||||
:min="1"
|
||||
:max="1440"
|
||||
@update:value="updateGoogleIndexConfig"
|
||||
:disabled="!credentialsFilePath"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">批处理大小</label>
|
||||
<n-input-number
|
||||
v-model:value="googleIndexConfig.batchSize"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
@update:value="updateGoogleIndexConfig"
|
||||
:disabled="!credentialsFilePath"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 凭据状态 -->
|
||||
<div v-if="credentialsStatus" class="mt-4 p-3 rounded-lg border"
|
||||
:class="{
|
||||
'bg-green-50 border-green-200 text-green-700 dark:bg-green-900/20 dark:border-green-800 dark:text-green-300': credentialsStatus === 'valid',
|
||||
'bg-yellow-50 border-yellow-200 text-yellow-700 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-300': credentialsStatus === 'invalid',
|
||||
'bg-blue-50 border-blue-200 text-blue-700 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-300': credentialsStatus === 'verifying'
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<i
|
||||
:class="{
|
||||
'fas fa-check-circle text-green-500 dark:text-green-400': credentialsStatus === 'valid',
|
||||
'fas fa-exclamation-circle text-yellow-500 dark:text-yellow-400': credentialsStatus === 'invalid',
|
||||
'fas fa-spinner fa-spin text-blue-500 dark:text-blue-400': credentialsStatus === 'verifying'
|
||||
}"
|
||||
class="mr-2"
|
||||
></i>
|
||||
<span>{{ credentialsStatusMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google索引统计 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<i class="fas fa-link text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">总URL数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ googleIndexStats.totalURLs || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">已索引</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ googleIndexStats.indexedURLs || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">错误数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ googleIndexStats.errorURLs || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||
<i class="fas fa-tasks text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">总任务数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ googleIndexStats.totalTasks || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 外部工具链接 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||
<i class="fas fa-chart-line text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Google Search Console</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">查看详细分析数据</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
:href="getSearchConsoleUrl()"
|
||||
target="_blank"
|
||||
class="px-3 py-1 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors text-sm"
|
||||
>
|
||||
打开控制台
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-orange-50 dark:bg-orange-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-orange-100 dark:bg-orange-900 rounded-lg">
|
||||
<i class="fas fa-chart-line text-orange-600 dark:text-orange-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Google Analytics</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">网站流量分析仪表板</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
:href="getAnalyticsUrl()"
|
||||
target="_blank"
|
||||
class="px-3 py-1 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors text-sm"
|
||||
>
|
||||
查看分析
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="$emit('manual-check-urls')"
|
||||
:loading="manualCheckLoading"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-search"></i>
|
||||
</template>
|
||||
手动检查URL
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="success"
|
||||
@click="submitSitemap"
|
||||
:loading="submitSitemapLoading"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-upload"></i>
|
||||
</template>
|
||||
提交网站地图
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="info"
|
||||
@click="$emit('refresh-status')"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</template>
|
||||
刷新状态
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-4">索引任务列表</h4>
|
||||
<n-data-table
|
||||
:columns="taskColumns"
|
||||
:data="tasks"
|
||||
:pagination="pagination"
|
||||
:loading="tasksLoading"
|
||||
:bordered="false"
|
||||
striped
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import { ref, computed, h, watch } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
systemConfig?: any
|
||||
googleIndexConfig: any
|
||||
googleIndexStats: any
|
||||
tasks: any[]
|
||||
credentialsStatus: string | null
|
||||
credentialsStatusMessage: string
|
||||
configLoading: boolean
|
||||
manualCheckLoading: boolean
|
||||
submitSitemapLoading: boolean
|
||||
tasksLoading: boolean
|
||||
pagination: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
systemConfig: null,
|
||||
googleIndexConfig: () => ({}),
|
||||
googleIndexStats: () => ({}),
|
||||
tasks: () => [],
|
||||
credentialsStatus: null,
|
||||
credentialsStatusMessage: '',
|
||||
configLoading: false,
|
||||
manualCheckLoading: false,
|
||||
submitSitemapLoading: false,
|
||||
tasksLoading: false,
|
||||
pagination: () => ({})
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:google-index-config': []
|
||||
'show-verification': []
|
||||
'show-credentials-guide': []
|
||||
'select-credentials-file': []
|
||||
'manual-check-urls': []
|
||||
'refresh-status': []
|
||||
}>()
|
||||
|
||||
// 获取消息组件
|
||||
const message = useMessage()
|
||||
|
||||
// 本地状态
|
||||
const validatingCredentials = ref(false)
|
||||
|
||||
// 计算属性,用于安全地访问凭据文件路径
|
||||
const credentialsFilePath = computed(() => {
|
||||
const path = props.googleIndexConfig?.credentialsFile || ''
|
||||
console.log('Component computed credentialsFilePath:', path)
|
||||
return path
|
||||
})
|
||||
|
||||
|
||||
// 任务表格列
|
||||
const taskColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 120,
|
||||
render: (row: any) => {
|
||||
const typeMap = {
|
||||
status_check: { text: '状态检查', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
||||
sitemap_submit: { text: '网站地图', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
url_indexing: { text: 'URL索引', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' }
|
||||
}
|
||||
const type = typeMap[row.type as keyof typeof typeMap] || { text: row.type, class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }
|
||||
return h('span', {
|
||||
class: `px-2 py-1 text-xs font-medium rounded ${type.class}`
|
||||
}, type.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (row: any) => {
|
||||
const statusMap = {
|
||||
pending: { text: '待处理', class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' },
|
||||
running: { text: '运行中', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
||||
completed: { text: '完成', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
failed: { text: '失败', class: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }
|
||||
}
|
||||
const status = statusMap[row.status as keyof typeof statusMap] || { text: row.status, class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }
|
||||
return h('span', {
|
||||
class: `px-2 py-1 text-xs font-medium rounded ${status.class}`
|
||||
}, status.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '总项目',
|
||||
key: 'totalItems',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '成功/失败',
|
||||
key: 'progress',
|
||||
width: 120,
|
||||
render: (row: any) => {
|
||||
return h('span', `${row.successful_items} / ${row.failed_items}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 150,
|
||||
render: (row: any) => {
|
||||
return row.created_at ? new Date(row.created_at).toLocaleString('zh-CN') : 'N/A'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (row: any) => {
|
||||
return h('div', { class: 'space-x-2' }, [
|
||||
h('button', {
|
||||
class: 'text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 text-sm',
|
||||
onClick: () => emit('view-task-items', row.id)
|
||||
}, '详情'),
|
||||
h('button', {
|
||||
class: 'text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300 text-sm',
|
||||
disabled: row.status !== 'pending' && row.status !== 'running',
|
||||
onClick: () => emit('start-task', row.id)
|
||||
}, '启动')
|
||||
].filter(btn => !btn.props?.disabled))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 验证凭据
|
||||
const validateCredentials = async () => {
|
||||
if (!credentialsFilePath.value) {
|
||||
message.warning('请先上传凭据文件')
|
||||
return
|
||||
}
|
||||
|
||||
validatingCredentials.value = true
|
||||
|
||||
try {
|
||||
const api = useApi()
|
||||
const response = await api.googleIndexApi.validateCredentials({})
|
||||
|
||||
if (response?.valid) {
|
||||
message.success('凭据验证成功')
|
||||
emit('update:google-index-config')
|
||||
} else {
|
||||
message.error('凭据验证失败:' + (response?.message || '凭据无效或权限不足'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('凭据验证失败:', error)
|
||||
message.error('凭据验证失败: ' + (error?.message || '网络错误'))
|
||||
} finally {
|
||||
validatingCredentials.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Google索引配置
|
||||
const updateGoogleIndexConfig = async () => {
|
||||
emit('update:google-index-config')
|
||||
}
|
||||
|
||||
// 提交网站地图
|
||||
const submitSitemap = async () => {
|
||||
const siteUrl = props.systemConfig?.site_url || ''
|
||||
if (!siteUrl) {
|
||||
message.warning('请先在站点配置中设置站点URL')
|
||||
return
|
||||
}
|
||||
|
||||
const sitemapUrl = siteUrl + '/sitemap.xml'
|
||||
|
||||
try {
|
||||
const api = useApi()
|
||||
const response = await api.googleIndexApi.createGoogleIndexTask({
|
||||
title: `网站地图提交任务 - ${new Date().toLocaleString('zh-CN')}`,
|
||||
type: 'sitemap_submit',
|
||||
description: `提交网站地图: ${sitemapUrl}`,
|
||||
SitemapURL: sitemapUrl
|
||||
})
|
||||
if (response) {
|
||||
message.success('网站地图提交任务已创建')
|
||||
emit('refresh-status')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交网站地图失败:', error)
|
||||
message.error('提交网站地图失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Google Search Console URL
|
||||
const getSearchConsoleUrl = () => {
|
||||
const siteUrl = props.systemConfig?.site_url || ''
|
||||
if (!siteUrl) {
|
||||
return 'https://search.google.com/search-console'
|
||||
}
|
||||
|
||||
// 格式化URL用于Google Search Console
|
||||
const normalizedUrl = siteUrl.startsWith('http') ? siteUrl : `https://${siteUrl}`
|
||||
return `https://search.google.com/search-console/performance/search-analytics?resource_id=${encodeURIComponent(normalizedUrl)}`
|
||||
}
|
||||
|
||||
// 获取Google Analytics URL
|
||||
const getAnalyticsUrl = () => {
|
||||
const siteUrl = props.systemConfig?.site_url || ''
|
||||
|
||||
// 格式化URL用于Google Analytics
|
||||
const normalizedUrl = siteUrl.startsWith('http') ? siteUrl : `https://${siteUrl}`
|
||||
|
||||
// 跳转到Google Analytics
|
||||
return 'https://analytics.google.com/'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
height: calc(100vh - 240px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
199
web/components/Admin/LinkBuildingTab.vue
Normal file
199
web/components/Admin/LinkBuildingTab.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="tab-content-container">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">外链建设(待开发)</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理和监控外部链接建设情况</p>
|
||||
</div>
|
||||
|
||||
<!-- 外链统计 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<i class="fas fa-link text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">总外链数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.total }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||
<i class="fas fa-check text-green-600 dark:text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">有效外链</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.valid }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
||||
<i class="fas fa-clock text-yellow-600 dark:text-yellow-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">待审核</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.pending }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
|
||||
<i class="fas fa-times text-red-600 dark:text-red-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">失效外链</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.invalid }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 外链列表 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">外链列表</h4>
|
||||
<n-button type="primary" @click="$emit('add-new-link')">
|
||||
<template #icon>
|
||||
<i class="fas fa-plus"></i>
|
||||
</template>
|
||||
添加外链
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-data-table
|
||||
:columns="linkColumns"
|
||||
:data="linkList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
striped
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
linkStats: {
|
||||
total: number
|
||||
valid: number
|
||||
pending: number
|
||||
invalid: number
|
||||
}
|
||||
linkList: Array<{
|
||||
id: number
|
||||
url: string
|
||||
title: string
|
||||
status: string
|
||||
domain: string
|
||||
created_at: string
|
||||
}>
|
||||
loading: boolean
|
||||
pagination: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
linkStats: () => ({
|
||||
total: 0,
|
||||
valid: 0,
|
||||
pending: 0,
|
||||
invalid: 0
|
||||
}),
|
||||
linkList: () => [],
|
||||
loading: false,
|
||||
pagination: () => ({})
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'add-new-link': []
|
||||
'edit-link': [row: any]
|
||||
'delete-link': [row: any]
|
||||
'load-link-list': [page: number]
|
||||
}>()
|
||||
|
||||
// 表格列配置
|
||||
const linkColumns = [
|
||||
{
|
||||
title: 'URL',
|
||||
key: 'url',
|
||||
width: 300,
|
||||
render: (row: any) => {
|
||||
return h('a', {
|
||||
href: row.url,
|
||||
target: '_blank',
|
||||
class: 'text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
}, row.url)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '域名',
|
||||
key: 'domain',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (row: any) => {
|
||||
const statusMap = {
|
||||
valid: { text: '有效', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
pending: { text: '待审核', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
||||
invalid: { text: '失效', class: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }
|
||||
}
|
||||
const status = statusMap[row.status as keyof typeof statusMap]
|
||||
return h('span', {
|
||||
class: `px-2 py-1 text-xs font-medium rounded ${status.class}`
|
||||
}, status.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render: (row: any) => {
|
||||
return h('div', { class: 'space-x-2' }, [
|
||||
h('button', {
|
||||
class: 'text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300',
|
||||
onClick: () => emit('edit-link', row)
|
||||
}, '编辑'),
|
||||
h('button', {
|
||||
class: 'text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300',
|
||||
onClick: () => emit('delete-link', row)
|
||||
}, '删除')
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
height: calc(100vh - 240px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
275
web/components/Admin/SiteSubmitTab.vue
Normal file
275
web/components/Admin/SiteSubmitTab.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="tab-content-container">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">站点提交(待开发)</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">向各大搜索引擎提交站点信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索引擎列表 -->
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 百度 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded flex items-center justify-center">
|
||||
<i class="fas fa-search text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">百度</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">baidu.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitToBaidu">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.baidu || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 谷歌 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-red-500 rounded flex items-center justify-center">
|
||||
<i class="fas fa-globe text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">谷歌</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">google.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitToGoogle">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.google || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 必应 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-green-500 rounded flex items-center justify-center">
|
||||
<i class="fas fa-search text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">必应</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">bing.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitToBing">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.bing || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜狗 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-orange-500 rounded flex items-center justify-center">
|
||||
<i class="fas fa-search text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">搜狗</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">sogou.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitToSogou">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.sogou || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 神马搜索 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded flex items-center justify-center">
|
||||
<i class="fas fa-mobile-alt text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">神马搜索</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">sm.cn</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitToShenma">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.shenma || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 360搜索 -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-green-600 rounded flex items-center justify-center">
|
||||
<i class="fas fa-shield-alt text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">360搜索</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">so.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-button size="small" type="primary" @click="submitTo360">
|
||||
<template #icon>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</template>
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
最后提交时间:{{ lastSubmitTime.so360 || '未提交' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量提交 -->
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="font-medium text-blue-900 dark:text-blue-100">批量提交</h4>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
||||
一键提交到所有支持的搜索引擎
|
||||
</p>
|
||||
</div>
|
||||
<n-button type="primary" @click="submitToAll">
|
||||
<template #icon>
|
||||
<i class="fas fa-rocket"></i>
|
||||
</template>
|
||||
批量提交
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
lastSubmitTime: {
|
||||
baidu: string
|
||||
google: string
|
||||
bing: string
|
||||
sogou: string
|
||||
shenma: string
|
||||
so360: string
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
lastSubmitTime: () => ({
|
||||
baidu: '',
|
||||
google: '',
|
||||
bing: '',
|
||||
sogou: '',
|
||||
shenma: '',
|
||||
so360: ''
|
||||
})
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:last-submit-time': [engine: string, time: string]
|
||||
}>()
|
||||
|
||||
// 获取消息组件
|
||||
const message = useMessage()
|
||||
|
||||
// 提交到百度
|
||||
const submitToBaidu = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'baidu', time)
|
||||
message.success('已提交到百度')
|
||||
}
|
||||
|
||||
// 提交到谷歌
|
||||
const submitToGoogle = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'google', time)
|
||||
message.success('已提交到谷歌')
|
||||
}
|
||||
|
||||
// 提交到必应
|
||||
const submitToBing = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'bing', time)
|
||||
message.success('已提交到必应')
|
||||
}
|
||||
|
||||
// 提交到搜狗
|
||||
const submitToSogou = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'sogou', time)
|
||||
message.success('已提交到搜狗')
|
||||
}
|
||||
|
||||
// 提交到神马搜索
|
||||
const submitToShenma = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'shenma', time)
|
||||
message.success('已提交到神马搜索')
|
||||
}
|
||||
|
||||
// 提交到360搜索
|
||||
const submitTo360 = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
emit('update:last-submit-time', 'so360', time)
|
||||
message.success('已提交到360搜索')
|
||||
}
|
||||
|
||||
// 批量提交
|
||||
const submitToAll = () => {
|
||||
const time = new Date().toLocaleString('zh-CN')
|
||||
const engines = ['baidu', 'google', 'bing', 'sogou', 'shenma', 'so360']
|
||||
|
||||
engines.forEach(engine => {
|
||||
emit('update:last-submit-time', engine, time)
|
||||
})
|
||||
|
||||
message.success('已批量提交到所有搜索引擎')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
height: calc(100vh - 240px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
226
web/components/Admin/SitemapTab.vue
Normal file
226
web/components/Admin/SitemapTab.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="tab-content-container">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<!-- Sitemap配置 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Sitemap配置</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理网站的Sitemap生成和配置</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-2">自动生成Sitemap</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
开启后系统将定期自动生成Sitemap文件
|
||||
</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="sitemapConfig.autoGenerate"
|
||||
@update:value="updateSitemapConfig"
|
||||
:loading="configLoading"
|
||||
size="large"
|
||||
>
|
||||
<template #checked>已开启</template>
|
||||
<template #unchecked>已关闭</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
|
||||
<!-- 配置详情 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">站点URL</label>
|
||||
<n-input
|
||||
:value="systemConfig?.site_url || '站点URL未配置'"
|
||||
:disabled="true"
|
||||
placeholder="请先在站点配置中设置站点URL"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="fas fa-globe text-gray-400"></i>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">最后生成时间</label>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ sitemapConfig.lastGenerate || '尚未生成' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sitemap统计 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<i class="fas fa-link text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">资源总数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.total_resources || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||
<i class="fas fa-sitemap text-green-600 dark:text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">页面数量</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.total_pages || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||
<i class="fas fa-history text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">最后更新</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.last_generate || 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="generateSitemap"
|
||||
:loading="isGenerating"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cog"></i>
|
||||
</template>
|
||||
生成Sitemap
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="success"
|
||||
@click="viewSitemap"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</template>
|
||||
查看Sitemap
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="info"
|
||||
@click="$emit('refresh-status')"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</template>
|
||||
刷新状态
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 生成状态 -->
|
||||
<div v-if="generateStatus" class="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||
<span class="text-blue-700 dark:text-blue-300">{{ generateStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
systemConfig?: any
|
||||
sitemapConfig: any
|
||||
sitemapStats: any
|
||||
configLoading: boolean
|
||||
isGenerating: boolean
|
||||
generateStatus: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
systemConfig: null,
|
||||
sitemapConfig: () => ({}),
|
||||
sitemapStats: () => ({}),
|
||||
configLoading: false,
|
||||
isGenerating: false,
|
||||
generateStatus: ''
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:sitemap-config': [value: boolean]
|
||||
'refresh-status': []
|
||||
}>()
|
||||
|
||||
// 获取消息组件
|
||||
const message = useMessage()
|
||||
|
||||
// 更新Sitemap配置
|
||||
const updateSitemapConfig = async (value: boolean) => {
|
||||
try {
|
||||
const api = useApi()
|
||||
await api.sitemapApi.updateSitemapConfig({
|
||||
autoGenerate: value,
|
||||
lastGenerate: props.sitemapConfig.lastGenerate,
|
||||
lastUpdate: new Date().toISOString()
|
||||
})
|
||||
message.success(value ? '自动生成功能已开启' : '自动生成功能已关闭')
|
||||
} catch (error) {
|
||||
message.error('更新配置失败')
|
||||
// 恢复之前的值
|
||||
props.sitemapConfig.autoGenerate = !value
|
||||
}
|
||||
}
|
||||
|
||||
// 生成Sitemap
|
||||
const generateSitemap = async () => {
|
||||
// 使用已经加载的系统配置
|
||||
const siteUrl = props.systemConfig?.site_url || ''
|
||||
if (!siteUrl) {
|
||||
message.warning('请先在站点配置中设置站点URL,然后再生成Sitemap')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const api = useApi()
|
||||
const response = await api.sitemapApi.generateSitemap({ site_url: siteUrl })
|
||||
|
||||
if (response) {
|
||||
message.success(`Sitemap生成任务已启动,使用站点URL: ${siteUrl}`)
|
||||
// 更新统计信息
|
||||
emit('refresh-status')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('Sitemap生成失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 查看Sitemap
|
||||
const viewSitemap = () => {
|
||||
window.open('/sitemap.xml', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
height: calc(100vh - 240px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user