Compare commits
11 Commits
309aeaedc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8193bab566 | ||
|
|
f4263af343 | ||
|
|
0dc24cfa85 | ||
|
|
183d98a699 | ||
|
|
ede67faafd | ||
|
|
07ce8409e1 | ||
|
|
0cbb91077d | ||
|
|
fc581b0476 | ||
|
|
109ac3c009 | ||
|
|
1dc5bdd93f | ||
|
|
4ae0ff7c2a |
754
README_API.md
754
README_API.md
@@ -1,335 +1,440 @@
|
||||
# 前端调用文档
|
||||
# 前端API调用文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于Vue 3的博客前端项目,提供文章展示、分类浏览、留言板等功能。项目使用Vue 3 Composition API进行开发,集成了Vue Router进行路由管理,并使用Element Plus等UI库提供用户界面。
|
||||
这是一个基于Vue 3的博客前端项目,提供文章展示、分类管理、留言板、用户认证和疯言疯语等功能。项目使用Vue 3 Composition API进行开发,集成了Vue Router进行路由管理,并使用Element Plus等UI库提供用户界面。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**: Vue 3
|
||||
- **构建工具**: Vite
|
||||
- **路由管理**: Vue Router 4
|
||||
- **UI组件库**: Element Plus、Ant Design Vue
|
||||
- **UI组件库**: Element Plus
|
||||
- **HTTP客户端**: Axios
|
||||
- **编程语言**: JavaScript/TypeScript
|
||||
- **类型定义**: TypeScript类型接口
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── App.vue # 应用根组件
|
||||
├── assets/ # 静态资源
|
||||
│ └── index.css # 全局样式
|
||||
├── img/ # 图片资源
|
||||
├── index.vue # 首页主组件
|
||||
├── main.js # 应用入口文件
|
||||
├── router/ # 路由配置
|
||||
│ └── Router.js # 路由定义
|
||||
└── views/ # 视图组件
|
||||
├── aboutme.vue # 关于页面
|
||||
├── aericle.vue # 文章分类目录页面
|
||||
├── articlecontents.vue # 文章内容页面
|
||||
├── home.vue # 首页内容组件
|
||||
├── leftmodlue.vue # 左侧边栏组件
|
||||
├── messageboard.vue # 留言板页面
|
||||
└── nonsense.vue # 疯言疯语页面
|
||||
├── services/ # API服务模块
|
||||
│ ├── apiService.js # 基础API服务配置
|
||||
│ ├── articleService.js # 文章相关API
|
||||
│ ├── categoryService.js # 分类相关API
|
||||
│ ├── categoryAttributeService.js # 分类属性相关API
|
||||
│ ├── loginService.js # 用户认证相关API
|
||||
│ ├── messageService.js # 留言相关API
|
||||
│ ├── nonsenseService.js # 疯言疯语相关API
|
||||
│ └── index.js # 服务导出文件
|
||||
├── types/ # 类型定义
|
||||
│ └── index.ts # 所有接口类型定义
|
||||
├── views/ # 视图组件
|
||||
└── components/ # 可复用组件
|
||||
```
|
||||
|
||||
## 路由配置
|
||||
## API服务模块详解
|
||||
|
||||
路由配置位于 `src/router/Router.js`,定义了应用的所有路由映射关系:
|
||||
### 1. 基础API服务 (apiService.js)
|
||||
|
||||
| 路由路径 | 组件 | 功能描述 |
|
||||
|---------|------|---------|
|
||||
| `/` | home.vue | 默认路由,重定向到首页 |
|
||||
| `/:type` | home.vue | 首页,可根据type参数筛选文章 |
|
||||
| `/aericle` | aericle.vue | 文章分类目录页面 |
|
||||
| `/nonsense` | nonsense.vue | 疯言疯语页面 |
|
||||
| `/message` | messageboard.vue | 留言板页面 |
|
||||
| `/about` | aboutme.vue | 关于页面 |
|
||||
| `/articlecontents/:url` | articlecontents.vue | 文章内容详情页 |
|
||||
|
||||
## 主要组件说明
|
||||
|
||||
### 1. App.vue
|
||||
|
||||
应用根组件,负责渲染主页面组件。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Index/>
|
||||
</template>
|
||||
<script setup>
|
||||
import Index from './index.vue';
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. index.vue
|
||||
|
||||
主页面组件,包含顶部导航栏、Hero区域和内容展示区。
|
||||
|
||||
主要功能:
|
||||
- 响应式布局,适配不同屏幕宽度
|
||||
- 导航菜单切换不同页面
|
||||
- 打字机效果展示欢迎语
|
||||
|
||||
### 3. home.vue
|
||||
|
||||
首页文章列表组件,用于展示文章卡片列表。
|
||||
|
||||
主要功能:
|
||||
- 展示文章标题、作者、发布时间等信息
|
||||
- 点击文章卡片跳转到文章详情页
|
||||
- 支持文章筛选功能
|
||||
|
||||
### 4. aericle.vue
|
||||
|
||||
文章分类目录组件,展示文章的分类结构。
|
||||
|
||||
主要功能:
|
||||
- 展示不同分类下的文章数量
|
||||
- 点击分类项可以跳转到对应的文章列表
|
||||
|
||||
### 5. articlecontents.vue
|
||||
|
||||
文章内容详情组件,用于展示单篇文章的完整内容。
|
||||
|
||||
主要功能:
|
||||
- 获取URL参数并展示对应文章内容
|
||||
- 支持通过参数向服务器请求相关文章
|
||||
|
||||
### 6. messageboard.vue
|
||||
|
||||
留言板组件,支持用户发表评论和回复。
|
||||
|
||||
主要功能:
|
||||
- 展示留言列表和回复
|
||||
- 支持发表新留言
|
||||
- 支持回复他人留言
|
||||
|
||||
### 7. nonsense.vue
|
||||
|
||||
疯言疯语组件,展示一些非正式的简短内容。
|
||||
|
||||
## 后端API接口
|
||||
|
||||
项目后端基于Spring Boot开发,提供了以下主要API接口:
|
||||
|
||||
### 文章管理API
|
||||
|
||||
#### 1. 获取文章列表
|
||||
|
||||
```
|
||||
GET /api/articles
|
||||
```
|
||||
|
||||
**功能**: 获取所有文章列表
|
||||
|
||||
**返回**: 包含文章列表的ResponseMessage对象
|
||||
|
||||
#### 2. 获取单篇文章
|
||||
|
||||
```
|
||||
GET /api/articles/{id}
|
||||
```
|
||||
|
||||
**功能**: 根据ID获取单篇文章详情
|
||||
|
||||
**参数**: id - 文章ID
|
||||
|
||||
**返回**: 包含文章信息的ResponseMessage对象
|
||||
|
||||
#### 3. 根据分类获取文章
|
||||
|
||||
```
|
||||
GET /api/articles/category/{categoryId}
|
||||
```
|
||||
|
||||
**功能**: 获取指定分类下的所有文章
|
||||
|
||||
**参数**: categoryId - 分类ID
|
||||
|
||||
**返回**: 包含文章列表的ResponseMessage对象
|
||||
|
||||
#### 4. 获取热门文章
|
||||
|
||||
```
|
||||
GET /api/articles/popular
|
||||
```
|
||||
|
||||
**功能**: 获取浏览量最高的文章列表
|
||||
|
||||
**返回**: 包含热门文章列表的ResponseMessage对象
|
||||
|
||||
#### 5. 创建文章 (需要认证)
|
||||
|
||||
```
|
||||
POST /api/articles
|
||||
```
|
||||
|
||||
**功能**: 创建新文章
|
||||
|
||||
**权限**: 需要AUTHOR角色
|
||||
|
||||
**参数**: ArticleDto对象 (JSON格式)
|
||||
|
||||
**返回**: 包含新创建文章信息的ResponseMessage对象
|
||||
|
||||
#### 6. 更新文章 (需要认证)
|
||||
|
||||
```
|
||||
PUT /api/articles/{id}
|
||||
```
|
||||
|
||||
**功能**: 更新现有文章
|
||||
|
||||
**权限**: 需要AUTHOR或ADMIN角色
|
||||
|
||||
**参数**:
|
||||
- id - 文章ID
|
||||
- ArticleDto对象 (JSON格式)
|
||||
|
||||
**返回**: 包含更新后文章信息的ResponseMessage对象
|
||||
|
||||
#### 7. 删除文章 (需要认证)
|
||||
|
||||
```
|
||||
DELETE /api/articles/{id}
|
||||
```
|
||||
|
||||
**功能**: 删除指定文章
|
||||
|
||||
**权限**: 需要AUTHOR或ADMIN角色
|
||||
|
||||
**参数**: id - 文章ID
|
||||
|
||||
**返回**: 包含被删除文章信息的ResponseMessage对象
|
||||
|
||||
### 留言管理API
|
||||
|
||||
#### 1. 获取所有留言
|
||||
|
||||
```
|
||||
GET /api/messages
|
||||
```
|
||||
|
||||
**功能**: 获取所有留言列表
|
||||
|
||||
**返回**: 包含留言列表的ResponseMessage对象
|
||||
|
||||
#### 2. 获取单条留言
|
||||
|
||||
```
|
||||
GET /api/messages/{id}
|
||||
```
|
||||
|
||||
**功能**: 根据ID获取单条留言详情
|
||||
|
||||
**参数**: id - 留言ID
|
||||
|
||||
**返回**: 包含留言信息的ResponseMessage对象
|
||||
|
||||
#### 3. 保存留言
|
||||
|
||||
```
|
||||
POST /api/messages
|
||||
```
|
||||
|
||||
**功能**: 保存新留言
|
||||
|
||||
**参数**: MessageDto对象 (JSON格式)
|
||||
|
||||
**返回**: 包含保存后留言信息的ResponseMessage对象
|
||||
|
||||
#### 4. 删除留言
|
||||
|
||||
```
|
||||
DELETE /api/messages/{id}
|
||||
```
|
||||
|
||||
**功能**: 删除指定留言
|
||||
|
||||
**参数**: id - 留言ID
|
||||
|
||||
**返回**: 包含删除结果的ResponseMessage对象
|
||||
|
||||
## 前端API调用说明
|
||||
|
||||
项目中使用Axios进行HTTP请求,推荐创建统一的API服务模块来封装所有后端API调用。以下是一个示例:
|
||||
基础API服务配置了Axios实例,设置请求拦截器和响应拦截器,统一处理认证信息和错误响应。
|
||||
|
||||
```javascript
|
||||
import axios from 'axios';
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8080/api', // 后端API基础URL
|
||||
baseURL:'http://localhost:8080/api', // API基础URL
|
||||
timeout: 10000, // 请求超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
// 可以在这里添加token等认证信息
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response.data;
|
||||
},
|
||||
error => {
|
||||
// 统一错误处理
|
||||
console.error('API请求错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 文章相关API
|
||||
export const articleAPI = {
|
||||
// 获取所有文章
|
||||
getAllArticles: () => api.get('/articles'),
|
||||
// 获取单篇文章
|
||||
getArticleById: (id) => api.get(`/articles/${id}`),
|
||||
// 根据分类获取文章
|
||||
getArticlesByCategory: (categoryId) => api.get(`/articles/category/${categoryId}`),
|
||||
// 获取热门文章
|
||||
getPopularArticles: () => api.get('/articles/popular'),
|
||||
// 创建文章
|
||||
createArticle: (articleData) => api.post('/articles', articleData),
|
||||
// 更新文章
|
||||
updateArticle: (id, articleData) => api.put(`/articles/${id}`, articleData),
|
||||
// 删除文章
|
||||
deleteArticle: (id) => api.delete(`/articles/${id}`)
|
||||
};
|
||||
|
||||
// 留言相关API
|
||||
export const messageAPI = {
|
||||
// 获取所有留言
|
||||
getAllMessages: () => api.get('/messages'),
|
||||
// 获取单条留言
|
||||
getMessageById: (id) => api.get(`/messages/${id}`),
|
||||
// 保存留言
|
||||
saveMessage: (messageData) => api.post('/messages', messageData),
|
||||
// 删除留言
|
||||
deleteMessage: (id) => api.delete(`/messages/${id}`)
|
||||
};
|
||||
|
||||
// 导出默认的api对象
|
||||
export default api;
|
||||
withCredentials: true // 允许跨域请求携带凭证
|
||||
})
|
||||
```
|
||||
|
||||
**核心功能**:
|
||||
- 自动添加认证token到请求头
|
||||
- 统一处理HTTP错误(401、403、404、500等)
|
||||
- 自动显示错误提示信息
|
||||
- 未授权时自动清除token并重定向到登录页
|
||||
|
||||
### 2. 文章服务 (articleService.js)
|
||||
|
||||
提供文章的CRUD操作和各种查询功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| getAllArticles | 获取已发布文章列表 | params: PaginationParams(可选) | Promise<ApiResponse<Article[]>> |
|
||||
| getArticlesByStatus | 根据状态获取文章列表 | status: number(0:未发表 1:已发表 2:已删除) | Promise<ApiResponse<Article[]>> |
|
||||
| getAllArticlesWithDeleted | 获取所有文章列表(包含已删除) | params: PaginationParams(可选) | Promise<ApiResponse<Article[]>> |
|
||||
| getArticleById | 根据ID获取文章详情 | articleid: number | Promise<ApiResponse<Article>> |
|
||||
| getArticlesByAttributeId | 根据属性ID获取文章列表 | attributeid: number | Promise<ApiResponse<Article[]>> |
|
||||
| getArticlesByTitle | 根据标题查询文章列表 | title: string | Promise<ApiResponse<Article[]>> |
|
||||
| getPopularArticles | 获取热门文章 | 无 | Promise<ApiResponse<Article[]>> |
|
||||
| createArticle | 创建文章 | articleData: ArticleDto | Promise<ApiResponse<Article>> |
|
||||
| updateArticle | 更新文章 | articleid: number, articleData: ArticleDto | Promise<ApiResponse<Article>> |
|
||||
| deleteArticle | 删除文章 | articleid: number | Promise<ApiResponse<boolean>> |
|
||||
| getArticlesByCategory | 根据分类获取文章 | categoryid: number | Promise<ApiResponse<Article[]>> |
|
||||
| incrementArticleViews | 增加文章浏览量 | articleid: number | Promise<ApiResponse<boolean>> |
|
||||
| getLatestArticlesByAttribute | 根据属性ID获取最新文章 | attributeid: number | Promise<ApiResponse<Article[]>> |
|
||||
| likeArticle | 点赞文章 | articleid: number | Promise<ApiResponse<boolean>> |
|
||||
|
||||
### 3. 分类服务 (categoryService.js)
|
||||
|
||||
提供分类的管理功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| getAllCategories | 获取所有分类 | 无 | Promise<ApiResponse<Category[]>> |
|
||||
| getCategory | 获取指定分类 | typeid: number | Promise<ApiResponse<Category>> |
|
||||
| createCategory | 创建新分类 | categoryData: CategoryDto | Promise<ApiResponse<Category>> |
|
||||
| updateCategory | 更新分类 | typeid: number, categoryData: CategoryDto | Promise<ApiResponse<Category>> |
|
||||
| deleteCategory | 删除分类 | typeid: number | Promise<ApiResponse<boolean>> |
|
||||
|
||||
### 4. 分类属性服务 (categoryAttributeService.js)
|
||||
|
||||
提供分类属性(标签)的管理功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| getAllAttributes | 获取所有分类属性 | 无 | Promise<ApiResponse<CategoryAttribute[]>> |
|
||||
| getAttributeById | 根据ID获取分类属性 | attributeid: number | Promise<ApiResponse<CategoryAttribute>> |
|
||||
| getAttributesByCategory | 根据分类ID获取属性列表 | categoryid: number | Promise<ApiResponse<CategoryAttribute[]>> |
|
||||
| createAttribute | 创建分类属性 | attributeData: CategoryAttributeDto | Promise<ApiResponse<CategoryAttribute>> |
|
||||
| updateAttribute | 更新分类属性 | attributeid: number, attributeData: CategoryAttributeDto | Promise<ApiResponse<CategoryAttribute>> |
|
||||
| deleteAttribute | 删除分类属性 | attributeid: number | Promise<ApiResponse<boolean>> |
|
||||
| checkAttributeExists | 检查分类下是否存在指定名称的属性 | categoryid: number, attributename: string | Promise<ApiResponse<boolean>> |
|
||||
|
||||
### 5. 用户认证服务 (loginService.js)
|
||||
|
||||
提供用户登录、注册和个人信息管理功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| login | 用户登录 | loginData: LoginDto | Promise<ApiResponse<User>> |
|
||||
| register | 用户注册 | registerData: RegisterDto | Promise<ApiResponse<User>> |
|
||||
| logout | 用户登出 | 无 | Promise<any> |
|
||||
| getCurrentUser | 获取当前用户信息 | 无 | Promise<ApiResponse<User>> |
|
||||
| updateUser | 更新用户信息 | userData: UserDto | Promise<ApiResponse<User>> |
|
||||
| changePassword | 修改密码 | passwordData: ChangePasswordDto | Promise<ApiResponse<boolean>> |
|
||||
|
||||
### 6. 留言服务 (messageService.js)
|
||||
|
||||
提供留言的管理和查询功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| getAllMessages | 获取所有留言 | 无 | Promise<ApiResponse<Message[]>> |
|
||||
| getMessageById | 获取单条留言 | messageid: number | Promise<ApiResponse<Message>> |
|
||||
| getMessagesByArticleId | 根据文章ID获取留言 | articleid: number | Promise<ApiResponse<Message[]>> |
|
||||
| getRootMessages | 获取根留言 | 无 | Promise<ApiResponse<Message[]>> |
|
||||
| getRepliesByParentId | 根据父留言ID获取回复 | parentid: number | Promise<ApiResponse<Message[]>> |
|
||||
| searchMessagesByNickname | 根据昵称搜索留言 | nickname: string | Promise<ApiResponse<Message[]>> |
|
||||
| getMessageCountByArticleId | 获取文章评论数量 | articleid: number | Promise<ApiResponse<number>> |
|
||||
| saveMessage | 创建留言 | messageData: MessageDto | Promise<ApiResponse<Message>> |
|
||||
| deleteMessage | 删除留言 | messageid: number | Promise<ApiResponse<boolean>> |
|
||||
| likeMessage | 点赞留言 | messageid: number | Promise<ApiResponse<boolean>> |
|
||||
|
||||
### 7. 疯言疯语服务 (nonsenseService.js)
|
||||
|
||||
提供疯言疯语内容的管理功能。
|
||||
|
||||
**主要方法**:
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回类型 |
|
||||
|-------|------|------|----------|
|
||||
| getAllNonsense | 获取所有疯言疯语内容 | 无 | Promise<ApiResponse<Nonsense[]>> |
|
||||
| getNonsenseByStatus | 根据状态获取疯言疯语内容 | status: number(1:已发表, 0:草稿) | Promise<ApiResponse<Nonsense[]>> |
|
||||
| saveNonsense | 保存疯言疯语内容 | nonsense: Nonsense | Promise<ApiResponse<Nonsense>> |
|
||||
| deleteNonsense | 删除疯言疯语内容 | id: number | Promise<ApiResponse<boolean>> |
|
||||
| updateNonsense | 更新疯言疯语内容 | nonsense: Nonsense | Promise<ApiResponse<Nonsense>> |
|
||||
|
||||
## 数据模型定义
|
||||
|
||||
### 1. 文章 (Article)
|
||||
|
||||
```typescript
|
||||
interface Article {
|
||||
articleid: number
|
||||
title: string
|
||||
content: string
|
||||
attributeid: Number
|
||||
categoryName: string
|
||||
img?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
viewCount?: number
|
||||
likes?: number
|
||||
commentCount?: number
|
||||
status?: number
|
||||
markdownscontent: string
|
||||
}
|
||||
|
||||
interface ArticleDto {
|
||||
id?: number
|
||||
title: string
|
||||
content: string
|
||||
attributeid: number
|
||||
img?: string
|
||||
status?: number
|
||||
viewCount?: number
|
||||
likes?: number
|
||||
markdownscontent: string
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 分类 (Category)
|
||||
|
||||
```typescript
|
||||
interface Category {
|
||||
typeid: number
|
||||
typename: string
|
||||
description?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
articleCount?: number
|
||||
}
|
||||
|
||||
interface CategoryDto {
|
||||
typename: string
|
||||
description?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 分类属性 (CategoryAttribute)
|
||||
|
||||
```typescript
|
||||
interface CategoryAttribute {
|
||||
attributeid: number
|
||||
categoryid: number
|
||||
attributename: string
|
||||
}
|
||||
|
||||
interface CategoryAttributeDto {
|
||||
categoryid: number
|
||||
attributename: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 留言 (Message)
|
||||
|
||||
```typescript
|
||||
interface Message {
|
||||
messageid: number
|
||||
content: string
|
||||
nickname: string
|
||||
email: string
|
||||
articleid?: number
|
||||
parentid?: number
|
||||
createdAt: string
|
||||
replyid?: number
|
||||
likes?: number
|
||||
messageimg?: string
|
||||
}
|
||||
|
||||
interface MessageDto {
|
||||
messageid?: number
|
||||
nickname?: string
|
||||
email?: string
|
||||
content?: string
|
||||
createdAt?: string
|
||||
parentid?: number
|
||||
replyid?: number
|
||||
articleid?: number
|
||||
messageimg?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 用户 (User)
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id?: number
|
||||
username?: string
|
||||
password?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
role?: number
|
||||
createTime?: string
|
||||
avatar?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
interface UserDto {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
phone: string
|
||||
role?: number
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 疯言疯语 (Nonsense)
|
||||
|
||||
```typescript
|
||||
interface Nonsense {
|
||||
nonsenseid: number
|
||||
content: string
|
||||
status?: number
|
||||
time: string
|
||||
}
|
||||
|
||||
interface NonsenseDto {
|
||||
content: string
|
||||
status?: number
|
||||
time?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 7. API响应 (ApiResponse)
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
code: number
|
||||
message?: string
|
||||
data?: T
|
||||
total?: number
|
||||
}
|
||||
```
|
||||
|
||||
## API调用示例
|
||||
|
||||
### 导入服务
|
||||
|
||||
```javascript
|
||||
import { articleService, categoryService, messageService, loginService } from '@/services'
|
||||
```
|
||||
|
||||
### 文章相关调用示例
|
||||
|
||||
```javascript
|
||||
// 获取文章列表
|
||||
async function fetchArticles() {
|
||||
try {
|
||||
const response = await articleService.getAllArticles({ page: 1, size: 10 })
|
||||
if (response.success) {
|
||||
console.log('文章列表:', response.data)
|
||||
} else {
|
||||
console.error('获取文章列表失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新文章
|
||||
async function createNewArticle() {
|
||||
try {
|
||||
const articleData = {
|
||||
title: '新文章标题',
|
||||
content: '文章内容',
|
||||
attributeid: 1,
|
||||
markdownscontent: '# 文章标题\n文章内容'
|
||||
}
|
||||
const response = await articleService.createArticle(articleData)
|
||||
if (response.success) {
|
||||
console.log('文章创建成功:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文章失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分类相关调用示例
|
||||
|
||||
```javascript
|
||||
// 获取所有分类
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
const response = await categoryService.getAllCategories()
|
||||
if (response.success) {
|
||||
console.log('分类列表:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新分类
|
||||
async function createCategory() {
|
||||
try {
|
||||
const categoryData = {
|
||||
typename: '新技术',
|
||||
description: '关于新技术的文章分类'
|
||||
}
|
||||
const response = await categoryService.createCategory(categoryData)
|
||||
if (response.success) {
|
||||
console.log('分类创建成功:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建分类失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 用户认证调用示例
|
||||
|
||||
```javascript
|
||||
// 用户登录
|
||||
async function userLogin() {
|
||||
try {
|
||||
const loginData = {
|
||||
username: 'testuser',
|
||||
password: 'password123'
|
||||
}
|
||||
const response = await loginService.login(loginData)
|
||||
if (response.success) {
|
||||
// 保存token
|
||||
localStorage.setItem('token', response.data.token)
|
||||
console.log('登录成功:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
async function getCurrentUserInfo() {
|
||||
try {
|
||||
const response = await loginService.getCurrentUser()
|
||||
if (response.success) {
|
||||
console.log('用户信息:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理机制
|
||||
|
||||
前端API服务集成了统一的错误处理机制,包括:
|
||||
|
||||
1. **HTTP状态码处理**:
|
||||
- 401: 未授权,自动清除token并跳转登录页
|
||||
- 403: 拒绝访问
|
||||
- 404: 请求资源不存在
|
||||
- 500: 服务器错误
|
||||
|
||||
2. **请求错误处理**:
|
||||
- 网络连接错误
|
||||
- 请求超时
|
||||
- 服务器无响应
|
||||
|
||||
3. **错误信息提示**:
|
||||
- 所有错误通过Element Plus的ElMessage组件显示
|
||||
- 支持自定义错误消息
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 安装依赖
|
||||
@@ -363,45 +468,14 @@ npm run preview
|
||||
## 部署说明
|
||||
|
||||
1. 确保后端服务已部署并运行在正确的端口上
|
||||
2. 修改前端API请求的基础URL指向实际的后端服务地址
|
||||
2. 修改 `apiService.js` 中的基础URL指向实际的后端服务地址
|
||||
3. 构建生产版本并部署到Web服务器
|
||||
4. 配置Web服务器以支持单页应用路由 (SPA fallback)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 项目使用了Element Plus和Ant Design Vue两个UI库,请注意组件的正确引入和使用方式
|
||||
2. 后端服务默认端口为8080,前端开发服务器端口为3000
|
||||
3. 确保CORS配置正确,允许前端域名访问后端API
|
||||
4. 对于需要认证的API,需要在请求头中添加正确的认证信息
|
||||
|
||||
## 附录:数据模型
|
||||
|
||||
### 文章数据模型 (Article)
|
||||
|
||||
```javascript
|
||||
{
|
||||
articleid: Number, // 文章ID
|
||||
title: String, // 文章标题
|
||||
content: String, // 文章内容
|
||||
author: String, // 作者
|
||||
authorid: Number, // 作者ID
|
||||
typeid: Number, // 分类ID
|
||||
publishedAt: String, // 发布时间
|
||||
viewCount: Number, // 浏览次数
|
||||
status: Number, // 状态 (1表示已发布)
|
||||
img: String // 文章图片
|
||||
}
|
||||
```
|
||||
|
||||
### 留言数据模型 (Message)
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: Number, // 留言ID
|
||||
nickname: String, // 昵称
|
||||
email: String, // 邮箱
|
||||
content: String, // 留言内容
|
||||
time: String, // 留言时间
|
||||
replies: Array // 回复列表
|
||||
}
|
||||
```
|
||||
1. 所有API调用都应该使用services目录下导出的服务实例
|
||||
2. 对于需要认证的操作,确保用户已登录并持有有效的token
|
||||
3. 在生产环境中,确保修改API基础URL为实际的后端服务地址
|
||||
4. 对于分页查询,合理设置page和size参数以优化性能
|
||||
5. 图片上传等大文件操作需要特别处理,避免超时
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/blogicon.jpg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>清疯不颠</title>
|
||||
</head>
|
||||
|
||||
1415
package-lock.json
generated
1415
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"antd": "^5.27.3",
|
||||
"axios": "^1.12.2",
|
||||
"element-plus": "^2.11.5",
|
||||
"md-editor-v3": "^6.1.0",
|
||||
"naive-ui": "^2.43.1",
|
||||
"pinia": "^3.0.3",
|
||||
"sass": "^1.93.2",
|
||||
"undraw-ui": "^1.3.2",
|
||||
|
||||
BIN
public/blogicon.jpg
Normal file
BIN
public/blogicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -34,12 +34,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="bot" :class="{ 'botrelative': scrollY }">
|
||||
<el-tabs v-model="activeName" stretch="true" class="demo-tabs">
|
||||
<el-tabs v-model="activeName" :stretch="true" class="demo-tabs">
|
||||
<el-tab-pane label="个人简介" name="first">
|
||||
<div class="mylogo">
|
||||
<el-avatar class="mylogo_avatar" :src="state.circleUrl" />
|
||||
</div>
|
||||
<a href="#">
|
||||
<a href="http://www.qf1121.top/">
|
||||
<h6 class="mylogo_name logo-text">清疯不颠</h6>
|
||||
</a>
|
||||
<h6 class="mylogo_description">重度精神失常患者</h6>
|
||||
@@ -51,13 +51,13 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="stat-link" @click.prevent="showCategories">
|
||||
<a href="#" class="stat-link" @click.prevent="showCategoriesModel">
|
||||
<span class="site-state-item-count">{{ categoryCount }}</span>
|
||||
<span class="site-state-item-name">分类</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="stat-link" @click.prevent="showAttributes">
|
||||
<a href="#" class="stat-link" @click.prevent="showAttributesModel">
|
||||
<span class="site-state-item-count">{{ AttributeCount }}</span>
|
||||
<span class="site-state-item-name">标签</span>
|
||||
</a>
|
||||
@@ -65,53 +65,12 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="功能" name="second">
|
||||
<div>还在开发中.....</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 分类蒙板组件 -->
|
||||
<Transition name="modal">
|
||||
<div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
|
||||
<div class="category-modal-content">
|
||||
<div class="category-modal-header">
|
||||
<h3>所有分类</h3>
|
||||
<button class="category-modal-close" @click="closeCategoryModal">×</button>
|
||||
</div>
|
||||
<div class="category-modal-body">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.typeid"
|
||||
class="category-button"
|
||||
@click="handleCategoryClick(category)"
|
||||
>
|
||||
{{ category.typename }} <span class="category-button-count">({{ category.count || 0 }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 标签蒙板组件 -->
|
||||
<Transition name="modal">
|
||||
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
|
||||
<div class="category-modal-content">
|
||||
<div class="category-modal-header">
|
||||
<h3>所有标签</h3>
|
||||
<button class="category-modal-close" @click="closeAttributeModal">×</button>
|
||||
</div>
|
||||
<div class="category-modal-body">
|
||||
<button
|
||||
v-for="attribute in attributes"
|
||||
:key="attribute.attributeid"
|
||||
class="category-button"
|
||||
@click="handleAttributeClick(attribute)"
|
||||
>
|
||||
{{ attribute.attributename }} <span class="category-button-count">({{ attribute.count || 0 }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -119,27 +78,22 @@
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { articleService, categoryService, categoryAttributeService } from "@/services";
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// 当前激活菜单
|
||||
const activeIndex = ref('/:type')
|
||||
const router = useRouter()
|
||||
const activeName = ref('first')
|
||||
const state = reactive({
|
||||
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
circleUrl: '/blogicon.jpg',
|
||||
squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
|
||||
sizeList: ['small', '', 'large'] as const,
|
||||
})
|
||||
|
||||
// 分类相关状态
|
||||
const categories = ref<any[]>([])
|
||||
const showCategoryModal = ref(false)
|
||||
// defineEmits
|
||||
const emit = defineEmits(['update-data', 'CategoryModal', 'AttributeModal'])
|
||||
|
||||
// 标签相关状态
|
||||
const attributes = ref<any[]>([])
|
||||
const showAttributeModal = ref(false)
|
||||
|
||||
// 处理菜单选择跳转
|
||||
const handleSelect = (key: string) => {
|
||||
router.push({ path: key })
|
||||
@@ -170,6 +124,9 @@ const fetchArticleCount = async () => {
|
||||
|
||||
// 获取分类数据
|
||||
const fetchCategories = async () => {
|
||||
|
||||
// 分类数据状态
|
||||
const categories = ref<any[]>([])
|
||||
try {
|
||||
const response = await categoryService.getAllCategories();
|
||||
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||
@@ -178,7 +135,7 @@ const fetchCategories = async () => {
|
||||
count: 0
|
||||
})) || [];
|
||||
categories.value.forEach(async (category: any) => {
|
||||
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.typeid)
|
||||
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.categoryid)
|
||||
if (attributeResponse.data?.length) {
|
||||
category.count = attributeResponse.data?.length || 0
|
||||
}
|
||||
@@ -193,10 +150,13 @@ const fetchCategories = async () => {
|
||||
];
|
||||
categoryCount.value = categories.value.length
|
||||
}
|
||||
return categories.value
|
||||
}
|
||||
|
||||
// 获取标签数据
|
||||
const fetchAttributes = async () => {
|
||||
// 标签数据状态
|
||||
const attributes = ref<any[]>([])
|
||||
try {
|
||||
const response = await categoryAttributeService.getAllAttributes();
|
||||
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||
@@ -215,56 +175,27 @@ const fetchAttributes = async () => {
|
||||
console.error('获取标签失败:', error)
|
||||
// 如果API调用失败,使用模拟数据
|
||||
attributes.value = [
|
||||
|
||||
];
|
||||
AttributeCount.value = attributes.value.length
|
||||
}
|
||||
return attributes.value
|
||||
}
|
||||
|
||||
// 显示分类蒙板
|
||||
const showCategories = () => {
|
||||
showCategoryModal.value = true
|
||||
// 向父组件传递标签数据
|
||||
const sendData = () => {
|
||||
const data = { fetchAttributes: fetchAttributes(), fetchCategories: fetchCategories() }
|
||||
emit('update-data', data)
|
||||
}
|
||||
|
||||
// 关闭分类蒙板
|
||||
const closeCategoryModal = () => {
|
||||
showCategoryModal.value = false
|
||||
// 显示标签蒙版
|
||||
const showAttributesModel = () => {
|
||||
emit('AttributeModal', { ifmodal: true })
|
||||
}
|
||||
// 显示分类蒙版
|
||||
const showCategoriesModel = () => {
|
||||
emit('CategoryModal', { ifmodal: true })
|
||||
}
|
||||
|
||||
// 处理分类点击
|
||||
const handleCategoryClick = (category: any) => {
|
||||
// 这里可以根据实际需求跳转到对应分类的文章列表页
|
||||
console.log('点击了分类:', category.typename)
|
||||
// 示例:router.push(`/article-list?category=${category.typeid}`)
|
||||
closeCategoryModal()
|
||||
}
|
||||
|
||||
// 显示标签蒙板
|
||||
const showAttributes = () => {
|
||||
showAttributeModal.value = true
|
||||
}
|
||||
|
||||
// 关闭标签蒙板
|
||||
const closeAttributeModal = () => {
|
||||
showAttributeModal.value = false
|
||||
}
|
||||
|
||||
// 处理标签点击
|
||||
const handleAttributeClick = (attribute: any) => {
|
||||
// 重置全局属性状态
|
||||
globalStore.removeValue('attribute')
|
||||
|
||||
globalStore.setValue('attribute', {
|
||||
id: attribute.attributeid,
|
||||
name: attribute.typename
|
||||
})
|
||||
console.log(attribute)
|
||||
router.push({
|
||||
path: '/home/aericletype',
|
||||
|
||||
})
|
||||
closeAttributeModal()
|
||||
}
|
||||
|
||||
// 控制底部模块吸顶效果
|
||||
const scrollY = ref(false)
|
||||
@@ -276,8 +207,7 @@ const handleScroll = () => {
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
fetchArticleCount() // 组件挂载时获取文章数量
|
||||
fetchCategories() // 组件挂载时获取分类数据
|
||||
fetchAttributes() // 组件挂载时获取标签数据
|
||||
sendData() // 组件挂载时获取标签数据和分类数据
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -313,15 +243,17 @@ onUnmounted(() => {
|
||||
|
||||
/* 内容区域样式 */
|
||||
#cont {
|
||||
padding:0 0 10px 0;
|
||||
padding: 0 0 10px 0;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
/* 白色半透明背景 */
|
||||
}
|
||||
#cont .cont1{
|
||||
|
||||
#cont .cont1 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#cont .cont2{
|
||||
|
||||
#cont .cont2 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
@@ -348,16 +280,12 @@ onUnmounted(() => {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
/* 白色半透明背景 */
|
||||
}
|
||||
|
||||
.cont2 .el-menu-vertical-demo li {
|
||||
font-size: 14px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) {
|
||||
/* border-radius: 0 0 10px 10px; */
|
||||
/* margin-bottom: 10px; */
|
||||
}
|
||||
|
||||
.cont2 .el-menu-vertical-demo .el-menu-item:hover {
|
||||
background-color: rgba(64, 158, 255, 0.9);
|
||||
}
|
||||
@@ -428,6 +356,7 @@ onUnmounted(() => {
|
||||
/* 白色半透明背景 */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.site-state-item-count {
|
||||
display: block;
|
||||
text-align: center;
|
||||
@@ -443,9 +372,11 @@ onUnmounted(() => {
|
||||
margin-left: 100px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.mylogo_name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mylogo_description {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
@@ -507,141 +438,6 @@ onUnmounted(() => {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* 分类蒙板样式 */
|
||||
.category-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.category-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.category-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.category-modal-close:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-body {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-button {
|
||||
background-color: rgba(102, 161, 216, 0.1);
|
||||
border: 1px solid rgba(102, 161, 216, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.category-button:hover {
|
||||
background-color: rgba(102, 161, 216, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(102, 161, 216, 0.2);
|
||||
}
|
||||
|
||||
.category-button-count {
|
||||
font-size: 12px;
|
||||
color: #66a1d8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 蒙板动画 */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .category-modal-content,
|
||||
.modal-leave-active .category-modal-content {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .category-modal-content {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.modal-leave-to .category-modal-content {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.category-modal-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.category-modal-body::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.category-modal-body::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.category-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-row justify="center">
|
||||
<el-col :span="6" v-if="windowwidth">
|
||||
<div class="grid-content ep-bg-purple-dark">
|
||||
<div class="logo-text">清疯不颠</div>
|
||||
<div class="logo-text"> <a href="http://www.qf1121.top/">清疯不颠</a></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14" justify="center">
|
||||
@@ -31,16 +31,16 @@
|
||||
<!-- 搜索功能 -->
|
||||
<div class="search-wrapper">
|
||||
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
|
||||
<svg t="1761567058506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5586" width="32" height="32"><path d="M512 858.3168c-194.816 0-352-166.2464-352-370.4832S317.184 117.3504 512 117.3504s352 166.2464 352 370.4832-157.184 370.4832-352 370.4832z m0-64c158.6688 0 288-136.8576 288-306.4832 0-169.6768-129.3312-306.4832-288-306.4832S224 318.1568 224 487.8336c0 169.6256 129.3312 306.4832 288 306.4832zM717.312 799.9488a32 32 0 0 1 46.4896-43.9808l91.4432 96.7168a32 32 0 0 1-46.4896 43.9808l-91.4432-96.768z" fill="#5A5A68" p-id="5587"></path></svg>
|
||||
<svg t="1761567058506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5586" width="32" height="32">
|
||||
<path
|
||||
d="M512 858.3168c-194.816 0-352-166.2464-352-370.4832S317.184 117.3504 512 117.3504s352 166.2464 352 370.4832-157.184 370.4832-352 370.4832z m0-64c158.6688 0 288-136.8576 288-306.4832 0-169.6768-129.3312-306.4832-288-306.4832S224 318.1568 224 487.8336c0 169.6256 129.3312 306.4832 288 306.4832zM717.312 799.9488a32 32 0 0 1 46.4896-43.9808l91.4432 96.7168a32 32 0 0 1-46.4896 43.9808l-91.4432-96.768z"
|
||||
fill="#5A5A68" p-id="5587"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文章..."
|
||||
class="search-input"
|
||||
@keyup.enter="performSearch"
|
||||
@blur="closeSearchBoxWithDelay"
|
||||
/>
|
||||
<el-input v-model="searchKeyword" placeholder="回车搜索文章..." class="search-input" @keyup.enter="performSearch"
|
||||
@blur="closeSearchBoxWithDelay" />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -48,31 +48,68 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero 区域 -->
|
||||
<div class="hero" :class="{ 'newhero': classhero }" v-if="windowwidth">
|
||||
<div class="hero" :class="{ 'small-hero': classsmallhero }" v-if="windowwidth"
|
||||
:style="{ marginBottom: heroMarginBottom, transform: heroTransform }">
|
||||
<h1 class="typewriter">{{ heroText }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 提示区域 -->
|
||||
<div id="content-section" :class="{ 'visible': isconts }">
|
||||
<!-- 内容区域 -->
|
||||
<div id="content-section" :class="{ 'visible': iscontentvisible }">
|
||||
<div class="nonsensetitle" v-if="classnonsenset">
|
||||
<div class="nonsensetitleconst">
|
||||
<h1>{{Cardtitle}}</h1>
|
||||
<h1>{{ Cardtitle }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧模块 -->
|
||||
<div class="leftmodluecontainer" v-if="isleftmodluecontainer">
|
||||
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classmoduleorrouter}" v-if="windowwidth" />
|
||||
<LeftModule class="leftmodluepage" @update-data="updateData" @CategoryModal="CategoryModal"
|
||||
@AttributeModal="AttributeModal" :class="{ 'nonsensetmargintop': classmoduleorrouter }" v-if="windowwidth" />
|
||||
</div>
|
||||
|
||||
<!-- 内容模块 -->
|
||||
<RouterView class="RouterViewpage" :class="{'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
|
||||
<div class="RouterViewpage">
|
||||
<RouterView :class="{ 'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
|
||||
<!-- 页脚 -->
|
||||
<Footer class="footer-container" v-if="windowwidth" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分类蒙板组件 -->
|
||||
<Transition name="modal">
|
||||
<div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
|
||||
<div class="category-modal-content">
|
||||
<div class="category-modal-header">
|
||||
<h3>所有分类</h3>
|
||||
<button class="category-modal-close" @click="closeCategoryModal">×</button>
|
||||
</div>
|
||||
<div class="category-modal-body">
|
||||
<button v-for="category in categories" :key="category.typeid" class="category-button"
|
||||
@click="handleCategoryClick(category)">
|
||||
{{ category.typename }} <span class="category-button-count">({{ category.count || 0 }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<div class="Pagination">
|
||||
<!-- 分页组件可以在这里添加 -->
|
||||
<!-- 标签蒙板组件 -->
|
||||
<Transition name="modal">
|
||||
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
|
||||
<div class="category-modal-content">
|
||||
<div class="category-modal-header">
|
||||
<h3>所有标签</h3>
|
||||
<button class="category-modal-close" @click="closeAttributeModal">×</button>
|
||||
</div>
|
||||
<div class="category-modal-body">
|
||||
<button v-for="attribute in attributes" :key="attribute.attributeid" class="category-button"
|
||||
@click="handleAttributeClick(attribute)">
|
||||
{{ attribute.attributename }} <span class="category-button-count">({{ attribute.count || 0 }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 管理员 -->
|
||||
<Establish class="establish-container" v-if="Login" />
|
||||
</template>
|
||||
|
||||
@@ -80,43 +117,48 @@
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import LeftModule from '@/components/LeftModule.vue';
|
||||
import Establish from '@/layouts/establish.vue'
|
||||
import Establish from '@/layouts/establish.vue';
|
||||
import Footer from '@/views/Footer.vue';
|
||||
|
||||
// ========== 组件初始化 ==========
|
||||
|
||||
// 路由相关
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 全局状态管理
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
import { useGlobalStore } from '@/store/globalStore';
|
||||
const globalStore = useGlobalStore();
|
||||
const Login = computed(() => globalStore.Login);
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const Login = computed(() => globalStore.Login)
|
||||
|
||||
// 响应式状态
|
||||
// 文章标题,用于显示在页面上的标题内容
|
||||
// ========== 响应式状态定义 ==========
|
||||
// 页面标题和样式相关状态
|
||||
const Cardtitle = ref('');
|
||||
// 控制模块或路由相关的CSS类名
|
||||
const classmoduleorrouter = ref(false);
|
||||
|
||||
// 控制左侧模块容器是否显示
|
||||
const isleftmodluecontainer = ref(true);
|
||||
|
||||
// 控制hero区域的CSS类名
|
||||
const classhero = ref(false);
|
||||
|
||||
// 控制内容区域是否可见
|
||||
const isconts = ref(false);
|
||||
|
||||
// 控制左侧模块是否处于滚动状态
|
||||
const isScrollingleftmodlue = ref(false);
|
||||
// 顶部导航栏的样式状态(transparent/solid/hide)
|
||||
const elrowtop = ref('transparent');
|
||||
|
||||
// 控制疯言疯语页面标题区域的显示
|
||||
const classnonsenset = ref(false);
|
||||
// 判断窗口是否为宽屏(大于768px)
|
||||
const windowwidth = ref(true);
|
||||
const activeIndex = ref('home');
|
||||
const localhome= 'home';
|
||||
const classsmallhero = ref(false);
|
||||
const elrowtop = ref('transparent');
|
||||
// hero区域的margin值,用于实现滚动时动态变化
|
||||
const heroMarginBottom = ref('45%');
|
||||
// hero区域的初始margin值,从CSS变量获取
|
||||
const initialHeroMarginBottom = 45;
|
||||
// hero是否开始向上移动
|
||||
const heroIsMoving = ref(false);
|
||||
// hero的transform值,用于实现向上移动和吸附效果
|
||||
const heroTransform = ref('translateY(450px)');
|
||||
const heroTransformValue = 450;
|
||||
// hero的位置状态:static(静态)、moving(移动中)、sticky(吸附顶部)
|
||||
const heroPosition = ref('static');
|
||||
|
||||
// 布局相关状态
|
||||
const isleftmodluecontainer = ref(true);
|
||||
const iscontentvisible = ref(false);
|
||||
const isScrollingleftmodlue = ref(false);
|
||||
const windowwidth = ref(true);
|
||||
|
||||
// 导航相关状态
|
||||
const activeIndex = ref('home');
|
||||
const localhome = 'home';
|
||||
let rpsliturl = route.path.split('/');
|
||||
|
||||
// 搜索相关状态
|
||||
@@ -130,33 +172,222 @@ const heroText = ref('');
|
||||
let heroIndex = 0;
|
||||
let heroTimer: number | undefined;
|
||||
|
||||
// 蒙版相关状态
|
||||
const categories = ref<any[]>([])
|
||||
const showCategoryModal = ref(false)
|
||||
const attributes = ref<any[]>([])
|
||||
const showAttributeModal = ref(false)
|
||||
// 显示分类蒙板
|
||||
const openCategoryModal = () => {
|
||||
showCategoryModal.value = true;
|
||||
}
|
||||
// 关闭分类蒙板
|
||||
const closeCategoryModal = () => {
|
||||
showCategoryModal.value = false;
|
||||
}
|
||||
// 显示标签蒙板
|
||||
const openAttributeModal = () => {
|
||||
showAttributeModal.value = true;
|
||||
}
|
||||
// 关闭标签蒙板
|
||||
const closeAttributeModal = () => {
|
||||
showAttributeModal.value = false;
|
||||
}
|
||||
// 左侧状态栏传值
|
||||
const updateData = (data: any) => {
|
||||
// 处理异步数据
|
||||
if (data.fetchCategories && typeof data.fetchCategories.then === 'function') {
|
||||
data.fetchCategories.then(result => {
|
||||
categories.value = result || []
|
||||
})
|
||||
} else {
|
||||
categories.value = data.fetchCategories || []
|
||||
}
|
||||
if (data.fetchAttributes && typeof data.fetchAttributes.then === 'function') {
|
||||
data.fetchAttributes.then(result => {
|
||||
attributes.value = result || []
|
||||
})
|
||||
} else {
|
||||
attributes.value = data.fetchAttributes || []
|
||||
}
|
||||
}
|
||||
// 分类相关状态
|
||||
const CategoryModal = async (data: any) => {
|
||||
if (data.ifmodal) {
|
||||
openCategoryModal()
|
||||
// console.log('打开分类蒙板')
|
||||
} else {
|
||||
closeCategoryModal()
|
||||
// console.log('关闭分类蒙板')
|
||||
}
|
||||
}
|
||||
// 标签相关状态
|
||||
const AttributeModal = async (data: any) => {
|
||||
if (data.ifmodal) {
|
||||
openAttributeModal()
|
||||
// console.log('打开标签蒙板')
|
||||
} else {
|
||||
closeAttributeModal()
|
||||
// console.log('关闭标签蒙板')
|
||||
}
|
||||
}
|
||||
// ========== 蒙版事件 ==========
|
||||
|
||||
// 处理分类点击
|
||||
const handleCategoryClick = (category: any) => {
|
||||
// 这里可以根据实际需求跳转到对应分类的文章列表页
|
||||
// 重置全局属性状态
|
||||
globalStore.removeValue('category')
|
||||
|
||||
globalStore.setValue('category', {
|
||||
id: category.categoryid,
|
||||
name: category.categoryname
|
||||
})
|
||||
console.log(category)
|
||||
router.push('/home/aericlecategory',)
|
||||
closeCategoryModal()
|
||||
}
|
||||
|
||||
// 处理标签点击
|
||||
const handleAttributeClick = (attribute: any) => {
|
||||
// 重置全局属性状态
|
||||
globalStore.removeValue('attribute')
|
||||
|
||||
globalStore.setValue('attribute', {
|
||||
id: attribute.attributeid,
|
||||
name: attribute.attributename
|
||||
})
|
||||
console.log(attribute)
|
||||
router.push('/home/aericletype',)
|
||||
closeAttributeModal()
|
||||
}
|
||||
|
||||
// ========== 打字机效果模块 ==========
|
||||
|
||||
/**
|
||||
* 打字机效果函数
|
||||
* 初始化并启动打字机效果
|
||||
* @param {string} text - 要显示的完整文本
|
||||
*/
|
||||
const startTypewriter = () => {
|
||||
const startTypewriter = (text: string) => {
|
||||
// 重置状态
|
||||
heroText.value = '';
|
||||
heroIndex = 0;
|
||||
if (heroTimer) clearInterval(heroTimer);
|
||||
|
||||
// 清除可能存在的定时器
|
||||
if (heroTimer) {
|
||||
clearInterval(heroTimer);
|
||||
}
|
||||
|
||||
// 设置新的定时器,逐字显示文本
|
||||
heroTimer = window.setInterval(() => {
|
||||
if (heroIndex < fullHeroText.length) {
|
||||
heroText.value += fullHeroText[heroIndex];
|
||||
if (heroIndex < text.length) {
|
||||
heroText.value += text[heroIndex];
|
||||
heroIndex++;
|
||||
} else {
|
||||
// 文本显示完毕,清除定时器
|
||||
clearInterval(heroTimer);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单选择跳转
|
||||
* 停止打字机效果并显示完整文本
|
||||
*/
|
||||
const stopTypewriter = () => {
|
||||
// 清除定时器
|
||||
if (heroTimer) {
|
||||
clearInterval(heroTimer);
|
||||
}
|
||||
// 非首页时清空hero内容
|
||||
if (rpsliturl[1] !== localhome) {
|
||||
heroText.value = '';
|
||||
} else {
|
||||
// 首页直接显示完整文本
|
||||
heroText.value = fullHeroText;
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 导航和路由处理模块 ==========
|
||||
|
||||
/**
|
||||
* 处理菜单选择并跳转到对应路由
|
||||
* @param {string} key - 菜单项的key值,对应路由路径
|
||||
*/
|
||||
const handleSelect = (key: string) => {
|
||||
// globalStore.clearAll()
|
||||
router.push({ path: '/' + key });
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换搜索框显示/隐藏
|
||||
* 设置当前激活的菜单项并存储路径信息
|
||||
* @param {string} path - 当前路由路径
|
||||
*/
|
||||
const setActiveIndex = (path: string) => {
|
||||
// 存储当前路径到全局状态
|
||||
globalStore.setValue('localpath', {
|
||||
name: path
|
||||
});
|
||||
|
||||
// 特殊处理消息页面,清除文章信息
|
||||
if (path === 'message') {
|
||||
globalStore.removeValue('articleInfo');
|
||||
}
|
||||
|
||||
// 更新激活菜单项
|
||||
activeIndex.value = path;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据路由路径设置页面状态
|
||||
*/
|
||||
const updatePageState = () => {
|
||||
// 根据是否为主页根路径设置hero区域状态
|
||||
classsmallhero.value = !(rpsliturl[1] == localhome && rpsliturl[2] == undefined);
|
||||
// 控制左侧模块容器的显示/隐藏
|
||||
isleftmodluecontainer.value = rpsliturl[1] !== "articlesave";
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新文章标题和相关显示状态
|
||||
*/
|
||||
const updateArticleTitle = () => {
|
||||
let articledata: any = null;
|
||||
// 根据不同路由参数获取文章标题数据
|
||||
if (rpsliturl[2] === 'aericleattribute') {
|
||||
// 按属性类型获取
|
||||
articledata = globalStore.getValue('attribute')?.name;
|
||||
}
|
||||
else if (rpsliturl[2] === 'aericletitle') {
|
||||
// 按标题搜索获取
|
||||
articledata = globalStore.getValue('articleserarch')?.name;
|
||||
}
|
||||
else if (rpsliturl[1] === 'nonsense') {
|
||||
// 疯言疯语页面特殊处理
|
||||
articledata = "疯言疯语";
|
||||
}
|
||||
|
||||
// 确定标题区域的显示状态
|
||||
const shouldHideTitle =
|
||||
// 特殊页面不需要显示标题
|
||||
(rpsliturl[1] === 'articlelist' ||
|
||||
rpsliturl[1] === 'message' ||
|
||||
rpsliturl[1] === 'about') ||
|
||||
// 在主页且无标题数据时,不显示标题
|
||||
(rpsliturl[1] === localhome && !articledata);
|
||||
|
||||
// 更新标题显示状态
|
||||
classnonsenset.value = !shouldHideTitle && !!articledata;
|
||||
classmoduleorrouter.value = !shouldHideTitle && !!articledata;
|
||||
|
||||
// 设置标题内容
|
||||
if (articledata) {
|
||||
Cardtitle.value = articledata;
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 搜索功能模块 ==========
|
||||
|
||||
/**
|
||||
* 切换搜索框的显示/隐藏状态
|
||||
*/
|
||||
const toggleSearchBox = () => {
|
||||
isSearchBoxOpen.value = !isSearchBoxOpen.value;
|
||||
@@ -168,20 +399,23 @@ const toggleSearchBox = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭搜索框
|
||||
* 立即关闭搜索框
|
||||
*/
|
||||
const closeSearchBox = () => {
|
||||
isSearchBoxOpen.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 带延迟关闭搜索框(处理点击搜索按钮后的情况)
|
||||
* 带延迟关闭搜索框
|
||||
* 用于处理点击搜索按钮后的情况,让用户有时间看到反馈
|
||||
*/
|
||||
const closeSearchBoxWithDelay = () => {
|
||||
// 清除可能存在的定时器
|
||||
if (searchCloseTimer) {
|
||||
clearTimeout(searchCloseTimer);
|
||||
}
|
||||
|
||||
// 设置新的延迟关闭定时器
|
||||
searchCloseTimer = window.setTimeout(() => {
|
||||
isSearchBoxOpen.value = false;
|
||||
}, 300);
|
||||
@@ -189,15 +423,19 @@ const closeSearchBoxWithDelay = () => {
|
||||
|
||||
/**
|
||||
* 执行搜索操作
|
||||
* 将搜索关键词存储到全局状态并跳转到搜索结果页面
|
||||
*/
|
||||
const performSearch = () => {
|
||||
// 验证搜索关键词不为空
|
||||
if (searchKeyword.value.trim()) {
|
||||
// 这里可以根据实际需求实现搜索逻辑
|
||||
// 存储文章信息到全局状态
|
||||
// 清除全局搜索关键词
|
||||
globalStore.removeValue('articleserarch');
|
||||
// 存储搜索关键词到全局状态
|
||||
globalStore.setValue('articleserarch', {
|
||||
name: searchKeyword.value
|
||||
})
|
||||
router.push({ path: `/home/aericletitle`});
|
||||
});
|
||||
// 跳转到搜索结果页面
|
||||
router.push({ path: `/home/aericletitle` });
|
||||
}
|
||||
|
||||
// 搜索后保持搜索框打开状态
|
||||
@@ -206,154 +444,185 @@ const performSearch = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据路由路径设置页面状态
|
||||
*/
|
||||
const updatePageState = () => {
|
||||
if (rpsliturl[1] == localhome && rpsliturl[2] == undefined) {
|
||||
classhero.value = false;
|
||||
} else {
|
||||
classhero.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置当前激活的菜单项
|
||||
*/
|
||||
const setActiveIndex = (path: string) => {
|
||||
// 存储文章信息到全局状态
|
||||
globalStore.setValue('localpath', {
|
||||
name: path
|
||||
})
|
||||
if (path === 'message') {
|
||||
globalStore.removeValue('articleInfo')
|
||||
}
|
||||
activeIndex.value =path;
|
||||
};
|
||||
|
||||
|
||||
// ========== 响应式处理模块 ==========
|
||||
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
* 根据屏幕宽度调整布局和内容显示
|
||||
*/
|
||||
const handleResize = () => {
|
||||
|
||||
// 更新窗口宽度状态
|
||||
windowwidth.value = window.innerWidth > 768;
|
||||
|
||||
// 根据屏幕大小调整内容区可见性
|
||||
// 首页特殊处理:小屏幕下默认显示内容区
|
||||
if (rpsliturl[1] === localhome) {
|
||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
||||
iscontentvisible.value = window.innerWidth <= 768;
|
||||
}
|
||||
// 移动端首页默认显示内容区,桌面端初始隐藏
|
||||
iscontentvisible.value = window.innerWidth <= 768;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理滚动事件
|
||||
* 处理页面滚动事件
|
||||
* 根据滚动位置调整导航栏样式和内容显示动画
|
||||
*/
|
||||
const handleScroll = () => {
|
||||
// 屏幕小于768时只切换导航栏样式,不做内容动画
|
||||
if (window.innerWidth < 768) {
|
||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
||||
let scrollY = 0;
|
||||
scrollY = window.scrollY;
|
||||
// 小屏幕设备只切换导航栏样式
|
||||
if (window.innerWidth <= 768) {
|
||||
updateNavbarStyle(scrollY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 导航栏样式切换
|
||||
if (window.scrollY > 1200) {
|
||||
elrowtop.value = 'hide';
|
||||
} else {
|
||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
||||
}
|
||||
// 首页内容区滚动动画
|
||||
// 大屏幕设备完整处理
|
||||
updateNavbarStyle(scrollY);
|
||||
// 仅在首页根路径应用滚动动画
|
||||
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||
isconts.value = window.scrollY > 200;
|
||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
||||
// 首页滚动动画处理
|
||||
if (scrollY <= 350) {
|
||||
HeroState(scrollY);
|
||||
}
|
||||
// 控制左侧模块的滚动状态
|
||||
isScrollingleftmodlue.value = scrollY > 600;
|
||||
}
|
||||
};
|
||||
// ========== 首页滚动动画模块 ==========
|
||||
const HeroState = (scrollY: number) => {
|
||||
const windowHeight = window.innerHeight;
|
||||
// 计算滚动距离与窗口高度的比例,用于内容渐显
|
||||
const contentScrollRatio = Math.min(scrollY / windowHeight, 1);
|
||||
// 计算新的底部margin值,从初始值45%逐渐减少到25%
|
||||
const newMarginBottom = Math.max(initialHeroMarginBottom - (initialHeroMarginBottom * contentScrollRatio), 0);
|
||||
// 更新hero的底部margin值
|
||||
heroMarginBottom.value = `${newMarginBottom}%`;
|
||||
// 计算新的translateY值,从初始值450px逐渐减少到150px
|
||||
const translateYValue = Math.max(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 90);
|
||||
heroTransform.value = `translateY(${translateYValue}px)`;
|
||||
// 当滚动超过100px时开始显示,滚动到一屏高度时完全显示
|
||||
iscontentvisible.value = scrollY > 100;
|
||||
// 当滚动超过287px时logo被顶出屏幕,触发移动状态
|
||||
if (scrollY > 287) {
|
||||
heroPosition.value = 'moving';
|
||||
const translateYValue = Math.min(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 0);
|
||||
heroTransform.value = `translateY(${translateYValue / 2}px)`;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 根据滚动位置更新导航栏样式
|
||||
* @param {number} scrollY - 当前滚动位置
|
||||
*/
|
||||
const updateNavbarStyle = (scrollY: number) => {
|
||||
// 根据滚动位置设置导航栏样式
|
||||
// 当滚动超过1200px且屏幕宽度大于768px时隐藏导航栏
|
||||
if (scrollY > 1200 && window.innerWidth > 768) {
|
||||
elrowtop.value = 'hide'; // 隐藏导航栏
|
||||
} else {
|
||||
elrowtop.value = scrollY > 50 ? 'solid' : 'transparent'; // 固定或透明样式
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新文章标题和相关状态
|
||||
*/
|
||||
const updateArticleTitle = () => {
|
||||
let articledata: any = null;
|
||||
|
||||
// 优先使用attributeId参数(新接口)
|
||||
if (rpsliturl[2] === 'aericletype') {
|
||||
articledata = globalStore.getValue('attribute')?.name;
|
||||
}
|
||||
// 搜索标题
|
||||
else if (rpsliturl[2] === 'aericletitle') {
|
||||
articledata = globalStore.getValue('title')?.name;
|
||||
}
|
||||
// 疯言疯语页面
|
||||
else if (rpsliturl[1] === 'nonsense') {
|
||||
articledata = "疯言疯语";
|
||||
}
|
||||
|
||||
// 特殊页面不需要显示标题
|
||||
if (rpsliturl[1] === 'article-list' || rpsliturl[1] === 'message' || rpsliturl[1] === 'about') {
|
||||
classnonsenset.value = false;
|
||||
classmoduleorrouter.value = false;
|
||||
}
|
||||
// 在主页且articledata为空时,不显示标题
|
||||
else if (rpsliturl[1] === localhome && !articledata) {
|
||||
classnonsenset.value = false;
|
||||
classmoduleorrouter.value = false;
|
||||
}
|
||||
|
||||
// 显示文章标题
|
||||
else if (articledata) {
|
||||
Cardtitle.value = articledata;
|
||||
classnonsenset.value = true;
|
||||
classmoduleorrouter.value = true;
|
||||
}
|
||||
};
|
||||
// ========== 路由监听和页面初始化 ==========
|
||||
|
||||
/**
|
||||
* 监听路由变化
|
||||
* 处理路由变化的核心函数
|
||||
* 更新页面状态、标题、激活菜单项等
|
||||
*/
|
||||
watch(() => route.path, () => {
|
||||
const handleRouteChange = () => {
|
||||
// 重新解析路由路径
|
||||
rpsliturl = route.path.split('/');
|
||||
// 更新页面相关状态
|
||||
updatePageState();
|
||||
setActiveIndex(rpsliturl[1]);
|
||||
updateArticleTitle();
|
||||
// 跳转后回到顶部
|
||||
|
||||
// 页面跳转后回到顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
// 首页内容区滚动动画仅大屏下生效
|
||||
|
||||
// 根据是否为首页决定是否启动打字机效果
|
||||
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||
// 首页时启动打字机效果
|
||||
startTypewriter();
|
||||
// 首页启动打字机效果
|
||||
startTypewriter(fullHeroText);
|
||||
// 重置hero的margin值为初始值
|
||||
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
|
||||
// 重置hero的移动状态
|
||||
heroTransform.value = `translateY(${heroTransformValue}px)`;
|
||||
heroIsMoving.value = false;
|
||||
heroPosition.value = 'static';
|
||||
} else {
|
||||
isconts.value = true;
|
||||
heroText.value =fullHeroText;
|
||||
if (heroTimer) clearInterval(heroTimer);
|
||||
iscontentvisible.value = true;
|
||||
startTypewriter(fullHeroText);
|
||||
heroMarginBottom.value = `${5}%`;
|
||||
heroTransform.value = ``;
|
||||
}
|
||||
// 非首页时关闭左侧状态栏
|
||||
if (rpsliturl[1] == "articlesave") {
|
||||
isleftmodluecontainer.value = false;
|
||||
} else {
|
||||
isleftmodluecontainer.value = true;
|
||||
}
|
||||
}, { immediate: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* 生命周期钩子
|
||||
* 初始化页面
|
||||
*/
|
||||
const initializePage = () => {
|
||||
// 初始化窗口大小
|
||||
handleResize();
|
||||
// 启动打字机效果(如果是首页)
|
||||
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||
// 首页启动打字机效果
|
||||
startTypewriter(fullHeroText);
|
||||
// 重置hero的margin值为初始值
|
||||
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
|
||||
// 重置hero的移动状态
|
||||
heroIsMoving.value = false;
|
||||
heroPosition.value = 'static';
|
||||
// 移动端首页默认显示内容区,桌面端初始隐藏
|
||||
iscontentvisible.value = window.innerWidth <= 768;
|
||||
} else {
|
||||
startTypewriter(fullHeroText);
|
||||
heroMarginBottom.value = `${2.5}%`;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// ========== 生命周期钩子 ==========
|
||||
|
||||
/**
|
||||
* 组件挂载时执行
|
||||
*/
|
||||
onMounted(() => {
|
||||
// 初始化窗口大小
|
||||
// 初始化页面
|
||||
initializePage();
|
||||
|
||||
|
||||
handleResize();
|
||||
// 添加事件监听器
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
/**
|
||||
* 组件卸载时执行
|
||||
*/
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听器
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
|
||||
// 清理定时器
|
||||
if (heroTimer) clearInterval(heroTimer);
|
||||
if (heroTimer) {
|
||||
clearInterval(heroTimer);
|
||||
}
|
||||
if (searchCloseTimer) {
|
||||
clearTimeout(searchCloseTimer);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 监听器 ==========
|
||||
|
||||
/**
|
||||
* 监听路由变化
|
||||
* 当路由变化时更新页面状态
|
||||
*/
|
||||
watch(
|
||||
() => route.path,
|
||||
handleRouteChange
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -403,6 +672,10 @@ onUnmounted(() => {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -427,10 +700,143 @@ onUnmounted(() => {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 搜索框样式 */
|
||||
/* ... 现有搜索框样式 ... */
|
||||
|
||||
/* 蒙版样式 */
|
||||
.category-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.category-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.category-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.category-modal-close:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-body {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-button {
|
||||
background-color: rgba(102, 161, 216, 0.1);
|
||||
border: 1px solid rgba(102, 161, 216, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.category-button:hover {
|
||||
background-color: rgba(102, 161, 216, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(102, 161, 216, 0.2);
|
||||
}
|
||||
|
||||
.category-button-count {
|
||||
font-size: 12px;
|
||||
color: #66a1d8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 蒙板动画 */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .category-modal-content,
|
||||
.modal-leave-active .category-modal-content {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .category-modal-content {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.modal-leave-to .category-modal-content {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.footer-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 防止搜索框在小屏幕上重叠 */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.search-box-container.open {
|
||||
width: 250px;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<div class="establish-component-root">
|
||||
<div class="establish-container">
|
||||
<!-- 弹出的按钮容器 -->
|
||||
<!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> -->
|
||||
<button
|
||||
v-for="(btn, index) in isbuttonsave()"
|
||||
:class="['action-button', { 'show': isExpanded }]"
|
||||
:style="getButtonStyle(index)"
|
||||
:key="btn.id"
|
||||
@click="handleButtonClick(btn)">
|
||||
<button v-for="(btn, index) in isbuttonsave()" :class="['action-button', { 'show': isExpanded }]"
|
||||
:style="getButtonStyle(index)" :key="btn.id" @click="handleButtonClick(btn)">
|
||||
<!-- <i :class="btn.icon"></i> -->
|
||||
<span>{{ btn.label }}</span>
|
||||
</button>
|
||||
@@ -18,33 +15,102 @@
|
||||
<i class="icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 标签蒙板组件 -->
|
||||
<Transition name="modal">
|
||||
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
|
||||
<div class="category-modal-content">
|
||||
<div class="category-modal-header">
|
||||
<h3>新建标签</h3>
|
||||
<button class="category-modal-close" @click="closeAttributeModal">×</button>
|
||||
</div>
|
||||
<div class="category-modal-body">
|
||||
|
||||
<el-select v-model="CategoryAttributeDto.categoryid" placeholder="请选择">
|
||||
<el-option v-for="item in categories" :key="item.value" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-input v-model="CategoryAttributeDto.name" placeholder="输入新标签" />
|
||||
<el-button type="primary"
|
||||
@click="saveAttribute(CategoryAttributeDto)">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 组件导入和初始化
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { articleService, messageService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services'
|
||||
import { articleService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services'
|
||||
|
||||
// 全局状态管理
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
// 路由参数
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 定义响应式状态
|
||||
// ==============================
|
||||
// 状态管理模块
|
||||
// ==============================
|
||||
/**
|
||||
* 按钮展开状态
|
||||
* @type {import('vue').Ref<boolean>}
|
||||
*/
|
||||
const isExpanded = ref(false)
|
||||
// 疯言疯语模态框状态
|
||||
|
||||
/**
|
||||
* 分类列表
|
||||
* @type {import('vue').Ref<Array<{value: string, label: string}>>}
|
||||
*/
|
||||
const categories = ref([])
|
||||
|
||||
/**
|
||||
* 标签模态框显示状态
|
||||
* @type {import('vue').Ref<boolean>}
|
||||
*/
|
||||
const showAttributeModal = ref(false)
|
||||
|
||||
/**
|
||||
* 疯言疯语模态框显示状态
|
||||
* @type {import('vue').Ref<boolean>}
|
||||
*/
|
||||
const isNonsenseModalVisible = ref(false)
|
||||
|
||||
/**
|
||||
* 疯言疯语内容
|
||||
* @type {import('vue').Ref<string>}
|
||||
*/
|
||||
const nonsenseContent = ref('')
|
||||
// 基础按钮配置
|
||||
|
||||
/**
|
||||
* 标签数据传输对象
|
||||
* @type {import('vue').Ref<{categoryid: string, name: string}>}
|
||||
*/
|
||||
const CategoryAttributeDto = ref({
|
||||
categoryid: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
// ==============================
|
||||
// 按钮配置和管理模块
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 基础按钮配置
|
||||
* @type {Array<{id: string, label: string, icon: string}>}
|
||||
*/
|
||||
const baseButtons = [
|
||||
{ id: 'logout', label: '登出', icon: 'icon-logout' },
|
||||
{ id: 'reload', label: '刷新', icon: 'icon-reload' }
|
||||
|
||||
]
|
||||
|
||||
// 页面特定按钮配置
|
||||
/**
|
||||
* 页面特定按钮配置映射
|
||||
* @type {Object.<string, Array<{id: string, label: string, icon: string}>>}
|
||||
*/
|
||||
const pageButtons = {
|
||||
// 文章详情页按钮
|
||||
article: [
|
||||
@@ -70,7 +136,8 @@ const pageButtons = {
|
||||
|
||||
// 分类页面按钮
|
||||
articlelist: [
|
||||
{ id: 'create-category', label: '新建', icon: 'icon-create-category' },
|
||||
{ id: 'create-category', label: '分类', icon: 'icon-create-category' },
|
||||
{ id: 'create-tag', label: '标签', icon: 'icon-create-tag' },
|
||||
...baseButtons
|
||||
],
|
||||
|
||||
@@ -89,23 +156,11 @@ const pageButtons = {
|
||||
// 默认按钮
|
||||
default: baseButtons
|
||||
}
|
||||
// 根据status状态获取按钮配置
|
||||
const getButtonsByStatus = (status) => {
|
||||
globalStore.removeValue('articlestatus')
|
||||
globalStore.setValue('articlestatus', {
|
||||
status: status
|
||||
})
|
||||
//跳转文章列表页面,添加时间戳参数确保页面刷新
|
||||
try {
|
||||
// 添加时间戳作为查询参数,确保页面强制刷新
|
||||
const timestamp = new Date().getTime();
|
||||
router.push({ path: `/home/aericlestatus`, query: { t: timestamp } })
|
||||
} catch (error) {
|
||||
console.error('页面跳转失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据当前页面返回对应的按钮配置
|
||||
/**
|
||||
* 根据当前页面返回对应的按钮配置
|
||||
* @returns {Array<{id: string, label: string, icon: string}>} 按钮配置数组
|
||||
*/
|
||||
const isbuttonsave = () => {
|
||||
try {
|
||||
// 获取当前页面路径名称
|
||||
@@ -117,15 +172,60 @@ const isbuttonsave = () => {
|
||||
return pageButtons.default;
|
||||
}
|
||||
}
|
||||
// 切换按钮显示状态
|
||||
|
||||
/**
|
||||
* 切换按钮展开/收起状态
|
||||
* @param {Event} event - 点击事件对象
|
||||
*/
|
||||
const toggleExpand = (event) => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
// 防止点击弹出的按钮后再次触发主按钮的点击事件
|
||||
event.stopPropagation()
|
||||
|
||||
}
|
||||
|
||||
// 显示疯言疯语模态框
|
||||
/**
|
||||
* 为每个按钮计算动态样式
|
||||
* @param {number} index - 按钮索引
|
||||
* @returns {Object} CSS样式对象
|
||||
*/
|
||||
const getButtonStyle = (index) => {
|
||||
// 计算过渡时间,确保显示时从上到下逐渐出现,隐藏时从下到上逐渐消失
|
||||
const showDelay = Math.max(0.1, 0.2 + index * 0.2);
|
||||
const hideDelay = 0.3 + index * 0.2;
|
||||
|
||||
// 根据是否展开返回不同的过渡样式
|
||||
if (isExpanded.value) {
|
||||
return {
|
||||
transition: `all ${showDelay}s ease-in-out`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
transition: `all ${hideDelay}s ease-in-out`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================
|
||||
// 模态框管理模块
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 显示标签模态框
|
||||
*/
|
||||
const showAttributes = () => {
|
||||
showAttributeModal.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭标签模态框
|
||||
*/
|
||||
const closeAttributeModal = () => {
|
||||
showAttributeModal.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示疯言疯语模态框
|
||||
*/
|
||||
const showNonsenseModal = () => {
|
||||
nonsenseContent.value = '' // 清空输入框
|
||||
isNonsenseModalVisible.value = true
|
||||
@@ -145,7 +245,43 @@ const showNonsenseModal = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 保存疯言疯语
|
||||
// ==============================
|
||||
// 功能操作方法模块
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 处理错误响应的工具函数
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} defaultMessage - 默认错误消息
|
||||
*/
|
||||
const handleErrorResponse = (error, defaultMessage = '操作失败') => {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.message || defaultMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文章状态获取并跳转文章列表
|
||||
* @param {number} status - 文章状态 0:未发表 1:已发表 2:已删除
|
||||
*/
|
||||
const getButtonsByStatus = (status) => {
|
||||
globalStore.removeValue('articlestatus')
|
||||
globalStore.setValue('articlestatus', {
|
||||
status: status
|
||||
})
|
||||
|
||||
// 跳转文章列表页面,添加时间戳参数确保页面刷新
|
||||
try {
|
||||
const timestamp = new Date().getTime();
|
||||
router.push({ path: `/home/aericlestatus`, query: { t: timestamp } })
|
||||
} catch (error) {
|
||||
console.error('页面跳转失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存疯言疯语
|
||||
* @param {string} content - 疯言疯语内容
|
||||
*/
|
||||
const saveNonsense = (content) => {
|
||||
if (!content || content.trim() === '') {
|
||||
ElMessage.warning('内容不能为空')
|
||||
@@ -153,23 +289,24 @@ const saveNonsense = (content) => {
|
||||
}
|
||||
|
||||
// 调用服务保存疯言疯语
|
||||
nonsenseService.saveNonsense({
|
||||
nonsenseService.createNonsense({
|
||||
content: content.trim(),
|
||||
time: new Date()
|
||||
time: new Date(),
|
||||
status: 1
|
||||
}).then(response => {
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('疯言疯语发布成功')
|
||||
// 刷新页面
|
||||
router.push({ path: `/nonsense`});
|
||||
} else {
|
||||
ElMessage.error(response.message || '发布失败')
|
||||
}
|
||||
}).catch(err => handleErrorResponse(err, '发布失败'))
|
||||
}
|
||||
// 处理错误响应的工具函数
|
||||
const handleErrorResponse = (error, defaultMessage = '操作失败') => {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error(error.message || defaultMessage)
|
||||
}
|
||||
// articlelist新建
|
||||
|
||||
/**
|
||||
* 新建分类
|
||||
*/
|
||||
const createCategory = () => {
|
||||
ElMessageBox.prompt('请输入分类名称:', '新建分类', {
|
||||
confirmButtonText: '保存',
|
||||
@@ -195,28 +332,80 @@ const createCategory = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 保存分类
|
||||
/**
|
||||
* 保存分类
|
||||
* @param {string} typename - 分类名称
|
||||
*/
|
||||
const saveCategory = (typename) => {
|
||||
categoryService.saveCategory({
|
||||
// console.log('保存分类')
|
||||
categoryService.createCategory({
|
||||
typename: typename
|
||||
}).then(response => {
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('分类创建成功');
|
||||
// 刷新页面以显示新分类
|
||||
const timestamp = new Date().getTime();
|
||||
router.push({ path: `/home/aericlelist`, query: { t: timestamp } });
|
||||
// 刷新页面
|
||||
router.push({ path: `/articlelist`});
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建分类失败');
|
||||
}
|
||||
}).catch(err => handleErrorResponse(err, '创建分类失败'));
|
||||
};
|
||||
// 删除文章方法
|
||||
|
||||
/**
|
||||
* 新建标签
|
||||
*/
|
||||
const createAttribute = async () => {
|
||||
showAttributes()
|
||||
try {
|
||||
const response = await categoryService.getAllCategories();
|
||||
if (response.code === 200) {
|
||||
categories.value = response.data.map(item => ({
|
||||
value: item.typeid,
|
||||
label: item.typename
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
handleErrorResponse(error, '获取分类失败');
|
||||
} finally {
|
||||
// console.log(categories.value)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存标签
|
||||
* @param {{categoryid: string, name: string}} dto - 标签数据传输对象
|
||||
*/
|
||||
const saveAttribute = (dto) => {
|
||||
if (!dto.categoryid || !dto.name) {
|
||||
ElMessage.warning('请选择分类和输入标签名称')
|
||||
return
|
||||
}
|
||||
|
||||
categoryAttributeService.createAttribute({
|
||||
categoryid: Number(dto.categoryid),
|
||||
attributename: dto.name
|
||||
}).then(response => {
|
||||
if (response.code === 200) {
|
||||
closeAttributeModal()
|
||||
ElMessage.success('标签创建成功');
|
||||
// 刷新页面
|
||||
router.push({ path: `/articlelist`});
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建标签失败');
|
||||
}
|
||||
}).catch(err => handleErrorResponse(err, '创建标签失败'));
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除文章
|
||||
*/
|
||||
const deleteArticle = () => {
|
||||
const articleId = globalStore.getValue('articleInfo')?.id
|
||||
const articleId = globalStore.getValue('articleInfo')?.articleid
|
||||
if (!articleId) {
|
||||
ElMessage.warning('缺少文章ID参数')
|
||||
return
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
ElMessageBox.confirm('确定删除该文章吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -239,13 +428,17 @@ const deleteArticle = () => {
|
||||
// 取消删除,静默处理
|
||||
})
|
||||
}
|
||||
// 修改文章方法
|
||||
|
||||
/**
|
||||
* 修改文章
|
||||
*/
|
||||
const updateArticle = () => {
|
||||
const articleId = globalStore.getValue('articleInfo')
|
||||
if (!articleId) {
|
||||
ElMessage.warning('缺少文章参数')
|
||||
return
|
||||
}
|
||||
|
||||
// 确认修改
|
||||
ElMessageBox.confirm('确定修改该文章吗?', '修改确认', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -259,7 +452,9 @@ const updateArticle = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 登出方法
|
||||
/**
|
||||
* 登出系统
|
||||
*/
|
||||
const logout = () => {
|
||||
// 调用登出接口
|
||||
loginService.logout()
|
||||
@@ -278,33 +473,38 @@ const logout = () => {
|
||||
})
|
||||
.catch(err => handleErrorResponse(err, '登出失败'))
|
||||
}
|
||||
// 刷新页面
|
||||
|
||||
/**
|
||||
* 刷新页面
|
||||
*/
|
||||
const reloadPage = () => {
|
||||
globalStore.clearAll()
|
||||
|
||||
}
|
||||
// 处理按钮点击事件
|
||||
|
||||
// ==============================
|
||||
// 事件处理模块
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 处理按钮点击事件
|
||||
* @param {{id: string, label: string, icon: string}} button - 按钮对象
|
||||
*/
|
||||
const handleButtonClick = (button) => {
|
||||
console.log('点击了按钮:', button.id, button.label)
|
||||
// console.log('点击了按钮:', button.id, button.label)
|
||||
|
||||
// 使用按钮ID进行处理,更可靠且易于维护
|
||||
switch (button.id) {
|
||||
// 新增操作
|
||||
case 'create-article':
|
||||
// 清除更新文章状态
|
||||
router.push({ path: '/articlesave' })
|
||||
break
|
||||
|
||||
case 'create-category':
|
||||
createCategory()
|
||||
break
|
||||
|
||||
case 'create-tag':
|
||||
// router.push({ path: '/tagsave' })
|
||||
createAttribute()
|
||||
break
|
||||
|
||||
case 'create-nonsense':
|
||||
// 显示疯言疯语模态框
|
||||
showNonsenseModal()
|
||||
break
|
||||
|
||||
@@ -322,24 +522,20 @@ const handleButtonClick = (button) => {
|
||||
case 'del-articles':
|
||||
getButtonsByStatus(2)
|
||||
break
|
||||
|
||||
case 'unpublished-articles':
|
||||
getButtonsByStatus(0)
|
||||
break
|
||||
|
||||
case 'published-articles':
|
||||
getButtonsByStatus(1)
|
||||
break
|
||||
|
||||
case 'view-articles':
|
||||
router.push({ path: '/home' })
|
||||
break
|
||||
|
||||
// 登出操作
|
||||
// 系统操作
|
||||
case 'logout':
|
||||
logout();
|
||||
break
|
||||
|
||||
case 'reload':
|
||||
reloadPage()
|
||||
break
|
||||
@@ -349,32 +545,14 @@ const handleButtonClick = (button) => {
|
||||
ElMessage.info(`功能 ${button.label} 暂未实现`)
|
||||
}
|
||||
|
||||
|
||||
// 点击后收起按钮菜单
|
||||
isExpanded.value = false
|
||||
}
|
||||
|
||||
// 为每个按钮计算动态样式
|
||||
const getButtonStyle = (index) => {
|
||||
// 计算过渡时间,确保显示时从上到下逐渐出现,隐藏时从下到上逐渐消失
|
||||
// 显示时间:0.9s - index * 0.2s (递减)
|
||||
// 隐藏时间:0.3s + index * 0.2s (递增)
|
||||
const showDelay = Math.max(0.1, 0.2 + index * 0.2);
|
||||
const hideDelay = 0.3 + index * 0.2;
|
||||
|
||||
// 根据是否展开返回不同的过渡样式
|
||||
if (isExpanded.value) {
|
||||
return {
|
||||
transition: `all ${showDelay}s ease-in-out`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
transition: `all ${hideDelay}s ease-in-out`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 点击外部区域收起按钮的处理函数
|
||||
/**
|
||||
* 点击外部区域收起按钮的处理函数
|
||||
* @param {Event} e - 点击事件对象
|
||||
*/
|
||||
const handleClickOutside = (e) => {
|
||||
const mainButton = document.querySelector('.main-button')
|
||||
const actionButtons = document.querySelectorAll('.action-button')
|
||||
@@ -391,12 +569,20 @@ const handleClickOutside = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子 - 挂载时添加事件监听
|
||||
// ==============================
|
||||
// 组件生命周期
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 组件挂载时添加事件监听
|
||||
*/
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
// 生命周期钩子 - 卸载前清理事件监听
|
||||
/**
|
||||
* 组件卸载前清理事件监听
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
@@ -494,6 +680,78 @@ onBeforeUnmount(() => {
|
||||
margin-left: 150px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 分类蒙板样式 */
|
||||
.category-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
/* 确保在任何情况下都能居中显示 */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.category-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
/* 确保内容块在父容器中完美居中 */
|
||||
}
|
||||
|
||||
.category-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.category-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.category-modal-close:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-modal-body {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.establish-container {
|
||||
|
||||
@@ -25,10 +25,16 @@ const routes = [
|
||||
meta: { title: '首页' },
|
||||
children: [
|
||||
{
|
||||
path: 'aericletype',
|
||||
name: 'homeByType',
|
||||
path: 'aericleattribute',
|
||||
name: 'homeByAttribute',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: 'aericlecategory',
|
||||
name: 'homeByCategory',
|
||||
component: HomePage
|
||||
},
|
||||
|
||||
{
|
||||
path: 'aericletitle',
|
||||
name: 'homeByTitle',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL:'http://localhost:8080/api', // API基础URL
|
||||
baseURL: '/api', // API基础URL,使用相对路径,通过Vite代理转发
|
||||
timeout: 10000, // 请求超时时间
|
||||
withCredentials: true // 允许跨域请求携带凭证(如cookies)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,17 @@ import api from './apiService'
|
||||
*/
|
||||
class ArticleService {
|
||||
/**
|
||||
* 分页查询文章列表
|
||||
* @param {import('../types').PaginationParams} params - 分页查询参数
|
||||
* @param status 文章状态(0:未发表 1:已发表 2:已删除)
|
||||
* @param page 页码,从0开始(可选,默认为0)
|
||||
* @param size 每页大小(可选,默认为10,最大为100)
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
getPagedArticles(params = {}) {
|
||||
return api.get(`/articles/status/page?title=${params.title || ''}&categoryid=${params.categoryid || 0}&attributeid=${params.attributeid || 0}&status=${params.status || 1}&page=${params.page || 0}&size=${params.size || 10}`)
|
||||
}
|
||||
/**
|
||||
* 获取已发布文章列表
|
||||
* @param {import('../types').PaginationParams} params - 查询参数
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
@@ -13,15 +24,7 @@ class ArticleService {
|
||||
getAllArticles(params = {}) {
|
||||
return api.get('/articles/published', { params })
|
||||
}
|
||||
/**
|
||||
* 根据状态获取文章列表
|
||||
* @param {number} status - 文章状态(0:未发表 1:已发表 2:已删除)
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
getArticlesByStatus(status) {
|
||||
return api.get(`/articles/status/${status}`)
|
||||
}
|
||||
/**
|
||||
/**
|
||||
* 获取所有文章列表(包含已删除)
|
||||
* @param {import('../types').PaginationParams} params - 查询参数
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
@@ -29,16 +32,7 @@ class ArticleService {
|
||||
getAllArticlesWithDeleted(params = {}) {
|
||||
return api.get('/articles', { params })
|
||||
}
|
||||
/**
|
||||
* 根据ID获取文章详情
|
||||
* @param {number} articleid - 文章ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
|
||||
*/
|
||||
getArticleById(articleid) {
|
||||
return api.get(`/articles/${articleid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 根据属性ID获取文章列表
|
||||
* @param {number} attributeid - 属性ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
@@ -47,16 +41,7 @@ class ArticleService {
|
||||
return api.get(`/articles/attribute/${attributeid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标题查询文章列表
|
||||
* @param {string} title - 文章标题
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
getArticlesByTitle(title) {
|
||||
return api.get(`/articles/title/${title}`)
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 获取热门文章
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
@@ -92,16 +77,7 @@ class ArticleService {
|
||||
return api.delete(`/articles/${articleid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类获取文章
|
||||
* @param {number} categoryid - 分类ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
getArticlesByCategory(categoryid) {
|
||||
return api.get(`/articles/category/${categoryid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 增加文章浏览量
|
||||
* @param {number} articleid - 文章ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
@@ -110,23 +86,7 @@ class ArticleService {
|
||||
return api.post(`/articles/view/${articleid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据属性ID获取最新文章
|
||||
* @param {number} attributeid - 属性ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||
*/
|
||||
getLatestArticlesByAttribute(attributeid) {
|
||||
return api.get(`/articles/attribute/${attributeid}/latest`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞文章
|
||||
* @param {number} articleid - 文章ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
likeArticle(articleid) {
|
||||
return api.post(`/articles/like/${articleid}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并导出服务实例
|
||||
|
||||
@@ -39,35 +39,6 @@ class CategoryAttributeService {
|
||||
createAttribute(attributeData) {
|
||||
return api.post('/category-attributes', attributeData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分类属性
|
||||
* @param {number} attributeid - 属性ID
|
||||
* @param {import('../types').CategoryAttributeDto} attributeData - 属性数据
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
|
||||
*/
|
||||
updateAttribute(attributeid, attributeData) {
|
||||
return api.put(`/category-attributes/${attributeid}`, attributeData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分类属性
|
||||
* @param {number} attributeid - 属性ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
deleteAttribute(attributeid) {
|
||||
return api.delete(`/category-attributes/${attributeid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查分类下是否存在指定名称的属性
|
||||
* @param {number} categoryid - 分类ID
|
||||
* @param {string} attributename - 属性名称
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
checkAttributeExists(categoryid, attributename) {
|
||||
return api.get(`/category-attributes/check-exists?categoryid=${categoryid}&attributename=${encodeURIComponent(attributename)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并导出服务实例
|
||||
|
||||
@@ -11,43 +11,6 @@ class CategoryService {
|
||||
getAllCategories() {
|
||||
return api.get('/categories')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分类
|
||||
* @param {number} typeid - 分类ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
|
||||
*/
|
||||
getCategory(typeid) {
|
||||
return api.get(`/categories/${typeid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新分类
|
||||
* @param {import('../types').CategoryDto} categoryData - 分类数据
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
|
||||
*/
|
||||
createCategory(categoryData) {
|
||||
return api.post('/categories', categoryData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分类
|
||||
* @param {number} typeid - 分类ID
|
||||
* @param {import('../types').CategoryDto} categoryData - 分类数据
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
|
||||
*/
|
||||
updateCategory(typeid, categoryData) {
|
||||
return api.put(`/categories/${typeid}`, categoryData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
* @param {number} typeid - 分类ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
deleteCategory(typeid) {
|
||||
return api.delete(`/categories/${typeid}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并导出服务实例
|
||||
|
||||
@@ -14,48 +14,11 @@ class LoginService {
|
||||
return api.post("/auth/login", loginData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {import('../types').RegisterDto} registerData - 注册数据
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
|
||||
*/
|
||||
register(registerData) {
|
||||
return api.post("/auth/register", registerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
logout() {
|
||||
|
||||
return api.post("/auth/logout");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
|
||||
*/
|
||||
getCurrentUser() {
|
||||
return api.get("/user/info");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {import('../types').UserDto} userData - 用户数据
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
|
||||
*/
|
||||
updateUser(userData) {
|
||||
return api.put("/user/update", userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param {import('../types').ChangePasswordDto} passwordData - 密码数据
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
changePassword(passwordData) {
|
||||
return api.post("/user/change-password", passwordData);
|
||||
}
|
||||
}
|
||||
export default new LoginService();
|
||||
@@ -6,22 +6,28 @@ import apiService from './apiService'
|
||||
*/
|
||||
class MessageService {
|
||||
/**
|
||||
* 获取所有留言
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||
* 获取留言数量
|
||||
* @param {number} articleid - 文章ID
|
||||
* @returns {Promise<import('../types').ApiResponse<number>>}
|
||||
*/
|
||||
getAllMessages() {
|
||||
return apiService.get('/messages')
|
||||
getMessageCountByArticleId(articleid) {
|
||||
return apiService.get(`/messages/count?articleid=${articleid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条留言
|
||||
* @param {number} messageid - 留言ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message>>}
|
||||
* 获取分页留言
|
||||
* @param {number} articleid - 文章ID
|
||||
* @param {number} pagenum - 页码
|
||||
* @param {number} pagesize - 每页数量
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||
*/
|
||||
getMessageById(messageid) {
|
||||
return apiService.get(`/messages/${messageid}`)
|
||||
getMessagesByPage(articleid, pagenum, pagesize) {
|
||||
// 如果文章ID不存在,查询所有留言
|
||||
if (!articleid) {
|
||||
return apiService.get(`/messages/page?pageNum=${pagenum}&pageSize=${pagesize}`)
|
||||
}
|
||||
return apiService.get(`/messages/page?articleid=${articleid}&pageNum=${pagenum}&pageSize=${pagesize}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文章ID获取留言
|
||||
* @param {number} articleid - 文章ID
|
||||
@@ -32,38 +38,11 @@ class MessageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根留言
|
||||
* 获取所有留言
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||
*/
|
||||
getRootMessages() {
|
||||
return apiService.get('/messages/root')
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父留言ID获取回复
|
||||
* @param {number} parentid - 父留言ID
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||
*/
|
||||
getRepliesByParentId(parentid) {
|
||||
return apiService.get(`/messages/${parentid}/replies`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据昵称搜索留言
|
||||
* @param {string} nickname - 昵称
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||
*/
|
||||
searchMessagesByNickname(nickname) {
|
||||
return apiService.get(`/messages/search?nickname=${nickname}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章评论数量
|
||||
* @param {number} articleid - 文章ID
|
||||
* @returns {Promise<import('../types').ApiResponse<number>>}
|
||||
*/
|
||||
getMessageCountByArticleId(articleid) {
|
||||
return apiService.get(`/messages/count/article/${articleid}`)
|
||||
getAllMessages() {
|
||||
return apiService.get('/messages')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +67,6 @@ class MessageService {
|
||||
* 点赞留言
|
||||
* @param {number} messageid - 留言ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*
|
||||
*/
|
||||
likeMessage(messageid) {
|
||||
return apiService.post(`/messages/${messageid}/like`)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 留言相关API服务
|
||||
// 疯言疯语相关API服务
|
||||
import apiService from './apiService'
|
||||
|
||||
class NonsenseService {
|
||||
@@ -22,7 +22,7 @@ class NonsenseService {
|
||||
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
|
||||
*/
|
||||
saveNonsense(nonsense){
|
||||
createNonsense(nonsense){
|
||||
return apiService.post('/nonsense', nonsense)
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
/* 内容区圆角 */
|
||||
|
||||
/* 首页 hero 区域高度和间距 */
|
||||
--hero-height: 768px;
|
||||
--hero-height: 100px;
|
||||
/* hero 默认高度 */
|
||||
--hero-height-small: 150px;
|
||||
--hero-height-small: 100px;
|
||||
/* hero 收缩后高度 */
|
||||
--hero-margin-top: 5%;
|
||||
/* hero 顶部外边距 */
|
||||
|
||||
--hero-margin-top-small: 6%;
|
||||
/* hero 收缩后顶部外边距 */
|
||||
/* 标题样式 */
|
||||
--title-font-size: 3.5rem;
|
||||
/* hero 主标题字号 */
|
||||
@@ -352,10 +351,14 @@ p {
|
||||
}
|
||||
/* 分页区样式 */
|
||||
.Pagination {
|
||||
align-self: center;
|
||||
background-color: var(--pagination-bg);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.Pagination .el-pagination {
|
||||
/* 水平垂直居中 */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧状态栏样式 */
|
||||
.leftmodluecontainer {
|
||||
width: var(--leftmodlue-width);
|
||||
@@ -364,19 +367,24 @@ p {
|
||||
/* 首页 hero 区域样式 */
|
||||
.hero {
|
||||
height: var(--hero-height);
|
||||
margin-top: var(--hero-margin-top);
|
||||
margin: var(--hero-margin);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
transition: height 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* hero 收缩状态 */
|
||||
.hero.newhero {
|
||||
.hero.small-hero {
|
||||
height: var(--hero-height-small);
|
||||
margin-top: 10%;
|
||||
margin-top: var(--hero-margin-top-small);
|
||||
/* 去除 hero 收缩状态下的position */
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
@@ -384,6 +392,10 @@ p {
|
||||
white-space: pre;
|
||||
min-height: 2.5em;
|
||||
font-size: var(--title-font-size, 2rem);
|
||||
/* 水平居中 */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
@@ -393,10 +405,6 @@ p {
|
||||
}
|
||||
|
||||
/* hero 主标题样式 */
|
||||
.hero h1 {
|
||||
font-size: var(--title-font-size);
|
||||
margin-bottom: var(--title-margin-bottom);
|
||||
}
|
||||
|
||||
/* hero 副标题样式 */
|
||||
.hero p {
|
||||
@@ -446,8 +454,7 @@ p {
|
||||
--nonsense-title-margin-bottom: 15px;
|
||||
--nav-padding-small: 0 8px;
|
||||
--nonsenset-margin-top: 10px;
|
||||
--body-background-img: url();
|
||||
/* 移动端不显示背景图片 */
|
||||
--body-background-img: url(../img/bg.jpg);
|
||||
}
|
||||
|
||||
.elrow-top {
|
||||
@@ -507,3 +514,23 @@ p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
/* 移动端适配:屏幕宽度小于820px时生效 */
|
||||
@media (max-width: 820px) {
|
||||
:root {
|
||||
--nav-padding: 0;
|
||||
--hero-height:300px;
|
||||
}
|
||||
.RouterViewpage {
|
||||
height: auto;
|
||||
}
|
||||
.hero {
|
||||
margin-top: 50%;
|
||||
}
|
||||
.el-col-14 {
|
||||
flex: 0 0 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.elrow-top {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface MessageDto {
|
||||
* 分类类型接口
|
||||
*/
|
||||
export interface Category {
|
||||
typeid: number
|
||||
Categoryid: number
|
||||
typename: string
|
||||
description?: string
|
||||
createdAt?: string
|
||||
@@ -86,6 +86,7 @@ export interface CategoryDto {
|
||||
description?: string
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分类属性接口
|
||||
*/
|
||||
@@ -161,8 +162,7 @@ export interface ApiResponse<T = any> {
|
||||
* 分页参数接口
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
size?: number
|
||||
keyword?: string
|
||||
[key: string]: any
|
||||
pagenum?: number
|
||||
pagesize?: number
|
||||
status?: number
|
||||
}
|
||||
136
src/views/Footer.vue
Normal file
136
src/views/Footer.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<!-- 页脚 -->
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div class="footer-content">
|
||||
<!-- 备案信息 -->
|
||||
<p class="footer-beian">
|
||||
备案号:
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
|
||||
皖ICP备2025105428号-1 || 皖公网安备34120202001634号
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<p class="footer-copyright">
|
||||
网站所有权利保留 © {{ new Date().getFullYear() }} 清疯不颠
|
||||
</p>
|
||||
|
||||
<!-- 运行时间 -->
|
||||
<p class="footer-runtime">
|
||||
运行时间:<span>{{ runtime }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 运行时间响应式状态
|
||||
const runtime = ref('计算中...')
|
||||
// 网站上线时间 (时间戳)
|
||||
const LAUNCH_DATE = new Date('2025-12-01T00:00:00').getTime()
|
||||
// 定时器引用
|
||||
let runtimeTimer: number | null = null
|
||||
|
||||
/**
|
||||
* 更新网站运行时间
|
||||
*/
|
||||
const updateRuntime = () => {
|
||||
const now = Date.now()
|
||||
const diff = now - LAUNCH_DATE
|
||||
|
||||
// 计算天、时、分、秒
|
||||
const days = Math.floor(diff / 86400000)
|
||||
const hours = Math.floor((diff % 86400000) / 3600000)
|
||||
const minutes = Math.floor((diff % 3600000) / 60000)
|
||||
const seconds = Math.floor((diff % 60000) / 1000)
|
||||
|
||||
// 更新运行时间显示
|
||||
runtime.value = `${days} 天 ${hours} 小时 ${minutes} 分钟 ${seconds} 秒`
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
// 立即更新一次
|
||||
updateRuntime()
|
||||
// 每秒更新一次
|
||||
runtimeTimer = window.setInterval(updateRuntime, 1000)
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (runtimeTimer) {
|
||||
clearInterval(runtimeTimer)
|
||||
runtimeTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页脚容器 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 页脚内容 */
|
||||
.footer-content {
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 2rem;
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 通用段落样式 */
|
||||
.footer-content p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 备案信息 */
|
||||
.footer-beian a {
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer-beian a:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 版权信息 */
|
||||
.footer-copyright {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 运行时间 */
|
||||
.footer-runtime {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
padding: 1rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.footer-content p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,15 +3,17 @@
|
||||
<div class="about-content-wrapper">
|
||||
<!-- 页面头部 -->
|
||||
<div class="about-page-header">
|
||||
<h1 class="about-page-title">关于</h1>
|
||||
<h3 class="about-page-title">关于</h3>
|
||||
</div>
|
||||
|
||||
<!-- 关于内容 -->
|
||||
<div class="about-main-content">
|
||||
<div class="about-personal-intro">
|
||||
<h4>名字</h4>
|
||||
<h4>清风--清疯--清疯不颠</h4>
|
||||
<h5>
|
||||
让我回忆回忆...大一的时候(还是上学的日子好哈哈哈哈哈!!)跟室友一块玩游戏,因为我的Steam名字叫“清风”,慢慢的这个名儿就这么成了我的外号。说来也怪,被他们这么一叫,心里那点初入大学的陌生和拘谨,
|
||||
<!-- “徐徐清风知我意,悠悠湖水荡人心。” -->
|
||||
名字从“清风”起步,但总觉得少了点年轻人的疯狂,于是干脆改叫“清疯”。疯久了,又怕真“颠”了,便在后面补上“不颠”二字,作为提醒。“清疯不颠”就此定下——风与疯,只在一念之间;疯与不颠,则是我给自己的清醒界限。
|
||||
<!-- 让我回忆回忆...大一的时候(还是上学的日子好哈哈哈哈哈!!)跟室友一块玩游戏,因为我的Steam名字叫“清风”,慢慢的这个名儿就这么成了我的外号。说来也怪,被他们这么一叫,心里那点初入大学的陌生和拘谨,
|
||||
好像真被一阵风吹散了似的,
|
||||
哈哈哈哈哈,我还蛮喜欢这个外号的。
|
||||
<br></br>
|
||||
@@ -21,14 +23,14 @@
|
||||
又过些日子,玩新游戏要起名,正盯着输入框里的“清疯”发呆, 两个字在脑海里冒出来……疯癫?哎,好像是疯了,但也没完全颠嘛!那种在理智边界试探、却绝不越线的微妙感,一下对味了——干脆就叫“清疯不颠”!
|
||||
名字一出自己先乐了。
|
||||
<br></br>
|
||||
哈哈哈哈哈哈!俗话说天才在左疯子在右,在我这儿,大概是左脑负责疯,右脑负责颠,两个家伙吵吵闹闹,反而让我在这个世界里自得其乐。
|
||||
哈哈哈哈哈哈!俗话说天才在左疯子在右,在我这儿,大概是左脑负责疯,右脑负责颠,两个家伙吵吵闹闹,反而让我在这个世界里自得其乐。 -->
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="about-personal-intro">
|
||||
<h4>疯言疯语</h4>
|
||||
<h5>
|
||||
我并没有网站的开发经验这是我的第一个项目,我想设计一下独属于清疯的页面,可是我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||
我想设计一下独属于清疯的页面,可是我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
@@ -279,14 +281,19 @@ const goToMessageBoard = () => {
|
||||
.about-page-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.about-page-header{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.about-content-wrapper {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
margin: 0 15px;
|
||||
margin: 0 5%;
|
||||
}
|
||||
|
||||
.about-page-title {
|
||||
font-size: 1.8rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.about-page-subtitle {
|
||||
@@ -304,7 +311,6 @@ const goToMessageBoard = () => {
|
||||
.contact-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.skills-display-list {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -18,17 +18,18 @@
|
||||
<div v-else-if="categories.length > 0" class="article-content" id="category-list">
|
||||
<p><strong></strong></p>
|
||||
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下,点击跳转</span> </strong></div>
|
||||
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid" class="category-group-container">
|
||||
<div v-for="categoryGroup in categories" :key="categoryGroup.Categoryid" class="category-group-container">
|
||||
<div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)">
|
||||
<h2 id="header-id-1">{{ categoryGroup.typename }}</h2>
|
||||
<!-- 计算该分类组中实际有文章的属性数量 -->
|
||||
<span class="badge badge-primary">共 {{ categoryGroup.attributes.reduce((total, cat) => total + (cat.articles && cat.articles.length ? cat.articles.length : 0), 0) }} 篇</span>
|
||||
<ul class="category-item-list">
|
||||
<li v-for="category in categoryGroup.attributes" :key="category.attributeid" >
|
||||
<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename
|
||||
}}</kbd></a>
|
||||
— —({{ category.articles.length }})
|
||||
<div v-for="attribute in categoryGroup.attributes" :key="attribute.attributeid">
|
||||
<li v-if="attribute.articles && attribute.articles.length > 0">
|
||||
<a class="category-link" @click="handleCategoryClick(attribute)"><kbd>{{ attribute.attributename}}</kbd></a>
|
||||
— —({{ attribute.articles.length }})
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +40,7 @@
|
||||
<div v-else class="empty-state-container">
|
||||
<el-empty description="暂无分类" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -76,7 +78,7 @@ const fetchCategories = async () => {
|
||||
// 使用Promise.all等待所有异步操作完成
|
||||
await Promise.all(
|
||||
processedCategories.map(async category => {
|
||||
const attributes = await categoryAttributeService.getAttributesByCategory(category.typeid);
|
||||
const attributes = await categoryAttributeService.getAttributesByCategory(category.categoryid);
|
||||
if (attributes.code === 200 && Array.isArray(attributes.data)) {
|
||||
const processedAttributes = await Promise.all(
|
||||
attributes.data.map(async item => {
|
||||
@@ -97,7 +99,7 @@ const fetchCategories = async () => {
|
||||
);
|
||||
}
|
||||
categories.value = processedCategories;
|
||||
console.log('获取分类列表成功:', categories.value);
|
||||
// console.log('获取分类列表成功:', categories.value);
|
||||
} catch (err) {
|
||||
console.error('获取分类列表失败:', err);
|
||||
ElMessage.error('获取分类列表失败,请稍后重试');
|
||||
@@ -114,11 +116,10 @@ const handleCategoryClick = (attribute: any) => {
|
||||
globalStore.removeValue('attribute')
|
||||
globalStore.setValue('attribute', {
|
||||
id: attribute.attributeid,
|
||||
name: attribute.typename
|
||||
name: attribute.attributename
|
||||
})
|
||||
console.log(attribute)
|
||||
router.push({
|
||||
path: '/home/aericletype',
|
||||
path: '/home/aericleattribute',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state-container">
|
||||
<el-alert :title="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchArticleDetail">重新加载</el-button>
|
||||
<el-button type="primary" @click="initializeArticleDetail">重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 文章详情 -->
|
||||
@@ -40,14 +40,11 @@
|
||||
<!-- 文章底部信息 -->
|
||||
<div class="article-footer-section">
|
||||
<div class="article-tag-list">
|
||||
<!-- <span v-for="tag in article.tags || []" :key="tag" class="el-tag el-tag--primary">
|
||||
{{ tag }}
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<!-- 文章操作按钮 -->
|
||||
<div class="article-actions-group">
|
||||
<el-button type="primary" @click="goBack" plain>
|
||||
<el-button type="primary" @click="navigateBack" plain>
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -57,11 +54,11 @@
|
||||
<div class="related-articles-section" v-if="relatedArticles.length > 0">
|
||||
<h3>相关文章</h3>
|
||||
<div class="related-articles-list">
|
||||
<div v-for="item in relatedArticles" :key="item.articleid" class="related-article-card"
|
||||
<!-- <div v-for="item in relatedArticles" :key="item.articleid" class="related-article-card"
|
||||
@click="handleRelatedArticleClick(item.articleid)">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,92 +77,185 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 导入必要的依赖
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
// =========================================================================
|
||||
// 组件导入和初始化
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 导入Vue核心功能
|
||||
*/
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
/**
|
||||
* 导入路由相关功能
|
||||
*/
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
/**
|
||||
* 导入状态管理和UI组件
|
||||
*/
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 导入类型和工具函数
|
||||
*/
|
||||
import type { Article } from '@/types'
|
||||
import { formatRelativeTime } from '@/utils/dateUtils'
|
||||
|
||||
/**
|
||||
* 导入子组件
|
||||
*/
|
||||
import messageboard from './messageboard.vue'
|
||||
import markdownViewer from './markdown.vue'
|
||||
// 路由相关
|
||||
|
||||
// =========================================================================
|
||||
// 路由和状态初始化
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 初始化路由
|
||||
*/
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式状态管理
|
||||
/**
|
||||
* 初始化全局状态管理
|
||||
*/
|
||||
const globalStore = useGlobalStore()
|
||||
const article = ref<Article | null>(null) // 使用 null 作为初始值,避免类型不匹配问题
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const relatedArticles = ref<Article[]>([])
|
||||
|
||||
/**
|
||||
* 响应式状态定义
|
||||
*/
|
||||
const article = ref<Article | null>(null) // 文章详情数据,初始为null避免类型不匹配
|
||||
const loading = ref(false) // 加载状态
|
||||
const error = ref('') // 错误信息
|
||||
const relatedArticles = ref<Article[]>([]) // 相关文章列表
|
||||
|
||||
// =========================================================================
|
||||
// 文章数据处理模块
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 获取文章ID
|
||||
* @returns {number | null} 文章ID或null
|
||||
*/
|
||||
const getArticleId = (): number | null => {
|
||||
return globalStore.getValue('articleInfo')?.articleid || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章详情数据
|
||||
* @returns {Promise<Article | null>} 文章数据或null
|
||||
*/
|
||||
const fetchArticleDetail = async () => {
|
||||
const fetchArticleData = async (): Promise<Article | null> => {
|
||||
try {
|
||||
// 从全局状态获取文章信息
|
||||
const response = await globalStore.getValue('articleInfo')
|
||||
return response || null
|
||||
} catch (err) {
|
||||
console.error('获取文章数据失败:', err)
|
||||
throw new Error('获取文章数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加文章浏览量
|
||||
* @param {Article} articleData 文章数据
|
||||
*/
|
||||
const incrementViewCount = (articleData: Article): void => {
|
||||
if (articleData.viewCount) {
|
||||
articleData.viewCount++
|
||||
} else {
|
||||
articleData.viewCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文章不存在的情况
|
||||
*/
|
||||
const handleArticleNotFound = (): void => {
|
||||
error.value = '文章不存在或已被删除'
|
||||
ElMessage.error(error.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理获取文章数据过程中的错误
|
||||
* @param {unknown} err 错误对象
|
||||
*/
|
||||
const handleArticleFetchError = (err: unknown): void => {
|
||||
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试'
|
||||
console.error('获取文章详情失败:', err)
|
||||
ElMessage.error(error.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化文章详情数据
|
||||
*/
|
||||
const initializeArticleDetail = async (): Promise<void> => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
// 获取路由参数
|
||||
const articleId = globalStore.getValue('articleInfo')?.articleid || null
|
||||
// 获取文章ID
|
||||
const articleId = getArticleId()
|
||||
if (!articleId) {
|
||||
throw new Error('文章不存在')
|
||||
}
|
||||
// 获取文章详情
|
||||
const response = await globalStore.getValue('articleInfo')
|
||||
// const markdowndata = await
|
||||
if (response) {
|
||||
article.value = response
|
||||
// 获取并设置分类名称
|
||||
// const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid))
|
||||
// article.value.categoryName = categoryResponse.data.attributename || '未分类'
|
||||
// 获取并设置评论量
|
||||
// const commentResponse = await messageService.getMessagesByArticleId(articleId)
|
||||
// article.value.commentCount = commentResponse.data.length || 0
|
||||
// 更新浏览量
|
||||
// 更新前端显示的浏览量
|
||||
if (article.value.viewCount) {
|
||||
article.value.viewCount++
|
||||
} else {
|
||||
article.value.viewCount = 1
|
||||
handleArticleNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文章数据
|
||||
const articleData = await fetchArticleData()
|
||||
if (articleData) {
|
||||
article.value = articleData
|
||||
// 增加浏览量
|
||||
incrementViewCount(articleData)
|
||||
} else {
|
||||
throw new Error('文章不存在或已被删除')
|
||||
handleArticleNotFound()
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试'
|
||||
console.error('获取文章详情失败:', err)
|
||||
ElMessage.error(error.value)
|
||||
handleArticleFetchError(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 导航和用户交互模块
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const goBack = () => {
|
||||
const navigateBack = (): void => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相关文章点击事件
|
||||
* 导航到相关文章
|
||||
* @param {number} id - 相关文章ID
|
||||
*/
|
||||
const handleRelatedArticleClick = (id: number) => {
|
||||
const navigateToRelatedArticle = (id: number): void => {
|
||||
router.push({
|
||||
path: '/article/:url',
|
||||
query: { url: id }
|
||||
})
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 组件生命周期和初始化
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 组件挂载时获取文章详情
|
||||
* 组件挂载时的初始化操作
|
||||
*/
|
||||
const setupComponent = (): void => {
|
||||
initializeArticleDetail()
|
||||
}
|
||||
/**
|
||||
* 组件挂载生命周期钩子
|
||||
*/
|
||||
onMounted(() => {
|
||||
fetchArticleDetail()
|
||||
setupComponent()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -393,18 +483,17 @@ onMounted(() => {
|
||||
.error-state-container,
|
||||
.empty-state-container {
|
||||
padding: 20px;
|
||||
margin: 0 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.article-main-title {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.article-actions-group {
|
||||
/* 文章操作按钮组 - 右对齐 */
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.article-meta-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -30,16 +30,30 @@
|
||||
</el-cascader>
|
||||
</span>
|
||||
</div>
|
||||
<div class="article-summary-section">
|
||||
<!-- 文章简介 -->
|
||||
<span class="meta-item summary-item">
|
||||
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入简介"
|
||||
v-model="Articleform.content">
|
||||
</el-input>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" />
|
||||
<!-- 编辑区域 -->
|
||||
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" noImgZoomIn
|
||||
noKatex />
|
||||
<!-- 返回列表 -->
|
||||
<el-button type="primary" @click="handleReturn">返回列表</el-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import { categoryService, categoryAttributeService, articleService } from '@/services';
|
||||
@@ -47,6 +61,10 @@ import type { Article } from '@/types/index.ts';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
// 测试
|
||||
// const id = 'preview-only';
|
||||
// const scrollElement = document.documentElement;
|
||||
|
||||
// 路由
|
||||
import router from '@/router/Router';
|
||||
const Articleform = ref<Article>({
|
||||
@@ -58,7 +76,6 @@ const Articleform = ref<Article>({
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
markdownscontent: '',
|
||||
status: 0 // 默认状态为草稿
|
||||
})
|
||||
|
||||
// 用于级联选择器的值绑定
|
||||
@@ -137,8 +154,8 @@ const loadCategories = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('分类选项:', optionsData);
|
||||
console.log('选中的值:', selectedValues.value);
|
||||
// console.log('分类选项:', optionsData);
|
||||
// console.log('选中的值:', selectedValues.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
@@ -179,8 +196,8 @@ const handleSave = (markdown) => {
|
||||
markdownscontent: Articleform.value.markdownscontent
|
||||
};
|
||||
|
||||
console.log('发送文章数据:', articleData);
|
||||
console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
|
||||
// console.log('发送文章数据:', articleData);
|
||||
// console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
|
||||
|
||||
// 根据articleid决定调用创建还是更新接口
|
||||
const savePromise = Articleform.value.articleid === 0
|
||||
@@ -188,7 +205,7 @@ const handleSave = (markdown) => {
|
||||
: articleService.updateArticle(Articleform.value.articleid, articleData);
|
||||
savePromise
|
||||
.then(res => {
|
||||
console.log('API响应:', res);
|
||||
// console.log('API响应:', res);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
|
||||
// 清除全局存储中的article
|
||||
@@ -238,6 +255,18 @@ const handleSave = (markdown) => {
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回列表页
|
||||
*/
|
||||
const handleReturn = () => {
|
||||
// 确定是否返回列表页
|
||||
if (window.confirm('确定要返回列表页吗?所有未保存的更改将丢失。')) {
|
||||
// 清除全局存储中的article
|
||||
globalStore.removeValue('updatearticle');
|
||||
router.push('/home');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -371,6 +400,11 @@ const handleSave = (markdown) => {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
/* 文章简介区域 */
|
||||
.article-summary-section {
|
||||
margin-top: 30px;
|
||||
padding: 0 30px 0 30px;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 手机 */
|
||||
@media (max-width: 480px) {
|
||||
@@ -399,7 +433,6 @@ const handleSave = (markdown) => {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.article-save-container {
|
||||
|
||||
@@ -8,13 +8,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 文章列表 -->
|
||||
<transition-group name="article-item" tag="div" v-else>
|
||||
<div
|
||||
class="article-card"
|
||||
v-for="article in articleList"
|
||||
:key="article.articleId"
|
||||
@click="handleArticleClick(article)"
|
||||
>
|
||||
<transition-group name="article-item" tag="div" class="article-list-content" v-else>
|
||||
<div class="article-card" v-for="article in articleList" :key="article.articleId"
|
||||
@click="handleArticleClick(article)">
|
||||
<h6 class="article-title">{{ article.title }}</h6>
|
||||
<div v-if="article.marked" class="article-special-tag">标记文章</div>
|
||||
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
|
||||
@@ -31,6 +27,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页区域 -->
|
||||
<el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
|
||||
</transition-group>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && articleList.length === 0" class="empty-state-container">
|
||||
@@ -42,97 +40,146 @@
|
||||
<script setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { formatDate, formatRelativeTime } from '@/utils/dateUtils'
|
||||
import { formatRelativeTime } from '@/utils/dateUtils'
|
||||
import { formatContentPreview } from '@/utils/stringUtils'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { articleService, messageService, categoryAttributeService } from '@/services'
|
||||
import PaginationComponent from '@/views/page.vue'
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
|
||||
// ========== 组件初始化 ==========
|
||||
|
||||
// 全局状态管理
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// 路由相关
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
// 分页属性
|
||||
const pageNum = ref(1) // 当前页码
|
||||
const pageSize = ref(10) // 每页数量
|
||||
const totalPages = ref(0) // 总页数
|
||||
const pageLayout = ref('pager, next')// 分页布局
|
||||
|
||||
// 响应式状态
|
||||
const articleList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 获取文章列表
|
||||
*/
|
||||
const fetchArticles = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
let response = {}
|
||||
|
||||
|
||||
// ========== 分页数据处理 ==========
|
||||
|
||||
/**
|
||||
* 处理分页组件的数据更新
|
||||
* @param {Array} data - 分页组件传递的当前页数据
|
||||
*/
|
||||
|
||||
// ========== 文章数据获取模块 ==========
|
||||
|
||||
/**
|
||||
* 根据路由路径获取对应的文章列表
|
||||
* @returns {Promise<Object>} 文章列表响应数据
|
||||
*/
|
||||
const getArticlesByRoute = async () => {
|
||||
// 检查URL参数,确定获取文章的方式
|
||||
const pathSegment = route.path.split('/')[2];
|
||||
console.log(pathSegment)
|
||||
// 根据不同路径获取不同文章
|
||||
const pathSegment = route.path.split('/')[2]
|
||||
// console.log('当前路由分段:', pathSegment)
|
||||
|
||||
switch (pathSegment) {
|
||||
case 'aericletype':
|
||||
case 'aericleattribute':
|
||||
// 按属性类型获取文章
|
||||
const attributeData = globalStore.getValue('attribute')
|
||||
response = await articleService.getArticlesByAttributeId(attributeData.id)
|
||||
break
|
||||
return await articleService.getPagedArticles({attributeid: attributeData?.id}, pageNum.value, pageSize.value)
|
||||
case 'aericlecategory':
|
||||
// 按分类类型获取文章
|
||||
const categoryData = globalStore.getValue('category')
|
||||
return await articleService.getPagedArticles({categoryid: categoryData?.id}, pageNum.value, pageSize.value)
|
||||
case 'aericletitle':
|
||||
// 按标题搜索文章
|
||||
const titleData = globalStore.getValue('articleserarch')
|
||||
response = await articleService.getArticlesByTitle(titleData.name)
|
||||
break
|
||||
console.log('按标题搜索文章:', titleData.name)
|
||||
return await articleService.getPagedArticles({title: titleData?.name}, pageNum.value, pageSize.value)
|
||||
case 'aericlestatus':
|
||||
// 按状态获取文章
|
||||
const statusData = globalStore.getValue('articlestatus')
|
||||
response = await articleService.getArticlesByStatus(statusData.status)
|
||||
break
|
||||
return await articleService.getPagedArticles({status: statusData?.status}, pageNum.value, pageSize.value)
|
||||
default:
|
||||
// 默认情况下,根据用户权限决定获取方式
|
||||
if (globalStore.Login) {
|
||||
// 获取所有文章(包含已删除)
|
||||
console.log('管理员获取所有文章列表(包含已删除)')
|
||||
response = await articleService.getAllArticlesWithDeleted()
|
||||
} else {
|
||||
// 获取所有文章
|
||||
console.log('获取所有文章列表')
|
||||
response = await articleService.getAllArticles()
|
||||
}
|
||||
// 默认获取所有文章
|
||||
// console.log('获取所有文章列表')
|
||||
return await articleService.getPagedArticles({status: 1}, pageNum.value, pageSize.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个文章获取留言数量和分类名称
|
||||
for (const article of response.data) {
|
||||
/**
|
||||
* 为单篇文章补充额外信息(留言数量、分类名称等)
|
||||
* @param {Object} article - 文章对象
|
||||
* @returns {Promise<Object>} 补充信息后的文章对象
|
||||
*/
|
||||
const enrichArticleWithExtraInfo = async (article) => {
|
||||
try {
|
||||
// 获取留言数量
|
||||
const messageResponse = await messageService.getMessagesByArticleId(article.articleid)
|
||||
// console.log(`文章ID: ${article.articleid}, 分类ID: ${article.attributeid}`)
|
||||
|
||||
// 获取分类名称
|
||||
const categoryResponse = await categoryAttributeService.getAttributeById(article.attributeid)
|
||||
|
||||
if (categoryResponse && categoryResponse.data) {
|
||||
article.categoryName = categoryResponse.data.attributename
|
||||
} else {
|
||||
article.categoryName = '未分类'
|
||||
}
|
||||
|
||||
// 设置分类名称
|
||||
article.categoryName = categoryResponse?.data?.attributename || '未分类'
|
||||
// 设置评论数量
|
||||
article.commentCount = messageResponse.data.length > 0 ? messageResponse.data.length : 0
|
||||
|
||||
// 标准化ID字段名
|
||||
article.articleId = article.articleid
|
||||
|
||||
article.commentCount = messageResponse?.data?.length || 0
|
||||
// 标准化标记字段名
|
||||
article.marked = article.mg
|
||||
|
||||
return article
|
||||
} catch (err) {
|
||||
console.error(`获取文章${article.articleid}留言数量失败:`, err)
|
||||
console.error(`获取文章${article.articleid}额外信息失败:`, err)
|
||||
// 错误情况下设置默认值
|
||||
article.commentCount = 0
|
||||
article.categoryName = '未分类'
|
||||
return article
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量为文章列表补充额外信息
|
||||
* @param {Array} articles - 原始文章列表
|
||||
* @returns {Promise<Array>} 补充信息后的文章列表
|
||||
*/
|
||||
const enrichArticlesWithExtraInfo = async (articles) => {
|
||||
const enrichedArticles = []
|
||||
|
||||
for (const article of articles) {
|
||||
const enrichedArticle = await enrichArticleWithExtraInfo(article)
|
||||
enrichedArticles.push(enrichedArticle)
|
||||
}
|
||||
|
||||
// 更新文章列表
|
||||
// console.log(response.data)
|
||||
articleList.value = response.data || []
|
||||
return enrichedArticles
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取文章列表主函数
|
||||
*/
|
||||
const fetchArticles = async () => {
|
||||
let response = {}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 1. 根据路由获取文章列表
|
||||
response = await getArticlesByRoute()
|
||||
// console.log('更新后的文章列表:', response)
|
||||
|
||||
// 2. 确保数据存在
|
||||
if (!response.data.content || !Array.isArray(response.data.content)) {
|
||||
articleList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 为文章列表补充额外信息
|
||||
const enrichedArticles = await enrichArticlesWithExtraInfo(response.data.content)
|
||||
|
||||
// 4. 更新文章列表
|
||||
articleList.value = enrichedArticles
|
||||
} catch (error) {
|
||||
console.error('获取文章列表失败:', error)
|
||||
ElMessage.error('获取文章列表失败,请稍后重试')
|
||||
@@ -141,15 +188,22 @@ const fetchArticles = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文章交互模块 ==========
|
||||
|
||||
/**
|
||||
* 处理文章点击事件
|
||||
* @param {Object} article - 文章对象
|
||||
*/
|
||||
const handleArticleClick = (article) => {
|
||||
// 增加文章浏览量
|
||||
articleService.incrementArticleViews(article.articleId)
|
||||
try {
|
||||
// 增加文章浏览量(异步操作,不阻塞后续流程)
|
||||
articleService.incrementArticleViews(article.articleId).catch(err => {
|
||||
console.error('增加文章浏览量失败:', err)
|
||||
})
|
||||
|
||||
// 清除之前的文章信息
|
||||
globalStore.removeValue('articleInfo')
|
||||
|
||||
// 存储文章信息到全局状态
|
||||
globalStore.setValue('articleInfo', article)
|
||||
|
||||
@@ -157,8 +211,43 @@ const handleArticleClick = (article) => {
|
||||
router.push({
|
||||
path: '/article',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('处理文章点击事件失败:', error)
|
||||
ElMessage.error('操作失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
//刷新时挂载获取数据
|
||||
/**
|
||||
* 处理分页变化事件
|
||||
* @param {number} newPage - 新的页码
|
||||
*/
|
||||
const changePage = (newPage) => {
|
||||
fetchArticles()
|
||||
// 根据当前页码优化分页布局
|
||||
if (page === 1) {
|
||||
// 第一页只显示页码和下一页按钮
|
||||
pageLayout.value = 'pager, next'
|
||||
} else if (page === totalPages.value) {
|
||||
// 最后一页只显示上一页按钮和页码
|
||||
pageLayout.value = 'prev, pager'
|
||||
} else {
|
||||
// 中间页显示完整的上一页、页码、下一页
|
||||
pageLayout.value = 'prev, pager, next'
|
||||
}
|
||||
}
|
||||
// ========== 生命周期和监听器 ==========
|
||||
|
||||
/**
|
||||
* 处理路由变化的回调函数
|
||||
*/
|
||||
const handleRouteChange = () => {
|
||||
fetchArticles()
|
||||
// console.log('路由变化,重新获取文章列表')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文章列表变化的回调函数
|
||||
* @param {Array} newList - 新的文章列表
|
||||
*/
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
@@ -170,10 +259,13 @@ watch(
|
||||
// 监听路由路径和查询参数变化
|
||||
() => [route.path, route.query],
|
||||
// 路由变化时触发获取文章列表
|
||||
() => {
|
||||
fetchArticles()
|
||||
console.log('路由变化,重新获取文章列表')
|
||||
},
|
||||
handleRouteChange,
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 监听原始文章列表变化,确保初始数据正确显示
|
||||
watch(
|
||||
() => articleList.value,
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
@@ -182,7 +274,6 @@ watch(
|
||||
/* 文章列表容器样式 */
|
||||
.article-list-container {
|
||||
max-width: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* 加载状态容器 */
|
||||
@@ -209,6 +300,7 @@ watch(
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 文章卡片悬停渐变效果 */
|
||||
.article-card::before {
|
||||
content: '';
|
||||
@@ -270,7 +362,17 @@ watch(
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.article-list-content {
|
||||
position: relative;
|
||||
padding: 0 0 30px 0;
|
||||
}
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 文章内容预览样式 */
|
||||
.article-content-preview {
|
||||
color: #555;
|
||||
@@ -278,7 +380,6 @@ watch(
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -318,7 +419,6 @@ watch(
|
||||
/* 响应式设计 - 平板和手机 */
|
||||
@media (max-width: 768px) {
|
||||
.article-list-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.article-card {
|
||||
|
||||
@@ -105,7 +105,7 @@ const togglePassword = () => {
|
||||
|
||||
// 验证单个字段
|
||||
const validateField = (field) => {
|
||||
console.log('validateField', field)
|
||||
// console.log('validateField', field)
|
||||
errors.value = {}
|
||||
|
||||
if (field === 'username' && !loginForm.username) {
|
||||
@@ -150,7 +150,7 @@ const handleLogin = async () => {
|
||||
ElMessage.error('登录失败,请检查用户名和密码')
|
||||
return
|
||||
}
|
||||
console.log('登录成功', user)
|
||||
// console.log('登录成功', user)
|
||||
// 这里应该是实际的登录API调用
|
||||
// console.log('登录请求数据:', loginForm)
|
||||
|
||||
@@ -162,7 +162,7 @@ const handleLogin = async () => {
|
||||
globalStore.setValue('loginhomestatus', {
|
||||
status: 1 // 2:删除 1:已发布 0:发布登录
|
||||
})
|
||||
console.log('globalStore.Login', globalStore.Login)
|
||||
// console.log('globalStore.Login', globalStore.Login)
|
||||
// 保存登录状态token
|
||||
if (user.token) {
|
||||
localStorage.setItem('token', user.token)
|
||||
|
||||
@@ -2,34 +2,28 @@
|
||||
<div class="markdown-viewer">
|
||||
<div v-if="loading" class="loading-state">加载中...</div>
|
||||
<div v-else-if="error" class="error-state">加载失败: {{ error }}</div>
|
||||
<v-md-editor
|
||||
<!-- <v-md-editor
|
||||
v-else
|
||||
class="editor"
|
||||
v-model="content"
|
||||
mode="preview"
|
||||
:preview-theme="theme"
|
||||
:height="height"
|
||||
/>
|
||||
/> -->
|
||||
<MdPreview
|
||||
v-else
|
||||
:modelValue="content"
|
||||
class="editor"
|
||||
previewTheme="github"
|
||||
codeTheme="github" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
// 导入v-md-editor相关组件和样式
|
||||
import VMdEditor from '@kangc/v-md-editor'
|
||||
import '@kangc/v-md-editor/lib/style/base-editor.css'
|
||||
// 导入主题(使用github主题)
|
||||
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
|
||||
import '@kangc/v-md-editor/lib/theme/style/github.css'
|
||||
// 导入代码高亮
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
|
||||
// 配置主题
|
||||
VMdEditor.use(githubTheme, {
|
||||
Hljs: hljs
|
||||
})
|
||||
|
||||
import { MdPreview } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 从父组件传入的markdownid内容
|
||||
@@ -37,21 +31,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 指定API端点
|
||||
apiEndpoint: {
|
||||
type: String,
|
||||
default: '/help'
|
||||
},
|
||||
// 编辑器高度
|
||||
height: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
// 主题配置
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'github'
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
@@ -90,15 +69,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.editor {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0rgba(0, 0, 0, 0);
|
||||
transition: box-shadow 0.3s ease;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.editor:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(255, 255, 255,0);
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
<div class="comment-header-info">
|
||||
<!-- 头像 -->
|
||||
<div class="avatar-container">
|
||||
<img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)" class="user-avatar" alt="头像">
|
||||
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(comment.displayName || comment.nickname)">
|
||||
<img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)"
|
||||
class="user-avatar" alt="头像">
|
||||
<div v-else class="letter-avatar"
|
||||
:style="getLetterAvatarStyle(comment.displayName || comment.nickname)">
|
||||
{{ getInitialLetter(comment.displayName || comment.nickname) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +29,7 @@
|
||||
</div>
|
||||
<div class="comment-content-text" v-html="comment.content"></div>
|
||||
<div class="comment-actions-bar">
|
||||
<span class="like-button" @click="handleLike(comment)">
|
||||
<span class="like-button" v-if="false" @click="handleLike(comment)">
|
||||
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes
|
||||
}}</span>
|
||||
👍 赞
|
||||
@@ -43,8 +45,10 @@
|
||||
<div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper">
|
||||
<div class="reply-header-info">
|
||||
<div class="avatar-container">
|
||||
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)" class="user-avatar" alt="头像">
|
||||
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(reply.displayName || reply.nickname)">
|
||||
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)"
|
||||
class="user-avatar" alt="头像">
|
||||
<div v-else class="letter-avatar"
|
||||
:style="getLetterAvatarStyle(reply.displayName || reply.nickname)">
|
||||
{{ getInitialLetter(reply.displayName || reply.nickname) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +59,8 @@
|
||||
</div>
|
||||
<div class="reply-content-text">{{ reply.content }}</div>
|
||||
<div class="reply-actions-bar">
|
||||
<span class="like-button" @click="handleLike(reply)">
|
||||
|
||||
<span class="like-button" v-if="false" @click="handleLike(reply)">
|
||||
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes
|
||||
}}</span>
|
||||
👍 赞
|
||||
@@ -74,7 +79,10 @@
|
||||
还没有留言,快来抢沙发吧!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页按钮 -->
|
||||
<div class="pagination-controls" v-if="totalPages > 1">
|
||||
<el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
|
||||
</div>
|
||||
<!-- 留言输入区 -->
|
||||
<div class="comment-form-section">
|
||||
<h2 class="comment-form-title">发送评论(请正确填写邮箱地址,否则将会当成垃圾评论处理)</h2>
|
||||
@@ -111,8 +119,8 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-input-row">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitting"
|
||||
<el-form-item class="submit-button-container">
|
||||
<el-button type="primary" @click="onSubmit" class="form-submit-button" :loading="submitting"
|
||||
:disabled="!form.content || !form.nickname">
|
||||
发送
|
||||
</el-button>
|
||||
@@ -131,6 +139,7 @@ import { ElMessage, ElForm } from 'element-plus'
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
import { formatDate } from '@/utils/dateUtils'
|
||||
|
||||
// ============================== 组件初始化 ==============================
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
comments: {
|
||||
@@ -138,65 +147,40 @@ const props = defineProps({
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// 全局状态管理
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// ============================== 响应式数据 ==============================
|
||||
|
||||
// 留言板数据状态
|
||||
const messageBoardData = ref([]) // 留言板留言(articleid为空的主留言及其回复)
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const replyingTo = ref({ id: null, nickname: '', content: '' })
|
||||
const formRef = ref()
|
||||
const loading = ref(false) // 加载状态
|
||||
const submitting = ref(false) // 提交状态
|
||||
const replyingTo = ref({ id: null, nickname: '', content: '' }) // 正在回复的信息
|
||||
const formRef = ref() // 表单引用
|
||||
|
||||
// 验证码相关状态
|
||||
const captchaHint = ref('')
|
||||
const captchaAnswer = ref('')
|
||||
const showCaptchaHint = ref(false)
|
||||
|
||||
const captchaHint = ref('') // 验证码提示
|
||||
const captchaAnswer = ref('') // 验证码答案
|
||||
const showCaptchaHint = ref(false) // 是否显示验证码提示
|
||||
// 分页状态
|
||||
const pageNum = ref(1) // 当前页码
|
||||
const pageSize = ref(5) // 每页数量
|
||||
const totalPages = ref(0) // 总页数
|
||||
const pageLayout = ref('pager, next')// 分页布局
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
parentid: null,
|
||||
replyid: null,
|
||||
articleid: null,
|
||||
content: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
captcha: ''
|
||||
parentid: null, // 父留言ID
|
||||
replyid: null, // 被回复留言ID
|
||||
articleid: null, // 文章ID
|
||||
content: '', // 评论内容
|
||||
nickname: '', // 昵称
|
||||
email: '', // 邮箱
|
||||
captcha: '' // 验证码
|
||||
})
|
||||
// ============================== 表单验证规则 ==============================
|
||||
|
||||
// 生成简单验证码
|
||||
const generateCaptcha = () => {
|
||||
// 随机选择数学题或字符验证码
|
||||
const isMathCaptcha = Math.random() > 0.5
|
||||
|
||||
if (isMathCaptcha) {
|
||||
// 简单数学题:加法或减法
|
||||
const num1 = Math.floor(Math.random() * 10) + 1
|
||||
const num2 = Math.floor(Math.random() * 10) + 1
|
||||
const operator = Math.random() > 0.5 ? '+' : '-'
|
||||
|
||||
let answer
|
||||
if (operator === '+') {
|
||||
answer = num1 + num2
|
||||
} else {
|
||||
// 确保减法结果为正
|
||||
const larger = Math.max(num1, num2)
|
||||
const smaller = Math.min(num1, num2)
|
||||
captchaHint.value = `${larger} - ${smaller} = ?`
|
||||
captchaAnswer.value = (larger - smaller).toString()
|
||||
return
|
||||
}
|
||||
|
||||
captchaHint.value = `${num1} ${operator} ${num2} = ?`
|
||||
captchaAnswer.value = answer.toString()
|
||||
} else {
|
||||
// 简单字符验证码
|
||||
const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'
|
||||
let captcha = ''
|
||||
for (let i = 0; i < 4; i++) {
|
||||
captcha += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
captchaHint.value = captcha
|
||||
captchaAnswer.value = captcha.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
content: [
|
||||
{ required: true, message: '请输入评论内容', trigger: 'blur' },
|
||||
@@ -237,42 +221,109 @@ const rules = {
|
||||
]
|
||||
}
|
||||
|
||||
// ============================== 验证码模块 ==============================
|
||||
|
||||
// 获取Gravatar头像URL
|
||||
/**
|
||||
* 生成验证码
|
||||
* 随机选择生成数学题验证码或字符验证码
|
||||
*/
|
||||
const generateCaptcha = () => {
|
||||
// 随机选择数学题或字符验证码
|
||||
const isMathCaptcha = Math.random() > 0.5
|
||||
|
||||
if (isMathCaptcha) {
|
||||
generateMathCaptcha()
|
||||
} else {
|
||||
generateCharCaptcha()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成数学题验证码
|
||||
* 包含简单的加法或减法运算
|
||||
*/
|
||||
const generateMathCaptcha = () => {
|
||||
// 简单数学题:加法或减法
|
||||
const num1 = Math.floor(Math.random() * 10) + 1
|
||||
const num2 = Math.floor(Math.random() * 10) + 1
|
||||
const operator = Math.random() > 0.5 ? '+' : '-'
|
||||
|
||||
if (operator === '+') {
|
||||
const answer = num1 + num2
|
||||
captchaHint.value = `${num1} ${operator} ${num2} = ?`
|
||||
captchaAnswer.value = answer.toString()
|
||||
} else {
|
||||
// 确保减法结果为正
|
||||
const larger = Math.max(num1, num2)
|
||||
const smaller = Math.min(num1, num2)
|
||||
captchaHint.value = `${larger} - ${smaller} = ?`
|
||||
captchaAnswer.value = (larger - smaller).toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成字符验证码
|
||||
* 随机生成4位大小写字母和数字的组合
|
||||
*/
|
||||
const generateCharCaptcha = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'
|
||||
let captcha = ''
|
||||
for (let i = 0; i < 4; i++) {
|
||||
captcha += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
captchaHint.value = captcha
|
||||
captchaAnswer.value = captcha.toLowerCase()
|
||||
}
|
||||
|
||||
// ============================== UI辅助模块 ==============================
|
||||
|
||||
/**
|
||||
* 获取Gravatar头像URL
|
||||
* @param {string} email - 用户邮箱
|
||||
* @returns {string|null} - 头像URL或null
|
||||
*/
|
||||
const getAvatarUrl = (email) => {
|
||||
if (!email) return null;
|
||||
|
||||
// 简单验证邮箱格式
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!emailRegex.test(email)) return null;
|
||||
|
||||
// 使用邮箱的MD5哈希(这里简化处理,实际项目中应该使用md5库)
|
||||
// 注意:在实际项目中应该使用正确的MD5哈希函数
|
||||
return `https://www.gravatar.com/avatar/${email}?d=404&s=40`;
|
||||
}
|
||||
|
||||
// 获取名字的首字母
|
||||
/**
|
||||
* 获取名字的首字母
|
||||
* 用于生成字母头像
|
||||
* @param {string} name - 用户昵称
|
||||
* @returns {string} - 首字母或问号
|
||||
*/
|
||||
const getInitialLetter = (name) => {
|
||||
if (!name || typeof name !== 'string') return '?';
|
||||
|
||||
// 移除可能的@回复前缀
|
||||
const cleanName = name.replace(/^.+@\s*/, '');
|
||||
|
||||
// 获取第一个字符
|
||||
const firstChar = cleanName.charAt(0).toUpperCase();
|
||||
|
||||
return firstChar;
|
||||
// 获取第一个字符并转为大写
|
||||
return cleanName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
// 获取首字母头像的样式
|
||||
/**
|
||||
* 获取首字母头像的样式
|
||||
* 根据名字生成一致的颜色
|
||||
* @param {string} name - 用户昵称
|
||||
* @returns {Object} - 包含背景色和文字颜色的样式对象
|
||||
*/
|
||||
const getLetterAvatarStyle = (name) => {
|
||||
// 颜色映射表,根据名字生成一致的颜色
|
||||
// 预定义的颜色数组
|
||||
const colors = [
|
||||
'#4A90E2', '#50E3C2', '#F5A623', '#D0021B', '#9013FE',
|
||||
'#B8E986', '#BD10E0', '#50E3C2', '#417505', '#7ED321',
|
||||
'#BD10E0', '#F8E71C', '#8B572A', '#9B9B9B', '#4A4A4A'
|
||||
];
|
||||
|
||||
// 根据名字生成一个一致的颜色索引
|
||||
// 根据名字生成一致的颜色索引
|
||||
let hash = 0;
|
||||
if (name) {
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
@@ -287,45 +338,16 @@ const getLetterAvatarStyle = (name) => {
|
||||
};
|
||||
}
|
||||
|
||||
// 从后端获取留言列表
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
let res = null
|
||||
// ============================== 数据处理模块 ==============================
|
||||
|
||||
// 优先使用props传递的articleid,其次使用globalStore中的数据
|
||||
let articleid = props.comments || null
|
||||
|
||||
if (!articleid) {
|
||||
// 安全获取文章ID,如果globalStore中没有articleInfo则返回null
|
||||
const articleData = globalStore.getValue('articleInfo')
|
||||
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData) ? articleData.articleid : null
|
||||
}
|
||||
|
||||
form.articleid = articleid
|
||||
|
||||
// 根据是否有文章ID选择不同的API调用
|
||||
if (articleid) {
|
||||
res = await messageService.getMessagesByArticleId(articleid)
|
||||
} else {
|
||||
res = await messageService.getAllMessages()
|
||||
// 过滤掉articleid不为空的留言,只保留articleid为空或不存在的留言
|
||||
if (res && res.data) {
|
||||
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证响应结果
|
||||
if (!res || !res.data) {
|
||||
console.warn('未获取到留言数据')
|
||||
messageBoardData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const allMessages = res.data
|
||||
|
||||
// 处理所有留言,为主留言添加replies数组
|
||||
const allMessagesWithReplies = allMessages.map(msg => ({
|
||||
/**
|
||||
* 处理留言数据,构建留言与回复的层级结构
|
||||
* @param {Array} messages - 原始留言数据
|
||||
* @returns {Array} - 处理后的留言数据(包含回复数组)
|
||||
*/
|
||||
const processMessageData = (messages) => {
|
||||
// 为主留言添加replies数组
|
||||
const allMessagesWithReplies = messages.map(msg => ({
|
||||
...msg,
|
||||
replies: []
|
||||
}))
|
||||
@@ -342,14 +364,38 @@ const fetchMessages = async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 将回复添加到对应的主留言中
|
||||
// 将回复添加到对应的主留言中并处理@回复显示
|
||||
processRepliesForMainMessages(mainMessages, replies)
|
||||
|
||||
return mainMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回复数据,将其添加到对应的主留言中
|
||||
* @param {Array} mainMessages - 主留言数组
|
||||
* @param {Array} replies - 回复数组
|
||||
*/
|
||||
const processRepliesForMainMessages = (mainMessages, replies) => {
|
||||
replies.forEach(reply => {
|
||||
// 找到父留言
|
||||
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
|
||||
if (parentMsg) {
|
||||
// 处理@回复的显示名称
|
||||
processReplyDisplayName(reply, replies)
|
||||
parentMsg.replies.push(reply)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回复的显示名称
|
||||
* 如果是回复其他回复,添加@标记
|
||||
* @param {Object} reply - 回复对象
|
||||
* @param {Array} allReplies - 所有回复数组
|
||||
*/
|
||||
const processReplyDisplayName = (reply, allReplies) => {
|
||||
if (reply.replyid) {
|
||||
const repliedMsg = replies.find(msg => msg.messageid === reply.replyid)
|
||||
const repliedMsg = allReplies.find(msg => msg.messageid === reply.replyid)
|
||||
if (repliedMsg) {
|
||||
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
|
||||
} else {
|
||||
@@ -358,81 +404,130 @@ const fetchMessages = async () => {
|
||||
} else {
|
||||
reply.displayName = reply.nickname
|
||||
}
|
||||
parentMsg.replies.push(reply)
|
||||
}
|
||||
|
||||
// ============================== API调用模块 ==============================
|
||||
/**
|
||||
* 切换分页
|
||||
* @param {number} page - 目标页码
|
||||
*/
|
||||
const changePage = (page) => {
|
||||
fetchMessages()
|
||||
// 根据当前页码优化分页布局
|
||||
if (page === 1) {
|
||||
// 第一页只显示页码和下一页按钮
|
||||
pageLayout.value = 'pager, next'
|
||||
} else if (page === totalPages.value) {
|
||||
// 最后一页只显示上一页按钮和页码
|
||||
pageLayout.value = 'prev, pager'
|
||||
} else {
|
||||
// 中间页显示完整的上一页、页码、下一页
|
||||
pageLayout.value = 'prev, pager, next'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 从后端获取留言列表
|
||||
* 根据articleid决定获取文章留言还是全局留言
|
||||
*/
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
let res = null
|
||||
|
||||
// 获取文章ID(优先使用props,其次使用全局状态)
|
||||
const articleid = getArticleId()
|
||||
form.articleid = articleid
|
||||
// 获取留言数量
|
||||
messageService.getMessageCountByArticleId(articleid).then(res => {
|
||||
if (res.code === 200) {
|
||||
totalPages.value = Math.ceil(res.data / pageSize.value)
|
||||
}
|
||||
})
|
||||
res = await (messageService.getMessagesByPage(articleid, pageNum.value - 1, pageSize.value))
|
||||
// 验证响应结果
|
||||
if (!res || !res.data) {
|
||||
handleEmptyResponse()
|
||||
return
|
||||
}
|
||||
|
||||
// 更新留言板数据
|
||||
messageBoardData.value = mainMessages
|
||||
// 处理留言数据
|
||||
messageBoardData.value = processMessageData(res.data)
|
||||
|
||||
// 根据当前页码更新分页布局
|
||||
if (pageNum.value === 1) {
|
||||
// 第一页只显示页码和下一页按钮
|
||||
pageLayout.value = 'pager, next'
|
||||
} else if (pageNum.value === totalPages.value) {
|
||||
// 最后一页只显示上一页按钮和页码
|
||||
pageLayout.value = 'prev, pager'
|
||||
} else {
|
||||
// 中间页显示完整的上一页、页码、下一页
|
||||
pageLayout.value = 'prev, pager, next'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取留言列表失败:', error)
|
||||
ElMessage.error('获取留言失败,请稍后重试')
|
||||
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
|
||||
handleFetchError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理回复
|
||||
const handleReply = (msg, reply) => {
|
||||
// 检查是否是回复模式
|
||||
if (msg !== null) {
|
||||
// 回复模式
|
||||
form.replyid = reply.messageid
|
||||
form.parentid = msg.messageid
|
||||
} else {
|
||||
// 普通回复模式
|
||||
form.replyid = null
|
||||
form.parentid = reply.messageid
|
||||
/**
|
||||
* 获取文章ID
|
||||
* 优先级:props.comments > globalStore中的articleInfo
|
||||
* @returns {number|null} - 文章ID或null
|
||||
*/
|
||||
const getArticleId = () => {
|
||||
let articleid = props.comments || null
|
||||
|
||||
if (!articleid) {
|
||||
// 安全获取文章ID,如果globalStore中没有articleInfo则返回null
|
||||
const articleData = globalStore.getValue('articleInfo')
|
||||
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData)
|
||||
? articleData.articleid
|
||||
: null
|
||||
}
|
||||
replyingTo.value = {
|
||||
id: reply.messageid,
|
||||
nickname: reply.nickname || '匿名用户',
|
||||
content: reply.content
|
||||
}
|
||||
// 滚动到输入框
|
||||
setTimeout(() => {
|
||||
document.querySelector('.message-form-section')?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 100)
|
||||
|
||||
return articleid
|
||||
}
|
||||
|
||||
// 取消回复
|
||||
const cancelReply = () => {
|
||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||||
form.replyid = null
|
||||
form.content = ''
|
||||
/**
|
||||
* 获取所有留言并过滤
|
||||
* 只保留articleid为空或不存在的全局留言
|
||||
* @returns {Promise} - API响应
|
||||
*/
|
||||
const fetchAllMessages = async () => {
|
||||
const res = await messageService.getAllMessages()
|
||||
// 过滤掉articleid不为空的留言,只保留articleid为空或不存在的留言
|
||||
if (res && res.data) {
|
||||
|
||||
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 监听articleid变化,重新加载留言
|
||||
watch(() => props.comments, (newVal) => {
|
||||
if (newVal) {
|
||||
fetchMessages()
|
||||
}
|
||||
}, { immediate: false })
|
||||
/**
|
||||
* 处理空响应
|
||||
*/
|
||||
const handleEmptyResponse = () => {
|
||||
console.warn('未获取到留言数据')
|
||||
messageBoardData.value = []
|
||||
}
|
||||
|
||||
// 组件挂载时获取留言列表
|
||||
onMounted(() => {
|
||||
fetchMessages()
|
||||
generateCaptcha() // 页面加载时生成验证码
|
||||
})
|
||||
/**
|
||||
* 处理获取留言错误
|
||||
* @param {Error} error - 错误对象
|
||||
*/
|
||||
const handleFetchError = (error) => {
|
||||
console.error('获取留言列表失败:', error)
|
||||
ElMessage.error('获取留言失败,请稍后重试')
|
||||
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
|
||||
}
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
// 表单验证
|
||||
await formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
ElMessage.warning('请检查表单填写是否正确');
|
||||
throw new Error('表单验证失败');
|
||||
}
|
||||
});
|
||||
console.log('提交留言表单:', form)
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
if (form.parentid) {
|
||||
// 回复模式
|
||||
/**
|
||||
* 提交留言
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const submitMessage = async () => {
|
||||
const res = await messageService.saveMessage({
|
||||
content: form.content,
|
||||
nickname: form.nickname,
|
||||
@@ -442,40 +537,89 @@ const onSubmit = async () => {
|
||||
articleid: form.articleid
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success('回复成功')
|
||||
fetchMessages() // 重新获取列表
|
||||
resetForm()
|
||||
cancelReply()
|
||||
} else {
|
||||
ElMessage.error('回复失败:' + (res.message || '未知错误'))
|
||||
}
|
||||
} else {
|
||||
// 普通留言
|
||||
const res = await messageService.saveMessage({
|
||||
content: form.content,
|
||||
nickname: form.nickname,
|
||||
email: form.email,
|
||||
articleid: form.articleid
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success('留言成功')
|
||||
fetchMessages() // 重新获取列表
|
||||
resetForm()
|
||||
/**
|
||||
* 点赞留言
|
||||
* @param {number} messageId - 留言ID
|
||||
* @returns {Promise} - API响应
|
||||
*/
|
||||
const likeMessage = async (messageId) => {
|
||||
return await messageService.likeMessage(messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除留言
|
||||
* @param {number} messageId - 留言ID
|
||||
* @returns {Promise} - API响应
|
||||
*/
|
||||
const deleteMessage = async (messageId) => {
|
||||
return await messageService.deleteMessage(messageId)
|
||||
}
|
||||
|
||||
// ============================== 交互操作模块 ==============================
|
||||
|
||||
/**
|
||||
* 处理回复操作
|
||||
* @param {Object|null} msg - 主留言对象,null表示直接回复主留言
|
||||
* @param {Object} reply - 被回复的留言对象
|
||||
*/
|
||||
const handleReply = (msg, reply) => {
|
||||
// 设置回复相关表单数据
|
||||
setupReplyFormData(msg, reply)
|
||||
|
||||
// 记录正在回复的信息
|
||||
replyingTo.value = {
|
||||
id: reply.messageid,
|
||||
nickname: reply.nickname || '匿名用户',
|
||||
content: reply.content
|
||||
}
|
||||
|
||||
// 滚动到输入框
|
||||
scrollToInput()
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回复表单数据
|
||||
* @param {Object|null} msg - 主留言对象
|
||||
* @param {Object} reply - 被回复的留言对象
|
||||
*/
|
||||
const setupReplyFormData = (msg, reply) => {
|
||||
if (msg !== null) {
|
||||
// 回复模式:回复某条回复
|
||||
form.replyid = reply.messageid
|
||||
form.parentid = msg.messageid
|
||||
} else {
|
||||
ElMessage.error('留言失败:' + (res.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('网络错误,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
// 普通回复模式:回复主留言
|
||||
form.replyid = null
|
||||
form.parentid = reply.messageid
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
/**
|
||||
* 滚动到输入框区域
|
||||
*/
|
||||
const scrollToInput = () => {
|
||||
setTimeout(() => {
|
||||
document.querySelector('.message-form-section')?.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消回复
|
||||
*/
|
||||
const cancelReply = () => {
|
||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||||
form.replyid = null
|
||||
form.content = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const resetForm = () => {
|
||||
form.replyid = null
|
||||
form.content = ''
|
||||
@@ -487,19 +631,79 @@ const resetForm = () => {
|
||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||||
}
|
||||
|
||||
const post_comment_reply_cancel = () => {
|
||||
form.content = ''
|
||||
form.replyid = null
|
||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||||
/**
|
||||
* 提交评论
|
||||
*/
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
// 表单验证
|
||||
await validateForm()
|
||||
// console.log('提交留言表单:', form)
|
||||
|
||||
submitting.value = true
|
||||
|
||||
// 发送留言并处理结果
|
||||
await handleMessageSubmission()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('网络错误,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理点赞
|
||||
/**
|
||||
* 验证表单
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const validateForm = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
ElMessage.warning('请检查表单填写是否正确');
|
||||
reject(new Error('表单验证失败'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理留言提交
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleMessageSubmission = async () => {
|
||||
const res = await submitMessage()
|
||||
|
||||
if (res.success) {
|
||||
// 提交成功
|
||||
ElMessage.success(form.parentid ? '回复成功' : '留言成功')
|
||||
await fetchMessages(0) // 重新获取列表
|
||||
resetForm()
|
||||
|
||||
// 如果是回复模式,取消回复状态
|
||||
if (form.parentid) {
|
||||
cancelReply()
|
||||
}
|
||||
} else {
|
||||
// 提交失败
|
||||
ElMessage.error(`${form.parentid ? '回复' : '留言'}失败:${res.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理点赞操作
|
||||
* @param {Object} msg - 留言对象
|
||||
*/
|
||||
const handleLike = async (msg) => {
|
||||
try {
|
||||
// 显示加载状态或禁用按钮
|
||||
// 显示加载状态
|
||||
msg.isLiking = true
|
||||
|
||||
const res = await messageService.likeMessage(msg.messageid)
|
||||
const res = await likeMessage(msg.messageid)
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 更新点赞数
|
||||
@@ -515,19 +719,23 @@ const handleLike = async (msg) => {
|
||||
msg.isLiking = false
|
||||
}
|
||||
}
|
||||
// 处理删除
|
||||
const handleDelete = async (msg) => {
|
||||
try {
|
||||
// 显示加载状态或禁用按钮
|
||||
// msg.isDeleting = true
|
||||
|
||||
/**
|
||||
* 处理删除操作
|
||||
* @param {number} messageId - 留言ID
|
||||
*/
|
||||
const handleDelete = async (messageId) => {
|
||||
// 确认删除
|
||||
if (!confirm('确定删除吗?')) {
|
||||
return
|
||||
}
|
||||
const res = await messageService.deleteMessage(msg.messageid)
|
||||
|
||||
try {
|
||||
const res = await deleteMessage(messageId)
|
||||
|
||||
if (res.success) {
|
||||
// 从列表中移除
|
||||
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== msg.messageid)
|
||||
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== messageId)
|
||||
ElMessage.success('删除成功')
|
||||
} else {
|
||||
ElMessage.error('删除失败:' + (res.message || '未知错误'))
|
||||
@@ -535,10 +743,32 @@ const handleDelete = async (msg) => {
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('网络错误,请稍后重试')
|
||||
} finally {
|
||||
// msg.isDeleting = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 另一个取消回复的方法(冗余但保留以兼容现有代码)
|
||||
*/
|
||||
const post_comment_reply_cancel = () => {
|
||||
form.content = ''
|
||||
form.replyid = null
|
||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||||
}
|
||||
|
||||
// ============================== 生命周期和监听器 ==============================
|
||||
|
||||
// 监听articleid变化,重新加载留言
|
||||
watch(() => props.comments, (newVal) => {
|
||||
if (newVal) {
|
||||
fetchMessages()
|
||||
}
|
||||
}, { immediate: false })
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
fetchMessages() // 获取留言列表
|
||||
generateCaptcha() // 生成验证码
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -712,16 +942,24 @@ const handleDelete = async (msg) => {
|
||||
|
||||
/**
|
||||
* 内联表单输入行样式
|
||||
* 用于将表单输入项与标签或其他元素对齐
|
||||
* 用于将表单输入项水平均匀分布
|
||||
*/
|
||||
.form-input-row--inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-input-row--inline div:nth-child(2) {
|
||||
margin-left: 9%;
|
||||
margin-right: 9%;
|
||||
.form-input-row--inline .el-form-item {
|
||||
flex: 1;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
width: calc(33.33% - 10px);
|
||||
}
|
||||
|
||||
.form-input-row--inline .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 回复项容器 */
|
||||
@@ -769,6 +1007,64 @@ const handleDelete = async (msg) => {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 分页控件样式优化 */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 30px 0;
|
||||
padding: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination.is-background .el-pager li {
|
||||
margin: 0 4px;
|
||||
background-color: rgba(102, 161, 216, 0.1);
|
||||
border: 1px solid rgba(102, 161, 216, 0.3);
|
||||
color: #333;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination.is-background .el-pager li:hover {
|
||||
background-color: rgba(102, 161, 216, 0.3);
|
||||
border-color: rgba(102, 161, 216, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination.is-background .el-pager li.is-active {
|
||||
background-color: rgba(102, 161, 216, 0.8);
|
||||
border-color: rgba(102, 161, 216, 0.8);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination.is-background .btn-prev,
|
||||
.pagination-controls .el-pagination.is-background .btn-next {
|
||||
background-color: rgba(102, 161, 216, 0.1);
|
||||
border: 1px solid rgba(102, 161, 216, 0.3);
|
||||
color: #333;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 12px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.pagination-controls .el-pagination.is-background .btn-prev:hover,
|
||||
.pagination-controls .el-pagination.is-background .btn-next:hover {
|
||||
background-color: rgba(102, 161, 216, 0.3);
|
||||
border-color: rgba(102, 161, 216, 0.5);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 评论表单区域 */
|
||||
.comment-form-section {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
@@ -866,17 +1162,27 @@ const handleDelete = async (msg) => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-input-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-input-row--inline .el-form-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comment-header-info,
|
||||
.reply-header-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
<template>
|
||||
<div class="nonsense-container">
|
||||
<div class="nonsense-list">
|
||||
<div class="nonsense-item" v-for="item in nonsenseList" :key="item.id">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state-container">
|
||||
<el-skeleton :count="5" />
|
||||
</div>
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state-container">
|
||||
<div>加载失败,请稍后重试</div>
|
||||
<el-button type="primary" @click="handleRetry">重试</el-button>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="displayedNonsenseList.length === 0" class="empty-state-container">
|
||||
<div>暂无吐槽内容</div>
|
||||
</div>
|
||||
<div class="nonsense-item" v-for="item in displayedNonsenseList" :key="item.id">
|
||||
<div class="nonsense-meta-info">
|
||||
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span>
|
||||
<div class="article-status-badge-container" v-if="globalStore.Login">
|
||||
@@ -17,6 +30,9 @@
|
||||
:style="getCharStyle(item.id, index)">{{ char }}</span>
|
||||
</div>
|
||||
</div>
|
||||
// 分页区域
|
||||
<PaginationComponent class="pagination-container" :list="nonsenseList" :pageSize="10"
|
||||
@changePage="handleCurrentDataUpdate" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,6 +41,8 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { nonsenseService } from '@/services'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import PaginationComponent from '@/views/page.vue'
|
||||
import { formatRelativeTime } from '@/utils/dateUtils'
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
@@ -37,6 +55,24 @@ const nonsenseList = ref([])
|
||||
// 存储字符引用和样式的映射
|
||||
const charRefs = ref(new Map())
|
||||
const charStyles = ref(new Map())
|
||||
// 显示的吐槽内容列表
|
||||
const displayedNonsenseList = ref([])
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
// 错误状态
|
||||
const error = ref(false)
|
||||
|
||||
// 处理分页数据更新
|
||||
const handleCurrentDataUpdate = (data) => {
|
||||
displayedNonsenseList.value = data
|
||||
// console.log(data)
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const handleRetry = () => {
|
||||
error.value = false
|
||||
loadNonsenseList()
|
||||
}
|
||||
|
||||
// 定时器引用
|
||||
let colorChangeTimer = null
|
||||
@@ -45,17 +81,24 @@ let colorChangeTimer = null
|
||||
* 加载所有吐槽内容
|
||||
*/
|
||||
const loadNonsenseList = async () => {
|
||||
// 设置加载状态
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const response = await nonsenseService.getNonsenseByStatus(1)
|
||||
if (response.code === 200) {
|
||||
nonsenseList.value = response.data
|
||||
} else {
|
||||
ElMessage.error('加载吐槽内容失败')
|
||||
error.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载吐槽内容失败:', error)
|
||||
} catch (err) {
|
||||
console.error('加载吐槽内容失败:', err)
|
||||
error.value = true
|
||||
} finally {
|
||||
console.log('加载吐槽内容完成')
|
||||
// 结束加载状态
|
||||
loading.value = false
|
||||
// console.log('加载吐槽内容完成')
|
||||
}
|
||||
}
|
||||
// 编辑吐槽内容
|
||||
@@ -266,7 +309,6 @@ onBeforeUnmount(() => {
|
||||
.nonsense-container {
|
||||
/* background-color: rgba(255, 255, 255, 0.85); */
|
||||
border-radius: 12px;
|
||||
padding: 32px 20px 24px 20px;
|
||||
/* box-shadow: 0 2px 12px rgba(0,0,0,0.06); */
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
@@ -328,6 +370,7 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nonsense-meta-info span {
|
||||
padding: 4px 8px;
|
||||
margin-right: 8px;
|
||||
@@ -345,8 +388,7 @@ onBeforeUnmount(() => {
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.nonsense-container {
|
||||
padding: 14px 4px 10px 4px;
|
||||
margin: 0 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nonsense-header h1 {
|
||||
@@ -358,9 +400,9 @@ onBeforeUnmount(() => {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.nonsense-list {
|
||||
/* .nonsense-list {
|
||||
gap: 12px;
|
||||
}
|
||||
} */
|
||||
|
||||
.nonsense-item {
|
||||
padding: 14px 16px 10px 16px;
|
||||
|
||||
199
src/views/page.vue
Normal file
199
src/views/page.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="pagination-container">
|
||||
<!-- 数据渲染区域 -->
|
||||
|
||||
<!-- 分页按钮区域 - 只在需要分页时显示(totalPages > 0 且 list.length <= pageSize) -->
|
||||
<div class="pagination-controls" v-if="totalPages > 1 ">
|
||||
<button
|
||||
v-for="page in totalPages"
|
||||
:key="page"
|
||||
:class="['pagination-btn', { active: currentPage === page }]"
|
||||
@click="changePage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'PaginationComponent'
|
||||
})
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
list: any[]
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
list: () => [],
|
||||
pageSize: 10
|
||||
})
|
||||
|
||||
// 定义响应式数据
|
||||
const currentPage = ref(1)
|
||||
const groupedData = ref<any[]>([])
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
changePage: [data: any[]]
|
||||
}>()
|
||||
|
||||
// 计算属性 - 计算总页数
|
||||
const totalPages = computed(() => {
|
||||
// 如果列表为空,返回0
|
||||
if (!props.list || props.list.length === 0) return 0
|
||||
// 如果列表长度小于pageSize,不进行分组,返回0表示不分页
|
||||
if (props.list.length <= props.pageSize) return 0
|
||||
// console.log(props.list.length, props.pageSize)
|
||||
|
||||
// 列表长度小于等于pageSize时,正常计算页数
|
||||
// 如果能整除,直接返回商
|
||||
if (props.list.length % props.pageSize === 0) {
|
||||
return props.list.length / props.pageSize
|
||||
}
|
||||
|
||||
// 不能整除时,返回商+1(用户需求:将余数添加到最后一组)
|
||||
return Math.floor(props.list.length / props.pageSize) + 1
|
||||
})
|
||||
|
||||
// 计算当前页数据
|
||||
const currentPageData = computed(() => {
|
||||
// 如果列表为空,返回空数组
|
||||
if (!props.list || props.list.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 如果列表长度小于pageSize,不分组,直接返回完整列表
|
||||
if (props.list.length <= props.pageSize) {
|
||||
return [...props.list]
|
||||
}
|
||||
|
||||
// 如果总页数为0(列表为空的情况已处理),返回空数组
|
||||
if (totalPages.value === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 确保当前页码不超过总页数
|
||||
if (currentPage.value > totalPages.value) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 正常分组逻辑
|
||||
const startIndex = (currentPage.value - 1) * props.pageSize
|
||||
let endIndex = startIndex + props.pageSize
|
||||
|
||||
// 对于最后一页,确保不会超出列表范围
|
||||
if (currentPage.value === totalPages.value) {
|
||||
endIndex = props.list.length
|
||||
}
|
||||
|
||||
return props.list.slice(startIndex, endIndex)
|
||||
})
|
||||
|
||||
// 方法 - 重新实现分组逻辑
|
||||
const groupData = () => {
|
||||
groupedData.value = []
|
||||
|
||||
// 如果列表为空或长度大于pageSize,不进行分组
|
||||
if (!props.list || props.list.length === 0 || props.list.length > props.pageSize) {
|
||||
return
|
||||
}
|
||||
|
||||
const listLength = props.list.length
|
||||
|
||||
// 正常分组逻辑,余数会自动添加到最后一组
|
||||
for (let i = 0; i < listLength; i += props.pageSize) {
|
||||
groupedData.value.push(props.list.slice(i, i + props.pageSize))
|
||||
}
|
||||
}
|
||||
|
||||
const changePage = (page: number) => {
|
||||
// 只有在需要分页的情况下才处理页码变化
|
||||
if (totalPages.value > 0 && page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
// 发出事件,传递当前页数据
|
||||
emit('changePage', currentPageData.value)
|
||||
} else if (props.list && props.list.length > 0) {
|
||||
// 当不分组时,直接传递完整列表
|
||||
emit('changePage', [...props.list])
|
||||
}
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.list, () => {
|
||||
currentPage.value = 1
|
||||
groupData()
|
||||
// 数据变化时,立即发出当前数据
|
||||
emit('changePage', currentPageData.value)
|
||||
}, { deep: true })
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
groupData()
|
||||
// 组件挂载时,立即发出初始数据
|
||||
emit('changePage', currentPageData.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
padding: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #d0d0d0;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.pagination-btn.active:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,16 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0'
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
// 配置API代理
|
||||
'/api': {
|
||||
// target: 'http://www.qf1121.top',
|
||||
target: 'http://localhost:7071',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user