Files
hjc-web/app/components/AboutSection.vue
T
weicw-dshsa b88c9df3b3 chore(templates): 只保留 template-07,删除其余 9 套模板
汇吉采仅使用 template-07(后端绑定 + .env.development),其余
template-01~06、08~10 为多租户 SaaS 平台时代遗留,全部删除:

- 删除 app/templates/ 下 9 套模板目录及注册表内对应配置
- 删除 public/images/template-{01,03,09,10} 图片目录
- 删除 templates/cloud-website 下 9 张模板预览图
- 三处 template-01 兜底(nuxt.config / useTemplate / default.vue)改为 07
- 清理指向已删模板的注释,README 模板系统章节同步更新

模板动态加载机制保留:新增模板 = 建目录 + 在 app/templates/index.ts 注册
2026-09-30 07:38:05 +08:00

324 lines
8.0 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>
<!--
关于我们通用组件(首页区块)
数据源: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-07 科技蓝风(--t1-* 变量见 template-07/theme.css) -->
* <AboutSection
* title-color="var(--t1-text)"
* accent-color="var(--t1-primary)"
* summary-color="var(--t1-text-secondary)"
* link-color="var(--t1-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>