1026 lines
29 KiB
HTML
Executable File
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()">×</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()">×</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 %} |