<template> <Head> <Title>{{ `${seo["title"] || "投票"} - 寄托天下出国留学网` }}</Title> <Meta name="keyword" :content="seo['keyword']" /> <Meta name="description" :content="seo['description']" /> </Head> <TopHead ref="topHeadRef"></TopHead> <div class="content flexflex" :style="{ '--main-color': colourValue[uniqidIndex]['main'], '--bg-color': colourValue[uniqidIndex]['bg'], '--bc-color': colourValue[uniqidIndex]['bc'] }"> <div class="header flexacenter"> <span>{{ info.title }}</span> </div> <div class="left"> <div class="info flexacenter"> <div class="info-left flexacenter"> <el-popover placement="bottom-start" :width="140" trigger="click" popper-class="avatar-box-popper" :show-arrow="false"> <template #reference> <div class="flexcenter"> <img class="avatar" :src="info.avatar" /> <div class="username">{{ info.nickname }}</div> </div> </template> <div class="avatar-box flexflex" v-if="info['uin'] || info['uid']"> <a class="avatar-item flexcenter" target="_blank" @click.prevent="sendMessage(info['uin'], info['uid'])"> <img class="avatar-icon" src="@/assets/img/send-messages-icon.png" /> 发送信息 </a> <a class="avatar-item flexcenter" target="_blank" @click.prevent="TAHomePage(info['uin'], info['uid'])"> <img class="avatar-icon" src="@/assets/img/homepage-icon.png" /> TA的主页 </a> </div> </el-popover> <div class="post-time" v-if="info.releasetime">{{ handleDate(info.releasetime) }}发布</div> </div> <div class="info-right flexacenter" v-if="info['status'] == 1"> <div class="cut-off">{{ handleDeadline(info.deadline) }}结束</div> <div class="state">进行中</div> </div> <div class="info-right flexacenter" v-else> <div class="cut-off" v-if="info.deadline">已于{{ info.deadline }}结束</div> <div class="state over">已结束</div> </div> </div> <div class="message">{{ info.message }}</div> <div class="hint">{{ info.status == 1 && isvote == 0 ? `已有 ${info.votes || 0} 人参与,` : `共有 ${info.votes || 0} 人参与` }} {{ `${isvote == 1 ? "你已投票" : info.status == 1 ? "参与投票即可查看实时结果" : ""}` }}</div> <ClientOnly> <template v-if="info['status'] == 0 && isNeedLogin"> <div class="option-list flexflex"> <div class="option-item flexflex" v-for="(item, index) in option" :key="item.id"> <div class="serial flexcenter">{{ index + 1 }}</div> <span class="flex1">{{ item.value }} </span> <div class="result">投票结果</div> </div> </div> <div class="need-login flexcenter"> 投票已结束, <div class="btn flexcenter" @click="goLogin">登录</div> 后可以查看投票结果 </div> </template> <div class="option-list flexflex" v-else-if="info['status'] == 1 && isvote == 0"> <div class="option-item flexflex" v-for="(item, index) in option" :key="item.id" @click="handleVote(item.id, index)"> <div class="serial flexcenter">{{ index + 1 }}</div> <span class="flex1">{{ item.value }} </span> </div> </div> <div class="option-area" v-else> <div class="option-item flexflex unselected" :class="{ pitch: item.selected, 'cursor-no': info.status == 0 }" v-for="(item, index) in option" :key="item.id" @click="handleUnvoteVote(index, item.selected)"> <div class="flexflex" style="padding: 2px 0px"> <div class="option-number flexcenter">{{ index + 1 }}</div> <img class="tick-icon" src="@/assets/img/tick-black.svg" /> <div class="option-content flex1">{{ item.value }}</div> </div> <div class="option-progress flexacenter"> <div class="option-progress-step" :style="{ width: item.percentage + '%' }"></div> <div class="option-progress-value">{{ item.count }}</div> </div> </div> </div> </ClientOnly> </div> <div class="right"> <template v-if="false"> <div class="respond" v-if="riposteoptions.length != 0"> <div class="respond-title flexacenter"> 回应 <div class="respond-amount">{{ ripostecount.total || 0 }}</div> <div v-if="ripostecount.user > 0" class="respond-list-btn" @click="openPopList"> 共 <span class="respond-list-btn-amount">{{ ripostecount.user }}</span >人回应 <img class="respond-list-btn-icon" src="@/assets/img/arrowsRight.svg" /> </div> </div> <div v-if="ripostelist.length == 0" class="respond-no flexflex"> <div class="respond-no-box flex1 flexflex"> <div class="item" v-for="item in randomEmojis" :key="item" v-html="jointriposte(item)" @click="selectEomji(item)"></div> </div> <RespondAdd></RespondAdd> </div> <div v-else class="respond-box"> <div class="item flexacenter" :class="{ pitch: item.selected }" v-for="(item, index) in ripostelist" :key="item" @click="selectListEomji(index)"> <div class="code flexacenter" v-html="jointriposte(item.item)"></div> {{ item.num }} </div> <div v-if="ripostelist.length < 3" class="respond-select flexflex"> <div class="respond-select-box flex1 flexflex"> <template v-for="(item, index) in randomEmojis" :key="item"> <div v-if="index < 5" class="respond-select-item" v-html="jointriposte(item)" @click="selectEomji(item)"></div> </template> </div> <RespondAdd></RespondAdd> </div> <RespondAdd v-else></RespondAdd> </div> </div> </template> <DetailsComments ref="commentsRef" :token="token" @update:commentComments="commentComments = $event"></DetailsComments> </div> <DetailsArea @closeDiscussInputFields="closeDiscussInputFields" :ripostecount="ripostecount" :commentComments="commentComments"></DetailsArea> </div> <el-dialog class="default-popup options-popup" v-model="cancelPopoverState" width="488px" align-center> <div class="options-popup-text">您要取消投票吗?</div> <div class="options-popup-btn flexflex"> <div class="options-popup-item options-no flexcenter" @click="unvoteVote">取消投票</div> <div class="options-popup-item options-yes flexcenter" @click="cancelPopoverState = false">不取消</div> </div> </el-dialog> <div class="respond-pop-mask" v-if="respondPopListState"> <div class="respond-pop"> <div class="respond-pop-no" v-if="JSON.stringify(respondDetail) == '{}'"> <img class="respond-title-icon" @click="closePopList()" src="@/assets/img/cross-grey.png" /> <img src="@/assets/img/no-discussion.png" class="respond-pop-no-icon" /> <div class="respond-pop-no-text">- 暂无数据 -</div> </div> <template v-else> <div class="respond-pop-title"> 共<span class="respond-pop-amount">{{ ripostecount.user }}</span >人回应 <img class="respond-title-icon" @click="closePopList()" src="@/assets/img/cross-grey.png" /> </div> <div class="respond-list"> <div class="respond-item" v-for="(item, index) in respondDetail" :key="index"> <div class="respond-code" :class="{ pitch: item.selected }" v-html="jointriposte(item.item)" @click="selectEomjiListPop(item.item)"></div> <div class="respond-content flex1"> <div class="respond-total">{{ item.user.length }} 人作此回应</div> <div class="user-item" v-for="(item, index) in item.user" :key="index" @click="TAHomePage(item['uin'])"> <img class="user-avatar" :src="item.avatar" /> {{ item.nickname || item.username }} </div> </div> </div> </div> </template> </div> </div> </template> <script setup> useHead({ script: [{ src: "https://app.gter.net/bottom?tpl=header&menukey=vote" }, { src: "https://app.gter.net/bottom?tpl=footer,popupnotification", body: true }] }); import { useRoute, useRouter } from "vue-router"; import { ElMessage } from "element-plus"; const route = useRoute(); const router = useRouter(); let isNeedLogin = inject("isNeedLogin"); const goLogin = inject("goLogin"); let commentComments = ref(0); let id = route.params.id; let uniqidIndex = ref(0); if (route.query.colorI) uniqidIndex.value = route.query.colorI; else uniqidIndex.value = Math.floor(Math.random() * 6); if (uniqidIndex.value > 6) uniqidIndex = 0; onMounted(() => { getDetails(); clearBottom(); }); let ripostelist = ref([]); let ripostecount = ref({}); let riposteoptions = ref([]); provide("riposteoptions", riposteoptions); const getRiposte = () => { getRiposteHttp({ token: token.value }).then((res) => { if (res.code != 200) return; let data = res.data; ripostecount.value = data.count || {}; ripostelist.value = data.list || []; riposteoptions.value = data.options || []; if (ripostelist.value.length <= 3) randomEmoji(); randomBottomEmoji(); }); }; let randomEmojis = ref([]); // 随机 五个 emoji let randomBottomEmojis = ref([]); // 随机 8个 emoji provide("randomEmojis", randomEmojis); provide("randomBottomEmojis", randomBottomEmojis); // 随机 7 个Emoji const randomEmoji = () => { let emojiList = ripostelist.value; // 需要排除的 Emoji let exclude = []; emojiList.forEach((element) => { exclude.push(element.item); }); let selectedList = []; // 待选择 Emoji To be selected // 默认是有点赞的 for (const key in riposteoptions.value[0].data) { if (key != "c150") selectedList.push(key); } const random = []; if (!exclude.includes("c150")) random.push("c150"); // 添加第一个点赞 emoji selectedList = selectedList.filter((itemB) => !exclude.includes(itemB)); // 生成随机索引,确保不重复 let indexes = []; while (indexes.length < 7) { let randomIndex = Math.floor(Math.random() * selectedList.length); if (indexes.indexOf(randomIndex) === -1) { indexes.push(randomIndex); random.push(selectedList[randomIndex]); } } randomEmojis.value = random; }; const randomBottomEmoji = () => { let selectedList = []; // 待选择 Emoji To be selected // 默认是有点赞的 for (const key in riposteoptions.value[0].data) { selectedList.push(key); } // 打乱数组顺序 selectedList.sort(() => Math.random() - 0.5); const randomItems = selectedList.slice(0, 8); randomBottomEmojis.value = randomItems; }; // 拼接 回应需要的 字符 const jointriposte = (item) => { return `&#x${item};`; }; provide("jointriposte", jointriposte); // 选择回应 const selectListEomji = (index) => { if (isNeedLogin.value) { goLogin(); return; } let emojiList = ripostelist.value; let target = emojiList[index]; if (riposteHttpState) return; riposteHttpState = true; riposteSubmitHttp({ token: token.value, item: target.item }) .then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; handleEmojiData(data); }) .finally(() => { riposteHttpState = false; }); }; let riposteHttpState = false; // 回应加载中 // 选择 emoji const selectEomji = (item) => { if (isNeedLogin.value) { goLogin(); return; } if (riposteHttpState) return; riposteHttpState = true; riposteSubmitHttp({ token: token.value, item }) .then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; handleEmojiData(data); }) .finally(() => { riposteHttpState = false; }); }; provide("selectEomji", selectEomji); // 选中 在 Emoji 弹窗中 选择 const selectEomjiPop = (key) => { if (isNeedLogin.value) { goLogin(); return; } let emojiList = ripostelist.value; // 判断 是否已经 有了 const index = emojiList.findIndex((item) => item.item == key); if (index != -1 && emojiList[index].selected) return; if (riposteHttpState) return; riposteHttpState = true; riposteSubmitHttp({ token: token.value, item: key }) .then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; handleEmojiData(data); }) .finally(() => { riposteHttpState = false; }); }; provide("selectEomjiPop", selectEomjiPop); // 专门处理 展示列表的 数据结构 const handleEmojiData = (data) => { let emojiList = ripostelist.value; let isnew = true; emojiList.forEach((element, index) => { if (element.item == data.item) { isnew = false; if (element.selected) element.num--; else element.num++; element.selected = !element.selected; } }); // 代表是新数据 if (isnew) { emojiList.push({ item: data.item, num: 1, selected: true, }); } let newArray = []; emojiList.forEach((item) => { if (item.num > 0) newArray.push(item); }); if (newArray.length < 3) randomEmoji(); ripostecount.value = data.count; ripostelist.value = newArray; }; let info = ref({}); let qrcode = ref(""); // 分享二维码 let iscollection = ref(0); // 是否收藏 let islike = ref(0); // 是否点赞 let ismyself = ref(0); // 是否是作者 let detailsLoading = ref(false); // 详情加载中 let isvote = ref(0); // 是否已经投票 let option = ref([]); let token = ref(""); let cancelPopoverState = ref(false); // 取消投票弹窗 let isLoaded = ref(false); // 是否加载了 let haveVotedValue = ref(""); // 已投的值 provide("info", info); provide("islike", islike); provide("iscollection", iscollection); provide("token", token); provide("qrcode", qrcode); provide("isLoaded", isLoaded); provide("haveVotedValue", haveVotedValue); const getDetails = async () => { detailsHttp({ uniqid: id }).then((res) => { if (res.code != 200) { ElMessage.error(res.message); goToURL("/index.html", false); return; } let data = res.data; info.value = data["info"]; isvote.value = data["isvote"]; iscollection.value = data["iscollection"]; islike.value = data["islike"]; ismyself.value = data["ismyself"]; option.value = data["option"]; qrcode.value = data.share?.qrcode; token.value = data["token"]; seo.value = data.seo; isLoaded.value = true; data["option"].forEach((element) => { if (element.selected) haveVotedValue.value = element.value; }); getRiposte(); }); }; provide("getDetails", getDetails); // 点击发送信息 const sendMessage = (uin) => { if (uin && typeof messagePrivateItem == "function") { messagePrivateItem({ uin: uin }); } else redirectToExternalWebsite(`https://bbs.gter.net/home.php?mod=space&showmsg=1&uid=${uin}`); }; // 点击ta的主页 const TAHomePage = (uin) => { redirectToExternalWebsite(`https://bbs.gter.net/home.php?mod=space&uid=${uin}`); }; // 跳转 url const redirectToExternalWebsite = (url) => { const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.click(); }; provide("sendMessage", sendMessage); provide("TAHomePage", TAHomePage); const commentsRef = ref(null); let voteLoading = false; // 处理点击投票的中转 const handleVotesTransfer = (index) => { const target = option.value[index]; if (info.value.status == 1 && isvote.value == 0) handleVote(target.id, index); else handleUnvoteVote(index); }; // 处理点击投票 const handleVote = (token, index) => { if (isNeedLogin.value) { goLogin(); return; } if (voteLoading) return; voteLoading = true; topHeadRef.value.count = {}; operationCollectHttp({ token }) .then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; let optionList = data["optionList"] || []; optionList.forEach((element) => { element["selected"] = 0; }); optionList[index]["selected"] = 1; option.value = optionList; isvote.value = 1; info.value.votes = data["votes"]; const value = optionList[index]["value"]; haveVotedValue.value = value; commentsRef.value.changeCommentVoteoption(value); ElMessage.success(res.message); if (index != optionList.length - 1) commentsRef.value.reviewsComment(optionList[index]["value"]); }) .finally(() => (voteLoading = false)); }; let unvoteVoteIndex = null; // 选项下标 // 点击 取消投票 const handleUnvoteVote = (index, selected) => { if (isNeedLogin.value) { goLogin(); return; } if (selected == 0 || info.value.status == 0) return; cancelPopoverState.value = true; unvoteVoteIndex = index; }; const unvoteVote = () => { if (isNeedLogin.value) { goLogin(); return; } const token = option.value[unvoteVoteIndex].id; if (voteLoading) return; voteLoading = true; topHeadRef.value.count = {}; unvoteCollectHttp({ token }) .then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; let optionList = data["optionList"] || []; optionList.forEach((element) => { element["selected"] = 0; }); option.value = optionList; isvote.value = 0; info.value.votes = data["votes"]; cancelPopoverState.value = false; commentsRef.value.wipeCommentVoteoption(); }) .finally(() => (voteLoading = false)); }; const clearAllData = () => { info.value = {}; qrcode.value = ""; iscollection.value = 0; islike.value = 0; ismyself.value = 0; isvote.value = 0; option.value = []; }; provide("clearAllData", clearAllData); // 取消了同页面的收藏 const unbookmarkSamePage = () => { iscollection.value = 0; info.value.favs--; }; provide("unbookmarkSamePage", unbookmarkSamePage); // 删除同页面的投票需要跳转到 首页 const unbookmark = () => router.push("/index.html"); provide("unbookmark", unbookmark); let seo = ref({}); // 清除底部的次数 let clearBottomCount = 0; // 清除 底部 const clearBottom = () => { const indexFooter = document.querySelector("section.index-footer"); if (!indexFooter) { clearBottomCount++; setTimeout(() => clearBottom(), 200); return; } if (clearBottomCount == 5) return; indexFooter.style.display = "none"; }; let topHeadRef = ref(null); provide("topHeadRef", topHeadRef); // 底部导航栏 的 点击评论输入值 let floorCommentInput = ref(""); // 底部导航栏 的 点击发送评论 type input back const floorCommentBtn = (type) => { if (type == "input") commentsRef.value.bottomNavigationBar(floorCommentInput.value); else floorCommentInput.value = ""; }; provide("floorCommentInput", floorCommentInput); provide("floorCommentBtn", floorCommentBtn); // 只刷新数据 const refreshDataOnly = () => { clearAllData(); getDetails(); }; provide("refreshDataOnly", refreshDataOnly); // 点击底部调用关闭讨论输入框 const closeDiscussInputFields = () => { commentsRef.value.closeAnswerCommentsChild(); }; let respondPopListState = ref(false); // 回应列表弹窗状态 let respondDetail = ref({}); // 已回应列表 // 打开回应弹窗列表 const openPopList = () => { if (isNeedLogin.value) { goLogin(); return; } respondPopListState.value = true; getRespondDetail(); }; // 关闭回应弹窗列表 const closePopList = () => { respondPopListState.value = false; }; // 回应详情 const getRespondDetail = () => { if (isNeedLogin.value) { goLogin(); return; } riposteDetailHttp({ token: token.value }).then((res) => { if (res.code != 200) return; respondDetail.value = res.data; }); }; // 点击回应列表的 const selectEomjiListPop = (key) => { if (isNeedLogin.value) { goLogin(); return; } // let respondDetail = respondDetail.value let target = respondDetail.value[key]; riposteSubmitHttp({ token: token.value, item: target.item }).then((res) => { if (res.code != 200) { ElMessage.error(res.message); return; } let data = res.data; handleEmojiData(data); if (target.selected) { target.user = target.user.filter((item) => item.uin != data.uin); } else { target.user.push(data); } let emojiList = ripostelist.value; if (target.user.length == 0) { emojiList = emojiList.filter((item) => item.item != key); delete respondDetail.value[key]; } else { target.selected = !target.selected; respondDetail.value[key] = target; } ripostelist.value = emojiList; }); }; const { $cache } = useNuxtApp(); try { if (process.server) { console.log(`----------------------------------`); const cacheKey = `details_${id}`; const cachedData = $cache.get(cacheKey); console.log(cachedData ? "缓存数据已存在" : "缓存数据不存在"); if (cachedData) { let data = cachedData; info.value = data["info"]; option.value = data["option"]; isvote.value = data["isvote"]; seo.value = data.seo; } else { await detailsHttp({ uniqid: id }).then((res) => { if (res.code == 200) { let data = res.data; info.value = data["info"]; option.value = data["option"]; isvote.value = data["isvote"]; seo.value = data.seo; $cache.set(cacheKey, data, 3600); } }); } } } catch (error) {} </script> <style scoped lang="less"> @import url(@/assets/css/details.css); </style> <style lang="less"> .default-popup { .el-dialog__header { padding: 0; .el-dialog__headerbtn { width: 36px; height: 36px; } } .el-dialog__body { padding: 0; } } </style>