hongshu-web v1.0
51
.gitignore
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
######################################################################
|
||||
# Build Tools
|
||||
|
||||
.gradle
|
||||
/build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
######################################################################
|
||||
# IDE
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### JRebel ###
|
||||
rebel.xml
|
||||
|
||||
### NetBeans ###
|
||||
nbproject/private/
|
||||
build/*
|
||||
nbbuild/
|
||||
dist/
|
||||
nbdist/
|
||||
.nb-gradle/
|
||||
|
||||
######################################################################
|
||||
# Others
|
||||
*.log
|
||||
*.xml.versionsBackup
|
||||
*.swp
|
||||
|
||||
!*/build/*.java
|
||||
!*/build/*.html
|
||||
!*/build/*.xml
|
||||
|
||||
node_modules
|
||||
*.idea
|
||||
|
36
README.en.md
@@ -1,36 +0,0 @@
|
||||
# HongShu-Web
|
||||
|
||||
#### Description
|
||||
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
|
||||
|
||||
#### Software Architecture
|
||||
Software architecture description
|
||||
|
||||
#### Installation
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Instructions
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Contribution
|
||||
|
||||
1. Fork the repository
|
||||
2. Create Feat_xxx branch
|
||||
3. Commit your code
|
||||
4. Create Pull Request
|
||||
|
||||
|
||||
#### Gitee Feature
|
||||
|
||||
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
||||
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
||||
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
||||
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
||||
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
158
README.md
Normal file → Executable file
@@ -1,39 +1,127 @@
|
||||
# HongShu-Web
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://image.mayongjian.cn/2024/07/03/1de3ee08e0a34ab6bf9a163d380fb596.png" style="width: 100px">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">HongShu v1.0</h1>
|
||||
<h4 align="center">基于SpringBoot+Vue前后端分离仿小红书项目</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/Maverick_Ma/hongshu-web/stargazers">
|
||||
<img src="https://gitee.com/Maverick_Ma/hongshu-web/badge/star.svg?theme=dark"></a>
|
||||
<a href="https://gitee.com/Maverick_Ma/hongshu-web">
|
||||
<img src="https://img.shields.io/badge/HongShu-v1.0-brightgreen.svg"></a>
|
||||
<a href="https://gitee.com/Maverick_Ma/hongshu-web/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||
</p>
|
||||
|
||||
#### 介绍
|
||||
{**以下是 Gitee 平台说明,您可以替换此简介**
|
||||
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
|
||||
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
|
||||
## 平台简介
|
||||
* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [ts](https://www.tslang.cn) 版本。
|
||||
* 配套后端代码仓库地址[HongShu](https://gitee.com/Maverick_Ma/hongshu.git)
|
||||
|
||||
#### 软件架构
|
||||
软件架构说明
|
||||
* 阿里云优惠券:[点我领取](https://www.aliyun.com/minisite/goods?source=5176.11533457&userCode=ojvsntx1)
|
||||
* 腾讯云优惠券:[点我领取](https://curl.qcloud.com/efTJbNyi)
|
||||
|
||||
## 前端运行
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/Maverick_Ma/hongshu-web.git
|
||||
|
||||
# 进入项目目录
|
||||
cd HongShu-Web
|
||||
|
||||
# 安装依赖
|
||||
yarn --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
yarn dev
|
||||
|
||||
# 构建测试环境 yarn build:stage
|
||||
# 构建生产环境 yarn build:prod
|
||||
# 前端访问地址 http://localhost:80
|
||||
```
|
||||
|
||||
## 用户端内置功能
|
||||
1. 瀑布流展示笔记,懒加载笔记图片
|
||||
2. 笔记分类查询,使用elastcsearch做关键词搜索查询笔记
|
||||
3. 关键词使用elastcsearch做高亮查询
|
||||
4. 动态展示,展示个人和好友动态
|
||||
5. 支持私信聊天,关注用户,评论笔记,点赞笔记和点赞图片功能,收藏笔记功能
|
||||
6. 使用websocket消息通知,用户发送的消息会实时通知,消息页面会实时收到当前用户未读消息数量
|
||||
7. 双token登陆,使用redis做对象缓存
|
||||
8. 发布和修改笔记功能,使用七牛云oss对象存储图片
|
||||
9. 个人信息展示,展示当前用户发布的笔记和点赞收藏的笔记
|
||||
|
||||
## 管理端内置功能
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
|
||||
3. 岗位管理:配置系统用户所属担任职务。
|
||||
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
14. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
15. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
16. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
17. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
|
||||
|
||||
## 2.0版本实现中⚠️
|
||||
1. 添加移动端版本
|
||||
2. 重构实现 SpringCloud 微服务架构版本
|
||||
3. 加入商城购物功能
|
||||
4. 加入协同过滤算法优化优化页面推荐功能
|
||||
5. 使用 MQ+Redis 优化点赞、收藏、浏览功能
|
||||
|
||||
- 如有项目运行问题、部署问题可联系
|
||||
|
||||
<img src="src/assets/images/wx.png" style="width: 150px"/>
|
||||
|
||||
## 在线体验
|
||||
* 因无服务器暂无体验地址,后续会采购演示服务器,也感谢小伙伴打赏支持❤️。
|
||||
|
||||
<img src="src/assets/images/pay.png" style="width: 300px"/>
|
||||
|
||||
* 文档及资料会暂时放到我的个人博客:[点我进入](https://mayongjian.cn)
|
||||
|
||||
## 视频演示
|
||||
[点击查看](https://www.bilibili.com/video/BV1QP8dekEGq/?spm_id_from=333.999.list.card_archive.click&vd_source=ec9224821314432ac6e12dc7d500d74b)
|
||||
|
||||
|
||||
#### 安装教程
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 使用说明
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
## 演示图
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/login.png"/></td>
|
||||
<td><img src="src/assets/images/dashboard.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/search.png"/></td>
|
||||
<td><img src="src/assets/images/message.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/follow.png"/></td>
|
||||
<td><img src="src/assets/images/trends.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/publish.png"/></td>
|
||||
<td><img src="src/assets/images/user.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/admin-login.png"/></td>
|
||||
<td><img src="src/assets/images/data.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/category.png"/></td>
|
||||
<td><img src="src/assets/images/member.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/note.png"/></td>
|
||||
<td><img src="src/assets/images/album.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="src/assets/images/comment.png"/></td>
|
||||
<td><img src="src/assets/images/log.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>红薯</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
13428
package-lock.json
generated
Normal file
49
package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "vite-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint:eslint": "eslint \"src/**/*.{vue,ts,js}\" --fix",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,ts,json,css,less,scss,vue,html,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@lhlyu/vue-virtual-waterfall": "^1.0.2",
|
||||
"@opentiny/vue": "3",
|
||||
"animate.css": "^4.1.1",
|
||||
"async-validator": "^4.2.5",
|
||||
"axios": "^1.5.1",
|
||||
"element-plus": "^2.4.1",
|
||||
"goeasy": "2.10.14",
|
||||
"lwaterfall": "^1.0.6",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-masonry": "^0.16.0",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-stick": "^1.0.7",
|
||||
"vue-waterfall-plugin-next": "^2.2.4",
|
||||
"vue3-lazy": "^1.0.0-alpha.1",
|
||||
"vue3-plock": "^0.0.2",
|
||||
"zego-zim-web": "^2.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"less": "^4.2.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.77.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vue-tsc": "^1.8.5",
|
||||
"water-fall3": "^0.0.3"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 19 KiB |
14
src/App.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<router-view :key="routerKey"></router-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
const route = useRoute();
|
||||
const routerKey = computed(() => {
|
||||
return route.path + Math.random();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
11
src/api/category.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import request from "@/utils/request";
|
||||
/**
|
||||
* 获取树形分类数据
|
||||
* @returns 分类数据
|
||||
*/
|
||||
export const getCategoryTreeData = () => {
|
||||
return request<any>({
|
||||
url: "/web/category/getCategoryTreeData",
|
||||
method: "get",
|
||||
});
|
||||
};
|
86
src/api/comment.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import request from "@/utils/request";
|
||||
import type { CommentDTO } from "@/type/comment";
|
||||
|
||||
/**
|
||||
* 得到所有的一级评论并携带二级评论
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @param noteId 笔记id
|
||||
* @returns 评论结果集
|
||||
*/
|
||||
export const getCommentWithCommentByNoteId = (
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
noteId: string
|
||||
) => {
|
||||
return request<any>({
|
||||
url: `/web/comment/getCommentWithCommentByNoteId/${currentPage}/${pageSize}`,
|
||||
method: "get",
|
||||
params: {
|
||||
noteId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存评论
|
||||
* @param data 评论实体
|
||||
* @returns 增加后的评论实体
|
||||
*/
|
||||
export const saveCommentByDTO = (data: CommentDTO) => {
|
||||
return request<any>({
|
||||
url: `/web/comment/saveCommentByDTO`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据评论id同步评论集
|
||||
* @param data 评论id数据集
|
||||
* @returns
|
||||
*/
|
||||
export const syncCommentByIds = (data: Array<string>) => {
|
||||
return request<any>({
|
||||
url: `/web/comment/syncCommentByIds`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据一级评论id获取所有的二级评论
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @param oneCommentId 一级评论id
|
||||
* @returns 评论结果集
|
||||
*/
|
||||
export const getTwoCommentPageByOneCommentId = (
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
oneCommentId: string
|
||||
) => {
|
||||
return request<any>({
|
||||
url: `/web/comment/getTwoCommentPageByOneCommentId/${currentPage}/${pageSize}`,
|
||||
method: "get",
|
||||
params: {
|
||||
oneCommentId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前用户通知的评论集
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @returns 评论结果集
|
||||
*/
|
||||
export const getNoticeComment = (
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
) => {
|
||||
return request<any>({
|
||||
url: `/web/comment/getNoticeComment/${currentPage}/${pageSize}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
57
src/api/follower.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 得到关注用户的所有动态
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @returns
|
||||
*/
|
||||
export const getFollowTrend = (currentPage: number, pageSize: number) => {
|
||||
return request<any>({
|
||||
url: `/web/follower/getFollowTrend/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
* @param followerId 关注用户id
|
||||
* @returns
|
||||
*/
|
||||
export const followById = (followerId: string) => {
|
||||
return request<any>({
|
||||
url: `/web/follower/followById`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
followerId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前用户是否关注
|
||||
* @param followerId 关注的用户id
|
||||
* @returns
|
||||
*/
|
||||
export const isFollow = (followerId: string) => {
|
||||
return request<any>({
|
||||
url: `/web/follower/isFollow`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
followerId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 得到当前用户的最新关注信息
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @returns FollowerVo
|
||||
*/
|
||||
export const getNoticeFollower = (currentPage: number, pageSize: number) => {
|
||||
return request<any>({
|
||||
url: `/web/follower/getNoticeFollower/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
74
src/api/im.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 得到所有聊天的记录数量
|
||||
* @returns 聊天数量
|
||||
*/
|
||||
export const getCountMessage = () => {
|
||||
return request<any>({
|
||||
url: "/web/im/chat/getCountMessage", // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前用户下所有聊天的用户信息
|
||||
* @returns 聊天的用户信息
|
||||
*/
|
||||
export const getChatUserList = () => {
|
||||
return request<any>({
|
||||
url: "/web/im/chat/getChatUserList", // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除聊天数量
|
||||
* @param sendUid 发送方的用户id
|
||||
* @param type 类型
|
||||
* @returns success
|
||||
*/
|
||||
export const clearMessageCount = (sendUid:string,type:number) => {
|
||||
return request<any>({
|
||||
url: "/web/im/chat/clearMessageCount", // mock接口
|
||||
method: "get",
|
||||
params:{
|
||||
sendUid,
|
||||
type
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有的聊天记录
|
||||
* @param currentPage 分页
|
||||
* @param pageSize 分页数
|
||||
* @param acceptUid 接收方的用户id
|
||||
* @returns 聊天记录
|
||||
*/
|
||||
export const getAllChatRecord = (
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
acceptUid: string
|
||||
) => {
|
||||
return request<any>({
|
||||
url: `/web/im/chat/getAllChatRecord/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
acceptUid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param data 消息实体
|
||||
* @returns success
|
||||
*/
|
||||
export const sendMsg = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/im/chat/sendMsg", // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
42
src/api/likeOrCollection.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import request from "@/utils/request";
|
||||
import type { LikeOrCollectionDTO } from "@/type/likeOrCollection";
|
||||
/**
|
||||
* 点赞或收藏
|
||||
* @param data 点赞收藏实体
|
||||
* @returns success
|
||||
*/
|
||||
export const likeOrCollectionByDTO = (data: LikeOrCollectionDTO) => {
|
||||
return request<any>({
|
||||
url: `/web/likeOrCollection/likeOrCollectionByDTO`, // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否点赞或收藏
|
||||
* @param data 点赞收藏实体
|
||||
* @returns
|
||||
*/
|
||||
export const isLikeOrCollection = (data: LikeOrCollectionDTO) => {
|
||||
return request<any>({
|
||||
url: `/web/likeOrCollection/isLikeOrCollection`, // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 得到当前用户最新的点赞和收藏信息
|
||||
* @param currentPage 当前页
|
||||
* @param pageSize 分页数
|
||||
* @returns page
|
||||
*/
|
||||
export const getNoticeLikeOrCollection = (currentPage: number, pageSize: number) => {
|
||||
return request<any>({
|
||||
url: `/web/likeOrCollection/getNoticeLikeOrCollection/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
|
72
src/api/note.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 根据笔记id获取笔记
|
||||
* @param noteId 笔记id
|
||||
* @returns 笔记
|
||||
*/
|
||||
export const getNoteById = (noteId: string) => {
|
||||
return request<any>({
|
||||
url: "/web/note/getNoteById", // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
noteId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存笔记
|
||||
* @param data 笔记实体
|
||||
* @returns 笔记id
|
||||
*/
|
||||
export const saveNoteByDTO = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/note/saveNoteByDTO", // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: { "Content-Type": "multipart/form-data;boundary=----WebKitFormBoundaryk4ZvuPo6pkphe7Pl" },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新笔记
|
||||
* @param data 笔记实体
|
||||
* @returns 笔记id
|
||||
*/
|
||||
export const updateNoteByDTO = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/note/updateNoteByDTO", // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: { "Content-Type": "multipart/form-data;boundary=----WebKitFormBoundaryk4ZvuPo6pkphe7Pl" },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 置顶笔记
|
||||
* @param noteId
|
||||
* @returns
|
||||
*/
|
||||
export const pinnedNote = (noteId: string) => {
|
||||
return request<any>({
|
||||
url: "/web/note/pinnedNote", // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
noteId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除笔记
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const deleteNoteByIds = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/note/deleteNoteByIds", // mock接口
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
};
|
79
src/api/search.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import request from "@/utils/request";
|
||||
import { NoteDTO } from "@/type/note"
|
||||
|
||||
/**
|
||||
*
|
||||
* @param currentPage
|
||||
* @param pageSize
|
||||
* @returns
|
||||
*/
|
||||
export const getRecommendNote = (currentPage: number, pageSize: number) => {
|
||||
return request<any>({
|
||||
url: `/web/es/note/getRecommendNote/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param currentPage
|
||||
* @param pageSize
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const getNoteByDTO = (currentPage: number, pageSize: number, data: NoteDTO) => {
|
||||
return request<any>({
|
||||
url: `/web/es/note/getNoteByDTO/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "post",
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
export const getCategoryAgg = (data: NoteDTO) => {
|
||||
return request<any>({
|
||||
url: `/web/es/note/getCategoryAgg`, // mock接口
|
||||
method: "post",
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param keyword
|
||||
* @returns
|
||||
*/
|
||||
export const getRecordByKeyWord = (keyword: string) => {
|
||||
return request<any>({
|
||||
url: `/web/es/record/getRecordByKeyWord`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
keyword
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getHotRecord = () => {
|
||||
return request<any>({
|
||||
url: `web/es/record/getHotRecord`, // mock接口
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param keyword
|
||||
* @returns
|
||||
*/
|
||||
export const addRecord = (keyword: string) => {
|
||||
return request<any>({
|
||||
url: `/web/es/record/addRecord`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
keyword
|
||||
}
|
||||
});
|
||||
};
|
18
src/api/tag.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param currentPage
|
||||
* @param pageSize
|
||||
* @param keyword
|
||||
* @returns
|
||||
*/
|
||||
export const getTagByKeyword = (currentPage: number, pageSize: number,keyword:string) => {
|
||||
return request<any>({
|
||||
url: `/web/tag/getTagByKeyword/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
params:{
|
||||
keyword
|
||||
}
|
||||
});
|
||||
};
|
146
src/api/user.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import request from "@/utils/request";
|
||||
import type { UserLogin } from "@/type/user";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const login = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/auth/login", // mock接口
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param deptId
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
export function importFile(deptId: number, file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return request({
|
||||
url: "/api/v1/users/_import",
|
||||
method: "post",
|
||||
params: { deptId: deptId },
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param accessToken
|
||||
* @returns
|
||||
*/
|
||||
export const getUserInfoByToken = (accessToken: string) => {
|
||||
return request<any>({
|
||||
url: "/web/auth/getUserInfoByToken", // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
export const refreshToken = (refreshToken: string) => {
|
||||
return request<any>({
|
||||
url: `/web/auth/refreshToken`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
refreshToken,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const loginByCode = (data: UserLogin) => {
|
||||
return request<any>({
|
||||
url: "/web/auth/loginByCode", // mock接口
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param currentPage
|
||||
* @param pageSize
|
||||
* @param userId
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const getTrendByUser = (currentPage:number,pageSize:number,userId:string,type:number) => {
|
||||
return request<any>({
|
||||
url: `/web/user/getTrendByUser/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
userId,
|
||||
type
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
* @returns
|
||||
*/
|
||||
export const getUserById = (userId:string) => {
|
||||
return request<any>({
|
||||
url: `/web/user/getUserById`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
userId
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
* @returns
|
||||
*/
|
||||
export const loginOut = (userId:string) => {
|
||||
return request<any>({
|
||||
url: `/web/auth/loginOut`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
userId
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUser = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/web/user/updateUser", // mock接口
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserByKeyword = (currentPage: number, pageSize: number, keyword: string) => {
|
||||
return request<any>({
|
||||
url: `/web/user/getUserByKeyword/${currentPage}/${pageSize}`, // mock接口
|
||||
method: "get",
|
||||
params: {
|
||||
keyword
|
||||
},
|
||||
});
|
||||
};
|
||||
|
10
src/api/util.ts
Executable file
@@ -0,0 +1,10 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
|
||||
export const toUp = (data: any) => {
|
||||
return request<any>({
|
||||
url: "/util/dm/toUp", // mock接口
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
BIN
src/assets/error.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
539
src/assets/font_4394635_lwuldvb474/demo.css
Normal file
@@ -0,0 +1,539 @@
|
||||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: "iconfont logo";
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "iconfont logo";
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
transition: font-size 0.25s linear, width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown>p,
|
||||
.markdown>blockquote,
|
||||
.markdown>.highlight,
|
||||
.markdown>ol,
|
||||
.markdown>ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul>li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown>ul li,
|
||||
.markdown blockquote ul>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown>ul li p,
|
||||
.markdown>ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol>li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown>ol li,
|
||||
.markdown blockquote ol>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table th,
|
||||
.markdown>table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown>br,
|
||||
.markdown>p>br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre)>code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre)>code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
234
src/assets/font_4394635_lwuldvb474/demo_index.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>iconfont Demo</title>
|
||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link rel="stylesheet" href="iconfont.css">
|
||||
<script src="iconfont.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
||||
<!-- 代码高亮 -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
||||
<style>
|
||||
.main .logo {
|
||||
margin-top: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main .logo .sub-title {
|
||||
margin-left: 0.5em;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
||||
|
||||
</a></h1>
|
||||
<div class="nav-tabs">
|
||||
<ul id="tabs" class="dib-box">
|
||||
<li class="dib active"><span>Unicode</span></li>
|
||||
<li class="dib"><span>Font class</span></li>
|
||||
<li class="dib"><span>Symbol</span></li>
|
||||
</ul>
|
||||
|
||||
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4394635" target="_blank" class="nav-more">查看项目</a>
|
||||
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">follow</div>
|
||||
<div class="code-name">&#xe83b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">follow-fill</div>
|
||||
<div class="code-name">&#xe83c;</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="unicode-">Unicode 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
||||
<ul>
|
||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
||||
</blockquote>
|
||||
<p>Unicode 使用步骤如下:</p>
|
||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1704005455654') format('woff2'),
|
||||
url('iconfont.woff?t=1704005455654') format('woff'),
|
||||
url('iconfont.ttf?t=1704005455654') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
<pre><code class="language-css"
|
||||
>.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
||||
<pre>
|
||||
<code class="language-html"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-follow"></span>
|
||||
<div class="name">
|
||||
follow
|
||||
</div>
|
||||
<div class="code-name">.icon-follow
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-follow-fill"></span>
|
||||
<div class="name">
|
||||
follow-fill
|
||||
</div>
|
||||
<div class="code-name">.icon-follow-fill
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="font-class-">font-class 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
||||
<ul>
|
||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"
|
||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-follow"></use>
|
||||
</svg>
|
||||
<div class="name">follow</div>
|
||||
<div class="code-name">#icon-follow</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-follow-fill"></use>
|
||||
</svg>
|
||||
<div class="name">follow-fill</div>
|
||||
<div class="code-name">#icon-follow-fill</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="symbol-">Symbol 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
||||
<ul>
|
||||
<li>支持多色图标了,不再受单色限制。</li>
|
||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.tab-container .content:first').show()
|
||||
|
||||
$('#tabs li').click(function (e) {
|
||||
var tabContent = $('.tab-container .content')
|
||||
var index = $(this).index()
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
return
|
||||
} else {
|
||||
$('#tabs li').removeClass('active')
|
||||
$(this).addClass('active')
|
||||
|
||||
tabContent.hide().eq(index).fadeIn()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
23
src/assets/font_4394635_lwuldvb474/iconfont.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4394635 */
|
||||
src: url('iconfont.woff2?t=1704005455654') format('woff2'),
|
||||
url('iconfont.woff?t=1704005455654') format('woff'),
|
||||
url('iconfont.ttf?t=1704005455654') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-follow:before {
|
||||
content: "\e83b";
|
||||
}
|
||||
|
||||
.icon-follow-fill:before {
|
||||
content: "\e83c";
|
||||
}
|
||||
|
1
src/assets/font_4394635_lwuldvb474/iconfont.js
Normal file
@@ -0,0 +1 @@
|
||||
window._iconfont_svg_string_4394635='<svg><symbol id="icon-follow" viewBox="0 0 1024 1024"><path d="M480.304762 237.738667l7.582476 7.68c6.826667 6.826667 14.872381 14.677333 24.112762 23.576381l10.50819-10.191238c8.289524-8.045714 15.36-15.067429 21.211429-21.065143a220.891429 220.891429 0 0 1 317.074286 0c86.747429 88.600381 87.576381 231.765333 2.438095 321.365333L512 902.095238 160.768 559.128381c-85.113905-89.624381-84.309333-232.789333 2.438095-321.389714a220.891429 220.891429 0 0 1 317.074286 0z m335.043048 265.313523c52.882286-61.44 50.492952-155.599238-6.826667-214.137904a147.748571 147.748571 0 0 0-212.553143 0l-17.286095 17.261714a1928.289524 1928.289524 0 0 1-5.022476 4.924952L512 370.492952l-56.32-54.223238-15.11619-14.774857-12.53181-12.580571a147.748571 147.748571 0 0 0-212.577524 0c-58.806857 60.074667-59.782095 157.671619-2.511238 218.940952L512 799.865905l299.056762-292.035048 4.291048-4.778667z" ></path></symbol><symbol id="icon-follow-fill" viewBox="0 0 1024 1024"><path d="M480.304762 237.738667l7.582476 7.68c6.826667 6.826667 14.872381 14.677333 24.112762 23.576381l10.50819-10.191238c8.289524-8.045714 15.36-15.067429 21.211429-21.065143a220.891429 220.891429 0 0 1 317.074286 0c86.747429 88.600381 87.576381 231.765333 2.438095 321.365333L512 902.095238 160.768 559.128381c-85.113905-89.624381-84.309333-232.789333 2.438095-321.389714a220.891429 220.891429 0 0 1 317.074286 0z" ></path></symbol></svg>',function(n){var t=(t=document.getElementsByTagName("script"))[t.length-1],e=t.getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var o,i,c,l,a,d=function(t,e){e.parentNode.insertBefore(t,e)};if(e&&!n.__iconfont__svg__cssinject__){n.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}o=function(){var t,e=document.createElement("div");e.innerHTML=n._iconfont_svg_string_4394635,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?d(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),o()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(c=o,l=n.document,a=!1,r(),l.onreadystatechange=function(){"complete"==l.readyState&&(l.onreadystatechange=null,s())})}function s(){a||(a=!0,c())}function r(){try{l.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}s()}}(window);
|
23
src/assets/font_4394635_lwuldvb474/iconfont.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "4394635",
|
||||
"name": "yanhuo",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "34453000",
|
||||
"name": "follow",
|
||||
"font_class": "follow",
|
||||
"unicode": "e83b",
|
||||
"unicode_decimal": 59451
|
||||
},
|
||||
{
|
||||
"icon_id": "34453001",
|
||||
"name": "follow-fill",
|
||||
"font_class": "follow-fill",
|
||||
"unicode": "e83c",
|
||||
"unicode_decimal": 59452
|
||||
}
|
||||
]
|
||||
}
|
BIN
src/assets/font_4394635_lwuldvb474/iconfont.ttf
Normal file
BIN
src/assets/font_4394635_lwuldvb474/iconfont.woff
Normal file
BIN
src/assets/font_4394635_lwuldvb474/iconfont.woff2
Normal file
BIN
src/assets/images/1.png
Executable file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/2.png
Executable file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/3.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
src/assets/images/admin-login.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/album.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/images/category.png
Normal file
After Width: | Height: | Size: 781 KiB |
BIN
src/assets/images/comment.png
Normal file
After Width: | Height: | Size: 440 KiB |
BIN
src/assets/images/dashboard.png
Normal file
After Width: | Height: | Size: 5.7 MiB |
BIN
src/assets/images/data.png
Normal file
After Width: | Height: | Size: 416 KiB |
BIN
src/assets/images/follow.png
Normal file
After Width: | Height: | Size: 467 KiB |
BIN
src/assets/images/log.png
Normal file
After Width: | Height: | Size: 635 KiB |
BIN
src/assets/images/login.png
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
src/assets/images/member.png
Normal file
After Width: | Height: | Size: 853 KiB |
BIN
src/assets/images/message.png
Normal file
After Width: | Height: | Size: 326 KiB |
BIN
src/assets/images/note.png
Normal file
After Width: | Height: | Size: 862 KiB |
BIN
src/assets/images/pay.png
Normal file
After Width: | Height: | Size: 731 KiB |
BIN
src/assets/images/publish.png
Normal file
After Width: | Height: | Size: 304 KiB |
BIN
src/assets/images/search.png
Normal file
After Width: | Height: | Size: 5.0 MiB |
BIN
src/assets/images/trends.png
Normal file
After Width: | Height: | Size: 1016 KiB |
BIN
src/assets/images/user.png
Normal file
After Width: | Height: | Size: 3.7 MiB |
BIN
src/assets/images/wx.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
src/assets/loading.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
451
src/components/Chat.vue
Executable file
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div
|
||||
class="container"
|
||||
style="transition: background-color 0.4s ease 0s;
|
||||
hsla(0,0%,100%,0.98)"
|
||||
>
|
||||
<div class="chat-container">
|
||||
<header class="chat-header">
|
||||
<div class="header-left"></div>
|
||||
<div class="header-user">
|
||||
<el-avatar :src="acceptUser.avatar" />
|
||||
<span style="margin-left: 0.3125rem">{{ acceptUser.username }}</span>
|
||||
</div>
|
||||
<div class="header-tool">
|
||||
<More class="icon-item"></More>
|
||||
</div>
|
||||
</header>
|
||||
<hr color="#f4f4f4" />
|
||||
<main class="chat-main">
|
||||
<div class="chat-record" ref="ChatRef" @scroll="showScroll()">
|
||||
<div v-for="(item, index) in dataList" :key="index">
|
||||
<div class="message-my-item" v-if="item.acceptUid === acceptUser.id">
|
||||
<Loading v-show="item.isLoading" style="width: 0.8em; height: 0.8em; margin-right: 0.5rem" />
|
||||
<div class="message-my-conent" v-if="item.msgType == 1">{{ item.content }}</div>
|
||||
<img :src="item.content" class="message-img" v-if="item.msgType == 2" />
|
||||
<div class="user-avatar">
|
||||
<el-avatar :src="currentUser.avatar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-item" v-else>
|
||||
<div class="user-avatar">
|
||||
<el-avatar :src="acceptUser.avatar" />
|
||||
</div>
|
||||
<div class="message-conent" v-if="item.msgType == 1">{{ item.content }}</div>
|
||||
<img :src="item.content" class="message-img" v-if="item.msgType == 2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr color="#f4f4f4" />
|
||||
<div class="chat-input">
|
||||
<div class="input-tool">
|
||||
<div class="tool-left">
|
||||
<PieChart class="icon-item"></PieChart>
|
||||
<el-upload :auto-upload="false" :show-file-list="false" :on-change="handleChange">
|
||||
<Picture class="icon-item"></Picture>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="tool-history">
|
||||
<Clock class="icon-item"></Clock>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <textarea type="textarea" v-model="content" class="input-content" rows="15" @keyup.enter="submit" /> -->
|
||||
<div class="input-content">
|
||||
<p
|
||||
id="post-textarea"
|
||||
ref="postContent"
|
||||
class="post-content"
|
||||
contenteditable="true"
|
||||
data-tribute="true"
|
||||
placeholder="请输入消息,支持发送图片哦~"
|
||||
@keyup.enter="submit"
|
||||
></p>
|
||||
<div class="input-btn">
|
||||
<div></div>
|
||||
<el-button type="primary" round style="width: 5.55rem" @click="submit">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="close-cricle" @click="close">
|
||||
<div class="close close-mask-white">
|
||||
<Close style="width: 1.2em; height: 1.2em; color: rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { More, PieChart, Picture, Clock, Close, Loading } from "@element-plus/icons-vue";
|
||||
import { ref, onMounted, watch, nextTick } from "vue";
|
||||
import { getUserById } from "@/api/user";
|
||||
import { getAllChatRecord, sendMsg } from "@/api/im";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import { useImStore } from "@/store/imStore";
|
||||
import type { UploadProps } from "element-plus";
|
||||
import { convertImgToBase64 } from "@/utils/util";
|
||||
import { getRandomString } from "@/utils/util";
|
||||
|
||||
const imStore = useImStore();
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps({
|
||||
acceptUid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const ChatRef = ref();
|
||||
const currentUser = ref<any>({});
|
||||
const acceptUser = ref<any>({});
|
||||
const dataList = ref<any>();
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 15;
|
||||
const messageTotal = ref(0);
|
||||
const postContent = ref(null);
|
||||
|
||||
watch(
|
||||
() => imStore.message,
|
||||
(newVal) => {
|
||||
if (newVal.sendUid === acceptUser.value.id) {
|
||||
insertMessage(newVal);
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const insertMessage = async (message: any) => {
|
||||
dataList.value?.push(message);
|
||||
await nextTick();
|
||||
// 滚动到最底部
|
||||
ChatRef.value.lastElementChild.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
const emit = defineEmits(["clickChat"]);
|
||||
|
||||
const close = () => {
|
||||
emit("clickChat", props.acceptUid);
|
||||
};
|
||||
|
||||
// 选择图片
|
||||
const handleChange: UploadProps["onChange"] = (uploadFile) => {
|
||||
const imgSrc = URL.createObjectURL(uploadFile.raw!);
|
||||
convertImgToBase64(
|
||||
uploadFile.raw!,
|
||||
(data: any) => {
|
||||
document.getElementById("post-textarea")!.innerHTML +=
|
||||
`<img src='${imgSrc}' text='${data}' style='width:3.75rem;height:3.75rem;object-fit: cover;'></img>`;
|
||||
},
|
||||
(error: any) => {
|
||||
console.log("error", error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const sendMessage = (message: any) => {
|
||||
new Promise((res) => {
|
||||
insertMessage(message);
|
||||
res(message);
|
||||
}).then((_message: any) => {
|
||||
sendMsg(_message).then(() => {
|
||||
const data = dataList.value.filter((item: any) => item.id === _message.id);
|
||||
data[0].isLoading = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
let htmlContent = document.getElementById("post-textarea")!.innerHTML;
|
||||
|
||||
if (htmlContent === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const imgReg = /<img.*?(?:>|\/>)/gi;
|
||||
const srcReg = /text=[\'\"]?([^\'\"]*)[\'\"]?/i;
|
||||
// let params = new FormData();
|
||||
// //注意此处对文件数组进行了参数循环添加
|
||||
const _contentImg = htmlContent.match(imgReg);
|
||||
|
||||
const replaceContent = htmlContent.replaceAll(imgReg, "#").replace(/<[^>]*>[^<]*(<[^>]*>)?/gi, "");
|
||||
// 内容分割
|
||||
const _splitContent = replaceContent.split("#");
|
||||
|
||||
_splitContent.forEach((item: string) => {
|
||||
if (item === null || item === "") {
|
||||
return;
|
||||
}
|
||||
//发送文字消息
|
||||
const message = {} as any;
|
||||
message.id = getRandomString(12);
|
||||
message.sendUid = currentUser.value.id;
|
||||
message.acceptUid = acceptUser.value.id;
|
||||
message.content = item;
|
||||
message.msgType = 1;
|
||||
message.chatType = 0;
|
||||
message.isLoading = true;
|
||||
sendMessage(message);
|
||||
});
|
||||
|
||||
// 图片分割
|
||||
_contentImg?.forEach((item: any) => {
|
||||
const src = item.match(srcReg);
|
||||
const message = {} as any;
|
||||
message.id = getRandomString(12);
|
||||
message.sendUid = currentUser.value.id;
|
||||
message.acceptUid = acceptUser.value.id;
|
||||
message.content = src[1];
|
||||
message.msgType = 2;
|
||||
message.chatType = 0;
|
||||
message.isLoading = true;
|
||||
sendMessage(message);
|
||||
});
|
||||
// const content = htmlContent.replace(/<[^>]*>[^<]*(<[^>]*>)?/gi, "");
|
||||
document.getElementById("post-textarea")!.innerHTML = "";
|
||||
};
|
||||
|
||||
const showScroll = () => {
|
||||
const topval = ChatRef.value.scrollTop;
|
||||
if (topval === 0) {
|
||||
loadMoreData();
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
currentPage.value++;
|
||||
getAllChatRecordMethod();
|
||||
};
|
||||
|
||||
const getAllChatRecordMethod = () => {
|
||||
getAllChatRecord(currentPage.value, pageSize, props.acceptUid).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
messageTotal.value = total;
|
||||
records.forEach((item: any) => {
|
||||
dataList.value?.splice(0, 0, item);
|
||||
});
|
||||
if (dataList.value.length >= total) {
|
||||
ChatRef.value.scrollTop = 0;
|
||||
} else {
|
||||
ChatRef.value.scrollTop += ChatRef.value.clientHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
currentUser.value = userStore.getUserInfo();
|
||||
getUserById(props.acceptUid).then((res) => {
|
||||
acceptUser.value = res.data;
|
||||
});
|
||||
dataList.value = [];
|
||||
getAllChatRecord(currentPage.value, pageSize, props.acceptUid).then(async (res) => {
|
||||
const { records, total } = res.data;
|
||||
messageTotal.value = total;
|
||||
records.forEach((item: any) => {
|
||||
dataList.value.splice(0, 0, item);
|
||||
});
|
||||
await nextTick();
|
||||
// 滚动到最底部
|
||||
ChatRef.value.lastElementChild.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.icon-item {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
margin-right: 0.3125rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
left: 45%;
|
||||
top: 50%;
|
||||
width: 90vw;
|
||||
height: 85vh;
|
||||
z-index: 20;
|
||||
overflow: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 10px; /* 可选:为容器添加圆角 */
|
||||
|
||||
.chat-container {
|
||||
width: 65%;
|
||||
margin: 0 auto;
|
||||
height: 90%;
|
||||
min-width: 50rem;
|
||||
transition:
|
||||
transform 0.4s ease 0s,
|
||||
width 0.4s ease 0s;
|
||||
transform: translate(6.5rem, 2rem) scale(1);
|
||||
overflow: visible;
|
||||
box-shadow:
|
||||
0 0.5rem 4rem 0 rgba(0, 0, 0, 0.04),
|
||||
0 0.0625rem 0.25rem 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 1.25rem;
|
||||
background-color: #fff;
|
||||
transform-origin: left top;
|
||||
z-index: 100;
|
||||
|
||||
.chat-header {
|
||||
height: 3.75rem;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-user {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
height: 100%;
|
||||
|
||||
.message-img {
|
||||
width: 15rem;
|
||||
height: 18.75rem;
|
||||
object-fit: cover;
|
||||
margin: 0 0.3125rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-record {
|
||||
height: 60%;
|
||||
padding: 0 1.25rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin: 1.25rem 0;
|
||||
|
||||
.message-conent {
|
||||
margin-left: 0.3125rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border: 0.0625rem solid #f4f4f4;
|
||||
background-color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.message-my-item {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
margin: 1.25rem 0;
|
||||
|
||||
.message-my-conent {
|
||||
margin-right: 0.3125rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
color: #fff;
|
||||
background-color: rgb(0, 170, 255);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 25%;
|
||||
|
||||
.input-tool {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 1.25rem;
|
||||
padding: 0 0.3125rem;
|
||||
|
||||
.tool-left {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
}
|
||||
}
|
||||
|
||||
.input-content {
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
resize: none;
|
||||
border: 0rem;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.input-btn {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #ccc;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
cursor: text;
|
||||
width: 100%;
|
||||
min-height: 60%;
|
||||
margin-bottom: 1.25rem;
|
||||
background: #fff;
|
||||
padding: 0rem 0.75rem 1.375rem;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-cricle {
|
||||
left: 18vw;
|
||||
top: 1.3vw;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
.close-mask-white {
|
||||
box-shadow:
|
||||
0 0.125rem 0.5rem 0 rgba(0, 0, 0, 0.04),
|
||||
0 0.0625rem 0.125rem 0 rgba(0, 0, 0, 0.02);
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 2.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background-color: #fff;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.2); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
411
src/components/Comment.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="comments-container">
|
||||
<div class="total">共{{ computedTotal }}条评论</div>
|
||||
<div class="list-container">
|
||||
<div class="parent-comment" v-for="(oneComment, oneIndex) in dataList" :key="oneIndex">
|
||||
<div class="comment-item">
|
||||
<div class="comment-inner-container">
|
||||
<div class="avatar">
|
||||
<img class="avatar-item" :src="oneComment.avatar" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="author-wrapper">
|
||||
<div class="author">
|
||||
<a class="name">{{ oneComment.username }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">{{ oneComment.content }}</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="date">
|
||||
<span>{{ oneComment.time }}</span>
|
||||
</div>
|
||||
<div class="interactions">
|
||||
<div class="like">
|
||||
<span
|
||||
class="like-wrapper"
|
||||
v-if="oneComment.isLike"
|
||||
@click="likeComment(oneComment, -1, oneIndex, -1)"
|
||||
>
|
||||
<i class="iconfont icon-follow-fill" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ oneComment.likeCount }}</span>
|
||||
</span>
|
||||
<span class="like-wrapper" v-else @click="likeComment(oneComment, 1, oneIndex, -1)">
|
||||
<i class="iconfont icon-follow" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ oneComment.likeCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="reply" @click="saveComment(oneComment, oneIndex, 0)">
|
||||
<span class="like-wrapper">
|
||||
<ChatRound style="width: 1.2em; height: 1.2em" />
|
||||
<span class="count">{{ oneComment.twoCommentCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reply-container">
|
||||
<div class="list-container">
|
||||
<div class="comment-item" v-for="(twoComment, twoIndex) in oneComment.children" :key="twoIndex">
|
||||
<div class="comment-inner-container">
|
||||
<div class="avatar">
|
||||
<img class="avatar-item" :src="twoComment.avatar" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="author-wrapper">
|
||||
<div class="author">
|
||||
<a class="name">{{ twoComment.username }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
回复<span style="color: rgba(61, 61, 61, 0.8)">{{ twoComment.replyUsername }}: </span
|
||||
>{{ twoComment.content }}
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="date">
|
||||
<span>{{ twoComment.time }}</span>
|
||||
</div>
|
||||
<div class="interactions">
|
||||
<div class="like">
|
||||
<span
|
||||
class="like-wrapper"
|
||||
v-if="twoComment.isLike"
|
||||
@click="likeComment(twoComment, -1, oneIndex, twoIndex)"
|
||||
>
|
||||
<i class="iconfont icon-follow-fill" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ twoComment.likeCount }}</span>
|
||||
</span>
|
||||
<span class="like-wrapper" @click="likeComment(twoComment, 1, oneIndex, twoIndex)" v-else>
|
||||
<i class="iconfont icon-follow" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ twoComment.likeCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="reply" @click="saveComment(twoComment, oneIndex, twoIndex)">
|
||||
<span class="like-wrapper">
|
||||
<ChatRound style="width: 1.2em; height: 1.2em" />
|
||||
<span class="count">回复</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="show-more"
|
||||
v-if="
|
||||
oneComment.twoCommentCount >= commentTotalMap.get(oneComment.id) &&
|
||||
oneComment.twoCommentCount > showTwoCommentCount
|
||||
"
|
||||
@click="loadTwoMore(oneComment.id, oneIndex)"
|
||||
>
|
||||
展开更多的回复
|
||||
</div>
|
||||
<div
|
||||
class="show-more"
|
||||
v-if="
|
||||
oneComment.twoCommentCount < commentTotalMap.get(oneComment.id) &&
|
||||
oneComment.twoCommentCount > showTwoCommentCount
|
||||
"
|
||||
@click="reback(oneComment.id, oneIndex)"
|
||||
>
|
||||
收起所有回复
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-bottom: 100px"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ChatRound } from "@element-plus/icons-vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { getCommentWithCommentByNoteId, getTwoCommentPageByOneCommentId } from "@/api/comment";
|
||||
import { likeOrCollectionByDTO } from "@/api/likeOrCollection";
|
||||
import type { LikeOrCollectionDTO } from "@/type/likeOrCollection";
|
||||
import { formateTime } from "@/utils/util";
|
||||
const props = defineProps({
|
||||
nid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
replyComment: {
|
||||
type: Object,
|
||||
// eslint-disable-next-line vue/require-valid-default-prop
|
||||
default: {},
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
seed: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["clickComment"]);
|
||||
|
||||
// const currentPage = ref(1);
|
||||
|
||||
const dataList = ref<Array<any>>([]);
|
||||
const commentTotal = ref(0);
|
||||
const computedTotal = ref(0);
|
||||
const oneIndex = ref(-1);
|
||||
const twoIndex = ref(-1);
|
||||
|
||||
const pageSize = 7;
|
||||
const twoPageSize = 10;
|
||||
const showTwoCommentCount = 3;
|
||||
const commentMap = new Map();
|
||||
const commentTotalMap = new Map();
|
||||
|
||||
const likeComment = (comment: any, status: number, one: number, two: number) => {
|
||||
const data = {} as LikeOrCollectionDTO;
|
||||
data.likeOrCollectionId = comment.id;
|
||||
data.publishUid = comment.uid;
|
||||
data.type = 2;
|
||||
likeOrCollectionByDTO(data).then(() => {
|
||||
if (two === -1) {
|
||||
dataList.value[one].isLike = status == 1;
|
||||
dataList.value[one].likeCount += status;
|
||||
} else {
|
||||
dataList.value[one].children[two].isLike = status == 1;
|
||||
dataList.value[one].children[two].likeCount += status;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveComment = (comment: any, one: number, two: number) => {
|
||||
oneIndex.value = one;
|
||||
twoIndex.value = two;
|
||||
emit("clickComment", comment);
|
||||
};
|
||||
|
||||
const addComment = () => {
|
||||
// if (props.replyComment.pid === undefined) return;
|
||||
|
||||
let comment = props.replyComment;
|
||||
console.log("comment", comment);
|
||||
comment.likeCount = 0;
|
||||
comment.twoCommentCount = 0;
|
||||
comment.time = formateTime(new Date().getTime());
|
||||
if (comment.pid === "0") {
|
||||
dataList.value.splice(0, 0, comment);
|
||||
} else {
|
||||
if (dataList.value[oneIndex.value].children == null) {
|
||||
dataList.value[oneIndex.value].children = [];
|
||||
}
|
||||
dataList.value[oneIndex.value].children.splice(twoIndex.value + 1, 0, comment);
|
||||
}
|
||||
computedTotal.value += 1;
|
||||
};
|
||||
|
||||
const loadTwoMore = (oneCommentId: string, index: number) => {
|
||||
let page = commentMap.get(oneCommentId);
|
||||
page += 1;
|
||||
getTwoCommentPageByOneCommentId(page, twoPageSize, oneCommentId).then((res) => {
|
||||
const { records } = res.data;
|
||||
if (page === 1) {
|
||||
const spliceData = records.splice(showTwoCommentCount, records.length);
|
||||
spliceData.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
dataList.value[index].children.push(item);
|
||||
});
|
||||
} else {
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
dataList.value[index].children.push(item);
|
||||
});
|
||||
}
|
||||
commentTotalMap.set(oneCommentId, commentTotalMap.get(oneCommentId) + twoPageSize);
|
||||
commentMap.set(oneCommentId, page);
|
||||
});
|
||||
};
|
||||
|
||||
const reback = (oneCommentId: string, index: number) => {
|
||||
commentTotalMap.set(oneCommentId, 0);
|
||||
commentMap.set(oneCommentId, 0);
|
||||
const twoSpliceComment = dataList.value[index].children.splice(0, showTwoCommentCount);
|
||||
dataList.value[index].children = twoSpliceComment;
|
||||
};
|
||||
|
||||
const getCommentData = () => {
|
||||
computedTotal.value = 0;
|
||||
getCommentWithCommentByNoteId(props.currentPage, pageSize, props.nid).then((res: any) => {
|
||||
const { records, total } = res.data;
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
const twoComments = item.children;
|
||||
// 设置每一个一级评论的集合
|
||||
commentMap.set(item.id, 0);
|
||||
commentTotalMap.set(item.id, 0);
|
||||
if (twoComments != null) {
|
||||
const twoData = [] as Array<any>;
|
||||
twoComments.forEach((element: any) => {
|
||||
element.time = formateTime(element.time);
|
||||
twoData.push(element);
|
||||
});
|
||||
|
||||
item.children = twoData;
|
||||
}
|
||||
computedTotal.value += item.twoCommentCount + 1;
|
||||
dataList.value.push(item);
|
||||
});
|
||||
console.log("---所有评论", dataList.value);
|
||||
commentTotal.value = total;
|
||||
if (pageSize * props.currentPage >= commentTotal.value) return;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.nid, props.seed, props.currentPage],
|
||||
([newNid, newSeed], [oldNid, oldSeed]) => {
|
||||
console.log("评论功能", newNid, oldNid, props.currentPage);
|
||||
if (newNid !== oldNid) {
|
||||
dataList.value = [];
|
||||
getCommentData();
|
||||
}
|
||||
if (newSeed !== oldSeed) {
|
||||
addComment();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.comments-container {
|
||||
padding: 16px;
|
||||
|
||||
.total {
|
||||
font-size: 14px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-left: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
position: relative;
|
||||
|
||||
.parent-comment {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.comment-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
|
||||
.comment-inner-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.avatar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
|
||||
.author-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 4px;
|
||||
line-height: 140%;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
|
||||
.date {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.interactions {
|
||||
display: flex;
|
||||
margin-left: -2px;
|
||||
|
||||
.like-wrapper {
|
||||
padding: 0 4px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-weight: 500;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.like-lottie {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-left: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-container {
|
||||
margin-left: 52px;
|
||||
|
||||
.show-more {
|
||||
margin-left: 44px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
color: #13386c;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
64
src/components/FloatingBtn.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="floating-btn-sets">
|
||||
<el-backtop :bottom="80" :right="24">
|
||||
<div class="back-top">
|
||||
<Top style="width: 1.2em; height: 1.2em" color="rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</el-backtop>
|
||||
|
||||
<div class="reload" @click="refresh">
|
||||
<Refresh style="width: 1.2em; height: 1.2em" color="rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Refresh, Top } from "@element-plus/icons-vue";
|
||||
|
||||
const emit = defineEmits(["clickRefresh"]);
|
||||
const refresh = () => {
|
||||
emit("clickRefresh", true);
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.floating-btn-sets {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
grid-gap: 8px;
|
||||
gap: 8px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
|
||||
.back-top {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 100px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reload {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 2px 8px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 100px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
265
src/components/Loading.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div class="com__box">
|
||||
<!-- loading -->
|
||||
<div class="loading">
|
||||
<div class="shape shape-4">
|
||||
<div class="shape-4-top"></div>
|
||||
<div class="shape-4-bottom"></div>
|
||||
<div class="shape-4-eye"></div>
|
||||
</div>
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- 说明:组件名 -->
|
||||
<!-- <h4 class="explain">L75.vue</h4> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.com__box {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shape {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
background-color: #1875e5;
|
||||
left: -5px;
|
||||
animation: animationShape1 7s linear infinite;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
background-color: #c5523f;
|
||||
left: 15px;
|
||||
animation: animationShape2 7s linear infinite;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
background-color: #499255;
|
||||
left: 35px;
|
||||
animation: animationShape3 7s linear infinite;
|
||||
}
|
||||
|
||||
.shape-4 {
|
||||
background-color: #f2b736;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: -40px;
|
||||
background-color: transparent !important;
|
||||
z-index: 2;
|
||||
animation: animationShape4 7s linear infinite;
|
||||
}
|
||||
|
||||
.shape-4 > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.shape-4 .shape-4-top {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fbbc05;
|
||||
clip: rect(0 30px 15px 0);
|
||||
transform: rotate(-30deg);
|
||||
animation: animationShape4Top 0.4s ease infinite alternate;
|
||||
}
|
||||
|
||||
.shape-4 .shape-4-bottom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fbbc05;
|
||||
clip: rect(15px 30px 30px 0);
|
||||
transform: rotate(45deg);
|
||||
animation: animationShape4Bottom 0.4s ease infinite alternate;
|
||||
}
|
||||
|
||||
.shape-4 .shape-4-eye {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
@keyframes animationShape4Top {
|
||||
0% {
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animationShape4Bottom {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animationShape1 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
17% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
19% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
85% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
95% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animationShape2 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
22% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
35% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
45% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animationShape3 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
27% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
29% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
64% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
65% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animationShape4 {
|
||||
0% {
|
||||
left: -40px;
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
45% {
|
||||
left: 50px;
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
left: 50px;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
95% {
|
||||
left: -40px;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
left: -40px;
|
||||
transform: rotateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
298
src/components/Note.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="feeds-container" v-infinite-scroll="loadMoreData" :infinite-scroll-distance="50">
|
||||
<Waterfall
|
||||
:list="noteList"
|
||||
:width="options.width"
|
||||
:gutter="options.gutter"
|
||||
:hasAroundGutter="options.hasAroundGutter"
|
||||
:animation-effect="options.animationEffect"
|
||||
:animation-duration="options.animationDuration"
|
||||
:animation-delay="options.animationDelay"
|
||||
:breakpoints="options.breakpoints"
|
||||
style="min-width: 740px"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<el-skeleton style="width: 240px" :loading="!item.isLoading" animated>
|
||||
<template #template>
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '240px',
|
||||
maxHeight: '300px',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
@load="handleLoad(item)"
|
||||
>
|
||||
</el-image>
|
||||
|
||||
<div style="padding: 14px">
|
||||
<el-skeleton-item variant="h3" style="width: 100%" />
|
||||
<div style="display: flex; align-items: center; margin-top: 2px; height: 16px">
|
||||
<el-skeleton style="--el-skeleton-circle-size: 20px">
|
||||
<template #template>
|
||||
<el-skeleton-item variant="circle" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-skeleton-item variant="text" style="margin-left: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="card" style="max-width: 240px">
|
||||
<div class="image-container">
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '240px',
|
||||
maxHeight: '300px',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
fit="cover"
|
||||
@click="toMain(item.id)"
|
||||
>
|
||||
</el-image>
|
||||
<div v-if="item.auditStatus === '0'" class="overlay">审核中</div>
|
||||
<div v-if="item.auditStatus === '2'" class="overlay not-passed">未通过⚠️</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a class="title">
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
<div class="author-wrapper">
|
||||
<a class="author">
|
||||
<img class="author-avatar" :src="item.avatar" />
|
||||
<span class="name">{{ item.username }}</span>
|
||||
</a>
|
||||
<span class="like-wrapper like-active">
|
||||
<i class="iconfont icon-follow" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ item.likeCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-tag-area" v-show="type === 1 && item.pinned === '1'">
|
||||
<div class="top-wrapper">置顶</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
</Waterfall>
|
||||
</div>
|
||||
|
||||
<Main
|
||||
v-show="mainShow"
|
||||
:nid="nid"
|
||||
:nowTime="new Date()"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-main="close"
|
||||
></Main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Waterfall } from "vue-waterfall-plugin-next";
|
||||
import "vue-waterfall-plugin-next/dist/style.css";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { getTrendByUser } from "@/api/user";
|
||||
import Main from "@/pages/main/main.vue";
|
||||
import { options } from "@/constant/constant";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.type],
|
||||
([newType]) => {
|
||||
currentPage.value = 1;
|
||||
noteList.value = [] as Array<any>;
|
||||
getNoteList(newType);
|
||||
}
|
||||
);
|
||||
|
||||
const noteList = ref<Array<any>>([]);
|
||||
const noteTotal = ref(0);
|
||||
const uid = route.query.uid as string;
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 10;
|
||||
const nid = ref("");
|
||||
const mainShow = ref(false);
|
||||
const isCurrentUser = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const currentUid = userStore.getUserInfo().id;
|
||||
|
||||
const handleLoad = (item: any) => {
|
||||
item.isLoading = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
mainShow.value = false;
|
||||
};
|
||||
|
||||
const toMain = (noteId: string) => {
|
||||
// router.push({ name: "main", state: { nid: nid } });
|
||||
nid.value = noteId;
|
||||
mainShow.value = true;
|
||||
};
|
||||
|
||||
const setData = (res: any) => {
|
||||
const { records, total } = res.data;
|
||||
noteTotal.value = total;
|
||||
// 过滤掉不是当前用户且状态“审核中”或“未通过”的记录
|
||||
const filteredRecords = records.filter((item: any) => {
|
||||
return item.uid === currentUid || (item.auditStatus !== "0" && item.auditStatus !== "2");
|
||||
});
|
||||
noteList.value.push(...filteredRecords);
|
||||
};
|
||||
|
||||
const getNoteList = (type: number) => {
|
||||
getTrendByUser(currentPage.value, pageSize, uid, type).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
currentPage.value += 1;
|
||||
getNoteList(props.type);
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
getNoteList(1);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5); /* 半透明背景 */
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.overlay.not-passed {
|
||||
color: red; /* 设置未通过状态的字体颜色为红色 */
|
||||
}
|
||||
.feeds-container {
|
||||
position: relative;
|
||||
transition: width 0.5s;
|
||||
margin: 0 auto;
|
||||
|
||||
.noteImg {
|
||||
width: 240px;
|
||||
max-height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
|
||||
.top-tag-area {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
z-index: 4;
|
||||
|
||||
.top-wrapper {
|
||||
background: #ff2442;
|
||||
border-radius: 999px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
line-height: 120%;
|
||||
font-size: 12px;
|
||||
padding: 5px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 140%;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.author-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 20px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-size: 12px;
|
||||
transition: color 1s;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 12px;
|
||||
|
||||
.author-avatar {
|
||||
margin-right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.like-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.count {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
88
src/components/SearchContainer.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="sug-container-wrapper sug-pad">
|
||||
<div class="sug-container">
|
||||
<!---->
|
||||
<div class="sug-box">
|
||||
<!---->
|
||||
<div class="sug-wrapper">
|
||||
<div class="sug-item" v-for="(item, index) in dataList" :key="index" @click="searchPage(item.content)">
|
||||
<!---->
|
||||
<span v-html="item.highlightContent"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { getRandomString } from "@/utils/util";
|
||||
import { useSearchStore } from "@/store/searchStore";
|
||||
import { useRouter } from "vue-router";
|
||||
import { addRecord } from "@/api/search";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const searchStore = useSearchStore();
|
||||
const props = defineProps({
|
||||
recordList: {
|
||||
type: Array<any>,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
|
||||
const dataList = ref<Array<any>>([]);
|
||||
|
||||
const searchPage = (keyword: string) => {
|
||||
addRecord(keyword);
|
||||
searchStore.setKeyword(keyword);
|
||||
searchStore.pushRecord(keyword);
|
||||
const seed = getRandomString(12);
|
||||
searchStore.setSeed(seed);
|
||||
router.push({ name: "search", query: { keyword: keyword } });
|
||||
};
|
||||
watchEffect(() => {
|
||||
dataList.value = [];
|
||||
if (props.recordList.length > 0) {
|
||||
dataList.value = props.recordList;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.sug-container-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sug-container-wrapper {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow:
|
||||
0 0.25rem 2rem 0 rgba(0, 0, 0, 0.08),
|
||||
0 0.0625rem 0.25rem 0 rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0.75rem;
|
||||
overflow: scroll;
|
||||
z-index: 9999;
|
||||
|
||||
.sug-container {
|
||||
padding-top: 0.25rem;
|
||||
|
||||
.sug-item {
|
||||
width: 100%;
|
||||
padding: 0 0.75rem;
|
||||
font-size: 1rem;
|
||||
height: 2.5rem;
|
||||
line-height: 120%;
|
||||
font-weight: 400;
|
||||
border-radius: 0.5rem;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sug-item:hover {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
385
src/components/SujContainer.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div class="sug-container-wrapper query-trending sug-pad">
|
||||
<div class="sug-container query-trending">
|
||||
<div class="history" v-show="historyRecordList != null && historyRecordList.length > 0">
|
||||
<div class="header">
|
||||
<span> 历史记录 </span>
|
||||
<div class="icon-group">
|
||||
<div class="icon-box" @click="showDeleteTag">
|
||||
<Delete style="width: 1.2em; height: 1.2em"></Delete>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-list">
|
||||
<div v-for="(item, index) in historyRecordList" :key="index">
|
||||
<div class="history-item">
|
||||
<span @click="searchPage(item)">{{ item }}</span>
|
||||
<!---->
|
||||
<span class="close-tag" v-show="showTagState" @click="deleteRecord(index)">X</span>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
<div class="sug-box">
|
||||
<div class="header">猜你想搜</div>
|
||||
<div class="sug-wrapper">
|
||||
<div
|
||||
class="sug-item query-trending query-trending hotspot"
|
||||
v-for="(item, index) in recommendRecords"
|
||||
:key="index"
|
||||
>
|
||||
<div class="sug-text" @click="searchPage(item)">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="hotspots">
|
||||
<div class="header">
|
||||
<span style="color: #888888">热点</span>
|
||||
<img src="@/assets/images/3.png" class="redian" crossorigin="anonymous" />
|
||||
</div>
|
||||
<div class="hotspot-list">
|
||||
<div class="hotspot-item" v-for="(item, index) in hotList" :key="index">
|
||||
<p class="hotspot-index">{{ index + 1 }}</p>
|
||||
<div class="hotspot-title" @click="searchPage(item.content)">
|
||||
<span class="text">{{ item.content }}</span>
|
||||
<img src="@/assets/images/1.png" crossorigin="anonymous" />
|
||||
<img v-show="item.searchCount > 10" src="@/assets/images/2.png" crossorigin="anonymous" />
|
||||
</div>
|
||||
<span class="hotspot-score">{{ item.searchCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Delete } from "@element-plus/icons-vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useSearchStore } from "@/store/searchStore";
|
||||
import { getHotRecord, addRecord } from "@/api/search";
|
||||
import { getRandomString } from "@/utils/util";
|
||||
import { storage } from "@/utils/storage";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
const historyRecordList = ref<Array<string>>([]);
|
||||
const hotList = ref<Array<any>>([]);
|
||||
const showTagState = ref(false);
|
||||
const recommendRecords = ["壁纸", "风景", "情侣", "头像", "动漫", "动物"];
|
||||
|
||||
const props = defineProps({
|
||||
closeHistoryRecord: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const showDeleteTag = () => {
|
||||
showTagState.value = !showTagState.value;
|
||||
};
|
||||
|
||||
const deleteRecord = (index: number) => {
|
||||
historyRecordList.value.splice(index, 1);
|
||||
// 使用store失效
|
||||
storage.set("historyRecords", historyRecordList.value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [searchStore.seed, props.closeHistoryRecord],
|
||||
() => {
|
||||
showTagState.value = false;
|
||||
historyRecordList.value = searchStore.getRecords();
|
||||
getHotRecord().then((res) => {
|
||||
hotList.value = res.data;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const searchPage = (keyword: string) => {
|
||||
addRecord(keyword);
|
||||
searchStore.setKeyword(keyword);
|
||||
searchStore.pushRecord(keyword);
|
||||
const seed = getRandomString(12);
|
||||
searchStore.setSeed(seed);
|
||||
router.push({ name: "search", query: { keyword: keyword } });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
historyRecordList.value = searchStore.getRecords();
|
||||
getHotRecord().then((res) => {
|
||||
hotList.value = res.data;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.redian {
|
||||
width: 90px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
//隐藏滚动条
|
||||
.sug-container-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sug-container-wrapper.query-trending {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.sug-container-wrapper {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow:
|
||||
0 0.25rem 2rem 0 rgba(0, 0, 0, 0.08),
|
||||
0 0.0625rem 0.25rem 0 rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0.75rem;
|
||||
overflow: scroll;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.sug-container.query-trending {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
.history {
|
||||
padding: 0.25rem;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0 0.25rem 0 0.75rem;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 120%;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
|
||||
.icon-group {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
grid-gap: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
|
||||
.icon-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
grid-gap: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
grid-gap: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.close-tag {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
text-align: center;
|
||||
line-height: 1rem;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
color: #888888;
|
||||
border: 0.0625rem solid #f4f4f4;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 120%;
|
||||
padding: 0 0.75rem;
|
||||
white-space: nowrap;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 62.4375rem;
|
||||
border: 0.0625rem solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.1); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sug-box {
|
||||
padding: 0.25rem;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0.6563rem 0.75rem;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 120%;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
|
||||
.sug-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.query-trending.hotspot:nth-child(odd) {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.query-trending.hotspot {
|
||||
width: calc(50% - 0.125rem);
|
||||
}
|
||||
|
||||
.query-trending {
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sug-item {
|
||||
width: 100%;
|
||||
padding: 0 0.75rem;
|
||||
font-size: 1rem;
|
||||
height: 2.5rem;
|
||||
line-height: 120%;
|
||||
font-weight: 400;
|
||||
border-radius: 0.5rem;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sug-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.1); /* 鼠标移入时按钮稍微放大 */
|
||||
transform-origin: left center; /* 调整缩放基点为左边中心 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hotspots {
|
||||
padding: 0.25rem;
|
||||
|
||||
.header {
|
||||
padding: 0.5rem 0.75rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.hotspot-item:first-child {
|
||||
color: #ff2442;
|
||||
}
|
||||
|
||||
.hotspot-item:nth-child(2) {
|
||||
color: rgb(128, 0, 94);
|
||||
}
|
||||
|
||||
.hotspot-item:nth-child(3) {
|
||||
color: #ff24a4;
|
||||
}
|
||||
|
||||
.hotspot-item {
|
||||
padding: 0 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5rem;
|
||||
cursor: pointer;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
|
||||
.hotspot-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 120%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.hotspot-title {
|
||||
margin: 0 0.375rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 120%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
margin-left: 0.375rem;
|
||||
height: 0.975rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.1); /* 鼠标移入时按钮稍微放大 */
|
||||
transform-origin: left center; /* 调整缩放基点为左边中心 */
|
||||
}
|
||||
|
||||
.hotspot-score {
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
margin-left: auto;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 120%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sug-container.query-trending {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
57
src/constant/constant.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import loading from "@/assets/loading.png";
|
||||
import error from "@/assets/error.png";
|
||||
import { reactive } from "vue";
|
||||
// websocket地址
|
||||
// export const wsKey = "ws://www.ccimgvideo.top/ws/";
|
||||
// // 项目url地址
|
||||
// export const baseURL = "http://www.ccimgvideo.top/api";
|
||||
|
||||
// 本地项目websocket地址
|
||||
export const wsKey = "ws://localhost:8080/web/ws/";
|
||||
// 本地项目url地址
|
||||
export const baseURL = "http://localhost:8080/";
|
||||
|
||||
// 图片必须保持在336和186之间
|
||||
|
||||
export const options = reactive({
|
||||
// 唯一key值
|
||||
rowKey: "id",
|
||||
// 卡片之间的间隙
|
||||
gutter: 10,
|
||||
// 是否有周围的gutter
|
||||
hasAroundGutter: false,
|
||||
// 卡片在PC上的宽度
|
||||
width: 240,
|
||||
// 自定义行显示个数,主要用于对移动端的适配
|
||||
breakpoints: {
|
||||
2000: {
|
||||
// 当屏幕宽度小于等于1200
|
||||
rowPerView: 5,
|
||||
},
|
||||
1200: {
|
||||
// 当屏幕宽度小于等于1200
|
||||
rowPerView: 4,
|
||||
},
|
||||
1000: {
|
||||
// 当屏幕宽度小于等于800
|
||||
rowPerView: 3,
|
||||
},
|
||||
},
|
||||
// 动画效果
|
||||
animationEffect: "animate__zoomIn",
|
||||
// 动画时间
|
||||
animationDuration: 2000,
|
||||
// 动画延迟
|
||||
animationDelay: 300,
|
||||
// 背景色
|
||||
backgroundColor: "#2C2E3A",
|
||||
// imgSelector
|
||||
imgSelector: "src.original",
|
||||
// 加载配置
|
||||
loadProps: {
|
||||
loading,
|
||||
error,
|
||||
},
|
||||
// 是否懒加载
|
||||
lazyload: true,
|
||||
});
|
19
src/main.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
import router from "./router/index";
|
||||
|
||||
import ElementPlus from "element-plus";
|
||||
import "element-plus/dist/index.css";
|
||||
|
||||
// 引入动画
|
||||
import 'animate.css';
|
||||
import "@/assets/font_4394635_lwuldvb474/iconfont.css";
|
||||
|
||||
app.use(router);
|
||||
app.use(ElementPlus);
|
||||
app.mount("#app");
|
||||
|
456
src/pages/dashboard/dashboard.vue
Normal file
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="feeds-page">
|
||||
<div class="channel-container">
|
||||
<div class="scroll-container channel-scroll-container">
|
||||
<div class="content-container">
|
||||
<div :class="categoryClass == '0' ? 'channel active' : 'channel'" @click="getNoteList">推荐</div>
|
||||
<div
|
||||
:class="categoryClass == item.id ? 'channel active' : 'channel'"
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
@click="getNoteListByCategory(item.id)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-container"></div>
|
||||
<div class="feeds-container" v-infinite-scroll="loadMoreData" :infinite-scroll-distance="50">
|
||||
<div class="feeds-loading-top animate__animated animate__zoomIn animate__delay-0.5s" v-show="topLoading">
|
||||
<Loading style="width: 1.2em; height: 1.2em"></Loading>
|
||||
</div>
|
||||
<Waterfall
|
||||
:list="noteList"
|
||||
:width="options.width"
|
||||
:gutter="options.gutter"
|
||||
:hasAroundGutter="options.hasAroundGutter"
|
||||
:animation-effect="options.animationEffect"
|
||||
:animation-duration="options.animationDuration"
|
||||
:animation-delay="options.animationDelay"
|
||||
:breakpoints="options.breakpoints"
|
||||
style="min-width: 740px"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<el-skeleton style="width: 240px" :loading="!item.isLoading" animated>
|
||||
<template #template>
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '240px',
|
||||
maxHeight: '300px',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
@load="handleLoad(item)"
|
||||
></el-image>
|
||||
<div style="padding: 14px">
|
||||
<el-skeleton-item variant="h3" style="width: 100%" />
|
||||
<div style="display: flex; align-items: center; margin-top: 2px; height: 16px">
|
||||
<el-skeleton style="--el-skeleton-circle-size: 20px">
|
||||
<template #template>
|
||||
<el-skeleton-item variant="circle" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-skeleton-item variant="text" style="margin-left: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="card" style="max-width: 240px">
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '240px',
|
||||
maxHeight: '300px',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
fit="cover"
|
||||
@click="toMain(item.id)"
|
||||
></el-image>
|
||||
<div class="footer">
|
||||
<a class="title">
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
<div class="author-wrapper">
|
||||
<a class="author">
|
||||
<img class="author-avatar" :src="item.avatar" />
|
||||
<span class="name">{{ item.username }}</span>
|
||||
</a>
|
||||
<span class="like-wrapper like-active">
|
||||
<i class="iconfont icon-follow" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ item.likeCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
</Waterfall>
|
||||
|
||||
<div class="feeds-loading" v-show="!isEnd">
|
||||
<Loading style="width: 1.2em; height: 1.2em"></Loading>
|
||||
</div>
|
||||
<div class="feeds-end" v-show="isEnd">······ 已经到底了 ·····</div>
|
||||
</div>
|
||||
<FloatingBtn @click-refresh="refresh"></FloatingBtn>
|
||||
<Main
|
||||
v-show="mainShow"
|
||||
:nid="nid"
|
||||
:nowTime="new Date()"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-main="close"
|
||||
></Main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Waterfall } from "vue-waterfall-plugin-next";
|
||||
import "vue-waterfall-plugin-next/dist/style.css";
|
||||
import { ref, watch } from "vue";
|
||||
import { getRecommendNote, getNoteByDTO, addRecord } from "@/api/search";
|
||||
import { getCategoryTreeData } from "@/api/category";
|
||||
import type { NoteDTO, NoteSearch } from "@/type/note";
|
||||
import type { Category } from "@/type/category";
|
||||
import Main from "@/pages/main/main.vue";
|
||||
import FloatingBtn from "@/components/FloatingBtn.vue";
|
||||
import { options } from "@/constant/constant";
|
||||
import { useSearchStore } from "@/store/searchStore";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
import { refreshTab } from "@/utils/util";
|
||||
const searchStore = useSearchStore();
|
||||
const topLoading = ref(false);
|
||||
const noteList = ref<Array<NoteSearch>>([]);
|
||||
const categoryList = ref<Array<Category>>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 20;
|
||||
const noteTotal = ref(0);
|
||||
const categoryClass = ref("0");
|
||||
const mainShow = ref(false);
|
||||
const nid = ref("");
|
||||
const isEnd = ref(false);
|
||||
const noteDTO = ref<NoteDTO>({
|
||||
keyword: "",
|
||||
type: 0,
|
||||
cid: "",
|
||||
cpid: "",
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [searchStore.seed],
|
||||
() => {
|
||||
noteDTO.value.keyword = searchStore.keyWord;
|
||||
noteDTO.value.cpid = "";
|
||||
categoryClass.value = "0";
|
||||
getNoteListByKeyword();
|
||||
addRecord(searchStore.keyWord);
|
||||
}
|
||||
);
|
||||
|
||||
const toMain = (noteId: string) => {
|
||||
// router.push({ name: "main", state: { nid: nid } });
|
||||
nid.value = noteId;
|
||||
mainShow.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
mainShow.value = false;
|
||||
};
|
||||
|
||||
const handleLoad = (item: any) => {
|
||||
item.isLoading = true;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
// 使用回调函数优化代码
|
||||
refreshTab(() => {
|
||||
topLoading.value = true;
|
||||
console.log(111);
|
||||
setTimeout(() => {
|
||||
currentPage.value = 1;
|
||||
noteList.value = [];
|
||||
getNoteList();
|
||||
topLoading.value = false;
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
if (noteList.value.length >= noteTotal.value) {
|
||||
isEnd.value = true;
|
||||
return; // 如果已经加载完所有数据,则不再请求
|
||||
}
|
||||
currentPage.value += 1;
|
||||
if (noteDTO.value.cpid === "" && noteDTO.value.keyword == "") {
|
||||
getRecommendNote(currentPage.value, pageSize).then((res: any) => {
|
||||
setData(res);
|
||||
});
|
||||
} else {
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setData = (res: any) => {
|
||||
const { records, total } = res.data;
|
||||
noteTotal.value = total;
|
||||
if (records.length === 0) {
|
||||
isEnd.value = true;
|
||||
} else {
|
||||
noteList.value.push(...records);
|
||||
}
|
||||
};
|
||||
|
||||
const getNoteList = async () => {
|
||||
categoryClass.value = "0";
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getRecommendNote(currentPage.value, pageSize).then((res: any) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getNoteListByCategory = (id: string) => {
|
||||
categoryClass.value = id;
|
||||
noteDTO.value.cpid = id;
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getNoteListByKeyword = () => {
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getCategoryData = () => {
|
||||
getCategoryTreeData().then((res: any) => {
|
||||
categoryList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
getCategoryData();
|
||||
getNoteList();
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.feeds-page {
|
||||
flex: 1;
|
||||
padding: 0 24px;
|
||||
padding-top: 72px;
|
||||
height: 100vh;
|
||||
|
||||
.channel-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
.channel-scroll-container {
|
||||
backdrop-filter: blur(20px);
|
||||
background-color: transparent;
|
||||
width: calc(100vw - 24px);
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
height: 40px;
|
||||
white-space: nowrap;
|
||||
height: 72px;
|
||||
|
||||
.content-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.active {
|
||||
font-weight: 600;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 999px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.channel {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.2); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feeds-container {
|
||||
position: relative;
|
||||
transition: width 0.5s;
|
||||
margin: 0 auto;
|
||||
|
||||
.feeds-loading {
|
||||
margin: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feeds-loading-top {
|
||||
text-align: center;
|
||||
line-height: 6vh;
|
||||
height: 6vh;
|
||||
}
|
||||
|
||||
.noteImg {
|
||||
width: 240px;
|
||||
max-height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 140%;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.author-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 20px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-size: 12px;
|
||||
transition: color 1s;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 12px;
|
||||
|
||||
.author-avatar {
|
||||
margin-right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.like-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.count {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-btn-sets {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
grid-gap: 8px;
|
||||
gap: 8px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
|
||||
.back-top {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 100px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reload {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 2px 8px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 100px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
//transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.feeds-end {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
314
src/pages/follow-trend/follow-trend.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="container" v-infinite-scroll="loadMoreData" :infinite-scroll-distance="50">
|
||||
<div v-if="isLogin">
|
||||
<ul class="trend-container">
|
||||
<li class="trend-item" v-for="(item, index) in trendData" :key="index">
|
||||
<a class="user-avatar">
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span>{{ item.time }}</span>
|
||||
</div>
|
||||
<div class="interaction-content" @click="toMain(item.nid)">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="interaction-imgs" @click="toMain(item.nid)">
|
||||
<div class="details-box" v-for="(url, index) in item.imgUrls" :key="index">
|
||||
<el-image
|
||||
v-if="!item.isLoading"
|
||||
:src="url"
|
||||
@load="handleLoad(item)"
|
||||
style="height: 230px; width: 100%"
|
||||
>
|
||||
</el-image>
|
||||
<el-image
|
||||
v-else
|
||||
:src="url"
|
||||
class="note-img animate__animated animate__fadeIn animate__delay-0.5s"
|
||||
fit="cover"
|
||||
>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interaction-footer">
|
||||
<div class="icon-item">
|
||||
<i
|
||||
class="iconfont icon-follow-fill"
|
||||
style="width: 1em; height: 1em"
|
||||
@click="like(item.nid, item.uid, index, -1)"
|
||||
v-if="item.isLike"
|
||||
>
|
||||
</i>
|
||||
<i
|
||||
class="iconfont icon-follow"
|
||||
style="width: 1em; height: 1em"
|
||||
@click="like(item.nid, item.uid, index, 1)"
|
||||
v-else
|
||||
></i
|
||||
><span class="count">{{ item.likeCount }}</span>
|
||||
</div>
|
||||
<div class="icon-item">
|
||||
<ChatRound style="width: 0.9em; height: 0.9em" /><span class="count">{{ item.commentCount }}</span>
|
||||
</div>
|
||||
<div class="icon-item">
|
||||
<More style="width: 1em; height: 1em" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<FloatingBtn @click-refresh="refresh"></FloatingBtn>
|
||||
<Main
|
||||
v-show="mainShow"
|
||||
:nid="nid"
|
||||
:nowTime="new Date()"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-main="close"
|
||||
></Main>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="用户未登录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ChatRound, More, Refresh } from "@element-plus/icons-vue";
|
||||
import { ref } from "vue";
|
||||
import { getFollowTrend } from "@/api/follower";
|
||||
import { formateTime, refreshTab } from "@/utils/util";
|
||||
import FloatingBtn from "@/components/FloatingBtn.vue";
|
||||
import Main from "@/pages/main/main.vue";
|
||||
import type { LikeOrCollectionDTO } from "@/type/likeOrCollection";
|
||||
import { likeOrCollectionByDTO } from "@/api/likeOrCollection";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(5);
|
||||
const trendData = ref<Array<any>>([]);
|
||||
const trendTotal = ref(0);
|
||||
const topLoading = ref(false);
|
||||
const mainShow = ref(false);
|
||||
const nid = ref("");
|
||||
const likeOrCollectionDTO = ref<LikeOrCollectionDTO>({
|
||||
likeOrCollectionId: "",
|
||||
publishUid: "",
|
||||
type: 0,
|
||||
});
|
||||
const isLogin = ref(false);
|
||||
|
||||
const handleLoad = (item: any) => {
|
||||
item.isLoading = true;
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
//router.push({ name: "user", state: { uid: uid } });
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const getFollowTrends = () => {
|
||||
getFollowTrend(currentPage.value, pageSize.value).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
console.log(records, total);
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
trendData.value.push(item);
|
||||
});
|
||||
trendTotal.value = total;
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
currentPage.value += 1;
|
||||
getFollowTrends();
|
||||
};
|
||||
|
||||
const toMain = (noteId: string) => {
|
||||
nid.value = noteId;
|
||||
mainShow.value = true;
|
||||
};
|
||||
|
||||
const close = (nid: string, val: any) => {
|
||||
const index = trendData.value.findIndex((item) => item.nid === nid);
|
||||
console.log("---val", val, index);
|
||||
const _data = trendData.value[index];
|
||||
if (_data.isLike != val.isLike) {
|
||||
_data.isLike = val.isLike;
|
||||
_data.likeCount += val.isLike ? 1 : -1;
|
||||
}
|
||||
if (val.isComment) {
|
||||
_data.commentCount += 1;
|
||||
}
|
||||
mainShow.value = false;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refreshTab(() => {
|
||||
topLoading.value = true;
|
||||
setTimeout(() => {
|
||||
currentPage.value = 1;
|
||||
trendData.value = [];
|
||||
getFollowTrends();
|
||||
topLoading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
const like = (nid: string, uid: string, index: number, val: number) => {
|
||||
likeOrCollectionDTO.value.likeOrCollectionId = nid;
|
||||
likeOrCollectionDTO.value.publishUid = uid;
|
||||
likeOrCollectionDTO.value.type = 1;
|
||||
likeOrCollectionByDTO(likeOrCollectionDTO.value).then(() => {
|
||||
if (val < 0 && trendData.value[index].likeCount == 0) {
|
||||
return;
|
||||
}
|
||||
trendData.value[index].isLike = val == 1;
|
||||
trendData.value[index].likeCount += val;
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
isLogin.value = userStore.isLogin();
|
||||
getFollowTrends();
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 0 24px;
|
||||
padding-top: 72px;
|
||||
width: 67%;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
|
||||
.feeds-loading {
|
||||
margin: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trend-container {
|
||||
.trend-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 24px;
|
||||
max-width: 100vw;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 14px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.interaction-imgs {
|
||||
display: flex;
|
||||
|
||||
.details-box {
|
||||
width: 25%;
|
||||
border-radius: 4px;
|
||||
margin: 8px 12px 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
.note-img {
|
||||
width: 100%;
|
||||
height: 230px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-footer {
|
||||
margin: 8px 12px 0 0;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
color: rgba(51, 51, 51, 0.929);
|
||||
|
||||
.count {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.2); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
928
src/pages/index.vue
Normal file
@@ -0,0 +1,928 @@
|
||||
<template>
|
||||
<div class="container" id="container">
|
||||
<div class="top">
|
||||
<header class="mask-paper">
|
||||
<a style="display: flex">
|
||||
<img src="@/assets/logo.png" style="width: 80px" />
|
||||
</a>
|
||||
<div class="tool-box"></div>
|
||||
<div class="input-box" id="sujContainer">
|
||||
<input
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
class="search-input"
|
||||
placeholder="搜索小红书"
|
||||
@input="changeInput"
|
||||
@focus="focusInput"
|
||||
@keyup.enter="searchPage"
|
||||
ref="SearchInput"
|
||||
/>
|
||||
<div class="input-button">
|
||||
<div class="close-icon" v-show="showClose" @click="clearInput">
|
||||
<Close style="width: 1.2em; height: 1.2em; margin-right: 20px; margin-top: 5px" />
|
||||
</div>
|
||||
<div class="search-icon" @click="searchPage">
|
||||
<a href="#">
|
||||
<Search style="width: 1.2em; height: 1.2em; margin-right: 20px; margin-top: 5px" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<SearchContainer v-show="showSearch" :recordList="recordList"></SearchContainer>
|
||||
<SujContainer v-show="showHistory" :closeHistoryRecord="showHistory"></SujContainer>
|
||||
</div>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="side-bar">
|
||||
<ul class="channel-list">
|
||||
<li :class="activeLink == 0 ? 'active-channel' : ''" @click="toLink(0)">
|
||||
<a class="link-wrapper">
|
||||
<House style="width: 1em; height: 1em; margin-right: 8px" /><span class="channel">发现</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="activeLink == 1 ? 'active-channel' : ''" @click="toLink(1)">
|
||||
<Star style="width: 1em; height: 1em; margin-right: 8px" /><span class="channel"> 动态</span>
|
||||
</li>
|
||||
<li :class="activeLink == 2 ? 'active-channel' : ''" @click="toLink(2)">
|
||||
<Bell style="width: 1em; height: 1em; margin-right: 8px" />
|
||||
|
||||
<el-badge is-dot class="item" v-if="messageCount > 0 && userInfo != null">
|
||||
<span class="channel"> 消息</span></el-badge
|
||||
>
|
||||
<span class="channel" v-else>消息</span>
|
||||
</li>
|
||||
<li :class="activeLink == 3 ? 'active-channel' : ''" @click="toLink(3)">
|
||||
<CirclePlus style="width: 1em; height: 1em; margin-right: 8px" /><span class="channel"> 发布</span>
|
||||
</li>
|
||||
<div v-if="userInfo == null">
|
||||
<el-button type="danger" round @click="login" class="custom-button">登录</el-button>
|
||||
</div>
|
||||
<li v-else :class="activeLink == 4 ? 'active-channel' : ''" @click="toLink(4)">
|
||||
<el-avatar :src="userInfo.avatar" :size="22" />
|
||||
<span class="channel">我</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="userInfo == null">
|
||||
<div data-v-6432121e="" data-v-7d49aed8="" class="floating-box visible">
|
||||
<div data-v-6432121e="" class="title">马上登录即可</div>
|
||||
<div data-v-6432121e="" class="line-container">
|
||||
<svg data-v-23d27ada="" data-v-6432121e="" class="reds-icon icon" width="16" height="16">
|
||||
<use data-v-23d27ada="" xlink:href="#thumbUp"></use>
|
||||
</svg>
|
||||
<span data-v-6432121e="" class="desc">刷到更懂你的优质内容</span>
|
||||
</div>
|
||||
<div data-v-6432121e="" class="line-container">
|
||||
<svg data-v-23d27ada="" data-v-6432121e="" class="reds-icon icon" width="16" height="16">
|
||||
<use data-v-23d27ada="" xlink:href="#convention_b"></use>
|
||||
</svg>
|
||||
<span data-v-6432121e="" class="desc">搜索最新种草、拔草信息</span>
|
||||
</div>
|
||||
<div data-v-6432121e="" class="line-container">
|
||||
<svg data-v-23d27ada="" data-v-6432121e="" class="reds-icon icon" width="16" height="16">
|
||||
<use data-v-23d27ada="" xlink:href="#collect"></use>
|
||||
</svg>
|
||||
<span data-v-6432121e="" class="desc">查看收藏、点赞的笔记</span>
|
||||
</div>
|
||||
<div data-v-6432121e="" class="line-container">
|
||||
<svg data-v-23d27ada="" data-v-6432121e="" class="reds-icon icon" width="16" height="16">
|
||||
<use data-v-23d27ada="" xlink:href="#chat"></use>
|
||||
</svg>
|
||||
<span data-v-6432121e="" class="desc">与他人更好地互动、交流</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 嵌入 SVG 图标定义 -->
|
||||
<svg style="display: none">
|
||||
<symbol id="thumbUp" viewBox="0 0 24 24">
|
||||
<!-- SVG 路径数据 -->
|
||||
<path
|
||||
d="M1 21h4V9H1v12zM23 10h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L16.17 3 9.59 9.59C9.21 9.95 9 10.45 9 11v8c0 .55.45 1 1 1h6c.38 0 .72-.21.89-.55l3.66-7.33c.08-.14.13-.3.13-.46V11c0-.55-.45-1-1-1zm-2 7h-4v-6h4v6z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol id="convention_b" viewBox="0 0 24 24">
|
||||
<!-- SVG 路径数据 -->
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol id="collect" viewBox="0 0 24 24">
|
||||
<!-- SVG 路径数据 -->
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol id="chat" viewBox="0 0 24 24">
|
||||
<!-- SVG 路径数据 -->
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v14l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 9H6V9h12v2zm0-3H6V6h12v2z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<div class="information-container" id="informationContainer">
|
||||
<div class="information-pad" v-show="padShow">
|
||||
<div class="container">
|
||||
<div class="group-wrapper">
|
||||
<a
|
||||
class="menu-item hover-effect links"
|
||||
target="_blank"
|
||||
href="https://agree.xiaohongshu.com/h5/terms/ZXXY20220331001/-1"
|
||||
>
|
||||
<span>关于小红书</span>
|
||||
<div class="icon">
|
||||
<ArrowRight style="width: 1em; height: 1em; margin-right: 8px" />
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="menu-item hover-effect links"
|
||||
target="_blank"
|
||||
href="https://agree.xiaohongshu.com/h5/terms/ZXXY20220509001/-1"
|
||||
>
|
||||
<span>隐私,协议</span>
|
||||
<div class="icon">
|
||||
<ArrowRight style="width: 1em; height: 1em; margin-right: 8px" />
|
||||
</div>
|
||||
</a>
|
||||
<div class="menu-item hover-effect">
|
||||
<a href="#" @click="toUpshow = true">
|
||||
<span> 帮助与客服 </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="group-wrapper">
|
||||
<div class="group-header">设置</div>
|
||||
<div class="menu-item hover-effect">
|
||||
<span>深色模式</span>
|
||||
<div class="multistage-toggle component">
|
||||
<button class="toggle-item active">
|
||||
<div class="icon-wrapper">
|
||||
<Sunny style="width: 1em; height: 1em" />
|
||||
</div>
|
||||
</button>
|
||||
<button class="toggle-item">
|
||||
<div class="icon-wrapper">
|
||||
<Moon style="width: 1em; height: 1em" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="userInfo != null">
|
||||
<div class="menu-item hover-effect" @click="logout">
|
||||
<a href="#"><span>退出登录</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="information-wrapper" @click="loadPad">
|
||||
<!-- <More style="width: 1em; height: 1em; margin-right: 8px" /> -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M19.75 12a.75.75 0 0 0-.75-.75H5a.75.75 0 0 0 0 1.5h14a.75.75 0 0 0 .75-.75m0-5a.75.75 0 0 0-.75-.75H5a.75.75 0 0 0 0 1.5h14a.75.75 0 0 0 .75-.75m0 10a.75.75 0 0 0-.75-.75H5a.75.75 0 0 0 0 1.5h14a.75.75 0 0 0 .75-.75"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="channel">更多</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-content with-side-bar">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<Login v-show="loginShow" @click-child="close"></Login>
|
||||
<ToUP v-show="toUpshow" @click-to-up="toUpshow = false"></ToUP>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Search, Sunny, Moon, Close, House, Star, Bell, ArrowRight, More, CirclePlus } from "@element-plus/icons-vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import Login from "@/pages/login.vue";
|
||||
import { ref, watch, onMounted, computed, watchEffect } from "vue";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import { useSearchStore } from "@/store/searchStore";
|
||||
import SujContainer from "@/components/SujContainer.vue";
|
||||
import SearchContainer from "@/components/SearchContainer.vue";
|
||||
import { addRecord, getRecordByKeyWord } from "@/api/search";
|
||||
import { getRandomString } from "@/utils/util";
|
||||
import { getChatUserList, getCountMessage } from "@/api/im";
|
||||
import { useImStore } from "@/store/imStore";
|
||||
import { loginOut } from "@/api/user";
|
||||
import { wsKey } from "@/constant/constant";
|
||||
import ToUP from "@/pages/to-up/index.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const searchStore = useSearchStore();
|
||||
const imStore = useImStore();
|
||||
const loginShow = ref(true);
|
||||
const userInfo = ref<any>({});
|
||||
const showHistory = ref(false);
|
||||
const showSearch = ref(false);
|
||||
const keyword = ref("");
|
||||
const showClose = ref(false);
|
||||
const SearchInput = ref();
|
||||
const recordList = ref<Array<string>>([]);
|
||||
const activeLink = ref(1);
|
||||
const padShow = ref(false);
|
||||
const ws = ref();
|
||||
const toUpshow = ref(false);
|
||||
const isShowDot = ref(false);
|
||||
|
||||
const routerList = ["/dashboard", "/followTrend", "/notice", "/push", "/user", "/search"];
|
||||
|
||||
// 监听外部点击
|
||||
onMounted(() => {
|
||||
document.getElementById("container")!.addEventListener("click", function (e) {
|
||||
let event = e || window.event;
|
||||
let target = event.target || (event.srcElement as any);
|
||||
isInDiv("sujContainer", target).then((data) => {
|
||||
if (!data) {
|
||||
showHistory.value = false;
|
||||
showSearch.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
isInDiv("informationContainer", target).then((data) => {
|
||||
if (!data) {
|
||||
padShow.value = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const isInDiv = (dom: string, target: any) => {
|
||||
return new Promise((res) => {
|
||||
const data = document.getElementById(dom)!.contains(target);
|
||||
res(data);
|
||||
});
|
||||
};
|
||||
|
||||
const searchPage = () => {
|
||||
// 1.storage中添加搜索记录
|
||||
searchStore.setKeyword(keyword.value);
|
||||
if (keyword.value.length > 0) {
|
||||
addRecord(keyword.value);
|
||||
searchStore.pushRecord(keyword.value);
|
||||
searchStore.setSeed(getRandomString(12));
|
||||
}
|
||||
showSearch.value = false;
|
||||
|
||||
router.push({ name: "search", query: { keyword: keyword.value } });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [searchStore.seed, route.query.date],
|
||||
(newVal, oldVal) => {
|
||||
if (newVal[0] != oldVal[0]) {
|
||||
keyword.value = searchStore.keyWord;
|
||||
showHistory.value = false;
|
||||
showSearch.value = false;
|
||||
}
|
||||
if (newVal[1] != oldVal[1]) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [imStore.countMessage],
|
||||
(val) => {
|
||||
const allCount = val[0].chatCount + val[0].likeOrCollectionCount + val[0].commentCount + val[0].followCount;
|
||||
if (allCount === 0) {
|
||||
isShowDot.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const messageCount = computed({
|
||||
get: () => {
|
||||
return (
|
||||
imStore.countMessage.chatCount +
|
||||
imStore.countMessage.likeOrCollectionCount +
|
||||
imStore.countMessage.commentCount +
|
||||
imStore.countMessage.followCount
|
||||
);
|
||||
},
|
||||
set: (val) => {
|
||||
imStore.setCountMessage(val);
|
||||
},
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const url = window.location.href;
|
||||
const _keyword = "keyword";
|
||||
if (url.indexOf("?") != -1 && url.indexOf(_keyword) != -1) {
|
||||
const val = url.substring(url.lastIndexOf(_keyword) + _keyword.length + 1, url.length);
|
||||
keyword.value = decodeURI(val);
|
||||
}
|
||||
});
|
||||
|
||||
const changeInput = (e: any) => {
|
||||
const { value } = e.target;
|
||||
keyword.value = value;
|
||||
showClose.value = keyword.value == "" ? false : true;
|
||||
showSearch.value = keyword.value.length == 0 ? false : true;
|
||||
showHistory.value = keyword.value.length > 0 ? false : true;
|
||||
if (keyword.value.length > 0) {
|
||||
getRecordByKeyWord(keyword.value).then((res) => {
|
||||
recordList.value = res.data;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const focusInput = () => {
|
||||
showClose.value = keyword.value == "" ? false : true;
|
||||
showSearch.value = keyword.value.length == 0 ? false : true;
|
||||
showHistory.value = keyword.value.length > 0 ? false : true;
|
||||
};
|
||||
|
||||
const clearInput = () => {
|
||||
keyword.value = "";
|
||||
showClose.value = false;
|
||||
showHistory.value = true;
|
||||
showSearch.value = false;
|
||||
SearchInput.value.focus();
|
||||
};
|
||||
|
||||
const toLink = (num: number) => {
|
||||
activeLink.value = num;
|
||||
const url = routerList[num];
|
||||
if (url === "/user") {
|
||||
router.push({ name: "user", query: { uid: userInfo.value.id } });
|
||||
return;
|
||||
}
|
||||
router.push({ path: url });
|
||||
};
|
||||
|
||||
const close = (val: boolean) => {
|
||||
loginShow.value = val;
|
||||
userInfo.value = userStore.getUserInfo();
|
||||
};
|
||||
|
||||
const loadPad = () => {
|
||||
padShow.value = !padShow.value;
|
||||
};
|
||||
|
||||
const maxRetries = 5; // 最大重试次数
|
||||
let retryCount = 0; // 当前重试次数
|
||||
|
||||
const connectWs = (uid: string) => {
|
||||
ws.value = new WebSocket(wsKey + uid);
|
||||
ws.value.onopen = () => {
|
||||
console.log("连接成功");
|
||||
retryCount = 0; // 重置重试计数
|
||||
};
|
||||
ws.value.onclose = () => {
|
||||
console.log("连接断开");
|
||||
if (userInfo.value != null && userInfo.value != undefined) {
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
const retryDelay = Math.min(1000 * retryCount, 5000); // 延迟时间(1秒,2秒,3秒,最多5秒)
|
||||
console.log(`尝试重新连接,第 ${retryCount} 次重试,将在 ${retryDelay} 毫秒后重试`);
|
||||
setTimeout(() => connectWs(userInfo.value.id), retryDelay);
|
||||
} else {
|
||||
console.log("已达到最大重试次数,停止重连");
|
||||
}
|
||||
}
|
||||
};
|
||||
ws.value.onmessage = (e: any) => {
|
||||
const message = JSON.parse(e.data);
|
||||
console.log("收到消息", message);
|
||||
|
||||
if (message.msgType === 0) {
|
||||
const content = message.content;
|
||||
const _countMessage = imStore.countMessage;
|
||||
_countMessage.likeOrCollectionCount = content.likeOrCollectionCount;
|
||||
_countMessage.commentCount = content.commentCount;
|
||||
_countMessage.followCount = content.followCount;
|
||||
imStore.setCountMessage(_countMessage);
|
||||
}
|
||||
if (message.msgType === 1) {
|
||||
imStore.setMessage(message);
|
||||
}
|
||||
if (message.msgType === 5) {
|
||||
const userList = message.content;
|
||||
imStore.setUserList(userList);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getChatUserListMethod = () => {
|
||||
return new Promise((resolve) => {
|
||||
getChatUserList().then((res: any) => {
|
||||
const data = res.data;
|
||||
const _countMessage = imStore.countMessage;
|
||||
data.forEach((item: any) => {
|
||||
_countMessage.chatCount += item.count;
|
||||
});
|
||||
imStore.setCountMessage(_countMessage);
|
||||
imStore.setUserList(data);
|
||||
resolve(_countMessage.chatCount);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getCountMessageMethod = () => {
|
||||
return new Promise((resolve) => {
|
||||
getCountMessage().then((res: any) => {
|
||||
const data = res.data;
|
||||
imStore.setCountMessage(data);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const login = () => {
|
||||
loginShow.value = true;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
loginOut(userInfo.value.id).then(() => {
|
||||
userStore.loginOut();
|
||||
userInfo.value = null;
|
||||
loginShow.value = true;
|
||||
activeLink.value = 0;
|
||||
ws.value.onclose();
|
||||
router.push({ path: "/" });
|
||||
});
|
||||
};
|
||||
|
||||
const getWsMessage = async () => {
|
||||
if (userInfo.value === null || userInfo.value === undefined) {
|
||||
return;
|
||||
}
|
||||
loginShow.value = false;
|
||||
connectWs(userInfo.value.id);
|
||||
const p = await getChatUserListMethod();
|
||||
const q = (await getCountMessageMethod()) as any;
|
||||
|
||||
// TODO: 需要修复显示数量bug
|
||||
const _countMessage = {} as any;
|
||||
_countMessage.chatCount = p;
|
||||
_countMessage.likeOrCollectionCount = q.likeOrCollectionCount;
|
||||
_countMessage.commentCount = q.commentCount;
|
||||
_countMessage.followCount = q.followCount;
|
||||
|
||||
messageCount.value = _countMessage;
|
||||
};
|
||||
|
||||
const getPath = () => {
|
||||
const url = window.location.href;
|
||||
let path = "";
|
||||
if (url.indexOf("?") != -1) {
|
||||
const index = url.indexOf("?");
|
||||
path = url.substring(url.lastIndexOf("/"), index);
|
||||
} else {
|
||||
path = url.substring(url.lastIndexOf("/"), url.length);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
userInfo.value = userStore.getUserInfo();
|
||||
const path = getPath();
|
||||
activeLink.value = path === "/search" ? 0 : routerList.findIndex((item) => item === path);
|
||||
getWsMessage();
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
|
||||
/* 添加选中标签的效果 */
|
||||
.channel-list li:hover {
|
||||
border-radius: 20px;
|
||||
background-color: #f0f0f0;
|
||||
cursor: pointer;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
/* 激活的选中标签样式 */
|
||||
.active-channel {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* 浮动框容器 */
|
||||
.floating-box {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 10px;
|
||||
padding: 20px; /* 调整内边距 */
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
max-width: 300px; /* 调整宽度 */
|
||||
margin: 0 auto; /* 使浮动框居中 */
|
||||
margin-left: -2px; /* 向左移动 2px */
|
||||
cursor: pointer;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 行容器样式 */
|
||||
.line-container {
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.reds-icon {
|
||||
fill: #ff2442; /* 图标颜色 */
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 描述文本样式 */
|
||||
.desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.custom-button {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 15px;
|
||||
margin-left: -2px;
|
||||
height: 48px;
|
||||
background: #ff2442;
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
border-radius: 999px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1728px;
|
||||
background-color: #fff;
|
||||
margin: 0 auto;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 72px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 1728px;
|
||||
height: 72px;
|
||||
padding: 0 16px 0 24px;
|
||||
z-index: 10;
|
||||
|
||||
.tool-box {
|
||||
width: 24px;
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
height: 40px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
|
||||
@media screen and (max-width: 695px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) and (max-width: 1191px) {
|
||||
width: calc(-36px + 50vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1192px) and (max-width: 1423px) {
|
||||
width: calc(-33.6px + 40vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1424px) and (max-width: 1727px) {
|
||||
width: calc(-42.66667px + 33.33333vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1728px) {
|
||||
width: 533.33333px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 0 84px 0 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 120%;
|
||||
color: #333;
|
||||
caret-color: #ff2442;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.input-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.close-icon .search-icon {
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.15); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
|
||||
.side-bar {
|
||||
@media screen and (max-width: 695px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 696px) and (max-width: 959px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) and (max-width: 1191px) {
|
||||
width: calc(-18px + 25vw);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1192px) and (max-width: 1423px) {
|
||||
width: calc(-16.8px + 20vw);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1424px) and (max-width: 1727px) {
|
||||
width: calc(-21.33333px + 16.66667vw);
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1728px) {
|
||||
width: 266.66667px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
height: calc(100vh - 72px);
|
||||
overflow-y: scroll;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding-top: 16px;
|
||||
margin-top: 72px;
|
||||
position: fixed;
|
||||
overflow: visible;
|
||||
|
||||
.channel-list {
|
||||
min-height: auto;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.active-channel {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 999px;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-left: 16px;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
|
||||
.link-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
margin-left: 7rem;
|
||||
background-color: red;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.channel {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-left: 12px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.information-container {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-left: -6px;
|
||||
|
||||
.information-pad {
|
||||
z-index: 16;
|
||||
margin-bottom: 4px;
|
||||
width: 90%;
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 4px 32px 0 rgba(0, 0, 0, 0.08),
|
||||
0 1px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
border-radius: 12px;
|
||||
|
||||
.divider {
|
||||
margin: 0px 12px;
|
||||
list-style: none;
|
||||
height: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-width: 1px 0 0;
|
||||
}
|
||||
|
||||
.group-wrapper {
|
||||
padding: 4px;
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-weight: 400;
|
||||
height: 32px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 40px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.icon {
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.component {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.multistage-toggle {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
display: flex();
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
|
||||
.active {
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 2px 8px 0 rgba(0, 0, 0, 0.04),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toggle-item {
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
|
||||
.icon-wrapper {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 添加选中标签的效果 */
|
||||
.menu-item:hover {
|
||||
border-radius: 20px;
|
||||
background-color: #f0f0f0;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.information-wrapper {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* 添加选中标签的效果 */
|
||||
.information-wrapper:hover {
|
||||
border-radius: 20px;
|
||||
background-color: #f0f0f0;
|
||||
cursor: pointer;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@media screen and (min-width: 960px) and (max-width: 1191px) {
|
||||
padding-left: calc(-6px + 25vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1192px) and (max-width: 1423px) {
|
||||
padding-left: calc(-4.8px + 20vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1424px) and (max-width: 1727px) {
|
||||
padding-left: calc(-5.33333px + 16.66667vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1728px) {
|
||||
padding-left: 282.66667px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
596
src/pages/login.vue
Normal file
748
src/pages/main/main.vue
Normal file
@@ -0,0 +1,748 @@
|
||||
<template>
|
||||
<div class="note-detail-mask" style="transition: background-color 0.4s ease 0s;hsla(0,0%,100%,0.98)">
|
||||
<div class="note-container">
|
||||
<div class="media-container">
|
||||
<el-carousel height="90vh" :autoplay="false">
|
||||
<el-carousel-item v-for="(item, index) in noteInfo.imgList" :key="index">
|
||||
<el-image
|
||||
style="width: 100%; height: 100%"
|
||||
:src="item"
|
||||
fit="contain"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
/>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
|
||||
<div class="interaction-container">
|
||||
<div class="author-container">
|
||||
<div class="author-me">
|
||||
<div class="info" @click="toUser(noteInfo.uid)">
|
||||
<img class="avatar-item" style="width: 40px; height: 40px" :src="noteInfo.avatar" />
|
||||
<span class="name">{{ noteInfo.username }}</span>
|
||||
</div>
|
||||
<div class="follow-btn" v-if="currentUid !== noteInfo.uid">
|
||||
<el-button type="info" size="large" round v-if="noteInfo.isFollow" @click="follow(noteInfo.uid, 1)"
|
||||
>已关注</el-button
|
||||
>
|
||||
<el-button type="danger" size="large" round v-else @click="follow(noteInfo.uid, 0)">关注</el-button>
|
||||
</div>
|
||||
<div class="follow-btn" v-else>
|
||||
<el-dropdown>
|
||||
<el-button type="danger" size="large" round>
|
||||
编辑
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="noteInfo.pinned === '0'" @click="pinned(noteInfo.id, '1')"
|
||||
>置顶</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item v-else @click="pinned(noteInfo.id, '0')">取消置顶</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteNote(noteInfo.id)">删除</el-dropdown-item>
|
||||
<el-dropdown-item @click="toEdit(noteInfo.id)">编辑</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-scroller" ref="noteScroller" @scroll="loadMoreData">
|
||||
<div class="note-content">
|
||||
<div class="title">{{ noteInfo.title }}</div>
|
||||
<div class="desc">
|
||||
<span>{{ noteInfo.content }} <br /></span>
|
||||
<a class="tag tag-search" v-for="(item, index) in noteInfo.tagList" :key="index">#{{ item.title }}</a>
|
||||
</div>
|
||||
<div class="bottom-container">
|
||||
<span class="date">{{ noteInfo.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider interaction-divider"></div>
|
||||
|
||||
<!-- 评论 -->
|
||||
<div class="comments-el">
|
||||
<Comment
|
||||
:nid="props.nid"
|
||||
:currentPage="currentPage"
|
||||
:replyComment="replyComment"
|
||||
:seed="seed"
|
||||
@click-comment="clickComment"
|
||||
>
|
||||
</Comment>
|
||||
</div>
|
||||
<!-- -->
|
||||
</div>
|
||||
|
||||
<div class="interactions-footer">
|
||||
<div class="buttons">
|
||||
<div class="left">
|
||||
<span class="like-wrapper"
|
||||
><span class="like-lottie" v-if="noteInfo.isCollection" @click="likeOrCollection(3, -1)">
|
||||
<StarFilled style="width: 0.9em; height: 0.9em; color: #333" />
|
||||
</span>
|
||||
<span class="like-lottie" v-else @click="likeOrCollection(3, 1)">
|
||||
<Star style="width: 0.8em; height: 0.8em; color: #333" />
|
||||
</span>
|
||||
<span class="count">{{ noteInfo.collectionCount }}</span></span
|
||||
>
|
||||
<span class="collect-wrapper">
|
||||
<span class="like-lottie" v-if="noteInfo.isLike" @click="likeOrCollection(1, -1)">
|
||||
<i class="iconfont icon-follow-fill" style="width: 0.8em; height: 0.8em; color: #333"></i>
|
||||
</span>
|
||||
<span class="like-lottie" v-else @click="likeOrCollection(1, 1)">
|
||||
<i class="iconfont icon-follow" style="width: 0.8em; height: 0.8em; color: #333"></i>
|
||||
</span>
|
||||
<span class="count">{{ noteInfo.likeCount }}</span>
|
||||
</span>
|
||||
<span class="chat-wrapper">
|
||||
<span class="like-lottie">
|
||||
<ChatRound style="width: 0.8em; height: 0.8em; color: #333" />
|
||||
</span>
|
||||
<span class="count">
|
||||
{{ noteInfo.commentCount }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="share-wrapper"></div>
|
||||
</div>
|
||||
<div :class="showSaveBtn ? 'comment-wrapper active comment-comp ' : 'comment-wrapper comment-comp '">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
class="comment-input"
|
||||
v-model="commentValue"
|
||||
type="text"
|
||||
:placeholder="commentPlaceVal"
|
||||
@input="commenInput"
|
||||
@keyup.enter="saveComment"
|
||||
/>
|
||||
<div class="input-buttons" @click="clearCommeent" v-show="showSaveBtn">
|
||||
<Close style="width: 1.2em; height: 1.2em" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit" @click="saveComment">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="close-cricle" @click="close">
|
||||
<div class="close close-mask-white">
|
||||
<Close style="width: 1.2em; height: 1.2em; color: rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-desk"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Close, Star, ChatRound, StarFilled, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ref, watch } from "vue";
|
||||
import { getNoteById, pinnedNote, deleteNoteByIds } from "@/api/note";
|
||||
import { likeOrCollectionByDTO } from "@/api/likeOrCollection";
|
||||
import type { NoteInfo } from "@/type/note";
|
||||
import type { LikeOrCollectionDTO } from "@/type/likeOrCollection";
|
||||
import { formateTime, getRandomString } from "@/utils/util";
|
||||
import { followById } from "@/api/follower";
|
||||
import Comment from "@/components/Comment.vue";
|
||||
import type { CommentDTO } from "@/type/comment";
|
||||
import { saveCommentByDTO, syncCommentByIds } from "@/api/comment";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
// 这是路由传参
|
||||
// nid.value = history.state.nid;
|
||||
|
||||
const emit = defineEmits(["clickMain"]);
|
||||
|
||||
const props = defineProps({
|
||||
nid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
nowTime: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const currentUid = ref("");
|
||||
const noteInfo = ref<NoteInfo>({
|
||||
id: "",
|
||||
title: "",
|
||||
content: "",
|
||||
noteCover: "",
|
||||
uid: "",
|
||||
username: "",
|
||||
avatar: "",
|
||||
imgList: [],
|
||||
type: -1,
|
||||
likeCount: 0,
|
||||
collectionCount: 0,
|
||||
commentCount: 0,
|
||||
tagList: [],
|
||||
time: "",
|
||||
isFollow: false,
|
||||
isLike: false,
|
||||
isCollection: false,
|
||||
pinned: "0",
|
||||
});
|
||||
const commentValue = ref("");
|
||||
const commentPlaceVal = ref("请输入内容");
|
||||
const commentObject = ref<any>({});
|
||||
const replyComment = ref<any>({});
|
||||
const showSaveBtn = ref(false);
|
||||
const currentPage = ref(1);
|
||||
const seed = ref("");
|
||||
const commentIds = ref<Array<string>>([]);
|
||||
const noteScroller = ref(null);
|
||||
const isLogin = ref(false);
|
||||
const likeOrComment = ref({
|
||||
isLike: false,
|
||||
isComment: false,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.nowTime],
|
||||
() => {
|
||||
currentPage.value = 1;
|
||||
if (props.nid !== null && props.nid !== "") {
|
||||
getNoteById(props.nid).then((res: any) => {
|
||||
console.log("---note", res.data);
|
||||
noteInfo.value = res.data;
|
||||
noteInfo.value.imgList = JSON.parse(res.data.urls);
|
||||
noteInfo.value.time = formateTime(res.data.time);
|
||||
likeOrComment.value.isLike = noteInfo.value.isLike;
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const noLoginNotice = () => {
|
||||
if (!isLogin.value) {
|
||||
ElMessage.warning("用户未登录");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
const _login = noLoginNotice();
|
||||
if (!_login) {
|
||||
return;
|
||||
}
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (isLogin.value) {
|
||||
syncCommentByIds(commentIds.value).then(() => {
|
||||
commentIds.value = [];
|
||||
emit("clickMain", props.nid, likeOrComment.value);
|
||||
});
|
||||
} else {
|
||||
emit("clickMain");
|
||||
}
|
||||
};
|
||||
|
||||
const follow = (fid: string, type: number) => {
|
||||
const _login = noLoginNotice();
|
||||
if (!_login) {
|
||||
return;
|
||||
}
|
||||
followById(fid).then(() => {
|
||||
noteInfo.value.isFollow = type == 0;
|
||||
});
|
||||
};
|
||||
|
||||
const likeOrCollection = (type: number, val: number) => {
|
||||
const _login = noLoginNotice();
|
||||
if (!_login) {
|
||||
return;
|
||||
}
|
||||
const likeOrCollectionDTO = {} as LikeOrCollectionDTO;
|
||||
likeOrCollectionDTO.likeOrCollectionId = noteInfo.value.id;
|
||||
likeOrCollectionDTO.publishUid = noteInfo.value.uid;
|
||||
likeOrCollectionDTO.type = type == 1 ? 1 : 3;
|
||||
likeOrCollectionByDTO(likeOrCollectionDTO).then(() => {
|
||||
if (type == 1) {
|
||||
noteInfo.value.isLike = val == 1;
|
||||
noteInfo.value.likeCount += val;
|
||||
likeOrComment.value.isLike = val == 1;
|
||||
} else {
|
||||
noteInfo.value.isCollection = val == 1;
|
||||
noteInfo.value.collectionCount += val;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const pinned = (noteId: string, type: string) => {
|
||||
pinnedNote(noteId)
|
||||
.then((res: any) => {
|
||||
if (res.data) {
|
||||
noteInfo.value.pinned = type;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.warning("最多只能置顶3个笔记");
|
||||
});
|
||||
};
|
||||
|
||||
const deleteNote = (noteId: string) => {
|
||||
const data = [] as Array<string>;
|
||||
data.push(noteId);
|
||||
deleteNoteByIds(data).then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
emit("clickMain");
|
||||
});
|
||||
};
|
||||
|
||||
const toEdit = (noteId: string) => {
|
||||
router.push({ path: "/push", query: { date: Date.now(), noteId: noteId } });
|
||||
};
|
||||
|
||||
const clickComment = (comment: any) => {
|
||||
commentObject.value = comment;
|
||||
commentPlaceVal.value = "回复" + comment.username;
|
||||
};
|
||||
|
||||
const commenInput = (e: any) => {
|
||||
const { value } = e.target;
|
||||
commentValue.value = value;
|
||||
showSaveBtn.value = commentValue.value.length > 0 || commentObject.value.pid !== undefined;
|
||||
};
|
||||
|
||||
const saveComment = () => {
|
||||
const _login = noLoginNotice();
|
||||
if (!_login) {
|
||||
return;
|
||||
}
|
||||
const comment = {} as CommentDTO;
|
||||
comment.nid = props.nid;
|
||||
comment.noteUid = noteInfo.value.uid;
|
||||
if (commentObject.value.pid === undefined) {
|
||||
comment.pid = "0";
|
||||
comment.replyId = "0";
|
||||
comment.replyUid = noteInfo.value.uid;
|
||||
comment.level = 1;
|
||||
} else if (commentObject.value.pid == "0") {
|
||||
comment.pid = commentObject.value.id;
|
||||
comment.replyId = commentObject.value.id;
|
||||
comment.replyUid = commentObject.value.uid;
|
||||
comment.level = 2;
|
||||
} else {
|
||||
comment.pid = commentObject.value.pid;
|
||||
comment.replyId = commentObject.value.id;
|
||||
comment.replyUid = commentObject.value.uid;
|
||||
comment.level = 2;
|
||||
}
|
||||
|
||||
comment.content = commentValue.value;
|
||||
saveCommentByDTO(comment).then((res: any) => {
|
||||
replyComment.value = res.data;
|
||||
replyComment.value.replyUsername = commentObject.value.username;
|
||||
commentValue.value = "";
|
||||
commentObject.value = {};
|
||||
commentPlaceVal.value = "请输入内容";
|
||||
showSaveBtn.value = false;
|
||||
seed.value = getRandomString(12);
|
||||
commentIds.value.push(res.data.id);
|
||||
likeOrComment.value.isComment = true;
|
||||
});
|
||||
};
|
||||
|
||||
const clearCommeent = () => {
|
||||
commentValue.value = "";
|
||||
commentObject.value = {};
|
||||
commentPlaceVal.value = "请输入内容";
|
||||
showSaveBtn.value = false;
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
console.log("main加载更多");
|
||||
const container = noteScroller.value as any;
|
||||
if (container.scrollTop + container.clientHeight >= container.scrollHeight) {
|
||||
currentPage.value += 1;
|
||||
console.log("到底了");
|
||||
}
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
isLogin.value = userStore.isLogin();
|
||||
if (isLogin.value) {
|
||||
currentUid.value = userStore.getUserInfo().id;
|
||||
}
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
|
||||
background-color: #f8f8f8;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.note-detail-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 20;
|
||||
overflow: auto;
|
||||
|
||||
.back-desk {
|
||||
position: fixed;
|
||||
background-color: #f4f4f4;
|
||||
opacity: 0.5;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.close-cricle {
|
||||
left: 1.3vw;
|
||||
top: 1.3vw;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
.close-mask-white {
|
||||
box-shadow:
|
||||
0 2px 8px 0 rgba(0, 0, 0, 0.04),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background-color: #fff;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.2); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
|
||||
.note-container {
|
||||
width: 65%;
|
||||
height: 86%;
|
||||
transition:
|
||||
transform 0.4s ease 0s,
|
||||
width 0.4s ease 0s;
|
||||
transform: translate(280px, 60px) scale(1);
|
||||
overflow: visible;
|
||||
|
||||
display: flex;
|
||||
box-shadow:
|
||||
0 8px 64px 0 rgba(0, 0, 0, 0.04),
|
||||
0 1px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 20px;
|
||||
background: #f8f8f8;
|
||||
transform-origin: left top;
|
||||
z-index: 100;
|
||||
|
||||
.media-container {
|
||||
width: 65%;
|
||||
height: auto;
|
||||
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
border-radius: 20px 0 0 20px;
|
||||
transform: translateZ(0);
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
min-width: 440px;
|
||||
}
|
||||
|
||||
.interaction-container {
|
||||
width: 35%;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0 20px 20px 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.author-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
padding-left: 12px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-scroller {
|
||||
transition: scroll 0.4s;
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
height: 80vh;
|
||||
|
||||
.note-content {
|
||||
padding: 0 24px 24px;
|
||||
color: var(--color-primary-label);
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.tag-search {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-right: 2px;
|
||||
color: #13386c;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
line-height: 120%;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-divider {
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 4px 8px;
|
||||
list-style: none;
|
||||
height: 0;
|
||||
border: solid rgba(0, 0, 0, 0.08);
|
||||
border-width: 1px 0 0;
|
||||
}
|
||||
|
||||
.comments-el {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.interactions-footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
padding: 12px 24px 24px;
|
||||
height: 130px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
flex-basis: 130px;
|
||||
z-index: 1;
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.count {
|
||||
margin-left: 6px;
|
||||
margin-right: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
|
||||
.like-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
|
||||
.like-lottie {
|
||||
transform: scale(1.7);
|
||||
}
|
||||
}
|
||||
|
||||
.collect-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
|
||||
.like-lottie {
|
||||
transform: scale(1.7);
|
||||
}
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.15); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
|
||||
.chat-wrapper {
|
||||
cursor: pointer;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.like-lottie {
|
||||
transform: scale(1.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-wrapper {
|
||||
&.active {
|
||||
.input-wrapper {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-wrapper {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
transition: flex 0.3s;
|
||||
|
||||
.comment-input:placeholder-shown {
|
||||
background-image: none;
|
||||
padding: 12px 92px 12px 36px;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEUAAAA0NDQyMjIzMzM2NjY2NjYyMjI0NDQ1NTU1NTUzMzM1NTU1NTUzMzM1NTUzMzM1NTU1NTVl84gVAAAAEnRSTlMAmUyGEzlgc2AmfRx9aToKQzCSoXt+AAAAhElEQVRIx+3Uuw6DMAyF4XOcBOdCafv+L9vQkQFyJBak/JOHT7K8GLM7epuHusRhHwP/mejJ77i32CpZh33aD+lDFDzgZFE8+tgUv5BB9NxEb9NPL3i46JvoUUhXPBKZFQ/rTPHI3ZXt8xr12KX055LoAVtXz9kKHprxNMMxXqRvmAn9ACQ7A/tTXYAxAAAAAElFTkSuQmCC);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
background-position: 16px 12px;
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
padding: 12px 92px 12px 16px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 16px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
caret-color: rgba(51, 51, 51, 0.3);
|
||||
border-radius: 22px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-buttons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 92px;
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-left: 8px;
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
background: #3d8af5;
|
||||
border-radius: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-comp {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
382
src/pages/message/children/comment.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="message-container" v-infinite-scroll="loadMore">
|
||||
<li class="message-item" v-for="(item, index) in dataList" :key="index">
|
||||
<a class="user-avatar">
|
||||
<!-- https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png -->
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span v-if="item.pid === '0'">评论了您的笔记</span>
|
||||
<span v-if="item.replyUid === currentUid && item.pid !== '0'">回复了您的评论</span>
|
||||
<span v-if="item.replyUid !== currentUid && item.pid !== '0'">回复了{{ item.replyUsername }}的评论</span>
|
||||
<span>{{ item.time }}</span>
|
||||
</div>
|
||||
<div class="interaction-content">
|
||||
<span>{{ item.content }}</span>
|
||||
</div>
|
||||
<div class="quote-info" v-show="item.replyContent !== null">
|
||||
{{ item.replyContent }}
|
||||
</div>
|
||||
<!-- <div class="action">
|
||||
<div class="action-reply">
|
||||
<ChatRound style="width: 1.2em; height: 1.2em" />
|
||||
<div class="action-text">回复</div>
|
||||
</div>
|
||||
<div class="action-like">
|
||||
<i class="iconfont icon-follow" style="color: #333"></i>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="extra" @click="toMain(item.nid)">
|
||||
<img class="extra-image" :src="item.noteCover" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<!-- <li class="message-item">
|
||||
<a class="user-avatar">
|
||||
<img
|
||||
class="avatar-item"
|
||||
src="https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg"
|
||||
/>
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>这是名词</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span>评论了您的笔记 </span><span>2021-10-9</span>
|
||||
</div>
|
||||
<div class="interaction-content">
|
||||
<span>这是具体内容</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<div class="comment-wrapper action-comment">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
rows="1"
|
||||
class="comment-input"
|
||||
type="text"
|
||||
placeholder="回复 你好"
|
||||
style="height: 40px"
|
||||
></textarea>
|
||||
<div class="input-buttons">
|
||||
<Star style="width: 1.2em; height: 1.2em; margin: 0 6px" />
|
||||
<Star style="width: 1.2em; height: 1.2em; margin: 0 6px" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit">发送</button>
|
||||
</div>
|
||||
<div class="action-cancel">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra">
|
||||
<img
|
||||
class="extra-image"
|
||||
src="https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import { ChatRound, Star } from "@element-plus/icons-vue";
|
||||
import { ref } from "vue";
|
||||
import { formateTime } from "@/utils/util";
|
||||
import { getNoticeComment } from "@/api/comment";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 12;
|
||||
const dataList = ref<Array<any>>([]);
|
||||
const dataTotal = ref(0);
|
||||
const currentUid = ref("");
|
||||
|
||||
const emit = defineEmits(["clickMain"]);
|
||||
|
||||
const loadMore = () => {
|
||||
currentPage.value += 1;
|
||||
getPageData();
|
||||
};
|
||||
|
||||
const toMain = (nid: string) => {
|
||||
emit("clickMain", nid);
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const getPageData = () => {
|
||||
getNoticeComment(currentPage.value, pageSize).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
dataTotal.value = total;
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
dataList.value.push(item);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
currentUid.value = userStore.getUserInfo().id;
|
||||
getPageData();
|
||||
};
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
width: 40rem;
|
||||
height: 90vh;
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 24px;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 14px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.msg-count {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-info {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quote-info::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
margin-right: 6px;
|
||||
width: 4px;
|
||||
height: 17px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.action-reply {
|
||||
cursor: pointer;
|
||||
width: 88px;
|
||||
height: 40px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.action-text {
|
||||
margin-left: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-like {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
|
||||
.action-comment {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
|
||||
.input-wrapper {
|
||||
height: auto;
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: calc(100% - 70px);
|
||||
flex-shrink: 0;
|
||||
transition: flex 0.3s;
|
||||
|
||||
.comment-input:placeholder-shown {
|
||||
background-image: none;
|
||||
padding: 12px 92px 12px 36px;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEUAAAA0NDQyMjIzMzM2NjY2NjYyMjI0NDQ1NTU1NTUzMzM1NTU1NTUzMzM1NTUzMzM1NTU1NTVl84gVAAAAEnRSTlMAmUyGEzlgc2AmfRx9aToKQzCSoXt+AAAAhElEQVRIx+3Uuw6DMAyF4XOcBOdCafv+L9vQkQFyJBak/JOHT7K8GLM7epuHusRhHwP/mejJ77i32CpZh33aD+lDFDzgZFE8+tgUv5BB9NxEb9NPL3i46JvoUUhXPBKZFQ/rTPHI3ZXt8xr12KX055LoAVtXz9kKHprxNMMxXqRvmAn9ACQ7A/tTXYAxAAAAAElFTkSuQmCC);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
background-position: 16px 12px;
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
padding: 12px 92px 12px 16px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 16px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
caret-color: rgba(51, 51, 51, 0.3);
|
||||
border-radius: 22px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-buttons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 92px;
|
||||
color: rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-left: 8px;
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
background: #3d8af5;
|
||||
border-radius: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-cancel {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
|
||||
.comment-wrapper {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extra {
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
.extra-image {
|
||||
cursor: pointer;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
220
src/pages/message/children/follower.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="agree-container" v-infinite-scroll="loadMore">
|
||||
<li class="agree-item" v-for="(item, index) in dataList" :key="index">
|
||||
<a class="user-avatar">
|
||||
<!-- https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png -->
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span>开始关注您了 </span><span>{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra">
|
||||
<el-button type="info" round size="large" v-if="item.isFollow" @click="follow(item.uid, index, 1)"
|
||||
>互相关注</el-button
|
||||
>
|
||||
<el-button type="danger" round size="large" v-else @click="follow(item.uid, index, -1)">回关</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { getNoticeFollower } from "@/api/follower";
|
||||
import { formateTime } from "@/utils/util";
|
||||
import { followById } from "@/api/follower";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 12;
|
||||
const dataList = ref<Array<any>>([]);
|
||||
const dataTotal = ref(0);
|
||||
|
||||
const getPageData = () => {
|
||||
getNoticeFollower(currentPage.value, pageSize).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
dataTotal.value = total;
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
dataList.value.push(item);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const follow = (fid: string, index: number, type: number) => {
|
||||
followById(fid).then(() => {
|
||||
dataList.value[index].isFollow = type == -1;
|
||||
});
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
currentPage.value += 1;
|
||||
getPageData();
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
getPageData();
|
||||
};
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.agree-container {
|
||||
width: 40rem;
|
||||
height: 90vh;
|
||||
|
||||
.agree-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 24px;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 14px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.msg-count {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-info {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quote-info::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
margin-right: 6px;
|
||||
width: 4px;
|
||||
height: 17px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.extra {
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
.follow-button {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.reds-button-new.large {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding: 0 24px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.reds-button-new.primary {
|
||||
background-color: #ff2e4d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reds-button-new {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
background: none;
|
||||
border: none;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
border-radius: 100px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
200
src/pages/message/children/like-collection.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="agree-container" v-infinite-scroll="loadMore">
|
||||
<li class="agree-item" v-for="(item, index) in dataList" :key="index">
|
||||
<a class="user-avatar">
|
||||
<!-- https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png -->
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span v-if="item.type == 1">赞了您的笔记</span>
|
||||
<span v-if="item.type == 2">赞了您的评论</span>
|
||||
<span v-if="item.type == 3">收藏您的笔记</span>
|
||||
<span v-if="item.type == 4">赞了您的{{ item.content }}专辑</span>
|
||||
<span>{{ item.time }}</span>
|
||||
</div>
|
||||
<!-- <div class="interaction-content">
|
||||
<span>这是具体内容</span>
|
||||
</div> -->
|
||||
<div class="quote-info" v-if="item.type == 2">{{ item.content }}</div>
|
||||
</div>
|
||||
<div class="extra" @click="toPage(item.itemId)">
|
||||
<img class="extra-image" :src="item.itemCover" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { getNoticeLikeOrCollection } from "@/api/likeOrCollection";
|
||||
import { formateTime } from "@/utils/util";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits(["clickMain"]);
|
||||
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 12;
|
||||
const dataList = ref<Array<any>>([]);
|
||||
const dataTotal = ref(0);
|
||||
|
||||
const toPage = (nid: string) => {
|
||||
emit("clickMain", nid);
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const getPageData = () => {
|
||||
getNoticeLikeOrCollection(currentPage.value, pageSize).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
dataTotal.value = total;
|
||||
records.forEach((item: any) => {
|
||||
item.time = formateTime(item.time);
|
||||
dataList.value.push(item);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
currentPage.value += 1;
|
||||
getPageData();
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
getPageData();
|
||||
};
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.agree-container {
|
||||
width: 40rem;
|
||||
height: 90vh;
|
||||
|
||||
.agree-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 24px;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 14px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.msg-count {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-info {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quote-info::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
margin-right: 6px;
|
||||
width: 4px;
|
||||
height: 17px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.extra {
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
.extra-image {
|
||||
cursor: pointer;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
171
src/pages/message/children/message.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="message-container">
|
||||
<li class="message-item" v-for="(item, index) in dataList" :key="index">
|
||||
<a class="user-avatar">
|
||||
<!-- https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png -->
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
<div class="interaction-hint">
|
||||
<span>{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="interaction-content" @click="toChat(item.uid, index)">
|
||||
<span>{{ item.content }}</span>
|
||||
<div class="msg-count" v-show="item.count > 0">{{ item.count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<Chat
|
||||
v-if="chatShow"
|
||||
:acceptUid="acceptUid"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-chat="close"
|
||||
></Chat>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useImStore } from "@/store/imStore";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { formateTime } from "@/utils/util";
|
||||
import Chat from "@/components/Chat.vue";
|
||||
import { clearMessageCount } from "@/api/im";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const imStore = useImStore();
|
||||
const dataList = ref<Array<any>>([]);
|
||||
const chatShow = ref(false);
|
||||
const acceptUid = ref("");
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
dataList.value = [];
|
||||
const _countMessage = imStore.countMessage;
|
||||
_countMessage.chatCount = 0;
|
||||
imStore.userList.forEach((item) => {
|
||||
item.time = formateTime(item.timestamp);
|
||||
_countMessage.chatCount += item.count;
|
||||
dataList.value.push(item);
|
||||
});
|
||||
imStore.setCountMessage(_countMessage);
|
||||
});
|
||||
|
||||
const toChat = (uid: string, index: number) => {
|
||||
const _countMessage = imStore.countMessage;
|
||||
clearMessageCount(uid, 3).then(() => {
|
||||
const chatCount = dataList.value[index].count;
|
||||
_countMessage.chatCount -= chatCount;
|
||||
dataList.value[index].count = 0;
|
||||
imStore.setCountMessage(_countMessage);
|
||||
acceptUid.value = uid;
|
||||
chatShow.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const close = (uid: string) => {
|
||||
const index = dataList.value.findIndex((item) => item.uid === uid);
|
||||
const _countMessage = imStore.countMessage;
|
||||
clearMessageCount(uid, 3).then(() => {
|
||||
const chatCount = dataList.value[index].count;
|
||||
_countMessage.chatCount -= chatCount;
|
||||
dataList.value[index].count = 0;
|
||||
imStore.setCountMessage(_countMessage);
|
||||
chatShow.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.message-container {
|
||||
width: 40rem;
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 24px;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.msg-count {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
212
src/pages/message/index.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="isLogin">
|
||||
<div class style="height: 72px">
|
||||
<div class="reds-sticky">
|
||||
<div class="reds-tabs-list">
|
||||
<el-badge :value="_countMessage.chatCount" :max="99" :hidden="_countMessage.chatCount == 0">
|
||||
<div :class="type === 3 ? 'reds-tab-item active tab-item' : 'reds-tab-item tab-item'">
|
||||
<div class="badge-container" @click="toPage(3)">
|
||||
<span>我的消息</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-badge>
|
||||
<el-badge :value="_countMessage.commentCount" :max="99" :hidden="_countMessage.commentCount == 0">
|
||||
<div :class="type === 1 ? 'reds-tab-item active tab-item' : 'reds-tab-item tab-item'">
|
||||
<div class="badge-container" @click="toPage(1)">
|
||||
<span>评论和@</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-badge>
|
||||
<el-badge
|
||||
:value="_countMessage.likeOrCollectionCount"
|
||||
:max="99"
|
||||
:hidden="_countMessage.likeOrCollectionCount == 0"
|
||||
>
|
||||
<div :class="type === 0 ? 'reds-tab-item active tab-item' : 'reds-tab-item tab-item'">
|
||||
<div class="badge-container" @click="toPage(0)">
|
||||
<span>赞和收藏</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-badge>
|
||||
<el-badge :value="_countMessage.followCount" :max="99" :hidden="_countMessage.followCount == 0">
|
||||
<div :class="type === 2 ? 'reds-tab-item active tab-item' : 'reds-tab-item tab-item'">
|
||||
<div class="badge-container" @click="toPage(2)">
|
||||
<span>新增关注</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-badge>
|
||||
</div>
|
||||
<div class="divider" style="margin: 16px 32px 0px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Message v-if="type == 3"></Message>
|
||||
<Comment v-if="type == 1" @click-main="toMain"></Comment>
|
||||
<LikeCollection v-if="type == 0" @click-main="toMain"></LikeCollection>
|
||||
<Follower v-if="type == 2"></Follower>
|
||||
<!-- <router-view /> -->
|
||||
|
||||
<Main
|
||||
v-show="mainShow"
|
||||
:nid="nid"
|
||||
:nowTime="new Date()"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-main="close"
|
||||
></Main>
|
||||
|
||||
<el-backtop :bottom="80" :right="24">
|
||||
<div class="back-top">
|
||||
<Top style="width: 1.2em; height: 1.2em" color="rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</el-backtop>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="用户未登录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Top } from "@element-plus/icons-vue";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import Message from "@/pages/message/children/message.vue";
|
||||
import LikeCollection from "@/pages/message/children/like-collection.vue";
|
||||
import Follower from "@/pages/message/children/follower.vue";
|
||||
import Comment from "@/pages/message/children/comment.vue";
|
||||
import Main from "@/pages/main/main.vue";
|
||||
import { useImStore } from "@/store/imStore";
|
||||
import { clearMessageCount } from "@/api/im";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
const imStore = useImStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const type = ref(3);
|
||||
const nid = ref("");
|
||||
const currentUid = ref("");
|
||||
const mainShow = ref(false);
|
||||
const _countMessage = ref({
|
||||
chatCount: 0,
|
||||
likeOrCollectionCount: 0,
|
||||
commentCount: 0,
|
||||
followCount: 0,
|
||||
});
|
||||
const isLogin = ref(false);
|
||||
|
||||
watchEffect(() => {
|
||||
_countMessage.value = imStore.countMessage;
|
||||
});
|
||||
|
||||
const toPage = (val: number) => {
|
||||
const _countMessage = imStore.countMessage;
|
||||
clearMessageCount(currentUid.value, val).then(() => {
|
||||
switch (val) {
|
||||
case 0:
|
||||
_countMessage.likeOrCollectionCount = 0;
|
||||
break;
|
||||
case 1:
|
||||
_countMessage.commentCount = 0;
|
||||
break;
|
||||
default:
|
||||
_countMessage.followCount = 0;
|
||||
break;
|
||||
}
|
||||
imStore.setCountMessage(_countMessage);
|
||||
type.value = val;
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
mainShow.value = false;
|
||||
};
|
||||
|
||||
const toMain = (val: string) => {
|
||||
nid.value = val;
|
||||
mainShow.value = true;
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
isLogin.value = userStore.isLogin();
|
||||
if (isLogin.value) {
|
||||
currentUid.value = userStore.getUserInfo().id;
|
||||
}
|
||||
};
|
||||
initData();
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 0 24px;
|
||||
padding-top: 72px;
|
||||
width: 67%;
|
||||
margin: 0 auto;
|
||||
|
||||
.reds-sticky {
|
||||
top: 72px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
width: 40rem;
|
||||
box-sizing: border-box;
|
||||
height: 72px;
|
||||
padding-top: 16px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
.reds-tabs-list {
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
padding: 0 32px;
|
||||
|
||||
.active {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.reds-tab-item {
|
||||
padding: 0px 16px;
|
||||
margin-right: 0px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
white-space: nowrap;
|
||||
transition: transform 0.3s cubic-bezier(0.2, 0, 0.25, 1);
|
||||
z-index: 1;
|
||||
|
||||
.badge-container {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 4px 8px;
|
||||
list-style: none;
|
||||
height: 0;
|
||||
border: solid rgba(0, 0, 0, 0.08);
|
||||
border-width: 1px 0 0;
|
||||
}
|
||||
|
||||
.back-top {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 100px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
607
src/pages/push/index.vue
Normal file
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div class="container" id="container">
|
||||
<div v-if="isLogin" class="push-container">
|
||||
<div class="header"><span class="header-icon"></span><span class="header-title">发布图文</span></div>
|
||||
<div class="img-list">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
action="http://localhost:88/api/util/oss/saveBatch/0"
|
||||
list-type="picture-card"
|
||||
multiple
|
||||
:limit="9"
|
||||
:headers="uploadHeader"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
<el-divider style="margin: 0.75rem; width: 96%" />
|
||||
<div class="push-content">
|
||||
<el-input
|
||||
v-model="note.title"
|
||||
maxlength="20"
|
||||
show-word-limit
|
||||
type="text"
|
||||
placeholder="请输入标题"
|
||||
class="input-title"
|
||||
/>
|
||||
<el-input
|
||||
v-model="note.content"
|
||||
maxlength="250"
|
||||
show-word-limit
|
||||
:autosize="{ minRows: 4, maxRows: 5 }"
|
||||
type="textarea"
|
||||
placeholder="填写更全面的描述信息,让更多的人看到你吧❤️"
|
||||
/>
|
||||
<div class="tag-list">
|
||||
<el-tag
|
||||
v-for="tag in dynamicTags"
|
||||
:key="tag"
|
||||
closable
|
||||
:disable-transitions="false"
|
||||
@close="handleClose(tag)"
|
||||
class="tag-item"
|
||||
type="danger"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="InputRef"
|
||||
v-model="inputValue"
|
||||
style="width: 3.125rem"
|
||||
size="small"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputBlur"
|
||||
/>
|
||||
<el-button v-else type="warning" size="small" @click="showInput" plain id="tagContainer"> + 标签 </el-button>
|
||||
</div>
|
||||
<!-- <div
|
||||
v-infinite-scroll="loadMoreData"
|
||||
class="scroll-tag-container"
|
||||
v-show="showTagState"
|
||||
:infinite-scroll-distance="50"
|
||||
>
|
||||
<p v-for="(item, index) in selectTagList" :key="index" class="scrollbar-tag-item" @click="selectTag(item)">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
</div> -->
|
||||
<div class="hot-tag">
|
||||
<span class="tag-title-text">推荐标签:</span>
|
||||
<el-tag v-for="tag in hotTags" :key="tag" class="hot-tag-item" type="danger" @click="selectHotTag(tag)">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 0.75rem; width: 96%" />
|
||||
<div class="categorys">
|
||||
<el-cascader
|
||||
ref="CascaderRef"
|
||||
v-model="categoryList"
|
||||
:options="options"
|
||||
@change="handleChange"
|
||||
:props="props"
|
||||
placeholder="请选择分类"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="btns">
|
||||
<button class="css-fm44j css-osq2ks dyn">
|
||||
<span class="btn-content" @click="addTag"># 话题</span></button
|
||||
><button class="css-fm44j css-osq2ks dyn">
|
||||
<span class="btn-content"><span>@</span> 用户</span></button
|
||||
><button class="css-fm44j css-osq2ks dyn">
|
||||
<span class="btn-content">
|
||||
<div class="smile"></div>
|
||||
表情
|
||||
</span>
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<div class="submit">
|
||||
<el-button type="danger" loading :disabled="true" v-if="pushLoading">发布</el-button>
|
||||
<button class="publishBtn" @click="pubslish()" v-else>
|
||||
<span class="btn-content">发布</span>
|
||||
</button>
|
||||
<button class="clearBtn">
|
||||
<span class="btn-content" @click="resetData">取消</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="用户未登录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick, onMounted } from "vue";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { UploadUserFile, CascaderProps, ElInput } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import { getCategoryTreeData } from "@/api/category";
|
||||
import { saveNoteByDTO, getNoteById, updateNoteByDTO } from "@/api/note";
|
||||
import { getTagByKeyword } from "@/api/tag";
|
||||
import { getFileFromUrl } from "@/utils/util";
|
||||
// import Schema from "async-validator";
|
||||
// import Crop from "@/components/Crop.vue";
|
||||
const props: CascaderProps = {
|
||||
label: "title",
|
||||
value: "id",
|
||||
checkStrictly: true, // 允许选择父级节点
|
||||
};
|
||||
const CascaderRef = ref<any>(null);
|
||||
|
||||
// const rules = {
|
||||
// title: { required: true, message: "标题不能为空" },
|
||||
// content: { required: true, message: "内容不能为空" },
|
||||
// category: { required: true, message: "分类不能为空" },
|
||||
// };
|
||||
// const validator = new Schema(rules);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
|
||||
const dialogImageUrl = ref("");
|
||||
const dialogVisible = ref(false);
|
||||
const uploadHeader = ref({
|
||||
accessToken: userStore.getToken(),
|
||||
});
|
||||
const categoryList = ref<Array<any>>([]);
|
||||
const options = ref([]);
|
||||
const note = ref<any>({});
|
||||
const showTagState = ref(false);
|
||||
const selectTagList = ref<Array<any>>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 10;
|
||||
const tagTotal = ref(0);
|
||||
const pushLoading = ref(false);
|
||||
const isLogin = ref(false);
|
||||
const inputValue = ref("");
|
||||
const dynamicTags = ref<Array<string>>([]);
|
||||
const inputVisible = ref(false);
|
||||
const InputRef = ref<InstanceType<typeof ElInput>>();
|
||||
const hotTags = ref<Array<string>>([]);
|
||||
|
||||
const handleClose = (tag: string) => {
|
||||
dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
inputVisible.value = false;
|
||||
// showTagState.value = false;
|
||||
};
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true;
|
||||
nextTick(() => {
|
||||
InputRef.value!.input!.focus();
|
||||
addTag();
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue.value) {
|
||||
dynamicTags.value.push(inputValue.value);
|
||||
}
|
||||
inputVisible.value = false;
|
||||
inputValue.value = "";
|
||||
showTagState.value = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => inputValue.value,
|
||||
() => {
|
||||
addTag();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听外部点击
|
||||
onMounted(() => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
}
|
||||
document.getElementById("container")!.addEventListener("click", function (e) {
|
||||
var event = e || window.event;
|
||||
var target = event.target || (event.srcElement as any);
|
||||
// if(target.id == "name") {
|
||||
const tagContainer = document.getElementById("tagContainer");
|
||||
if (tagContainer == null) return;
|
||||
|
||||
if (tagContainer.contains(target)) {
|
||||
console.log("in");
|
||||
} else {
|
||||
showTagState.value = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const addTag = () => {
|
||||
selectTagList.value = [];
|
||||
currentPage.value = 1;
|
||||
setData();
|
||||
showTagState.value = true;
|
||||
};
|
||||
|
||||
const setData = () => {
|
||||
getTagByKeyword(currentPage.value, pageSize, inputValue.value).then((res) => {
|
||||
const { records, total } = res.data;
|
||||
selectTagList.value.push(...records);
|
||||
tagTotal.value = total;
|
||||
});
|
||||
};
|
||||
|
||||
// const selectTag = (val: any) => {
|
||||
// dynamicTags.value.push(val.title);
|
||||
// showTagState.value = false;
|
||||
// inputVisible.value = false;
|
||||
// inputValue.value = "";
|
||||
// };
|
||||
|
||||
const selectHotTag = (val: string) => {
|
||||
dynamicTags.value.push(val);
|
||||
};
|
||||
|
||||
// const loadMoreData = () => {
|
||||
// currentPage.value += 1;
|
||||
// setData();
|
||||
// };
|
||||
|
||||
const handleChange = (ids: Array<any>) => {
|
||||
categoryList.value = ids;
|
||||
// 选中后关闭下拉框
|
||||
CascaderRef.value.togglePopperVisible();
|
||||
};
|
||||
|
||||
const getHotTag = () => {
|
||||
getTagByKeyword(1, pageSize, "").then((res) => {
|
||||
const { records } = res.data;
|
||||
records.forEach((item: any) => {
|
||||
hotTags.value.push(item.title);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getNoteByIdMethod = (noteId: string) => {
|
||||
getNoteById(noteId).then((res) => {
|
||||
const { data } = res;
|
||||
note.value = data;
|
||||
const urls = JSON.parse(data.urls);
|
||||
urls.forEach((item: string) => {
|
||||
const fileName = item.substring(item.lastIndexOf("/") + 1);
|
||||
|
||||
getFileFromUrl(item, fileName).then((res: any) => {
|
||||
fileList.value.push({ name: fileName, url: item, raw: res });
|
||||
});
|
||||
});
|
||||
categoryList.value.push(data.cpid);
|
||||
categoryList.value.push(data.cid);
|
||||
|
||||
data.tagList.forEach((item: any) => {
|
||||
dynamicTags.value.push(item.title);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 上传图片功能
|
||||
const pubslish = () => {
|
||||
//验证
|
||||
if (fileList.value.length <= 0 || note.value.title === null || categoryList.value.length <= 0) {
|
||||
ElMessage.error("请选择图片,标签,分类~");
|
||||
return;
|
||||
}
|
||||
pushLoading.value = true;
|
||||
let params = new FormData();
|
||||
//注意此处对文件数组进行了参数循环添加
|
||||
|
||||
fileList.value.forEach((file: any) => {
|
||||
params.append("uploadFiles", file.raw);
|
||||
});
|
||||
|
||||
note.value.count = fileList.value.length;
|
||||
note.value.type = 1;
|
||||
// note.value.content = document.getElementById("post-textarea")!.innerHTML.replace(/<[^>]*>[^<]*(<[^>]*>)?/gi, "");
|
||||
note.value.cpid = categoryList.value[0];
|
||||
note.value.cid = categoryList.value[1];
|
||||
note.value.tagList = dynamicTags.value;
|
||||
const coverImage = new Image();
|
||||
coverImage.src = fileList.value[0].url!;
|
||||
coverImage.onload = () => {
|
||||
const size = coverImage.width / coverImage.height;
|
||||
note.value.noteCoverHeight = size >= 1.3 ? 200 : 300;
|
||||
const noteData = JSON.stringify(note.value);
|
||||
params.append("noteData", noteData);
|
||||
if (note.value.id !== null && note.value.id !== undefined) {
|
||||
updateNote(params);
|
||||
} else {
|
||||
saveNote(params);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const updateNote = (params: FormData) => {
|
||||
updateNoteByDTO(params)
|
||||
.then(() => {
|
||||
resetData();
|
||||
ElMessage.success("修改成功");
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("修改失败");
|
||||
})
|
||||
.finally(() => {
|
||||
pushLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const saveNote = (params: FormData) => {
|
||||
saveNoteByDTO(params)
|
||||
.then(() => {
|
||||
resetData();
|
||||
ElMessage.success("发布成功,请等待审核结果");
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("发布失败");
|
||||
})
|
||||
.finally(() => {
|
||||
pushLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
note.value = {};
|
||||
categoryList.value = [];
|
||||
fileList.value = [];
|
||||
pushLoading.value = false;
|
||||
dynamicTags.value = [];
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
isLogin.value = userStore.isLogin();
|
||||
if (isLogin.value) {
|
||||
const noteId = route.query.noteId as string;
|
||||
if (noteId !== "" && noteId !== undefined) {
|
||||
getNoteByIdMethod(noteId);
|
||||
}
|
||||
getCategoryTreeData().then((res) => {
|
||||
options.value = res.data;
|
||||
});
|
||||
getHotTag();
|
||||
}
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
:deep(.el-upload-list__item.is-success .el-upload-list__item-status-label) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-upload--picture-card) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
padding-top: 72px;
|
||||
|
||||
.push-container {
|
||||
margin-left: 11vw;
|
||||
margin-top: 1vw;
|
||||
width: 720px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
|
||||
.header {
|
||||
padding: 15px 20px;
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
.header-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 16px;
|
||||
background: #3a64ff;
|
||||
|
||||
border-radius: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.img-list {
|
||||
width: 650px;
|
||||
margin: auto;
|
||||
padding: 6px 6px 6px 6px;
|
||||
}
|
||||
|
||||
.push-content {
|
||||
padding: 6px 12px 6px 12px;
|
||||
position: relative;
|
||||
|
||||
.hot-tag {
|
||||
.tag-title-text {
|
||||
font-size: 0.875rem;
|
||||
color: #484848;
|
||||
margin: 0.125rem 0;
|
||||
}
|
||||
.hot-tag-item {
|
||||
cursor: pointer;
|
||||
margin: 0.3125rem 0.3125rem 0 0.3125rem;
|
||||
}
|
||||
:hover {
|
||||
transform: scale(1.2); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
margin: 0.825rem 0;
|
||||
.tag-item {
|
||||
margin-right: 0.3125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-tag-container {
|
||||
position: absolute;
|
||||
width: 98%;
|
||||
background-color: #fff;
|
||||
z-index: 99999;
|
||||
border: 1px solid #f4f4f4;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
.scrollbar-tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding-left: 2px;
|
||||
color: #484848;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scrollbar-tag-item:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
|
||||
.input-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.post-content:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
cursor: text;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
min-height: 90px;
|
||||
max-height: 300px;
|
||||
margin-bottom: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px 22px;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.post-content:focus,
|
||||
.post-content:hover {
|
||||
border: 1px solid #3a64ff;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
padding: 0 12px 10px 12px;
|
||||
|
||||
button {
|
||||
min-width: 62px;
|
||||
width: 62px;
|
||||
margin: 0 6px 0 0;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.css-fm44j {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
appearance: none;
|
||||
font-family:
|
||||
RedNum,
|
||||
RedZh,
|
||||
RedEn,
|
||||
-apple-system;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgb(217, 217, 217);
|
||||
outline: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
color: rgb(38, 38, 38);
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.categorys {
|
||||
padding: 0 12px 10px 12px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 10px 12px 10px 12px;
|
||||
margin-top: 10px;
|
||||
|
||||
button {
|
||||
width: 100px;
|
||||
height: 36px;
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
margin-left: 250px;
|
||||
margin-bottom: 2px;
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
color 0.3s; /* 添加过渡效果 */
|
||||
}
|
||||
button:hover {
|
||||
transform: scale(1.05); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
|
||||
.publishBtn {
|
||||
background-color: #ff2442;
|
||||
color: #fff;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.clearBtn {
|
||||
border-radius: 24px;
|
||||
margin-left: 10px;
|
||||
border: 1px solid rgb(217, 217, 217);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
781
src/pages/search/index.vue
Executable file
@@ -0,0 +1,781 @@
|
||||
<template>
|
||||
<div class="feeds-page">
|
||||
<div class="middle">
|
||||
<div id="search-type">
|
||||
<div data-v-7f9e6aac="" class="scroll-container channel-scroll-container">
|
||||
<div class="content-container">
|
||||
<div :class="typeClass == 0 ? 'channel active' : 'channel'" @click="getNoteByType(0)">全部</div>
|
||||
<div :class="typeClass == 1 ? 'channel active' : 'channel'" @click="getNoteByType(1)">最热</div>
|
||||
<div :class="typeClass == 2 ? 'channel active' : 'channel'" @click="getNoteByType(2)">最新</div>
|
||||
<div style="line-height: 40px">|</div>
|
||||
<div :class="typeClass == 3 ? 'channel active' : 'channel'" @click="getUserData">用户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-box">
|
||||
<div>
|
||||
<div class="filter">
|
||||
<!-- <span>综合</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider rec-filter"></div>
|
||||
<div class="note-container" v-if="!isShowUser">
|
||||
<div class="channel-container">
|
||||
<div class="scroll-container channel-scroll-container">
|
||||
<div class="content-container">
|
||||
<div :class="categoryClass == '0' ? 'channel active' : 'channel'" @click="getNoteList">推荐</div>
|
||||
<div
|
||||
:class="categoryClass == item.id ? 'channel active' : 'channel'"
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
@click="getNoteListByCategory(item.id)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-container"></div>
|
||||
<div
|
||||
class="feeds-container"
|
||||
v-infinite-scroll="loadMoreData"
|
||||
:infinite-scroll-distance="50"
|
||||
:infinite-scroll-disabled="mainShow || loadingMore"
|
||||
>
|
||||
<div class="feeds-loading-top animate__animated animate__zoomIn animate__delay-0.5s" v-show="topLoading">
|
||||
<Loading style="width: 1.2em; height: 1.2em"></Loading>
|
||||
</div>
|
||||
<Waterfall
|
||||
:list="noteList"
|
||||
:width="options.width"
|
||||
:gutter="options.gutter"
|
||||
:hasAroundGutter="options.hasAroundGutter"
|
||||
:animation-effect="options.animationEffect"
|
||||
:animation-duration="options.animationDuration"
|
||||
:animation-delay="options.animationDelay"
|
||||
:breakpoints="options.breakpoints"
|
||||
style="min-width: 46.25rem"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<el-skeleton style="width: 15rem" :loading="!item.isLoading" animated>
|
||||
<template #template>
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '15rem',
|
||||
maxHeight: '18.75rem',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '.5rem',
|
||||
}"
|
||||
@load="handleLoad(item)"
|
||||
></el-image>
|
||||
<div style="padding: 0.875rem">
|
||||
<el-skeleton-item variant="h3" style="width: 100%" />
|
||||
<div style="display: flex; align-items: center; margin-top: 0.125rem; height: 1rem">
|
||||
<el-skeleton style="--el-skeleton-circle-size: 1.25rem">
|
||||
<template #template>
|
||||
<el-skeleton-item variant="circle" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-skeleton-item variant="text" style="margin-left: 0.625rem" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="card" style="max-width: 15rem">
|
||||
<el-image
|
||||
:src="item.noteCover"
|
||||
:style="{
|
||||
width: '15rem',
|
||||
maxHeight: '18.75rem',
|
||||
height: item.noteCoverHeight + 'px',
|
||||
borderRadius: '.5rem',
|
||||
}"
|
||||
fit="cover"
|
||||
@click="toMain(item.id)"
|
||||
></el-image>
|
||||
<div class="footer">
|
||||
<a class="title">
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
<div class="author-wrapper">
|
||||
<a class="author">
|
||||
<img class="author-avatar" :src="item.avatar" />
|
||||
<span class="name">{{ item.username }}</span>
|
||||
</a>
|
||||
<span class="like-wrapper like-active">
|
||||
<i class="iconfont icon-follow" style="width: 1em; height: 1em"></i>
|
||||
<span class="count">{{ item.likeCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
</Waterfall>
|
||||
|
||||
<div class="feeds-loading">
|
||||
<Loading style="width: 1.2em; height: 1.2em" v-show="botLoading"></Loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-container" v-else>
|
||||
<ul class="agree-container" v-infinite-scroll="loadMoreUser" :infinite-scroll-distance="20">
|
||||
<li class="agree-item" v-for="(item, index) in userDataList" :key="index">
|
||||
<a class="user-avatar">
|
||||
<img class="avatar-item" :src="item.avatar" @click="toUser(item.id)" />
|
||||
</a>
|
||||
<div class="main">
|
||||
<div class="info">
|
||||
<div class="user-info">
|
||||
<a class>{{ item.username }}</a>
|
||||
</div>
|
||||
<div class="interaction-hint">
|
||||
<span>粉丝:</span><span>{{ item.fanCount }}</span
|
||||
> | <span>关注: </span><span>{{ item.followerCount }}</span
|
||||
> | <span>作品: </span><span>{{ item.trendCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra">
|
||||
<el-button type="info" round size="large" v-if="item.isFollow" @click="follow(item.uid, index, 1)"
|
||||
>已关注</el-button
|
||||
>
|
||||
<el-button type="danger" round size="large" v-else @click="follow(item.uid, index, -1)">回关</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<FloatingBtn @click-refresh="refresh"></FloatingBtn>
|
||||
<Main
|
||||
v-show="mainShow"
|
||||
:nid="nid"
|
||||
:nowTime="new Date()"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-main="close"
|
||||
>
|
||||
</Main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Waterfall } from "vue-waterfall-plugin-next";
|
||||
import "vue-waterfall-plugin-next/dist/style.css";
|
||||
import { ref, watch } from "vue";
|
||||
import { getNoteByDTO, getCategoryAgg } from "@/api/search";
|
||||
import type { NoteDTO, NoteSearch } from "@/type/note";
|
||||
import type { Category } from "@/type/category";
|
||||
import Main from "@/pages/main/main.vue";
|
||||
import FloatingBtn from "@/components/FloatingBtn.vue";
|
||||
import { options } from "@/constant/constant";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
import { refreshTab } from "@/utils/util";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getUserByKeyword } from "@/api/user";
|
||||
import { followById } from "@/api/follower";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const topLoading = ref(false);
|
||||
const botLoading = ref(false);
|
||||
const noteList = ref<Array<NoteSearch>>([]);
|
||||
const categoryList = ref<Array<Category>>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 20;
|
||||
const noteTotal = ref(0);
|
||||
const currentUserPage = ref(1);
|
||||
const userPageSize = 15;
|
||||
const categoryClass = ref("0");
|
||||
const typeClass = ref(0);
|
||||
const mainShow = ref(false);
|
||||
const nid = ref("");
|
||||
const noteDTO = ref<NoteDTO>({
|
||||
keyword: "",
|
||||
type: 0,
|
||||
cid: "",
|
||||
cpid: "",
|
||||
});
|
||||
const loadingMore = ref(false);
|
||||
const isShowUser = ref(false);
|
||||
const userDataList = ref<Array<any>>([]);
|
||||
const userTotal = ref(0);
|
||||
|
||||
const getUserData = () => {
|
||||
typeClass.value = 3;
|
||||
isShowUser.value = true;
|
||||
currentUserPage.value = 1;
|
||||
userDataList.value = [];
|
||||
getUserByKeyword(currentUserPage.value, userPageSize, noteDTO.value.keyword).then((res: any) => {
|
||||
const { records, total } = res.data;
|
||||
userDataList.value.push(...records);
|
||||
userTotal.value = total;
|
||||
});
|
||||
};
|
||||
|
||||
const follow = (fid: string, index: number, type: number) => {
|
||||
followById(fid).then(() => {
|
||||
userDataList.value[index].isFollow = type == -1;
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreUser = () => {
|
||||
botLoading.value = true;
|
||||
loadingMore.value = true;
|
||||
currentUserPage.value += 1;
|
||||
new Promise((resolve) => {
|
||||
getUserByKeyword(currentUserPage.value, userPageSize, noteDTO.value.keyword).then((res: any) => {
|
||||
const { records, total } = res.data;
|
||||
userDataList.value.push(...records);
|
||||
userTotal.value = total;
|
||||
resolve(false);
|
||||
});
|
||||
}).then((data) => {
|
||||
loadingMore.value = data as boolean;
|
||||
setTimeout(() => {
|
||||
botLoading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [route.query.keyword],
|
||||
(newVal) => {
|
||||
noteDTO.value.keyword = newVal[0] as string;
|
||||
noteDTO.value.cid = "";
|
||||
noteDTO.value.type = 0;
|
||||
categoryClass.value = "0";
|
||||
isShowUser.value = false;
|
||||
typeClass.value = 0;
|
||||
getNoteListByKeyword();
|
||||
getCategoryData();
|
||||
}
|
||||
);
|
||||
|
||||
const getNoteByType = (type: number) => {
|
||||
isShowUser.value = false;
|
||||
noteDTO.value.type = type;
|
||||
typeClass.value = type;
|
||||
getNoteListByKeyword();
|
||||
};
|
||||
|
||||
const toMain = (noteId: string) => {
|
||||
// router.push({ name: "main", state: { nid: nid } });
|
||||
nid.value = noteId;
|
||||
mainShow.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
mainShow.value = false;
|
||||
};
|
||||
|
||||
const handleLoad = (item: any) => {
|
||||
item.isLoading = true;
|
||||
};
|
||||
|
||||
const toUser = (uid: string) => {
|
||||
router.push({ name: "user", query: { uid: uid } });
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
// 使用回调函数优化代码
|
||||
refreshTab(() => {
|
||||
topLoading.value = true;
|
||||
isShowUser.value = false;
|
||||
typeClass.value = 0;
|
||||
currentPage.value = 1;
|
||||
currentUserPage.value = 1;
|
||||
noteList.value = [];
|
||||
userDataList.value = [];
|
||||
setTimeout(() => {
|
||||
getNoteList();
|
||||
topLoading.value = false;
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreData = () => {
|
||||
botLoading.value = true;
|
||||
loadingMore.value = true;
|
||||
currentPage.value += 1;
|
||||
new Promise((resolve) => {
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
resolve(false);
|
||||
});
|
||||
}).then((data) => {
|
||||
loadingMore.value = data as boolean;
|
||||
setTimeout(() => {
|
||||
botLoading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
const setData = (res: any) => {
|
||||
const { records, total } = res.data;
|
||||
noteTotal.value = total;
|
||||
noteList.value.push(...records);
|
||||
};
|
||||
|
||||
const getNoteList = () => {
|
||||
noteDTO.value.type = 0;
|
||||
noteDTO.value.cid = "";
|
||||
categoryClass.value = "0";
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getNoteListByCategory = (id: string) => {
|
||||
categoryClass.value = id;
|
||||
noteDTO.value.cid = id;
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getNoteListByKeyword = () => {
|
||||
noteList.value = [] as Array<any>;
|
||||
currentPage.value = 1;
|
||||
getNoteByDTO(currentPage.value, pageSize, noteDTO.value).then((res) => {
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getCategoryData = () => {
|
||||
getCategoryAgg(noteDTO.value).then((res: any) => {
|
||||
categoryList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
const keyword = route.query.keyword as string;
|
||||
if (keyword.trim().length > 0) {
|
||||
noteDTO.value.keyword = keyword as string;
|
||||
noteDTO.value.cid = "";
|
||||
categoryClass.value = "0";
|
||||
getNoteListByKeyword();
|
||||
getCategoryData();
|
||||
}
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.feeds-page {
|
||||
flex: 1;
|
||||
padding: 0 1.5rem;
|
||||
padding-top: 4.5rem;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.channel-scroll-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
height: 4.5rem;
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.channel {
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active,
|
||||
.channel:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 62.4375rem;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.filter {
|
||||
height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 120%;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.divider.rec-filter {
|
||||
height: 0.0625rem;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.channel-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
.channel-scroll-container {
|
||||
backdrop-filter: blur(1.25rem);
|
||||
background-color: transparent;
|
||||
width: calc(100vw - 1.5rem);
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
height: 4.5rem;
|
||||
|
||||
.content-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
|
||||
.active {
|
||||
font-weight: 600;
|
||||
background: rgba(226, 86, 105, 0.06);
|
||||
color: #ff2442;
|
||||
}
|
||||
|
||||
.channel {
|
||||
height: 2.5rem;
|
||||
margin-right: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
//background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feeds-container {
|
||||
position: relative;
|
||||
transition: width 0.5s;
|
||||
margin: 0 auto;
|
||||
|
||||
.feeds-loading {
|
||||
margin: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feeds-loading-top {
|
||||
text-align: center;
|
||||
line-height: 6vh;
|
||||
height: 6vh;
|
||||
}
|
||||
|
||||
.noteImg {
|
||||
width: 15rem;
|
||||
max-height: 18.75rem;
|
||||
object-fit: cover;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.75rem;
|
||||
|
||||
.title {
|
||||
margin-bottom: 0.5rem;
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 140%;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.author-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 1.25rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
font-size: 0.75rem;
|
||||
transition: color 1s;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.75rem;
|
||||
|
||||
.author-avatar {
|
||||
margin-right: 0.375rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 1.25rem;
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.like-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.count {
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-btn-sets {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 2.5rem;
|
||||
grid-gap: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
right: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
|
||||
.back-top {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: #fff;
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6.25rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reload {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: #fff;
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 0.125rem 0.5rem 0 rgba(0, 0, 0, 0.1),
|
||||
0 0.0625rem 0.125rem 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6.25rem;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
//transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.agree-container {
|
||||
.agree-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-item {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.interaction-content {
|
||||
display: flex;
|
||||
font-size: 0.875rem;
|
||||
color: #333;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.msg-count {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-info {
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quote-info::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-radius: 0.5rem;
|
||||
margin-right: 0.375rem;
|
||||
width: 0.25rem;
|
||||
height: 1.0625rem;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.extra {
|
||||
min-width: 3rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 1.5rem;
|
||||
|
||||
.follow-button {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.reds-button-new.large {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
padding: 0 1.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.reds-button-new.primary {
|
||||
background-color: #ff2e4d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reds-button-new {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
background: none;
|
||||
border: none;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
border-radius: 6.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
145
src/pages/to-up/index.vue
Executable file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div
|
||||
class="note-detail-mask"
|
||||
style="transition: background-color 0.4s ease 0s;
|
||||
hsla(0,0%,100%,0.98)"
|
||||
>
|
||||
<div class="note-container">
|
||||
<div class="bug-info">
|
||||
<el-input v-model="emailVal" placeholder="请输入邮箱" style="margin-bottom: 0.625rem" />
|
||||
<el-input
|
||||
v-model="content"
|
||||
type="textarea"
|
||||
placeholder="有什么想对博主说"
|
||||
:autosize="{ minRows: 5, maxRows: 8 }"
|
||||
style="margin-bottom: 1.25rem"
|
||||
/>
|
||||
<el-button type="primary" @click="submit" style="width: 6.25rem"> 发送 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="close-cricle" @click="close">
|
||||
<div class="close close-mask-white">
|
||||
<Close style="width: 1.2em; height: 1.2em; color: rgba(51, 51, 51, 0.8)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-desk"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { toUp } from "@/api/util";
|
||||
|
||||
const emailVal = ref("");
|
||||
const content = ref("");
|
||||
|
||||
const emit = defineEmits(["clickToUp"]);
|
||||
|
||||
const close = () => {
|
||||
emit("clickToUp");
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const reg = /^([a-zA-Z]|[0-9])(\w|-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
|
||||
if (!reg.test(emailVal.value)) {
|
||||
ElMessage.warning("请输入正确的邮箱");
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.value === "") {
|
||||
ElMessage.warning("请输入内容");
|
||||
return;
|
||||
}
|
||||
|
||||
const dto = {} as any;
|
||||
dto.email = emailVal.value;
|
||||
dto.content = content.value;
|
||||
toUp(dto).then(() => {
|
||||
content.value = "";
|
||||
ElMessage.success("发送成功");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.note-detail-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 20;
|
||||
overflow: auto;
|
||||
|
||||
.back-desk {
|
||||
position: fixed;
|
||||
background-color: #f4f4f4;
|
||||
opacity: 0.5;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.close-cricle {
|
||||
left: 40vw;
|
||||
top: 1.3vw;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
.close-mask-white {
|
||||
box-shadow:
|
||||
0 0.125rem 0.5rem 0 rgba(0, 0, 0, 0.04),
|
||||
0 0.0625rem 0.125rem 0 rgba(0, 0, 0, 0.02);
|
||||
border: 0.0625rem solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 2.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.note-container {
|
||||
margin-left: 38vw;
|
||||
width: 20%;
|
||||
height: 50%;
|
||||
transition:
|
||||
transform 0.4s ease 0s,
|
||||
width 0.4s ease 0s;
|
||||
transform: translate(6.5rem, 2rem) scale(1);
|
||||
overflow: visible;
|
||||
|
||||
display: flex;
|
||||
box-shadow:
|
||||
0 0.5rem 4rem 0 rgba(0, 0, 0, 0.04),
|
||||
0 0.0625rem 0.25rem 0 rgba(0, 0, 0, 0.02);
|
||||
border-radius: 1.25rem;
|
||||
background: #f8f8f8;
|
||||
transform-origin: left top;
|
||||
z-index: 100;
|
||||
|
||||
.bug-info {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
557
src/pages/user/index.vue
Executable file
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<div class="user">
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<div class="avatar-wrapper">
|
||||
<img :src="userInfo.avatar" class="user-image" style="border: 0.0625rem solid rgba(0, 0, 0, 0.08)" />
|
||||
<div class="img-edit">
|
||||
<el-upload
|
||||
v-show="uid === currentUid"
|
||||
class="upload-demo"
|
||||
:action="fileAction"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:headers="uploadHeader"
|
||||
>
|
||||
<button class="btn-avatr">更换</button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-part">
|
||||
<div class="info">
|
||||
<div class="basic-info">
|
||||
<div class="user-basic">
|
||||
<div class="user-nickname">
|
||||
<div class="user-name" v-if="uid === currentUid">
|
||||
<span v-if="!_isEditInfo"> {{ userInfo.username }}</span>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="userInfo.username"
|
||||
style="width: 15rem"
|
||||
maxlength="10"
|
||||
placeholder="Please input"
|
||||
show-word-limit
|
||||
@keyup.enter="confirmUserInfo"
|
||||
type="text"
|
||||
/>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
v-show="!_isEditInfo"
|
||||
@click="_isEditInfo = true"
|
||||
circle
|
||||
size="small"
|
||||
style="margin-left: 0.3125rem"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-name" v-else>
|
||||
{{ userInfo.username }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-content">
|
||||
<span class="user-redId">小红书号:{{ userInfo.hsId }}</span
|
||||
><span class="user-IP"> IP属地:{{ userInfo.address }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-desc">
|
||||
<div v-if="!_isEditInfo">
|
||||
<span v-if="userInfo.description === null">这个人什么都没有写~</span>
|
||||
<span v-else>{{ userInfo.description }}</span>
|
||||
</div>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="userInfo.description"
|
||||
maxlength="250"
|
||||
placeholder="Please input"
|
||||
@keyup.enter="confirmUserInfo"
|
||||
show-word-limit
|
||||
:autosize="true"
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-tags">
|
||||
<el-tag
|
||||
style="margin-left: 0.3125rem"
|
||||
v-for="tag in tagList"
|
||||
:key="tag"
|
||||
:closable="uid === currentUid"
|
||||
:disable-transitions="false"
|
||||
@close="handleClose(tag)"
|
||||
effect="light"
|
||||
type="info"
|
||||
round
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="InputRef"
|
||||
v-model="inputTagValue"
|
||||
style="width: 3.125rem; margin-left: 0.3125rem"
|
||||
size="small"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
/>
|
||||
<el-button
|
||||
style="margin-left: 0.3125rem"
|
||||
v-else
|
||||
class="button-new-tag"
|
||||
size="small"
|
||||
@click="showInput"
|
||||
round
|
||||
v-show="uid === currentUid"
|
||||
>
|
||||
+
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="data-info">
|
||||
<div class="user-interactions">
|
||||
<div>
|
||||
<span class="count">{{ userInfo.trendCount }}</span
|
||||
><span class="shows">作品</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="count">{{ userInfo.followerCount }}</span
|
||||
><span class="shows">关注</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="count">{{ userInfo.fanCount }}</span
|
||||
><span class="shows">粉丝</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="follow"></div>
|
||||
</div>
|
||||
<div class="tool-btn" v-show="uid !== currentUid">
|
||||
<el-button :icon="ChatLineRound" circle @click="chatShow = true" />
|
||||
<el-button type="info" round v-if="_isFollow" @click="follow(uid, 1)">已关注</el-button>
|
||||
<el-button type="danger" round v-else @click="follow(uid, 0)">关注</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reds-sticky-box user-page-sticky" style="--1ee3a37c: all 0.4s cubic-bezier(0.2, 0, 0.25, 1) 0s">
|
||||
<div class="reds-sticky" style="">
|
||||
<div class="tertiary center reds-tabs-list" style="padding: 0rem 0.75rem">
|
||||
<div
|
||||
:class="type == 1 ? 'reds-tab-item active' : 'reds-tab-item'"
|
||||
style="padding: 0rem 1rem; margin-right: 0rem; font-size: 1rem"
|
||||
>
|
||||
<span @click="toPage(1)">笔记</span>
|
||||
</div>
|
||||
<div
|
||||
:class="type == 2 ? 'reds-tab-item active' : 'reds-tab-item'"
|
||||
style="padding: 0rem 1rem; margin-right: 0rem; font-size: 1rem"
|
||||
>
|
||||
<span @click="toPage(2)">点赞</span>
|
||||
</div>
|
||||
<div
|
||||
:class="type == 3 ? 'reds-tab-item active' : 'reds-tab-item'"
|
||||
style="padding: 0rem 1rem; margin-right: 0rem; font-size: 1rem"
|
||||
>
|
||||
<span @click="toPage(3)">收藏</span>
|
||||
</div>
|
||||
<div class="active-tag" style="width: 4rem; left: 39.1875rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feeds-tab-container" style="--1ee3a37c: all 0.4s cubic-bezier(0.2, 0, 0.25, 1) 0s">
|
||||
<Chat
|
||||
v-if="chatShow"
|
||||
:acceptUid="uid"
|
||||
class="animate__animated animate__zoomIn animate__delay-0.5s"
|
||||
@click-chat="chatShow = false"
|
||||
>
|
||||
</Chat>
|
||||
<Note :type="type"> </Note>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ChatLineRound, Edit } from "@element-plus/icons-vue";
|
||||
import { ref, nextTick } from "vue";
|
||||
import { getUserById, updateUser } from "@/api/user";
|
||||
import Note from "@/components/Note.vue";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import Chat from "@/components/Chat.vue";
|
||||
import { followById, isFollow } from "@/api/follower";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElInput, ElMessage, UploadProps } from "element-plus";
|
||||
import { baseURL } from "@/constant/constant";
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const uploadHeader = ref({
|
||||
accessToken: userStore.getToken(),
|
||||
});
|
||||
const currentUid = userStore.getUserInfo().id;
|
||||
const userInfo = ref<any>({});
|
||||
//const uid = history.state.uid;
|
||||
const uid = route.query.uid as string;
|
||||
const type = ref(1);
|
||||
const chatShow = ref(false);
|
||||
const _isFollow = ref(false);
|
||||
const _isEditInfo = ref(false);
|
||||
const tagList = ref<string[]>([]);
|
||||
const inputVisible = ref(false);
|
||||
const InputRef = ref<InstanceType<typeof ElInput>>();
|
||||
const inputTagValue = ref("");
|
||||
const fileAction = baseURL + "web/oss/save/1";
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true;
|
||||
nextTick(() => {
|
||||
InputRef.value!.input!.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = (tag: string) => {
|
||||
tagList.value.splice(tagList.value.indexOf(tag), 1);
|
||||
commonUpdateUser();
|
||||
};
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputTagValue.value) {
|
||||
tagList.value.push(inputTagValue.value);
|
||||
commonUpdateUser();
|
||||
}
|
||||
// _isClosable.value = false;
|
||||
inputVisible.value = false;
|
||||
inputTagValue.value = "";
|
||||
};
|
||||
|
||||
const commonUpdateUser = () => {
|
||||
// 检查标签数量并处理
|
||||
if (tagList.value.length > 4) {
|
||||
tagList.value.splice(4);
|
||||
ElMessage.warning("最多支持4个标签!");
|
||||
return;
|
||||
}
|
||||
// 创建用户DTO对象并赋值
|
||||
let userDTO = {
|
||||
id: userInfo.value.id,
|
||||
avatar: userInfo.value.avatar,
|
||||
username: userInfo.value.username,
|
||||
description: userInfo.value.description,
|
||||
tags: JSON.stringify(tagList.value),
|
||||
};
|
||||
updateUser(userDTO)
|
||||
.then(() => {
|
||||
// 更新用户存储信息
|
||||
ElMessage.success("修改成功~");
|
||||
const user = userStore.getUserInfo();
|
||||
user.avatar = userInfo.value.avatar;
|
||||
userStore.setUserInfo(user);
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("更新失败,请稍后再试!");
|
||||
});
|
||||
};
|
||||
|
||||
const confirmUserInfo = () => {
|
||||
_isEditInfo.value = false;
|
||||
commonUpdateUser();
|
||||
};
|
||||
|
||||
const toPage = (val: number) => {
|
||||
type.value = val;
|
||||
};
|
||||
|
||||
const handleAvatarSuccess: UploadProps["onSuccess"] = (response) => {
|
||||
userInfo.value.avatar = response.data;
|
||||
commonUpdateUser();
|
||||
};
|
||||
|
||||
const follow = (fid: string, type: number) => {
|
||||
followById(fid).then(() => {
|
||||
_isFollow.value = type == 0;
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
getUserById(uid).then((res) => {
|
||||
userInfo.value = res.data;
|
||||
if (res.data.tags != null) {
|
||||
tagList.value = JSON.parse(res.data.tags);
|
||||
}
|
||||
});
|
||||
isFollow(uid).then((res) => {
|
||||
_isFollow.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
initData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.el-button:hover) {
|
||||
background-color: #fff;
|
||||
color: black;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.user-page {
|
||||
background: #fff;
|
||||
height: 100vh;
|
||||
|
||||
.user {
|
||||
padding-top: 4.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 3rem 0;
|
||||
|
||||
.avatar {
|
||||
.avatar-wrapper {
|
||||
text-align: center;
|
||||
width: 15.6667rem;
|
||||
height: 10.9667rem;
|
||||
|
||||
.user-image {
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.btn-avatr {
|
||||
border: 0.0625rem solid #f4f4f4;
|
||||
width: 2.875rem;
|
||||
font-size: 0.75rem;
|
||||
height: 1.75rem;
|
||||
color: #1f1e1e;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.btn-avatr:hover {
|
||||
background-color: #f4f4f4;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-part {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.info {
|
||||
@media screen and (min-width: 108rem) {
|
||||
width: 33.3333rem;
|
||||
}
|
||||
|
||||
margin-left: 2rem;
|
||||
|
||||
.basic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-basic {
|
||||
width: 100%;
|
||||
|
||||
.user-nickname {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 6rem);
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 120%;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.user-content {
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 120%;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.user-redId {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-desc {
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
line-height: 140%;
|
||||
color: #333;
|
||||
margin-top: 1rem;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.user-tags {
|
||||
height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
line-height: 120%;
|
||||
|
||||
.tag-item :first-child {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
grid-gap: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 2.5625rem;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin-right: 0.375rem;
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.15); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
}
|
||||
|
||||
.data-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1.25rem;
|
||||
|
||||
.user-interactions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.count {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.shows {
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
font-size: 0.875rem;
|
||||
line-height: 120%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-interactions > div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow {
|
||||
position: absolute;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.tool-btn {
|
||||
@media screen and (min-width: 108rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
@media screen and (min-width: 90.3958rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
@media screen and (min-width: 109.375rem) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reds-sticky {
|
||||
padding: 1rem 0;
|
||||
z-index: 5 !important;
|
||||
background: hsla(0, 0%, 100%, 0.98);
|
||||
|
||||
.reds-tabs-list {
|
||||
@media screen and (min-width: 108rem) {
|
||||
width: 90.3333rem;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
justify-content: center;
|
||||
|
||||
.reds-tab-item {
|
||||
padding: 0rem 1rem;
|
||||
margin-right: 0rem;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 2.5rem;
|
||||
cursor: pointer;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
white-space: nowrap;
|
||||
transition: transform 0.3s cubic-bezier(0.2, 0, 0.25, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer; /* 显示小手指针 */
|
||||
transform: scale(1.15); /* 鼠标移入时按钮稍微放大 */
|
||||
}
|
||||
|
||||
.reds-tab-item.active {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feeds-tab-container {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
62
src/router/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Login from "@/pages/login.vue";
|
||||
import Dashboard from "@/pages/dashboard/dashboard.vue";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/index",
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
path: "/login",
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
path: "/index",
|
||||
component: () => import("@/pages/index.vue"),
|
||||
redirect: "/dashboard",
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard",
|
||||
component: Dashboard,
|
||||
name: "dashboard",
|
||||
},
|
||||
{
|
||||
path: "/followTrend",
|
||||
component: () => import("@/pages/follow-trend/follow-trend.vue"),
|
||||
name: "followTrend",
|
||||
},
|
||||
{
|
||||
path: "/notice",
|
||||
component: () => import("@/pages/message/index.vue"),
|
||||
name: "notice",
|
||||
},
|
||||
{
|
||||
path: "/user",
|
||||
component: () => import("@/pages/user/index.vue"),
|
||||
name: "user",
|
||||
},
|
||||
{
|
||||
path: "/push",
|
||||
component: () => import("@/pages/push/index.vue"),
|
||||
name: "push",
|
||||
},
|
||||
{
|
||||
path: "/search",
|
||||
component: () => import("@/pages/search/index.vue"),
|
||||
name: "search",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
router.beforeEach((to, from, next) => {
|
||||
next();
|
||||
});
|
||||
export default router;
|
36
src/store/imStore.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { store } from "@/store";
|
||||
|
||||
// 使用setup模式定义
|
||||
export const imStore = defineStore("imStore", () => {
|
||||
const userList = ref<Array<any>>([]);
|
||||
|
||||
const message = ref<any>({});
|
||||
|
||||
const countMessage = ref({
|
||||
chatCount: 0,
|
||||
likeOrCollectionCount: 0,
|
||||
commentCount: 0,
|
||||
followCount: 0,
|
||||
});
|
||||
|
||||
|
||||
const setUserList = (data: Array<any>) => {
|
||||
userList.value = data;
|
||||
};
|
||||
|
||||
const setCountMessage = (data: any) => {
|
||||
countMessage.value = data;
|
||||
};
|
||||
|
||||
const setMessage = (data: any) => {
|
||||
message.value = data;
|
||||
};
|
||||
|
||||
return { userList, countMessage, message, setUserList, setCountMessage, setMessage };
|
||||
});
|
||||
|
||||
export function useImStore() {
|
||||
return imStore(store);
|
||||
}
|
11
src/store/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
69
src/store/searchStore.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { storage } from "@/utils/storage";
|
||||
import { store } from "@/store";
|
||||
|
||||
// 使用setup模式定义
|
||||
export const searchStore = defineStore("searchStore", () => {
|
||||
const keyWord = ref("");
|
||||
const seed = ref("");
|
||||
const historyRecords = ref<Array<string>>([]);
|
||||
|
||||
const setKeyword = (val: string) => {
|
||||
keyWord.value = val;
|
||||
storage.set("keyword", val);
|
||||
};
|
||||
|
||||
const setSeed = (val: string) => {
|
||||
seed.value = val;
|
||||
storage.set("seed", val);
|
||||
};
|
||||
|
||||
const pushRecord = (val: string) => {
|
||||
if (storage.get("historyRecords") == null) {
|
||||
storage.set("historyRecords", []);
|
||||
}
|
||||
let data = storage.get("historyRecords") as Array<string>;
|
||||
data = data.filter((item) => item != val);
|
||||
data.splice(0, 0, val);
|
||||
if (data.length > 10) {
|
||||
data = data.slice(0, data.length - 1);
|
||||
}
|
||||
storage.set("historyRecords", data);
|
||||
};
|
||||
|
||||
const setRecords = (val: Array<string>) => {
|
||||
storage.set("historyRecords", val);
|
||||
};
|
||||
|
||||
const getRecords = () => {
|
||||
return storage.get("historyRecords") as Array<string>;
|
||||
};
|
||||
|
||||
const clearAllRecord = () => {
|
||||
storage.set("historyRecords", []);
|
||||
};
|
||||
|
||||
const deleteRecord = (index: number) => {
|
||||
historyRecords.value = storage.get("historyRecords") as Array<string>;
|
||||
historyRecords.value.splice(index, 1);
|
||||
console.log(historyRecords.value);
|
||||
storage.set("historyRecords", historyRecords.value);
|
||||
};
|
||||
|
||||
return {
|
||||
keyWord,
|
||||
seed,
|
||||
setKeyword,
|
||||
pushRecord,
|
||||
deleteRecord,
|
||||
clearAllRecord,
|
||||
getRecords,
|
||||
setSeed,
|
||||
setRecords,
|
||||
};
|
||||
});
|
||||
|
||||
export function useSearchStore() {
|
||||
return searchStore(store);
|
||||
}
|
50
src/store/userStore.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { storage } from "@/utils/storage";
|
||||
import { refreshToken } from "@/api/user";
|
||||
import { store } from "@/store";
|
||||
import type { User } from "@/type/user";
|
||||
|
||||
// 使用setup模式定义
|
||||
export const userStore = defineStore("userStore", () => {
|
||||
const token = ref("");
|
||||
|
||||
const getToken = () => {
|
||||
return storage.get("accessToken");
|
||||
};
|
||||
|
||||
const getUserInfo = (): User => {
|
||||
return storage.get("userInfo") as User;
|
||||
};
|
||||
|
||||
const setUserInfo = (data: User) => {
|
||||
storage.set("userInfo", data);
|
||||
};
|
||||
|
||||
const isLogin = () => {
|
||||
const user = storage.get("userInfo") as User;
|
||||
return user != null && user != undefined;
|
||||
};
|
||||
|
||||
const getNewToken = (token: string) => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
refreshToken(token)
|
||||
.then((res) => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const loginOut = () => {
|
||||
window.localStorage.clear();
|
||||
};
|
||||
|
||||
return { token, getToken, getNewToken, getUserInfo, setUserInfo, loginOut, isLogin };
|
||||
});
|
||||
|
||||
export function useUserStore() {
|
||||
return userStore(store);
|
||||
}
|
39
src/style.css
Normal file
@@ -0,0 +1,39 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-user-select: auto;
|
||||
user-select: auto;
|
||||
scrollbar-width: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
button, input, optgroup, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li{
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
|
||||
button, input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button, input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
6
src/type/category.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Category {
|
||||
id: string,
|
||||
title: string,
|
||||
pid: string,
|
||||
likeCount: number;
|
||||
}
|
30
src/type/comment.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface CommentDTO {
|
||||
nid: string;
|
||||
noteUid: string;
|
||||
pid: string;
|
||||
replyId: string;
|
||||
replyUid: string;
|
||||
replyUsername: string;
|
||||
level: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
pid: string;
|
||||
nid: string;
|
||||
noteCover: string;
|
||||
uid: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
replyId: string;
|
||||
replyUid: string;
|
||||
replyUsername: string;
|
||||
content: string;
|
||||
replyContent: string;
|
||||
time: number;
|
||||
likeCount: number;
|
||||
isLike: boolean;
|
||||
twoCommentCount: number;
|
||||
children: Array<Comment>;
|
||||
}
|
5
src/type/likeOrCollection.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface LikeOrCollectionDTO {
|
||||
likeOrCollectionId: string;
|
||||
publishUid: string;
|
||||
type: number;
|
||||
}
|
43
src/type/note.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface NoteSearch {
|
||||
id: string;
|
||||
title: string;
|
||||
src: string;
|
||||
uid: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
cid: string;
|
||||
cpid: string;
|
||||
urls: string;
|
||||
count: number;
|
||||
type: number;
|
||||
likeCount: number;
|
||||
time: number | string;
|
||||
}
|
||||
|
||||
export interface NoteInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
noteCover: string;
|
||||
uid: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
imgList: Array<string>;
|
||||
type: number;
|
||||
likeCount: number;
|
||||
collectionCount: number;
|
||||
commentCount: number;
|
||||
tagList: Array<any>;
|
||||
time: string;
|
||||
isFollow: boolean,
|
||||
isLike: boolean,
|
||||
isCollection: boolean,
|
||||
pinned: string
|
||||
}
|
||||
|
||||
export interface NoteDTO {
|
||||
keyword: string;
|
||||
type: number;
|
||||
cid: string;
|
||||
cpid: string;
|
||||
}
|
22
src/type/user.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface UserLogin {
|
||||
phone: string;
|
||||
email: string;
|
||||
code: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
yxId: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
gender: number;
|
||||
description: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
userCover: string;
|
||||
trendCount: number;
|
||||
followerCount: number;
|
||||
fanCount: number;
|
||||
}
|
97
src/utils/request.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import { storage } from "./storage";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { baseURL } from "@/constant/constant";
|
||||
// 刷新 token 后, 将缓存的接口重新请求一次
|
||||
// 是否正在刷新 token
|
||||
let isRefreshing: boolean = false;
|
||||
|
||||
// 存储待重发的请求
|
||||
let requestsQueue: ((token: string) => any)[] = [];
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||
// baseURL: baseURL,
|
||||
timeout: 50000,
|
||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const userStore = useUserStore();
|
||||
if (userStore.getToken()) {
|
||||
config.headers.accessToken = userStore.getToken();
|
||||
config.headers.userId = userStore.getUserInfo().id;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: any) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { code } = response.data;
|
||||
if (code === 200) {
|
||||
return response.data;
|
||||
}
|
||||
// 响应数据为二进制流处理(Excel导出)
|
||||
// if (response.data instanceof ArrayBuffer) {
|
||||
// return response;
|
||||
// }
|
||||
const config = response.config;
|
||||
|
||||
const userStore = useUserStore();
|
||||
if (code === 501) {
|
||||
// 无感刷新Token
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
storage.set("accessToken", "");
|
||||
const refreshToken = storage.get("refreshToken") as string;
|
||||
console.log("refreshToken", refreshToken);
|
||||
return userStore
|
||||
.getNewToken(refreshToken)
|
||||
.then(async (rftRes) => {
|
||||
console.log("rftRes", rftRes);
|
||||
const { accessToken, refreshToken } = rftRes.data;
|
||||
storage.set("accessToken", accessToken);
|
||||
storage.set("refreshToken", refreshToken);
|
||||
config.headers.accessToken = accessToken;
|
||||
// 重新请求一下第一个 501 的接口
|
||||
const firstReqRes = await service.request(config);
|
||||
// token 刷新后将数组的方法重新执行
|
||||
requestsQueue.forEach((cb: any) => cb(accessToken));
|
||||
// 队列中的请求执行完毕后,清空数组
|
||||
requestsQueue = [];
|
||||
return firstReqRes;
|
||||
}).finally(() => {
|
||||
isRefreshing = false;
|
||||
});
|
||||
} else {
|
||||
// 如果正在 refreshToken
|
||||
|
||||
// 如果有多个请求同时发起,第一个请求 401 了,refreshToken 又正在进行中
|
||||
// 那么把第一个以外的请求暂存起来
|
||||
return new Promise((resolve) => {
|
||||
// 用函数形式将 resolve 存入,等待 refreshToken 完成后再执行
|
||||
requestsQueue.push((token: string) => {
|
||||
config.headers.Authorization = token;
|
||||
resolve(service.request(config));
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (code == 401) {
|
||||
ElMessage.error("登录过期,请重新登录");
|
||||
window.localStorage.clear();
|
||||
}
|
||||
return Promise.reject(response.data);
|
||||
}
|
||||
);
|
||||
|
||||
// 导出 axios 实例
|
||||
export default service;
|
42
src/utils/storage.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 封装操作localstorage本地存储的方法
|
||||
*/
|
||||
export const storage = {
|
||||
//存储
|
||||
set(key: string, value: any) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
//取出数据
|
||||
get<T>(key: string) {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value && value != "undefined" && value != "null") {
|
||||
return <T>JSON.parse(value);
|
||||
}
|
||||
},
|
||||
// 删除数据
|
||||
remove(key: string) {
|
||||
localStorage.removeItem(key);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 封装操作sessionStorage本地存储的方法
|
||||
*/
|
||||
export const sessionStorage = {
|
||||
//存储
|
||||
set(key: string, value: any) {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
//取出数据
|
||||
get(key: string) {
|
||||
const value = window.sessionStorage.getItem(key);
|
||||
if (value && value != "undefined" && value != "null") {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 删除数据
|
||||
remove(key: string) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
},
|
||||
};
|
167
src/utils/util.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
|
||||
|
||||
import axios from 'axios'
|
||||
export const formatDate = (t: number): string => {
|
||||
t = t || Date.now();
|
||||
const time = new Date(t);
|
||||
let str = time.getFullYear() as any;
|
||||
str += "-";
|
||||
str = time.getMonth() < 9 ? "0" + (time.getMonth() + 1) : (time.getMonth() + 1);
|
||||
str += "-";
|
||||
str += time.getDate() < 10 ? "0" + time.getDate() : time.getDate();
|
||||
str += " ";
|
||||
str += time.getHours();
|
||||
str += ":";
|
||||
str += time.getMinutes() < 10 ? "0" + time.getMinutes() : time.getMinutes();
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* 距当前时间点的时长
|
||||
* @prama time 13位时间戳
|
||||
* @return str x秒 / x分钟 / x小时
|
||||
*/
|
||||
export const formateTime = (time: number): string => {
|
||||
const second = 1000;
|
||||
const minute = second * 60;
|
||||
const hour = minute * 60;
|
||||
const day = hour * 24;
|
||||
const month = day * 30;
|
||||
const now = new Date().getTime();
|
||||
const diffValue = now - time;
|
||||
|
||||
// 计算差异时间的量级
|
||||
const secondC = diffValue / second;
|
||||
const minC = diffValue / minute;
|
||||
const hourC = diffValue / hour;
|
||||
const dayC = diffValue / day;
|
||||
const monthC = diffValue / month;
|
||||
|
||||
if (monthC > 3) {
|
||||
return formatDate(time);
|
||||
} else if (monthC >= 1 && monthC <= 3) {
|
||||
return parseInt(monthC.toString()) + "月前";
|
||||
} else if (dayC >= 1) {
|
||||
return parseInt(dayC.toString()) + "天前";
|
||||
} else if (hourC >= 1) {
|
||||
return parseInt(hourC.toString()) + "小时前";
|
||||
} else if (minC >= 1) {
|
||||
return parseInt(minC.toString()) + "分钟前";
|
||||
} else if (secondC >= 1) {
|
||||
return parseInt(secondC.toString()) + "秒前";
|
||||
} else {
|
||||
return "刚刚";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机生成字符串
|
||||
* @param len 指定生成字符串长度
|
||||
*/
|
||||
export const getRandomString = (len: number) => {
|
||||
const _charStr = "abacdefghjklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789",
|
||||
min = 0,
|
||||
max = _charStr.length - 1;
|
||||
let _str = ""; //定义随机字符串 变量
|
||||
//判断是否指定长度,否则默认长度为15
|
||||
len = len || 15;
|
||||
//循环生成字符串
|
||||
for (let i = 0, index; i < len; i++) {
|
||||
index = (function (randomIndexFunc, i) {
|
||||
return randomIndexFunc(min, max, i, randomIndexFunc);
|
||||
})(function (min: any, max: any, i: any, _self: any) {
|
||||
let indexTemp = Math.floor(Math.random() * (max - min + 1) + min);
|
||||
const numStart = _charStr.length - 10;
|
||||
if (i == 0 && indexTemp >= numStart) {
|
||||
indexTemp = _self(min, max, i, _self);
|
||||
}
|
||||
return indexTemp;
|
||||
}, i);
|
||||
_str += _charStr[index];
|
||||
}
|
||||
return _str;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getFileFromUrl = async (url: string, fileName: string) => {
|
||||
try {
|
||||
// 第一步:使用axios获取网络图片数据
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' })
|
||||
// 第二步:将图片数据转换为Blob对象
|
||||
const blob = new Blob([response.data], {
|
||||
type: response.headers['content-type']
|
||||
})
|
||||
// 第三步:创建一个新的File对象
|
||||
const file = new File([blob], fileName, {
|
||||
type: response.headers['content-type']
|
||||
})
|
||||
return file
|
||||
} catch (error) {
|
||||
console.error('将图片转换为File对象时发生错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到html标签中的内容
|
||||
* @param content
|
||||
*/
|
||||
export const getHtmlContent = (html: string) => {
|
||||
const pattern = /<[a-z]+[1-6]?\b[^>]*>(.*?)<\/[a-z]+[1-6]?>/g;
|
||||
const res = [];
|
||||
let match;
|
||||
while ((match = pattern.exec(html)) !== null) {
|
||||
const content = match[1].trim();
|
||||
res.push(content);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 滚动平滑导航栏
|
||||
*/
|
||||
export const refreshTab = (f: any) => {
|
||||
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const clientHeight =
|
||||
window.innerHeight || Math.min(document.documentElement.clientHeight, document.body.clientHeight);
|
||||
|
||||
if (scrollTop <= clientHeight * 2) {
|
||||
const timeTop = setInterval(() => {
|
||||
document.documentElement.scrollTop = document.body.scrollTop = scrollTop -= 100;
|
||||
if (scrollTop <= 0) {
|
||||
clearInterval(timeTop);
|
||||
f();
|
||||
}
|
||||
}, 10); //定时调用函数使其更顺滑
|
||||
} else {
|
||||
document.documentElement.scrollTop = 0;
|
||||
f();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将图片转成base64格式
|
||||
*
|
||||
* @param imageFile 图片文件
|
||||
* @param callback 转成成功函数回调(这里是接收转换成功结果的函数)
|
||||
* @param errorCallback 转成失败函数回调(这里是接收转换失败结果的函数)
|
||||
*/
|
||||
export const convertImgToBase64 = (imageFile: any, callback: any, errorCallback: any) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onload = function (e) {
|
||||
if (callback) {
|
||||
const base64Str = e.target!.result;
|
||||
callback(base64Str);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const vueComponent: DefineComponent<{}, {}, any>;
|
||||
export default vueComponent;
|
||||
}
|
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
49
vite.config.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
import { resolve } from "path";
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "./src"),
|
||||
},
|
||||
//extensions: [".ts", ".js", ".vue", ".json", ".mjs"],
|
||||
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
|
||||
},
|
||||
// server: {
|
||||
// port: 3000,
|
||||
// open: true,
|
||||
// // 配置代理
|
||||
// proxy: {
|
||||
// // 请求的路径前缀只要是 /testaxios 就会被拦截走这个代理
|
||||
// '/oss': {
|
||||
// /**
|
||||
// * 请求的目标资源再经过替换成 /httphwm/getList 后,
|
||||
// * 会加上 http://127.0.0.1:9693 这个前缀,
|
||||
// * 最后请求的URL为: http://127.0.0.1:9693/httphwm/getList
|
||||
// */
|
||||
// target: 'http://localhost:8080/',
|
||||
// ws: true,
|
||||
// changeOrigin: true,
|
||||
// // 拦截到的请求路径 testaxios/httphwm/getList,/testaxios会被替换成空
|
||||
// rewrite: (path) => path.replace(/^\/oss/, 'oss'),
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
server: {
|
||||
port: 80,
|
||||
host: true,
|
||||
open: true,
|
||||
proxy: {
|
||||
// https://cn.vitejs.dev/config/#server-proxy
|
||||
'/dev-api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|