新增用戶管理編輯功能和版本發布表單資料保存
Browse files- 新增授權編輯功能支援修改用戶名稱和電子郵件
- 編輯按鈕位於授權詳情標題右側
- 編輯完成後自動返回授權詳情頁面
- 版本發布表單支援頁籤切換時保存資料
- 移除表單保存相關的控制台日誌輸出
- app/api/license.py +16 -3
- app/models/license.py +5 -0
- app/services/license_service.py +54 -11
- frontend/js/api.js +8 -1
- frontend/js/pages/users.js +125 -9
- frontend/versions.html +58 -1
app/api/license.py
CHANGED
|
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request, Depends
|
|
| 6 |
from fastapi.responses import JSONResponse
|
| 7 |
from typing import List, Dict, Any
|
| 8 |
from app.models.license import (
|
| 9 |
-
LicenseActivation, LicenseValidation, LicenseCreate, LicenseExtend,
|
| 10 |
ActivationResponse, ValidationResponse, APIResponse
|
| 11 |
)
|
| 12 |
from app.services.license_service import license_service
|
|
@@ -168,8 +168,8 @@ async def extend_license(license_id: str, extend_data: LicenseExtend):
|
|
| 168 |
"""延長授權天數 (管理界面使用)"""
|
| 169 |
try:
|
| 170 |
result = await license_service.extend_license(
|
| 171 |
-
license_id,
|
| 172 |
-
extend_data.extend_days,
|
| 173 |
extend_data.reason
|
| 174 |
)
|
| 175 |
return APIResponse(
|
|
@@ -177,5 +177,18 @@ async def extend_license(license_id: str, extend_data: LicenseExtend):
|
|
| 177 |
message=result["message"],
|
| 178 |
data=result.get("data")
|
| 179 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
except Exception as e:
|
| 181 |
raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
|
|
|
|
| 6 |
from fastapi.responses import JSONResponse
|
| 7 |
from typing import List, Dict, Any
|
| 8 |
from app.models.license import (
|
| 9 |
+
LicenseActivation, LicenseValidation, LicenseCreate, LicenseExtend, LicenseUpdate,
|
| 10 |
ActivationResponse, ValidationResponse, APIResponse
|
| 11 |
)
|
| 12 |
from app.services.license_service import license_service
|
|
|
|
| 168 |
"""延長授權天數 (管理界面使用)"""
|
| 169 |
try:
|
| 170 |
result = await license_service.extend_license(
|
| 171 |
+
license_id,
|
| 172 |
+
extend_data.extend_days,
|
| 173 |
extend_data.reason
|
| 174 |
)
|
| 175 |
return APIResponse(
|
|
|
|
| 177 |
message=result["message"],
|
| 178 |
data=result.get("data")
|
| 179 |
)
|
| 180 |
+
except Exception as e:
|
| 181 |
+
raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
|
| 182 |
+
|
| 183 |
+
@router.patch("/license/{license_id}")
|
| 184 |
+
async def update_license(license_id: str, update_data: LicenseUpdate):
|
| 185 |
+
"""更新授權資訊 (管理界面使用)"""
|
| 186 |
+
try:
|
| 187 |
+
result = await license_service.update_license(license_id, update_data)
|
| 188 |
+
return APIResponse(
|
| 189 |
+
success=result["success"],
|
| 190 |
+
message=result["message"],
|
| 191 |
+
data=result.get("data")
|
| 192 |
+
)
|
| 193 |
except Exception as e:
|
| 194 |
raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
|
app/models/license.py
CHANGED
|
@@ -33,6 +33,11 @@ class LicenseExtend(BaseModel):
|
|
| 33 |
extend_days: int
|
| 34 |
reason: Optional[str] = None
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
# === API 回應模型 ===
|
| 37 |
|
| 38 |
class APIResponse(BaseModel):
|
|
|
|
| 33 |
extend_days: int
|
| 34 |
reason: Optional[str] = None
|
| 35 |
|
| 36 |
+
class LicenseUpdate(BaseModel):
|
| 37 |
+
"""更新授權請求"""
|
| 38 |
+
user_name: Optional[str] = None
|
| 39 |
+
user_email: Optional[EmailStr] = None
|
| 40 |
+
|
| 41 |
# === API 回應模型 ===
|
| 42 |
|
| 43 |
class APIResponse(BaseModel):
|
app/services/license_service.py
CHANGED
|
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta, timezone
|
|
| 9 |
from typing import Optional, List, Dict, Any
|
| 10 |
from app.models.supabase_clients import supabase_clients
|
| 11 |
from app.models.license import (
|
| 12 |
-
LicenseActivation, LicenseValidation, LicenseCreate,
|
| 13 |
ActivationResponse, ValidationResponse, License, UsageLog, LicenseStats
|
| 14 |
)
|
| 15 |
|
|
@@ -422,19 +422,19 @@ class LicenseService:
|
|
| 422 |
if not self.supabase:
|
| 423 |
print("❌ extend_license: Supabase client not available")
|
| 424 |
return {"success": False, "message": "資料庫連接失敗"}
|
| 425 |
-
|
| 426 |
if extend_days <= 0:
|
| 427 |
return {"success": False, "message": "延長天數必須大於 0"}
|
| 428 |
-
|
| 429 |
# 取得目前授權資料
|
| 430 |
result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
|
| 431 |
-
|
| 432 |
if not result.data:
|
| 433 |
return {"success": False, "message": "授權不存在"}
|
| 434 |
-
|
| 435 |
license_data = result.data[0]
|
| 436 |
current_expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
|
| 437 |
-
|
| 438 |
# 計算新的到期時間:從原到期日開始加天數(-8hr對應台灣時間)
|
| 439 |
if current_expires_at < datetime.now(timezone.utc):
|
| 440 |
# 如果已過期,從今天開始計算
|
|
@@ -447,20 +447,20 @@ class LicenseService:
|
|
| 447 |
next_day = expire_date + timedelta(days=1)
|
| 448 |
start_date = datetime.combine(next_day, datetime.min.time(), timezone.utc) - timedelta(hours=8)
|
| 449 |
new_expires_at = start_date + timedelta(days=extend_days) - timedelta(seconds=1)
|
| 450 |
-
|
| 451 |
# 更新授權
|
| 452 |
update_data = {
|
| 453 |
"expires_at": new_expires_at.isoformat(),
|
| 454 |
"is_active": True # 延長時自動啟用授權
|
| 455 |
}
|
| 456 |
-
|
| 457 |
update_result = self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
|
| 458 |
-
|
| 459 |
if update_result.data:
|
| 460 |
# 記錄延長操作
|
| 461 |
extend_reason = reason or f"延長 {extend_days} 天"
|
| 462 |
await self._log_usage(license_id, "extend", None, None, extend_reason)
|
| 463 |
-
|
| 464 |
return {
|
| 465 |
"success": True,
|
| 466 |
"message": f"授權已延長 {extend_days} 天",
|
|
@@ -472,10 +472,53 @@ class LicenseService:
|
|
| 472 |
}
|
| 473 |
else:
|
| 474 |
return {"success": False, "message": "延長失敗"}
|
| 475 |
-
|
| 476 |
except Exception as e:
|
| 477 |
print(f"❌ 延長授權錯誤: {e}")
|
| 478 |
return {"success": False, "message": f"延長失敗: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
|
| 481 |
hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
|
|
|
|
| 9 |
from typing import Optional, List, Dict, Any
|
| 10 |
from app.models.supabase_clients import supabase_clients
|
| 11 |
from app.models.license import (
|
| 12 |
+
LicenseActivation, LicenseValidation, LicenseCreate, LicenseUpdate,
|
| 13 |
ActivationResponse, ValidationResponse, License, UsageLog, LicenseStats
|
| 14 |
)
|
| 15 |
|
|
|
|
| 422 |
if not self.supabase:
|
| 423 |
print("❌ extend_license: Supabase client not available")
|
| 424 |
return {"success": False, "message": "資料庫連接失敗"}
|
| 425 |
+
|
| 426 |
if extend_days <= 0:
|
| 427 |
return {"success": False, "message": "延長天數必須大於 0"}
|
| 428 |
+
|
| 429 |
# 取得目前授權資料
|
| 430 |
result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
|
| 431 |
+
|
| 432 |
if not result.data:
|
| 433 |
return {"success": False, "message": "授權不存在"}
|
| 434 |
+
|
| 435 |
license_data = result.data[0]
|
| 436 |
current_expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
|
| 437 |
+
|
| 438 |
# 計算新的到期時間:從原到期日開始加天數(-8hr對應台灣時間)
|
| 439 |
if current_expires_at < datetime.now(timezone.utc):
|
| 440 |
# 如果已過期,從今天開始計算
|
|
|
|
| 447 |
next_day = expire_date + timedelta(days=1)
|
| 448 |
start_date = datetime.combine(next_day, datetime.min.time(), timezone.utc) - timedelta(hours=8)
|
| 449 |
new_expires_at = start_date + timedelta(days=extend_days) - timedelta(seconds=1)
|
| 450 |
+
|
| 451 |
# 更新授權
|
| 452 |
update_data = {
|
| 453 |
"expires_at": new_expires_at.isoformat(),
|
| 454 |
"is_active": True # 延長時自動啟用授權
|
| 455 |
}
|
| 456 |
+
|
| 457 |
update_result = self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
|
| 458 |
+
|
| 459 |
if update_result.data:
|
| 460 |
# 記錄延長操作
|
| 461 |
extend_reason = reason or f"延長 {extend_days} 天"
|
| 462 |
await self._log_usage(license_id, "extend", None, None, extend_reason)
|
| 463 |
+
|
| 464 |
return {
|
| 465 |
"success": True,
|
| 466 |
"message": f"授權已延長 {extend_days} 天",
|
|
|
|
| 472 |
}
|
| 473 |
else:
|
| 474 |
return {"success": False, "message": "延長失敗"}
|
| 475 |
+
|
| 476 |
except Exception as e:
|
| 477 |
print(f"❌ 延長授權錯誤: {e}")
|
| 478 |
return {"success": False, "message": f"延長失敗: {str(e)}"}
|
| 479 |
+
|
| 480 |
+
async def update_license(self, license_id: str, update_data: LicenseUpdate) -> Dict[str, Any]:
|
| 481 |
+
"""更新授權資訊"""
|
| 482 |
+
try:
|
| 483 |
+
if not self.supabase:
|
| 484 |
+
print("❌ update_license: Supabase client not available")
|
| 485 |
+
return {"success": False, "message": "資料庫連接失敗"}
|
| 486 |
+
|
| 487 |
+
# 檢查授權是否存在
|
| 488 |
+
result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
|
| 489 |
+
|
| 490 |
+
if not result.data:
|
| 491 |
+
return {"success": False, "message": "授權不存在"}
|
| 492 |
+
|
| 493 |
+
# 準備更新資料(只更新有提供的欄位)
|
| 494 |
+
updates = {}
|
| 495 |
+
if update_data.user_name is not None:
|
| 496 |
+
updates["user_name"] = update_data.user_name
|
| 497 |
+
if update_data.user_email is not None:
|
| 498 |
+
updates["user_email"] = update_data.user_email
|
| 499 |
+
|
| 500 |
+
if not updates:
|
| 501 |
+
return {"success": False, "message": "沒有要更新的資料"}
|
| 502 |
+
|
| 503 |
+
# 執行更新
|
| 504 |
+
update_result = self.supabase.table("licenses").update(updates).eq("id", license_id).execute()
|
| 505 |
+
|
| 506 |
+
if update_result.data:
|
| 507 |
+
# 記錄更新操作
|
| 508 |
+
update_fields = ", ".join(updates.keys())
|
| 509 |
+
await self._log_usage(license_id, "update", None, None, f"更新授權資訊: {update_fields}")
|
| 510 |
+
|
| 511 |
+
return {
|
| 512 |
+
"success": True,
|
| 513 |
+
"message": "授權資訊更新成功",
|
| 514 |
+
"data": update_result.data[0]
|
| 515 |
+
}
|
| 516 |
+
else:
|
| 517 |
+
return {"success": False, "message": "更新失敗"}
|
| 518 |
+
|
| 519 |
+
except Exception as e:
|
| 520 |
+
print(f"❌ 更新授權錯誤: {e}")
|
| 521 |
+
return {"success": False, "message": f"更新失敗: {str(e)}"}
|
| 522 |
|
| 523 |
async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
|
| 524 |
hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
|
frontend/js/api.js
CHANGED
|
@@ -311,7 +311,14 @@ class ApiClient {
|
|
| 311 |
body: extendData
|
| 312 |
});
|
| 313 |
}
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
// Stats endpoints
|
| 316 |
async getLicenseStats() {
|
| 317 |
if (this.isDevMode) {
|
|
|
|
| 311 |
body: extendData
|
| 312 |
});
|
| 313 |
}
|
| 314 |
+
|
| 315 |
+
async updateLicense(licenseId, updateData) {
|
| 316 |
+
return this.request(`/api/license/${licenseId}`, {
|
| 317 |
+
method: 'PATCH',
|
| 318 |
+
body: updateData
|
| 319 |
+
});
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
// Stats endpoints
|
| 323 |
async getLicenseStats() {
|
| 324 |
if (this.isDevMode) {
|
frontend/js/pages/users.js
CHANGED
|
@@ -584,7 +584,14 @@ class UsersPage {
|
|
| 584 |
const status = Utils.getLicenseStatus(license);
|
| 585 |
|
| 586 |
const modal = Components.createModal({
|
| 587 |
-
title:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
size: 'lg',
|
| 589 |
content: `
|
| 590 |
<div class="license-details-modal">
|
|
@@ -654,7 +661,7 @@ class UsersPage {
|
|
| 654 |
<i class="fas fa-calendar-plus"></i>
|
| 655 |
延長授權
|
| 656 |
</button>
|
| 657 |
-
<button type="button" class="btn ${license.is_active ? 'btn-warning' : 'btn-success'}"
|
| 658 |
onclick="usersPage.toggleLicenseStatus('${license.id}', ${license.is_active}); Components.closeModal(this.closest('.modal-overlay'));">
|
| 659 |
<i class="fas ${license.is_active ? 'fa-pause' : 'fa-play'}"></i>
|
| 660 |
${license.is_active ? '停用' : '啟用'}
|
|
@@ -740,24 +747,24 @@ class UsersPage {
|
|
| 740 |
const formData = new FormData(form);
|
| 741 |
const extendDays = parseInt(formData.get('extendDays'));
|
| 742 |
const reason = formData.get('extendReason')?.trim();
|
| 743 |
-
|
| 744 |
if (!extendDays) {
|
| 745 |
Utils.showError('請選擇延長天數');
|
| 746 |
return;
|
| 747 |
}
|
| 748 |
-
|
| 749 |
const submitBtn = form.querySelector('button[type="submit"]');
|
| 750 |
const originalContent = submitBtn.innerHTML;
|
| 751 |
-
|
| 752 |
try {
|
| 753 |
submitBtn.disabled = true;
|
| 754 |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 延長中...';
|
| 755 |
-
|
| 756 |
const response = await api.extendLicense(licenseId, {
|
| 757 |
extend_days: extendDays,
|
| 758 |
reason: reason
|
| 759 |
});
|
| 760 |
-
|
| 761 |
if (response && response.success) {
|
| 762 |
Utils.showSuccess('授權延長成功');
|
| 763 |
Components.closeModal(modal);
|
|
@@ -765,7 +772,7 @@ class UsersPage {
|
|
| 765 |
} else {
|
| 766 |
throw new Error(response?.message || '延長授權失敗');
|
| 767 |
}
|
| 768 |
-
|
| 769 |
} catch (error) {
|
| 770 |
console.error('❌ Extend license error:', error);
|
| 771 |
Utils.handleError(error, '延長授權時');
|
|
@@ -773,7 +780,116 @@ class UsersPage {
|
|
| 773 |
submitBtn.innerHTML = originalContent;
|
| 774 |
}
|
| 775 |
}
|
| 776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
destroy() {
|
| 778 |
// Cleanup if needed
|
| 779 |
}
|
|
|
|
| 584 |
const status = Utils.getLicenseStatus(license);
|
| 585 |
|
| 586 |
const modal = Components.createModal({
|
| 587 |
+
title: `
|
| 588 |
+
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 12px;">
|
| 589 |
+
<span>授權詳情</span>
|
| 590 |
+
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); Components.closeModal(this.closest('.modal-overlay')); usersPage.showEditLicenseModal('${license.id}')" style="width: 24px; height: 24px; padding: 0; display: flex; align-items: center; justify-content: center;" title="編輯">
|
| 591 |
+
<i class="fas fa-edit" style="font-size: 12px;"></i>
|
| 592 |
+
</button>
|
| 593 |
+
</div>
|
| 594 |
+
`,
|
| 595 |
size: 'lg',
|
| 596 |
content: `
|
| 597 |
<div class="license-details-modal">
|
|
|
|
| 661 |
<i class="fas fa-calendar-plus"></i>
|
| 662 |
延長授權
|
| 663 |
</button>
|
| 664 |
+
<button type="button" class="btn ${license.is_active ? 'btn-warning' : 'btn-success'}"
|
| 665 |
onclick="usersPage.toggleLicenseStatus('${license.id}', ${license.is_active}); Components.closeModal(this.closest('.modal-overlay'));">
|
| 666 |
<i class="fas ${license.is_active ? 'fa-pause' : 'fa-play'}"></i>
|
| 667 |
${license.is_active ? '停用' : '啟用'}
|
|
|
|
| 747 |
const formData = new FormData(form);
|
| 748 |
const extendDays = parseInt(formData.get('extendDays'));
|
| 749 |
const reason = formData.get('extendReason')?.trim();
|
| 750 |
+
|
| 751 |
if (!extendDays) {
|
| 752 |
Utils.showError('請選擇延長天數');
|
| 753 |
return;
|
| 754 |
}
|
| 755 |
+
|
| 756 |
const submitBtn = form.querySelector('button[type="submit"]');
|
| 757 |
const originalContent = submitBtn.innerHTML;
|
| 758 |
+
|
| 759 |
try {
|
| 760 |
submitBtn.disabled = true;
|
| 761 |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 延長中...';
|
| 762 |
+
|
| 763 |
const response = await api.extendLicense(licenseId, {
|
| 764 |
extend_days: extendDays,
|
| 765 |
reason: reason
|
| 766 |
});
|
| 767 |
+
|
| 768 |
if (response && response.success) {
|
| 769 |
Utils.showSuccess('授權延長成功');
|
| 770 |
Components.closeModal(modal);
|
|
|
|
| 772 |
} else {
|
| 773 |
throw new Error(response?.message || '延長授權失敗');
|
| 774 |
}
|
| 775 |
+
|
| 776 |
} catch (error) {
|
| 777 |
console.error('❌ Extend license error:', error);
|
| 778 |
Utils.handleError(error, '延長授權時');
|
|
|
|
| 780 |
submitBtn.innerHTML = originalContent;
|
| 781 |
}
|
| 782 |
}
|
| 783 |
+
|
| 784 |
+
showEditLicenseModal(licenseId) {
|
| 785 |
+
const license = this.licenses.find(l => l.id === licenseId);
|
| 786 |
+
if (!license) return;
|
| 787 |
+
|
| 788 |
+
const modal = Components.createModal({
|
| 789 |
+
title: '編輯授權資訊',
|
| 790 |
+
size: 'md',
|
| 791 |
+
content: `
|
| 792 |
+
<form id="editLicenseForm">
|
| 793 |
+
<div class="form-group">
|
| 794 |
+
<label for="editUserName" class="form-label">
|
| 795 |
+
<i class="fas fa-user"></i>
|
| 796 |
+
用戶名稱 *
|
| 797 |
+
</label>
|
| 798 |
+
<input type="text"
|
| 799 |
+
id="editUserName"
|
| 800 |
+
name="user_name"
|
| 801 |
+
class="form-input"
|
| 802 |
+
value="${license.user_name || ''}"
|
| 803 |
+
required>
|
| 804 |
+
</div>
|
| 805 |
+
|
| 806 |
+
<div class="form-group">
|
| 807 |
+
<label for="editUserEmail" class="form-label">
|
| 808 |
+
<i class="fas fa-envelope"></i>
|
| 809 |
+
電子郵件
|
| 810 |
+
</label>
|
| 811 |
+
<input type="email"
|
| 812 |
+
id="editUserEmail"
|
| 813 |
+
name="user_email"
|
| 814 |
+
class="form-input"
|
| 815 |
+
value="${license.user_email || ''}"
|
| 816 |
+
placeholder="user@example.com">
|
| 817 |
+
</div>
|
| 818 |
+
|
| 819 |
+
<div class="form-group">
|
| 820 |
+
<div class="form-info">
|
| 821 |
+
<i class="fas fa-info-circle"></i>
|
| 822 |
+
<span>只能修改用戶名稱和電子郵件,其他資訊不可變更</span>
|
| 823 |
+
</div>
|
| 824 |
+
</div>
|
| 825 |
+
|
| 826 |
+
<div class="modal-actions">
|
| 827 |
+
<button type="button" class="btn btn-secondary" onclick="Components.closeModal(this.closest('.modal-overlay')); usersPage.showLicenseDetails('${licenseId}')">
|
| 828 |
+
取消
|
| 829 |
+
</button>
|
| 830 |
+
<button type="submit" class="btn btn-primary">
|
| 831 |
+
<i class="fas fa-save"></i>
|
| 832 |
+
儲存變更
|
| 833 |
+
</button>
|
| 834 |
+
</div>
|
| 835 |
+
</form>
|
| 836 |
+
`
|
| 837 |
+
});
|
| 838 |
+
|
| 839 |
+
// Setup form handler
|
| 840 |
+
const form = modal.querySelector('#editLicenseForm');
|
| 841 |
+
form.addEventListener('submit', async (e) => {
|
| 842 |
+
e.preventDefault();
|
| 843 |
+
await this.handleEditLicense(licenseId, form, modal);
|
| 844 |
+
});
|
| 845 |
+
|
| 846 |
+
Components.showModal(modal);
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
async handleEditLicense(licenseId, form, modal) {
|
| 850 |
+
const formData = new FormData(form);
|
| 851 |
+
const userName = formData.get('user_name')?.trim();
|
| 852 |
+
const userEmail = formData.get('user_email')?.trim();
|
| 853 |
+
|
| 854 |
+
if (!userName) {
|
| 855 |
+
Utils.showError('用戶名稱為必填項');
|
| 856 |
+
return;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
const submitBtn = form.querySelector('button[type="submit"]');
|
| 860 |
+
const originalContent = submitBtn.innerHTML;
|
| 861 |
+
|
| 862 |
+
try {
|
| 863 |
+
submitBtn.disabled = true;
|
| 864 |
+
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 更新中...';
|
| 865 |
+
|
| 866 |
+
const updateData = {
|
| 867 |
+
user_name: userName
|
| 868 |
+
};
|
| 869 |
+
|
| 870 |
+
if (userEmail) {
|
| 871 |
+
updateData.user_email = userEmail;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
const response = await api.updateLicense(licenseId, updateData);
|
| 875 |
+
|
| 876 |
+
if (response && response.success) {
|
| 877 |
+
Utils.showSuccess('授權資訊更新成功');
|
| 878 |
+
Components.closeModal(modal);
|
| 879 |
+
await this.refresh();
|
| 880 |
+
// 更新完成後重新開啟詳情頁面
|
| 881 |
+
this.showLicenseDetails(licenseId);
|
| 882 |
+
} else {
|
| 883 |
+
throw new Error(response?.message || '更新授權失敗');
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
} catch (error) {
|
| 887 |
+
Utils.handleError(error, '更新授權時');
|
| 888 |
+
submitBtn.disabled = false;
|
| 889 |
+
submitBtn.innerHTML = originalContent;
|
| 890 |
+
}
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
destroy() {
|
| 894 |
// Cleanup if needed
|
| 895 |
}
|
frontend/versions.html
CHANGED
|
@@ -1335,7 +1335,7 @@
|
|
| 1335 |
<i class="fas fa-rocket"></i>
|
| 1336 |
發布版本
|
| 1337 |
</button>
|
| 1338 |
-
<button type="reset" class="btn btn-secondary">
|
| 1339 |
<i class="fas fa-undo"></i>
|
| 1340 |
重置表單
|
| 1341 |
</button>
|
|
@@ -1358,6 +1358,20 @@
|
|
| 1358 |
|
| 1359 |
if (!form || !dropZone || !fileInput) return;
|
| 1360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
// 檔案上傳事件
|
| 1362 |
dropZone.addEventListener('click', () => fileInput.click());
|
| 1363 |
|
|
@@ -1522,6 +1536,7 @@
|
|
| 1522 |
Utils.showSuccess('發布成功', `${productType === 'autocad' ? 'AutoCAD' : 'Revit'} 版本 ${version} 已成功發布`);
|
| 1523 |
e.target.reset();
|
| 1524 |
this.clearFileSelection();
|
|
|
|
| 1525 |
this.switchPage('versions');
|
| 1526 |
}
|
| 1527 |
} catch (error) {
|
|
@@ -1533,6 +1548,48 @@
|
|
| 1533 |
}
|
| 1534 |
}
|
| 1535 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1536 |
async loadDownloadsPage(container) {
|
| 1537 |
container.innerHTML = `
|
| 1538 |
<div class="page-section">
|
|
|
|
| 1335 |
<i class="fas fa-rocket"></i>
|
| 1336 |
發布版本
|
| 1337 |
</button>
|
| 1338 |
+
<button type="reset" class="btn btn-secondary" onclick="versionApp.clearReleaseFormData()">
|
| 1339 |
<i class="fas fa-undo"></i>
|
| 1340 |
重置表單
|
| 1341 |
</button>
|
|
|
|
| 1358 |
|
| 1359 |
if (!form || !dropZone || !fileInput) return;
|
| 1360 |
|
| 1361 |
+
// 恢復暫存的表單資料
|
| 1362 |
+
this.restoreReleaseFormData();
|
| 1363 |
+
|
| 1364 |
+
// 監聽表單輸入變化,自動暫存
|
| 1365 |
+
const formInputs = form.querySelectorAll('input, textarea, select');
|
| 1366 |
+
formInputs.forEach(input => {
|
| 1367 |
+
input.addEventListener('input', () => {
|
| 1368 |
+
this.saveReleaseFormData();
|
| 1369 |
+
});
|
| 1370 |
+
input.addEventListener('change', () => {
|
| 1371 |
+
this.saveReleaseFormData();
|
| 1372 |
+
});
|
| 1373 |
+
});
|
| 1374 |
+
|
| 1375 |
// 檔案上傳事件
|
| 1376 |
dropZone.addEventListener('click', () => fileInput.click());
|
| 1377 |
|
|
|
|
| 1536 |
Utils.showSuccess('發布成功', `${productType === 'autocad' ? 'AutoCAD' : 'Revit'} 版本 ${version} 已成功發布`);
|
| 1537 |
e.target.reset();
|
| 1538 |
this.clearFileSelection();
|
| 1539 |
+
this.clearReleaseFormData(); // 清除暫存資料
|
| 1540 |
this.switchPage('versions');
|
| 1541 |
}
|
| 1542 |
} catch (error) {
|
|
|
|
| 1548 |
}
|
| 1549 |
}
|
| 1550 |
|
| 1551 |
+
// 保存發布表單資料到 sessionStorage
|
| 1552 |
+
saveReleaseFormData() {
|
| 1553 |
+
const form = document.getElementById('releaseForm');
|
| 1554 |
+
if (!form) return;
|
| 1555 |
+
|
| 1556 |
+
const formData = {
|
| 1557 |
+
productType: document.getElementById('productType')?.value || '',
|
| 1558 |
+
version: document.getElementById('version')?.value || '',
|
| 1559 |
+
title: document.getElementById('title')?.value || '',
|
| 1560 |
+
changelog: document.getElementById('changelog')?.value || ''
|
| 1561 |
+
};
|
| 1562 |
+
|
| 1563 |
+
sessionStorage.setItem('releaseFormData', JSON.stringify(formData));
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
// 從 sessionStorage 恢復發布表單資料
|
| 1567 |
+
restoreReleaseFormData() {
|
| 1568 |
+
const savedData = sessionStorage.getItem('releaseFormData');
|
| 1569 |
+
if (!savedData) return;
|
| 1570 |
+
|
| 1571 |
+
try {
|
| 1572 |
+
const formData = JSON.parse(savedData);
|
| 1573 |
+
|
| 1574 |
+
const productTypeEl = document.getElementById('productType');
|
| 1575 |
+
const versionEl = document.getElementById('version');
|
| 1576 |
+
const titleEl = document.getElementById('title');
|
| 1577 |
+
const changelogEl = document.getElementById('changelog');
|
| 1578 |
+
|
| 1579 |
+
if (productTypeEl && formData.productType) productTypeEl.value = formData.productType;
|
| 1580 |
+
if (versionEl && formData.version) versionEl.value = formData.version;
|
| 1581 |
+
if (titleEl && formData.title) titleEl.value = formData.title;
|
| 1582 |
+
if (changelogEl && formData.changelog) changelogEl.value = formData.changelog;
|
| 1583 |
+
} catch (error) {
|
| 1584 |
+
// 靜默失敗,不影響使用者體驗
|
| 1585 |
+
}
|
| 1586 |
+
}
|
| 1587 |
+
|
| 1588 |
+
// 清除暫存的發布表單資料
|
| 1589 |
+
clearReleaseFormData() {
|
| 1590 |
+
sessionStorage.removeItem('releaseFormData');
|
| 1591 |
+
}
|
| 1592 |
+
|
| 1593 |
async loadDownloadsPage(container) {
|
| 1594 |
container.innerHTML = `
|
| 1595 |
<div class="page-section">
|