mirror of
https://github.com/WenPai-org/bulk-plugin-installer.git
synced 2025-08-04 20:05:15 +08:00
调整目录结构,新增服务器安装列表
This commit is contained in:
parent
814e65f90e
commit
c5bdfeabcb
6 changed files with 272 additions and 109 deletions
210
assets/css/admin.css
Normal file
210
assets/css/admin.css
Normal file
|
@ -0,0 +1,210 @@
|
|||
.bpi-wrap {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.bpi-container {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.bpi-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.bpi-tabs-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bpi-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: #23282d;
|
||||
}
|
||||
|
||||
.bpi-tab.active {
|
||||
border-bottom: 2px solid #007cba;
|
||||
font-weight: 600;
|
||||
background: #f0f0f1;
|
||||
}
|
||||
|
||||
.bpi-tab:hover:not(.active) {
|
||||
background: #f0f0f1;
|
||||
border-bottom-color: #dcdcde;
|
||||
}
|
||||
|
||||
.bpi-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bpi-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bpi-form-row {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.bpi-form-row label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bpi-select {
|
||||
min-width: 200px;
|
||||
height: 35px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.bpi-form textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
border: 2px dashed #b4b9be;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-upload-container:hover,
|
||||
.file-upload-container.dragover {
|
||||
border-color: #007cba;
|
||||
background: #f0f0f1;
|
||||
}
|
||||
|
||||
.file-upload-container input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-instructions {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.selected-files {
|
||||
margin-top: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
background: #fff;
|
||||
padding: 8px 12px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-file .remove-file {
|
||||
color: #dc3232;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.source-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.source-input.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bpi-results .notice,
|
||||
#settings-status {
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.bpi-results .notice-success,
|
||||
#settings-status.notice-success {
|
||||
background-color: #dff0d8;
|
||||
border-left: 4px solid #46b450;
|
||||
}
|
||||
|
||||
.bpi-results .notice-error,
|
||||
#settings-status.notice-error {
|
||||
background-color: #f2dede;
|
||||
border-left: 4px solid #dc3232;
|
||||
}
|
||||
|
||||
.bpi-results .notice-info,
|
||||
#settings-status.notice-info {
|
||||
background-color: #e5f5fa;
|
||||
border-left: 4px solid #00a0d2;
|
||||
}
|
||||
|
||||
.installation-list {
|
||||
list-style: none;
|
||||
margin: 10px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.installation-list li {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.installation-list .spinner {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.installation-list .item-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.installation-list .status {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.installation-list .success {
|
||||
color: #46b450;
|
||||
}
|
||||
|
||||
.installation-list .error {
|
||||
color: #dc3232;
|
||||
}
|
||||
|
||||
.progress-count {
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #007cba;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #005a87;
|
||||
}
|
326
assets/js/admin.js
Normal file
326
assets/js/admin.js
Normal file
|
@ -0,0 +1,326 @@
|
|||
jQuery(document).ready(function($) {
|
||||
const $results = $('#installation-results');
|
||||
|
||||
$('.bpi-tab').on('click', function() {
|
||||
$('.bpi-tab').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
var tab = $(this).data('tab');
|
||||
$('.bpi-tab-content').removeClass('active').hide();
|
||||
$('#' + tab).addClass('active').show();
|
||||
});
|
||||
|
||||
$('.bpi-select').on('change', function() {
|
||||
const $form = $(this).closest('.bpi-form');
|
||||
const selectedType = $(this).val();
|
||||
|
||||
$form.find('.source-input').removeClass('active').hide();
|
||||
$form.find('textarea[name="items"]').val('');
|
||||
$form.find('input[type="file"]').val('');
|
||||
$form.find('.selected-files').empty();
|
||||
|
||||
$form.find('.' + selectedType + '-source').addClass('active').show();
|
||||
});
|
||||
|
||||
$('.file-upload-container').each(function() {
|
||||
const $container = $(this);
|
||||
const $fileInput = $container.find('input[type="file"]');
|
||||
const $selectedFiles = $container.find('.selected-files');
|
||||
|
||||
$container.on('click', function(e) {
|
||||
if (e.target === this || $(e.target).hasClass('upload-instructions')) {
|
||||
$fileInput.trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$container.on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).addClass('dragover');
|
||||
});
|
||||
|
||||
$container.on('dragleave drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).removeClass('dragover');
|
||||
});
|
||||
|
||||
$container.on('drop', function(e) {
|
||||
const files = e.originalEvent.dataTransfer.files;
|
||||
$fileInput[0].files = files;
|
||||
handleFiles(files);
|
||||
});
|
||||
|
||||
$fileInput.on('change', function() {
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
$selectedFiles.empty();
|
||||
Array.from(files).forEach(file => {
|
||||
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
|
||||
const $fileElement = $(`
|
||||
<div class="selected-file">
|
||||
<span class="filename">${escapeHtml(file.name)}</span>
|
||||
<span class="remove-file dashicons dashicons-no-alt"></span>
|
||||
</div>
|
||||
`);
|
||||
$selectedFiles.append($fileElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$selectedFiles.on('click', '.remove-file', function() {
|
||||
$(this).closest('.selected-file').remove();
|
||||
if ($selectedFiles.children().length === 0) {
|
||||
$fileInput.val('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#bulk-plugin-form, #bulk-theme-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const $form = $(this);
|
||||
const action = $form.attr('id') === 'bulk-plugin-form' ? 'bpi_install_plugins' : 'bpi_install_themes';
|
||||
const type = $form.find('.bpi-select').val();
|
||||
const $submitButton = $form.find('button[type="submit"]');
|
||||
|
||||
let items = [];
|
||||
let errorMessage = '';
|
||||
|
||||
if (type === 'upload') {
|
||||
const $fileInput = $form.find('input[type="file"]');
|
||||
const files = $fileInput[0].files;
|
||||
if (!files || files.length === 0) {
|
||||
errorMessage = 'Please select at least one ZIP file.';
|
||||
} else {
|
||||
items = Array.from(files).map(file => file.name);
|
||||
}
|
||||
} else {
|
||||
const $textarea = $form.find('.' + type + '-source textarea[name="items"]');
|
||||
items = $textarea.val().split('\n')
|
||||
.map(item => item.trim())
|
||||
.filter(item => item.length > 0);
|
||||
if (items.length === 0) {
|
||||
errorMessage = (type === 'repository' || type === 'wenpai') ?
|
||||
'Please enter at least one slug.' :
|
||||
'Please enter at least one URL.';
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
alert(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
$submitButton.prop('disabled', true).text('Installing...');
|
||||
$results.html(`<div class="notice notice-info"><p>Installation in progress... (Large ZIP files may take some time)</p><div class="progress-count">0/${items.length} completed (0% done, ${items.length} remaining)</div><ul class="installation-list"></ul></div>`);
|
||||
|
||||
const $list = $results.find('.installation-list');
|
||||
const $progress = $results.find('.progress-count');
|
||||
let completed = 0;
|
||||
|
||||
if (type === 'upload') {
|
||||
const formData = new FormData($form[0]);
|
||||
formData.append('action', action);
|
||||
formData.append('nonce', bpiAjax.nonce);
|
||||
formData.append('install_type', type);
|
||||
|
||||
$.ajax({
|
||||
url: bpiAjax.ajaxurl,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
console.log('Upload response:', response); // 调试输出
|
||||
if (response.success) {
|
||||
Object.keys(response.data).forEach((item, index) => {
|
||||
handleResponse(response, item, index);
|
||||
});
|
||||
} else {
|
||||
$list.append(`<li><span class="item-name">Upload Error</span><span class="status error">✗ ${escapeHtml(response.data || 'Unknown upload error')}</span></li>`);
|
||||
}
|
||||
installationComplete();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('Upload error:', xhr, status, error); // 调试输出
|
||||
$list.append(`<li><span class="item-name">Upload Error</span><span class="status error">✗ ${escapeHtml(xhr.responseText || error)}</span></li>`);
|
||||
installationComplete();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
processNextItem(0);
|
||||
}
|
||||
|
||||
function processNextItem(index) {
|
||||
if (index >= items.length) {
|
||||
installationComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items[index];
|
||||
$list.append(`<li id="item-${index}"><span class="spinner is-active"></span><span class="item-name">${escapeHtml(item)}</span><span class="status"></span></li>`);
|
||||
|
||||
$.ajax({
|
||||
url: bpiAjax.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: action,
|
||||
nonce: bpiAjax.nonce,
|
||||
items: JSON.stringify([item]),
|
||||
install_type: type
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('Item response:', response); // 调试输出
|
||||
handleResponse(response, item, index);
|
||||
processNextItem(index + 1);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('Item error:', xhr, status, error); // 调试输出
|
||||
handleError(xhr, status, error, item, index);
|
||||
processNextItem(index + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleResponse(response, item, index) {
|
||||
const $item = $(`#item-${index}`) || $list.find('li:last');
|
||||
$item.find('.spinner').removeClass('is-active');
|
||||
|
||||
if (response.success && response.data[item]) {
|
||||
const result = response.data[item];
|
||||
$item.addClass(result.success ? 'success' : 'error');
|
||||
let statusHtml = '';
|
||||
if (result.success) {
|
||||
statusHtml = result.skipped ? 'ⓘ ' + escapeHtml(result.message) : '✓ ' + escapeHtml(result.message);
|
||||
} else {
|
||||
statusHtml = '✗ ' + escapeHtml(result.message);
|
||||
if (result.retry) {
|
||||
statusHtml += ' <button class="retry-btn" data-item="' + escapeHtml(item) + '" data-type="' + type + '">Retry</button>';
|
||||
}
|
||||
}
|
||||
$item.find('.status').html(statusHtml);
|
||||
} else {
|
||||
$item.addClass('error')
|
||||
.find('.status')
|
||||
.html('✗ ' + escapeHtml(response.data || 'Unknown error') + ' <button class="retry-btn" data-item="' + escapeHtml(item) + '" data-type="' + type + '">Retry</button>');
|
||||
}
|
||||
|
||||
completed++;
|
||||
const percentage = Math.round((completed / items.length) * 100);
|
||||
const remaining = items.length - completed;
|
||||
$progress.text(`${completed}/${items.length} completed (${percentage}% done, ${remaining} remaining)`);
|
||||
}
|
||||
|
||||
function handleError(xhr, status, error, item, index) {
|
||||
const $item = $(`#item-${index}`) || $list.find('li:last');
|
||||
$item.find('.spinner').removeClass('is-active')
|
||||
.addClass('error')
|
||||
.find('.status')
|
||||
.html(`✗ ${escapeHtml(xhr.responseText || 'Installation failed: ' + error)} <button class="retry-btn" data-item="${escapeHtml(item)}" data-type="${type}">Retry</button>`);
|
||||
completed++;
|
||||
const percentage = Math.round((completed / items.length) * 100);
|
||||
const remaining = items.length - completed;
|
||||
$progress.text(`${completed}/${items.length} completed (${percentage}% done, ${remaining} remaining)`);
|
||||
}
|
||||
|
||||
function installationComplete() {
|
||||
$submitButton.prop('disabled', false).text(`Install ${action === 'bpi_install_plugins' ? 'Plugins' : 'Themes'}`);
|
||||
const $notice = $results.find('.notice').removeClass('notice-info').addClass('notice-success');
|
||||
$notice.find('p').html('Installation completed! Check the results below. Failed items can be retried using the "Retry" buttons if applicable.');
|
||||
}
|
||||
});
|
||||
|
||||
$results.on('click', '.retry-btn', function() {
|
||||
const $button = $(this);
|
||||
const item = $button.data('item');
|
||||
const type = $button.data('type');
|
||||
const action = $('#bulk-plugin-form').is(':visible') ? 'bpi_install_plugins' : 'bpi_install_themes';
|
||||
const $li = $button.closest('li');
|
||||
$li.find('.spinner').addClass('is-active');
|
||||
$li.find('.status').html('');
|
||||
|
||||
$.ajax({
|
||||
url: bpiAjax.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: action,
|
||||
nonce: bpiAjax.nonce,
|
||||
items: JSON.stringify([item]),
|
||||
install_type: type
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('Retry response:', response); // 调试输出
|
||||
$li.find('.spinner').removeClass('is-active');
|
||||
if (response.success && response.data[item]) {
|
||||
const result = response.data[item];
|
||||
$li.removeClass('error success').addClass(result.success ? 'success' : 'error');
|
||||
let statusHtml = '';
|
||||
if (result.success) {
|
||||
statusHtml = result.skipped ? 'ⓘ ' + escapeHtml(result.message) : '✓ ' + escapeHtml(result.message);
|
||||
} else {
|
||||
statusHtml = '✗ ' + escapeHtml(result.message);
|
||||
if (result.retry) {
|
||||
statusHtml += ' <button class="retry-btn" data-item="' + escapeHtml(item) + '" data-type="' + type + '">Retry</button>';
|
||||
}
|
||||
}
|
||||
$li.find('.status').html(statusHtml);
|
||||
} else {
|
||||
$li.addClass('error')
|
||||
.find('.status')
|
||||
.html('✗ ' + escapeHtml(response.data || 'Unknown error') + ' <button class="retry-btn" data-item="' + escapeHtml(item) + '" data-type="' + type + '">Retry</button>');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('Retry error:', xhr, status, error); // 调试输出
|
||||
$li.find('.spinner').removeClass('is-active')
|
||||
.addClass('error')
|
||||
.find('.status')
|
||||
.html(`✗ ${escapeHtml(xhr.responseText || 'Retry failed: ' + error)} <button class="retry-btn" data-item="${escapeHtml(item)}" data-type="${type}">Retry</button>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#bpi-settings-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const $form = $(this);
|
||||
const $submitButton = $form.find('button[type="submit"]');
|
||||
const $status = $('#settings-status');
|
||||
|
||||
$submitButton.prop('disabled', true).text('Saving...');
|
||||
$status.removeClass('notice-success notice-error').addClass('notice-info').text('Saving...').show();
|
||||
|
||||
$.ajax({
|
||||
url: bpiAjax.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'bpi_save_settings',
|
||||
nonce: bpiAjax.nonce,
|
||||
bpi_allowed_roles: $form.find('input[name="bpi_allowed_roles[]"]:checked').map(function() { return this.value; }).get(),
|
||||
bpi_custom_domains: $form.find('textarea[name="bpi_custom_domains"]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$status.removeClass('notice-info').addClass('notice-success').text(response.data || 'Settings saved successfully!').show().delay(3000).fadeOut();
|
||||
} else {
|
||||
$status.removeClass('notice-info').addClass('notice-error').text(response.data || 'Failed to save settings.').show();
|
||||
}
|
||||
$submitButton.prop('disabled', false).text('Save Settings');
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$status.removeClass('notice-info').addClass('notice-error').text('An error occurred while saving settings: ' + (xhr.responseText || error)).show();
|
||||
$submitButton.prop('disabled', false).text('Save Settings');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue