跳到主要内容

朋友圈

最后更新于:

朋友圈

提出问题

 101.
 2请帮我生成一个 个人朋友圈 前端页面(要求 我平常发表的动态可以包含多张图片,加一些文字,按日期由近及远排序)。
 3
 402.
 5请把以上前端代码 按如下方式重新生成下:
 6vue3版本的组件,不要使用Tailwind CSS,样式直接使用普通css写在组件内。
 7请给出完整的代码。
 8
 903.
10但是这个 新增 发布文章后,为啥再次运行项目,刚才新创建的数据就丢失了呢?

v1-2025.9.13-豆包生成-本地html版

提出问题

1请帮我生成一个 个人朋友圈 前端页面(要求 我平常发表的动态可以包含多张图片,加一些文字,按日期由近及远排序)。

豆包输出

我将为您创建一个个人朋友圈前端页面,满足展示包含多张图片和文字的动态,并按日期由近及远排序的需求。这个页面采用现代设计风格,包含发布功能、动态展示、图片预览等完整功能。

个人朋友圈前端页面

V1 moments.html

   1<!DOCTYPE html>
   2<html lang="zh-CN">
   3<head>
   4  <meta charset="UTF-8">
   5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
   6  <title>我的朋友圈</title>
   7  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
   8  <style>
   9    /* 基础样式 */
  10    * {
  11      margin: 0;
  12      padding: 0;
  13      box-sizing: border-box;
  14      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  15    }
  16
  17    body {
  18      background-color: #f5f5f5;
  19      color: #333;
  20      line-height: 1.6;
  21    }
  22
  23    .container {
  24      max-width: 800px;
  25      margin: 0 auto;
  26      background-color: white;
  27      min-height: 100vh;
  28      box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
  29    }
  30
  31    /* 头部样式 */
  32    header {
  33      padding: 16px 20px;
  34      border-bottom: 1px solid #eee;
  35      display: flex;
  36      justify-content: space-between;
  37      align-items: center;
  38      background-color: #fff;
  39      position: sticky;
  40      top: 0;
  41      z-index: 100;
  42    }
  43
  44    .header-title {
  45      display: flex;
  46      align-items: center;
  47      gap: 10px;
  48    }
  49
  50    .avatar {
  51      width: 40px;
  52      height: 40px;
  53      border-radius: 50%;
  54      object-fit: cover;
  55    }
  56
  57    .page-title {
  58      font-size: 1.2rem;
  59      font-weight: 600;
  60    }
  61
  62    .publish-btn {
  63      background-color: #00c853;
  64      color: white;
  65      border: none;
  66      padding: 8px 16px;
  67      border-radius: 20px;
  68      font-weight: 500;
  69      cursor: pointer;
  70      display: flex;
  71      align-items: center;
  72      gap: 6px;
  73      transition: background-color 0.2s;
  74    }
  75
  76    .publish-btn:hover {
  77      background-color: #00b248;
  78    }
  79
  80    /* 发布动态区域 */
  81    .publish-area {
  82      padding: 15px 20px;
  83      border-bottom: 10px solid #f5f5f5;
  84    }
  85
  86    .publish-container {
  87      display: flex;
  88      gap: 12px;
  89    }
  90
  91    .publish-input {
  92      flex: 1;
  93      padding: 12px 15px;
  94      border: 1px solid #e0e0e0;
  95      border-radius: 25px;
  96      outline: none;
  97      transition: border-color 0.2s;
  98      font-size: 1rem;
  99    }
 100
 101    .publish-input:focus {
 102      border-color: #b3e5fc;
 103    }
 104
 105    .publish-actions {
 106      display: flex;
 107      justify-content: space-between;
 108      margin-top: 12px;
 109      padding-left: 52px;
 110    }
 111
 112    .action-icons {
 113      display: flex;
 114      gap: 20px;
 115    }
 116
 117    .action-icon {
 118      color: #666;
 119      cursor: pointer;
 120      display: flex;
 121      align-items: center;
 122      gap: 5px;
 123      font-size: 0.9rem;
 124      transition: color 0.2s;
 125    }
 126
 127    .action-icon:hover {
 128      color: #007bff;
 129    }
 130
 131    /* 动态列表 */
 132    .moments-list {
 133      padding-bottom: 20px;
 134    }
 135
 136    .moment-item {
 137      padding: 15px 20px;
 138      border-bottom: 1px solid #eee;
 139      transition: background-color 0.1s;
 140    }
 141
 142    .moment-item:hover {
 143      background-color: #fafafa;
 144    }
 145
 146    .moment-header {
 147      display: flex;
 148      gap: 10px;
 149      margin-bottom: 10px;
 150    }
 151
 152    .moment-avatar {
 153      width: 45px;
 154      height: 45px;
 155      border-radius: 50%;
 156      object-fit: cover;
 157    }
 158
 159    .user-info {
 160      flex: 1;
 161    }
 162
 163    .username {
 164      font-weight: 600;
 165      margin-bottom: 2px;
 166      display: inline-block;
 167    }
 168
 169    .post-time {
 170      font-size: 0.8rem;
 171      color: #999;
 172    }
 173
 174    .moment-content {
 175      margin-bottom: 10px;
 176      font-size: 1rem;
 177      line-height: 1.6;
 178    }
 179
 180    /* 图片网格 */
 181    .images-grid {
 182      display: grid;
 183      grid-template-columns: repeat(3, 1fr);
 184      gap: 5px;
 185      margin-bottom: 10px;
 186    }
 187
 188    .grid-img {
 189      width: 100%;
 190      aspect-ratio: 1/1;
 191      object-fit: cover;
 192      border-radius: 4px;
 193      cursor: pointer;
 194      transition: transform 0.2s;
 195    }
 196
 197    .grid-img:hover {
 198      transform: scale(1.02);
 199    }
 200
 201    /* 处理不同数量图片的布局 */
 202    .images-grid.single {
 203      grid-template-columns: 1fr;
 204      max-width: 300px;
 205    }
 206
 207    .images-grid.double {
 208      grid-template-columns: repeat(2, 1fr);
 209    }
 210
 211    /* 互动按钮 */
 212    .moment-actions {
 213      display: flex;
 214      justify-content: space-around;
 215      padding-top: 5px;
 216      border-top: 1px solid #f0f0f0;
 217      margin-top: 8px;
 218    }
 219
 220    .action-btn {
 221      flex: 1;
 222      background: none;
 223      border: none;
 224      padding: 8px 0;
 225      color: #666;
 226      font-size: 0.9rem;
 227      cursor: pointer;
 228      display: flex;
 229      align-items: center;
 230      justify-content: center;
 231      gap: 5px;
 232      transition: color 0.2s, background-color 0.2s;
 233      border-radius: 4px;
 234    }
 235
 236    .action-btn:hover {
 237      background-color: #f5f5f5;
 238      color: #007bff;
 239    }
 240
 241    .action-btn.liked {
 242      color: #e53935;
 243    }
 244
 245    /* 发布弹窗 */
 246    .modal-overlay {
 247      position: fixed;
 248      top: 0;
 249      left: 0;
 250      right: 0;
 251      bottom: 0;
 252      background-color: rgba(0, 0, 0, 0.5);
 253      display: flex;
 254      align-items: center;
 255      justify-content: center;
 256      z-index: 1000;
 257      opacity: 0;
 258      visibility: hidden;
 259      transition: opacity 0.3s, visibility 0.3s;
 260    }
 261
 262    .modal-overlay.active {
 263      opacity: 1;
 264      visibility: visible;
 265    }
 266
 267    .modal {
 268      background-color: white;
 269      width: 90%;
 270      max-width: 600px;
 271      border-radius: 10px;
 272      max-height: 90vh;
 273      display: flex;
 274      flex-direction: column;
 275    }
 276
 277    .modal-header {
 278      padding: 15px 20px;
 279      border-bottom: 1px solid #eee;
 280      display: flex;
 281      justify-content: space-between;
 282      align-items: center;
 283    }
 284
 285    .modal-title {
 286      font-size: 1.1rem;
 287      font-weight: 600;
 288    }
 289
 290    .close-btn {
 291      background: none;
 292      border: none;
 293      font-size: 1.5rem;
 294      cursor: pointer;
 295      color: #666;
 296      transition: color 0.2s;
 297    }
 298
 299    .close-btn:hover {
 300      color: #333;
 301    }
 302
 303    .modal-body {
 304      padding: 20px;
 305      flex: 1;
 306      overflow-y: auto;
 307    }
 308
 309    .post-content {
 310      width: 100%;
 311      min-height: 120px;
 312      border: none;
 313      outline: none;
 314      resize: none;
 315      font-size: 1rem;
 316      margin-bottom: 20px;
 317      line-height: 1.6;
 318    }
 319
 320    .image-upload-area {
 321      margin-bottom: 20px;
 322    }
 323
 324    .upload-title {
 325      font-size: 0.9rem;
 326      color: #666;
 327      margin-bottom: 10px;
 328    }
 329
 330    .preview-container {
 331      display: flex;
 332      flex-wrap: wrap;
 333      gap: 10px;
 334    }
 335
 336    .preview-item {
 337      width: 80px;
 338      height: 80px;
 339      position: relative;
 340    }
 341
 342    .preview-img {
 343      width: 100%;
 344      height: 100%;
 345      object-fit: cover;
 346      border-radius: 4px;
 347    }
 348
 349    .remove-img {
 350      position: absolute;
 351      top: -5px;
 352      right: -5px;
 353      background-color: rgba(255, 255, 255, 0.9);
 354      border-radius: 50%;
 355      width: 20px;
 356      height: 20px;
 357      display: flex;
 358      align-items: center;
 359      justify-content: center;
 360      font-size: 12px;
 361      cursor: pointer;
 362      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
 363      transition: background-color 0.2s;
 364    }
 365
 366    .remove-img:hover {
 367      background-color: white;
 368    }
 369
 370    .upload-btn {
 371      width: 80px;
 372      height: 80px;
 373      border: 2px dashed #ddd;
 374      border-radius: 4px;
 375      display: flex;
 376      align-items: center;
 377      justify-content: center;
 378      color: #999;
 379      cursor: pointer;
 380      transition: border-color 0.2s, color 0.2s;
 381    }
 382
 383    .upload-btn:hover {
 384      border-color: #007bff;
 385      color: #007bff;
 386    }
 387
 388    .upload-input {
 389      display: none;
 390    }
 391
 392    .modal-footer {
 393      padding: 15px 20px;
 394      border-top: 1px solid #eee;
 395      display: flex;
 396      justify-content: flex-end;
 397      gap: 10px;
 398    }
 399
 400    .cancel-btn {
 401      padding: 8px 16px;
 402      border: 1px solid #ddd;
 403      background: none;
 404      border-radius: 4px;
 405      cursor: pointer;
 406      transition: background-color 0.2s;
 407    }
 408
 409    .cancel-btn:hover {
 410      background-color: #f5f5f5;
 411    }
 412
 413    .submit-btn {
 414      padding: 8px 16px;
 415      background-color: #007bff;
 416      color: white;
 417      border: none;
 418      border-radius: 4px;
 419      cursor: pointer;
 420      transition: background-color 0.2s;
 421    }
 422
 423    .submit-btn:hover {
 424      background-color: #0069d9;
 425    }
 426
 427    .submit-btn:disabled {
 428      background-color: #81b8f0;
 429      cursor: not-allowed;
 430    }
 431
 432    /* 图片预览弹窗 */
 433    .image-preview-overlay {
 434      position: fixed;
 435      top: 0;
 436      left: 0;
 437      right: 0;
 438      bottom: 0;
 439      background-color: rgba(0, 0, 0, 0.9);
 440      z-index: 2000;
 441      display: flex;
 442      align-items: center;
 443      justify-content: center;
 444      opacity: 0;
 445      visibility: hidden;
 446      transition: opacity 0.3s, visibility 0.3s;
 447    }
 448
 449    .image-preview-overlay.active {
 450      opacity: 1;
 451      visibility: visible;
 452    }
 453
 454    .preview-content {
 455      position: relative;
 456      max-width: 90%;
 457      max-height: 90vh;
 458    }
 459
 460    .preview-image {
 461      max-width: 100%;
 462      max-height: 90vh;
 463      object-fit: contain;
 464    }
 465
 466    .preview-close {
 467      position: absolute;
 468      top: -40px;
 469      right: 0;
 470      color: white;
 471      background: none;
 472      border: none;
 473      font-size: 1.5rem;
 474      cursor: pointer;
 475    }
 476
 477    .preview-nav {
 478      position: absolute;
 479      top: 50%;
 480      width: 100%;
 481      display: flex;
 482      justify-content: space-between;
 483      transform: translateY(-50%);
 484    }
 485
 486    .nav-btn {
 487      background-color: rgba(0, 0, 0, 0.3);
 488      color: white;
 489      border: none;
 490      width: 40px;
 491      height: 40px;
 492      border-radius: 50%;
 493      display: flex;
 494      align-items: center;
 495      justify-content: center;
 496      cursor: pointer;
 497      font-size: 1.2rem;
 498      transition: background-color 0.2s;
 499    }
 500
 501    .nav-btn:hover {
 502      background-color: rgba(0, 0, 0, 0.5);
 503    }
 504
 505    .preview-count {
 506      position: absolute;
 507      bottom: -30px;
 508      left: 50%;
 509      transform: translateX(-50%);
 510      color: white;
 511      font-size: 0.9rem;
 512    }
 513
 514    /* 空状态 */
 515    .empty-state {
 516      padding: 50px 20px;
 517      text-align: center;
 518      color: #999;
 519    }
 520
 521    .empty-icon {
 522      font-size: 3rem;
 523      margin-bottom: 15px;
 524      opacity: 0.5;
 525    }
 526
 527    /* 响应式调整 */
 528    @media (max-width: 600px) {
 529      .images-grid {
 530        gap: 3px;
 531      }
 532
 533      .action-btn span {
 534        font-size: 0.8rem;
 535      }
 536
 537      .publish-actions {
 538        padding-left: 0;
 539        flex-direction: column;
 540        gap: 10px;
 541      }
 542
 543      .action-icons {
 544        justify-content: space-around;
 545      }
 546    }
 547  </style>
 548</head>
 549<body>
 550  <div class="container">
 551    <!-- 头部 -->
 552    <header>
 553      <div class="header-title">
 554        <img src="https://picsum.photos/200/200?random=1" alt="个人头像" class="avatar">
 555        <h1 class="page-title">我的朋友圈</h1>
 556      </div>
 557      <button class="publish-btn" id="openPublishModal">
 558        <i class="fas fa-plus"></i> 发布
 559      </button>
 560    </header>
 561
 562    <!-- 发布区域 -->
 563    <div class="publish-area">
 564      <div class="publish-container">
 565        <img src="https://picsum.photos/200/200?random=1" alt="个人头像" class="avatar">
 566        <input type="text" class="publish-input" placeholder="分享你的想法..." id="quickPublish">
 567      </div>
 568      <div class="publish-actions">
 569        <div class="action-icons">
 570          <div class="action-icon">
 571            <i class="fas fa-image"></i>
 572            <span>图片</span>
 573          </div>
 574          <div class="action-icon">
 575            <i class="fas fa-smile"></i>
 576            <span>表情</span>
 577          </div>
 578          <div class="action-icon">
 579            <i class="fas fa-map-marker-alt"></i>
 580            <span>位置</span>
 581          </div>
 582        </div>
 583      </div>
 584    </div>
 585
 586    <!-- 动态列表 -->
 587    <div class="moments-list" id="momentsList">
 588      <!-- 动态内容将通过JavaScript动态生成 -->
 589    </div>
 590
 591    <!-- 空状态 -->
 592    <div class="empty-state" id="emptyState">
 593      <div class="empty-icon">
 594        <i class="far fa-comment-dots"></i>
 595      </div>
 596      <p>还没有动态,发布你的第一条动态吧~</p>
 597    </div>
 598  </div>
 599
 600  <!-- 发布动态弹窗 -->
 601  <div class="modal-overlay" id="publishModal">
 602    <div class="modal">
 603      <div class="modal-header">
 604        <h2 class="modal-title">发布动态</h2>
 605        <button class="close-btn" id="closeModal">&times;</button>
 606      </div>
 607      <div class="modal-body">
 608        <textarea class="post-content" placeholder="分享你的想法..." id="postContent"></textarea>
 609        <div class="image-upload-area">
 610          <div class="upload-title">添加图片</div>
 611          <div class="preview-container" id="previewContainer">
 612            <label class="upload-btn" for="imageUpload">
 613              <i class="fas fa-plus"></i>
 614              <input type="file" id="imageUpload" class="upload-input" accept="image/*" multiple>
 615            </label>
 616          </div>
 617        </div>
 618      </div>
 619      <div class="modal-footer">
 620        <button class="cancel-btn" id="cancelPost">取消</button>
 621        <button class="submit-btn" id="submitPost" disabled>发布</button>
 622      </div>
 623    </div>
 624  </div>
 625
 626  <!-- 图片预览弹窗 -->
 627  <div class="image-preview-overlay" id="imagePreview">
 628    <div class="preview-content">
 629      <button class="preview-close" id="closePreview">&times;</button>
 630      <img src="" alt="图片预览" class="preview-image" id="previewImage">
 631      <div class="preview-nav">
 632        <button class="nav-btn prev-btn" id="prevImage">
 633          <i class="fas fa-chevron-left"></i>
 634        </button>
 635        <button class="nav-btn next-btn" id="nextImage">
 636          <i class="fas fa-chevron-right"></i>
 637        </button>
 638      </div>
 639      <div class="preview-count" id="previewCount">1/1</div>
 640    </div>
 641  </div>
 642
 643  <script>
 644    // 存储动态数据
 645    let moments = [
 646      {
 647        id: 1,
 648        content: "今天天气真好,去公园散步了~ 🌸",
 649        images: [
 650          "https://picsum.photos/600/400?random=10",
 651          "https://picsum.photos/600/400?random=11"
 652        ],
 653        time: new Date(Date.now() - 3600000).getTime(), // 1小时前
 654        likes: 15,
 655        liked: false,
 656        comments: 3
 657      },
 658      {
 659        id: 2,
 660        content: "分享一下新做的晚餐,味道还不错!",
 661        images: [
 662          "https://picsum.photos/600/400?random=20"
 663        ],
 664        time: new Date(Date.now() - 86400000).getTime(), // 1天前
 665        likes: 28,
 666        liked: true,
 667        comments: 7
 668      },
 669      {
 670        id: 3,
 671        content: "周末去看了一场精彩的电影,强烈推荐!",
 672        images: [],
 673        time: new Date(Date.now() - 172800000).getTime(), // 2天前
 674        likes: 12,
 675        liked: false,
 676        comments: 2
 677      }
 678    ];
 679
 680    // DOM元素
 681    const momentsList = document.getElementById('momentsList');
 682    const emptyState = document.getElementById('emptyState');
 683    const publishModal = document.getElementById('publishModal');
 684    const openPublishModalBtn = document.getElementById('openPublishModal');
 685    const closeModalBtn = document.getElementById('closeModal');
 686    const cancelPostBtn = document.getElementById('cancelPost');
 687    const submitPostBtn = document.getElementById('submitPost');
 688    const postContent = document.getElementById('postContent');
 689    const imageUpload = document.getElementById('imageUpload');
 690    const previewContainer = document.getElementById('previewContainer');
 691    const quickPublish = document.getElementById('quickPublish');
 692    const imagePreview = document.getElementById('imagePreview');
 693    const previewImage = document.getElementById('previewImage');
 694    const closePreviewBtn = document.getElementById('closePreview');
 695    const prevImageBtn = document.getElementById('prevImage');
 696    const nextImageBtn = document.getElementById('nextImage');
 697    const previewCount = document.getElementById('previewCount');
 698
 699    // 预览相关变量
 700    let currentPreviewIndex = 0;
 701    let currentPreviewImages = [];
 702
 703    // 初始化页面
 704    function init() {
 705      renderMoments();
 706      setupEventListeners();
 707    }
 708
 709    // 渲染动态列表
 710    function renderMoments() {
 711      // 按时间排序(最新的在前)
 712      moments.sort((a, b) => b.time - a.time);
 713
 714      if (moments.length === 0) {
 715        emptyState.style.display = 'block';
 716        momentsList.style.display = 'none';
 717        return;
 718      }
 719
 720      emptyState.style.display = 'none';
 721      momentsList.style.display = 'block';
 722      momentsList.innerHTML = '';
 723
 724      moments.forEach(moment => {
 725        const momentElement = createMomentElement(moment);
 726        momentsList.appendChild(momentElement);
 727      });
 728    }
 729
 730    // 创建动态元素
 731    function createMomentElement(moment) {
 732      const div = document.createElement('div');
 733      div.className = 'moment-item';
 734      div.dataset.id = moment.id;
 735
 736      // 格式化时间
 737      const timeString = formatTime(moment.time);
 738
 739      // 构建图片网格
 740      let imagesHtml = '';
 741      if (moment.images.length > 0) {
 742        let gridClass = 'images-grid';
 743        if (moment.images.length === 1) gridClass += ' single';
 744        else if (moment.images.length === 2) gridClass += ' double';
 745
 746        imagesHtml = `<div class="${gridClass}">`;
 747        moment.images.forEach((img, index) => {
 748          imagesHtml += `<img src="${img}" alt="动态图片" class="grid-img" data-index="${index}">`;
 749        });
 750        imagesHtml += `</div>`;
 751      }
 752
 753      // 点赞图标
 754      const likeIcon = moment.liked ? 'fas fa-heart' : 'far fa-heart';
 755
 756      div.innerHTML = `
 757        <div class="moment-header">
 758          <img src="https://picsum.photos/200/200?random=1" alt="用户头像" class="moment-avatar">
 759          <div class="user-info">
 760            <div class="username">我</div>
 761            <div class="post-time">${timeString}</div>
 762          </div>
 763        </div>
 764        ${moment.content ? `<div class="moment-content">${moment.content}</div>` : ''}
 765        ${imagesHtml}
 766        <div class="moment-actions">
 767          <button class="action-btn like-btn ${moment.liked ? 'liked' : ''}">
 768            <i class="${likeIcon}"></i>
 769            <span>${moment.likes}</span>
 770          </button>
 771          <button class="action-btn comment-btn">
 772            <i class="far fa-comment"></i>
 773            <span>${moment.comments}</span>
 774          </button>
 775          <button class="action-btn share-btn">
 776            <i class="far fa-share-square"></i>
 777            <span>分享</span>
 778          </button>
 779        </div>
 780      `;
 781
 782      // 添加事件监听
 783      const likeBtn = div.querySelector('.like-btn');
 784      likeBtn.addEventListener('click', () => toggleLike(moment.id));
 785
 786      // 图片点击事件
 787      const images = div.querySelectorAll('.grid-img');
 788      images.forEach(img => {
 789        img.addEventListener('click', (e) => {
 790          const index = parseInt(e.target.dataset.index);
 791          openImagePreview(moment.images, index);
 792        });
 793      });
 794
 795      return div;
 796    }
 797
 798    // 格式化时间显示
 799    function formatTime(timestamp) {
 800      const now = Date.now();
 801      const diff = now - timestamp;
 802      const second = 1000;
 803      const minute = second * 60;
 804      const hour = minute * 60;
 805      const day = hour * 24;
 806      const week = day * 7;
 807
 808      if (diff < minute) {
 809        return '刚刚';
 810      } else if (diff < hour) {
 811        return `${Math.floor(diff / minute)}分钟前`;
 812      } else if (diff < day) {
 813        return `${Math.floor(diff / hour)}小时前`;
 814      } else if (diff < week) {
 815        return `${Math.floor(diff / day)}天前`;
 816      } else {
 817        const date = new Date(timestamp);
 818        return `${date.getMonth() + 1}${date.getDate()}日`;
 819      }
 820    }
 821
 822    // 切换点赞状态
 823    function toggleLike(id) {
 824      const moment = moments.find(m => m.id === id);
 825      if (!moment) return;
 826
 827      if (moment.liked) {
 828        moment.likes--;
 829      } else {
 830        moment.likes++;
 831      }
 832      moment.liked = !moment.liked;
 833
 834      renderMoments();
 835    }
 836
 837    // 打开发布弹窗
 838    function openPublishModal() {
 839      publishModal.classList.add('active');
 840      postContent.focus();
 841      // 清空内容
 842      postContent.value = '';
 843      // 移除预览图片(保留上传按钮)
 844      const previewItems = previewContainer.querySelectorAll('.preview-item');
 845      previewItems.forEach(item => item.remove());
 846      checkSubmitDisabled();
 847    }
 848
 849    // 关闭发布弹窗
 850    function closePublishModal() {
 851      publishModal.classList.remove('active');
 852    }
 853
 854    // 检查发布按钮是否禁用
 855    function checkSubmitDisabled() {
 856      const content = postContent.value.trim();
 857      const hasImages = previewContainer.querySelectorAll('.preview-item').length > 0;
 858      submitPostBtn.disabled = !(content || hasImages);
 859    }
 860
 861    // 处理图片上传
 862    function handleImageUpload(e) {
 863      const files = e.target.files;
 864      if (!files.length) return;
 865
 866      // 最多上传9张图片
 867      const existingCount = previewContainer.querySelectorAll('.preview-item').length;
 868      const maxAdd = 9 - existingCount;
 869      const filesToUpload = Array.from(files).slice(0, maxAdd);
 870
 871      filesToUpload.forEach(file => {
 872        const reader = new FileReader();
 873        reader.onload = function(event) {
 874          const previewItem = document.createElement('div');
 875          previewItem.className = 'preview-item';
 876          previewItem.innerHTML = `
 877            <img src="${event.target.result}" alt="预览图" class="preview-img">
 878            <div class="remove-img" data-src="${event.target.result}">×</div>
 879          `;
 880          
 881          // 插入到上传按钮前
 882          const uploadBtn = previewContainer.querySelector('.upload-btn');
 883          previewContainer.insertBefore(previewItem, uploadBtn);
 884
 885          // 添加删除图片事件
 886          const removeBtn = previewItem.querySelector('.remove-img');
 887          removeBtn.addEventListener('click', function() {
 888            previewItem.remove();
 889            checkSubmitDisabled();
 890          });
 891
 892          checkSubmitDisabled();
 893        };
 894        reader.readAsDataURL(file);
 895      });
 896
 897      // 重置input值,允许重复选择同一文件
 898      imageUpload.value = '';
 899    }
 900
 901    // 发布动态
 902    function publishMoment() {
 903      const content = postContent.value.trim();
 904      const previewImages = previewContainer.querySelectorAll('.preview-item img');
 905      const images = Array.from(previewImages).map(img => img.src);
 906
 907      if (!content && images.length === 0) return;
 908
 909      // 创建新动态
 910      const newMoment = {
 911        id: Date.now(), // 使用时间戳作为唯一ID
 912        content: content,
 913        images: images,
 914        time: Date.now(),
 915        likes: 0,
 916        liked: false,
 917        comments: 0
 918      };
 919
 920      // 添加到动态列表
 921      moments.unshift(newMoment);
 922
 923      // 重新渲染
 924      renderMoments();
 925
 926      // 关闭弹窗
 927      closePublishModal();
 928    }
 929
 930    // 快速发布
 931    function handleQuickPublish() {
 932      const content = quickPublish.value.trim();
 933      if (!content) return;
 934
 935      // 创建新动态
 936      const newMoment = {
 937        id: Date.now(),
 938        content: content,
 939        images: [],
 940        time: Date.now(),
 941        likes: 0,
 942        liked: false,
 943        comments: 0
 944      };
 945
 946      moments.unshift(newMoment);
 947      renderMoments();
 948      quickPublish.value = '';
 949    }
 950
 951    // 打开图片预览
 952    function openImagePreview(images, index) {
 953      currentPreviewImages = images;
 954      currentPreviewIndex = index;
 955      updatePreviewImage();
 956      imagePreview.classList.add('active');
 957      document.body.style.overflow = 'hidden';
 958    }
 959
 960    // 关闭图片预览
 961    function closeImagePreview() {
 962      imagePreview.classList.remove('active');
 963      document.body.style.overflow = '';
 964    }
 965
 966    // 更新预览图片
 967    function updatePreviewImage() {
 968      previewImage.src = currentPreviewImages[currentPreviewIndex];
 969      previewCount.textContent = `${currentPreviewIndex + 1}/${currentPreviewImages.length}`;
 970    }
 971
 972    // 上一张图片
 973    function prevImage() {
 974      currentPreviewIndex = (currentPreviewIndex - 1 + currentPreviewImages.length) % currentPreviewImages.length;
 975      updatePreviewImage();
 976    }
 977
 978    // 下一张图片
 979    function nextImage() {
 980      currentPreviewIndex = (currentPreviewIndex + 1) % currentPreviewImages.length;
 981      updatePreviewImage();
 982    }
 983
 984    // 设置事件监听
 985    function setupEventListeners() {
 986      // 发布弹窗相关
 987      openPublishModalBtn.addEventListener('click', openPublishModal);
 988      closeModalBtn.addEventListener('click', closePublishModal);
 989      cancelPostBtn.addEventListener('click', closePublishModal);
 990      submitPostBtn.addEventListener('click', publishMoment);
 991      postContent.addEventListener('input', checkSubmitDisabled);
 992      imageUpload.addEventListener('change', handleImageUpload);
 993
 994      // 快速发布
 995      quickPublish.addEventListener('keydown', (e) => {
 996        if (e.key === 'Enter') {
 997          handleQuickPublish();
 998        }
 999      });
1000
1001      // 图片预览相关
1002      closePreviewBtn.addEventListener('click', closeImagePreview);
1003      prevImageBtn.addEventListener('click', prevImage);
1004      nextImageBtn.addEventListener('click', nextImage);
1005
1006      // 点击预览背景关闭预览
1007      imagePreview.addEventListener('click', (e) => {
1008        if (e.target === imagePreview) {
1009          closeImagePreview();
1010        }
1011      });
1012
1013      // 键盘导航预览图片
1014      document.addEventListener('keydown', (e) => {
1015        if (!imagePreview.classList.contains('active')) return;
1016
1017        if (e.key === 'Escape') {
1018          closeImagePreview();
1019        } else if (e.key === 'ArrowLeft') {
1020          prevImage();
1021        } else if (e.key === 'ArrowRight') {
1022          nextImage();
1023        }
1024      });
1025    }
1026
1027    // 初始化
1028    init();
1029  </script>
1030</body>
1031</html>

