This commit is contained in:
WanP
2025-10-27 15:03:40 +08:00
parent 45ceb9c77d
commit 5eb5c59e4b
18 changed files with 1340 additions and 636 deletions

102
src/api/api.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* 获取H5帖子列表
* @param {Object} data - 请求参数
* @returns {Promise} uni.request返回的Promise对象
*/
export const getPostList = data => {
return uni.request({
url: '/api/v1/h5-posts',
method: 'get',
data: data,
header: {
'Content-Type': 'application/json',
'platform': 'H5'
}
})
}
export const getPostLImage = filename => {
return uni.request({
url: '/api/v1/h5-content/' + filename,
method: 'get',
header: {
'Content-Type': 'application/json',
'platform': 'H5'
},
responseType: 'arraybuffer',
timeout: 60000 // 添加超时设置
})
}
export const getPostVideo = filename => {
// 如果filename已经是完整路径直接使用否则拼接路径
let url = filename
if (!filename.startsWith('/')) {
url = '/api/v1/h5-video/' + filename
}
return uni.request({
url: url,
method: 'get',
header: {
'Content-Type': 'application/json',
'platform': 'H5'
},
responseType: 'arraybuffer',
timeout: 60000 // 添加超时设置
})
}
/**
* 获取用户头像图片
* @param {string} filename - 图片文件名或完整路径
* @returns {Promise} uni.request返回的Promise对象
*/
export const getUserImg = filename => {
// 处理filename只保留default_avatar.png字段
let processedFilename = filename
// 如果传入的是完整路径,提取文件名
if (filename.includes('/')) {
const parts = filename.split('/')
processedFilename = parts[parts.length - 1]
}
const url = '/api/v1/h5-avatar/' + processedFilename
// console.log(url)
return uni.request({
url: url,
method: 'GET',
header: {
'Content-Type': 'application/json',
'platform': 'H5'
},
responseType: 'arraybuffer',
timeout: 60000 // 添加超时设置
})
}
/**
* 获取H5评论列表
* @param {Object} params - 请求参数
* @returns {Promise} uni.request返回的Promise对象
*/
export const getCommentList = params => {
return uni.request({
url: '/api/v1/h5-comments?postId=' + params.postId,
method: 'get',
header: {
'Content-Type': 'application/json',
'platform': 'H5'
},
timeout: 60000 // 添加超时设置
})
}
// 默认导出所有接口
export default {
getPostList,
getCommentList,
getPostLImage,
getPostVideo,
getUserImg
}

View File

@@ -9,13 +9,13 @@
{ {
"path": "pages/post", "path": "pages/post",
"style": { "style": {
"navigationBarTitleText": "帖子" "navigationBarTitleText": ""
} }
}, },
{ {
"path": "pages/video", "path": "pages/video",
"style": { "style": {
"navigationBarTitleText": "视频" "navigationBarTitleText": ""
} }
}, },
{ {
@@ -39,7 +39,13 @@
{ {
"path": "pages/news", "path": "pages/news",
"style": { "style": {
"navigationBarTitleText": "新闻" "navigationBarTitleText": ""
}
},
{
"path": "pages/intereact/intereact",
"style": {
"navigationBarTitleText": ""
} }
} }
], ],

View File

