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

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

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

File diff suppressed because one or more lines are too long

748
src/pages/main/main.vue Normal file
View 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>

View 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>
&nbsp;<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>评论了您的笔记&nbsp;</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>

View 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>开始关注您了&nbsp;</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>

View 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>
&nbsp;<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>

View 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
View 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
View 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
View 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
>&nbsp;|&nbsp;<span>关注:&nbsp;</span><span>{{ item.followerCount }}</span
>&nbsp;|&nbsp;<span>作品:&nbsp;</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
View 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
View 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>