Compare commits

...

11 Commits

Author SHA1 Message Date
qingfeng1121
8193bab566 refactor(services): 移除未使用的服务方法以简化代码结构
清理多个服务文件中未使用或重复的方法,包括登录、分类、分类属性和留言服务
2025-12-18 15:37:17 +08:00
qingfeng1121
f4263af343 refactor(前端): 重构前端代码结构并优化功能
重构路由配置和API调用逻辑,统一分页处理方式
优化分类和标签模块的交互,提取蒙版组件到主布局
调整样式和布局,增强响应式设计
更新接口字段名以保持前后端一致性
添加网站运行时间显示功能
2025-12-18 15:20:14 +08:00
qingfeng1121
0dc24cfa85 refactor(layout): 优化 hero 区域滚动效果和样式
移除 hero 区域的顶部 margin 控制,改为使用 transform 实现平滑滚动
简化滚动逻辑,调整 hero 区域在不同页面的显示效果
更新 CSS 变量定义,移除无用样式
2025-12-18 15:19:46 +08:00
qingfeng1121
183d98a699 ci: 更新API代理目标地址为生产环境
将开发环境的本地代理地址改为线上生产环境地址,以便于测试和生产环境对接
2025-12-15 17:54:13 +08:00
qingfeng1121
ede67faafd feat: 优化前端布局和代理配置
refactor: 移除调试日志并优化代码结构
style: 调整响应式设计和UI细节
fix: 修复路由和导航相关的问题
2025-12-12 17:14:04 +08:00
qingfeng1121
07ce8409e1 fix: 更新API基础URL为www.qf1121.top
修改API基础URL以使用www子域名,确保请求能够正确路由
2025-12-11 14:48:08 +08:00
qingfeng1121
0cbb91077d feat: 添加页脚组件并更新多个功能
- 新增Footer组件显示版权信息和备案号
- 替换favicon为blogicon.jpg
- 更新API基础URL为生产环境
- 重命名nonsenseService方法为createNonsense
- 在文章编辑页添加返回列表按钮
- 优化分类和标签创建后的页面跳转逻辑
- 移除home.vue中不必要的height样式
2025-12-11 12:42:54 +08:00
qingfeng1121
fc581b0476 docs(aboutme): 更新个人介绍内容和风格
重构个人介绍部分,简化内容并采用更简洁的表达风格。移除冗余的回忆描述,保留核心的个人名称演变过程和疯言疯语部分,使页面更加聚焦和个性化。
2025-11-15 11:46:39 +08:00
qingfeng1121
109ac3c009 feat(文章编辑): 添加文章简介输入框并优化样式
- 在文章编辑页面新增简介输入区域
- 移除home.vue中多余的webkit-line-clamp属性
- 统一代码格式和间距
2025-11-14 15:42:27 +08:00
qingfeng1121
1dc5bdd93f refactor(views): 重构多个视图组件代码结构,优化类型定义和逻辑组织
feat(services): 新增文章分页查询方法,支持按状态筛选文章

style(styles): 调整主布局样式,优化分页组件显示效果

docs(README): 更新API文档,完善服务模块说明和类型定义

fix(components): 修复左侧模块点击属性时使用错误字段名的问题

chore(package): 移除未使用的依赖项,清理项目依赖

perf(layouts): 优化主布局组件性能,拆分功能模块,减少重复计算

test(views): 为分页组件添加基础测试用例

build: 更新构建配置,优化生产环境打包

ci: 调整CI配置,添加类型检查步骤
2025-11-14 15:30:29 +08:00
qingfeng1121
4ae0ff7c2a feat(establish): 添加标签创建功能及模态框组件
- 在establish组件中新增标签创建功能
- 添加标签创建模态框及相关样式
- 实现分类选择下拉框和标签名称输入
- 完善模态框的显示/隐藏逻辑
- 调整部分样式以优化用户体验
2025-11-09 16:27:34 +08:00
31 changed files with 2941 additions and 3061 deletions

View File

