hongshu-web v1.0
This commit is contained in:
451
src/components/Chat.vue
Executable file
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
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
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
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
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
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
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>
|
Reference in New Issue
Block a user