581 lines
17 KiB
HTML
Executable File
581 lines
17 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-plus-circle"></i>
|
||
<h3>添加单个应用</h3>
|
||
</div>
|
||
</div>
|
||
<form onsubmit="submitAddAppForm(event, this)" enctype="multipart/form-data" class="admin-form">
|
||
<div class="form-group">
|
||
<label for="app-name">应用名称</label>
|
||
<input type="text" id="app-name" name="name" placeholder="请输入应用名称" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="app-icon">应用图标</label>
|
||
<div class="file-input-wrapper">
|
||
<input type="file" id="app-icon" name="icon">
|
||
<label for="app-icon" class="file-input-label">
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
<span>选择文件</span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="icon-url">或输入图标URL</label>
|
||
<input type="url" id="icon-url" name="icon_url" placeholder="http://example.com/icon.png">
|
||
</div>
|
||
</div>
|
||
<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="download-url">下载链接(可选)</label>
|
||
<input type="url" id="download-url" name="download_url" placeholder="http://example.com/download">
|
||
</div>
|
||
<button type="submit" class="btn-primary">
|
||
<i class="fas fa-plus"></i> 添加应用
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 批量导入 -->
|
||
<div class="admin-card">
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
<h3>批量导入应用</h3>
|
||
</div>
|
||
</div>
|
||
<form id="batch-import-form" class="admin-form">
|
||
<div class="form-group">
|
||
<label for="batch-category">选择分类</label>
|
||
<select id="batch-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="icon-type-options">
|
||
<label class="icon-option">
|
||
<input type="radio" name="icon_type" value="service" onchange="updateIconUrl()" required>
|
||
<span>元服务</span>
|
||
</label>
|
||
<label class="icon-option">
|
||
<input type="radio" name="icon_type" value="app" onchange="updateIconUrl()" required>
|
||
<span>应用</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="batch-icon-url" name="icon_url">
|
||
<div class="form-group">
|
||
<label for="batch-names">应用名称(用英文逗号分隔)</label>
|
||
<textarea id="batch-names" name="app_names" rows="5" placeholder="输入应用名称,多个应用用英文逗号分隔" required></textarea>
|
||
</div>
|
||
<button type="submit" class="btn-primary">
|
||
<i class="fas fa-cloud-upload-alt"></i> 批量导入
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 修改导入结果列表的标题和图标 -->
|
||
<div class="admin-card full-width" id="import-result" style="display: none;">
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<i class="fas fa-clock"></i>
|
||
<h3>最近导入</h3>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>图标</th>
|
||
<th>名称</th>
|
||
<th>分类</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="result-tbody">
|
||
<!-- 导入结果将在这里显示 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 添加通知函数
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
notification.innerHTML = message;
|
||
document.body.appendChild(notification);
|
||
|
||
// 添加动画样式
|
||
setTimeout(() => {
|
||
notification.classList.add('show');
|
||
}, 10);
|
||
|
||
// 3秒后移除通知
|
||
setTimeout(() => {
|
||
notification.classList.remove('show');
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// 原有的单个应用添加函数
|
||
function submitAddAppForm(event, form) {
|
||
event.preventDefault();
|
||
const formData = new FormData(form);
|
||
|
||
fetch('{{ url_for("add_app") }}', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification(data.message, 'success');
|
||
form.reset();
|
||
} else {
|
||
showNotification(data.error, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showNotification('操作失败,请重试', 'error');
|
||
console.error('Error:', error);
|
||
});
|
||
}
|
||
|
||
// 修改图标URL更新函数
|
||
function updateIconUrl() {
|
||
const iconType = document.querySelector('input[name="icon_type"]:checked')?.value;
|
||
const iconUrlInput = document.getElementById('batch-icon-url');
|
||
|
||
if (iconType === 'service') {
|
||
iconUrlInput.value = 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/yuanfuwuicon.png';
|
||
} else if (iconType === 'app') {
|
||
iconUrlInput.value = 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/harmonyos-next/images/hero/harmonyos-next-kv-2x.webp';
|
||
} else {
|
||
iconUrlInput.value = '';
|
||
}
|
||
}
|
||
|
||
// 修改批量导入处理函数
|
||
document.getElementById('batch-import-form').onsubmit = function(e) {
|
||
e.preventDefault();
|
||
|
||
const categoryId = document.getElementById('batch-category').value;
|
||
const iconUrl = document.getElementById('batch-icon-url').value;
|
||
const names = document.getElementById('batch-names').value;
|
||
|
||
if (!iconUrl) {
|
||
showNotification('请选择图标类型', 'error');
|
||
return;
|
||
}
|
||
|
||
// 分割应用名称并过滤空值
|
||
const apps = names.split(',')
|
||
.map(name => name.trim())
|
||
.filter(name => name)
|
||
.map(name => ({
|
||
name: name,
|
||
icon_url: iconUrl,
|
||
category_id: categoryId
|
||
}));
|
||
|
||
if (apps.length === 0) {
|
||
showNotification('请输入至少一个应用名称', 'error');
|
||
return;
|
||
}
|
||
|
||
// 发送请求前显示加载状态
|
||
const submitBtn = this.querySelector('button[type="submit"]');
|
||
const originalText = submitBtn.innerHTML;
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 导入中...';
|
||
|
||
fetch('{{ url_for("admin_batch_add") }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest' // 添加这个头部
|
||
},
|
||
body: JSON.stringify({apps: apps})
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification(data.message, 'success');
|
||
displayImportResult(apps, data.results);
|
||
this.reset();
|
||
updateIconUrl();
|
||
} else {
|
||
showNotification(data.error || '导入失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('导入失败,请重试', 'error');
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = originalText;
|
||
});
|
||
};
|
||
|
||
// 添加错误处理函数
|
||
function handleFetchError(response) {
|
||
if (!response.ok) {
|
||
return response.text().then(text => {
|
||
try {
|
||
// 尝试解析错误响应为 JSON
|
||
const data = JSON.parse(text);
|
||
throw new Error(data.error || '请求失败');
|
||
} catch (e) {
|
||
// 如果不是 JSON,返回原始错误文本
|
||
throw new Error(text || '请求失败');
|
||
}
|
||
});
|
||
}
|
||
return response.json();
|
||
}
|
||
|
||
function displayImportResult(apps, results) {
|
||
const tbody = document.getElementById('result-tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
apps.forEach(app => {
|
||
const tr = document.createElement('tr');
|
||
const isSuccess = results.success.includes(app.name);
|
||
const failReason = results.failed[app.name];
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<div class="app-icon-small">
|
||
<img src="${app.icon_url}" alt="${app.name}" loading="lazy">
|
||
</div>
|
||
</td>
|
||
<td>${app.name}</td>
|
||
<td>${document.getElementById('batch-category').options[document.getElementById('batch-category').selectedIndex].text}</td>
|
||
<td>
|
||
${isSuccess ?
|
||
`<button onclick="deleteApp('${app.name}')" class="btn-delete" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>` :
|
||
`<span class="error-text" title="${failReason}">导入失败</span>`
|
||
}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
document.getElementById('import-result').style.display = 'block';
|
||
}
|
||
|
||
// 修改删除应用函<E794A8><E587BD>
|
||
function deleteApp(appName) {
|
||
if (confirm(`确定要删除应用 "${appName}" 吗?`)) {
|
||
fetch(`/delete_app_by_name/${encodeURIComponent(appName)}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification('删除成功', 'success');
|
||
// 找到并移除对应的表格行
|
||
const rows = document.querySelectorAll('#result-tbody tr');
|
||
for (let row of rows) {
|
||
if (row.querySelector('td:nth-child(2)').textContent === appName) {
|
||
row.remove();
|
||
break;
|
||
}
|
||
}
|
||
// 如果表格为空,隐藏结果区域
|
||
if (document.getElementById('result-tbody').children.length === 0) {
|
||
document.getElementById('import-result').style.display = 'none';
|
||
}
|
||
} else {
|
||
showNotification(data.error || '删除失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('删除失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加 contains 选择器的 polyfill
|
||
if (!HTMLElement.prototype.contains) {
|
||
HTMLElement.prototype.contains = function(node) {
|
||
return this.textContent.includes(node);
|
||
}
|
||
}
|
||
|
||
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;
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
#batch-names {
|
||
width: 100%;
|
||
min-height: 100px;
|
||
padding: 10px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.preview-list {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.btn-primary {
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: help;
|
||
}
|
||
|
||
.status-badge.success {
|
||
background: #34c759;
|
||
color: white;
|
||
}
|
||
|
||
.status-badge.error {
|
||
background: #ff3b30;
|
||
color: white;
|
||
}
|
||
|
||
/* 通知样式 */
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 15px 25px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 1000;
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.notification.show {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.notification.success {
|
||
background: #34c759;
|
||
color: white;
|
||
}
|
||
|
||
.notification.error {
|
||
background: #ff3b30;
|
||
color: white;
|
||
}
|
||
|
||
/* 添加失败原因样式 */
|
||
.fail-reason {
|
||
font-size: 12px;
|
||
color: #ff3b30;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.icon-type-options {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.icon-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
padding: 8px 16px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.icon-option:hover {
|
||
border-color: #0066cc;
|
||
}
|
||
|
||
.icon-option input[type="radio"] {
|
||
margin: 0;
|
||
}
|
||
|
||
.icon-option input[type="radio"]:checked + span {
|
||
color: #0066cc;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.icon-option:has(input[type="radio"]:checked) {
|
||
border-color: #0066cc;
|
||
background-color: #f5f8ff;
|
||
}
|
||
|
||
/* 修改删除按钮样式 */
|
||
.btn-delete {
|
||
background: #ff3b30;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-delete:hover {
|
||
background: #ff1a1a;
|
||
}
|
||
|
||
.btn-delete i {
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 添加错误文本样式 */
|
||
.error-text {
|
||
color: #ff3b30;
|
||
font-size: 12px;
|
||
cursor: help;
|
||
}
|
||
|
||
/* 修改表格样式 */
|
||
.admin-table td {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.app-icon-small {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app-icon-small img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.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%;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %} |