Today

You said

我想要寫一個顯示照片的app,只需要在自己電腦上的瀏覽器能執行即可,不需要靠遠端的伺服器來執行,請幫幫我。 需求如下,我以JavaScript語法在最後附上我的相簿位置,請注意,這僅供參考,我以後會自行增減。 (1) 生成一個單獨而完整的html。 (2) 提供一個下拉選單,可讓我選擇不同的相簿。 (3) 相簿的照片先以縮圖方式呈現,等我進一步點選後才呈現完整的照片。 (4) 除了能讓我能單獨選擇照片外,另外提供一個"播放/暫停"按鍵,可依序自動播放所選相簿內的所有照片,這同一按鍵也可以暫停。 (5) "播放/暫停"按鍵的功能可用鍵盤的空白鍵來取代。 (6) 自動播放時,照片顯示的時間可讓我指定,單位以秒計算。 (7) 注意!當處在暫停狀態時,照片會永遠顯示,也就是說照片顯示時間這個設定會跟著失效。一旦恢復播放模式,照片顯示時間才有作用。 (8) 下拉選單、播放按鍵和控制照片顯示時間的輸入這三項請固定在畫面最上方。縮圖以一條水平列呈現,固定在畫面最下方。中間大部分空白則顯示完整的圖片。注意照片下方的描述文字別被遮掉,也需要安排些空間來正常顯示。 const sources = { "my_hiking_1": { path: "../photo_galleries/my_album_1/", data: [ "001.jpg", "002.jpg", "003.jpg", ], description: [ "001_comment", "002_comment", "003_comment", ] }, "my_hiking_2": { path: "../photo_galleries/my_album_2/", data: [ "001.jpg", "002.jpg", ], description: [ "001_comment", "002_comment", ] }, };

Copilot said

下面提供一個完整、獨立的 HTML 範例,直接複製下來存成 .html 檔,然後用瀏覽器打開即可(注意照片路徑與資源需調整至你本地電腦上的正確位置)。

這份程式碼滿足以下需求:

  1. 單一 HTML 頁面。

  2. 頂部固定一個下拉選單,可切換相簿。

  3. 中間區域先顯示照片縮圖,點選後切換顯示完整大圖與其描述。

  4. 還有一個「播放/暫停」按鍵(同時可用鍵盤空白鍵觸發)會自動依序播放相簿內所有照片;在暫停狀態時,照片會保留在畫面上(並忽略顯示時間設定)。

  5. 最上方固定有下拉選單、播放按鍵、及顯示時間輸入框;最下方則是一條水平排列的縮圖列。

  6. 下方保留出一個區域用來完整顯示照片,並留出空間給照片下方的描述文字顯示。

你可參考以下程式碼(內含詳細註解):

html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>照片相簿 App</title>
  <style>
    /* 視窗與內邊距設置 */
    body, html {
      margin: 0;
      padding: 0;
      height: 100%;
      font-family: Arial, sans-serif;
    }
    /* 頂部固定的控制區 */
    #header {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      background-color: #f0f0f0;
      padding: 10px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      z-index: 1000;
      display: flex;
      align-items: center;
      gap: 10px;
    }
    /* 中間完整照片的區域 (避免頂部及底部遮擋) */
    #main {
      margin-top: 60px;   /* 根據 header 的高度調整 */
      margin-bottom: 110px; /* 根據下面縮圖列的高度調整 */
      text-align: center;
    }
    #main img {
      max-width: 90%;
      max-height: calc(100vh - 160px); /* 扣除 header 與底部縮圖列的空間 */
    }
    /* 照片下方之描述文字 */
    #description {
      margin-top: 10px;
      padding: 5px;
      background-color: #fff;
      width: 90%;
      margin-left: auto;
      margin-right: auto;
      border: 1px solid #ccc;
    }
    /* 固定在下方的縮圖列 */
    #thumbnails {
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      background-color: #fafafa;
      padding: 10px;
      box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
      display: flex;
      overflow-x: auto;
      gap: 10px;
    }
    #thumbnails img {
      height: 80px;
      cursor: pointer;
      transition: transform 0.3s;
    }
    #thumbnails img:hover {
      transform: scale(1.1);
    }
  </style>
