feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user