第一次提交
This commit is contained in:
242
app/web/templates/license/export.html
Normal file
242
app/web/templates/license/export.html
Normal file
@@ -0,0 +1,242 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}导出卡密 - 太一软件授权管理系统{% endblock %}
|
||||
|
||||
{% block page_title %}导出卡密{% endblock %}
|
||||
|
||||
{% block page_actions %}
|
||||
<a href="{{ url_for('web.licenses') }}" 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="export-form">
|
||||
<div class="mb-3">
|
||||
<label for="product_id" class="form-label">选择产品</label>
|
||||
<select class="form-select" id="product_id" name="product_id">
|
||||
<option value="">全部产品</option>
|
||||
{% for product in products %}
|
||||
<option value="{{ product.product_id }}">{{ product.product_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">卡密状态</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="0">未激活</option>
|
||||
<option value="1">已激活</option>
|
||||
<option value="2">已过期</option>
|
||||
<option value="3">已禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">卡密类型</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="">全部类型</option>
|
||||
<option value="1">正式卡密</option>
|
||||
<option value="0">试用卡密</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="format" class="form-label">导出格式 *</label>
|
||||
<select class="form-select" id="format" name="format" required>
|
||||
<option value="excel">Excel (.xlsx)</option>
|
||||
<option value="csv">CSV (.csv)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success" id="submit-btn">
|
||||
<i class="fas fa-file-export me-2"></i>
|
||||
<span id="submit-text">导出卡密</span>
|
||||
</button>
|
||||
<a href="{{ url_for('web.licenses') }}" 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>导出格式支持Excel和CSV两种</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('export-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
exportLicenses();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出卡密
|
||||
function exportLicenses() {
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
|
||||
// 获取表单数据
|
||||
const formData = {
|
||||
product_id: document.getElementById('product_id').value || null,
|
||||
status: document.getElementById('status').value ? parseInt(document.getElementById('status').value) : null,
|
||||
type: document.getElementById('type').value ? parseInt(document.getElementById('type').value) : null,
|
||||
format: document.getElementById('format').value
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = '导出中...';
|
||||
showLoading();
|
||||
|
||||
// 构建API URL(复用base.html中的URL构建逻辑)
|
||||
let apiUrl = '/api/v1/licenses/export';
|
||||
const frontendDomain = window.FRONTEND_DOMAIN || '';
|
||||
|
||||
if (apiUrl.startsWith('/')) {
|
||||
if (frontendDomain && !apiUrl.startsWith(frontendDomain)) {
|
||||
let cleanDomain = frontendDomain;
|
||||
try {
|
||||
const urlObj = new URL(frontendDomain.startsWith('http') ? frontendDomain : 'http://' + frontendDomain);
|
||||
cleanDomain = urlObj.origin;
|
||||
} catch (e) {
|
||||
if (frontendDomain.includes('/')) {
|
||||
cleanDomain = frontendDomain.split('/')[0];
|
||||
}
|
||||
}
|
||||
apiUrl = cleanDomain + apiUrl;
|
||||
} else if (!frontendDomain) {
|
||||
apiUrl = window.location.origin + apiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 直接使用fetch处理文件下载,不使用apiRequest(因为apiRequest会尝试解析JSON)
|
||||
fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => {
|
||||
hideLoading();
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
// 处理错误响应
|
||||
if (response.status === 401) {
|
||||
showNotification('会话已过期,请重新登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1500);
|
||||
throw new Error('未授权访问');
|
||||
} else if (response.status === 403) {
|
||||
return response.json().then(errorData => {
|
||||
showNotification(errorData.message || '权限不足,无法执行此操作', 'error');
|
||||
throw new Error(`403: ${errorData.message || '权限不足'}`);
|
||||
}).catch(() => {
|
||||
showNotification('权限不足,无法执行此操作', 'error');
|
||||
throw new Error('403: 权限不足');
|
||||
});
|
||||
} else {
|
||||
// 尝试解析错误信息
|
||||
return response.json().then(errorData => {
|
||||
showNotification(errorData.message || `导出失败: ${response.statusText}`, 'error');
|
||||
throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
|
||||
}).catch(() => {
|
||||
showNotification(`导出失败: ${response.statusText}`, 'error');
|
||||
throw new Error(`${response.status}: ${response.statusText}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 成功响应,处理文件下载
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'licenses.xlsx';
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].replace(/['"]/g, '');
|
||||
// 处理URL编码的文件名
|
||||
try {
|
||||
filename = decodeURIComponent(filename);
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始文件名
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
return response.blob().then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
showNotification('导出成功', 'success');
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
console.error('Failed to export licenses:', error);
|
||||
// 如果错误消息不是我们自定义的,显示通用错误消息
|
||||
if (error.message && !error.message.includes(':')) {
|
||||
showNotification('导出失败: ' + error.message, 'error');
|
||||
} else if (!error.message.includes('未授权') && !error.message.includes('权限不足')) {
|
||||
showNotification('导出失败,请稍后重试', 'error');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = '导出卡密';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
254
app/web/templates/license/generate.html
Normal file
254
app/web/templates/license/generate.html
Normal file
@@ -0,0 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}生成卡密 - 太一软件授权管理系统{% endblock %}
|
||||
|
||||
{% block page_title %}生成卡密{% endblock %}
|
||||
|
||||
{% block page_actions %}
|
||||
<a href="{{ url_for('web.licenses') }}" 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="generate-form">
|
||||
<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 }}">{{ product.product_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="license_type" class="form-label">卡密类型 *</label>
|
||||
<select class="form-select" id="license_type" name="license_type" required>
|
||||
<option value="1">正式卡密</option>
|
||||
<option value="0">试用卡密</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">卡密时长 *</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="btn-group" role="group" id="duration-type-group">
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-day" value="1" checked>
|
||||
<label class="btn btn-outline-primary" for="duration-day">天卡(1天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-month" value="30">
|
||||
<label class="btn btn-outline-primary" for="duration-month">月卡(30天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-quarter" value="90">
|
||||
<label class="btn btn-outline-primary" for="duration-quarter">季卡(90天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-year" value="365">
|
||||
<label class="btn btn-outline-primary" for="duration-year">年卡(365天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-permanent" value="-1">
|
||||
<label class="btn btn-outline-primary" for="duration-permanent">永久卡</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-custom" value="custom">
|
||||
<label class="btn btn-outline-primary" for="duration-custom">自定义</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input type="number" class="form-control" id="expire_days" name="expire_days" min="1" max="3650" value="1" style="display: none;">
|
||||
<div class="form-text" id="duration-help-text">选择卡密的有效期类型</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="quantity" class="form-label">生成数量 *</label>
|
||||
<input type="number" class="form-control" id="quantity" name="quantity" min="1" max="1000" value="1" required>
|
||||
<div class="form-text">单次最多生成1000个卡密</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="max_devices" class="form-label">最大绑定设备数</label>
|
||||
<input type="number" class="form-control" id="max_devices" name="max_devices" min="1" max="100" value="1">
|
||||
<div class="form-text">同一卡密最多可绑定的设备数量</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">备注</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||
<i class="fas fa-key me-2"></i>
|
||||
<span id="submit-text">生成卡密</span>
|
||||
</button>
|
||||
<a href="{{ url_for('web.licenses') }}" 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>试用卡密会自动添加TRIAL_前缀</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();
|
||||
// 初始化时长选择器
|
||||
initDurationSelector();
|
||||
});
|
||||
|
||||
// 初始化事件监听器
|
||||
function initEventListeners() {
|
||||
// 表单提交
|
||||
document.getElementById('generate-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
generateLicenses();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化时长选择器
|
||||
function initDurationSelector() {
|
||||
// 获取所有时长类型单选按钮
|
||||
const durationRadios = document.querySelectorAll('input[name="duration_type"]');
|
||||
const expireDaysInput = document.getElementById('expire_days');
|
||||
const durationHelpText = document.getElementById('duration-help-text');
|
||||
|
||||
// 为每个单选按钮添加事件监听器
|
||||
durationRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.value === 'custom') {
|
||||
// 显示自定义输入框
|
||||
expireDaysInput.style.display = 'block';
|
||||
expireDaysInput.focus();
|
||||
durationHelpText.textContent = '请输入自定义有效期天数(1-3650天)';
|
||||
} else {
|
||||
// 隐藏自定义输入框
|
||||
expireDaysInput.style.display = 'none';
|
||||
durationHelpText.textContent = '选择卡密的有效期类型';
|
||||
|
||||
// 如果选择了永久卡,更新帮助文本
|
||||
if (this.value === '-1') {
|
||||
durationHelpText.textContent = '永久卡密永不过期';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 生成卡密
|
||||
function generateLicenses() {
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
|
||||
// 获取表单数据
|
||||
const formData = {
|
||||
product_id: document.getElementById('product_id').value,
|
||||
type: parseInt(document.getElementById('license_type').value),
|
||||
count: parseInt(document.getElementById('quantity').value),
|
||||
valid_days: getSelectedDuration(),
|
||||
prefix: '',
|
||||
length: 32
|
||||
};
|
||||
|
||||
// 基础验证
|
||||
if (!formData.product_id) {
|
||||
showNotification('请选择产品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.count < 1 || formData.count > 10000) {
|
||||
showNotification('生成数量必须在1-10000之间', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.valid_days && (formData.valid_days < -1 || formData.valid_days > 3650 || formData.valid_days === 0)) {
|
||||
showNotification('有效期必须在1-3650天之间,或-1表示永久', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.valid_days === null) {
|
||||
showNotification('请选择卡密时长类型', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = '生成中...';
|
||||
|
||||
// 发送请求
|
||||
apiRequest('/api/v1/licenses', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(`成功生成 ${data.data.count} 个卡密`, 'success');
|
||||
// 延迟跳转到卡密列表
|
||||
setTimeout(() => {
|
||||
window.location.href = '/licenses';
|
||||
}, 1500);
|
||||
} else {
|
||||
showNotification(data.message || '生成失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to generate licenses:', error);
|
||||
showNotification('网络错误,请稍后重试', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = '生成卡密';
|
||||
});
|
||||
}
|
||||
|
||||
// 获取选中的时长
|
||||
function getSelectedDuration() {
|
||||
const selectedRadio = document.querySelector('input[name="duration_type"]:checked');
|
||||
if (!selectedRadio) return null;
|
||||
|
||||
if (selectedRadio.value === 'custom') {
|
||||
const expireDaysInput = document.getElementById('expire_days');
|
||||
const customValue = parseInt(expireDaysInput.value);
|
||||
return isNaN(customValue) ? null : customValue;
|
||||
}
|
||||
|
||||
return parseInt(selectedRadio.value);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
279
app/web/templates/license/import.html
Normal file
279
app/web/templates/license/import.html
Normal file
@@ -0,0 +1,279 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}导入卡密 - 太一软件授权管理系统{% endblock %}
|
||||
|
||||
{% block page_title %}导入卡密{% endblock %}
|
||||
|
||||
{% block page_actions %}
|
||||
<a href="{{ url_for('web.licenses') }}" 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="import-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 }}">{{ product.product_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="license_file" class="form-label">选择文件 *</label>
|
||||
<input type="file" class="form-control" id="license_file" name="license_file" accept=".txt,.csv" required>
|
||||
<div class="form-text">支持TXT或CSV格式文件,每行一个卡密</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="license_type" class="form-label">卡密类型</label>
|
||||
<select class="form-select" id="license_type" name="license_type">
|
||||
<option value="">自动识别</option>
|
||||
<option value="1">正式卡密</option>
|
||||
<option value="0">试用卡密</option>
|
||||
</select>
|
||||
<div class="form-text">如果选择自动识别,系统会根据卡密前缀判断类型</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">卡密时长</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="btn-group" role="group" id="duration-type-group">
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-day" value="1" checked>
|
||||
<label class="btn btn-outline-primary" for="duration-day">天卡(1天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-month" value="30">
|
||||
<label class="btn btn-outline-primary" for="duration-month">月卡(30天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-quarter" value="90">
|
||||
<label class="btn btn-outline-primary" for="duration-quarter">季卡(90天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-year" value="365">
|
||||
<label class="btn btn-outline-primary" for="duration-year">年卡(365天)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-permanent" value="-1">
|
||||
<label class="btn btn-outline-primary" for="duration-permanent">永久卡</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duration_type" id="duration-custom" value="custom">
|
||||
<label class="btn btn-outline-primary" for="duration-custom">自定义</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input type="number" class="form-control" id="expire_days" name="expire_days" min="1" max="3650" value="1" style="display: none;">
|
||||
<div class="form-text" id="duration-help-text">选择卡密的有效期类型</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="max_devices" class="form-label">最大绑定设备数</label>
|
||||
<input type="number" class="form-control" id="max_devices" name="max_devices" min="1" max="100" value="1">
|
||||
<div class="form-text">同一卡密最多可绑定的设备数量</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">备注</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||
<i class="fas fa-file-import me-2"></i>
|
||||
<span id="submit-text">导入卡密</span>
|
||||
</button>
|
||||
<a href="{{ url_for('web.licenses') }}" 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>支持CSV格式,第一列为卡密</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>
|
||||
|
||||
<h6 class="mt-3">示例文件格式</h6>
|
||||
<pre class="bg-light p-2 small">LICENSE_KEY_001
|
||||
LICENSE_KEY_002
|
||||
TRIAL_KEY_001
|
||||
TRIAL_KEY_002</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEventListeners();
|
||||
// 初始化时长选择器
|
||||
initDurationSelector();
|
||||
});
|
||||
|
||||
// 初始化事件监听器
|
||||
function initEventListeners() {
|
||||
// 表单提交
|
||||
document.getElementById('import-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
importLicenses();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化时长选择器
|
||||
function initDurationSelector() {
|
||||
// 获取所有时长类型单选按钮
|
||||
const durationRadios = document.querySelectorAll('input[name="duration_type"]');
|
||||
const expireDaysInput = document.getElementById('expire_days');
|
||||
const durationHelpText = document.getElementById('duration-help-text');
|
||||
|
||||
// 为每个单选按钮添加事件监听器
|
||||
durationRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.value === 'custom') {
|
||||
// 显示自定义输入框
|
||||
expireDaysInput.style.display = 'block';
|
||||
expireDaysInput.focus();
|
||||
durationHelpText.textContent = '请输入自定义有效期天数(1-3650天)';
|
||||
} else {
|
||||
// 隐藏自定义输入框
|
||||
expireDaysInput.style.display = 'none';
|
||||
durationHelpText.textContent = '选择卡密的有效期类型';
|
||||
|
||||
// 如果选择了永久卡,更新帮助文本
|
||||
if (this.value === '-1') {
|
||||
durationHelpText.textContent = '永久卡密永不过期';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 导入卡密
|
||||
function importLicenses() {
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
const fileInput = document.getElementById('license_file');
|
||||
|
||||
// 获取表单数据
|
||||
const formData = new FormData();
|
||||
formData.append('product_id', document.getElementById('product_id').value);
|
||||
formData.append('license_file', fileInput.files[0]);
|
||||
|
||||
const licenseType = document.getElementById('license_type').value;
|
||||
if (licenseType) {
|
||||
formData.append('license_type', licenseType);
|
||||
}
|
||||
|
||||
// 获取选中的时长
|
||||
const validDays = getSelectedDuration();
|
||||
if (validDays !== null) {
|
||||
formData.append('expire_days', validDays);
|
||||
}
|
||||
|
||||
const maxDevices = document.getElementById('max_devices').value;
|
||||
if (maxDevices) {
|
||||
formData.append('max_devices', maxDevices);
|
||||
}
|
||||
|
||||
const description = document.getElementById('description').value.trim();
|
||||
if (description) {
|
||||
formData.append('description', description);
|
||||
}
|
||||
|
||||
// 基础验证
|
||||
if (!document.getElementById('product_id').value) {
|
||||
showNotification('请选择产品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileInput.files[0]) {
|
||||
showNotification('请选择要导入的文件', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = '导入中...';
|
||||
|
||||
// 发送请求
|
||||
showLoading();
|
||||
apiRequest('/api/v1/licenses/import', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
hideLoading();
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(`成功导入 ${data.data.imported} 个卡密,跳过 ${data.data.skipped} 个重复卡密`, 'success');
|
||||
// 延迟跳转到卡密列表
|
||||
setTimeout(() => {
|
||||
window.location.href = '/licenses';
|
||||
}, 2000);
|
||||
} else {
|
||||
showNotification(data.message || '导入失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
console.error('Failed to import licenses:', error);
|
||||
showNotification('导入失败,请检查文件格式后重试', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = '导入卡密';
|
||||
});
|
||||
}
|
||||
|
||||
// 获取选中的时长
|
||||
function getSelectedDuration() {
|
||||
const selectedRadio = document.querySelector('input[name="duration_type"]:checked');
|
||||
if (!selectedRadio) return null;
|
||||
|
||||
if (selectedRadio.value === 'custom') {
|
||||
const expireDaysInput = document.getElementById('expire_days');
|
||||
const customValue = parseInt(expireDaysInput.value);
|
||||
return isNaN(customValue) ? null : customValue;
|
||||
}
|
||||
|
||||
return parseInt(selectedRadio.value);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
1192
app/web/templates/license/list.html
Normal file
1192
app/web/templates/license/list.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user