初始化鸿蒙应用展示平台项目 - 前后端分离架构

This commit is contained in:
Nvex
2025-10-25 11:45:17 +08:00
commit c0f81dbbe2
92 changed files with 40210 additions and 0 deletions

581
templates/admin_add.html Executable file
View 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 %}