创建时间:11:18

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

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

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

效果测试

image-20250913150715955

v2-2025.9.13-豆包生成-vue版

提出问题

1请把以上前端代码 按如下方式重新生成下:
2vue3版本的组件,不要使用Tailwind CSS,样式直接使用普通css写在组件内。
3请给出完整的代码。

豆包输出

我将为您创建一个 Vue3 版本的个人朋友圈组件,满足发布包含多张图片和文字的动态,并按日期由近及远排序的需求。组件使用普通 CSS 编写样式,不依赖 Tailwind。

Vue3 个人朋友圈组件

V1 Moments.vue

  1<template>
  2  <div class="moments-container">
  3    <!-- 头部区域 -->
  4    <header class="header">
  5      <div class="header-content">
  6        <img 
  7          src="https://picsum.photos/200/200?random=1" 
  8          alt="个人头像" 
  9          class="avatar"
 10        >
 11        <h1 class="page-title">我的朋友圈</h1>
 12      </div>
 13      <button class="publish-btn" @click="showPublishModal = true">
 14        <i class="icon-add">+</i> 发布
 15      </button>
 16    </header>
 17
 18    <!-- 快速发布区域 -->
 19    <div class="quick-publish">
 20      <img 
 21        src="https://picsum.photos/200/200?random=1" 
 22        alt="个人头像" 
 23        class="publish-avatar"
 24      >
 25      <input 
 26        type="text" 
 27        class="quick-input" 
 28        placeholder="分享你的想法..."
 29        v-model="quickContent"
 30        @keyup.enter="handleQuickPublish"
 31      >
 32    </div>
 33
 34    <!-- 动态列表 -->
 35    <div class="moments-list">
 36      <!-- 动态项 -->
 37      <div 
 38        class="moment-item" 
 39        v-for="moment in sortedMoments" 
 40        :key="moment.id"
 41      >
 42        <div class="moment-header">
 43          <img 
 44            src="https://picsum.photos/200/200?random=1" 
 45            alt="用户头像" 
 46            class="moment-avatar"
 47          >
 48          <div class="user-info">
 49            <div class="username"></div>
 50            <div class="post-time">{{ formatTime(moment.time) }}</div>
 51          </div>
 52        </div>
 53
 54        <div class="moment-content" v-if="moment.content">
 55          {{ moment.content }}
 56        </div>
 57
 58        <!-- 图片网格 -->
 59        <div 
 60          class="images-grid"
 61          :class="{
 62            'single': moment.images.length === 1,
 63            'double': moment.images.length === 2,
 64            'triple': moment.images.length === 3
 65          }"
 66          v-if="moment.images.length > 0"
 67        >
 68          <img 
 69            v-for="(img, index) in moment.images" 
 70            :key="index"
 71            :src="img" 
 72            alt="动态图片"
 73            class="grid-img"
 74            @click="openImagePreview(moment.images, index)"
 75          >
 76        </div>
 77
 78        <!-- 互动按钮 -->
 79        <div class="moment-actions">
 80          <button 
 81            class="action-btn like-btn"
 82            :class="{ liked: moment.liked }"
 83            @click="toggleLike(moment.id)"
 84          >
 85            <i class="icon-like">{{ moment.liked ? '♥' : '♡' }}</i>
 86            <span>{{ moment.likes }}</span>
 87          </button>
 88          <button class="action-btn comment-btn">
 89            <i class="icon-comment">💬</i>
 90            <span>{{ moment.comments }}</span>
 91          </button>
 92          <button class="action-btn share-btn">
 93            <i class="icon-share">🔗</i>
 94            <span>分享</span>
 95          </button>
 96        </div>
 97      </div>
 98
 99      <!-- 空状态 -->
