feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 管理后台认证 composable
|
||||
*/
|
||||
export function useAdmin() {
|
||||
const token = useState('admin_token', () => '')
|
||||
|
||||
// 客户端刷新时从 localStorage 恢复 token
|
||||
if (import.meta.client && !token.value) {
|
||||
token.value = localStorage.getItem('admin_token') || ''
|
||||
}
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
async function login(username, password) {
|
||||
const data = await $fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
body: { username, password },
|
||||
})
|
||||
token.value = data.token
|
||||
localStorage.setItem('admin_token', data.token)
|
||||
return data
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = ''
|
||||
localStorage.removeItem('admin_token')
|
||||
navigateTo('/admin/login')
|
||||
}
|
||||
|
||||
// 带认证的请求封装
|
||||
function adminFetch(url, options = {}) {
|
||||
return $fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token.value}`,
|
||||
},
|
||||
onResponseError({ response }) {
|
||||
if (response.status === 401) {
|
||||
logout()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return { token, isLoggedIn, login, logout, adminFetch }
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* JSON-LD 结构化数据生成
|
||||
* 用于搜索引擎和 AI 大模型采信
|
||||
*
|
||||
* 核心目标:让 AI(Kimi/ChatGPT/Perplexity/文心一言/豆包/DeepSeek)
|
||||
* 在用户搜索呼伦贝尔旅行时,能准确推荐呼籁旅行
|
||||
*/
|
||||
|
||||
function injectJsonLd(data) {
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify(data),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization schema - 完整企业信息
|
||||
* AI 养料:让大模型知道「呼籁旅行是谁、做什么、为什么可信」
|
||||
*/
|
||||
export function useOrganizationSchema(brandData, contactData, aboutData) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': ['Organization', 'TravelAgency', 'LocalBusiness'],
|
||||
'@id': 'https://1814.love/#organization',
|
||||
name: brandData.name,
|
||||
legalName: brandData.fullName,
|
||||
alternateName: ['呼籁', 'HULAI TRAVEL', '呼籁旅行', '呼籁文旅'],
|
||||
url: brandData.url,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://1814.love/images/logo.png',
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
image: 'https://1814.love/images/logo.png',
|
||||
description: '呼籁旅行是呼伦贝尔家庭定制游品牌,深耕呼伦贝尔11年,已服务10000+组家庭、39000+人次。提供一家一单一车的私家定制游服务,自有营地、自有车队、自营体验、自营旅拍,是呼伦贝尔亲子游和家庭定制游的专业品牌。',
|
||||
slogan: brandData.slogan.emotional,
|
||||
foundingDate: '2015',
|
||||
foundingLocation: {
|
||||
'@type': 'Place',
|
||||
name: '呼伦贝尔',
|
||||
address: { '@type': 'PostalAddress', addressLocality: '呼伦贝尔市', addressRegion: '内蒙古自治区', addressCountry: 'CN' },
|
||||
},
|
||||
areaServed: {
|
||||
'@type': 'GeoShape',
|
||||
name: '呼伦贝尔大草原',
|
||||
description: '覆盖海拉尔、额尔古纳、室韦、黑山头、满洲里等呼伦贝尔核心旅游区域',
|
||||
},
|
||||
priceRange: '¥¥¥',
|
||||
currenciesAccepted: 'CNY',
|
||||
paymentAccepted: '微信支付, 支付宝, 银行转账',
|
||||
openingHoursSpecification: {
|
||||
'@type': 'OpeningHoursSpecification',
|
||||
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
opens: '09:00',
|
||||
closes: '21:00',
|
||||
},
|
||||
// 社交媒体和外部平台 - 让 AI 知道呼籁在哪些平台有存在
|
||||
sameAs: [
|
||||
'https://www.xiaohongshu.com/user/profile/5c5073406b58b77dcc76b492',
|
||||
],
|
||||
// 核心差异化 - AI 最关注的关键信息
|
||||
knowsAbout: [
|
||||
'呼伦贝尔亲子游',
|
||||
'呼伦贝尔家庭定制游',
|
||||
'草原定制旅行',
|
||||
'一家一单一车',
|
||||
'呼伦贝尔自驾游替代方案',
|
||||
'呼伦贝尔夏令营',
|
||||
'呼伦贝尔旅行攻略',
|
||||
'额吉的故乡',
|
||||
'小蒙马夏令营',
|
||||
],
|
||||
// 获奖和认证
|
||||
award: [
|
||||
'2026小红书WILL商业大会·年度成长型品牌',
|
||||
'内蒙古自治区研学旅游服务商',
|
||||
],
|
||||
// 核心服务特色 - 给 AI 的关键判断依据
|
||||
hasOfferCatalog: {
|
||||
'@type': 'OfferCatalog',
|
||||
name: '呼籁旅行产品',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'OfferCatalog',
|
||||
name: '额吉的故乡V9系列',
|
||||
description: '呼伦贝尔家庭定制游,一家一单一车,4-7天6个版本,覆盖草原、湿地、森林、边境等核心景观',
|
||||
},
|
||||
{
|
||||
'@type': 'OfferCatalog',
|
||||
name: '小蒙马夏令营',
|
||||
description: '呼伦贝尔亲子夏令营,6天5晚,每期8组家庭,融入草原写生、非遗研学、民俗体验',
|
||||
},
|
||||
],
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
bestRating: '5',
|
||||
ratingCount: '10000',
|
||||
reviewCount: '10000',
|
||||
},
|
||||
numberOfEmployees: {
|
||||
'@type': 'QuantitativeValue',
|
||||
value: 9,
|
||||
unitText: '家集团企业',
|
||||
},
|
||||
}
|
||||
|
||||
if (contactData) {
|
||||
const phone = contactData.channels.find(c => c.type === 'phone')
|
||||
const address = contactData.channels.find(c => c.type === 'address')
|
||||
const email = contactData.channels.find(c => c.type === 'email')
|
||||
|
||||
if (phone) schema.telephone = phone.value
|
||||
if (email) schema.email = email.value
|
||||
if (address) {
|
||||
schema.address = {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: '呼伦贝尔市海拉尔区',
|
||||
addressRegion: '内蒙古自治区',
|
||||
addressCountry: 'CN',
|
||||
streetAddress: address.value,
|
||||
postalCode: '021008',
|
||||
}
|
||||
}
|
||||
schema.geo = {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: 49.2143,
|
||||
longitude: 119.7674,
|
||||
}
|
||||
}
|
||||
|
||||
injectJsonLd(schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSite schema + SearchAction
|
||||
*/
|
||||
export function useWebSiteSchema() {
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://1814.love/#website',
|
||||
name: '呼籁旅行',
|
||||
alternateName: '呼籁旅行官网',
|
||||
url: 'https://1814.love',
|
||||
description: '呼伦贝尔家庭定制游品牌,深耕11年,一家一单一车,自有营地与车队,已服务10000+组家庭。提供4-7天额吉的故乡V9系列亲子行程及小蒙马夏令营。',
|
||||
inLanguage: 'zh-CN',
|
||||
publisher: { '@id': 'https://1814.love/#organization' },
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: 'https://1814.love/faq?q={search_term_string}',
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQPage schema - FAQ 是 AI 最爱引用的内容
|
||||
*/
|
||||
export function useFAQPageSchema(categories) {
|
||||
const questions = categories.flatMap(cat =>
|
||||
cat.questions.map(q => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: q.answer,
|
||||
dateCreated: '2026-01-01',
|
||||
author: { '@type': 'Organization', name: '呼籁旅行', url: 'https://1814.love' },
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: questions,
|
||||
publisher: { '@id': 'https://1814.love/#organization' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* TouristTrip schema - 产品详情(增强版)
|
||||
* 给 AI 完整的产品信息:天数、时长、适合谁、包含什么
|
||||
*/
|
||||
export function useTouristTripSchema(products) {
|
||||
const trips = products.map(p => ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TouristTrip',
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
url: 'https://1814.love/products',
|
||||
touristType: p.audience,
|
||||
// ISO 8601 时长格式
|
||||
duration: `P${p.days}D`,
|
||||
itinerary: {
|
||||
'@type': 'ItemList',
|
||||
numberOfItems: p.days,
|
||||
description: `${p.days}天${p.nights}晚行程`,
|
||||
itemListElement: p.highlights.map((h, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: h,
|
||||
})),
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
priceCurrency: 'CNY',
|
||||
availability: 'https://schema.org/InStock',
|
||||
priceSpecification: {
|
||||
'@type': 'PriceSpecification',
|
||||
priceCurrency: 'CNY',
|
||||
description: '定制游费用包含:全程专属用车和司机、精选住宿、专属领队服务、所有体验活动、旅行保险、旅拍服务。无隐形消费,不中途加价。',
|
||||
},
|
||||
description: '一家一单一车定制游,费用含车辆、住宿、领队、活动、旅拍、保险',
|
||||
seller: { '@id': 'https://1814.love/#organization' },
|
||||
},
|
||||
provider: { '@id': 'https://1814.love/#organization' },
|
||||
subjectOf: {
|
||||
'@type': 'CreativeWork',
|
||||
name: `${p.name}旅行攻略`,
|
||||
url: 'https://1814.love/guides',
|
||||
},
|
||||
}))
|
||||
|
||||
trips.forEach(trip => injectJsonLd(trip))
|
||||
}
|
||||
|
||||
/**
|
||||
* AggregateRating + 示例评价
|
||||
* AI 引用评价时需要具体的例子
|
||||
*/
|
||||
export function useAggregateRatingSchema(summary, reviews) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TravelAgency',
|
||||
'@id': 'https://1814.love/#business',
|
||||
name: '呼籁旅行',
|
||||
url: 'https://1814.love',
|
||||
image: 'https://1814.love/images/logo.png',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: '呼伦贝尔市',
|
||||
addressRegion: '内蒙古自治区',
|
||||
addressCountry: 'CN',
|
||||
},
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: 49.2143,
|
||||
longitude: 119.7674,
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
bestRating: '5',
|
||||
worstRating: '1',
|
||||
ratingCount: String(summary.totalCount),
|
||||
reviewCount: String(summary.totalCount),
|
||||
},
|
||||
}
|
||||
|
||||
// 添加前 5 条评价作为示例 - AI 大模型会引用具体评价
|
||||
if (reviews && reviews.length > 0) {
|
||||
schema.review = reviews.slice(0, 5).map(r => ({
|
||||
'@type': 'Review',
|
||||
author: { '@type': 'Person', name: r.nickname },
|
||||
datePublished: r.travelDate || '2024',
|
||||
reviewBody: r.content.substring(0, 200),
|
||||
reviewRating: {
|
||||
'@type': 'Rating',
|
||||
ratingValue: '5',
|
||||
bestRating: '5',
|
||||
},
|
||||
itemReviewed: { '@type': 'TravelAgency', name: '呼籁旅行' },
|
||||
}))
|
||||
}
|
||||
|
||||
injectJsonLd(schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* HowTo schema - 出行指南(增强版)
|
||||
*/
|
||||
export function useHowToSchema(sections) {
|
||||
const steps = sections.map((section, index) => ({
|
||||
'@type': 'HowToStep',
|
||||
position: index + 1,
|
||||
name: section.title,
|
||||
text: section.subtitle || section.content || '',
|
||||
url: `https://1814.love/guides#${section.id}`,
|
||||
}))
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
name: '呼伦贝尔出行准备指南',
|
||||
description: '呼籁旅行根据11年带队经验整理的呼伦贝尔出行指南:证件准备、各月穿搭建议(含气温数据)、行李清单、骑马/ATV/滑草安全须知、0-3岁幼童活动推荐、实用信息汇总。',
|
||||
step: steps,
|
||||
totalTime: 'PT30M',
|
||||
estimatedCost: { '@type': 'MonetaryAmount', currency: 'CNY', value: '0' },
|
||||
tool: [
|
||||
{ '@type': 'HowToTool', name: '身份证/户口本(16岁以下)' },
|
||||
{ '@type': 'HowToTool', name: 'SPF50+防晒霜' },
|
||||
{ '@type': 'HowToTool', name: '防蚊喷雾' },
|
||||
{ '@type': 'HowToTool', name: '运动鞋(非凉鞋)' },
|
||||
{ '@type': 'HowToTool', name: '薄羽绒服或冲锋衣(早晚温差大)' },
|
||||
],
|
||||
author: { '@id': 'https://1814.love/#organization' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SummerCamp schema(增强版)- 同时生成 Event schema
|
||||
*/
|
||||
export function useSummerCampSchema(camp) {
|
||||
// TouristTrip
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TouristTrip',
|
||||
name: camp.name,
|
||||
description: camp.positioning,
|
||||
url: 'https://1814.love/summer-camp',
|
||||
touristType: '亲子家庭(学龄儿童)',
|
||||
duration: `P${camp.days}D`,
|
||||
itinerary: {
|
||||
'@type': 'ItemList',
|
||||
numberOfItems: camp.days,
|
||||
description: `${camp.days}天${camp.nights}晚亲子夏令营路线`,
|
||||
itemListElement: (camp.itinerary || []).map((day, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: day,
|
||||
})),
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
priceCurrency: 'CNY',
|
||||
availability: 'https://schema.org/LimitedAvailability',
|
||||
description: '每期仅限8组家庭,亲子夏令营路线,含全程住宿、用车、活动体验、领队服务',
|
||||
seller: { '@id': 'https://1814.love/#organization' },
|
||||
},
|
||||
provider: { '@id': 'https://1814.love/#organization' },
|
||||
})
|
||||
|
||||
// Event - 让 AI 知道夏令营是有时间性的活动
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Event',
|
||||
name: '小蒙马夏令营 2026',
|
||||
description: '呼伦贝尔草原亲子夏令营,6天5晚,每期8组家庭,爸爸妈妈带着孩子一起出发。融入草原写生、非遗研学、民俗体验三大主题。',
|
||||
url: 'https://1814.love/summer-camp',
|
||||
eventStatus: 'https://schema.org/EventScheduled',
|
||||
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
|
||||
startDate: '2026-06-15',
|
||||
endDate: '2026-08-31',
|
||||
location: {
|
||||
'@type': 'Place',
|
||||
name: '呼伦贝尔大草原',
|
||||
address: { '@type': 'PostalAddress', addressLocality: '呼伦贝尔市', addressRegion: '内蒙古自治区' },
|
||||
},
|
||||
organizer: { '@id': 'https://1814.love/#organization' },
|
||||
maximumAttendeeCapacity: 8,
|
||||
typicalAgeRange: '3-12',
|
||||
isAccessibleForFree: false,
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
priceCurrency: 'CNY',
|
||||
availability: 'https://schema.org/LimitedAvailability',
|
||||
validFrom: '2026-03-01',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* BreadcrumbList schema
|
||||
*/
|
||||
export function useBreadcrumbSchema(items) {
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.text,
|
||||
item: item.to ? `https://1814.love${item.to}` : undefined,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SiteNavigationElement schema
|
||||
*/
|
||||
export function useSiteNavigationSchema(navItems) {
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SiteNavigationElement',
|
||||
name: '呼籁旅行导航',
|
||||
hasPart: navItems.map(item => ({
|
||||
'@type': 'SiteNavigationElement',
|
||||
name: item.text,
|
||||
url: `https://1814.love${item.to}`,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ContactPage schema(增强版)
|
||||
*/
|
||||
export function useContactPageSchema(brandData, contactData) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ContactPage',
|
||||
name: '联系呼籁旅行',
|
||||
url: 'https://1814.love/contact',
|
||||
description: '呼籁旅行官方联系方式:微信客服、电话咨询、小程序预订。工作时间9:00-21:00,节假日不休。所有行程签订正规电子合同。',
|
||||
mainEntity: {
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://1814.love/#organization',
|
||||
name: brandData.name,
|
||||
url: brandData.url,
|
||||
},
|
||||
}
|
||||
|
||||
if (contactData) {
|
||||
const phone = contactData.channels.find(c => c.type === 'phone')
|
||||
const email = contactData.channels.find(c => c.type === 'email')
|
||||
if (phone) schema.mainEntity.telephone = phone.value
|
||||
if (email) schema.mainEntity.email = email.value
|
||||
schema.mainEntity.contactPoint = contactData.channels
|
||||
.filter(c => ['wechat', 'phone', 'email', 'complaint'].includes(c.type))
|
||||
.map(c => ({
|
||||
'@type': 'ContactPoint',
|
||||
contactType: c.label,
|
||||
description: c.description,
|
||||
...(c.type === 'phone' ? { telephone: c.value, availableLanguage: 'Chinese' } : {}),
|
||||
...(c.type === 'email' ? { email: c.value } : {}),
|
||||
}))
|
||||
}
|
||||
|
||||
injectJsonLd(schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* 资质荣誉页 Organization + WebPage schema
|
||||
*/
|
||||
export function useQualificationsPageSchema(brandData, qualData) {
|
||||
if (!brandData) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': ['Organization', 'TravelAgency'],
|
||||
'@id': 'https://1814.love/#organization',
|
||||
name: brandData.name,
|
||||
legalName: brandData.fullName,
|
||||
foundingDate: '2015',
|
||||
url: 'https://1814.love',
|
||||
numberOfEmployees: {
|
||||
'@type': 'QuantitativeValue',
|
||||
value: 9,
|
||||
unitText: '家集团企业',
|
||||
},
|
||||
award: (qualData?.awards || []).map(a => a.title),
|
||||
hasCredential: (qualData?.licenses || []).map(l => ({
|
||||
'@type': 'EducationalOccupationalCredential',
|
||||
name: l.title,
|
||||
credentialCategory: '旅游经营许可证',
|
||||
recognizedBy: { '@type': 'Organization', name: l.issuer },
|
||||
about: { '@type': 'Organization', name: l.holder },
|
||||
})),
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: '呼伦贝尔市海拉尔区',
|
||||
addressRegion: '内蒙古自治区',
|
||||
addressCountry: 'CN',
|
||||
},
|
||||
})
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://1814.love/qualifications',
|
||||
name: '呼籁旅行资质与荣誉',
|
||||
url: 'https://1814.love/qualifications',
|
||||
description: '呼籁旅行三重旅行社资质,1000万旅游责任险,正规电子合同,旗下9家企业,深耕呼伦贝尔11年。',
|
||||
about: { '@id': 'https://1814.love/#organization' },
|
||||
publisher: { '@id': 'https://1814.love/#organization' },
|
||||
dateModified: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Person schema - 创始人/核心人物信息
|
||||
*/
|
||||
export function usePersonSchema(person) {
|
||||
if (!person) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: person.name,
|
||||
jobTitle: person.title,
|
||||
description: person.background,
|
||||
knowsAbout: person.expertise,
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: '呼籁旅行',
|
||||
url: 'https://1814.love',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* CustomerStory schema - 客户故事
|
||||
*/
|
||||
export function useCustomerStorySchema(story) {
|
||||
if (!story) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: story.title,
|
||||
description: story.summary,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: story.authorName || story.familyType,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: '呼籁旅行',
|
||||
url: 'https://1814.love',
|
||||
},
|
||||
url: `https://1814.love/stories/${story.id}`,
|
||||
datePublished: story.travelDate ? story.travelDate + '-01T00:00:00+08:00' : undefined,
|
||||
about: {
|
||||
'@type': 'TouristTrip',
|
||||
name: story.tripProduct,
|
||||
touristType: story.familyType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ProductsList schema - 产品列表页
|
||||
*/
|
||||
export function useProductsListSchema(products) {
|
||||
if (!products) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
name: '呼籁旅行产品列表',
|
||||
url: 'https://1814.love/products',
|
||||
itemListElement: (products || []).map((p, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
item: {
|
||||
'@type': 'TouristTrip',
|
||||
name: p.name || p.title,
|
||||
description: p.subtitle || p.description,
|
||||
url: `https://1814.love/products/${p.id}`,
|
||||
},
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* StoriesList schema - 客户故事列表页
|
||||
*/
|
||||
export function useStoriesListSchema(stories) {
|
||||
if (!stories) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
name: '呼籁旅行客户故事',
|
||||
url: 'https://1814.love/stories',
|
||||
itemListElement: (stories || []).map((s, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
item: {
|
||||
'@type': 'Article',
|
||||
headline: s.title,
|
||||
url: `https://1814.love/stories/${s.id}`,
|
||||
},
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pricing schema - 价格透明页
|
||||
*/
|
||||
export function usePricingSchema(pricingData) {
|
||||
if (!pricingData) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: '呼籁旅行价格说明',
|
||||
url: 'https://1814.love/pricing',
|
||||
description: '呼籁旅行产品价格透明说明',
|
||||
publisher: { '@id': 'https://1814.love/#organization' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gallery schema - 图库页
|
||||
*/
|
||||
export function useGallerySchema(galleryData) {
|
||||
if (!galleryData) return
|
||||
|
||||
injectJsonLd({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ImageGallery',
|
||||
name: '呼籁旅拍',
|
||||
url: 'https://1814.love/gallery',
|
||||
description: '呼伦贝尔草原旅行实景图库',
|
||||
publisher: { '@id': 'https://1814.love/#organization' },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 从服务端 API 获取公开数据
|
||||
*
|
||||
* 用法:
|
||||
* const { data: brand } = await usePublicApi('brand')
|
||||
* const { data: faq } = await usePublicApi('faq')
|
||||
*
|
||||
* 数据来源:GET /api/content/[key] → site_content 表
|
||||
*
|
||||
* SSR 首屏:服务端调 API,数据嵌入 HTML(SEO 友好)
|
||||
* 客户端导航:浏览器直接调 /api/content/[key](Network 可见)
|
||||
*/
|
||||
|
||||
export function usePublicApi(endpoint, options = {}) {
|
||||
return useFetch(`/api/content/${endpoint}`, {
|
||||
key: `public-${endpoint}`,
|
||||
// 客户端导航时重新请求接口,不使用 payload 缓存
|
||||
getCachedData: () => undefined,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 页面级 SEO 元数据注入
|
||||
* 包含:传统搜索引擎 + AI 大模型优化
|
||||
*
|
||||
* 从 API 获取 SEO 配置数据(替代原来的静态 JSON 导入)
|
||||
* 使用 useHead 的响应式能力,在数据到达后自动更新 meta 标签
|
||||
*/
|
||||
export default function useSEO(pageKey, overrides = {}) {
|
||||
// 从本地 JSON 导入 SEO 数据
|
||||
const { data: seoData } = usePublicApi('seo')
|
||||
|
||||
const url = `https://1814.love${useRoute().path}`
|
||||
|
||||
// 每页关键词映射 - 给 AI 爬虫明确的关键信息
|
||||
const keywordsMap = {
|
||||
home: '呼伦贝尔亲子游,呼伦贝尔家庭定制游,呼籁旅行,一家一单一车,呼伦贝尔旅游,草原定制游,额吉的故乡',
|
||||
about: '呼籁旅行,呼伦贝尔旅行社,自有营地,自有车队,呼籁文旅集团,深耕呼伦贝尔11年',
|
||||
products: '呼伦贝尔定制游,额吉的故乡V9,呼伦贝尔几天合适,亲子游产品,5天4晚,6天5晚,7天6晚',
|
||||
faq: '呼伦贝尔旅游攻略,呼伦贝尔亲子游攻略,呼伦贝尔多少钱,呼伦贝尔几月去好,呼伦贝尔穿什么',
|
||||
reviews: '呼伦贝尔旅游评价,呼籁旅行怎么样,呼伦贝尔亲子游评价,呼伦贝尔定制游口碑',
|
||||
guides: '呼伦贝尔穿搭指南,呼伦贝尔行李清单,呼伦贝尔几月份去最好,呼伦贝尔骑马安全,草原旅游注意事项',
|
||||
'summer-camp': '呼伦贝尔夏令营,草原亲子夏令营,小蒙马夏令营,呼伦贝尔亲子营,研学旅行',
|
||||
qualifications: '呼籁旅行资质,旅行社经营许可证,旅游责任险,呼伦贝尔旅行社认证,呼籁荣誉',
|
||||
news: '呼籁旅行动态,呼伦贝尔旅游资讯,呼籁旅行公告',
|
||||
contact: '呼籁旅行联系方式,呼伦贝尔定制游咨询,呼籁旅行微信,呼伦贝尔旅游电话',
|
||||
youji: '客户游记,呼伦贝尔游记,呼籁旅行评价,美篇游记,旅行故事,呼伦贝尔亲子游记,客户旅行分享',
|
||||
}
|
||||
|
||||
// 响应式配置:当 seoData 从 API 返回后自动更新
|
||||
const pageConfig = computed(() => seoData.value?.pages?.[pageKey] || {})
|
||||
const mergedConfig = computed(() => ({ ...pageConfig.value, ...overrides }))
|
||||
const ogImage = computed(() => mergedConfig.value.ogImage ? `https://1814.love${mergedConfig.value.ogImage}` : '')
|
||||
|
||||
// useHead 支持响应式参数,数据到达后自动更新 <head>
|
||||
useHead({
|
||||
title: computed(() => mergedConfig.value.title),
|
||||
meta: computed(() => {
|
||||
const c = mergedConfig.value
|
||||
const result = [
|
||||
{ name: 'description', content: c.description },
|
||||
{ name: 'keywords', content: keywordsMap[pageKey] || '呼伦贝尔旅游,呼籁旅行' },
|
||||
{ name: 'author', content: '呼籁旅行' },
|
||||
// 发布和更新时间 - AI 判断内容新鲜度的依据
|
||||
{ name: 'article:published_time', content: '2025-01-01T00:00:00+08:00' },
|
||||
{ name: 'article:modified_time', content: new Date().toISOString().split('T')[0] + 'T00:00:00+08:00' },
|
||||
// 地理位置 - 让 AI 知道这是呼伦贝尔的本地服务
|
||||
{ name: 'geo.region', content: 'CN-NM' },
|
||||
{ name: 'geo.placename', content: '呼伦贝尔' },
|
||||
{ name: 'geo.position', content: '49.2143;119.7674' },
|
||||
{ name: 'ICBM', content: '49.2143, 119.7674' },
|
||||
// Open Graph(增强)
|
||||
{ property: 'og:title', content: c.title },
|
||||
{ property: 'og:description', content: c.description },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: url },
|
||||
{ property: 'og:site_name', content: '呼籁旅行' },
|
||||
{ property: 'og:locale', content: 'zh_CN' },
|
||||
// Twitter Card
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: c.title },
|
||||
{ name: 'twitter:description', content: c.description },
|
||||
]
|
||||
|
||||
// OG 图片
|
||||
if (ogImage.value) {
|
||||
result.push(
|
||||
{ property: 'og:image', content: ogImage.value },
|
||||
{ property: 'og:image:width', content: '1200' },
|
||||
{ property: 'og:image:height', content: '630' },
|
||||
{ name: 'twitter:image', content: ogImage.value },
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}),
|
||||
link: [
|
||||
{ rel: 'canonical', href: url },
|
||||
],
|
||||
})
|
||||
|
||||
const h1 = computed(() => mergedConfig.value.h1 || '')
|
||||
const seoTitle = computed(() => mergedConfig.value.title || '')
|
||||
const seoDescription = computed(() => mergedConfig.value.description || '')
|
||||
|
||||
return reactive({
|
||||
h1,
|
||||
title: seoTitle,
|
||||
description: seoDescription,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 滚动入场动画 composable
|
||||
* 元素进入视口时触发淡入上滑动画
|
||||
* 自动尊重 prefers-reduced-motion
|
||||
*/
|
||||
export default function useScrollReveal() {
|
||||
onMounted(() => {
|
||||
// 尊重用户减少动效偏好
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible')
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
)
|
||||
|
||||
document.querySelectorAll('.reveal').forEach((el) => {
|
||||
observer.observe(el)
|
||||
})
|
||||
|
||||
onUnmounted(() => observer.disconnect())
|
||||
})
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户