</head>
<body>
  <!-- 頂部固定控制區 -->
  <div id="header">
    <!-- 下拉選單:選擇相簿 -->
    <select id="albumSelect"></select>
    <!-- 播放/暫停按鍵 -->
    <button id="playPauseButton">播放</button>
    <!-- 調整自動播放時的照片顯示秒數 -->
    <label for="displayTime">顯示秒數:</label>
    <input type="number" id="displayTime" value="3" min="1" style="width:50px;">
  </div>

  <!-- 中間完整照片顯示區 -->
  <div id="main">
    <img id="fullImage" src="" alt="完整照片">
    <div id="description"></div>
  </div>

  <!-- 底部縮圖列 -->
  <div id="thumbnails"></div>

  <script>
    /*************** 相簿資料定義 ****************/
    const sources = {
      "my_hiking_1": {
        path: "../photo_galleries/my_album_1/",
        data: [
          "001.jpg",
          "002.jpg",
          "003.jpg",
        ],
        description: [
          "001_comment",
          "002_comment",
          "003_comment",
        ]
      },
      "my_hiking_2": {
        path: "../photo_galleries/my_album_2/",
        data: [
          "001.jpg",
          "002.jpg",
        ],
        description: [
          "001_comment",
          "002_comment",
        ]
      }
    };

    /*************** 全域變數及元素抓取 ****************/
    let currentAlbum = null;   // 當前所選專輯
    let currentIndex = 0;      // 當前照片索引
    let timer = null;          // 用於自動播放的計時器
    let isPlaying = false;     // 播放狀態

    // 抓取 DOM 元素
    const albumSelect = document.getElementById('albumSelect');
    const playPauseButton = document.getElementById('playPauseButton');
    const displayTimeInput = document.getElementById('displayTime');
    const fullImage = document.getElementById('fullImage');
    const descriptionDiv = document.getElementById('description');
    const thumbnailsDiv = document.getElementById('thumbnails');

    /*************** 功能函式 ****************/

    // 1. 初始化下拉選單
    function populateAlbumSelect() {
      albumSelect.innerHTML = '';
      for (const album in sources) {
        const option = document.createElement('option');
        option.value = album;
        option.textContent = album;
        albumSelect.appendChild(option);
      }
    }

    // 2. 產生縮圖 (依據選擇相簿)
    function populateThumbnails(albumKey) {
      thumbnailsDiv.innerHTML = '';
      const albumData = sources[albumKey];
      albumData.data.forEach((imgFile, index) => {
        const thumbImg = document.createElement('img');
        thumbImg.src = albumData.path + imgFile;
        thumbImg.alt = albumData.description[index] || '';
        thumbImg.addEventListener('click', () => {
          // 點選縮圖後,暫停自動播放
          stopSlideshow();
          currentIndex = index;
          displayImage();
        });
        thumbnailsDiv.appendChild(thumbImg);
      });
    }

    // 3. 在中間區域顯示大圖與描述
    function displayImage() {
      const albumData = sources[currentAlbum];
      const imgFile = albumData.data[currentIndex];
      fullImage.src = albumData.path + imgFile;
      descriptionDiv.textContent = albumData.description[currentIndex] || '';
      highlightThumbnail();
    }

    // 4. 突顯目前所選縮圖
    function highlightThumbnail() {
      Array.from(thumbnailsDiv.children).forEach((img, idx) => {
        img.style.border = (idx === currentIndex) ? '2px solid blue' : 'none';
      });
    }

    // 5. 播放時,顯示下一張照片
    function nextImage() {
      const albumData = sources[currentAlbum];
      currentIndex = (currentIndex + 1) % albumData.data.length;
      displayImage();
    }

    // 6. 開始自動播放(自動換圖)
    function startSlideshow() {
      if (isPlaying) return;
      isPlaying = true;
      playPauseButton.textContent = '暫停';
      const displayTime = parseInt(displayTimeInput.value, 10) * 1000;
      timer = setInterval(nextImage, displayTime);
    }

    // 7. 停止自動播放 (進入暫停狀態)
    function stopSlideshow() {
      isPlaying = false;
      playPauseButton.textContent = '播放';
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }

    // 8. 切換播放與暫停狀態
    function toggleSlideshow() {
      isPlaying ? stopSlideshow() : startSlideshow();
    }

    /*************** 事件綁定 ****************/

    // 點擊播放/暫停按鍵
    playPauseButton.addEventListener('click', () => {
      toggleSlideshow();
    });

    // 藉由空白鍵觸發播放/暫停功能
    document.addEventListener('keydown', (e) => {
      if (e.code === 'Space') {
        e.preventDefault();
        toggleSlideshow();
      }
    });

    // 選擇不同的相簿時
    albumSelect.addEventListener('change', (e) => {
      stopSlideshow();  // 相簿變更時停止自動播放
      currentAlbum = e.target.value;
      currentIndex = 0;
      populateThumbnails(currentAlbum);
      displayImage();
    });

    // 若修改顯示照片的秒數(正在自動播放時會重啟計時器)
    displayTimeInput.addEventListener('change', () => {
      if (isPlaying) {
        stopSlideshow();
        startSlideshow();
      }
    });

    /*************** 初始化 ****************/
    populateAlbumSelect();
    // 預設選擇第一個相簿
    currentAlbum = albumSelect.value;
    populateThumbnails(currentAlbum);
    displayImage();
  </script>