100      <div class="empty-state" v-if="sortedMoments.length === 0">
101        <div class="empty-icon">📝</div>
102        <p>还没有动态发布你的第一条动态吧</p>
103      </div>
104    </div>
105
106    <!-- 发布动态弹窗 -->
107    <div class="modal-overlay" v-if="showPublishModal" @click="closePublishModal">
108      <div class="modal" @click.stop>
109        <div class="modal-header">
110          <h2 class="modal-title">发布动态</h2>
111          <button class="close-btn" @click="closePublishModal">×</button>
112        </div>
113        <div class="modal-body">
114          <textarea 
115            class="post-content" 
116            placeholder="分享你的想法..."
117            v-model="postContent"
118            @input="checkPublishable"
119          ></textarea>
120
121          <div class="image-upload">
122            <div class="upload-title">添加图片最多9张</div>
123            <div class="preview-container">
124              <div 
125                class="preview-item" 
126                v-for="(img, index) in previewImages" 
127                :key="index"
128              >
129                <img :src="img" alt="预览图" class="preview-img">
130                <button 
131                  class="remove-img" 
132                  @click="removeImage(index)"
133                >×</button>
134              </div>
135
136              <label class="upload-btn" v-if="previewImages.length < 9">
137                <input 
138                  type="file" 
139                  accept="image/*" 
140                  multiple
141                  @change="handleImageUpload"
142                  class="upload-input"
143                >
144                <div class="upload-icon">+</div>
145              </label>
146            </div>
147          </div>
148        </div>
149        <div class="modal-footer">
150          <button class="cancel-btn" @click="closePublishModal">取消</button>
151          <button 
152            class="publish-btn" 
153            @click="publishMoment"
154            :disabled="!isPublishable"
155          >
156            发布
157          </button>
158        </div>
159      </div>
160    </div>
161
162    <!-- 图片预览弹窗 -->
163    <div class="image-preview" v-if="showImagePreview" @click="closeImagePreview">
164      <div class="preview-content" @click.stop>
165        <button class="preview-close" @click="closeImagePreview">×</button>
166        <img 
167          :src="currentPreviewImage" 
168          alt="图片预览" 
169          class="preview-img-large"
170        >
171        <div class="preview-nav" v-if="previewImagesList.length > 1">
172          <button class="nav-btn prev-btn" @click="prevImage"></button>
173          <button class="nav-btn next-btn" @click="nextImage"></button>
174        </div>
175        <div class="preview-counter">
176          {{ currentPreviewIndex + 1 }} / {{ previewImagesList.length }}
177        </div>
178      </div>
179    </div>
180  </div>
181</template>
182
183<script setup>
184import { ref, computed, watch } from 'vue';
185
186// 动态数据
187const moments = ref([
188  {
189    id: 1,
190    content: "今天天气真好,去公园散步了~ 🌸",
191    images: [
192      "https://picsum.photos/600/400?random=10",
193      "https://picsum.photos/600/400?random=11"
194    ],
195    time: new Date(Date.now() - 3600000).getTime(), // 1小时前
196    likes: 15,
197    liked: false,
198    comments: 3
199  },
200  {
201    id: 2,
202    content: "分享一下新做的晚餐,味道还不错!",
203    images: [
204      "https://picsum.photos/600/400?random=20"
205    ],
206    time: new Date(Date.now() - 86400000).getTime(), // 1天前
207    likes: 28,
208    liked: true,
209    comments: 7
210  },
211  {
212    id: 3,
213    content: "周末去看了一场精彩的电影,强烈推荐!",
214    images: [],
215    time: new Date(Date.now() - 172800000).getTime(), // 2天前
216    likes: 12,
217    liked: false,
218    comments: 2
219  }
220]);
221
222// 按日期排序(最新的在前)
223const sortedMoments = computed(() => {
224  return [...moments.value].sort((a, b) => b.time - a.time);
225});
226
227// 快速发布相关
228const quickContent = ref('');
229
230// 发布弹窗相关
231const showPublishModal = ref(false);
232const postContent = ref('');
233const previewImages = ref([]);
234const isPublishable = ref(false);
235
236// 图片预览相关
237const showImagePreview = ref(false);
238const previewImagesList = ref([]);
239const currentPreviewIndex = ref(0);
240const currentPreviewImage = computed(() => {
241  return previewImagesList.value[currentPreviewIndex.value] || '';
242});
243
244// 检查是否可以发布
245const checkPublishable = () => {
246  isPublishable.value = !!postContent.value.trim() || previewImages.value.length > 0;
247};
248
249// 格式化时间显示
250const formatTime = (timestamp) => {
251  const now = Date.now();
252  const diff = now - timestamp;
253  const second = 1000;
254  const minute = second * 60;
255  const hour = minute * 60;
256  const day = hour * 24;
257  const week = day * 7;
258
259  if (diff < minute) {
260    return '刚刚';
261  } else if (diff < hour) {
262    return `${Math.floor(diff / minute)}分钟前`;
263  } else if (diff < day) {
264    return `${Math.floor(diff / hour)}小时前`;
265  } else if (diff < week) {
266    return `${Math.floor(diff / day)}天前`;
267  } else {
268    const date = new Date(timestamp);
269    return `${date.getMonth() + 1}${date.getDate()}日`;
270  }
271};
272
273// 切换点赞状态
274const toggleLike = (id) => {
275  const moment = moments.value.find(m => m.id === id);
276  if (moment) {
277    if (moment.liked) {
278      moment.likes--;
279    } else {
280      moment.likes++;
281    }
282    moment.liked = !moment.liked;
283  }
284};
285
286// 打开发布弹窗
287const openPublishModal = () => {
288  showPublishModal.value = true;
289  postContent.value = '';
290  previewImages.value = [];
291  isPublishable.value = false;
292};
293
294// 关闭发布弹窗
295const closePublishModal = () => {
296  showPublishModal.value = false;
297};
298
299// 处理图片上传
300const handleImageUpload = (e) => {
301  const files = e.target.files;
302  if (!files.length) return;
303
304  // 最多上传9张
305  const maxAdd = 9 - previewImages.value.length;
306  const filesToProcess = Array.from(files).slice(0, maxAdd);
307
308  filesToProcess.forEach(file => {
309    const reader = new FileReader();
310    reader.onload = (event) => {
311      previewImages.value.push(event.target.result);
312      checkPublishable();
313    };
314    reader.readAsDataURL(file);
315  });
316
317  // 清空input值,允许重复选择相同文件
318  e.target.value = '';
319};
320
321// 移除预览图片
322const removeImage = (index) => {
323  previewImages.value.splice(index, 1);
324  checkPublishable();
325};
326
327// 发布动态
328const publishMoment = () => {
329  const content = postContent.value.trim();
330  if (!content && previewImages.value.length === 0) return;
331
332  const newMoment = {
333    id: Date.now(),
334    content: content,
335    images: [...previewImages.value],
336    time: Date.now(),
337    likes: 0,
338    liked: false,
339    comments: 0
340  };
341
342  moments.value.unshift(newMoment);
343  closePublishModal();
344};
345
346// 快速发布
347const handleQuickPublish = () => {
348  const content = quickContent.value.trim();
349  if (!content) return;
350
351  const newMoment = {
352    id: Date.now(),
353    content: content,
354    images: [],
355    time: Date.now(),
356    likes: 0,
357    liked: false,
358    comments: 0
359  };
360
361  moments.value.unshift(newMoment);
362  quickContent.value = '';
363};
364
365// 打开图片预览
366const openImagePreview = (images, index) => {
367  previewImagesList.value = images;
368  currentPreviewIndex.value = index;
369  showImagePreview.value = true;
370  document.body.style.overflow = 'hidden';
371};
372
373// 关闭图片预览
374const closeImagePreview = () => {
375  showImagePreview.value = false;
376  document.body.style.overflow = '';
377};
378
379// 上一张图片
380const prevImage = () => {
381  currentPreviewIndex.value = (currentPreviewIndex.value - 1 + previewImagesList.value.length) % previewImagesList.value.length;
382};
383
384// 下一张图片
385const nextImage = () => {
386  currentPreviewIndex.value = (currentPreviewIndex.value + 1) % previewImagesList.value.length;
387};
388
389// 监听键盘事件用于图片预览导航
390watch(showImagePreview, (isVisible) => {
391  const handleKeydown = (e) => {
392    if (!isVisible) return;
393
394    if (e.key === 'Escape') {
395      closeImagePreview();
396    } else if (e.key === 'ArrowLeft') {
397      prevImage();
398    } else if (e.key === 'ArrowRight') {
399      nextImage();
400    }
401  };
402
403  document.addEventListener('keydown', handleKeydown);
404  return () => {
405    document.removeEventListener('keydown', handleKeydown);
406  };
407});
408</script>
409
410<style scoped>
411/* 基础样式 */
412.moments-container {
413  max-width: 800px;
414  margin: 0 auto;
415  background-color: #fff;
416  min-height: 100vh;
417  box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
418  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
419  color: #333;
420}
421
422/* 头部样式 */
423.header {
424  padding: 16px 20px;
425  border-bottom: 1px solid #eee;
426  display: flex;
427  justify-content: space-between;
428  align-items: center;
429  background-color: #fff;
430  position: sticky;
431  top: 0;
432  z-index: 10;
433}
434
435.header-content {
436  display: flex;
437  align-items: center;
438  gap: 10px;
439}
440
441.avatar {
442  width: 40px;
443  height: 40px;
444  border-radius: 50%;
445  object-fit: cover;
446}
447
448.page-title {
449  font-size: 1.2rem;
450  font-weight: 600;
451  margin: 0;
452}
453
454.publish-btn {
455  background-color: #00c853;
456  color: white;
457  border: none;
458  padding: 8px 16px;
459  border-radius: 20px;
460  font-weight: 500;
461  cursor: pointer;
462  display: flex;
463  align-items: center;
464  gap: 6px;
465  transition: background-color 0.2s;
466}
467
468.publish-btn:hover {
469  background-color: #00b248;
470}
471
472.publish-btn:disabled {
473  background-color: #a5d6a7;
474  cursor: not-allowed;
475}
476
477/* 快速发布区域 */
478.quick-publish {
479  padding: 15px 20px;
480  border-bottom: 10px solid #f5f5f5;
481  display: flex;
482  align-items: center;
483  gap: 12px;
484}
485
486.publish-avatar {
487  width: 40px;
488  height: 40px;
489  border-radius: 50%;
490  object-fit: cover;
491}
492
493.quick-input {
494  flex: 1;
495  padding: 12px 15px;
496  border: 1px solid #e0e0e0;
497  border-radius: 25px;
498  outline: none;
499  transition: border-color 0.2s;
500  font-size: 1rem;
501}
502
503.quick-input:focus {
504  border-color: #b3e5fc;
505}
506
507/* 动态列表 */
508.moments-list {
509  padding-bottom: 20px;
510}
511
512.moment-item {
513  padding: 15px 20px;
514  border-bottom: 1px solid #eee;
515  transition: background-color 0.1s;
516}
517
518.moment-item:hover {
519  background-color: #fafafa;
520}
521
522.moment-header {
523  display: flex;
524  gap: 10px;
525  margin-bottom: 10px;
526}
527
528.moment-avatar {
529  width: 45px;
530  height: 45px;
531  border-radius: 50%;
532  object-fit: cover;
533}
534
535.user-info {
536  flex: 1;
537}
538
539.username {
540  font-weight: 600;
541  margin-bottom: 2px;
542  display: inline-block;
543}
544
545.post-time {
546  font-size: 0.8rem;
547  color: #999;
548}
549
550.moment-content {
551  margin-bottom: 10px;
552  font-size: 1rem;
553  line-height: 1.6;
554}
555
556/* 图片网格 */
557.images-grid {
558  display: grid;
559  grid-template-columns: repeat(3, 1fr);
560  gap: 5px;
561  margin-bottom: 10px;
562}
563
564.grid-img {
565  width: 100%;
566  aspect-ratio: 1/1;
567  object-fit: cover;
568  border-radius: 4px;
569  cursor: pointer;
570  transition: transform 0.2s;
571}
572
573.grid-img:hover {
574  transform: scale(1.02);
575}
576
577/* 不同图片数量的布局 */
578.images-grid.single {
579  grid-template-columns: 1fr;
580  max-width: 300px;
581}
582
583.images-grid.double {
584  grid-template-columns: repeat(2, 1fr);
585}
586
587/* 互动按钮 */
588.moment-actions {
589  display: flex;
590  justify-content: space-around;
591  padding-top: 5px;
592  border-top: 1px solid #f0f0f0;
593  margin-top: 8px;
594}
595
596.action-btn {
597  flex: 1;
598  background: none;
599  border: none;
600  padding: 8px 0;
601  color: #666;
602  font-size: 0.9rem;
603  cursor: pointer;
604  display: flex;
605  align-items: center;
606  justify-content: center;
607  gap: 5px;
608  transition: color 0.2s, background-color 0.2s;
609  border-radius: 4px;
610}
611
612.action-btn:hover {
613  background-color: #f5f5f5;
614  color: #007bff;
615}
616
617.like-btn.liked {
618  color: #e53935;
619}
620
621/* 空状态 */
622.empty-state {
623  padding: 50px 20px;
624  text-align: center;
625  color: #999;
626}
627
628.empty-icon {
629  font-size: 3rem;
630  margin-bottom: 15px;
631}
632
633/* 发布弹窗 */
634.modal-overlay {
635  position: fixed;
636  top: 0;
637  left: 0;
638  right: 0;
639  bottom: 0;
640  background-color: rgba(0, 0, 0, 0.5);
641  display: flex;
642  align-items: center;
643  justify-content: center;
644  z-index: 100;
645  opacity: 0;
646  visibility: hidden;
647  transition: opacity 0.3s, visibility 0.3s;
648}
649
650.modal-overlay:not([v-if="false"]) {
651  opacity: 1;
652  visibility: visible;
653}
654
655.modal {
656  background-color: white;
657  width: 90%;
658  max-width: 600px;
659  border-radius: 10px;
660  max-height: 90vh;
661  display: flex;
662  flex-direction: column;
663}
664
665.modal-header {
666  padding: 15px 20px;
667  border-bottom: 1px solid #eee;
668  display: flex;
669  justify-content: space-between;
670  align-items: center;
671}
672
673.modal-title {
674  font-size: 1.1rem;
675  font-weight: 600;
676  margin: 0;
677}
678
679.close-btn {
680  background: none;
681  border: none;
682  font-size: 1.5rem;
683  cursor: pointer;
684  color: #666;
685  transition: color 0.2s;
686}
687
688.close-btn:hover {
689  color: #333;
690}
691
692.modal-body {
693  padding: 20px;
694  flex: 1;
695  overflow-y: auto;
696}
697
698.post-content {
699  width: 100%;
700  min-height: 120px;
701  border: none;
702  outline: none;
703  resize: none;
704  font-size: 1rem;
705  margin-bottom: 20px;
706  line-height: 1.6;
707  font-family: inherit;
708}
709
710.image-upload {
711  margin-bottom: 20px;
712}
713
714.upload-title {
715  font-size: 0.9rem;
716  color: #666;
717  margin-bottom: 10px;
718}
719
720.preview-container {
721  display: flex;
722  flex-wrap: wrap;
723  gap: 10px;
724}
725
726.preview-item {
727  width: 80px;
728  height: 80px;
729  position: relative;
730}
731
732.preview-img {
733  width: 100%;
734  height: 100%;
735  object-fit: cover;
736  border-radius: 4px;
737}
738
739.remove-img {
740  position: absolute;
741  top: -5px;
742  right: -5px;
743  background-color: rgba(255, 255, 255, 0.9);
744  border-radius: 50%;
745  width: 20px;
746  height: 20px;
747  display: flex;
748  align-items: center;
749  justify-content: center;
750  font-size: 12px;
751  cursor: pointer;
752  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
753  border: none;
754  transition: background-color 0.2s;
755}
756
757.remove-img:hover {
758  background-color: white;
759}
760
761.upload-btn {
762  width: 80px;
763  height: 80px;
764  border: 2px dashed #ddd;
765  border-radius: 4px;
766  display: flex;
767  align-items: center;
768  justify-content: center;
769  color: #999;
770  cursor: pointer;
771  transition: border-color 0.2s, color 0.2s;
772}
773
774.upload-btn:hover {
775  border-color: #007bff;
776  color: #007bff;
777}
778
779.upload-input {
780  display: none;
781}
782
783.modal-footer {
784  padding: 15px 20px;
785  border-top: 1px solid #eee;
786  display: flex;
787  justify-content: flex-end;
788  gap: 10px;
789}
790
791.cancel-btn {
792  padding: 8px 16px;
793  border: 1px solid #ddd;
794  background: none;
795  border-radius: 4px;
796  cursor: pointer;
797  transition: background-color 0.2s;
798}
799
800.cancel-btn:hover {
801  background-color: #f5f5f5;
802}
803
804/* 图片预览弹窗 */
805.image-preview {
806  position: fixed;
807  top: 0;
808  left: 0;
809  right: 0;
810  bottom: 0;
811  background-color: rgba(0, 0, 0, 0.9);
812  z-index: 200;
813  display: flex;
814  align-items: center;
815  justify-content: center;
816  opacity: 0;
817  visibility: hidden;
818  transition: opacity 0.3s, visibility 0.3s;
819}
820
821.image-preview:not([v-if="false"]) {
822  opacity: 1;
823  visibility: visible;
824}
825
826.preview-content {
827  position: relative;
828  max-width: 90%;
829  max-height: 90vh;
830}
831
832.preview-img-large {
833  max-width: 100%;
834  max-height: 90vh;
835  object-fit: contain;
836}
837
838.preview-close {
839  position: absolute;
840  top: -40px;
841  right: 0;
842  color: white;
843  background: none;
844  border: none;
845  font-size: 1.5rem;
846  cursor: pointer;
847}
848
849.preview-nav {
850  position: absolute;
851  top: 50%;
852  width: 100%;
853  display: flex;
854  justify-content: space-between;
855  transform: translateY(-50%);
856}
857
858.nav-btn {
859  background-color: rgba(0, 0, 0, 0.3);
860  color: white;
861  border: none;
862  width: 40px;
863  height: 40px;
864  border-radius: 50%;
865  display: flex;
866  align-items: center;
867  justify-content: center;
868  cursor: pointer;
869  font-size: 1.2rem;
870  transition: background-color 0.2s;
871}
872
873.nav-btn:hover {
874  background-color: rgba(0, 0, 0, 0.5);
875}
876
877.preview-counter {
878  position: absolute;
879  bottom: -30px;
880  left: 50%;
881  transform: translateX(-50%);
882  color: white;
883  font-size: 0.9rem;
884}
885
886/* 响应式调整 */
887@media (max-width: 600px) {
888  .images-grid {
889    gap: 3px;
890  }
891
892  .action-btn span {
893    font-size: 0.8rem;
894  }
895}
896</style>

