hongshu-web v1.0

This commit is contained in:
mayongjian
2024-07-03 16:56:33 +08:00
parent 4fbf472ba0
commit 05f8f39dde
93 changed files with 29139 additions and 71 deletions

451
src/components/Chat.vue Executable file
View 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
View 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>

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

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

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