|
- /**
- * AI 资讯(文章)模块共用逻辑
- *
- * 栏目与路由完全沿用原 PC 站,保证 SEO 不断链:
- * col_id 1 前沿资讯 → /info/new 详情 /info/new/{id}.html
- * col_id 2 使用教程 → /info/course 详情 /info/course/{id}.html
- * col_id 3 AI 问答 → /info/what 详情 /info/what/{id}.html
- * 标签页 → /info/tag/{key}
- * 资讯首页(全部) → /info
- */
- import dayjs from 'dayjs'
- import {
- LEGACY_DEFAULT_DESCRIPTION,
- LEGACY_DEFAULT_KEYWORDS,
- legacyPageTitle,
- } from '~/composables/useLegacySeo'
-
- /** 栏目定义 */
- export const COLUMNS = [
- { colId: 1, key: 'new', name: '前沿资讯', path: '/info/new', icon: 'i-lucide-newspaper' },
- { colId: 2, key: 'course', name: '使用教程', path: '/info/course', icon: 'i-lucide-graduation-cap' },
- { colId: 3, key: 'what', name: 'AI 问答', path: '/info/what', icon: 'i-lucide-circle-help' },
- ]
-
- /** col_id → 详情 URL(保留 .html 后缀,与原站一致) */
- export function articleUrl(item) {
- const col = COLUMNS.find(c => c.colId === Number(item?.col_id)) || COLUMNS[0]
- return `${col.path}/${item.id}.html`
- }
-
- /** col_id → 栏目名 */
- export function colName(colId) {
- return COLUMNS.find(c => c.colId === Number(colId))?.name || '资讯'
- }
-
- /** col_id → 栏目列表页路径 */
- export function colPath(colId) {
- return COLUMNS.find(c => c.colId === Number(colId))?.path || '/info'
- }
-
- /** 列表分页 URL(沿用原站:/info2、/info/new2、/info/tag/key-2.html) */
- export function pageUrl(basePath, page, tagKey) {
- if (tagKey) return page > 1 ? `/info/tag/${tagKey}-${page}.html` : `/info/tag/${tagKey}.html`
- return page > 1 ? `${basePath}${page}` : basePath
- }
-
- /** 秒级时间戳格式化 */
- export function formatTime(value, fmt = 'YYYY-MM-DD HH:mm') {
- if (!value) return ''
- return dayjs(Number(value) * 1000).format(fmt)
- }
-
- /** 从路由参数中取出文章 id(去掉 .html 后缀) */
- export function parseArticleId(param) {
- return String(param || '').replace(/\.html$/i, '')
- }
-
- /**
- * 资讯页共用数据:列表 + 热门排行 + 热门标签
- * 用 useAsyncData 做 SSR,保证爬虫可直接抓到内容
- */
- export function useArticleData(options) {
- const { key, colId, tagKey, page, pageSize = 10, isRecommend } = options
- const { post } = useApi()
-
- return useAsyncData(
- key,
- async () => {
- const listParams = { page_no: unref(page) || 1, page_size: pageSize }
- if (colId) listParams.col_id = colId
- if (tagKey) listParams.tag_key = unref(tagKey)
- if (isRecommend) listParams.is_recommend = 1
-
- const [listRes, hotRes, tagRes] = await Promise.all([
- post('/article/getlist', listParams).catch(() => ({ list: [], data: {} })),
- post('/article/gethotlist', { page_no: 1, page_size: 10 }).catch(() => ({ list: [] })),
- post('/article/gettaglist', { is_hot: 1, page_size: 20 }).catch(() => ({ list: [] })),
- ])
-
- return {
- list: (listRes.list || []).map(i => ({ ...i, url: articleUrl(i), time: formatTime(i.publish_time) })),
- total: Number(listRes.data?.total || 0),
- tagName: listRes.data?.tag_name || '',
- hotList: (hotRes.list || []).map((i, idx) => ({ ...i, rank: idx + 1, url: articleUrl(i) })),
- tagList: tagRes.list || [],
- }
- },
- { watch: [page, tagKey].filter(Boolean) },
- )
- }
-
- /**
- * 文章详情共用数据(SSR),并统一处理 SEO
- * @param {number} colId 当前栏目(用于校验路由与文章栏目是否一致)
- */
- export async function useArticleDetail(colId) {
- const route = useRoute()
- const nuxtApp = useNuxtApp()
- const { post } = useApi()
- const id = parseArticleId(route.params.id)
-
- const asyncData = await useAsyncData(`article-${id}`, async () => {
- const [detailRes, hotRes, tagRes] = await Promise.all([
- post('/article/getdetail', { id, prev_next: 1 }),
- post('/article/gethotlist', { page_no: 1, page_size: 10 }).catch(() => ({ list: [] })),
- post('/article/gettaglist', { is_hot: 1, page_size: 20 }).catch(() => ({ list: [] })),
- ])
- return {
- detail: detailRes.data || {},
- hotList: (hotRes.list || []).map((i, idx) => ({ ...i, rank: idx + 1, url: articleUrl(i) })),
- tagList: tagRes.list || [],
- }
- })
-
- const { data, error } = asyncData
- const detail = computed(() => data.value?.detail || {})
- const canonical = computed(() => `https://tool.aionline.cc${articleUrl(detail.value)}`)
-
- // 文章不存在 → 404,避免无效页面被搜索引擎收录
- if (error.value || !data.value?.detail?.id) {
- throw createError({ statusCode: 404, statusMessage: '文章不存在', fatal: true })
- }
-
- await nuxtApp.runWithContext(() => {
- useSeoMeta({
- title: () => legacyPageTitle(detail.value.title),
- description: () => detail.value.info || LEGACY_DEFAULT_DESCRIPTION,
- keywords: () => detail.value.title || LEGACY_DEFAULT_KEYWORDS,
- ogTitle: () => detail.value.title,
- ogDescription: () => detail.value.info || detail.value.title,
- ogType: 'article',
- ogImage: () => detail.value.cover || '',
- articlePublishedTime: () => formatTime(detail.value.publish_time, 'YYYY-MM-DDTHH:mm:ssZ'),
- })
-
- useHead({
- link: [{ rel: 'canonical', href: canonical }],
- // 结构化数据,利于搜索结果富摘要
- script: [{
- type: 'application/ld+json',
- innerHTML: computed(() => JSON.stringify({
- '@context': 'https://schema.org',
- '@type': 'Article',
- headline: detail.value.title,
- description: detail.value.info || '',
- image: detail.value.cover ? [detail.value.cover] : undefined,
- datePublished: formatTime(detail.value.publish_time, 'YYYY-MM-DD'),
- author: { '@type': 'Organization', name: 'AI在线' },
- publisher: { '@type': 'Organization', name: 'AI在线' },
- mainEntityOfPage: { '@type': 'WebPage', '@id': canonical.value },
- })),
- }],
- })
- })
-
- // 访问的栏目路径与文章实际栏目不一致 → 301 到规范地址(避免重复内容)
- if (colId && Number(detail.value.col_id) !== Number(colId)) {
- await nuxtApp.runWithContext(() =>
- navigateTo(articleUrl(detail.value), { redirectCode: 301, replace: true }))
- }
-
- return { data, detail }
- }
|