@@ -1,9 +1,10 @@
<template> <template>
<!-- 评论区域 --> <!-- 评论区域 -->
<view v-if="comments" class="comment"> <view class="comment">
<main>
<!-- 评论头部信息 --> <!-- 评论头部信息 -->
<view class="commenthead"> <view class="commenthead">
<text class="commentcount">{{ totalCommentCount }}条评论</text> <text class="commentcount">{{ comments.length }}条评论</text>
<view class="headswitch"> <view class="headswitch">
<text class="inact" @tap="handleOpenApp">默认</text> <text class="inact" @tap="handleOpenApp">默认</text>
<view class="act" @tap="handleOpenApp">最新</view> <view class="act" @tap="handleOpenApp">最新</view>
@@ -11,10 +12,13 @@
</view> </view>
<!-- 评论主体 --> <!-- 评论主体 -->
<view v-for="commentItem in highlightedList" :key="commentItem.id" class="commentdetail" @tap="onDelegateTap">
<view v-if="comments.list.length > 0">
<view v-for="commentItem in comments.list" :key="commentItem.id" class="commentdetail" @tap="onDelegateTap">
<!-- 头像 --> <!-- 头像 -->
<view class="commentdetailleft"> <view class="commentdetailleft">
<image src="/static/logo.png" mode="aspectFill" alt="用户头像"></image> <image :src="commentItem.userAvatar" mode="aspectFill" alt="用户头像"></image>
</view> </view>
<!-- 右侧主评论 + 子评论 + 展开条 --> <!-- 右侧主评论 + 子评论 + 展开条 -->
@@ -25,8 +29,10 @@
<text class="commentusername">{{ commentItem.userName }}</text> <text class="commentusername">{{ commentItem.userName }}</text>
<rich-text class="commentusercontent" :nodes="commentItem.renderNodes" /> <rich-text class="commentusercontent" :nodes="commentItem.renderNodes" />
<view class="date-reply"> <view class="date-reply">
<uni-dateformat :date="Date.parse(commentItem.date.replace(/-/g, '/'))" :threshold="[0, 0]" <uni-dateformat v-if="commentItem.createdAt"
format="yyyy-MM-dd" class="date-text" /> :date="Date.parse(commentItem.createdAt.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd"
class="date-text" />
<text v-else class="date-text">未知时间</text>
<text class="replytext" @tap.stop="handleOpenApp">回复</text> <text class="replytext" @tap.stop="handleOpenApp">回复</text>
</view> </view>
</view> </view>
@@ -41,14 +47,15 @@
<view v-if="commentItem.showChild" class="commentchildcontainer"> <view v-if="commentItem.showChild" class="commentchildcontainer">
<view v-for="child in commentItem.children" :key="child.id" class="commentchild"> <view v-for="child in commentItem.children" :key="child.id" class="commentchild">
<view class="commentchildleft"> <view class="commentchildleft">
<image src="/static/logo.png" mode="aspectFill" alt="用户头像"></image> <image :src="child.userAvatar || '/static/logo.png'" mode="aspectFill" alt="用户头像"></image>
</view> </view>
<view class="commentchildright"> <view class="commentchildright">
<text class="commentusername">{{ child.userName }}</text> <text class="commentusername">{{ child.userName }}</text>
<rich-text class="commentusercontent" :nodes="child.renderNodes" /> <rich-text class="commentusercontent" :nodes="child.renderNodes" />
<view class="date-reply"> <view class="date-reply">
<uni-dateformat :date="Date.parse(child.date.replace(/-/g, '/'))" :threshold="[0, 0]" <uni-dateformat v-if="child.date" :date="Date.parse(child.date.replace(/-/g, '/'))"
format="yyyy-MM-dd" class="date-text" /> :threshold="[0, 0]" format="yyyy-MM-dd" class="date-text" />
<text v-else class="date-text">未知时间</text>
<text class="replytext" @tap.stop="handleOpenApp">回复</text> <text class="replytext" @tap.stop="handleOpenApp">回复</text>
</view> </view>
</view> </view>
@@ -60,70 +67,65 @@
</view> </view>
<!-- 展开/收起按钮 --> <!-- 展开/收起按钮 -->
<view v-if="commentItem.children.length" class="expandcomment"> <view v-if="commentItem.reply.length" class="expandcomment">
<view style="width:20px;height:1px;background:rgba(65,60,67,.2)"></view> <view style="width:20px;height:1px;background:rgba(65,60,67,.2)"></view>
<text class="expandcommenttext" :data-cid="commentItem.id"> <text class="expandcommenttext" :data-cid="commentItem.id">
{{ commentItem.showChild ? '收起' : `展开${commentItem.children.length}条回复` }} {{ commentItem.showChild ? '收起' : `展开${commentItem.reply.length}条回复` }}
</text> </text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 占位 -->
<view v-else class="nocomments">
<image src="/static/imgs/nocomments/nocomments@3x.webp" mode="aspectFit" alt="暂无评论"></image>
<text class="nocommentstext">空空如也~</text>
</view>
<!-- 占位视图 -->
<view class="spacerview"></view> <view class="spacerview"></view>
</main>
<!-- 互动区域 --> <!-- 互动区域 -->
<view v-if="showInteraction" class="interaction"> <Intereact />
<view class="editarea" @tap="handleOpenApp">
<image src="/static/imgs/editicon/icon@2x.webp" mode="aspectFit" class="editicon" alt="编辑标签"></image>
<text class="edittext">快来互动吧</text>
</view>
<view class="spacerview"></view>
<view class="collection" @tap="handleOpenApp">
<image src="@/static/imgs/staricon/icon@3x.webp" mode="aspectFit" class="collectionicon" alt="收藏标签"></image>
<text class="collectioncount">{{ formatCount(collectsum) }}</text>
</view>
<view class="spacerview"></view>
<view class="like" @tap="handleOpenApp">
<image src="@/static/imgs/likeicon/icon@3x.webp" mode="aspectFit" class="likeicon" alt="点赞标签"></image>
<text class="likecount">{{ formatCount(likesum) }}</text>
</view>
</view> </view>
<!-- Findmore -->
<Findmore />
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getCommentList, getUserImg } from '@/api/api.js'
import { useCommonStore } from '@/stores/common.js' import { useCommonStore } from '@/stores/common.js'
import Findmore from '@/pages/findmore/findmore.vue'
const props = defineProps({ import Intereact from '@/pages/intereact/intereact.vue'
comments: {
type: Array,
default: () => []
},
showInteraction: {
type: Boolean,
default: false
},
collectsum: {
type: Number,
default: 0
},
likesum: {
type: Number,
default: 0
}
})
const common = useCommonStore() const common = useCommonStore()
const comments = reactive({
list: [],
length: 0
})
const props = defineProps({
postid: {
type: [String, Number],
default: ''
}
})
// 处理评论点击事件 // 处理评论点击事件
function onDelegateTap(e) { function onDelegateTap(e) {
const cid = e.target?.dataset?.cid const cid = e.target?.dataset?.cid
if (!cid || !props.comments) return if (!cid || !comments.list) return
const item = props.comments.find(v => v.id == cid) const item = comments.list.find(v => v.id == cid)
if (!item || !item.children || !item.children.length) return if (!item || !item.reply || !item.reply.length) return
item.showChild = !item.showChild item.showChild = !item.showChild
} }
@@ -137,31 +139,146 @@ function formatCount(count) {
return common.formatCount(count) return common.formatCount(count)
} }
// 处理评论数据,添加渲染节点 onLoad(() => {
const highlightedList = computed(() => const params = {
props.comments ? props.comments.map(c => ({ postId: props.postid
...c, }
renderNodes: [ getCommentList(params).then(res => {
...common.atUsersToNodes(c.atUsers), try {
{ type: 'text', text: c.content } // 检查响应状态
], if (res.statusCode === 200 && res.data) {
children: c.children ? c.children.map(child => ({ console.log(res.data)
...child, comments.list = res.data.list || []
renderNodes: [ comments.length = res.data.total || 0
...common.atUsersToNodes(child.atUsers),
{ type: 'text', text: child.content } // 处理每条评论
] comments.list.forEach(comment => {
})) : [] processComment(comment)
})) : [] })
) } else {
throw new Error(`请求失败: ${res.statusCode}`)
}
} catch (error) {
console.error('处理评论数据时出错:', error)
uni.showToast({
title: '加载评论失败',
icon: 'error'
})
// 设置默认空数据
comments.list = []
comments.length = 0
}
}).catch(error => {
console.error('获取评论列表失败:', error)
uni.showToast({
title: '网络连接异常',
icon: 'error'
})
// 设置默认空数据
comments.list = []
comments.length = 0
})
})
/**
* 处理单条评论数据
* @param {Object} comment - 评论对象
*/
function processComment(comment) {
// 确保comment对象有必要的属性
if (!comment.children || comment.children.length === 0) {
comment.showChild = false
}
// 确保user对象存在
if (!comment.user) {
comment.user = {}
}
// 设置评论基本属性
comment.content = comment.content || ''
comment.likeCount = comment.likeCount || 0
comment.userName = comment.user.nickName || '匿名用户'
comment.createdAt = comment.createdAt || comment.date || ''
// 生成renderNodes用于富文本渲染
comment.renderNodes = common.contentToRenderNodes(comment.content, comment.atUsers)
// 处理用户头像
handleCommentAvatar(comment)
// 处理子评论
if (comment.reply && comment.reply.length > 0) {
// 将reply赋值给children确保模板和代码一致
comment.children = comment.reply
comment.children.forEach(childComment => {
processChildComment(childComment)
})
}
}
/**
* 处理子评论数据
* @param {Object} childComment - 子评论对象
*/
function processChildComment(childComment) {
// 确保user对象存在
if (!childComment.user) {
childComment.user = {}
}
// 设置子评论基本属性
childComment.content = childComment.content || ''
childComment.likeCount = childComment.likeCount || 0
childComment.userName = childComment.user.nickName || '匿名用户'
childComment.date = childComment.createdAt || childComment.date || ''
// 处理子评论头像
handleCommentAvatar(childComment)
// 为子评论生成renderNodes
childComment.renderNodes = common.contentToRenderNodes(childComment.content, childComment.atUsers)
}
/**
* 处理评论头像
* @param {Object} comment - 评论对象(主评论或子评论)
*/
function handleCommentAvatar(comment) {
// 如果已经有头像URL直接使用
if (comment.userAvatar) {
return
}
// 确保有头像路径时才请求用户信息
const avatarPath = comment.user.avatar
if (avatarPath) {
getUserImg(avatarPath).then(res => {
try {
if (res.statusCode === 200 && res.data) {
// 将arrayBuffer转换为base64
const base64 = uni.arrayBufferToBase64(res.data)
const userImgUrl = 'data:image/webp;base64,' + base64
comment.userAvatar = userImgUrl
} else {
// 使用原始头像URL作为备选
comment.userAvatar = avatarPath || ''
}
} catch (error) {
console.warn('处理用户头像信息失败:', error)
comment.userAvatar = avatarPath || ''
}
}).catch(error => {
console.warn('获取用户头像失败:', error)
comment.userAvatar = avatarPath || ''
})
} else {
// 没有头像路径时使用空字符串
comment.userAvatar = ''
}
}
// 总评论数 = 主评论条数 + 所有子评论条数
const totalCommentCount = computed(() =>
props.comments ? props.comments.reduce(
(sum, c) => sum + 1 + (c.children ? c.children.length : 0),
0
) : 0
)
</script> </script>
@@ -175,11 +292,16 @@ const totalCommentCount = computed(() =>
.comment { .comment {
width: 100%; width: 100%;
max-width: 430px; max-width: 430px;
padding: 16px; height: 50vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 auto; margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
position: relative;
}
main {
padding: 16px;
} }
.commenthead { .commenthead {
@@ -354,13 +476,12 @@ const totalCommentCount = computed(() =>
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 14px 0 0;
} }
.commentchild { .commentchild {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 8px; /* margin-bottom: 8px; */
} }
.commentchildleft { .commentchildleft {
@@ -388,6 +509,8 @@ const totalCommentCount = computed(() =>
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
transition: all 0.5s ease;
cursor: pointer;
} }
.expandcommenttext { .expandcommenttext {
@@ -402,83 +525,25 @@ const totalCommentCount = computed(() =>
color: #110c13; color: #110c13;
} }
.interaction { .nocomments {
width: 100%;
max-width: 430px;
border: solid 1px #faf9fb;
background-color: #faf9fb;
display: flex; display: flex;
align-items: center; flex-direction: column;
padding: 11.5px 16px; text-align: center;
flex-direction: row;
box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
} max-width: 100%;
.editarea {
flex: 1;
height: 40px;
background-color: #fff;
border-radius: 24px;
gap: 12px; gap: 12px;
padding: 0 20px; padding: 12px;
box-sizing: border-box;
display: flex;
align-items: center;
cursor: pointer;
} }
.editicon { .nocommentstext {
width: 13.3px;
height: 14.6px;
flex-shrink: 0;
align-self: center;
}
.edittext {
flex: 1;
flex-grow: 0;
font-size: 14px; font-size: 14px;
font-family: 'PingFangSC';
font-weight: normal; font-weight: normal;
font-stretch: normal; font-stretch: normal;
font-style: normal; font-style: normal;
line-height: 40px;
letter-spacing: normal;
text-align: left;
color: #918e93;
white-space: nowrap;
}
.collection,
.like {
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
}
.collectionicon,
.likeicon {
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.collectioncount,
.likecount {
height: 17px;
font-family: 'SFPro';
font-size: 14px;
font-weight: 500;
font-stretch: normal;
font-style: normal;
line-height: normal; line-height: normal;
letter-spacing: normal; letter-spacing: normal;
text-align: left; text-align: center;
color: #000; color: #918e93;
} }
</style> </style>

View File

@@ -0,0 +1,141 @@
<template>
<!-- 互动区域 -->
<view class="interaction">
<view class="editarea" @tap="handleOpenApp">
<image src="/static/imgs/editicon/icon@2x.webp" mode="aspectFit" class="editicon" alt="编辑标签"></image>
<text class="edittext">快来互动吧</text>
</view>
<view class="spacerview"></view>
<view class="collection" @tap="handleOpenApp">
<image src="@/static/imgs/staricon/icon@3x.webp" mode="aspectFit" class="collectionicon" alt="收藏标签"></image>
<text class="collectioncount">{{ formatCount(collectsum) }}</text>
</view>
<view class="spacerview"></view>
<view class="like" @tap="handleOpenApp">
<image src="@/static/imgs/likeicon/icon@3x.webp" mode="aspectFit" class="likeicon" alt="点赞标签"></image>
<text class="likecount">{{ formatCount(countLike) }}</text>
</view>
</view>
</template>
<script setup>
import { useCommonStore } from '@/stores/common.js'
const props = defineProps({
countLike: {
type: Number,
default: 0
},
collectsum: {
type: Number,
default: 0
}
})
const common = useCommonStore()
function handleOpenApp() {
common.openapp()
}
// 格式化数字显示
function formatCount(count) {
return common.formatCount(count)
}
</script>
<style scoped>
.spacerview {
flex: 1;
pointer-events: none
}
.interaction {
width: 100%;
max-width: 430px;
display: flex;
align-items: center;
box-sizing: border-box;
margin: auto auto 0;
}
.interaction {
width: 100%;
max-width: 430px;
border: solid 1px #faf9fb;
background-color: #faf9fb;
display: flex;
align-items: center;
padding: 11.5px 16px;
flex-direction: row;
box-sizing: border-box;
margin: auto auto 0;
}
.editarea {
flex: 1;
height: 40px;
background-color: #fff;
border-radius: 24px;
gap: 12px;
padding: 0 20px;
box-sizing: border-box;
display: flex;
align-items: center;
cursor: pointer;
}
.editicon {
width: 13.3px;
height: 14.6px;
flex-shrink: 0;
align-self: center;
}
.edittext {
flex: 1;
flex-grow: 0;
font-size: 14px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 40px;
letter-spacing: normal;
text-align: left;
color: #918e93;
white-space: nowrap;
}
.collection,
.like {
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
}
.collectionicon,
.likeicon {
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.collectioncount,
.likecount {
height: 17px;
font-family: 'SFPro';
font-size: 14px;
font-weight: 500;
font-stretch: normal;
font-style: normal;
line-height: normal;
letter-spacing: normal;
text-align: left;
color: #000;
}
</style>

View File

@@ -1,22 +1,30 @@
<template> <template>
<view class="page"> <view v-if="loading" class="page">
<Head></Head> <Head></Head>
<!-- 内容区 --> <!-- 内容区 -->
<view class="content"> <main class="content">
<!-- 新闻区域 --> <!-- 新闻区域 -->
<view class="news"> <section class="news">
<!-- 轮播图 --> <!-- 轮播图 -->
<swiper v-if="post.imgs && post.imgs.length" :indicator-dots="false" :autoplay="false" :interval="3000" <swiper v-if="post.imgs && post.imgs.length > 0" :indicator-dots="false" :autoplay="false" :interval="3000"
:duration="1000" :circular="true" class="swiper-banner" @change="onChange"> :duration="1000" :circular="true" class="swiper-banner" @change="onChange">
<swiper-item v-for="(item, i) in post.imgs" :key="i"> <swiper-item v-for="(item, i) in post.imgs" :key="i">
<view class="swiper-center"> <view class="swiper-center">
<image v-if="item.type === 'img'" :src="item.src" class="swiper-item" mode="aspectFit" alt="新闻图片" /> <image v-if="item.type === 'img'" :src="item.src" class="swiper-item" mode="aspectFit" alt="新闻图片"
<video v-else :src="item.src" class="swiper-item" object-fit="contain" muted style="pointer-events:none;" @load="onImageLoad(i)" @error="onImageError(i)" />
alt="新闻视频" /> <video v-else-if="item.type === 'video'" :src="item.src" class="swiper-item" object-fit="contain" muted
style="pointer-events:none;" alt="新闻视频" />
<cover-view class="swiper-mask" @tap="common.openapp"> <cover-view class="swiper-mask" @tap="common.openapp">
</cover-view> </cover-view>
<!-- 图片加载状态 -->
<view v-if="item.loading" class="image-loading">
<text>图片加载中...</text>
</view>
<view v-if="item.error" class="image-error">
<text>图片加载失败</text>
</view>
</view> </view>
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -31,16 +39,14 @@
<text class="title">{{ post.title }}</text> <text class="title">{{ post.title }}</text>
</view> </view>
<!-- 底部文案 --> <!-- 新闻信息 -->
<view v-if="post.copywriting && post.date" class="newsbottom"> <view v-if="post.copywriting && post.date" class="newsbottom">
<text class="copywriting">{{ post.copywriting }}</text> <text class="copywriting">{{ post.copywriting }}</text>
<view class="meta-info"> <view class="meta-info">
<text v-if="post.source" class="source">{{ post.source }}</text> <text v-if="post.source" class="source">{{ post.source }}</text>
<text v-if="post.source" class="date-text">·</text> <text v-if="post.source" class="date-text">·</text>
<view class="datetextview"> <view class="datetextview">
<uni-dateformat :date="Date.parse(post.date.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd" <text class="date-text">{{ common.formatDate(post.time) }}</text>
class="date-text" />
<text class="date-text">{{ post.time }}</text>
</view> </view>
<view class="spacerview"></view> <view class="spacerview"></view>
<view class="toseeall" @tap="handleInteraction"> <view class="toseeall" @tap="handleInteraction">
@@ -51,16 +57,26 @@
</view> </view>
</view> </view>
</view> </section>
</view> </main>
<!-- 评论区域 + 互动区域 --> <!-- 评论区域 + 互动区域 -->
<Comments :comments="post.comments" :showInteraction="true" :collectsum="post.collectsum" :likesum="post.likesum" /> <Comments :postid="post.id" />
<!-- 互动区域 -->
<!-- <Intereact :countLike="post.countLike" :collectsum="post.collectsum" /> -->
<!-- Findmore --> <!-- Findmore -->
<Findmore /> <Findmore />
</view> </view>
<!-- 错误显示 -->
<!-- <view v-else class="errorload">
<image src="/static/imgs/errorload/errorload@3x.webp" mode="aspectFit" alt="加载失败"></image>
</view> -->
</template> </template>
<script setup> <script setup>
@@ -70,6 +86,9 @@ import { useCommonStore } from '@/stores/common.js'
import Head from '@/pages/head/head.vue' import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Findmore from '@/pages/findmore/findmore.vue' import Findmore from '@/pages/findmore/findmore.vue'
import { getPostList, getPostLImage, getPostVideo } from '../api/api.js'
// import Intereact from '@/pages/intereact/intereact.vue'
const common = useCommonStore() const common = useCommonStore()
// 当前 dot // 当前 dot
@@ -78,107 +97,135 @@ const onChange = e => current.value = e.detail.current
// 动态数据 // 动态数据
const post = ref({}) const post = ref({})
// 资源加载状态
const loading = ref(false)
// 模拟获取数据函数 // 图片加载成功处理
const fetchPostData = async () => { const onImageLoad = (index) => {
try { if (post.value.imgs && post.value.imgs[index]) {
// 使用uni.request替代axios post.value.imgs[index].loading = false
const response = await new Promise((resolve, reject) => { post.value.imgs[index].error = false
uni.request({ }
url: '', // 这里可以填写实际API地址 }
method: 'GET',
timeout: 1000, // 图片加载失败处理
success: resolve, const onImageError = (index) => {
fail: reject if (post.value.imgs && post.value.imgs[index]) {
}) post.value.imgs[index].loading = false
}) post.value.imgs[index].error = true
console.warn(`图片加载失败: index ${index}`)
// 处理数据...
const mockData = {
userId: 1000011,
userName: 'Kun Chang-Min',
userImg: '/static/logo.png',
date: '2024-12-03',
time: '14:00',
title: '智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。',
source: '36氪',
copywriting: '智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。',
collectsum: 114514,
likesum: 114514,
imgs: [
{ type: 'video', src: '/static/videos/beauty.mp4' }
],
comments: [
{
id: 101,
userName: 'Brad Lewin',
atUsers: ['JackyLove'],
content: '今天天气真不错~',
date: '2024-12-01',
likeCount: 99999,
showChild: false,
children: [
{ id: 201, userName: 'Alice', content: '快智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。智能体是人类智慧的延伸', atUsers: ['Brad Lewin', 'JackyLove'], date: '2024-12-01', likeCount: 2 },
{ id: 202, userName: 'Bob', content: '我同意你的观点。我记得喜欢这个版本。这个新版本对我来说Nothing好处', atUsers: [], date: '2024-12-01', likeCount: 1 }
]
},
{
id: 102,
userName: 'Leanne Simpson',
atUsers: ['Theshy'],
content: '有人一起开黑吗?',
date: '2024-12-02',
likeCount: 1145140,
showChild: false,
children: [
{ id: 203, userName: 'Carol', content: '带我一个', atUsers: [], date: '2024-12-02', likeCount: 3 }
]
},
{
id: 103,
userName: 'Tom Hardy',
atUsers: [],
content: '主评论3分享一张今晚的月亮。',
date: '2024-12-03',
likeCount: 999,
showChild: false,
children: []
}
]
}
// 将模拟数据赋值给post
post.value = mockData
} catch (error) {
console.error('获取数据失败:', error)
// 如果获取失败,使用默认数据
post.value = {
userId: 1000011,
userName: 'Kun Chang-Min',
userImg: '/static/logo.png',
date: '2024-12-03',
title: '人工智能技术的最新突破',
source: '科技日报',
copywriting: '智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。',
collectsum: 114514,
likesum: 114514,
imgs: [
{ type: 'img', src: '/static/logo.png' }
],
comments: []
}
} }
} }
// 处理点击查看全文
const handleInteraction = () => { const handleInteraction = () => {
common.openapp(); common.openapp();
} }
// 组件挂载时获取数据 // 组件挂载时获取数据
onLoad(() => { onLoad(() => {
fetchPostData() const params = {
newsFilter: 'news_only'
}
getPostList(params).then(res => {
try {
const data = res.data.data
// console.log(data)
// 处理新闻相关字段 - 根据实际API返回字段优化
data.id = data.id || ''
data.title = data.newsTitle || data.title || ''
data.source = data.newsSource || data.source || ''
data.copywriting = data.newsContent || data.textContent || data.copywriting || ''
data.date = data.time || data.date || ''
data.countLike = data.likeCount || data.countLike || 0
data.collectsum = data.collectsum || 0
data.comments = data.comments || []
const mediaPromises = []
// 处理图片资源
if (data.images && Array.isArray(data.images)) {
data.images.forEach(image => {
mediaPromises.push(getPostLImage(image.original_url).then(imageRes => {
if (imageRes.statusCode === 200 && imageRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(imageRes.data)
const imageUrl = 'data:image/webp;base64,' + base64
return {
type: 'img',
src: imageUrl,
original_url: image.original_url,
loading: true, // 初始加载状态
error: false // 错误状态
}
}
return null
}).catch(error => {
console.warn('图片资源加载失败:', error)
return null
}))
}) })
}
//处理视频资源
if (data.video && data.video.original_url) {
mediaPromises.push(getPostVideo(data.video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64
return {
type: 'video',
src: videoUrl,
original_url: data.video.original_url,
loading: true, // 初始加载状态
error: false // 错误状态
}
}
return null
}).catch(error => {
console.warn('视频资源加载失败:', error)
return null
}))
}
// 统一等待所有媒体资源加载完成
if (mediaPromises.length > 0) {
Promise.all(mediaPromises).then(mediaItems => {
// 过滤掉null值将有效的媒体资源合并到imgs数组中
data.imgs = mediaItems.filter(item => item !== null)
post.value = data
loading.value = true
})
} else {
// 如果没有媒体资源,设置空数组
data.imgs = []
post.value = data
}
} catch (error) {
handleError('数据处理错误:', error, '数据加载失败', '抱歉,新闻内容加载失败,请稍后重试')
}
}).catch(error => {
handleError('网络请求错误:', error, '网络错误', '网络连接异常,请检查网络设置')
})
// 统一的错误处理函数
const handleError = (errorType, error, title, message) => {
console.error(errorType, error)
post.value = {
title: title,
source: '系统',
copywriting: message,
date: new Date().toISOString(),
countLike: 0,
collectsum: 0,
comments: [],
imgs: []
}
}
})
</script> </script>
@@ -194,7 +241,7 @@ page,
position: relative; position: relative;
} }
.content { main {
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@@ -320,9 +367,36 @@ swiper-item {
gap: 8px; gap: 8px;
} }
/* 图片加载状态样式 */
.image-loading,
.image-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
z-index: 10;
}
.image-error {
background: rgba(255, 0, 0, 0.7);
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
font-size: 16px;
color: #666;
}
.spacerview { .spacerview {
flex: 1; flex: 1;
height: 0;
pointer-events: none pointer-events: none
} }
@@ -345,4 +419,19 @@ swiper-item {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
.errorload {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
.errorload image {
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page"> <view v-if="loading" class="page">
<Head></Head> <Head></Head>
<!-- 内容区 --> <!-- 内容区 -->
@@ -8,8 +8,10 @@
<view class="moment"> <view class="moment">
<!-- 用户栏 --> <!-- 用户栏 -->
<view class="userbar"> <view class="userbar">
<image :src="post.userImg" mode="aspectFill" class="userimg" alt="用户头像" /> <image v-if="post.user && post.user.userImg" :src="post.user.userImg" mode="aspectFill" class="userimg"
<text class="username">{{ post.userName }}</text> alt="用户头像" />
<image v-else src="/static/imgs/default-avatar.png" mode="aspectFill" class="userimg" alt="默认头像" />
<text class="username">{{ post.user ? post.user.nickName : '未知用户' }}</text>
<button class="follow" @tap="common.openapp"> <button class="follow" @tap="common.openapp">
<uni-icons v-if="post.isfollow" type="checkmarkempty" size="20" color="#333"></uni-icons> <uni-icons v-if="post.isfollow" type="checkmarkempty" size="20" color="#333"></uni-icons>
<text v-else>关注</text> <text v-else>关注</text>
@@ -17,15 +19,28 @@
</view> </view>
<!-- 轮播图 --> <!-- 轮播图 -->
<swiper v-if="post.imgs && post.imgs.length" :indicator-dots="false" :autoplay="false" :interval="3000" <swiper v-if="post.imgs && post.imgs.length > 0" :indicator-dots="false" :autoplay="false" :interval="3000"
:duration="1000" :circular="true" class="swiper-banner" @change="onChange"> :duration="1000" :circular="true" class="swiper-banner" @change="onChange">
<swiper-item v-for="(item, i) in post.imgs" :key="i"> <swiper-item v-for="(item, i) in post.imgs" :key="i">
<view class="swiper-center"> <view class="swiper-center">
<image v-if="item.type === 'img'" :src="item.src" class="swiper-item" mode="aspectFit" alt="动态图片内容" /> <image v-if="item.type === 'img'" :src="item.src" class="swiper-item" mode="aspectFit" alt="动态图片内容"
<video v-else :src="item.src" class="swiper-item" object-fit="contain" muted style="pointer-events:none;" @load="onImageLoad(i)" @error="onImageError(i)" />
alt="动态视频内容" /> <video v-else-if="item.type === 'video'" id="videoid" :src="item.src" class="swiper-item"
<cover-view class="swiper-mask" @tap="common.openapp"> :controls="false" @tap="pausevideo" object-fit="contain" alt="动态视频内容" />
</cover-view> <!-- 播放按钮 - 使用浏览器复制的样式 -->
<view v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo">
<view class="uni-video-cover-play-button uni-video-icon"></view>
</view>
<!-- <cover-view class="swiper-mask" @tap="common.openapp">
</cover-view> -->
<!-- 图片加载状态 -->
<!-- <view v-if="item.loading" class="image-loading">
<text>图片加载中...</text>
</view>
<view v-if="item.error" class="image-error">
<text>图片加载失败</text>
</view> -->
</view> </view>
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -47,21 +62,30 @@
</view> </view>
<!-- 评论区域 + 互动区域 --> <!-- 评论区域 + 互动区域 -->
<Comments :comments="post.comments" :showInteraction="true" :collectsum="post.collectsum" :likesum="post.likesum" /> <Comments :postid="post.id" />
<!-- 互动区域 -->
<!-- <Intereact :collectsum="post.collectsum" :likesum="post.likesum" /> -->
<!-- Findmore --> <!-- Findmore -->
<Findmore /> <Findmore />
</view> </view>
<view v-else class="loading-container">
<text>页面加载中...</text>
</view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { useCommonStore } from '@/stores/common.js' import { useCommonStore } from '@/stores/common.js'
import Head from '@/pages/head/head.vue' import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Findmore from '@/pages/findmore/findmore.vue' import Findmore from '@/pages/findmore/findmore.vue'
import { getPostList, getPostLImage, getPostVideo, getUserImg } from '../api/api.js'
const common = useCommonStore() const common = useCommonStore()
// 当前 dot // 当前 dot
@@ -70,102 +94,203 @@ const onChange = e => current.value = e.detail.current
// 动态数据 // 动态数据
const post = ref({}) const post = ref({})
// 播放状态
const isPlaying = ref(true) // 默认播放状态
// 资源加载状态
const loading = ref(false)
// 模拟获取数据函数 // 图片加载成功处理
const fetchPostData = async () => { const onImageLoad = (index) => {
try { if (post.value.imgs && post.value.imgs[index]) {
// 使用uni.request替代axios post.value.imgs[index].loading = false
const response = await new Promise((resolve, reject) => { post.value.imgs[index].error = false
uni.request({
url: '', // 这里可以填写实际API地址
method: 'GET',
timeout: 1000,
success: resolve,
fail: reject
})
})
// 处理数据...
const mockData = {
userId: 1000011,
userName: 'Kun Chang-Min',
userImg: '/static/logo.png',
date: '2024-12-03',
copywriting: '智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。',
isfollow: false,
collectsum: 114514,
likesum: 114514,
imgs: [
{ type: 'img', src: '/static/logo.png' },
{ type: 'video', src: '/static/videos/beauty.mp4' },
{ type: 'img', src: '/static/imgs/comment.webp' },
{ type: 'img', src: '/static/imgs/image-138.webp' }
],
comments: [
{
id: 101,
userName: 'Brad Lewin',
atUsers: ['JackyLove'],
content: '今天天气真不错~',
date: '2024-12-01',
likeCount: 99999,
showChild: false,
children: [
{ id: 201, userName: 'Alice', content: '快智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。智能体是人类智慧的延伸', atUsers: ['Brad Lewin', 'JackyLove'], date: '2024-12-01', likeCount: 2 },
{ id: 202, userName: 'Bob', content: '我同意你的观点。我记得喜欢这个版本。这个新版本对我来说Nothing好处', atUsers: [], date: '2024-12-01', likeCount: 1 }
]
},
{
id: 102,
userName: 'Leanne Simpson',
atUsers: ['Theshy'],
content: '有人一起开黑吗?',
date: '2024-12-02',
likeCount: 1145140,
showChild: false,
children: [
{ id: 203, userName: 'Carol', content: '带我一个', atUsers: [], date: '2024-12-02', likeCount: 3 }
]
},
{
id: 103,
userName: 'Tom Hardy',
atUsers: [],
content: '主评论3分享一张今晚的月亮。',
date: '2024-12-03',
likeCount: 999,
showChild: false,
children: []
} }
]
} }
// 将模拟数据赋值给post // 图片加载失败处理
post.value = mockData const onImageError = (index) => {
if (post.value.imgs && post.value.imgs[index]) {
post.value.imgs[index].loading = false
post.value.imgs[index].error = true
console.warn(`图片加载失败: index ${index}`)
}
}
} catch (error) { //暂停/播放
console.error('获取数据失败:', error) const pausevideo = () => {
// 如果获取失败,使用默认数据 // 获取VideoContext实例
post.value = { const videoCtx = uni.createVideoContext('videoid', getCurrentInstance());
userId: 1000011, // 检查视频状态并切换播放/暂停
userName: 'Kun Chang-Min', if (videoCtx) {
userImg: '/static/logo.png', // 使用一个状态变量来跟踪播放状态
date: '2024-12-03', if (isPlaying.value) {
copywriting: '智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。', videoCtx.pause()
isfollow: false, isPlaying.value = false
collectsum: 114514, } else {
likesum: 114514, videoCtx.play()
imgs: [ isPlaying.value = true
{ type: 'img', src: '/static/logo.png' }
],
comments: []
} }
} }
} }
// 组件挂载时获取数据 // 组件挂载时获取数据
onLoad(() => { onLoad(() => {
fetchPostData() const params = ''
getPostList(params).then(res => {
try {
// console.log(res.data.data)
const data = res.data.data
// 处理帖子相关字段 - 根据实际API返回字段优化
data.id = data.id || ''
data.title = data.newsTitle || data.title || ''
data.source = data.newsSource || data.source || ''
data.copywriting = data.textContent || data.newsContent || ''
data.date = data.time || data.date || ''
data.countLike = data.likeCount || data.countLike || 0
data.collectsum = data.collectsum || 0
// data.user.nickName = data.user.nickName || ''
// data.user.userImg = data.user.avatar || data.userImg || ''
// data.comments = data.comments || []
const mediaPromises = []
// 处理图片资源
if (data.imgs && Array.isArray(data.imgs)) {
data.imgs.forEach(image => {
mediaPromises.push(getPostLImage(image.original_url).then(imageRes => {
if (imageRes.statusCode === 200 && imageRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(imageRes.data)
const imageUrl = 'data:image/webp;base64,' + base64
return {
type: 'img',
src: imageUrl,
original_url: image.original_url,
loading: true, // 初始加载状态
error: false // 错误状态
}
}
return null
}).catch(error => {
uni.showToast({
title: error.message,
icon: 'error'
})
return null
}))
})
}
//处理视频资源
if (data.videos && Array.isArray(data.videos)) {
data.videos.forEach(video => {
mediaPromises.push(getPostVideo(video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64
return {
type: 'video',
src: videoUrl,
original_url: video.original_url,
loading: true, // 初始加载状态
error: false // 错误状态
}
}
return null
}).catch(error => {
uni.showToast({
title: error.message,
icon: 'error'
})
return null
}))
})
}
//处理用户信息
if (data.user && data.user.constructor === Object) {
// 如果有头像路径,则获取头像资源
const avatarPath = data.user.avatar || data.userImg
if (avatarPath) {
mediaPromises.push(getUserImg(avatarPath).then(userImgRes => {
if (userImgRes.statusCode === 200 && userImgRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(userImgRes.data)
const userImgUrl = 'data:image/webp;base64,' + base64
return {
type: 'userImg',
src: userImgUrl,
original_url: avatarPath
}
}
return null
}).catch(error => {
console.warn('用户头像加载失败:', error)
return null
}))
}
data.user = {
nickName: data.user.nickName || '',
userImg: avatarPath || '' // 先使用原始路径后续会替换为base64
}
}
// 统一等待所有媒体资源加载完成
if (mediaPromises.length > 0) {
Promise.all(mediaPromises).then(mediaItems => {
// 过滤掉null值将有效的媒体资源合并到imgs数组中
const validMediaItems = mediaItems.filter(item => item !== null)
// 分离用户头像和其他媒体资源
const userImgItem = validMediaItems.find(item => item.type === 'userImg')
const otherMediaItems = validMediaItems.filter(item => item.type !== 'userImg')
// 如果有用户头像资源,更新用户头像
if (userImgItem) {
data.user.userImg = userImgItem.src
}
// 设置其他媒体资源
data.imgs = otherMediaItems
// post.value = data
console.log(data)
Object.assign(post.value, data)
loading.value = true
})
} else {
// 如果没有媒体资源,设置空数组
data.imgs = []
post.value = data
}
} catch (error) {
handleError('数据处理错误:', error, '数据加载失败', '抱歉,新闻内容加载失败,请稍后重试')
}
}).catch(error => {
handleError('网络请求错误:', error, '网络错误', '网络连接异常,请检查网络设置')
})
// 统一的错误处理函数
const handleError = (errorType, error, title, message) => {
console.error(errorType, error)
post.value = {
title: title,
source: '系统',
copywriting: message,
date: new Date().toISOString(),
countLike: 0,
collectsum: 0,
comments: [],
imgs: []
}
}
}) })
</script> </script>
@@ -253,7 +378,8 @@ swiper-item {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center justify-content: center;
position: relative;
} }
.swiper-item { .swiper-item {
@@ -261,17 +387,63 @@ swiper-item {
height: 100% height: 100%
} }
.swiper-center { ::v-deep .uni-video-cover {
position: relative; background-color: transparent;
} }
.swiper-mask { ::v-deep .uni-video-cover-play-button {
width: 100% !important;
height: 100% !important;
display: flex;
align-items: center;
justify-content: center;
}
.uni-video-cover {
position: absolute; position: absolute;
left: 0;
top: 0; top: 0;
right: 0; left: 0;
bottom: 0; bottom: 0;
background: transparent; width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
}
.uni-video-icon {
font-family: 'uni-video-icon';
text-align: center;
}
.uni-video-cover-play-button {
width: 100% !important;
height: 100% !important;
display: flex;
align-items: center;
justify-content: center;
line-height: 75px;
font-size: 56px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
}
.uni-video-cover-play-button::after {
content: '\ea24';
}
.uni-video-cover {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
} }
.dots-bar { .dots-bar {
@@ -319,4 +491,11 @@ swiper-item {
font-size: 12px; font-size: 12px;
color: #b1aeb2; color: #b1aeb2;
} }
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style> </style>

View File

@@ -1,31 +1,22 @@
<template> <template>
<view class="videopage"> <main>
<!-- 视频容器 -->
<section class="video-container">
<Head></Head> <Head></Head>
<video id="videoid" ref="videoRef" :src="videoData.videoUrl" :danmu-list="videoData.danmuList" enable-danmu loop
:muted="isMuted" :show-mute-btn="true" :controls="false" object-fit="contain" @tap="pausevideo"></video>
<!-- 播放按钮 - 使用浏览器复制的样式 --> <video id="videoid" :src="videoData.videoUrl" :danmu-list="videoData.danmuList" enable-danmu loop :muted="isMuted"
<view v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo"> :show-mute-btn="true" :controls="false" object-fit="contain" @tap="pausevideo">
<!-- 播放按钮 - 直接使用浏览器复制的样式 -->
<section v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo">
<view class="uni-video-cover-play-button uni-video-icon"></view> <view class="uni-video-cover-play-button uni-video-icon"></view>
</view> </section>
<!-- 视频信息 -->
<view class="videoinfo">
<view class="vedioinfohead">
<text class="username">@{{ videoData.userName }}</text>
<text class="datetime">{{ formatDate(videoData.date) }}</text>
</view>
<view class="content-container"> </video>
<text class="content" :class="{ 'expanded': isExpanded }">{{ videoData.copywriting }}</text>
<view class="flodbtncontainer">
<image v-if="videoData.copywriting.length > 30" class="expand-btn" :class="{ 'rotated': isExpanded }"
:src="isExpanded ? '/static/imgs/foldicon/flodicon@3x.webp' : '/static/imgs/foldicon/flodicon@3x.webp'"
mode="aspectFit" @tap="toggleExpand"></image>
</view>
</view>
</view>
<!-- 右侧交互按钮 --> <!-- 右侧交互按钮 -->
<view class="interaction-panel"> <view class="interaction-panel">
@@ -33,7 +24,7 @@
<view class="user-section"> <view class="user-section">
<image :src="videoData.userImg" class="user-avatar" mode="aspectFill"></image> <image :src="videoData.userImg" class="user-avatar" mode="aspectFill"></image>
<view class="follow-btn-container" @tap="() => handleInteraction('follow')"> <view class="follow-btn-container" @tap="() => handleInteraction('follow')">
<image src="/static/imgs/followbtn/btn@3x.webp" mode="aspectFit"></image> <image src="/static/imgs/followbtn/btn@3x.webp" mode="aspectFit" style="height: 24px;"></image>
</view> </view>
</view> </view>
@@ -60,132 +51,102 @@
class="mutebtn" mode="aspectFit" @tap="toggleMute"></image> class="mutebtn" mode="aspectFit" @tap="toggleMute"></image>
</view> </view>
<!-- 视频信息 -->
<section class="videoinfo">
<view class="vedioinfohead" @tap="() => handleInteraction('user')">
<text class="username">{{ '@' + videoData.userName }}</text>
<time class="datetime">{{ formatDate(videoData.date) }}</time>
</view> </view>
<view class="content-container">
<text class="content" :class="{ 'expanded': isExpanded }">{{ isExpanded ? videoData.copywriting :
truncatedText }}</text>
<view class="flodbtncontainer">
<image v-if="showExpandButton" class="expand-btn" :class="{ 'rotated': isExpanded }"
:src="isExpanded ? '/static/imgs/foldicon/flodicon@3x.webp' : '/static/imgs/expandicon/expandicon@3x.webp'"
mode="aspectFit" @tap="toggleExpand"></image>
</view>
</view>
</section>
</section>
<!-- 评论区域 + 互动区域 --> <!-- 评论区域 + 互动区域 -->
<Comments :comments="videoData.comments" :showInteraction="true" :collectsum="videoData.collectsum" <Comments :postid="videoData.id" />
:likesum="videoData.likesum" />
<view class="spacerview"></view>
<!-- 互动区域 -->
<!-- <Intereact /> -->
</main>
<!-- Findmore --> <!-- Findmore -->
<Findmore /> <!-- <Findmore /> -->
</template> </template>
<script setup> <script setup>
import { ref, reactive, getCurrentInstance } from 'vue' import { onLoad } from '@dcloudio/uni-app'
import { ref, reactive, getCurrentInstance, computed } from 'vue'
import { useCommonStore } from '@/stores/common.js' import { useCommonStore } from '@/stores/common.js'
import Head from '@/pages/head/head.vue' import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Findmore from '@/pages/findmore/findmore.vue' import Intereact from '@/pages/intereact/intereact.vue'
import { getPostList, getPostVideo, getUserImg } from '@/api/api.js'
const common = useCommonStore() const common = useCommonStore()
const formatCount = common.formatCount const formatCount = common.formatCount
const formatDate = common.formatDate const formatDate = common.formatDate
const videoData = reactive({})
// 折叠展开状态 // 折叠展开状态
const isExpanded = ref(false) const isExpanded = ref(false)
const videoRef = ref(null) // const videoRef = ref(null)
// 静音状态 // 静音状态
const isMuted = ref(false) const isMuted = ref(false)
// 播放状态 // 播放状态
const isPlaying = ref(true) // 默认播放状态 const isPlaying = ref(true) // 默认播放状态
// 视频数据(包含弹幕) // 计算属性:是否需要显示展开按钮
const videoData = reactive({ const showExpandButton = computed(() => {
userId: 1000012, if (!videoData.copywriting) return false
userName: '蔡徐坤', // 考虑中文字符每个中文字符算2个字符宽度
userImg: '/static/logo.png', const textLength = videoData.copywriting.replace(/[^\x00-\xff]/g, '**').length
date: '2024-01-15', return textLength > 30
copywriting: '鸡你太美!练习时长两年半的个人练习生!鸡你太美!练习时长两年半的个人练习生!鸡你太美!练习时长两年半的个人练习生!', })
likesum: 1145140,
collectsum: 114514, // 计算属性:截断后的文本
commentsum: 114514, const truncatedText = computed(() => {
sharesum: 1145, if (!videoData.copywriting) return ''
videoUrl: '/static/videos/beauty.mp4', if (!showExpandButton.value) return videoData.copywriting
comments: [
{ // 智能截断,保留完整的中文字符
id: 101, let result = ''
userName: 'Brad Lewin', let charCount = 0
atUsers: ['JackyLove'],
content: '今天天气真不错~', for (let i = 0; i < videoData.copywriting.length; i++) {
date: '2024-12-01', const char = videoData.copywriting[i]
likeCount: 99999, // 中文字符算2个字符其他算1个
showChild: false, charCount += /[^\x00-\xff]/.test(char) ? 2 : 1
children: [
{ id: 201, userName: 'Alice', content: '快智能体是人类智慧的延伸,它们将成为我们最强大的工具,也是最亲密的朋友。智能体是人类智慧的延伸', atUsers: ['Brad Lewin', 'JackyLove'], date: '2024-12-01', likeCount: 2 }, if (charCount <= 30) {
{ id: 202, userName: 'Bob', content: '我同意你的观点。我记得喜欢这个版本。这个新版本对我来说Nothing好处', atUsers: [], date: '2024-12-01', likeCount: 1 } result += char
] } else {
}, break
{
id: 102,
userName: 'Leanne Simpson',
atUsers: ['Theshy'],
content: '有人一起开黑吗?',
date: '2024-12-02',
likeCount: 1145140,
showChild: false,
children: [
{ id: 203, userName: 'Carol', content: '带我一个', atUsers: [], date: '2024-12-02', likeCount: 3 }
]
},
{
id: 103,
userName: 'Tom Hardy',
atUsers: [],
content: '主评论3分享一张今晚的月亮。',
date: '2024-12-03',
likeCount: 999,
showChild: false,
children: []
} }
], }
danmuList: [
{ text: '鸡你太美!', color: '#FFFFFF', time: 1 }, // 如果截断后的文本和原文本相同,不需要加省略号
{ text: '练习时长两年半', color: '#FF0000', time: 1 }, if (result === videoData.copywriting) {
{ text: '个人练习生', color: '#00FF00', time: 1 }, return result
{ text: '坤坤加油!', color: '#FFFF00', time: 3 }, }
{ text: '篮球高手', color: '#FFA500', time: 3 },
{ text: '坤坤跳舞真帅', color: '#FF69B4', time: 5 }, return result + '...'
{ text: '这舞步绝了', color: '#00FFFF', time: 5 },
{ text: '音乐太洗脑了', color: '#FFD700', time: 7 },
{ text: '坤坤我爱你', color: '#FF1493', time: 7 },
{ text: '练习生之光', color: '#7CFC00', time: 9 },
{ text: '这节奏太上头', color: '#FF6347', time: 9 },
{ text: '坤坤的招牌动作', color: '#9370DB', time: 11 },
{ text: '鸡你太美循环中', color: '#20B2AA', time: 11 },
{ text: '两年半的功力', color: '#FF4500', time: 13 },
{ text: '坤坤的舞姿', color: '#DA70D6', time: 13 },
{ text: '这歌太魔性了', color: '#32CD32', time: 15 },
{ text: '练习生天花板', color: '#FF8C00', time: 15 },
{ text: '坤坤的台风', color: '#00BFFF', time: 17 },
{ text: '鸡你太美经典', color: '#FF00FF', time: 17 },
{ text: '坤坤的舞台魅力', color: '#FFDAB9', time: 19 },
{ text: '这舞步太经典', color: '#98FB98', time: 19 },
{ text: '坤坤的招牌笑容', color: '#FFB6C1', time: 21 },
{ text: '练习生传奇', color: '#87CEFA', time: 21 },
{ text: '坤坤的舞蹈功底', color: '#FFA07A', time: 23 },
{ text: '坤坤太帅了', color: '#FF1493', time: 25 },
{ text: '这舞步太丝滑', color: '#00CED1', time: 25 },
{ text: '坤坤的节奏感', color: '#FF69B4', time: 27 },
{ text: '鸡你太美神曲', color: '#32CD32', time: 27 },
{ text: '坤坤的舞台表现', color: '#FF4500', time: 29 },
{ text: '这舞蹈太经典', color: '#9370DB', time: 29 },
{ text: '坤坤的粉丝来了', color: '#FFD700', time: 2 },
{ text: '鸡你太美循环播放', color: '#00BFFF', time: 4 },
{ text: '坤坤的舞蹈功底', color: '#FF6347', time: 6 },
{ text: '这歌太魔性了', color: '#20B2AA', time: 8 },
{ text: '坤坤的招牌动作', color: '#FF8C00', time: 10 },
{ text: '练习时长两年半', color: '#FF00FF', time: 12 },
{ text: '坤坤的舞台魅力', color: '#98FB98', time: 14 },
{ text: '鸡你太美经典', color: '#FFB6C1', time: 16 },
{ text: '坤坤的舞姿', color: '#87CEFA', time: 18 },
{ text: '这节奏太上头', color: '#FFA07A', time: 20 },
{ text: '坤坤的台风', color: '#FF1493', time: 22 },
{ text: '鸡你太美神曲', color: '#00CED1', time: 24 },
{ text: '坤坤的舞蹈功底', color: '#FF69B4', time: 26 },
{ text: '这舞步太丝滑', color: '#32CD32', time: 28 },
{ text: '坤坤太帅了', color: '#FF4500', time: 30 }
]
}) })
// 切换展开状态 // 切换展开状态
@@ -226,30 +187,163 @@ const toggleMute = () => {
} }
} }
onLoad(() => {
const params = 'video_only'
getPostList(params).then(res => {
try {
// 检查响应状态
if (res.statusCode === 200 && res.data) {
const data = res.data.data
console.log('视频数据:', data)
// 创建Promise数组来处理异步资源
const resourcePromises = []
// 处理视频资源 - 直接取第一个视频
if (data.videos && Array.isArray(data.videos) && data.videos.length > 0) {
const video = data.videos[0] // 直接取第一个视频
resourcePromises.push(getPostVideo(video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) {
// 使用uni.arrayBufferToBase64方法参考post.vue的实现
const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64
return {
type: 'video',
src: videoUrl,
original_url: video.original_url,
loading: true,
error: false
}
}
return null
}).catch(error => {
console.warn('获取视频失败:', error)
uni.showToast({
title: '视频加载失败',
icon: 'error'
})
return null
}))
}
// 处理用户头像
if (data.user && data.user.avatar) {
resourcePromises.push(getUserImg(data.user.avatar).then(avatarRes => {
if (avatarRes.statusCode === 200 && avatarRes.data) {
// 将arrayBuffer转换为base64
const base64 = uni.arrayBufferToBase64(avatarRes.data)
const userImgUrl = 'data:image/webp;base64,' + base64
return {
type: 'avatar',
url: userImgUrl
}
}
return null
}).catch(error => {
console.warn('获取用户头像失败:', error)
return null
}))
}
// 等待所有资源加载完成
Promise.all(resourcePromises).then(results => {
// 处理视频结果
const videoResults = results.filter(result => result && result.type === 'video')
if (videoResults.length > 0) {
// 使用第一个视频作为主视频
const mainVideo = videoResults[0]
videoData.videoUrl = mainVideo.src
}
// 处理头像结果
const avatarResult = results.find(result => result && result.type === 'avatar')
if (avatarResult) {
videoData.userImg = avatarResult.url
}
// 更新视频数据 - 沿用data对象
data.userName = data.user?.nickName || '匿名用户'
data.date = data.time || data.date || ''
data.copywriting = data.textContent || ''
data.likesum = data.likeCount || 0
data.commentsum = data.commentCount || 0
data.sharesum = data.shareCount || 0
// 最后用data覆盖整个videoData
Object.assign(videoData, data)
}).catch(error => {
console.error('资源加载失败:', error)
uni.showToast({
title: '资源加载失败',
icon: 'error'
})
})
} else {
throw new Error(`请求失败: ${res.statusCode}`)
}
} catch (error) {
console.error('获取视频数据失败:', error)
uni.showToast({
title: '加载视频失败',
icon: 'error'
})
}
}).catch(error => {
console.error('获取视频列表失败:', error)
uni.showToast({
title: '网络连接异常',
icon: 'error'
})
})
})
</script> </script>
<style scoped> <style scoped>
.videopage { main {
display: flex;
flex-direction: column;
position: relative;
max-width: 430px;
max-height: 980px;
margin: 0 auto;
}
.spacerview {
flex: 1;
pointer-events: none
}
/* 视频容器 */
.video-container {
position: relative;
width: 100%; width: 100%;
max-width: 430px; max-width: 430px;
height: 100vh; height: 100vh;
max-height: 980px;
flex-shrink: 0;
margin: 0 auto;
display: flex;
flex-direction: column;
}
video {
width: 100%;
height: 100%;
min-height: 680px;
object-fit: cover;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 auto; margin: 0 auto;
position: relative; z-index: 0;
} flex: 1;
.videopage video {
width: 100%;
max-width: 430px;
height: 100%;
min-height: 680px;
max-height: 980px;
object-fit: cover;
} }
::v-deep .uni-video-cover { ::v-deep .uni-video-cover {
background-color: transparent; background-color: transparent;
cursor: pointer;
z-index: 1;
} }
::v-deep .uni-video-cover-play-button { ::v-deep .uni-video-cover-play-button {
@@ -258,12 +352,13 @@ const toggleMute = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer;
z-index: 1;
} }
/* 弹幕区域 */ /* 弹幕区域 */
::v-deep .uni-video-danmu { ::v-deep .uni-video-danmu {
height: 15% !important; height: 15% !important;
z-index: 2;
margin: 32px 0 0; margin: 32px 0 0;
} }
@@ -277,6 +372,7 @@ const toggleMute = () => {
align-items: center; align-items: center;
gap: 16px; gap: 16px;
z-index: 2; z-index: 2;
pointer-events: auto;
} }
/* 用户信息区域 */ /* 用户信息区域 */
@@ -321,10 +417,7 @@ const toggleMute = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 4px; text-align: center;
padding: 8px;
border-radius: 8px;
transition: background-color 0.3s;
} }
.action-icon { .action-icon {
@@ -333,6 +426,11 @@ const toggleMute = () => {
} }
.action-count { .action-count {
width: 100%;
height: 17px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px; font-size: 12px;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
@@ -352,11 +450,12 @@ const toggleMute = () => {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
z-index: 2;
display: flex; display: flex;
flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
z-index: 2;
} }
.vedioinfohead { .vedioinfohead {
@@ -365,34 +464,25 @@ const toggleMute = () => {
gap: 8px; gap: 8px;
} }
.username,
.datetime {
display: flex;
align-items: center;
height: 100%;
}
.username { .username {
font-size: 17px; font-size: 17px;
font-weight: 600; font-weight: 600;
font-stretch: normal; /* font-family: 'SFPro'; */
font-style: normal;
line-height: normal;
letter-spacing: normal;
color: #fff; color: #fff;
display: flex; line-height: 1;
align-items: center;
height: 24px;
white-space: nowrap;
flex-shrink: 0;
} }
.datetime { .datetime {
font-size: 11px; font-size: 11px;
font-weight: normal; font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: normal;
letter-spacing: normal;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
height: 24px;
white-space: nowrap;
flex-shrink: 0;
} }
.content { .content {
@@ -460,7 +550,6 @@ const toggleMute = () => {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1;
} }
.uni-video-icon { .uni-video-icon {
@@ -480,40 +569,6 @@ const toggleMute = () => {
cursor: pointer; cursor: pointer;
} }
.uni-video-cover-play-button::after {
content: '\ea24';
}
.uni-video-cover {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
}
.uni-video-icon {
font-family: 'uni-video-icon';
text-align: center;
}
.uni-video-cover-play-button {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
line-height: 75px;
font-size: 56px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
}
.uni-video-cover-play-button::after { .uni-video-cover-play-button::after {
content: '\ea24'; content: '\ea24';
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -26,6 +26,64 @@ export const useCommonStore = defineStore('common', () => {
})) }))
} }
/**
* 将评论内容转换为富文本节点
* @param {string} content - 评论内容
* @param {string[]} atUsers - @用户数组
* @returns {Array} rich-text nodes
*/
function contentToRenderNodes(content = '', atUsers = []) {
if (!content) {
return [{ type: 'text', text: '' }]
}
// 如果有@用户,处理@高亮
if (atUsers && atUsers.length > 0) {
const nodes = []
let remainingContent = content
// 为每个@用户创建高亮节点
atUsers.forEach(user => {
const atText = `@${user}`
const atIndex = remainingContent.indexOf(atText)
if (atIndex !== -1) {
// 添加@前面的普通文本
if (atIndex > 0) {
nodes.push({
type: 'text',
text: remainingContent.substring(0, atIndex)
})
}
// 添加@高亮节点
nodes.push({
type: 'node',
name: 'span',
attrs: { style: 'color:#0969DA;', 'data-name': user },
children: [{ type: 'text', text: atText }]
})
// 更新剩余内容
remainingContent = remainingContent.substring(atIndex + atText.length)
}
})
// 添加剩余文本
if (remainingContent) {
nodes.push({
type: 'text',
text: remainingContent
})
}
return nodes
}
// 没有@用户,直接返回普通文本
return [{ type: 'text', text: content }]
}
/* 日期处理函数 */ /* 日期处理函数 */
/** /**
* 格式化日期,如果年份是当年则不显示年份 * 格式化日期,如果年份是当年则不显示年份
@@ -39,12 +97,12 @@ export const useCommonStore = defineStore('common', () => {
if (inputYear === currentYear) { if (inputYear === currentYear) {
// 当年:显示月份和日期 // 当年:显示月份和日期
return `${inputDate.getMonth() + 1}-${inputDate.getDate()}`; return `${inputDate.getMonth() + 1}/${inputDate.getDate()}`;
} else { } else {
// 非当年:显示完整年月日 // 非当年:显示完整年月日
return `${inputYear}-${inputDate.getMonth() + 1}-${inputDate.getDate()}`; return `${inputYear}/${inputDate.getMonth() + 1}/${inputDate.getDate()}`;
} }
} }
return { download, openapp, formatCount, atUsersToNodes, formatDate } return { download, openapp, formatCount, atUsersToNodes, formatDate, contentToRenderNodes }
}) })

View File

@@ -5,4 +5,13 @@ export default defineConfig({
plugins: [ plugins: [
uni(), uni(),
], ],
server: {
proxy: {
'/api': {
target: 'http://192.168.1.7:8088', // 请替换为你的实际后端服务器地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
}
}) })