创建时间:11:22

这个 Vue3 朋友圈组件具有以下特点:

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

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

分享

效果测试

image-20250913151035243

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

提出问题

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

豆包输出

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

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

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

下面是使用 localStorage 进行数据持久化的修改方案,只需在原来的代码基础上添加数据读写逻辑:

带数据持久化的Vue3朋友圈组件

V1 Moments.vue

  1<template>
  2  <!-- 模板内容和之前保持一致 -->
  3  <div class="moments-container">
  4    <!-- 头部区域 -->
  5    <header class="header">
  6      <div class="header-content">
  7        <img 
  8          src="https://picsum.photos/200/200?random=1" 
  9          alt="个人头像" 
 10          class="avatar"
 11        >
 12        <h1 class="page-title">我的朋友圈</h1>
 13      </div>
 14      <button class="publish-btn" @click="showPublishModal = true">
 15        <i class="icon-add">+</i> 发布
 16      </button>
 17    </header>
 18
 19    <!-- 快速发布区域 -->
 20    <div class="quick-publish">
 21      <img 
 22        src="https://picsum.photos/200/200?random=1" 
 23        alt="个人头像" 
 24        class="publish-avatar"
 25      >
 26      <input 
 27        type="text" 
 28        class="quick-input" 
 29        placeholder="分享你的想法..."
 30        v-model="quickContent"
 31        @keyup.enter="handleQuickPublish"
 32      >
 33    </div>
 34
 35    <!-- 动态列表 -->
 36    <div class="moments-list">
 37      <!-- 动态项 -->
 38      <div 
 39        class="moment-item" 
 40        v-for="moment in sortedMoments" 
 41        :key="moment.id"
 42      >
 43        <div class="moment-header">
 44          <img 
 45            src="https://picsum.photos/200/200?random=1" 
 46            alt="用户头像" 
 47            class="moment-avatar"
 48          >
 49          <div class="user-info">
 50            <div class="username"></div>
 51            <div class="post-time">{{ formatTime(moment.time) }}</div>
 52          </div>
 53        </div>
 54
 55        <div class="moment-content" v-if="moment.content">
 56          {{ moment.content }}
 57        </div>
 58
 59        <!-- 图片网格 -->
 60        <div 
 61          class="images-grid"
 62          :class="{
 63            'single': moment.images.length === 1,
 64            'double': moment.images.length === 2,
 65            'triple': moment.images.length === 3
 66          }"
 67          v-if="moment.images.length > 0"
 68        >
 69          <img 
 70            v-for="(img, index) in moment.images" 
 71            :key="index"
 72            :src="img" 
 73            alt="动态图片"
 74            class="grid-img"
 75            @click="openImagePreview(moment.images, index)"
 76          >
 77        </div>
 78
 79        <!-- 互动按钮 -->
 80        <div class="moment-actions">
 81          <button 
 82            class="action-btn like-btn"
 83            :class="{ liked: moment.liked }"
 84            @click="toggleLike(moment.id)"
 85          >
 86            <i class="icon-like">{{ moment.liked ? '♥' : '♡' }}</i>
 87            <span>{{ moment.likes }}</span>
 88          </button>
 89          <button class="action-btn comment-btn">
 90            <i class="icon-comment">💬</i>
 91            <span>{{ moment.comments }}</span>
 92          </button>
 93          <button class="action-btn share-btn">
 94            <i class="icon-share">🔗</i>
 95            <span>分享</span>
 96          </button>
 97        </div>
 98      </div>
 99
