|
@@ -1,7 +1,6 @@
|
|
|
<template>
|
|
|
<div>
|
|
|
- <div v-if="['success'].includes(status)">
|
|
|
-
|
|
|
+ <div v-if="['success'].includes(status)" @click="pageClick">
|
|
|
<div class="p-10">
|
|
|
<img :src="detailData.tourismUrl" class="aspect-[316/204] w-full object-cover rounded" />
|
|
|
<div class="flex mt-10">
|
|
@@ -90,34 +89,170 @@
|
|
|
</van-row>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="mt-10" v-for="con in detailData.travelNotesContent" :key="con.id">
|
|
|
+ <div class="mt-10 w-full" v-for="con in detailData.travelNotesContent" :key="con.id">
|
|
|
<template v-if="con.type == 'image'">
|
|
|
- <img :src="con.content" class="w-[full] rounded-xl" alt="">
|
|
|
+ <img :src="con.content" class="w-full rounded-xl" alt="">
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
<div v-html="con.content"></div>
|
|
|
</template>
|
|
|
</div>
|
|
|
|
|
|
- <div class="flex justify-end items-center text-[12px] text-[#999] mt-10 ">
|
|
|
+ <div class="flex justify-end items-center text-[14px] text-[#999] mt-10 ">
|
|
|
<div class="flex items-center mr-10">
|
|
|
<van-icon name="eye-o" class="mr-5" />{{ detailData.pageViewCount }}
|
|
|
</div>
|
|
|
- <div class="flex items-center mr-10">
|
|
|
- <van-icon name="chat-o" class="mr-5" />{{ detailData.pageViewCount }}
|
|
|
+ <div @click="doStar" class="flex items-center mr-10" :style="{ color: detailData.isLike ? '#FD9A00' : '' }">
|
|
|
+ <van-icon v-if="detailData.isLike" name="good-job" class="mr-5" />
|
|
|
+ <van-icon v-else name="good-job-o" class="mr-5" />
|
|
|
+ {{ detailData.likeCount }}
|
|
|
</div>
|
|
|
- <div class="flex items-center mr-10">
|
|
|
- <van-icon name="good-job-o" class="mr-5" />{{ detailData.likeCount }}
|
|
|
+ <div class="flex items-center mr-10" @click="handleCollect" :style="{ color: isCollect ? '#FD9A00' : '' }">
|
|
|
+ <van-icon v-if="isCollect" name="star" class="mr-5" />
|
|
|
+ <van-icon v-else name="star-o" class="mr-5" />
|
|
|
+ 收藏
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div @click="share" class="flex items-center">
|
|
|
+ <img src="~/assets/img/yj/share.png" alt="" class="w-[12px] h-[12px] mr-5">分享
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="detailData?.contactCodeConvert"
|
|
|
+ class="flex items-center h-[112px] border rounded-[20px] overflow-hidden mt-10">
|
|
|
+ <div class="w-[112px] h-full bg-[#F5F5F5] flex items-center justify-center rounded-[20px]">
|
|
|
+ <img class="h-[80%] aspect-[1/1] " :src="detailData?.contactCodeConvert" alt="">
|
|
|
</div>
|
|
|
- <div class="flex items-center">
|
|
|
- <van-icon name="like-o" class="mr-5" />{{ detailData.hotValue }}
|
|
|
+ <div class="flex items-center ml-15">
|
|
|
+ <img src="~/assets/img/yj/leftArrow.png" class="w-[28px]" alt="">
|
|
|
+ <div class="text-[#666666] text-[16px]">扫码添加好友</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template v-if="commentList.length">
|
|
|
+ <template v-for="item in commentList" :key="item.id">
|
|
|
+ <div class="flex mt-10 justify-between">
|
|
|
+ <div class="w-[32px] h-[32px] bg-[#ddd] rounded-full shrink-0">
|
|
|
+ <img v-if="item?.headImageUrlDictMap?.name" class="w-full h-full object-cover rounded-full"
|
|
|
+ :src="item?.headImageUrlDictMap?.name" alt="">
|
|
|
+ <img v-else class="w-full h-full object-cover rounded-full"
|
|
|
+ src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png" alt="">
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 pl-10 flex justify-between">
|
|
|
+ <div>
|
|
|
+ <div>
|
|
|
+ <span class="text-[#f40] text-[12px]">{{ item?.commentUserIdDictMap?.name }}:</span>
|
|
|
+ <span class="text-[14px] text-[#333]" v-html="coveredContent(item?.commentContent)"></span>
|
|
|
+ </div>
|
|
|
+ <div class="text-[#666] text-[10px]">{{ item?.createTime }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="mt-3 shrink-0">
|
|
|
+ <van-icon @click="deleteComment(item.id)" v-if="item.self" class="mr-15" name="delete-o" size="20px"
|
|
|
+ color="#666" />
|
|
|
+ <van-icon @click.stop="addReply(item)" name="comment-o" size="20px" color="#666" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template v-if="Array.isArray(item.childrenList) && item.childrenList.length">
|
|
|
+ <div class="pl-[32px] mt-10">
|
|
|
+ <div class="pl-12" style="border-left:1px solid #ccc">
|
|
|
+ <div v-for="subItem in item.childrenList" :key="subItem.id">
|
|
|
+ <div class="flex justify-between items-start">
|
|
|
+ <div>
|
|
|
+ <span class="text-[#f40] text-[12px]">{{ subItem?.commentUserIdDictMap?.name }}<span
|
|
|
+ class="text-[#333]">回复</span>{{ subItem?.replyUserIdDictMap?.name }}:</span>
|
|
|
+ <span class="text-[14px] text-[#333]" v-html="coveredContent(subItem?.commentContent)"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center mt-3">
|
|
|
+ <van-icon v-if="subItem.self" @click="deleteComment(subItem.id)" class="mr-15" name="delete-o"
|
|
|
+ size="20px" color="#666" />
|
|
|
+ <van-icon @click.stop="addReply(subItem, item.id)" name="comment-o" size="20px" color="#666" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="text-[#666] text-[10px]">{{ subItem.createTime }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="bg-[#F5F5F5] rounded-[20px] flex items-center justify-center h-[140px] mt-10">
|
|
|
+ <div>
|
|
|
+ <div>
|
|
|
+ <img class="w-[84px]" src="~/assets/img/yj/no-comments.png" alt="">
|
|
|
+ </div>
|
|
|
+ <div class="text-center text-[#999]">
|
|
|
+ 没有评论
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="mt-10">
|
|
|
+ <div class="flex items-center space-x-10">
|
|
|
+ <img src="~/assets/img/yj/recomend_travel_note_icon.png" class="h-[27px] w-[36px] object-cover" alt="" srcset="" />
|
|
|
+ <div class="text-[16px] font-bold text-black-3">热门游记</div>
|
|
|
+ </div>
|
|
|
+ <div class="mt-5 divide-y">
|
|
|
+ <NuxtLink :to="`/yj/${item.id}`" class="group flex cursor-pointer items-center space-x-10 py-15" v-for="(item, index) in dataList.dataList
|
|
|
+" :key="item.id">
|
|
|
+ <img :src="item?.homeHotPicturesAfterConvert?.length ? item.homeHotPicturesAfterConvert[0] : ''"
|
|
|
+ class="h-80 w-80 shrink-0 rounded-xl object-cover" alt="" />
|
|
|
+ <div class="w-0 flex-1">
|
|
|
+ <div class="truncate text-xl font-semibold text-black-3 group-hover:text-linkHover">
|
|
|
+ {{ item.projectTitle }}
|
|
|
+ </div>
|
|
|
+ <div class="mt-5 flex items-center justify-between space-x-20 text-base">
|
|
|
+ <div class="flex-1 truncate text-black-6">
|
|
|
+ {{ item.remarks }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </NuxtLink>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="h-[100px]"></div>
|
|
|
+ <div @click.stop="" class="fixed bottom-0 left-0 w-full bg-[#fff] pt-10 pb-20 border">
|
|
|
+ <div v-if="replyComment.id" class="text-center relative">
|
|
|
+ 回复 <span class="text-[#f40]">{{ replyComment?.commentUserIdDictMap?.name }}:</span>
|
|
|
+ <div class="absolute right-10 top-0">
|
|
|
+ <van-icon name="cross" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center ">
|
|
|
+ <div class="rounded-full overflow-hidden w-[32px] h-[32px] shrink-0 ml-5">
|
|
|
+ <img v-if="userInfo?.headImageUrl" class="w-full h-full object-cover rounded-full"
|
|
|
+ :src="userInfo?.headImageUrl" alt="" />
|
|
|
+ <img v-else class="w-full h-full object-cover rounded-full"
|
|
|
+ src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png" alt="" />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="relative flex-1 ml-5 mr-5 pl-5 pr-5 pt-4 pb-4 rounded-full h-[50px] border border-[#FD9A00] flex items-center justify-between">
|
|
|
+ <textarea v-model="commentValue" ref="textareaRef" :placeholder="replyComment.id ? '回复:' : '发布您的评论'"
|
|
|
+ @focus="textareaFocus" class="ml-8 flex-1 h-full" style="resize: none;"></textarea>
|
|
|
+ <img @click="openEmoji" src="~/assets/img/yj/emoji.png" class="w-[22px] h-[22px]" alt="">
|
|
|
+
|
|
|
+ </div>
|
|
|
+ <div @click="addComment"
|
|
|
+ class="pt-6 pb-6 pl-20 pr-20 mr-5 bg-[#FD9A00] text-[#fff] flex items-center justify-center rounded-full">
|
|
|
+ 评论
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="showEmoji" class=" h-[300px] bg-[#fff] overflow-auto">
|
|
|
+ <div @click="closeEmojiBox" class="flex justify-end pr-15 text-[#999] text-[12px]">
|
|
|
+ 收起表情
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center flex-wrap">
|
|
|
+ <div v-for="(item, index) in emojiJson" :key="index"
|
|
|
+ class="hover:bg-[#ddd] text-[22px] w-[10%] aspect-[1/1] flex items-center justify-center">
|
|
|
+ <div @click="selectEmoji(index)" v-html="coveredContent(index)"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-else class=" text-[#999] mt-100">
|
|
|
<div class="text-center">
|
|
|
- 不好意思,游记走丢了~
|
|
|
+ 不好意思,找不到这篇游记了~
|
|
|
</div>
|
|
|
<div @click="goBack" class="text-center underline mt-10 cursor-pointer text-[#4B99EA]">
|
|
|
返回
|
|
@@ -128,29 +263,234 @@
|
|
|
|
|
|
<script setup>
|
|
|
import dashBorder2 from '~/assets/img/dash-border.png'
|
|
|
-const id = useRouteParam("id");
|
|
|
-const router = useRouter();
|
|
|
+import emojiArr from './emoji'
|
|
|
+import emojiJson from './emoji.json'
|
|
|
+const userInfoStore = useUserInfoStore()
|
|
|
+const authStore = useAuthStore()
|
|
|
+const { token } = storeToRefs(authStore)
|
|
|
|
|
|
+const id = useRouteParam("id")
|
|
|
+const router = useRouter()
|
|
|
+const textareaRef = ref(null)
|
|
|
+
|
|
|
+// 游记内容
|
|
|
const { data: detailData, status } = useMyFetch(
|
|
|
`website/tourism/projectTravelNotes/travelNotesDetail?id=${id.value}`
|
|
|
-);
|
|
|
-console.log('detailData::', detailData)
|
|
|
-const htmlContent = computed(() => {
|
|
|
- return detailData?.value?.tourTourismTravelNotesContent?.content?.replace(
|
|
|
- /<img/g,
|
|
|
- "<img style='width:100%; height:auto;'"
|
|
|
- );
|
|
|
-});
|
|
|
+)
|
|
|
+
|
|
|
+// 热门游记列表
|
|
|
+const { data: dataList } = await useMyFetch(
|
|
|
+ `/website/tourism/projectTravelNotes/homeList?isHotspot=1&pageNum=1&pageSize=4`
|
|
|
+)
|
|
|
+
|
|
|
+// 是否已收藏
|
|
|
+const isCollect = ref(false)
|
|
|
+async function isCollectTravelNotes() {
|
|
|
+ if (!token.value) return
|
|
|
+ const { data } = await request(`website/tourism/projectTravelNotes/isCollectTravelNotes?travelNotesId=${id.value}`)
|
|
|
+ isCollect.value = data
|
|
|
+}
|
|
|
+
|
|
|
+// 收藏/取消收藏
|
|
|
+async function handleCollect() {
|
|
|
+ const type = isCollect.value ? 0 : 1
|
|
|
+ showLoadingToast({
|
|
|
+ forbidClick: true,
|
|
|
+ duration: 0,
|
|
|
+ })
|
|
|
+ await request(`/website/tourism/projectTravelNotes/userCollectTravelNotesUpdate`, { method: 'post', body: { type, travelNotesId: id.value } }).finally(() => closeToast())
|
|
|
+ isCollectTravelNotes()
|
|
|
+ showToast(type == 1 ? '收藏成功' : '已取消收藏')
|
|
|
+}
|
|
|
+
|
|
|
+// 获取点赞数量
|
|
|
+async function getStars() {
|
|
|
+ const { data } = await request(`/website/tourism/projectTravelNotes/getLikeCount?travelNotesId=${id.value}`)
|
|
|
+ if (isNaN(data)) return
|
|
|
+ detailData.value.likeCount = data
|
|
|
+}
|
|
|
+
|
|
|
+// 点赞
|
|
|
+const canDoStar = ref(true)
|
|
|
+async function doStar() {
|
|
|
+ if (!canDoStar.value) return
|
|
|
+ canDoStar.value = false
|
|
|
+ request(`/website/tourism/projectTravelNotes/userLikeTravelNotesUpdate`, {
|
|
|
+ method: 'post',
|
|
|
+ body: { travelNotesId: id.value }
|
|
|
+ }).then(() => {
|
|
|
+ detailData.value.isLike = true
|
|
|
+ getStars()
|
|
|
+ }).finally(() => canDoStar.value = true)
|
|
|
+}
|
|
|
+
|
|
|
+// 获取用户信息
|
|
|
+const userInfo = ref({})
|
|
|
+async function getUserInfo() {
|
|
|
+ if (!token.value) return userInfo.value = {}
|
|
|
+ request('/website/tourism/user/view').then(res => {
|
|
|
+ userInfo.value = res.data || {}
|
|
|
+ }, () => {
|
|
|
+ userInfo.value = {}
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 评论内容
|
|
|
+const commentValue = ref('')
|
|
|
+
|
|
|
+// 是否展示表情
|
|
|
+const showEmoji = ref(false)
|
|
|
+
|
|
|
+// 文本域失焦
|
|
|
+function textareaFocus() {
|
|
|
+ showEmoji.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 获取评论列表
|
|
|
+const commentList = ref([])
|
|
|
+async function getComments() {
|
|
|
+ showLoadingToast({
|
|
|
+ forbidClick: true,
|
|
|
+ duration: 0,
|
|
|
+ })
|
|
|
+ const { data } = await request("/website/comment/tourTravelNotesComment/list?travelNoteId=" + id.value).finally(()=>closeToast())
|
|
|
+ if (!Array.isArray(data) || !data.length) return commentList.value = []
|
|
|
+ commentList.value = data
|
|
|
+}
|
|
|
+
|
|
|
+// 转换评论中的一些非字符emoji
|
|
|
+function coveredContent(val) {
|
|
|
+ if (!val) return ''
|
|
|
+ return val.replace(/\[.*?]/g, function (str) {
|
|
|
+ const baseApi = import.meta.env.VITE_APP_EMOJI_API
|
|
|
+ const emojiName = emojiJson[str]
|
|
|
+ if (!emojiName) return str
|
|
|
+ return `<img src=${baseApi}${emojiJson[str]} style="width: 20px; height: 20px;display: inline-block; vertical-align: middle"/>`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 删除评论
|
|
|
+async function deleteComment(id) {
|
|
|
+ if (!id) return
|
|
|
+ showConfirmDialog({
|
|
|
+ message: '确认要删除这条评论吗?',
|
|
|
+ theme: 'round-button',
|
|
|
+ }).then(async () => {
|
|
|
+ showLoadingToast({
|
|
|
+ forbidClick: true,
|
|
|
+ duration: 0,
|
|
|
+ })
|
|
|
+ await request('/website/comment/tourTravelNotesComment/delete', {
|
|
|
+ method: 'post',
|
|
|
+ body: { id }
|
|
|
+ }).finally(() => closeToast())
|
|
|
+ showToast('删除成功')
|
|
|
+ getComments()
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+const cursorIndex = ref(0)
|
|
|
+// 打开表情
|
|
|
+function openEmoji() {
|
|
|
+ nextTick(() => {
|
|
|
+ textareaRef.value.selectionStart && (cursorIndex.value = textareaRef.value.selectionStart)
|
|
|
+ showEmoji.value = true
|
|
|
+ })
|
|
|
+}
|
|
|
+// 收起表情
|
|
|
+function closeEmojiBox() {
|
|
|
+ showEmoji.value = false
|
|
|
+ nextTick(() => {
|
|
|
+ textareaRef.value.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+// 选择表情
|
|
|
+function selectEmoji(emojiStr = '') {
|
|
|
+ const length = emojiStr.length
|
|
|
+ commentValue.value = commentValue.value.slice(0, cursorIndex.value) + emojiStr + commentValue.value.slice(cursorIndex.value)
|
|
|
+ nextTick(() => {
|
|
|
+ cursorIndex.value += length
|
|
|
+ textareaRef.value.setSelectionRange(cursorIndex.value, cursorIndex.value)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 回复评论
|
|
|
+const replyComment = ref({})
|
|
|
+function addReply(item, parentId) {
|
|
|
+ if (!token.value) {
|
|
|
+ showConfirmDialog({
|
|
|
+ showConfirmDialog: true,
|
|
|
+ title: '提示',
|
|
|
+ message: '登录后可以发布评论',
|
|
|
+ theme: 'round-button',
|
|
|
+ }).then(async () => { navigateTo({ path: '/login' }) })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ textareaRef.value.focus()
|
|
|
+ replyComment.value = item
|
|
|
+ parentId && (replyComment.value.parentId = parentId)
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 发布评论
|
|
|
+const canAddComment = ref(true)
|
|
|
+async function addComment() {
|
|
|
+ if (!canAddComment.value) return
|
|
|
+ if (!token.value) {
|
|
|
+ showConfirmDialog({
|
|
|
+ showConfirmDialog: true,
|
|
|
+ title: '提示',
|
|
|
+ message: '登录后可以发布评论',
|
|
|
+ theme: 'round-button',
|
|
|
+ }).then(async () => { navigateTo({ path: '/login' }) })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!commentValue.value.trim()) {
|
|
|
+ showToast('评论内容不能为空!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ canAddComment.value = false
|
|
|
+ const body = { travelNoteId: id.value, commentContent: commentValue.value }
|
|
|
+ if (replyComment.value.id) {
|
|
|
+ body.replyCommentId = replyComment.value.id
|
|
|
+ body.replyUserId = replyComment.value.createUserId
|
|
|
+ body.parentId = replyComment.value.parentId
|
|
|
+ }
|
|
|
+ request('website/comment/tourTravelNotesComment/add', { method: 'post', body }).then(() => {
|
|
|
+ commentValue.value = ''
|
|
|
+ showToast('评论发布成功')
|
|
|
+ getComments()
|
|
|
+ showEmoji.value = false
|
|
|
+ }).finally(() => canAddComment.value = true, replyComment.value = {}, cursorIndex.value = 0)
|
|
|
+}
|
|
|
+function pageClick() {
|
|
|
+ nextTick(() => {
|
|
|
+ showEmoji.value = false
|
|
|
+ textareaRef.value.blur()
|
|
|
+ replyComment.value = {}
|
|
|
+ cursorIndex.value = 0
|
|
|
+ })
|
|
|
+}
|
|
|
+// 分享
|
|
|
+function share() {
|
|
|
+ navigator.clipboard.writeText(location.href).then(() => {
|
|
|
+ showNotify({ type: 'success', message: '游记链接已复制成功,快去分享给好友吧!' });
|
|
|
+ }, (err) => {
|
|
|
+ showNotify({ message: '游记链接分享失败' });
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
-const lableList = computed(() => {
|
|
|
- return detailData.value?.projectLabel?.split("&") ?? [];
|
|
|
-});
|
|
|
function goBack() {
|
|
|
router.back()
|
|
|
}
|
|
|
useSeoMeta({
|
|
|
title: "游记详情",
|
|
|
});
|
|
|
+onMounted(() => {
|
|
|
+ getComments()
|
|
|
+ getUserInfo()
|
|
|
+ isCollectTravelNotes()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
-<style lang="scss" scoped></style>
|
|
|
+<style lang="scss" scoped></style>
|