KyrosDev commited on
Commit
471bcb5
·
1 Parent(s): 5ad24a9

新增版本編輯功能

Browse files

前端:
- 新增編輯按鈕至基本資訊區塊右側
- 實作編輯版本對話框顯示版本標題和更新日誌表單
- 支援開發模式模擬測試
- 包含表單驗證和錯誤處理

後端:
- 新增 PUT /api/admin/versions/{version}/edit 端點
- 支援編輯版本標題和更新日誌
- 支援 Revit 和 AutoCAD 產品類型
- 包含輸入驗證和錯誤處理
- 同步更新 Supabase 資料庫

Files changed (2) hide show
  1. app/api/version_routes.py +72 -0
  2. frontend/versions.html +177 -3
app/api/version_routes.py CHANGED
@@ -544,6 +544,78 @@ async def replace_version_file(
544
  logger.error(f"Replace version file error: {e}")
545
  raise HTTPException(status_code=500, detail=str(e))
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  @router.get("/admin/licenses/stats")
548
  async def get_license_stats(current_user = Depends(verify_admin)):
549
  """
 
544
  logger.error(f"Replace version file error: {e}")
545
  raise HTTPException(status_code=500, detail=str(e))
546
 
547
+ @router.put("/admin/versions/{version}/edit")
548
+ async def edit_version_metadata(
549
+ version: str,
550
+ product_type: str = "revit",
551
+ request: Request = None,
552
+ current_user = Depends(verify_admin)
553
+ ):
554
+ """
555
+ 編輯版本的元資料(標題和更新日誌)
556
+ - 更新版本標題 (title)
557
+ - 更新更新日誌 (changelog)
558
+ - 不影響檔案、下載 URL 等其他欄位
559
+ 需要管理員權限
560
+ 支援 product_type 參數: 'revit' 或 'autocad'
561
+ """
562
+ try:
563
+ client = supabase_clients.get_version_client()
564
+ if not client:
565
+ raise HTTPException(status_code=503, detail="Version system unavailable")
566
+
567
+ # 驗證產品類型
568
+ if product_type not in ['revit', 'autocad']:
569
+ raise HTTPException(status_code=400, detail="Invalid product_type. Must be 'revit' or 'autocad'")
570
+
571
+ # 根據產品類型選擇對應的資料表
572
+ table_name = 'versions_autocad' if product_type == 'autocad' else 'versions'
573
+
574
+ # 查詢版本資訊
575
+ version_query = client.table(table_name).select('*').eq('version', version).execute()
576
+
577
+ if not version_query.data:
578
+ raise HTTPException(status_code=404, detail=f"Version {version} not found in {product_type}")
579
+
580
+ # 解析請求資料
581
+ body = await request.json()
582
+ title = body.get('title', '').strip()
583
+ changelog = body.get('changelog', '').strip()
584
+
585
+ # 驗證必填欄位
586
+ if not title:
587
+ raise HTTPException(status_code=400, detail="Title is required")
588
+
589
+ if not changelog:
590
+ raise HTTPException(status_code=400, detail="Changelog is required")
591
+
592
+ # 更新資料庫記錄
593
+ update_data = {
594
+ 'title': title,
595
+ 'changelog': changelog
596
+ }
597
+
598
+ update_result = client.table(table_name).update(update_data).eq('version', version).execute()
599
+
600
+ if not update_result.data:
601
+ raise HTTPException(status_code=500, detail=f"Failed to update version {version} in database")
602
+
603
+ return {
604
+ "success": True,
605
+ "message": f"{product_type.capitalize()} version {version} metadata updated successfully",
606
+ "product_type": product_type,
607
+ "updated_fields": {
608
+ "title": title,
609
+ "changelog": changelog
610
+ }
611
+ }
612
+
613
+ except HTTPException:
614
+ raise
615
+ except Exception as e:
616
+ logger.error(f"Edit version metadata error: {e}")
617
+ raise HTTPException(status_code=500, detail=str(e))
618
+
619
  @router.get("/admin/licenses/stats")
620
  async def get_license_stats(current_user = Depends(verify_admin)):
621
  """
frontend/versions.html CHANGED
@@ -622,6 +622,25 @@
622
  border-top: 1px solid var(--border-primary);
623
  }
