mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
feat: add backup UI page and frontend model fields
This commit is contained in:
parent
50d3b2cd7e
commit
8a861894c9
4 changed files with 203 additions and 1 deletions
37
docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md
Normal file
37
docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Task Record
|
||||
|
||||
Date: 2026-04-26
|
||||
Related Module: web (frontend)
|
||||
Change Type: Add
|
||||
|
||||
## Background
|
||||
Add backup management UI page and corresponding frontend model fields to the settings page. The backup feature allows users to configure scheduled backups, manually create backups, and manage (download/restore/delete) existing backup files.
|
||||
|
||||
## Changes
|
||||
- Added 4 backup fields to `AllSetting` class in `web/assets/js/model/setting.js`: `backupEnabled`, `backupFrequency`, `backupHour`, `backupMaxCount`
|
||||
- Created `web/html/settings/backup.html` with Vue template for backup configuration, manual operations, and backup list table
|
||||
- Added Backup tab (key="7") to settings.html `<a-tabs>` component
|
||||
- Added Vue data properties: `backupList`, `backupColumns`, `backupLoading`, `backupCreating`, `backupRefreshInterval`
|
||||
- Added Vue methods: `fetchBackups`, `createBackup`, `restoreBackup`, `deleteBackup`, `downloadBackup`, `formatFileSize`, `formatBackupTime`
|
||||
- Modified `onSettingsTabChange` to fetch backups when tab 7 is selected
|
||||
- Added 30-second backup refresh interval in `mounted()` lifecycle hook
|
||||
- Added `beforeDestroy()` lifecycle hook to clear the backup refresh interval
|
||||
|
||||
## Impact
|
||||
- New template file: `web/html/settings/backup.html`
|
||||
- Modified: `web/assets/js/model/setting.js`, `web/html/settings.html`
|
||||
- Uses Vue `[[ ]]` delimiters for interpolation (project standard)
|
||||
- API endpoints referenced: `/panel/api/server/listBackups`, `/panel/api/server/backup`, `/panel/api/server/restore/`, `/panel/api/server/deleteBackup/`, `/panel/api/server/downloadBackup/`
|
||||
- Uses `this.authHeaders` for axios requests (expected to be defined separately)
|
||||
- `AllSetting.equals()` will now factor in backup fields for change detection
|
||||
|
||||
## Verification
|
||||
- `go build ./...` passes
|
||||
- `gofmt -l -w .` passes with no changes needed
|
||||
- Template compilation verified via build
|
||||
- Cannot runtime test without running panel server (no local dev environment)
|
||||
|
||||
## Risks And Follow-Up
|
||||
- `this.authHeaders` is referenced but not defined in the current codebase; must be defined before runtime use
|
||||
- Backup API endpoints must exist on the backend for the page to function
|
||||
- `AllSetting.equals()` will use the new backup fields for save-button change detection (existing behavior, no regression expected)
|
||||
|
|
@ -81,6 +81,11 @@ class AllSetting {
|
|||
this.ldapDefaultExpiryDays = 0;
|
||||
this.ldapDefaultLimitIP = 0;
|
||||
|
||||
this.backupEnabled = false;
|
||||
this.backupFrequency = "daily";
|
||||
this.backupHour = 3;
|
||||
this.backupMaxCount = 10;
|
||||
|
||||
this.dbType = "sqlite";
|
||||
this.dbHost = "127.0.0.1";
|
||||
this.dbPort = "3306";
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@
|
|||
</template>
|
||||
{{ template "settings/panel/subscription/clash" . }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="7" :style="{ paddingTop: '20px' }">
|
||||
<template #tab>
|
||||
<a-icon type="database"></a-icon>
|
||||
<span>Backup</span>
|
||||
</template>
|
||||
{{ template "settings/backup" . }}
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -251,10 +258,21 @@
|
|||
const sample = [];
|
||||
this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
|
||||
this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);
|
||||
}
|
||||
},
|
||||
backupList: [],
|
||||
backupColumns: [
|
||||
{ title: 'Filename', dataIndex: 'filename', key: 'filename' },
|
||||
{ title: 'Timestamp', dataIndex: 'timestamp', key: 'timestamp', scopedSlots: { customRender: 'timestamp' } },
|
||||
{ title: 'Size', dataIndex: 'size', key: 'size', scopedSlots: { customRender: 'size' } },
|
||||
{ title: 'Actions', key: 'action', scopedSlots: { customRender: 'action' } }
|
||||
],
|
||||
backupLoading: false,
|
||||
backupCreating: false,
|
||||
backupRefreshInterval: null,
|
||||
},
|
||||
methods: {
|
||||
onSettingsTabChange(key) {
|
||||
if (key === '7') { this.fetchBackups(); }
|
||||
},
|
||||
loading(spinning = true) {
|
||||
this.loadingStates.spinning = spinning;
|
||||
|
|
@ -451,6 +469,65 @@
|
|||
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
fetchBackups() {
|
||||
this.backupLoading = true;
|
||||
axios.get(this.entryHost + 'panel/api/server/listBackups', {
|
||||
headers: this.authHeaders
|
||||
}).then(res => {
|
||||
this.backupList = res.data.obj || [];
|
||||
}).catch(err => {
|
||||
this.$message.error('Failed to load backups');
|
||||
}).finally(() => {
|
||||
this.backupLoading = false;
|
||||
});
|
||||
},
|
||||
createBackup() {
|
||||
this.backupCreating = true;
|
||||
axios.post(this.entryHost + 'panel/api/server/backup', {}, {
|
||||
headers: this.authHeaders
|
||||
}).then(res => {
|
||||
this.$message.success('Backup created successfully');
|
||||
this.fetchBackups();
|
||||
}).catch(err => {
|
||||
this.$message.error('Backup failed');
|
||||
}).finally(() => {
|
||||
this.backupCreating = false;
|
||||
});
|
||||
},
|
||||
restoreBackup(filename) {
|
||||
axios.post(this.entryHost + 'panel/api/server/restore/' + filename, {}, {
|
||||
headers: this.authHeaders
|
||||
}).then(res => {
|
||||
this.$message.success('Restore completed');
|
||||
this.fetchBackups();
|
||||
}).catch(err => {
|
||||
this.$message.error('Restore failed');
|
||||
});
|
||||
},
|
||||
deleteBackup(filename) {
|
||||
axios.post(this.entryHost + 'panel/api/server/deleteBackup/' + filename, {}, {
|
||||
headers: this.authHeaders
|
||||
}).then(res => {
|
||||
this.$message.success('Backup deleted');
|
||||
this.fetchBackups();
|
||||
}).catch(err => {
|
||||
this.$message.error('Delete failed');
|
||||
});
|
||||
},
|
||||
downloadBackup(filename) {
|
||||
window.open(this.entryHost + 'panel/api/server/downloadBackup/' + filename, '_blank');
|
||||
},
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
},
|
||||
formatBackupTime(ts) {
|
||||
if (!ts) return '';
|
||||
return ts.replace(/(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})/, '$1-$2-$3 $4:$5:$6');
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
ldapInboundTagList: {
|
||||
|
|
@ -655,6 +732,7 @@
|
|||
} finally {
|
||||
this.loadingStates.fetched = true;
|
||||
}
|
||||
this.backupRefreshInterval = setInterval(() => { this.fetchBackups(); }, 30000);
|
||||
if (!settingsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -666,6 +744,9 @@
|
|||
console.error('Settings change detection error:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.backupRefreshInterval) { clearInterval(this.backupRefreshInterval); }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
79
web/html/settings/backup.html
Normal file
79
web/html/settings/backup.html
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<div>
|
||||
<a-row :gutter="[16, 16]" :style="{ marginTop: '16px' }">
|
||||
<a-col :span="24">
|
||||
<a-card :title="'Backup Configuration'" size="small">
|
||||
<a-form-model :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" label-align="left">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-model-item :label="'Enable Scheduled Backup'">
|
||||
<a-switch v-model="allSetting.backupEnabled"></a-switch>
|
||||
</a-form-model-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-model-item :label="'Frequency'">
|
||||
<a-select v-model="allSetting.backupFrequency" :disabled="!allSetting.backupEnabled">
|
||||
<a-select-option value="hourly">Every Hour</a-select-option>
|
||||
<a-select-option value="every12h">Every 12 Hours</a-select-option>
|
||||
<a-select-option value="daily">Every Day</a-select-option>
|
||||
<a-select-option value="weekly">Every Week</a-select-option>
|
||||
</a-select>
|
||||
</a-form-model-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
v-if="allSetting.backupFrequency === 'daily' || allSetting.backupFrequency === 'weekly'">
|
||||
<a-form-model-item :label="'Hour (0-23)'">
|
||||
<a-input-number v-model="allSetting.backupHour" :min="0" :max="23"
|
||||
:disabled="!allSetting.backupEnabled"></a-input-number>
|
||||
</a-form-model-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-model-item :label="'Max Backups (1-100)'">
|
||||
<a-input-number v-model="allSetting.backupMaxCount" :min="1" :max="100"
|
||||
:disabled="!allSetting.backupEnabled"></a-input-number>
|
||||
</a-form-model-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-model>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-card :title="'Manual Operations'" size="small">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="plus" @click="createBackup" :loading="backupCreating">
|
||||
Create Backup Now
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-card size="small">
|
||||
<span slot="title">Backup List
|
||||
<a-badge :count="backupList.length" :number-style="{ backgroundColor: '#52c41a' }"
|
||||
:style="{ marginLeft: '8px' }" />
|
||||
</span>
|
||||
<a-table :columns="backupColumns" :data-source="backupList" :pagination="false" :loading="backupLoading"
|
||||
size="small" row-key="filename">
|
||||
<template slot="size" slot-scope="text">
|
||||
[[ formatFileSize(text) ]]
|
||||
</template>
|
||||
<template slot="timestamp" slot-scope="text">
|
||||
[[ formatBackupTime(text) ]]
|
||||
</template>
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-space>
|
||||
<a-button size="small" icon="download" @click="downloadBackup(record.filename)">Download</a-button>
|
||||
<a-popconfirm :title="'Restore will stop the panel temporarily. Continue?'" ok-text="Yes"
|
||||
cancel-text="No" @confirm="restoreBackup(record.filename)">
|
||||
<a-button size="small" type="danger" icon="redo">Restore</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="Delete this backup?" ok-text="Yes" cancel-text="No"
|
||||
@confirm="deleteBackup(record.filename)">
|
||||
<a-button size="small" icon="delete">Delete</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
Loading…
Reference in a new issue