新增版本編輯功能
Browse files前端:
- 新增編輯按鈕至基本資訊區塊右側
- 實作編輯版本對話框顯示版本標題和更新日誌表單
- 支援開發模式模擬測試
- 包含表單驗證和錯誤處理
後端:
- 新增 PUT /api/admin/versions/{version}/edit 端點
- 支援編輯版本標題和更新日誌
- 支援 Revit 和 AutoCAD 產品類型
- 包含輸入驗證和錯誤處理
- 同步更新 Supabase 資料庫
- app/api/version_routes.py +72 -0
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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';
|