feat: add Community Edition restore UI (#13431)

This commit is contained in:
ssongliu
2026-07-31 18:15:24 +08:00
committed by GitHub
parent 7bd11fe73e
commit 9f9e3aacfc
18 changed files with 480 additions and 9 deletions

View File

@@ -28,10 +28,11 @@ import (
)
const (
rollbackPath = "1panel/tmp"
upgradePath = "1panel/tmp/upgrade"
uploadPath = "1panel/uploads"
downloadPath = "1panel/download"
rollbackPath = "1panel/tmp"
communityRestorePath = "1panel/tmp/community-restore"
upgradePath = "1panel/tmp/upgrade"
uploadPath = "1panel/uploads"
downloadPath = "1panel/download"
)
func (u *DeviceService) Scan() dto.CleanData {
@@ -113,12 +114,15 @@ func (u *DeviceService) Clean(req []dto.Clean) {
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
case "rollback_app":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
case "rollback_database":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
case "rollback_website":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
case "rollback_community_restore":
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))
case "upload":
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
@@ -214,6 +218,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
oldUpgradeFiles, _ := os.ReadDir(upgrades)
@@ -611,6 +616,7 @@ func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore", fileOp)
return treeData
}

View File

@@ -307,6 +307,13 @@ export namespace Setting {
status: string;
message: string;
}
export interface CommunityRestoreStatus {
state: 'Ready' | 'Running' | 'Failed';
message: string;
packageExist: boolean;
packageDirectory: string;
}
export type CommunityRestoreMode = 'online' | 'offline';
export interface NodeItem {
id: number;
groupID?: number;

View File

@@ -65,6 +65,12 @@ export const getEnterpriseLicense = () => {
export const getEnterpriseLicenseStatus = () => {
return http.get<Setting.LicenseStatus>(`/core/enterprise/licenses/status`);
};
export const getCommunityRestoreStatus = () => {
return http.get<Setting.CommunityRestoreStatus>(`/core/enterprise/licenses/community-restore/status`);
};
export const restoreCommunity = (mode: Setting.CommunityRestoreMode) => {
return http.post(`/core/enterprise/licenses/community-restore`, { mode });
};
// agent
export const loadBaseDir = (node?: string) => {

View File

@@ -3190,6 +3190,20 @@ const message = {
'Please copy the device ID first to obtain a license. You can log in after importing the license.',
licenseRequiredUserTip:
'The Enterprise Edition license is not bound. Contact a super administrator to import the license.',
restoreCommunity: 'Restore Community Edition',
restoreCommunityOnline: 'Online restore',
restoreCommunityOffline: 'Offline restore',
restoreCommunityPackageFound:
'A usable offline Community Edition package was detected. Offline restore is available.',
restoreCommunityPackageMissing:
'No usable offline Community Edition package was detected. Offline restore is currently unavailable.',
restoreCommunityOnlineHelper:
'The Community Edition package for the current version will be downloaded automatically, and Enterprise-only data will be cleaned up.',
restoreCommunityOfflineHelper:
'Place an official package no older than the current Enterprise version in {path}. Switching is enabled after a usable package is detected.',
restoreCommunityConfirm:
'After switching, you can sign in to Community Edition with the current Enterprise super administrator account. Enterprise-only databases will be deleted during the switch.',
restoreCommunityStarting: 'Preparing Community Edition. The panel will restart automatically when ready.',
levelUpPro: 'Upgrade to Commercial Edition',
licenseSync: 'License Sync',
knowMorePro: 'Learn More',

View File

@@ -3254,6 +3254,21 @@ const message = {
'Primero copie el ID del dispositivo para obtener una licencia. Podrá iniciar sesión después de importarla.',
licenseRequiredUserTip:
'La licencia Enterprise Edition no está vinculada. Contacte con un superadministrador para importar la licencia.',
restoreCommunity: 'Restaurar Community Edition',
restoreCommunityOnline: 'Restauración en línea',
restoreCommunityOffline: 'Restauración sin conexión',
restoreCommunityPackageFound:
'Se detectó un paquete sin conexión de Community Edition válido. La restauración sin conexión está disponible.',
restoreCommunityPackageMissing:
'No se detectó ningún paquete sin conexión de Community Edition válido. La restauración sin conexión no está disponible actualmente.',
restoreCommunityOnlineHelper:
'Se descargará automáticamente el paquete Community Edition de la versión actual y se limpiarán los datos exclusivos de Enterprise.',
restoreCommunityOfflineHelper:
'Coloque en {path} un paquete oficial cuya versión no sea anterior a la versión Enterprise actual. El cambio se habilitará al detectar un paquete utilizable.',
restoreCommunityConfirm:
'Después del cambio, podrá iniciar sesión en Community Edition con la cuenta actual de superadministrador de Enterprise. Las bases de datos exclusivas de Enterprise se eliminarán durante el cambio.',
restoreCommunityStarting:
'Preparando Community Edition. El panel se reiniciará automáticamente cuando termine.',
levelUpPro: 'Actualizar a la edición comercial',
licenseSync: 'Sincronizar licencia',
knowMorePro: 'Ver más',

View File

@@ -3162,6 +3162,21 @@ const message = {
licenseRequiredShortTip:
'لطفاً ابتدا شناسه دستگاه را کپی کنید تا مجوز دریافت کنید. پس از وارد کردن مجوز می‌توانید وارد شوید.',
licenseRequiredUserTip: 'مجوز نسخه سازمانی متصل نیست. برای وارد کردن مجوز با ابرمدیر تماس بگیرید.',
restoreCommunity: 'بازیابی نسخه Community',
restoreCommunityOnline: 'بازیابی آنلاین',
restoreCommunityOffline: 'بازیابی آفلاین',
restoreCommunityPackageFound:
'یک بسته آفلاین قابل استفاده Community Edition شناسایی شد. بازیابی آفلاین در دسترس است.',
restoreCommunityPackageMissing:
'هیچ بسته آفلاین قابل استفاده Community Edition شناسایی نشد. بازیابی آفلاین در حال حاضر در دسترس نیست.',
restoreCommunityOnlineHelper:
'بسته نسخه Community برای نسخه فعلی به‌طور خودکار دانلود و داده‌های ویژه Enterprise پاک‌سازی می‌شوند.',
restoreCommunityOfflineHelper:
'یک بسته رسمی با نسخه‌ای قدیمی‌تر نبودن از نسخه فعلی Enterprise را در {path} قرار دهید. پس از شناسایی بسته قابل استفاده، تغییر نسخه فعال می‌شود.',
restoreCommunityConfirm:
'پس از تغییر، می‌توانید با حساب ابرمدیر فعلی Enterprise وارد نسخه Community شوید. پایگاه‌های داده ویژه Enterprise در طول تغییر حذف می‌شوند.',
restoreCommunityStarting:
'نسخه Community در حال آماده‌سازی است. پس از تکمیل، پنل به‌طور خودکار راه‌اندازی مجدد می‌شود.',
levelUpPro: 'ارتقاء به نسخه تجاری',
licenseSync: 'همگام‌سازی مجوز',
knowMorePro: 'بیشتر بدانید',

View File

@@ -3196,6 +3196,20 @@ const message = {
'まずデバイス ID をコピーしてライセンスを取得しライセンスをインポートするとログインできます',
licenseRequiredUserTip:
'Enterprise Edition のライセンスがバインドされていませんスーパー管理者に連絡してライセンスをインポートしてください',
restoreCommunity: 'コミュニティ版に戻す',
restoreCommunityOnline: 'オンライン復元',
restoreCommunityOffline: 'オフライン復元',
restoreCommunityPackageFound:
'使用可能なコミュニティ版オフラインパッケージが検出されましたオフライン復元を実行できます',
restoreCommunityPackageMissing:
'使用可能なコミュニティ版オフラインパッケージが検出されていないため現在オフライン復元を実行できません',
restoreCommunityOnlineHelper:
'現在のバージョンのコミュニティ版パッケージを自動的にダウンロードしEnterprise 専用データを削除します',
restoreCommunityOfflineHelper:
'現在の Enterprise バージョン以上の公式パッケージを {path} に配置してください使用可能なパッケージが検出されると切り替えが有効になります',
restoreCommunityConfirm:
'切り替え後は現在の Enterprise スーパー管理者アカウントでコミュニティ版にログインできます切り替え中に Enterprise 専用データベースが削除されます',
restoreCommunityStarting: 'コミュニティ版を準備しています完了後パネルは自動的に再起動します',
technicalAdvice: '技術的な相談',
advice: '相談',
levelUpPro: '商用版にアップグレード',

View File

@@ -3127,6 +3127,20 @@ const message = {
licenseRequiredShortTip: '먼저 장치 ID를 복사해 라이선스를 받은 뒤, 라이선스를 가져오면 로그인할 수 있습니다.',
licenseRequiredUserTip:
'Enterprise Edition 라이선스가 바인딩되지 않았습니다. 슈퍼 관리자에게 라이선스 가져오기를 요청하세요.',
restoreCommunity: '커뮤니티 버전 복원',
restoreCommunityOnline: '온라인 복원',
restoreCommunityOffline: '오프라인 복원',
restoreCommunityPackageFound:
'사용 가능한 Community Edition 오프라인 설치 패키지가 감지되었습니다. 오프라인 복원을 실행할 수 있습니다.',
restoreCommunityPackageMissing:
'사용 가능한 Community Edition 오프라인 설치 패키지가 감지되지 않아 현재 오프라인 복원을 실행할 수 없습니다.',
restoreCommunityOnlineHelper:
'현재 버전의 커뮤니티 버전 패키지를 자동으로 다운로드하고 Enterprise 전용 데이터를 정리합니다.',
restoreCommunityOfflineHelper:
'현재 Enterprise 버전 이상인 공식 패키지를 {path}에 저장하세요. 사용 가능한 패키지가 감지되면 전환이 활성화됩니다.',
restoreCommunityConfirm:
'전환 후 현재 Enterprise 슈퍼 관리자 계정으로 커뮤니티 버전에 로그인할 수 있습니다. 전환 과정에서 Enterprise 전용 데이터베이스가 삭제됩니다.',
restoreCommunityStarting: '커뮤니티 버전을 준비 중입니다. 완료되면 패널이 자동으로 다시 시작됩니다.',
technicalAdvice: '기술 상담',
advice: '상담',
levelUpPro: '상용 버전으로 업그레이드',

View File

@@ -3116,6 +3116,20 @@ const message = {
licenseRequiredShortTip:
'ກະລຸນາຄັດລອກ ID ອຸປະກອນກ່ອນເພື່ອຮັບລິຂະສິດ. ທ່ານສາມາດເຂົ້າລະບົບໄດ້ຫຼັງຈາກນຳເຂົ້າລິຂະສິດແລ້ວ.',
licenseRequiredUserTip: 'ລິຂະສິດເວີຊັນ Enterprise ບໍ່ໄດ້ຖືກຜູກ. ຕິດຕໍ່ຜູ້ດູແລລະບົບສູງສຸດເພື່ອນຳເຂົ້າລິຂະສິດ.',
restoreCommunity: 'ກູ້ຄືນ Community Edition',
restoreCommunityOnline: 'ກູ້ຄືນອອນລາຍ',
restoreCommunityOffline: 'ກູ້ຄືນອອບລາຍ',
restoreCommunityPackageFound:
'ກວດພົບແພັກເກດ Community Edition ອອບລາຍທີ່ໃຊ້ໄດ້ ແລະສາມາດດຳເນີນການກູ້ຄືນອອບລາຍໄດ້.',
restoreCommunityPackageMissing:
'ບໍ່ພົບແພັກເກດ Community Edition ອອບລາຍທີ່ໃຊ້ໄດ້ ຈຶ່ງຍັງບໍ່ສາມາດກູ້ຄືນອອບລາຍໄດ້.',
restoreCommunityOnlineHelper:
'ລະບົບຈະດາວໂຫຼດແພັກເກດ Community Edition ສຳລັບເວີຊັນປັດຈຸບັນອັດຕະໂນມັດ ແລະ ລ້າງຂໍ້ມູນສະເພາະ Enterprise.',
restoreCommunityOfflineHelper:
'ວາງແພັກເກດທາງການທີ່ມີເວີຊັນບໍ່ຕ່ຳກວ່າ Enterprise ປັດຈຸບັນໄວ້ໃນ {path}. ການສະຫຼັບຈະເປີດໃຊ້ເມື່ອກວດພົບແພັກເກດທີ່ໃຊ້ໄດ້.',
restoreCommunityConfirm:
'ຫຼັງຈາກສະຫຼັບ ສາມາດໃຊ້ບັນຊີຜູ້ດູແລສູງສຸດ Enterprise ປັດຈຸບັນເຂົ້າ Community Edition. ຖານຂໍ້ມູນສະເພາະ Enterprise ຈະຖືກລຶບໃນລະຫວ່າງການສະຫຼັບ.',
restoreCommunityStarting: 'ກຳລັງກະກຽມ Community Edition. ແຜງຄວບຄຸມຈະເລີ່ມໃໝ່ອັດຕະໂນມັດເມື່ອສຳເລັດ.',
levelUpPro: 'ອັບເກຣດເປັນເວີຊັນ Commercial',
licenseSync: 'ຊິງຄ໌ລິຂະສິດ',
knowMorePro: 'ຮຽນຮູ້ເພີ່ມເຕີມ',

View File

@@ -3252,6 +3252,21 @@ const message = {
'Sila salin ID peranti terlebih dahulu untuk mendapatkan lesen. Anda boleh log masuk selepas mengimport lesen.',
licenseRequiredUserTip:
'Lesen Enterprise Edition belum diikat. Hubungi pentadbir super untuk mengimport lesen.',
restoreCommunity: 'Pulihkan Community Edition',
restoreCommunityOnline: 'Pemulihan dalam talian',
restoreCommunityOffline: 'Pemulihan luar talian',
restoreCommunityPackageFound:
'Pakej luar talian Community Edition yang boleh digunakan telah dikesan. Pemulihan luar talian tersedia.',
restoreCommunityPackageMissing:
'Tiada pakej luar talian Community Edition yang boleh digunakan dikesan. Pemulihan luar talian tidak tersedia pada masa ini.',
restoreCommunityOnlineHelper:
'Pakej Community Edition untuk versi semasa akan dimuat turun secara automatik dan data khusus Enterprise akan dibersihkan.',
restoreCommunityOfflineHelper:
'Letakkan pakej rasmi yang tidak lebih lama daripada versi Enterprise semasa dalam {path}. Pertukaran akan didayakan selepas pakej yang boleh digunakan dikesan.',
restoreCommunityConfirm:
'Selepas pertukaran, anda boleh log masuk ke Community Edition menggunakan akaun pentadbir super Enterprise semasa. Pangkalan data khusus Enterprise akan dipadamkan semasa pertukaran.',
restoreCommunityStarting:
'Community Edition sedang disediakan. Panel akan dimulakan semula secara automatik apabila selesai.',
technicalAdvice: 'Konsultasi Teknikal',
advice: 'Konsultasi',
levelUpPro: 'Naik taraf ke Edisi Komersial',

View File

@@ -3373,6 +3373,21 @@ const message = {
'Copie primeiro o ID do dispositivo para obter a licença. Você poderá entrar depois de importá-la.',
licenseRequiredUserTip:
'A licença Enterprise Edition não está vinculada. Entre em contato com o superadministrador para importar a licença.',
restoreCommunity: 'Restaurar Community Edition',
restoreCommunityOnline: 'Restauração online',
restoreCommunityOffline: 'Restauração offline',
restoreCommunityPackageFound:
'Um pacote offline utilizável da Community Edition foi detectado. A restauração offline está disponível.',
restoreCommunityPackageMissing:
'Nenhum pacote offline utilizável da Community Edition foi detectado. A restauração offline não está disponível no momento.',
restoreCommunityOnlineHelper:
'O pacote Community Edition da versão atual será baixado automaticamente e os dados exclusivos da Enterprise serão limpos.',
restoreCommunityOfflineHelper:
'Coloque em {path} um pacote oficial cuja versão não seja anterior à versão Enterprise atual. A troca será habilitada quando um pacote utilizável for detectado.',
restoreCommunityConfirm:
'Após a troca, você poderá entrar na Community Edition com a conta atual de superadministrador da Enterprise. Os bancos de dados exclusivos da Enterprise serão excluídos durante a troca.',
restoreCommunityStarting:
'Preparando a Community Edition. O painel será reiniciado automaticamente quando estiver pronto.',
technicalAdvice: 'Consultoria técnica',
advice: 'Consultoria',
levelUpPro: 'Fazer upgrade para a edição comercial',

View File

@@ -3230,6 +3230,21 @@ const message = {
'Сначала скопируйте ID устройства, чтобы получить лицензию. После импорта лицензии вы сможете войти.',
licenseRequiredUserTip:
'Лицензия Enterprise Edition не привязана. Обратитесь к суперадминистратору для импорта лицензии.',
restoreCommunity: 'Восстановить Community Edition',
restoreCommunityOnline: 'Онлайн-восстановление',
restoreCommunityOffline: 'Офлайн-восстановление',
restoreCommunityPackageFound:
'Обнаружен подходящий офлайн-пакет Community Edition. Офлайн-восстановление доступно.',
restoreCommunityPackageMissing:
'Подходящий офлайн-пакет Community Edition не обнаружен. Офлайн-восстановление сейчас недоступно.',
restoreCommunityOnlineHelper:
'Пакет Community Edition текущей версии будет загружен автоматически, а данные, предназначенные только для Enterprise, будут очищены.',
restoreCommunityOfflineHelper:
'Поместите официальный пакет версии не ниже текущей Enterprise в {path}. Переключение станет доступно после обнаружения подходящего пакета.',
restoreCommunityConfirm:
'После переключения можно войти в Community Edition с текущей учётной записью суперадминистратора Enterprise. Базы данных Enterprise будут удалены во время переключения.',
restoreCommunityStarting:
'Подготавливается Community Edition. После завершения панель автоматически перезапустится.',
technicalAdvice: 'Техническая консультация',
advice: 'Консультация',
levelUpPro: 'Обновить до коммерческой версии',

View File

@@ -3234,6 +3234,21 @@ const message = {
'Lütfen önce cihaz kimliğini kopyalayarak lisansı alın. Lisansı içe aktardıktan sonra giriş yapabilirsiniz.',
licenseRequiredUserTip:
'Enterprise Edition lisansı bağlı değil. Lisansı içe aktarması için süper yöneticiyle iletişime geçin.',
restoreCommunity: 'Community Edition sürümünü geri yükle',
restoreCommunityOnline: 'Çevrimiçi geri yükleme',
restoreCommunityOffline: 'Çevrimdışı geri yükleme',
restoreCommunityPackageFound:
'Kullanılabilir bir çevrimdışı Community Edition paketi algılandı. Çevrimdışı geri yükleme kullanılabilir.',
restoreCommunityPackageMissing:
'Kullanılabilir bir çevrimdışı Community Edition paketi algılanmadı. Çevrimdışı geri yükleme şu anda kullanılamıyor.',
restoreCommunityOnlineHelper:
'Geçerli sürümün Community Edition paketi otomatik olarak indirilecek ve Enterprise sürümüne özel veriler temizlenecektir.',
restoreCommunityOfflineHelper:
'Geçerli Enterprise sürümünden eski olmayan resmi bir paketi {path} dizinine yerleştirin. Kullanılabilir bir paket algılandığında geçiş etkinleştirilir.',
restoreCommunityConfirm:
'Geçişten sonra mevcut Enterprise süper yönetici hesabıyla Community Edition sürümüne giriş yapabilirsiniz. Enterprise sürümüne özel veritabanları geçiş sırasında silinecektir.',
restoreCommunityStarting:
'Community Edition hazırlanıyor. Hazır olduğunda panel otomatik olarak yeniden başlatılacaktır.',
levelUpPro: 'Ticari sürüme yükselt',
licenseSync: 'Lisans Senkronizasyonu',
knowMorePro: 'Daha Fazla Bilgi Edinin',

View File

@@ -2964,6 +2964,16 @@ const message = {
licenseRequiredTip: '目前尚未匯入許可證,複製裝置 ID 後取得許可證並匯入後才能登入使用。',
licenseRequiredShortTip: '請先複製裝置 ID 取得許可證,匯入許可證後即可登入。',
licenseRequiredUserTip: '企業版許可證未綁定,請聯絡超級管理員匯入許可證。',
restoreCommunity: '恢復社群版',
restoreCommunityOnline: '線上恢復',
restoreCommunityOffline: '離線恢復',
restoreCommunityPackageFound: '已偵測到可用的社群版離線安裝包,可以執行離線恢復。',
restoreCommunityPackageMissing: '未偵測到可用的社群版離線安裝包,暫時無法執行離線恢復。',
restoreCommunityOnlineHelper: '切換後將自動下載目前版本的社群版安裝包,並清理企業版專屬資料。',
restoreCommunityOfflineHelper:
'請先將版本不低於目前企業版的官方安裝包放入 {path},偵測到可用安裝包後即可執行切換。',
restoreCommunityConfirm: '切換後可使用目前企業版超級管理員帳號登入社群版,切換過程中將刪除企業版專屬資料庫。',
restoreCommunityStarting: '正在準備社群版,完成後面板將自動重新啟動。',
levelUpPro: '升級商業版',
licenseSync: '許可證同步',
knowMorePro: '了解更多',

View File

@@ -2959,6 +2959,15 @@ const message = {
licenseRequiredTip: '当前尚未导入许可证,复制设备 ID 后获取许可证并导入后才能登录使用。',
licenseRequiredShortTip: '请先复制设备 ID 获取许可证,导入许可证后即可登录。',
licenseRequiredUserTip: '企业版许可证未绑定,请联系超级管理员导入许可证。',
restoreCommunity: '恢复社区版',
restoreCommunityOnline: '在线恢复',
restoreCommunityOffline: '离线恢复',
restoreCommunityPackageFound: '已检测到可用的社区版离线安装包,可以执行离线恢复。',
restoreCommunityPackageMissing: '未检测到可用的社区版离线安装包,暂时无法执行离线恢复。',
restoreCommunityOnlineHelper: '将自动下载当前版本的社区版安装包,完成切换。',
restoreCommunityOfflineHelper: '请先将版本不低于当前企业版的官方安装包放入 {path} 后执行切换。',
restoreCommunityConfirm: '切换后可使用当前企业版超级管理员账号登录社区版,切换过程中将删除企业版专属数据库。',
restoreCommunityStarting: '正在准备社区版,完成后面板将自动重启。',
levelUpPro: '升级商业版',
licenseSync: '许可证同步',
knowMorePro: '了解商业版',

View File

@@ -0,0 +1,172 @@
<template>
<DialogPro
v-model="open"
:title="$t('license.restoreCommunity')"
size="large"
append-to-body
destroy-on-close
@closed="handleClosed"
>
<div v-loading="loading" class="restore-dialog-content">
<el-alert
v-if="restoreInfo.state === 'Failed'"
:title="restoreInfo.message"
type="error"
:closable="false"
show-icon
/>
<el-alert :title="$t('license.restoreCommunityConfirm')" type="warning" :closable="false" show-icon />
<el-radio-group v-model="restoreMode" @change="changeRestoreMode">
<el-radio value="online" border>{{ $t('license.restoreCommunityOnline') }}</el-radio>
<el-radio value="offline" border>{{ $t('license.restoreCommunityOffline') }}</el-radio>
</el-radio-group>
<span class="input-help">
{{
restoreMode === 'offline'
? $t('license.restoreCommunityOfflineHelper', { path: restoreInfo.packageDirectory })
: $t('license.restoreCommunityOnlineHelper')
}}
</span>
<el-alert
v-if="restoreMode === 'offline'"
v-loading="packageChecking"
:title="
restoreInfo.packageExist
? $t('license.restoreCommunityPackageFound')
: $t('license.restoreCommunityPackageMissing')
"
:type="restoreInfo.packageExist ? 'success' : 'warning'"
:closable="false"
show-icon
/>
</div>
<template #footer>
<el-button :disabled="loading" @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
type="primary"
:loading="loading"
:disabled="restoreMode === 'offline' && (packageChecking || !restoreInfo.packageExist)"
@click="submit"
>
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DialogPro>
</template>
<script setup lang="ts">
import { onUnmounted, reactive, ref } from 'vue';
import { getCommunityRestoreStatus, restoreCommunity } from '@/api/modules/setting';
const emit = defineEmits<{
started: [];
}>();
const open = ref(false);
const loading = ref(false);
const packageChecking = ref(false);
const restoreMode = ref<'online' | 'offline'>('online');
const restoreInfo = reactive({
state: 'Ready',
message: '',
packageExist: false,
packageDirectory: '',
});
let packageTimer: number | undefined;
const loadRestoreInfo = async () => {
const res = await getCommunityRestoreStatus();
Object.assign(restoreInfo, res.data);
};
const refreshOfflinePackage = async (showLoading = false) => {
packageChecking.value = showLoading;
try {
await loadRestoreInfo();
} finally {
if (showLoading) {
packageChecking.value = false;
}
}
};
const schedulePackagePoll = () => {
window.clearTimeout(packageTimer);
packageTimer = window.setTimeout(pollOfflinePackage, 3000);
};
const pollOfflinePackage = async () => {
try {
await refreshOfflinePackage();
} finally {
if (open.value && restoreMode.value === 'offline') {
schedulePackagePoll();
}
}
};
const changeRestoreMode = async () => {
window.clearTimeout(packageTimer);
if (restoreMode.value !== 'offline') {
return;
}
await refreshOfflinePackage(true);
schedulePackagePoll();
};
const acceptParams = async () => {
restoreMode.value = 'online';
open.value = true;
await loadRestoreInfo();
};
const handleClosed = () => {
window.clearTimeout(packageTimer);
restoreMode.value = 'online';
loading.value = false;
packageChecking.value = false;
};
const submit = async () => {
loading.value = true;
await restoreCommunity(restoreMode.value)
.then(() => {
emit('started');
open.value = false;
})
.finally(() => {
loading.value = false;
});
};
onUnmounted(() => {
window.clearTimeout(packageTimer);
});
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.restore-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.restore-mode-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
:deep(.el-radio) {
width: 100%;
margin-right: 0;
}
}
</style>

View File

@@ -1,11 +1,26 @@
<template>
<div class="flex items-center justify-center min-h-screen relative bg-gray-100">
<div
class="flex items-center justify-center min-h-screen relative bg-gray-100"
v-loading="restoring"
:element-loading-text="$t('license.restoreCommunityStarting')"
fullscreen
>
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat" :style="backgroundStyle"></div>
<div
:style="{ width: containerWidth, height: containerHeight }"
class="bg-white shadow-lg relative z-10 border border-gray-200 flex overflow-hidden"
id="login-container"
>
<el-button
class="community-restore-link"
type="primary"
link
:loading="restoring"
:disabled="restoring"
@click="communityRestoreRef?.acceptParams()"
>
{{ $t('license.restoreCommunity') }}
</el-button>
<div class="grid grid-cols-1 md:grid-cols-2 items-stretch w-full">
<div v-if="showLogo" class="flex justify-center" :style="{ height: containerHeight }">
<img
@@ -85,6 +100,8 @@
</div>
</div>
</div>
<CommunityRestoreDialog ref="communityRestoreRef" @started="handleCommunityRestoreStarted" />
</div>
</template>
@@ -93,37 +110,43 @@ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import type { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import { genFileId } from 'element-plus';
import { getEnterpriseLicense, uploadEnterpriseLicense } from '@/api/modules/setting';
import { getCommunityRestoreStatus, getEnterpriseLicense, uploadEnterpriseLicense } from '@/api/modules/setting';
import { getLoginSetting } from '@/api/modules/auth';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { preloadImage } from '@/utils/browser';
import { adjustColorToRGBA } from '@/utils/color';
import { getXpackSettingForTheme } from '@/utils/xpack';
import { copyText } from '@/utils/clipboard';
import i18n from '@/lang';
import CommunityRestoreDialog from './community-restore/index.vue';
const router = useRouter();
const {
globalStore,
entrance,
isEnterprise,
isEnterpriseLicenseLoaded,
isEnterpriseLicensed,
isFxplay,
isIntl,
isLoading: restoring,
isLogin,
isMasterProductPro,
isOffline,
isOnRestart,
menuAccordion,
openMenuTabs,
themeConfig,
} = useGlobalStore();
const loading = ref(false);
const communityRestoreRef = ref<InstanceType<typeof CommunityRestoreDialog>>();
const uploadRef = ref<UploadInstance>();
const uploaderFiles = ref<UploadFiles>([]);
const licenseInfo = reactive({
deviceID: '',
});
let restoreTimer: number | undefined;
const defaultLoginImage = new URL('@/assets/images/1panel-login-enterprise.png', import.meta.url).href;
const defaultLoginBgImage = new URL('@/assets/images/1panel-login-bg.jpg', import.meta.url).href;
const loadedLoginImage = ref<string | null>(null);
@@ -133,6 +156,7 @@ const imgLoaded = ref(false);
const FIXED_WIDTH = 1000;
const FIXED_HEIGHT = 415;
const RESTORE_POLL_INTERVAL = 10_000;
const containerWidth = computed(() => `${FIXED_WIDTH}px`);
const containerHeight = computed(() => `${FIXED_HEIGHT}px`);
const width = ref(window.innerWidth);
@@ -183,7 +207,7 @@ const applyLoginThemeColor = () => {
);
document.documentElement.style.setProperty(
'--login-loading-mask-color',
adjustColorToRGBA(loginBtnLinkColor, 30, 15),
adjustColorToRGBA(loginBtnLinkColor, 0, 60),
);
};
@@ -259,6 +283,72 @@ const loadLicenseInfo = async () => {
isEnterpriseLicensed.value = false;
};
const loadRestoreInfo = async () => {
const res = await getCommunityRestoreStatus();
restoring.value = res.data.state === 'Running';
isOnRestart.value = restoring.value;
};
const scheduleRestorePoll = () => {
window.clearTimeout(restoreTimer);
restoreTimer = window.setTimeout(pollRestoreStatus, RESTORE_POLL_INTERVAL);
};
const redirectToCommunityLogin = () => {
restoring.value = false;
isOnRestart.value = false;
globalStore.setLogStatus(false);
globalStore.clearAuthInfo();
isEnterprise.value = false;
isEnterpriseLicensed.value = false;
isEnterpriseLicenseLoaded.value = true;
window.location.replace(entrance.value ? `/${entrance.value}` : '/');
};
const waitForCommunityService = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/core/auth/setting`, {
credentials: 'include',
cache: 'no-store',
});
if (response.ok) {
const result = await response.json();
if (result?.data?.isEnterprise === false) {
redirectToCommunityLogin();
return;
}
}
} catch {
// The core service is expected to be temporarily unavailable while binaries are switched.
}
scheduleRestorePoll();
};
const pollRestoreStatus = async () => {
try {
const res = await getCommunityRestoreStatus();
if (res.data.state === 'Failed') {
restoring.value = false;
isOnRestart.value = false;
MsgError(res.data.message);
return;
}
restoring.value = res.data.state === 'Running';
isOnRestart.value = restoring.value;
if (restoring.value) {
scheduleRestorePoll();
}
} catch {
await waitForCommunityService();
}
};
const handleCommunityRestoreStarted = () => {
restoring.value = true;
isOnRestart.value = true;
scheduleRestorePoll();
};
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
uploaderFiles.value = uploadFiles;
};
@@ -317,6 +407,12 @@ onMounted(async () => {
await loadXpackLoginTheme();
await loadBackground();
await loadLicenseInfo();
if (!isEnterpriseLicensed.value) {
await loadRestoreInfo();
if (restoring.value) {
scheduleRestorePoll();
}
}
} finally {
loading.value = false;
}
@@ -324,6 +420,7 @@ onMounted(async () => {
onUnmounted(() => {
window.removeEventListener('resize', updateSize);
window.clearTimeout(restoreTimer);
});
</script>
@@ -332,6 +429,18 @@ onUnmounted(() => {
border-radius: 0;
}
.community-restore-link {
position: absolute;
top: 16px;
right: 20px;
z-index: 1;
color: var(--login-btn-link-color, #005eeb);
&:hover {
color: var(--login-btn-link-hover-color, #0054d3);
}
}
.login-form {
max-width: 560px;
gap: 16px;
@@ -449,7 +558,11 @@ onUnmounted(() => {
background-color: var(--login-loading-mask-color) !important;
.el-loading-spinner .path {
stroke: var(--login-btn-link-color);
stroke: #ffffff;
}
.el-loading-text {
color: #ffffff;
}
}
</style>

View File

@@ -510,6 +510,8 @@ function load18n(label: string) {
return i18n.global.t('clean.snapshot');
case 'rollback':
return i18n.global.t('clean.rollback');
case 'rollback_community_restore':
return i18n.global.t('license.restoreCommunity');
case 'tmp_backup':
return i18n.global.t('clean.tmpBackup');
case 'unknown_app':