Skip to content
0

朋友圈

朋友圈

提出问题

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">&times;</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">&times;</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

这个个人朋友圈页面具有以下特点和功能:

  1. 核心功能
    • 支持发布包含文字和多张图片(最多 9 张)的动态
    • 动态按日期由近及远排序展示
    • 图片预览功能,支持左右切换浏览
    • 点赞功能,可实时更新点赞数
  2. 界面设计
    • 简洁现代的 UI 设计,符合主流社交应用风格
    • 响应式布局,适配不同屏幕尺寸
    • 动态卡片式展示,包含头像、时间、内容、图片和互动按钮
    • 根据图片数量自动调整网格布局
  3. 交互体验
    • 发布动态弹窗,支持图片预览和删除
    • 图片点击放大预览,支持键盘导航(左右箭头切换,ESC 关闭)
    • 快速发布功能,输入文字按回车即可发布
    • 点赞按钮状态变化和动画效果
  4. 技术实现
    • 使用原生 HTML、CSS 和 JavaScript 实现,无需框架依赖
    • 清晰的代码结构,便于维护和扩展
    • 模块化函数设计,实现不同功能分离

页面使用了 Font Awesome 图标库提供图标支持,整体风格简洁大方,交互流畅,完全满足个人朋友圈的使用需求。

效果测试

image-20250913150715955

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 朋友圈组件具有以下特点:

  1. 核心功能
    • 支持发布包含文字和多张图片(最多 9 张)的动态
    • 动态按日期由近及远自动排序
    • 图片预览功能,支持左右切换和键盘导航
    • 点赞功能,实时更新点赞状态和数量
    • 快速发布和完整发布两种发布方式
  2. 组件结构
    • 头部区域:显示头像和页面标题,包含发布按钮
    • 快速发布区:提供便捷的文字发布功能
    • 动态列表:以卡片形式展示所有动态
    • 发布弹窗:支持详细编辑和图片上传
    • 图片预览弹窗:大图查看多张图片
  3. 交互体验
    • 图片网格根据数量自动调整布局
    • 点赞按钮状态变化和颜色反馈
    • 图片点击放大预览,支持键盘操作
    • 发布按钮状态随内容变化(禁用 / 启用)
  4. 样式设计
    • 使用普通 CSS 编写,不依赖任何 CSS 框架
    • 响应式布局,适配不同屏幕尺寸
    • 卡片式设计,清晰展示每条动态
    • 适当的过渡动画,提升用户体验

组件使用 Vue3 的 Composition API 编写,代码结构清晰,逻辑分明,便于维护和扩展。您可以直接将此组件集成到 Vue3 项目中使用。

分享

效果测试

image-20250913151035243

v3-2025.9.13-豆包生成-临时解决刷新后会丢失刚创建动态的问题

提出问题

bash
但是这个 新增 发布文章后,为啥再次运行项目,刚才新创建的数据就丢失了呢?

豆包输出

这是因为当前代码中的数据是存储在组件的内存中的(使用ref定义的moments变量),当你刷新页面或重新运行项目时,内存中的数据会被清空,所以新创建的数据会丢失。

要解决这个问题,我们需要将数据持久化存储,常见的方案有:

  1. 本地存储(localStorage)- 适合简单数据
  2. 后端数据库 - 适合需要多设备同步的数据

下面是使用 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

主要修改点说明:

  1. 添加了数据持久化逻辑:
    • 创建了loadMoments函数,从 localStorage 加载数据
    • 创建了saveMoments函数,将数据保存到 localStorage
    • 使用watch监听moments数据变化,自动保存到 localStorage
    • 使用onMounted钩子在组件挂载时加载数据
  2. 优化了数据更新方式:
    • 在点赞状态变化时,通过创建新数组触发 watch 更新
    • 确保所有数据变更都会被持久化保存
  3. 数据存储原理:
    • localStorage 是浏览器提供的本地存储机制,数据会保存在用户的浏览器中
    • 数据以 JSON 格式存储,支持字符串、数字、布尔值、数组和对象等类型
    • 只要不清除浏览器数据,即使关闭页面或重启浏览器,数据也会保留

这种方案适合个人使用的简单应用,数据仅保存在当前设备的浏览器中。如果需要多设备同步,就需要使用后端数据库配合 API 接口来实现数据存储了。

效果测试

如上所言,只是临时解决。

image-20250913151035243

最近更新