Files
ns2.0/templates/admin_coming.html

680 lines
19 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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-clock"></i>
<h3>即将上线应用管理</h3>
</div>
</div>
<form onsubmit="submitAddComingAppForm(event, this)" class="admin-form" enctype="multipart/form-data">
<div class="form-group">
<label for="app-name">应用名称</label>
<input type="text" id="app-name" name="name" required placeholder="请输入应用名称">
</div>
<div class="form-group">
<label>图标选择方式</label>
<div class="icon-method-selector">
<label>
<input type="radio" name="icon_method" value="upload" checked onchange="toggleIconMethod(this)">
上传图片
</label>
<label>
<input type="radio" name="icon_method" value="url" onchange="toggleIconMethod(this)">
图标链接
</label>
</div>
</div>
<div class="form-group" id="icon-upload-group">
<label for="app-icon">应用图标</label>
<div class="file-input-wrapper">
<input type="file" id="app-icon" name="icon" accept="image/*">
<label for="app-icon" class="file-input-label">
<i class="fas fa-cloud-upload-alt"></i>
<span>选择图标</span>
</label>
</div>
</div>
<div class="form-group" id="icon-url-group" style="display: none;">
<label for="icon-url">图标链接</label>
<input type="url" id="icon-url" name="icon_url" placeholder="请输入图标链接 (http:// 或 https://)">
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-plus"></i> 添加到即将上线
</button>
</form>
<div class="apps-list">
<div class="apps-header">
<div class="sort-handle-header"></div>
<div>图标</div>
<div>名称</div>
<div>添加时间</div>
<div>操作</div>
</div>
<div id="sortableApps">
{% for app in coming_apps %}
<div class="app-item" data-id="{{ app.id }}">
<div class="sort-handle">
<i class="fas fa-grip-vertical"></i>
</div>
<div class="app-icon-cell">
<div class="app-icon-container">
<div class="app-icon-small">
{% if 'http' in app.icon_path %}
<img src="{{ app.icon_path }}" alt="{{ app.name }}" id="icon-{{ app.id }}">
{% else %}
<img src="{{ url_for('static', filename='uploads/' + app.icon_path) }}" alt="{{ app.name }}" id="icon-{{ app.id }}">
{% endif %}
<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>
</div>
<div class="app-name">{{ app.name }}</div>
<div class="app-time">{{ app.created_at }}</div>
<div class="app-actions">
<button onclick="removeFromComing({{ app.id }})" class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 初始化拖拽排序
const sortableContainer = document.getElementById('sortableApps');
if (sortableContainer) {
try {
new Sortable(sortableContainer, {
animation: 150,
handle: '.sort-handle',
ghostClass: 'dragging',
onEnd: function(evt) {
const items = sortableContainer.getElementsByClassName('app-item');
const newOrder = Array.from(items).map(item => parseInt(item.dataset.id));
fetch('/admin/coming/update_order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin',
body: JSON.stringify({ order: newOrder })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showNotification('排序已更新', 'success');
} else {
throw new Error(data.error || '更新失败');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('更新失败,请重试', 'error');
setTimeout(() => location.reload(), 2000);
});
}
});
} catch (error) {
console.error('Failed to initialize Sortable:', error);
}
}
// 修复图标URL更新函数
function updateIconUrl(appId, newUrl) {
if (!newUrl) return;
if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) {
showNotification('请输入有效的URL地址', 'error');
return;
}
const appItem = document.querySelector(`.app-item[data-id="${appId}"]`);
if (!appItem) {
console.error('App item not found:', appId);
return;
}
const input = appItem.querySelector('.icon-url-input');
const img = appItem.querySelector('.app-icon-small img');
if (!input || !img) {
console.error('Required elements not found');
return;
}
const originalUrl = img.src;
input.disabled = true;
img.style.opacity = '0.5';
fetch('/admin/coming/update_icon_url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
app_id: appId,
icon_url: newUrl
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('图标链接更新成功', 'success');
img.src = newUrl;
} else {
throw new Error(data.error || '更新失败');
}
})
.catch(error => {
showNotification(error.message || '更新失败,请重试', 'error');
img.src = originalUrl;
input.value = originalUrl;
})
.finally(() => {
input.disabled = false;
img.style.opacity = '1';
});
}
// 将 updateIconUrl 函数添加到全局作用域
window.updateIconUrl = updateIconUrl;
});
// 更新图标
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;
}
if (file.size > 5 * 1024 * 1024) {
showNotification('文件太大请选择5MB以下的图片', 'error');
return;
}
const formData = new FormData();
formData.append('icon', file);
formData.append('app_id', appId);
const imgElement = input.closest('.app-icon-small').querySelector('img');
const originalSrc = imgElement.src;
imgElement.style.opacity = '0.5';
fetch('/admin/coming/update_icon', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('图标更新成功', 'success');
imgElement.src = data.icon_path.startsWith('http') ?
data.icon_path :
`/static/uploads/${data.icon_path}`;
} else {
showNotification(data.error || '更新失败', 'error');
imgElement.src = originalSrc;
}
})
.catch(error => {
showNotification('更新失败,请重试', 'error');
imgElement.src = originalSrc;
})
.finally(() => {
imgElement.style.opacity = '1';
input.value = '';
});
}
// 显示通知
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);
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);
}
// 保持原有的其他函数不变
function toggleIconMethod(radio) {
const uploadGroup = document.getElementById('icon-upload-group');
const urlGroup = document.getElementById('icon-url-group');
if (radio.value === 'upload') {
uploadGroup.style.display = 'block';
urlGroup.style.display = 'none';
document.getElementById('icon-url').value = '';
} else {
uploadGroup.style.display = 'none';
urlGroup.style.display = 'block';
document.getElementById('app-icon').value = '';
}
}
function submitAddComingAppForm(event, form) {
event.preventDefault();
const formData = new FormData(form);
// 根据选择的方式处理图标
const iconMethod = form.querySelector('input[name="icon_method"]:checked').value;
if (iconMethod === 'upload' && !formData.get('icon').size) {
alert('请选择图标文件');
return;
}
if (iconMethod === 'url' && !formData.get('icon_url')) {
alert('请输入图标链接');
return;
}
fetch('/admin/coming/add', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || '添加失败');
}
});
}
function removeFromComing(appId) {
if (confirm('确定要从即将上线列表中移除这个应用吗?')) {
fetch(`/admin/coming/remove/${appId}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const appItem = document.querySelector(`.app-item[data-id="${appId}"]`);
if (appItem) {
appItem.remove();
showNotification('应用已移除', 'success');
}
} else {
throw new Error(data.message || '删除失败');
}
})
.catch(error => {
console.error('Error:', error);
showNotification(error.message || '删除失败,请重试', 'error');
});
}
}
</script>
<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;
}
/* 保持原有的其他样式不变 */
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
.file-input-label {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.file-input-label:hover {
background: #e5e5e5;
}
[data-theme="dark"] .file-input-label {
background: #2c2c2e;
}
[data-theme="dark"] .file-input-label:hover {
background: #3c3c3e;
}
.icon-method-selector {
display: flex;
gap: 20px;
margin-bottom: 10px;
}
.icon-method-selector label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.icon-method-selector input[type="radio"] {
margin: 0;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 500px;
border-radius: 8px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.icon-edit-tabs {
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.tab-btn {
padding: 10px 20px;
border: none;
background: none;
cursor: pointer;
}
.tab-btn.active {
border-bottom: 2px solid #007bff;
color: #007bff;
}
.tab-content {
padding: 20px 0;
}
.handle {
cursor: move;
color: #666;
}
.icon-actions {
margin-top: 5px;
}
.btn-icon {
background: none;
border: none;
color: #007bff;
cursor: pointer;
padding: 5px;
}
.btn-icon:hover {
color: #0056b3;
}
[data-theme="dark"] .modal-content {
background-color: #2c2c2e;
color: #fff;
}
[data-theme="dark"] .close {
color: #fff;
}
[data-theme="dark"] .tab-btn {
color: #fff;
}
[data-theme="dark"] .tab-btn.active {
border-bottom-color: #0a84ff;
color: #0a84ff;
}
.apps-list {
margin-top: 20px;
}
.apps-header {
display: grid;
grid-template-columns: 50px 300px 1fr 150px 100px;
padding: 10px;
background: #f5f5f7;
border-radius: 8px;
margin-bottom: 10px;
font-weight: 500;
}
.app-item {
display: grid;
grid-template-columns: 50px 300px 1fr 150px 100px;
align-items: center;
padding: 12px;
background: white;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.3s ease;
cursor: move;
}
.app-item:hover {
background: #f8f9fa;
}
.sort-handle {
color: #999;
cursor: move;
display: flex;
align-items: center;
justify-content: center;
}
.sort-handle i {
font-size: 16px;
}
.app-name {
font-size: 14px;
}
.app-time {
font-size: 13px;
color: #666;
}
.app-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* 拖拽时的样式 */
.app-item.dragging {
opacity: 0.5;
background: #f0f0f0;
}
.app-item.drag-over {
border-top: 2px solid #007AFF;
}
</style>
{% endblock %}