624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  /* 替換執行檔按鈕樣式 */
626
  .btn-replace {
627
  background: linear-gradient(135deg, #f59e0b, #f97316);
@@ -1821,7 +1840,12 @@
1821
  // 創建彈窗內容 (不包含 modal-header,因為 Components.createModal 會自動添加)
1822
  const modalContent = `
1823
  <div class="version-detail-section">
1824
- <h4><i class="fas fa-info-circle"></i> 基本資訊</h4>
 
 
 
 
 
1825
  <div class="detail-grid">
1826
  <div class="detail-item">
1827
  <label>版本標題:</label>
@@ -1841,14 +1865,14 @@
1841
  </div>
1842
  </div>
1843
  </div>
1844
-
1845
  ${version.changelog ? `
1846
  <div class="version-detail-section">
1847
  <h4><i class="fas fa-list"></i> 更新日誌</h4>
1848
  <div class="version-changelog-detail">${version.changelog.trim()}</div>
1849
  </div>
1850
  ` : ''}
1851
-
1852
  <div class="modal-actions">
1853
  <button class="btn btn-replace" onclick="versionApp.showReplaceFileDialog('${version.id}', '${version.version}', '${version.product_type}'); Components.closeModal(this.closest('.modal-overlay'));">
1854
  <i class="fas fa-sync-alt"></i> 替換執行檔
@@ -2081,6 +2105,156 @@
2081
  }
2082
  }
2083
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2084
  // 工具方法
2085
  formatFileSize(bytes) {
2086
  if (bytes === 0) return '0 Bytes';
 
622
  border-top: 1px solid var(--border-primary);
623
  }
624
 
625
+ /* 編輯按鈕樣式 */
626
+ .btn-edit {
627
+ background: linear-gradient(135deg, #8b5cf6, #7c3aed);
628
+ border: none;
629
+ color: white;
630
+ box-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);
631
+ transition: all 0.2s ease;
632
+ }
633
+
634
+ .btn-edit:hover {
635
+ background: linear-gradient(135deg, #7c3aed, #6d28d9);
636
+ transform: translateY(-1px);
637
+ box-shadow: 0 4px 8px rgba(139, 92, 246, 0.4);
638
+ }
639
+
640
+ .btn-edit i {
641
+ margin-right: 0.5rem;
642
+ }
643
+
644
  /* 替換執行檔按鈕樣式 */
645
  .btn-replace {
646
  background: linear-gradient(135deg, #f59e0b, #f97316);
 
1840
  // 創建彈窗內容 (不包含 modal-header,因為 Components.createModal 會自動添加)
1841
  const modalContent = `
1842
  <div class="version-detail-section">
1843
+ <h4 style="display: flex; justify-content: space-between; align-items: center;">
1844
+ <span><i class="fas fa-info-circle"></i> 基本資訊</span>
1845
+ <button onclick="versionApp.showEditVersionDialog('${version.id}', '${version.version}', '${version.product_type}'); Components.closeModal(this.closest('.modal-overlay'));" style="margin: 0; padding: 0.5rem; background: linear-gradient(135deg, #8b5cf6, #7c3aed); border: none; border-radius: 6px; cursor: pointer; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);" title="編輯版本資訊" onmouseover="this.style.background='linear-gradient(135deg, #7c3aed, #6d28d9)'; this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 8px rgba(139, 92, 246, 0.4)'" onmouseout="this.style.background='linear-gradient(135deg, #8b5cf6, #7c3aed)'; this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(139, 92, 246, 0.3)'">
1846
+ <i class="fas fa-pen" style="margin: 0; font-size: 0.85rem; color: white;"></i>
1847
+ </button>
1848
+ </h4>
1849
  <div class="detail-grid">
1850
  <div class="detail-item">
1851
  <label>版本標題:</label>
 
1865
  </div>
1866
  </div>
1867
  </div>
1868
+
1869
  ${version.changelog ? `
1870
  <div class="version-detail-section">
1871
  <h4><i class="fas fa-list"></i> 更新日誌</h4>
1872
  <div class="version-changelog-detail">${version.changelog.trim()}</div>
1873
  </div>
1874
  ` : ''}
1875
+
1876
  <div class="modal-actions">
1877
  <button class="btn btn-replace" onclick="versionApp.showReplaceFileDialog('${version.id}', '${version.version}', '${version.product_type}'); Components.closeModal(this.closest('.modal-overlay'));">
1878
  <i class="fas fa-sync-alt"></i> 替換執行檔
 
2105
  }
2106
  }
