Files
ns2.0/templates/auto_import.html

1026 lines
29 KiB
HTML
Executable File

{% extends "base.html" %}
{% block content %}
{% include 'admin_nav.html' %}
<div class="admin-container">
<div class="admin-content">
<div class="admin-card" style="margin-bottom: 20px;">
<div class="card-header">
<div class="header-left">
<i class="fas fa-cloud-download-alt"></i>
<h3>自动导入应用</h3>
</div>
</div>
<form id="import-form" class="admin-form">
<div class="form-group">
<label for="category">选择分类</label>
<select id="category" name="category_id" required>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>显示区域</label>
<div class="platform-buttons">
<button type="button" class="platform-btn active" data-value="mobile">
<i class="fas fa-mobile-alt"></i>
仅手机区
</button>
<button type="button" class="platform-btn" data-value="tablet">
<i class="fas fa-tablet-alt"></i>
仅平板区
</button>
<button type="button" class="platform-btn" data-value="both">
<i class="fas fa-desktop"></i>
全部显示
</button>
</div>
<input type="hidden" name="platform" id="platform" value="mobile">
</div>
<div class="form-group">
<label for="app-name">单个应用导入</label>
<div class="search-input-group">
<input type="text" id="app-name" name="app_name" required placeholder="输入应用名称">
<button type="button" onclick="fetchAppInfo()" class="btn-primary">
搜索
</button>
</div>
</div>
<div class="form-group">
<label for="batch-apps">批量导入</label>
<div class="batch-input-group">
<textarea id="batch-apps" name="batch_apps" rows="5" placeholder="输入多个应用名称,用英文逗号分隔"></textarea>
<button type="button" onclick="batchImport()" class="btn-primary">
<i class="fas fa-cloud-upload-alt"></i> 批量导入
</button>
</div>
</div>
</form>
<!-- 搜索结果预览区域 -->
<div id="search-preview" style="display: none;" class="search-preview">
<div class="preview-header">
<h4>搜索结果</h4>
</div>
<div class="preview-content">
<div class="app-preview-card">
<img id="preview-icon" src="" alt="应用图标" class="preview-icon">
<div class="preview-info">
<div class="name-compare">
<p class="search-name"><strong>搜索:</strong><span id="search-name"></span></p>
<p class="found-name"><strong>找到:</strong><span id="found-name"></span></p>
</div>
<div class="preview-actions">
<button onclick="confirmImport()" class="btn-primary">
<i class="fas fa-cloud-upload-alt"></i> 确认导入
</button>
<button onclick="cancelPreview()" class="btn-secondary">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div id="loading-indicator" style="display: none;" class="loading-indicator">
<div class="spinner"></div>
<p>正在搜索,请稍候...</p>
</div>
<!-- 最近导入记录 -->
<div class="admin-card full-width">
<div class="card-header">
<div class="header-left">
<i class="fas fa-history"></i>
<h3>最近导入</h3>
</div>
</div>
<div id="import-history" class="import-history">
<!-- 这里将通过JavaScript动态添加导入历史 -->
</div>
</div>
</div>
</div>
<!-- 添加批量导入结果弹窗 -->
<div id="batch-result-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>批量导入结果</h3>
<span class="close" onclick="closeBatchResultModal()">&times;</span>
</div>
<div class="batch-result-content">
<div id="success-list"></div>
<div id="failed-list"></div>
</div>
</div>
</div>
<!-- 添加确认对话框 -->
<div id="confirm-import-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>确认导入应用</h3>
<span class="close" onclick="closeConfirmModal()">&times;</span>
</div>
<div class="confirm-app-info">
<img id="confirm-app-icon" src="" alt="应用图标">
<div class="confirm-app-details">
<p><strong>搜索名称:</strong><span id="search-name"></span></p>
<p><strong>找到应用:</strong><span id="found-name"></span></p>
</div>
</div>
<div class="modal-footer">
<button onclick="confirmImport()" class="btn-primary">确认导入</button>
<button onclick="closeConfirmModal()" class="btn-secondary">取消</button>
</div>
</div>
</div>
<script>
let currentAppInfo = null;
let searchTimeout = null;
let searchInterval = null;
let pendingImport = null;
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notification.style.position = 'fixed';
notification.style.bottom = '20px';
notification.style.right = '20px';
notification.style.padding = '10px 20px';
notification.style.borderRadius = '4px';
notification.style.backgroundColor = type === 'success' ? '#4CAF50' : '#f44336';
notification.style.color = 'white';
notification.style.zIndex = '1000';
notification.style.animation = 'fadeInOut 3s forwards';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
function fetchAppInfo() {
const appName = document.getElementById('app-name').value.trim();
const categoryId = document.getElementById('category').value;
const platform = document.getElementById('platform').value;
if (!appName) {
showNotification('请输入应用名称', 'error');
return;
}
// 显示加载指示器
const loadingIndicator = document.getElementById('loading-indicator');
loadingIndicator.style.display = 'block';
fetch('/admin/auto_import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_name: appName,
category_id: categoryId,
platform: platform,
force_import: false
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 直接导入成功
showNotification(data.message, 'success');
document.getElementById('app-name').value = '';
hideSearchPreview();
if (data.app_info) {
updateImportHistory(data.app_info);
}
} else if (data.needs_confirmation) {
// 显示预览确认
showSearchPreview(appName, data.found_app);
} else {
showNotification(data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('导入失败,请重试', 'error');
})
.finally(() => {
loadingIndicator.style.display = 'none';
});
}
function showPreview(appInfo) {
const previewArea = document.getElementById('preview-area');
const previewIcon = document.getElementById('preview-icon');
const previewName = document.getElementById('preview-name');
if (appInfo.icon_url) {
previewIcon.src = appInfo.icon_url;
previewIcon.style.display = 'block';
} else {
previewIcon.style.display = 'none';
}
previewName.textContent = appInfo.name || '';
previewArea.style.display = 'block';
}
// 在成功导入后更新最近导入记录
function updateImportHistory(appInfo) {
const historyContainer = document.getElementById('import-history');
const entry = document.createElement('div');
entry.className = 'history-item';
entry.innerHTML = `
<img src="${appInfo.icon_url}" alt="${appInfo.name}" class="history-icon">
<div class="history-info">
<p><strong>${appInfo.name}</strong></p>
</div>
<a href="#" class="btn-delete" onclick="deleteHistoryApp('${appInfo.name}'); return false;">
<i class="fas fa-trash"></i>
</a>
`;
historyContainer.prepend(entry);
}
function importApp() {
if (!currentAppInfo) {
showNotification('请先搜索应用', 'error');
return;
}
const categoryId = document.getElementById('category').value;
const formData = new FormData();
formData.append('name', currentAppInfo.name);
formData.append('icon_url', currentAppInfo.icon_url);
formData.append('category_id', categoryId);
formData.append('download_url', ''); // 空的下载链接
fetch('/import_app', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
// 在导入成功后清空输入框
document.getElementById('app-name').value = '';
document.getElementById('preview-area').style.display = 'none';
updateImportHistory(currentAppInfo); // 更新最近导入记录
currentAppInfo = null;
} else {
showNotification(data.error, 'error');
}
})
.catch(error => {
showNotification('导入失败', 'error');
console.error('Error:', error);
});
}
// 添加回车搜索功能
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('import-form');
const appNameInput = document.getElementById('app-name');
// 阻止表单默认提交行为
form.addEventListener('submit', function(e) {
e.preventDefault();
});
// 添加回车搜索
appNameInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault(); // 阻止默认的表单提交
fetchAppInfo(); // 执搜索
}
});
});
async function batchImport() {
const batchInput = document.getElementById('batch-apps');
const categoryId = document.getElementById('category').value;
const platform = document.getElementById('platform').value;
const appNames = batchInput.value.split(',').map(name => name.trim()).filter(name => name);
if (appNames.length === 0) {
showNotification('请输入应用名称', 'error');
return;
}
const loadingIndicator = document.getElementById('loading-indicator');
loadingIndicator.style.display = 'block';
const results = {
success: [],
failed: {}
};
const maxRetries = 5; // 增加到5次重试
const retryDelay = 2000; // 增加到2秒的重试间隔
const importDelay = 1000; // 每个应用导入之间的间隔
async function tryImportApp(appName, retryCount = 0) {
try {
// 每次尝试前添加随机延迟,避免同时请求
const randomDelay = Math.floor(Math.random() * 1000); // 0-1秒的随机延迟
await new Promise(resolve => setTimeout(resolve, randomDelay));
const response = await fetch('/admin/auto_import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_name: appName,
category_id: categoryId,
platform: platform,
force_import: false
})
});
const data = await response.json();
if (data.success) {
results.success.push(appName);
if (data.app_info) {
updateImportHistory(data.app_info);
}
return true;
} else if (data.needs_confirmation) {
// 如果需要确认,直接使用找到的应用信息进行导入
await new Promise(resolve => setTimeout(resolve, 500)); // 添加短暂延迟
const confirmResponse = await fetch('/admin/auto_import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_name: appName,
category_id: categoryId,
platform: platform,
force_import: true
})
});
const confirmData = await confirmResponse.json();
if (confirmData.success) {
results.success.push(appName);
if (confirmData.app_info) {
updateImportHistory(confirmData.app_info);
}
return true;
}
}
// 如果是"未找到应用"错误且还有重试次数,则重试
if ((data.error === '未找到应用' || data.error === '搜索应用失败,请稍后重试') && retryCount < maxRetries) {
console.log(`重试导入 ${appName},第 ${retryCount + 1} 次,等待 ${retryDelay}ms`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
return await tryImportApp(appName, retryCount + 1);
}
results.failed[appName] = `${data.error} (已重试${retryCount}次)`;
return false;
} catch (error) {
console.error('Error importing app:', appName, error);
// 网络错误时也进行重试
if (retryCount < maxRetries) {
const currentDelay = retryDelay * Math.pow(1.5, retryCount); // 指数退避
console.log(`网络错误重试 ${appName},第 ${retryCount + 1} 次,等待 ${currentDelay}ms`);
await new Promise(resolve => setTimeout(resolve, currentDelay));
return await tryImportApp(appName, retryCount + 1);
}
results.failed[appName] = `导入失败 (已重试${retryCount}次): ${error.message}`;
return false;
}
}
for (const appName of appNames) {
await tryImportApp(appName);
// 每个应用导入后添加延迟,避免请求过快
await new Promise(resolve => setTimeout(resolve, importDelay));
// 更新进度显示
const progress = document.querySelector('.loading-indicator p');
if (progress) {
progress.textContent = `正在处理`;
}
}
loadingIndicator.style.display = 'none';
showBatchResults(results);
batchInput.value = ''; // 清空输入框
}
// 修改显示结果的函数,添加重试次数信息
function showBatchResults(results) {
const modal = document.getElementById('batch-result-modal');
const successList = document.getElementById('success-list');
const failedList = document.getElementById('failed-list');
successList.innerHTML = results.success.length > 0
? `<h4>✅ 导入成功 (${results.success.length})</h4>
<ul>${results.success.map(name => `<li>${name}</li>`).join('')}</ul>`
: '';
failedList.innerHTML = Object.keys(results.failed).length > 0
? `<h4>❌ 导入失败 (${Object.keys(results.failed).length})</h4>
<ul>${Object.entries(results.failed).map(([name, reason]) =>
`<li>${name}: ${reason}</li>`).join('')}</ul>
<p class="retry-note">* 系统已自动重试最多3次</p>`
: '';
modal.style.display = 'block';
}
function closeBatchResultModal() {
document.getElementById('batch-result-modal').style.display = 'none';
}
// 修改点击事件处理
window.onclick = function(event) {
// 移除点击窗口外部关闭的功能
// const modal = document.getElementById('batch-result-modal');
// if (event.target == modal) {
// modal.style.display = 'none';
// }
}
// 只能通过关闭按钮关闭弹窗
function closeBatchResultModal() {
document.getElementById('batch-result-modal').style.display = 'none';
}
// 修改删除历史记录的函数
function deleteHistoryApp(appName) {
if (confirm(`确定要删除 ${appName} 吗?`)) {
fetch(`/delete_app_by_name/${encodeURIComponent(appName)}`, {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
// 删除成功
showNotification('删除成功', 'success');
// 从DOM中移除对应的历史记录
const historyItems = document.querySelectorAll('.history-item');
historyItems.forEach(item => {
if (item.querySelector('strong').textContent === appName) {
item.remove();
}
});
} else {
// 服务器返回错误信息
throw new Error(data.error || '删除失败');
}
})
.catch(error => {
// 只有在真正发生错误时才显示错误消息
if (error.message !== 'Network response was not ok') {
showNotification(error.message, 'error');
}
console.error('Error:', error);
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const platformButtons = document.querySelectorAll('.platform-btn');
const platformInput = document.getElementById('platform');
platformButtons.forEach(button => {
button.addEventListener('click', function() {
// 移除所有按钮的 active 类
platformButtons.forEach(btn => btn.classList.remove('active'));
// 添加当前按钮的 active 类
this.classList.add('active');
// 更新隐藏输入框的值
platformInput.value = this.dataset.value;
});
});
});
function showConfirmModal(searchName, foundApp) {
const modal = document.getElementById('confirm-import-modal');
document.getElementById('search-name').textContent = searchName;
document.getElementById('found-name').textContent = foundApp.name;
document.getElementById('confirm-app-icon').src = foundApp.icon_url;
modal.style.display = 'block';
}
function closeConfirmModal() {
const modal = document.getElementById('confirm-import-modal');
modal.style.display = 'none';
pendingImport = null;
}
function confirmImport() {
if (!pendingImport) return;
fetch('/admin/auto_import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_name: pendingImport.searchName,
category_id: pendingImport.categoryId,
platform: pendingImport.platform,
force_import: true
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
document.getElementById('app-name').value = '';
if (data.app_info) {
updateImportHistory(data.app_info);
}
} else {
showNotification(data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('导入失败,请重试', 'error');
})
.finally(() => {
closeConfirmModal();
});
}
// 点击窗口外部关闭确认对话框
window.onclick = function(event) {
const modal = document.getElementById('confirm-import-modal');
if (event.target == modal) {
closeConfirmModal();
}
}
// 显示搜索预览
function showSearchPreview(searchName, foundApp) {
const previewArea = document.getElementById('search-preview');
document.getElementById('preview-icon').src = foundApp.icon_url;
document.getElementById('search-name').textContent = searchName;
document.getElementById('found-name').textContent = foundApp.name;
// 保存当前应用信息用于确认导入
window.currentAppInfo = {
searchName: searchName,
foundApp: foundApp,
categoryId: document.getElementById('category').value,
platform: document.getElementById('platform').value
};
previewArea.style.display = 'block';
}
// 隐藏搜索预览
function hideSearchPreview() {
const previewArea = document.getElementById('search-preview');
previewArea.style.display = 'none';
window.currentAppInfo = null;
}
// 取消预览
function cancelPreview() {
hideSearchPreview();
}
// 确认导入
function confirmImport() {
if (!window.currentAppInfo) return;
fetch('/admin/auto_import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_name: window.currentAppInfo.searchName,
category_id: window.currentAppInfo.categoryId,
platform: window.currentAppInfo.platform,
force_import: true
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
document.getElementById('app-name').value = '';
hideSearchPreview();
if (data.app_info) {
updateImportHistory(data.app_info);
}
} else {
showNotification(data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('导入失败,请重试', 'error');
});
}
</script>
<style>
.search-input-group {
display: flex;
gap: 10px;
}
.preview-area {
margin-top: 20px;
padding: 20px;
background: #f5f5f7;
border-radius: 10px;
}
.preview-content {
display: flex;
align-items: center;
gap: 20px;
margin: 15px 0;
}
.preview-icon {
width: 80px;
height: 80px;
border-radius: 15px;
object-fit: cover;
}
.preview-info {
flex: 1;
}
.preview-info p {
margin: 5px 0;
}
.history-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
border-bottom: 1px solid #eee;
position: relative;
}
.history-icon {
width: 40px;
height: 40px;
border-radius: 8px;
}
.history-info {
flex: 1;
}
.loading-indicator {
text-align: center;
margin-top: 20px;
font-size: 16px;
color: #666;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: #09f;
animation: spin 1s ease infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: relative;
background-color: #fff;
margin: 10% auto;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close {
font-size: 24px;
cursor: pointer;
color: #666;
}
.close:hover {
color: #333;
}
.batch-result-content {
max-height: 400px;
overflow-y: auto;
}
#success-list, #failed-list {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
}
#success-list {
background-color: #e8f5e9;
color: #2e7d32;
}
#failed-list {
background-color: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
}
#failed-list ul {
margin: 10px 0;
padding-left: 20px;
}
#failed-list li {
margin: 5px 0;
font-size: 14px;
}
.batch-input-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.batch-input-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #d2d2d7;
border-radius: 8px;
resize: vertical;
font-family: inherit;
}
.batch-input-group button {
align-self: flex-end;
}
.btn-delete-history {
background: none;
border: none;
color: #ff3b30;
cursor: pointer;
padding: 5px;
opacity: 0.7;
transition: opacity 0.3s ease;
margin-left: auto; /* 将删除按钮推到右边 */
}
.btn-delete-history:hover {
opacity: 1;
}
.btn-delete-history i {
font-size: 14px;
}
.platform-buttons {
display: flex;
gap: 10px;
margin-top: 8px;
}
.platform-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
border: 1px solid #d2d2d7;
border-radius: 8px;
background: white;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
}
.platform-btn i {
font-size: 16px;
}
.platform-btn:hover {
border-color: #007AFF;
color: #007AFF;
}
.platform-btn.active {
background: #007AFF;
color: white;
border-color: #007AFF;
}
@media (max-width: 768px) {
.platform-buttons {
flex-direction: column;
}
.platform-btn {
width: 100%;
}
}
/* 确认对话框样式 */
.confirm-app-info {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin: 15px 0;
}
#confirm-app-icon {
width: 80px;
height: 80px;
border-radius: 12px;
object-fit: cover;
}
.confirm-app-details {
flex: 1;
}
.confirm-app-details p {
margin: 8px 0;
font-size: 14px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 15px;
}
.btn-secondary {
padding: 8px 16px;
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: #e5e5e5;
}
/* 添加搜索结果预览样式 */
.search-preview {
margin-top: 20px;
padding: 15px;
border-top: 1px solid #eee;
}
.preview-header {
margin-bottom: 15px;
}
.preview-header h4 {
color: #333;
font-size: 16px;
margin: 0;
}
.app-preview-card {
display: flex;
align-items: flex-start;
gap: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.preview-icon {
width: 80px;
height: 80px;
border-radius: 12px;
object-fit: cover;
}
.preview-info {
flex: 1;
}
.name-compare {
margin-bottom: 15px;
}
.name-compare p {
margin: 5px 0;
font-size: 14px;
}
.search-name {
color: #666;
}
.found-name {
color: #333;
}
.preview-actions {
display: flex;
gap: 10px;
}
.preview-actions button {
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
}
.preview-actions .btn-primary {
background: #007AFF;
color: white;
border: none;
}
.preview-actions .btn-primary:hover {
background: #0056b3;
}
.preview-actions .btn-secondary {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
}
.preview-actions .btn-secondary:hover {
background: #e5e5e5;
}
.retry-note {
font-size: 12px;
color: #666;
margin-top: 10px;
font-style: italic;
}
</style>
{% endblock %}