利用 `
<!-- 頁面模板 --> <form id="uploadForm" action="/api/upload/avatar" method="POST" enctype="multipart/form-data" target="uploadIframe"> <div class="avatar-preview"> <img id="avatarImg" src="= $user->avatarUrl ?: '/static/default-avatar.png' ?>" width="100" height="100" alt="頭像"> </div> <input type="file" name="avatar" id="avatarInput" accept="image/*" required> <input type="hidden" name="csrf_token" value="= Yii::app()->request->csrfToken ?>"> <button type="submit" id="uploadBtn" style="display:none">上傳</button> </form> <!-- 隱藏 iframe 接收回應 --> <iframe id="uploadIframe" name="uploadIframe" style="display:none;"></iframe>
<?php // /api/upload/avatar public function actionUploadAvatar() { // 僅接受 iframe POST if (!Yii::app()->request->isPostRequest) { $this->renderError('非法請求'); } $user = User::current(); if (!$user) { $this->renderError('請先登入'); } $file = CUploadedFile::getInstanceByName('avatar'); if (!$file) { $this->renderError('未選擇檔案'); } // 驗證 $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!in_array($file->getType(), $allowedTypes)) { $this->renderError('僅支援 JPG/PNG/GIF/WebP 格式'); } if ($file->getSize() > 2 * 1024 * 1024) { // 2MB $this->renderError('檔案不得超過 2MB'); } // 儲存路徑:/uploads/avatar/{uid}.{ext} $dir = Yii::getPathOfAlias('webroot') . '/uploads/avatar/'; if (!is_dir($dir)) mkdir($dir, 0755, true); // 刪除舊頭像 (同 uid 不同副檔名) foreach (glob($dir . $user->id . '.*') as $old) @unlink($old); $ext = strtolower($file->getExtensionName()); $saveName = $user->id . '.' . $ext; $savePath = $dir . $saveName; if (!$file->saveAs($savePath)) { $this->renderError('檔案儲存失敗'); } // 產生縮圖 (使用 PHPThumb/Intervention/GD) $this->createThumbs($savePath, $dir, $user->id); // 更新用戶記錄 (用時間戳防快取) $user->avatar_updated = time(); $user->save(false); // 成功回應:iframe 會載入此 HTML $this->renderSuccess($saveName); } // 統一 iframe 回應格式 private function renderSuccess($filename) { header('Content-Type: text/html; charset=utf-8'); echo '上傳成功'; Yii::app()->end(); } private function renderError($msg) { header('Content-Type: text/html; charset=utf-8'); echo '' . htmlspecialchars($msg) . ''; Yii::app()->end(); } ?>
上傳成功
' . htmlspecialchars($msg) . '
<script> $(function() { const $form = $('#uploadForm'); const $input = $('#avatarInput'); const $btn = $('#uploadBtn'); const $img = $('#avatarImg'); const $iframe = $('#uploadIframe'); // 選擇檔案後自動提交 $input.on('change', function() { if (this.files.length) { $btn.trigger('click'); // 觸發隱藏 submit } }); // iframe 載入完成事件 $iframe.on('load', function() { try { const doc = this.contentDocument || this.contentWindow.document; const $msg = $(doc).find('.iframe-msg'); if (!$msg.length) return; // 非預期回應 const status = $msg.data('status'); const message = $msg.text(); if (status == '1') { // 成功:更新頭像圖片 (加時間戳防快取) const filename = $msg.data('filename'); const newSrc = '/uploads/avatar/' + filename + '?t=' + Date.now(); $img.attr('src', newSrc); showToast('頭像更新成功'); } else { showToast('上傳失敗: ' + message); } } catch (e) { console.error('iframe 回應解析失敗', e); showToast('上傳回應異常'); } }); }); </script>
<!-- 頁面加入 Cropper 模態框 --> <div id="cropperModal" class="modal" style="display:none;"> <img id="cropperImage" src=""> </div> <script> // 選擇檔案 → 讀取預覽 → 開啟裁切模態框 $input.on('change', function() { const file = this.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { $('#cropperImage').attr('src', e.target.result); $('#cropperModal').show(); // 初始化 Cropper const cropper = new Cropper('#cropperImage', { aspectRatio: 1, viewMode: 1, autoCropArea: 1, }); // 確認裁切 → 轉 Blob → 追加到 FormData → AJAX 上傳 $('#cropConfirm').one('click', () => { cropper.getCroppedCanvas().toBlob((blob) => { const formData = new FormData(); formData.append('avatar', blob, 'avatar.jpg'); // 此處改用 AJAX 上傳 (非 iframe) $.ajax({ url: '/api/upload/avatar', type: 'POST', data: formData, processData: false, contentType: false, success: (res) => { /* 同前處理 */ } }); }, 'image/jpeg', 0.9); }); }; reader.readAsDataURL(file); }); </script>
finfo
php_flag engine off
location ~ \.php$ { deny all; }
upload_max_filesize
client_max_body_size
若專案支援 IE10+,建議改用 FormData + AJAX (fetch/XHR) 完全取代 iframe,體驗更好、錯誤處理更簡單。iframe 方案僅作為舊瀏覽器相容備案。
為什麼需要快取? 高併發下,資料庫查詢、複雜運算、外部 AP
痛點 TP 手冊分頁代碼冗長:每次需實例化 Page、設置配
結合 PHP 後端、MySQL、jQuery AJAX 實作
LNMP (Linux + Nginx + MySQL +