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