components/comment/comment-item.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
components/comment/comment-popup.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
components/comment/comment-sub-item.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
composables/usePlatformLoginType.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
package.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
plugins/storage.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
sub-pages/film-list/film-detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
types/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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> .sub-comment { margin-left: 60rpx; margin-top: 10rpx; } } </style> 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> 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<{ composables/usePlatformLoginType.ts
@@ -13,7 +13,7 @@ apitype.value = 'loginPartner' // #endif // #ifdef PUB_FILM // #ifdef PUB_CUSTOMER apitype.value = 'loginCustomer' // #endif package.json
@@ -9,7 +9,7 @@ "PUB_TYPE": "film" }, "define": { "PUB_FILM": true "PUB_CUSTOMER": true } } } 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 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> 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; }