Files
product-uniapp/pages/square/detiles.vue
T
2025-12-13 17:30:18 +08:00

1039 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="detail-page">
<uni-nav-bar leftText=" " :fixed="true" :border="false" leftIcon="back" @clickLeft="leftClick" color="#000"
backgroundColor="#fff" :statusBar="true" :title="detail.sku.product_name"></uni-nav-bar>
<button open-type="share" class="circle-btn">
<image src="/static/image/shares.png" class="share" mode=""></image>
</button>
<scroll-view scroll-y class="page-scroll">
<!-- 图集 -->
<view class="gallery">
<image v-if="detail.media && detail.media.length > 0" class="main-img" :src="detail.media[currentIndex].url_pull" mode="aspectFit"></image>
<scroll-view class="thumb-row" scroll-x>
<view class="thumb-container">
<image
v-for="(img, idx) in detail.media"
:key="idx"
:src="img.url_pull"
mode="aspectFill"
class="thumb"
:class="{'active-thumb': idx === currentIndex}"
@click="setCurrent(idx)"
/>
</view>
</scroll-view>
</view>
<!-- 产品信息 -->
<view class="product-card">
<view class="title">{{detail.content}}</view>
<view class="price-row">
<text class="price">¥{{ detail.price }}</text>
<text class="retail">零售价:{{ detail.sku.price }}</text>
<text class="model">{{ detail.sku.model }}</text>
</view>
<view class="spec-row">
<text class="spec-label">品牌:</text>
<text class="spec-value">{{ detail.sku.brand }}</text>
</view>
<view class="spec-row" v-for="(item, idx) in detail.sku.spec" :key="idx">
<text class="spec-label">{{item.attr_name}}:</text>
<text class="spec-value">{{ item.attr_value }}</text>
</view>
</view>
<!-- Tabs -->
<view class="tabs">
<view :class="['tab', currentTab==='detail'?'on':'']" @click="switchTab('detail')">详情</view>
<view class="divider"></view>
<view :class="['tab', currentTab==='comment'?'on':'']" @click="switchTab('comment')">评论</view>
</view>
<!-- 详情 -->
<view v-if="currentTab === 'detail'" class="detail-box">
<rich-text :nodes="processedDesc"></rich-text>
</view>
<!-- 评论 -->
<view v-else class="comment-section">
<view v-if="comments.length == 0">
<view class="comment-item">
<text>暂无评论</text>
</view>
</view>
<view v-else>
<view v-for="(item, index) in comments" :key="index" class="comment-item">
<view class="comment-heard">
<image class="comment-avatar" :src="item.user.avatar_pull"></image>
<view class="comment-header">
<view class="comment-header-left">
<view class="comment-name">{{item.user.nickname}}</view>
<view class="comment-time">{{item.created_at}}</view>
</view>
<image @click="replyComment(item)" src="/static/image/msg.png" mode="" class="icons20"></image>
<uni-icons @click="delComment(item.id)" v-if="openid==item.user.openid ||openid==detail.user.openid" type="trash" color="#999" size="20"></uni-icons>
</view>
</view>
<view class="comment-body">
<text class="comment-text">{{item.content}}</text>
<view class="comment-footer">
<text class="comment-reply" @click="replyComment(item)">回复</text>
</view>
<view v-if="item.child && item.child.length" class="comment-reply-box">
<view v-for="(reply, rIndex) in item.child" :key="rIndex" class="reply-item">
<view class="reply-item-left">
<image class="reply-avatar" :src="reply.user.avatar_pull"></image>
<text class="reply-name">{{reply.user.nickname}}:</text>
<uni-icons type="trash" @click="delComment(reply.id)" v-if="openid==reply.user.openid||openid==detail.user.openid" color="#999" size="20"></uni-icons>
</view>
<view>
<view class="reply-text">{{reply.content}}</view>
<view class="reply-text">{{reply.created_at}}</view>
</view>
</view>
<text class="comment-reply" v-if="item.count> 1" @click="viewMoreReply(item)">展开{{item.count-1}}条回复</text>
</view>
</view>
</view>
</view>
<view v-if="!hasMore && comments.length" class="comment-end">
<text class="comment-end-text">没有更多评论了</text>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<view class="input-box">
<uni-icons type="compose" size="20" color="#bbb"></uni-icons>
<input class="input"
type="text"
:placeholder="commentPlaceholder"
v-model="commentText"
confirm-type="send"
@confirm="sendComment"
:focus="commentFocus"
@focus="commentFocus = true"
@blur="handleInputBlur"
/>
<view class="send-btn" :class="{disabled: !commentText.trim()}" :disabled="!commentText.trim()" @click="sendComment">发送</view>
</view>
<view class="bottom-actions">
<view class="bottom-action" @click="toggleCollect(detail)">
<uni-icons :type="detail.is_favorite ? 'star-filled' : 'star'" :color="detail.is_favorite ? '#05b3a8' : '#333'" size="24"></uni-icons>
<text>{{detail.favorite}}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { request,showToast } from '@/api/apiT.js'
export default {
data() {
return {
showlogin: false,
openid:uni.getStorageSync('openid'),
detail: {},
comments: [],
commentstall:0,
postid:'',
commentText: '',
commentPlaceholder: '说点什么...',
commentFocus: false,
replyTarget: null,
page: 1,
pageSize: 10,
hasMore: true,
loading: false,
refreshing: false,
selfChecked: false,
loadingMore: false,
article:{},
currentTab:'detail',
share:'',
currentIndex: 0
}
},
computed: {
// 处理富文本内容,让图片自适应
processedDesc() {
if (!this.detail.sku || !this.detail.sku.desc) return ''
return this.processRichText(this.detail.sku.desc)
}
},
onLoad(options) {
this.postid = options.id;
this.share = options.share
this.getshows()
this.loadComments(true)
this.getCommentCount()
},
methods: {
// 处理富文本HTML,让图片自适应
processRichText(html) {
if (!html) return ''
// 使用正则表达式给所有 img 标签添加样式
return html.replace(/<img([^>]*)>/gi, (match, attrs) => {
// 检查是否已有 style 属性
if (attrs.includes('style=')) {
// 如果已有 style,添加 max-width
return match.replace(/style=["']([^"']*)["']/i, (styleMatch, styleContent) => {
// 移除已有的 max-width 和 width
let newStyle = styleContent.replace(/max-width\s*:\s*[^;]+;?/gi, '').replace(/width\s*:\s*[^;]+;?/gi, '')
newStyle = newStyle.trim()
if (newStyle && !newStyle.endsWith(';')) {
newStyle += ';'
}
newStyle += 'max-width: 100%; height: auto;'
return `style="${newStyle}"`
})
} else {
// 如果没有 style,添加 style 属性
return `<img${attrs} style="max-width: 100%; height: auto;">`
}
})
},
setCurrent(idx) {
this.currentIndex = idx
},
switchTab(key) {
this.currentTab = key
},
leftClick() {
if(this.share=='share'){
uni.reLaunch({
url: '/pages/index/index',// 替换为你要跳转的页面路径
});
}else{
uni.navigateBack()
}
},
goprivacy(){
uni.navigateTo({
url: '/pages/login/agreement',// 替换为你要跳转的页面路径
});
},
selfChangde(e) {
this.selfChecked = !this.selfChecked;
},
loginbtn(){
if(!this.selfChecked){
uni.showToast({
title:this.$t('home.arginimg'),
icon: 'none'
});
return false
}
},
getPhoneNumber(e) {
let that = this;
request({
url:'miniapp/phone',
method:'POST',
data:{
phone_code: e.detail.code,
}
}).then(res => {
if (res.data.code == -1) {
uni.showToast({
title: res.data.msg,
icon: 'none'
});
} else if (res.data.code == 200) {
//返回数据接收成功
uni.setStorageSync('is_login', true);
}
})
},
gouser(){
uni.navigateTo({
url: '/pages/login/agreementuser',// 替换为你要跳转的页面路径
});
},
//获取详情
getshows(){
let that = this;
request({
url:'common/post/show',
method:'GET',
data:{
id:that.postid
}
}).then(res => {
if (res.data.code == 200) {
this.article = res.data.data
this.detail = res.data.data
// 确保 currentIndex 在有效范围内
if (this.detail.media && this.detail.media.length > 0) {
if (this.currentIndex >= this.detail.media.length) {
this.currentIndex = 0
}
}
}
})
},
// 获取评论(分页)
loadComments(isRefresh = false){
if (this.loading) return;
if (isRefresh) {
this.page = 1;
this.hasMore = true;
} else if (!this.hasMore) {
this.loadingMore = false;
return;
}
this.loading = true;
request({
url:'common/post/comment/list',
method:'GET',
data:{
post_id:this.postid,
parent_id:0,
current:this.page,
size:this.pageSize
}
}).then(res => {
if (res.data.code == 200) {
const records = res.data.data.records || [];
this.commentstall = res.data.data.total
if (isRefresh) {
this.comments = records;
} else {
const existingIds = new Set(this.comments.map(item => item.id));
const appendList = records.filter(item => !existingIds.has(item.id));
this.comments = [...this.comments, ...appendList];
}
if (records.length < this.pageSize) {
this.hasMore = false;
} else {
this.page += 1;
}
}else{
uni.showToast({
title:res.data.message || '获取评论失败',
icon: 'none'
});
}
}).finally(()=>{
this.loading = false;
if (isRefresh) {
this.refreshing = false;
}
this.loadingMore = false;
})
},
onRefreshComments(){
if (this.refreshing) return;
this.refreshing = true;
this.loadComments(true);
},
onLoadMoreComments(){
if (this.loadingMore || this.refreshing || this.loading || !this.hasMore) return;
this.loadingMore = true;
this.loadComments(false);
},
islogin(){
if(uni.getStorageSync('is_login')){
return true
}else{
return false
}
},
toggleLike(items) {
if(!this.islogin()){
this.showlogin = true
return false
}
const likeUrl = this.detail.is_like ? 'common/post/like/destroy' : 'common/post/like/store'
const method = this.detail.is_like ? 'GET' : 'POST'
const data = {
post_id: items.id
}
if (this.detail.is_like) {
data.user_id = this.detail.user_id
}
request({
url: likeUrl,
method,
data
}).then(res => {
if(res.data.code === 200){
if(this.detail.is_like){
this.detail.is_like = 0
this.detail.like += this.detail.is_like ? 1 : -1
// newTarget.like = Math.max(0, (Number(newTarget.like) || 0) - 1)
uni.showToast({
title: '取消成功',
icon: 'none'
});
}else{
this.detail.is_like =1
this.detail.like += this.detail.is_like ? 1 : -1
uni.showToast({
title: '点赞成功',
icon: 'none'
});
}
}else{
uni.showToast({
title:res.data.message,
icon: 'none'
});
}
})
},
toggleCollect(items) {
if(!this.islogin()){
this.showlogin = true
return false
}
const collectUrl = this.detail.is_favorite ? 'common/post/favorite/destroy' : 'common/post/favorite/store'
const method = this.detail.is_favorite ? 'GET' : 'POST'
const data = {
post_id: this.detail.id
}
if (this.detail.is_favorite) {
data.user_id = this.detail.user_id
}
request({
url: collectUrl,
method,
data
}).then(res => {
if(res.data.code === 200){
if(this.detail.is_favorite){
this.detail.is_favorite = 0
this.detail.favorite += this.detail.is_favorite ? 1 : -1
uni.showToast({
title: '取消收藏成功',
icon: 'none'
});
}else{
this.detail.is_favorite = 1
this.detail.favorite += this.detail.is_favorite ? 1 : -1
uni.showToast({
title: '收藏成功',
icon: 'none'
});
}
}else{
uni.showToast({
title:res.data.message,
icon: 'none'
});
}
})
},
startComment(target = null) {
if(!this.islogin()){
this.showlogin = true
return false
}
this.replyTarget = target
this.commentPlaceholder = target ? `回复 ${target.user.nickname}...` : '说点什么...'
this.commentText = ''
this.commentFocus = true
},
handleInputBlur() {
this.commentFocus = false
},
replyComment(item) {
if(!this.islogin()){
this.showlogin = true
return false
}
this.commentText = ''
this.startComment(item)
},
viewMoreReply(item) {
let that = this;
request({
url:'common/post/comment/list',
method:'GET',
data:{
post_id:that.postid,
parent_id:item.id,
received_id:item.child[0].id
}
}).then(res => {
if (res.data.code == 200) {
item.child = item.child || []
const index = this.comments.findIndex(item1 => item1.id === item.id)
if (index > -1) {
this.$set(
this.comments[index],
'child',
[...this.comments[index].child, ...res.data.data.records]
)
this.comments[index].count = 0
}
}
})
},
//获取评论数量
getCommentCount(){
let that = this;
request({
url:'common/post/interaction',
method:'GET',
data:{
id:that.postid
}
}).then(res => {
if (res.data.code == 200) {
this.detail.comment = res.data.data.comment
}
})
},
sendComment() {
if (!this.commentText.trim()) return
if(!this.islogin()){
this.showlogin = true
return false
}
const data = {
post_id: this.detail.id,
parent_id: 0,
content: this.commentText
}
if(this.replyTarget){
data.parent_id = this.replyTarget.id
}
request({
url: 'common/post/comment/store',
method: 'POST',
data: data
}).then(res => {
if(res.data.code === 200){
uni.showToast({
title: '评论成功',
icon: 'none'
});
this.loadComments(true)
this.getCommentCount()
this.commentText = ''
this.commentPlaceholder = '说点什么...'
this.replyTarget = null
this.commentFocus = false
}else{
uni.showToast({
title:res.data.message,
icon: 'none'
});
}
})
},
//删除评论
delComment(item){
const that = this;
uni.showModal({
title: '提示',
content: '确定要删除当前评论吗?',
success: function (res) {
if (res.confirm) {
request({
url:'common/post/comment/destroy',
method:'POST',
data:{
id:item
}
}).then(res => {
if(res.data.code === 200){
// 从评论列表中移除
// 先尝试在主评论列表中找到
const mainIndex = that.comments.findIndex(comment => comment.id === item);
if(mainIndex > -1){
// 删除主评论
that.comments.splice(mainIndex, 1);
} else {
// 在子评论中查找并删除
for(let i = 0; i < that.comments.length; i++){
if(that.comments[i].child && that.comments[i].child.length > 0){
const childIndex = that.comments[i].child.findIndex(child => child.id === item);
if(childIndex > -1){
that.comments[i].child.splice(childIndex, 1);
// 如果子评论数组为空,可以删除child属性或保持为空数组
if(that.comments[i].child.length === 0){
that.$set(that.comments[i], 'child', []);
}
break;
}
}
}
}
// 更新评论数量
that.getCommentCount();
uni.showToast({
title:res.data.message || '删除成功',
icon: 'none'
});
} else {
uni.showToast({
title:res.data.message || '删除失败',
icon: 'none'
});
}
})
}
}
});
},
// 预览图片(点击放大)
previewImage(index) {
if (!this.detail.media || this.detail.media.length === 0) return
const urls = this.detail.media.map(item => item.url_pull)
uni.previewImage({
current: index,
urls: urls
})
},
// 下载图片(长按)
downloadImage(img) {
if (!img || !img.url_pull) return
uni.showActionSheet({
itemList: ['保存图片'],
success: (res) => {
if (res.tapIndex === 0) {
// 保存图片
uni.showLoading({
title: '下载中...',
mask: true
})
uni.downloadFile({
url: img.url_pull,
success: (downloadRes) => {
if (downloadRes.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: downloadRes.tempFilePath,
success: () => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
},
fail: (err) => {
uni.hideLoading()
if (err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要授权保存图片到相册',
showCancel: false
})
} else {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
})
} else {
uni.hideLoading()
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
fail: () => {
uni.hideLoading()
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
})
}
}
})
}
},
// 分享给好友
onShareAppMessage() {
return {
title: this.article.content,
path: `/pages/square/detiles?share=share&id=${this.article.id}`,
imageUrl:this.article.media[0].url_pull // 封面图
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: this.article.content,
query: `id=${this.article.id}`,
imageUrl: "/static/image/share.jpg"
}
}
}
</script>
<style lang="scss">
page,
.detail-page {
height: 100%;
background: #f7f7f7;
}
.page-scroll {
height: 100%;
}
.gallery {
background: #fff;
padding: 20rpx 0;
display: flex;
flex-direction: column;
align-items: center;
}
.main-img {
width: 520rpx;
height: 520rpx;
background: #f5f5f5;
}
.thumb-row {
margin-top: 12rpx;
width: 100%;
white-space: nowrap;
}
.thumb-container {
display: inline-flex;
gap: 12rpx;
padding: 0 20rpx;
}
.thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f5f5f5;
border: 2rpx solid transparent;
flex-shrink: 0;
}
.active-thumb {
border-color: #4cb866;
}
.product-card {
background: #fff;
margin-top: 16rpx;
padding: 24rpx 24rpx 12rpx;
}
.title {
font-size: 32rpx;
color: #111;
font-weight: 600;
line-height: 1.4;
}
.price-row {
margin-top: 12rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.price {
font-size: 36rpx;
color: #111;
font-weight: 700;
}
.retail {
font-size: 22rpx;
color: #888;
}
.model {
margin-left: auto;
font-size: 22rpx;
color: #777;
}
.spec-row {
margin-top: 18rpx;
display: flex;
flex-wrap: wrap;
font-size: 26rpx;
color: #555;
gap: 6rpx;
}
.spec-label {
color: #777;
}
.spec-value {
color: #444;
margin-right: 20rpx;
}
.tabs {
height: 80rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-around;
gap: 24rpx;
position: relative;
font-size: 28rpx;
color: #666;
margin-top: 20rpx;
}
.tab {
position: relative;
padding: 12rpx 20rpx 18rpx;
color: #333;
}
.tab::after {
content: '';
position: absolute;
left: 50%;
bottom: 4rpx;
width: 50rpx;
height: 4rpx;
background: #05b3a8;
border-radius: 4rpx;
transform: translateX(-50%) scaleX(0);
transition: transform 0.2s;
}
.tab.on {
color: #05b3a8;
font-weight: 600;
}
.tab.on::after {
transform: translateX(-50%) scaleX(1);
}
.divider {
width: 1px;
height: 26rpx;
background: #dcdcdc;
}
.detail-box {
margin-bottom: 80rpx;
background: #fff;
padding: 20rpx 24rpx 190rpx;
font-size: 26rpx;
color: #444;
line-height: 1.7;
overflow: hidden;
word-wrap: break-word;
word-break: break-all;
}
.icons20{
width: 36rpx;
height: 36rpx;
margin-right: 20rpx;
}
.comment-section {
padding: 20rpx 30rpx 190rpx;
background: #fff;
}
.comment-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.comment-item {
// display: flex;
padding: 20rpx 0;
border-bottom: 1px solid #F0F0F0;
}
.comment-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.comment-body {
margin-top: 10rpx;
flex: 1;
}
.comment-heard{
display: flex;
gap: 12rpx;
}
.comment-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
flex: 1;
}
.comment-header-left{
flex: 1;
// width: 78%;
}
.comment-name {
font-size: 28rpx;
color: #333;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// width: 40%;
}
.comment-time {
font-size: 24rpx;
color: #999;
}
.comment-text {
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.comment-footer {
margin-top: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.comment-reply {
font-size: 26rpx;
color: #05b3a8;
}
.comment-icons {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #999;
}
.comment-reply-box {
margin-top: 20rpx;
background: #F8F8F8;
padding: 20rpx;
border-radius: 16rpx;
font-size: 26rpx;
color: #333;
}
.comment-end {
padding: 24rpx 0 40rpx;
display: flex;
justify-content: center;
}
.comment-end-text {
font-size: 26rpx;
color: #999;
}
.reply-item {
margin-bottom: 16rpx;
line-height: 1.4;
font-size: 22rpx;
}
.reply-item-left{
display: flex;
align-items: center;
}
.reply-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 10rpx;
flex-shrink: 0;
}
.reply-name {
color: #333;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.reply-text {
color: #555;
margin-left: 22px;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top: 1px solid #F0F0F0;
padding: 16rpx 30rpx calc(env(safe-area-inset-bottom) + 16rpx);
display: flex;
align-items: center;
gap: 20rpx;
}
.input-box {
flex: 1;
display: flex;
align-items: center;
background: #F6F7F8;
border-radius: 999rpx;
padding: 16rpx 24rpx;
gap: 10rpx;
}
.input {
flex: 1;
font-size: 26rpx;
margin-right: 20rpx;
}
.send-btn {
background: #05b3a8;
color: #fff;
border: none;
border-radius: 999rpx;
padding: 0 20rpx;
height: 46rpx;
line-height: 46rpx;
font-size: 22rpx;
}
.send-btn.disabled {
background: #dcdcdc;
color: #aaa;
}
.bottom-actions {
display: flex;
align-items: center;
gap: 30rpx;
font-size: 26rpx;
color: #333;
}
.bottom-action {
display: flex;
align-items: center;
gap: 6rpx;
}
.circle-btn {
z-index: 11;
position: fixed;
right: 20rpx;
margin-top: 20rpx;
top: 180rpx;
border-radius: 50%;
width: 80rpx;
height: 80rpx;
background: #fff;
padding: 10rpx;
display: flex;
align-items: center;
justify-content: center;
.share{
width: 34rpx;
height: 34rpx;
}
}
</style>