Compare commits
22 Commits
b8362e7835
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8193bab566 | ||
|
|
f4263af343 | ||
|
|
0dc24cfa85 | ||
|
|
183d98a699 | ||
|
|
ede67faafd | ||
|
|
07ce8409e1 | ||
|
|
0cbb91077d | ||
|
|
fc581b0476 | ||
|
|
109ac3c009 | ||
|
|
1dc5bdd93f | ||
|
|
4ae0ff7c2a | ||
|
|
309aeaedc1 | ||
|
|
ad893b3e5c | ||
|
|
a927ad5a4d | ||
|
|
6d90b5842f | ||
|
|
85bf3214cc | ||
|
|
6c4d14d06a | ||
|
|
5b3fba7bfb | ||
|
|
b042e2a511 | ||
|
|
02d17d7260 | ||
|
|
266310dea3 | ||
|
|
ed09611d02 |
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
|
- **前端框架**: Vue 3
|
||||||
- **构建工具**: Vite
|
- **构建工具**: Vite
|
||||||
- **路由管理**: Vue Router 4
|
- **路由管理**: Vue Router 4
|
||||||
- **UI组件库**: Element Plus、Ant Design Vue
|
- **UI组件库**: Element Plus
|
||||||
- **HTTP客户端**: Axios
|
- **HTTP客户端**: Axios
|
||||||
- **编程语言**: JavaScript/TypeScript
|
- **类型定义**: TypeScript类型接口
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── App.vue # 应用根组件
|
├── App.vue # 应用根组件
|
||||||
├── assets/ # 静态资源
|
|
||||||
│ └── index.css # 全局样式
|
|
||||||
├── img/ # 图片资源
|
|
||||||
├── index.vue # 首页主组件
|
|
||||||
├── main.js # 应用入口文件
|
├── main.js # 应用入口文件
|
||||||
├── router/ # 路由配置
|
├── router/ # 路由配置
|
||||||
│ └── Router.js # 路由定义
|
├── services/ # API服务模块
|
||||||
└── views/ # 视图组件
|
│ ├── apiService.js # 基础API服务配置
|
||||||
├── aboutme.vue # 关于页面
|
│ ├── articleService.js # 文章相关API
|
||||||
├── aericle.vue # 文章分类目录页面
|
│ ├── categoryService.js # 分类相关API
|
||||||
├── articlecontents.vue # 文章内容页面
|
│ ├── categoryAttributeService.js # 分类属性相关API
|
||||||
├── home.vue # 首页内容组件
|
│ ├── loginService.js # 用户认证相关API
|
||||||
├── leftmodlue.vue # 左侧边栏组件
|
│ ├── messageService.js # 留言相关API
|
||||||
├── messageboard.vue # 留言板页面
|
│ ├── nonsenseService.js # 疯言疯语相关API
|
||||||
└── nonsense.vue # 疯言疯语页面
|
│ └── index.js # 服务导出文件
|
||||||
|
├── types/ # 类型定义
|
||||||
|
│ └── index.ts # 所有接口类型定义
|
||||||
|
├── views/ # 视图组件
|
||||||
|
└── components/ # 可复用组件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 路由配置
|
## API服务模块详解
|
||||||
|
|
||||||
路由配置位于 `src/router/Router.js`,定义了应用的所有路由映射关系:
|
### 1. 基础API服务 (apiService.js)
|
||||||
|
|
||||||
| 路由路径 | 组件 | 功能描述 |
|
基础API服务配置了Axios实例,设置请求拦截器和响应拦截器,统一处理认证信息和错误响应。
|
||||||
|---------|------|---------|
|
|
||||||
| `/` | 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调用。以下是一个示例:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'http://localhost:8080/api', // 后端API基础URL
|
baseURL:'http://localhost:8080/api', // API基础URL
|
||||||
timeout: 10000, // 请求超时时间
|
timeout: 10000, // 请求超时时间
|
||||||
headers: {
|
withCredentials: true // 允许跨域请求携带凭证
|
||||||
'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;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 自动添加认证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. 确保后端服务已部署并运行在正确的端口上
|
1. 确保后端服务已部署并运行在正确的端口上
|
||||||
2. 修改前端API请求的基础URL指向实际的后端服务地址
|
2. 修改 `apiService.js` 中的基础URL指向实际的后端服务地址
|
||||||
3. 构建生产版本并部署到Web服务器
|
3. 构建生产版本并部署到Web服务器
|
||||||
4. 配置Web服务器以支持单页应用路由 (SPA fallback)
|
4. 配置Web服务器以支持单页应用路由 (SPA fallback)
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. 项目使用了Element Plus和Ant Design Vue两个UI库,请注意组件的正确引入和使用方式
|
1. 所有API调用都应该使用services目录下导出的服务实例
|
||||||
2. 后端服务默认端口为8080,前端开发服务器端口为3000
|
2. 对于需要认证的操作,确保用户已登录并持有有效的token
|
||||||
3. 确保CORS配置正确,允许前端域名访问后端API
|
3. 在生产环境中,确保修改API基础URL为实际的后端服务地址
|
||||||
4. 对于需要认证的API,需要在请求头中添加正确的认证信息
|
4. 对于分页查询,合理设置page和size参数以优化性能
|
||||||
|
5. 图片上传等大文件操作需要特别处理,避免超时
|
||||||
## 附录:数据模型
|
|
||||||
|
|
||||||
### 文章数据模型 (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 // 回复列表
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>清疯不颠</title>
|
<title>清疯不颠</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
2807
package-lock.json
generated
2807
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -13,10 +13,13 @@
|
|||||||
"test:unit": "vitest"
|
"test:unit": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ant-design-vue": "^4.2.6",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"antd": "^5.27.3",
|
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"element-plus": "^2.11.2",
|
"element-plus": "^2.11.5",
|
||||||
|
"md-editor-v3": "^6.1.0",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"sass": "^1.93.2",
|
||||||
|
"undraw-ui": "^1.3.2",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"unplugin-auto-import": "^20.1.0",
|
"unplugin-auto-import": "^20.1.0",
|
||||||
"unplugin-vue-components": "^29.0.0",
|
"unplugin-vue-components": "^29.2.0",
|
||||||
"vite": "^7.0.6",
|
"vite": "^7.0.6",
|
||||||
"vite-plugin-vue-devtools": "^8.0.0",
|
"vite-plugin-vue-devtools": "^8.0.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
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 |
BIN
public/fonts/HanTang.woff2
Normal file
BIN
public/fonts/HanTang.woff2
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="apps">
|
||||||
<MainLayout />
|
<MainLayout />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +14,7 @@ import MainLayout from './layouts/MainLayout.vue'
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#apps {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
// 创建axios实例
|
|
||||||
const service = axios.create({
|
|
||||||
baseURL: 'http://localhost:8080/api', // api的base_url
|
|
||||||
timeout: 3000, // 请求超时时间
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
// withCredentials: true, // 跨域请求时是否需要使用凭证
|
|
||||||
})
|
|
||||||
|
|
||||||
// 请求拦截器 - 添加认证token
|
|
||||||
service.interceptors.request.use(
|
|
||||||
config => {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 响应拦截器 - 统一处理响应
|
|
||||||
service.interceptors.response.use(
|
|
||||||
response => {
|
|
||||||
// 检查响应是否成功
|
|
||||||
if (response.data && response.data.success) {
|
|
||||||
return response.data
|
|
||||||
} else {
|
|
||||||
// 处理业务错误
|
|
||||||
return Promise.reject(new Error(response.data?.message || '请求失败'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
// 处理HTTP错误
|
|
||||||
console.error('API请求错误:', error)
|
|
||||||
// 可以在这里添加全局错误处理,如显示错误提示
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 文章相关API
|
|
||||||
export const articleAPI = {
|
|
||||||
// 获取所有文章
|
|
||||||
getAllArticles: () => service.get('/articles'),
|
|
||||||
// 获取单篇文章
|
|
||||||
getArticleById: (id) => service.get(`/articles/${id}`),
|
|
||||||
// 根据分类获取文章
|
|
||||||
getArticlesByCategory: (categoryId) => service.get(`/articles/category/${categoryId}`),
|
|
||||||
// 获取热门文章
|
|
||||||
getPopularArticles: () => service.get('/articles/popular'),
|
|
||||||
// 创建文章
|
|
||||||
createArticle: (articleData) => service.post('/articles', articleData),
|
|
||||||
// 更新文章
|
|
||||||
updateArticle: (id, articleData) => service.put(`/articles/${id}`, articleData),
|
|
||||||
// 删除文章
|
|
||||||
deleteArticle: (id) => service.delete(`/articles/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 留言相关API
|
|
||||||
export const messageAPI = {
|
|
||||||
// 获取所有留言
|
|
||||||
getAllMessages: () => service.get('/messages'),
|
|
||||||
// 获取单条留言
|
|
||||||
getMessageById: (id) => service.get(`/messages/${id}`),
|
|
||||||
// 根据文章ID获取留言
|
|
||||||
getMessagesByArticleId: (articleId) => service.get(`/messages/article/${articleId}`),
|
|
||||||
// 获取根留言
|
|
||||||
getRootMessages: () => service.get('/messages/root'),
|
|
||||||
// 根据父留言ID获取回复
|
|
||||||
getRepliesByParentId: (parentId) => service.get(`/messages/parent/${parentId}`),
|
|
||||||
// 根据昵称搜索留言
|
|
||||||
searchMessagesByNickname: (nickname) => service.get(`/messages/search?nickname=${nickname}`),
|
|
||||||
// 获取文章评论数量
|
|
||||||
getMessageCountByArticleId: (articleId) => service.get(`/messages/count/${articleId}`),
|
|
||||||
// 创建留言
|
|
||||||
saveMessage: (messageData) => service.post('/messages', messageData),
|
|
||||||
// 删除留言
|
|
||||||
deleteMessage: (id) => service.delete(`/messages/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default service
|
|
||||||
@@ -10,20 +10,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="cont">
|
<div id="cont">
|
||||||
<div class="cont1">
|
<div class="cont1">
|
||||||
<h2>小颠片刻</h2>
|
<h3>小颠片刻</h3>
|
||||||
<p>左眼右右眼左,四十五度成就美</p>
|
<p>左眼右右眼左,四十五度成就美</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cont2">
|
<div class="cont2">
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-vertical-demo" @select="handleSelect">
|
<el-menu :default-active="activeIndex" class="el-menu-vertical-demo" @select="handleSelect">
|
||||||
<el-menu-item index="/:type">
|
<el-menu-item index="/home">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>首页</span>
|
<span>首页</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/article-list">
|
<el-menu-item index="/articlelist">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>文章</span>
|
<span>目录</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/nonsense">
|
<el-menu-item index="/nonsense">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
@@ -34,36 +34,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bot" :class="{ 'botrelative': scrollY }">
|
<div id="bot" :class="{ 'botrelative': scrollY }">
|
||||||
<el-tabs v-model="activeName" class="demo-tabs">
|
<el-tabs v-model="activeName" :stretch="true" class="demo-tabs">
|
||||||
<el-tab-pane label="个人简介" name="first">
|
<el-tab-pane label="个人简介" name="first">
|
||||||
<div class="mylogo">
|
<div class="mylogo">
|
||||||
<el-avatar :src="state.circleUrl" />
|
<el-avatar class="mylogo_avatar" :src="state.circleUrl" />
|
||||||
|
</div>
|
||||||
|
<a href="http://www.qf1121.top/">
|
||||||
|
<h6 class="mylogo_name logo-text">清疯不颠</h6>
|
||||||
|
</a>
|
||||||
|
<h6 class="mylogo_description">重度精神失常患者</h6>
|
||||||
|
<div class="stat-container">
|
||||||
|
<div>
|
||||||
|
<a href="#" class="stat-link">
|
||||||
|
<span class="site-state-item-count">{{ articleCount }}</span>
|
||||||
|
<span class="site-state-item-name">文章</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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="showAttributesModel">
|
||||||
|
<span class="site-state-item-count">{{ AttributeCount }}</span>
|
||||||
|
<span class="site-state-item-name">标签</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>清疯不颠</p>
|
|
||||||
<p>重度精神失常患者</p>
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="功能" name="second">
|
<el-tab-pane label="功能" name="second">
|
||||||
|
<div>还在开发中.....</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { articleService, categoryService, categoryAttributeService } from "@/services";
|
||||||
|
|
||||||
// 当前激活菜单
|
// 当前激活菜单
|
||||||
const activeIndex = ref('/:type')
|
const activeIndex = ref('/:type')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const activeName = ref('first')
|
const activeName = ref('first')
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
circleUrl: '/blogicon.jpg',
|
||||||
squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
|
squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
|
||||||
sizeList: ['small', '', 'large'] as const,
|
sizeList: ['small', '', 'large'] as const,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 分类相关状态
|
||||||
|
// defineEmits
|
||||||
|
const emit = defineEmits(['update-data', 'CategoryModal', 'AttributeModal'])
|
||||||
|
|
||||||
|
// 标签相关状态
|
||||||
// 处理菜单选择跳转
|
// 处理菜单选择跳转
|
||||||
const handleSelect = (key: string) => {
|
const handleSelect = (key: string) => {
|
||||||
router.push({ path: key })
|
router.push({ path: key })
|
||||||
@@ -74,6 +104,99 @@ router.beforeEach((to) => {
|
|||||||
activeIndex.value = to.path
|
activeIndex.value = to.path
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 文章数量状态
|
||||||
|
const articleCount = ref(0)
|
||||||
|
// 分类数量状态
|
||||||
|
const categoryCount = ref(0)
|
||||||
|
// 标签数量状态
|
||||||
|
const AttributeCount = ref(0)
|
||||||
|
|
||||||
|
// 获取文章数量
|
||||||
|
const fetchArticleCount = async () => {
|
||||||
|
try {
|
||||||
|
const response = await articleService.getAllArticles();
|
||||||
|
articleCount.value = response.data?.length || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文章数量失败:', error)
|
||||||
|
articleCount.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类数据
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
|
||||||
|
// 分类数据状态
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
try {
|
||||||
|
const response = await categoryService.getAllCategories();
|
||||||
|
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||||
|
categories.value = response.data?.map((category: any) => ({
|
||||||
|
...category,
|
||||||
|
count: 0
|
||||||
|
})) || [];
|
||||||
|
categories.value.forEach(async (category: any) => {
|
||||||
|
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.categoryid)
|
||||||
|
if (attributeResponse.data?.length) {
|
||||||
|
category.count = attributeResponse.data?.length || 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
categoryCount.value = categories.value.length
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
console.error('获取分类失败:', error)
|
||||||
|
// 如果API调用失败,使用模拟数据
|
||||||
|
categories.value = [
|
||||||
|
|
||||||
|
];
|
||||||
|
categoryCount.value = categories.value.length
|
||||||
|
}
|
||||||
|
return categories.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标签数据
|
||||||
|
const fetchAttributes = async () => {
|
||||||
|
// 标签数据状态
|
||||||
|
const attributes = ref<any[]>([])
|
||||||
|
try {
|
||||||
|
const response = await categoryAttributeService.getAllAttributes();
|
||||||
|
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||||
|
attributes.value = response.data?.map((attribute: any) => ({
|
||||||
|
...attribute,
|
||||||
|
count: 0
|
||||||
|
})) || [];
|
||||||
|
attributes.value.forEach(async (attribute: any) => {
|
||||||
|
const articleResponse = await articleService.getArticlesByAttributeId(attribute.attributeid)
|
||||||
|
if (articleResponse.data?.length) {
|
||||||
|
attribute.count = articleResponse.data?.length || 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AttributeCount.value = attributes.value.length
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签失败:', error)
|
||||||
|
// 如果API调用失败,使用模拟数据
|
||||||
|
attributes.value = [
|
||||||
|
];
|
||||||
|
AttributeCount.value = attributes.value.length
|
||||||
|
}
|
||||||
|
return attributes.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向父组件传递标签数据
|
||||||
|
const sendData = () => {
|
||||||
|
const data = { fetchAttributes: fetchAttributes(), fetchCategories: fetchCategories() }
|
||||||
|
emit('update-data', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示标签蒙版
|
||||||
|
const showAttributesModel = () => {
|
||||||
|
emit('AttributeModal', { ifmodal: true })
|
||||||
|
}
|
||||||
|
// 显示分类蒙版
|
||||||
|
const showCategoriesModel = () => {
|
||||||
|
emit('CategoryModal', { ifmodal: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 控制底部模块吸顶效果
|
// 控制底部模块吸顶效果
|
||||||
const scrollY = ref(false)
|
const scrollY = ref(false)
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@@ -83,6 +206,8 @@ const handleScroll = () => {
|
|||||||
// 生命周期管理事件监听,防止内存泄漏
|
// 生命周期管理事件监听,防止内存泄漏
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('scroll', handleScroll)
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
fetchArticleCount() // 组件挂载时获取文章数量
|
||||||
|
sendData() // 组件挂载时获取标签数据和分类数据
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -100,52 +225,78 @@ onUnmounted(() => {
|
|||||||
#top {
|
#top {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: rgba(102, 161, 216, 0.9); /* 蓝色半透明背景 */
|
background-color: rgba(102, 161, 216, 0.9);
|
||||||
|
/* 蓝色半透明背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.top1 {
|
#alld #top .top1 {
|
||||||
padding-top: 10px;
|
padding-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top2 {
|
#alld #top .top2 {
|
||||||
padding-top: 10px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域样式 */
|
/* 内容区域样式 */
|
||||||
#cont {
|
#cont {
|
||||||
|
padding: 0 0 10px 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
/* 白色半透明背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
#cont .cont1 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cont .cont2 {
|
||||||
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cont1 {
|
.cont1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 25px;
|
padding: 25px 10px 25px 10px;
|
||||||
|
background-color: rgba(102, 161, 216, 0.9);
|
||||||
|
/* 蓝色半透明背景 */
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cont1 h2 {
|
.cont1 h3 {
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cont1 p {
|
.cont1 p {
|
||||||
color: #666;
|
color: white;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 菜单样式 */
|
/* 菜单样式 */
|
||||||
.cont2 {
|
.cont2 .el-menu-vertical-demo {
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.cont2 .el-menu-vertical-demo{
|
|
||||||
display: block;
|
display: block;
|
||||||
background-color: rgba(0, 0, 0,0 ); /* 白色半透明背景 */
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
/* 白色半透明背景 */
|
||||||
}
|
}
|
||||||
.cont2 .el-menu-vertical-demo ul li:hover{
|
|
||||||
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
|
.cont2 .el-menu-vertical-demo li {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont2 .el-menu-vertical-demo .el-menu-item:hover {
|
||||||
|
background-color: rgba(64, 158, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont2 .el-menu-vertical-demo .el-menu-item.is-active:hover {
|
||||||
|
color: black;
|
||||||
|
/* 蓝色半透明背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont2 .el-menu-vertical-demo .el-menu-item.is-active {
|
||||||
|
color: var(--nav-is-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分类列表样式 */
|
/* 分类列表样式 */
|
||||||
@@ -201,22 +352,71 @@ onUnmounted(() => {
|
|||||||
/* 底部标签页样式 */
|
/* 底部标签页样式 */
|
||||||
#bot {
|
#bot {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
padding: 15px;
|
/* 白色半透明背景 */
|
||||||
}
|
padding: 10px;
|
||||||
.demo-tabs .el-tabs__header .el-tabs__nav-wrap .el-tabs__nav-scroll{
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
/* 头像样式 */
|
|
||||||
.mylogo {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-avatar {
|
.site-state-item-count {
|
||||||
width: 80px;
|
display: block;
|
||||||
height: 80px;
|
text-align: center;
|
||||||
|
color: #32325d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-tabs {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-scroll .el-tabs__nav .el-tabs__item {
|
||||||
|
margin-left: 100px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mylogo_name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mylogo_description {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #c21f30;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.stat-container div:not(:first-child) {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像样式 */
|
||||||
|
#pane-first .mylogo {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mylogo_avatar {
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 直接应用到el-avatar组件上的悬浮效果 */
|
||||||
|
.el-avatar.mylogo_avatar {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-avatar.mylogo_avatar:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 吸顶效果 */
|
/* 吸顶效果 */
|
||||||
@@ -232,9 +432,12 @@ onUnmounted(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-20px);
|
transform: translateY(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 MiB |
BIN
src/img/bg.jpg
Normal file
BIN
src/img/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
210
src/index.vue
210
src/index.vue
@@ -1,210 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="elrow-top" :class="elrowtop">
|
|
||||||
<el-row justify="center">
|
|
||||||
<el-col :span="4" v-if="windowwidth">
|
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
|
||||||
<div>清疯不颠</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="14" justify="center">
|
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" :collapse="false" @select="handleSelect">
|
|
||||||
<el-menu-item index="/:type">
|
|
||||||
首页
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/aericle">
|
|
||||||
文章
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/nonsense">
|
|
||||||
疯言疯语
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/about">
|
|
||||||
关于
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/message">
|
|
||||||
留言板
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="2" class="search-container" v-if="windowwidth">
|
|
||||||
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
<div class="hero" :class="{ 'newhero': classhero }" v-if="windowwidth">
|
|
||||||
<h1 class="typewriter">{{ heroText }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content-section" :class="{ 'visible': isconts }">
|
|
||||||
<div class="nonsensetitle" v-if="classnonsenset">
|
|
||||||
<div class="nonsensetitleconst">
|
|
||||||
<h1>发癫中QAQ</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 状态模块 -->
|
|
||||||
<leftmodlue class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
|
|
||||||
<!-- 内容模块 -->
|
|
||||||
<RouterView class="RouterViewpage" :class="{ 'nonsensetmargintop': classnonsenset }" />
|
|
||||||
</div>
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="Pagination">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
// import leftmodlue from '@/views/leftmodlue.vue';
|
|
||||||
|
|
||||||
// hero 区域样式控制
|
|
||||||
const classhero = ref(false);
|
|
||||||
// 内容区可见性
|
|
||||||
const isconts = ref(false);
|
|
||||||
// 左侧模块滚动状态
|
|
||||||
const isScrollingleftmodlue = ref(false);
|
|
||||||
// 导航栏样式类名
|
|
||||||
const elrowtop = ref('transparent');
|
|
||||||
// 疯言疯语标题区显示
|
|
||||||
const classnonsenset = ref(false);
|
|
||||||
// 屏幕宽度标记(true为大屏,false为小屏)
|
|
||||||
const windowwidth = ref(true);
|
|
||||||
// 当前激活菜单
|
|
||||||
const activeIndex = ref('/:type');
|
|
||||||
|
|
||||||
// 打字机效果相关
|
|
||||||
const fullHeroText = '如果感到累了撸一管就好了';
|
|
||||||
const heroText = ref('');
|
|
||||||
let heroIndex = 0;
|
|
||||||
let heroTimer: number | undefined;
|
|
||||||
|
|
||||||
const startTypewriter = () => {
|
|
||||||
heroText.value = '';
|
|
||||||
heroIndex = 0;
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
heroTimer = window.setInterval(() => {
|
|
||||||
if (heroIndex < fullHeroText.length) {
|
|
||||||
heroText.value += fullHeroText[heroIndex];
|
|
||||||
heroIndex++;
|
|
||||||
} else {
|
|
||||||
clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单选择跳转
|
|
||||||
*/
|
|
||||||
const handleSelect = (key: string) => {
|
|
||||||
router.push({ path: key });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据路由路径设置页面状态
|
|
||||||
*/
|
|
||||||
const updatePageState = (url: string) => {
|
|
||||||
classhero.value = url !== '/:type';
|
|
||||||
classnonsenset.value = url == '/nonsense';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路由切换时处理页面状态和滚动
|
|
||||||
*/
|
|
||||||
router.beforeEach((to) => {
|
|
||||||
updatePageState(to.path);
|
|
||||||
setActiveIndex(to.path);
|
|
||||||
// 跳转后回到顶部
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
// 首页内容区滚动动画仅大屏下生效
|
|
||||||
if (to.path === '/:type') {
|
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
|
||||||
// 首页时启动打字机
|
|
||||||
startTypewriter();
|
|
||||||
} else {
|
|
||||||
isconts.value = true;
|
|
||||||
heroText.value = '';
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滚动事件处理
|
|
||||||
*/
|
|
||||||
const handleScroll = () => {
|
|
||||||
// 屏幕小于768时只切换导航栏样式,不做内容动画
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 导航栏样式切换
|
|
||||||
if (window.scrollY > 1200) {
|
|
||||||
elrowtop.value = 'hide';
|
|
||||||
} else {
|
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
|
||||||
}
|
|
||||||
// 首页内容区滚动动画
|
|
||||||
if (location.pathname === '/:type') {
|
|
||||||
isconts.value = window.scrollY > 200;
|
|
||||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
|
||||||
} else {
|
|
||||||
isconts.value = true;
|
|
||||||
isScrollingleftmodlue.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加滚动监听
|
|
||||||
*/
|
|
||||||
const addScrollListener = () => {
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除滚动监听
|
|
||||||
*/
|
|
||||||
const removeScrollListener = () => {
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 屏幕尺寸变化处理
|
|
||||||
* 小屏时移除滚动监听并设置相关状态
|
|
||||||
* 大屏时添加滚动监听
|
|
||||||
*/
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
windowwidth.value = false;
|
|
||||||
isScrollingleftmodlue.value = true;
|
|
||||||
classnonsenset.value = false;
|
|
||||||
isconts.value = true;
|
|
||||||
} else {
|
|
||||||
windowwidth.value = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前激活菜单
|
|
||||||
*/
|
|
||||||
const setActiveIndex = (locationpathname) => {
|
|
||||||
activeIndex.value = locationpathname;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生命周期钩子
|
|
||||||
onMounted(() => {
|
|
||||||
handleResize(); addScrollListener();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
// 初始进入时如果是首页,启动打字机
|
|
||||||
if (window.location.pathname === '/:type') {
|
|
||||||
startTypewriter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// removeScrollListener();
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style></style>
|
|
||||||
@@ -1,206 +1,842 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="elrow-top" :class="elrowtop">
|
<div class="elrow-top" :class="elrowtop">
|
||||||
<el-row justify="center">
|
<el-row justify="center">
|
||||||
<el-col :span="4" v-if="windowwidth">
|
<el-col :span="6" v-if="windowwidth">
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
<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>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="14" justify="center">
|
<el-col :span="14" justify="center">
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
<div class="grid-content ep-bg-purple-dark">
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" :collapse="false" @select="handleSelect">
|
<el-menu :default-active="activeIndex" class="el-menu-demo" :collapse="false" @select="handleSelect">
|
||||||
<el-menu-item index="/:type">
|
<el-menu-item index="home">
|
||||||
首页
|
首页
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/article-list">
|
<el-menu-item index="articlelist">
|
||||||
目录
|
目录
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/nonsense">
|
<el-menu-item index="nonsense">
|
||||||
疯言疯语
|
疯言疯语
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/about">
|
<el-menu-item index="about">
|
||||||
关于
|
关于
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/message">
|
<el-menu-item index="message">
|
||||||
留言板
|
留言板
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="search-container" v-if="windowwidth">
|
<el-col :span="2" class="search-container" v-if="windowwidth">
|
||||||
<!-- 搜索框可以在这里添加 -->
|
<!-- 搜索功能 -->
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
|
||||||
|
<el-input v-model="searchKeyword" placeholder="回车搜索文章..." class="search-input" @keyup.enter="performSearch"
|
||||||
|
@blur="closeSearchBoxWithDelay" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero 区域 -->
|
<!-- 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>
|
<h1 class="typewriter">{{ heroText }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div id="content-section" :class="{ 'visible': isconts }">
|
<div id="content-section" :class="{ 'visible': iscontentvisible }">
|
||||||
<div class="nonsensetitle" v-if="classnonsenset">
|
<div class="nonsensetitle" v-if="classnonsenset">
|
||||||
<div class="nonsensetitleconst">
|
<div class="nonsensetitleconst">
|
||||||
<h1>发癫中QAQ</h1>
|
<h1>{{ Cardtitle }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 左侧模块 -->
|
<!-- 左侧模块 -->
|
||||||
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
|
<div class="leftmodluecontainer" v-if="isleftmodluecontainer">
|
||||||
|
<LeftModule class="leftmodluepage" @update-data="updateData" @CategoryModal="CategoryModal"
|
||||||
|
@AttributeModal="AttributeModal" :class="{ 'nonsensetmargintop': classmoduleorrouter }" v-if="windowwidth" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 内容模块 -->
|
<!-- 内容模块 -->
|
||||||
<RouterView class="RouterViewpage" :class="{ 'nonsensetmargintop': classnonsenset }" />
|
<div class="RouterViewpage">
|
||||||
</div>
|
<RouterView :class="{ 'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
|
||||||
|
<!-- 页脚 -->
|
||||||
<!-- 分页区域 -->
|
<Footer class="footer-container" v-if="windowwidth" />
|
||||||
<div class="Pagination">
|
</div>
|
||||||
<!-- 分页组件可以在这里添加 -->
|
|
||||||
</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>
|
||||||
|
|
||||||
|
<!-- 标签蒙板组件 -->
|
||||||
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import LeftModule from '@/components/LeftModule.vue';
|
import LeftModule from '@/components/LeftModule.vue';
|
||||||
|
import Establish from '@/layouts/establish.vue';
|
||||||
|
import Footer from '@/views/Footer.vue';
|
||||||
|
|
||||||
|
// ========== 组件初始化 ==========
|
||||||
|
|
||||||
// 路由相关
|
// 路由相关
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
// 响应式状态
|
// 全局状态管理
|
||||||
const classhero = ref(false);
|
import { useGlobalStore } from '@/store/globalStore';
|
||||||
const isconts = ref(false);
|
const globalStore = useGlobalStore();
|
||||||
const isScrollingleftmodlue = ref(false);
|
const Login = computed(() => globalStore.Login);
|
||||||
const elrowtop = ref('transparent');
|
|
||||||
|
// ========== 响应式状态定义 ==========
|
||||||
|
// 页面标题和样式相关状态
|
||||||
|
const Cardtitle = ref('');
|
||||||
|
const classmoduleorrouter = ref(false);
|
||||||
const classnonsenset = ref(false);
|
const classnonsenset = ref(false);
|
||||||
|
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 windowwidth = ref(true);
|
||||||
const activeIndex = ref('/:type');
|
|
||||||
|
// 导航相关状态
|
||||||
|
const activeIndex = ref('home');
|
||||||
|
const localhome = 'home';
|
||||||
|
let rpsliturl = route.path.split('/');
|
||||||
|
|
||||||
|
// 搜索相关状态
|
||||||
|
const isSearchBoxOpen = ref(false);
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
let searchCloseTimer: number | undefined;
|
||||||
|
|
||||||
// 打字机效果相关
|
// 打字机效果相关
|
||||||
const fullHeroText = '如果感到累了撸一管就好了';
|
let fullHeroText = '清疯不颠';
|
||||||
const heroText = ref('');
|
const heroText = ref('');
|
||||||
let heroIndex = 0;
|
let heroIndex = 0;
|
||||||
let heroTimer: number | undefined;
|
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 = '';
|
heroText.value = '';
|
||||||
heroIndex = 0;
|
heroIndex = 0;
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
|
// 清除可能存在的定时器
|
||||||
|
if (heroTimer) {
|
||||||
|
clearInterval(heroTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的定时器,逐字显示文本
|
||||||
heroTimer = window.setInterval(() => {
|
heroTimer = window.setInterval(() => {
|
||||||
if (heroIndex < fullHeroText.length) {
|
if (heroIndex < text.length) {
|
||||||
heroText.value += fullHeroText[heroIndex];
|
heroText.value += text[heroIndex];
|
||||||
heroIndex++;
|
heroIndex++;
|
||||||
} else {
|
} else {
|
||||||
|
// 文本显示完毕,清除定时器
|
||||||
clearInterval(heroTimer);
|
clearInterval(heroTimer);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 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) => {
|
const handleSelect = (key: string) => {
|
||||||
router.push({ path: key });
|
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 = (url: string) => {
|
const updatePageState = () => {
|
||||||
classhero.value = url !== '/:type';
|
// 根据是否为主页根路径设置hero区域状态
|
||||||
classnonsenset.value = url === '/nonsense';
|
classsmallhero.value = !(rpsliturl[1] == localhome && rpsliturl[2] == undefined);
|
||||||
|
// 控制左侧模块容器的显示/隐藏
|
||||||
|
isleftmodluecontainer.value = rpsliturl[1] !== "articlesave";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置当前激活的菜单项
|
* 更新文章标题和相关显示状态
|
||||||
*/
|
*/
|
||||||
const setActiveIndex = (path: string) => {
|
const updateArticleTitle = () => {
|
||||||
activeIndex.value = path;
|
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;
|
||||||
|
|
||||||
|
// 如果打开搜索框,清除之前的延时关闭定时器
|
||||||
|
if (isSearchBoxOpen.value && searchCloseTimer) {
|
||||||
|
clearTimeout(searchCloseTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即关闭搜索框
|
||||||
|
*/
|
||||||
|
const closeSearchBox = () => {
|
||||||
|
isSearchBoxOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带延迟关闭搜索框
|
||||||
|
* 用于处理点击搜索按钮后的情况,让用户有时间看到反馈
|
||||||
|
*/
|
||||||
|
const closeSearchBoxWithDelay = () => {
|
||||||
|
// 清除可能存在的定时器
|
||||||
|
if (searchCloseTimer) {
|
||||||
|
clearTimeout(searchCloseTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的延迟关闭定时器
|
||||||
|
searchCloseTimer = window.setTimeout(() => {
|
||||||
|
isSearchBoxOpen.value = false;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行搜索操作
|
||||||
|
* 将搜索关键词存储到全局状态并跳转到搜索结果页面
|
||||||
|
*/
|
||||||
|
const performSearch = () => {
|
||||||
|
// 验证搜索关键词不为空
|
||||||
|
if (searchKeyword.value.trim()) {
|
||||||
|
// 清除全局搜索关键词
|
||||||
|
globalStore.removeValue('articleserarch');
|
||||||
|
// 存储搜索关键词到全局状态
|
||||||
|
globalStore.setValue('articleserarch', {
|
||||||
|
name: searchKeyword.value
|
||||||
|
});
|
||||||
|
// 跳转到搜索结果页面
|
||||||
|
router.push({ path: `/home/aericletitle` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索后保持搜索框打开状态
|
||||||
|
if (searchCloseTimer) {
|
||||||
|
clearTimeout(searchCloseTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== 响应式处理模块 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理窗口大小变化
|
* 处理窗口大小变化
|
||||||
|
* 根据屏幕宽度调整布局和内容显示
|
||||||
*/
|
*/
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
|
||||||
|
// 更新窗口宽度状态
|
||||||
windowwidth.value = window.innerWidth > 768;
|
windowwidth.value = window.innerWidth > 768;
|
||||||
|
|
||||||
// 根据屏幕大小调整内容区可见性
|
// 首页特殊处理:小屏幕下默认显示内容区
|
||||||
if (route.path === '/:type') {
|
if (rpsliturl[1] === localhome) {
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
iscontentvisible.value = window.innerWidth <= 768;
|
||||||
}
|
}
|
||||||
|
// 移动端首页默认显示内容区,桌面端初始隐藏
|
||||||
|
iscontentvisible.value = window.innerWidth <= 768;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理滚动事件
|
* 处理页面滚动事件
|
||||||
|
* 根据滚动位置调整导航栏样式和内容显示动画
|
||||||
*/
|
*/
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
// 屏幕小于768时只切换导航栏样式,不做内容动画
|
let scrollY = 0;
|
||||||
if (window.innerWidth < 768) {
|
scrollY = window.scrollY;
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
// 小屏幕设备只切换导航栏样式
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
updateNavbarStyle(scrollY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导航栏样式切换
|
// 大屏幕设备完整处理
|
||||||
if (window.scrollY > 1200) {
|
updateNavbarStyle(scrollY);
|
||||||
elrowtop.value = 'hide';
|
// 仅在首页根路径应用滚动动画
|
||||||
} else {
|
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
// 首页滚动动画处理
|
||||||
|
if (scrollY <= 350) {
|
||||||
|
HeroState(scrollY);
|
||||||
|
}
|
||||||
|
// 控制左侧模块的滚动状态
|
||||||
|
isScrollingleftmodlue.value = scrollY > 600;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
// 首页内容区滚动动画
|
// ========== 首页滚动动画模块 ==========
|
||||||
if (route.path === '/:type') {
|
const HeroState = (scrollY: number) => {
|
||||||
isconts.value = window.scrollY > 200;
|
const windowHeight = window.innerHeight;
|
||||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
// 计算滚动距离与窗口高度的比例,用于内容渐显
|
||||||
|
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 handleRouteChange = () => {
|
||||||
|
// 重新解析路由路径
|
||||||
|
rpsliturl = route.path.split('/');
|
||||||
|
// 更新页面相关状态
|
||||||
|
updatePageState();
|
||||||
|
setActiveIndex(rpsliturl[1]);
|
||||||
|
updateArticleTitle();
|
||||||
|
|
||||||
|
// 页面跳转后回到顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
|
||||||
|
// 根据是否为首页决定是否启动打字机效果
|
||||||
|
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||||
|
// 首页启动打字机效果
|
||||||
|
startTypewriter(fullHeroText);
|
||||||
|
// 重置hero的margin值为初始值
|
||||||
|
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
|
||||||
|
// 重置hero的移动状态
|
||||||
|
heroTransform.value = `translateY(${heroTransformValue}px)`;
|
||||||
|
heroIsMoving.value = false;
|
||||||
|
heroPosition.value = 'static';
|
||||||
|
} else {
|
||||||
|
iscontentvisible.value = true;
|
||||||
|
startTypewriter(fullHeroText);
|
||||||
|
heroMarginBottom.value = `${5}%`;
|
||||||
|
heroTransform.value = ``;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听路由变化
|
* 初始化页面
|
||||||
*/
|
*/
|
||||||
watch(() => route.path, (newPath) => {
|
const initializePage = () => {
|
||||||
updatePageState(newPath);
|
|
||||||
setActiveIndex(newPath);
|
|
||||||
|
|
||||||
// 跳转后回到顶部
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
|
|
||||||
// 首页内容区滚动动画仅大屏下生效
|
|
||||||
if (newPath === '/:type') {
|
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
|
||||||
// 首页时启动打字机
|
|
||||||
startTypewriter();
|
|
||||||
} else {
|
|
||||||
isconts.value = true;
|
|
||||||
heroText.value = '';
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生命周期钩子
|
|
||||||
*/
|
|
||||||
onMounted(() => {
|
|
||||||
// 初始化窗口大小
|
// 初始化窗口大小
|
||||||
handleResize();
|
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();
|
||||||
|
|
||||||
// 添加事件监听器
|
// 添加事件监听器
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件卸载时执行
|
||||||
|
*/
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清理事件监听器
|
// 清理事件监听器
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
if (heroTimer) {
|
||||||
|
clearInterval(heroTimer);
|
||||||
|
}
|
||||||
|
if (searchCloseTimer) {
|
||||||
|
clearTimeout(searchCloseTimer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== 监听器 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听路由变化
|
||||||
|
* 当路由变化时更新页面状态
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
handleRouteChange
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 搜索框样式 */
|
||||||
|
.search-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
/* padding: 8px; */
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon-btn:hover,
|
||||||
|
.search-icon-btn.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); */
|
||||||
|
overflow: hidden;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-container.open {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
/* padding: 0 8px; */
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn,
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #606266;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:hover,
|
||||||
|
.close-btn:hover {
|
||||||
|
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: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
783
src/layouts/establish.vue
Normal file
783
src/layouts/establish.vue
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
<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)">
|
||||||
|
<!-- <i :class="btn.icon"></i> -->
|
||||||
|
<span>{{ btn.label }}</span>
|
||||||
|
</button>
|
||||||
|
<!-- </div> -->
|
||||||
|
|
||||||
|
<!-- 主圆形按钮 -->
|
||||||
|
<button class="main-button" :class="{ 'active': isExpanded }" @click="toggleExpand">
|
||||||
|
<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 } from 'vue-router';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { articleService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services'
|
||||||
|
|
||||||
|
// 全局状态管理
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 状态管理模块
|
||||||
|
// ==============================
|
||||||
|
/**
|
||||||
|
* 按钮展开状态
|
||||||
|
* @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: [
|
||||||
|
{ id: 'edit-article', label: '修改', icon: 'icon-add-article' },
|
||||||
|
{ id: 'delete-article', label: '删除', icon: 'icon-create-category' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 首页按钮
|
||||||
|
home: [
|
||||||
|
{ id: 'create-article', label: '新建', icon: 'icon-add-article' },
|
||||||
|
{ id: 'del-articles', label: '已删除', icon: 'icon-new-tag' },
|
||||||
|
{ id: 'unpublished-articles', label: '未发表', icon: 'icon-new-tag' },
|
||||||
|
{ id: 'published-articles', label: '已发表', icon: 'icon-new-tag' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 疯言疯语页面按钮
|
||||||
|
nonsense: [
|
||||||
|
{ id: 'create-nonsense', label: '说说', icon: 'icon-upload-file' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 分类页面按钮
|
||||||
|
articlelist: [
|
||||||
|
{ id: 'create-category', label: '分类', icon: 'icon-create-category' },
|
||||||
|
{ id: 'create-tag', label: '标签', icon: 'icon-create-tag' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 标签页面按钮
|
||||||
|
tag: [
|
||||||
|
{ id: 'create-tag', label: '新建', icon: 'icon-new-tag' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 文章保存页面按钮
|
||||||
|
articlesave: [
|
||||||
|
{ id: 'view-articles', label: '查看文章列表', icon: 'icon-new-tag' },
|
||||||
|
...baseButtons
|
||||||
|
],
|
||||||
|
|
||||||
|
// 默认按钮
|
||||||
|
default: baseButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前页面返回对应的按钮配置
|
||||||
|
* @returns {Array<{id: string, label: string, icon: string}>} 按钮配置数组
|
||||||
|
*/
|
||||||
|
const isbuttonsave = () => {
|
||||||
|
try {
|
||||||
|
// 获取当前页面路径名称
|
||||||
|
const currentPath = globalStore.getValue('localpath')?.name || 'default';
|
||||||
|
// 返回对应页面的按钮配置,如果没有则返回默认配置
|
||||||
|
return pageButtons[currentPath] || pageButtons.default;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取页面按钮配置失败:', error);
|
||||||
|
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
|
||||||
|
|
||||||
|
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
|
||||||
|
confirmButtonText: '保存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPlaceholder: '在这里输入您想说的话...',
|
||||||
|
inputType: 'textarea',
|
||||||
|
inputRows: 4,
|
||||||
|
showCancelButton: true
|
||||||
|
}).then(({ value }) => {
|
||||||
|
// 保存疯言疯语
|
||||||
|
saveNonsense(value)
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消操作,静默处理
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 功能操作方法模块
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理错误响应的工具函数
|
||||||
|
* @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('内容不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用服务保存疯言疯语
|
||||||
|
nonsenseService.createNonsense({
|
||||||
|
content: content.trim(),
|
||||||
|
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 createCategory = () => {
|
||||||
|
ElMessageBox.prompt('请输入分类名称:', '新建分类', {
|
||||||
|
confirmButtonText: '保存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPlaceholder: '请输入分类名称',
|
||||||
|
inputType: 'text',
|
||||||
|
showCancelButton: true,
|
||||||
|
// 输入验证
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return '分类名称不能为空';
|
||||||
|
}
|
||||||
|
if (value.trim().length < 2) {
|
||||||
|
return '分类名称至少需要2个字符';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).then(({ value }) => {
|
||||||
|
// 保存分类
|
||||||
|
saveCategory(value.trim());
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消操作,静默处理
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存分类
|
||||||
|
* @param {string} typename - 分类名称
|
||||||
|
*/
|
||||||
|
const saveCategory = (typename) => {
|
||||||
|
// console.log('保存分类')
|
||||||
|
categoryService.createCategory({
|
||||||
|
typename: typename
|
||||||
|
}).then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('分类创建成功');
|
||||||
|
// 刷新页面
|
||||||
|
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')?.articleid
|
||||||
|
if (!articleId) {
|
||||||
|
ElMessage.warning('缺少文章ID参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
ElMessageBox.confirm('确定删除该文章吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
// 调用删除文章接口
|
||||||
|
articleService.deleteArticle(Number(articleId))
|
||||||
|
.then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('文章删除成功')
|
||||||
|
// 跳转回文章列表页
|
||||||
|
router.push({ path: '/home' })
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => handleErrorResponse(err, '删除失败'))
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消删除,静默处理
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改文章
|
||||||
|
*/
|
||||||
|
const updateArticle = () => {
|
||||||
|
const articleId = globalStore.getValue('articleInfo')
|
||||||
|
if (!articleId) {
|
||||||
|
ElMessage.warning('缺少文章参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认修改
|
||||||
|
ElMessageBox.confirm('确定修改该文章吗?', '修改确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
globalStore.setValue('updatearticle', articleId)
|
||||||
|
router.push({ path: '/articlesave' })
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消修改,静默处理
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出系统
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
// 调用登出接口
|
||||||
|
loginService.logout()
|
||||||
|
.then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('登出成功')
|
||||||
|
// 清空全局状态
|
||||||
|
globalStore.clearAll()
|
||||||
|
// 清除localStorage中的token
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
// 跳转首页
|
||||||
|
router.push({ path: '/' })
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '登出失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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)
|
||||||
|
|
||||||
|
// 使用按钮ID进行处理,更可靠且易于维护
|
||||||
|
switch (button.id) {
|
||||||
|
// 新增操作
|
||||||
|
case 'create-article':
|
||||||
|
router.push({ path: '/articlesave' })
|
||||||
|
break
|
||||||
|
case 'create-category':
|
||||||
|
createCategory()
|
||||||
|
break
|
||||||
|
case 'create-tag':
|
||||||
|
createAttribute()
|
||||||
|
break
|
||||||
|
case 'create-nonsense':
|
||||||
|
showNonsenseModal()
|
||||||
|
break
|
||||||
|
|
||||||
|
// 修改操作
|
||||||
|
case 'edit-article':
|
||||||
|
updateArticle()
|
||||||
|
break
|
||||||
|
|
||||||
|
// 删除操作
|
||||||
|
case 'delete-article':
|
||||||
|
deleteArticle();
|
||||||
|
break
|
||||||
|
|
||||||
|
// 查看操作
|
||||||
|
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
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('未处理的按钮类型:', button.id, button.label)
|
||||||
|
ElMessage.info(`功能 ${button.label} 暂未实现`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击后收起按钮菜单
|
||||||
|
isExpanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击外部区域收起按钮的处理函数
|
||||||
|
* @param {Event} e - 点击事件对象
|
||||||
|
*/
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
const mainButton = document.querySelector('.main-button')
|
||||||
|
const actionButtons = document.querySelectorAll('.action-button')
|
||||||
|
|
||||||
|
// 检查是否点击了主按钮
|
||||||
|
const clickedMainButton = mainButton && mainButton.contains(e.target)
|
||||||
|
|
||||||
|
// 检查是否点击了任何一个操作按钮
|
||||||
|
const clickedActionButton = Array.from(actionButtons).some(button => button.contains(e.target))
|
||||||
|
|
||||||
|
// 如果点击的不是主按钮和操作按钮,则收起
|
||||||
|
if (!clickedMainButton && !clickedActionButton) {
|
||||||
|
isExpanded.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 组件生命周期
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件挂载时添加事件监听
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件卸载前清理事件监听
|
||||||
|
*/
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.establish-container {
|
||||||
|
width: 230px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
bottom: 50px;
|
||||||
|
right: -80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主圆形按钮样式 */
|
||||||
|
.main-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #409eff;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1001;
|
||||||
|
margin-left: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button.active {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button.active:hover {
|
||||||
|
background-color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button .icon {
|
||||||
|
font-style: normal;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 弹出的按钮样式 */
|
||||||
|
.action-button {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0;
|
||||||
|
/* 从右侧滑入 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover span {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-style: normal;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button span {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮展开状态样式 */
|
||||||
|
.action-button.show {
|
||||||
|
margin-left: 70px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮默认状态样式 */
|
||||||
|
.action-button {
|
||||||
|
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 {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-buttons.show {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
src/main.js
19
src/main.js
@@ -1,11 +1,30 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import Router from './router/Router'
|
import Router from './router/Router'
|
||||||
|
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
|
||||||
|
import UndrawUi from 'undraw-ui'
|
||||||
|
import 'undraw-ui/dist/style.css'
|
||||||
|
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
import './styles/MainLayout.css'
|
import './styles/MainLayout.css'
|
||||||
|
|
||||||
|
|
||||||
|
// 创建Pinia实例
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(UndrawUi)
|
||||||
app.use(Router)
|
app.use(Router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
|
app.use(pinia) // 添加Pinia支持
|
||||||
|
|
||||||
|
// 注册所有Element Plus图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { createWebHistory, createRouter } from 'vue-router'
|
import { createWebHistory, createRouter } from 'vue-router'
|
||||||
// 导入视图组件
|
// 导入视图组件
|
||||||
import ArticleList from '../views/aericle.vue'
|
import ArticleList from '../views/aericlelist.vue'
|
||||||
import HomePage from '../views/home.vue'
|
import HomePage from '../views/home.vue'
|
||||||
import NonsensePage from '../views/nonsense.vue'
|
import NonsensePage from '../views/nonsense.vue'
|
||||||
import MessageBoardPage from '../views/messageboard.vue'
|
import MessageBoardPage from '../views/messageboard.vue'
|
||||||
import AboutMePage from '../views/aboutme.vue'
|
import AboutMePage from '../views/aboutme.vue'
|
||||||
import ArticleContentPage from '../views/articlecontents.vue'
|
import ArticleContentPage from '../views/articlecontents.vue'
|
||||||
|
import LoginPage from '../views/login.vue'
|
||||||
|
import ArticleSavePage from '../views/articlesave.vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由配置数组
|
* 路由配置数组
|
||||||
@@ -14,18 +16,40 @@ import ArticleContentPage from '../views/articlecontents.vue'
|
|||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/all' // 默认跳转到首页,显示所有文章
|
redirect: '/home' // 默认跳转到首页,显示所有文章
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:type',
|
path: '/home',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomePage,
|
component: HomePage,
|
||||||
meta: {
|
meta: { title: '首页' },
|
||||||
title: '首页'
|
children: [
|
||||||
}
|
{
|
||||||
|
path: 'aericleattribute',
|
||||||
|
name: 'homeByAttribute',
|
||||||
|
component: HomePage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aericlecategory',
|
||||||
|
name: 'homeByCategory',
|
||||||
|
component: HomePage
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'aericletitle',
|
||||||
|
name: 'homeByTitle',
|
||||||
|
component: HomePage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aericlestatus',
|
||||||
|
name: 'homeByStatus',
|
||||||
|
component: HomePage
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/article-list',
|
path: '/articlelist',
|
||||||
name: 'articleList',
|
name: 'articleList',
|
||||||
component: ArticleList,
|
component: ArticleList,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -57,12 +81,28 @@ const routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/article/:url',
|
path: '/article',
|
||||||
name: 'articleContent',
|
name: 'articleContent',
|
||||||
component: ArticleContentPage,
|
component: ArticleContentPage,
|
||||||
meta: {
|
meta: {
|
||||||
title: '文章详情'
|
title: '文章详情'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: LoginPage,
|
||||||
|
meta: {
|
||||||
|
title: '登录'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/articlesave',
|
||||||
|
name: 'articlesave',
|
||||||
|
component: ArticleSavePage,
|
||||||
|
meta: {
|
||||||
|
title: '保存文章'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,80 @@
|
|||||||
// 基础 API 服务配置
|
// 基础API服务
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建axios实例
|
||||||
const apiService = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'http://localhost:8080/api', // api的base_url
|
baseURL: '/api', // API基础URL,使用相对路径,通过Vite代理转发
|
||||||
timeout: 10000, // 请求超时时间
|
timeout: 10000, // 请求超时时间
|
||||||
headers: {
|
withCredentials: true // 允许跨域请求携带凭证(如cookies)
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器 - 添加认证token
|
// 请求拦截器
|
||||||
apiService.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
const token = localStorage.getItem('token')
|
// 从localStorage获取token
|
||||||
|
let token = localStorage.getItem('token')
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
// 确保不重复添加Bearer前缀
|
||||||
|
if (!token.startsWith('Bearer ')) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
} else {
|
||||||
|
config.headers['Authorization'] = token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
// 请求错误处理
|
||||||
|
console.error('请求错误:', error)
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 响应拦截器 - 统一处理响应
|
// 响应拦截器
|
||||||
apiService.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
response => {
|
response => {
|
||||||
// 检查响应是否成功
|
// 直接返回响应数据
|
||||||
if (response.data && response.data.success) {
|
return response.data
|
||||||
return response.data
|
|
||||||
} else {
|
|
||||||
// 处理业务错误
|
|
||||||
return Promise.reject(new Error(response.data?.message || '请求失败'))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
// 处理HTTP错误
|
// 错误处理
|
||||||
console.error('API请求错误:', error)
|
console.error('响应错误:', error)
|
||||||
// 可以在这里添加全局错误处理,如显示错误提示
|
|
||||||
|
let message = '请求失败'
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
// 服务器响应了但状态码不是2xx
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 401:
|
||||||
|
message = '未授权,请重新登录'
|
||||||
|
// 可以在这里处理登出逻辑
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
// 跳转到登录页
|
||||||
|
window.location.href = '/login'
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message = '拒绝访问'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message = '请求资源不存在'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message = '服务器错误'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message = error.response.data?.message || message
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发出但没有收到响应
|
||||||
|
message = '网络错误,请检查网络连接'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Element Plus的消息组件显示错误信息
|
||||||
|
ElMessage.error(message)
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export default apiService
|
export default api
|
||||||
@@ -1,75 +1,97 @@
|
|||||||
// 文章相关API服务
|
// 文章相关API服务
|
||||||
import apiService from './apiService'
|
import api from './apiService'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文章服务类
|
* 文章服务类
|
||||||
*/
|
*/
|
||||||
class ArticleService {
|
class ArticleService {
|
||||||
/**
|
/**
|
||||||
* 获取所有文章
|
* 分页查询文章列表
|
||||||
* @param {Object} params - 查询参数
|
* @param {import('../types').PaginationParams} params - 分页查询参数
|
||||||
* @returns {Promise}
|
* @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[]>>}
|
||||||
*/
|
*/
|
||||||
getAllArticles(params = {}) {
|
getAllArticles(params = {}) {
|
||||||
return apiService.get('/articles', { params })
|
return api.get('/articles/published', { params })
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
/**
|
* 获取所有文章列表(包含已删除)
|
||||||
* 获取单篇文章
|
* @param {import('../types').PaginationParams} params - 查询参数
|
||||||
* @param {number} id - 文章ID
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
*/
|
||||||
getArticleById(id) {
|
getAllArticlesWithDeleted(params = {}) {
|
||||||
return apiService.get(`/articles/${id}`)
|
return api.get('/articles', { params })
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据属性ID获取文章列表
|
||||||
|
* @param {number} attributeid - 属性ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||||
|
*/
|
||||||
|
getArticlesByAttributeId(attributeid) {
|
||||||
|
return api.get(`/articles/attribute/${attributeid}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取热门文章
|
* 获取热门文章
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
|
||||||
*/
|
*/
|
||||||
getPopularArticles() {
|
getPopularArticles() {
|
||||||
return apiService.get('/articles/popular')
|
return api.get('/articles/popular')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建文章
|
* 创建文章
|
||||||
* @param {Object} articleData - 文章数据
|
* @param {import('../types').ArticleDto} articleData - 文章数据
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
|
||||||
*/
|
*/
|
||||||
createArticle(articleData) {
|
createArticle(articleData) {
|
||||||
return apiService.post('/articles', articleData)
|
return api.post('/articles', articleData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新文章
|
* 更新文章
|
||||||
* @param {number} id - 文章ID
|
* @param {number} articleid - 文章ID
|
||||||
* @param {Object} articleData - 文章数据
|
* @param {import('../types').ArticleDto} articleData - 文章数据
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
|
||||||
*/
|
*/
|
||||||
updateArticle(id, articleData) {
|
updateArticle(articleid, articleData) {
|
||||||
return apiService.put(`/articles/${id}`, articleData)
|
return api.put(`/articles/${articleid}`, articleData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除文章
|
* 删除文章
|
||||||
* @param {number} id - 文章ID
|
* @param {number} articleid - 文章ID
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||||
*/
|
*/
|
||||||
deleteArticle(id) {
|
deleteArticle(articleid) {
|
||||||
return apiService.delete(`/articles/${id}`)
|
return api.delete(`/articles/${articleid}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增加文章浏览量
|
* 增加文章浏览量
|
||||||
* @param {number} id - 文章ID
|
* @param {number} articleid - 文章ID
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||||
*/
|
*/
|
||||||
incrementArticleViews(id) {
|
incrementArticleViews(articleid) {
|
||||||
return apiService.post(`/articles/${id}/views`)
|
return api.post(`/articles/view/${articleid}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出文章服务实例
|
// 创建并导出服务实例
|
||||||
export default new ArticleService()
|
const articleService = new ArticleService()
|
||||||
|
export default articleService
|
||||||
|
|
||||||
|
// 导出服务类供特殊场景使用
|
||||||
|
export { ArticleService }
|
||||||
49
src/services/categoryAttributeService.js
Normal file
49
src/services/categoryAttributeService.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 分类属性相关API服务
|
||||||
|
import api from './apiService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类属性服务类
|
||||||
|
*/
|
||||||
|
class CategoryAttributeService {
|
||||||
|
/**
|
||||||
|
* 获取所有分类属性
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute[]>>}
|
||||||
|
*/
|
||||||
|
getAllAttributes() {
|
||||||
|
return api.get('/category-attributes')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取分类属性
|
||||||
|
* @param {number} attributeid - 属性ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
|
||||||
|
*/
|
||||||
|
getAttributeById(attributeid) {
|
||||||
|
return api.get(`/category-attributes/${attributeid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据分类ID获取属性列表
|
||||||
|
* @param {number} categoryid - 分类ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute[]>>}
|
||||||
|
*/
|
||||||
|
getAttributesByCategory(categoryid) {
|
||||||
|
return api.get(`/category-attributes/category/${categoryid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类属性
|
||||||
|
* @param {import('../types').CategoryAttributeDto} attributeData - 属性数据
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
|
||||||
|
*/
|
||||||
|
createAttribute(attributeData) {
|
||||||
|
return api.post('/category-attributes', attributeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建并导出服务实例
|
||||||
|
const categoryAttributeService = new CategoryAttributeService()
|
||||||
|
export default categoryAttributeService
|
||||||
|
|
||||||
|
// 导出服务类供特殊场景使用
|
||||||
|
export { CategoryAttributeService }
|
||||||
21
src/services/categoryService.js
Normal file
21
src/services/categoryService.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import api from './apiService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类服务
|
||||||
|
*/
|
||||||
|
class CategoryService {
|
||||||
|
/**
|
||||||
|
* 获取所有分类
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Category[]>>}
|
||||||
|
*/
|
||||||
|
getAllCategories() {
|
||||||
|
return api.get('/categories')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建并导出服务实例
|
||||||
|
const categoryService = new CategoryService()
|
||||||
|
export default categoryService
|
||||||
|
|
||||||
|
// 导出服务类供特殊场景使用
|
||||||
|
export { CategoryService }
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
// 导出所有服务
|
// 导出所有服务
|
||||||
import articleService from './articleService'
|
import articleService from './articleService'
|
||||||
|
import categoryService from './categoryService'
|
||||||
|
import categoryAttributeService from './categoryAttributeService'
|
||||||
|
import loginService from './loginService'
|
||||||
import messageService from './messageService'
|
import messageService from './messageService'
|
||||||
|
import nonsenseService from './nonsenseService'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 导出服务类供特殊场景使用
|
||||||
export {
|
export {
|
||||||
articleService,
|
articleService,
|
||||||
messageService
|
categoryService,
|
||||||
|
categoryAttributeService,
|
||||||
|
loginService,
|
||||||
|
messageService,
|
||||||
|
nonsenseService
|
||||||
}
|
}
|
||||||
24
src/services/loginService.js
Normal file
24
src/services/loginService.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// 登录相关API服务
|
||||||
|
import api from "./apiService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录服务类
|
||||||
|
*/
|
||||||
|
class LoginService {
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param {import('../types').LoginDto} loginData - 登录数据
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
|
||||||
|
*/
|
||||||
|
login(loginData) {
|
||||||
|
return api.post("/auth/login", loginData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
logout() {
|
||||||
|
return api.post("/auth/logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new LoginService();
|
||||||
@@ -5,71 +5,50 @@ import apiService from './apiService'
|
|||||||
* 留言服务类
|
* 留言服务类
|
||||||
*/
|
*/
|
||||||
class MessageService {
|
class MessageService {
|
||||||
|
/**
|
||||||
|
* 获取留言数量
|
||||||
|
* @param {number} articleid - 文章ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<number>>}
|
||||||
|
*/
|
||||||
|
getMessageCountByArticleId(articleid) {
|
||||||
|
return apiService.get(`/messages/count?articleid=${articleid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分页留言
|
||||||
|
* @param {number} articleid - 文章ID
|
||||||
|
* @param {number} pagenum - 页码
|
||||||
|
* @param {number} pagesize - 每页数量
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||||
|
*/
|
||||||
|
getMessagesByArticleId(articleid) {
|
||||||
|
return apiService.get(`/messages/article/${articleid}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有留言
|
* 获取所有留言
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
|
||||||
*/
|
*/
|
||||||
getAllMessages() {
|
getAllMessages() {
|
||||||
return apiService.get('/messages')
|
return apiService.get('/messages')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单条留言
|
|
||||||
* @param {number} id - 留言ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
getMessageById(id) {
|
|
||||||
return apiService.get(`/messages/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据文章ID获取留言
|
|
||||||
* @param {number} articleId - 文章ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
getMessagesByArticleId(articleId) {
|
|
||||||
return apiService.get(`/messages/article/${articleId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取根留言
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
getRootMessages() {
|
|
||||||
return apiService.get('/messages/root')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据父留言ID获取回复
|
|
||||||
* @param {number} parentId - 父留言ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
getRepliesByParentId(parentId) {
|
|
||||||
return apiService.get(`/messages/parent/${parentId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据昵称搜索留言
|
|
||||||
* @param {string} nickname - 昵称
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
searchMessagesByNickname(nickname) {
|
|
||||||
return apiService.get(`/messages/search?nickname=${nickname}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文章评论数量
|
|
||||||
* @param {number} articleId - 文章ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
getMessageCountByArticleId(articleId) {
|
|
||||||
return apiService.get(`/messages/count/${articleId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建留言
|
* 创建留言
|
||||||
* @param {Object} messageData - 留言数据
|
* @param {import('../types').MessageDto} messageData - 留言数据
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Message>>}
|
||||||
*/
|
*/
|
||||||
saveMessage(messageData) {
|
saveMessage(messageData) {
|
||||||
return apiService.post('/messages', messageData)
|
return apiService.post('/messages', messageData)
|
||||||
@@ -77,11 +56,20 @@ class MessageService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除留言
|
* 删除留言
|
||||||
* @param {number} id - 留言ID
|
* @param {number} messageid - 留言ID
|
||||||
* @returns {Promise}
|
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||||
*/
|
*/
|
||||||
deleteMessage(id) {
|
deleteMessage(messageid) {
|
||||||
return apiService.delete(`/messages/${id}`)
|
return apiService.delete(`/messages/${messageid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点赞留言
|
||||||
|
* @param {number} messageid - 留言ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||||
|
*/
|
||||||
|
likeMessage(messageid) {
|
||||||
|
return apiService.post(`/messages/${messageid}/like`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
src/services/nonsenseService.js
Normal file
45
src/services/nonsenseService.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 疯言疯语相关API服务
|
||||||
|
import apiService from './apiService'
|
||||||
|
|
||||||
|
class NonsenseService {
|
||||||
|
/**
|
||||||
|
* 获取所有疯言疯语内容
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
|
||||||
|
*/
|
||||||
|
getAllNonsense() {
|
||||||
|
return apiService.get('/nonsense')
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据状态获取疯言疯语内容
|
||||||
|
* @param {number} status - 状态值(1:已发表, 0:草稿)
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
|
||||||
|
*/
|
||||||
|
getNonsenseByStatus(status){
|
||||||
|
return apiService.get(`/nonsense/status/${status}`)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 保存疯言疯语内容
|
||||||
|
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
|
||||||
|
*/
|
||||||
|
createNonsense(nonsense){
|
||||||
|
return apiService.post('/nonsense', nonsense)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除疯言疯语内容
|
||||||
|
* @param {number} id - 疯言疯语内容ID
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||||
|
*/
|
||||||
|
deleteNonsense(id){
|
||||||
|
return apiService.delete(`/nonsense/${id}`)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新疯言疯语内容
|
||||||
|
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
|
||||||
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
|
||||||
|
*/
|
||||||
|
updateNonsense(nonsense){
|
||||||
|
return apiService.put('/nonsense', nonsense)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new NonsenseService()
|
||||||
189
src/store/globalStore.js
Normal file
189
src/store/globalStore.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import Login from '@/views/login.vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局状态管理store
|
||||||
|
* 提供全局的传值和获取值的功能
|
||||||
|
* 任何页面都可以调用和获取其中的值
|
||||||
|
* 添加了localStorage持久化功能,确保刷新页面后数据不会丢失
|
||||||
|
*/
|
||||||
|
export const useGlobalStore = defineStore('global', {
|
||||||
|
// 状态定义
|
||||||
|
state: () => {
|
||||||
|
// 从localStorage读取持久化的数据
|
||||||
|
const savedGlobalData = localStorage.getItem('globalStoreData')
|
||||||
|
const initialGlobalData = savedGlobalData ? JSON.parse(savedGlobalData) : {}
|
||||||
|
|
||||||
|
// 从localStorage读取特定状态的持久化数据
|
||||||
|
const savedSpecificData = localStorage.getItem('globalStoreSpecificData')
|
||||||
|
const initialSpecificData = savedSpecificData ? JSON.parse(savedSpecificData) : {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 全局数据对象,存储所有需要共享的数据
|
||||||
|
globalData: initialGlobalData,
|
||||||
|
// 特定状态属性,从localStorage读取初始值
|
||||||
|
username: initialSpecificData.username || null,
|
||||||
|
Login: initialSpecificData.Login || false,
|
||||||
|
notifications: initialSpecificData.notifications || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算属性 - 用于获取和转换状态
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* 检查全局数据中是否存在指定的键
|
||||||
|
* @param {string} key - 要检查的键名
|
||||||
|
* @returns {boolean} - 如果存在返回true,否则返回false
|
||||||
|
*/
|
||||||
|
hasValue: (state) => (key) => {
|
||||||
|
return Object.prototype.hasOwnProperty.call(state.globalData, key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定键的值
|
||||||
|
* @param {string} key - 要获取的键名
|
||||||
|
* @returns {any} - 对应的值,如果不存在则返回undefined
|
||||||
|
*/
|
||||||
|
getValue: (state) => (key) => {
|
||||||
|
return state.globalData[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有全局数据
|
||||||
|
* @returns {Object} - 所有全局数据
|
||||||
|
*/
|
||||||
|
getAllData: (state) => {
|
||||||
|
return { ...state.globalData }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 操作方法 - 用于修改状态
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 设置全局数据
|
||||||
|
* @param {string} key - 键名
|
||||||
|
* @param {any} value - 要存储的值
|
||||||
|
*/
|
||||||
|
setValue(key, value) {
|
||||||
|
this.globalData[key] = value
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据持久化到localStorage的内部方法
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_persistData() {
|
||||||
|
try {
|
||||||
|
// 持久化globalData
|
||||||
|
localStorage.setItem('globalStoreData', JSON.stringify(this.globalData))
|
||||||
|
// 持久化特定状态属性
|
||||||
|
localStorage.setItem('globalStoreSpecificData', JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
Login: this.Login,
|
||||||
|
notifications: this.notifications
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist data to localStorage:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置多个键值对
|
||||||
|
* @param {Object} data - 包含多个键值对的对象
|
||||||
|
*/
|
||||||
|
setMultipleValues(data) {
|
||||||
|
if (typeof data === 'object' && data !== null) {
|
||||||
|
Object.assign(this.globalData, data)
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的全局数据
|
||||||
|
* @param {string} key - 要删除的键名
|
||||||
|
*/
|
||||||
|
removeValue(key) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.globalData, key)) {
|
||||||
|
delete this.globalData[key]
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有全局数据
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
this.globalData = {}
|
||||||
|
this.username = null
|
||||||
|
this.Login = false
|
||||||
|
this.notifications = []
|
||||||
|
// 清除localStorage中的数据
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('globalStoreData')
|
||||||
|
localStorage.removeItem('globalStoreSpecificData')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear data from localStorage:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户信息
|
||||||
|
* @param {Object} userInfo - 用户信息对象
|
||||||
|
*/
|
||||||
|
setUsername(username) {
|
||||||
|
this.username = username
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置加载状态
|
||||||
|
* @param {boolean} status - 加载状态
|
||||||
|
*/
|
||||||
|
setLoading(status) {
|
||||||
|
this.loading = status
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加通知
|
||||||
|
* @param {Object} notification - 通知对象
|
||||||
|
*/
|
||||||
|
addNotification(notification) {
|
||||||
|
this.notifications.push({
|
||||||
|
id: Date.now(),
|
||||||
|
...notification
|
||||||
|
})
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除通知
|
||||||
|
* @param {number} id - 通知ID
|
||||||
|
*/
|
||||||
|
removeNotification(id) {
|
||||||
|
const index = this.notifications.findIndex(notification => notification.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.notifications.splice(index, 1)
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录状态
|
||||||
|
* @param {boolean} status - 登录状态
|
||||||
|
*/
|
||||||
|
setLoginStatus(status) {
|
||||||
|
this.Login = status
|
||||||
|
// 持久化到localStorage
|
||||||
|
this._persistData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
208
src/store/useExample.md
Normal file
208
src/store/useExample.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Pinia 全局状态管理使用示例
|
||||||
|
|
||||||
|
## 1. 基础使用方法
|
||||||
|
|
||||||
|
### 在任意组件中使用全局状态
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>全局状态管理示例</h2>
|
||||||
|
|
||||||
|
<!-- 显示全局数据 -->
|
||||||
|
<div v-if="globalStore.hasValue('exampleData')">
|
||||||
|
<p>从全局store获取的数据: {{ globalStore.getValue('exampleData') }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>全局数据尚未设置</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置全局数据 -->
|
||||||
|
<el-input v-model="inputValue" placeholder="输入要存储的全局数据" />
|
||||||
|
<el-button @click="setGlobalData">设置全局数据</el-button>
|
||||||
|
<el-button @click="checkGlobalData">检查是否存在</el-button>
|
||||||
|
<el-button @click="clearGlobalData" type="danger">清空全局数据</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
|
||||||
|
// 获取全局store实例
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const inputValue = ref('')
|
||||||
|
|
||||||
|
// 设置全局数据
|
||||||
|
const setGlobalData = () => {
|
||||||
|
globalStore.setValue('exampleData', inputValue.value)
|
||||||
|
ElMessage.success('数据已保存到全局状态')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据是否存在
|
||||||
|
const checkGlobalData = () => {
|
||||||
|
if (globalStore.hasValue('exampleData')) {
|
||||||
|
ElMessage.info(`数据存在: ${globalStore.getValue('exampleData')}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('数据不存在')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空全局数据
|
||||||
|
const clearGlobalData = () => {
|
||||||
|
globalStore.removeValue('exampleData')
|
||||||
|
ElMessage.success('数据已从全局状态中移除')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 页面间数据传递示例
|
||||||
|
|
||||||
|
### 页面A - 设置数据
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const navigateToPageB = () => {
|
||||||
|
// 存储页面间需要传递的数据
|
||||||
|
globalStore.setValue('sharedData', {
|
||||||
|
id: 123,
|
||||||
|
name: '测试数据',
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转到页面B
|
||||||
|
router.push('/page-b')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面B - 获取数据
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查并获取从页面A传递过来的数据
|
||||||
|
if (globalStore.hasValue('sharedData')) {
|
||||||
|
const data = globalStore.getValue('sharedData')
|
||||||
|
console.log('从页面A接收的数据:', data)
|
||||||
|
// 使用接收到的数据进行操作
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 批量操作示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
|
||||||
|
// 批量设置多个键值对
|
||||||
|
globalStore.setMultipleValues({
|
||||||
|
appConfig: {
|
||||||
|
theme: 'dark',
|
||||||
|
language: 'zh-CN'
|
||||||
|
},
|
||||||
|
userPreferences: {
|
||||||
|
notifications: true,
|
||||||
|
fontSize: 16
|
||||||
|
},
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取所有全局数据
|
||||||
|
const allData = globalStore.getAllData()
|
||||||
|
console.log('所有全局数据:', allData)
|
||||||
|
|
||||||
|
// 清空所有数据
|
||||||
|
globalStore.clearAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 用户状态管理示例
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="globalStore.user">
|
||||||
|
<h3>欢迎回来,{{ globalStore.user.username }}</h3>
|
||||||
|
<el-button @click="logout">登出</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h3>请登录</h3>
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态指示器 -->
|
||||||
|
<el-loading v-loading="globalStore.loading" :text="'加载中...'">
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
</el-loading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
|
||||||
|
const login = async (credentials) => {
|
||||||
|
try {
|
||||||
|
// 设置加载状态
|
||||||
|
globalStore.setLoading(true)
|
||||||
|
|
||||||
|
// 模拟登录请求
|
||||||
|
const response = await authService.login(credentials)
|
||||||
|
|
||||||
|
// 保存用户信息到全局状态
|
||||||
|
globalStore.setUser(response.user)
|
||||||
|
|
||||||
|
// 添加登录成功通知
|
||||||
|
globalStore.addNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: '登录成功!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
} finally {
|
||||||
|
// 无论成功失败都清除加载状态
|
||||||
|
globalStore.setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
// 清除用户信息
|
||||||
|
globalStore.setUser(null)
|
||||||
|
|
||||||
|
// 添加登出通知
|
||||||
|
globalStore.addNotification({
|
||||||
|
type: 'info',
|
||||||
|
message: '您已成功登出'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 注意事项
|
||||||
|
|
||||||
|
1. **数据持久化**:默认情况下,Pinia状态只在内存中存在,页面刷新后会丢失。如果需要持久化,可以考虑:
|
||||||
|
- 使用localStorage/sessionStorage结合Pinia插件
|
||||||
|
- 使用Cookie存储关键信息
|
||||||
|
|
||||||
|
2. **性能考虑**:避免在全局状态中存储过大的数据,这可能会影响应用性能
|
||||||
|
|
||||||
|
3. **类型安全**:如果使用TypeScript,可以为store添加类型定义
|
||||||
|
|
||||||
|
4. **模块化**:随着应用复杂度增加,可以考虑将不同功能的数据分开存储到多个store中
|
||||||
|
|
||||||
|
5. **调试**:Pinia提供了良好的开发工具支持,可以通过Vue Devtools进行调试
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
|
/* 汉仪唐韵字体声明 */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'HanTang';
|
||||||
|
src: url('/fonts/HanTang.woff2') format('woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
/* 全局字体设置 */
|
||||||
|
--main-font-family: 'HanTang', sans-serif;
|
||||||
/* 页面通用间距和圆角 */
|
/* 页面通用间距和圆角 */
|
||||||
--main-padding: 100px 10%;
|
--main-padding: 8px 10%;
|
||||||
/* 内容区内边距 */
|
/* 内容区内边距 */
|
||||||
--main-radius: 10px;
|
--main-radius: 10px;
|
||||||
/* 内容区圆角 */
|
/* 内容区圆角 */
|
||||||
|
|
||||||
/* 首页 hero 区域高度和间距 */
|
/* 首页 hero 区域高度和间距 */
|
||||||
--hero-height: 600px;
|
--hero-height: 100px;
|
||||||
/* hero 默认高度 */
|
/* hero 默认高度 */
|
||||||
--hero-height-small: 150px;
|
--hero-height-small: 100px;
|
||||||
/* hero 收缩后高度 */
|
/* hero 收缩后高度 */
|
||||||
--hero-margin-top: 5%;
|
--hero-margin-top-small: 6%;
|
||||||
/* hero 顶部外边距 */
|
/* hero 收缩后顶部外边距 */
|
||||||
|
|
||||||
/* 标题样式 */
|
/* 标题样式 */
|
||||||
--title-font-size: 3.5rem;
|
--title-font-size: 3.5rem;
|
||||||
/* hero 主标题字号 */
|
/* hero 主标题字号 */
|
||||||
@@ -22,7 +32,7 @@
|
|||||||
/* 内容区宽度和外边距 */
|
/* 内容区宽度和外边距 */
|
||||||
--content-width: 80%;
|
--content-width: 80%;
|
||||||
/* 主内容区宽度 */
|
/* 主内容区宽度 */
|
||||||
--content-margin: 0 5%;
|
--content-margin: 0 5% 0 5%;
|
||||||
/* 主内容区左右外边距 */
|
/* 主内容区左右外边距 */
|
||||||
|
|
||||||
/* 左侧状态栏宽度 */
|
/* 左侧状态栏宽度 */
|
||||||
@@ -30,7 +40,7 @@
|
|||||||
/* 左侧模块宽度 */
|
/* 左侧模块宽度 */
|
||||||
|
|
||||||
/* 疯言疯语标题区样式 */
|
/* 疯言疯语标题区样式 */
|
||||||
--nonsense-title-width: 76.5%;
|
--nonsense-title-width: 77.1%;
|
||||||
/* 疯言疯语标题区宽度 */
|
/* 疯言疯语标题区宽度 */
|
||||||
--nonsense-title-padding: 20px;
|
--nonsense-title-padding: 20px;
|
||||||
/* 疯言疯语标题区内边距 */
|
/* 疯言疯语标题区内边距 */
|
||||||
@@ -40,7 +50,7 @@
|
|||||||
/* 疯言疯语标题区下边距 */
|
/* 疯言疯语标题区下边距 */
|
||||||
--nonsense-titleconst-padding: 1.5rem;
|
--nonsense-titleconst-padding: 1.5rem;
|
||||||
/* 疯言疯语标题内容区内边距 */
|
/* 疯言疯语标题内容区内边距 */
|
||||||
--nonsenset-margin-top: 130px;
|
--nonsenset-margin-top: 150px;
|
||||||
/* 疯言疯语内容区顶部外边距 */
|
/* 疯言疯语内容区顶部外边距 */
|
||||||
|
|
||||||
/* 分页背景色 */
|
/* 分页背景色 */
|
||||||
@@ -48,11 +58,11 @@
|
|||||||
/* 分页区背景色 */
|
/* 分页区背景色 */
|
||||||
|
|
||||||
/* 导航栏样式 */
|
/* 导航栏样式 */
|
||||||
--nav-padding: 20px 40px;
|
--nav-padding: 15px 35px;
|
||||||
/* 导航栏默认内边距 */
|
/* 导航栏默认内边距 */
|
||||||
--nav-padding-small: 15px 40px;
|
--nav-padding-small: 10px 25px;
|
||||||
/* 导航栏收缩后内边距 */
|
/* 导航栏收缩后内边距 */
|
||||||
--nav-bg: rgba(145, 196, 238, 0.85);
|
--nav-bg: rgba(145, 196, 238, 0.95);
|
||||||
/* 导航栏背景色 */
|
/* 导航栏背景色 */
|
||||||
--nav-bg-transparent: transparent;
|
--nav-bg-transparent: transparent;
|
||||||
/* 导航栏透明背景 */
|
/* 导航栏透明背景 */
|
||||||
@@ -60,20 +70,23 @@
|
|||||||
/* 导航栏隐藏时背景 */
|
/* 导航栏隐藏时背景 */
|
||||||
--nav-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
--nav-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
/* 导航栏阴影 */
|
/* 导航栏阴影 */
|
||||||
|
--nav-is-active: rgba(255, 0, 255, 0.75);
|
||||||
|
/* 导航栏激活项字体颜色 */
|
||||||
/* 字体颜色和菜单样式 */
|
/* 字体颜色和菜单样式 */
|
||||||
--font-color-title: #32325d;
|
--font-color-title: #AF7AC5;
|
||||||
/* 标题字体颜色 */
|
/* 标题字体颜色 */
|
||||||
--font-size-menu: 20px;
|
--font-size-menu: 20px;
|
||||||
/* 菜单字体大小 */
|
/* 菜单字体大小 */
|
||||||
--font-weight-menu: bold;
|
--font-weight-menu: bold;
|
||||||
/* 菜单字体加粗 */
|
/* 菜单字体加粗 */
|
||||||
--body-background-img: url('../img/8.21.1.jpg');
|
--body-background-img: url('../img/bg.jpg');
|
||||||
/* 页面背景图片 */
|
/* 页面背景图片 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面背景设置 */
|
/* 页面背景设置 */
|
||||||
body {
|
body {
|
||||||
|
font-family: var(--main-font-family);
|
||||||
|
/* 设置全局字体 */
|
||||||
background-image: var(--body-background-img);
|
background-image: var(--body-background-img);
|
||||||
/* 背景图片 */
|
/* 背景图片 */
|
||||||
background-size: 120% 120%;
|
background-size: 120% 120%;
|
||||||
@@ -85,7 +98,17 @@ body {
|
|||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
/* 背景固定 */
|
/* 背景固定 */
|
||||||
}
|
}
|
||||||
|
/* a 标签样式 */
|
||||||
|
a {
|
||||||
|
font-family: var(--main-font-family);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: default;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
p, li ,ul,ol,dl,dt,dd,span,strong,em,code,pre,input,textarea,select,option,label,th,td,tr,tbody,td,th,div{
|
||||||
|
font-family: var(--main-font-family);
|
||||||
|
}
|
||||||
/* 标题通用样式 */
|
/* 标题通用样式 */
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
@@ -93,10 +116,9 @@ h3,
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
font-family: inherit;
|
font-family: var(--main-font-family);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: .5rem;
|
|
||||||
color: var(--font-color-title);
|
color: var(--font-color-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +128,90 @@ p {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
/* 编辑按钮样式 */
|
||||||
|
.edit-button{
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button:hover {
|
||||||
|
color: rgb(247, 243, 2);
|
||||||
|
background-color: rgba(231, 205, 60, 0.3);
|
||||||
|
}
|
||||||
|
/* 删除按钮样式 */
|
||||||
|
.delete-button {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button:hover {
|
||||||
|
color: rgb(231, 76, 60);
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
|
}
|
||||||
|
/* 文章span 样式 */
|
||||||
|
|
||||||
|
/* 发布日期样式 */
|
||||||
|
.article-publish-date {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阅读数量样式 */
|
||||||
|
.article-views-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-views-count::before {
|
||||||
|
content: '|';
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类标签样式 */
|
||||||
|
.article-category-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 10px;
|
||||||
|
background-color: rgba(52, 152, 219, 0.1);
|
||||||
|
color: #3498db;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点赞数量样式 */
|
||||||
|
.article-likes-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-likes-count::before {
|
||||||
|
content: '|';
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 评论数量样式 */
|
||||||
|
.article-comments-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-comments-count::before {
|
||||||
|
content: '|';
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 顶部导航栏样式 */
|
/* 顶部导航栏样式 */
|
||||||
.elrow-top {
|
.elrow-top {
|
||||||
@@ -115,9 +221,8 @@ p {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--nav-padding);
|
padding: var(--nav-padding);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
transition: all 0.4s ease;
|
transition: all 1s ease;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
/* display: flex; */
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -129,10 +234,10 @@ p {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: 'Microsoft YaHei', 'Ma Shan Zheng', cursive;
|
font-family: 'HanTang', sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: linear-gradient(94.75deg, rgb(60, 172, 247) 0%, rgb(131, 101, 253) 43.66%, rgb(255, 141, 112) 64.23%, rgb(247, 201, 102) 83.76%, rgb(172, 143, 100) 100%);
|
background: linear-gradient(94.75deg, rgb(60, 172, 247) 0%, rgb(131, 101, 253) 43.66%, rgb(255, 141, 112) 64.23%, rgb(247, 201, 102) 83.76%, rgb(172, 143, 100) 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
@@ -172,19 +277,28 @@ p {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
font-weight: var(--font-weight-menu);
|
font-weight: var(--font-weight-menu);
|
||||||
font-size: var(--font-size-menu);
|
font-size: var(--font-size-menu);
|
||||||
left: 18%;
|
/* left: 18%; */
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-demo .el-menu-item {
|
.el-menu-demo .el-menu-item {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
color: rgba(255, 255, 255, .95);
|
||||||
|
}
|
||||||
|
.el-menu-demo .el-menu-item.is-active{
|
||||||
|
color: var(--nav-is-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 菜单项悬停和激活状态 */
|
/* 菜单项悬停和激活状态 */
|
||||||
.el-menu-item:hover,
|
.el-menu-demo .el-menu-item:hover{
|
||||||
.el-menu-item.is-active {
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
color: rgba(255, 200, 255, 0.5);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.el-menu-demo .el-menu-item.is-active {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--nav-is-active);
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 去除菜单项悬停和激活时的背景色 */
|
/* 去除菜单项悬停和激活时的背景色 */
|
||||||
@@ -232,32 +346,45 @@ p {
|
|||||||
margin: var(--content-margin);
|
margin: var(--content-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.RouterViewpage.forbidwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
/* 分页区样式 */
|
/* 分页区样式 */
|
||||||
.Pagination {
|
.Pagination {
|
||||||
align-self: center;
|
margin-bottom: 30px;
|
||||||
background-color: var(--pagination-bg);
|
}
|
||||||
|
.Pagination .el-pagination {
|
||||||
|
/* 水平垂直居中 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧状态栏样式 */
|
/* 左侧状态栏样式 */
|
||||||
.leftmodluepage {
|
.leftmodluecontainer {
|
||||||
width: var(--leftmodlue-width);
|
width: var(--leftmodlue-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 首页 hero 区域样式 */
|
/* 首页 hero 区域样式 */
|
||||||
.hero {
|
.hero {
|
||||||
height: var(--hero-height);
|
height: var(--hero-height);
|
||||||
margin-top: var(--hero-margin-top);
|
margin: var(--hero-margin);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
transition: height 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* hero 收缩状态 */
|
/* hero 收缩状态 */
|
||||||
.hero.newhero {
|
.hero.small-hero {
|
||||||
height: var(--hero-height-small);
|
height: var(--hero-height-small);
|
||||||
|
margin-top: var(--hero-margin-top-small);
|
||||||
|
/* 去除 hero 收缩状态下的position */
|
||||||
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 打字机效果 */
|
/* 打字机效果 */
|
||||||
@@ -265,6 +392,10 @@ p {
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
min-height: 2.5em;
|
min-height: 2.5em;
|
||||||
font-size: var(--title-font-size, 2rem);
|
font-size: var(--title-font-size, 2rem);
|
||||||
|
/* 水平居中 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
@@ -274,10 +405,6 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* hero 主标题样式 */
|
/* hero 主标题样式 */
|
||||||
.hero h1 {
|
|
||||||
font-size: var(--title-font-size);
|
|
||||||
margin-bottom: var(--title-margin-bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* hero 副标题样式 */
|
/* hero 副标题样式 */
|
||||||
.hero p {
|
.hero p {
|
||||||
@@ -327,8 +454,7 @@ p {
|
|||||||
--nonsense-title-margin-bottom: 15px;
|
--nonsense-title-margin-bottom: 15px;
|
||||||
--nav-padding-small: 0 8px;
|
--nav-padding-small: 0 8px;
|
||||||
--nonsenset-margin-top: 10px;
|
--nonsenset-margin-top: 10px;
|
||||||
--body-background-img: url();
|
--body-background-img: url(../img/bg.jpg);
|
||||||
/* 移动端不显示背景图片 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.elrow-top {
|
.elrow-top {
|
||||||
@@ -378,6 +504,7 @@ p {
|
|||||||
|
|
||||||
.RouterViewpage {
|
.RouterViewpage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: 11%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nonsensetitle {
|
.nonsensetitle {
|
||||||
@@ -386,4 +513,24 @@ p {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 1rem;
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,47 +4,149 @@
|
|||||||
* 文章类型接口
|
* 文章类型接口
|
||||||
*/
|
*/
|
||||||
export interface Article {
|
export interface Article {
|
||||||
id: number
|
articleid: number
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
author: string
|
attributeid: Number
|
||||||
createTime: string
|
categoryName: string
|
||||||
updateTime: string
|
img?: string
|
||||||
categoryId: number
|
createdAt: string
|
||||||
categoryName?: string
|
updatedAt: string
|
||||||
tags?: string
|
viewCount?: number
|
||||||
views?: number
|
likes?: number
|
||||||
commentCount?: number
|
commentCount?: number
|
||||||
articleid?: string
|
status?: number
|
||||||
publishedAt?: string
|
markdownscontent: string
|
||||||
mg?: string
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章DTO接口
|
||||||
|
*/
|
||||||
|
export interface ArticleDto {
|
||||||
|
id?: number
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
attributeid: number
|
||||||
|
img?: string
|
||||||
|
status?: number
|
||||||
|
viewCount?: number
|
||||||
|
likes?: number
|
||||||
|
markdownscontent: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 留言类型接口
|
* 留言类型接口
|
||||||
*/
|
*/
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id: number
|
messageid: number
|
||||||
content: string
|
content: string
|
||||||
nickname: string
|
nickname: string
|
||||||
email: string
|
email: string
|
||||||
articleId?: number
|
articleid?: number
|
||||||
parentId?: number
|
parentid?: number
|
||||||
createdAt: string
|
createdAt: string
|
||||||
replies?: Message[]
|
replyid?: number
|
||||||
time?: string
|
likes?: number
|
||||||
|
messageimg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 留言DTO接口
|
||||||
|
*/
|
||||||
|
export interface MessageDto {
|
||||||
|
messageid?: number
|
||||||
|
nickname?: string
|
||||||
|
email?: string
|
||||||
|
content?: string
|
||||||
|
createdAt?: string
|
||||||
|
parentid?: number
|
||||||
|
replyid?: number
|
||||||
|
articleid?: number
|
||||||
|
messageimg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分类类型接口
|
* 分类类型接口
|
||||||
*/
|
*/
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: number
|
Categoryid: number
|
||||||
name: string
|
typename: string
|
||||||
description?: string
|
description?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
articleCount?: number
|
articleCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类DTO接口
|
||||||
|
*/
|
||||||
|
export interface CategoryDto {
|
||||||
|
typename: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类属性接口
|
||||||
|
*/
|
||||||
|
export interface CategoryAttribute {
|
||||||
|
map(arg0: (item: any) => any): unknown
|
||||||
|
attributeid: number
|
||||||
|
categoryid: number
|
||||||
|
attributename: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类属性DTO接口
|
||||||
|
*/
|
||||||
|
export interface CategoryAttributeDto {
|
||||||
|
categoryid: number
|
||||||
|
attributename: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息接口
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id?: number
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
role?: number
|
||||||
|
createTime?: string
|
||||||
|
avatar?: string
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户DTO接口
|
||||||
|
*/
|
||||||
|
export interface UserDto {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
email: string
|
||||||
|
phone: string
|
||||||
|
role?: number
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 疯言疯语类型接口
|
||||||
|
*/
|
||||||
|
export interface Nonsense {
|
||||||
|
nonsenseid: number
|
||||||
|
content: string
|
||||||
|
status?: number
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 疯言疯语DTO接口
|
||||||
|
*/
|
||||||
|
export interface NonsenseDto {
|
||||||
|
content: string
|
||||||
|
status?: number
|
||||||
|
time?: string
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* API响应接口
|
* API响应接口
|
||||||
*/
|
*/
|
||||||
@@ -60,20 +162,7 @@ export interface ApiResponse<T = any> {
|
|||||||
* 分页参数接口
|
* 分页参数接口
|
||||||
*/
|
*/
|
||||||
export interface PaginationParams {
|
export interface PaginationParams {
|
||||||
page?: number
|
pagenum?: number
|
||||||
size?: number
|
pagesize?: number
|
||||||
keyword?: string
|
status?: number
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户信息接口
|
|
||||||
*/
|
|
||||||
export interface User {
|
|
||||||
id?: number
|
|
||||||
username?: string
|
|
||||||
email?: string
|
|
||||||
avatar?: string
|
|
||||||
role?: string
|
|
||||||
token?: string
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
// 日期格式化工具函数
|
// 日期格式化工具函数
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化日期为指定格式
|
* 格式化日期为指定格式,默认去掉秒,且三天内显示为今天、昨天、前天
|
||||||
* @param {string|Date} date - 日期对象或日期字符串
|
* @param {string|Date} date - 日期对象或日期字符串
|
||||||
* @param {string} format - 格式化模板,如 'YYYY-MM-DD HH:mm:ss'
|
* @param {string} format - 格式化模板,默认 'YYYY-MM-DD HH:mm'
|
||||||
* @returns {string} 格式化后的日期字符串
|
* @returns {string} 格式化后的日期字符串
|
||||||
*/
|
*/
|
||||||
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm') => {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
|
|
||||||
// 如果是字符串,转换为日期对象
|
// 如果是字符串,转换为日期对象
|
||||||
@@ -15,12 +15,19 @@ export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
|||||||
// 检查日期对象是否有效
|
// 检查日期对象是否有效
|
||||||
if (isNaN(dateObj.getTime())) return ''
|
if (isNaN(dateObj.getTime())) return ''
|
||||||
|
|
||||||
|
// 检查是否在三天内,如果是则返回相对时间
|
||||||
|
const relativeTime = getRelativeDay(dateObj)
|
||||||
|
if (relativeTime) {
|
||||||
|
const hours = String(dateObj.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
|
||||||
|
return `${relativeTime} ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
const year = dateObj.getFullYear()
|
const year = dateObj.getFullYear()
|
||||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0')
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0')
|
||||||
const day = String(dateObj.getDate()).padStart(2, '0')
|
const day = String(dateObj.getDate()).padStart(2, '0')
|
||||||
const hours = String(dateObj.getHours()).padStart(2, '0')
|
const hours = String(dateObj.getHours()).padStart(2, '0')
|
||||||
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
|
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
|
||||||
const seconds = String(dateObj.getSeconds()).padStart(2, '0')
|
|
||||||
|
|
||||||
// 替换模板中的日期部分
|
// 替换模板中的日期部分
|
||||||
return format
|
return format
|
||||||
@@ -29,7 +36,31 @@ export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
|||||||
.replace('DD', day)
|
.replace('DD', day)
|
||||||
.replace('HH', hours)
|
.replace('HH', hours)
|
||||||
.replace('mm', minutes)
|
.replace('mm', minutes)
|
||||||
.replace('ss', seconds)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取相对天数(今天、昨天、前天)
|
||||||
|
* @param {Date} dateObj - 日期对象
|
||||||
|
* @returns {string|null} 相对天数或null
|
||||||
|
*/
|
||||||
|
export const getRelativeDay = (dateObj) => {
|
||||||
|
const now = new Date()
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||||
|
const targetDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate())
|
||||||
|
|
||||||
|
// 计算天数差
|
||||||
|
const diffTime = today - targetDate
|
||||||
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (diffDays === 0) {
|
||||||
|
return '今天'
|
||||||
|
} else if (diffDays === 1) {
|
||||||
|
return '昨天'
|
||||||
|
} else if (diffDays === 2) {
|
||||||
|
return '前天'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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>
|
||||||
@@ -1,63 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="about-page-container">
|
||||||
<div class="about-wrapper">
|
<div class="about-content-wrapper">
|
||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="about-header">
|
<div class="about-page-header">
|
||||||
<h1 class="about-title">关于我</h1>
|
<h3 class="about-page-title">关于</h3>
|
||||||
<div class="about-subtitle">一个热爱技术的全栈开发者</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关于内容 -->
|
<!-- 关于内容 -->
|
||||||
<div class="about-content">
|
<div class="about-main-content">
|
||||||
<div class="about-intro">
|
<div class="about-personal-intro">
|
||||||
<p>你好!欢迎来到我的个人博客。我是一名热爱技术的全栈开发者,热衷于探索新技术和解决复杂问题。</p>
|
<h4>清风--清疯--清疯不颠</h4>
|
||||||
<p>这个博客是我分享技术见解、学习心得和生活感悟的地方。希望通过这个平台,能够与更多志同道合的朋友交流和学习。</p>
|
<h5>
|
||||||
|
<!-- “徐徐清风知我意,悠悠湖水荡人心。” -->
|
||||||
|
名字从“清风”起步,但总觉得少了点年轻人的疯狂,于是干脆改叫“清疯”。疯久了,又怕真“颠”了,便在后面补上“不颠”二字,作为提醒。“清疯不颠”就此定下——风与疯,只在一念之间;疯与不颠,则是我给自己的清醒界限。
|
||||||
|
<!-- 让我回忆回忆...大一的时候(还是上学的日子好哈哈哈哈哈!!)跟室友一块玩游戏,因为我的Steam名字叫“清风”,慢慢的这个名儿就这么成了我的外号。说来也怪,被他们这么一叫,心里那点初入大学的陌生和拘谨,
|
||||||
|
好像真被一阵风吹散了似的,
|
||||||
|
哈哈哈哈哈,我还蛮喜欢这个外号的。
|
||||||
|
<br></br>
|
||||||
|
有段时间,不知道怎么了,精神状态不是很好,好想发疯,突然这疯就像钉子一样扎在我脑海里(哈哈哈哈!)。于是就干脆改名叫“清疯”了——清风,清疯,念起来几乎没差,
|
||||||
|
但内核却从一种理想的淡然,切换成了真实的、带点毛边的鲜活。“悠悠清风荡我心”,只是如今这阵清风熬成了清疯,终于在我心里刮起一场疯——疯啦!
|
||||||
|
<br></br>
|
||||||
|
又过些日子,玩新游戏要起名,正盯着输入框里的“清疯”发呆, 两个字在脑海里冒出来……疯癫?哎,好像是疯了,但也没完全颠嘛!那种在理智边界试探、却绝不越线的微妙感,一下对味了——干脆就叫“清疯不颠”!
|
||||||
|
名字一出自己先乐了。
|
||||||
|
<br></br>
|
||||||
|
哈哈哈哈哈哈!俗话说天才在左疯子在右,在我这儿,大概是左脑负责疯,右脑负责颠,两个家伙吵吵闹闹,反而让我在这个世界里自得其乐。 -->
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-skills">
|
<div class="about-personal-intro">
|
||||||
<h3>前端技术栈</h3>
|
<h4>疯言疯语</h4>
|
||||||
<div class="skills-list">
|
<h5>
|
||||||
<el-tag type="primary">HTML5</el-tag>
|
我想设计一下独属于清疯的页面,可是我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||||
<el-tag type="primary">CSS3</el-tag>
|
</h5>
|
||||||
<el-tag type="primary">JavaScript</el-tag>
|
</div>
|
||||||
<el-tag type="primary">TypeScript</el-tag>
|
|
||||||
<el-tag type="primary">Vue.js</el-tag>
|
<div class="about-skill-section">
|
||||||
<el-tag type="primary">React</el-tag>
|
<h4>前端技术栈</h4>
|
||||||
<el-tag type="primary">Node.js</el-tag>
|
<div class="skills-display-list">
|
||||||
<el-tag type="primary">Webpack</el-tag>
|
|
||||||
<el-tag type="primary">Git</el-tag>
|
<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">HTML5</el-tag></a>
|
||||||
|
<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">CSS3</el-tag></a>
|
||||||
|
<a href="https://tailwindcss.com/" target="_blank" class="skill-tag-link"><el-tag type="primary">Tailwind
|
||||||
|
CSS</el-tag></a>
|
||||||
|
<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript" target="_blank"
|
||||||
|
class="skill-tag-link"><el-tag type="primary">JavaScript (ES6+)</el-tag></a>
|
||||||
|
<a href="https://www.typescriptlang.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">TypeScript</el-tag></a>
|
||||||
|
<a href="https://vuejs.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Vue.js
|
||||||
|
3</el-tag></a>
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">Pinia</el-tag></a>
|
||||||
|
<a href="https://react.dev/" target="_blank" class="skill-tag-link"><el-tag type="primary">React
|
||||||
|
18</el-tag></a>
|
||||||
|
<a href="https://nodejs.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">Node.js</el-tag></a>
|
||||||
|
<a href="https://vite.dev/" target="_blank" class="skill-tag-link"><el-tag type="primary">Vite</el-tag></a>
|
||||||
|
<a href="https://webpack.js.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">Webpack</el-tag></a>
|
||||||
|
<a href="https://element-plus.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Element
|
||||||
|
Plus</el-tag></a>
|
||||||
|
<a href="https://git-scm.com/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="primary">Git</el-tag></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-skills">
|
<div class="about-skill-section">
|
||||||
<h3>后端技术栈</h3>
|
<h4>后端技术栈</h4>
|
||||||
<div class="skills-list">
|
<div class="skills-display-list">
|
||||||
<el-tag type="success">Spring Boot 2.6.13</el-tag>
|
|
||||||
<el-tag type="success">Spring Security</el-tag>
|
<a href="https://spring.io/projects/spring-boot" target="_blank" class="skill-tag-link"><el-tag
|
||||||
<el-tag type="success">Spring Data JPA</el-tag>
|
type="success">Spring Boot 3.x</el-tag></a>
|
||||||
<el-tag type="success">MyBatis</el-tag>
|
<a href="https://spring.io/projects/spring-security" target="_blank" class="skill-tag-link"><el-tag
|
||||||
<el-tag type="success">MySQL</el-tag>
|
type="success">Spring Security</el-tag></a>
|
||||||
<el-tag type="success">Lombok</el-tag>
|
<a href="https://spring.io/projects/spring-data-jpa" target="_blank" class="skill-tag-link"><el-tag
|
||||||
<el-tag type="success">EHCache</el-tag>
|
type="success">Spring Data JPA</el-tag></a>
|
||||||
<el-tag type="success">Maven</el-tag>
|
<a href="https://mybatis.org/mybatis-3/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
<el-tag type="success">Java 8</el-tag>
|
type="success">MyBatis-Plus</el-tag></a>
|
||||||
|
<a href="https://www.mysql.com/" target="_blank" class="skill-tag-link"><el-tag type="success">MySQL
|
||||||
|
8</el-tag></a>
|
||||||
|
<a href="https://www.postgresql.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="success">PostgreSQL</el-tag></a>
|
||||||
|
<a href="https://redis.io/" target="_blank" class="skill-tag-link"><el-tag type="success">Redis</el-tag></a>
|
||||||
|
<a href="https://projectlombok.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="success">Lombok</el-tag></a>
|
||||||
|
<a href="https://mapstruct.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="success">MapStruct</el-tag></a>
|
||||||
|
<a href="https://maven.apache.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="success">Maven</el-tag></a>
|
||||||
|
<a href="https://gradle.org/" target="_blank" class="skill-tag-link"><el-tag
|
||||||
|
type="success">Gradle</el-tag></a>
|
||||||
|
<a href="https://www.oracle.com/java/technologies/java17.html" target="_blank"
|
||||||
|
class="skill-tag-link"><el-tag type="success">Java 17+</el-tag></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-hobbies">
|
<div class="about-contact-section">
|
||||||
<h3>兴趣爱好</h3>
|
<h4>联系方式</h4>
|
||||||
<ul>
|
<p>如果你有任何问题或建议,欢迎随时联系我!(我文笔真的很烂QAQ)</p>
|
||||||
<li>阅读技术书籍和博客</li>
|
<div class="contact-options">
|
||||||
<li>参与开源项目</li>
|
<el-button type="primary" plain @click="goToMessageBoard">留言板</el-button>
|
||||||
<li>学习新技术和框架</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="about-contact">
|
|
||||||
<h3>联系方式</h3>
|
|
||||||
<p>如果你有任何问题或建议,欢迎随时联系我!</p>
|
|
||||||
<div class="contact-list">
|
|
||||||
<el-button type="primary" plain>留言板</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,145 +112,207 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 跳转到留言板
|
/**
|
||||||
|
* 跳转到留言板页面
|
||||||
|
* 当用户点击留言板按钮时触发
|
||||||
|
*/
|
||||||
const goToMessageBoard = () => {
|
const goToMessageBoard = () => {
|
||||||
router.push('/message')
|
router.push('/message')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 主容器样式 */
|
||||||
|
.about-page-container {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
.about-wrapper {
|
/* 内容包装器样式 */
|
||||||
|
.about-content-wrapper {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-color: white;
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面头部 */
|
/* 内容包装器悬浮效果 */
|
||||||
.about-header {
|
.about-content-wrapper:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部样式 */
|
||||||
|
.about-page-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
border-bottom: 2px solid #ecf0f1;
|
border-bottom: 2px solid #ecf0f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-title {
|
/* 页面标题样式 */
|
||||||
|
.about-page-title {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-subtitle {
|
/* 页面副标题样式 */
|
||||||
|
.about-page-subtitle {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关于内容 */
|
/* 主要内容区域样式 */
|
||||||
.about-content {
|
.about-main-content {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #34495e;
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 个人介绍 */
|
/* 个人介绍样式 */
|
||||||
.about-intro {
|
.about-personal-intro {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-intro p {
|
.about-personal-intro h5 {
|
||||||
|
color: #34495e;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 技术栈 */
|
/* 技能部分样式 */
|
||||||
.about-skills {
|
.about-skill-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-skills h3 {
|
.about-skill-section h4 {
|
||||||
color: #2c3e50;
|
text-align: left;
|
||||||
font-size: 1.3rem;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills-list {
|
/* 技能列表样式 */
|
||||||
|
.skills-display-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 兴趣爱好 */
|
/* 技能标签链接样式 */
|
||||||
.about-hobbies {
|
.skill-tag-link {
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 技能标签链接悬浮效果 */
|
||||||
|
.skill-tag-link:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-tag-link:hover .el-tag {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兴趣爱好部分样式 */
|
||||||
|
.about-hobbies-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-hobbies h3 {
|
.about-hobbies-section h3 {
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-hobbies ul {
|
/* 兴趣爱好列表样式 */
|
||||||
|
.hobbies-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-hobbies li {
|
.hobbies-list li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-hobbies li::before {
|
.hobbies-list li:hover {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hobbies-list li::before {
|
||||||
content: '✦';
|
content: '✦';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 联系方式 */
|
.hobbies-list li:hover::before {
|
||||||
.about-contact {
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 联系方式部分样式 */
|
||||||
|
.about-contact-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-contact h3 {
|
.about-contact-section h3 {
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-contact p {
|
/* 联系方式选项样式 */
|
||||||
margin-bottom: 16px;
|
.contact-options {
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#about-container {
|
.about-page-container {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
.about-wrapper {
|
.about-page-header{
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.about-content-wrapper {
|
||||||
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 0 15px;
|
margin: 0 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-title {
|
.about-page-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-subtitle {
|
.about-page-subtitle {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-content {
|
.about-main-content {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header {
|
.about-page-header {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-options {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.skills-display-list {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="allstyle">
|
|
||||||
<div class="header">
|
|
||||||
<h1>文章目录</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading" class="loading-container">
|
|
||||||
<el-skeleton :count="5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
|
||||||
<div v-else-if="error" class="error-container">
|
|
||||||
<el-alert :title="error" type="error" show-icon />
|
|
||||||
<el-button type="primary" @click="fetchCategories">重新加载</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分类列表 -->
|
|
||||||
<div v-else-if="categories.length > 0" class="post_content">
|
|
||||||
<div v-for="categoryGroup in categories" :key="categoryGroup.name" class="category-group">
|
|
||||||
<div class="category-header">
|
|
||||||
<h2>{{ categoryGroup.name }}</h2>
|
|
||||||
<span class="badge badge-primary">共{{ getCategorySum(categoryGroup.categories) }}篇</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="pcont_ul">
|
|
||||||
<li class="pcont_li" v-for="category in categoryGroup.categories" :key="category.typeid">
|
|
||||||
<button class="btn" @click="handleCategoryClick(category.typeid)">
|
|
||||||
<kbd>{{ category.content }}</kbd>
|
|
||||||
</button>
|
|
||||||
<span class="category-count">({{ category.sum }})</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div v-else class="empty-container">
|
|
||||||
<el-empty description="暂无分类" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { articleService } from '@/services'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { Category } from '@/types'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// 响应式状态
|
|
||||||
const categories = ref<any[]>([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文章分类列表
|
|
||||||
*/
|
|
||||||
const fetchCategories = async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
// 获取所有文章数据,然后从中提取分类信息
|
|
||||||
const res = await articleService.getAllArticles()
|
|
||||||
|
|
||||||
if (res.data && res.data.length > 0) {
|
|
||||||
// 假设数据结构是嵌套的分类组
|
|
||||||
categories.value = res.data
|
|
||||||
} else {
|
|
||||||
categories.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('获取分类列表成功:', categories.value)
|
|
||||||
} catch (err) {
|
|
||||||
error.value = '获取分类列表失败,请稍后重试'
|
|
||||||
console.error('获取分类列表失败:', err)
|
|
||||||
ElMessage.error(error.value)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
console.log('分类列表加载完成')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理分类点击事件
|
|
||||||
* @param {string} typeid - 分类ID
|
|
||||||
*/
|
|
||||||
const handleCategoryClick = (typeid: string) => {
|
|
||||||
router.push({
|
|
||||||
path: '/:type',
|
|
||||||
query: {
|
|
||||||
type: typeid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算分类组中的文章总数
|
|
||||||
* @param {Array} categoryItems - 分类项数组
|
|
||||||
* @returns {number} 文章总数
|
|
||||||
*/
|
|
||||||
const getCategorySum = (categoryItems: any[]): number => {
|
|
||||||
if (!categoryItems || !Array.isArray(categoryItems)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return categoryItems.reduce((total, item) => {
|
|
||||||
return total + (item.sum || 0)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件挂载时获取分类列表
|
|
||||||
*/
|
|
||||||
onMounted(() => {
|
|
||||||
fetchCategories()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-group {
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 2px solid #ecf0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header h2 {
|
|
||||||
color: #34495e;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pcont_ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pcont_li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: rgba(245, 247, 250, 0.7);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pcont_li:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
background-color: rgba(236, 240, 241, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
position: relative;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #34495e;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: inline-block;
|
|
||||||
z-index: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 透明方块效果 */
|
|
||||||
.btn::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
z-index: -1;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
transform: scale(0.95);
|
|
||||||
opacity: 0.8;
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 悬浮效果 */
|
|
||||||
.btn:hover::before {
|
|
||||||
transform: scale(1.1);
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
background: rgba(145, 196, 238, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-count {
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标签样式 */
|
|
||||||
.badge {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-primary {
|
|
||||||
color: #2643e9;
|
|
||||||
background-color: rgba(203, 210, 246, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
font-size: 66%;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
display: inline-block;
|
|
||||||
padding: .35rem .375rem;
|
|
||||||
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: baseline;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载状态 */
|
|
||||||
.loading-container {
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误状态 */
|
|
||||||
.error-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container .el-button {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空状态 */
|
|
||||||
.empty-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_content {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-group {
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pcont_ul {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
409
src/views/aericlelist.vue
Normal file
409
src/views/aericlelist.vue
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
<template>
|
||||||
|
<div id="allstyle">
|
||||||
|
<div class="page-header">
|
||||||
|
<h3>文章目录</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading-state-container">
|
||||||
|
<el-skeleton :count="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="error" class="error-state-container">
|
||||||
|
<el-alert :title="error" type="error" show-icon />
|
||||||
|
<el-button type="primary" @click="fetchCategories">重新加载</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- 分类列表 -->
|
||||||
|
<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.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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="empty-state-container">
|
||||||
|
<el-empty description="暂无分类" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
// 由于模块 '@/services' 没有导出的成员 'CategoryService',请确认正确的导出名称后修改此处
|
||||||
|
// 以下代码仅作示例,实际需根据 '@/services' 模块的真实导出调整
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { categoryService, categoryAttributeService, articleService } from '@/services'
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 响应式状态
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文章分类列表
|
||||||
|
*/
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await categoryService.getAllCategories();
|
||||||
|
let processedCategories: any[] = [];
|
||||||
|
if (res.code === 200) {
|
||||||
|
processedCategories = res.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
attributes: [] // 使用更清晰的命名
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用Promise.all等待所有异步操作完成
|
||||||
|
await Promise.all(
|
||||||
|
processedCategories.map(async category => {
|
||||||
|
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 => {
|
||||||
|
const articleItem = {
|
||||||
|
...item,
|
||||||
|
articles: []
|
||||||
|
};
|
||||||
|
const articlesRes = await articleService.getArticlesByAttributeId(item.attributeid);
|
||||||
|
if(articlesRes.code === 200 && Array.isArray(articlesRes.data)){
|
||||||
|
articleItem.articles = articlesRes.data;
|
||||||
|
}
|
||||||
|
return articleItem;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
category.attributes = processedAttributes;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
categories.value = processedCategories;
|
||||||
|
// console.log('获取分类列表成功:', categories.value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取分类列表失败:', err);
|
||||||
|
ElMessage.error('获取分类列表失败,请稍后重试');
|
||||||
|
}finally{
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 处理分类点击事件
|
||||||
|
* 注意:现在实际上使用的是属性ID而不是分类ID
|
||||||
|
* @param {string | number} attributeId - 属性ID
|
||||||
|
*/
|
||||||
|
const handleCategoryClick = (attribute: any) => {
|
||||||
|
globalStore.removeValue('attribute')
|
||||||
|
globalStore.setValue('attribute', {
|
||||||
|
id: attribute.attributeid,
|
||||||
|
name: attribute.attributename
|
||||||
|
})
|
||||||
|
router.push({
|
||||||
|
path: '/home/aericleattribute',
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分类组中的文章总数
|
||||||
|
* @param {Array} categoryItems - 分类项数组
|
||||||
|
* @returns {number} 文章总数
|
||||||
|
*/
|
||||||
|
const getCategorySum = (categoryItems: any[]): number => {
|
||||||
|
if (!categoryItems || !Array.isArray(categoryItems)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryItems.reduce((total, item) => {
|
||||||
|
return total + (item.sum || 0)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件挂载时获取分类列表
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 页面标题样式 */
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容区域 */
|
||||||
|
.article-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类组样式 */
|
||||||
|
.category-group-container ul li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类链接样式 */
|
||||||
|
.category-group-container ul li .category-link::before {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-group-container ul li .category-link:hover {
|
||||||
|
background-color: rgb(245, 247, 250, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示框样式 */
|
||||||
|
.alert:not(.alert-secondary) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-primary {
|
||||||
|
color: #fff;
|
||||||
|
border-color: #7889e8;
|
||||||
|
background-color: #7889e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-inner-text {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 键盘样式 */
|
||||||
|
.category-link kbd {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: .0625rem solid transparent;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类组容器样式 */
|
||||||
|
.category-group-container {
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 徽章样式 */
|
||||||
|
.badge {
|
||||||
|
padding: .25em .4em;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-radius: .25rem;
|
||||||
|
margin: 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类标题样式 */
|
||||||
|
.category-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header h2 {
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 项目列表样式 */
|
||||||
|
.project-content-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-content-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(245, 247, 250, 0.7);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-content-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background-color: rgba(236, 240, 241, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类链接样式 */
|
||||||
|
.category-link {
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #34495e;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 透明方块效果 */
|
||||||
|
.category-link::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: -1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.8;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬浮效果 */
|
||||||
|
.category-link:hover::before {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
background: rgba(145, 196, 238, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类计数样式 */
|
||||||
|
.category-count {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签样式 */
|
||||||
|
.badge {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary {
|
||||||
|
color: #2643e9;
|
||||||
|
background-color: rgba(203, 210, 246, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 66%;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
padding: .35rem .375rem;
|
||||||
|
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: baseline;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state-container {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误状态 */
|
||||||
|
.error-state-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state-container .el-button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-group-container {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-content-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,223 +1,321 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div id="article-detail-page">
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="loading-container">
|
<div v-if="loading" class="loading-state-container">
|
||||||
<el-skeleton :count="1" />
|
<el-skeleton :count="1" />
|
||||||
<el-skeleton :count="3" />
|
<el-skeleton :count="3" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
<!-- 错误状态 -->
|
||||||
<div v-else-if="error" class="error-container">
|
<div v-else-if="error" class="error-state-container">
|
||||||
<el-alert :title="error" type="error" show-icon />
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 文章详情 -->
|
<!-- 文章详情 -->
|
||||||
<div v-else-if="article && Object.keys(article).length > 0" class="article-wrapper">
|
<div v-else-if="article && Object.keys(article).length > 0" class="article-detail-wrapper">
|
||||||
<!-- 文章头部 -->
|
<!-- 文章头部信息 -->
|
||||||
<div class="article-header">
|
<div class="article-header-section">
|
||||||
<h1 class="article-title">{{ article.title }}</h1>
|
<h1 class="article-main-title">{{ article.title }}</h1>
|
||||||
|
|
||||||
<div class="article-meta">
|
<div class="article-meta-info">
|
||||||
<span class="meta-item">
|
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt) }}</span>
|
||||||
<i class="el-icon-date"></i>
|
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
|
||||||
{{ formatDate(article.createTime) }}
|
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
|
||||||
</span>
|
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
|
||||||
<span class="meta-item">
|
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
|
||||||
<i class="el-icon-folder"></i>
|
<div class="article-status-badge-container" v-if="globalStore.Login">
|
||||||
{{ article.categoryName || '未分类' }}
|
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
|
||||||
</span>
|
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
|
||||||
<span class="meta-item">
|
<span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
|
||||||
<i class="el-icon-view"></i>
|
</div>
|
||||||
{{ article.views || 0 }} 阅读
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章内容 -->
|
<!-- 文章内容区域 -->
|
||||||
<div class="article-content">
|
<div class="article-content-area">
|
||||||
<div v-html="article.content"></div>
|
<markdownViewer :markdownContent="article.markdownscontent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章底部 -->
|
<!-- 文章底部信息 -->
|
||||||
<div class="article-footer">
|
<div class="article-footer-section">
|
||||||
<div class="tag-list">
|
<div class="article-tag-list">
|
||||||
<span
|
|
||||||
v-for="tag in article.tags || []"
|
|
||||||
:key="tag"
|
|
||||||
class="el-tag el-tag--primary"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章操作 -->
|
<!-- 文章操作按钮 -->
|
||||||
<div class="article-actions">
|
<div class="article-actions-group">
|
||||||
<el-button
|
<el-button type="primary" @click="navigateBack" plain>
|
||||||
type="primary"
|
|
||||||
icon="el-icon-arrow-left"
|
|
||||||
@click="goBack"
|
|
||||||
plain
|
|
||||||
>
|
|
||||||
返回
|
返回
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 相关文章 -->
|
<!-- 相关文章推荐 -->
|
||||||
<div class="related-articles" v-if="relatedArticles.length > 0">
|
<div class="related-articles-section" v-if="relatedArticles.length > 0">
|
||||||
<h3>相关文章</h3>
|
<h3>相关文章</h3>
|
||||||
<div class="related-articles-list">
|
<div class="related-articles-list">
|
||||||
<div
|
<!-- <div v-for="item in relatedArticles" :key="item.articleid" class="related-article-card"
|
||||||
v-for="item in relatedArticles"
|
@click="handleRelatedArticleClick(item.articleid)">
|
||||||
:key="item.id"
|
|
||||||
class="related-article-item"
|
|
||||||
@click="handleRelatedArticleClick(item.id)"
|
|
||||||
>
|
|
||||||
<i class="el-icon-document"></i>
|
<i class="el-icon-document"></i>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 - 文章不存在 -->
|
||||||
<div v-else class="empty-container">
|
<div v-else class="empty-state-container">
|
||||||
<el-empty description="文章不存在" />
|
<el-empty description="文章不存在" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 评论区组件 -->
|
||||||
|
<div>
|
||||||
|
<messageboard class="comment-section" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
// =========================================================================
|
||||||
import { ref, onMounted } from 'vue'
|
// 组件导入和初始化
|
||||||
import { articleService } from '@/services'
|
// =========================================================================
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { Article } from '@/types'
|
|
||||||
import { formatDate } from '@/utils/dateUtils'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// 响应式状态
|
|
||||||
const article = ref<Article | null>(null) // 使用 null 作为初始值,避免类型不匹配问题
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
const relatedArticles = ref<Article[]>([])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文章详情
|
* 导入Vue核心功能
|
||||||
*/
|
*/
|
||||||
const fetchArticleDetail = async () => {
|
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[]>([]) // 相关文章列表
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 文章数据处理模块
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文章ID
|
||||||
|
* @returns {number | null} 文章ID或null
|
||||||
|
*/
|
||||||
|
const getArticleId = (): number | null => {
|
||||||
|
return globalStore.getValue('articleInfo')?.articleid || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文章详情数据
|
||||||
|
* @returns {Promise<Article | null>} 文章数据或null
|
||||||
|
*/
|
||||||
|
const fetchArticleData = async (): Promise<Article | null> => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
// 从全局状态获取文章信息
|
||||||
error.value = ''
|
const response = await globalStore.getValue('articleInfo')
|
||||||
|
return response || null
|
||||||
// 获取路由参数
|
|
||||||
const articleId = route.query.url as string
|
|
||||||
console.log('获取文章ID:', articleId)
|
|
||||||
if (!articleId) {
|
|
||||||
throw new Error('文章ID不存在')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文章详情
|
|
||||||
const res = await articleService.getArticleById(Number(articleId))
|
|
||||||
|
|
||||||
if (res.data) {
|
|
||||||
article.value = res.data
|
|
||||||
|
|
||||||
// 增加文章浏览量
|
|
||||||
try {
|
|
||||||
await articleService.incrementArticleViews(Number(articleId))
|
|
||||||
console.log('文章浏览量增加成功')
|
|
||||||
// 更新前端显示的浏览量
|
|
||||||
if (article.value.views) {
|
|
||||||
article.value.views++
|
|
||||||
} else {
|
|
||||||
article.value.views = 1
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('增加文章浏览量失败:', err)
|
|
||||||
// 不阻止主流程
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取相关文章(同分类下的其他文章)
|
|
||||||
if (article.value.categoryId) {
|
|
||||||
try {
|
|
||||||
const relatedRes = await articleService.createArticle(article.value.categoryId)
|
|
||||||
// 过滤掉当前文章,并取前5篇作为相关文章
|
|
||||||
relatedArticles.value = relatedRes.data
|
|
||||||
? relatedRes.data.filter((item: Article) => item.id !== article.value?.id).slice(0, 5)
|
|
||||||
: []
|
|
||||||
} catch (err) {
|
|
||||||
console.error('获取相关文章失败:', err)
|
|
||||||
// 不阻止主流程
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('文章不存在或已被删除')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('获取文章详情成功:', article.value)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试'
|
console.error('获取文章数据失败:', err)
|
||||||
console.error('获取文章详情失败:', err)
|
throw new Error('获取文章数据失败')
|
||||||
ElMessage.error(error.value)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
console.log('文章详情加载完成')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加文章浏览量
|
||||||
|
* @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 = ''
|
||||||
|
|
||||||
|
// 获取文章ID
|
||||||
|
const articleId = getArticleId()
|
||||||
|
if (!articleId) {
|
||||||
|
handleArticleNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文章数据
|
||||||
|
const articleData = await fetchArticleData()
|
||||||
|
if (articleData) {
|
||||||
|
article.value = articleData
|
||||||
|
// 增加浏览量
|
||||||
|
incrementViewCount(articleData)
|
||||||
|
} else {
|
||||||
|
handleArticleNotFound()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleArticleFetchError(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 导航和用户交互模块
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回上一页
|
* 返回上一页
|
||||||
*/
|
*/
|
||||||
const goBack = () => {
|
const navigateBack = (): void => {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理相关文章点击
|
* 导航到相关文章
|
||||||
|
* @param {number} id - 相关文章ID
|
||||||
*/
|
*/
|
||||||
const handleRelatedArticleClick = (id: number) => {
|
const navigateToRelatedArticle = (id: number): void => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/article/:url',
|
path: '/article/:url',
|
||||||
query: { url: id }
|
query: { url: id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 组件生命周期和初始化
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件挂载时获取文章详情
|
* 组件挂载时的初始化操作
|
||||||
|
*/
|
||||||
|
const setupComponent = (): void => {
|
||||||
|
initializeArticleDetail()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 组件挂载生命周期钩子
|
||||||
*/
|
*/
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchArticleDetail()
|
setupComponent()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#article-container {
|
/* 文章详情容器 */
|
||||||
min-height: 100vh;
|
.article-detail-wrapper {
|
||||||
background-color: #f5f7fa;
|
|
||||||
padding: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-wrapper {
|
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-color: white;
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章头部 */
|
/* 加载状态容器 */
|
||||||
.article-header {
|
.loading-state-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误状态容器 */
|
||||||
|
.error-state-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 40px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state-container .el-button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态容器 */
|
||||||
|
.empty-state-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 80px 40px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章头部区域 */
|
||||||
|
.article-header-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
border-bottom: 2px solid #ecf0f1;
|
border-bottom: 2px solid #ecf0f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-title {
|
/* 文章标题 */
|
||||||
|
.article-main-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
@@ -225,7 +323,8 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-meta {
|
/* 文章元信息 */
|
||||||
|
.article-meta-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -233,90 +332,109 @@ onMounted(() => {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 元信息项 */
|
||||||
.meta-item {
|
.meta-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章内容 */
|
/* 文章内容区域 */
|
||||||
.article-content {
|
.article-content-area {
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #333;
|
color: #34495e;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content p {
|
/* 文章内容中的段落 */
|
||||||
|
.article-content-area p {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content h2 {
|
/* 文章内容中的二级标题 */
|
||||||
font-size: 1.6rem;
|
.article-content-area h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1.5rem;
|
||||||
margin: 32px 0 16px 0;
|
margin: 32px 0 16px 0;
|
||||||
color: #2c3e50;
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #ecf0f1;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content h3 {
|
/* 文章内容中的三级标题 */
|
||||||
font-size: 1.4rem;
|
.article-content-area h3 {
|
||||||
margin: 24px 0 16px 0;
|
color: #34495e;
|
||||||
color: #2c3e50;
|
font-size: 1.3rem;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content img {
|
/* 文章内容中的图片 */
|
||||||
|
.article-content-area img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 16px 0;
|
margin: 20px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content blockquote {
|
/* 文章内容中的引用 */
|
||||||
|
.article-content-area blockquote {
|
||||||
border-left: 4px solid #3498db;
|
border-left: 4px solid #3498db;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章底部 */
|
/* 文章底部区域 */
|
||||||
.article-footer {
|
.article-footer-section {
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
border-top: 1px solid #ecf0f1;
|
border-top: 2px solid #ecf0f1;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-list {
|
/* 标签列表 */
|
||||||
|
.article-tag-list {
|
||||||
|
margin-bottom: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-actions {
|
/* 文章操作按钮组 */
|
||||||
|
.article-actions-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 相关文章 */
|
/* 相关文章区域 */
|
||||||
.related-articles {
|
.related-articles-section {
|
||||||
padding-top: 32px;
|
padding-top: 32px;
|
||||||
border-top: 2px solid #ecf0f1;
|
border-top: 2px solid #ecf0f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-articles h3 {
|
/* 相关文章标题 */
|
||||||
|
.related-articles-section h3 {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 相关文章列表容器 */
|
||||||
.related-articles-list {
|
.related-articles-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-article-item {
|
/* 相关文章卡片 */
|
||||||
|
.related-article-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -325,160 +443,102 @@ onMounted(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-article-item:hover {
|
/* 相关文章卡片悬停效果 */
|
||||||
|
.related-article-card:hover {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
transform: translateX(5px);
|
transform: translateX(5px);
|
||||||
|
border-color: #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-article-item i {
|
.related-article-card i {
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-article-item span {
|
.related-article-card span {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 错误和空状态 */
|
.related-article-card:hover span {
|
||||||
.error-container {
|
color: #3498db;
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-container {
|
/* 评论区样式 */
|
||||||
max-width: 600px;
|
.comment-section {
|
||||||
margin: 0 auto;
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 - 平板和手机 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.article-wrapper {
|
#article-detail-page {
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-meta {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* 文章内容 */
|
|
||||||
.article-content {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #34495e;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content p {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content h2 {
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 32px 0 16px 0;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid #ecf0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content h3 {
|
|
||||||
color: #34495e;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
margin: 24px 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文章底部 */
|
|
||||||
.article-footer {
|
|
||||||
padding-top: 24px;
|
|
||||||
border-top: 2px solid #ecf0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-list {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载状态 */
|
|
||||||
.loading-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误状态 */
|
|
||||||
.error-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 60px 40px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container .el-button {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空状态 */
|
|
||||||
.empty-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 80px 40px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
#article-container {
|
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-wrapper,
|
.article-detail-wrapper,
|
||||||
.loading-container,
|
.loading-state-container,
|
||||||
.error-container,
|
.error-state-container,
|
||||||
.empty-container {
|
.empty-state-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 0 15px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
.article-main-title {
|
||||||
.article-title {
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
.article-actions-group {
|
||||||
.article-meta {
|
/* 文章操作按钮组 - 右对齐 */
|
||||||
gap: 15px;
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.article-meta-info {
|
||||||
|
gap: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content {
|
.article-content-area {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles-section h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card span {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 小屏幕手机 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.article-detail-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
480
src/views/articlesave.vue
Normal file
480
src/views/articlesave.vue
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<template>
|
||||||
|
<div class="article-save-container">
|
||||||
|
<div class="article-content-wrapper">
|
||||||
|
<div class="article-header-section">
|
||||||
|
<div class="title-container">
|
||||||
|
<input type="text" v-model="Articleform.title" class="article-main-title" placeholder="请输入标题"
|
||||||
|
@focus="($event.target as HTMLInputElement).placeholder = ''"
|
||||||
|
@blur="($event.target as HTMLInputElement).placeholder = '请输入标题'" />
|
||||||
|
</div>
|
||||||
|
<div class="article-meta-info">
|
||||||
|
<span class="meta-item date-item">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span> {{ new Date().toLocaleDateString() }} </span>
|
||||||
|
</span>
|
||||||
|
<span class="meta-item status-item">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<el-select v-model="Articleform.status" placeholder="请选择状态" class="meta-select">
|
||||||
|
<el-option v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</span>
|
||||||
|
<span class="meta-item category-item">
|
||||||
|
<i class="el-icon-folder"></i>
|
||||||
|
<el-cascader :options="categorieoptions" v-model="selectedValues" @change="handleCascaderChange"
|
||||||
|
placeholder="选择分类和属性" class="meta-cascader">
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ data.label }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</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" noImgZoomIn
|
||||||
|
noKatex />
|
||||||
|
<!-- 返回列表 -->
|
||||||
|
<el-button type="primary" @click="handleReturn">返回列表</el-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { MdEditor } from 'md-editor-v3';
|
||||||
|
import 'md-editor-v3/lib/style.css';
|
||||||
|
import { categoryService, categoryAttributeService, articleService } from '@/services';
|
||||||
|
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>({
|
||||||
|
articleid: 0,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
attributeid: 0,
|
||||||
|
categoryName: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
markdownscontent: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 用于级联选择器的值绑定
|
||||||
|
const selectedValues = ref([]);
|
||||||
|
const categorieoptions = ref([]);
|
||||||
|
const statusoptions = ref([
|
||||||
|
{
|
||||||
|
label: '草稿',
|
||||||
|
value: '0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '发布',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
value: '2'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const categories = ref([]);
|
||||||
|
// 编辑文章
|
||||||
|
const editArticle = globalStore.getValue('updatearticle')
|
||||||
|
|
||||||
|
if (editArticle) {
|
||||||
|
Articleform.value = {
|
||||||
|
...editArticle,
|
||||||
|
// 确保status是字符串格式,与statusoptions的value格式匹配
|
||||||
|
status: editArticle.status !== undefined ? String(editArticle.status) : '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 初始化加载分类和属性,构建级联选择器的options
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await categoryService.getAllCategories();
|
||||||
|
if (response.code === 200) {
|
||||||
|
categories.value = response.data;
|
||||||
|
|
||||||
|
// 为每个分类加载对应的属性,并构建options格式
|
||||||
|
const optionsData = await Promise.all(
|
||||||
|
categories.value.map(async (category) => {
|
||||||
|
try {
|
||||||
|
const attrResponse = await categoryAttributeService.getAttributesByCategory(category.typeid);
|
||||||
|
const children = attrResponse.code === 200 && attrResponse.data ?
|
||||||
|
attrResponse.data.map(attr => ({
|
||||||
|
label: attr.attributename,
|
||||||
|
value: attr.attributeid.toString()
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: category.typename,
|
||||||
|
value: category.typeid.toString(),
|
||||||
|
children
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载分类 ${category.typename} 的属性失败:`, error);
|
||||||
|
return {
|
||||||
|
label: category.typename,
|
||||||
|
value: category.typeid.toString(),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
categorieoptions.value = optionsData;
|
||||||
|
|
||||||
|
// 如果是编辑模式且有attributeid,设置级联选择器的默认值
|
||||||
|
if (Articleform.value.articleid !== 0 && Articleform.value.attributeid !== 0) {
|
||||||
|
// 查找属性所属的分类
|
||||||
|
for (const category of optionsData) {
|
||||||
|
const foundAttribute = category.children.find(attr => attr.value === Articleform.value.attributeid.toString());
|
||||||
|
if (foundAttribute) {
|
||||||
|
// 设置级联选择器的值:[分类ID, 属性ID]
|
||||||
|
selectedValues.value = [category.value, foundAttribute.value];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('分类选项:', optionsData);
|
||||||
|
// console.log('选中的值:', selectedValues.value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理级联选择变化
|
||||||
|
const handleCascaderChange = (values) => {
|
||||||
|
if (values && values.length > 0) {
|
||||||
|
// 最后一个值是属性ID
|
||||||
|
Articleform.value.attributeid = Number(values[values.length - 1]);
|
||||||
|
} else {
|
||||||
|
Articleform.value.attributeid = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载分类和属性
|
||||||
|
loadCategories();
|
||||||
|
|
||||||
|
const handleSave = (markdown) => {
|
||||||
|
Articleform.value.markdownscontent = markdown;
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!Articleform.value.title || !Articleform.value.attributeid) {
|
||||||
|
ElMessage.warning('请填写必填字段:标题和分类属性');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
const articleData = {
|
||||||
|
articleid: Articleform.value.articleid,
|
||||||
|
title: Articleform.value.title,
|
||||||
|
content: Articleform.value.content,
|
||||||
|
attributeid: Number(Articleform.value.attributeid),
|
||||||
|
status: Number(Articleform.value.status),
|
||||||
|
viewCount: 0,
|
||||||
|
likes: 0,
|
||||||
|
markdownscontent: Articleform.value.markdownscontent
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log('发送文章数据:', articleData);
|
||||||
|
// console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
|
||||||
|
|
||||||
|
// 根据articleid决定调用创建还是更新接口
|
||||||
|
const savePromise = Articleform.value.articleid === 0
|
||||||
|
? articleService.createArticle(articleData)
|
||||||
|
: articleService.updateArticle(Articleform.value.articleid, articleData);
|
||||||
|
savePromise
|
||||||
|
.then(res => {
|
||||||
|
// console.log('API响应:', res);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
|
||||||
|
// 清除全局存储中的article
|
||||||
|
globalStore.removeValue('updatearticle');
|
||||||
|
// 重置表单或保留编辑状态
|
||||||
|
if (Articleform.value.articleid === 0) {
|
||||||
|
// 创建新文章后重置表单
|
||||||
|
Articleform.value = {
|
||||||
|
articleid: 0,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
attributeid: 0,
|
||||||
|
categoryName: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
markdownscontent: ''
|
||||||
|
};
|
||||||
|
selectedValues.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回列表页
|
||||||
|
router.push('/home');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('保存失败错误详情:', err);
|
||||||
|
// 更详细的错误信息
|
||||||
|
if (err.response) {
|
||||||
|
console.error('错误状态码:', err.response.status);
|
||||||
|
console.error('错误响应数据:', err.response.data);
|
||||||
|
|
||||||
|
const operationType = Articleform.value.articleid === 0 ? '创建' : '更新';
|
||||||
|
|
||||||
|
if (err.response.status === 401) {
|
||||||
|
ElMessage.error('未授权访问,请先登录');
|
||||||
|
} else if (err.response.status === 403) {
|
||||||
|
ElMessage.error(`没有权限${operationType}文章,请检查账号权限`);
|
||||||
|
} else if (err.response.status === 400) {
|
||||||
|
ElMessage.error('数据验证失败: ' + (err.response.data?.message || '请检查输入'));
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`请求被拒绝,错误代码: ${err.response.status}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(err.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回列表页
|
||||||
|
*/
|
||||||
|
const handleReturn = () => {
|
||||||
|
// 确定是否返回列表页
|
||||||
|
if (window.confirm('确定要返回列表页吗?所有未保存的更改将丢失。')) {
|
||||||
|
// 清除全局存储中的article
|
||||||
|
globalStore.removeValue('updatearticle');
|
||||||
|
router.push('/home');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 主容器 */
|
||||||
|
.article-save-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容包装器 */
|
||||||
|
.article-content-wrapper {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章头部区域 */
|
||||||
|
.article-header-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题容器 */
|
||||||
|
.title-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章标题 */
|
||||||
|
.article-main-title {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #2c3e50;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title::placeholder {
|
||||||
|
color: #bdc3c7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title:focus {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章元信息 */
|
||||||
|
.article-meta-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 元信息项 */
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item:hover {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 元信息选择器样式 */
|
||||||
|
.meta-select {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-cascader {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器容器 */
|
||||||
|
.editor-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown编辑器样式 */
|
||||||
|
.markdown-editor {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 平板 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.article-content-wrapper {
|
||||||
|
padding: 30px 20px;
|
||||||
|
margin: 0 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-select,
|
||||||
|
.meta-cascader {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 文章简介区域 */
|
||||||
|
.article-summary-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 0 30px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 手机 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.article-save-container {
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-wrapper {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 0 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-select,
|
||||||
|
.meta-cascader {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 深色模式支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.article-save-container {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-wrapper {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-header-section {
|
||||||
|
border-bottom-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title::placeholder {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item:hover {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 级联选择器ul元素自适应高度 */
|
||||||
|
:deep(.el-cascader-menu__list) {
|
||||||
|
height: auto !important;
|
||||||
|
max-height: none !important;
|
||||||
|
overflow-y: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保级联选择器的每个菜单项都能正确显示 */
|
||||||
|
:deep(.el-cascader-menu) {
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
<!-- 文章列表组件 -->
|
<!-- 文章列表组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="article-list-container">
|
<div class="article-list-container">
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="loading-container">
|
<div v-if="loading" class="loading-container">
|
||||||
<el-skeleton :count="5" />
|
<el-skeleton :count="5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章列表 -->
|
<!-- 文章列表 -->
|
||||||
<transition-group name="article-item" tag="div" v-else>
|
<transition-group name="article-item" tag="div" class="article-list-content" v-else>
|
||||||
<div
|
<div class="article-card" v-for="article in articleList" :key="article.articleId"
|
||||||
class="article-card"
|
@click="handleArticleClick(article)">
|
||||||
v-for="item in datas"
|
<h6 class="article-title">{{ article.title }}</h6>
|
||||||
:key="item.id || (item.title + item.publishedAt)"
|
<div v-if="article.marked" class="article-special-tag">标记文章</div>
|
||||||
@click="handleArticleClick(item.articleid)"
|
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
|
||||||
>
|
<div class="article-meta-info">
|
||||||
<h2 class="article-title">{{ item.title }}</h2>
|
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span>
|
||||||
<div class="article-meta">
|
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
|
||||||
<span class="article-author">{{ item.author || '清疯不颠' }}</span>
|
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
|
||||||
<span class="article-date">{{ formatDateDisplay(item.publishedAt || item.createTime) }}</span>
|
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
|
||||||
<span v-if="item.categoryName" class="article-category">{{ item.categoryName }}</span>
|
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
|
||||||
<span v-if="item.views" class="article-views">{{ item.views }} 阅读</span>
|
<div class="article-status-badge-container" v-if="globalStore.Login">
|
||||||
<span v-if="item.commentCount" class="article-comments">{{ item.commentCount }} 评论</span>
|
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
|
||||||
|
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
|
||||||
|
<span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.mg" class="article-tag">mg</div>
|
|
||||||
<p class="article-preview">{{ formatContentPreview(item.content, 150) }}</p>
|
|
||||||
</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>
|
</transition-group>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="!loading && datas.length === 0" class="empty-container">
|
<div v-if="!loading && articleList.length === 0" class="empty-state-container">
|
||||||
<el-empty description="暂无文章" />
|
<el-empty description="暂无文章" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,109 +39,269 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { articleService } from '@/services'
|
import { formatRelativeTime } from '@/utils/dateUtils'
|
||||||
import { formatDate, formatRelativeTime } from '@/utils/dateUtils'
|
|
||||||
import { formatContentPreview } from '@/utils/stringUtils'
|
import { formatContentPreview } from '@/utils/stringUtils'
|
||||||
import { ElMessage } from 'element-plus'
|
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 router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
// 分页属性
|
||||||
|
const pageNum = ref(1) // 当前页码
|
||||||
|
const pageSize = ref(10) // 每页数量
|
||||||
|
const totalPages = ref(0) // 总页数
|
||||||
|
const pageLayout = ref('pager, next')// 分页布局
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const datas = ref([])
|
const articleList = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ========== 分页数据处理 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文章列表
|
* 处理分页组件的数据更新
|
||||||
|
* @param {Array} data - 分页组件传递的当前页数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ========== 文章数据获取模块 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据路由路径获取对应的文章列表
|
||||||
|
* @returns {Promise<Object>} 文章列表响应数据
|
||||||
|
*/
|
||||||
|
const getArticlesByRoute = async () => {
|
||||||
|
// 检查URL参数,确定获取文章的方式
|
||||||
|
const pathSegment = route.path.split('/')[2]
|
||||||
|
// console.log('当前路由分段:', pathSegment)
|
||||||
|
|
||||||
|
switch (pathSegment) {
|
||||||
|
case 'aericleattribute':
|
||||||
|
// 按属性类型获取文章
|
||||||
|
const attributeData = globalStore.getValue('attribute')
|
||||||
|
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')
|
||||||
|
console.log('按标题搜索文章:', titleData.name)
|
||||||
|
return await articleService.getPagedArticles({title: titleData?.name}, pageNum.value, pageSize.value)
|
||||||
|
case 'aericlestatus':
|
||||||
|
// 按状态获取文章
|
||||||
|
const statusData = globalStore.getValue('articlestatus')
|
||||||
|
return await articleService.getPagedArticles({status: statusData?.status}, pageNum.value, pageSize.value)
|
||||||
|
default:
|
||||||
|
// 默认获取所有文章
|
||||||
|
// console.log('获取所有文章列表')
|
||||||
|
return await articleService.getPagedArticles({status: 1}, pageNum.value, pageSize.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为单篇文章补充额外信息(留言数量、分类名称等)
|
||||||
|
* @param {Object} article - 文章对象
|
||||||
|
* @returns {Promise<Object>} 补充信息后的文章对象
|
||||||
|
*/
|
||||||
|
const enrichArticleWithExtraInfo = async (article) => {
|
||||||
|
try {
|
||||||
|
// 获取留言数量
|
||||||
|
const messageResponse = await messageService.getMessagesByArticleId(article.articleid)
|
||||||
|
// 获取分类名称
|
||||||
|
const categoryResponse = await categoryAttributeService.getAttributeById(article.attributeid)
|
||||||
|
|
||||||
|
// 设置分类名称
|
||||||
|
article.categoryName = categoryResponse?.data?.attributename || '未分类'
|
||||||
|
// 设置评论数量
|
||||||
|
article.commentCount = messageResponse?.data?.length || 0
|
||||||
|
// 标准化标记字段名
|
||||||
|
article.marked = article.mg
|
||||||
|
|
||||||
|
return article
|
||||||
|
} catch (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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enrichedArticles
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文章列表主函数
|
||||||
*/
|
*/
|
||||||
const fetchArticles = async () => {
|
const fetchArticles = async () => {
|
||||||
|
let response = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await articleService.getAllArticles()
|
|
||||||
datas.value = res.data || []
|
// 1. 根据路由获取文章列表
|
||||||
console.log('获取文章列表成功:', datas.value)
|
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) {
|
} catch (error) {
|
||||||
console.error('获取文章列表失败:', error)
|
console.error('获取文章列表失败:', error)
|
||||||
ElMessage.error('获取文章列表失败,请稍后重试')
|
ElMessage.error('获取文章列表失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
console.log('文章列表加载完成')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 文章交互模块 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理文章点击事件
|
* 处理文章点击事件
|
||||||
* @param {Object} article - 文章对象
|
* @param {Object} article - 文章对象
|
||||||
*/
|
*/
|
||||||
const handleArticleClick = (article) => {
|
const handleArticleClick = (article) => {
|
||||||
console.log('文章点击:', article)
|
try {
|
||||||
router.push({
|
// 增加文章浏览量(异步操作,不阻塞后续流程)
|
||||||
path: '/article/:url',
|
articleService.incrementArticleViews(article.articleId).catch(err => {
|
||||||
query: { url: article }
|
console.error('增加文章浏览量失败:', err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 清除之前的文章信息
|
||||||
|
globalStore.removeValue('articleInfo')
|
||||||
|
|
||||||
|
// 存储文章信息到全局状态
|
||||||
|
globalStore.setValue('articleInfo', 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 {string} dateString - 日期字符串
|
* @param {Array} newList - 新的文章列表
|
||||||
* @returns {string} 格式化后的日期
|
|
||||||
*/
|
*/
|
||||||
const formatDateDisplay = (dateString) => {
|
|
||||||
if (!dateString) return ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 如果是今天或昨天的文章,显示相对时间
|
|
||||||
const date = new Date(dateString)
|
|
||||||
const now = new Date()
|
|
||||||
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24))
|
|
||||||
|
|
||||||
if (diffDays < 2) {
|
|
||||||
return formatRelativeTime(dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则显示具体日期
|
|
||||||
return formatDate(dateString, 'YYYY-MM-DD')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('日期格式化错误:', error)
|
|
||||||
return dateString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 组件挂载时获取数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchArticles()
|
fetchArticles()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听路由变化,确保刷新时也能重新获取数据
|
||||||
|
watch(
|
||||||
|
// 监听路由路径和查询参数变化
|
||||||
|
() => [route.path, route.query],
|
||||||
|
// 路由变化时触发获取文章列表
|
||||||
|
handleRouteChange,
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听原始文章列表变化,确保初始数据正确显示
|
||||||
|
watch(
|
||||||
|
() => articleList.value,
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 文章列表容器样式 */
|
||||||
.article-list-container {
|
.article-list-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分类筛选区域 */
|
/* 加载状态容器 */
|
||||||
/* 加载状态 */
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章卡片 */
|
/* 文章卡片样式 */
|
||||||
.article-card {
|
.article-card {
|
||||||
|
text-align: left;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.8rem;
|
gap: 12px;
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
transition: all 0.4s ease;
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文章卡片悬停渐变效果 */
|
||||||
.article-card::before {
|
.article-card::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -147,83 +309,102 @@ onMounted(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(255,255,255,0.6) 0%,
|
rgba(255, 255, 255, 0.6) 0%,
|
||||||
rgba(255,255,255,0.2) 50%,
|
rgba(255, 255, 255, 0.2) 50%,
|
||||||
rgba(255,255,255,0.05) 100%);
|
rgba(255, 255, 255, 0.05) 100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.4s ease;
|
transition: opacity 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文章卡片悬停效果 */
|
||||||
.article-card:hover {
|
.article-card:hover {
|
||||||
transform: translateY(-5px) perspective(2000px) rotateX(0);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25),
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15),
|
||||||
0 0 50px rgba(255, 255, 255, 0.3);
|
0 0 30px rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: rgba(52, 152, 219, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-card:hover::before {
|
.article-card:hover::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章标题 */
|
/* 文章过渡动画 */
|
||||||
|
.article-item {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章标题样式 */
|
||||||
.article-title {
|
.article-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-card:hover .article-title {
|
.article-card:hover .article-title {
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文章元信息 */
|
/* 特殊标记标签样式 */
|
||||||
.article-meta {
|
.article-special-tag {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文章分类 */
|
|
||||||
.article-category {
|
|
||||||
padding: 2px 8px;
|
|
||||||
background-color: rgba(52, 152, 219, 0.1);
|
|
||||||
color: #3498db;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文章标签 */
|
|
||||||
.article-tag {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 3px 10px;
|
||||||
background-color: rgba(52, 152, 219, 0.1);
|
background-color: rgba(52, 152, 219, 0.15);
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
.article-list-content {
|
||||||
/* 文章预览 */
|
position: relative;
|
||||||
.article-preview {
|
padding: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
.pagination-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
/* 文章内容预览样式 */
|
||||||
|
.article-content-preview {
|
||||||
color: #555;
|
color: #555;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
/* 文章元信息容器 */
|
||||||
.empty-container {
|
.article-meta-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态容器样式 */
|
||||||
|
.empty-state-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 过渡动画 */
|
/* 过渡动画配置 */
|
||||||
.article-item-enter-active,
|
.article-item-enter-active,
|
||||||
.article-item-leave-active {
|
.article-item-leave-active {
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
@@ -235,19 +416,54 @@ onMounted(() => {
|
|||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 - 平板和手机 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.article-list-container {
|
||||||
|
}
|
||||||
|
|
||||||
.article-card {
|
.article-card {
|
||||||
padding: 15px;
|
padding: 18px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 18px;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-title {
|
.article-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-meta {
|
.article-meta-info {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-preview {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式元信息分隔符 */
|
||||||
|
.article-views-count::before,
|
||||||
|
.article-category-badge::before,
|
||||||
|
.article-likes-count::before,
|
||||||
|
.article-comments-count::before {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 小屏幕手机 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.article-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-special-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
561
src/views/login.vue
Normal file
561
src/views/login.vue
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-form-wrapper">
|
||||||
|
<!-- 登录表单卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="login-header">
|
||||||
|
<h2 class="login-title">欢迎回来</h2>
|
||||||
|
<p class="login-subtitle">请登录您的账号继续访问</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单区域 -->
|
||||||
|
<form @submit.prevent="handleLogin" class="login-form">
|
||||||
|
<!-- 用户名输入 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<!-- <i class="el-icon-user input-icon"></i> -->
|
||||||
|
<input id="username" v-model="loginForm.username" type="text" placeholder="请输入用户名" class="form-input"
|
||||||
|
:class="{ 'input-error': loginForm.username }" @blur="validateField('username')" />
|
||||||
|
</div>
|
||||||
|
<span v-if="errors.username" class="error-message">{{ errors.username }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码输入 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<!-- <i class="el-icon-lock input-icon"></i> -->
|
||||||
|
<input id="password" v-model="loginForm.password" :type="showPassword ? 'text' : 'password'"
|
||||||
|
placeholder="请输入密码" class="form-input" :class="{ 'input-error': loginForm.password }"
|
||||||
|
@blur="validateField('password')" />
|
||||||
|
<i :class="showPassword ? 'el-icon-view' : 'el-icon-view-off'" class="toggle-password-icon"
|
||||||
|
@click="togglePassword"></i>
|
||||||
|
</div>
|
||||||
|
<span v-if="errors.password" class="error-message">{{ errors.password }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记住我和忘记密码 -->
|
||||||
|
<div class="form-options">
|
||||||
|
<label class="remember-me">
|
||||||
|
<input type="checkbox" v-model="loginForm.rememberMe" class="remember-checkbox" />
|
||||||
|
<span>记住我</span>
|
||||||
|
</label>
|
||||||
|
<a href="#" class="forgot-password" @click.prevent="handleForgotPassword">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<button type="submit" class="login-button" :disabled="isLoading">
|
||||||
|
<span v-if="!isLoading">登录</span>
|
||||||
|
<span v-else class="loading-text">
|
||||||
|
<i class="el-icon-loading"></i> 登录中...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 其他登录方式 -->
|
||||||
|
<div class="other-login-methods">
|
||||||
|
<div class="divider">
|
||||||
|
<span>其他登录方式</span>
|
||||||
|
</div>
|
||||||
|
<div class="social-login-buttons">
|
||||||
|
<button type="button" class="social-button wechat" @click="handleSocialLogin('wechat')">
|
||||||
|
<i class="el-icon-chat-dot-square"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="social-button qq" @click="handleSocialLogin('qq')">
|
||||||
|
<i class="el-icon-message"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="social-button github" @click="handleSocialLogin('github')">
|
||||||
|
<i class="el-icon-document-checked"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 注册提示 -->
|
||||||
|
<div class="register-prompt">
|
||||||
|
<span>还没有账号?</span>
|
||||||
|
<router-link to="/register" class="register-link">立即注册</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { loginService } from '@/services'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
// 全局状态管理
|
||||||
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const errors = ref({
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
})
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换密码显示状态
|
||||||
|
const togglePassword = () => {
|
||||||
|
form.showPassword = !form.showPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证单个字段
|
||||||
|
const validateField = (field) => {
|
||||||
|
// console.log('validateField', field)
|
||||||
|
errors.value = {}
|
||||||
|
|
||||||
|
if (field === 'username' && !loginForm.username) {
|
||||||
|
errors.value.username = '请输入用户名'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'password' && !loginForm.password) {
|
||||||
|
errors.value.password = '请输入密码'
|
||||||
|
} else if (field === 'password' && loginForm.password.length < 6) {
|
||||||
|
errors.value.password = '密码长度至少为6位'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证整个表单
|
||||||
|
const validateForm = () => {
|
||||||
|
errors.value = {}
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
if (!loginForm.username) {
|
||||||
|
errors.value.username = '请输入用户名'
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginForm.password) {
|
||||||
|
errors.value.password = '请输入密码'
|
||||||
|
isValid = false
|
||||||
|
} else if (loginForm.password.length < 6) {
|
||||||
|
errors.value.password = '密码长度至少为6位'
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 模拟登录请求
|
||||||
|
let user = await (await loginService.login(loginForm)).data
|
||||||
|
if (!user) {
|
||||||
|
ElMessage.error('登录失败,请检查用户名和密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// console.log('登录成功', user)
|
||||||
|
// 这里应该是实际的登录API调用
|
||||||
|
// console.log('登录请求数据:', loginForm)
|
||||||
|
|
||||||
|
// 模拟登录成功
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
// 登录成功后,设置全局状态为已登录
|
||||||
|
globalStore.setLoginStatus(true)
|
||||||
|
// 登录成功后,添加全局状态
|
||||||
|
globalStore.setValue('loginhomestatus', {
|
||||||
|
status: 1 // 2:删除 1:已发布 0:发布登录
|
||||||
|
})
|
||||||
|
// console.log('globalStore.Login', globalStore.Login)
|
||||||
|
// 保存登录状态token
|
||||||
|
if (user.token) {
|
||||||
|
localStorage.setItem('token', user.token)
|
||||||
|
}
|
||||||
|
if (user.username) {
|
||||||
|
// 记住用户名
|
||||||
|
globalStore.setUsername(user.username)
|
||||||
|
}
|
||||||
|
// 跳转到首页
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('登录失败,请检查用户名和密码')
|
||||||
|
console.error('登录错误:', error)
|
||||||
|
} finally {
|
||||||
|
globalStore.setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理忘记密码
|
||||||
|
const handleForgotPassword = () => {
|
||||||
|
ElMessage.info('忘记密码功能开发中')
|
||||||
|
}
|
||||||
|
// 处理第三方登录
|
||||||
|
const handleSocialLogin = (provider) => {
|
||||||
|
ElMessage.info(`${provider} 登录功能开发中`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mounted() {
|
||||||
|
// // 从localStorage中恢复用户名
|
||||||
|
// const savedUsername = localStorage.getItem('username')
|
||||||
|
// if (savedUsername) {
|
||||||
|
// loginForm.username = savedUsername
|
||||||
|
// loginForm.rememberMe = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 登录页面容器 */
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 20px;
|
||||||
|
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加背景装饰 */
|
||||||
|
.login-container::before,
|
||||||
|
.login-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container::before {
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container::after {
|
||||||
|
bottom: -100px;
|
||||||
|
left: -100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录表单包装器 */
|
||||||
|
.login-form-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片 */
|
||||||
|
.login-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录头部 */
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 45px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.input-error {
|
||||||
|
border-color: #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password-icon:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f56565;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单选项 */
|
||||||
|
.form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-checkbox {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password:hover {
|
||||||
|
color: #5a67d8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 其他登录方式 */
|
||||||
|
.other-login-methods {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider span {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #e1e5e9;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-login-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button.wechat:hover {
|
||||||
|
border-color: #07c160;
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button.qq:hover {
|
||||||
|
border-color: #1da1f2;
|
||||||
|
color: #1da1f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button.github:hover {
|
||||||
|
border-color: #333;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册提示 */
|
||||||
|
.register-prompt {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link:hover {
|
||||||
|
color: #5a67d8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 30px 20px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-card {
|
||||||
|
padding: 25px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-login-buttons {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/views/markdown.vue
Normal file
116
src/views/markdown.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<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-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 { MdPreview } from 'md-editor-v3'
|
||||||
|
import 'md-editor-v3/lib/style.css'
|
||||||
|
// 定义组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
// 从父组件传入的markdownid内容
|
||||||
|
markdownContent: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const content = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
// 判断内容是否为HTML格式
|
||||||
|
|
||||||
|
// 从API获取markdown内容
|
||||||
|
const loadMarkdown = async () => {
|
||||||
|
// 如果父组件提供了markdown内容,直接使用
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
content.value = props.markdownContent
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : '未知错误'
|
||||||
|
console.error('加载markdown失败:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载markdown
|
||||||
|
onMounted(() => {
|
||||||
|
loadMarkdown()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-viewer {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
box-shadow: 0rgba(0, 0, 0, 0);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
background-color: rgba(255, 255, 255,0);
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态样式 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误状态样式 */
|
||||||
|
.error-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border: 1px solid #fbc4c4;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.v-md-editor.v-md-editor--preview {
|
||||||
|
background-color: rgba(240, 240, 240, 0);
|
||||||
|
}
|
||||||
|
/* 确保编辑器内容在移动设备上正常显示 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.error-state {
|
||||||
|
padding: 30px 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,98 +1,411 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="aericle-style">
|
<div class="nonsense-container">
|
||||||
<div class="aericle-list">
|
<div class="nonsense-list">
|
||||||
<div
|
<!-- 加载状态 -->
|
||||||
class="aericle-item"
|
<div v-if="loading" class="loading-state-container">
|
||||||
v-for="item in aericleList"
|
<el-skeleton :count="5" />
|
||||||
:key="item.id"
|
|
||||||
>
|
|
||||||
<div class="aericle-meta">
|
|
||||||
<span class="aericle-time">{{ item.time }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="aericle-content">{{ item.content }}</div>
|
|
||||||
</div>
|
</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">
|
||||||
|
<span v-if="item.status === 0" class="article-status-badge badge badge-warning">未发表</span>
|
||||||
|
<span v-if="item.status === 1" class="article-status-badge badge badge-success">已发表</span>
|
||||||
|
<span v-if="item.status === 2" class="article-status-badge badge badge-danger">已删除</span>
|
||||||
|
<span class="edit-button" @click="handleEdit(item)">编辑</span>
|
||||||
|
<span class="delete-button" @click="handleDelete(item.id)">删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nonsense-content">
|
||||||
|
<span v-for="(char, index) in item.content.split('')" :key="index" :ref="el => setCharRef(el, item.id, index)"
|
||||||
|
:style="getCharStyle(item.id, index)">{{ char }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
// 分页区域
|
||||||
|
<PaginationComponent class="pagination-container" :list="nonsenseList" :pageSize="10"
|
||||||
|
@changePage="handleCurrentDataUpdate" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
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()
|
||||||
|
/**
|
||||||
|
* 吐槽数据列表
|
||||||
|
* 仅站长可见/可发
|
||||||
|
*/
|
||||||
|
const nonsenseList = ref([])
|
||||||
|
|
||||||
// 吐槽数据(仅站长可见/可发)
|
// 存储字符引用和样式的映射
|
||||||
const aericleList = ref([
|
const charRefs = ref(new Map())
|
||||||
{
|
const charStyles = ref(new Map())
|
||||||
id: 1,
|
// 显示的吐槽内容列表
|
||||||
content: '嘿嘿 嘿嘿嘿(流口水ing)',
|
const displayedNonsenseList = ref([])
|
||||||
time: '2025-09-26 09:30'
|
// 加载状态
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载所有吐槽内容
|
||||||
|
*/
|
||||||
|
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 (err) {
|
||||||
|
console.error('加载吐槽内容失败:', err)
|
||||||
|
error.value = true
|
||||||
|
} finally {
|
||||||
|
// 结束加载状态
|
||||||
|
loading.value = false
|
||||||
|
// console.log('加载吐槽内容完成')
|
||||||
}
|
}
|
||||||
])
|
}
|
||||||
|
// 编辑吐槽内容
|
||||||
|
const handleEdit = (item) => {
|
||||||
|
// 清除更新文章状态
|
||||||
|
|
||||||
|
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
|
||||||
|
confirmButtonText: '保存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputValue: item.content,
|
||||||
|
inputType: 'textarea',
|
||||||
|
inputRows: 4,
|
||||||
|
showCancelButton: true
|
||||||
|
}).then(({ value }) => {
|
||||||
|
// 保存疯言疯语
|
||||||
|
updateNonsense(value, item.id)
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消操作,静默处理
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 更新吐槽内容
|
||||||
|
const updateNonsense = (content, id) => {
|
||||||
|
if (!content || content.trim() === '') {
|
||||||
|
ElMessage.warning('内容不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 调用服务更新疯言疯语
|
||||||
|
nonsenseService.updateNonsense({
|
||||||
|
id,
|
||||||
|
content: content.trim(),
|
||||||
|
time: new Date(),
|
||||||
|
status: 1
|
||||||
|
}).then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('疯言疯语更新成功')
|
||||||
|
loadNonsenseList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '更新失败')
|
||||||
|
}
|
||||||
|
}).catch(err => handleErrorResponse(err, '更新失败'))
|
||||||
|
}
|
||||||
|
// 删除吐槽内容
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
// 确认删除
|
||||||
|
const confirm = window.confirm('确定删除吗?')
|
||||||
|
if (!confirm) return
|
||||||
|
const response = await nonsenseService.deleteNonsense(id)
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadNonsenseList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置字符引用
|
||||||
|
const setCharRef = (el, itemId, index) => {
|
||||||
|
if (el) {
|
||||||
|
const key = `${itemId}_${index}`
|
||||||
|
charRefs.value.set(key, el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字符样式
|
||||||
|
const getCharStyle = (itemId, index) => {
|
||||||
|
const key = `${itemId}_${index}`
|
||||||
|
return charStyles.value.get(key) || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机颜色
|
||||||
|
const getRandomColor = () => {
|
||||||
|
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#6c5ce7', '#e84393', '#00b894', '#fdcb6e']
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机改变部分字体颜色并添加信号故障效果
|
||||||
|
const randomChangeColors = () => {
|
||||||
|
const keys = Array.from(charRefs.value.keys())
|
||||||
|
if (keys.length === 0) return
|
||||||
|
|
||||||
|
// 随机选择20-30%的字符改变颜色
|
||||||
|
const countToChange = Math.floor(keys.length * (Math.random() * 0.1 + 0.2))
|
||||||
|
const shuffledKeys = [...keys].sort(() => 0.5 - Math.random())
|
||||||
|
const selectedKeys = shuffledKeys.slice(0, countToChange)
|
||||||
|
|
||||||
|
// 创建信号故障效果
|
||||||
|
createSignalGlitchEffect(selectedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数字噪点效果
|
||||||
|
const createDigitalNoiseEffect = (selectedKeys) => {
|
||||||
|
// 噪点字符集
|
||||||
|
const noiseChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
|
selectedKeys.forEach(key => {
|
||||||
|
const charElement = charRefs.value.get(key);
|
||||||
|
if (charElement) {
|
||||||
|
const rect = charElement.getBoundingClientRect();
|
||||||
|
const container = charElement.closest('.nonsense-item');
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 生成3-5个噪点
|
||||||
|
const noiseCount = Math.floor(Math.random() * 3) + 3;
|
||||||
|
|
||||||
|
for (let i = 0; i < noiseCount; i++) {
|
||||||
|
const noiseEl = document.createElement('span');
|
||||||
|
noiseEl.textContent = noiseChars.charAt(Math.floor(Math.random() * noiseChars.length));
|
||||||
|
|
||||||
|
// 随机位置相对于原字符
|
||||||
|
const x = Math.random() * 30 - 20; // -20到20px
|
||||||
|
const y = Math.random() * 20 - 15; // -15到15px
|
||||||
|
|
||||||
|
// 随机大小和透明度
|
||||||
|
const size = Math.random() * 8 + 8; // 8-16px
|
||||||
|
const opacity = Math.random() * 0.6 + 0.2; // 0.2-0.8
|
||||||
|
|
||||||
|
noiseEl.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
left: ${rect.left - containerRect.left + x}px;
|
||||||
|
top: ${rect.top - containerRect.top + y}px;
|
||||||
|
font-size: ${size}px;
|
||||||
|
opacity: ${opacity};
|
||||||
|
color: ${getRandomColor()};
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
font-family: monospace;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
container.appendChild(noiseEl);
|
||||||
|
|
||||||
|
// 噪点逐渐消失
|
||||||
|
setTimeout(() => {
|
||||||
|
noiseEl.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (container.contains(noiseEl)) {
|
||||||
|
container.removeChild(noiseEl);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}, Math.random() * 300 + 200); // 200-500ms后开始消失
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建信号故障效果
|
||||||
|
const createSignalGlitchEffect = (selectedKeys) => {
|
||||||
|
// 第一步:随机偏移字符位置,模拟信号干扰
|
||||||
|
selectedKeys.forEach(key => {
|
||||||
|
const glitchOffset = {
|
||||||
|
transform: `translate(${Math.random() * 6 - 3}px, ${Math.random() * 6 - 3}px)`,
|
||||||
|
opacity: Math.random() * 0.4 + 0.6, // 随机透明度
|
||||||
|
transition: 'all 0.1s ease'
|
||||||
|
}
|
||||||
|
charStyles.value.set(key, glitchOffset)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 同时创建数字噪点效果
|
||||||
|
createDigitalNoiseEffect(selectedKeys);
|
||||||
|
|
||||||
|
// 第二步:快速恢复并闪烁
|
||||||
|
setTimeout(() => {
|
||||||
|
selectedKeys.forEach(key => {
|
||||||
|
const flashStyle = {
|
||||||
|
transform: 'translate(0, 0)',
|
||||||
|
opacity: Math.random() > 0.5 ? 0 : 1, // 闪烁效果
|
||||||
|
transition: 'all 0.05s ease'
|
||||||
|
}
|
||||||
|
charStyles.value.set(key, flashStyle)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 第三步:最终设置新颜色
|
||||||
|
setTimeout(() => {
|
||||||
|
selectedKeys.forEach(key => {
|
||||||
|
const finalStyle = {
|
||||||
|
color: getRandomColor(),
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translate(0, 0)',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}
|
||||||
|
charStyles.value.set(key, finalStyle)
|
||||||
|
})
|
||||||
|
}, 80)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时获取数据并启动定时器
|
||||||
|
onMounted(() => {
|
||||||
|
loadNonsenseList()
|
||||||
|
|
||||||
|
// 启动定时器
|
||||||
|
colorChangeTimer = setInterval(randomChangeColors, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清除定时器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (colorChangeTimer) {
|
||||||
|
clearInterval(colorChangeTimer)
|
||||||
|
colorChangeTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#aericle-style {
|
/* 吐槽页面主容器样式 */
|
||||||
background: rgba(255,255,255,0.95);
|
.nonsense-container {
|
||||||
|
/* background-color: rgba(255, 255, 255, 0.85); */
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 32px 20px 24px 20px;
|
/* box-shadow: 0 2px 12px rgba(0,0,0,0.06); */
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
transition: box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aericle-header {
|
/* 吐槽页面主容器悬浮效果 */
|
||||||
|
.nonsense-container:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 吐槽头部样式 */
|
||||||
|
.nonsense-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
.aericle-header h1 {
|
|
||||||
|
.nonsense-header h1 {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
.aericle-desc {
|
|
||||||
|
.nonsense-description {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aericle-list {
|
/* 吐槽列表样式 */
|
||||||
|
.nonsense-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aericle-item {
|
/* 吐槽项样式 */
|
||||||
background: #f8fafd;
|
.nonsense-item {
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 18px 20px 14px 20px;
|
padding: 18px 20px 14px 20px;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.03);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s, transform 0.2s ease;
|
||||||
}
|
|
||||||
.aericle-item:hover {
|
|
||||||
box-shadow: 0 4px 16px rgba(64,158,255,0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aericle-meta {
|
/* 吐槽项悬浮效果 */
|
||||||
|
.nonsense-item:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 吐槽元信息样式 */
|
||||||
|
.nonsense-meta-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aericle-content {
|
.nonsense-meta-info span {
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 吐槽内容样式 */
|
||||||
|
.nonsense-content {
|
||||||
font-size: 1.08rem;
|
font-size: 1.08rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#aericle-style {
|
.nonsense-container {
|
||||||
padding: 14px 4px 10px 4px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.aericle-header h1 {
|
|
||||||
|
.nonsense-header h1 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
.aericle-content {
|
|
||||||
|
.nonsense-content {
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .nonsense-list {
|
||||||
|
gap: 12px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.nonsense-item {
|
||||||
|
padding: 14px 16px 10px 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
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>
|
||||||
@@ -1,18 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="max-width: 600px">
|
<div class="alert-demo-container">
|
||||||
<el-alert title="Primary alert" type="primary" />
|
<!-- 主要警告 -->
|
||||||
<el-alert title="Success alert" type="success" />
|
<el-alert title="主要警告" type="primary" />
|
||||||
<el-alert title="Info alert" type="info" />
|
|
||||||
<el-alert title="Warning alert" type="warning" />
|
<!-- 成功警告 -->
|
||||||
<el-alert title="Error alert" type="error" />
|
<el-alert title="成功警告" type="success" />
|
||||||
|
|
||||||
|
<!-- 信息警告 -->
|
||||||
|
<el-alert title="信息警告" type="info" />
|
||||||
|
|
||||||
|
<!-- 警告提醒 -->
|
||||||
|
<el-alert title="警告提醒" type="warning" />
|
||||||
|
|
||||||
|
<!-- 错误警告 -->
|
||||||
|
<el-alert title="错误警告" type="error" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-alert {
|
/* 警告框演示容器样式 */
|
||||||
margin: 20px 0 0;
|
.alert-demo-container {
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
.el-alert:first-child {
|
|
||||||
|
/* 警告框样式 */
|
||||||
|
.alert-demo-container .el-alert {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第一个警告框的特殊样式(移除顶部外边距) */
|
||||||
|
.alert-demo-container .el-alert:first-child {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* 警告框悬浮效果 */
|
||||||
|
.alert-demo-container .el-alert:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.alert-demo-container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-demo-container .el-alert {
|
||||||
|
margin: 15px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,7 +26,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
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