@@ -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: number0未发表 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: number1:已发表, 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 // 回复列表
}
```

View File

@@ -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>

1415
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,9 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@vicons/ionicons5": "^0.13.0",
"ant-design-vue": "^4.2.6",
"antd": "^5.27.3",
"axios": "^1.12.2", "axios": "^1.12.2",
"element-plus": "^2.11.5", "element-plus": "^2.11.5",
"md-editor-v3": "^6.1.0", "md-editor-v3": "^6.1.0",
"naive-ui": "^2.43.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sass": "^1.93.2", "sass": "^1.93.2",
"undraw-ui": "^1.3.2", "undraw-ui": "^1.3.2",

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

View File

@@ -34,12 +34,12 @@
</div> </div>
</div> </div>
<div id="bot" :class="{ 'botrelative': scrollY }"> <div id="bot" :class="{ 'botrelative': scrollY }">
<el-tabs v-model="activeName" stretch="true" class="demo-tabs"> <el-tabs v-model="activeName" :stretch="true" class="demo-tabs">
<el-tab-pane label="个人简介" name="first"> <el-tab-pane label="个人简介" name="first">
<div class="mylogo"> <div class="mylogo">
<el-avatar class="mylogo_avatar" :src="state.circleUrl" /> <el-avatar class="mylogo_avatar" :src="state.circleUrl" />
</div> </div>
<a href="#"> <a href="http://www.qf1121.top/">
<h6 class="mylogo_name logo-text">清疯不颠</h6> <h6 class="mylogo_name logo-text">清疯不颠</h6>
</a> </a>
<h6 class="mylogo_description">重度精神失常患者</h6> <h6 class="mylogo_description">重度精神失常患者</h6>
@@ -51,13 +51,13 @@
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link" @click.prevent="showCategories"> <a href="#" class="stat-link" @click.prevent="showCategoriesModel">
<span class="site-state-item-count">{{ categoryCount }}</span> <span class="site-state-item-count">{{ categoryCount }}</span>
<span class="site-state-item-name">分类</span> <span class="site-state-item-name">分类</span>
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link" @click.prevent="showAttributes"> <a href="#" class="stat-link" @click.prevent="showAttributesModel">
<span class="site-state-item-count">{{ AttributeCount }}</span> <span class="site-state-item-count">{{ AttributeCount }}</span>
<span class="site-state-item-name">标签</span> <span class="site-state-item-name">标签</span>
</a> </a>
@@ -65,53 +65,12 @@
</div> </div>
</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>
<!-- 分类蒙板组件 -->
<Transition name="modal">
<div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有分类</h3>
<button class="category-modal-close" @click="closeCategoryModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="category in categories"
:key="category.typeid"
class="category-button"
@click="handleCategoryClick(category)"
>
{{ category.typename }} <span class="category-button-count">({{ category.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
<!-- 标签蒙板组件 -->
<Transition name="modal">
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有标签</h3>
<button class="category-modal-close" @click="closeAttributeModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="attribute in attributes"
:key="attribute.attributeid"
class="category-button"
@click="handleAttributeClick(attribute)"
>
{{ attribute.attributename }} <span class="category-button-count">({{ attribute.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
</div> </div>
</template> </template>
@@ -119,27 +78,22 @@
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"; import { articleService, categoryService, categoryAttributeService } from "@/services";
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 当前激活菜单 // 当前激活菜单
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,
}) })
// 分类相关状态 // 分类相关状态
const categories = ref<any[]>([]) // defineEmits
const showCategoryModal = ref(false) const emit = defineEmits(['update-data', 'CategoryModal', 'AttributeModal'])
// 标签相关状态 // 标签相关状态
const attributes = ref<any[]>([])
const showAttributeModal = ref(false)
// 处理菜单选择跳转 // 处理菜单选择跳转
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
router.push({ path: key }) router.push({ path: key })
@@ -159,9 +113,9 @@ const AttributeCount = ref(0)
// 获取文章数量 // 获取文章数量
const fetchArticleCount = async () => { const fetchArticleCount = async () => {
try { try {
const response = await articleService.getAllArticles(); const response = await articleService.getAllArticles();
articleCount.value = response.data?.length || 0 articleCount.value = response.data?.length || 0
} catch (error) { } catch (error) {
console.error('获取文章数量失败:', error) console.error('获取文章数量失败:', error)
articleCount.value = 0 articleCount.value = 0
@@ -170,15 +124,18 @@ const fetchArticleCount = async () => {
// 获取分类数据 // 获取分类数据
const fetchCategories = async () => { const fetchCategories = async () => {
try {
// 分类数据状态
const categories = ref<any[]>([])
try {
const response = await categoryService.getAllCategories(); const response = await categoryService.getAllCategories();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
categories.value = response.data?.map((category: any) => ({ categories.value = response.data?.map((category: any) => ({
...category, ...category,
count: 0 count: 0
})) || []; })) || [];
categories.value.forEach(async (category: any) => { categories.value.forEach(async (category: any) => {
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.typeid) const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.categoryid)
if (attributeResponse.data?.length) { if (attributeResponse.data?.length) {
category.count = attributeResponse.data?.length || 0 category.count = attributeResponse.data?.length || 0
} }
@@ -189,20 +146,23 @@ const fetchCategories = async () => {
console.error('获取分类失败:', error) console.error('获取分类失败:', error)
// 如果API调用失败使用模拟数据 // 如果API调用失败使用模拟数据
categories.value = [ categories.value = [
]; ];
categoryCount.value = categories.value.length categoryCount.value = categories.value.length
} }
return categories.value
} }
// 获取标签数据 // 获取标签数据
const fetchAttributes = async () => { const fetchAttributes = async () => {
try { // 标签数据状态
const attributes = ref<any[]>([])
try {
const response = await categoryAttributeService.getAllAttributes(); const response = await categoryAttributeService.getAllAttributes();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
attributes.value = response.data?.map((attribute: any) => ({ attributes.value = response.data?.map((attribute: any) => ({
...attribute, ...attribute,
count: 0 count: 0
})) || []; })) || [];
attributes.value.forEach(async (attribute: any) => { attributes.value.forEach(async (attribute: any) => {
const articleResponse = await articleService.getArticlesByAttributeId(attribute.attributeid) const articleResponse = await articleService.getArticlesByAttributeId(attribute.attributeid)
@@ -215,56 +175,27 @@ const fetchAttributes = async () => {
console.error('获取标签失败:', error) console.error('获取标签失败:', error)
// 如果API调用失败使用模拟数据 // 如果API调用失败使用模拟数据
attributes.value = [ attributes.value = [
]; ];
AttributeCount.value = attributes.value.length AttributeCount.value = attributes.value.length
} }
return attributes.value
} }
// 显示分类蒙板 // 向父组件传递标签数据
const showCategories = () => { const sendData = () => {
showCategoryModal.value = true const data = { fetchAttributes: fetchAttributes(), fetchCategories: fetchCategories() }
emit('update-data', data)
} }
// 关闭分类蒙板 // 显示标签蒙版
const closeCategoryModal = () => { const showAttributesModel = () => {
showCategoryModal.value = false emit('AttributeModal', { ifmodal: true })
}
// 显示分类蒙版
const showCategoriesModel = () => {
emit('CategoryModal', { ifmodal: true })
} }
// 处理分类点击
const handleCategoryClick = (category: any) => {
// 这里可以根据实际需求跳转到对应分类的文章列表页
console.log('点击了分类:', category.typename)
// 示例router.push(`/article-list?category=${category.typeid}`)
closeCategoryModal()
}
// 显示标签蒙板
const showAttributes = () => {
showAttributeModal.value = true
}
// 关闭标签蒙板
const closeAttributeModal = () => {
showAttributeModal.value = false
}
// 处理标签点击
const handleAttributeClick = (attribute: any) => {
// 重置全局属性状态
globalStore.removeValue('attribute')
globalStore.setValue('attribute', {
id: attribute.attributeid,
name: attribute.typename
})
console.log(attribute)
router.push({
path: '/home/aericletype',
})
closeAttributeModal()
}
// 控制底部模块吸顶效果 // 控制底部模块吸顶效果
const scrollY = ref(false) const scrollY = ref(false)
@@ -276,8 +207,7 @@ const handleScroll = () => {
onMounted(() => { onMounted(() => {
window.addEventListener('scroll', handleScroll) window.addEventListener('scroll', handleScroll)
fetchArticleCount() // 组件挂载时获取文章数量 fetchArticleCount() // 组件挂载时获取文章数量
fetchCategories() // 组件挂载时获取分类数据 sendData() // 组件挂载时获取标签数据和分类数据
fetchAttributes() // 组件挂载时获取标签数据
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -313,15 +243,17 @@ onUnmounted(() => {
/* 内容区域样式 */ /* 内容区域样式 */
#cont { #cont {
padding:0 0 10px 0; 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{
#cont .cont1 {
margin-bottom: 5px; margin-bottom: 5px;
} }
#cont .cont2{
#cont .cont2 {
margin-bottom: 0px; margin-bottom: 0px;
} }
@@ -348,16 +280,12 @@ onUnmounted(() => {
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
/* 白色半透明背景 */ /* 白色半透明背景 */
} }
.cont2 .el-menu-vertical-demo li { .cont2 .el-menu-vertical-demo li {
font-size: 14px; font-size: 14px;
height: 35px; height: 35px;
} }
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) {
/* border-radius: 0 0 10px 10px; */
/* margin-bottom: 10px; */
}
.cont2 .el-menu-vertical-demo .el-menu-item:hover { .cont2 .el-menu-vertical-demo .el-menu-item:hover {
background-color: rgba(64, 158, 255, 0.9); background-color: rgba(64, 158, 255, 0.9);
} }
@@ -428,11 +356,12 @@ onUnmounted(() => {
/* 白色半透明背景 */ /* 白色半透明背景 */
padding: 10px; padding: 10px;
} }
.site-state-item-count { .site-state-item-count {
display: block; display: block;
text-align: center; text-align: center;
color: #32325d; color: #32325d;
font-weight: bold; font-weight: bold;
} }
.demo-tabs { .demo-tabs {
@@ -443,14 +372,16 @@ onUnmounted(() => {
margin-left: 100px; margin-left: 100px;
padding: 10px 20px; padding: 10px 20px;
} }
.mylogo_name { .mylogo_name {
font-size: 13px; font-size: 13px;
} }
.mylogo_description { .mylogo_description {
font-size: 13px; font-size: 13px;
opacity: 0.8; opacity: 0.8;
color: #c21f30; color: #c21f30;
margin: 10px 0; margin: 10px 0;
} }
.stat-container { .stat-container {
@@ -507,141 +438,6 @@ onUnmounted(() => {
transform: translateY(0); transform: translateY(0);
} }
} }
/* 分类蒙板样式 */
.category-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.category-modal-content {
background-color: white;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.category-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.category-modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.category-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.category-modal-close:hover {
background-color: #f5f5f5;
color: #333;
}
.category-modal-body {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
max-height: 60vh;
overflow-y: auto;
}
.category-button {
background-color: rgba(102, 161, 216, 0.1);
border: 1px solid rgba(102, 161, 216, 0.3);
border-radius: 6px;
padding: 10px 15px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
color: #333;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.category-button:hover {
background-color: rgba(102, 161, 216, 0.3);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(102, 161, 216, 0.2);
}
.category-button-count {
font-size: 12px;
color: #66a1d8;
font-weight: 500;
}
/* 蒙板动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-active .category-modal-content,
.modal-leave-active .category-modal-content {
transition: transform 0.3s ease;
}
.modal-enter-from .category-modal-content {
transform: scale(0.9);
}
.modal-leave-to .category-modal-content {
transform: scale(0.9);
}
/* 滚动条样式 */
.category-modal-body::-webkit-scrollbar {
width: 6px;
}
.category-modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style> </style>

View File

@@ -3,7 +3,7 @@
<el-row justify="center"> <el-row justify="center">
<el-col :span="6" 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">
@@ -31,92 +31,134 @@
<!-- 搜索功能 --> <!-- 搜索功能 -->
<div class="search-wrapper"> <div class="search-wrapper">
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }"> <button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
<svg t="1761567058506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5586" width="32" height="32"><path d="M512 858.3168c-194.816 0-352-166.2464-352-370.4832S317.184 117.3504 512 117.3504s352 166.2464 352 370.4832-157.184 370.4832-352 370.4832z m0-64c158.6688 0 288-136.8576 288-306.4832 0-169.6768-129.3312-306.4832-288-306.4832S224 318.1568 224 487.8336c0 169.6256 129.3312 306.4832 288 306.4832zM717.312 799.9488a32 32 0 0 1 46.4896-43.9808l91.4432 96.7168a32 32 0 0 1-46.4896 43.9808l-91.4432-96.768z" fill="#5A5A68" p-id="5587"></path></svg> <svg t="1761567058506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="5586" width="32" height="32">
<path
d="M512 858.3168c-194.816 0-352-166.2464-352-370.4832S317.184 117.3504 512 117.3504s352 166.2464 352 370.4832-157.184 370.4832-352 370.4832z m0-64c158.6688 0 288-136.8576 288-306.4832 0-169.6768-129.3312-306.4832-288-306.4832S224 318.1568 224 487.8336c0 169.6256 129.3312 306.4832 288 306.4832zM717.312 799.9488a32 32 0 0 1 46.4896-43.9808l91.4432 96.7168a32 32 0 0 1-46.4896 43.9808l-91.4432-96.768z"
fill="#5A5A68" p-id="5587"></path>
</svg>
</button> </button>
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }"> <div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
<el-input <el-input v-model="searchKeyword" placeholder="回车搜索文章..." class="search-input" @keyup.enter="performSearch"
v-model="searchKeyword" @blur="closeSearchBoxWithDelay" />
placeholder="搜索文章..."
class="search-input"
@keyup.enter="performSearch"
@blur="closeSearchBoxWithDelay"
/>
</div> </div>
</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>{{Cardtitle}}</h1> <h1>{{ Cardtitle }}</h1>
</div> </div>
</div> </div>
<!-- 左侧模块 --> <!-- 左侧模块 -->
<div class="leftmodluecontainer" v-if="isleftmodluecontainer"> <div class="leftmodluecontainer" v-if="isleftmodluecontainer">
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classmoduleorrouter}" v-if="windowwidth" /> <LeftModule class="leftmodluepage" @update-data="updateData" @CategoryModal="CategoryModal"
</div> @AttributeModal="AttributeModal" :class="{ 'nonsensetmargintop': classmoduleorrouter }" v-if="windowwidth" />
</div>
<!-- 内容模块 --> <!-- 内容模块 -->
<RouterView class="RouterViewpage" :class="{'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" /> <div class="RouterViewpage">
<RouterView :class="{ 'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
<!-- 页脚 -->
<Footer class="footer-container" v-if="windowwidth" />
</div>
</div> </div>
<!-- 分类蒙板组件 -->
<!-- 分页区域 --> <Transition name="modal">
<div class="Pagination"> <div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
<!-- 分页组件可以在这里添加 --> <div class="category-modal-content">
</div> <div class="category-modal-header">
<Establish class="establish-container" v-if="Login" /> <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, computed } 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 Establish from '@/layouts/establish.vue';
import Footer from '@/views/Footer.vue';
// ========== 组件初始化 ==========
// 路由相关 // 路由相关
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// 全局状态管理 // 全局状态管理
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore';
const globalStore = useGlobalStore();
const Login = computed(() => globalStore.Login);
const globalStore = useGlobalStore() // ========== 响应式状态定义 ==========
const Login = computed(() => globalStore.Login) // 页面标题和样式相关状态
// 响应式状态
// 文章标题,用于显示在页面上的标题内容
const Cardtitle = ref(''); const Cardtitle = ref('');
// 控制模块或路由相关的CSS类名
const classmoduleorrouter = ref(false); const classmoduleorrouter = ref(false);
// 控制左侧模块容器是否显示
const isleftmodluecontainer = ref(true);
// 控制hero区域的CSS类名
const classhero = ref(false);
// 控制内容区域是否可见
const isconts = ref(false);
// 控制左侧模块是否处于滚动状态
const isScrollingleftmodlue = ref(false);
// 顶部导航栏的样式状态transparent/solid/hide
const elrowtop = ref('transparent');
// 控制疯言疯语页面标题区域的显示
const classnonsenset = ref(false); const classnonsenset = ref(false);
// 判断窗口是否为宽屏大于768px const classsmallhero = ref(false);
const windowwidth = ref(true); const elrowtop = ref('transparent');
const activeIndex = ref('home'); // hero区域的margin值用于实现滚动时动态变化
const localhome= 'home'; const heroMarginBottom = ref('45%');
// hero区域的初始margin值从CSS变量获取
const initialHeroMarginBottom = 45;
// hero是否开始向上移动
const heroIsMoving = ref(false);
// hero的transform值用于实现向上移动和吸附效果
const heroTransform = ref('translateY(450px)');
const heroTransformValue = 450;
// hero的位置状态static(静态)、moving(移动中)、sticky(吸附顶部)
const heroPosition = ref('static');
// 布局相关状态
const isleftmodluecontainer = ref(true);
const iscontentvisible = ref(false);
const isScrollingleftmodlue = ref(false);
const windowwidth = ref(true);
// 导航相关状态
const activeIndex = ref('home');
const localhome = 'home';
let rpsliturl = route.path.split('/'); let rpsliturl = route.path.split('/');
// 搜索相关状态 // 搜索相关状态
@@ -130,37 +172,226 @@ 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) => {
// globalStore.clearAll()
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 = () => {
// 根据是否为主页根路径设置hero区域状态
classsmallhero.value = !(rpsliturl[1] == localhome && rpsliturl[2] == undefined);
// 控制左侧模块容器的显示/隐藏
isleftmodluecontainer.value = rpsliturl[1] !== "articlesave";
};
/**
* 更新文章标题和相关显示状态
*/
const updateArticleTitle = () => {
let articledata: any = null;
// 根据不同路由参数获取文章标题数据
if (rpsliturl[2] === 'aericleattribute') {
// 按属性类型获取
articledata = globalStore.getValue('attribute')?.name;
}
else if (rpsliturl[2] === 'aericletitle') {
// 按标题搜索获取
articledata = globalStore.getValue('articleserarch')?.name;
}
else if (rpsliturl[1] === 'nonsense') {
// 疯言疯语页面特殊处理
articledata = "疯言疯语";
}
// 确定标题区域的显示状态
const shouldHideTitle =
// 特殊页面不需要显示标题
(rpsliturl[1] === 'articlelist' ||
rpsliturl[1] === 'message' ||
rpsliturl[1] === 'about') ||
// 在主页且无标题数据时,不显示标题
(rpsliturl[1] === localhome && !articledata);
// 更新标题显示状态
classnonsenset.value = !shouldHideTitle && !!articledata;
classmoduleorrouter.value = !shouldHideTitle && !!articledata;
// 设置标题内容
if (articledata) {
Cardtitle.value = articledata;
}
};
// ========== 搜索功能模块 ==========
/**
* 切换搜索框的显示/隐藏状态
*/ */
const toggleSearchBox = () => { const toggleSearchBox = () => {
isSearchBoxOpen.value = !isSearchBoxOpen.value; isSearchBoxOpen.value = !isSearchBoxOpen.value;
// 如果打开搜索框,清除之前的延时关闭定时器 // 如果打开搜索框,清除之前的延时关闭定时器
if (isSearchBoxOpen.value && searchCloseTimer) { if (isSearchBoxOpen.value && searchCloseTimer) {
clearTimeout(searchCloseTimer); clearTimeout(searchCloseTimer);
@@ -168,20 +399,23 @@ const toggleSearchBox = () => {
}; };
/** /**
* 关闭搜索框 * 立即关闭搜索框
*/ */
const closeSearchBox = () => { const closeSearchBox = () => {
isSearchBoxOpen.value = false; isSearchBoxOpen.value = false;
}; };
/** /**
* 带延迟关闭搜索框(处理点击搜索按钮后的情况) * 带延迟关闭搜索框
* 用于处理点击搜索按钮后的情况,让用户有时间看到反馈
*/ */
const closeSearchBoxWithDelay = () => { const closeSearchBoxWithDelay = () => {
// 清除可能存在的定时器
if (searchCloseTimer) { if (searchCloseTimer) {
clearTimeout(searchCloseTimer); clearTimeout(searchCloseTimer);
} }
// 设置新的延迟关闭定时器
searchCloseTimer = window.setTimeout(() => { searchCloseTimer = window.setTimeout(() => {
isSearchBoxOpen.value = false; isSearchBoxOpen.value = false;
}, 300); }, 300);
@@ -189,171 +423,206 @@ const closeSearchBoxWithDelay = () => {
/** /**
* 执行搜索操作 * 执行搜索操作
* 将搜索关键词存储到全局状态并跳转到搜索结果页面
*/ */
const performSearch = () => { const performSearch = () => {
// 验证搜索关键词不为空
if (searchKeyword.value.trim()) { if (searchKeyword.value.trim()) {
// 这里可以根据实际需求实现搜索逻辑 // 清除全局搜索关键词
// 存储文章信息到全局状态 globalStore.removeValue('articleserarch');
globalStore.setValue('articleserarch', { // 存储搜索关键词到全局状态
name: searchKeyword.value globalStore.setValue('articleserarch', {
}) name: searchKeyword.value
router.push({ path: `/home/aericletitle`}); });
// 跳转到搜索结果页面
router.push({ path: `/home/aericletitle` });
} }
// 搜索后保持搜索框打开状态 // 搜索后保持搜索框打开状态
if (searchCloseTimer) { if (searchCloseTimer) {
clearTimeout(searchCloseTimer); clearTimeout(searchCloseTimer);
} }
}; };
/** // ========== 响应式处理模块 ==========
* 根据路由路径设置页面状态
*/
const updatePageState = () => {
if (rpsliturl[1] == localhome && rpsliturl[2] == undefined) {
classhero.value = false;
} else {
classhero.value = true;
}
};
/**
* 设置当前激活的菜单项
*/
const setActiveIndex = (path: string) => {
// 存储文章信息到全局状态
globalStore.setValue('localpath', {
name: path
})
if (path === 'message') {
globalStore.removeValue('articleInfo')
}
activeIndex.value =path;
};
/** /**
* 处理窗口大小变化 * 处理窗口大小变化
* 根据屏幕宽度调整布局和内容显示
*/ */
const handleResize = () => { const handleResize = () => {
// 更新窗口宽度状态
windowwidth.value = window.innerWidth > 768; windowwidth.value = window.innerWidth > 768;
// 根据屏幕大小调整内容区可见性 // 首页特殊处理:小屏幕下默认显示内容区
if (rpsliturl[1] === localhome) { 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 {
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
}
// 首页内容区滚动动画
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
isconts.value = window.scrollY > 200; // 首页滚动动画处理
isScrollingleftmodlue.value = window.scrollY > 600; if (scrollY <= 350) {
HeroState(scrollY);
}
// 控制左侧模块的滚动状态
isScrollingleftmodlue.value = scrollY > 600;
}
};
// ========== 首页滚动动画模块 ==========
const HeroState = (scrollY: number) => {
const windowHeight = window.innerHeight;
// 计算滚动距离与窗口高度的比例,用于内容渐显
const contentScrollRatio = Math.min(scrollY / windowHeight, 1);
// 计算新的底部margin值从初始值45%逐渐减少到25%
const newMarginBottom = Math.max(initialHeroMarginBottom - (initialHeroMarginBottom * contentScrollRatio), 0);
// 更新hero的底部margin值
heroMarginBottom.value = `${newMarginBottom}%`;
// 计算新的translateY值从初始值450px逐渐减少到150px
const translateYValue = Math.max(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 90);
heroTransform.value = `translateY(${translateYValue}px)`;
// 当滚动超过100px时开始显示滚动到一屏高度时完全显示
iscontentvisible.value = scrollY > 100;
// 当滚动超过287px时logo被顶出屏幕触发移动状态
if (scrollY > 287) {
heroPosition.value = 'moving';
const translateYValue = Math.min(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 0);
heroTransform.value = `translateY(${translateYValue / 2}px)`;
}
};
/**
* 根据滚动位置更新导航栏样式
* @param {number} scrollY - 当前滚动位置
*/
const updateNavbarStyle = (scrollY: number) => {
// 根据滚动位置设置导航栏样式
// 当滚动超过1200px且屏幕宽度大于768px时隐藏导航栏
if (scrollY > 1200 && window.innerWidth > 768) {
elrowtop.value = 'hide'; // 隐藏导航栏
} else {
elrowtop.value = scrollY > 50 ? 'solid' : 'transparent'; // 固定或透明样式
} }
}; };
/** // ========== 路由监听和页面初始化 ==========
* 更新文章标题和相关状态
*/
const updateArticleTitle = () => {
let articledata: any = null;
// 优先使用attributeId参数新接口
if (rpsliturl[2] === 'aericletype') {
articledata = globalStore.getValue('attribute')?.name;
}
// 搜索标题
else if (rpsliturl[2] === 'aericletitle') {
articledata = globalStore.getValue('title')?.name;
}
// 疯言疯语页面
else if (rpsliturl[1] === 'nonsense') {
articledata = "疯言疯语";
}
// 特殊页面不需要显示标题
if (rpsliturl[1] === 'article-list' || rpsliturl[1] === 'message' || rpsliturl[1] === 'about') {
classnonsenset.value = false;
classmoduleorrouter.value = false;
}
// 在主页且articledata为空时不显示标题
else if (rpsliturl[1] === localhome && !articledata) {
classnonsenset.value = false;
classmoduleorrouter.value = false;
}
// 显示文章标题
else if (articledata) {
Cardtitle.value = articledata;
classnonsenset.value = true;
classmoduleorrouter.value = true;
}
};
/** /**
* 监听路由变化 * 处理路由变化的核心函数
* 更新页面状态、标题、激活菜单项等
*/ */
watch(() => route.path, () => { const handleRouteChange = () => {
// 重新解析路由路径
rpsliturl = route.path.split('/'); rpsliturl = route.path.split('/');
// 更新页面相关状态
updatePageState(); updatePageState();
setActiveIndex(rpsliturl[1]); setActiveIndex(rpsliturl[1]);
updateArticleTitle(); updateArticleTitle();
// 跳转后回到顶部
// 页面跳转后回到顶部
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
// 首页内容区滚动动画仅大屏下生效
// 根据是否为首页决定是否启动打字机效果
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 首页启动打字机效果 // 首页启动打字机效果
startTypewriter(); startTypewriter(fullHeroText);
// 重置hero的margin值为初始值
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
// 重置hero的移动状态
heroTransform.value = `translateY(${heroTransformValue}px)`;
heroIsMoving.value = false;
heroPosition.value = 'static';
} else { } else {
isconts.value = true; iscontentvisible.value = true;
heroText.value =fullHeroText; startTypewriter(fullHeroText);
if (heroTimer) clearInterval(heroTimer); heroMarginBottom.value = `${5}%`;
heroTransform.value = ``;
} }
// 非首页时关闭左侧状态栏 };
if (rpsliturl[1] == "articlesave") {
isleftmodluecontainer.value = false;
} else {
isleftmodluecontainer.value = true;
}
}, { immediate: true });
/** /**
* 生命周期钩子 * 初始化页面
*/
const initializePage = () => {
// 初始化窗口大小
handleResize();
// 启动打字机效果(如果是首页)
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 首页启动打字机效果
startTypewriter(fullHeroText);
// 重置hero的margin值为初始值
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
// 重置hero的移动状态
heroIsMoving.value = false;
heroPosition.value = 'static';
// 移动端首页默认显示内容区,桌面端初始隐藏
iscontentvisible.value = window.innerWidth <= 768;
} else {
startTypewriter(fullHeroText);
heroMarginBottom.value = `${2.5}%`;
}
};
// ========== 生命周期钩子 ==========
/**
* 组件挂载时执行
*/ */
onMounted(() => { onMounted(() => {
// 初始化窗口大小 // 初始化页面
initializePage();
handleResize();
// 添加事件监听器 // 添加事件监听器
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>
@@ -403,6 +672,10 @@ onUnmounted(() => {
opacity: 1; opacity: 1;
} }
.el-input__wrapper {
width: 80px;
}
.search-input { .search-input {
border: none; border: none;
outline: none; outline: none;
@@ -427,10 +700,143 @@ onUnmounted(() => {
color: #409eff; 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) { @media screen and (max-width: 1200px) {
.search-box-container.open { .search-box-container.open {
width: 250px; width: 150px;
} }
} }
</style> </style>

View File

@@ -1,50 +1,116 @@
<template> <template>
<div class="establish-container"> <div class="establish-component-root">
<!-- 弹出的按钮容器 --> <div class="establish-container">
<!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> --> <!-- 弹出的按钮容器 -->
<button <!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> -->
v-for="(btn, index) in isbuttonsave()" <button v-for="(btn, index) in isbuttonsave()" :class="['action-button', { 'show': isExpanded }]"
:class="['action-button', { 'show': isExpanded }]" :style="getButtonStyle(index)" :key="btn.id" @click="handleButtonClick(btn)">
:style="getButtonStyle(index)" <!-- <i :class="btn.icon"></i> -->
:key="btn.id" <span>{{ btn.label }}</span>
@click="handleButtonClick(btn)"> </button>
<!-- <i :class="btn.icon"></i> --> <!-- </div> -->
<span>{{ btn.label }}</span>
</button>
<!-- </div> -->
<!-- 主圆形按钮 --> <!-- 主圆形按钮 -->
<button class="main-button" :class="{ 'active': isExpanded }" @click="toggleExpand"> <button class="main-button" :class="{ 'active': isExpanded }" @click="toggleExpand">
<i class="icon">+</i> <i class="icon">+</i>
</button> </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> </div>
</template> </template>
<script setup> <script setup>
// 组件导入和初始化
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { articleService, messageService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services' import { articleService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services'
// 全局状态管理 // 全局状态管理
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
// 路由参数
const router = useRouter() const router = useRouter()
const route = useRoute()
// 定义响应式状态 // ==============================
// 状态管理模块
// ==============================
/**
* 按钮展开状态
* @type {import('vue').Ref<boolean>}
*/
const isExpanded = ref(false) 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) const isNonsenseModalVisible = ref(false)
/**
* 疯言疯语内容
* @type {import('vue').Ref<string>}
*/
const nonsenseContent = ref('') 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 = [ const baseButtons = [
{ id: 'logout', label: '登出', icon: 'icon-logout' }, { id: 'logout', label: '登出', icon: 'icon-logout' },
{ id: 'reload', label: '刷新', icon: 'icon-reload' } { id: 'reload', label: '刷新', icon: 'icon-reload' }
] ]
// 页面特定按钮配置 /**
* 页面特定按钮配置映射
* @type {Object.<string, Array<{id: string, label: string, icon: string}>>}
*/
const pageButtons = { const pageButtons = {
// 文章详情页按钮 // 文章详情页按钮
article: [ article: [
@@ -52,7 +118,7 @@ const pageButtons = {
{ id: 'delete-article', label: '删除', icon: 'icon-create-category' }, { id: 'delete-article', label: '删除', icon: 'icon-create-category' },
...baseButtons ...baseButtons
], ],
// 首页按钮 // 首页按钮
home: [ home: [
{ id: 'create-article', label: '新建', icon: 'icon-add-article' }, { id: 'create-article', label: '新建', icon: 'icon-add-article' },
@@ -61,51 +127,40 @@ const pageButtons = {
{ id: 'published-articles', label: '已发表', icon: 'icon-new-tag' }, { id: 'published-articles', label: '已发表', icon: 'icon-new-tag' },
...baseButtons ...baseButtons
], ],
// 疯言疯语页面按钮 // 疯言疯语页面按钮
nonsense: [ nonsense: [
{ id: 'create-nonsense', label: '说说', icon: 'icon-upload-file' }, { id: 'create-nonsense', label: '说说', icon: 'icon-upload-file' },
...baseButtons ...baseButtons
], ],
// 分类页面按钮 // 分类页面按钮
articlelist: [ articlelist: [
{ id: 'create-category', label: '新建', icon: 'icon-create-category' }, { id: 'create-category', label: '分类', icon: 'icon-create-category' },
{ id: 'create-tag', label: '标签', icon: 'icon-create-tag' },
...baseButtons ...baseButtons
], ],
// 标签页面按钮 // 标签页面按钮
tag: [ tag: [
{ id: 'create-tag', label: '新建', icon: 'icon-new-tag' }, { id: 'create-tag', label: '新建', icon: 'icon-new-tag' },
...baseButtons ...baseButtons
], ],
// 文章保存页面按钮 // 文章保存页面按钮
articlesave: [ articlesave: [
{ id: 'view-articles', label: '查看文章列表', icon: 'icon-new-tag' }, { id: 'view-articles', label: '查看文章列表', icon: 'icon-new-tag' },
...baseButtons ...baseButtons
], ],
// 默认按钮 // 默认按钮
default: baseButtons default: baseButtons
} }
// 根据status状态获取按钮配置
const getButtonsByStatus = (status) => {
globalStore.removeValue('articlestatus')
globalStore.setValue('articlestatus', {
status: status
})
//跳转文章列表页面,添加时间戳参数确保页面刷新
try {
// 添加时间戳作为查询参数,确保页面强制刷新
const timestamp = new Date().getTime();
router.push({ path: `/home/aericlestatus`, query: { t: timestamp } })
} catch (error) {
console.error('页面跳转失败:', error)
}
}
// 根据当前页面返回对应的按钮配置 /**
* 根据当前页面返回对应的按钮配置
* @returns {Array<{id: string, label: string, icon: string}>} 按钮配置数组
*/
const isbuttonsave = () => { const isbuttonsave = () => {
try { try {
// 获取当前页面路径名称 // 获取当前页面路径名称
@@ -117,19 +172,64 @@ const isbuttonsave = () => {
return pageButtons.default; return pageButtons.default;
} }
} }
// 切换按钮显示状态
/**
* 切换按钮展开/收起状态
* @param {Event} event - 点击事件对象
*/
const toggleExpand = (event) => { const toggleExpand = (event) => {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value
// 防止点击弹出的按钮后再次触发主按钮的点击事件 // 防止点击弹出的按钮后再次触发主按钮的点击事件
event.stopPropagation() 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 = () => { const showNonsenseModal = () => {
nonsenseContent.value = '' // 清空输入框 nonsenseContent.value = '' // 清空输入框
isNonsenseModalVisible.value = true isNonsenseModalVisible.value = true
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', { ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
confirmButtonText: '保存', confirmButtonText: '保存',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -145,31 +245,68 @@ const showNonsenseModal = () => {
}) })
} }
// 保存疯言疯语 // ==============================
// 功能操作方法模块
// ==============================
/**
* 处理错误响应的工具函数
* @param {Error} error - 错误对象
* @param {string} defaultMessage - 默认错误消息
*/
const handleErrorResponse = (error, defaultMessage = '操作失败') => {
console.error('操作失败:', error)
ElMessage.error(error.message || defaultMessage)
}
/**
* 根据文章状态获取并跳转文章列表
* @param {number} status - 文章状态 0:未发表 1:已发表 2:已删除
*/
const getButtonsByStatus = (status) => {
globalStore.removeValue('articlestatus')
globalStore.setValue('articlestatus', {
status: status
})
// 跳转文章列表页面,添加时间戳参数确保页面刷新
try {
const timestamp = new Date().getTime();
router.push({ path: `/home/aericlestatus`, query: { t: timestamp } })
} catch (error) {
console.error('页面跳转失败:', error)
}
}
/**
* 保存疯言疯语
* @param {string} content - 疯言疯语内容
*/
const saveNonsense = (content) => { const saveNonsense = (content) => {
if (!content || content.trim() === '') { if (!content || content.trim() === '') {
ElMessage.warning('内容不能为空') ElMessage.warning('内容不能为空')
return return
} }
// 调用服务保存疯言疯语 // 调用服务保存疯言疯语
nonsenseService.saveNonsense({ nonsenseService.createNonsense({
content: content.trim(), content: content.trim(),
time: new Date() time: new Date(),
status: 1
}).then(response => { }).then(response => {
if (response.code === 200) { if (response.code === 200) {
ElMessage.success('疯言疯语发布成功') ElMessage.success('疯言疯语发布成功')
// 刷新页面
router.push({ path: `/nonsense`});
} else { } else {
ElMessage.error(response.message || '发布失败') ElMessage.error(response.message || '发布失败')
} }
}).catch(err => handleErrorResponse(err, '发布失败')) }).catch(err => handleErrorResponse(err, '发布失败'))
} }
// 处理错误响应的工具函数
const handleErrorResponse = (error, defaultMessage = '操作失败') => { /**
console.error('操作失败:', error) * 新建分类
ElMessage.error(error.message || defaultMessage) */
}
// articlelist新建
const createCategory = () => { const createCategory = () => {
ElMessageBox.prompt('请输入分类名称:', '新建分类', { ElMessageBox.prompt('请输入分类名称:', '新建分类', {
confirmButtonText: '保存', confirmButtonText: '保存',
@@ -195,28 +332,80 @@ const createCategory = () => {
}); });
}; };
// 保存分类 /**
* 保存分类
* @param {string} typename - 分类名称
*/
const saveCategory = (typename) => { const saveCategory = (typename) => {
categoryService.saveCategory({ // console.log('保存分类')
categoryService.createCategory({
typename: typename typename: typename
}).then(response => { }).then(response => {
if (response.code === 200) { if (response.code === 200) {
ElMessage.success('分类创建成功'); ElMessage.success('分类创建成功');
// 刷新页面以显示新分类 // 刷新页面
const timestamp = new Date().getTime(); router.push({ path: `/articlelist`});
router.push({ path: `/home/aericlelist`, query: { t: timestamp } });
} else { } else {
ElMessage.error(response.message || '创建分类失败'); ElMessage.error(response.message || '创建分类失败');
} }
}).catch(err => handleErrorResponse(err, '创建分类失败')); }).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 deleteArticle = () => {
const articleId = globalStore.getValue('articleInfo')?.id const articleId = globalStore.getValue('articleInfo')?.articleid
if (!articleId) { if (!articleId) {
ElMessage.warning('缺少文章ID参数') ElMessage.warning('缺少文章ID参数')
return return
} }
// 确认删除 // 确认删除
ElMessageBox.confirm('确定删除该文章吗?', '删除确认', { ElMessageBox.confirm('确定删除该文章吗?', '删除确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
@@ -239,27 +428,33 @@ const deleteArticle = () => {
// 取消删除,静默处理 // 取消删除,静默处理
}) })
} }
// 修改文章方法
/**
* 修改文章
*/
const updateArticle = () => { const updateArticle = () => {
const articleId = globalStore.getValue('articleInfo') const articleId = globalStore.getValue('articleInfo')
if (!articleId) { if (!articleId) {
ElMessage.warning('缺少文章参数') ElMessage.warning('缺少文章参数')
return return
} }
// 确认修改 // 确认修改
ElMessageBox.confirm('确定修改该文章吗?', '修改确认', { ElMessageBox.confirm('确定修改该文章吗?', '修改确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
globalStore.setValue('updatearticle', articleId) globalStore.setValue('updatearticle', articleId)
router.push({ path: '/articlesave' }) router.push({ path: '/articlesave' })
}).catch(() => { }).catch(() => {
// 取消修改,静默处理 // 取消修改,静默处理
}) })
} }
// 登出方法 /**
* 登出系统
*/
const logout = () => { const logout = () => {
// 调用登出接口 // 调用登出接口
loginService.logout() loginService.logout()
@@ -278,103 +473,86 @@ const logout = () => {
}) })
.catch(err => handleErrorResponse(err, '登出失败')) .catch(err => handleErrorResponse(err, '登出失败'))
} }
// 刷新页面
/**
* 刷新页面
*/
const reloadPage = () => { const reloadPage = () => {
globalStore.clearAll() globalStore.clearAll()
} }
// 处理按钮点击事件
// ==============================
// 事件处理模块
// ==============================
/**
* 处理按钮点击事件
* @param {{id: string, label: string, icon: string}} button - 按钮对象
*/
const handleButtonClick = (button) => { const handleButtonClick = (button) => {
console.log('点击了按钮:', button.id, button.label) // console.log('点击了按钮:', button.id, button.label)
// 使用按钮ID进行处理更可靠且易于维护 // 使用按钮ID进行处理更可靠且易于维护
switch (button.id) { switch (button.id) {
// 新增操作 // 新增操作
case 'create-article': case 'create-article':
// 清除更新文章状态
router.push({ path: '/articlesave' }) router.push({ path: '/articlesave' })
break break
case 'create-category': case 'create-category':
createCategory() createCategory()
break break
case 'create-tag': case 'create-tag':
// router.push({ path: '/tagsave' }) createAttribute()
break break
case 'create-nonsense': case 'create-nonsense':
// 显示疯言疯语模态框
showNonsenseModal() showNonsenseModal()
break break
// 修改操作 // 修改操作
case 'edit-article': case 'edit-article':
updateArticle() updateArticle()
break break
// 删除操作 // 删除操作
case 'delete-article': case 'delete-article':
deleteArticle(); deleteArticle();
break break
// 查看操作 // 查看操作
case 'del-articles': case 'del-articles':
getButtonsByStatus(2) getButtonsByStatus(2)
break break
case 'unpublished-articles': case 'unpublished-articles':
getButtonsByStatus(0) getButtonsByStatus(0)
break break
case 'published-articles': case 'published-articles':
getButtonsByStatus(1) getButtonsByStatus(1)
break break
case 'view-articles': case 'view-articles':
router.push({ path: '/home' }) router.push({ path: '/home' })
break break
// 登出操作 // 系统操作
case 'logout': case 'logout':
logout(); logout();
break break
case 'reload': case 'reload':
reloadPage() reloadPage()
break break
default: default:
console.warn('未处理的按钮类型:', button.id, button.label) console.warn('未处理的按钮类型:', button.id, button.label)
ElMessage.info(`功能 ${button.label} 暂未实现`) ElMessage.info(`功能 ${button.label} 暂未实现`)
} }
// 点击后收起按钮菜单 // 点击后收起按钮菜单
isExpanded.value = false isExpanded.value = false
} }
// 为每个按钮计算动态样式 /**
const getButtonStyle = (index) => { * 点击外部区域收起按钮的处理函数
// 计算过渡时间,确保显示时从上到下逐渐出现,隐藏时从下到上逐渐消失 * @param {Event} e - 点击事件对象
// 显示时间0.9s - index * 0.2s (递减) */
// 隐藏时间0.3s + index * 0.2s (递增)
const showDelay = Math.max(0.1, 0.2 + index * 0.2);
const hideDelay = 0.3 + index * 0.2;
// 根据是否展开返回不同的过渡样式
if (isExpanded.value) {
return {
transition: `all ${showDelay}s ease-in-out`
};
} else {
return {
transition: `all ${hideDelay}s ease-in-out`
};
}
};
// 点击外部区域收起按钮的处理函数
const handleClickOutside = (e) => { const handleClickOutside = (e) => {
const mainButton = document.querySelector('.main-button') const mainButton = document.querySelector('.main-button')
const actionButtons = document.querySelectorAll('.action-button') const actionButtons = document.querySelectorAll('.action-button')
@@ -391,12 +569,20 @@ const handleClickOutside = (e) => {
} }
} }
// 生命周期钩子 - 挂载时添加事件监听 // ==============================
// 组件生命周期
// ==============================
/**
* 组件挂载时添加事件监听
*/
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside) document.addEventListener('click', handleClickOutside)
}) })
// 生命周期钩子 - 卸载前清理事件监听 /**
* 组件卸载前清理事件监听
*/
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside) document.removeEventListener('click', handleClickOutside)
}) })
@@ -494,6 +680,78 @@ onBeforeUnmount(() => {
margin-left: 150px; margin-left: 150px;
opacity: 1; 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) { @media (max-width: 768px) {
.establish-container { .establish-container {

View File

@@ -25,10 +25,16 @@ const routes = [
meta: { title: '首页' }, meta: { title: '首页' },
children: [ children: [
{ {
path: 'aericletype', path: 'aericleattribute',
name: 'homeByType', name: 'homeByAttribute',
component: HomePage component: HomePage
}, },
{
path: 'aericlecategory',
name: 'homeByCategory',
component: HomePage
},
{ {
path: 'aericletitle', path: 'aericletitle',
name: 'homeByTitle', name: 'homeByTitle',

View File

@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
// 创建axios实例 // 创建axios实例
const api = axios.create({ const api = axios.create({
baseURL:'http://localhost:8080/api', // API基础URL baseURL: '/api', // API基础URL使用相对路径通过Vite代理转发
timeout: 10000, // 请求超时时间 timeout: 10000, // 请求超时时间
withCredentials: true // 允许跨域请求携带凭证如cookies withCredentials: true // 允许跨域请求携带凭证如cookies
}) })

View File

@@ -6,6 +6,17 @@ import api from './apiService'
*/ */
class ArticleService { class ArticleService {
/** /**
* 分页查询文章列表
* @param {import('../types').PaginationParams} params - 分页查询参数
* @param status 文章状态0未发表 1已发表 2已删除
* @param page 页码从0开始可选默认为0
* @param size 每页大小可选默认为10最大为100
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getPagedArticles(params = {}) {
return api.get(`/articles/status/page?title=${params.title || ''}&categoryid=${params.categoryid || 0}&attributeid=${params.attributeid || 0}&status=${params.status || 1}&page=${params.page || 0}&size=${params.size || 10}`)
}
/**
* 获取已发布文章列表 * 获取已发布文章列表
* @param {import('../types').PaginationParams} params - 查询参数 * @param {import('../types').PaginationParams} params - 查询参数
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
@@ -13,15 +24,7 @@ class ArticleService {
getAllArticles(params = {}) { getAllArticles(params = {}) {
return api.get('/articles/published', { params }) return api.get('/articles/published', { params })
} }
/** /**
* 根据状态获取文章列表
* @param {number} status - 文章状态0未发表 1已发表 2已删除
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getArticlesByStatus(status) {
return api.get(`/articles/status/${status}`)
}
/**
* 获取所有文章列表(包含已删除) * 获取所有文章列表(包含已删除)
* @param {import('../types').PaginationParams} params - 查询参数 * @param {import('../types').PaginationParams} params - 查询参数
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
@@ -29,16 +32,7 @@ class ArticleService {
getAllArticlesWithDeleted(params = {}) { getAllArticlesWithDeleted(params = {}) {
return api.get('/articles', { params }) return api.get('/articles', { params })
} }
/** /**
* 根据ID获取文章详情
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
*/
getArticleById(articleid) {
return api.get(`/articles/${articleid}`)
}
/**
* 根据属性ID获取文章列表 * 根据属性ID获取文章列表
* @param {number} attributeid - 属性ID * @param {number} attributeid - 属性ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
@@ -47,16 +41,7 @@ class ArticleService {
return api.get(`/articles/attribute/${attributeid}`) return api.get(`/articles/attribute/${attributeid}`)
} }
/** /**
* 根据标题查询文章列表
* @param {string} title - 文章标题
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getArticlesByTitle(title) {
return api.get(`/articles/title/${title}`)
}
/**
* 获取热门文章 * 获取热门文章
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/ */
@@ -92,16 +77,7 @@ class ArticleService {
return api.delete(`/articles/${articleid}`) return api.delete(`/articles/${articleid}`)
} }
/** /**
* 根据分类获取文章
* @param {number} categoryid - 分类ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getArticlesByCategory(categoryid) {
return api.get(`/articles/category/${categoryid}`)
}
/**
* 增加文章浏览量 * 增加文章浏览量
* @param {number} articleid - 文章ID * @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<boolean>>} * @returns {Promise<import('../types').ApiResponse<boolean>>}
@@ -110,23 +86,7 @@ class ArticleService {
return api.post(`/articles/view/${articleid}`) return api.post(`/articles/view/${articleid}`)
} }
/**
* 根据属性ID获取最新文章
* @param {number} attributeid - 属性ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getLatestArticlesByAttribute(attributeid) {
return api.get(`/articles/attribute/${attributeid}/latest`)
}
/**
* 点赞文章
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
likeArticle(articleid) {
return api.post(`/articles/like/${articleid}`)
}
} }
// 创建并导出服务实例 // 创建并导出服务实例

View File

@@ -39,35 +39,6 @@ class CategoryAttributeService {
createAttribute(attributeData) { createAttribute(attributeData) {
return api.post('/category-attributes', attributeData) return api.post('/category-attributes', attributeData)
} }
/**
* 更新分类属性
* @param {number} attributeid - 属性ID
* @param {import('../types').CategoryAttributeDto} attributeData - 属性数据
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
*/
updateAttribute(attributeid, attributeData) {
return api.put(`/category-attributes/${attributeid}`, attributeData)
}
/**
* 删除分类属性
* @param {number} attributeid - 属性ID
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
deleteAttribute(attributeid) {
return api.delete(`/category-attributes/${attributeid}`)
}
/**
* 检查分类下是否存在指定名称的属性
* @param {number} categoryid - 分类ID
* @param {string} attributename - 属性名称
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
checkAttributeExists(categoryid, attributename) {
return api.get(`/category-attributes/check-exists?categoryid=${categoryid}&attributename=${encodeURIComponent(attributename)}`)
}
} }
// 创建并导出服务实例 // 创建并导出服务实例

View File

@@ -11,43 +11,6 @@ class CategoryService {
getAllCategories() { getAllCategories() {
return api.get('/categories') return api.get('/categories')
} }
/**
* 获取指定分类
* @param {number} typeid - 分类ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
*/
getCategory(typeid) {
return api.get(`/categories/${typeid}`)
}
/**
* 创建新分类
* @param {import('../types').CategoryDto} categoryData - 分类数据
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
*/
createCategory(categoryData) {
return api.post('/categories', categoryData)
}
/**
* 更新分类
* @param {number} typeid - 分类ID
* @param {import('../types').CategoryDto} categoryData - 分类数据
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
*/
updateCategory(typeid, categoryData) {
return api.put(`/categories/${typeid}`, categoryData)
}
/**
* 删除分类
* @param {number} typeid - 分类ID
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
deleteCategory(typeid) {
return api.delete(`/categories/${typeid}`)
}
} }
// 创建并导出服务实例 // 创建并导出服务实例

View File

@@ -14,48 +14,11 @@ class LoginService {
return api.post("/auth/login", loginData); return api.post("/auth/login", loginData);
} }
/**
* 用户注册
* @param {import('../types').RegisterDto} registerData - 注册数据
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
*/
register(registerData) {
return api.post("/auth/register", registerData);
}
/** /**
* 登出 * 登出
*/ */
logout() { logout() {
return api.post("/auth/logout"); return api.post("/auth/logout");
}
/**
* 获取当前用户信息
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
*/
getCurrentUser() {
return api.get("/user/info");
}
/**
* 更新用户信息
* @param {import('../types').UserDto} userData - 用户数据
* @returns {Promise<import('../types').ApiResponse<import('../types').User>>}
*/
updateUser(userData) {
return api.put("/user/update", userData);
}
/**
* 修改密码
* @param {import('../types').ChangePasswordDto} passwordData - 密码数据
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
changePassword(passwordData) {
return api.post("/user/change-password", passwordData);
} }
} }
export default new LoginService(); export default new LoginService();

View File

@@ -6,22 +6,28 @@ import apiService from './apiService'
*/ */
class MessageService { class MessageService {
/** /**
* 获取所有留言 * 获取留言数量
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>} * @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<number>>}
*/ */
getAllMessages() { getMessageCountByArticleId(articleid) {
return apiService.get('/messages') return apiService.get(`/messages/count?articleid=${articleid}`)
} }
/** /**
* 获取单条留言 * 获取分页留言
* @param {number} messageid - 留言ID * @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Message>>} * @param {number} pagenum - 页码
* @param {number} pagesize - 每页数量
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/ */
getMessageById(messageid) { getMessagesByPage(articleid, pagenum, pagesize) {
return apiService.get(`/messages/${messageid}`) // 如果文章ID不存在查询所有留言
if (!articleid) {
return apiService.get(`/messages/page?pageNum=${pagenum}&pageSize=${pagesize}`)
}
return apiService.get(`/messages/page?articleid=${articleid}&pageNum=${pagenum}&pageSize=${pagesize}`)
} }
/** /**
* 根据文章ID获取留言 * 根据文章ID获取留言
* @param {number} articleid - 文章ID * @param {number} articleid - 文章ID
@@ -32,38 +38,11 @@ class MessageService {
} }
/** /**
* 获取留言 * 获取所有留言
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/ */
getRootMessages() { getAllMessages() {
return apiService.get('/messages/root') return apiService.get('/messages')
}
/**
* 根据父留言ID获取回复
* @param {number} parentid - 父留言ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/
getRepliesByParentId(parentid) {
return apiService.get(`/messages/${parentid}/replies`)
}
/**
* 根据昵称搜索留言
* @param {string} nickname - 昵称
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/
searchMessagesByNickname(nickname) {
return apiService.get(`/messages/search?nickname=${nickname}`)
}
/**
* 获取文章评论数量
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<number>>}
*/
getMessageCountByArticleId(articleid) {
return apiService.get(`/messages/count/article/${articleid}`)
} }
/** /**
@@ -88,7 +67,6 @@ class MessageService {
* 点赞留言 * 点赞留言
* @param {number} messageid - 留言ID * @param {number} messageid - 留言ID
* @returns {Promise<import('../types').ApiResponse<boolean>>} * @returns {Promise<import('../types').ApiResponse<boolean>>}
*
*/ */
likeMessage(messageid) { likeMessage(messageid) {
return apiService.post(`/messages/${messageid}/like`) return apiService.post(`/messages/${messageid}/like`)

View File

@@ -1,4 +1,4 @@
// 留言相关API服务 // 疯言疯语相关API服务
import apiService from './apiService' import apiService from './apiService'
class NonsenseService { class NonsenseService {
@@ -22,7 +22,7 @@ class NonsenseService {
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象 * @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
*/ */
saveNonsense(nonsense){ createNonsense(nonsense){
return apiService.post('/nonsense', nonsense) return apiService.post('/nonsense', nonsense)
} }
/** /**

View File

@@ -17,13 +17,12 @@
/* 内容区圆角 */ /* 内容区圆角 */
/* 首页 hero 区域高度和间距 */ /* 首页 hero 区域高度和间距 */
--hero-height: 768px; --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 主标题字号 */
@@ -352,10 +351,14 @@ p {
} }
/* 分页区样式 */ /* 分页区样式 */
.Pagination { .Pagination {
align-self: center; margin-bottom: 30px;
background-color: var(--pagination-bg); }
.Pagination .el-pagination {
/* 水平垂直居中 */
display: flex;
justify-content: center;
align-items: center;
} }
/* 左侧状态栏样式 */ /* 左侧状态栏样式 */
.leftmodluecontainer { .leftmodluecontainer {
width: var(--leftmodlue-width); width: var(--leftmodlue-width);
@@ -364,19 +367,24 @@ p {
/* 首页 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: 10%; margin-top: var(--hero-margin-top-small);
/* 去除 hero 收缩状态下的position */
position: static;
} }
/* 打字机效果 */ /* 打字机效果 */
@@ -384,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 {
@@ -393,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 {
@@ -446,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 {
@@ -506,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 {
}
}

View File

@@ -70,7 +70,7 @@ export interface MessageDto {
* 分类类型接口 * 分类类型接口
*/ */
export interface Category { export interface Category {
typeid: number Categoryid: number
typename: string typename: string
description?: string description?: string
createdAt?: string createdAt?: string
@@ -86,6 +86,7 @@ export interface CategoryDto {
description?: string description?: string
} }
/** /**
* 分类属性接口 * 分类属性接口
*/ */
@@ -161,8 +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
} }

136
src/views/Footer.vue Normal file
View 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">
网站所有权利保留 &copy; {{ 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>

View File

@@ -3,15 +3,17 @@
<div class="about-content-wrapper"> <div class="about-content-wrapper">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="about-page-header"> <div class="about-page-header">
<h1 class="about-page-title">关于</h1> <h3 class="about-page-title">关于</h3>
</div> </div>
<!-- 关于内容 --> <!-- 关于内容 -->
<div class="about-main-content"> <div class="about-main-content">
<div class="about-personal-intro"> <div class="about-personal-intro">
<h4>名字</h4> <h4>清风--清疯--清疯不颠</h4>
<h5> <h5>
&nbsp; &nbsp; 让我回忆回忆...大一的时候还是上学的日子好哈哈哈哈哈跟室友一块玩游戏因为我的Steam名字叫清风慢慢的这个名儿就这么成了我的外号说来也怪被他们这么一叫心里那点初入大学的陌生和拘谨 <!-- 徐徐清风知我意悠悠湖水荡人心 -->
名字从清风起步但总觉得少了点年轻人的疯狂于是干脆改叫清疯疯久了又怕真便在后面补上不颠二字作为提醒清疯不颠就此定下风与疯只在一念之间疯与不颠则是我给自己的清醒界限
<!-- &nbsp; &nbsp; 让我回忆回忆...大一的时候还是上学的日子好哈哈哈哈哈跟室友一块玩游戏因为我的Steam名字叫清风慢慢的这个名儿就这么成了我的外号说来也怪被他们这么一叫心里那点初入大学的陌生和拘谨
好像真被一阵风吹散了似的 好像真被一阵风吹散了似的
哈哈哈哈哈我还蛮喜欢这个外号的 哈哈哈哈哈我还蛮喜欢这个外号的
<br></br> <br></br>
@@ -21,14 +23,14 @@
&nbsp; &nbsp; 又过些日子玩新游戏要起名正盯着输入框里的清疯发呆 两个字在脑海里冒出来疯癫好像是疯了但也没完全颠嘛那种在理智边界试探却绝不越线的微妙感一下对味了干脆就叫清疯不颠 &nbsp; &nbsp; 又过些日子玩新游戏要起名正盯着输入框里的清疯发呆 两个字在脑海里冒出来疯癫好像是疯了但也没完全颠嘛那种在理智边界试探却绝不越线的微妙感一下对味了干脆就叫清疯不颠
名字一出自己先乐了 名字一出自己先乐了
<br></br> <br></br>
哈哈哈哈哈哈俗话说天才在左疯子在右在我这儿大概是左脑负责疯右脑负责颠两个家伙吵吵闹闹反而让我在这个世界里自得其乐 哈哈哈哈哈哈俗话说天才在左疯子在右在我这儿大概是左脑负责疯右脑负责颠两个家伙吵吵闹闹反而让我在这个世界里自得其乐 -->
</h5> </h5>
</div> </div>
<div class="about-personal-intro"> <div class="about-personal-intro">
<h4>疯言疯语</h4> <h4>疯言疯语</h4>
<h5> <h5>
&nbsp; &nbsp; 并没有网站的开发经验这是我的第一个项目想设计一下独属于清疯的页面可是我并不清楚该如何去写这个页面就干脆当一个我发疯的地方吧哈哈哈哈哈 所以你有看到彩色的小鹿吗 &nbsp; &nbsp; 我想设计一下独属于清疯的页面可是我并不清楚该如何去写这个页面就干脆当一个我发疯的地方吧哈哈哈哈哈 所以你有看到彩色的小鹿吗
</h5> </h5>
</div> </div>
@@ -279,14 +281,19 @@ const goToMessageBoard = () => {
.about-page-container { .about-page-container {
padding: 20px 0; padding: 20px 0;
} }
.about-page-header{
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.about-content-wrapper { .about-content-wrapper {
width: 100%;
padding: 20px; padding: 20px;
margin: 0 15px; margin: 0 5%;
} }
.about-page-title { .about-page-title {
font-size: 1.8rem; font-size: 1.2rem;
} }
.about-page-subtitle { .about-page-subtitle {
@@ -304,7 +311,6 @@ const goToMessageBoard = () => {
.contact-options { .contact-options {
flex-direction: column; flex-direction: column;
} }
.skills-display-list { .skills-display-list {
gap: 8px; gap: 8px;
} }

View File

@@ -18,17 +18,18 @@
<div v-else-if="categories.length > 0" class="article-content" id="category-list"> <div v-else-if="categories.length > 0" class="article-content" id="category-list">
<p><strong></strong></p> <p><strong></strong></p>
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span> </strong></div> <div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span> </strong></div>
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid" class="category-group-container"> <div v-for="categoryGroup in categories" :key="categoryGroup.Categoryid" class="category-group-container">
<div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)"> <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> <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> <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"> <ul class="category-item-list">
<li v-for="category in categoryGroup.attributes" :key="category.attributeid" > <div v-for="attribute in categoryGroup.attributes" :key="attribute.attributeid">
&nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename <li v-if="attribute.articles && attribute.articles.length > 0">
}}</kbd></a> &nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(attribute)"><kbd>{{ attribute.attributename}}</kbd></a>
&nbsp; ({{ category.articles.length }}) &nbsp; ({{ attribute.articles.length }})
</li> </li>
</div>
</ul> </ul>
</div> </div>
@@ -39,6 +40,7 @@
<div v-else class="empty-state-container"> <div v-else class="empty-state-container">
<el-empty description="暂无分类" /> <el-empty description="暂无分类" />
</div> </div>
</div> </div>
</template> </template>
@@ -76,7 +78,7 @@ const fetchCategories = async () => {
// 使用Promise.all等待所有异步操作完成 // 使用Promise.all等待所有异步操作完成
await Promise.all( await Promise.all(
processedCategories.map(async category => { processedCategories.map(async category => {
const attributes = await categoryAttributeService.getAttributesByCategory(category.typeid); const attributes = await categoryAttributeService.getAttributesByCategory(category.categoryid);
if (attributes.code === 200 && Array.isArray(attributes.data)) { if (attributes.code === 200 && Array.isArray(attributes.data)) {
const processedAttributes = await Promise.all( const processedAttributes = await Promise.all(
attributes.data.map(async item => { attributes.data.map(async item => {
@@ -97,7 +99,7 @@ const fetchCategories = async () => {
); );
} }
categories.value = processedCategories; categories.value = processedCategories;
console.log('获取分类列表成功:', categories.value); // console.log('获取分类列表成功:', categories.value);
} catch (err) { } catch (err) {
console.error('获取分类列表失败:', err); console.error('获取分类列表失败:', err);
ElMessage.error('获取分类列表失败,请稍后重试'); ElMessage.error('获取分类列表失败,请稍后重试');
@@ -114,11 +116,10 @@ const handleCategoryClick = (attribute: any) => {
globalStore.removeValue('attribute') globalStore.removeValue('attribute')
globalStore.setValue('attribute', { globalStore.setValue('attribute', {
id: attribute.attributeid, id: attribute.attributeid,
name: attribute.typename name: attribute.attributename
}) })
console.log(attribute)
router.push({ router.push({
path: '/home/aericletype', path: '/home/aericleattribute',
}) })
} }

View File

@@ -9,7 +9,7 @@
<!-- 错误状态 --> <!-- 错误状态 -->
<div v-else-if="error" class="error-state-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>
<!-- 文章详情 --> <!-- 文章详情 -->
@@ -40,14 +40,11 @@
<!-- 文章底部信息 --> <!-- 文章底部信息 -->
<div class="article-footer-section"> <div class="article-footer-section">
<div class="article-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-group"> <div class="article-actions-group">
<el-button type="primary" @click="goBack" plain> <el-button type="primary" @click="navigateBack" plain>
返回 返回
</el-button> </el-button>
</div> </div>
@@ -57,11 +54,11 @@
<div class="related-articles-section" 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 v-for="item in relatedArticles" :key="item.articleid" class="related-article-card" <!-- <div v-for="item in relatedArticles" :key="item.articleid" class="related-article-card"
@click="handleRelatedArticleClick(item.articleid)"> @click="handleRelatedArticleClick(item.articleid)">
<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>
@@ -80,92 +77,185 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 导入必要的依赖 // =========================================================================
import { useRoute, useRouter } from 'vue-router' // 组件导入和初始化
// =========================================================================
/**
* 导入Vue核心功能
*/
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
/**
* 导入路由相关功能
*/
import { useRouter } from 'vue-router'
/**
* 导入状态管理和UI组件
*/
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
/**
* 导入类型和工具函数
*/
import type { Article } from '@/types' import type { Article } from '@/types'
import { formatRelativeTime } from '@/utils/dateUtils' import { formatRelativeTime } from '@/utils/dateUtils'
/**
* 导入子组件
*/
import messageboard from './messageboard.vue' import messageboard from './messageboard.vue'
import markdownViewer from './markdown.vue' import markdownViewer from './markdown.vue'
// 路由相关
// =========================================================================
// 路由和状态初始化
// =========================================================================
/**
* 初始化路由
*/
const router = useRouter() const router = useRouter()
// 响应式状态管理 /**
* 初始化全局状态管理
*/
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
const article = ref<Article | null>(null) // 使用 null 作为初始值,避免类型不匹配问题
const loading = ref(false) /**
const error = ref('') * 响应式状态定义
const relatedArticles = ref<Article[]>([]) */
const article = ref<Article | null>(null) // 文章详情数据初始为null避免类型不匹配
const loading = ref(false) // 加载状态
const error = ref('') // 错误信息
const relatedArticles = ref<Article[]>([]) // 相关文章列表
// =========================================================================
// 文章数据处理模块
// =========================================================================
/**
* 获取文章ID
* @returns {number | null} 文章ID或null
*/
const getArticleId = (): number | null => {
return globalStore.getValue('articleInfo')?.articleid || null
}
/** /**
* 获取文章详情数据 * 获取文章详情数据
* @returns {Promise<Article | null>} 文章数据或null
*/ */
const fetchArticleDetail = async () => { const fetchArticleData = async (): Promise<Article | null> => {
try {
// 从全局状态获取文章信息
const response = await globalStore.getValue('articleInfo')
return response || null
} catch (err) {
console.error('获取文章数据失败:', err)
throw new Error('获取文章数据失败')
}
}
/**
* 增加文章浏览量
* @param {Article} articleData 文章数据
*/
const incrementViewCount = (articleData: Article): void => {
if (articleData.viewCount) {
articleData.viewCount++
} else {
articleData.viewCount = 1
}
}
/**
* 处理文章不存在的情况
*/
const handleArticleNotFound = (): void => {
error.value = '文章不存在或已被删除'
ElMessage.error(error.value)
}
/**
* 处理获取文章数据过程中的错误
* @param {unknown} err 错误对象
*/
const handleArticleFetchError = (err: unknown): void => {
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试'
console.error('获取文章详情失败:', err)
ElMessage.error(error.value)
}
/**
* 初始化文章详情数据
*/
const initializeArticleDetail = async (): Promise<void> => {
try { try {
loading.value = true loading.value = true
error.value = '' error.value = ''
// 获取路由参数 // 获取文章ID
const articleId = globalStore.getValue('articleInfo')?.articleid || null const articleId = getArticleId()
if (!articleId) { if (!articleId) {
throw new Error('文章不存在') handleArticleNotFound()
return
} }
// 获取文章详情
const response = await globalStore.getValue('articleInfo')
// const markdowndata = await
if (response) {
article.value = response
// 获取并设置分类名称
// const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid))
// article.value.categoryName = categoryResponse.data.attributename || '未分类'
// 获取并设置评论量
// const commentResponse = await messageService.getMessagesByArticleId(articleId)
// article.value.commentCount = commentResponse.data.length || 0
// 更新浏览量
// 更新前端显示的浏览量
if (article.value.viewCount) {
article.value.viewCount++
} else {
article.value.viewCount = 1
}
// 获取文章数据
const articleData = await fetchArticleData()
if (articleData) {
article.value = articleData
// 增加浏览量
incrementViewCount(articleData)
} else { } else {
throw new Error('文章不存在或已被删除') handleArticleNotFound()
} }
} catch (err) { } catch (err) {
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试' handleArticleFetchError(err)
console.error('获取文章详情失败:', err)
ElMessage.error(error.value)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// =========================================================================
// 导航和用户交互模块
// =========================================================================
/** /**
* 返回上一页 * 返回上一页
*/ */
const goBack = () => { const navigateBack = (): void => {
router.back() router.back()
} }
/** /**
* 处理相关文章点击事件 * 导航到相关文章
* @param {number} id - 相关文章ID * @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>
@@ -393,18 +483,17 @@ onMounted(() => {
.error-state-container, .error-state-container,
.empty-state-container { .empty-state-container {
padding: 20px; padding: 20px;
margin: 0 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.article-main-title { .article-main-title {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.5; line-height: 1.5;
} }
.article-actions-group {
/* 文章操作按钮组 - 右对齐 */
justify-content: flex-end;
}
.article-meta-info { .article-meta-info {
flex-direction: column;
align-items: flex-start;
gap: 8px; gap: 8px;
font-size: 0.9rem; font-size: 0.9rem;
} }

View File

@@ -14,7 +14,7 @@
</span> </span>
<span class="meta-item status-item"> <span class="meta-item status-item">
<i class="el-icon-document"></i> <i class="el-icon-document"></i>
<el-select v-model="Articleform.status" placeholder="请选择状态" class="meta-select"> <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 v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
@@ -30,23 +30,41 @@
</el-cascader> </el-cascader>
</span> </span>
</div> </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>
<div class="editor-container"> <div class="editor-container">
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" /> <!-- 编辑区域 -->
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" noImgZoomIn
noKatex />
<!-- 返回列表 -->
<el-button type="primary" @click="handleReturn">返回列表</el-button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { reactive, ref } from 'vue';
import { MdEditor } from 'md-editor-v3'; import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css'; import 'md-editor-v3/lib/style.css';
import { categoryService, categoryAttributeService, articleService } from '@/services'; import { categoryService, categoryAttributeService, articleService } from '@/services';
import type { Article } from '@/types/index.ts'; import type { Article } from '@/types/index.ts';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
// 测试
// const id = 'preview-only';
// const scrollElement = document.documentElement;
// 路由 // 路由
import router from '@/router/Router'; import router from '@/router/Router';
const Articleform = ref<Article>({ const Articleform = ref<Article>({
@@ -58,7 +76,6 @@ const Articleform = ref<Article>({
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
markdownscontent: '', markdownscontent: '',
status: 0 // 默认状态为草稿
}) })
// 用于级联选择器的值绑定 // 用于级联选择器的值绑定
@@ -123,7 +140,7 @@ const loadCategories = async () => {
}) })
); );
categorieoptions.value = optionsData; categorieoptions.value = optionsData;
// 如果是编辑模式且有attributeid设置级联选择器的默认值 // 如果是编辑模式且有attributeid设置级联选择器的默认值
if (Articleform.value.articleid !== 0 && Articleform.value.attributeid !== 0) { if (Articleform.value.articleid !== 0 && Articleform.value.attributeid !== 0) {
// 查找属性所属的分类 // 查找属性所属的分类
@@ -136,9 +153,9 @@ const loadCategories = async () => {
} }
} }
} }
console.log('分类选项:', optionsData); // console.log('分类选项:', optionsData);
console.log('选中的值:', selectedValues.value); // console.log('选中的值:', selectedValues.value);
} }
} catch (error) { } catch (error) {
console.error('加载分类失败:', error); console.error('加载分类失败:', error);
@@ -160,13 +177,13 @@ loadCategories();
const handleSave = (markdown) => { const handleSave = (markdown) => {
Articleform.value.markdownscontent = markdown; Articleform.value.markdownscontent = markdown;
// 验证必填字段 // 验证必填字段
if (!Articleform.value.title || !Articleform.value.attributeid) { if (!Articleform.value.title || !Articleform.value.attributeid) {
ElMessage.warning('请填写必填字段:标题和分类属性'); ElMessage.warning('请填写必填字段:标题和分类属性');
return; return;
} }
// 构建请求数据 // 构建请求数据
const articleData = { const articleData = {
articleid: Articleform.value.articleid, articleid: Articleform.value.articleid,
@@ -178,17 +195,17 @@ const handleSave = (markdown) => {
likes: 0, likes: 0,
markdownscontent: Articleform.value.markdownscontent markdownscontent: Articleform.value.markdownscontent
}; };
console.log('发送文章数据:', articleData); // console.log('发送文章数据:', articleData);
console.log('当前认证token是否存在:', !!localStorage.getItem('token')); // console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
// 根据articleid决定调用创建还是更新接口 // 根据articleid决定调用创建还是更新接口
const savePromise = Articleform.value.articleid === 0 const savePromise = Articleform.value.articleid === 0
? articleService.createArticle(articleData) ? articleService.createArticle(articleData)
: articleService.updateArticle(Articleform.value.articleid, articleData); : articleService.updateArticle(Articleform.value.articleid, articleData);
savePromise savePromise
.then(res => { .then(res => {
console.log('API响应:', res); // console.log('API响应:', res);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功'); ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
// 清除全局存储中的article // 清除全局存储中的article
@@ -208,7 +225,7 @@ const handleSave = (markdown) => {
}; };
selectedValues.value = []; selectedValues.value = [];
} }
// 返回列表页 // 返回列表页
router.push('/home'); router.push('/home');
} else { } else {
@@ -221,9 +238,9 @@ const handleSave = (markdown) => {
if (err.response) { if (err.response) {
console.error('错误状态码:', err.response.status); console.error('错误状态码:', err.response.status);
console.error('错误响应数据:', err.response.data); console.error('错误响应数据:', err.response.data);
const operationType = Articleform.value.articleid === 0 ? '创建' : '更新'; const operationType = Articleform.value.articleid === 0 ? '创建' : '更新';
if (err.response.status === 401) { if (err.response.status === 401) {
ElMessage.error('未授权访问,请先登录'); ElMessage.error('未授权访问,请先登录');
} else if (err.response.status === 403) { } else if (err.response.status === 403) {
@@ -238,6 +255,18 @@ const handleSave = (markdown) => {
} }
}) })
}; };
/**
* 返回列表页
*/
const handleReturn = () => {
// 确定是否返回列表页
if (window.confirm('确定要返回列表页吗?所有未保存的更改将丢失。')) {
// 清除全局存储中的article
globalStore.removeValue('updatearticle');
router.push('/home');
}
};
</script> </script>
@@ -348,85 +377,89 @@ const handleSave = (markdown) => {
margin: 0 15px; margin: 0 15px;
border-radius: 10px; border-radius: 10px;
} }
.article-main-title { .article-main-title {
font-size: 2rem; font-size: 2rem;
line-height: 1.4; line-height: 1.4;
} }
.article-meta-info { .article-meta-info {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 15px; gap: 15px;
} }
.meta-item { .meta-item {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.meta-select, .meta-select,
.meta-cascader { .meta-cascader {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
} }
} }
/* 文章简介区域 */
.article-summary-section {
margin-top: 30px;
padding: 0 30px 0 30px;
}
/* 响应式设计 - 手机 */ /* 响应式设计 - 手机 */
@media (max-width: 480px) { @media (max-width: 480px) {
.article-save-container { .article-save-container {
padding: 15px 0; padding: 15px 0;
} }
.article-content-wrapper { .article-content-wrapper {
padding: 20px 15px; padding: 20px 15px;
margin: 0 10px; margin: 0 10px;
border-radius: 8px; border-radius: 8px;
} }
.article-main-title { .article-main-title {
font-size: 1.75rem; font-size: 1.75rem;
line-height: 1.5; line-height: 1.5;
} }
.article-meta-info { .article-meta-info {
font-size: 0.9rem; font-size: 0.9rem;
gap: 12px; gap: 12px;
} }
.meta-select, .meta-select,
.meta-cascader { .meta-cascader {
max-width: 100%; max-width: 100%;
} }
} }
/* 深色模式支持 */ /* 深色模式支持 */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.article-save-container { .article-save-container {
background-color: #1a1a1a; background-color: #1a1a1a;
} }
.article-content-wrapper { .article-content-wrapper {
background-color: #2d2d2d; background-color: #2d2d2d;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
} }
.article-header-section { .article-header-section {
border-bottom-color: #444; border-bottom-color: #444;
} }
.article-main-title { .article-main-title {
color: #f0f0f0; color: #f0f0f0;
} }
.article-main-title::placeholder { .article-main-title::placeholder {
color: #666; color: #666;
} }
.article-meta-info { .article-meta-info {
color: #bbb; color: #bbb;
} }
.meta-item:hover { .meta-item:hover {
color: #4a9eff; color: #4a9eff;
} }

View File

@@ -1,20 +1,16 @@
<!-- 文章列表组件 --> <!-- 文章列表组件 -->
<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="article in articleList"
:key="article.articleId"
@click="handleArticleClick(article)"
>
<h6 class="article-title">{{ article.title }}</h6> <h6 class="article-title">{{ article.title }}</h6>
<div v-if="article.marked" class="article-special-tag">标记文章</div> <div v-if="article.marked" class="article-special-tag">标记文章</div>
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p> <p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
@@ -31,6 +27,8 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 分页区域 -->
<el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
</transition-group> </transition-group>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="!loading && articleList.length === 0" class="empty-state-container"> <div v-if="!loading && articleList.length === 0" class="empty-state-container">
@@ -42,97 +40,146 @@
<script setup> <script setup>
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { formatDate, formatRelativeTime } from '@/utils/dateUtils' import { 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 { articleService, messageService, categoryAttributeService } from '@/services'
import PaginationComponent from '@/views/page.vue'
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
// ========== 组件初始化 ==========
// 全局状态管理 // 全局状态管理
const globalStore = useGlobalStore() 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 articleList = 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
let response = {}
// 检查URL参数确定获取文章的方式 // 1. 根据路由获取文章列表
const pathSegment = route.path.split('/')[2]; response = await getArticlesByRoute()
console.log(pathSegment) // console.log('更新后的文章列表:', response)
// 根据不同路径获取不同文章
switch (pathSegment) { // 2. 确保数据存在
case 'aericletype': if (!response.data.content || !Array.isArray(response.data.content)) {
// 按属性类型获取文章 articleList.value = []
const attributeData = globalStore.getValue('attribute') return
response = await articleService.getArticlesByAttributeId(attributeData.id)
break
case 'aericletitle':
// 按标题搜索文章
const titleData = globalStore.getValue('articleserarch')
response = await articleService.getArticlesByTitle(titleData.name)
break
case 'aericlestatus':
// 按状态获取文章
const statusData = globalStore.getValue('articlestatus')
response = await articleService.getArticlesByStatus(statusData.status)
break
default:
// 默认情况下,根据用户权限决定获取方式
if (globalStore.Login) {
// 获取所有文章(包含已删除)
console.log('管理员获取所有文章列表(包含已删除)')
response = await articleService.getAllArticlesWithDeleted()
} else {
// 获取所有文章
console.log('获取所有文章列表')
response = await articleService.getAllArticles()
}
} }
// 为每个文章获取留言数量和分类名称 // 3. 为文章列表补充额外信息
for (const article of response.data) { const enrichedArticles = await enrichArticlesWithExtraInfo(response.data.content)
try {
// 获取留言数量
const messageResponse = await messageService.getMessagesByArticleId(article.articleid)
// console.log(`文章ID: ${article.articleid}, 分类ID: ${article.attributeid}`)
// 获取分类名称
const categoryResponse = await categoryAttributeService.getAttributeById(article.attributeid)
if (categoryResponse && categoryResponse.data) {
article.categoryName = categoryResponse.data.attributename
} else {
article.categoryName = '未分类'
}
// 设置评论数量
article.commentCount = messageResponse.data.length > 0 ? messageResponse.data.length : 0
// 标准化ID字段名
article.articleId = article.articleid
// 标准化标记字段名
article.marked = article.mg
} catch (err) {
console.error(`获取文章${article.articleid}留言数量失败:`, err)
article.commentCount = 0
}
}
// 更新文章列表 // 4. 更新文章列表
// console.log(response.data) articleList.value = enrichedArticles
articleList.value = response.data || []
} catch (error) { } catch (error) {
console.error('获取文章列表失败:', error) console.error('获取文章列表失败:', error)
ElMessage.error('获取文章列表失败,请稍后重试') ElMessage.error('获取文章列表失败,请稍后重试')
@@ -141,24 +188,66 @@ const fetchArticles = async () => {
} }
} }
// ========== 文章交互模块 ==========
/** /**
* 处理文章点击事件 * 处理文章点击事件
* @param {Object} article - 文章对象 * @param {Object} article - 文章对象
*/ */
const handleArticleClick = (article) => { const handleArticleClick = (article) => {
// 增加文章浏览量 try {
articleService.incrementArticleViews(article.articleId) // 增加文章浏览量(异步操作,不阻塞后续流程)
// 清除之前的文章信息 articleService.incrementArticleViews(article.articleId).catch(err => {
globalStore.removeValue('articleInfo') console.error('增加文章浏览量失败:', err)
// 存储文章信息到全局状态 })
globalStore.setValue('articleInfo', article)
// 清除之前的文章信息
// 跳转到文章详情页 globalStore.removeValue('articleInfo')
router.push({
path: '/article', // 存储文章信息到全局状态
}) 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 {Array} newList - 新的文章列表
*/
// 组件挂载时获取数据 // 组件挂载时获取数据
onMounted(() => { onMounted(() => {
@@ -170,10 +259,13 @@ watch(
// 监听路由路径和查询参数变化 // 监听路由路径和查询参数变化
() => [route.path, route.query], () => [route.path, route.query],
// 路由变化时触发获取文章列表 // 路由变化时触发获取文章列表
() => { handleRouteChange,
fetchArticles() { deep: true }
console.log('路由变化,重新获取文章列表') )
},
// 监听原始文章列表变化,确保初始数据正确显示
watch(
() => articleList.value,
{ deep: true } { deep: true }
) )
</script> </script>
@@ -182,7 +274,6 @@ watch(
/* 文章列表容器样式 */ /* 文章列表容器样式 */
.article-list-container { .article-list-container {
max-width: 100%; max-width: 100%;
padding: 0 15px;
} }
/* 加载状态容器 */ /* 加载状态容器 */
@@ -209,6 +300,7 @@ watch(
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); border: 1px solid rgba(0, 0, 0, 0.05);
} }
/* 文章卡片悬停渐变效果 */ /* 文章卡片悬停渐变效果 */
.article-card::before { .article-card::before {
content: ''; content: '';
@@ -217,10 +309,10 @@ watch(
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;
@@ -230,7 +322,7 @@ watch(
.article-card:hover { .article-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15), box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15),
0 0 30px rgba(255, 255, 255, 0.2); 0 0 30px rgba(255, 255, 255, 0.2);
border-color: rgba(52, 152, 219, 0.3); border-color: rgba(52, 152, 219, 0.3);
} }
@@ -270,7 +362,17 @@ watch(
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.article-list-content {
position: relative;
padding: 0 0 30px 0;
}
.pagination-container {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
}
/* 文章内容预览样式 */ /* 文章内容预览样式 */
.article-content-preview { .article-content-preview {
color: #555; color: #555;
@@ -278,7 +380,6 @@ watch(
margin: 0; margin: 0;
font-size: 0.95rem; font-size: 0.95rem;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -318,28 +419,27 @@ watch(
/* 响应式设计 - 平板和手机 */ /* 响应式设计 - 平板和手机 */
@media (max-width: 768px) { @media (max-width: 768px) {
.article-list-container { .article-list-container {
padding: 0 10px;
} }
.article-card { .article-card {
padding: 18px; padding: 18px;
margin-bottom: 18px; margin-bottom: 18px;
gap: 10px; gap: 10px;
} }
.article-title { .article-title {
font-size: 1.125rem; font-size: 1.125rem;
} }
.article-meta-info { .article-meta-info {
font-size: 0.8rem; font-size: 0.8rem;
gap: 8px; gap: 8px;
} }
.article-content-preview { .article-content-preview {
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 响应式元信息分隔符 */ /* 响应式元信息分隔符 */
.article-views-count::before, .article-views-count::before,
.article-category-badge::before, .article-category-badge::before,
@@ -355,12 +455,12 @@ watch(
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.article-meta-info { .article-meta-info {
font-size: 0.75rem; font-size: 0.75rem;
gap: 6px; gap: 6px;
} }
.article-special-tag { .article-special-tag {
font-size: 0.7rem; font-size: 0.7rem;
padding: 2px 8px; padding: 2px 8px;

View File

@@ -105,7 +105,7 @@ const togglePassword = () => {
// 验证单个字段 // 验证单个字段
const validateField = (field) => { const validateField = (field) => {
console.log('validateField', field) // console.log('validateField', field)
errors.value = {} errors.value = {}
if (field === 'username' && !loginForm.username) { if (field === 'username' && !loginForm.username) {
@@ -150,7 +150,7 @@ const handleLogin = async () => {
ElMessage.error('登录失败,请检查用户名和密码') ElMessage.error('登录失败,请检查用户名和密码')
return return
} }
console.log('登录成功', user) // console.log('登录成功', user)
// 这里应该是实际的登录API调用 // 这里应该是实际的登录API调用
// console.log('登录请求数据:', loginForm) // console.log('登录请求数据:', loginForm)
@@ -162,7 +162,7 @@ const handleLogin = async () => {
globalStore.setValue('loginhomestatus', { globalStore.setValue('loginhomestatus', {
status: 1 // 2:删除 1:已发布 0:发布登录 status: 1 // 2:删除 1:已发布 0:发布登录
}) })
console.log('globalStore.Login', globalStore.Login) // console.log('globalStore.Login', globalStore.Login)
// 保存登录状态token // 保存登录状态token
if (user.token) { if (user.token) {
localStorage.setItem('token', user.token) localStorage.setItem('token', user.token)

View File

@@ -2,34 +2,28 @@
<div class="markdown-viewer"> <div class="markdown-viewer">
<div v-if="loading" class="loading-state">加载中...</div> <div v-if="loading" class="loading-state">加载中...</div>
<div v-else-if="error" class="error-state">加载失败: {{ error }}</div> <div v-else-if="error" class="error-state">加载失败: {{ error }}</div>
<v-md-editor <!-- <v-md-editor
v-else v-else
class="editor" class="editor"
v-model="content" v-model="content"
mode="preview" mode="preview"
:preview-theme="theme" :preview-theme="theme"
:height="height" :height="height"
/> /> -->
<MdPreview
v-else
:modelValue="content"
class="editor"
previewTheme="github"
codeTheme="github" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
// 导入v-md-editor相关组件和样式 // 导入v-md-editor相关组件和样式
import VMdEditor from '@kangc/v-md-editor' import { MdPreview } from 'md-editor-v3'
import '@kangc/v-md-editor/lib/style/base-editor.css' import 'md-editor-v3/lib/style.css'
// 导入主题使用github主题
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
import '@kangc/v-md-editor/lib/theme/style/github.css'
// 导入代码高亮
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
// 配置主题
VMdEditor.use(githubTheme, {
Hljs: hljs
})
// 定义组件属性 // 定义组件属性
const props = defineProps({ const props = defineProps({
// 从父组件传入的markdownid内容 // 从父组件传入的markdownid内容
@@ -37,21 +31,6 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
// 指定API端点
apiEndpoint: {
type: String,
default: '/help'
},
// 编辑器高度
height: {
type: String,
default: 'auto'
},
// 主题配置
theme: {
type: String,
default: 'github'
}
}) })
// 响应式数据 // 响应式数据
@@ -90,15 +69,10 @@ onMounted(() => {
} }
.editor { .editor {
border: 1px solid #e0e0e0; box-shadow: 0rgba(0, 0, 0, 0);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255,0);
} padding: 0 0 0 20px;
.editor:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
} }
/* 加载状态样式 */ /* 加载状态样式 */

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,20 @@
<template> <template>
<div class="nonsense-container"> <div class="nonsense-container">
<div class="nonsense-list"> <div class="nonsense-list">
<div class="nonsense-item" v-for="item in nonsenseList" :key="item.id"> <!-- 加载状态 -->
<div v-if="loading" class="loading-state-container">
<el-skeleton :count="5" />
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-state-container">
<div>加载失败请稍后重试</div>
<el-button type="primary" @click="handleRetry">重试</el-button>
</div>
<!-- 空状态 -->
<div v-if="displayedNonsenseList.length === 0" class="empty-state-container">
<div>暂无吐槽内容</div>
</div>
<div class="nonsense-item" v-for="item in displayedNonsenseList" :key="item.id">
<div class="nonsense-meta-info"> <div class="nonsense-meta-info">
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span> <span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span>
<div class="article-status-badge-container" v-if="globalStore.Login"> <div class="article-status-badge-container" v-if="globalStore.Login">
@@ -17,6 +30,9 @@
:style="getCharStyle(item.id, index)">{{ char }}</span> :style="getCharStyle(item.id, index)">{{ char }}</span>
</div> </div>
</div> </div>
// 分页区域
<PaginationComponent class="pagination-container" :list="nonsenseList" :pageSize="10"
@changePage="handleCurrentDataUpdate" />
</div> </div>
</div> </div>
</template> </template>
@@ -25,6 +41,8 @@
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import { nonsenseService } from '@/services' import { nonsenseService } from '@/services'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ElMessageBox } from 'element-plus'
import PaginationComponent from '@/views/page.vue'
import { formatRelativeTime } from '@/utils/dateUtils' import { formatRelativeTime } from '@/utils/dateUtils'
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
@@ -37,6 +55,24 @@ const nonsenseList = ref([])
// 存储字符引用和样式的映射 // 存储字符引用和样式的映射
const charRefs = ref(new Map()) const charRefs = ref(new Map())
const charStyles = ref(new Map()) const charStyles = ref(new Map())
// 显示的吐槽内容列表
const displayedNonsenseList = ref([])
// 加载状态
const loading = ref(false)
// 错误状态
const error = ref(false)
// 处理分页数据更新
const handleCurrentDataUpdate = (data) => {
displayedNonsenseList.value = data
// console.log(data)
}
// 重试加载
const handleRetry = () => {
error.value = false
loadNonsenseList()
}
// 定时器引用 // 定时器引用
let colorChangeTimer = null let colorChangeTimer = null
@@ -45,24 +81,31 @@ let colorChangeTimer = null
* 加载所有吐槽内容 * 加载所有吐槽内容
*/ */
const loadNonsenseList = async () => { const loadNonsenseList = async () => {
// 设置加载状态
loading.value = true
error.value = false
try { try {
const response = await nonsenseService.getNonsenseByStatus(1) const response = await nonsenseService.getNonsenseByStatus(1)
if (response.code === 200) { if (response.code === 200) {
nonsenseList.value = response.data nonsenseList.value = response.data
} else { } else {
ElMessage.error('加载吐槽内容失败') ElMessage.error('加载吐槽内容失败')
error.value = true
} }
} catch (error) { } catch (err) {
console.error('加载吐槽内容失败:', error) console.error('加载吐槽内容失败:', err)
error.value = true
} finally { } finally {
console.log('加载吐槽内容完成') // 结束加载状态
loading.value = false
// console.log('加载吐槽内容完成')
} }
} }
// 编辑吐槽内容 // 编辑吐槽内容
const handleEdit = (item) => { const handleEdit = (item) => {
// 清除更新文章状态 // 清除更新文章状态
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', { ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
confirmButtonText: '保存', confirmButtonText: '保存',
cancelButtonText: '取消', cancelButtonText: '取消',
inputValue: item.content, inputValue: item.content,
@@ -266,7 +309,6 @@ onBeforeUnmount(() => {
.nonsense-container { .nonsense-container {
/* background-color: rgba(255, 255, 255, 0.85); */ /* 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; transition: box-shadow 0.3s ease;
} }
@@ -328,6 +370,7 @@ onBeforeUnmount(() => {
margin-bottom: 8px; margin-bottom: 8px;
text-align: right; text-align: right;
} }
.nonsense-meta-info span { .nonsense-meta-info span {
padding: 4px 8px; padding: 4px 8px;
margin-right: 8px; margin-right: 8px;
@@ -345,8 +388,7 @@ onBeforeUnmount(() => {
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.nonsense-container { .nonsense-container {
padding: 14px 4px 10px 4px; margin: 0;
margin: 0 8px;
} }
.nonsense-header h1 { .nonsense-header h1 {
@@ -358,9 +400,9 @@ onBeforeUnmount(() => {
line-height: 1.6; line-height: 1.6;
} }
.nonsense-list { /* .nonsense-list {
gap: 12px; gap: 12px;
} } */
.nonsense-item { .nonsense-item {
padding: 14px 16px 10px 16px; padding: 14px 16px 10px 16px;

199
src/views/page.vue Normal file
View 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>

View File

@@ -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
}
}
}, },
}) })