966 lines
27 KiB
HTML
Executable File
966 lines
27 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 full-width">
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<i class="fas fa-th-large"></i>
|
||
<h3>应用管理 {% if search %}(搜索结果){% else %}(共{{ total_count }}个){% endif %}</h3>
|
||
</div>
|
||
<div class="header-actions">
|
||
<select id="category-filter" onchange="filterByCategory(this.value)" class="category-select">
|
||
<option value="">全部分类</option>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}">{{ category.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<form class="search-form header-search" method="GET" action="{{ url_for('admin_apps') }}">
|
||
<div class="search-wrapper">
|
||
<i class="fas fa-search"></i>
|
||
<input type="text" name="search" placeholder="搜索应用..." value="{{ search }}">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>图标</th>
|
||
<th>名称</th>
|
||
<th>分类</th>
|
||
<th>添加时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="apps-tbody">
|
||
<!-- 应用列表将通过JavaScript动态加载 -->
|
||
</tbody>
|
||
</table>
|
||
<!-- 加载指示器 -->
|
||
<div id="loading-indicator" style="display: none; text-align: center; padding: 20px;">
|
||
<div class="spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
<!-- 加载更多按钮 -->
|
||
<div id="load-more" style="text-align: center; padding: 20px;">
|
||
<button onclick="loadMoreApps()" class="btn-primary">
|
||
<i class="fas fa-sync"></i> 加载更多
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.app-icon-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 300px;
|
||
}
|
||
|
||
.app-icon-small {
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app-icon-small img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.icon-edit-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.app-icon-small:hover .icon-edit-overlay {
|
||
opacity: 1;
|
||
}
|
||
|
||
.icon-edit-btn {
|
||
color: white;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transition: all 0.3s ease;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.icon-edit-btn:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.icon-url-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 4px 8px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.icon-url-input:hover {
|
||
border-color: #b2b2b2;
|
||
}
|
||
|
||
.icon-url-input:focus {
|
||
border-color: #0066cc;
|
||
outline: none;
|
||
color: #333;
|
||
}
|
||
|
||
/* 调整表格第一列的宽度 */
|
||
.admin-table td:first-child {
|
||
width: 320px;
|
||
max-width: 320px;
|
||
}
|
||
|
||
.tablet-filter {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.tablet-filter input {
|
||
padding: 8px 12px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.tablet-filter input:focus {
|
||
border-color: #007AFF;
|
||
outline: none;
|
||
}
|
||
|
||
.tablet-filter button {
|
||
padding: 8px 16px;
|
||
background: #007AFF;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.tablet-filter button:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
/* 添加成功提示样式 */
|
||
.filter-success {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 10px 20px;
|
||
background: #4CAF50;
|
||
color: white;
|
||
border-radius: 4px;
|
||
z-index: 1000;
|
||
animation: fadeInOut 3s forwards;
|
||
}
|
||
|
||
@keyframes fadeInOut {
|
||
0% { opacity: 0; transform: translateY(-20px); }
|
||
10% { opacity: 1; transform: translateY(0); }
|
||
90% { opacity: 1; transform: translateY(0); }
|
||
100% { opacity: 0; transform: translateY(-20px); }
|
||
}
|
||
|
||
.filter-card {
|
||
margin-bottom: 20px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.filter-content {
|
||
padding: 15px;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-group input {
|
||
flex: 1;
|
||
padding: 8px 12px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
min-width: 300px;
|
||
}
|
||
|
||
.filter-group input:focus {
|
||
border-color: #007AFF;
|
||
outline: none;
|
||
}
|
||
|
||
.filter-group button {
|
||
padding: 8px 16px;
|
||
background: #007AFF;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.3s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-group button:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
/* 批量筛选结果弹窗样式 */
|
||
.filter-result-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.filter-result-content {
|
||
position: relative;
|
||
background: white;
|
||
margin: 10% auto;
|
||
padding: 20px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.filter-result-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.filter-result-header h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.filter-result-close {
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
}
|
||
|
||
.filter-result-list {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.filter-result-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-result-item.success {
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.filter-result-item.error {
|
||
color: #f44336;
|
||
}
|
||
</style>
|
||
|
||
<div id="filterResultModal" class="filter-result-modal">
|
||
<div class="filter-result-content">
|
||
<div class="filter-result-header">
|
||
<h3>筛选结果</h3>
|
||
<span class="filter-result-close" onclick="closeFilterResultModal()">×</span>
|
||
</div>
|
||
<div id="filterResultList" class="filter-result-list">
|
||
<!-- 结果将在这里动态显示 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentPage = 1;
|
||
const pageSize = 50;
|
||
let loading = false;
|
||
let hasMore = true;
|
||
let lastScrollPosition = 0;
|
||
|
||
// 初始加载
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadApps();
|
||
});
|
||
|
||
// 滚动加载
|
||
document.addEventListener('scroll', () => {
|
||
// 获取当前滚动位置
|
||
const currentScroll = window.scrollY;
|
||
|
||
// 只在向下滚动时检查是否需要加载更多
|
||
if (currentScroll > lastScrollPosition) {
|
||
const scrollThreshold = document.documentElement.scrollHeight - window.innerHeight - 200;
|
||
if (currentScroll > scrollThreshold && !loading && hasMore) {
|
||
loadMoreApps();
|
||
}
|
||
}
|
||
|
||
// 更新上次滚动位置
|
||
lastScrollPosition = currentScroll;
|
||
});
|
||
|
||
// 添加分类筛选函数
|
||
function filterByCategory(categoryId) {
|
||
resetLoadState();
|
||
const tbody = document.getElementById('apps-tbody');
|
||
tbody.innerHTML = '';
|
||
loadApps(1, categoryId);
|
||
}
|
||
|
||
// 修改loadApps函数以支持分类筛选
|
||
function loadApps(page = 1, categoryId = '') {
|
||
if (loading || !hasMore) return;
|
||
|
||
loading = true;
|
||
const loadingIndicator = document.getElementById('loading-indicator');
|
||
loadingIndicator.style.display = 'block';
|
||
|
||
let url = "{{ url_for('admin_get_apps') }}?page=" + page + "&size=" + pageSize;
|
||
if (categoryId) {
|
||
url += "&category_id=" + categoryId;
|
||
}
|
||
{% if search %}
|
||
url += "&search={{ search }}";
|
||
{% endif %}
|
||
|
||
fetch(url)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const tbody = document.getElementById('apps-tbody');
|
||
|
||
if (page === 1) {
|
||
tbody.innerHTML = '';
|
||
}
|
||
|
||
if (data.success) {
|
||
data.apps.forEach(app => {
|
||
const tr = createAppRow(app);
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
hasMore = data.has_more;
|
||
document.getElementById('load-more').style.display = hasMore ? 'block' : 'none';
|
||
} else {
|
||
showNotification(data.error || '加载失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('加载失败,请重试', 'error');
|
||
})
|
||
.finally(() => {
|
||
loading = false;
|
||
loadingIndicator.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
function loadMoreApps() {
|
||
if (loading || !hasMore) return;
|
||
|
||
loading = true;
|
||
const loadingIndicator = document.getElementById('loading-indicator');
|
||
loadingIndicator.style.display = 'block';
|
||
|
||
let url = "{{ url_for('admin_get_apps') }}?page=" + currentPage + "&size=" + pageSize;
|
||
const categoryId = document.getElementById('category-filter')?.value;
|
||
if (categoryId) {
|
||
url += "&category_id=" + categoryId;
|
||
}
|
||
{% if search %}
|
||
url += "&search={{ search }}";
|
||
{% endif %}
|
||
|
||
fetch(url)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const tbody = document.getElementById('apps-tbody');
|
||
|
||
if (data.success) {
|
||
data.apps.forEach(app => {
|
||
const tr = createAppRow(app);
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
hasMore = data.has_more;
|
||
if (hasMore) {
|
||
currentPage++;
|
||
}
|
||
document.getElementById('load-more').style.display = hasMore ? 'block' : 'none';
|
||
} else {
|
||
showNotification(data.error || '加载失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('加载失败,请重试', 'error');
|
||
})
|
||
.finally(() => {
|
||
loading = false;
|
||
loadingIndicator.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
function createAppRow(app) {
|
||
const tr = document.createElement('tr');
|
||
tr.dataset.appId = app.id;
|
||
tr.innerHTML = `
|
||
<td>
|
||
<div class="app-icon-container">
|
||
<div class="app-icon-small">
|
||
<img src="${app.icon_path.startsWith('http') ? app.icon_path : '/static/uploads/' + app.icon_path}"
|
||
alt="${app.name}"
|
||
loading="lazy">
|
||
<div class="icon-edit-overlay">
|
||
<label class="icon-edit-btn" title="上传图标">
|
||
<i class="fas fa-camera"></i>
|
||
<input type="file"
|
||
class="icon-input"
|
||
accept="image/*"
|
||
onchange="updateIcon(${app.id}, this)"
|
||
style="display: none;">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<input type="url"
|
||
value="${app.icon_path}"
|
||
placeholder="图标URL"
|
||
onchange="updateIconUrl(${app.id}, this.value)"
|
||
class="icon-url-input">
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<form onsubmit="submitEditForm(event, this)" class="edit-form">
|
||
<input type="hidden" name="app_id" value="${app.id}">
|
||
<div class="edit-group">
|
||
<input type="text" name="name" value="${app.name}" required placeholder="应用名称">
|
||
<select name="category_id" required>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}"
|
||
${app.category_id === {{ category.id }} ? 'selected' : ''}>
|
||
{{ category.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<button type="submit" class="btn-edit" title="保存">
|
||
<i class="fas fa-save"></i>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</td>
|
||
<td>${app.category_name}</td>
|
||
<td>${app.created_at}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<a href="#" class="btn-delete" onclick="deleteApp(${app.id}); return false;">
|
||
<i class="fas fa-trash"></i>
|
||
</a>
|
||
</div>
|
||
</td>
|
||
`;
|
||
return tr;
|
||
}
|
||
|
||
function submitEditForm(event, form) {
|
||
event.preventDefault();
|
||
const formData = new FormData(form);
|
||
const appId = formData.get('app_id');
|
||
|
||
fetch('/edit_app/' + appId, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 显示成功消息
|
||
showNotification('修改成功', 'success');
|
||
|
||
// 更新分类名称显示
|
||
const row = form.closest('tr');
|
||
const categorySelect = form.querySelector('select[name="category_id"]');
|
||
const selectedOption = categorySelect.options[categorySelect.selectedIndex];
|
||
const categoryCell = row.querySelector('td:nth-child(3)'); // 第三列是分类显示列
|
||
categoryCell.textContent = selectedOption.textContent;
|
||
} else {
|
||
showNotification(data.error || '修改失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('保存失败,请重试', 'error');
|
||
});
|
||
}
|
||
|
||
function deleteApp(appId) {
|
||
if (confirm('确定要删除这个应用吗?')) {
|
||
fetch(`{{ url_for('delete_app', app_id=0) }}`.replace('0', appId), {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification(data.message, 'success');
|
||
// 找到并移除对应的表格行
|
||
const row = document.querySelector(`tr[data-app-id="${appId}"]`);
|
||
if (row) {
|
||
row.remove();
|
||
}
|
||
// 保持当前滚动位置
|
||
const currentScroll = window.scrollY;
|
||
window.scrollTo(0, currentScroll);
|
||
} else {
|
||
showNotification(data.error, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showNotification('删除失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
function updateIcon(appId, input) {
|
||
if (!input.files || !input.files[0]) {
|
||
showNotification('请选择图片文件', 'error');
|
||
return;
|
||
}
|
||
|
||
// 检查文件类型
|
||
const file = input.files[0];
|
||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||
if (!allowedTypes.includes(file.type)) {
|
||
showNotification('不支持的文件类型,请选择 jpg、png 或 gif 格式的图片', 'error');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小(限制为5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
showNotification('文件太大,请选择5MB以下的图片', 'error');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('icon', file);
|
||
|
||
// 修改获取图片元素的方式
|
||
const parentTd = input.closest('td');
|
||
if (!parentTd) {
|
||
showNotification('找不到图片元素', 'error');
|
||
return;
|
||
}
|
||
const imgElement = parentTd.querySelector('.app-icon-small img');
|
||
if (!imgElement) {
|
||
showNotification('找不到图片元素', 'error');
|
||
return;
|
||
}
|
||
|
||
const originalSrc = imgElement.src;
|
||
imgElement.style.opacity = '0.5';
|
||
|
||
console.log('开始上传图标:', {
|
||
appId: appId,
|
||
fileName: file.name,
|
||
fileType: file.type,
|
||
fileSize: file.size
|
||
});
|
||
|
||
fetch(`/admin/update_app_icon/${appId}`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'same-origin'
|
||
})
|
||
.then(response => {
|
||
console.log('服务器响应:', response.status);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
console.log('服务器响应数据:', data);
|
||
if (data.success) {
|
||
showNotification(data.message || '图标更新成功', 'success');
|
||
const newSrc = '/static/uploads/' + data.icon_path;
|
||
console.log('更新图标路径:', newSrc);
|
||
imgElement.src = newSrc;
|
||
} else {
|
||
console.error('更新失败:', data.error);
|
||
showNotification(data.error || '更新失败', 'error');
|
||
imgElement.src = originalSrc;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('请求错误:', error);
|
||
showNotification(`更新失败: ${error.message}`, 'error');
|
||
imgElement.src = originalSrc;
|
||
})
|
||
.finally(() => {
|
||
imgElement.style.opacity = '1';
|
||
input.value = '';
|
||
console.log('图标更新操作完成');
|
||
});
|
||
}
|
||
|
||
function showNotification(message, type = 'success') {
|
||
console.log(`显示通知: ${type} - ${message}`);
|
||
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.opacity = '0';
|
||
notification.style.transform = 'translateY(20px)';
|
||
notification.style.transition = 'all 0.3s ease';
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
setTimeout(() => {
|
||
notification.style.opacity = '1';
|
||
notification.style.transform = 'translateY(0)';
|
||
}, 10);
|
||
|
||
setTimeout(() => {
|
||
notification.style.opacity = '0';
|
||
notification.style.transform = 'translateY(20px)';
|
||
setTimeout(() => notification.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// 添加样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.app-icon-small {
|
||
position: relative;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.app-icon-small img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.icon-edit-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.app-icon-small:hover .icon-edit-overlay {
|
||
opacity: 1;
|
||
}
|
||
|
||
.app-icon-small:hover img {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.icon-edit-btn {
|
||
color: white;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transition: all 0.3s ease;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.icon-edit-btn:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.icon-edit-btn i {
|
||
font-size: 14px;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// 添加样式
|
||
const filterStyle = document.createElement('style');
|
||
filterStyle.textContent = `
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
}
|
||
|
||
.category-select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #1d1d1f;
|
||
background-color: white;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.category-select:hover {
|
||
border-color: #0066cc;
|
||
}
|
||
|
||
.category-select:focus {
|
||
outline: none;
|
||
border-color: #0066cc;
|
||
box-shadow: 0 0 0 2px rgba(0,102,204,0.1);
|
||
}
|
||
`;
|
||
document.head.appendChild(filterStyle);
|
||
|
||
function updateIconUrl(appId, newUrl) {
|
||
if (!newUrl) return;
|
||
|
||
if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) {
|
||
showNotification('请输入有效的URL地址', 'error');
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
const tr = document.querySelector(`tr[data-app-id="${appId}"]`);
|
||
const input = tr ? tr.querySelector('.icon-url-input') : null;
|
||
const img = tr ? tr.querySelector('.app-icon-small img') : null;
|
||
const originalUrl = img ? img.src : '';
|
||
|
||
if (input) input.disabled = true;
|
||
if (img) img.style.opacity = '0.5';
|
||
|
||
// 添加错误处理和重试逻辑
|
||
const updateRequest = async (retryCount = 0) => {
|
||
try {
|
||
// 修改为使用 url_for 生成的完整路径
|
||
const response = await fetch("{{ url_for('update_app_icon_url', app_id=0) }}".replace('0', appId), {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ icon_url: newUrl }),
|
||
credentials: 'same-origin'
|
||
});
|
||
|
||
// 检查响应状态
|
||
if (!response.ok) {
|
||
// 如果是 401 未授权,重定向到登录页面
|
||
if (response.status === 401) {
|
||
window.location.href = "{{ url_for('login') }}";
|
||
return;
|
||
}
|
||
|
||
// 如果是 429 请求过多,等待后重试
|
||
if (response.status === 429 && retryCount < 3) {
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
return updateRequest(retryCount + 1);
|
||
}
|
||
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showNotification('图标链接更新成功', 'success');
|
||
if (img) {
|
||
img.src = newUrl;
|
||
img.style.opacity = '1';
|
||
}
|
||
} else {
|
||
throw new Error(data.error || '更新失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
showNotification(error.message || '更新失败,请重试', 'error');
|
||
|
||
// 恢复原始状态
|
||
if (img) {
|
||
img.src = originalUrl;
|
||
img.style.opacity = '1';
|
||
}
|
||
if (input) {
|
||
input.value = originalUrl;
|
||
}
|
||
} finally {
|
||
if (input) input.disabled = false;
|
||
if (img) img.style.opacity = '1';
|
||
}
|
||
};
|
||
|
||
// 开始更新请求
|
||
updateRequest();
|
||
}
|
||
|
||
// 修改防抖函数
|
||
const debounce = (func, wait) => {
|
||
let timeout;
|
||
return (...args) => {
|
||
clearTimeout(timeout);
|
||
return new Promise(resolve => {
|
||
timeout = setTimeout(() => {
|
||
resolve(func.apply(this, args));
|
||
}, wait);
|
||
});
|
||
};
|
||
};
|
||
|
||
// 使用防抖包装更新函数
|
||
const debouncedUpdateIconUrl = debounce(updateIconUrl, 500);
|
||
|
||
// 修改事件监听器
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const tbody = document.getElementById('apps-tbody');
|
||
|
||
// 使事件委托处理图标URL输入
|
||
tbody.addEventListener('change', async (e) => {
|
||
if (e.target.classList.contains('icon-url-input')) {
|
||
const tr = e.target.closest('tr');
|
||
const appId = tr.dataset.appId;
|
||
if (appId) {
|
||
try {
|
||
await debouncedUpdateIconUrl(appId, e.target.value);
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
showNotification('更新失败,请重试', 'error');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 添加通知函数
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
notification.textContent = message;
|
||
|
||
Object.assign(notification.style, {
|
||
position: 'fixed',
|
||
bottom: '20px',
|
||
right: '20px',
|
||
padding: '10px 20px',
|
||
borderRadius: '4px',
|
||
backgroundColor: type === 'success' ? '#4CAF50' : '#f44336',
|
||
color: 'white',
|
||
zIndex: '1000',
|
||
opacity: '0',
|
||
transform: 'translateY(20px)',
|
||
transition: 'all 0.3s ease'
|
||
});
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// 触发重排以应用过渡效果
|
||
notification.offsetHeight;
|
||
|
||
notification.style.opacity = '1';
|
||
notification.style.transform = 'translateY(0)';
|
||
|
||
setTimeout(() => {
|
||
notification.style.opacity = '0';
|
||
notification.style.transform = 'translateY(20px)';
|
||
setTimeout(() => notification.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// 添加回车键触发筛选
|
||
document.getElementById('tabletAppFilter').addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
filterTabletApps();
|
||
}
|
||
});
|
||
|
||
// 点击弹窗外部关闭
|
||
window.onclick = function(event) {
|
||
const modal = document.getElementById('filterResultModal');
|
||
if (event.target == modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 添加重置加载状态的函数
|
||
function resetLoadState() {
|
||
currentPage = 1;
|
||
hasMore = true;
|
||
loading = false;
|
||
lastScrollPosition = 0;
|
||
}
|
||
</script>
|
||
{% endblock %} |