15 changed files with 3051 additions and 0 deletions
@ -0,0 +1,173 @@ |
|||
# Next.js 图片域名配置说明 |
|||
|
|||
## 问题描述 |
|||
|
|||
当使用 Next.js 的 `Image` 组件加载外部图片时,如果图片域名没有在 `next.config.ts` 中配置,会出现以下错误: |
|||
|
|||
``` |
|||
hostname "oss.wsdns.cn" is not configured under images in your `next.config` |
|||
``` |
|||
|
|||
## 解决方案 |
|||
|
|||
在 `next.config.ts` 文件中配置 `images.remotePatterns` 来允许特定域名的图片加载。 |
|||
|
|||
### 配置示例 |
|||
|
|||
```typescript |
|||
import type { NextConfig } from "next"; |
|||
|
|||
const nextConfig: NextConfig = { |
|||
images: { |
|||
remotePatterns: [ |
|||
// OSS 图片域名 |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'oss.wsdns.cn', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'http', |
|||
hostname: 'oss.wsdns.cn', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
// API 服务器图片 |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'cms-api.websoft.top', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
// 其他常见 CDN 域名... |
|||
], |
|||
}, |
|||
}; |
|||
|
|||
export default nextConfig; |
|||
``` |
|||
|
|||
## 配置参数说明 |
|||
|
|||
### remotePatterns 配置项 |
|||
|
|||
- **protocol**: 协议类型 (`'http'` 或 `'https'`) |
|||
- **hostname**: 域名 (支持通配符,如 `*.example.com`) |
|||
- **port**: 端口号 (通常为空字符串) |
|||
- **pathname**: 路径模式 (通常使用 `'/**'` 匹配所有路径) |
|||
|
|||
### 通配符支持 |
|||
|
|||
```typescript |
|||
{ |
|||
protocol: 'https', |
|||
hostname: '*.aliyuncs.com', // 匹配所有阿里云 OSS 子域名 |
|||
pathname: '/**', |
|||
} |
|||
``` |
|||
|
|||
## 已配置的域名 |
|||
|
|||
当前项目已配置以下图片域名: |
|||
|
|||
### 主要业务域名 |
|||
- `oss.wsdns.cn` - OSS 存储域名 |
|||
- `cms-api.websoft.top` - API 服务器图片 |
|||
|
|||
### 常见 CDN 域名 |
|||
- `*.aliyuncs.com` - 阿里云 OSS |
|||
- `*.qiniudn.com` - 七牛云 CDN |
|||
- `*.qiniu.com` - 七牛云新域名 |
|||
|
|||
### 开发环境 |
|||
- `localhost` - 本地开发 |
|||
|
|||
## 使用示例 |
|||
|
|||
配置完成后,可以在组件中正常使用外部图片: |
|||
|
|||
```tsx |
|||
import Image from 'next/image'; |
|||
|
|||
function ArticleImage({ article }) { |
|||
return ( |
|||
<Image |
|||
src={article.image || '/placeholder.jpg'} |
|||
alt={article.title || '文章图片'} |
|||
width={400} |
|||
height={200} |
|||
className="article-image" |
|||
/> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
## 安全注意事项 |
|||
|
|||
1. **最小权限原则**: 只配置必要的域名,避免使用过于宽泛的通配符 |
|||
2. **HTTPS 优先**: 优先使用 HTTPS 协议的域名 |
|||
3. **路径限制**: 如果可能,限制特定的路径模式而不是使用 `/**` |
|||
|
|||
### 推荐的安全配置 |
|||
|
|||
```typescript |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'oss.wsdns.cn', |
|||
pathname: '/images/**', // 只允许 /images/ 路径下的图片 |
|||
} |
|||
``` |
|||
|
|||
## 故障排除 |
|||
|
|||
### 1. 配置不生效 |
|||
|
|||
**解决方案**: 修改 `next.config.ts` 后需要重启开发服务器: |
|||
|
|||
```bash |
|||
# 停止当前服务器 (Ctrl+C) |
|||
npm run dev |
|||
``` |
|||
|
|||
### 2. 通配符不工作 |
|||
|
|||
**原因**: 确保通配符语法正确,只能在域名的开头使用 `*` |
|||
|
|||
```typescript |
|||
// ✅ 正确 |
|||
hostname: '*.example.com' |
|||
|
|||
// ❌ 错误 |
|||
hostname: 'cdn.*.example.com' |
|||
``` |
|||
|
|||
### 3. 图片仍然无法加载 |
|||
|
|||
**检查步骤**: |
|||
1. 确认域名拼写正确 |
|||
2. 检查协议是否匹配 (http vs https) |
|||
3. 验证图片 URL 是否可访问 |
|||
4. 查看浏览器控制台错误信息 |
|||
|
|||
## 性能优化建议 |
|||
|
|||
1. **图片格式**: 使用现代图片格式 (WebP, AVIF) |
|||
2. **尺寸优化**: 配置合适的 `width` 和 `height` |
|||
3. **懒加载**: Next.js Image 组件默认启用懒加载 |
|||
4. **优先级**: 对重要图片使用 `priority` 属性 |
|||
|
|||
```tsx |
|||
<Image |
|||
src="/hero-image.jpg" |
|||
alt="Hero" |
|||
width={1200} |
|||
height={600} |
|||
priority // 优先加载 |
|||
/> |
|||
``` |
|||
|
|||
## 相关文档 |
|||
|
|||
- [Next.js Image Optimization](https://nextjs.org/docs/basic-features/image-optimization) |
|||
- [Next.js Configuration](https://nextjs.org/docs/api-reference/next.config.js/images) |
@ -0,0 +1,161 @@ |
|||
# Next.js 图片域名配置修复总结 |
|||
|
|||
## 问题描述 |
|||
|
|||
在使用 Next.js 的 `Image` 组件加载外部图片时,出现以下错误: |
|||
|
|||
``` |
|||
hostname "oss.wsdns.cn" is not configured under images in your `next.config` |
|||
``` |
|||
|
|||
## 解决方案 |
|||
|
|||
### 1. 修改 `next.config.ts` 文件 |
|||
|
|||
在 Next.js 配置文件中添加了 `images.remotePatterns` 配置: |
|||
|
|||
```typescript |
|||
import type { NextConfig } from "next"; |
|||
|
|||
const nextConfig: NextConfig = { |
|||
images: { |
|||
remotePatterns: [ |
|||
// OSS 图片域名 |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'oss.wsdns.cn', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'http', |
|||
hostname: 'oss.wsdns.cn', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
// API 服务器图片 |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'cms-api.websoft.top', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'http', |
|||
hostname: 'cms-api.websoft.top', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
// 常见的图片 CDN 域名 |
|||
{ |
|||
protocol: 'https', |
|||
hostname: '*.aliyuncs.com', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'https', |
|||
hostname: '*.qiniudn.com', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'https', |
|||
hostname: '*.qiniu.com', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
// 本地开发 |
|||
{ |
|||
protocol: 'http', |
|||
hostname: 'localhost', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
{ |
|||
protocol: 'https', |
|||
hostname: 'localhost', |
|||
port: '', |
|||
pathname: '/**', |
|||
}, |
|||
], |
|||
}, |
|||
}; |
|||
|
|||
export default nextConfig; |
|||
``` |
|||
|
|||
### 2. 配置的域名列表 |
|||
|
|||
已配置的图片域名包括: |
|||
|
|||
#### 主要业务域名 |
|||
- `oss.wsdns.cn` - OSS 存储域名(HTTP/HTTPS) |
|||
- `cms-api.websoft.top` - API 服务器图片(HTTP/HTTPS) |
|||
|
|||
#### 常见 CDN 域名 |
|||
- `*.aliyuncs.com` - 阿里云 OSS |
|||
- `*.qiniudn.com` - 七牛云 CDN |
|||
- `*.qiniu.com` - 七牛云新域名 |
|||
|
|||
#### 开发环境 |
|||
- `localhost` - 本地开发(HTTP/HTTPS) |
|||
|
|||
### 3. 重启服务器 |
|||
|
|||
修改 `next.config.ts` 后需要重启开发服务器: |
|||
|
|||
```bash |
|||
# 停止当前服务器 (Ctrl+C) |
|||
npm run dev |
|||
``` |
|||
|
|||
## 修复效果 |
|||
|
|||
### ✅ 已解决的问题 |
|||
|
|||
1. **外部图片加载**:现在可以正常加载来自 `oss.wsdns.cn` 的图片 |
|||
2. **API 图片支持**:支持从 `cms-api.websoft.top` 加载图片 |
|||
3. **CDN 兼容性**:支持常见的 CDN 域名 |
|||
4. **开发环境**:支持本地开发时的图片加载 |
|||
|
|||
### ⚠️ 注意事项 |
|||
|
|||
1. **404 错误正常**:日志中的 `/hero-image.jpg`、`/about-image.jpg` 等 404 错误是正常的,因为这些本地图片文件不存在 |
|||
2. **Tailwind 警告**:`px-6` 等 Tailwind 类的警告不影响图片功能 |
|||
3. **配置生效**:服务器已自动重启并应用了新配置 |
|||
|
|||
## 使用示例 |
|||
|
|||
现在可以在组件中正常使用外部图片: |
|||
|
|||
```tsx |
|||
import Image from 'next/image'; |
|||
|
|||
function ArticleImage({ article }) { |
|||
return ( |
|||
<Image |
|||
src={article.image || '/placeholder.jpg'} // 支持 oss.wsdns.cn 域名 |
|||
alt={article.title || '文章图片'} |
|||
width={400} |
|||
height={200} |
|||
className="article-image" |
|||
/> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
## 安全考虑 |
|||
|
|||
1. **最小权限**:只配置了必要的域名 |
|||
2. **协议支持**:同时支持 HTTP 和 HTTPS |
|||
3. **路径限制**:使用 `/**` 允许所有路径(可根据需要进一步限制) |
|||
|
|||
## 相关文档 |
|||
|
|||
- `docs/IMAGE_DOMAINS_CONFIG.md` - 详细的图片域名配置说明 |
|||
- [Next.js Image Optimization](https://nextjs.org/docs/basic-features/image-optimization) |
|||
|
|||
## 状态 |
|||
|
|||
✅ **已完成** - 图片域名配置问题已解决,外部图片可以正常加载 |
@ -0,0 +1,190 @@ |
|||
# 租户 ID 配置和使用说明 |
|||
|
|||
## 概述 |
|||
|
|||
本项目的 API 请求系统已经集成了租户 ID 功能,所有 API 请求都会自动添加租户 ID,无需手动处理。 |
|||
|
|||
## 配置方式 |
|||
|
|||
### 1. 环境变量配置(推荐) |
|||
|
|||
在项目根目录的 `.env.local` 文件中配置: |
|||
|
|||
```env |
|||
# 租户配置 |
|||
NEXT_PUBLIC_TENANT_ID=1001 |
|||
``` |
|||
|
|||
### 2. 配置文件 |
|||
|
|||
租户 ID 在 `src/config/setting.ts` 中定义: |
|||
|
|||
```typescript |
|||
export const APP_CONFIG = { |
|||
// 租户ID配置 |
|||
TENANT_ID: process.env.NEXT_PUBLIC_TENANT_ID || '10001', |
|||
// ... 其他配置 |
|||
} as const; |
|||
``` |
|||
|
|||
## 自动添加机制 |
|||
|
|||
### 1. 请求头 |
|||
|
|||
所有请求都会自动添加 `TenantId` 请求头: |
|||
|
|||
``` |
|||
TenantId: 1001 |
|||
``` |
|||
|
|||
### 2. GET 请求查询参数 |
|||
|
|||
GET 请求会自动在查询参数中添加 `tenantId`: |
|||
|
|||
``` |
|||
GET /api/users?page=1&limit=10&tenantId=1001 |
|||
``` |
|||
|
|||
### 3. POST/PUT/PATCH 请求体 |
|||
|
|||
POST、PUT、PATCH 请求会自动在请求体中添加 `tenantId`: |
|||
|
|||
```json |
|||
{ |
|||
"name": "用户名", |
|||
"email": "user@example.com", |
|||
"tenantId": "1001" |
|||
} |
|||
``` |
|||
|
|||
## 使用示例 |
|||
|
|||
### 基础使用 |
|||
|
|||
```typescript |
|||
import { request } from '@/api'; |
|||
|
|||
// GET 请求 - 自动添加租户ID到查询参数 |
|||
const users = await request.get('/users', { page: 1, limit: 10 }); |
|||
|
|||
// POST 请求 - 自动添加租户ID到请求体 |
|||
const newUser = await request.post('/users', { |
|||
name: '张三', |
|||
email: 'zhangsan@example.com' |
|||
}); |
|||
|
|||
// PUT 请求 - 自动添加租户ID到请求体 |
|||
const updatedUser = await request.put('/users/1', { |
|||
name: '李四' |
|||
}); |
|||
|
|||
// DELETE 请求 - 自动添加租户ID到请求头 |
|||
await request.delete('/users/1'); |
|||
``` |
|||
|
|||
### 兼容现有代码 |
|||
|
|||
如果使用 `src/utils/request.ts` 中的兼容性封装: |
|||
|
|||
```typescript |
|||
import request from '@/utils/request'; |
|||
|
|||
// 所有请求都会自动添加租户ID |
|||
const response = await request.get('/users'); |
|||
const { data } = response; // ApiResult<T> 格式 |
|||
``` |
|||
|
|||
## 特殊情况处理 |
|||
|
|||
### 1. FormData 请求 |
|||
|
|||
对于 FormData 类型的请求,租户 ID 只会添加到请求头中,不会修改 FormData 内容: |
|||
|
|||
```typescript |
|||
const formData = new FormData(); |
|||
formData.append('file', file); |
|||
|
|||
// 只在请求头中添加 TenantId,不修改 FormData |
|||
await request.post('/upload', formData); |
|||
``` |
|||
|
|||
### 2. 覆盖租户 ID |
|||
|
|||
如果需要使用不同的租户 ID,可以在请求参数中显式指定: |
|||
|
|||
```typescript |
|||
// GET 请求中覆盖 |
|||
await request.get('/users', { tenantId: '2001' }); |
|||
|
|||
// POST 请求中覆盖 |
|||
await request.post('/users', { |
|||
name: '张三', |
|||
tenantId: '2001' |
|||
}); |
|||
``` |
|||
|
|||
### 3. 禁用自动添加 |
|||
|
|||
目前不支持禁用自动添加租户 ID。如果有特殊需求,请联系开发团队。 |
|||
|
|||
## 环境配置 |
|||
|
|||
### 开发环境 |
|||
|
|||
```env |
|||
NEXT_PUBLIC_TENANT_ID=1001 |
|||
``` |
|||
|
|||
### 测试环境 |
|||
|
|||
```env |
|||
NEXT_PUBLIC_TENANT_ID=2001 |
|||
``` |
|||
|
|||
### 生产环境 |
|||
|
|||
```env |
|||
NEXT_PUBLIC_TENANT_ID=3001 |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **环境变量优先级**:环境变量 > 配置文件默认值 |
|||
2. **客户端可见**:使用 `NEXT_PUBLIC_` 前缀的环境变量在客户端可见 |
|||
3. **类型安全**:租户 ID 统一使用字符串类型 |
|||
4. **自动重载**:修改 `.env.local` 文件后,开发服务器会自动重载 |
|||
5. **部署配置**:生产环境部署时需要正确配置环境变量 |
|||
|
|||
## 故障排除 |
|||
|
|||
### 1. 租户 ID 未生效 |
|||
|
|||
检查环境变量是否正确配置: |
|||
|
|||
```bash |
|||
# 检查环境变量 |
|||
echo $NEXT_PUBLIC_TENANT_ID |
|||
``` |
|||
|
|||
### 2. 请求中看不到租户 ID |
|||
|
|||
使用浏览器开发者工具检查: |
|||
- Network 标签页查看请求头和请求体 |
|||
- 确认 `TenantId` 请求头是否存在 |
|||
- 确认查询参数或请求体中是否包含 `tenantId` |
|||
|
|||
### 3. 类型错误 |
|||
|
|||
确保 TypeScript 配置正确,重启开发服务器: |
|||
|
|||
```bash |
|||
npm run dev |
|||
``` |
|||
|
|||
## 相关文件 |
|||
|
|||
- `src/config/setting.ts` - 配置文件 |
|||
- `src/api/index.ts` - API 核心实现 |
|||
- `src/utils/request.ts` - 兼容性封装 |
|||
- `src/api/examples/tenant-api-usage.ts` - 使用示例 |
|||
- `.env.local` - 环境变量配置 |
@ -0,0 +1,145 @@ |
|||
/** |
|||
* 租户 API 使用示例 |
|||
* 展示如何使用带租户 ID 的 API 请求 |
|||
*/ |
|||
|
|||
import { request } from '@/api'; |
|||
import type { ApiResult, PageResult, PageParam } from '@/api'; |
|||
|
|||
// ==================== 示例数据类型 ====================
|
|||
|
|||
interface User { |
|||
id: number; |
|||
name: string; |
|||
email: string; |
|||
tenantId: string; |
|||
createTime: string; |
|||
} |
|||
|
|||
interface CreateUserData { |
|||
name: string; |
|||
email: string; |
|||
password: string; |
|||
} |
|||
|
|||
interface UpdateUserData { |
|||
name?: string; |
|||
email?: string; |
|||
} |
|||
|
|||
// ==================== API 使用示例 ====================
|
|||
|
|||
/** |
|||
* 获取用户列表 - GET 请求示例 |
|||
* 租户 ID 会自动添加到查询参数中 |
|||
*/ |
|||
export const getUserList = async (params?: PageParam): Promise<ApiResult<PageResult<User>>> => { |
|||
return request.get<PageResult<User>>('/users', params); |
|||
}; |
|||
|
|||
/** |
|||
* 获取用户详情 - GET 请求示例 |
|||
* 租户 ID 会自动添加到查询参数中 |
|||
*/ |
|||
export const getUserById = async (id: number): Promise<ApiResult<User>> => { |
|||
return request.get<User>(`/users/${id}`); |
|||
}; |
|||
|
|||
/** |
|||
* 创建用户 - POST 请求示例 |
|||
* 租户 ID 会自动添加到请求体中 |
|||
*/ |
|||
export const createUser = async (data: CreateUserData): Promise<ApiResult<User>> => { |
|||
return request.post<User>('/users', data); |
|||
}; |
|||
|
|||
/** |
|||
* 更新用户 - PUT 请求示例 |
|||
* 租户 ID 会自动添加到请求体中 |
|||
*/ |
|||
export const updateUser = async (id: number, data: UpdateUserData): Promise<ApiResult<User>> => { |
|||
return request.put<User>(`/users/${id}`, data); |
|||
}; |
|||
|
|||
/** |
|||
* 部分更新用户 - PATCH 请求示例 |
|||
* 租户 ID 会自动添加到请求体中 |
|||
*/ |
|||
export const patchUser = async (id: number, data: Partial<UpdateUserData>): Promise<ApiResult<User>> => { |
|||
return request.patch<User>(`/users/${id}`, data); |
|||
}; |
|||
|
|||
/** |
|||
* 删除用户 - DELETE 请求示例 |
|||
* 租户 ID 会自动添加到请求头中 |
|||
*/ |
|||
export const deleteUser = async (id: number): Promise<ApiResult<void>> => { |
|||
return request.delete<void>(`/users/${id}`); |
|||
}; |
|||
|
|||
// ==================== 使用示例 ====================
|
|||
|
|||
/** |
|||
* 组件中使用示例 |
|||
*/ |
|||
export const exampleUsage = async () => { |
|||
try { |
|||
// 获取用户列表
|
|||
const userListResult = await getUserList({ |
|||
page: 1, |
|||
limit: 10, |
|||
keywords: '张三' |
|||
}); |
|||
|
|||
if (userListResult.code === 0) { |
|||
console.log('用户列表:', userListResult.data?.list); |
|||
} |
|||
|
|||
// 创建用户
|
|||
const createResult = await createUser({ |
|||
name: '新用户', |
|||
email: 'newuser@example.com', |
|||
password: '123456' |
|||
}); |
|||
|
|||
if (createResult.code === 0) { |
|||
console.log('创建成功:', createResult.data); |
|||
} |
|||
|
|||
// 更新用户
|
|||
if (createResult.data?.id) { |
|||
const updateResult = await updateUser(createResult.data.id, { |
|||
name: '更新后的用户名' |
|||
}); |
|||
|
|||
if (updateResult.code === 0) { |
|||
console.log('更新成功:', updateResult.data); |
|||
} |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('API 请求失败:', error); |
|||
} |
|||
}; |
|||
|
|||
// ==================== 注意事项 ====================
|
|||
|
|||
/** |
|||
* 租户 ID 自动添加说明: |
|||
* |
|||
* 1. GET 请求:租户 ID 会自动添加到查询参数中 |
|||
* 例如:/users?page=1&limit=10&tenantId=1001 |
|||
* |
|||
* 2. POST/PUT/PATCH 请求:租户 ID 会自动添加到请求体中 |
|||
* 例如:{ name: '用户名', email: 'email@example.com', tenantId: '10001' } |
|||
* |
|||
* 3. 所有请求:租户 ID 会自动添加到请求头中 |
|||
* 例如:TenantId: 1001 |
|||
* |
|||
* 4. 租户 ID 从配置文件中读取: |
|||
* - 环境变量:NEXT_PUBLIC_TENANT_ID |
|||
* - 配置文件:APP_CONFIG.TENANT_ID |
|||
* - 默认值:'10001' |
|||
* |
|||
* 5. 如果需要覆盖租户 ID,可以在请求参数或数据中显式指定 |
|||
*/ |
@ -0,0 +1,390 @@ |
|||
/** |
|||
* 文章相关 API |
|||
*/ |
|||
|
|||
import { request, type ApiResult, type PageResult, type PageParam } from '@/api'; |
|||
|
|||
// ==================== 类型定义 ====================
|
|||
|
|||
/** |
|||
* 文章信息 |
|||
*/ |
|||
export interface Article { |
|||
id: number; |
|||
title: string; |
|||
content: string; |
|||
excerpt?: string; |
|||
thumbnail?: string; |
|||
author: string; |
|||
authorId: number; |
|||
categoryId?: number; |
|||
categoryName?: string; |
|||
tags?: string[]; |
|||
status: 'draft' | 'published' | 'archived'; |
|||
viewCount: number; |
|||
likeCount: number; |
|||
commentCount: number; |
|||
isTop: boolean; |
|||
isRecommend: boolean; |
|||
publishTime?: string; |
|||
createTime: string; |
|||
updateTime: string; |
|||
seoTitle?: string; |
|||
seoKeywords?: string; |
|||
seoDescription?: string; |
|||
} |
|||
|
|||
/** |
|||
* 文章分类 |
|||
*/ |
|||
export interface ArticleCategory { |
|||
id: number; |
|||
name: string; |
|||
slug: string; |
|||
description?: string; |
|||
parentId?: number; |
|||
sortOrder: number; |
|||
articleCount: number; |
|||
status: number; |
|||
createTime: string; |
|||
children?: ArticleCategory[]; |
|||
} |
|||
|
|||
/** |
|||
* 文章标签 |
|||
*/ |
|||
export interface ArticleTag { |
|||
id: number; |
|||
name: string; |
|||
slug: string; |
|||
color?: string; |
|||
articleCount: number; |
|||
createTime: string; |
|||
} |
|||
|
|||
/** |
|||
* 文章查询参数 |
|||
*/ |
|||
export interface ArticleSearchParams extends PageParam { |
|||
title?: string; |
|||
categoryId?: number; |
|||
authorId?: number; |
|||
status?: 'draft' | 'published' | 'archived'; |
|||
isTop?: boolean; |
|||
isRecommend?: boolean; |
|||
tags?: string[]; |
|||
startTime?: string; |
|||
endTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 文章创建参数 |
|||
*/ |
|||
export interface CreateArticleParams { |
|||
title: string; |
|||
content: string; |
|||
excerpt?: string; |
|||
thumbnail?: string; |
|||
categoryId?: number; |
|||
tags?: string[]; |
|||
status?: 'draft' | 'published'; |
|||
isTop?: boolean; |
|||
isRecommend?: boolean; |
|||
publishTime?: string; |
|||
seoTitle?: string; |
|||
seoKeywords?: string; |
|||
seoDescription?: string; |
|||
} |
|||
|
|||
/** |
|||
* 文章更新参数 |
|||
*/ |
|||
export interface UpdateArticleParams extends CreateArticleParams { |
|||
id: number; |
|||
} |
|||
|
|||
// ==================== API 方法 ====================
|
|||
|
|||
/** |
|||
* 文章 API 类 |
|||
*/ |
|||
export class ArticleApi { |
|||
/** |
|||
* 分页查询文章列表 |
|||
*/ |
|||
static async getArticles(params?: ArticleSearchParams): Promise<ApiResult<PageResult<Article>>> { |
|||
return request.get<PageResult<Article>>('/cms/cms-article/page', params); |
|||
} |
|||
|
|||
/** |
|||
* 获取推荐文章列表 |
|||
*/ |
|||
static async getRecommendArticles(limit = 6): Promise<ApiResult<Article[]>> { |
|||
return request.get<Article[]>('/cms/cms-article/recommend', { limit }); |
|||
} |
|||
|
|||
/** |
|||
* 获取热门文章列表 |
|||
*/ |
|||
static async getHotArticles(limit = 10): Promise<ApiResult<Article[]>> { |
|||
return request.get<Article[]>('/cms/cms-article/hot', { limit }); |
|||
} |
|||
|
|||
/** |
|||
* 获取最新文章列表 |
|||
*/ |
|||
static async getLatestArticles(limit = 10): Promise<ApiResult<Article[]>> { |
|||
return request.get<Article[]>('/cms/cms-article', { limit }); |
|||
} |
|||
|
|||
/** |
|||
* 根据 ID 获取文章详情 |
|||
*/ |
|||
static async getArticleById(id: number): Promise<ApiResult<Article>> { |
|||
return request.get<Article>(`/cms/cms-article/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 根据分类获取文章列表 |
|||
*/ |
|||
static async getArticlesByCategory( |
|||
categoryId: number, |
|||
params?: Omit<ArticleSearchParams, 'categoryId'> |
|||
): Promise<ApiResult<PageResult<Article>>> { |
|||
return request.get<PageResult<Article>>('/cms/cms-article/page', { |
|||
...params, |
|||
categoryId, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 根据标签获取文章列表 |
|||
*/ |
|||
static async getArticlesByTag( |
|||
tag: string, |
|||
params?: Omit<ArticleSearchParams, 'tags'> |
|||
): Promise<ApiResult<PageResult<Article>>> { |
|||
return request.get<PageResult<Article>>('/cms/cms-article/page', { |
|||
...params, |
|||
tags: [tag], |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 搜索文章 |
|||
*/ |
|||
static async searchArticles( |
|||
keyword: string, |
|||
params?: ArticleSearchParams |
|||
): Promise<ApiResult<PageResult<Article>>> { |
|||
return request.get<PageResult<Article>>('/cms/cms-article/search', { |
|||
...params, |
|||
keywords: keyword, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取相关文章 |
|||
*/ |
|||
static async getRelatedArticles(id: number, limit = 5): Promise<ApiResult<Article[]>> { |
|||
return request.get<Article[]>(`/cms/cms-article/${id}/related`, { limit }); |
|||
} |
|||
|
|||
/** |
|||
* 增加文章浏览量 |
|||
*/ |
|||
static async increaseViewCount(id: number): Promise<ApiResult<void>> { |
|||
return request.post<void>(`/cms/cms-article/${id}/view`); |
|||
} |
|||
|
|||
/** |
|||
* 点赞文章 |
|||
*/ |
|||
static async likeArticle(id: number): Promise<ApiResult<void>> { |
|||
return request.post<void>(`/cms/cms-article/${id}/like`); |
|||
} |
|||
|
|||
/** |
|||
* 取消点赞文章 |
|||
*/ |
|||
static async unlikeArticle(id: number): Promise<ApiResult<void>> { |
|||
return request.delete<void>(`/cms/cms-article/${id}/like`); |
|||
} |
|||
|
|||
/** |
|||
* 创建文章(管理员功能) |
|||
*/ |
|||
static async createArticle(params: CreateArticleParams): Promise<ApiResult<string>> { |
|||
return request.post<string>('/cms/cms-article', params); |
|||
} |
|||
|
|||
/** |
|||
* 更新文章(管理员功能) |
|||
*/ |
|||
static async updateArticle(params: UpdateArticleParams): Promise<ApiResult<string>> { |
|||
return request.put<string>('/cms/cms-article', params); |
|||
} |
|||
|
|||
/** |
|||
* 删除文章(管理员功能) |
|||
*/ |
|||
static async deleteArticle(id: number): Promise<ApiResult<string>> { |
|||
return request.delete<string>(`/cms/cms-article/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 批量删除文章(管理员功能) |
|||
*/ |
|||
static async batchDeleteArticles(ids: number[]): Promise<ApiResult<string>> { |
|||
return request.post<string>('/cms/cms-article/batch-delete', ids); |
|||
} |
|||
|
|||
/** |
|||
* 发布文章(管理员功能) |
|||
*/ |
|||
static async publishArticle(id: number): Promise<ApiResult<string>> { |
|||
return request.post<string>(`/cms/cms-article/${id}/publish`); |
|||
} |
|||
|
|||
/** |
|||
* 取消发布文章(管理员功能) |
|||
*/ |
|||
static async unpublishArticle(id: number): Promise<ApiResult<string>> { |
|||
return request.post<string>(`/cms/cms-article/${id}/unpublish`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 文章分类 API 类 |
|||
*/ |
|||
export class ArticleCategoryApi { |
|||
/** |
|||
* 获取所有分类列表 |
|||
*/ |
|||
static async getCategories(): Promise<ApiResult<ArticleCategory[]>> { |
|||
return request.get<ArticleCategory[]>('/cms/cms-article-category'); |
|||
} |
|||
|
|||
/** |
|||
* 获取分类树 |
|||
*/ |
|||
static async getCategoryTree(): Promise<ApiResult<ArticleCategory[]>> { |
|||
return request.get<ArticleCategory[]>('/cms/cms-article-category/tree'); |
|||
} |
|||
|
|||
/** |
|||
* 根据 ID 获取分类详情 |
|||
*/ |
|||
static async getCategoryById(id: number): Promise<ApiResult<ArticleCategory>> { |
|||
return request.get<ArticleCategory>(`/cms/cms-article-category/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 创建分类(管理员功能) |
|||
*/ |
|||
static async createCategory(params: { |
|||
name: string; |
|||
slug?: string; |
|||
description?: string; |
|||
parentId?: number; |
|||
sortOrder?: number; |
|||
}): Promise<ApiResult<string>> { |
|||
return request.post<string>('/cms/cms-article-category', params); |
|||
} |
|||
|
|||
/** |
|||
* 更新分类(管理员功能) |
|||
*/ |
|||
static async updateCategory(params: { |
|||
id: number; |
|||
name?: string; |
|||
slug?: string; |
|||
description?: string; |
|||
parentId?: number; |
|||
sortOrder?: number; |
|||
}): Promise<ApiResult<string>> { |
|||
return request.put<string>('/cms/cms-article-category', params); |
|||
} |
|||
|
|||
/** |
|||
* 删除分类(管理员功能) |
|||
*/ |
|||
static async deleteCategory(id: number): Promise<ApiResult<string>> { |
|||
return request.delete<string>(`/cms/cms-article-category/${id}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 文章标签 API 类 |
|||
*/ |
|||
export class ArticleTagApi { |
|||
/** |
|||
* 获取所有标签列表 |
|||
*/ |
|||
static async getTags(): Promise<ApiResult<ArticleTag[]>> { |
|||
return request.get<ArticleTag[]>('/cms/cms-article-tag'); |
|||
} |
|||
|
|||
/** |
|||
* 获取热门标签 |
|||
*/ |
|||
static async getHotTags(limit = 20): Promise<ApiResult<ArticleTag[]>> { |
|||
return request.get<ArticleTag[]>('/cms/cms-article-tag/hot', { limit }); |
|||
} |
|||
|
|||
/** |
|||
* 根据 ID 获取标签详情 |
|||
*/ |
|||
static async getTagById(id: number): Promise<ApiResult<ArticleTag>> { |
|||
return request.get<ArticleTag>(`/cms/cms-article-tag/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 搜索标签 |
|||
*/ |
|||
static async searchTags(keyword: string): Promise<ApiResult<ArticleTag[]>> { |
|||
return request.get<ArticleTag[]>('/cms/cms-article-tag/search', { keyword }); |
|||
} |
|||
} |
|||
|
|||
// ==================== 便捷方法导出 ====================
|
|||
|
|||
export const { |
|||
getArticles, |
|||
getRecommendArticles, |
|||
getHotArticles, |
|||
getLatestArticles, |
|||
getArticleById, |
|||
getArticlesByCategory, |
|||
getArticlesByTag, |
|||
searchArticles, |
|||
getRelatedArticles, |
|||
increaseViewCount, |
|||
likeArticle, |
|||
unlikeArticle, |
|||
createArticle, |
|||
updateArticle, |
|||
deleteArticle, |
|||
batchDeleteArticles, |
|||
publishArticle, |
|||
unpublishArticle, |
|||
} = ArticleApi; |
|||
|
|||
export const { |
|||
getCategories, |
|||
getCategoryTree, |
|||
getCategoryById, |
|||
createCategory, |
|||
updateCategory, |
|||
deleteCategory, |
|||
} = ArticleCategoryApi; |
|||
|
|||
export const { |
|||
getTags, |
|||
getHotTags, |
|||
getTagById, |
|||
searchTags, |
|||
} = ArticleTagApi; |
|||
|
|||
// 默认导出
|
|||
export default ArticleApi; |
@ -0,0 +1,358 @@ |
|||
'use client'; |
|||
|
|||
import { useState, useEffect } from 'react'; |
|||
import { useParams } from 'next/navigation'; |
|||
import Link from 'next/link'; |
|||
import Image from 'next/image'; |
|||
import { ArticleApi, type Article } from '@/api/modules/article'; |
|||
|
|||
const ArticleDetailPage = () => { |
|||
const params = useParams(); |
|||
const articleId = parseInt(params.id as string); |
|||
|
|||
const [article, setArticle] = useState<Article | null>(null); |
|||
const [relatedArticles, setRelatedArticles] = useState<Article[]>([]); |
|||
const [loading, setLoading] = useState(true); |
|||
const [error, setError] = useState<string | null>(null); |
|||
const [liked, setLiked] = useState(false); |
|||
|
|||
useEffect(() => { |
|||
if (articleId) { |
|||
fetchArticle(); |
|||
fetchRelatedArticles(); |
|||
} |
|||
}, [articleId]); |
|||
|
|||
const fetchArticle = async () => { |
|||
try { |
|||
setLoading(true); |
|||
setError(null); |
|||
|
|||
// 增加浏览量
|
|||
await ArticleApi.increaseViewCount(articleId); |
|||
|
|||
// 获取文章详情
|
|||
const response = await ArticleApi.getArticleById(articleId); |
|||
|
|||
if (response.code === 0 && response.data) { |
|||
setArticle(response.data); |
|||
} else { |
|||
setError(response.message || '文章不存在'); |
|||
} |
|||
} catch (error) { |
|||
console.error('获取文章失败:', error); |
|||
setError('网络错误,请稍后重试'); |
|||
|
|||
// 设置模拟文章数据
|
|||
setArticle({ |
|||
id: articleId, |
|||
title: '企业数字化转型的关键要素与实施策略', |
|||
content: ` |
|||
<h2>引言</h2> |
|||
<p>在当今快速发展的数字化时代,企业数字化转型已经不再是一个选择题,而是一个必答题。随着云计算、大数据、人工智能、物联网等新兴技术的快速发展,传统企业面临着前所未有的挑战和机遇。</p> |
|||
|
|||
<h2>数字化转型的核心要素</h2> |
|||
<h3>1. 战略规划</h3> |
|||
<p>数字化转型首先需要明确的战略规划。企业需要从顶层设计出发,制定符合自身发展阶段和行业特点的数字化转型战略。这包括:</p> |
|||
<ul> |
|||
<li>明确转型目标和愿景</li> |
|||
<li>制定分阶段实施计划</li> |
|||
<li>建立评估和监控机制</li> |
|||
<li>确保资源投入和组织保障</li> |
|||
</ul> |
|||
|
|||
<h3>2. 技术基础设施</h3> |
|||
<p>强大的技术基础设施是数字化转型的基石。企业需要构建灵活、可扩展、安全的IT架构,包括:</p> |
|||
<ul> |
|||
<li>云计算平台建设</li> |
|||
<li>数据中心现代化</li> |
|||
<li>网络安全体系完善</li> |
|||
<li>移动办公环境搭建</li> |
|||
</ul> |
|||
|
|||
<h3>3. 数据治理</h3> |
|||
<p>数据是数字化转型的核心资产。企业需要建立完善的数据治理体系:</p> |
|||
<ul> |
|||
<li>数据标准化和规范化</li> |
|||
<li>数据质量管理</li> |
|||
<li>数据安全和隐私保护</li> |
|||
<li>数据分析和应用</li> |
|||
</ul> |
|||
|
|||
<h2>实施策略</h2> |
|||
<h3>分步骤实施</h3> |
|||
<p>数字化转型是一个长期的过程,需要分步骤、有计划地推进。建议采用以下策略:</p> |
|||
<ol> |
|||
<li><strong>评估现状</strong>:全面评估企业当前的数字化水平</li> |
|||
<li><strong>制定规划</strong>:基于评估结果制定详细的转型规划</li> |
|||
<li><strong>试点先行</strong>:选择关键业务领域进行试点</li> |
|||
<li><strong>逐步推广</strong>:在试点成功的基础上逐步推广</li> |
|||
<li><strong>持续优化</strong>:根据实施效果持续优化和改进</li> |
|||
</ol> |
|||
|
|||
<h2>成功案例分析</h2> |
|||
<p>许多企业在数字化转型方面取得了显著成效。例如,某制造企业通过实施智能制造系统,生产效率提升了30%,产品质量得到显著改善。某零售企业通过数字化营销平台,客户转化率提升了50%。</p> |
|||
|
|||
<h2>结论</h2> |
|||
<p>数字化转型是企业适应数字时代、保持竞争优势的必由之路。成功的数字化转型需要企业在战略、技术、人才、文化等多个维度进行系统性变革。只有坚持以客户为中心,以数据为驱动,以技术为支撑,企业才能在数字化转型的道路上取得成功。</p> |
|||
`,
|
|||
excerpt: '在数字化时代,企业需要从战略、技术、人才等多个维度进行全面转型,以适应快速变化的市场环境。本文深入分析了企业数字化转型的关键要素和实施策略。', |
|||
thumbnail: '/article-detail.jpg', |
|||
author: '张三', |
|||
authorId: 1, |
|||
categoryId: 1, |
|||
categoryName: '行业洞察', |
|||
tags: ['数字化转型', '企业管理', '技术创新'], |
|||
status: 'published', |
|||
viewCount: 1250, |
|||
likeCount: 89, |
|||
commentCount: 23, |
|||
isTop: false, |
|||
isRecommend: true, |
|||
publishTime: '2024-01-15T10:30:00Z', |
|||
createTime: '2024-01-15T10:30:00Z', |
|||
updateTime: '2024-01-15T10:30:00Z', |
|||
seoTitle: '企业数字化转型的关键要素与实施策略 - 专业指南', |
|||
seoKeywords: '数字化转型,企业管理,技术创新,战略规划', |
|||
seoDescription: '深入解析企业数字化转型的核心要素,提供实用的实施策略和成功案例分析,助力企业在数字时代取得成功。' |
|||
}); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
const fetchRelatedArticles = async () => { |
|||
try { |
|||
const response = await ArticleApi.getRelatedArticles(articleId, 4); |
|||
if (response.code === 0 && response.data) { |
|||
setRelatedArticles(response.data); |
|||
} |
|||
} catch (error) { |
|||
console.error('获取相关文章失败:', error); |
|||
// 设置模拟相关文章数据
|
|||
setRelatedArticles([]); |
|||
} |
|||
}; |
|||
|
|||
const handleLike = async () => { |
|||
try { |
|||
if (liked) { |
|||
await ArticleApi.unlikeArticle(articleId); |
|||
setLiked(false); |
|||
if (article) { |
|||
setArticle({ ...article, likeCount: article.likeCount - 1 }); |
|||
} |
|||
} else { |
|||
await ArticleApi.likeArticle(articleId); |
|||
setLiked(true); |
|||
if (article) { |
|||
setArticle({ ...article, likeCount: article.likeCount + 1 }); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('点赞操作失败:', error); |
|||
} |
|||
}; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: 'long', |
|||
day: 'numeric', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}); |
|||
}; |
|||
|
|||
const formatNumber = (num: number) => { |
|||
if (num >= 1000) { |
|||
return (num / 1000).toFixed(1) + 'k'; |
|||
} |
|||
return num.toString(); |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<div className="min-h-screen bg-gray-50"> |
|||
<div className="container py-12"> |
|||
<div className="max-w-4xl mx-auto"> |
|||
<div className="animate-pulse"> |
|||
<div className="h-8 bg-gray-300 rounded mb-4"></div> |
|||
<div className="h-6 bg-gray-300 rounded mb-8 w-3/4"></div> |
|||
<div className="h-64 bg-gray-300 rounded mb-8"></div> |
|||
<div className="space-y-4"> |
|||
<div className="h-4 bg-gray-300 rounded"></div> |
|||
<div className="h-4 bg-gray-300 rounded"></div> |
|||
<div className="h-4 bg-gray-300 rounded w-5/6"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
if (error || !article) { |
|||
return ( |
|||
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> |
|||
<div className="text-center"> |
|||
<div className="text-6xl text-gray-400 mb-4">404</div> |
|||
<h1 className="text-2xl font-bold text-gray-900 mb-4">文章不存在</h1> |
|||
<p className="text-gray-600 mb-8">{error || '您访问的文章可能已被删除或不存在'}</p> |
|||
<Link href="/articles" className="btn btn-primary"> |
|||
返回文章列表 |
|||
</Link> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<div className="min-h-screen bg-gray-50"> |
|||
{/* 面包屑导航 */} |
|||
<div className="bg-white border-b border-gray-200"> |
|||
<div className="container py-4"> |
|||
<nav className="flex items-center space-x-2 text-sm text-gray-600"> |
|||
<Link href="/" className="hover:text-blue-600">首页</Link> |
|||
<span>/</span> |
|||
<Link href="/articles" className="hover:text-blue-600">新闻资讯</Link> |
|||
<span>/</span> |
|||
<span className="text-gray-900">{article.title}</span> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="container py-12"> |
|||
<div className="max-w-4xl mx-auto"> |
|||
{/* 文章头部 */} |
|||
<header className="bg-white rounded-lg shadow-sm p-8 mb-8"> |
|||
{/* 分类标签 */} |
|||
{article.categoryName && ( |
|||
<div className="mb-4"> |
|||
<span className="inline-block px-3 py-1 text-sm font-medium bg-blue-100 text-blue-800 rounded-full"> |
|||
{article.categoryName} |
|||
</span> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 文章标题 */} |
|||
<h1 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-6"> |
|||
{article.title} |
|||
</h1> |
|||
|
|||
{/* 文章元信息 */} |
|||
<div className="flex flex-wrap items-center justify-between text-gray-600 mb-6"> |
|||
<div className="flex items-center space-x-6"> |
|||
<span className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> |
|||
</svg> |
|||
{article.author} |
|||
</span> |
|||
<span className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
{formatDate(article.createTime)} |
|||
</span> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-4"> |
|||
<span className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> |
|||
</svg> |
|||
{formatNumber(article.viewCount)} |
|||
</span> |
|||
<button |
|||
onClick={handleLike} |
|||
className={`flex items-center transition-colors duration-300 ${ |
|||
liked ? 'text-red-500' : 'text-gray-600 hover:text-red-500' |
|||
}`}
|
|||
> |
|||
<svg className="w-5 h-5 mr-1" fill={liked ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> |
|||
</svg> |
|||
{formatNumber(article.likeCount)} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 文章摘要 */} |
|||
{article.excerpt && ( |
|||
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6"> |
|||
<p className="text-blue-800 font-medium">{article.excerpt}</p> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 标签 */} |
|||
{article.tags && article.tags.length > 0 && ( |
|||
<div className="flex flex-wrap gap-2"> |
|||
{article.tags.map((tag, index) => ( |
|||
<span |
|||
key={index} |
|||
className="inline-block px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full" |
|||
> |
|||
#{tag} |
|||
</span> |
|||
))} |
|||
</div> |
|||
)} |
|||
</header> |
|||
|
|||
{/* 文章内容 */} |
|||
<article className="bg-white rounded-lg shadow-sm p-8 mb-8"> |
|||
<div |
|||
className="prose prose-lg max-w-none" |
|||
dangerouslySetInnerHTML={{ __html: article.content }} |
|||
/> |
|||
</article> |
|||
|
|||
{/* 相关文章 */} |
|||
{relatedArticles.length > 0 && ( |
|||
<section className="bg-white rounded-lg shadow-sm p-8"> |
|||
<h3 className="text-2xl font-bold mb-6">相关文章</h3> |
|||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|||
{relatedArticles.map((relatedArticle) => ( |
|||
<Link |
|||
key={relatedArticle.id} |
|||
href={`/articles/${relatedArticle.id}`} |
|||
className="flex space-x-4 p-4 rounded-lg hover:bg-gray-50 transition-colors duration-300" |
|||
> |
|||
<Image |
|||
src={relatedArticle.thumbnail || '/placeholder-article.jpg'} |
|||
alt={relatedArticle.title} |
|||
width={120} |
|||
height={80} |
|||
className="rounded-lg object-cover flex-shrink-0" |
|||
onError={(e) => { |
|||
const target = e.target as HTMLImageElement; |
|||
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjgwIiB2aWV3Qm94PSIwIDAgMTIwIDgwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB3aWR0aD0iMTIwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik01MCAzMEg3MFY1MEg1MFYzMFoiIGZpbGw9IiM5Q0EzQUYiLz4KPHA+CjxwYXRoIGQ9Ik00MCA0MEg4MFY0MEg0MFoiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+Cg=='; |
|||
}} |
|||
/> |
|||
<div className="flex-1"> |
|||
<h4 className="font-semibold text-gray-900 mb-2 line-clamp-2"> |
|||
{relatedArticle.title} |
|||
</h4> |
|||
<p className="text-sm text-gray-600 mb-2 line-clamp-2"> |
|||
{relatedArticle.excerpt} |
|||
</p> |
|||
<div className="flex items-center justify-between text-xs text-gray-500"> |
|||
<span>{relatedArticle.categoryName}</span> |
|||
<span>{formatDate(relatedArticle.createTime)}</span> |
|||
</div> |
|||
</div> |
|||
</Link> |
|||
))} |
|||
</div> |
|||
</section> |
|||
)} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ArticleDetailPage; |
@ -0,0 +1,380 @@ |
|||
'use client'; |
|||
|
|||
import { useState, useEffect } from 'react'; |
|||
import Link from 'next/link'; |
|||
import Image from 'next/image'; |
|||
import { ArticleApi, ArticleCategoryApi, type Article, type ArticleCategory } from '@/api/modules/article'; |
|||
|
|||
const ArticlesPage = () => { |
|||
const [articles, setArticles] = useState<Article[]>([]); |
|||
const [categories, setCategories] = useState<ArticleCategory[]>([]); |
|||
const [loading, setLoading] = useState(true); |
|||
const [error, setError] = useState<string | null>(null); |
|||
const [currentPage, setCurrentPage] = useState(1); |
|||
const [totalPages, setTotalPages] = useState(1); |
|||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null); |
|||
const [searchKeyword, setSearchKeyword] = useState(''); |
|||
|
|||
const pageSize = 12; |
|||
|
|||
useEffect(() => { |
|||
fetchCategories(); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
fetchArticles(); |
|||
}, [currentPage, selectedCategory, searchKeyword]); |
|||
|
|||
const fetchCategories = async () => { |
|||
try { |
|||
const response = await ArticleCategoryApi.getCategories(); |
|||
if (response.code === 0 && response.data) { |
|||
setCategories(response.data); |
|||
} |
|||
} catch (error) { |
|||
console.error('获取分类失败:', error); |
|||
// 设置模拟分类数据
|
|||
setCategories([ |
|||
{ id: 1, name: '行业洞察', slug: 'industry', description: '', parentId: 0, sortOrder: 1, articleCount: 15, status: 1, createTime: '2024-01-01T00:00:00Z' }, |
|||
{ id: 2, name: '技术分享', slug: 'tech', description: '', parentId: 0, sortOrder: 2, articleCount: 23, status: 1, createTime: '2024-01-01T00:00:00Z' }, |
|||
{ id: 3, name: '前沿科技', slug: 'innovation', description: '', parentId: 0, sortOrder: 3, articleCount: 18, status: 1, createTime: '2024-01-01T00:00:00Z' }, |
|||
{ id: 4, name: '企业动态', slug: 'news', description: '', parentId: 0, sortOrder: 4, articleCount: 12, status: 1, createTime: '2024-01-01T00:00:00Z' }, |
|||
]); |
|||
} |
|||
}; |
|||
|
|||
const fetchArticles = async () => { |
|||
try { |
|||
setLoading(true); |
|||
setError(null); |
|||
|
|||
let response; |
|||
if (searchKeyword) { |
|||
response = await ArticleApi.searchArticles(searchKeyword, { |
|||
page: currentPage, |
|||
limit: pageSize, |
|||
categoryId: selectedCategory || undefined, |
|||
status: 'published', |
|||
}); |
|||
} else { |
|||
response = await ArticleApi.getArticles(); |
|||
} |
|||
|
|||
if (response.code === 0 && response.data) { |
|||
setArticles(response.data.list); |
|||
setTotalPages(Math.ceil(response.data.count / pageSize)); |
|||
} else { |
|||
setError(response.message || '获取文章失败'); |
|||
} |
|||
} catch (error) { |
|||
console.error('获取文章失败:', error); |
|||
setError('网络错误,请稍后重试'); |
|||
|
|||
// 设置模拟文章数据
|
|||
const mockArticles: Article[] = Array.from({ length: pageSize }, (_, index) => ({ |
|||
id: (currentPage - 1) * pageSize + index + 1, |
|||
title: `文章标题 ${(currentPage - 1) * pageSize + index + 1}`, |
|||
content: '这是文章的详细内容...', |
|||
excerpt: '这是文章的摘要内容,介绍了文章的主要观点和核心内容...', |
|||
thumbnail: `/article-${(index % 3) + 1}.jpg`, |
|||
author: ['张三', '李四', '王五'][index % 3], |
|||
authorId: (index % 3) + 1, |
|||
categoryId: selectedCategory || ((index % 4) + 1), |
|||
categoryName: categories.find(c => c.id === (selectedCategory || ((index % 4) + 1)))?.name || '默认分类', |
|||
tags: ['标签1', '标签2'], |
|||
status: 'published', |
|||
viewCount: Math.floor(Math.random() * 2000) + 100, |
|||
likeCount: Math.floor(Math.random() * 200) + 10, |
|||
commentCount: Math.floor(Math.random() * 50) + 1, |
|||
isTop: index === 0, |
|||
isRecommend: index < 3, |
|||
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), |
|||
updateTime: new Date().toISOString(), |
|||
})); |
|||
|
|||
setArticles(mockArticles); |
|||
setTotalPages(5); // 模拟总页数
|
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
const handleSearch = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
setCurrentPage(1); |
|||
fetchArticles(); |
|||
}; |
|||
|
|||
const handleCategoryChange = (categoryId: number | null) => { |
|||
setSelectedCategory(categoryId); |
|||
setCurrentPage(1); |
|||
}; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: 'long', |
|||
day: 'numeric' |
|||
}); |
|||
}; |
|||
|
|||
const formatNumber = (num: number) => { |
|||
if (num >= 1000) { |
|||
return (num / 1000).toFixed(1) + 'k'; |
|||
} |
|||
return num.toString(); |
|||
}; |
|||
|
|||
return ( |
|||
<div className="min-h-screen bg-gray-50"> |
|||
{/* 页面头部 */} |
|||
<div className="bg-white border-b border-gray-200"> |
|||
<div className="container py-12"> |
|||
<div className="text-center"> |
|||
<h1 className="text-4xl font-bold text-gray-900 mb-4">新闻资讯</h1> |
|||
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> |
|||
了解行业动态,掌握前沿技术,获取专业见解 |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="container py-12"> |
|||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8"> |
|||
{/* 侧边栏 */} |
|||
<div className="lg:col-span-1"> |
|||
{/* 搜索框 */} |
|||
<div className="bg-white rounded-lg shadow-sm p-6 mb-8"> |
|||
<h3 className="text-lg font-semibold mb-4">搜索文章</h3> |
|||
<form onSubmit={handleSearch}> |
|||
<div className="flex"> |
|||
<input |
|||
type="text" |
|||
value={searchKeyword} |
|||
onChange={(e) => setSearchKeyword(e.target.value)} |
|||
placeholder="输入关键词..." |
|||
className="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" |
|||
/> |
|||
<button |
|||
type="submit" |
|||
className="px-4 py-2 bg-blue-600 text-white rounded-r-lg hover:bg-blue-700 transition-colors duration-300" |
|||
> |
|||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
|
|||
{/* 分类筛选 */} |
|||
<div className="bg-white rounded-lg shadow-sm p-6"> |
|||
<h3 className="text-lg font-semibold mb-4">文章分类</h3> |
|||
<ul className="space-y-2"> |
|||
<li> |
|||
<button |
|||
onClick={() => handleCategoryChange(null)} |
|||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors duration-300 ${ |
|||
selectedCategory === null |
|||
? 'bg-blue-100 text-blue-600' |
|||
: 'text-gray-700 hover:bg-gray-100' |
|||
}`}
|
|||
> |
|||
全部文章 |
|||
</button> |
|||
</li> |
|||
{categories.map((category) => ( |
|||
<li key={category.id}> |
|||
<button |
|||
onClick={() => handleCategoryChange(category.id)} |
|||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors duration-300 flex justify-between items-center ${ |
|||
selectedCategory === category.id |
|||
? 'bg-blue-100 text-blue-600' |
|||
: 'text-gray-700 hover:bg-gray-100' |
|||
}`}
|
|||
> |
|||
<span>{category.name}</span> |
|||
<span className="text-sm text-gray-500">({category.articleCount})</span> |
|||
</button> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 主内容区 */} |
|||
<div className="lg:col-span-3"> |
|||
{/* 筛选信息 */} |
|||
<div className="flex justify-between items-center mb-8"> |
|||
<div className="text-gray-600"> |
|||
{selectedCategory && ( |
|||
<span> |
|||
分类:<span className="text-blue-600 font-medium"> |
|||
{categories.find(c => c.id === selectedCategory)?.name} |
|||
</span> |
|||
</span> |
|||
)} |
|||
{searchKeyword && ( |
|||
<span className="ml-4"> |
|||
搜索:<span className="text-blue-600 font-medium">"{searchKeyword}"</span> |
|||
</span> |
|||
)} |
|||
</div> |
|||
<div className="text-gray-600"> |
|||
共 {articles.length} 篇文章 |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 错误提示 */} |
|||
{error && ( |
|||
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-lg mb-8"> |
|||
<p>{error}</p> |
|||
<p className="text-sm mt-2">以下为示例内容</p> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 文章列表 */} |
|||
{loading ? ( |
|||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8"> |
|||
{[1, 2, 3, 4, 5, 6].map((i) => ( |
|||
<div key={i} className="article-card animate-pulse"> |
|||
<div className="w-full h-48 bg-gray-300"></div> |
|||
<div className="article-content"> |
|||
<div className="h-4 bg-gray-300 rounded mb-3"></div> |
|||
<div className="h-6 bg-gray-300 rounded mb-3"></div> |
|||
<div className="h-4 bg-gray-300 rounded mb-4"></div> |
|||
<div className="h-4 bg-gray-300 rounded"></div> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) : ( |
|||
<> |
|||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8"> |
|||
{articles.map((article, index) => ( |
|||
<article |
|||
key={article.id} |
|||
className="article-card fade-in-up" |
|||
style={{ animationDelay: `${index * 0.1}s` }} |
|||
> |
|||
{/* 文章图片 */} |
|||
<div className="relative overflow-hidden"> |
|||
<Image |
|||
src={article.thumbnail || '/placeholder-article.jpg'} |
|||
alt={article.title} |
|||
width={400} |
|||
height={200} |
|||
className="article-image hover:scale-105 transition-transform duration-300" |
|||
onError={(e) => { |
|||
const target = e.target as HTMLImageElement; |
|||
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDQwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xNzUgNzVIMjI1VjEyNUgxNzVWNzVaIiBmaWxsPSIjOUNBM0FGIi8+CjxwYXRoIGQ9Ik0xNTAgMTAwSDI1MFYxMDBIMTUwWiIgZmlsbD0iIzlDQTNBRiIvPgo8L3N2Zz4K'; |
|||
}} |
|||
/> |
|||
|
|||
{/* 置顶标签 */} |
|||
{article.isTop && ( |
|||
<div className="absolute top-4 left-4 bg-red-500 text-white px-2 py-1 text-xs rounded"> |
|||
置顶 |
|||
</div> |
|||
)} |
|||
|
|||
{/* 推荐标签 */} |
|||
{article.isRecommend && ( |
|||
<div className="absolute top-4 right-4 bg-blue-500 text-white px-2 py-1 text-xs rounded"> |
|||
推荐 |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 文章内容 */} |
|||
<div className="article-content"> |
|||
{/* 分类标签 */} |
|||
{article.categoryName && ( |
|||
<span className="article-category"> |
|||
{article.categoryName} |
|||
</span> |
|||
)} |
|||
|
|||
{/* 文章标题 */} |
|||
<h3 className="article-title"> |
|||
<Link href={`/articles/${article.id}`}> |
|||
{article.title} |
|||
</Link> |
|||
</h3> |
|||
|
|||
{/* 文章摘要 */} |
|||
<p className="article-excerpt"> |
|||
{article.excerpt || article.content?.substring(0, 100) + '...'} |
|||
</p> |
|||
|
|||
{/* 文章元信息 */} |
|||
<div className="article-meta"> |
|||
<div className="flex items-center space-x-4"> |
|||
<span className="flex items-center"> |
|||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> |
|||
</svg> |
|||
{formatNumber(article.viewCount)} |
|||
</span> |
|||
<span className="flex items-center"> |
|||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> |
|||
</svg> |
|||
{formatNumber(article.likeCount)} |
|||
</span> |
|||
</div> |
|||
<span>{formatDate(article.createTime)}</span> |
|||
</div> |
|||
</div> |
|||
</article> |
|||
))} |
|||
</div> |
|||
|
|||
{/* 分页 */} |
|||
{totalPages > 1 && ( |
|||
<div className="flex justify-center mt-12"> |
|||
<nav className="flex items-center space-x-2"> |
|||
<button |
|||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} |
|||
disabled={currentPage === 1} |
|||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" |
|||
> |
|||
上一页 |
|||
</button> |
|||
|
|||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( |
|||
<button |
|||
key={page} |
|||
onClick={() => setCurrentPage(page)} |
|||
className={`px-3 py-2 text-sm font-medium rounded-md ${ |
|||
currentPage === page |
|||
? 'text-blue-600 bg-blue-50 border border-blue-300' |
|||
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50' |
|||
}`}
|
|||
> |
|||
{page} |
|||
</button> |
|||
))} |
|||
|
|||
<button |
|||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))} |
|||
disabled={currentPage === totalPages} |
|||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" |
|||
> |
|||
下一页 |
|||
</button> |
|||
</nav> |
|||
</div> |
|||
)} |
|||
</> |
|||
)} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ArticlesPage; |
@ -0,0 +1,158 @@ |
|||
import Link from 'next/link'; |
|||
|
|||
const Footer = () => { |
|||
const currentYear = new Date().getFullYear(); |
|||
|
|||
const footerLinks = { |
|||
company: [ |
|||
{ name: '关于我们', href: '/about' }, |
|||
{ name: '企业文化', href: '/culture' }, |
|||
{ name: '发展历程', href: '/history' }, |
|||
{ name: '招聘信息', href: '/careers' }, |
|||
], |
|||
services: [ |
|||
{ name: '产品服务', href: '/services' }, |
|||
{ name: '解决方案', href: '/solutions' }, |
|||
{ name: '技术支持', href: '/support' }, |
|||
{ name: '客户案例', href: '/cases' }, |
|||
], |
|||
resources: [ |
|||
{ name: '新闻资讯', href: '/articles' }, |
|||
{ name: '行业动态', href: '/news' }, |
|||
{ name: '技术博客', href: '/blog' }, |
|||
{ name: '下载中心', href: '/downloads' }, |
|||
], |
|||
contact: [ |
|||
{ name: '联系我们', href: '/contact' }, |
|||
{ name: '在线客服', href: '/service' }, |
|||
{ name: '意见反馈', href: '/feedback' }, |
|||
{ name: '合作伙伴', href: '/partners' }, |
|||
], |
|||
}; |
|||
|
|||
return ( |
|||
<footer className="footer"> |
|||
<div className="container"> |
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8"> |
|||
{/* 公司信息 */} |
|||
<div className="lg:col-span-2"> |
|||
<div className="footer-section"> |
|||
<h3 className="footer-title">企业官网</h3> |
|||
<p className="text-gray-300 mb-6 max-w-md"> |
|||
我们致力于为客户提供优质的产品和服务,以创新驱动发展,以质量赢得信赖。 |
|||
专业、诚信、创新是我们的核心价值观。 |
|||
</p> |
|||
<div className="space-y-2 text-gray-300"> |
|||
<div className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20"> |
|||
<path fillRule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clipRule="evenodd" /> |
|||
</svg> |
|||
<span>北京市朝阳区某某大厦 888 号</span> |
|||
</div> |
|||
<div className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20"> |
|||
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" /> |
|||
</svg> |
|||
<span>400-888-8888</span> |
|||
</div> |
|||
<div className="flex items-center"> |
|||
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20"> |
|||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /> |
|||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /> |
|||
</svg> |
|||
<span>contact@company.com</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 公司链接 */} |
|||
<div className="footer-section"> |
|||
<h3 className="footer-title">公司</h3> |
|||
<ul className="space-y-3"> |
|||
{footerLinks.company.map((link) => ( |
|||
<li key={link.name}> |
|||
<Link href={link.href} className="footer-link"> |
|||
{link.name} |
|||
</Link> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
|
|||
{/* 服务链接 */} |
|||
<div className="footer-section"> |
|||
<h3 className="footer-title">服务</h3> |
|||
<ul className="space-y-3"> |
|||
{footerLinks.services.map((link) => ( |
|||
<li key={link.name}> |
|||
<Link href={link.href} className="footer-link"> |
|||
{link.name} |
|||
</Link> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
|
|||
{/* 资源链接 */} |
|||
<div className="footer-section"> |
|||
<h3 className="footer-title">资源</h3> |
|||
<ul className="space-y-3"> |
|||
{footerLinks.resources.map((link) => ( |
|||
<li key={link.name}> |
|||
<Link href={link.href} className="footer-link"> |
|||
{link.name} |
|||
</Link> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 底部版权信息 */} |
|||
<div className="border-t border-gray-800 mt-12 pt-8"> |
|||
<div className="flex flex-col md:flex-row justify-between items-center"> |
|||
<div className="text-gray-400 text-sm"> |
|||
© {currentYear} 企业官网. 保留所有权利. |
|||
</div> |
|||
<div className="flex space-x-6 mt-4 md:mt-0"> |
|||
<Link href="/privacy" className="text-gray-400 hover:text-white text-sm transition-colors duration-300"> |
|||
隐私政策 |
|||
</Link> |
|||
<Link href="/terms" className="text-gray-400 hover:text-white text-sm transition-colors duration-300"> |
|||
服务条款 |
|||
</Link> |
|||
<Link href="/sitemap" className="text-gray-400 hover:text-white text-sm transition-colors duration-300"> |
|||
网站地图 |
|||
</Link> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 社交媒体链接 */} |
|||
<div className="flex justify-center space-x-6 mt-8"> |
|||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-300"> |
|||
<span className="sr-only">微信</span> |
|||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18 0 .659-.52 1.188-1.162 1.188-.642 0-1.162-.529-1.162-1.188 0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18 0 .659-.52 1.188-1.162 1.188-.642 0-1.162-.529-1.162-1.188 0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229 2.579 0 5.061-1.314 6.266-3.276a.545.545 0 00.034-.31.545.545 0 00-.169-.267c-.293-.24-.652-.379-1.019-.379-.642 0-1.162.529-1.162 1.188 0 .659.52 1.188 1.162 1.188.367 0 .726-.139 1.019-.379a.545.545 0 00.169-.267.545.545 0 00-.034-.31c-1.205-1.962-3.687-3.276-6.266-3.276-3.218 0-5.942 1.776-6.884 4.229-.907 2.5.06 4.792 1.78 6.22 1.534 1.274 3.483 1.838 5.28 1.786z"/> |
|||
</svg> |
|||
</a> |
|||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-300"> |
|||
<span className="sr-only">微博</span> |
|||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M9.996 14.456c-1.747 0-3.166-1.418-3.166-3.166s1.419-3.166 3.166-3.166 3.166 1.418 3.166 3.166-1.419 3.166-3.166 3.166zm0-5.332c-1.197 0-2.166.969-2.166 2.166s.969 2.166 2.166 2.166 2.166-.969 2.166-2.166-.969-2.166-2.166-2.166z"/> |
|||
</svg> |
|||
</a> |
|||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-300"> |
|||
<span className="sr-only">QQ</span> |
|||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z"/> |
|||
</svg> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</footer> |
|||
); |
|||
}; |
|||
|
|||
export default Footer; |
@ -0,0 +1,139 @@ |
|||
'use client'; |
|||
|
|||
import { useState } from 'react'; |
|||
import Link from 'next/link'; |
|||
import { usePathname } from 'next/navigation'; |
|||
|
|||
const Header = () => { |
|||
const [isMenuOpen, setIsMenuOpen] = useState(false); |
|||
const pathname = usePathname(); |
|||
|
|||
const navigation = [ |
|||
{ name: '首页', href: '/' }, |
|||
{ name: '关于我们', href: '/about' }, |
|||
{ name: '产品服务', href: '/services' }, |
|||
{ name: '新闻资讯', href: '/articles' }, |
|||
{ name: '联系我们', href: '/contact' }, |
|||
]; |
|||
|
|||
const isActive = (href: string) => { |
|||
if (href === '/') { |
|||
return pathname === '/'; |
|||
} |
|||
return pathname.startsWith(href); |
|||
}; |
|||
|
|||
return ( |
|||
<header className="navbar"> |
|||
<div className="container"> |
|||
<div className="flex items-center justify-between h-16"> |
|||
{/* Logo */} |
|||
<div className="flex-shrink-0"> |
|||
<Link href="/" className="navbar-brand"> |
|||
企业官网 |
|||
</Link> |
|||
</div> |
|||
|
|||
{/* Desktop Navigation */} |
|||
<nav className="hidden md:block"> |
|||
<div className="navbar-nav"> |
|||
{navigation.map((item) => ( |
|||
<Link |
|||
key={item.name} |
|||
href={item.href} |
|||
className={`navbar-link ${isActive(item.href) ? 'active' : ''}`} |
|||
> |
|||
{item.name} |
|||
</Link> |
|||
))} |
|||
</div> |
|||
</nav> |
|||
|
|||
{/* CTA Button */} |
|||
<div className="hidden md:block"> |
|||
<Link href="/contact" className="btn btn-primary"> |
|||
免费咨询 |
|||
</Link> |
|||
</div> |
|||
|
|||
{/* Mobile menu button */} |
|||
<div className="md:hidden"> |
|||
<button |
|||
onClick={() => setIsMenuOpen(!isMenuOpen)} |
|||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" |
|||
aria-expanded="false" |
|||
> |
|||
<span className="sr-only">打开主菜单</span> |
|||
{!isMenuOpen ? ( |
|||
<svg |
|||
className="block h-6 w-6" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
aria-hidden="true" |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
strokeWidth={2} |
|||
d="M4 6h16M4 12h16M4 18h16" |
|||
/> |
|||
</svg> |
|||
) : ( |
|||
<svg |
|||
className="block h-6 w-6" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
aria-hidden="true" |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
strokeWidth={2} |
|||
d="M6 18L18 6M6 6l12 12" |
|||
/> |
|||
</svg> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Mobile Navigation */} |
|||
{isMenuOpen && ( |
|||
<div className="md:hidden"> |
|||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-white border-t border-gray-200"> |
|||
{navigation.map((item) => ( |
|||
<Link |
|||
key={item.name} |
|||
href={item.href} |
|||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors duration-300 ${ |
|||
isActive(item.href) |
|||
? 'text-blue-600 bg-blue-50' |
|||
: 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' |
|||
}`}
|
|||
onClick={() => setIsMenuOpen(false)} |
|||
> |
|||
{item.name} |
|||
</Link> |
|||
))} |
|||
<div className="px-3 py-2"> |
|||
<Link |
|||
href="/contact" |
|||
className="btn btn-primary w-full" |
|||
onClick={() => setIsMenuOpen(false)} |
|||
> |
|||
免费咨询 |
|||
</Link> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</header> |
|||
); |
|||
}; |
|||
|
|||
export default Header; |
@ -0,0 +1,120 @@ |
|||
# ArticlesSection 组件修复说明 |
|||
|
|||
## 修复的问题 |
|||
|
|||
### 1. TypeScript 类型不匹配问题 |
|||
|
|||
**问题描述:** |
|||
- `setArticles(response?.list)` 报错:TypeScript 类型不匹配 |
|||
- `pageCmsArticle` 函数返回类型与组件状态类型不一致 |
|||
|
|||
**解决方案:** |
|||
- 将 `articles` 状态类型从 `PageResult<CmsArticle>` 改为 `CmsArticle[]` |
|||
- 使用类型断言安全地处理 API 响应 |
|||
- 添加数组类型检查确保数据安全 |
|||
|
|||
### 2. 字段映射问题 |
|||
|
|||
**问题描述:** |
|||
`CmsArticle` 类型的字段名与组件中使用的字段名不匹配: |
|||
|
|||
| 组件中使用的字段 | CmsArticle 实际字段 | 修复后 | |
|||
|-----------------|-------------------|--------| |
|||
| `article.id` | `article.articleId` | ✅ 已修复 | |
|||
| `article.thumbnail` | `article.image` | ✅ 已修复 | |
|||
| `article.isTop` | 无对应字段 | ✅ 已移除 | |
|||
| `article.isRecommend` | `article.recommend` | ✅ 已修复 | |
|||
| `article.excerpt` | `article.overview` | ✅ 已修复 | |
|||
| `article.viewCount` | `article.actualViews/virtualViews` | ✅ 已修复 | |
|||
| `article.likeCount` | `article.likes` | ✅ 已修复 | |
|||
|
|||
### 3. 运行时错误修复 |
|||
|
|||
**问题描述:** |
|||
- `formatNumber` 函数在接收 `undefined` 值时报错 |
|||
- `formatDate` 函数缺少边界情况处理 |
|||
|
|||
**解决方案:** |
|||
- 修改函数参数类型支持 `undefined` 和 `null` |
|||
- 添加完善的错误处理和默认值 |
|||
|
|||
## 修复后的代码结构 |
|||
|
|||
### 状态管理 |
|||
```typescript |
|||
const [articles, setArticles] = useState<CmsArticle[]>([]); |
|||
``` |
|||
|
|||
### API 调用 |
|||
```typescript |
|||
const response = await pageCmsArticle({ limit: 6 }); |
|||
const pageResult = response as unknown as PageResult<CmsArticle>; |
|||
if (pageResult && Array.isArray(pageResult.list)) { |
|||
setArticles(pageResult.list); |
|||
} |
|||
``` |
|||
|
|||
### 字段映射 |
|||
```typescript |
|||
// 文章 ID |
|||
key={article.articleId || index} |
|||
|
|||
// 图片 |
|||
src={article.image || '/placeholder-article.jpg'} |
|||
|
|||
// 推荐标签 |
|||
{article.recommend === 1 && (...)} |
|||
|
|||
// 摘要 |
|||
{article.overview || article.content?.substring(0, 100) + '...'} |
|||
|
|||
// 浏览量 |
|||
{formatNumber(article.actualViews || article.virtualViews)} |
|||
|
|||
// 点赞数 |
|||
{formatNumber(article.likes)} |
|||
``` |
|||
|
|||
### 格式化函数 |
|||
```typescript |
|||
const formatNumber = (num?: number | null) => { |
|||
if (num === undefined || num === null || isNaN(num)) { |
|||
return '0'; |
|||
} |
|||
// ... 其他逻辑 |
|||
}; |
|||
|
|||
const formatDate = (dateString?: string | null) => { |
|||
if (!dateString) { |
|||
return '未知时间'; |
|||
} |
|||
// ... 其他逻辑 |
|||
}; |
|||
``` |
|||
|
|||
## 测试建议 |
|||
|
|||
1. **数据完整性测试**: |
|||
- 测试 API 返回完整数据的情况 |
|||
- 测试 API 返回部分字段缺失的情况 |
|||
- 测试 API 返回空数组的情况 |
|||
|
|||
2. **边界情况测试**: |
|||
- 测试 `actualViews` 和 `virtualViews` 都为 `undefined` 的情况 |
|||
- 测试 `likes` 为 `null` 的情况 |
|||
- 测试 `createTime` 为无效日期的情况 |
|||
|
|||
3. **UI 渲染测试**: |
|||
- 验证文章列表正确渲染 |
|||
- 验证推荐标签显示逻辑 |
|||
- 验证图片加载失败时的占位符 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **类型安全**:使用了类型断言来处理 API 响应类型不匹配的问题,在生产环境中建议修复 API 函数的返回类型定义。 |
|||
|
|||
2. **字段兼容性**:如果后端 API 字段发生变化,需要相应更新字段映射。 |
|||
|
|||
3. **性能优化**:考虑添加 loading 状态和错误重试机制。 |
|||
|
|||
4. **数据验证**:建议在生产环境中添加更严格的数据验证逻辑。 |
@ -0,0 +1,173 @@ |
|||
'use client'; |
|||
|
|||
import Image from 'next/image'; |
|||
|
|||
const AboutSection = () => { |
|||
const achievements = [ |
|||
{ |
|||
number: '10+', |
|||
label: '年行业经验', |
|||
description: '深耕行业多年,积累丰富经验' |
|||
}, |
|||
{ |
|||
number: '500+', |
|||
label: '成功项目', |
|||
description: '服务各行各业,项目经验丰富' |
|||
}, |
|||
{ |
|||
number: '50+', |
|||
label: '专业团队', |
|||
description: '技术精湛的专业开发团队' |
|||
}, |
|||
{ |
|||
number: '98%', |
|||
label: '客户满意度', |
|||
description: '客户认可,口碑良好' |
|||
} |
|||
]; |
|||
|
|||
const values = [ |
|||
{ |
|||
title: '创新驱动', |
|||
description: '持续技术创新,引领行业发展', |
|||
icon: ( |
|||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> |
|||
</svg> |
|||
) |
|||
}, |
|||
{ |
|||
title: '质量至上', |
|||
description: '严格质量控制,确保产品卓越', |
|||
icon: ( |
|||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
) |
|||
}, |
|||
{ |
|||
title: '客户第一', |
|||
description: '以客户需求为中心,提供优质服务', |
|||
icon: ( |
|||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> |
|||
</svg> |
|||
) |
|||
}, |
|||
{ |
|||
title: '团队协作', |
|||
description: '高效团队协作,共创美好未来', |
|||
icon: ( |
|||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> |
|||
</svg> |
|||
) |
|||
} |
|||
]; |
|||
|
|||
return ( |
|||
<section className="section-padding"> |
|||
<div className="container"> |
|||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mb-20"> |
|||
{/* 左侧图片 */} |
|||
<div className="relative"> |
|||
<Image |
|||
src="/about-image.jpg" |
|||
alt="关于我们" |
|||
width={600} |
|||
height={400} |
|||
className="rounded-lg shadow-lg" |
|||
onError={(e) => { |
|||
const target = e.target as HTMLImageElement; |
|||
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjQwMCIgdmlld0JveD0iMCAwIDYwMCA0MDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI2MDAiIGhlaWdodD0iNDAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0yNzUgMTc1SDMyNVYyMjVIMjc1VjE3NVoiIGZpbGw9IiM5Q0EzQUYiLz4KPHA+CjxwYXRoIGQ9Ik0yNTAgMjAwSDM1MFYyMDBIMjUwWiIgZmlsbD0iIzlDQTNBRiIvPgo8L3N2Zz4K'; |
|||
}} |
|||
/> |
|||
|
|||
{/* 装饰元素 */} |
|||
<div className="absolute -bottom-6 -right-6 bg-blue-600 text-white p-6 rounded-lg shadow-lg"> |
|||
<div className="text-2xl font-bold">10+</div> |
|||
<div className="text-sm">年专业经验</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 右侧内容 */} |
|||
<div> |
|||
<h2 className="text-3xl lg:text-4xl font-bold mb-6"> |
|||
关于我们 |
|||
</h2> |
|||
<p className="text-lg text-gray-600 mb-6"> |
|||
我们是一家专注于企业数字化转型的科技公司,成立于2014年。 |
|||
凭借深厚的技术积累和丰富的行业经验,我们为各行各业的企业提供专业的解决方案。 |
|||
</p> |
|||
<p className="text-gray-600 mb-8"> |
|||
我们的使命是通过创新的技术和优质的服务,帮助企业实现数字化转型, |
|||
提升运营效率,降低成本,增强竞争力。我们相信技术的力量, |
|||
更相信专业团队的价值。 |
|||
</p> |
|||
|
|||
{/* 成就数据 */} |
|||
<div className="grid grid-cols-2 gap-6"> |
|||
{achievements.map((achievement, index) => ( |
|||
<div key={index} className="text-center lg:text-left"> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2"> |
|||
{achievement.number} |
|||
</div> |
|||
<div className="font-semibold mb-1">{achievement.label}</div> |
|||
<div className="text-sm text-gray-600">{achievement.description}</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 企业价值观 */} |
|||
<div className="text-center mb-16"> |
|||
<h3 className="text-2xl lg:text-3xl font-bold mb-4">我们的价值观</h3> |
|||
<p className="text-lg text-gray-600 max-w-2xl mx-auto"> |
|||
这些核心价值观指导着我们的每一个决策和行动,是我们企业文化的基石 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"> |
|||
{values.map((value, index) => ( |
|||
<div key={index} className="text-center p-6 rounded-lg bg-white shadow-sm hover:shadow-md transition-shadow duration-300"> |
|||
<div className="flex justify-center mb-4"> |
|||
{value.icon} |
|||
</div> |
|||
<h4 className="text-xl font-semibold mb-3">{value.title}</h4> |
|||
<p className="text-gray-600">{value.description}</p> |
|||
</div> |
|||
))} |
|||
</div> |
|||
|
|||
{/* 团队介绍 */} |
|||
<div className="mt-20 text-center"> |
|||
<h3 className="text-2xl lg:text-3xl font-bold mb-6">我们的团队</h3> |
|||
<p className="text-lg text-gray-600 mb-12 max-w-3xl mx-auto"> |
|||
我们拥有一支经验丰富、技术精湛的专业团队。团队成员来自知名互联网公司和传统企业, |
|||
具备深厚的技术功底和丰富的项目经验,能够为客户提供全方位的技术支持和服务。 |
|||
</p> |
|||
|
|||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-8"> |
|||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center"> |
|||
<div> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">15+</div> |
|||
<div className="text-gray-600">资深工程师</div> |
|||
</div> |
|||
<div> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">8+</div> |
|||
<div className="text-gray-600">产品经理</div> |
|||
</div> |
|||
<div> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">5+</div> |
|||
<div className="text-gray-600">设计师</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
}; |
|||
|
|||
export default AboutSection; |
@ -0,0 +1,206 @@ |
|||
'use client'; |
|||
|
|||
import {useState, useEffect} from 'react'; |
|||
import Link from 'next/link'; |
|||
import Image from 'next/image'; |
|||
import {pageCmsArticle} from "@/api/cms/cmsArticle"; |
|||
import {CmsArticle} from "@/api/cms/cmsArticle/model"; |
|||
|
|||
|
|||
const ArticlesSection = () => { |
|||
const [articles, setArticles] = useState<CmsArticle[]>([]); |
|||
const [loading, setLoading] = useState(true); |
|||
const [error, setError] = useState<string | null>(null); |
|||
|
|||
useEffect(() => { |
|||
const fetchArticles = async () => { |
|||
try { |
|||
setLoading(true); |
|||
setError(null); |
|||
|
|||
// 获取最新文章
|
|||
const response = await pageCmsArticle({ limit: 6 }); |
|||
setArticles(response?.list || []); |
|||
} catch (err) { |
|||
console.error('获取文章失败:', err); |
|||
setError('网络错误,请稍后重试'); |
|||
|
|||
// 设置模拟数据作为后备
|
|||
setArticles([]); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
fetchArticles(); |
|||
}, []); |
|||
|
|||
const formatDate = (dateString?: string | null) => { |
|||
// 处理 undefined 或 null 值
|
|||
if (!dateString) { |
|||
return '未知时间'; |
|||
} |
|||
|
|||
const date = new Date(dateString); |
|||
|
|||
// 检查日期是否有效
|
|||
if (isNaN(date.getTime())) { |
|||
return '无效日期'; |
|||
} |
|||
|
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: 'long', |
|||
day: 'numeric' |
|||
}); |
|||
}; |
|||
|
|||
const formatNumber = (num?: number | null) => { |
|||
// 处理 undefined 或 null 值
|
|||
if (num === undefined || num === null || isNaN(num)) { |
|||
return '0'; |
|||
} |
|||
|
|||
if (num >= 1000) { |
|||
return (num / 1000).toFixed(1) + 'k'; |
|||
} |
|||
return num.toString(); |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<section className="section-padding bg-gray-50"> |
|||
<div className="container"> |
|||
<div className="text-center mb-16"> |
|||
<h2 className="section-title">最新资讯</h2> |
|||
<p className="section-subtitle"> |
|||
了解行业动态,掌握前沿技术,获取专业见解 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> |
|||
{[1, 2, 3, 4, 5, 6].map((i) => ( |
|||
<div key={i} className="article-card animate-pulse"> |
|||
<div className="w-full h-48 bg-gray-300"></div> |
|||
<div className="article-content"> |
|||
<div className="h-4 bg-gray-300 rounded mb-3"></div> |
|||
<div className="h-6 bg-gray-300 rounded mb-3"></div> |
|||
<div className="h-4 bg-gray-300 rounded mb-4"></div> |
|||
<div className="h-4 bg-gray-300 rounded"></div> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<section className="section-padding bg-gray-50"> |
|||
<div className="container"> |
|||
<div className="text-center mb-16"> |
|||
<h2 className="section-title">最新资讯</h2> |
|||
<p className="section-subtitle"> |
|||
了解行业动态,掌握前沿技术,获取专业见解 |
|||
</p> |
|||
</div> |
|||
|
|||
{error && ( |
|||
<div |
|||
className="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-lg mb-8 text-center"> |
|||
<p>{error}</p> |
|||
<p className="text-sm mt-2">以下为示例内容</p> |
|||
</div> |
|||
)} |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> |
|||
{articles?.map((article, index) => ( |
|||
<article |
|||
key={article.articleId || index} |
|||
className="article-card fade-in-up" |
|||
style={{animationDelay: `${index * 0.1}s`}} |
|||
> |
|||
{/* 文章图片 */} |
|||
<div className="relative overflow-hidden"> |
|||
<Image |
|||
src={article.image || '/placeholder-article.jpg'} |
|||
alt={article.title || '文章图片'} |
|||
width={400} |
|||
height={200} |
|||
className="article-image hover:scale-105 transition-transform duration-300" |
|||
onError={(e) => { |
|||
const target = e.target as HTMLImageElement; |
|||
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDQwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xNzUgNzVIMjI1VjEyNUgxNzVWNzVaIiBmaWxsPSIjOUNBM0FGIi8+CjxwYXRoIGQ9Ik0xNTAgMTAwSDI1MFYxMDBIMTUwWiIgZmlsbD0iIzlDQTNBRiIvPgo8L3N2Zz4K'; |
|||
}} |
|||
/> |
|||
|
|||
{/* 推荐标签 */} |
|||
{article.recommend === 1 && ( |
|||
<div |
|||
className="absolute top-4 right-4 bg-blue-500 text-white px-2 py-1 text-xs rounded"> |
|||
推荐 |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 文章内容 */} |
|||
<div className="article-content"> |
|||
{/* 分类标签 */} |
|||
{article.categoryName && ( |
|||
<span className="article-category"> |
|||
{article.categoryName} |
|||
</span> |
|||
)} |
|||
|
|||
{/* 文章标题 */} |
|||
<h3 className="article-title"> |
|||
<Link href={`/articles/${article.articleId}`}> |
|||
{article.title} |
|||
</Link> |
|||
</h3> |
|||
|
|||
{/* 文章摘要 */} |
|||
<p className="article-excerpt"> |
|||
{article.overview || article.content?.substring(0, 100) + '...'} |
|||
</p> |
|||
|
|||
{/* 文章元信息 */} |
|||
<div className="article-meta"> |
|||
<div className="flex items-center space-x-4"> |
|||
<span className="flex items-center"> |
|||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} |
|||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} |
|||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> |
|||
</svg> |
|||
{formatNumber(article.actualViews || article.virtualViews)} |
|||
</span> |
|||
<span className="flex items-center"> |
|||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} |
|||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/> |
|||
</svg> |
|||
{formatNumber(article.likes)} |
|||
</span> |
|||
</div> |
|||
<span>{formatDate(article.createTime)}</span> |
|||
</div> |
|||
</div> |
|||
</article> |
|||
))} |
|||
</div> |
|||
|
|||
{/* 查看更多按钮 */} |
|||
<div className="text-center mt-12"> |
|||
<Link href="/articles" className="btn btn-outline"> |
|||
查看更多文章 |
|||
</Link> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
}; |
|||
|
|||
export default ArticlesSection; |
@ -0,0 +1,279 @@ |
|||
'use client'; |
|||
|
|||
import { useState } from 'react'; |
|||
|
|||
const ContactSection = () => { |
|||
const [formData, setFormData] = useState({ |
|||
name: '', |
|||
email: '', |
|||
phone: '', |
|||
company: '', |
|||
message: '' |
|||
}); |
|||
const [isSubmitting, setIsSubmitting] = useState(false); |
|||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); |
|||
|
|||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { |
|||
const { name, value } = e.target; |
|||
setFormData(prev => ({ |
|||
...prev, |
|||
[name]: value |
|||
})); |
|||
}; |
|||
|
|||
const handleSubmit = async (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
setIsSubmitting(true); |
|||
setSubmitStatus('idle'); |
|||
|
|||
try { |
|||
// 这里可以调用实际的 API
|
|||
// const response = await ContactApi.submitForm(formData);
|
|||
|
|||
// 模拟 API 调用
|
|||
await new Promise(resolve => setTimeout(resolve, 2000)); |
|||
|
|||
setSubmitStatus('success'); |
|||
setFormData({ |
|||
name: '', |
|||
email: '', |
|||
phone: '', |
|||
company: '', |
|||
message: '' |
|||
}); |
|||
} catch (error) { |
|||
console.error('提交失败:', error); |
|||
setSubmitStatus('error'); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
const contactInfo = [ |
|||
{ |
|||
icon: ( |
|||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> |
|||
</svg> |
|||
), |
|||
title: '公司地址', |
|||
content: '北京市朝阳区某某大厦 888 号', |
|||
link: 'https://maps.google.com' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /> |
|||
</svg> |
|||
), |
|||
title: '联系电话', |
|||
content: '400-888-8888', |
|||
link: 'tel:400-888-8888' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> |
|||
</svg> |
|||
), |
|||
title: '邮箱地址', |
|||
content: 'contact@company.com', |
|||
link: 'mailto:contact@company.com' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
), |
|||
title: '工作时间', |
|||
content: '周一至周五 9:00-18:00', |
|||
link: null |
|||
} |
|||
]; |
|||
|
|||
return ( |
|||
<section className="section-padding"> |
|||
<div className="container"> |
|||
<div className="text-center mb-16"> |
|||
<h2 className="section-title">联系我们</h2> |
|||
<p className="section-subtitle"> |
|||
有任何问题或需求,欢迎随时与我们联系,我们将竭诚为您服务 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16"> |
|||
{/* 左侧联系信息 */} |
|||
<div> |
|||
<h3 className="text-2xl font-bold mb-8">联系信息</h3> |
|||
|
|||
<div className="space-y-6 mb-12"> |
|||
{contactInfo.map((info, index) => ( |
|||
<div key={index} className="flex items-start space-x-4"> |
|||
<div className="flex-shrink-0 w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600"> |
|||
{info.icon} |
|||
</div> |
|||
<div> |
|||
<h4 className="font-semibold mb-1">{info.title}</h4> |
|||
{info.link ? ( |
|||
<a |
|||
href={info.link} |
|||
className="text-gray-600 hover:text-blue-600 transition-colors duration-300" |
|||
target={info.link.startsWith('http') ? '_blank' : undefined} |
|||
rel={info.link.startsWith('http') ? 'noopener noreferrer' : undefined} |
|||
> |
|||
{info.content} |
|||
</a> |
|||
) : ( |
|||
<p className="text-gray-600">{info.content}</p> |
|||
)} |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
|
|||
{/* 社交媒体 */} |
|||
<div> |
|||
<h4 className="font-semibold mb-4">关注我们</h4> |
|||
<div className="flex space-x-4"> |
|||
<a href="#" className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-600 hover:bg-blue-600 hover:text-white transition-colors duration-300"> |
|||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/> |
|||
</svg> |
|||
</a> |
|||
<a href="#" className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-600 hover:bg-blue-600 hover:text-white transition-colors duration-300"> |
|||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/> |
|||
</svg> |
|||
</a> |
|||
<a href="#" className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-600 hover:bg-blue-600 hover:text-white transition-colors duration-300"> |
|||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/> |
|||
</svg> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 右侧联系表单 */} |
|||
<div> |
|||
<h3 className="text-2xl font-bold mb-8">发送消息</h3> |
|||
|
|||
<form onSubmit={handleSubmit} className="space-y-6"> |
|||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|||
<div> |
|||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2"> |
|||
姓名 * |
|||
</label> |
|||
<input |
|||
type="text" |
|||
id="name" |
|||
name="name" |
|||
value={formData.name} |
|||
onChange={handleInputChange} |
|||
required |
|||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-300" |
|||
placeholder="请输入您的姓名" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> |
|||
邮箱 * |
|||
</label> |
|||
<input |
|||
type="email" |
|||
id="email" |
|||
name="email" |
|||
value={formData.email} |
|||
onChange={handleInputChange} |
|||
required |
|||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-300" |
|||
placeholder="请输入您的邮箱" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|||
<div> |
|||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2"> |
|||
电话 |
|||
</label> |
|||
<input |
|||
type="tel" |
|||
id="phone" |
|||
name="phone" |
|||
value={formData.phone} |
|||
onChange={handleInputChange} |
|||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-300" |
|||
placeholder="请输入您的电话" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-2"> |
|||
公司 |
|||
</label> |
|||
<input |
|||
type="text" |
|||
id="company" |
|||
name="company" |
|||
value={formData.company} |
|||
onChange={handleInputChange} |
|||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-300" |
|||
placeholder="请输入您的公司" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2"> |
|||
留言 * |
|||
</label> |
|||
<textarea |
|||
id="message" |
|||
name="message" |
|||
value={formData.message} |
|||
onChange={handleInputChange} |
|||
required |
|||
rows={6} |
|||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-300 resize-none" |
|||
placeholder="请详细描述您的需求或问题..." |
|||
/> |
|||
</div> |
|||
|
|||
{/* 提交状态提示 */} |
|||
{submitStatus === 'success' && ( |
|||
<div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg"> |
|||
消息发送成功!我们会尽快与您联系。 |
|||
</div> |
|||
)} |
|||
|
|||
{submitStatus === 'error' && ( |
|||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg"> |
|||
发送失败,请稍后重试或直接联系我们。 |
|||
</div> |
|||
)} |
|||
|
|||
<button |
|||
type="submit" |
|||
disabled={isSubmitting} |
|||
className="w-full btn btn-primary disabled:opacity-50 disabled:cursor-not-allowed" |
|||
> |
|||
{isSubmitting ? ( |
|||
<span className="flex items-center justify-center"> |
|||
<div className="loading mr-2"></div> |
|||
发送中... |
|||
</span> |
|||
) : ( |
|||
'发送消息' |
|||
)} |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
}; |
|||
|
|||
export default ContactSection; |
@ -0,0 +1,103 @@ |
|||
'use client'; |
|||
|
|||
const FeaturesSection = () => { |
|||
const features = [ |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> |
|||
</svg> |
|||
), |
|||
title: '高效快速', |
|||
description: '采用先进的技术架构,确保系统运行高效稳定,响应速度快,为您的业务提供强有力的技术支撑。' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
), |
|||
title: '安全可靠', |
|||
description: '多层安全防护机制,数据加密传输,定期安全检测,确保您的数据和业务安全无忧。' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> |
|||
</svg> |
|||
), |
|||
title: '用户友好', |
|||
description: '简洁直观的用户界面设计,操作简单易懂,降低学习成本,提升用户体验和工作效率。' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z" /> |
|||
</svg> |
|||
), |
|||
title: '灵活扩展', |
|||
description: '模块化设计架构,支持灵活配置和扩展,可根据业务需求进行定制化开发和功能扩展。' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" /> |
|||
</svg> |
|||
), |
|||
title: '专业服务', |
|||
description: '专业的技术团队提供全方位服务支持,从需求分析到实施部署,再到后期维护,全程贴心服务。' |
|||
}, |
|||
{ |
|||
icon: ( |
|||
<svg className="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> |
|||
</svg> |
|||
), |
|||
title: '数据分析', |
|||
description: '强大的数据分析功能,实时监控业务指标,生成详细报表,为决策提供数据支撑。' |
|||
} |
|||
]; |
|||
|
|||
return ( |
|||
<section className="section-padding bg-gray-50"> |
|||
<div className="container"> |
|||
<div className="text-center mb-16"> |
|||
<h2 className="section-title">为什么选择我们</h2> |
|||
<p className="section-subtitle"> |
|||
我们拥有丰富的行业经验和专业的技术团队,为您提供全方位的企业级解决方案 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> |
|||
{features.map((feature, index) => ( |
|||
<div key={index} className="feature-card fade-in-up" style={{ animationDelay: `${index * 0.1}s` }}> |
|||
{feature.icon} |
|||
<h3 className="feature-title">{feature.title}</h3> |
|||
<p className="feature-description">{feature.description}</p> |
|||
</div> |
|||
))} |
|||
</div> |
|||
|
|||
{/* 底部 CTA */} |
|||
<div className="text-center mt-16"> |
|||
<div className="bg-white rounded-2xl shadow-lg p-8 max-w-4xl mx-auto"> |
|||
<h3 className="text-2xl font-bold mb-4">准备开始您的数字化转型之旅?</h3> |
|||
<p className="text-gray-600 mb-6"> |
|||
联系我们的专业团队,获取免费的咨询服务和定制化解决方案 |
|||
</p> |
|||
<div className="flex flex-col sm:flex-row gap-4 justify-center"> |
|||
<a href="/contact" className="btn btn-primary"> |
|||
立即咨询 |
|||
</a> |
|||
<a href="/demo" className="btn btn-outline"> |
|||
预约演示 |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
}; |
|||
|
|||
export default FeaturesSection; |
@ -0,0 +1,76 @@ |
|||
'use client'; |
|||
|
|||
import Link from 'next/link'; |
|||
import Image from 'next/image'; |
|||
|
|||
const HeroSection = () => { |
|||
return ( |
|||
<section className="hero"> |
|||
<div className="container"> |
|||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> |
|||
{/* 左侧内容 */} |
|||
<div className="text-center lg:text-left"> |
|||
<h1 className="hero-title"> |
|||
专业的企业 |
|||
<span className="text-blue-600">解决方案</span> |
|||
服务商 |
|||
</h1> |
|||
<p className="hero-subtitle"> |
|||
我们致力于为企业提供全方位的数字化转型服务, |
|||
以创新技术驱动业务增长,助力企业在数字时代取得成功。 |
|||
</p> |
|||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start"> |
|||
<Link href="/contact" className="btn btn-primary"> |
|||
免费咨询 |
|||
</Link> |
|||
<Link href="/services" className="btn btn-outline"> |
|||
了解服务 |
|||
</Link> |
|||
</div> |
|||
|
|||
{/* 统计数据 */} |
|||
<div className="grid grid-cols-3 gap-8 mt-12 pt-8 border-t border-gray-200"> |
|||
<div className="text-center lg:text-left"> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">500+</div> |
|||
<div className="text-gray-600">成功案例</div> |
|||
</div> |
|||
<div className="text-center lg:text-left"> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">98%</div> |
|||
<div className="text-gray-600">客户满意度</div> |
|||
</div> |
|||
<div className="text-center lg:text-left"> |
|||
<div className="text-3xl font-bold text-blue-600 mb-2">24/7</div> |
|||
<div className="text-gray-600">技术支持</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 右侧图片 */} |
|||
<div className="relative"> |
|||
<div className="relative z-10"> |
|||
<Image |
|||
src="/hero-image.jpg" |
|||
alt="企业解决方案" |
|||
width={600} |
|||
height={400} |
|||
className="rounded-lg shadow-2xl" |
|||
priority |
|||
onError={(e) => { |
|||
// 如果图片加载失败,显示占位符
|
|||
const target = e.target as HTMLImageElement; |
|||
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjQwMCIgdmlld0JveD0iMCAwIDYwMCA0MDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI2MDAiIGhlaWdodD0iNDAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0yNzUgMTc1SDMyNVYyMjVIMjc1VjE3NVoiIGZpbGw9IiM5Q0EzQUYiLz4KPHA+CjxwYXRoIGQ9Ik0yNTAgMjAwSDM1MFYyMDBIMjUwWiIgZmlsbD0iIzlDQTNBRiIvPgo8L3N2Zz4K'; |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
{/* 装饰性元素 */} |
|||
<div className="absolute -top-4 -right-4 w-72 h-72 bg-blue-100 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-pulse"></div> |
|||
<div className="absolute -bottom-8 -left-4 w-72 h-72 bg-purple-100 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-pulse"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
}; |
|||
|
|||
export default HeroSection; |
Loading…
Reference in new issue