2107
 
2108
+ // 顯示編輯版本對話框
2109
+ showEditVersionDialog(versionId, versionString, productType) {
2110
+ console.log('✏️ 開啟編輯版本對話框:', { versionId, versionString, productType });
2111
+
2112
+ // 查找版本資訊
2113
+ let version = null;
2114
+ if (this.currentVersions && this.currentVersions.length > 0) {
2115
+ version = this.currentVersions.find(v => v.id === versionId || v.version === versionString);
2116
+ }
2117
+
2118
+ if (!version) {
2119
+ console.error('❌ 找不到版本資訊:', versionId);
2120
+ Utils.showError('找不到版本資訊', `版本 ID: ${versionId}`);
2121
+ return;
2122
+ }
2123
+
2124
+ const modalContent = `
2125
+ <div class="version-detail-section">
2126
+ <h4><i class="fas fa-info-circle"></i> 版本資訊</h4>
2127
+ <div class="detail-grid">
2128
+ <div class="detail-item">
2129
+ <label>版本號:</label>
2130
+ <span>v${versionString}</span>
2131
+ </div>
2132
+ <div class="detail-item">
2133
+ <label>產品類型:</label>
2134
+ <span>${productType === 'autocad' ? 'AutoCAD' : 'Revit'}</span>
2135
+ </div>
2136
+ </div>
2137
+ </div>
2138
+
2139
+ <div class="version-detail-section">
2140
+ <h4><i class="fas fa-edit"></i> 編輯內容</h4>
2141
+ <div class="form-group">
2142
+ <label for="editTitle">版本標題 <span class="required">*</span></label>
2143
+ <input type="text" id="editTitle" value="${version.title || ''}" placeholder="例如:重大功能更新" required style="width: 100%; padding: 0.6rem 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border-primary); border-radius: 6px; color: var(--text-primary); font-size: 0.85rem;">
2144
+ <small class="form-hint">簡短描述此版本的特色</small>
2145
+ </div>
2146
+ <div class="form-group" style="margin-top: 1rem;">
2147
+ <label for="editChangelog">更新日誌 <span class="required">*</span></label>
2148
+ <textarea id="editChangelog" placeholder="請詳細描述此版本的更新內容" required rows="8" style="width: 100%; padding: 0.6rem 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border-primary); border-radius: 6px; color: var(--text-primary); font-size: 0.85rem; resize: vertical; min-height: 150px; font-family: 'Monaco', 'Menlo', 'Consolas', monospace;">${version.changelog || ''}</textarea>
2149
+ <small class="form-hint">詳細說明新功能、修復的問題和改善項目</small>
2150
+ </div>
2151
+ </div>
2152
+
2153
+ <div class="modal-actions">
2154
+ <button class="btn btn-primary" id="confirmEditBtn">
2155
+ <i class="fas fa-save"></i> 儲存變更
2156
+ </button>
2157
+ <button class="btn btn-secondary" onclick="Components.closeModal(this.closest('.modal-overlay'))">
2158
+ 取消
2159
+ </button>
2160
+ </div>
2161
+ `;
2162
+
2163
+ const modal = Components.createModal({
2164
+ title: `<i class="fas fa-pen"></i> 編輯版本 - v${versionString}`,
2165
+ content: modalContent,
2166
+ size: 'md'
2167
+ });
2168
+
2169
+ Components.showModal(modal);
2170
+
2171
+ // 設置儲存按鈕事件
2172
+ setTimeout(() => {
2173
+ const confirmBtn = document.getElementById('confirmEditBtn');
2174
+ if (confirmBtn) {
2175
+ confirmBtn.addEventListener('click', async () => {
2176
+ const titleInput = document.getElementById('editTitle');
2177
+ const changelogInput = document.getElementById('editChangelog');
2178
+
2179
+ if (!titleInput.value.trim()) {
2180
+ Utils.showError('欄位驗證失敗', '版本標題不能為空');
2181
+ return;
2182
+ }
2183
+
2184
+ if (!changelogInput.value.trim()) {
2185
+ Utils.showError('欄位驗證失敗', '更新日誌不能為空');
2186
+ return;
2187
+ }
2188
+
2189
+ await this.handleEditVersion(versionId, versionString, productType, titleInput.value.trim(), changelogInput.value.trim());
2190
+ });
2191
+ }
2192
+ }, 100);
2193
+ }
2194
+
2195
+ // 執行版本編輯
2196
+ async handleEditVersion(versionId, versionString, productType, title, changelog) {
2197
+ const confirmBtn = document.getElementById('confirmEditBtn');
2198
+ const originalText = confirmBtn ? confirmBtn.innerHTML : '';
2199
+
2200
+ try {
2201
+ if (confirmBtn) {
2202
+ confirmBtn.disabled = true;
2203
+ confirmBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 儲存中...';
2204
+ }
2205
+
2206
+ if (this.isDevMode) {
2207
+ // 開發模式模擬編輯
2208
+ await new Promise(resolve => setTimeout(resolve, 1500));
2209
+ Utils.showSuccess('儲存成功', `版本 ${versionString} 的資訊已成功更新`);
2210
+ Components.closeModal(document.querySelector('.modal-overlay'));
2211
+ this.loadVersionsList();
2212
+ } else {
2213
+ // 生產模式實際編輯
2214
+ const requestData = {
2215
+ title: title,
2216
+ changelog: changelog
2217
+ };
2218
+
2219
+ // 使用統一的 authManager 獲取 Token
2220
+ if (!window.authManager || !window.authManager.supabase) {
2221
+ throw new Error('認證系統未初始化');
2222
+ }
2223
+
2224
+ const { data: { session }, error: sessionError } = await window.authManager.supabase.auth.getSession();
2225
+ if (sessionError || !session || !session.access_token) {
2226
+ throw new Error('無法獲取認證令牌,請重新登入');
2227
+ }
2228
+
2229
+ const response = await fetch(`/api/admin/versions/${versionString}/edit?product_type=${productType}`, {
2230
+ method: 'PUT',
2231
+ headers: {
2232
+ 'Authorization': `Bearer ${session.access_token}`,
2233
+ 'Content-Type': 'application/json'
2234
+ },
2235
+ body: JSON.stringify(requestData)
2236
+ });
2237
+
2238
+ if (!response.ok) {
2239
+ const error = await response.json();
2240
+ throw new Error(error.detail || '儲存失敗');
2241
+ }
2242
+
2243
+ Utils.showSuccess('儲存成功', `版本 ${versionString} 的資訊已成功更新`);
2244
+ Components.closeModal(document.querySelector('.modal-overlay'));
2245
+ this.loadVersionsList();
2246
+ }
2247
+ } catch (error) {
2248
+ console.error('儲存失敗:', error);
2249
+ Utils.showError('儲存失敗', error.message);
2250
+ } finally {
2251
+ if (confirmBtn) {
2252
+ confirmBtn.disabled = false;
2253
+ confirmBtn.innerHTML = originalText;
2254
+ }
2255
+ }
2256
+ }
2257
+
2258
  // 工具方法
2259
  formatFileSize(bytes) {
2260
  if (bytes === 0) return '0 Bytes';