2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
324 lines
7.9 KiB
Vue
324 lines
7.9 KiB
Vue
<template>
|
||
<!--
|
||
关于我们通用组件(首页区块)
|
||
|
||
数据源:cms_page 表 path=about 的单页记录
|
||
链路:/api/page/detail?path=about → server/api/page/detail.get.ts → 上游 /cms/cms-page/getByPath/about
|
||
|
||
布局:左侧(标题 + 摘要 + 了解更多链接)| 右侧(配图)
|
||
无数据时自动隐藏(v-if="ready"),不影响首页其他区块
|
||
-->
|
||
<section
|
||
v-if="ready"
|
||
class="about-section"
|
||
:class="[customClass, { 'about-section--reversed': reversed }]"
|
||
:style="rootStyle"
|
||
>
|
||
<div :class="containerClass">
|
||
<!-- 左:文字区 -->
|
||
<div class="about-section__text">
|
||
<h2 v-if="displayTitle" class="about-section__title" :style="{ color: titleColor }">
|
||
{{ displayTitle }}
|
||
</h2>
|
||
<div
|
||
v-if="titleUnderline"
|
||
class="about-section__underline"
|
||
:style="{ backgroundColor: accentColor }"
|
||
/>
|
||
<p v-if="summaryText" class="about-section__summary" :style="{ color: summaryColor }">
|
||
{{ summaryText }}
|
||
</p>
|
||
<NuxtLink
|
||
v-if="aboutLink"
|
||
:to="aboutLink"
|
||
class="about-section__more"
|
||
:style="{ color: linkColor }"
|
||
>
|
||
{{ moreText }}
|
||
<svg
|
||
class="about-section__arrow"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M9 5l7 7-7 7"
|
||
/>
|
||
</svg>
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<!-- 右:图片区 -->
|
||
<div class="about-section__image-wrap" :style="imageWrapStyle">
|
||
<img
|
||
v-if="imageUrl"
|
||
:src="imageUrl"
|
||
:alt="displayTitle"
|
||
class="about-section__image"
|
||
:style="imageStyle"
|
||
>
|
||
<div v-else class="about-section__image-placeholder" :style="placeholderStyle">
|
||
{{ placeholderText }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { PageDetail } from '~/types'
|
||
import { stripHtml } from '~/utils'
|
||
|
||
/**
|
||
* AboutSection — 首页「关于我们」通用组件
|
||
*
|
||
* 从 cms_page (path=about) 读取单页数据,左侧摘要 + 右侧配图。
|
||
* 各模板通过 props 定制颜色/圆角/容器等视觉细节。
|
||
*
|
||
* ## 使用方式
|
||
* ```vue
|
||
* <!-- 最简调用(使用全部默认值) -->
|
||
* <AboutSection />
|
||
*
|
||
* <!-- template-08 金棕风 -->
|
||
* <AboutSection
|
||
* title-color="var(--t8-text)"
|
||
* accent-color="var(--t8-primary)"
|
||
* summary-color="var(--t8-text-secondary)"
|
||
* link-color="var(--t8-primary)"
|
||
* container-class="container mx-auto px-4 sm:px-6 lg:px-8 grid lg:grid-cols-2 gap-12 items-center"
|
||
* />
|
||
* ```
|
||
*/
|
||
|
||
// ==================== Props ====================
|
||
|
||
const props = withDefaults(defineProps<{
|
||
/** 区块标题,空字符串时隐藏标题行 */
|
||
title?: string
|
||
/** 标题颜色 */
|
||
titleColor?: string
|
||
/** 装饰线 / 强调色(下划线、链接、占位背景) */
|
||
accentColor?: string
|
||
/** 摘要文字颜色 */
|
||
summaryColor?: string
|
||
/** "了解更多"链接文字颜色 */
|
||
linkColor?: string
|
||
/** "了解更多"文案 */
|
||
moreText?: string
|
||
/** 摘要最大字符数(stripHtml 后截断) */
|
||
maxChars?: number
|
||
/** 是否显示标题下方装饰线 */
|
||
titleUnderline?: boolean
|
||
/** 是否左右翻转(图片在左文字在右) */
|
||
reversed?: boolean
|
||
/** 外层容器额外 class */
|
||
customClass?: string
|
||
/** 内层网格容器 class(控制布局与间距) */
|
||
containerClass?: string
|
||
/** 区块背景色 */
|
||
bgColor?: string
|
||
/** 区块内边距 py / 上下 padding */
|
||
paddingY?: string
|
||
/** 图片容器圆角 */
|
||
imageRadius?: string
|
||
/** 图片容器背景色(无图时的占位背景) */
|
||
imageBgColor?: string
|
||
/** 无图时的占位文字 */
|
||
placeholderText?: string
|
||
/** 图片 object-fit */
|
||
imageObjectFit?: string
|
||
/** 图片高度 */
|
||
imageHeight?: string
|
||
}>(), {
|
||
title: '',
|
||
titleColor: '',
|
||
accentColor: '',
|
||
summaryColor: '',
|
||
linkColor: '',
|
||
moreText: '了解更多',
|
||
maxChars: 150,
|
||
titleUnderline: true,
|
||
reversed: false,
|
||
customClass: '',
|
||
containerClass: '',
|
||
bgColor: '#ffffff',
|
||
paddingY: 'py-16 lg:py-20',
|
||
imageRadius: 'rounded-2xl',
|
||
imageBgColor: '',
|
||
placeholderText: '',
|
||
imageObjectFit: 'object-contain',
|
||
imageHeight: '',
|
||
})
|
||
|
||
// ==================== 数据获取 ====================
|
||
|
||
const { siteInfo } = useSite()
|
||
const { fileUrl } = useFileUrl()
|
||
|
||
/** 单页详情(key 与 About.vue 一致,复用 SSR payload) */
|
||
const { data: pageData } = await useFetch<PageDetail>('/api/page/detail', {
|
||
key: 'page-about',
|
||
query: { path: 'about' },
|
||
})
|
||
|
||
// ==================== 计算属性 ====================
|
||
|
||
/** 是否有足够数据渲染 */
|
||
const ready = computed(() => {
|
||
const p = pageData.value
|
||
if (!p) return false
|
||
// ok/empty 都展示(empty 时用 description 或站点简介兜底)
|
||
return p.status === 'ok' || p.status === 'empty'
|
||
})
|
||
|
||
/** 显示标题 */
|
||
const displayTitle = computed(() => {
|
||
if (props.title) return props.title
|
||
return pageData.value?.title?.trim() || '关于我们'
|
||
})
|
||
|
||
/**
|
||
* 摘要文本
|
||
* 优先级:pageData.description > stripHtml(pageData.content) > siteInfo.comments
|
||
*/
|
||
const summaryText = computed(() => {
|
||
const p = pageData.value
|
||
if (!p) return ''
|
||
|
||
// 1. 单页 SEO 描述
|
||
const desc = (p.description || '').trim()
|
||
if (desc) return desc.slice(0, props.maxChars)
|
||
|
||
// 2. 正文截取
|
||
const content = (p.content || '').trim()
|
||
if (content) {
|
||
const text = stripHtml(content).slice(0, props.maxChars)
|
||
if (text) return text
|
||
}
|
||
|
||
// 3. 站点简介兜底
|
||
const comments = (siteInfo.value?.comments || '').trim()
|
||
return comments.slice(0, props.maxChars) || ''
|
||
})
|
||
|
||
/** 配图 URL(经 fileUrl 转换) */
|
||
const imageUrl = computed(() => {
|
||
const photo = pageData.value?.photo
|
||
if (!photo) return ''
|
||
return fileUrl(photo)
|
||
})
|
||
|
||
/** "了解更多"链接目标 */
|
||
const aboutLink = computed(() => '/about')
|
||
|
||
// ==================== 内联样式(props → style) ====================
|
||
|
||
const rootStyle = computed(() => ({
|
||
backgroundColor: props.bgColor || undefined,
|
||
paddingTop: undefined,
|
||
paddingBottom: undefined,
|
||
}))
|
||
|
||
const imageWrapStyle = computed(() => ({
|
||
borderRadius: undefined, // 由 class 控制
|
||
backgroundColor: props.imageBgColor || undefined,
|
||
minHeight: props.imageHeight || undefined,
|
||
}))
|
||
|
||
const imageStyle = computed(() => ({
|
||
objectFit: props.imageObjectFit as any || undefined,
|
||
maxHeight: props.imageHeight || undefined,
|
||
}))
|
||
|
||
const placeholderStyle = computed(() => ({
|
||
color: props.accentColor || undefined,
|
||
}))
|
||
</script>
|
||
|
||
<style scoped>
|
||
.about-section {
|
||
position: relative;
|
||
}
|
||
|
||
.about-section__text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.about-section__title {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.about-section__underline {
|
||
width: 4rem;
|
||
height: 4px;
|
||
border-radius: 9999px;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.about-section__summary {
|
||
font-size: 1rem;
|
||
line-height: 1.75;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.about-section__more {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
transition: opacity 0.2s;
|
||
width: fit-content;
|
||
}
|
||
|
||
.about-section__more:hover {
|
||
opacity: 0.75;
|
||
}
|
||
|
||
.about-section__arrow {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
margin-left: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.about-section__image-wrap {
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 16rem;
|
||
max-height: 20rem;
|
||
}
|
||
|
||
.about-section__image {
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.about-section__image-placeholder {
|
||
font-size: 0.9rem;
|
||
opacity: 0.45;
|
||
user-select: none;
|
||
}
|
||
|
||
/* 翻转布局 */
|
||
.about-section--reversed .about-section__text {
|
||
order: 2;
|
||
}
|
||
|
||
.about-section--reversed .about-section__image-wrap {
|
||
order: 1;
|
||
}
|
||
</style>
|