PC-vote/pages/details/[id].vue
2024-07-17 12:25:33 +08:00

955 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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