Files
urldb/web/pages/admin/seo.vue
2025-11-21 09:30:33 +08:00

790 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<AdminPageLayout>
<!-- 页面头部 - 标题 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">SEO管理</h1>
<p class="text-gray-600 dark:text-gray-400">搜索引擎优化管理</p>
</div>
</template>
<!-- 内容区 -->
<template #content>
<div class="config-content h-full">
<!-- Tab导航 -->
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="site-submit" tab="站点提交">
<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>
</n-tab-pane>
<n-tab-pane name="link-building" tab="外链建设">
<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="addNewLink">
<template #icon>
<i class="fas fa-plus"></i>
</template>
添加外链
</n-button>
</div>
<n-data-table
:columns="linkColumns"
:data="linkList"
:pagination="linkPagination"
:loading="linkLoading"
:bordered="false"
striped
/>
</div>
</div>
</div>
</n-tab-pane>
<n-tab-pane name="sitemap" tab="Sitemap管理">
<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">Sitemap管理</h3>
<p class="text-gray-600 dark:text-gray-400">管理和生成网站sitemap文件提升搜索引擎收录效果</p>
</div>
<!-- Sitemap配置 -->
<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">自动生成功能</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>
<!-- 站点URL显示 -->
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
<div class="flex items-center justify-between">
<div>
<h5 class="font-medium text-gray-900 dark:text-white mb-1">当前站点URL</h5>
<p class="text-sm text-gray-600 dark:text-gray-400">
Sitemap生成时将使用此URL作为基础域名
</p>
</div>
<div class="text-right">
<div v-if="systemConfig?.site_url" class="text-sm font-medium text-blue-600 dark:text-blue-400 mb-2">
{{ systemConfig.site_url }}
</div>
<div v-else class="text-sm text-yellow-600 dark:text-yellow-400 mb-2">
未配置
</div>
<NuxtLink to="/admin/site-config" class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
前往配置
</NuxtLink>
</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-database 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 }}</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-file-code text-green-600 dark:text-green-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">Sitemap数量</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.total_pages }}</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 || '从未' }}</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-cogs"></i>
</template>
手动生成Sitemap
</n-button>
<n-button
type="default"
@click="viewSitemap"
size="large"
>
<template #icon>
<i class="fas fa-external-link-alt"></i>
</template>
查看Sitemap
</n-button>
<n-button
type="info"
@click="refreshSitemapStatus"
size="large"
>
<template #icon>
<i class="fas fa-sync-alt"></i>
</template>
刷新状态
</n-button>
</div>
<!-- 生成状态 -->
<div v-if="generateStatus" class="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div class="flex items-start">
<i class="fas fa-info-circle text-yellow-600 dark:text-yellow-400 mt-0.5 mr-2"></i>
<div>
<h4 class="font-medium text-yellow-800 dark:text-yellow-200 mb-1">生成状态</h4>
<p class="text-sm text-yellow-700 dark:text-yellow-300">{{ generateStatus }}</p>
</div>
</div>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// SEO管理页面
definePageMeta({
layout: 'admin'
})
import { useMessage } from 'naive-ui'
// 获取消息组件
const message = useMessage()
// 当前激活的Tab
const activeTab = ref('site-submit')
// 最后提交时间
const lastSubmitTime = ref({
baidu: '',
google: '',
bing: '',
sogou: '',
shenma: '',
so360: ''
})
// 外链统计
const linkStats = ref({
total: 156,
valid: 142,
pending: 8,
invalid: 6
})
// 外链列表
const linkList = ref([
{
id: 1,
url: 'https://example1.com',
title: '示例外链1',
status: 'valid',
domain: 'example1.com',
created_at: '2024-01-15'
},
{
id: 2,
url: 'https://example2.com',
title: '示例外链2',
status: 'pending',
domain: 'example2.com',
created_at: '2024-01-16'
},
{
id: 3,
url: 'https://example3.com',
title: '示例外链3',
status: 'invalid',
domain: 'example3.com',
created_at: '2024-01-17'
}
])
const linkLoading = ref(false)
// 分页配置
const linkPagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
linkPagination.value.page = page
loadLinkList()
},
onUpdatePageSize: (pageSize: number) => {
linkPagination.value.pageSize = pageSize
linkPagination.value.page = 1
loadLinkList()
}
})
// 表格列配置
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: () => editLink(row)
}, '编辑'),
h('button', {
class: 'text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300',
onClick: () => deleteLink(row)
}, '删除')
])
}
}
]
// 提交到百度
const submitToBaidu = () => {
// 模拟提交
lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
message.success('已提交到百度')
}
// 提交到谷歌
const submitToGoogle = () => {
// 模拟提交
lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
message.success('已提交到谷歌')
}
// 提交到必应
const submitToBing = () => {
// 模拟提交
lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
message.success('已提交到必应')
}
// 提交到搜狗
const submitToSogou = () => {
// 模拟提交
lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
message.success('已提交到搜狗')
}
// 提交到神马搜索
const submitToShenma = () => {
// 模拟提交
lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
message.success('已提交到神马搜索')
}
// 提交到360搜索
const submitTo360 = () => {
// 模拟提交
lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
message.success('已提交到360搜索')
}
// 批量提交
const submitToAll = () => {
// 模拟批量提交
lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
message.success('已批量提交到所有搜索引擎')
}
// 加载外链列表
const loadLinkList = () => {
// 模拟加载数据
linkLoading.value = true
setTimeout(() => {
linkLoading.value = false
}, 1000)
}
// 添加新外链
const addNewLink = () => {
message.info('添加外链功能开发中')
}
// 编辑外链
const editLink = (row: any) => {
message.info(`编辑外链: ${row.title}`)
}
// 删除外链
const deleteLink = (row: any) => {
message.warning(`删除外链: ${row.title}`)
}
// Sitemap管理相关
const sitemapConfig = ref({
autoGenerate: false,
lastGenerate: '',
lastUpdate: ''
})
const sitemapStats = ref({
total_resources: 0,
total_pages: 0,
last_generate: ''
})
const configLoading = ref(false)
const isGenerating = ref(false)
const generateStatus = ref('')
// 获取Sitemap配置
const loadSitemapConfig = async () => {
try {
const sitemapApi = useSitemapApi()
const response = await sitemapApi.getSitemapConfig()
if (response) {
sitemapConfig.value = response
}
} catch (error) {
message.error('获取Sitemap配置失败')
}
}
// 更新Sitemap配置
const updateSitemapConfig = async (value: boolean) => {
configLoading.value = true
try {
const sitemapApi = useSitemapApi()
await sitemapApi.updateSitemapConfig({
autoGenerate: value,
lastGenerate: sitemapConfig.value.lastGenerate,
lastUpdate: new Date().toISOString()
})
message.success(value ? '自动生成功能已开启' : '自动生成功能已关闭')
} catch (error) {
message.error('更新配置失败')
// 恢复之前的值
sitemapConfig.value.autoGenerate = !value
} finally {
configLoading.value = false
}
}
// 生成Sitemap
const generateSitemap = async () => {
isGenerating.value = true
generateStatus.value = '正在启动生成任务...'
try {
// 使用已经加载的系统配置
const siteUrl = systemConfig.value?.site_url || ''
if (!siteUrl) {
message.warning('请先在站点配置中设置站点URL然后再生成Sitemap')
isGenerating.value = false
return
}
const sitemapApi = useSitemapApi()
const response = await sitemapApi.generateSitemap({ site_url: siteUrl })
if (response) {
generateStatus.value = response.message || '生成任务已启动'
message.success(`Sitemap生成任务已启动使用站点URL: ${siteUrl}`)
// 更新统计信息
sitemapStats.value.total_resources = response.total_resources || 0
sitemapStats.value.total_pages = response.total_pages || 0
}
} catch (error: any) {
generateStatus.value = '生成失败: ' + (error.message || '未知错误')
message.error('Sitemap生成失败')
} finally {
isGenerating.value = false
}
}
// 刷新Sitemap状态
const refreshSitemapStatus = async () => {
try {
const sitemapApi = useSitemapApi()
const response = await sitemapApi.getSitemapStatus()
if (response) {
sitemapStats.value = response
generateStatus.value = '状态已刷新'
}
} catch (error) {
message.error('刷新状态失败')
}
}
// 查看Sitemap
const viewSitemap = () => {
window.open('/sitemap.xml', '_blank')
}
// 获取系统配置
const systemConfig = ref<any>(null)
// 加载系统配置
const loadSystemConfig = async () => {
try {
const { useSystemConfigStore } = await import('~/stores/systemConfig')
const systemConfigStore = useSystemConfigStore()
await systemConfigStore.initConfig(true, true)
systemConfig.value = systemConfigStore.config
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
// 初始化
onMounted(async () => {
await loadSystemConfig()
loadLinkList()
await loadSitemapConfig()
await refreshSitemapStatus()
})
</script>
<style scoped>
/* SEO管理页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>