402 lines
17 KiB
HTML
402 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}发布版本 - 软件授权管理系统{% endblock %}
|
||
|
||
{% block page_title %}发布版本{% endblock %}
|
||
|
||
{% block page_actions %}
|
||
<a href="{{ url_for('web.versions') }}" class="btn btn-outline-secondary">
|
||
<i class="fas fa-arrow-left me-2"></i>
|
||
返回列表
|
||
</a>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<div class="card shadow">
|
||
<div class="card-body">
|
||
<form id="version-form" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<label for="product_id" class="form-label">选择产品 *</label>
|
||
<select class="form-select" id="product_id" name="product_id" required>
|
||
<option value="">请选择产品</option>
|
||
{% for product in products %}
|
||
<option value="{{ product.product_id }}" {% if request.args.get('product_id') == product.product_id %}selected{% endif %}>
|
||
{{ product.product_name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="version_num" class="form-label">版本号 *</label>
|
||
<input type="text" class="form-control" id="version_num" name="version_num" required>
|
||
<div class="form-text">版本号格式建议使用 x.x.x 格式,如 1.0.0</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="platform" class="form-label">平台</label>
|
||
<input type="text" class="form-control" id="platform" name="platform" placeholder="如: Windows, macOS, Linux">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="description" class="form-label">版本描述</label>
|
||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="update_log" class="form-label">更新日志</label>
|
||
<textarea class="form-control" id="update_log" name="update_log" rows="5"></textarea>
|
||
</div>
|
||
|
||
<!-- 文件提供方式选择 -->
|
||
<div class="mb-3">
|
||
<label class="form-label">文件提供方式 *</label>
|
||
<ul class="nav nav-tabs" id="fileSourceTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload-content" type="button" role="tab">上传文件</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link-content" type="button" role="tab">文件链接</button>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content border border-top-0 p-3" id="fileSourceTabsContent">
|
||
<!-- 文件上传区域 -->
|
||
<div class="tab-pane fade show active" id="upload-content" role="tabpanel">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<button type="button" class="btn btn-outline-primary btn-sm" id="upload-file-btn">
|
||
<i class="fas fa-upload me-1"></i>选择文件
|
||
</button>
|
||
<input type="file" id="version_file" name="version_file" accept=".zip,.rar,.7z,.exe,.dmg,.pkg" class="d-none">
|
||
<div id="upload-status" class="text-muted">未选择文件</div>
|
||
</div>
|
||
<div id="file-info" class="small text-muted d-none">
|
||
<div>文件名: <span id="file-name"></span></div>
|
||
<div>文件大小: <span id="file-size"></span></div>
|
||
<div>文件哈希: <span id="file-hash"></span></div>
|
||
<input type="hidden" id="file_url" name="file_url">
|
||
<input type="hidden" id="file_hash" name="file_hash">
|
||
</div>
|
||
<div class="form-text">支持常见的压缩包和安装包格式</div>
|
||
</div>
|
||
|
||
<!-- 文件链接区域 -->
|
||
<div class="tab-pane fade" id="link-content" role="tabpanel">
|
||
<div class="mb-3">
|
||
<label for="file_link" class="form-label">文件下载链接</label>
|
||
<input type="url" class="form-control" id="file_link" name="download_url" placeholder="https://example.com/software.zip">
|
||
<div class="form-text">请输入完整的文件下载链接</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="link_file_name" class="form-label">文件名(可选)</label>
|
||
<input type="text" class="form-control" id="link_file_name" name="link_file_name" placeholder="software.zip">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3 form-check">
|
||
<input type="checkbox" class="form-check-input" id="publish_now" name="publish_now">
|
||
<label class="form-check-label" for="publish_now">立即发布</label>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||
<i class="fas fa-save me-2"></i>
|
||
<span id="submit-text">创建版本</span>
|
||
</button>
|
||
<a href="{{ url_for('web.versions') }}" class="btn btn-secondary">取消</a>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card shadow">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">发布说明</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<ul class="list-unstyled">
|
||
<li class="mb-2">
|
||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||
<small>必须选择一个产品才能创建版本</small>
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||
<small>版本号建议使用语义化版本格式</small>
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||
<small>可以先创建版本再上传文件,或直接上传文件</small>
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||
<small>立即发布选项会直接将版本设为可用状态</small>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initEventListeners();
|
||
});
|
||
|
||
// 初始化事件监听器
|
||
function initEventListeners() {
|
||
// 表单提交
|
||
document.getElementById('version-form').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
createVersion();
|
||
});
|
||
|
||
// 文件选择按钮点击事件
|
||
document.getElementById('upload-file-btn').addEventListener('click', function() {
|
||
document.getElementById('version_file').click();
|
||
});
|
||
|
||
// 文件选择变化事件
|
||
document.getElementById('version_file').addEventListener('change', function(e) {
|
||
if (e.target.files.length > 0) {
|
||
uploadFile(e.target.files[0]);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 上传文件
|
||
function uploadFile(file) {
|
||
const uploadBtn = document.getElementById('upload-file-btn');
|
||
const uploadStatus = document.getElementById('upload-status');
|
||
|
||
// 显示上传状态
|
||
uploadBtn.disabled = true;
|
||
uploadStatus.textContent = '上传中...';
|
||
|
||
// 创建FormData对象
|
||
const formData = new FormData();
|
||
formData.append('version_file', file);
|
||
|
||
// 发送上传请求
|
||
const uploadUrl = '/api/v1/versions/upload';
|
||
|
||
apiRequest(uploadUrl, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(data => {
|
||
console.log('Upload response data:', data);
|
||
if (data.success) {
|
||
// 显示文件信息
|
||
document.getElementById('file-name').textContent = data.data.file_name;
|
||
document.getElementById('file-size').textContent = formatFileSize(data.data.file_size);
|
||
document.getElementById('file-hash').textContent = data.data.file_hash;
|
||
document.getElementById('file_url').value = data.data.file_url;
|
||
document.getElementById('file_hash').value = data.data.file_hash;
|
||
|
||
// 显示文件信息区域
|
||
document.getElementById('file-info').classList.remove('d-none');
|
||
uploadStatus.textContent = '上传成功';
|
||
} else {
|
||
uploadStatus.textContent = data.message || '上传失败';
|
||
showNotification(data.message || '文件上传失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('文件上传失败:', error);
|
||
uploadStatus.textContent = '上传失败';
|
||
showNotification('文件上传失败,请稍后重试: ' + error.message, 'error');
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
uploadBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// 格式化文件大小
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
// 创建版本
|
||
function createVersion() {
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
const submitText = document.getElementById('submit-text');
|
||
|
||
// 获取表单数据
|
||
const formData = new FormData();
|
||
formData.append('product_id', document.getElementById('product_id').value);
|
||
formData.append('version_num', document.getElementById('version_num').value.trim());
|
||
|
||
const platform = document.getElementById('platform').value.trim();
|
||
if (platform) {
|
||
formData.append('platform', platform);
|
||
}
|
||
|
||
const description = document.getElementById('description').value.trim();
|
||
if (description) {
|
||
formData.append('description', description);
|
||
}
|
||
|
||
const updateLog = document.getElementById('update_log').value.trim();
|
||
if (updateLog) {
|
||
formData.append('update_log', updateLog);
|
||
}
|
||
|
||
// 检查当前选择的文件提供方式
|
||
const activeTab = document.querySelector('.nav-link.active').id;
|
||
|
||
if (activeTab === 'upload-tab') {
|
||
// 上传文件方式
|
||
const fileUrl = document.getElementById('file_url').value;
|
||
if (fileUrl) {
|
||
formData.append('download_url', fileUrl);
|
||
}
|
||
|
||
const fileHash = document.getElementById('file_hash').value;
|
||
if (fileHash) {
|
||
formData.append('file_hash', fileHash);
|
||
}
|
||
} else if (activeTab === 'link-tab') {
|
||
// 文件链接方式
|
||
const downloadUrl = document.getElementById('file_link').value.trim();
|
||
console.log('Link tab selected, downloadUrl:', downloadUrl);
|
||
if (downloadUrl) {
|
||
formData.append('download_url', downloadUrl);
|
||
console.log('Added download_url to formData:', downloadUrl);
|
||
|
||
// 如果提供了文件名,则使用提供的文件名
|
||
const linkFileName = document.getElementById('link_file_name').value.trim();
|
||
console.log('Link file name:', linkFileName);
|
||
if (linkFileName) {
|
||
// 生成一个虚拟的文件哈希(基于文件名)
|
||
const fileHash = generateSimpleHash(linkFileName);
|
||
formData.append('file_hash', fileHash);
|
||
console.log('Generated file hash:', fileHash);
|
||
} else {
|
||
// 如果没有提供文件名但有URL,基于URL生成一个简单的哈希
|
||
const fileHash = generateSimpleHash(downloadUrl);
|
||
formData.append('file_hash', fileHash);
|
||
console.log('Generated file hash from URL:', fileHash);
|
||
}
|
||
} else {
|
||
console.log('No download URL provided');
|
||
}
|
||
}
|
||
|
||
// 额外检查:确保无论哪个tab被激活,只要download_url元素中有值就添加到formData
|
||
// 这是为了防止tab切换状态不一致的问题
|
||
if (!formData.has('download_url')) {
|
||
// 首先检查链接输入框
|
||
const downloadUrlInput = document.getElementById('file_link');
|
||
if (downloadUrlInput && downloadUrlInput.value.trim()) {
|
||
formData.append('download_url', downloadUrlInput.value.trim());
|
||
console.log('Fallback: Added download_url from file_link input:', downloadUrlInput.value.trim());
|
||
}
|
||
// 如果链接输入框没有值,检查上传文件的URL
|
||
else {
|
||
const fileUrlInput = document.getElementById('file_url');
|
||
if (fileUrlInput && fileUrlInput.value.trim()) {
|
||
formData.append('download_url', fileUrlInput.value.trim());
|
||
console.log('Fallback: Added download_url from file_url input:', fileUrlInput.value.trim());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 调试:打印所有表单数据
|
||
console.log('FormData contents:');
|
||
for (let pair of formData.entries()) {
|
||
console.log(pair[0] + ': ' + pair[1]);
|
||
}
|
||
|
||
const publishNow = document.getElementById('publish_now').checked;
|
||
formData.append('publish_now', publishNow);
|
||
|
||
// 基础验证
|
||
if (!document.getElementById('product_id').value) {
|
||
showNotification('请选择产品', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!document.getElementById('version_num').value.trim()) {
|
||
showNotification('请输入版本号', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 验证文件提供方式
|
||
if (activeTab === 'upload-tab') {
|
||
// 上传方式不需要强制要求文件,但如果有文件则使用
|
||
} else if (activeTab === 'link-tab') {
|
||
const downloadUrl = document.getElementById('file_link').value.trim();
|
||
if (downloadUrl && !isValidUrl(downloadUrl)) {
|
||
showNotification('请输入有效的文件链接', 'warning');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 显示加载状态
|
||
submitBtn.disabled = true;
|
||
submitText.textContent = '创建中...';
|
||
|
||
// 发送请求
|
||
showLoading();
|
||
const createUrl = '/api/v1/versions';
|
||
|
||
apiRequest(createUrl, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification('版本创建成功', 'success');
|
||
// 延迟跳转到版本列表
|
||
setTimeout(() => {
|
||
window.location.href = '/versions';
|
||
}, 1500);
|
||
} else {
|
||
showNotification(data.message || '创建失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideLoading();
|
||
console.error('Failed to create version:', error);
|
||
showNotification('网络错误,请稍后重试', 'error');
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
submitBtn.disabled = false;
|
||
submitText.textContent = '创建版本';
|
||
});
|
||
}
|
||
|
||
// 简单的哈希生成函数
|
||
function generateSimpleHash(str) {
|
||
let hash = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash; // 转换为32位整数
|
||
}
|
||
return Math.abs(hash).toString(16);
|
||
}
|
||
|
||
// 验证URL格式
|
||
function isValidUrl(string) {
|
||
try {
|
||
new URL(string);
|
||
return true;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %} |