From 0f3987c123d59222eca5496bc538551b72fd7417 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com>
Date: Sun, 6 Jul 2025 13:34:48 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dts=E7=B1=BB=E5=9E=8B=E9=94=99?=
=?UTF-8?q?=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/CMS_AD_TYPE_FIX.md | 325 ++++++++++++++++++
src/api/cms/cmsAd/model/index.ts | 20 +-
src/api/cms/cmsArticle/index.ts | 2 +-
src/api/cms/cmsArticle/model/index.ts | 2 +-
src/api/cms/cmsDesign/model/index.ts | 2 +-
src/api/cms/cmsLink/index.ts | 2 +-
src/api/cms/cmsWebsite/model/index.ts | 2 +-
src/api/index.ts | 2 +-
src/api/layout/model/index.ts | 4 +-
src/api/system/cache/index.ts | 4 +-
src/api/system/cache/model/index.ts | 2 +-
src/api/system/chat/index.ts | 2 +-
src/api/system/chatMessage/model/index.ts | 4 +-
src/api/system/dict-data/model/index.ts | 2 +-
src/api/system/environment/index.ts | 14 -
src/api/system/file/model/index.ts | 2 +-
src/api/system/menu/index.ts | 6 +-
src/api/system/plug/index.ts | 4 +-
src/api/system/role/model/index.ts | 4 +-
src/api/system/setting/model/index.ts | 2 +-
src/api/system/user-group/model/index.ts | 2 +-
src/api/system/user/model/index.ts | 6 +-
src/api/system/userRole/model/index.ts | 4 +-
src/api/system/version/model/index.ts | 2 +-
src/components/layout/Container.tsx | 50 +++
src/components/sections/NavigationDisplay.tsx | 159 +++++++++
src/components/sections/SiteInfoDisplay.tsx | 113 ++++++
src/hooks/useSiteInfo.ts | 109 ++++++
src/stores/useSiteStore.ts | 218 ++++++++++++
29 files changed, 1021 insertions(+), 49 deletions(-)
create mode 100644 docs/CMS_AD_TYPE_FIX.md
create mode 100644 src/components/layout/Container.tsx
create mode 100644 src/components/sections/NavigationDisplay.tsx
create mode 100644 src/components/sections/SiteInfoDisplay.tsx
create mode 100644 src/hooks/useSiteInfo.ts
create mode 100644 src/stores/useSiteStore.ts
diff --git a/docs/CMS_AD_TYPE_FIX.md b/docs/CMS_AD_TYPE_FIX.md
new file mode 100644
index 0000000..98ffe14
--- /dev/null
+++ b/docs/CMS_AD_TYPE_FIX.md
@@ -0,0 +1,325 @@
+# CmsAd 类型修复说明
+
+## 📋 问题描述
+
+原始代码中 `CmsAd` 接口的 `images` 和 `imageList` 字段使用了 `any` 类型,违反了 TypeScript ESLint 规则:
+
+```typescript
+// ❌ 修复前 - 使用 any 类型
+export interface CmsAd {
+ images?: any; // Error: Unexpected any
+ imageList?: any; // Error: Unexpected any
+}
+```
+
+## 🔧 修复方案
+
+### 1. 新增 CmsAdImage 接口
+
+```typescript
+/**
+ * 广告图片信息
+ */
+export interface CmsAdImage {
+ id?: number; // 图片ID
+ url?: string; // 图片URL
+ alt?: string; // 图片alt属性
+ title?: string; // 图片标题
+ link?: string; // 图片链接
+ sortOrder?: number; // 排序顺序
+}
+```
+
+### 2. 更新 CmsAd 接口
+
+```typescript
+// ✅ 修复后 - 使用具体类型
+export interface CmsAd {
+ // 广告图片(单个图片或图片URL)
+ images?: string | CmsAdImage | CmsAdImage[];
+ // 广告图片列表
+ imageList?: CmsAdImage[];
+}
+```
+
+## 🎯 类型设计说明
+
+### images 字段类型
+```typescript
+images?: string | CmsAdImage | CmsAdImage[];
+```
+
+支持三种数据格式:
+- **`string`**: 单个图片URL字符串
+- **`CmsAdImage`**: 单个图片对象
+- **`CmsAdImage[]`**: 图片对象数组
+
+### imageList 字段类型
+```typescript
+imageList?: CmsAdImage[];
+```
+
+专门用于图片列表,只接受图片对象数组。
+
+## 📖 使用示例
+
+### 1. 基础用法
+
+```typescript
+import { CmsAd, CmsAdImage } from '@/api/cms/cmsAd/model';
+
+// 示例1:使用字符串URL
+const ad1: CmsAd = {
+ adId: 1,
+ name: '首页横幅',
+ images: 'https://example.com/banner.jpg'
+};
+
+// 示例2:使用单个图片对象
+const ad2: CmsAd = {
+ adId: 2,
+ name: '侧边栏广告',
+ images: {
+ id: 1,
+ url: 'https://example.com/sidebar.jpg',
+ alt: '侧边栏广告',
+ link: '/products'
+ }
+};
+
+// 示例3:使用图片数组
+const ad3: CmsAd = {
+ adId: 3,
+ name: '轮播广告',
+ images: [
+ {
+ id: 1,
+ url: 'https://example.com/slide1.jpg',
+ alt: '轮播图1',
+ link: '/page1'
+ },
+ {
+ id: 2,
+ url: 'https://example.com/slide2.jpg',
+ alt: '轮播图2',
+ link: '/page2'
+ }
+ ]
+};
+```
+
+### 2. 类型守卫函数
+
+```typescript
+// 检查是否为字符串URL
+function isImageUrl(images: CmsAd['images']): images is string {
+ return typeof images === 'string';
+}
+
+// 检查是否为单个图片对象
+function isSingleImage(images: CmsAd['images']): images is CmsAdImage {
+ return typeof images === 'object' && !Array.isArray(images) && images !== null;
+}
+
+// 检查是否为图片数组
+function isImageArray(images: CmsAd['images']): images is CmsAdImage[] {
+ return Array.isArray(images);
+}
+
+// 使用示例
+function renderAdImages(ad: CmsAd) {
+ if (isImageUrl(ad.images)) {
+ return
;
+ }
+
+ if (isSingleImage(ad.images)) {
+ return (
+
ad.images.link && window.open(ad.images.link)}
+ />
+ );
+ }
+
+ if (isImageArray(ad.images)) {
+ return (
+
+ {ad.images.map(img => (
+

img.link && window.open(img.link)}
+ />
+ ))}
+
+ );
+ }
+
+ return null;
+}
+```
+
+### 3. React 组件示例
+
+```typescript
+import React from 'react';
+import { CmsAd } from '@/api/cms/cmsAd/model';
+
+interface AdComponentProps {
+ ad: CmsAd;
+}
+
+const AdComponent: React.FC = ({ ad }) => {
+ const renderImages = () => {
+ if (!ad.images) return null;
+
+ // 处理字符串URL
+ if (typeof ad.images === 'string') {
+ return (
+
+ );
+ }
+
+ // 处理单个图片对象
+ if (!Array.isArray(ad.images)) {
+ return (
+
+
+
+ );
+ }
+
+ // 处理图片数组
+ return (
+
+ {ad.images.map((img, index) => (
+
+
+
+ ))}
+
+ );
+ };
+
+ return (
+
+ {renderImages()}
+ {ad.imageList && ad.imageList.length > 0 && (
+
+ {ad.imageList.map(img => (
+

+ ))}
+
+ )}
+
+ );
+};
+
+export default AdComponent;
+```
+
+## 🔍 类型检查优势
+
+### 1. 编译时类型安全
+```typescript
+// ✅ 正确用法
+const ad: CmsAd = {
+ images: 'https://example.com/image.jpg' // string
+};
+
+const ad2: CmsAd = {
+ images: { url: 'https://example.com/image.jpg' } // CmsAdImage
+};
+
+// ❌ 错误用法 - TypeScript 会报错
+const ad3: CmsAd = {
+ images: 123 // Error: Type 'number' is not assignable
+};
+```
+
+### 2. 智能代码提示
+```typescript
+const ad: CmsAd = {
+ images: {
+ // IDE 会提供智能提示:id, url, alt, title, link, sortOrder
+ }
+};
+```
+
+### 3. 重构安全性
+当修改 `CmsAdImage` 接口时,TypeScript 会自动检查所有使用该类型的代码,确保类型一致性。
+
+## 📈 性能优化建议
+
+### 1. 图片懒加载
+```typescript
+const LazyAdImage: React.FC<{ image: CmsAdImage }> = ({ image }) => {
+ return (
+
+ );
+};
+```
+
+### 2. 图片预加载
+```typescript
+function preloadAdImages(ad: CmsAd) {
+ const urls: string[] = [];
+
+ if (typeof ad.images === 'string') {
+ urls.push(ad.images);
+ } else if (Array.isArray(ad.images)) {
+ urls.push(...ad.images.map(img => img.url).filter(Boolean));
+ } else if (ad.images?.url) {
+ urls.push(ad.images.url);
+ }
+
+ urls.forEach(url => {
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = url;
+ document.head.appendChild(link);
+ });
+}
+```
+
+## ✅ 修复结果
+
+- ✅ 消除了 `@typescript-eslint/no-explicit-any` 错误
+- ✅ 提供了完整的类型安全
+- ✅ 支持多种图片数据格式
+- ✅ 保持了向后兼容性
+- ✅ 提供了清晰的类型文档
+
+现在 `CmsAd` 接口具有完整的类型安全,可以在开发时提供更好的智能提示和错误检查!
diff --git a/src/api/cms/cmsAd/model/index.ts b/src/api/cms/cmsAd/model/index.ts
index 1a7f1c1..0094a26 100644
--- a/src/api/cms/cmsAd/model/index.ts
+++ b/src/api/cms/cmsAd/model/index.ts
@@ -1,5 +1,17 @@
import type { PageParam } from '@/api';
+/**
+ * 广告图片信息
+ */
+export interface CmsAdImage {
+ id?: number;
+ url?: string;
+ alt?: string;
+ title?: string;
+ link?: string;
+ sortOrder?: number;
+}
+
/**
* 广告位
*/
@@ -24,10 +36,10 @@ export interface CmsAd {
height?: string;
// css样式
style?: string;
- // 广告图片
- images?: any;
- // 广告图片
- imageList?: any;
+ // 广告图片(单个图片或图片URL)
+ images?: string | CmsAdImage | CmsAdImage[];
+ // 广告图片列表
+ imageList?: CmsAdImage[];
// 路由/链接地址
path?: string;
// 用户ID
diff --git a/src/api/cms/cmsArticle/index.ts b/src/api/cms/cmsArticle/index.ts
index 63974a2..0cd8de1 100644
--- a/src/api/cms/cmsArticle/index.ts
+++ b/src/api/cms/cmsArticle/index.ts
@@ -66,7 +66,7 @@ export async function updateCmsArticle(data: CmsArticle) {
/**
* 批量修改文章
*/
-export async function updateBatchCmsArticle(data: any) {
+export async function updateBatchCmsArticle(data?: CmsArticleParam) {
const res = await request.put>(
MODULES_API_URL + '/cms/cms-article/batch',
data
diff --git a/src/api/cms/cmsArticle/model/index.ts b/src/api/cms/cmsArticle/model/index.ts
index 6980048..079caa2 100644
--- a/src/api/cms/cmsArticle/model/index.ts
+++ b/src/api/cms/cmsArticle/model/index.ts
@@ -19,7 +19,7 @@ export interface CmsArticle {
// 话题
topic?: string;
// 标签
- tags?: any;
+ tags?: string;
// 父级ID
parentId?: number;
parentName?: string;
diff --git a/src/api/cms/cmsDesign/model/index.ts b/src/api/cms/cmsDesign/model/index.ts
index 6c76236..6fb71b4 100644
--- a/src/api/cms/cmsDesign/model/index.ts
+++ b/src/api/cms/cmsDesign/model/index.ts
@@ -43,7 +43,7 @@ export interface CmsDesign {
// 关联网站导航ID
navigationId?: number;
showLayout?: boolean;
- btn?: any[];
+ btn?: string[];
showBanner?: boolean;
showButton?: boolean;
// 是否同步翻译其他语言版本
diff --git a/src/api/cms/cmsLink/index.ts b/src/api/cms/cmsLink/index.ts
index 63558af..efb6341 100644
--- a/src/api/cms/cmsLink/index.ts
+++ b/src/api/cms/cmsLink/index.ts
@@ -79,7 +79,7 @@ export async function removeCmsLink(id?: number) {
/**
* 批量修改常用链接
*/
-export async function updateBatchCmsLink(data: any) {
+export async function updateBatchCmsLink(data: string | []) {
const res = await request.put>(
MODULES_API_URL + '/cms/cms-link/batch',
data
diff --git a/src/api/cms/cmsWebsite/model/index.ts b/src/api/cms/cmsWebsite/model/index.ts
index 54a4394..5ffb342 100644
--- a/src/api/cms/cmsWebsite/model/index.ts
+++ b/src/api/cms/cmsWebsite/model/index.ts
@@ -105,7 +105,7 @@ export interface CmsWebsite {
// 修改时间
updateTime?: string;
// 网站配置
- config?: any;
+ config?: string;
// 短信验证码
smsCode?: string;
// 短信验证码
diff --git a/src/api/index.ts b/src/api/index.ts
index 6595e8b..1f75dfa 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -49,7 +49,7 @@ export interface PageParam {
// 商户ID
merchantId?: number;
merchantName?: string;
- categoryIds?: any;
+ categoryIds?: string;
// 商品分类
categoryId?: number;
categoryName?: string;
diff --git a/src/api/layout/model/index.ts b/src/api/layout/model/index.ts
index 317b25d..74a57e5 100644
--- a/src/api/layout/model/index.ts
+++ b/src/api/layout/model/index.ts
@@ -10,8 +10,8 @@ export interface Layout {
hover?: string;
// 背景颜色
backgroundColor?: string;
- headerStyle?: any;
- siteNameStyle?: any;
+ headerStyle?: string;
+ siteNameStyle?: string;
}
/**
diff --git a/src/api/system/cache/index.ts b/src/api/system/cache/index.ts
index aa4c8e6..b6b3065 100644
--- a/src/api/system/cache/index.ts
+++ b/src/api/system/cache/index.ts
@@ -23,7 +23,7 @@ export async function listCache(params?: CacheParam) {
* 获取缓存数据
* @param key
*/
-export async function getCache(key: String) {
+export async function getCache(key: string) {
const res = await request.get>(
SERVER_API_URL + '/system/cache/' + key
);
@@ -51,7 +51,7 @@ export async function updateCache(cache: Cache) {
* 删除缓存数据
* @param key
*/
-export async function removeCache(key?: String) {
+export async function removeCache(key?: string) {
const res = await request.delete>(
SERVER_API_URL + '/system/cache/' + key
);
diff --git a/src/api/system/cache/model/index.ts b/src/api/system/cache/model/index.ts
index dc28d00..2386f7a 100644
--- a/src/api/system/cache/model/index.ts
+++ b/src/api/system/cache/model/index.ts
@@ -6,7 +6,7 @@ import type { PageParam } from '@/api';
export interface Cache {
key?: string;
content?: string;
- uploadMethod?: any;
+ uploadMethod?: string;
expireTime?: number; // 过期时间(秒)
}
diff --git a/src/api/system/chat/index.ts b/src/api/system/chat/index.ts
index e926ecd..2f6fa3d 100644
--- a/src/api/system/chat/index.ts
+++ b/src/api/system/chat/index.ts
@@ -86,7 +86,7 @@ export async function addChatConversation(data: ChatConversation) {
/**
* 修改日志
*/
-export async function updateChatConversation(data: any) {
+export async function updateChatConversation(data: ChatConversation) {
const res = await request.put>(
SERVER_API_URL + '/system/chat-conversation',
data
diff --git a/src/api/system/chatMessage/model/index.ts b/src/api/system/chatMessage/model/index.ts
index 4352a3f..9567045 100644
--- a/src/api/system/chatMessage/model/index.ts
+++ b/src/api/system/chatMessage/model/index.ts
@@ -22,10 +22,10 @@ export interface ChatMessage {
withdraw?: number;
// 文件信息
fileInfo?: string;
- toUserName?: any;
+ toUserName?: string;
formUserName?: string;
// 批量发送
- toUserIds?: any[];
+ toUserIds?: string[];
// 存在联系方式
hasContact?: number;
// 状态, 0未读, 1已读
diff --git a/src/api/system/dict-data/model/index.ts b/src/api/system/dict-data/model/index.ts
index 6383ed3..e8e9cda 100644
--- a/src/api/system/dict-data/model/index.ts
+++ b/src/api/system/dict-data/model/index.ts
@@ -21,7 +21,7 @@ export interface DictData {
// 字典标识
dictCode?: string;
// 排序号
- sortNumber?: any;
+ sortNumber?: number;
// 备注
comments?: string;
// 创建时间
diff --git a/src/api/system/environment/index.ts b/src/api/system/environment/index.ts
index 01d542b..59ea4d4 100644
--- a/src/api/system/environment/index.ts
+++ b/src/api/system/environment/index.ts
@@ -96,20 +96,6 @@ export async function checkExistence(
return Promise.reject(new Error(res.data.message));
}
-// 搜索历史
-export async function searchHistory(params?: String) {
- const res = await request.get>(
- SERVER_API_URL + '/system/environment/search-history',
- {
- params
- }
- );
- if (res.data.code === 0 && res.data.data) {
- return res.data.data;
- }
- return Promise.reject(new Error(res.data.message));
-}
-
/**
* 制作插件
*/
diff --git a/src/api/system/file/model/index.ts b/src/api/system/file/model/index.ts
index 5a20f8b..4ccf82a 100644
--- a/src/api/system/file/model/index.ts
+++ b/src/api/system/file/model/index.ts
@@ -36,7 +36,7 @@ export interface FileRecord {
// 是否编辑状态
isUpdate?: boolean;
// 商品SKU索引
- index?: any;
+ index?: string;
}
/**
diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts
index 7c58a66..2cc36c3 100644
--- a/src/api/system/menu/index.ts
+++ b/src/api/system/menu/index.ts
@@ -93,7 +93,7 @@ export async function deleteParentMenu(id?: number) {
/**
* 安装应用
*/
-export async function installApp(data: any) {
+export async function installApp(data: Menu) {
const res = await request.post>(
SERVER_API_URL + '/system/menu/install',
data
@@ -107,7 +107,7 @@ export async function installApp(data: any) {
/**
* 卸载应用
*/
-export async function uninstallApp(data: any) {
+export async function uninstallApp(data: Menu) {
const res = await request.post>(
SERVER_API_URL + '/system/menu/uninstall',
data
@@ -119,7 +119,7 @@ export async function uninstallApp(data: any) {
}
// 菜单克隆
-export async function clone(data: any) {
+export async function clone(data: Menu) {
const res = await request.post>(
SERVER_API_URL + '/system/menu/clone',
data
diff --git a/src/api/system/plug/index.ts b/src/api/system/plug/index.ts
index bd27986..12588cc 100644
--- a/src/api/system/plug/index.ts
+++ b/src/api/system/plug/index.ts
@@ -97,8 +97,8 @@ export async function checkExistence(
}
// 搜索历史
-export async function searchHistory(params?: String) {
- const res = await request.get>(
+export async function searchHistory(params?: string) {
+ const res = await request.get>(
SERVER_API_URL + '/system/plug/search-history',
{
params
diff --git a/src/api/system/role/model/index.ts b/src/api/system/role/model/index.ts
index d4c9a50..8b65de1 100644
--- a/src/api/system/role/model/index.ts
+++ b/src/api/system/role/model/index.ts
@@ -10,9 +10,9 @@ export interface Role {
roleCode?: string;
// 角色名称
roleName?: string;
- sortNumber?: any;
+ sortNumber?: number;
// 备注
- comments?: any;
+ comments?: string;
// 创建时间
createTime?: string;
}
diff --git a/src/api/system/setting/model/index.ts b/src/api/system/setting/model/index.ts
index c0bee23..66535c2 100644
--- a/src/api/system/setting/model/index.ts
+++ b/src/api/system/setting/model/index.ts
@@ -109,7 +109,7 @@ export interface Setting {
theme?: string;
// 云存储
- uploadMethod?: any;
+ uploadMethod?: string;
fileUrl?: string;
bucketName?: string;
bucketEndpoint?: string;
diff --git a/src/api/system/user-group/model/index.ts b/src/api/system/user-group/model/index.ts
index dc858cb..1833d99 100644
--- a/src/api/system/user-group/model/index.ts
+++ b/src/api/system/user-group/model/index.ts
@@ -4,7 +4,7 @@ export interface Group {
groupId?: number;
name?: string;
status?: number;
- comments?: any;
+ comments?: string;
sortNumber?: number;
deleted?: number;
tenantId?: number;
diff --git a/src/api/system/user/model/index.ts b/src/api/system/user/model/index.ts
index e7a09a6..e1f9553 100644
--- a/src/api/system/user/model/index.ts
+++ b/src/api/system/user/model/index.ts
@@ -94,7 +94,7 @@ export interface User {
idCard?: string;
comments?: string;
recommend?: number;
- system?: any;
+ system?: string;
// 头像地址
avatarUrl?: string;
// 1男,2女
@@ -133,8 +133,8 @@ export interface User {
* 用户搜索条件
*/
export interface UserParam extends PageParam {
- keywords?: any;
- type?: any;
+ keywords?: string;
+ type?: number;
userId?: number;
username?: string;
nickname?: string;
diff --git a/src/api/system/userRole/model/index.ts b/src/api/system/userRole/model/index.ts
index 6a04ca3..6798714 100644
--- a/src/api/system/userRole/model/index.ts
+++ b/src/api/system/userRole/model/index.ts
@@ -23,8 +23,8 @@ export interface UserRole {
* 用户搜索条件
*/
export interface UserRoleParam extends PageParam {
- keywords?: any;
+ keywords?: string;
roleId?: number;
userId?: number;
- userIds?: any;
+ userIds?: string;
}
diff --git a/src/api/system/version/model/index.ts b/src/api/system/version/model/index.ts
index 8899e2f..6f2d6de 100644
--- a/src/api/system/version/model/index.ts
+++ b/src/api/system/version/model/index.ts
@@ -7,7 +7,7 @@ export interface Version {
vueDownloadUrl?: string;
androidDownloadUrl?: string;
iosDownloadUrl?: string;
- updateInfo?: any;
+ updateInfo?: string;
isHard?: boolean;
isHot?: boolean;
status?: number;
diff --git a/src/components/layout/Container.tsx b/src/components/layout/Container.tsx
new file mode 100644
index 0000000..6b50bf2
--- /dev/null
+++ b/src/components/layout/Container.tsx
@@ -0,0 +1,50 @@
+import { ReactNode } from 'react';
+
+interface ContainerProps {
+ children: ReactNode;
+ className?: string;
+ size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | 'full';
+ padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
+}
+
+/**
+ * 通用容器组件,用于内容居中和响应式布局
+ */
+const Container = ({
+ children,
+ className = '',
+ size = '7xl',
+ padding = 'md'
+}: ContainerProps) => {
+ // 根据 size 参数设置最大宽度
+ const maxWidthClass = {
+ 'sm': 'max-w-sm',
+ 'md': 'max-w-md',
+ 'lg': 'max-w-lg',
+ 'xl': 'max-w-xl',
+ '2xl': 'max-w-2xl',
+ '3xl': 'max-w-3xl',
+ '4xl': 'max-w-4xl',
+ '5xl': 'max-w-5xl',
+ '6xl': 'max-w-6xl',
+ '7xl': 'max-w-7xl',
+ 'full': 'max-w-full'
+ }[size];
+
+ // 根据 padding 参数设置内边距
+ const paddingClass = {
+ 'none': '',
+ 'sm': 'px-2 sm:px-4',
+ 'md': 'px-4 sm:px-6 lg:px-8',
+ 'lg': 'px-6 sm:px-8 lg:px-12',
+ 'xl': 'px-8 sm:px-12 lg:px-16'
+ }[padding];
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Container;
diff --git a/src/components/sections/NavigationDisplay.tsx b/src/components/sections/NavigationDisplay.tsx
new file mode 100644
index 0000000..2e2b098
--- /dev/null
+++ b/src/components/sections/NavigationDisplay.tsx
@@ -0,0 +1,159 @@
+'use client';
+import { useSiteInfoValue } from '@/hooks/useSiteInfo';
+import Link from 'next/link';
+
+/**
+ * 导航菜单展示组件
+ * 演示如何使用全局状态中的导航菜单数据
+ */
+const NavigationDisplay = () => {
+ const { bottomNavs, topNavs, navsLoading, navsError, isNavsLoaded } = useSiteInfoValue();
+
+ if (navsLoading) {
+ return (
+
+ );
+ }
+
+ if (navsError) {
+ return (
+
+
加载导航菜单失败: {navsError}
+
+ );
+ }
+
+ // 按父级分组底部导航
+ const groupedBottomNavs = bottomNavs.reduce((groups: Record, nav) => {
+ if (nav.parentId === 0) {
+ // 顶级菜单作为分组标题
+ groups[nav.title || 'default'] = bottomNavs.filter(child => child.parentId === nav.navigationId);
+ }
+ return groups;
+ }, {});
+
+ // 按父级分组顶部导航
+ const groupedTopNavs = topNavs.reduce((groups: Record, nav) => {
+ if (nav.parentId === 0) {
+ // 顶级菜单作为分组标题
+ groups[nav.title || 'default'] = topNavs.filter(child => child.parentId === nav.navigationId);
+ }
+ return groups;
+ }, {});
+
+ return (
+
+ {/* 顶部导航菜单 */}
+ {Object.keys(groupedTopNavs).length > 0 && (
+
+
+
+ 顶部导航菜单
+
+
+
+ {Object.entries(groupedTopNavs).map(([groupTitle, navs]) => (
+
+
{groupTitle}
+
+ {navs.map((nav) => (
+ -
+
+ {nav.icon && (
+
+ )}
+ {nav.title}
+
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ {/* 底部导航菜单 */}
+ {Object.keys(groupedBottomNavs).length > 0 && (
+
+
+
+ 底部导航菜单
+
+
+
+ {Object.entries(groupedBottomNavs).map(([groupTitle, navs]) => (
+
+
{groupTitle}
+
+ {navs.map((nav) => (
+ -
+
+ {nav.icon && (
+
+ )}
+ {nav.title}
+
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ {/* 无数据状态 */}
+ {!isNavsLoaded && !navsLoading && (
+
+
+
暂无导航菜单数据
+
+ 请在后台管理系统中配置导航菜单,或者检查 API 接口是否正常返回数据。
+
+
+ )}
+
+ {/* 导航菜单统计信息 */}
+ {isNavsLoaded && (
+
+
+
+ 导航菜单统计: 顶部 {topNavs.length} 个,底部 {bottomNavs.length} 个
+
+
+ 数据来源: 全局状态缓存
+
+
+
+ )}
+
+ );
+};
+
+export default NavigationDisplay;
diff --git a/src/components/sections/SiteInfoDisplay.tsx b/src/components/sections/SiteInfoDisplay.tsx
new file mode 100644
index 0000000..4888e81
--- /dev/null
+++ b/src/components/sections/SiteInfoDisplay.tsx
@@ -0,0 +1,113 @@
+'use client';
+import { useSiteInfoValue } from '@/hooks/useSiteInfo';
+
+/**
+ * 站点信息展示组件
+ * 演示如何在任意组件中使用全局站点信息
+ */
+const SiteInfoDisplay = () => {
+ const { siteInfo, loading, error, isLoaded } = useSiteInfoValue();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!isLoaded || !siteInfo) {
+ return (
+
+ );
+ }
+
+ return (
+
+
站点信息
+
+
+
+
+
{siteInfo.websiteName || '未设置'}
+
+
+
+
+
{siteInfo.websiteCode || '未设置'}
+
+
+
+
+
{siteInfo.phone || '未设置'}
+
+
+
+
+
{siteInfo.email || '未设置'}
+
+
+
+
+
{siteInfo.icpNo || '未设置'}
+
+
+
+
+
+ {siteInfo.status === 1 ? '运行中' : '未运行'}
+
+
+
+
+ {siteInfo.address && (
+
+
+
{siteInfo.address}
+
+ )}
+
+ {siteInfo.comments && (
+
+
+
{siteInfo.comments}
+
+ )}
+
+ );
+};
+
+export default SiteInfoDisplay;
diff --git a/src/hooks/useSiteInfo.ts b/src/hooks/useSiteInfo.ts
new file mode 100644
index 0000000..151fdba
--- /dev/null
+++ b/src/hooks/useSiteInfo.ts
@@ -0,0 +1,109 @@
+import { useEffect } from 'react'
+import { useSiteStore } from '@/stores/useSiteStore'
+
+/**
+ * 站点信息 Hook
+ * 自动获取站点信息,提供缓存和错误处理
+ */
+export const useSiteInfo = (options?: {
+ /** 是否自动获取数据 */
+ autoFetch?: boolean
+ /** 是否强制刷新 */
+ forceRefresh?: boolean
+ /** 是否同时获取导航菜单 */
+ includeNavs?: boolean
+}) => {
+ const {
+ siteInfo,
+ bottomNavs,
+ topNavs,
+ loading,
+ navsLoading,
+ error,
+ navsError,
+ lastFetched,
+ navsLastFetched,
+ fetchSiteInfo,
+ fetchNavigations,
+ refreshSiteInfo,
+ refreshNavigations,
+ clearCache,
+ setSiteInfo,
+ setNavigations
+ } = useSiteStore()
+
+ const { autoFetch = true, forceRefresh = false, includeNavs = true } = options || {}
+
+ useEffect(() => {
+ if (autoFetch) {
+ if (forceRefresh) {
+ refreshSiteInfo()
+ if (includeNavs) {
+ refreshNavigations()
+ }
+ } else {
+ fetchSiteInfo()
+ if (includeNavs) {
+ fetchNavigations()
+ }
+ }
+ }
+ }, [autoFetch, forceRefresh, includeNavs, fetchSiteInfo, fetchNavigations, refreshSiteInfo, refreshNavigations])
+
+ return {
+ // 数据状态
+ siteInfo,
+ bottomNavs,
+ topNavs,
+ loading,
+ navsLoading,
+ error,
+ navsError,
+ lastFetched,
+ navsLastFetched,
+
+ // 计算属性
+ isLoaded: !!siteInfo,
+ isNavsLoaded: bottomNavs.length > 0 || topNavs.length > 0,
+ isCacheValid: lastFetched ? (Date.now() - lastFetched) < 30 * 60 * 1000 : false,
+ isNavsCacheValid: navsLastFetched ? (Date.now() - navsLastFetched) < 30 * 60 * 1000 : false,
+
+ // 操作方法
+ refetch: fetchSiteInfo,
+ refetchNavs: fetchNavigations,
+ refresh: refreshSiteInfo,
+ refreshNavs: refreshNavigations,
+ clearCache,
+ setSiteInfo,
+ setNavigations,
+
+ // 便捷方法
+ retry: () => refreshSiteInfo(),
+ retryNavs: () => refreshNavigations()
+ }
+}
+
+/**
+ * 仅获取站点信息的 Hook(不自动请求)
+ */
+export const useSiteInfoValue = () => {
+ const siteInfo = useSiteStore(state => state.siteInfo)
+ const bottomNavs = useSiteStore(state => state.bottomNavs)
+ const topNavs = useSiteStore(state => state.topNavs)
+ const loading = useSiteStore(state => state.loading)
+ const navsLoading = useSiteStore(state => state.navsLoading)
+ const error = useSiteStore(state => state.error)
+ const navsError = useSiteStore(state => state.navsError)
+
+ return {
+ siteInfo,
+ bottomNavs,
+ topNavs,
+ loading,
+ navsLoading,
+ error,
+ navsError,
+ isLoaded: !!siteInfo,
+ isNavsLoaded: bottomNavs.length > 0 || topNavs.length > 0
+ }
+}
diff --git a/src/stores/useSiteStore.ts b/src/stores/useSiteStore.ts
new file mode 100644
index 0000000..8aa80b1
--- /dev/null
+++ b/src/stores/useSiteStore.ts
@@ -0,0 +1,218 @@
+import { create } from 'zustand'
+import { persist, createJSONStorage } from 'zustand/middleware'
+import { getSiteInfo, getBottomNavigations, getTopNavigations } from '@/api/layout'
+import { CmsWebsite } from '@/api/cms/cmsWebsite/model'
+import { CmsNavigation } from '@/api/cms/cmsNavigation/model'
+
+interface SiteState {
+ // 状态
+ siteInfo: CmsWebsite | null
+ bottomNavs: CmsNavigation[]
+ topNavs: CmsNavigation[]
+ loading: boolean
+ navsLoading: boolean
+ error: string | null
+ navsError: string | null
+ lastFetched: number | null
+ navsLastFetched: number | null
+
+ // 操作方法
+ fetchSiteInfo: () => Promise
+ fetchNavigations: () => Promise
+ refreshSiteInfo: () => Promise
+ refreshNavigations: () => Promise
+ clearCache: () => void
+ setSiteInfo: (siteInfo: CmsWebsite) => void
+ setNavigations: (bottomNavs: CmsNavigation[], topNavs: CmsNavigation[]) => void
+}
+
+// 缓存有效期:30分钟
+const CACHE_DURATION = 30 * 60 * 1000
+
+export const useSiteStore = create()(
+ persist(
+ (set, get) => ({
+ // 初始状态
+ siteInfo: null,
+ bottomNavs: [],
+ topNavs: [],
+ loading: false,
+ navsLoading: false,
+ error: null,
+ navsError: null,
+ lastFetched: null,
+ navsLastFetched: null,
+
+ // 获取站点信息(带缓存逻辑)
+ fetchSiteInfo: async () => {
+ const { siteInfo, lastFetched } = get()
+ const now = Date.now()
+
+ // 如果有缓存且未过期,直接返回
+ if (siteInfo && lastFetched && (now - lastFetched) < CACHE_DURATION) {
+ return
+ }
+
+ set({ loading: true, error: null })
+
+ try {
+ const data = await getSiteInfo()
+ set({
+ siteInfo: data || null,
+ loading: false,
+ error: null,
+ lastFetched: now
+ })
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '获取站点信息失败'
+ set({
+ loading: false,
+ error: errorMessage
+ })
+ console.error('获取站点信息失败:', error)
+ }
+ },
+
+ // 获取导航菜单(带缓存逻辑)
+ fetchNavigations: async () => {
+ const { bottomNavs, navsLastFetched } = get()
+ const now = Date.now()
+
+ // 如果有缓存且未过期,直接返回
+ if (bottomNavs.length > 0 && navsLastFetched && (now - navsLastFetched) < CACHE_DURATION) {
+ return
+ }
+
+ set({ navsLoading: true, navsError: null })
+
+ try {
+ const [bottomNavsData, topNavsData] = await Promise.all([
+ getBottomNavigations(),
+ getTopNavigations()
+ ])
+
+ set({
+ bottomNavs: bottomNavsData || [],
+ topNavs: topNavsData || [],
+ navsLoading: false,
+ navsError: null,
+ navsLastFetched: now
+ })
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '获取导航菜单失败'
+ set({
+ navsLoading: false,
+ navsError: errorMessage
+ })
+ console.error('获取导航菜单失败:', error)
+ }
+ },
+
+ // 强制刷新站点信息
+ refreshSiteInfo: async () => {
+ set({ loading: true, error: null })
+
+ try {
+ const data = await getSiteInfo()
+ set({
+ siteInfo: data || null,
+ loading: false,
+ error: null,
+ lastFetched: Date.now()
+ })
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '刷新站点信息失败'
+ set({
+ loading: false,
+ error: errorMessage
+ })
+ console.error('刷新站点信息失败:', error)
+ }
+ },
+
+ // 强制刷新导航菜单
+ refreshNavigations: async () => {
+ set({ navsLoading: true, navsError: null })
+
+ try {
+ const [bottomNavsData, topNavsData] = await Promise.all([
+ getBottomNavigations(),
+ getTopNavigations()
+ ])
+
+ set({
+ bottomNavs: bottomNavsData || [],
+ topNavs: topNavsData || [],
+ navsLoading: false,
+ navsError: null,
+ navsLastFetched: Date.now()
+ })
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '刷新导航菜单失败'
+ set({
+ navsLoading: false,
+ navsError: errorMessage
+ })
+ console.error('刷新导航菜单失败:', error)
+ }
+ },
+
+ // 清除缓存
+ clearCache: () => {
+ set({
+ siteInfo: null,
+ bottomNavs: [],
+ topNavs: [],
+ loading: false,
+ navsLoading: false,
+ error: null,
+ navsError: null,
+ lastFetched: null,
+ navsLastFetched: null
+ })
+ },
+
+ // 手动设置站点信息
+ setSiteInfo: (siteInfo: CmsWebsite) => {
+ set({
+ siteInfo,
+ lastFetched: Date.now(),
+ error: null
+ })
+ },
+
+ // 手动设置导航菜单
+ setNavigations: (bottomNavs: CmsNavigation[], topNavs: CmsNavigation[]) => {
+ set({
+ bottomNavs,
+ topNavs,
+ navsLastFetched: Date.now(),
+ navsError: null
+ })
+ }
+ }),
+ {
+ name: 'site-storage', // 存储键名
+ storage: createJSONStorage(() => {
+ // 优先使用 sessionStorage,降级到 localStorage
+ if (typeof window !== 'undefined') {
+ return window.sessionStorage || window.localStorage
+ }
+ // 服务端渲染时的占位符
+ return {
+ getItem: () => null,
+ setItem: () => {},
+ removeItem: () => {}
+ }
+ }),
+ // 只持久化必要的数据,不持久化 loading 状态
+ partialize: (state) => ({
+ siteInfo: state.siteInfo,
+ bottomNavs: state.bottomNavs,
+ topNavs: state.topNavs,
+ lastFetched: state.lastFetched,
+ navsLastFetched: state.navsLastFetched
+ })
+ }
+ )
+)