Files
urldb/web/components/Admin/GoogleIndexTab.vue
2025-11-24 18:18:19 +08:00

539 lines
18 KiB
Vue

<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>