<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<title>黑马点评</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="./css/element.css">
<link href="./css/blog-detail.css" rel="stylesheet">
<link href="./css/main.css" rel="stylesheet">
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.comment-input {
@apply w-full min-h-[120px] border border-gray-300 text-lg leading-relaxed resize-none outline-none bg-white mb-3 px-4 py-3 rounded-lg transition-all duration-300;
}
.comment-input:focus {
@apply border-primary ring-1 ring-primary/20;
}
.comment-input::placeholder {
@apply text-gray-400;
}
.comment-box {
@apply flex p-4 border-b border-gray-100;
}
.comment-icon {
@apply w-10 h-10 rounded-full overflow-hidden mr-3 flex-shrink-0;
}
.comment-icon img {
@apply w-full h-full object-cover;
}
.comment-user {
@apply font-medium text-gray-800;
}
.comment-info {
@apply flex-1;
}
.send-btn {
@apply bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-full transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed;
}
}
</style>
<style>
.header {
position: relative;
}
.foot-view span {
font-size: 12px;
}
.liked {
color: #ff6633;
}
/* 评论输入框样式 */
.comment-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: white;
z-index: 150;
display: flex;
flex-direction: column;
box-shadow: -2px -2px 10px rgba(0,0,0,0.05);
transition: height 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.comment-container.active {
height: 60%; /* 增加评论容器高度 */
}
.comment-header {
height: 56px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #E5E7EB;
}
.comment-header .cancel-btn {
color: #6B7280;
font-size: 16px;
background: none;
border: none;
cursor: pointer;
}
.comment-header .send-btn {
color: white;
background-color: #FF7A00;
border: 1px solid #FF7A00; /* 橙色边框 */
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
.comment-header .send-btn:hover {
background-color: #FF6600;
border-color: #FF6600; /* 悬停时边框颜色加深 */
}
.comment-content {
padding: 16px;
display: flex;
flex-direction: column;
height: 100%;
}
/* 新增样式 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
color: #9CA3AF;
}
.empty-comment {
text-align: center;
padding: 20px 0;
color: #9CA3AF;
font-size: 14px;
}
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 15px 0;
color: #6B7280;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<div class="header-back-btn" @click="goBack"><i class="el-icon-arrow-left"></i></div>
<div class="header-title"></div>
<div class="header-share">...</div>
</div>
<div style="height: 85%; overflow-y: scroll; overflow-x: hidden">
<div class="blog-info-box" ref="swiper"
@touchstart="moveStart"
@touchmove="moving"
@touchend="moveEnd">
<div class="swiper-item" v-for="(img, i) in blog.images" :key="i">
<img :src="img" alt="" style="width: 100%" height="100%">
</div>
</div>
<div class="basic">
<div class="basic-icon" @click="toOtherInfo">
<img :src="blog.icon || '/imgs/icons/default-icon.png'" alt="用户头像">
</div>
<div class="basic-info">
<div class="name">{{blog.name}}</div>
<span class="time">{{formatTime(new Date(blog.createTime))}}</span>
</div>
<div style="width: 20%">
<div class="logout-btn" @click="follow" v-show="!user || user.id !== blog.userId ">
{{followed ? '取消关注' : '关注'}}
</div>
</div>
</div>
<div class="blog-text" v-html="blog.content">
</div>
<div class="shop-basic">
<div class="shop-icon">
<img :src="shop.image" alt="店铺图片">
</div>
<div style="width: 80%">
<div class="name">{{shop.name}}</div>
<div>
<el-rate
v-model="shop.score/10">
</el-rate>
</div>
<div class="shop-avg">¥{{shop.avgPrice}}/人</div>
</div>
</div>
<div class="zan-box">
<div>
<svg t="1646634642977" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2187" width="20" height="20">
<path d="M160 944c0 8.8-7.2 16-16 16h-32c-26.5 0-48-21.5-48-48V528c0-26.5 21.5-48 48-48h32c8.8 0 16 7.2 16 16v448zM96 416c-53 0-96 43-96 96v416c0 53 43 96 96 96h96c17.7 0 32-14.3 32-32V448c0-17.7-14.3-32-32-32H96zM505.6 64c16.2 0 26.4 8.7 31 13.9 4.6 5.2 12.1 16.3 10.3 32.4l-23.5 203.4c-4.9 42.2 8.6 84.6 36.8 116.4 28.3 31.7 68.9 49.9 111.4 49.9h271.2c6.6 0 10.8 3.3 13.2 6.1s5 7.5 4 14l-48 303.4c-6.9 43.6-29.1 83.4-62.7 112C815.8 944.2 773 960 728.9 960h-317c-33.1 0-59.9-26.8-59.9-59.9v-455c0-6.1 1.7-12 5-17.1 69.5-109 106.4-234.2 107-364h41.6z m0-64h-44.9C427.2 0 400 27.2 400 60.7c0 127.1-39.1 251.2-112 355.3v484.1c0 68.4 55.5 123.9 123.9 123.9h317c122.7 0 227.2-89.3 246.3-210.5l47.9-303.4c7.8-49.4-30.4-94.1-80.4-94.1H671.6c-50.9 0-90.5-44.4-84.6-95l23.5-203.4C617.7 55 568.7 0 505.6 0z" p-id="2188" :fill="blog.isLike ? '#ff6633' : '#82848a'"></path>
</svg>
</div>
<div class="zan-list">
<div class="user-icon-mini" v-for="u in likes" :key="u.id">
<img :src="u.icon || '/imgs/icons/default-icon.png'" alt="点赞用户头像">
</div>
<div style="margin-left:10px;text-align: center;line-height: 24px;">{{blog.liked}}人点赞</div>
</div>
</div>
<div class="blog-divider"></div>
<div class="blog-comments">
<div class="comments-head">
<div>网友评价 <span>{{totalComments}}条</span></div>
<div @click="loadMoreComments" v-if="hasMore && !isLoading">
<i class="el-icon-arrow-right"></i>
</div>
<div v-else-if="isLoading">
<i class="el-icon-loading is-spinning"></i> 加载中...
</div>
<div v-else>没有更多评论了</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading && comments.length === 0" class="loading">
<i class="el-icon-loading is-spinning"></i>
<span>加载评论中...</span>
</div>
<!-- 空评论状态 -->
<div v-else-if="comments.length === 0 && !isLoading" class="empty-comment">
暂无评论,快来发表第一条评论吧
</div>
<!-- 评论列表 -->
<div class="comment-list" v-else>
<div class="comment-box" v-for="comment in comments" :key="comment.id">
<div class="comment-icon">
<img :src="comment.icon || '/imgs/icons/default-icon.png'" alt="评论用户头像">
</div>
<div class="comment-info">
<div class="comment-user">
{{comment.name}}
<span v-if="comment.level">Lv{{comment.level}}</span>
</div>
<div style="padding: 5px 0; font-size: 14px">
{{comment.content}}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 5px; color: #999;">
<div>{{formatCommentTime(comment.createTime)}}</div>
</div>
</div>
</div>
<!-- 加载更多按钮 -->
<div v-if="hasMore && !isLoading" class="load-more" @click="loadMoreComments">
查看更多评论 <i class="el-icon-arrow-down"></i>
</div>
</div>
</div>
<div class="blog-divider"></div>
</div>
<div class="foot">
<div class="foot-box">
<div class="foot-view" @click="addLike()">
<svg t="1646634642977" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2187" width="26" height="26">
<path d="M160 944c0 8.8-7.2 16-16 16h-32c-26.5 0-48-21.5-48-48V528c0-26.5 21.5-48 48-48h32c8.8 0 16 7.2 16 16v448zM96 416c-53 0-96 43-96 96v416c0 53 43 96 96 96h96c17.7 0 32-14.3 32-32V448c0-17.7-14.3-32-32-32H96zM505.6 64c16.2 0 26.4 8.7 31 13.9 4.6 5.2 12.1 16.3 10.3 32.4l-23.5 203.4c-4.9 42.2 8.6 84.6 36.8 116.4 28.3 31.7 68.9 49.9 111.4 49.9h271.2c6.6 0 10.8 3.3 13.2 6.1s5 7.5 4 14l-48 303.4c-6.9 43.6-29.1 83.4-62.7 112C815.8 944.2 773 960 728.9 960h-317c-33.1 0-59.9-26.8-59.9-59.9v-455c0-6.1 1.7-12 5-17.1 69.5-109 106.4-234.2 107-364h41.6z m0-64h-44.9C427.2 0 400 27.2 400 60.7c0 127.1-39.1 251.2-112 355.3v484.1c0 68.4 55.5 123.9 123.9 123.9h317c122.7 0 227.2-89.3 246.3-210.5l47.9-303.4c7.8-49.4-30.4-94.1-80.4-94.1H671.6c-50.9 0-90.5-44.4-84.6-95l23.5-203.4C617.7 55 568.7 0 505.6 0z" p-id="2188" :fill="blog.isLike ? '#ff6633' : '#82848a'"></path>
</svg>
<span :class="{liked: blog.isLike}">{{blog.liked}}</span>
</div>
</div>
<div style="width: 40%">
</div>
<div class="foot-box">
<div class="foot-view" @click="showCommentBox">
<i class="el-icon-chat-square"></i>
</div>
</div>
</div>
<!-- 评论输入框 -->
<div class="comment-container" ref="commentContainer">
<div class="comment-header">
<button class="cancel-btn" @click="hideCommentBox">取消</button>
<h3>评论</h3>
<button class="send-btn" @click="sendComment" :disabled="!commentText.trim()">发送</button>
</div>
<div class="comment-content">
<textarea class="comment-input" v-model="commentText" placeholder="输入评论内容..." ref="commentInput"></textarea>
</div>
</div>
</div>
<script src="./js/vue.js"></script>
<script src="./js/axios.min.js"></script>
<!-- 引入组件库 -->
<script src="./js/element.js"></script>
<script src="./js/common.js"></script>
<script>
let each = function (ary, callback) {
for (let i = 0, l = ary.length; i < l; i++) {
if (callback(ary[i], i) === false) break
}
}
const app = new Vue({
el: "#app",
data: {
util,
blog: {},
shop: {},
likes: [],
user: {}, // 登录用户
followed: false, // 是否关注了
_width: 0,
duration: 300,
container: null,
items: [],
active: 0,
start: {
x: 0,
y: 0
},
move: {
x: 0,
y: 0
},
sensitivity: 60,
resistance: 0.3,
commentText: '', // 评论内容
commentContainer: null,
// 评论相关数据
comments: [], // 当前页评论列表
totalComments: 0, // 评论总数
currentPage: 1, // 当前页码
pageSize: 10, // 每页大小
hasMore: false, // 是否有更多数据
isLoading: false, // 是否正在加载
},
created() {
let id = util.getUrlParam("id");
this.queryBlogById(id);
this.fetchComments(id);
},
mounted() {
this.commentContainer = this.$refs.commentContainer;
},
methods: {
init() {
// 获得父容器节点
this.container = this.$refs.swiper
// 获得所有的子节点
this.items = this.container.querySelectorAll('.swiper-item')
this.updateItemWidth()
this.setTransform()
this.setTransition('none')
},
goBack() {
history.back();
},
toOtherInfo(){
if(this.blog.userId === this.user.id){
location.href = "/info.html"
}else{
location.href = "/other-info.html?id=" + this.blog.userId
}
},
queryBlogById(id) {
axios.get("/blog/" + id)
.then(({data}) => {
data.images = data.images.split(",")
this.blog = data;
this.$nextTick(this.init);
this.queryShopById(data.shopId)
this.queryLikeList(id);
this.queryLoginUser();
})
.catch(this.$message.error)
},
queryShopById(shopId) {
axios.get("/shop/" + shopId)
.then(({data}) => {
data.image = data.images.split(",")[0]
this.shop = data
})
.catch(this.$message.error)
},
queryLikeList(id){
axios.get("/blog/likes/" + id)
.then(({data}) => this.likes = data)
.catch(this.$message.error)
},
addLike(){
axios.put("/blog/like/" +this.blog.id)
.then(({data}) => {
axios.get("/blog/" + this.blog.id)
.then(({data}) => {
data.images = data.images.split(",")
this.blog = data;
this.queryLikeList(this.blog.id);
})
.catch(this.$message.error)
})
.catch(err => {
this.$message.error(err)
})
},
isFollowed(){
axios.get("/follow/or/not/" + this.blog.userId)
.then(({data}) => this.followed = data)
.catch(this.$message.error)
},
follow(){
axios.put("/follow/" + this.blog.userId + "/" + !this.followed)
.then(() => {
this.$message.success(this.followed ? "已取消关注" : "已关注")
this.followed = !this.followed
})
.catch(this.$message.error)
},
formatTime(b) {
return b.getFullYear() + "年" + (b.getMonth() + 1) + "月" + b.getDate() + "日 ";
},
formatMinutes(m) {
if (m < 10) m = "0" + m
return m;
},
queryLoginUser(){
// 查询用户信息
axios.get("/user/me")
.then(({ data }) => {
// 保存用户
this.user = data;
if(this.user.id !== this.blog.userId){
this.isFollowed();
}
})
.catch(console.log)
},
// 轮播图相关方法
updateItemWidth() {
this._width = this.container.offsetWidth || document.documentElement.offsetWidth
},
setTransform(offset) {
offset = offset || 0
each(this.items, (item, i) => {
let distance = (i - this.active) * this._width + offset
let transform = `translate3d(${distance}px, 0, 0)`
item.style.webkitTransform = transform
item.style.transform = transform
})
},
setTransition(duration) {
duration = duration || this.duration
duration = typeof duration === 'number' ? (duration + 'ms') : duration
each(this.items, (item) => {
item.style.webkitTransition = duration
item.style.transition = duration
})
},
moveStart(e) {
this.start.x = e.changedTouches[0].pageX
this.start.y = e.changedTouches[0].pageY
this.setTransition('none')
},
moving(e) {
e.preventDefault()
e.stopPropagation()
let distanceX = e.changedTouches[0].pageX - this.start.x
let distanceY = e.changedTouches[0].pageY - this.start.y
if (Math.abs(distanceX) > Math.abs(distanceY)) {
this.isMoving = true
this.move.x = this.start.x + distanceX
this.move.y = this.start.y + distanceY
if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) {
distanceX = distanceX * this.resistance
}
this.setTransform(distanceX)
}
},
moveEnd(e) {
if (this.isMoving) {
e.preventDefault()
e.stopPropagation()
let distance = this.move.x - this.start.x
if (Math.abs(distance) > this.sensitivity) {
if (distance < 0) {
this.next()
} else {
this.prev()
}
} else {
this.back()
}
this.reset()
this.isMoving = false;
}
},
next() {
let index = this.active + 1
this.go(index)
},
prev() {
let index = this.active - 1
this.go(index)
},
reset() {
this.start.x = 0
this.start.y = 0
this.move.x = 0
this.move.y = 0
},
back() {
this.setTransition()
this.setTransform()
},
destroy() {
this.setTransition('none')
},
go(index) {
this.active = index
if (this.active < 0) {
this.active = 0
} else if (this.active > this.items.length - 1) {
this.active = this.items.length - 1
}
this.$emit('change', this.active)
this.setTransition()
this.setTransform()
},
// 评论相关方法
showCommentBox() {
if (!this.user.id) {
this.$message.warning("请先登录");
return;
}
this.commentContainer.classList.add('active');
setTimeout(() => {
this.$refs.commentInput.focus();
}, 300);
},
hideCommentBox() {
this.commentContainer.classList.remove('active');
this.commentText = '';
},
sendComment() {
const content = this.commentText.trim();
if (content) {
// 使用FormData格式,与后端接口参数匹配
const formData = new FormData();
formData.append('comment', content);
formData.append('blogId', this.blog.id);
axios.post("/blog-comments", formData)
.then(() => {
this.$message.success("评论发送成功");
this.hideCommentBox();
this.commentText = '';
// 刷新第一页评论
this.fetchComments(this.blog.id, 1);
})
.catch(err => {
this.$message.error("评论发送失败: " + err.response?.data?.message || err.message);
console.error(err);
});
} else {
this.$message.warning("请输入评论内容");
}
},
// 获取评论列表 - 适配后端分页接口
fetchComments(blogId, page = 1) {
// 如果是加载第一页,清空现有评论
if (page === 1) {
this.comments = [];
}
this.isLoading = true;
axios.get(`/blog-comments/${blogId}`, {
params: {
page: page,
size: this.pageSize
}
})
.then((response) => {
// 确保响应数据存在
if (!response.data) {
this.$message.warning("评论数据为空");
this.hasMore = false;
this.isLoading = false;
return;
}
const pageResult = response.data;
// 验证必要字段存在
if (pageResult.records === undefined || pageResult.totalRecords === undefined) {
this.$message.warning("评论数据格式异常,缺少必要字段");
this.hasMore = false;
this.isLoading = false;
return;
}
// 更新评论列表
if (page === 1) {
this.comments = pageResult.records || [];
} else {
this.comments = [...this.comments, ...(pageResult.records || [])];
}
// 更新分页信息
this.totalComments = pageResult.totalRecords || 0;
this.currentPage = pageResult.currentPage || page;
this.pageSize = pageResult.pageSize || this.pageSize;
// 判断是否还有更多数据
this.hasMore = this.comments.length < this.totalComments;
})
.catch(err => {
this.$message.error('评论加载失败: ' + err.response?.data?.message || '网络错误');
console.error(err);
})
.finally(() => {
this.isLoading = false;
});
},
// 加载更多评论
loadMoreComments() {
if (!this.isLoading && this.hasMore) {
this.fetchComments(this.blog.id, this.currentPage + 1);
}
},
// 格式化评论时间(最终修复版)
formatCommentTime(timestamp) {
if (!timestamp) return '';
// 尝试将时间戳转换为数字
let timeValue;
// 处理字符串类型的时间戳
if (typeof timestamp === 'string') {
// 移除所有非数字字符
const cleanTimestamp = timestamp.replace(/[^\d]/g, '');
// 如果清理后为空字符串,尝试其他解析方法
if (!cleanTimestamp) {
// 尝试直接解析原始字符串
try {
timeValue = Number(timestamp);
} catch (e) {
console.error('Failed to parse timestamp:', timestamp);
return '未知时间';
}
} else {
timeValue = Number(cleanTimestamp);
}
} else if (typeof timestamp === 'number') {
timeValue = timestamp;
} else {
console.error('Unsupported timestamp type:', typeof timestamp);
return '未知时间';
}
// 验证转换后的时间戳是否为有效数字
if (isNaN(timeValue)) {
console.error('Invalid timestamp value:', timestamp);
return '未知时间';
}
// 处理零值或负值时间戳
if (timeValue <= 0) {
return '未知时间';
}
// 根据时间戳范围智能判断(优化处理超长时间戳)
let commentDate;
// 特殊处理:如果时间戳大于当前时间的2倍,可能是前端时间戳生成问题
const currentTime = Date.now();
if (timeValue > currentTime * 2) {
// 尝试将超长时间戳除以10的幂,直到合理范围
let adjustedTimestamp = timeValue;
while (adjustedTimestamp > currentTime * 2 && adjustedTimestamp > 1e15) {
adjustedTimestamp = Math.floor(adjustedTimestamp / 10);
}
// 使用调整后的时间戳
commentDate = new Date(adjustedTimestamp);
} else {
// 默认作为毫秒级时间戳处理
commentDate = new Date(timeValue);
}
// 验证日期有效性
if (isNaN(commentDate.getTime())) {
console.error('Invalid date from timestamp:', timestamp);
return '未知时间';
}
// 计算与当前时间的差值(毫秒)
const now = new Date();
const diffMs = now - commentDate;
// 处理未来时间(diffMs为负数)
if (diffMs < 0) {
// 未来时间显示完整日期
return commentDate.toISOString().slice(0, 10);
}
// 计算差值(分钟、小时、天)
const diffMinutes = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
// 相对时间显示
if (diffMinutes < 1) return '刚刚';
if (diffMinutes < 60) return `${diffMinutes}分钟前`;
if (diffHours < 24) return `${diffHours}小时前`;
if (diffDays < 7) return `${diffDays}天前`;
// 超过7天,显示完整日期
return commentDate.toISOString().slice(0, 10);
}
}
})
</script>
</body>
</html>时间显示错了,传进来的"createTime": 1749989266662,后端System.currentTimeMillis()产生的,时间显示错了