f103148ebf
基于 Flask + MySQL + Bootstrap 5 的全栈个人资料库管理系统。 主要功能: - 管理员/普通用户双角色权限体系,全站登录保护 - 资源管理:文本、图片、音频、视频四类资源 - 三种添加方式:本地上传(拖拽)、URL 后台下载、磁力下载(aria2c) - 在线预览:文本、图片、HTML5 音视频播放器 - 安全:bcrypt 加盐密码哈希、CSRF 防护、SQLAlchemy ORM 防注入 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
335 lines
14 KiB
HTML
335 lines
14 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}添加资源{% endblock %}
|
|
|
|
{% block breadcrumb %}
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('resources.list_resources') }}">我的资源</a></li>
|
|
<li class="breadcrumb-item active">添加资源</li>
|
|
</ol>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h4 class="mb-4"><i class="bi bi-plus-circle me-2"></i>添加资源</h4>
|
|
|
|
<!-- Tab 导航 -->
|
|
<ul class="nav nav-tabs mb-4" id="uploadTabs" role="tablist">
|
|
<li class="nav-item">
|
|
<button class="nav-link {{ 'active' if tab == 'upload' }}"
|
|
data-bs-toggle="tab" data-bs-target="#tabUpload" type="button">
|
|
<i class="bi bi-upload me-1"></i>本地上传
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link {{ 'active' if tab == 'url' }}"
|
|
id="urlTabBtn"
|
|
data-bs-toggle="tab" data-bs-target="#tabUrl" type="button">
|
|
<i class="bi bi-link-45deg me-1"></i>URL 下载
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link {{ 'active' if tab == 'magnet' }}"
|
|
id="magnetTabBtn"
|
|
data-bs-toggle="tab" data-bs-target="#tabMagnet" type="button">
|
|
<i class="bi bi-magnet me-1"></i>磁力下载
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
|
|
<!-- ── 本地上传 ── -->
|
|
<div class="tab-pane fade {{ 'show active' if tab == 'upload' else '' }}" id="tabUpload">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-7">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-4">
|
|
<form method="POST" action="{{ url_for('resources.upload') }}"
|
|
enctype="multipart/form-data" novalidate id="uploadForm">
|
|
{{ form.hidden_tag() if tab == 'upload' else '' }}
|
|
{% if tab == 'upload' %}{{ form.hidden_tag() }}{% endif %}
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">标题 <span class="text-danger">*</span></label>
|
|
<input type="text" name="title" class="form-control" placeholder="资源标题" required
|
|
{% if tab=='upload' and form.title.data %}value="{{ form.title.data }}"{% endif %}>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">选择文件 <span class="text-danger">*</span></label>
|
|
<!-- 拖拽上传区 -->
|
|
<div class="upload-drop-zone" id="dropZone">
|
|
<i class="bi bi-cloud-arrow-up fs-1 text-muted"></i>
|
|
<p class="mt-2 mb-1">拖拽文件到此处,或点击选择</p>
|
|
<p class="text-muted small">支持文本、图片、音频、视频</p>
|
|
<input type="file" name="file" id="fileInput" class="d-none"
|
|
accept=".txt,.md,.csv,.json,.xml,.log,.html,.htm,
|
|
.jpg,.jpeg,.png,.gif,.webp,.bmp,.svg,
|
|
.mp3,.wav,.ogg,.flac,.m4a,.aac,
|
|
.mp4,.webm,.avi,.mkv,.mov,.wmv">
|
|
</div>
|
|
<div id="filePreview" class="mt-2 d-none">
|
|
<div class="d-flex align-items-center gap-2 p-2 border rounded">
|
|
<i class="bi bi-file-earmark fs-4 text-primary" id="fileIcon"></i>
|
|
<div class="flex-grow-1 overflow-hidden">
|
|
<div class="text-truncate fw-medium" id="fileName"></div>
|
|
<div class="text-muted small" id="fileSize"></div>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
|
onclick="clearFile()"><i class="bi bi-x"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">类型</label>
|
|
<select name="resource_type" class="form-select">
|
|
<option value="">— 自动识别 —</option>
|
|
<option value="text">文本</option>
|
|
<option value="image">图片</option>
|
|
<option value="audio">音频</option>
|
|
<option value="video">视频</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">标签</label>
|
|
<input type="text" name="tags" class="form-control"
|
|
placeholder="用逗号分隔多个标签">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label fw-medium">描述</label>
|
|
<textarea name="description" class="form-control" rows="2"
|
|
placeholder="可选描述"></textarea>
|
|
</div>
|
|
|
|
<!-- 上传进度 -->
|
|
<div id="uploadProgress" class="d-none mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="small">上传中…</span>
|
|
<span class="small" id="uploadPct">0%</span>
|
|
</div>
|
|
<div class="progress">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
id="uploadBar" style="width:0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary w-100" id="submitBtn">
|
|
<i class="bi bi-cloud-upload me-1"></i>立即上传
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── URL 下载 ── -->
|
|
<div class="tab-pane fade {{ 'show active' if tab == 'url' else '' }}" id="tabUrl">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-7">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="alert alert-info small">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
填写 URL 后,系统将在后台下载文件,可在资源详情页查看进度。
|
|
</div>
|
|
<form method="POST" action="{{ url_for('resources.url_download') }}" novalidate>
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">标题 <span class="text-danger">*</span></label>
|
|
<input type="text" name="title" class="form-control" placeholder="资源标题" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">下载 URL <span class="text-danger">*</span></label>
|
|
<input type="url" name="source_url" class="form-control"
|
|
placeholder="https://example.com/file.mp4" required>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">类型</label>
|
|
<select name="resource_type" class="form-select">
|
|
<option value="">— 自动识别 —</option>
|
|
<option value="text">文本</option>
|
|
<option value="image">图片</option>
|
|
<option value="audio">音频</option>
|
|
<option value="video">视频</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">标签</label>
|
|
<input type="text" name="tags" class="form-control" placeholder="逗号分隔">
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="form-label fw-medium">描述</label>
|
|
<textarea name="description" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-success w-100">
|
|
<i class="bi bi-cloud-download me-1"></i>开始下载
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── 磁力下载 ── -->
|
|
<div class="tab-pane fade {{ 'show active' if tab == 'magnet' else '' }}" id="tabMagnet">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-7">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="alert alert-warning small">
|
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
|
磁力下载需要服务器安装 <strong>aria2c</strong>。下载任务在后台进行,可在资源详情页查看进度。
|
|
</div>
|
|
<form method="POST" action="{{ url_for('resources.magnet_download') }}" novalidate>
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">标题 <span class="text-danger">*</span></label>
|
|
<input type="text" name="title" class="form-control" placeholder="资源标题" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-medium">磁力链接 <span class="text-danger">*</span></label>
|
|
<textarea name="magnet_uri" class="form-control" rows="3"
|
|
placeholder="magnet:?xt=urn:btih:..." required></textarea>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">类型</label>
|
|
<select name="resource_type" class="form-select">
|
|
<option value="video">视频</option>
|
|
<option value="audio">音频</option>
|
|
<option value="image">图片</option>
|
|
<option value="text">文本</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">标签</label>
|
|
<input type="text" name="tags" class="form-control" placeholder="逗号分隔">
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="form-label fw-medium">描述</label>
|
|
<textarea name="description" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-warning w-100">
|
|
<i class="bi bi-magnet me-1"></i>开始磁力下载
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /tab-content -->
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// ── 拖拽上传区 ──────────────────────────────────────────────────────────────
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
if (dropZone && fileInput) {
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
|
|
['dragenter','dragover'].forEach(e =>
|
|
dropZone.addEventListener(e, ev => {
|
|
ev.preventDefault();
|
|
dropZone.classList.add('drop-active');
|
|
})
|
|
);
|
|
['dragleave','drop'].forEach(e =>
|
|
dropZone.addEventListener(e, ev => {
|
|
ev.preventDefault();
|
|
dropZone.classList.remove('drop-active');
|
|
})
|
|
);
|
|
dropZone.addEventListener('drop', ev => {
|
|
const dt = ev.dataTransfer;
|
|
if (dt.files.length) {
|
|
fileInput.files = dt.files;
|
|
showFilePreview(dt.files[0]);
|
|
}
|
|
});
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files.length) showFilePreview(fileInput.files[0]);
|
|
});
|
|
}
|
|
|
|
function showFilePreview(file) {
|
|
document.getElementById('fileName').textContent = file.name;
|
|
document.getElementById('fileSize').textContent = formatSize(file.size);
|
|
document.getElementById('filePreview').classList.remove('d-none');
|
|
dropZone.classList.add('d-none');
|
|
// 自动填入标题
|
|
const titleInput = document.querySelector('input[name="title"]');
|
|
if (titleInput && !titleInput.value) {
|
|
titleInput.value = file.name.replace(/\.[^.]+$/, '');
|
|
}
|
|
}
|
|
|
|
function clearFile() {
|
|
fileInput.value = '';
|
|
document.getElementById('filePreview').classList.add('d-none');
|
|
dropZone.classList.remove('d-none');
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
const units = ['B','KB','MB','GB'];
|
|
let i = 0;
|
|
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; }
|
|
return `${bytes.toFixed(1)} ${units[i]}`;
|
|
}
|
|
|
|
// ── 上传进度(XHR)────────────────────────────────────────────────────────────
|
|
const uploadForm = document.getElementById('uploadForm');
|
|
if (uploadForm) {
|
|
uploadForm.addEventListener('submit', function(e) {
|
|
if (!fileInput || !fileInput.files.length) return; // 让浏览器验证
|
|
e.preventDefault();
|
|
const bar = document.getElementById('uploadBar');
|
|
const pct = document.getElementById('uploadPct');
|
|
document.getElementById('uploadProgress').classList.remove('d-none');
|
|
document.getElementById('submitBtn').disabled = true;
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.upload.addEventListener('progress', ev => {
|
|
if (ev.lengthComputable) {
|
|
const p = Math.round(ev.loaded * 100 / ev.total);
|
|
bar.style.width = p + '%';
|
|
pct.textContent = p + '%';
|
|
}
|
|
});
|
|
xhr.addEventListener('load', () => {
|
|
if (xhr.responseURL) window.location.href = xhr.responseURL;
|
|
else location.reload();
|
|
});
|
|
xhr.addEventListener('error', () => {
|
|
alert('上传失败,请重试');
|
|
document.getElementById('submitBtn').disabled = false;
|
|
});
|
|
xhr.open('POST', uploadForm.action);
|
|
xhr.send(new FormData(uploadForm));
|
|
});
|
|
}
|
|
|
|
// ── 激活正确的 Tab ────────────────────────────────────────────────────────────
|
|
const activeTab = '{{ tab }}';
|
|
if (activeTab === 'url') {
|
|
document.getElementById('urlTabBtn')?.click();
|
|
} else if (activeTab === 'magnet') {
|
|
document.getElementById('magnetTabBtn')?.click();
|
|
}
|
|
</script>
|
|
{% endblock %}
|