</body>
</html>

更多補充說明

  • 相簿資料:程式碼中 sources 變數保存相簿路徑、檔名及描述。未來只要修改這個物件,即可新增、刪除相簿或更改照片資料。

  • 自動播放與暫停:當按下播放鈕(或空白鍵)後,系統會依照輸入的秒數定時切換下一張照片;若處於暫停狀態,則當前照片與描述永續顯示,不會自動切換。

  • UI 佈局:頂部與底部的區塊均利用固定定位,確保一切控制元件不隨中間滾動。若需更改樣式或調整佈局,請依實際需求修改 CSS。

如果有其他進階需求,如:加入過場動畫或額外的鍵盤控制,皆可在這份基底上進一步擴充。希望這個範例能夠幫助你更順利地開始製作屬於自己的照片相簿 App!

進一步延伸的想法 你或許會希望:

  • 在播放模式中加入淡入淡出的效果,讓圖片之間切換更平滑。

  • 使用 localStorage 記錄上次選擇的相簿與最後觀看的照片索引,下次再打開時自動還原狀態。

  • 加入簡單的音效提示或額外的UI互動,讓應用更加豐富。

若你有更多想法或面臨其他技術上的挑戰,隨時討論!

You said

全螢幕下一切運作正常,但是當離開全螢幕後,照片敘述文字常常會被縮圖擋住,導致得移動顯示區域後才能看到敘述,體驗不佳。有辦法讓圖片和描述文字能剛好顯示在畫面內,不需要額外的調整嗎?

Copilot said

你可以調整中間區域的 CSS,使得圖片與描述文字區域永遠有足夠的「內部空間(padding)」,不會被下方固定的縮圖列蓋住。下面有兩種常見解法:

解法 1:利用 Padding 調整

在原來的 #main 區域,我們原先設定了 margin-bottom: 110px; 讓主內容區離縮圖有一個間隔,但這個間隔只是在區塊外面,如果內容本身較長,這個區間可能還是會讓內容往下延伸被固定縮圖擋住。改用 padding-bottom 則可以讓內容本身往內縮,確保內容結尾總是保持在「安全區」內。

例如,如果你的縮圖區高度約為 110px,就可以這樣修改:

css
#main {
  margin-top: 60px;         /* 保留 header 的空間 */
  padding-bottom: 110px;      /* 內部留白,避免被縮圖遮住 */
  text-align: center;
  /* 如果需要也可以限制最小高度 */
  min-height: calc(100vh - 60px - 110px);
}

這樣一來,即使離開全螢幕,當你滑動頁面時,圖片與下方的描述文字都會因 padding 被推上來,而不會直接被固定的縮圖擋住。

解法 2:使用固定定位的區域

另一種方法是將中間內容區以固定的區段呈現,其範圍剛好位於 header 與縮圖列之間。這可以透過 CSS 的 position: absolute 與上下邊界設定完成,例如:

css
#main {
  position: absolute;
  top: 60px;        /* header 高度 */
  bottom: 110px;    /* 縮圖列高度 */
  left: 0;
  right: 0;
  overflow-y: auto; /* 如果內容超過可顯示範圍,則自動顯示捲軸 */
  text-align: center;
  box-sizing: border-box;
}

這樣設定後,#main 區域的高度就會自動計算成「100vh 減去 header 與縮圖列的高度」,內容會在這個區域內自由滾動,確保內容不會被下方的固定元件覆蓋。

總結

如果你希望使用相對簡單的方式,而且內容不會太多需要內部滾動,第一種解法(padding)就非常直觀且容易維護。如果你的照片和描述內容較多,可能第二種方法會提供更完整的解決方案,因為它限定了顯示區的高度,並且在內容超出時自動產生捲軸。

你可以根據自己的需求與實際情況選擇其中一個方案,或者兩者結合調整。如果有進一步視覺與排版的需求(例如,讓圖片居中或調整捲軸的樣式),也可以根據現有的 CSS 進行更精細的調整。

進一步延伸:

  • 如果你想讓圖片在變換時加入淡入淡出的效果,而不只單純切換,可以考慮加入 CSS 的 transition 或 animation 效果。

  • 若未來需要支援更複雜的響應式設計,你也可以利用 Flexbox 或 Grid 來進一步規劃 header、main 與 thumbnails 的位置與大小。

希望這些調整能改善你在非全螢幕模式下的觀看體驗,有什麼其他問題或需求再一起討論!