初始化鸿蒙应用展示平台项目 - 前后端分离架构
This commit is contained in:
680
templates/admin_coming.html
Executable file
680
templates/admin_coming.html
Executable file
@@ -0,0 +1,680 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user