<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']"> <a class="avatar-item flexcenter" target="_blank" @click.prevent="sendMessage(info['uin'])"> <img class="avatar-icon" src="@/assets/img/send-messages-icon.png" /> 发送信息 </a> <a class="avatar-item flexcenter" target="_blank" @click.prevent="TAHomePage(info['uin'])"> <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> <div class="option-list flexflex" v-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" :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"> <div class="respond" v-if="riposteoptions.length != 0"> <div class="respond-title flexacenter"> 回应 <div class="respond-amount">{{ ripostecount.total || 0 }}</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"> <div class="respond-select-item" v-for="item in randomEmojis" :key="item" v-html="jointriposte(item)" @click="selectEomji(item)"></div> </div> <RespondAdd></RespondAdd> </div> <RespondAdd v-else></RespondAdd> </div> </div> <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> </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" import { da } from "element-plus/es/locale" 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 || [] // console.log("ripostelist", ripostelist.value) // console.log("ripostecount", ripostecount.value) // console.log("riposteoptions", riposteoptions.value) 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) { 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 }) return } 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() } try { if (process.server) { await detailsHttp({ uniqid: id }).then(res => { if (res.code != 200) { ElMessage.error(res.message) router.push("/index.html") return } let data = res.data info.value = data["info"] option.value = data["option"] isvote.value = data["isvote"] seo.value = data.seo }) } } catch (error) {} </script> <style scoped lang="less"> @font-face { font-family: "emojifont"; src: url("https://oss.x-php.com/static/riposte/emojifont-sbix.ttf"); } .content { width: 1200px; margin: 0 auto; border-radius: 16px; background: #fff; flex-wrap: wrap; --main-color: rgba(44, 186, 230, 1); --bg-color: rgba(234, 245, 248, 1); --bc-color: rgba(213, 235, 242, 1); .header { width: 100%; height: 80px; padding: 0 30px; border-bottom: 1px solid #ebebeb; font-weight: 650; font-size: 20px; color: #000000; line-height: 20px; justify-content: space-between; .views { font-size: 12px; color: #aaa; font-weight: 400; .eye-icon { margin-right: 5px; } } } .left { width: 658px; min-height: calc(100vh - 165px); padding: 30px 42px 100px 30px; border-right: 16px solid #f6f6f6; .info { font-size: 13px; justify-content: space-between; margin-bottom: 24px; .info-left { .avatar { width: 24px; height: 24px; margin-right: 10px; cursor: pointer; border-radius: 50%; } .username { color: #333; margin-right: 10px; cursor: pointer; } .post-time { line-height: 22px; color: #aaa; } } .info-right { .cut-off { color: #aaa; } .state { height: 20px; line-height: 20px; padding: 0 7px; color: #fff; background: var(--main-color); border-radius: 25px; font-size: 12px; margin-left: 10px; &.over { background: rgba(51, 51, 51, 1); } } } } .message { font-size: 14px; line-height: 24px; color: #333; margin-bottom: 30px; word-wrap: break-word; white-space: break-spaces; } .hint { font-size: 13px; line-height: 22px; color: #aaaaaa; margin-bottom: 16px; } .tick-icon { width: 14px; height: 14px; margin-top: 3px; margin-right: 6px; } .option-list { flex-direction: column; .option-item { width: 570px; border: 1px solid var(--bc-color); border-radius: 10px; word-break: break-all; font-size: 14px; line-height: 20px; color: #333333; padding: 9px 15px; cursor: pointer; position: relative; overflow: hidden; z-index: 1; &::after { background-color: var(--bg-color); content: ""; width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: -1; } &:hover::after { background-color: var(--main-color); opacity: 0.156862745098039; } &:not(:last-of-type) { margin-bottom: 10px; } &.pitch { .option-number { display: none; } .tick-icon { display: block; } .option-content { color: #000000; font-weight: 650; } } .serial { width: 14px; height: 14px; border-radius: 50%; background: var(--main-color); font-size: 11px; color: #ffffff; margin-top: 3px; margin-right: 6px; } .option-progress, .option-number, .tick-icon { display: none; } } } .option-area { width: 570px; background-color: var(--bg-color); border: 1px solid var(--bc-color); border-radius: 10px; padding: 8px 0; .option-item { padding: 7px 15px 10px; flex-direction: column; word-break: break-all; cursor: no-drop; &:not(:last-of-type) { border-bottom: 1px solid var(--bc-color); } &.pitch { cursor: pointer; .option-number { display: none; } .tick-icon { display: block; } .option-content { font-weight: 650; color: #000000; } } &.cursor-no { cursor: no-drop; } .serial { display: none; } .option-number { font-size: 11px; color: #ffffff; width: 14px; height: 14px; background-color: var(--main-color); border-radius: 50%; margin-right: 6px; margin-top: 3px; } .tick-icon { display: none; } .option-content { font-size: 14px; color: #333; line-height: 20px; word-break: break-word; } .option-progress { height: 5px; width: 100%; justify-content: flex-end; margin-top: 3px; .option-progress-step { width: 24%; background-color: var(--main-color); opacity: 0.49803922; height: 4px; border-radius: 66px; margin-right: 14px; } .option-progress-value { font-size: 12px; color: var(--main-color); line-height: 20px; } } } } } .right { flex: 1; .respond { padding: 22px 42px 30px; border-bottom: 5px solid #f6f6f6; .respond-title { font-size: 16px; line-height: 20px; font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; font-weight: 650; color: #000000; margin-bottom: 20px; .respond-amount { color: #555555; font-weight: 400; margin-left: 8px; } } .respond-no { width: 377px; height: 30px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(235, 235, 235, 1); border-radius: 208px; margin-bottom: 10px; position: relative; .respond-no-box { justify-content: space-around; .item { line-height: 30px; font-size: 16px; font-family: "emojifont"; cursor: pointer; } } } .respond-box { display: flex; flex-wrap: wrap; position: relative; .item { font-size: 12px; color: #555555; height: 30px; // border: 1px solid rgba(215, 215, 215, 1); background: rgba(246, 246, 246, 1); border-radius: 8px; padding: 0 6px; display: inline-flex; margin-right: 10px; margin-bottom: 10px; cursor: pointer; user-select: none; &.pitch { // border: none; border: 1px solid rgba(215, 215, 215, 1); background: #fff; } .code { margin-right: 4px; line-height: 30px; font-size: 16px; font-family: "emojifont"; } } .respond-select { width: 250px; height: 30px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(235, 235, 235, 1); border-radius: 208px; .respond-select-box { justify-content: space-around; .respond-select-item { cursor: pointer; font-size: 16px; font-family: "emojifont"; line-height: 30px; } } } } } } } </style> <style lang="less"> .default-popup { .el-dialog__header { padding: 0; .el-dialog__headerbtn { width: 36px; height: 36px; } } .el-dialog__body { padding: 0; } } </style>