00:00:00
朋友圈
朋友圈
提出问题
bash
01.
请帮我生成一个 个人朋友圈 前端页面(要求 我平常发表的动态可以包含多张图片,加一些文字,按日期由近及远排序)。
02.
请把以上前端代码 按如下方式重新生成下:
vue3版本的组件,不要使用Tailwind CSS,样式直接使用普通css写在组件内。
请给出完整的代码。
03.
但是这个 新增 发布文章后,为啥再次运行项目,刚才新创建的数据就丢失了呢?
v1-2025.9.13-豆包生成-本地html版
提出问题
bash
请帮我生成一个 个人朋友圈 前端页面(要求 我平常发表的动态可以包含多张图片,加一些文字,按日期由近及远排序)。
豆包输出
我将为您创建一个个人朋友圈前端页面,满足展示包含多张图片和文字的动态,并按日期由近及远排序的需求。这个页面采用现代设计风格,包含发布功能、动态展示、图片预览等完整功能。
个人朋友圈前端页面
V1 moments.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的朋友圈</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
min-height: 100vh;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
}
/* 头部样式 */
header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
position: sticky;
top: 0;
z-index: 100;
}
.header-title {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.page-title {
font-size: 1.2rem;
font-weight: 600;
}
.publish-btn {
background-color: #00c853;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background-color 0.2s;
}
.publish-btn:hover {
background-color: #00b248;
}
/* 发布动态区域 */
.publish-area {
padding: 15px 20px;
border-bottom: 10px solid #f5f5f5;
}
.publish-container {
display: flex;
gap: 12px;
}
.publish-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 25px;
outline: none;
transition: border-color 0.2s;
font-size: 1rem;
}
.publish-input:focus {
border-color: #b3e5fc;
}
.publish-actions {
display: flex;
justify-content: space-between;
margin-top: 12px;
padding-left: 52px;
}
.action-icons {
display: flex;
gap: 20px;
}
.action-icon {
color: #666;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
transition: color 0.2s;
}
.action-icon:hover {
color: #007bff;
}
/* 动态列表 */
.moments-list {
padding-bottom: 20px;
}
.moment-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
transition: background-color 0.1s;
}
.moment-item:hover {
background-color: #fafafa;
}
.moment-header {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.moment-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.username {
font-weight: 600;
margin-bottom: 2px;
display: inline-block;
}
.post-time {
font-size: 0.8rem;
color: #999;
}
.moment-content {
margin-bottom: 10px;
font-size: 1rem;
line-height: 1.6;
}
/* 图片网格 */
.images-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-bottom: 10px;
}
.grid-img {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s;
}
.grid-img:hover {
transform: scale(1.02);
}
/* 处理不同数量图片的布局 */
.images-grid.single {
grid-template-columns: 1fr;
max-width: 300px;
}
.images-grid.double {
grid-template-columns: repeat(2, 1fr);
}
/* 互动按钮 */
.moment-actions {
display: flex;
justify-content: space-around;
padding-top: 5px;
border-top: 1px solid #f0f0f0;
margin-top: 8px;
}
.action-btn {
flex: 1;
background: none;
border: none;
padding: 8px 0;
color: #666;
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: color 0.2s, background-color 0.2s;
border-radius: 4px;
}
.action-btn:hover {
background-color: #f5f5f5;
color: #007bff;
}
.action-btn.liked {
color: #e53935;
}
/* 发布弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background-color: white;
width: 90%;
max-width: 600px;
border-radius: 10px;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.1rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.post-content {
width: 100%;
min-height: 120px;
border: none;
outline: none;
resize: none;
font-size: 1rem;
margin-bottom: 20px;
line-height: 1.6;
}
.image-upload-area {
margin-bottom: 20px;
}
.upload-title {
font-size: 0.9rem;
color: #666;
margin-bottom: 10px;
}
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.preview-item {
width: 80px;
height: 80px;
position: relative;
}
.preview-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.remove-img {
position: absolute;
top: -5px;
right: -5px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: background-color 0.2s;
}
.remove-img:hover {
background-color: white;
}
.upload-btn {
width: 80px;
height: 80px;
border: 2px dashed #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.upload-btn:hover {
border-color: #007bff;
color: #007bff;
}
.upload-input {
display: none;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.cancel-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.cancel-btn:hover {
background-color: #f5f5f5;
}
.submit-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.submit-btn:hover {
background-color: #0069d9;
}
.submit-btn:disabled {
background-color: #81b8f0;
cursor: not-allowed;
}
/* 图片预览弹窗 */
.image-preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.image-preview-overlay.active {
opacity: 1;
visibility: visible;
}
.preview-content {
position: relative;
max-width: 90%;
max-height: 90vh;
}
.preview-image {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.preview-close {
position: absolute;
top: -40px;
right: 0;
color: white;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.preview-nav {
position: absolute;
top: 50%;
width: 100%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
}
.nav-btn {
background-color: rgba(0, 0, 0, 0.3);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.2rem;
transition: background-color 0.2s;
}
.nav-btn:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.preview-count {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 0.9rem;
}
/* 空状态 */
.empty-state {
padding: 50px 20px;
text-align: center;
color: #999;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.5;
}
/* 响应式调整 */
@media (max-width: 600px) {
.images-grid {
gap: 3px;
}
.action-btn span {
font-size: 0.8rem;
}
.publish-actions {
padding-left: 0;
flex-direction: column;
gap: 10px;
}
.action-icons {
justify-content: space-around;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<header>
<div class="header-title">
<img src="https://picsum.photos/200/200?random=1" alt="个人头像" class="avatar">
<h1 class="page-title">我的朋友圈</h1>
</div>
<button class="publish-btn" id="openPublishModal">
<i class="fas fa-plus"></i> 发布
</button>
</header>
<!-- 发布区域 -->
<div class="publish-area">
<div class="publish-container">
<img src="https://picsum.photos/200/200?random=1" alt="个人头像" class="avatar">
<input type="text" class="publish-input" placeholder="分享你的想法..." id="quickPublish">
</div>
<div class="publish-actions">
<div class="action-icons">
<div class="action-icon">
<i class="fas fa-image"></i>
<span>图片</span>
</div>
<div class="action-icon">
<i class="fas fa-smile"></i>
<span>表情</span>
</div>
<div class="action-icon">
<i class="fas fa-map-marker-alt"></i>
<span>位置</span>
</div>
</div>
</div>
</div>
<!-- 动态列表 -->
<div class="moments-list" id="momentsList">
<!-- 动态内容将通过JavaScript动态生成 -->
</div>
<!-- 空状态 -->
<div class="empty-state" id="emptyState">
<div class="empty-icon">
<i class="far fa-comment-dots"></i>
</div>
<p>还没有动态,发布你的第一条动态吧~</p>
</div>
</div>
<!-- 发布动态弹窗 -->
<div class="modal-overlay" id="publishModal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">发布动态</h2>
<button class="close-btn" id="closeModal">×</button>
</div>
<div class="modal-body">
<textarea class="post-content" placeholder="分享你的想法..." id="postContent"></textarea>
<div class="image-upload-area">
<div class="upload-title">添加图片</div>
<div class="preview-container" id="previewContainer">
<label class="upload-btn" for="imageUpload">
<i class="fas fa-plus"></i>
<input type="file" id="imageUpload" class="upload-input" accept="image/*" multiple>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" id="cancelPost">取消</button>
<button class="submit-btn" id="submitPost" disabled>发布</button>
</div>
</div>
</div>
<!-- 图片预览弹窗 -->
<div class="image-preview-overlay" id="imagePreview">
<div class="preview-content">
<button class="preview-close" id="closePreview">×</button>
<img src="" alt="图片预览" class="preview-image" id="previewImage">
<div class="preview-nav">
<button class="nav-btn prev-btn" id="prevImage">
<i class="fas fa-chevron-left"></i>
</button>
<button class="nav-btn next-btn" id="nextImage">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<div class="preview-count" id="previewCount">1/1</div>
</div>
</div>
<script>
// 存储动态数据
let moments = [
{
id: 1,
content: "今天天气真好,去公园散步了~ 🌸",
images: [
"https://picsum.photos/600/400?random=10",
"https://picsum.photos/600/400?random=11"
],
time: new Date(Date.now() - 3600000).getTime(), // 1小时前
likes: 15,
liked: false,
comments: 3
},
{
id: 2,
content: "分享一下新做的晚餐,味道还不错!",
images: [
"https://picsum.photos/600/400?random=20"
],
time: new Date(Date.now() - 86400000).getTime(), // 1天前
likes: 28,
liked: true,
comments: 7
},
{
id: 3,
content: "周末去看了一场精彩的电影,强烈推荐!",
images: [],
time: new Date(Date.now() - 172800000).getTime(), // 2天前
likes: 12,
liked: false,
comments: 2
}
];
// DOM元素
const momentsList = document.getElementById('momentsList');
const emptyState = document.getElementById('emptyState');
const publishModal = document.getElementById('publishModal');
const openPublishModalBtn = document.getElementById('openPublishModal');
const closeModalBtn = document.getElementById('closeModal');
const cancelPostBtn = document.getElementById('cancelPost');
const submitPostBtn = document.getElementById('submitPost');
const postContent = document.getElementById('postContent');
const imageUpload = document.getElementById('imageUpload');
const previewContainer = document.getElementById('previewContainer');
const quickPublish = document.getElementById('quickPublish');
const imagePreview = document.getElementById('imagePreview');
const previewImage = document.getElementById('previewImage');
const closePreviewBtn = document.getElementById('closePreview');
const prevImageBtn = document.getElementById('prevImage');
const nextImageBtn = document.getElementById('nextImage');
const previewCount = document.getElementById('previewCount');
// 预览相关变量
let currentPreviewIndex = 0;
let currentPreviewImages = [];
// 初始化页面
function init() {
renderMoments();
setupEventListeners();
}
// 渲染动态列表
function renderMoments() {
// 按时间排序(最新的在前)
moments.sort((a, b) => b.time - a.time);
if (moments.length === 0) {
emptyState.style.display = 'block';
momentsList.style.display = 'none';
return;
}
emptyState.style.display = 'none';
momentsList.style.display = 'block';
momentsList.innerHTML = '';
moments.forEach(moment => {
const momentElement = createMomentElement(moment);
momentsList.appendChild(momentElement);
});
}
// 创建动态元素
function createMomentElement(moment) {
const div = document.createElement('div');
div.className = 'moment-item';
div.dataset.id = moment.id;
// 格式化时间
const timeString = formatTime(moment.time);
// 构建图片网格
let imagesHtml = '';
if (moment.images.length > 0) {
let gridClass = 'images-grid';
if (moment.images.length === 1) gridClass += ' single';
else if (moment.images.length === 2) gridClass += ' double';
imagesHtml = `<div class="${gridClass}">`;
moment.images.forEach((img, index) => {
imagesHtml += `<img src="${img}" alt="动态图片" class="grid-img" data-index="${index}">`;
});
imagesHtml += `</div>`;
}
// 点赞图标
const likeIcon = moment.liked ? 'fas fa-heart' : 'far fa-heart';
div.innerHTML = `
<div class="moment-header">
<img src="https://picsum.photos/200/200?random=1" alt="用户头像" class="moment-avatar">
<div class="user-info">
<div class="username">我</div>
<div class="post-time">${timeString}</div>
</div>
</div>
${moment.content ? `<div class="moment-content">${moment.content}</div>` : ''}
${imagesHtml}
<div class="moment-actions">
<button class="action-btn like-btn ${moment.liked ? 'liked' : ''}">
<i class="${likeIcon}"></i>
<span>${moment.likes}</span>
</button>
<button class="action-btn comment-btn">
<i class="far fa-comment"></i>
<span>${moment.comments}</span>
</button>
<button class="action-btn share-btn">
<i class="far fa-share-square"></i>
<span>分享</span>
</button>
</div>
`;
// 添加事件监听
const likeBtn = div.querySelector('.like-btn');
likeBtn.addEventListener('click', () => toggleLike(moment.id));
// 图片点击事件
const images = div.querySelectorAll('.grid-img');
images.forEach(img => {
img.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
openImagePreview(moment.images, index);
});
});
return div;
}
// 格式化时间显示
function formatTime(timestamp) {
const now = Date.now();
const diff = now - timestamp;
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`;
} else if (diff < week) {
return `${Math.floor(diff / day)}天前`;
} else {
const date = new Date(timestamp);
return `${date.getMonth() + 1}月${date.getDate()}日`;
}
}
// 切换点赞状态
function toggleLike(id) {
const moment = moments.find(m => m.id === id);
if (!moment) return;
if (moment.liked) {
moment.likes--;
} else {
moment.likes++;
}
moment.liked = !moment.liked;
renderMoments();
}
// 打开发布弹窗
function openPublishModal() {
publishModal.classList.add('active');
postContent.focus();
// 清空内容
postContent.value = '';
// 移除预览图片(保留上传按钮)
const previewItems = previewContainer.querySelectorAll('.preview-item');
previewItems.forEach(item => item.remove());
checkSubmitDisabled();
}
// 关闭发布弹窗
function closePublishModal() {
publishModal.classList.remove('active');
}
// 检查发布按钮是否禁用
function checkSubmitDisabled() {
const content = postContent.value.trim();
const hasImages = previewContainer.querySelectorAll('.preview-item').length > 0;
submitPostBtn.disabled = !(content || hasImages);
}
// 处理图片上传
function handleImageUpload(e) {
const files = e.target.files;
if (!files.length) return;
// 最多上传9张图片
const existingCount = previewContainer.querySelectorAll('.preview-item').length;
const maxAdd = 9 - existingCount;
const filesToUpload = Array.from(files).slice(0, maxAdd);
filesToUpload.forEach(file => {
const reader = new FileReader();
reader.onload = function(event) {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
previewItem.innerHTML = `
<img src="${event.target.result}" alt="预览图" class="preview-img">
<div class="remove-img" data-src="${event.target.result}">×</div>
`;
// 插入到上传按钮前
const uploadBtn = previewContainer.querySelector('.upload-btn');
previewContainer.insertBefore(previewItem, uploadBtn);
// 添加删除图片事件
const removeBtn = previewItem.querySelector('.remove-img');
removeBtn.addEventListener('click', function() {
previewItem.remove();
checkSubmitDisabled();
});
checkSubmitDisabled();
};
reader.readAsDataURL(file);
});
// 重置input值,允许重复选择同一文件
imageUpload.value = '';
}
// 发布动态
function publishMoment() {
const content = postContent.value.trim();
const previewImages = previewContainer.querySelectorAll('.preview-item img');
const images = Array.from(previewImages).map(img => img.src);
if (!content && images.length === 0) return;
// 创建新动态
const newMoment = {
id: Date.now(), // 使用时间戳作为唯一ID
content: content,
images: images,
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
// 添加到动态列表
moments.unshift(newMoment);
// 重新渲染
renderMoments();
// 关闭弹窗
closePublishModal();
}
// 快速发布
function handleQuickPublish() {
const content = quickPublish.value.trim();
if (!content) return;
// 创建新动态
const newMoment = {
id: Date.now(),
content: content,
images: [],
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
moments.unshift(newMoment);
renderMoments();
quickPublish.value = '';
}
// 打开图片预览
function openImagePreview(images, index) {
currentPreviewImages = images;
currentPreviewIndex = index;
updatePreviewImage();
imagePreview.classList.add('active');
document.body.style.overflow = 'hidden';
}
// 关闭图片预览
function closeImagePreview() {
imagePreview.classList.remove('active');
document.body.style.overflow = '';
}
// 更新预览图片
function updatePreviewImage() {
previewImage.src = currentPreviewImages[currentPreviewIndex];
previewCount.textContent = `${currentPreviewIndex + 1}/${currentPreviewImages.length}`;
}
// 上一张图片
function prevImage() {
currentPreviewIndex = (currentPreviewIndex - 1 + currentPreviewImages.length) % currentPreviewImages.length;
updatePreviewImage();
}
// 下一张图片
function nextImage() {
currentPreviewIndex = (currentPreviewIndex + 1) % currentPreviewImages.length;
updatePreviewImage();
}
// 设置事件监听
function setupEventListeners() {
// 发布弹窗相关
openPublishModalBtn.addEventListener('click', openPublishModal);
closeModalBtn.addEventListener('click', closePublishModal);
cancelPostBtn.addEventListener('click', closePublishModal);
submitPostBtn.addEventListener('click', publishMoment);
postContent.addEventListener('input', checkSubmitDisabled);
imageUpload.addEventListener('change', handleImageUpload);
// 快速发布
quickPublish.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
handleQuickPublish();
}
});
// 图片预览相关
closePreviewBtn.addEventListener('click', closeImagePreview);
prevImageBtn.addEventListener('click', prevImage);
nextImageBtn.addEventListener('click', nextImage);
// 点击预览背景关闭预览
imagePreview.addEventListener('click', (e) => {
if (e.target === imagePreview) {
closeImagePreview();
}
});
// 键盘导航预览图片
document.addEventListener('keydown', (e) => {
if (!imagePreview.classList.contains('active')) return;
if (e.key === 'Escape') {
closeImagePreview();
} else if (e.key === 'ArrowLeft') {
prevImage();
} else if (e.key === 'ArrowRight') {
nextImage();
}
});
}
// 初始化
init();
</script>
</body>
</html>
创建时间:11:18
这个个人朋友圈页面具有以下特点和功能:
- 核心功能:
- 支持发布包含文字和多张图片(最多 9 张)的动态
- 动态按日期由近及远排序展示
- 图片预览功能,支持左右切换浏览
- 点赞功能,可实时更新点赞数
- 界面设计:
- 简洁现代的 UI 设计,符合主流社交应用风格
- 响应式布局,适配不同屏幕尺寸
- 动态卡片式展示,包含头像、时间、内容、图片和互动按钮
- 根据图片数量自动调整网格布局
- 交互体验:
- 发布动态弹窗,支持图片预览和删除
- 图片点击放大预览,支持键盘导航(左右箭头切换,ESC 关闭)
- 快速发布功能,输入文字按回车即可发布
- 点赞按钮状态变化和动画效果
- 技术实现:
- 使用原生 HTML、CSS 和 JavaScript 实现,无需框架依赖
- 清晰的代码结构,便于维护和扩展
- 模块化函数设计,实现不同功能分离
页面使用了 Font Awesome 图标库提供图标支持,整体风格简洁大方,交互流畅,完全满足个人朋友圈的使用需求。
效果测试
v2-2025.9.13-豆包生成-vue版
提出问题
bash
请把以上前端代码 按如下方式重新生成下:
vue3版本的组件,不要使用Tailwind CSS,样式直接使用普通css写在组件内。
请给出完整的代码。
豆包输出
我将为您创建一个 Vue3 版本的个人朋友圈组件,满足发布包含多张图片和文字的动态,并按日期由近及远排序的需求。组件使用普通 CSS 编写样式,不依赖 Tailwind。
Vue3 个人朋友圈组件
V1 Moments.vue
vue
<template>
<div class="moments-container">
<!-- 头部区域 -->
<header class="header">
<div class="header-content">
<img
src="https://picsum.photos/200/200?random=1"
alt="个人头像"
class="avatar"
>
<h1 class="page-title">我的朋友圈</h1>
</div>
<button class="publish-btn" @click="showPublishModal = true">
<i class="icon-add">+</i> 发布
</button>
</header>
<!-- 快速发布区域 -->
<div class="quick-publish">
<img
src="https://picsum.photos/200/200?random=1"
alt="个人头像"
class="publish-avatar"
>
<input
type="text"
class="quick-input"
placeholder="分享你的想法..."
v-model="quickContent"
@keyup.enter="handleQuickPublish"
>
</div>
<!-- 动态列表 -->
<div class="moments-list">
<!-- 动态项 -->
<div
class="moment-item"
v-for="moment in sortedMoments"
:key="moment.id"
>
<div class="moment-header">
<img
src="https://picsum.photos/200/200?random=1"
alt="用户头像"
class="moment-avatar"
>
<div class="user-info">
<div class="username">我</div>
<div class="post-time">{{ formatTime(moment.time) }}</div>
</div>
</div>
<div class="moment-content" v-if="moment.content">
{{ moment.content }}
</div>
<!-- 图片网格 -->
<div
class="images-grid"
:class="{
'single': moment.images.length === 1,
'double': moment.images.length === 2,
'triple': moment.images.length === 3
}"
v-if="moment.images.length > 0"
>
<img
v-for="(img, index) in moment.images"
:key="index"
:src="img"
alt="动态图片"
class="grid-img"
@click="openImagePreview(moment.images, index)"
>
</div>
<!-- 互动按钮 -->
<div class="moment-actions">
<button
class="action-btn like-btn"
:class="{ liked: moment.liked }"
@click="toggleLike(moment.id)"
>
<i class="icon-like">{{ moment.liked ? '♥' : '♡' }}</i>
<span>{{ moment.likes }}</span>
</button>
<button class="action-btn comment-btn">
<i class="icon-comment">💬</i>
<span>{{ moment.comments }}</span>
</button>
<button class="action-btn share-btn">
<i class="icon-share">🔗</i>
<span>分享</span>
</button>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="sortedMoments.length === 0">
<div class="empty-icon">📝</div>
<p>还没有动态,发布你的第一条动态吧~</p>
</div>
</div>
<!-- 发布动态弹窗 -->
<div class="modal-overlay" v-if="showPublishModal" @click="closePublishModal">
<div class="modal" @click.stop>
<div class="modal-header">
<h2 class="modal-title">发布动态</h2>
<button class="close-btn" @click="closePublishModal">×</button>
</div>
<div class="modal-body">
<textarea
class="post-content"
placeholder="分享你的想法..."
v-model="postContent"
@input="checkPublishable"
></textarea>
<div class="image-upload">
<div class="upload-title">添加图片(最多9张)</div>
<div class="preview-container">
<div
class="preview-item"
v-for="(img, index) in previewImages"
:key="index"
>
<img :src="img" alt="预览图" class="preview-img">
<button
class="remove-img"
@click="removeImage(index)"
>×</button>
</div>
<label class="upload-btn" v-if="previewImages.length < 9">
<input
type="file"
accept="image/*"
multiple
@change="handleImageUpload"
class="upload-input"
>
<div class="upload-icon">+</div>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="closePublishModal">取消</button>
<button
class="publish-btn"
@click="publishMoment"
:disabled="!isPublishable"
>
发布
</button>
</div>
</div>
</div>
<!-- 图片预览弹窗 -->
<div class="image-preview" v-if="showImagePreview" @click="closeImagePreview">
<div class="preview-content" @click.stop>
<button class="preview-close" @click="closeImagePreview">×</button>
<img
:src="currentPreviewImage"
alt="图片预览"
class="preview-img-large"
>
<div class="preview-nav" v-if="previewImagesList.length > 1">
<button class="nav-btn prev-btn" @click="prevImage">←</button>
<button class="nav-btn next-btn" @click="nextImage">→</button>
</div>
<div class="preview-counter">
{{ currentPreviewIndex + 1 }} / {{ previewImagesList.length }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
// 动态数据
const moments = ref([
{
id: 1,
content: "今天天气真好,去公园散步了~ 🌸",
images: [
"https://picsum.photos/600/400?random=10",
"https://picsum.photos/600/400?random=11"
],
time: new Date(Date.now() - 3600000).getTime(), // 1小时前
likes: 15,
liked: false,
comments: 3
},
{
id: 2,
content: "分享一下新做的晚餐,味道还不错!",
images: [
"https://picsum.photos/600/400?random=20"
],
time: new Date(Date.now() - 86400000).getTime(), // 1天前
likes: 28,
liked: true,
comments: 7
},
{
id: 3,
content: "周末去看了一场精彩的电影,强烈推荐!",
images: [],
time: new Date(Date.now() - 172800000).getTime(), // 2天前
likes: 12,
liked: false,
comments: 2
}
]);
// 按日期排序(最新的在前)
const sortedMoments = computed(() => {
return [...moments.value].sort((a, b) => b.time - a.time);
});
// 快速发布相关
const quickContent = ref('');
// 发布弹窗相关
const showPublishModal = ref(false);
const postContent = ref('');
const previewImages = ref([]);
const isPublishable = ref(false);
// 图片预览相关
const showImagePreview = ref(false);
const previewImagesList = ref([]);
const currentPreviewIndex = ref(0);
const currentPreviewImage = computed(() => {
return previewImagesList.value[currentPreviewIndex.value] || '';
});
// 检查是否可以发布
const checkPublishable = () => {
isPublishable.value = !!postContent.value.trim() || previewImages.value.length > 0;
};
// 格式化时间显示
const formatTime = (timestamp) => {
const now = Date.now();
const diff = now - timestamp;
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`;
} else if (diff < week) {
return `${Math.floor(diff / day)}天前`;
} else {
const date = new Date(timestamp);
return `${date.getMonth() + 1}月${date.getDate()}日`;
}
};
// 切换点赞状态
const toggleLike = (id) => {
const moment = moments.value.find(m => m.id === id);
if (moment) {
if (moment.liked) {
moment.likes--;
} else {
moment.likes++;
}
moment.liked = !moment.liked;
}
};
// 打开发布弹窗
const openPublishModal = () => {
showPublishModal.value = true;
postContent.value = '';
previewImages.value = [];
isPublishable.value = false;
};
// 关闭发布弹窗
const closePublishModal = () => {
showPublishModal.value = false;
};
// 处理图片上传
const handleImageUpload = (e) => {
const files = e.target.files;
if (!files.length) return;
// 最多上传9张
const maxAdd = 9 - previewImages.value.length;
const filesToProcess = Array.from(files).slice(0, maxAdd);
filesToProcess.forEach(file => {
const reader = new FileReader();
reader.onload = (event) => {
previewImages.value.push(event.target.result);
checkPublishable();
};
reader.readAsDataURL(file);
});
// 清空input值,允许重复选择相同文件
e.target.value = '';
};
// 移除预览图片
const removeImage = (index) => {
previewImages.value.splice(index, 1);
checkPublishable();
};
// 发布动态
const publishMoment = () => {
const content = postContent.value.trim();
if (!content && previewImages.value.length === 0) return;
const newMoment = {
id: Date.now(),
content: content,
images: [...previewImages.value],
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
moments.value.unshift(newMoment);
closePublishModal();
};
// 快速发布
const handleQuickPublish = () => {
const content = quickContent.value.trim();
if (!content) return;
const newMoment = {
id: Date.now(),
content: content,
images: [],
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
moments.value.unshift(newMoment);
quickContent.value = '';
};
// 打开图片预览
const openImagePreview = (images, index) => {
previewImagesList.value = images;
currentPreviewIndex.value = index;
showImagePreview.value = true;
document.body.style.overflow = 'hidden';
};
// 关闭图片预览
const closeImagePreview = () => {
showImagePreview.value = false;
document.body.style.overflow = '';
};
// 上一张图片
const prevImage = () => {
currentPreviewIndex.value = (currentPreviewIndex.value - 1 + previewImagesList.value.length) % previewImagesList.value.length;
};
// 下一张图片
const nextImage = () => {
currentPreviewIndex.value = (currentPreviewIndex.value + 1) % previewImagesList.value.length;
};
// 监听键盘事件用于图片预览导航
watch(showImagePreview, (isVisible) => {
const handleKeydown = (e) => {
if (!isVisible) return;
if (e.key === 'Escape') {
closeImagePreview();
} else if (e.key === 'ArrowLeft') {
prevImage();
} else if (e.key === 'ArrowRight') {
nextImage();
}
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
});
</script>
<style scoped>
/* 基础样式 */
.moments-container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
}
/* 头部样式 */
.header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.header-content {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.page-title {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.publish-btn {
background-color: #00c853;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background-color 0.2s;
}
.publish-btn:hover {
background-color: #00b248;
}
.publish-btn:disabled {
background-color: #a5d6a7;
cursor: not-allowed;
}
/* 快速发布区域 */
.quick-publish {
padding: 15px 20px;
border-bottom: 10px solid #f5f5f5;
display: flex;
align-items: center;
gap: 12px;
}
.publish-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.quick-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 25px;
outline: none;
transition: border-color 0.2s;
font-size: 1rem;
}
.quick-input:focus {
border-color: #b3e5fc;
}
/* 动态列表 */
.moments-list {
padding-bottom: 20px;
}
.moment-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
transition: background-color 0.1s;
}
.moment-item:hover {
background-color: #fafafa;
}
.moment-header {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.moment-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.username {
font-weight: 600;
margin-bottom: 2px;
display: inline-block;
}
.post-time {
font-size: 0.8rem;
color: #999;
}
.moment-content {
margin-bottom: 10px;
font-size: 1rem;
line-height: 1.6;
}
/* 图片网格 */
.images-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-bottom: 10px;
}
.grid-img {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s;
}
.grid-img:hover {
transform: scale(1.02);
}
/* 不同图片数量的布局 */
.images-grid.single {
grid-template-columns: 1fr;
max-width: 300px;
}
.images-grid.double {
grid-template-columns: repeat(2, 1fr);
}
/* 互动按钮 */
.moment-actions {
display: flex;
justify-content: space-around;
padding-top: 5px;
border-top: 1px solid #f0f0f0;
margin-top: 8px;
}
.action-btn {
flex: 1;
background: none;
border: none;
padding: 8px 0;
color: #666;
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: color 0.2s, background-color 0.2s;
border-radius: 4px;
}
.action-btn:hover {
background-color: #f5f5f5;
color: #007bff;
}
.like-btn.liked {
color: #e53935;
}
/* 空状态 */
.empty-state {
padding: 50px 20px;
text-align: center;
color: #999;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 15px;
}
/* 发布弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal-overlay:not([v-if="false"]) {
opacity: 1;
visibility: visible;
}
.modal {
background-color: white;
width: 90%;
max-width: 600px;
border-radius: 10px;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.post-content {
width: 100%;
min-height: 120px;
border: none;
outline: none;
resize: none;
font-size: 1rem;
margin-bottom: 20px;
line-height: 1.6;
font-family: inherit;
}
.image-upload {
margin-bottom: 20px;
}
.upload-title {
font-size: 0.9rem;
color: #666;
margin-bottom: 10px;
}
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.preview-item {
width: 80px;
height: 80px;
position: relative;
}
.preview-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.remove-img {
position: absolute;
top: -5px;
right: -5px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
border: none;
transition: background-color 0.2s;
}
.remove-img:hover {
background-color: white;
}
.upload-btn {
width: 80px;
height: 80px;
border: 2px dashed #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.upload-btn:hover {
border-color: #007bff;
color: #007bff;
}
.upload-input {
display: none;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.cancel-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.cancel-btn:hover {
background-color: #f5f5f5;
}
/* 图片预览弹窗 */
.image-preview {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.image-preview:not([v-if="false"]) {
opacity: 1;
visibility: visible;
}
.preview-content {
position: relative;
max-width: 90%;
max-height: 90vh;
}
.preview-img-large {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.preview-close {
position: absolute;
top: -40px;
right: 0;
color: white;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.preview-nav {
position: absolute;
top: 50%;
width: 100%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
}
.nav-btn {
background-color: rgba(0, 0, 0, 0.3);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.2rem;
transition: background-color 0.2s;
}
.nav-btn:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.preview-counter {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 0.9rem;
}
/* 响应式调整 */
@media (max-width: 600px) {
.images-grid {
gap: 3px;
}
.action-btn span {
font-size: 0.8rem;
}
}
</style>
创建时间:11:22
这个 Vue3 朋友圈组件具有以下特点:
- 核心功能:
- 支持发布包含文字和多张图片(最多 9 张)的动态
- 动态按日期由近及远自动排序
- 图片预览功能,支持左右切换和键盘导航
- 点赞功能,实时更新点赞状态和数量
- 快速发布和完整发布两种发布方式
- 组件结构:
- 头部区域:显示头像和页面标题,包含发布按钮
- 快速发布区:提供便捷的文字发布功能
- 动态列表:以卡片形式展示所有动态
- 发布弹窗:支持详细编辑和图片上传
- 图片预览弹窗:大图查看多张图片
- 交互体验:
- 图片网格根据数量自动调整布局
- 点赞按钮状态变化和颜色反馈
- 图片点击放大预览,支持键盘操作
- 发布按钮状态随内容变化(禁用 / 启用)
- 样式设计:
- 使用普通 CSS 编写,不依赖任何 CSS 框架
- 响应式布局,适配不同屏幕尺寸
- 卡片式设计,清晰展示每条动态
- 适当的过渡动画,提升用户体验
组件使用 Vue3 的 Composition API 编写,代码结构清晰,逻辑分明,便于维护和扩展。您可以直接将此组件集成到 Vue3 项目中使用。
分享
效果测试
v3-2025.9.13-豆包生成-临时解决刷新后会丢失刚创建动态的问题
提出问题
bash
但是这个 新增 发布文章后,为啥再次运行项目,刚才新创建的数据就丢失了呢?
豆包输出
这是因为当前代码中的数据是存储在组件的内存中的(使用ref
定义的moments
变量),当你刷新页面或重新运行项目时,内存中的数据会被清空,所以新创建的数据会丢失。
要解决这个问题,我们需要将数据持久化存储,常见的方案有:
- 本地存储(localStorage)- 适合简单数据
- 后端数据库 - 适合需要多设备同步的数据
下面是使用 localStorage 进行数据持久化的修改方案,只需在原来的代码基础上添加数据读写逻辑:
带数据持久化的Vue3朋友圈组件
V1 Moments.vue
vue
<template>
<!-- 模板内容和之前保持一致 -->
<div class="moments-container">
<!-- 头部区域 -->
<header class="header">
<div class="header-content">
<img
src="https://picsum.photos/200/200?random=1"
alt="个人头像"
class="avatar"
>
<h1 class="page-title">我的朋友圈</h1>
</div>
<button class="publish-btn" @click="showPublishModal = true">
<i class="icon-add">+</i> 发布
</button>
</header>
<!-- 快速发布区域 -->
<div class="quick-publish">
<img
src="https://picsum.photos/200/200?random=1"
alt="个人头像"
class="publish-avatar"
>
<input
type="text"
class="quick-input"
placeholder="分享你的想法..."
v-model="quickContent"
@keyup.enter="handleQuickPublish"
>
</div>
<!-- 动态列表 -->
<div class="moments-list">
<!-- 动态项 -->
<div
class="moment-item"
v-for="moment in sortedMoments"
:key="moment.id"
>
<div class="moment-header">
<img
src="https://picsum.photos/200/200?random=1"
alt="用户头像"
class="moment-avatar"
>
<div class="user-info">
<div class="username">我</div>
<div class="post-time">{{ formatTime(moment.time) }}</div>
</div>
</div>
<div class="moment-content" v-if="moment.content">
{{ moment.content }}
</div>
<!-- 图片网格 -->
<div
class="images-grid"
:class="{
'single': moment.images.length === 1,
'double': moment.images.length === 2,
'triple': moment.images.length === 3
}"
v-if="moment.images.length > 0"
>
<img
v-for="(img, index) in moment.images"
:key="index"
:src="img"
alt="动态图片"
class="grid-img"
@click="openImagePreview(moment.images, index)"
>
</div>
<!-- 互动按钮 -->
<div class="moment-actions">
<button
class="action-btn like-btn"
:class="{ liked: moment.liked }"
@click="toggleLike(moment.id)"
>
<i class="icon-like">{{ moment.liked ? '♥' : '♡' }}</i>
<span>{{ moment.likes }}</span>
</button>
<button class="action-btn comment-btn">
<i class="icon-comment">💬</i>
<span>{{ moment.comments }}</span>
</button>
<button class="action-btn share-btn">
<i class="icon-share">🔗</i>
<span>分享</span>
</button>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="sortedMoments.length === 0">
<div class="empty-icon">📝</div>
<p>还没有动态,发布你的第一条动态吧~</p>
</div>
</div>
<!-- 发布动态弹窗 -->
<div class="modal-overlay" v-if="showPublishModal" @click="closePublishModal">
<div class="modal" @click.stop>
<div class="modal-header">
<h2 class="modal-title">发布动态</h2>
<button class="close-btn" @click="closePublishModal">×</button>
</div>
<div class="modal-body">
<textarea
class="post-content"
placeholder="分享你的想法..."
v-model="postContent"
@input="checkPublishable"
></textarea>
<div class="image-upload">
<div class="upload-title">添加图片(最多9张)</div>
<div class="preview-container">
<div
class="preview-item"
v-for="(img, index) in previewImages"
:key="index"
>
<img :src="img" alt="预览图" class="preview-img">
<button
class="remove-img"
@click="removeImage(index)"
>×</button>
</div>
<label class="upload-btn" v-if="previewImages.length < 9">
<input
type="file"
accept="image/*"
multiple
@change="handleImageUpload"
class="upload-input"
>
<div class="upload-icon">+</div>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="closePublishModal">取消</button>
<button
class="publish-btn"
@click="publishMoment"
:disabled="!isPublishable"
>
发布
</button>
</div>
</div>
</div>
<!-- 图片预览弹窗 -->
<div class="image-preview" v-if="showImagePreview" @click="closeImagePreview">
<div class="preview-content" @click.stop>
<button class="preview-close" @click="closeImagePreview">×</button>
<img
:src="currentPreviewImage"
alt="图片预览"
class="preview-img-large"
>
<div class="preview-nav" v-if="previewImagesList.length > 1">
<button class="nav-btn prev-btn" @click="prevImage">←</button>
<button class="nav-btn next-btn" @click="nextImage">→</button>
</div>
<div class="preview-counter">
{{ currentPreviewIndex + 1 }} / {{ previewImagesList.length }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
// 从localStorage加载数据或使用初始数据
const loadMoments = () => {
const saved = localStorage.getItem('momentsData');
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
console.error('Failed to parse moments data', e);
return [];
}
}
// 初始示例数据
return [
{
id: 1,
content: "今天天气真好,去公园散步了~ 🌸",
images: [
"https://picsum.photos/600/400?random=10",
"https://picsum.photos/600/400?random=11"
],
time: new Date(Date.now() - 3600000).getTime(), // 1小时前
likes: 15,
liked: false,
comments: 3
},
{
id: 2,
content: "分享一下新做的晚餐,味道还不错!",
images: [
"https://picsum.photos/600/400?random=20"
],
time: new Date(Date.now() - 86400000).getTime(), // 1天前
likes: 28,
liked: true,
comments: 7
},
{
id: 3,
content: "周末去看了一场精彩的电影,强烈推荐!",
images: [],
time: new Date(Date.now() - 172800000).getTime(), // 2天前
likes: 12,
liked: false,
comments: 2
}
];
};
// 保存数据到localStorage
const saveMoments = (data) => {
try {
localStorage.setItem('momentsData', JSON.stringify(data));
} catch (e) {
console.error('Failed to save moments data', e);
}
};
// 动态数据 - 从localStorage加载
const moments = ref(loadMoments());
// 监听数据变化,自动保存到localStorage
watch(moments, (newValue) => {
saveMoments(newValue);
}, { deep: true });
// 组件挂载时加载数据
onMounted(() => {
moments.value = loadMoments();
});
// 按日期排序(最新的在前)
const sortedMoments = computed(() => {
return [...moments.value].sort((a, b) => b.time - a.time);
});
// 快速发布相关
const quickContent = ref('');
// 发布弹窗相关
const showPublishModal = ref(false);
const postContent = ref('');
const previewImages = ref([]);
const isPublishable = ref(false);
// 图片预览相关
const showImagePreview = ref(false);
const previewImagesList = ref([]);
const currentPreviewIndex = ref(0);
const currentPreviewImage = computed(() => {
return previewImagesList.value[currentPreviewIndex.value] || '';
});
// 检查是否可以发布
const checkPublishable = () => {
isPublishable.value = !!postContent.value.trim() || previewImages.value.length > 0;
};
// 格式化时间显示
const formatTime = (timestamp) => {
const now = Date.now();
const diff = now - timestamp;
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`;
} else if (diff < week) {
return `${Math.floor(diff / day)}天前`;
} else {
const date = new Date(timestamp);
return `${date.getMonth() + 1}月${date.getDate()}日`;
}
};
// 切换点赞状态
const toggleLike = (id) => {
const moment = moments.value.find(m => m.id === id);
if (moment) {
if (moment.liked) {
moment.likes--;
} else {
moment.likes++;
}
moment.liked = !moment.liked;
// 触发watch更新
moments.value = [...moments.value];
}
};
// 打开发布弹窗
const openPublishModal = () => {
showPublishModal.value = true;
postContent.value = '';
previewImages.value = [];
isPublishable.value = false;
};
// 关闭发布弹窗
const closePublishModal = () => {
showPublishModal.value = false;
};
// 处理图片上传
const handleImageUpload = (e) => {
const files = e.target.files;
if (!files.length) return;
// 最多上传9张
const maxAdd = 9 - previewImages.value.length;
const filesToProcess = Array.from(files).slice(0, maxAdd);
filesToProcess.forEach(file => {
const reader = new FileReader();
reader.onload = (event) => {
previewImages.value.push(event.target.result);
checkPublishable();
};
reader.readAsDataURL(file);
});
// 清空input值,允许重复选择相同文件
e.target.value = '';
};
// 移除预览图片
const removeImage = (index) => {
previewImages.value.splice(index, 1);
checkPublishable();
};
// 发布动态
const publishMoment = () => {
const content = postContent.value.trim();
if (!content && previewImages.value.length === 0) return;
const newMoment = {
id: Date.now(),
content: content,
images: [...previewImages.value],
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
moments.value.unshift(newMoment);
closePublishModal();
};
// 快速发布
const handleQuickPublish = () => {
const content = quickContent.value.trim();
if (!content) return;
const newMoment = {
id: Date.now(),
content: content,
images: [],
time: Date.now(),
likes: 0,
liked: false,
comments: 0
};
moments.value.unshift(newMoment);
quickContent.value = '';
};
// 打开图片预览
const openImagePreview = (images, index) => {
previewImagesList.value = images;
currentPreviewIndex.value = index;
showImagePreview.value = true;
document.body.style.overflow = 'hidden';
};
// 关闭图片预览
const closeImagePreview = () => {
showImagePreview.value = false;
document.body.style.overflow = '';
};
// 上一张图片
const prevImage = () => {
currentPreviewIndex.value = (currentPreviewIndex.value - 1 + previewImagesList.value.length) % previewImagesList.value.length;
};
// 下一张图片
const nextImage = () => {
currentPreviewIndex.value = (currentPreviewIndex.value + 1) % previewImagesList.value.length;
};
// 监听键盘事件用于图片预览导航
watch(showImagePreview, (isVisible) => {
const handleKeydown = (e) => {
if (!isVisible) return;
if (e.key === 'Escape') {
closeImagePreview();
} else if (e.key === 'ArrowLeft') {
prevImage();
} else if (e.key === 'ArrowRight') {
nextImage();
}
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
});
</script>
<style scoped>
/* 样式和之前保持一致 */
.moments-container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
}
/* 头部样式 */
.header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.header-content {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.page-title {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.publish-btn {
background-color: #00c853;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background-color 0.2s;
}
.publish-btn:hover {
background-color: #00b248;
}
.publish-btn:disabled {
background-color: #a5d6a7;
cursor: not-allowed;
}
/* 快速发布区域 */
.quick-publish {
padding: 15px 20px;
border-bottom: 10px solid #f5f5f5;
display: flex;
align-items: center;
gap: 12px;
}
.publish-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.quick-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 25px;
outline: none;
transition: border-color 0.2s;
font-size: 1rem;
}
.quick-input:focus {
border-color: #b3e5fc;
}
/* 动态列表 */
.moments-list {
padding-bottom: 20px;
}
.moment-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
transition: background-color 0.1s;
}
.moment-item:hover {
background-color: #fafafa;
}
.moment-header {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.moment-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.username {
font-weight: 600;
margin-bottom: 2px;
display: inline-block;
}
.post-time {
font-size: 0.8rem;
color: #999;
}
.moment-content {
margin-bottom: 10px;
font-size: 1rem;
line-height: 1.6;
}
/* 图片网格 */
.images-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-bottom: 10px;
}
.grid-img {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s;
}
.grid-img:hover {
transform: scale(1.02);
}
/* 不同图片数量的布局 */
.images-grid.single {
grid-template-columns: 1fr;
max-width: 300px;
}
.images-grid.double {
grid-template-columns: repeat(2, 1fr);
}
/* 互动按钮 */
.moment-actions {
display: flex;
justify-content: space-around;
padding-top: 5px;
border-top: 1px solid #f0f0f0;
margin-top: 8px;
}
.action-btn {
flex: 1;
background: none;
border: none;
padding: 8px 0;
color: #666;
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: color 0.2s, background-color 0.2s;
border-radius: 4px;
}
.action-btn:hover {
background-color: #f5f5f5;
color: #007bff;
}
.like-btn.liked {
color: #e53935;
}
/* 空状态 */
.empty-state {
padding: 50px 20px;
text-align: center;
color: #999;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 15px;
}
/* 发布弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal-overlay:not([v-if="false"]) {
opacity: 1;
visibility: visible;
}
.modal {
background-color: white;
width: 90%;
max-width: 600px;
border-radius: 10px;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.post-content {
width: 100%;
min-height: 120px;
border: none;
outline: none;
resize: none;
font-size: 1rem;
margin-bottom: 20px;
line-height: 1.6;
font-family: inherit;
}
.image-upload {
margin-bottom: 20px;
}
.upload-title {
font-size: 0.9rem;
color: #666;
margin-bottom: 10px;
}
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.preview-item {
width: 80px;
height: 80px;
position: relative;
}
.preview-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.remove-img {
position: absolute;
top: -5px;
right: -5px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
border: none;
transition: background-color 0.2s;
}
.remove-img:hover {
background-color: white;
}
.upload-btn {
width: 80px;
height: 80px;
border: 2px dashed #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.upload-btn:hover {
border-color: #007bff;
color: #007bff;
}
.upload-input {
display: none;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.cancel-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.cancel-btn:hover {
background-color: #f5f5f5;
}
/* 图片预览弹窗 */
.image-preview {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.image-preview:not([v-if="false"]) {
opacity: 1;
visibility: visible;
}
.preview-content {
position: relative;
max-width: 90%;
max-height: 90vh;
}
.preview-img-large {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.preview-close {
position: absolute;
top: -40px;
right: 0;
color: white;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.preview-nav {
position: absolute;
top: 50%;
width: 100%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
}
.nav-btn {
background-color: rgba(0, 0, 0, 0.3);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.2rem;
transition: background-color 0.2s;
}
.nav-btn:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.preview-counter {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 0.9rem;
}
/* 响应式调整 */
@media (max-width: 600px) {
.images-grid {
gap: 3px;
}
.action-btn span {
font-size: 0.8rem;
}
}
</style>
创建时间:11:27
主要修改点说明:
- 添加了数据持久化逻辑:
- 创建了
loadMoments
函数,从 localStorage 加载数据 - 创建了
saveMoments
函数,将数据保存到 localStorage - 使用
watch
监听moments
数据变化,自动保存到 localStorage - 使用
onMounted
钩子在组件挂载时加载数据
- 创建了
- 优化了数据更新方式:
- 在点赞状态变化时,通过创建新数组触发 watch 更新
- 确保所有数据变更都会被持久化保存
- 数据存储原理:
- localStorage 是浏览器提供的本地存储机制,数据会保存在用户的浏览器中
- 数据以 JSON 格式存储,支持字符串、数字、布尔值、数组和对象等类型
- 只要不清除浏览器数据,即使关闭页面或重启浏览器,数据也会保留
这种方案适合个人使用的简单应用,数据仅保存在当前设备的浏览器中。如果需要多设备同步,就需要使用后端数据库配合 API 接口来实现数据存储了。
效果测试
如上所言,只是临时解决。