From 4789476b3fe734b4366ce72079330cea32a4997d Mon Sep 17 00:00:00 2001 From: tj <1378534974@qq.com> Date: 星期五, 06 六月 2025 18:04:20 +0800 Subject: [PATCH] 1.评论 --- components/comment/comment-item.vue | 295 ++++++++++++----------- components/comment/comment-popup.vue | 206 ++++++++++------- types/index.ts | 71 +++++ sub-pages/film-list/film-detail.vue | 130 ++++++--- components/comment/comment-sub-item.vue | 2 package.json | 2 plugins/storage.js | 3 composables/usePlatformLoginType.ts | 2 8 files changed, 434 insertions(+), 277 deletions(-) diff --git a/components/comment/comment-item.vue b/components/comment/comment-item.vue index b905df7..5278054 100644 --- a/components/comment/comment-item.vue +++ b/components/comment/comment-item.vue @@ -1,161 +1,174 @@ <template> - <view class="comment-item"> - <view class="comment-item-header"> - <view class="comment-item-header-left"> - <view class="avatar"> - <up-avatar :src="avatar" size="50rpx" shape="circle" /> + <view class="comment-item"> + <view class="comment-item-header"> + <view class="comment-item-header-left"> + <view class="avatar"> + <up-avatar :src="avatar" size="50rpx" shape="circle" /> + </view> + <view class="comment-content"> + <view class="author"> + <text class="nickname">{{ nickname }}</text> + <view v-if="isAuthor" class="author-flag">作者</view> </view> - <view class="comment-content"> - <view class="author"> - <text class="nickname">{{ nickname }}</text> - <view v-if="isAuthor" class="author-flag">作者</view> - </view> - <view class="comment-content-text"> - <text>{{ content }}</text> - <up-album :urls="images" multipleSize="180rpx" singleSize="500rpx" /> - <text class="date">{{ date }}</text> - <text class="address">{{ address }}</text> - <text class="reply" @click="onReply">回复</text> - </view> - </view> - <view class="comment-opeartor"> - <up-icon name="heart" size="30rpx" /> - <view class="comment-opeartor-heart-number">{{ likes }}</view> + <view class="comment-content-text"> + <text>{{ content }}</text> + <up-album :urls="images" multipleSize="180rpx" singleSize="500rpx" /> + <text class="date">{{ date }}</text> + <text class="address">{{ address }}</text> + <text class="reply" @click="onReply">回复</text> </view> </view> - </view> - - <!-- 子评论区域 --> - <view class="sub-comment"> - <comment-sub-item - avatar="https://img.yzcdn.cn/vant/cat.jpeg" - nickname="图墙精选" - :isAuthor="true" - content="如果路线里全是常规景区,没一个特殊的点位..." - :images="urls2" - date="2天前" - address="湖北" - :likes="30" - @reply="emitReply" - /> + <view class="comment-opeartor"> + <up-icon name="heart" size="30rpx" /> + <view class="comment-opeartor-heart-number">{{ likes }}</view> + </view> </view> </view> - </template> - - <script setup lang="ts"> - import { ref, onMounted } from 'vue' - interface Props { - avatar: string - nickname: string - isAuthor: boolean - content: string - images: string[] - date: string - address: string - likes: number + + <!-- 子评论区域 --> + <view class="sub-comment"> + <comment-sub-item + v-for="(item, index) in child" + :avatar="item.picture" + :nickname="item.commentUserName" + :isAuthor="item.createBy === filmInfo.createBy" + :content="item.content" + :images="getImageList(item)" + :date="item.createTime" + address="湖北" + :likes="item.likeCount" + @reply="emitReply" + :filmInfo="filmInfo" + /> + </view> + </view> +</template> + +<script setup lang="ts"> +import { ref, onMounted } from 'vue' +import { CommentDTO } from '@/types/index' +interface Props { + avatar: string + nickname: string + isAuthor: boolean + content: string + images: string[] + date: string + address: string + likes: number + child: CommentDTO[] + filmInfo: CommentDTO +} + +const props = defineProps<Props>() + +const emit = defineEmits<{ + (e: 'reply'): void +}>() + + +const getImageList = (item: any): string[] => { + if (!item || !item.filmPictures || typeof item.filmPictures !== 'string') { + return []; } - - const props = defineProps<Props>() - - const emit = defineEmits<{ - (e: 'reply'): void - }>() - - const urls2 = ref<string[]>([ - 'https://img.yzcdn.cn/vant/cat.jpeg', - 'https://img.yzcdn.cn/vant/cat.jpeg', - 'https://img.yzcdn.cn/vant/cat.jpeg', - 'https://img.yzcdn.cn/vant/cat.jpeg', - 'https://img.yzcdn.cn/vant/cat.jpeg' - ]) - - const onReply = () => { - emit('reply') + + try { + const pictures = JSON.parse(item.filmPictures); + return Array.isArray(pictures) ? pictures.map((p: any) => p.url) : []; + } catch (e) { + console.error('filmPictures JSON parse error:', e, item.filmPictures); + return []; } - - const emitReply = () => { - emit('reply') - } - </script> - - <style lang="scss" scoped> - .comment-item { - .comment-item-header { +}; + + +const onReply = () => { + emit('reply') +} + +const emitReply = () => { + emit('reply') +} +</script> + +<style lang="scss" scoped> +.comment-item { + .comment-item-header { + display: flex; + align-items: center; + + .comment-item-header-left { display: flex; - align-items: center; - - .comment-item-header-left { + width: 100%; + + .avatar { + margin-right: 10rpx; + } + + .comment-content { + width: 92%; display: flex; - width: 100%; - - .avatar { - margin-right: 10rpx; - } - - .comment-content { - width: 92%; + flex-direction: column; + + .author { display: flex; - flex-direction: column; - - .author { - display: flex; - align-items: center; - - .nickname { - font-size: 30rpx; - color: #858585; - } - - .author-flag { - font-size: 18rpx; - color: #ff4d4f; - font-weight: bold; - border-radius: 50rpx; - padding: 5rpx 10rpx; - background-color: rgba(219, 19, 22, 0.219); - margin-left: 10rpx; - height: 25rpx; - } + align-items: center; + + .nickname { + font-size: 30rpx; + color: #858585; } - - .comment-content-text { - margin-top: 10rpx; - font-size: 26rpx; - letter-spacing: 1rpx; - line-height: 1.5; - - .date, - .address { - font-size: 20rpx; - padding: 10rpx; - color: #858585; - } - - .reply { - font-size: 24rpx; - padding: 10rpx; - color: #2979ff; - } + + .author-flag { + font-size: 18rpx; + color: #ff4d4f; + font-weight: bold; + border-radius: 50rpx; + padding: 5rpx 10rpx; + background-color: rgba(219, 19, 22, 0.219); + margin-left: 10rpx; + height: 25rpx; } } - - .comment-opeartor { - display: flex; - flex-direction: column; - align-items: flex-start; - - .comment-opeartor-heart-number { - text-align: center; + + .comment-content-text { + margin-top: 10rpx; + font-size: 26rpx; + letter-spacing: 1rpx; + line-height: 1.5; + + .date, + .address { font-size: 20rpx; + padding: 10rpx; + color: #858585; + } + + .reply { + font-size: 24rpx; + padding: 10rpx; + color: #2979ff; } } } - } - - .sub-comment { - margin-left: 60rpx; - margin-top: 10rpx; + + .comment-opeartor { + display: flex; + flex-direction: column; + align-items: flex-start; + + .comment-opeartor-heart-number { + width: 100%; + text-align: center; + font-size: 20rpx; + } + } } } - </style> - \ No newline at end of file + + .sub-comment { + margin-left: 60rpx; + margin-top: 10rpx; + } +} +</style> \ No newline at end of file diff --git a/components/comment/comment-popup.vue b/components/comment/comment-popup.vue index 49f9e5d..48fbdc3 100644 --- a/components/comment/comment-popup.vue +++ b/components/comment/comment-popup.vue @@ -1,20 +1,7 @@ <template> - <up-popup - v-model:show="showPopup" - mode="bottom" - @open="handleOpen" - @close="handleClose" - close-on-click-overlay - > + <up-popup v-model:show="showPopup" mode="bottom" @open="handleOpen" @close="handleClose" close-on-click-overlay> <view class="comment-popup"> - <up-textarea - v-model="commentContent" - placeholder="请输入内容" - count - :focus="isFocus" - :cursor-spacing="200" - - /> + <up-textarea v-model="comment.content" placeholder="请输入内容" count :focus="isFocus" :cursor-spacing="200" /> <view class="comment-btn-view"> <view class="comment-btn-icon"> @@ -24,30 +11,13 @@ <up-icon name="plus-circle" size="60rpx" color="#999999" /> </view> <view class="comment-btn"> - <up-button - :disabled="!canSend" - type="error" - shape="circle" - text="发送" - size="mini" - @click="sendComment" - /> + <up-button :disabled="!canSend" type="error" shape="circle" text="发送" size="mini" @click="sendComment" /> </view> </view> <view class="comment-image-upload" v-if="fileList.length > 0"> - <up-upload - :file-list="fileList" - name="file" - :auto-upload="false" - :max-count="9" - upload-text="上传图片" - width="130rpx" - height="130rpx" - :cursor-spacing="200" - multiple - @delete="handleDelete" - /> + <up-upload :file-list="fileList" name="file" :auto-upload="false" :max-count="9" upload-text="上传图片" + width="130rpx" height="130rpx" :cursor-spacing="200" multiple @delete="handleDelete" /> </view> </view> </up-popup> @@ -55,26 +25,35 @@ <script setup lang="ts"> import { ref, watch, computed } from 'vue'; - -interface FileItem { - url: string; - status: string; -} +import { useGlobal } from '@/composables/useGlobal' +const { $http, $message, $store } = useGlobal() +import { CommentDTO, FileItem } from '@/types/index' const props = defineProps<{ - modelValue: boolean; // 修改这里 + modelValue: boolean; + parentId: string; + filmId: string; }>(); -const emit = defineEmits(['update:modelValue']); // 修改这里 +const emit = defineEmits(['update:modelValue', 'success']); // 修改这里 const showPopup = ref(props.modelValue); - -const commentContent = ref(''); const isFocus = ref(false); const focusLock = ref(false); const fileList = ref<FileItem[]>([]); +const pictureList = ref<FileItem[]>([]); +const comment = ref<CommentDTO>({ + parentId: props.parentId, + filmId: props.filmId, + content: '', + fileList: [], + filmPictures:'', +}) -const uploadUrl = 'https://your-api.com/upload'; // 替换成你的上传接口地址 +watch(pictureList, (newList) => { + comment.value.fileList = newList; + comment.value.filmPictures=JSON.stringify(newList); +}, { immediate: true, deep: true }); // 监听外部传入的 modelValue,同步到内部 showPopup watch( @@ -93,7 +72,7 @@ }); const canSend = computed(() => { - return commentContent.value.trim() !== '' || fileList.value.length > 0; + return comment.value.content.trim() !== '' || comment.value.fileList.length > 0; }); const handleOpen = () => { @@ -106,43 +85,83 @@ }; const handleClose = () => { - isFocus.value = false; - showPopup.value = false; + // isFocus.value = false; + // showPopup.value = false; }; const chooseImage = () => { - uni.chooseImage({ - count: 9 - fileList.value.length, - sizeType: ['original', 'compressed'], - sourceType: ['album', 'camera'], - success: (res) => { - res.tempFilePaths.forEach((filePath) => { - const fileItem: FileItem = { - url: filePath, - status: 'ready', - }; - fileList.value.push(fileItem); - uploadImage(filePath); - }); - }, - fail: (err) => { - console.error('选择图片失败:', err); - }, - }); -}; -const uploadImage = (filePath: string) => { - uni.uploadFile({ - url: uploadUrl, - filePath, - name: 'file', - success: (res) => { - console.log('上传成功:', res); - // 可解析 res 并更新 fileList 中的 fileItem.url 为服务器返回地址 - }, - fail: (err) => { - console.error('上传失败:', err); - }, + uni.chooseImage({ + count: 9 - fileList.value.length, // 最多可以选择的图片张数,默认9 + sizeType: ['compressed'], //original 原图,compressed 压缩图,默认二者都有 + sourceType: ['camera', 'album'], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项 + success: async function (res: any) { + let errMsg = res.errMsg; + if (errMsg === 'chooseImage:ok') { + // 检查文件大小 + let oversizedFile = res.tempFiles.find(file => file.size > 1024 * 1024 * 5); + if (oversizedFile) { + $message.confirm('图片最多支持5M大小,超出大小限制'); + return; + } + + // 显示加载提示 + $message.showLoading(); + + // 获取所有文件的 path 数组 + res.tempFiles.forEach((file) => { + const fileItem: FileItem = { + name: file.name || '', + size:file.size, + url: file.path, + status: 'ready', + }; + fileList.value.push(fileItem); + }); + + + + // 创建上传请求的数组 + const uploadPromises = fileList.value.map(tmpfile => { + return $http.upload(tmpfile.url) + .then(res => { + let pic = res.data && res.data.length > 0 && res.data[0].url || ''; + return pic; + }) + .catch(err => { + console.error(err); + return null; // 上传失败时返回 null + }); + }); + + // 使用 Promise.all 并发上传 + try { + const resImages = await Promise.all(uploadPromises); + + console.log("resImages",resImages) + + // 检查上传结果 + const successfulImages = resImages.filter(pic => pic !== null); + const fileItem: FileItem = { + name: '', + size:0, + url: successfulImages, + status: 'ready', + }; + pictureList.value.push(fileItem); + console.log('上传成功:', pictureList.value); + $message.hideLoading(); + + if (successfulImages.length !== fileList.value.length) { + // 部分上传失败 + $message.showToast('部分文件上传失败,请重新尝试!'); + } + } catch (err) { + console.error(err); + $message.showToast('文件上传失败,请联系管理员'); + } + } + } }); }; @@ -150,16 +169,33 @@ fileList.value.splice(index, 1); }; -const sendComment = () => { +const sendComment = async () => { if (!canSend.value) return; - console.log('发送评论:', commentContent.value); - console.log('附带图片:', fileList.value.map((f) => f.url)); + console.log("评论添加:", comment.value) + const { + code, data + } = await $http.request('post', '/api/comment/create', { + data: comment.value + }) + if (code == 0) { + // 保存成功,返回评论? + console.log("评论新增成功") + emit('success'); // 告诉父组件刷新列表 + emit('update:modelValue', false); // 关闭弹窗 - // 提交评论逻辑 - commentContent.value = ''; + } else { + $message.showToast('系统异常,无法获取当前微信是否已经绑定过账号') + } + fileList.value = []; - handleClose(); + // handleClose(); +}; + + +const submitComment = async () => { + + }; </script> diff --git a/components/comment/comment-sub-item.vue b/components/comment/comment-sub-item.vue index fa270cf..ded7a6b 100644 --- a/components/comment/comment-sub-item.vue +++ b/components/comment/comment-sub-item.vue @@ -28,6 +28,7 @@ </template> <script setup lang="ts"> +import { CommentDTO } from '@/types/index' const props = defineProps<{ avatar: string nickname: string @@ -37,6 +38,7 @@ date: string address: string likes: number + filmInfo: CommentDTO }>() const emit = defineEmits<{ diff --git a/composables/usePlatformLoginType.ts b/composables/usePlatformLoginType.ts index 45e0c28..6f8e84b 100644 --- a/composables/usePlatformLoginType.ts +++ b/composables/usePlatformLoginType.ts @@ -13,7 +13,7 @@ apitype.value = 'loginPartner' // #endif - // #ifdef PUB_FILM + // #ifdef PUB_CUSTOMER apitype.value = 'loginCustomer' // #endif diff --git a/package.json b/package.json index 2f686c9..a4a8b42 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "PUB_TYPE": "film" }, "define": { - "PUB_FILM": true + "PUB_CUSTOMER": true } } } diff --git a/plugins/storage.js b/plugins/storage.js index 457bd2a..d492ebd 100644 --- a/plugins/storage.js +++ b/plugins/storage.js @@ -1,8 +1,7 @@ // import Vue from 'vue' // let APPID = 'film-token' + process.env.PUB_TYPE -let APPID = 'film-token' + import.meta.env.PUB_TYPE - +let APPID = 'film-token' // #ifdef PUB_CUSTOMER APPID = 'film-token-customer' // #endif diff --git a/sub-pages/film-list/film-detail.vue b/sub-pages/film-list/film-detail.vue index 2f4e4e9..398d5c7 100644 --- a/sub-pages/film-list/film-detail.vue +++ b/sub-pages/film-list/film-detail.vue @@ -31,15 +31,20 @@ <view class="article-content"> <view class="title content-item"> - <text>{{ filmInfo.coverTitle }}</text> + <text>{{ filmInfo?.coverTitle }}</text> </view> <view class="content-item"> <!-- <rich-text :nodes="filmInfo.filmContent" /> --> - <up-parse :content="filmInfo.filmContent"></up-parse> + <up-parse :content="filmInfo?.filmContent" :tag-style="{ + p: 'margin-bottom: 16px; line-height: 1.6;', + h3: 'font-size: 18px; font-weight: bold; margin: 20px 0;', + hr: 'margin: 24px 0; border: none; border-top: 1px solid #ccc;' + }" /> + <!-- <view v-html="filmInfo.filmContent||'暂无'" class="rich" style="overflow: scroll;"></view> --> </view> <view class="annotation content-item"> - <text>{{ formatRelativeTime(filmInfo.createTime) }} 美国</text> + <text>{{ formatRelativeTime(filmInfo?.createTime) }} 美国</text> </view> </view> @@ -54,9 +59,10 @@ </view> <!-- 示例评论项,comment-item 可替换为实际组件 --> - <comment-item avatar="https://img.yzcdn.cn/vant/cat.jpeg" nickname="图墙精选" :isAuthor="true" - content="如果路线里全是常规景区..." :images="urls2" date="2天前" address="湖北" :likes="30" - @reply="showCommentLayer" /> + <comment-item v-for="(item, index) in commentList" :avatar="item.picture" + :nickname="item.commentUserName" :isAuthor="item.createBy === filmInfo.createBy" + :content="item.content" :images="getImageList(item)" :date="item.createTime" address="湖北" + :likes="item.likeCount" :child="item.child" :filmInfo="filmInfo" @reply="showCommentLayer" /> </view> </scroll-view> @@ -73,7 +79,8 @@ </view> </view> - <comment-popup v-model="commentShow" /> + <comment-popup v-model="commentShow" :film-id="filmInfo?.id" :parent-id="commentParendId" + @success="handleCommentSuccess" /> </view> </template> @@ -85,12 +92,17 @@ import { useGlobal } from '@/composables/useGlobal' const { $http, $message, $store } = useGlobal() -import { FilmInfo,FilmPicture } from '@/types/index' +import { FilmInfo, FilmPicture, CommentDTO } from '@/types/index' import { formatRelativeTime } from '@/utils/time' // Swiper 当前页 const currentNum = ref(0) const commentShow = ref(false) + +const commentParendId = ref<String>('') + +const commentList = ref<CommentDTO[]>([]) +const filmId = ref<string>(''); // 本地变量 const user = reactive({ id: 3, @@ -98,17 +110,8 @@ avatar: 'https://img.yzcdn.cn/vant/cat.jpeg' }) -const urls2 = ref<string[]>([ - 'https://img.yzcdn.cn/vant/cat.jpeg' -]) -const desc = ref(` - 😭…… - 刚从新疆旅游回来,真的踩了好多坑!!...<br/> - #新疆是个好地方 #新疆旅行攻略... - `) - -onLoad((options:any) => { +onLoad((options: any) => { const theme = uni.getStorageSync('theme') || 'light' console.log('theme:', theme) @@ -119,10 +122,36 @@ const id = options.id const type = options.type console.log('id:', id, 'type:', type) - if(id){ + if (id) { + filmId.value = id getFilmInfoById(id) + getCommentList(id) } }) + +const getImageList = (item: any): string[] => { + if (!item || !item.filmPictures || typeof item.filmPictures !== 'string') { + return []; + } + + try { + const pictures = JSON.parse(item.filmPictures); + return Array.isArray(pictures) ? pictures.map((p: any) => p.url) : []; + } catch (e) { + console.error('filmPictures JSON parse error:', e, item.filmPictures); + return []; + } +}; + + + +const getCommentList = async (id: string) => { + $message.showLoading() + const { data } = await $http.request('get', '/api/comment/getCommentByFilmId?filmId=' + id, {}) + commentList.value = data + console.log("评论", data) + $message.hideLoading() +} const onSwiperChange = (e: any) => { currentNum.value = e.detail.current @@ -139,44 +168,51 @@ commentShow.value = true } +const handleCommentSuccess = (data) => { + // 例如重新请求评论列表 + // fetchCommentList(); + getCommentList(filmId.value) + commentShow.value = false +}; + onShow(() => { - + }); const filmInfo = ref<FilmInfo>() const filmPictureList = ref<string[]>([]) -const getFilmInfoById = async (id:String)=>{ +const getFilmInfoById = async (id: String) => { const { - code, data - } = await $http.request('get', '/api/filmWorks/list/view', { - params: { - id: id - } - }) - if (code == 0) { - filmInfo.value=data - console.log("详情",filmInfo.value) - if(data && data.filmPictures){ - // 只获取里面的url - // filmPictureList.value=JSON.parse(data.filmPictures) as Array<FilmPicture> - const tmpPicture = JSON.parse(data.filmPictures) as FilmPicture[] - filmPictureList.value = tmpPicture.map(item => item.url) - // 如果 filmPictureList.value是空的情况下,则把封面放入到图片列表中 - // debugger; - if (filmPictureList.value.length === 0) { - filmPictureList.value.push(data.coverUrl) + code, data + } = await $http.request('get', '/api/filmWorks/list/view', { + params: { + id: id } - }else{ - if (filmPictureList.value.length === 0) { - filmPictureList.value.push(data.coverUrl) + }) + if (code == 0) { + filmInfo.value = data + console.log("详情", filmInfo.value) + if (data && data.filmPictures) { + // 只获取里面的url + // filmPictureList.value=JSON.parse(data.filmPictures) as Array<FilmPicture> + const tmpPicture = JSON.parse(data.filmPictures) as FilmPicture[] + filmPictureList.value = tmpPicture.map(item => item.url) + // 如果 filmPictureList.value是空的情况下,则把封面放入到图片列表中 + // debugger; + if (filmPictureList.value.length === 0) { + filmPictureList.value.push(data.coverUrl) + } + } else { + if (filmPictureList.value.length === 0) { + filmPictureList.value.push(data.coverUrl) + } } + console.log("图片列表", filmPictureList.value) + } else { + $message.showToast('系统异常,无法获取数据') + return null; } - console.log("图片列表",filmPictureList.value) - } else { - $message.showToast('系统异常,无法获取数据') - return null; - } } </script> diff --git a/types/index.ts b/types/index.ts index 27e31d1..aa993f3 100644 --- a/types/index.ts +++ b/types/index.ts @@ -229,3 +229,74 @@ createTime: string; updateBy: string | null; } + + +/** + * 评论 + */ +export interface CommentDTO { + /** 评论编号 */ + id?: number; + + /** 父评论id */ + parentId?: number; + + /** 评论内容 */ + content?: string; + + /** 被评论帖子id */ + filmId?: number; + + /** 状态(0禁用,1启用) */ + state?: boolean; + + /** 逻辑删除(0正常,1删除) */ + deleted?: boolean; + + /** 评论用户id */ + createBy?: string; + + /** 评论用户名称 */ + commentUserName?: string; + + /** 用户头像 */ + picture?: string; + + /** 等级(Lv6) */ + level?: string; + + /** 是否点赞 */ + isLike?: boolean; + + /** 点赞数量 */ + likeCount?: number; // Long 对应 number + + /** 回复数量 */ + repliesCount?: number; + + /** 评论深度 */ + depth?: number; + + /** 创建时间 */ + createTime?: string; // LocalDateTime 转为 ISO 字符串 + + /** 更新时间 */ + updateTime?: string; + + /** 子评论 */ + child?: CommentDTO[]; + /** + * 图片列表 + */ + fileList?:string[]; + + filmPictures?:string; + + } + + interface FileItem { + name: string; + size: number; + url: string; + status: string; + } \ No newline at end of file -- Gitblit v1.9.3