2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
634 lines
17 KiB
TypeScript
634 lines
17 KiB
TypeScript
/**
|
||
* 统一 API 响应封装
|
||
*/
|
||
export type ApiEnvelope<T = unknown> = {
|
||
code?: number
|
||
message?: string
|
||
msg?: string
|
||
data?: T
|
||
} & Record<string, unknown>
|
||
|
||
/**
|
||
* 分页结果(CMS API 标准格式)
|
||
*/
|
||
export interface PageResult<T = unknown> {
|
||
list?: T[]
|
||
count?: number
|
||
total?: number
|
||
page?: number
|
||
limit?: number
|
||
pages?: number
|
||
}
|
||
|
||
/**
|
||
* 分页参数
|
||
*/
|
||
export interface PageParam {
|
||
page?: number
|
||
limit?: number
|
||
}
|
||
|
||
// ==================== 轮播图 ====================
|
||
|
||
/**
|
||
* CMS 轮播图(/cms/cms-banner/page 返回项)
|
||
*
|
||
* 字段命名以 CMS 实际返回为准,这里用宽松的 unknown 兜底,
|
||
* 避免后端字段名微调(image / imageUrl / pic / url)导致取不到图。
|
||
* 前端统一经 ensureFullUrl() 处理图片地址(OSS 直连或 /api/file/ 代理)。
|
||
*/
|
||
export interface CmsBanner {
|
||
id?: number
|
||
/** 轮播图地址(CMS 可能用 image / imageUrl / pic / url 任一字段名) */
|
||
image?: string
|
||
imageUrl?: string
|
||
pic?: string
|
||
url?: string
|
||
/** 标题 / 副标题(可选,部分模板会叠加在图上) */
|
||
title?: string
|
||
subtitle?: string
|
||
/** 点击跳转链接(可选) */
|
||
link?: string
|
||
/** 排序(升序)—— 上游字段名为 sortNum */
|
||
sortNum?: number
|
||
/** 排序(升序,兼容旧字段名) */
|
||
sortOrder?: number
|
||
/** 状态:banner 表约定 1=启用/已发布、2=下架(与 article/product/case 的 0=发布 相反,前端按「非 2 即显示」处理) */
|
||
status?: number
|
||
/** 逻辑删除标记:1=已删除 */
|
||
deleted?: number
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 站点信息 ====================
|
||
|
||
/**
|
||
* CMS getSiteInfo 返回的完整站点信息
|
||
*/
|
||
export interface CmsSiteInfo {
|
||
websiteId?: number
|
||
websiteName?: string
|
||
websiteCode?: string
|
||
websiteIcon?: string
|
||
websiteLogo?: string
|
||
websiteDarkLogo?: string
|
||
websiteType?: string
|
||
/**
|
||
* 模板主键(cms_website.template_id → app_template.id)
|
||
* ⚠️ 主键存在跳号风险(历史上 id=8 被跳过),不可直接补零当目录名,请优先用 templateCode。
|
||
*/
|
||
templateId?: number
|
||
/**
|
||
* 模板标识(app_template.code,形如 'template-08'),与前端 app/templates/ 目录一一对应。
|
||
* 上游 getSiteInfo 若未返回,由 SSR 侧 server/utils/template-map.ts 反查模板表回填。
|
||
*/
|
||
templateCode?: string
|
||
domain?: string
|
||
keywords?: string
|
||
/** 品牌标语 / Slogan(后台「网站设置」可配置) */
|
||
slogan?: string
|
||
phone?: string
|
||
email?: string
|
||
address?: string
|
||
icpNo?: string
|
||
policeNo?: string
|
||
content?: string | null
|
||
comments?: string
|
||
prefix?: string
|
||
status?: number
|
||
running?: number
|
||
expirationTime?: string
|
||
tenantId?: number
|
||
topNavs?: CmsNavigation[]
|
||
bottomNavs?: CmsNavigation[]
|
||
slide?: unknown
|
||
ads?: unknown
|
||
layout?: unknown
|
||
/** 社交外部链接(后台「网站设置」录入;标准字段名为 socialLinks,可为数组或 JSON 字符串) */
|
||
socialLinks?: SocialLink[] | unknown
|
||
/** @deprecated 兼容旧字段名,优先使用 socialLinks */
|
||
links?: SocialLink[] | unknown
|
||
/** 二维码(顶层字段,通常为 null;微信公众号二维码优先取 config.wxQrcode) */
|
||
qrCode?: string | null
|
||
config?: SiteConfig
|
||
setting?: SiteSetting
|
||
serverTime?: ServerTime
|
||
}
|
||
|
||
/**
|
||
* 站点配置(来自 config 字段)
|
||
*/
|
||
export interface SiteConfig {
|
||
icpNo?: string
|
||
copyright?: string
|
||
SysDomain?: string
|
||
Domain?: string
|
||
address?: string
|
||
tel?: string
|
||
email?: string
|
||
wxQrcode?: string
|
||
wxQrcodeText?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/**
|
||
* 站点设置
|
||
*/
|
||
export interface SiteSetting {
|
||
id?: number
|
||
websiteId?: number
|
||
official?: boolean
|
||
market?: boolean
|
||
search?: boolean
|
||
share?: boolean
|
||
articleReview?: boolean
|
||
plugin?: boolean
|
||
editor?: number
|
||
searchBtn?: boolean
|
||
loginBtn?: boolean
|
||
floatTool?: boolean
|
||
copyrightLink?: boolean
|
||
maxMenuNum?: boolean
|
||
sortNumber?: number
|
||
deleted?: number
|
||
tenantId?: number
|
||
/**
|
||
* 在线留言是否要求滑块验证码。
|
||
* true=必须完成滑块验证才能提交;未设置/false=不显示滑块、直接提交。
|
||
* 由后台「网站设置」下发,配合前端 ContactForm 与后端 submit 校验。
|
||
*/
|
||
requireCaptcha?: boolean
|
||
/**
|
||
* 首页「优势模块」配置(JSON 字符串,存于 cms_website_setting.features)
|
||
* 结构见 FeatureSectionSetting;未配置(null/空)时前端回退模板默认数据。
|
||
*/
|
||
features?: string
|
||
}
|
||
|
||
/**
|
||
* 首页优势模块 - 单卡片
|
||
*
|
||
* icon 为图标名(对应前端 FeatureIcon 的 36 个通用图标之一,如 building / box / case / message)。
|
||
* 后台只存名字,不存 SVG,避免样式耦合。
|
||
*/
|
||
export interface FeatureItem {
|
||
/** 卡片标题 */
|
||
title: string
|
||
/** 卡片描述 */
|
||
desc: string
|
||
/** 图标名(FeatureIcon 组件支持的名称集合) */
|
||
icon: string
|
||
}
|
||
|
||
/**
|
||
* 首页单区块开关(预留位共用)
|
||
*/
|
||
export interface HomeBlockToggle {
|
||
/** 是否显示该区块;默认 true */
|
||
enabled?: boolean
|
||
}
|
||
|
||
/**
|
||
* 首页首屏特性卡片区块
|
||
*/
|
||
export interface HeroBlock extends HomeBlockToggle {
|
||
/** 首屏右侧特性卡片文案(纯文本,最多 4 条) */
|
||
features?: string[]
|
||
}
|
||
|
||
/**
|
||
* 首页优势模块区块
|
||
*/
|
||
export interface AdvantagesBlock extends HomeBlockToggle {
|
||
/** 模块标题,如「我们的优势」 */
|
||
title?: string
|
||
/** 模块副标题 */
|
||
subtitle?: string
|
||
/** 卡片列表,最多 4 个 */
|
||
items?: FeatureItem[]
|
||
}
|
||
|
||
/**
|
||
* 首页各区块统一开关集合
|
||
*/
|
||
export interface HomeBlocks {
|
||
/** 首屏特性卡片 */
|
||
hero?: HeroBlock
|
||
/** 我们的优势 */
|
||
advantages?: AdvantagesBlock
|
||
/** 产品展示(预留位) */
|
||
products?: HomeBlockToggle
|
||
/** 案例展示(预留位) */
|
||
cases?: HomeBlockToggle
|
||
/** 新闻动态(预留位) */
|
||
news?: HomeBlockToggle
|
||
/** Banner 轮播 */
|
||
banner?: HomeBlockToggle
|
||
/** CTA 联系 */
|
||
cta?: HomeBlockToggle
|
||
}
|
||
|
||
/**
|
||
* 首页优势模块 - 完整配置
|
||
*
|
||
* 存于 cms_website_setting.features(JSON 字符串)。
|
||
* 前端 useFeatures() 解析:未配置 → 回退模板默认 4 项;enabled=false → 隐藏整块。
|
||
*
|
||
* 2026-08-05 后台新增「首页区块总览」,支持区块化新结构:
|
||
* { hero, advantages, products, cases, news, banner, cta }
|
||
* 同时兼容旧结构(顶层 enabled/title/subtitle/items/heroFeatures)。
|
||
*/
|
||
export interface FeatureSectionSetting extends HomeBlocks {
|
||
/** 旧结构:是否显示整块(默认 true) */
|
||
enabled?: boolean
|
||
/** 旧结构:模块标题 */
|
||
title?: string
|
||
/** 旧结构:模块副标题 */
|
||
subtitle?: string
|
||
/** 旧结构:卡片列表 */
|
||
items?: FeatureItem[]
|
||
/** 旧结构:Hero 首屏右侧特性卡片文案 */
|
||
heroFeatures?: string[]
|
||
}
|
||
|
||
/**
|
||
* 服务器时间
|
||
*/
|
||
export interface ServerTime {
|
||
now?: string
|
||
today?: string
|
||
tomorrow?: string
|
||
afterDay?: string
|
||
week?: number
|
||
nextWeek?: string
|
||
timestamp?: number
|
||
}
|
||
|
||
// ==================== 导航 ====================
|
||
|
||
/**
|
||
* CMS 导航项(来自 topNavs / bottomNavs)
|
||
*/
|
||
export interface CmsNavigation {
|
||
navigationId?: number
|
||
type?: number
|
||
title?: string
|
||
parentId?: number
|
||
/** 栏目背景图(后台「栏目管理」上传,存 cms_navigation.background 列) */
|
||
background?: string
|
||
/** 模型类型: index | page | article | product */
|
||
model?: string
|
||
code?: string
|
||
/** 跳转路径/路由(CMS 原字段) */
|
||
path?: string
|
||
/** 链接地址(自定义 URL,优先级高于 path,用于导航跳转) */
|
||
url?: string
|
||
component?: string
|
||
suffix?: string | null
|
||
target?: string
|
||
icon?: string | null
|
||
banner?: string | null
|
||
mpBanner?: string | null
|
||
color?: string | null
|
||
hide?: number
|
||
permission?: number
|
||
password?: string
|
||
position?: number
|
||
top?: number
|
||
bottom?: number
|
||
active?: unknown
|
||
meta?: unknown
|
||
style?: unknown
|
||
sortNumber?: number
|
||
home?: number
|
||
recommend?: boolean
|
||
comments?: string
|
||
deleted?: number
|
||
status?: number
|
||
tenantId?: number
|
||
pageId?: number
|
||
/** 绑定单页的访问路径 slug(VO 透传,来自 cms_page.path);存在时 getNavLink 优先跳 /page/{slug} */
|
||
pagePath?: string
|
||
/** 绑定单页的标题(VO 透传,来自 cms_page.title) */
|
||
pageTitle?: string
|
||
/** 绑定单页的状态(VO 透传,来自 cms_page.status) */
|
||
pageStatus?: number
|
||
itemId?: number
|
||
isMpWeixin?: boolean
|
||
gutter?: number
|
||
span?: number
|
||
readNum?: number
|
||
userId?: number
|
||
merchantId?: number
|
||
lang?: string
|
||
langCategoryId?: number
|
||
/** 子导航 */
|
||
children?: CmsNavigation[]
|
||
/** 分类名称 */
|
||
categoryName?: string
|
||
/** 分类路径 */
|
||
categoryPath?: string
|
||
photo?: string | null
|
||
text?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 文章 ====================
|
||
|
||
/**
|
||
* CMS 文章
|
||
*/
|
||
export interface Article {
|
||
id?: number
|
||
articleId?: number
|
||
title?: string
|
||
summary?: string
|
||
content?: string
|
||
/** 文章附件(结构同 PageAttachment:name/url/size/ext/sort) */
|
||
files?: PageAttachment[]
|
||
/** 文章封面图(CMS 实际返回字段,优先级最高) */
|
||
image?: string
|
||
cover?: string
|
||
photo?: string
|
||
author?: string
|
||
source?: string
|
||
categoryId?: number
|
||
categoryName?: string
|
||
navigationId?: number
|
||
tags?: string[]
|
||
viewCount?: number
|
||
readNum?: number
|
||
sortNumber?: number
|
||
status?: number
|
||
/** 推荐标识:1=推荐(后台勾选「推荐」)。上游 cms-article 实际返回字段为 recommend。 */
|
||
recommend?: number
|
||
/** 置顶标识:>0 为置顶(部分租户文章表含 top 字段;上游未普遍支持时以 recommend 为准) */
|
||
top?: number
|
||
publishTime?: string
|
||
createTime?: string
|
||
updateTime?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 产品 ====================
|
||
|
||
/**
|
||
* CMS 产品
|
||
*/
|
||
export interface Product {
|
||
id?: number
|
||
productId?: number
|
||
productName?: string
|
||
subtitle?: string
|
||
description?: string
|
||
content?: string
|
||
cover?: string
|
||
photo?: string
|
||
images?: string[]
|
||
categoryId?: number
|
||
categoryName?: string
|
||
tags?: string[]
|
||
price?: string
|
||
sortNumber?: number
|
||
status?: number
|
||
/** 置顶标识:>0 为置顶(首页明星产品据此过滤) */
|
||
top?: number
|
||
/** 推荐标识:1=推荐(部分 CMS 版本产品表含 recommend 字段,与 top 并存) */
|
||
recommend?: number
|
||
createTime?: string
|
||
updateTime?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 案例 ====================
|
||
|
||
/**
|
||
* CMS 案例
|
||
*/
|
||
export interface CaseItem {
|
||
id?: number
|
||
title?: string
|
||
summary?: string
|
||
content?: string
|
||
cover?: string
|
||
images?: string[]
|
||
categoryId?: number
|
||
categoryName?: string
|
||
clientName?: string
|
||
projectTime?: string
|
||
tags?: string[]
|
||
sortNumber?: number
|
||
status?: number
|
||
/** 置顶标识:>0 为置顶(当前上游 cms-case 表暂无此字段,预留以便后续版本自动生效) */
|
||
top?: number
|
||
/** 推荐标识:1=推荐(当前上游 cms-case 表暂无此字段,预留以便后续版本自动生效) */
|
||
recommend?: number
|
||
createTime?: string
|
||
updateTime?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 单页详情(/api/page/detail) ====================
|
||
|
||
/**
|
||
* 单页详情返回结构
|
||
*
|
||
* 代理层 server/api/page/detail.get.ts 有两个数据来源,返回结构统一:
|
||
* 1. ?navigationId=4296 → 上游 /cms/cms-navigation/{id},把节点内嵌的 design 扁平化
|
||
* 2. ?path=about → 上游 /cms/cms-page/getByPath/{slug},即「单页管理」cms_page 表
|
||
*
|
||
* 因此 navigationId(来源1专有)与 pageId/path/keywords/description(来源2专有)
|
||
* 均为可选字段,使用前先判空。前端用 status 区分三种状态:
|
||
* - 'ok' :记录存在且 content 非空,渲染正文
|
||
* - 'empty' :记录不存在,或记录存在但正文未录入
|
||
* - 'error' :上游接口异常
|
||
*/
|
||
export interface PageDetail {
|
||
status: 'ok' | 'empty' | 'error'
|
||
/** 导航节点 ID(仅 navigationId 入口返回) */
|
||
navigationId?: number
|
||
/** 单页 ID(仅 cms_page 入口返回) */
|
||
pageId?: number
|
||
/** 单页路径 slug,如 about / contact(仅 cms_page 入口返回) */
|
||
path?: string
|
||
title: string
|
||
model: string
|
||
parentId: number
|
||
/** 正文 HTML(来源1为 design.content,来源2为 cms_page.content) */
|
||
content: string
|
||
/** 结构化设计(来自 design.layout,预留) */
|
||
layout: unknown
|
||
/** 头图(来源1为 design.photo,来源2为 cms_page.image) */
|
||
photo: string | null
|
||
/** SEO 关键词(仅 cms_page 入口返回) */
|
||
keywords?: string
|
||
/** SEO 描述(仅 cms_page 入口返回) */
|
||
description?: string
|
||
hasContent: boolean
|
||
updateTime: string | null
|
||
/** 页面附件列表(仅 cms_page 入口返回,来自 cms_page.attachments JSON) */
|
||
attachments?: PageAttachment[]
|
||
/** empty / error 时的说明 */
|
||
message?: string
|
||
}
|
||
|
||
/**
|
||
* 单页附件(来自后台「单页管理」cms_page.attachments)
|
||
*
|
||
* 与 website-admin 上传组件写入结构保持一致:
|
||
* { name, url, size, ext, sort }
|
||
*/
|
||
export interface PageAttachment {
|
||
/** 原始文件名(含扩展名),用于展示与下载文件名 */
|
||
name: string
|
||
/** 文件直链(MinIO / OSS 全 URL) */
|
||
url: string
|
||
/** 文件大小(字节) */
|
||
size?: number
|
||
/** 扩展名(小写,不含点),如 xlsx / pdf */
|
||
ext?: string
|
||
/** 排序(升序) */
|
||
sort?: number
|
||
}
|
||
|
||
// ==================== 兼容旧类型 ====================
|
||
|
||
/**
|
||
* 站点信息(兼容层,映射到 CmsSiteInfo)
|
||
* @deprecated 使用 CmsSiteInfo
|
||
*/
|
||
export type SiteInfo = CmsSiteInfo
|
||
|
||
/**
|
||
* CMS 页面(兼容旧代码)
|
||
*/
|
||
export interface CmsPage {
|
||
id?: number
|
||
siteId?: number
|
||
slug?: string
|
||
title?: string
|
||
description?: string
|
||
keywords?: string
|
||
type?: string
|
||
sortNumber?: number
|
||
showInNav?: boolean
|
||
navName?: string
|
||
content?: string
|
||
cover?: string
|
||
seoTitle?: string
|
||
seoDescription?: string
|
||
createTime?: string
|
||
updateTime?: string
|
||
}
|
||
|
||
// ==================== 订阅与域名 ====================
|
||
|
||
/**
|
||
* 应用订阅状态
|
||
*/
|
||
export interface SubscriptionStatus {
|
||
subscribed?: boolean
|
||
status?: string
|
||
expireTime?: string
|
||
startTime?: string
|
||
expired?: boolean
|
||
subscriptionId?: number
|
||
productId?: number
|
||
productName?: string
|
||
}
|
||
|
||
/**
|
||
* 域名解析结果
|
||
* 后端改造后可直接返回完整 AppProduct
|
||
*/
|
||
export interface DomainResolveResult {
|
||
tenantId?: string
|
||
appId?: string
|
||
templateId?: string
|
||
found?: boolean
|
||
/** 后端可选返回的完整应用产品信息 */
|
||
appProduct?: AppProduct
|
||
}
|
||
|
||
// ==================== 应用产品 ====================
|
||
|
||
/**
|
||
* 应用产品信息(app_product 表)
|
||
* 后端库位于 https://websopy-api.websoft.top,表前缀 app_
|
||
*/
|
||
export interface AppProduct {
|
||
productId?: number
|
||
productName?: string
|
||
/** 应用唯一标识码 */
|
||
productCode?: string
|
||
/** 应用类型:1=网站 等 */
|
||
appType?: number
|
||
/** 绑定域名(核心字段) */
|
||
domain?: string
|
||
/** 域名前缀 */
|
||
prefix?: string
|
||
/** 前台访问 URL */
|
||
homeUrl?: string
|
||
/** 后台管理 URL */
|
||
adminUrl?: string
|
||
/** 应用图标 */
|
||
icon?: string
|
||
/** 应用 Logo */
|
||
logo?: string
|
||
/**
|
||
* 关联模板主键(app_template.id),也可能已是字符串 "template-XX"。
|
||
* ⚠️ 主键存在跳号风险,解析目录名请优先用 templateCode,本字段仅兜底。
|
||
*/
|
||
templateId?: number | string
|
||
/** 关联模板标识(app_template.code,形如 'template-08'),与前端模板目录一一对应 */
|
||
templateCode?: string
|
||
status?: number
|
||
running?: number
|
||
expirationTime?: string
|
||
tenantId?: number
|
||
createTime?: string
|
||
updateTime?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/**
|
||
* 应用域名绑定(app_domain 表)
|
||
* 自定义域名 → 租户/应用的绑定关系,作为「app_product.domain」之外的兜底解析来源
|
||
*/
|
||
export interface AppDomain {
|
||
/** 绑定域名 */
|
||
domain?: string
|
||
/** 关联租户 ID(核心字段,用于下游接口 TenantId 头) */
|
||
tenantId?: number
|
||
/** 关联应用产品 ID(用于回查完整 AppProduct 取模板/品牌) */
|
||
productId?: number
|
||
/** 关联模板 ID(可选,优先以 AppProduct 为准) */
|
||
templateId?: number | string
|
||
status?: number
|
||
[key: string]: unknown
|
||
}
|
||
|
||
// ==================== 租户上下文 ====================
|
||
|
||
/**
|
||
* 租户上下文(Server Middleware 识别结果)
|
||
*/
|
||
export interface TenantContext {
|
||
tenantId: string
|
||
appId: string
|
||
templateId?: string
|
||
source?: 'domain' | 'subdomain' | 'env' | 'header'
|
||
host?: string
|
||
/** 应用产品信息(中间件查询后填充) */
|
||
appProduct?: AppProduct
|
||
}
|
||
|
||
// ==================== 社交链接 ====================
|
||
|
||
export interface SocialLink {
|
||
type?: string
|
||
name?: string
|
||
url?: string
|
||
icon?: string
|
||
}
|