第一次提交

This commit is contained in:
2026-03-25 15:24:22 +08:00
commit 0f8ac68d4d
156 changed files with 42365 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}创建套餐 - {{ config.SITE_NAME or '太一软件授权管理系统' }}{% endblock %}
{% block page_title %}创建套餐{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.packages', product_id=product.product_id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回套餐列表
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<form id="package-form">
<input type="hidden" id="product_id" value="{{ product.product_id }}">
<div class="mb-3">
<label for="name" class="form-label">套餐名称 *</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">套餐描述</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="price" class="form-label">价格 *</label>
<input type="number" class="form-control" id="price" step="0.01" min="0" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="duration" class="form-label">时长(天) *</label>
<select class="form-select" id="duration" required>
<option value="1">1天(天卡)</option>
<option value="30">30天(月卡)</option>
<option value="90">90天(季卡)</option>
<option value="365">365天(年卡)</option>
<option value="-1">永久</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="max_devices" class="form-label">最大设备数</label>
<input type="number" class="form-control" id="max_devices" min="1" value="1">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="stock" class="form-label">库存(-1为无限)</label>
<input type="number" class="form-control" id="stock" min="-1" value="-1">
</div>
</div>
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">排序</label>
<input type="number" class="form-control" id="sort_order" min="0" value="0">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="status" checked>
<label class="form-check-label" for="status">
启用
</label>
</div>
</div>
<button type="submit" class="btn btn-primary" id="save-btn">
<i class="fas fa-save me-2"></i>
<span id="save-text">保存套餐</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 表单提交
document.getElementById('package-form').addEventListener('submit', function(e) {
e.preventDefault();
savePackage();
});
}
// 保存套餐
function savePackage() {
const saveBtn = document.getElementById('save-btn');
const saveText = document.getElementById('save-text');
// 显示加载状态
saveBtn.disabled = true;
saveText.textContent = '保存中...';
// 收集表单数据
const formData = {
product_id: document.getElementById('product_id').value,
name: document.getElementById('name').value,
description: document.getElementById('description').value,
price: parseFloat(document.getElementById('price').value),
duration: parseInt(document.getElementById('duration').value),
max_devices: parseInt(document.getElementById('max_devices').value),
stock: parseInt(document.getElementById('stock').value),
sort_order: parseInt(document.getElementById('sort_order').value),
status: document.getElementById('status').checked ? 1 : 0
};
// 发送请求保存套餐
apiRequest('/api/v1/packages', {
method: 'POST',
body: JSON.stringify(formData)
})
.then(data => {
if (data.success) {
showNotification('套餐创建成功', 'success');
// 跳转到套餐列表页面
setTimeout(() => {
window.location.href = `/packages?product_id=${formData.product_id}`;
}, 1000);
} else {
showNotification('保存失败: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('保存失败,请查看控制台了解详情', 'error');
})
.finally(() => {
saveBtn.disabled = false;
saveText.textContent = '保存套餐';
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,158 @@
{% extends "base.html" %}
{% block title %}编辑套餐 - {{ config.SITE_NAME or '太一软件授权管理系统' }}{% endblock %}
{% block page_title %}编辑套餐{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.packages', product_id=product.product_id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回套餐列表
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<form id="package-form">
<input type="hidden" id="package_id" value="{{ package.package_id }}">
<input type="hidden" id="product_id" value="{{ product.product_id }}">
<div class="mb-3">
<label for="name" class="form-label">套餐名称 *</label>
<input type="text" class="form-control" id="name" value="{{ package.name }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">套餐描述</label>
<textarea class="form-control" id="description" rows="3">{{ package.description or '' }}</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="price" class="form-label">价格 *</label>
<input type="number" class="form-control" id="price" step="0.01" min="0" value="{{ package.price }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="duration" class="form-label">时长(天) *</label>
<select class="form-select" id="duration" required>
<option value="1" {% if package.duration == 1 %}selected{% endif %}>1天(天卡)</option>
<option value="30" {% if package.duration == 30 %}selected{% endif %}>30天(月卡)</option>
<option value="90" {% if package.duration == 90 %}selected{% endif %}>90天(季卡)</option>
<option value="365" {% if package.duration == 365 %}selected{% endif %}>365天(年卡)</option>
<option value="-1" {% if package.duration == -1 %}selected{% endif %}>永久</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="max_devices" class="form-label">最大设备数</label>
<input type="number" class="form-control" id="max_devices" min="1" value="{{ package.max_devices }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="stock" class="form-label">库存(-1为无限)</label>
<input type="number" class="form-control" id="stock" min="-1" value="{{ package.stock }}">
</div>
</div>
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">排序</label>
<input type="number" class="form-control" id="sort_order" min="0" value="{{ package.sort_order }}">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="status" {% if package.status == 1 %}checked{% endif %}>
<label class="form-check-label" for="status">
启用
</label>
</div>
</div>
<button type="submit" class="btn btn-primary" id="save-btn">
<i class="fas fa-save me-2"></i>
<span id="save-text">更新套餐</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 表单提交
document.getElementById('package-form').addEventListener('submit', function(e) {
e.preventDefault();
updatePackage();
});
}
// 更新套餐
function updatePackage() {
const saveBtn = document.getElementById('save-btn');
const saveText = document.getElementById('save-text');
const packageId = document.getElementById('package_id').value;
// 显示加载状态
saveBtn.disabled = true;
saveText.textContent = '保存中...';
// 收集表单数据
const formData = {
name: document.getElementById('name').value,
description: document.getElementById('description').value,
price: parseFloat(document.getElementById('price').value),
duration: parseInt(document.getElementById('duration').value),
max_devices: parseInt(document.getElementById('max_devices').value),
stock: parseInt(document.getElementById('stock').value),
sort_order: parseInt(document.getElementById('sort_order').value),
status: document.getElementById('status').checked ? 1 : 0
};
// 发送请求更新套餐
apiRequest(`/api/v1/packages/${packageId}`, {
method: 'PUT',
body: JSON.stringify(formData)
})
.then(data => {
if (data.success) {
showNotification('套餐更新成功', 'success');
// 跳转到套餐列表页面
setTimeout(() => {
window.location.href = `/packages?product_id=${document.getElementById('product_id').value}`;
}, 1000);
} else {
showNotification('更新失败: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('更新失败,请查看控制台了解详情', 'error');
})
.finally(() => {
saveBtn.disabled = false;
saveText.textContent = '更新套餐';
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,218 @@
{% extends "base.html" %}
{% block title %}套餐管理 - {{ config.SITE_NAME or '太一软件授权管理系统' }}{% endblock %}
{% block page_title %}套餐管理{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.products') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回产品列表
</a>
<a href="{{ url_for('web.create_package') }}?product_id={{ product.product_id }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
创建套餐
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="mb-0">产品信息</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">产品ID:</dt>
<dd class="col-sm-8"><code>{{ product.product_id }}</code></dd>
<dt class="col-sm-4">产品名称:</dt>
<dd class="col-sm-8">{{ product.product_name }}</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">状态:</dt>
<dd class="col-sm-8">
<span class="badge {% if product.status == 1 %}bg-success{% else %}bg-secondary{% endif %}">
{{ product.status_name }}
</span>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>套餐名称</th>
<th>价格</th>
<th>时长</th>
<th>最大设备数</th>
<th>库存</th>
<th>状态</th>
<th>排序</th>
<th>操作</th>
</tr>
</thead>
<tbody id="package-list">
<tr>
<td colspan="8" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
确定要删除套餐 "<strong id="delete-package-name"></strong>" 吗?此操作不可恢复。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirm-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadPackages();
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 删除确认
document.getElementById('confirm-delete').addEventListener('click', function() {
const packageId = this.dataset.packageId;
deletePackage(packageId);
});
}
// 加载套餐列表
function loadPackages() {
const product_id = "{{ product.product_id }}";
apiRequest(`/api/v1/packages?product_id=${product_id}`)
.then(data => {
if (data && data.success) {
renderPackageList(data.data.packages);
} else {
renderPackageList([]);
showNotification('加载套餐列表失败: ' + (data.message || '未知错误'), 'error');
}
})
.catch(error => {
renderPackageList([]);
showNotification('加载套餐列表失败: ' + error.message, 'error');
});
}
// 渲染套餐列表
function renderPackageList(packages) {
const tbody = document.getElementById('package-list');
if (!Array.isArray(packages) || packages.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = packages.map(pkg => `
<tr>
<td>
<strong>${pkg.name}</strong>
${pkg.description ? `<br><small class="text-muted">${pkg.description}</small>` : ''}
</td>
<td>¥${pkg.price.toFixed(2)}</td>
<td>${pkg.duration_text}</td>
<td>${pkg.max_devices}</td>
<td>${pkg.stock === -1 ? '无限' : pkg.stock}</td>
<td>
<span class="badge ${pkg.status === 1 ? 'bg-success' : 'bg-secondary'}">
${pkg.status_name}
</span>
</td>
<td>${pkg.sort_order}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/packages/${pkg.package_id}/edit" class="btn btn-outline-primary" title="编辑">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger btn-delete"
data-package-id="${pkg.package_id}"
data-package-name="${pkg.name}"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('');
// 绑定删除按钮事件
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', function() {
const packageId = this.dataset.packageId;
const packageName = this.dataset.packageName;
showDeleteModal(packageId, packageName);
});
});
}
// 显示删除确认模态框
function showDeleteModal(packageId, packageName) {
document.getElementById('delete-package-name').textContent = packageName;
document.getElementById('confirm-delete').dataset.packageId = packageId;
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
// 删除套餐
function deletePackage(packageId) {
apiRequest(`/api/v1/packages/${packageId}`, {
method: 'DELETE'
})
.then(data => {
if (data && data.success) {
showNotification('套餐删除成功', 'success');
loadPackages();
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
modal.hide();
} else {
showNotification('删除失败: ' + (data.message || '未知错误'), 'error');
}
})
.catch(error => {
showNotification('删除失败: ' + error.message, 'error');
});
}
</script>
{% endblock %}