100      <!-- 空状态 -->
101      <div class="empty-state" v-if="sortedMoments.length === 0">
102        <div class="empty-icon">📝</div>
103        <p>还没有动态发布你的第一条动态吧</p>
104      </div>
105    </div>
106
107    <!-- 发布动态弹窗 -->
108    <div class="modal-overlay" v-if="showPublishModal" @click="closePublishModal">
109      <div class="modal" @click.stop>
110        <div class="modal-header">
111          <h2 class="modal-title">发布动态</h2>
112          <button class="close-btn" @click="closePublishModal">×</button>
113        </div>
114        <div class="modal-body">
115          <textarea 
116            class="post-content" 
117            placeholder="分享你的想法..."
118            v-model="postContent"
119            @input="checkPublishable"
120          ></textarea>
121
122          <div class="image-upload">
123            <div class="upload-title">添加图片最多9张</div>
124            <div class="preview-container">
125              <div 
126                class="preview-item" 
127                v-for="(img, index) in previewImages" 
128                :key="index"
129              >
130                <img :src="img" alt="预览图" class="preview-img">
131                <button 
132                  class="remove-img" 
133                  @click="removeImage(index)"
134                >×</button>
135              </div>
136
137              <label class="upload-btn" v-if="previewImages.length < 9">
138                <input 
139                  type="file" 
140                  accept="image/*" 
141                  multiple
142                  @change="handleImageUpload"
143                  class="upload-input"
144                >
145                <div class="upload-icon">+</div>
146              </label>
147            </div>
148          </div>
149        </div>
150        <div class="modal-footer">
151          <button class="cancel-btn" @click="closePublishModal">取消</button>
152          <button 
153            class="publish-btn" 
154            @click="publishMoment"
155            :disabled="!isPublishable"
156          >
157            发布
158          </button>
159        </div>
160      </div>
161    </div>
162
163    <!-- 图片预览弹窗 -->
164    <div class="image-preview" v-if="showImagePreview" @click="closeImagePreview">
165      <div class="preview-content" @click.stop>
166        <button class="preview-close" @click="closeImagePreview">×</button>
167        <img 
168          :src="currentPreviewImage" 
169          alt="图片预览" 
170          class="preview-img-large"
171        >
172        <div class="preview-nav" v-if="previewImagesList.length > 1">
173          <button class="nav-btn prev-btn" @click="prevImage"></button>
174          <button class="nav-btn next-btn" @click="nextImage"></button>
175        </div>
176        <div class="preview-counter">
177          {{ currentPreviewIndex + 1 }} / {{ previewImagesList.length }}
178        </div>
179      </div>
180    </div>
181  </div>
182</template>
183
184<script setup>
185import { ref, computed, watch, onMounted } from 'vue';
186
187// 从localStorage加载数据或使用初始数据
188const loadMoments = () => {
189  const saved = localStorage.getItem('momentsData');
190  if (saved) {
191    try {
192      return JSON.parse(saved);
193    } catch (e) {
194      console.error('Failed to parse moments data', e);
195      return [];
196    }
197  }
198  // 初始示例数据
199  return [
200    {
201      id: 1,
202      content: "今天天气真好,去公园散步了~ 🌸",
203      images: [
204        "https://picsum.photos/600/400?random=10",
205        "https://picsum.photos/600/400?random=11"
206      ],
207      time: new Date(Date.now() - 3600000).getTime(), // 1小时前
208      likes: 15,
209      liked: false,
210      comments: 3
211    },
212    {
213      id: 2,
214      content: "分享一下新做的晚餐,味道还不错!",
215      images: [
216        "https://picsum.photos/600/400?random=20"
217      ],
218      time: new Date(Date.now() - 86400000).getTime(), // 1天前
219      likes: 28,
220      liked: true,
221      comments: 7
222    },
223    {
224      id: 3,
225      content: "周末去看了一场精彩的电影,强烈推荐!",
226      images: [],
227      time: new Date(Date.now() - 172800000).getTime(), // 2天前
228      likes: 12,
229      liked: false,
230      comments: 2
231    }
232  ];
233};
234
235// 保存数据到localStorage
236const saveMoments = (data) => {
237  try {
238    localStorage.setItem('momentsData', JSON.stringify(data));
239  } catch (e) {
240    console.error('Failed to save moments data', e);
241  }
242};
243
244// 动态数据 - 从localStorage加载
245const moments = ref(loadMoments());
246
247// 监听数据变化,自动保存到localStorage
248watch(moments, (newValue) => {
249  saveMoments(newValue);
250}, { deep: true });
251
252// 组件挂载时加载数据
253onMounted(() => {
254  moments.value = loadMoments();
255});
256
257// 按日期排序(最新的在前)
258const sortedMoments = computed(() => {
259  return [...moments.value].sort((a, b) => b.time - a.time);
260});
261
262// 快速发布相关
263const quickContent = ref('');
264
265// 发布弹窗相关
266const showPublishModal = ref(false);
267const postContent = ref('');
268const previewImages = ref([]);
269const isPublishable = ref(false);
270
271// 图片预览相关
272const showImagePreview = ref(false);
273const previewImagesList = ref([]);
274const currentPreviewIndex = ref(0);
275const currentPreviewImage = computed(() => {
276  return previewImagesList.value[currentPreviewIndex.value] || '';
277});
278
279// 检查是否可以发布
280const checkPublishable = () => {
281  isPublishable.value = !!postContent.value.trim() || previewImages.value.length > 0;
282};
283
284// 格式化时间显示
285const formatTime = (timestamp) => {
286  const now = Date.now();
287  const diff = now - timestamp;
288  const second = 1000;
289  const minute = second * 60;
290  const hour = minute * 60;
291  const day = hour * 24;
292  const week = day * 7;
293
294  if (diff < minute) {
295    return '刚刚';
296  } else if (diff < hour) {
297    return `${Math.floor(diff / minute)}分钟前`;
298  } else if (diff < day) {
299    return `${Math.floor(diff / hour)}小时前`;
300  } else if (diff < week) {
301    return `${Math.floor(diff / day)}天前`;
302  } else {
303    const date = new Date(timestamp);
304    return `${date.getMonth() + 1}${date.getDate()}日`;
305  }
306};
307
308// 切换点赞状态
309const toggleLike = (id) => {
310  const moment = moments.value.find(m => m.id === id);
311  if (moment) {
312    if (moment.liked) {
313      moment.likes--;
314    } else {
315      moment.likes++;
316    }
317    moment.liked = !moment.liked;
318    // 触发watch更新
319    moments.value = [...moments.value];
320  }
321};
322
323// 打开发布弹窗
324const openPublishModal = () => {
325  showPublishModal.value = true;
326  postContent.value = '';
327  previewImages.value = [];
328  isPublishable.value = false;
329};
330
331// 关闭发布弹窗
332const closePublishModal = () => {
333  showPublishModal.value = false;
334};
335
336// 处理图片上传
337const handleImageUpload = (e) => {
338  const files = e.target.files;
339  if (!files.length) return;
340
341  // 最多上传9张
342  const maxAdd = 9 - previewImages.value.length;
343  const filesToProcess = Array.from(files).slice(0, maxAdd);
344
345  filesToProcess.forEach(file => {
346    const reader = new FileReader();
347    reader.onload = (event) => {
348      previewImages.value.push(event.target.result);
349      checkPublishable();
350    };
351    reader.readAsDataURL(file);
352  });
353
354  // 清空input值,允许重复选择相同文件
355  e.target.value = '';
356};
357
358// 移除预览图片
359const removeImage = (index) => {
360  previewImages.value.splice(index, 1);
361  checkPublishable();
362};
363
364// 发布动态
365const publishMoment = () => {
366  const content = postContent.value.trim();
367  if (!content && previewImages.value.length === 0) return;
368
369  const newMoment = {
370    id: Date.now(),
371    content: content,
372    images: [...previewImages.value],
373    time: Date.now(),
374    likes: 0,
375    liked: false,
376    comments: 0
377  };
378
379  moments.value.unshift(newMoment);
380  closePublishModal();
381};
382
383// 快速发布
384const handleQuickPublish = () => {
385  const content = quickContent.value.trim();
386  if (!content) return;
387
388  const newMoment = {
389    id: Date.now(),
390    content: content,
391    images: [],
392    time: Date.now(),
393    likes: 0,
394    liked: false,
395    comments: 0
396  };
397
398  moments.value.unshift(newMoment);
399  quickContent.value = '';
400};
401
402// 打开图片预览
403const openImagePreview = (images, index) => {
404  previewImagesList.value = images;
405  currentPreviewIndex.value = index;
406  showImagePreview.value = true;
407  document.body.style.overflow = 'hidden';
408};
409
410// 关闭图片预览
411const closeImagePreview = () => {
412  showImagePreview.value = false;
413  document.body.style.overflow = '';
414};
415
416// 上一张图片
417const prevImage = () => {
418  currentPreviewIndex.value = (currentPreviewIndex.value - 1 + previewImagesList.value.length) % previewImagesList.value.length;
419};
420
421// 下一张图片
422const nextImage = () => {
423  currentPreviewIndex.value = (currentPreviewIndex.value + 1) % previewImagesList.value.length;
424};
425
426// 监听键盘事件用于图片预览导航
427watch(showImagePreview, (isVisible) => {
428  const handleKeydown = (e) => {
429    if (!isVisible) return;
430
431    if (e.key === 'Escape') {
432      closeImagePreview();
433    } else if (e.key === 'ArrowLeft') {
434      prevImage();
435    } else if (e.key === 'ArrowRight') {
436      nextImage();
437    }
438  };
439
440  document.addEventListener('keydown', handleKeydown);
441  return () => {
442    document.removeEventListener('keydown', handleKeydown);
443  };
444});
445</script>
446
447<style scoped>
448/* 样式和之前保持一致 */
449.moments-container {
450  max-width: 800px;
451  margin: 0 auto;
452  background-color: #fff;
453  min-height: 100vh;
454  box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
455  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
456  color: #333;
457}
458
459/* 头部样式 */
460.header {
461  padding: 16px 20px;
462  border-bottom: 1px solid #eee;
463  display: flex;
464  justify-content: space-between;
465  align-items: center;
466  background-color: #fff;
467  position: sticky;
468  top: 0;
469  z-index: 10;
470}
471
472.header-content {
473  display: flex;
474  align-items: center;
475  gap: 10px;
476}
477
478.avatar {
479  width: 40px;
480  height: 40px;
481  border-radius: 50%;
482  object-fit: cover;
483}
484
485.page-title {
486  font-size: 1.2rem;
487  font-weight: 600;
488  margin: 0;
489}
490
491.publish-btn {
492  background-color: #00c853;
493  color: white;
494  border: none;
495  padding: 8px 16px;
496  border-radius: 20px;
497  font-weight: 500;
498  cursor: pointer;
499  display: flex;
500  align-items: center;
501  gap: 6px;
502  transition: background-color 0.2s;
503}
504
505.publish-btn:hover {
506  background-color: #00b248;
507}
508
509.publish-btn:disabled {
510  background-color: #a5d6a7;
511  cursor: not-allowed;
512}
513
514/* 快速发布区域 */
515.quick-publish {
516  padding: 15px 20px;
517  border-bottom: 10px solid #f5f5f5;
518  display: flex;
519  align-items: center;
520  gap: 12px;
521}
522
523.publish-avatar {
524  width: 40px;
525  height: 40px;
526  border-radius: 50%;
527  object-fit: cover;
528}
529
530.quick-input {
531  flex: 1;
532  padding: 12px 15px;
533  border: 1px solid #e0e0e0;
534  border-radius: 25px;
535  outline: none;
536  transition: border-color 0.2s;
537  font-size: 1rem;
538}
539
540.quick-input:focus {
541  border-color: #b3e5fc;
542}
543
544/* 动态列表 */
545.moments-list {
546  padding-bottom: 20px;
547}
548
549.moment-item {
550  padding: 15px 20px;
551  border-bottom: 1px solid #eee;
552  transition: background-color 0.1s;
553}
554
555.moment-item:hover {
556  background-color: #fafafa;
557}
558
559.moment-header {
560  display: flex;
561  gap: 10px;
562  margin-bottom: 10px;
563}
564
565.moment-avatar {
566  width: 45px;
567  height: 45px;
568  border-radius: 50%;
569  object-fit: cover;
570}
571
572.user-info {
573  flex: 1;
574}
575
576.username {
577  font-weight: 600;
578  margin-bottom: 2px;
579  display: inline-block;
580}
581
582.post-time {
583  font-size: 0.8rem;
584  color: #999;
585}
586
587.moment-content {
588  margin-bottom: 10px;
589  font-size: 1rem;
590  line-height: 1.6;
591}
592
593/* 图片网格 */
594.images-grid {
595  display: grid;
596  grid-template-columns: repeat(3, 1fr);
597  gap: 5px;
598  margin-bottom: 10px;
599}
600
601.grid-img {
602  width: 100%;
603  aspect-ratio: 1/1;
604  object-fit: cover;
605  border-radius: 4px;
606  cursor: pointer;
607  transition: transform 0.2s;
608}
609
610.grid-img:hover {
611  transform: scale(1.02);
612}
613
614/* 不同图片数量的布局 */
615.images-grid.single {
616  grid-template-columns: 1fr;
617  max-width: 300px;
618}
619
620.images-grid.double {
621  grid-template-columns: repeat(2, 1fr);
622}
623
624/* 互动按钮 */
625.moment-actions {
626  display: flex;
627  justify-content: space-around;
628  padding-top: 5px;
629  border-top: 1px solid #f0f0f0;
630  margin-top: 8px;
631}
632
633.action-btn {
634  flex: 1;
635  background: none;
636  border: none;
637  padding: 8px 0;
638  color: #666;
639  font-size: 0.9rem;
640  cursor: pointer;
641  display: flex;
642  align-items: center;
643  justify-content: center;
644  gap: 5px;
645  transition: color 0.2s, background-color 0.2s;
646  border-radius: 4px;
647}
648
649.action-btn:hover {
650  background-color: #f5f5f5;
651  color: #007bff;
652}
653
654.like-btn.liked {
655  color: #e53935;
656}
657
658/* 空状态 */
659.empty-state {
660  padding: 50px 20px;
661  text-align: center;
662  color: #999;
663}
664
665.empty-icon {
666  font-size: 3rem;
667  margin-bottom: 15px;
668}
669
670/* 发布弹窗 */
671.modal-overlay {
672  position: fixed;
673  top: 0;
674  left: 0;
675  right: 0;
676  bottom: 0;
677  background-color: rgba(0, 0, 0, 0.5);
678  display: flex;
679  align-items: center;
680  justify-content: center;
681  z-index: 100;
682  opacity: 0;
683  visibility: hidden;
684  transition: opacity 0.3s, visibility 0.3s;
685}
686
687.modal-overlay:not([v-if="false"]) {
688  opacity: 1;
689  visibility: visible;
690}
691
692.modal {
693  background-color: white;
694  width: 90%;
695  max-width: 600px;
696  border-radius: 10px;
697  max-height: 90vh;
698  display: flex;
699  flex-direction: column;
700}
701
702.modal-header {
703  padding: 15px 20px;
704  border-bottom: 1px solid #eee;
705  display: flex;
706  justify-content: space-between;
707  align-items: center;
708}
709
710.modal-title {
711  font-size: 1.1rem;
712  font-weight: 600;
713  margin: 0;
714}
715
716.close-btn {
717  background: none;
718  border: none;
719  font-size: 1.5rem;
720  cursor: pointer;
721  color: #666;
722  transition: color 0.2s;
723}
724
725.close-btn:hover {
726  color: #333;
727}
728
729.modal-body {
730  padding: 20px;
731  flex: 1;
732  overflow-y: auto;
733}
734
735.post-content {
736  width: 100%;
737  min-height: 120px;
738  border: none;
739  outline: none;
740  resize: none;
741  font-size: 1rem;
742  margin-bottom: 20px;
743  line-height: 1.6;
744  font-family: inherit;
745}
746
747.image-upload {
748  margin-bottom: 20px;
749}
750
751.upload-title {
752  font-size: 0.9rem;
753  color: #666;
754  margin-bottom: 10px;
755}
756
757.preview-container {
758  display: flex;
759  flex-wrap: wrap;
760  gap: 10px;
761}
762
763.preview-item {
764  width: 80px;
765  height: 80px;
766  position: relative;
767}
768
769.preview-img {
770  width: 100%;
771  height: 100%;
772  object-fit: cover;
773  border-radius: 4px;
774}
775
776.remove-img {
777  position: absolute;
778  top: -5px;
779  right: -5px;
780  background-color: rgba(255, 255, 255, 0.9);
781  border-radius: 50%;
782  width: 20px;
783  height: 20px;
784  display: flex;
785  align-items: center;
786  justify-content: center;
787  font-size: 12px;
788  cursor: pointer;
789  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
790  border: none;
791  transition: background-color 0.2s;
792}
793
794.remove-img:hover {
795  background-color: white;
796}
797
798.upload-btn {
799  width: 80px;
800  height: 80px;
801  border: 2px dashed #ddd;
802  border-radius: 4px;
803  display: flex;
804  align-items: center;
805  justify-content: center;
806  color: #999;
807  cursor: pointer;
808  transition: border-color 0.2s, color 0.2s;
809}
810
811.upload-btn:hover {
812  border-color: #007bff;
813  color: #007bff;
814}
815
816.upload-input {
817  display: none;
818}
819
820.modal-footer {
821  padding: 15px 20px;
822  border-top: 1px solid #eee;
823  display: flex;
824  justify-content: flex-end;
825  gap: 10px;
826}
827
828.cancel-btn {
829  padding: 8px 16px;
830  border: 1px solid #ddd;
831  background: none;
832  border-radius: 4px;
833  cursor: pointer;
834  transition: background-color 0.2s;
835}
836
837.cancel-btn:hover {
838  background-color: #f5f5f5;
839}
840
841/* 图片预览弹窗 */
842.image-preview {
843  position: fixed;
844  top: 0;
845  left: 0;
846  right: 0;
847  bottom: 0;
848  background-color: rgba(0, 0, 0, 0.9);
849  z-index: 200;
850  display: flex;
851  align-items: center;
852  justify-content: center;
853  opacity: 0;
854  visibility: hidden;
855  transition: opacity 0.3s, visibility 0.3s;
856}
857
858.image-preview:not([v-if="false"]) {
859  opacity: 1;
860  visibility: visible;
861}
862
863.preview-content {
864  position: relative;
865  max-width: 90%;
866  max-height: 90vh;
867}
868
869.preview-img-large {
870  max-width: 100%;
871  max-height: 90vh;
872  object-fit: contain;
873}
874
875.preview-close {
876  position: absolute;
877  top: -40px;
878  right: 0;
879  color: white;
880  background: none;
881  border: none;
882  font-size: 1.5rem;
883  cursor: pointer;
884}
885
886.preview-nav {
887  position: absolute;
888  top: 50%;
889  width: 100%;
890  display: flex;
891  justify-content: space-between;
892  transform: translateY(-50%);
893}
894
895.nav-btn {
896  background-color: rgba(0, 0, 0, 0.3);
897  color: white;
898  border: none;
899  width: 40px;
900  height: 40px;
901  border-radius: 50%;
902  display: flex;
903  align-items: center;
904  justify-content: center;
905  cursor: pointer;
906  font-size: 1.2rem;
907  transition: background-color 0.2s;
908}
909
910.nav-btn:hover {
911  background-color: rgba(0, 0, 0, 0.5);
912}
913
914.preview-counter {
915  position: absolute;
916  bottom: -30px;
917  left: 50%;
918  transform: translateX(-50%);
919  color: white;
920  font-size: 0.9rem;
921}
922
923/* 响应式调整 */
924@media (max-width: 600px) {
925  .images-grid {
926    gap: 3px;
927  }
928
929  .action-btn span {
930    font-size: 0.8rem;
931  }
932}
933</style>

创建时间:11:27

主要修改点说明:

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

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

效果测试

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

image-20250913151035243

最新文章

文档导航