修复登录错误

This commit is contained in:
taiyi 2025-11-12 15:11:05 +08:00
parent ecca300271
commit c7264ff597
19 changed files with 1984 additions and 167 deletions

View File

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
</project>

View File

@ -123,4 +123,112 @@ def delete_device(device_id):
except Exception as e:
db.session.rollback()
current_app.logger.error(f"删除设备失败: {str(e)}")
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/devices/batch', methods=['DELETE'])
@require_admin
def batch_delete_devices():
"""批量删除设备"""
try:
data = request.get_json()
if not data or 'device_ids' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少device_ids字段'
}), 400
device_ids = data['device_ids']
if not isinstance(device_ids, list) or len(device_ids) == 0:
return jsonify({
'success': False,
'message': 'device_ids必须是非空列表'
}), 400
# 查找所有要删除的设备
devices = Device.query.filter(Device.device_id.in_(device_ids)).all()
if len(devices) != len(device_ids):
found_ids = [d.device_id for d in devices]
missing_ids = [did for did in device_ids if did not in found_ids]
return jsonify({
'success': False,
'message': f'以下设备不存在: {", ".join(map(str, missing_ids))}'
}), 404
# 批量删除设备
for device in devices:
db.session.delete(device)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功删除 {len(devices)} 个设备'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量删除设备失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/devices/batch/status', methods=['PUT'])
@require_admin
def batch_update_device_status():
"""批量更新设备状态"""
try:
data = request.get_json()
if not data or 'device_ids' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少device_ids/status字段'
}), 400
device_ids = data['device_ids']
status = data['status']
if not isinstance(device_ids, list) or len(device_ids) == 0:
return jsonify({
'success': False,
'message': 'device_ids必须是非空列表'
}), 400
if status not in [0, 1, 2]:
return jsonify({
'success': False,
'message': 'status必须是0(禁用)、1(正常)或2(离线)'
}), 400
# 查找所有要更新的设备
devices = Device.query.filter(Device.device_id.in_(device_ids)).all()
if len(devices) != len(device_ids):
found_ids = [d.device_id for d in devices]
missing_ids = [did for did in device_ids if did not in found_ids]
return jsonify({
'success': False,
'message': f'以下设备不存在: {", ".join(map(str, missing_ids))}'
}), 404
# 批量更新设备状态
for device in devices:
device.status = status
db.session.commit()
status_names = {0: '禁用', 1: '正常', 2: '离线'}
status_name = status_names.get(status, '未知')
return jsonify({
'success': True,
'message': f'成功将 {len(devices)} 个设备状态更新为{status_name}'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新设备状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500

View File

@ -719,3 +719,130 @@ def delete_license(license_key):
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/licenses/batch', methods=['DELETE'])
@require_admin
def batch_delete_licenses():
"""批量删除卡密"""
try:
data = request.get_json()
if not data or 'license_keys' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少license_keys字段'
}), 400
license_keys = data['license_keys']
if not isinstance(license_keys, list) or len(license_keys) == 0:
return jsonify({
'success': False,
'message': 'license_keys必须是非空列表'
}), 400
# 查找所有要删除的卡密
licenses = License.query.filter(License.license_key.in_(license_keys)).all()
if len(licenses) != len(license_keys):
found_keys = [l.license_key for l in licenses]
missing_keys = [key for key in license_keys if key not in found_keys]
return jsonify({
'success': False,
'message': f'以下卡密不存在: {", ".join(missing_keys)}'
}), 404
# 检查是否有已激活的卡密
active_licenses = [l for l in licenses if l.status == 1]
if active_licenses:
active_keys = [l.license_key for l in active_licenses]
return jsonify({
'success': False,
'message': f'以下卡密已激活,不能直接删除: {", ".join(active_keys)},请先禁用或解绑设备'
}), 400
# 批量删除卡密
for license_obj in licenses:
db.session.delete(license_obj)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功删除 {len(licenses)} 个卡密'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量删除卡密失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/licenses/batch/status', methods=['PUT'])
@require_admin
def batch_update_license_status():
"""批量更新卡密状态"""
try:
data = request.get_json()
if not data or 'license_keys' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少license_keys/status字段'
}), 400
license_keys = data['license_keys']
status = data['status']
if not isinstance(license_keys, list) or len(license_keys) == 0:
return jsonify({
'success': False,
'message': 'license_keys必须是非空列表'
}), 400
if status not in [0, 1, 2, 3]:
return jsonify({
'success': False,
'message': 'status必须是0(未激活)、1(已激活)、2(已过期)或3(已禁用)'
}), 400
# 查找所有要更新的卡密
licenses = License.query.filter(License.license_key.in_(license_keys)).all()
if len(licenses) != len(license_keys):
found_keys = [l.license_key for l in licenses]
missing_keys = [key for key in license_keys if key not in found_keys]
return jsonify({
'success': False,
'message': f'以下卡密不存在: {", ".join(missing_keys)}'
}), 404
# 检查是否有已激活的卡密要改为未激活
if status == 0:
active_licenses = [l for l in licenses if l.status == 1]
if active_licenses:
active_keys = [l.license_key for l in active_licenses]
return jsonify({
'success': False,
'message': f'以下卡密已激活,不能改为未激活状态: {", ".join(active_keys)}'
}), 400
# 批量更新卡密状态
for license_obj in licenses:
license_obj.status = status
db.session.commit()
status_names = {0: '未激活', 1: '已激活', 2: '已过期', 3: '已禁用'}
status_name = status_names.get(status, '未知')
return jsonify({
'success': True,
'message': f'成功将 {len(licenses)} 个卡密状态更新为{status_name}'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新卡密状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500

View File

@ -220,6 +220,131 @@ def delete_product(product_id):
except Exception as e:
db.session.rollback()
current_app.logger.error(f"删除产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/batch', methods=['DELETE'])
@require_admin
def batch_delete_products():
"""批量删除产品"""
try:
data = request.get_json()
if not data or 'product_ids' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少product_ids字段'
}), 400
product_ids = data['product_ids']
if not isinstance(product_ids, list) or len(product_ids) == 0:
return jsonify({
'success': False,
'message': 'product_ids必须是非空列表'
}), 400
# 查找所有要删除的产品
products = Product.query.filter(Product.product_id.in_(product_ids)).all()
if len(products) != len(product_ids):
found_ids = [p.product_id for p in products]
missing_ids = [pid for pid in product_ids if pid not in found_ids]
return jsonify({
'success': False,
'message': f'以下产品不存在: {", ".join(missing_ids)}'
}), 404
# 检查是否有产品有关联的卡密
undeletable_products = []
for product in products:
license_count = product.licenses.count()
if license_count > 0:
undeletable_products.append({
'product_id': product.product_id,
'product_name': product.product_name,
'license_count': license_count
})
if undeletable_products:
return jsonify({
'success': False,
'message': '部分产品无法删除,因为有关联的卡密',
'undeletable_products': undeletable_products
}), 400
# 批量删除产品
for product in products:
db.session.delete(product)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功删除 {len(products)} 个产品'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量删除产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/batch/status', methods=['PUT'])
@require_admin
def batch_update_product_status():
"""批量更新产品状态"""
try:
data = request.get_json()
if not data or 'product_ids' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少product_ids/status字段'
}), 400
product_ids = data['product_ids']
status = data['status']
if not isinstance(product_ids, list) or len(product_ids) == 0:
return jsonify({
'success': False,
'message': 'product_ids必须是非空列表'
}), 400
if status not in [0, 1]:
return jsonify({
'success': False,
'message': 'status必须是0(禁用)或1(启用)'
}), 400
# 查找所有要更新的产品
products = Product.query.filter(Product.product_id.in_(product_ids)).all()
if len(products) != len(product_ids):
found_ids = [p.product_id for p in products]
missing_ids = [pid for pid in product_ids if pid not in found_ids]
return jsonify({
'success': False,
'message': f'以下产品不存在: {", ".join(missing_ids)}'
}), 404
# 批量更新产品状态
for product in products:
product.status = status
db.session.commit()
status_name = '启用' if status == 1 else '禁用'
return jsonify({
'success': True,
'message': f'成功{status_name} {len(products)} 个产品'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新产品状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'

View File

@ -14,7 +14,7 @@ def get_tickets():
per_page = min(request.args.get('per_page', 20, type=int), 100)
status = request.args.get('status', type=int)
priority = request.args.get('priority', type=int)
product_id = request.args.get('product_id')
product_id = request.args.get('product_id') # 修复:确保参数名与前端一致
query = Ticket.query
@ -89,4 +89,62 @@ def create_ticket():
except Exception as e:
db.session.rollback()
current_app.logger.error(f"创建工单失败: {str(e)}")
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/tickets/batch/status', methods=['PUT'])
@require_admin
def batch_update_ticket_status():
"""批量更新工单状态"""
try:
data = request.get_json()
if not data or 'ticket_ids' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少ticket_ids/status字段'
}), 400
ticket_ids = data['ticket_ids']
status = data['status']
remark = data.get('remark', '')
if not isinstance(ticket_ids, list) or len(ticket_ids) == 0:
return jsonify({
'success': False,
'message': 'ticket_ids必须是非空列表'
}), 400
if status not in [0, 1, 2, 3]:
return jsonify({
'success': False,
'message': 'status必须是0(待处理)、1(处理中)、2(已解决)或3(已关闭)'
}), 400
# 查找所有要更新的工单
tickets = Ticket.query.filter(Ticket.ticket_id.in_(ticket_ids)).all()
if len(tickets) != len(ticket_ids):
found_ids = [t.ticket_id for t in tickets]
missing_ids = [tid for tid in ticket_ids if tid not in found_ids]
return jsonify({
'success': False,
'message': f'以下工单不存在: {", ".join(map(str, missing_ids))}'
}), 404
# 批量更新工单状态
for ticket in tickets:
ticket.update_status(status, remark)
status_names = {0: '待处理', 1: '处理中', 2: '已解决', 3: '已关闭'}
status_name = status_names.get(status, '未知')
return jsonify({
'success': True,
'message': f'成功将 {len(tickets)} 个工单状态更新为{status_name}'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新工单状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500

View File

@ -393,3 +393,152 @@ def delete_version(version_id):
db.session.rollback()
current_app.logger.error(f"删除版本失败: {str(e)}")
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/versions/batch', methods=['DELETE'])
@require_admin
def batch_delete_versions():
"""批量删除版本"""
try:
data = request.get_json()
if not data or 'version_ids' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少version_ids字段'
}), 400
version_ids = data['version_ids']
if not isinstance(version_ids, list) or len(version_ids) == 0:
return jsonify({
'success': False,
'message': 'version_ids必须是非空列表'
}), 400
# 查找所有要删除的版本
versions = Version.query.filter(Version.version_id.in_(version_ids)).all()
if len(versions) != len(version_ids):
found_ids = [v.version_id for v in versions]
missing_ids = [vid for vid in version_ids if vid not in found_ids]
return jsonify({
'success': False,
'message': f'以下版本不存在: {", ".join(map(str, missing_ids))}'
}), 404
# 检查是否有已发布的版本
published_versions = [v for v in versions if v.is_published()]
if published_versions:
published_ids = [str(v.version_id) for v in published_versions]
return jsonify({
'success': False,
'message': f'以下版本已发布,不能直接删除: {", ".join(published_ids)},请先取消发布后再删除'
}), 400
# 检查是否有设备正在使用这些版本
undeletable_versions = []
for version in versions:
active_device_count = version.get_active_device_count()
if active_device_count > 0:
undeletable_versions.append({
'version_id': version.version_id,
'version_num': version.version_num,
'active_device_count': active_device_count
})
if undeletable_versions:
undeletable_info = [f'{v["version_num"]}({v["active_device_count"]}个设备)' for v in undeletable_versions]
return jsonify({
'success': False,
'message': f'以下版本有活跃设备正在使用,不能直接删除: {", ".join(undeletable_info)},请先将这些设备解绑或禁用后再删除版本'
}), 400
# 批量删除版本
for version in versions:
# 删除版本相关的文件
if version.download_url:
filename = extract_filename_from_url(version.download_url)
if filename:
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
# 检查文件是否存在,如果存在则删除
if os.path.exists(file_path):
try:
os.remove(file_path)
current_app.logger.info(f"已删除版本文件: {file_path}")
except Exception as e:
current_app.logger.error(f"删除版本文件失败: {str(e)}")
# 即使文件删除失败也继续删除版本记录
db.session.delete(version)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功删除 {len(versions)} 个版本'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量删除版本失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/versions/batch/status', methods=['PUT'])
@require_admin
def batch_update_version_status():
"""批量更新版本状态(发布/取消发布)"""
try:
data = request.get_json()
if not data or 'version_ids' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少version_ids/status字段'
}), 400
version_ids = data['version_ids']
status = data['status']
if not isinstance(version_ids, list) or len(version_ids) == 0:
return jsonify({
'success': False,
'message': 'version_ids必须是非空列表'
}), 400
if status not in [0, 1]:
return jsonify({
'success': False,
'message': 'status必须是0(草稿)或1(已发布)'
}), 400
# 查找所有要更新的版本
versions = Version.query.filter(Version.version_id.in_(version_ids)).all()
if len(versions) != len(version_ids):
found_ids = [v.version_id for v in versions]
missing_ids = [vid for vid in version_ids if vid not in found_ids]
return jsonify({
'success': False,
'message': f'以下版本不存在: {", ".join(map(str, missing_ids))}'
}), 404
# 批量更新版本状态
for version in versions:
if status == 1:
version.publish()
else:
version.unpublish()
status_name = '发布' if status == 1 else '取消发布'
return jsonify({
'success': True,
'message': f'成功{status_name} {len(versions)} 个版本'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新版本状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500

View File

@ -31,7 +31,13 @@ class Admin(UserMixin, db.Model):
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password_hash, password)
from werkzeug.security import check_password_hash
print(f"DEBUG: Checking password for user {self.username} (ID: {self.admin_id})")
print(f"DEBUG: Stored hash: {self.password_hash}")
print(f"DEBUG: Provided password: {password}")
result = check_password_hash(self.password_hash, password)
print(f"DEBUG: Password check result: {result}")
return result
def is_super_admin(self):
"""是否为超级管理员"""

View File

@ -129,8 +129,8 @@ class Ticket(db.Model):
'operator': self.operator,
'remark': self.remark,
'processing_days': self.get_processing_days(),
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S'),
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None,
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None,
'resolve_time': self.resolve_time.strftime('%Y-%m-%d %H:%M:%S') if self.resolve_time else None,
'close_time': self.close_time.strftime('%Y-%m-%d %H:%M:%S') if self.close_time else None
}

View File

@ -1,9 +1,9 @@
# 创建Web蓝图
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from app.models.admin import Admin
from app import db
# 创建Web蓝图
web_bp = Blueprint('web', __name__)
@web_bp.route('/')
@ -19,17 +19,25 @@ def login():
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
print(f"DEBUG: Login attempt - Username: '{username}', Password: '{password}'")
print(f"DEBUG: Password length: {len(password) if password else 0}")
print(f"DEBUG: Password repr: {repr(password) if password else 'None'}")
if not username or not password:
flash('请输入用户名和密码', 'error')
return render_template('login.html')
# 查找用户
admin = Admin.query.filter_by(username=username).first()
# 查找用户
admin = Admin.query.filter_by(username=username).first()
if admin and admin.verify_password(password) and admin.is_active():
print(f"DEBUG: Admin found: {admin.username if admin else 'None'} (ID: {admin.admin_id if admin else 'N/A'})")
if admin:
print(f"DEBUG: Admin details - Role: {admin.role}, Status: {admin.status}, Deleted: {admin.is_deleted}")
password_check_result = admin.check_password(password)
print(f"DEBUG: Password check: {password_check_result}")
print(f"DEBUG: Active check: {admin.is_active}")
print(f"DEBUG: All conditions: admin={bool(admin)}, password={password_check_result}, active={admin.is_active}")
if admin and admin.check_password(password) and admin.is_active:
print("DEBUG: Authentication successful")
# 登录成功
login_user(admin, remember=True)
@ -55,10 +63,12 @@ def login():
# 获取next参数
next_page = request.args.get('next')
print(f"DEBUG: Next page: {next_page}")
if next_page:
return redirect(next_page)
return redirect(url_for('web.dashboard'))
else:
print("DEBUG: Authentication failed")
flash('用户名或密码错误', 'error')
return render_template('login.html')

View File

@ -5,14 +5,16 @@
{% block page_title %}设备管理{% endblock %}
{% block content %}
<!-- 搜索和筛选 -->
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索设备信息...">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="设备ID或机器码">
</div>
<div class="col-md-2">
<label for="search-status" class="form-label">状态</label>
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">离线</option>
@ -21,60 +23,107 @@
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="search-product" placeholder="产品ID或名称...">
<label for="search-product" class="form-label">产品</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
</div>
<div class="col-md-2">
<input type="text" class="form-control" id="search-license" placeholder="卡密...">
<label for="search-license" class="form-label">卡密</label>
<input type="text" class="form-control" id="search-license" placeholder="卡密">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
<div class="col-md-2 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-danger btn-sm" id="batch-delete-btn" style="display: none;">
<i class="fas fa-trash me-1"></i>批量删除
</button>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-success" id="batch-enable-btn">
<i class="fas fa-check-circle me-1"></i>批量启用
</button>
<button type="button" class="btn btn-outline-secondary" id="batch-disable-btn">
<i class="fas fa-ban me-1"></i>批量禁用
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 设备列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>设备ID</th>
<th>产品/卡密</th>
<th>机器码</th>
<th>IP地址</th>
<th>状态</th>
<th>激活时间</th>
<th>最后在线</th>
<th>最后验证</th>
<th>操作</th>
</tr>
</thead>
<tbody id="device-list">
<tr>
<td colspan="8" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="设备列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
</div>
<!-- 批量删除确认模态框 -->
<div class="modal fade" id="batchDeleteModal" 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="batch-delete-count"></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-batch-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
@ -104,6 +153,42 @@ function initEventListeners() {
currentPage = 1;
loadDevices();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-status').value = '';
document.getElementById('search-product').value = '';
document.getElementById('search-license').value = '';
currentPage = 1;
loadDevices();
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteDevices();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.device-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量启用
document.getElementById('batch-enable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateDeviceStatus(1);
});
// 批量禁用
document.getElementById('batch-disable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateDeviceStatus(2);
});
}
// 加载设备列表
@ -151,12 +236,15 @@ function renderDeviceList(devices) {
const tbody = document.getElementById('device-list');
if (devices.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = devices.map(device => `
<tr>
<td>
<input type="checkbox" class="device-checkbox" data-device-id="${device.device_id}">
</td>
<td>
<code>${String(device.device_id).substring(0, 8)}...</code>
<br><small class="text-muted">${device.device_id}</small>
@ -169,7 +257,7 @@ function renderDeviceList(devices) {
<code>${device.machine_code || '-'}</code>
</td>
<td>
-
${device.ip_address || '-'}
</td>
<td>
<span class="badge ${getDeviceStatusClass(device.status)}">
@ -240,6 +328,15 @@ function renderDeviceList(devices) {
deleteDevice(deviceId);
});
});
// 绑定复选框事件
document.querySelectorAll('.device-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 获取设备状态文本
@ -380,5 +477,111 @@ function deleteDevice(deviceId) {
showNotification('删除失败', 'error');
});
}
</script>
{% endblock %}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.device-checkbox:checked').length;
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchStatusBtn.style.display = 'inline-block';
} else {
batchDeleteBtn.style.display = 'none';
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`;
}
// 显示批量删除确认模态框
function showBatchDeleteModal() {
const selectedCount = document.querySelectorAll('.device-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除设备
function batchDeleteDevices() {
const selectedCheckboxes = document.querySelectorAll('.device-checkbox:checked');
const deviceIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.deviceId));
apiRequest('/api/v1/devices/batch', {
method: 'DELETE',
body: JSON.stringify({ device_ids: deviceIds })
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadDevices(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
console.error('Failed to batch delete devices:', error);
showNotification('批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新设备状态
function batchUpdateDeviceStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.device-checkbox:checked');
const deviceIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.deviceId));
if (deviceIds.length === 0) {
showNotification('请至少选择一个设备', 'warning');
return;
}
apiRequest('/api/v1/devices/batch/status', {
method: 'PUT',
body: JSON.stringify({
device_ids: deviceIds,
status: status
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadDevices(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update device status:', error);
showNotification('批量更新状态失败', 'error');
});
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>

View File

@ -6,24 +6,30 @@
{% block page_actions %}
<a href="{{ url_for('web.generate_license') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
<i class="fas fa-key me-2"></i>
生成卡密
</a>
<a href="{{ url_for('web.import_license') }}" class="btn btn-outline-success">
<a href="{{ url_for('web.import_license') }}" class="btn btn-outline-warning">
<i class="fas fa-file-import me-2"></i>
导入卡密
</a>
<a href="{{ url_for('web.export_license') }}" class="btn btn-outline-success">
<i class="fas fa-file-export me-2"></i>
导出卡密
</a>
{% endblock %}
{% block content %}
<!-- 搜索和筛选 -->
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索卡密或设备信息...">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="卡密或产品名称">
</div>
<div class="col-md-2">
<label for="search-status" class="form-label">状态</label>
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">未激活</option>
@ -33,37 +39,73 @@
</select>
</div>
<div class="col-md-2">
<label for="search-type" class="form-label">类型</label>
<select class="form-select" id="search-type">
<option value="">全部类型</option>
<option value="0">试用卡密</option>
<option value="1">正式卡密</option>
<option value="2">试用卡密</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="search-product" placeholder="产品ID或名称...">
<label for="search-product" class="form-label">产品ID</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
<div class="col-md-2 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-danger btn-sm" id="batch-delete-btn" style="display: none;">
<i class="fas fa-trash me-1"></i>批量删除
</button>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-success" id="batch-enable-btn">
<i class="fas fa-check-circle me-1"></i>批量启用
</button>
<button type="button" class="btn btn-outline-secondary" id="batch-disable-btn">
<i class="fas fa-ban me-1"></i>批量禁用
</button>
<button type="button" class="btn btn-outline-warning" id="batch-expire-btn">
<i class="fas fa-clock me-1"></i>批量过期
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 卡密列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>卡密</th>
<th>产品</th>
<th>类型</th>
<th>状态</th>
<th>设备信息</th>
<th>绑定信息</th>
<th>激活时间</th>
<th>过期时间</th>
<th>操作</th>
@ -71,26 +113,40 @@
</thead>
<tbody id="license-list">
<tr>
<td colspan="8" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="卡密列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
</div>
<!-- 批量删除确认模态框 -->
<div class="modal fade" id="batchDeleteModal" 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="batch-delete-count"></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-batch-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
@ -108,6 +164,48 @@ function initEventListeners() {
currentPage = 1;
loadLicenses();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-status').value = '';
document.getElementById('search-type').value = '';
document.getElementById('search-product').value = '';
currentPage = 1;
loadLicenses();
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteLicenses();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.license-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量启用
document.getElementById('batch-enable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(1);
});
// 批量禁用
document.getElementById('batch-disable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(3);
});
// 批量设为过期
document.getElementById('batch-expire-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(2);
});
}
// 加载卡密列表
@ -148,12 +246,15 @@ function renderLicenseList(licenses) {
const tbody = document.getElementById('license-list');
if (licenses.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = licenses.map(license => `
<tr>
<td>
<input type="checkbox" class="license-checkbox" data-license-key="${license.license_key}">
</td>
<td>
<code>${formatLicenseKey(license.license_key)}</code>
<br><small class="text-muted">${license.license_key}</small>
@ -244,6 +345,15 @@ function renderLicenseList(licenses) {
deleteLicense(licenseKey);
});
});
// 绑定复选框事件
document.querySelectorAll('.license-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 获取卡密状态文本
@ -566,5 +676,110 @@ function formatLicenseKey(licenseKey) {
return licenseKey || '';
}
</script>
{% endblock %}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.license-checkbox:checked').length;
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchStatusBtn.style.display = 'inline-block';
} else {
batchDeleteBtn.style.display = 'none';
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`;
}
// 显示批量删除确认模态框
function showBatchDeleteModal() {
const selectedCount = document.querySelectorAll('.license-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除卡密
function batchDeleteLicenses() {
const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked');
const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey);
apiRequest('/api/v1/licenses/batch', {
method: 'DELETE',
body: JSON.stringify({ license_keys: licenseKeys })
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadLicenses(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
console.error('Failed to batch delete licenses:', error);
showNotification('批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新卡密状态
function batchUpdateLicenseStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked');
const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey);
if (licenseKeys.length === 0) {
showNotification('请至少选择一个卡密', 'warning');
return;
}
apiRequest('/api/v1/licenses/batch/status', {
method: 'PUT',
body: JSON.stringify({
license_keys: licenseKeys,
status: status
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadLicenses(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update license status:', error);
showNotification('批量更新状态失败', 'error');
});
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>

View File

@ -12,34 +12,62 @@
{% endblock %}
{% block content %}
<!-- 搜索和筛选 -->
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-8">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索产品名称或描述...">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-redo me-2"></i>
重置
</button>
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="产品名称或ID">
</div>
<div class="col-md-8 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-danger btn-sm" id="batch-delete-btn" style="display: none;">
<i class="fas fa-trash me-1"></i>批量删除
</button>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-success" id="batch-enable-btn">
<i class="fas fa-check-circle me-1"></i>批量启用
</button>
<button type="button" class="btn btn-outline-secondary" id="batch-disable-btn">
<i class="fas fa-ban me-1"></i>批量禁用
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 产品列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>产品ID</th>
<th>产品名称</th>
<th>描述</th>
@ -52,19 +80,15 @@
</thead>
<tbody id="product-list">
<tr>
<td colspan="8" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="产品列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
@ -79,19 +103,36 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>确定要删除产品 "<span id="delete-product-name"></span>" 吗?</p>
<p class="text-danger">注意:删除后无法恢复,且如果产品下有关联的卡密将无法删除。</p>
确定要删除产品 "<strong id="delete-product-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>
<button type="button" class="btn btn-danger" id="confirm-delete">确定删除</button>
</div>
</div>
</div>
</div>
<!-- 批量删除确认模态框 -->
<div class="modal fade" id="batchDeleteModal" 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="batch-delete-count"></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-batch-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
let currentKeyword = '';
@ -125,6 +166,32 @@ function initEventListeners() {
const productId = document.getElementById('confirm-delete').dataset.productId;
deleteProduct(productId);
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteProducts();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.product-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量启用
document.getElementById('batch-enable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateProductStatus(1);
});
// 批量禁用
document.getElementById('batch-disable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateProductStatus(0);
});
}
// 加载产品列表
@ -158,12 +225,15 @@ function renderProductList(products) {
const tbody = document.getElementById('product-list');
if (products.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = products.map(product => `
<tr>
<td>
<input type="checkbox" class="product-checkbox" data-product-id="${product.product_id}">
</td>
<td><code>${product.product_id}</code></td>
<td>
<strong>${product.product_name}</strong>
@ -216,6 +286,15 @@ function renderProductList(products) {
showDeleteModal(productId, productName);
});
});
// 绑定复选框事件
document.querySelectorAll('.product-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 渲染分页
@ -313,5 +392,119 @@ function deleteProduct(productId) {
showNotification('删除失败', 'error');
});
}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.product-checkbox:checked').length;
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchStatusBtn.style.display = 'inline-block';
} else {
batchDeleteBtn.style.display = 'none';
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`;
}
// 显示批量删除确认模态框
function showBatchDeleteModal() {
const selectedCount = document.querySelectorAll('.product-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除产品
function batchDeleteProducts() {
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
apiRequest('/api/v1/products/batch', {
method: 'DELETE',
body: JSON.stringify({ product_ids: productIds })
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadProducts(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 如果有无法删除的产品,显示详细信息
if (data.undeletable_products) {
const undeletableInfo = data.undeletable_products.map(p =>
`${p.product_name} (${p.license_count}个卡密)`
).join(', ');
showNotification(`以下产品无法删除: ${undeletableInfo}`, 'error');
}
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
console.error('Failed to batch delete products:', error);
showNotification('批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新产品状态
function batchUpdateProductStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
if (productIds.length === 0) {
showNotification('请至少选择一个产品', 'warning');
return;
}
apiRequest('/api/v1/products/batch/status', {
method: 'PUT',
body: JSON.stringify({
product_ids: productIds,
status: status
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadProducts(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update product status:', error);
showNotification('批量更新状态失败', 'error');
});
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>
{% endblock %}

View File

@ -5,21 +5,23 @@
{% block page_title %}工单管理{% endblock %}
{% block page_actions %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createTicketModal">
<button type="button" class="btn btn-primary" id="create-ticket-btn" data-bs-toggle="modal" data-bs-target="#createTicketModal">
<i class="fas fa-plus me-2"></i>
创建工单
</button>
{% endblock %}
{% block content %}
<!-- 搜索和筛选 -->
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索工单标题或内容...">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="工单标题">
</div>
<div class="col-md-2">
<label for="search-status" class="form-label">状态</label>
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">待处理</option>
@ -29,6 +31,7 @@
</select>
</div>
<div class="col-md-2">
<label for="search-priority" class="form-label">优先级</label>
<select class="form-select" id="search-priority">
<option value="">全部优先级</option>
<option value="0"></option>
@ -37,50 +40,81 @@
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="search-product" placeholder="产品ID或名称...">
<label for="search-product" class="form-label">产品</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
<div class="col-md-2 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-secondary" id="batch-pending-btn">
<i class="fas fa-clock me-1"></i>批量待处理
</button>
<button type="button" class="btn btn-outline-info" id="batch-processing-btn">
<i class="fas fa-cog me-1"></i>批量处理中
</button>
<button type="button" class="btn btn-outline-success" id="batch-resolved-btn">
<i class="fas fa-check-circle me-1"></i>批量已解决
</button>
<button type="button" class="btn btn-outline-dark" id="batch-closed-btn">
<i class="fas fa-times-circle me-1"></i>批量已关闭
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 工单列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>工单ID</th>
<th>标题</th>
<th>产品</th>
<th>优先级</th>
<th>状态</th>
<th>创建时间</th>
<th>最后更新</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="ticket-list">
<tr>
<td colspan="8" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="工单列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
@ -98,46 +132,66 @@
<form id="create-ticket-form">
<div class="mb-3">
<label for="ticket_title" class="form-label">标题 *</label>
<input type="text" class="form-control" id="ticket_title" name="title" required>
<input type="text" class="form-control" id="ticket_title" required>
</div>
<div class="mb-3">
<label for="ticket_product" class="form-label">关联产品</label>
<select class="form-select" id="ticket_product" 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="ticket_priority" class="form-label">优先级 *</label>
<select class="form-select" id="ticket_priority" name="priority" required>
<option value="0"></option>
<option value="1" selected></option>
<option value="2"></option>
</select>
<div class="row mb-3">
<div class="col-md-6">
<label for="ticket_product" class="form-label">产品</label>
<input type="text" class="form-control" id="ticket_product" placeholder="产品ID">
</div>
<div class="col-md-6">
<label for="ticket_priority" class="form-label">优先级</label>
<select class="form-select" id="ticket_priority">
<option value="0"></option>
<option value="1" selected></option>
<option value="2"></option>
</select>
</div>
</div>
<div class="mb-3">
<label for="ticket_content" class="form-label">详细描述 *</label>
<textarea class="form-control" id="ticket_content" name="content" rows="5" required></textarea>
<textarea class="form-control" id="ticket_content" rows="5" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="create-ticket-btn">创建</button>
<button type="button" class="btn btn-primary" id="submit-create-ticket">创建工单</button>
</div>
</div>
</div>
</div>
<!-- 批量更新状态模态框 -->
<div class="modal fade" id="batchUpdateStatusModal" 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">
<p>确定要将选中的 <strong id="batch-update-count"></strong> 个工单状态更新为 <strong id="batch-update-status-text"></strong> 吗?</p>
<div class="mb-3">
<label for="batch-update-remark" class="form-label">备注</label>
<textarea class="form-control" id="batch-update-remark" rows="3" placeholder="请输入备注信息"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-batch-update">确定更新</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
let batchUpdateStatus = null; // 用于存储批量更新的状态
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
@ -154,10 +208,58 @@ function initEventListeners() {
loadTickets();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-status').value = '';
document.getElementById('search-priority').value = '';
document.getElementById('search-product').value = '';
currentPage = 1;
loadTickets();
});
// 创建工单按钮
document.getElementById('create-ticket-btn').addEventListener('click', function() {
document.getElementById('submit-create-ticket').addEventListener('click', function() {
createTicket();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量设为待处理
document.getElementById('batch-pending-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(0, '待处理');
});
// 批量设为处理中
document.getElementById('batch-processing-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(1, '处理中');
});
// 批量设为已解决
document.getElementById('batch-resolved-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(2, '已解决');
});
// 批量设为已关闭
document.getElementById('batch-closed-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(3, '已关闭');
});
// 确认批量更新
document.getElementById('confirm-batch-update').addEventListener('click', function() {
batchUpdateTicketStatus();
});
}
// 加载工单列表
@ -176,7 +278,7 @@ function loadTickets(page = 1) {
if (keyword) params.append('keyword', keyword);
if (status) params.append('status', status);
if (priority) params.append('priority', priority);
if (product) params.append('product', product);
if (product) params.append('product_id', product);
apiRequest(`/api/v1/tickets?${params}`)
.then(data => {
@ -198,12 +300,15 @@ function renderTicketList(tickets) {
const tbody = document.getElementById('ticket-list');
if (tickets.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = tickets.map(ticket => `
<tr>
<td>
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${ticket.ticket_id}">
</td>
<td>
<code>${ticket.ticket_id}</code>
</td>
@ -254,6 +359,15 @@ function renderTicketList(tickets) {
updateTicketStatus(ticketId);
});
});
// 绑定复选框事件
document.querySelectorAll('.ticket-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 获取优先级徽章
@ -403,5 +517,81 @@ function updateTicketStatus(ticketId) {
// 这里可以实现状态更新功能
showNotification('功能开发中...', 'info');
}
</script>
{% endblock %}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length;
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchStatusBtn.style.display = 'inline-block';
} else {
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`;
}
// 显示批量更新状态模态框
function showBatchUpdateStatusModal(status, statusText) {
const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length;
if (selectedCount === 0) {
showNotification('请至少选择一个工单', 'warning');
return;
}
batchUpdateStatus = status;
document.getElementById('batch-update-count').textContent = selectedCount;
document.getElementById('batch-update-status-text').textContent = statusText;
document.getElementById('batch-update-remark').value = '';
const modal = new bootstrap.Modal(document.getElementById('batchUpdateStatusModal'));
modal.show();
}
// 批量更新工单状态
function batchUpdateTicketStatus() {
const selectedCheckboxes = document.querySelectorAll('.ticket-checkbox:checked');
const ticketIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.ticketId));
const remark = document.getElementById('batch-update-remark').value.trim();
if (ticketIds.length === 0) {
showNotification('请至少选择一个工单', 'warning');
return;
}
if (batchUpdateStatus === null) {
showNotification('请选择要更新的状态', 'warning');
return;
}
apiRequest('/api/v1/tickets/batch/status', {
method: 'PUT',
body: JSON.stringify({
ticket_ids: ticketIds,
status: batchUpdateStatus,
remark: remark
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadTickets(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchUpdateStatusModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update ticket status:', error);
showNotification('批量更新状态失败', 'error');
});
}
</script>

View File

@ -12,14 +12,16 @@
{% endblock %}
{% block content %}
<!-- 搜索和筛选 -->
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索版本号或描述...">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="版本号">
</div>
<div class="col-md-3">
<label for="search-product" class="form-label">产品</label>
<select class="form-select" id="search-product">
<option value="">全部产品</option>
{% for product in products %}
@ -28,29 +30,61 @@
</select>
</div>
<div class="col-md-3">
<label for="search-status" class="form-label">状态</label>
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">未发布</option>
<option value="1">已发布</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
<div class="col-md-2 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-danger btn-sm" id="batch-delete-btn" style="display: none;">
<i class="fas fa-trash me-1"></i>批量删除
</button>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-success" id="batch-publish-btn">
<i class="fas fa-paper-plane me-1"></i>批量发布
</button>
<button type="button" class="btn btn-outline-secondary" id="batch-unpublish-btn">
<i class="fas fa-undo me-1"></i>批量取消发布
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 版本列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>版本号</th>
<th>产品</th>
<th>描述</th>
@ -62,26 +96,40 @@
</thead>
<tbody id="version-list">
<tr>
<td colspan="7" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
<td colspan="8" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="版本列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
</div>
<!-- 批量删除确认模态框 -->
<div class="modal fade" id="batchDeleteModal" 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="batch-delete-count"></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-batch-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
@ -99,6 +147,41 @@ function initEventListeners() {
currentPage = 1;
loadVersions();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-product').value = '';
document.getElementById('search-status').value = '';
currentPage = 1;
loadVersions();
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteVersions();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.version-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量发布
document.getElementById('batch-publish-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateVersionStatus(1);
});
// 批量取消发布
document.getElementById('batch-unpublish-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateVersionStatus(0);
});
}
// 加载版本列表
@ -137,12 +220,15 @@ function renderVersionList(versions) {
const tbody = document.getElementById('version-list');
if (versions.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = versions.map(version => `
<tr>
<td>
<input type="checkbox" class="version-checkbox" data-version-id="${version.version_id}">
</td>
<td>
<strong>${version.version_num}</strong>
<br><small class="text-muted">${version.platform || '-'}</small>
@ -215,6 +301,15 @@ function renderVersionList(versions) {
deleteVersion(versionId);
});
});
// 绑定复选框事件
document.querySelectorAll('.version-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 渲染分页
@ -333,5 +428,111 @@ function deleteVersion(versionId) {
}
});
}
</script>
{% endblock %}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.version-checkbox:checked').length;
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchStatusBtn.style.display = 'inline-block';
} else {
batchDeleteBtn.style.display = 'none';
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`;
}
// 显示批量删除确认模态框
function showBatchDeleteModal() {
const selectedCount = document.querySelectorAll('.version-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除版本
function batchDeleteVersions() {
const selectedCheckboxes = document.querySelectorAll('.version-checkbox:checked');
const versionIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.versionId));
apiRequest('/api/v1/versions/batch', {
method: 'DELETE',
body: JSON.stringify({ version_ids: versionIds })
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadVersions(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
console.error('Failed to batch delete versions:', error);
showNotification('批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新版本状态
function batchUpdateVersionStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.version-checkbox:checked');
const versionIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.versionId));
if (versionIds.length === 0) {
showNotification('请至少选择一个版本', 'warning');
return;
}
apiRequest('/api/v1/versions/batch/status', {
method: 'PUT',
body: JSON.stringify({
version_ids: versionIds,
status: status
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadVersions(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update version status:', error);
showNotification('批量更新状态失败', 'error');
});
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>

View File

@ -80,6 +80,13 @@ def import_license():
"""导入卡密页面"""
return render_template('license/import.html')
@web_bp.route('/licenses/export')
@login_required
def export_license():
"""导出卡密页面"""
products = Product.query.filter_by(status=1).all()
return render_template('license/export.html', products=products)
# 版本管理页面
@web_bp.route('/versions')
@login_required

View File

@ -1,16 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app import create_app
def check_routes():
"""检查路由注册情况"""
app = create_app()
with app.app_context():
print("=== 路由注册情况 ===")
for rule in app.url_map.iter_rules():
print(f"{rule.rule} -> {rule.endpoint}")
app = create_app()
if __name__ == "__main__":
check_routes()
print("API routes:")
routes = []
for rule in app.url_map.iter_rules():
methods = rule.methods if rule.methods else set()
routes.append((rule.rule, ','.join(sorted(methods)), rule.endpoint))
routes.sort()
for r in routes:
if r[2].startswith('api'):
print(f'{r[0]:30} {r[1]:20} {r[2]}')

Binary file not shown.

220
test.py Normal file
View File

@ -0,0 +1,220 @@
import json
import zipfile
import os
import shutil
from typing import Optional, Dict, Any
class JianYingConverter:
def __init__(self, input_cmp: str, output_cmp: str, target_version: str = "4.0"):
"""
初始化剪映草稿转换器
:param input_cmp: 输入高版本.cmp草稿路径
:param output_cmp: 输出低版本.cmp草稿路径
:param target_version: 目标低版本支持 "3.0" / "4.0"
"""
self.input_cmp = input_cmp
self.output_cmp = output_cmp
self.target_version = target_version
self.temp_dir = "temp_jianying" # 临时解压目录
self.core_json = "project.json" # 核心配置文件
def _unzip_cmp(self) -> bool:
"""解压.cmp文件到临时目录"""
if not os.path.exists(self.input_cmp):
print(f"错误:输入文件不存在 → {self.input_cmp}")
return False
# 清空并创建临时目录
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
os.makedirs(self.temp_dir)
try:
with zipfile.ZipFile(self.input_cmp, 'r') as zip_ref:
zip_ref.extractall(self.temp_dir)
print(f"成功解压到临时目录 → {self.temp_dir}")
return True
except Exception as e:
print(f"解压失败:{str(e)}")
return False
def _load_project_json(self) -> Optional[Dict[str, Any]]:
"""读取project.json"""
json_path = os.path.join(self.temp_dir, self.core_json)
if not os.path.exists(json_path):
print(f"错误:未找到核心配置文件 → {json_path}")
return None
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print("成功读取project.json")
return data
except json.JSONDecodeError:
# 部分高版本草稿可能有简单加密如Base64尝试解密
try:
import base64
with open(json_path, 'r', encoding='utf-8') as f:
encrypted = f.read()
decrypted = base64.b64decode(encrypted).decode('utf-8')
data = json.loads(decrypted)
print("成功解密并读取project.json")
return data
except Exception as e:
print(f"读取/解密JSON失败{str(e)}")
return None
def _downgrade_json(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""根据目标版本降级JSON结构"""
print(f"正在降级到剪映 v{self.target_version} 格式...")
# 1. 移除高版本新增的顶层字段(基于逆向分析)
high_version_fields = [
"meta_info", "plugin_version", "advanced_settings",
"ai_editor_info", "cloud_project_info", "multi_track_info"
]
for field in high_version_fields:
data.pop(field, None)
# 2. 降级项目配置(强制设置兼容版本号)
if "project_config" in data:
if self.target_version == "3.0":
data["project_config"]["version"] = "3.9.0"
data["project_config"]["compatible_version"] = "3.0.0"
else: # 4.0
data["project_config"]["version"] = "4.9.0"
data["project_config"]["compatible_version"] = "4.0.0"
# 3. 处理轨道数据(移除高版本特效/转场)
if "tracks" in data:
self._downgrade_tracks(data["tracks"])
# 4. 处理资源列表(移除云资源引用)
if "resources" in data:
data["resources"] = [res for res in data["resources"] if not res.get("is_cloud", False)]
print("JSON降级完成")
return data
def _downgrade_tracks(self, tracks: list):
"""降级轨道数据(移除低版本不支持的效果)"""
for track in tracks:
if "clips" not in track:
continue
for clip in track["clips"]:
# 移除高版本转场(保留基础转场)
if "transition" in clip:
transition_type = clip["transition"].get("type", "")
# 低版本支持的基础转场列表(可根据需求扩展)
supported_transitions = ["none", "fade", "slide", "push", "zoom"]
if transition_type not in supported_transitions:
clip["transition"] = {"type": "none", "duration": 0.3}
# 移除高版本特效如AI特效、高级滤镜
if "effects" in clip:
clip["effects"] = [
eff for eff in clip["effects"]
if eff.get("type") in ["filter", "adjust", "text", "sticker"] # 保留基础效果
]
# 降级音频效果移除3D音效等高级功能
if "audio_effects" in clip:
clip["audio_effects"] = [eff for eff in clip["audio_effects"] if eff.get("type") == "volume"]
def _save_project_json(self, data: Dict[str, Any]) -> bool:
"""保存降级后的JSON到临时目录"""
json_path = os.path.join(self.temp_dir, self.core_json)
try:
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print("成功保存降级后的project.json")
return True
except Exception as e:
print(f"保存JSON失败{str(e)}")
return False
def _zip_to_cmp(self) -> bool:
"""将临时目录重新压缩为.cmp文件"""
try:
with zipfile.ZipFile(self.output_cmp, 'w', zipfile.ZIP_DEFLATED) as zip_ref:
# 遍历临时目录所有文件,添加到压缩包
for root, dirs, files in os.walk(self.temp_dir):
for file in files:
file_path = os.path.join(root, file)
# 保持压缩包内的相对路径
arcname = os.path.relpath(file_path, self.temp_dir)
zip_ref.write(file_path, arcname)
print(f"成功生成低版本草稿 → {self.output_cmp}")
return True
except Exception as e:
print(f"压缩失败:{str(e)}")
return False
def clean_temp(self):
"""清理临时目录"""
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
print("临时目录已清理")
def convert(self) -> bool:
"""执行完整转换流程"""
print("=" * 50)
print("剪映草稿高版本转低版本工具")
print(f"输入:{self.input_cmp}")
print(f"输出:{self.output_cmp}")
print(f"目标版本v{self.target_version}")
print("=" * 50)
try:
# 1. 解压
if not self._unzip_cmp():
return False
# 2. 读取JSON
data = self._load_project_json()
if not data:
return False
# 3. 降级JSON
downgraded_data = self._downgrade_json(data)
# 4. 保存JSON
if not self._save_project_json(downgraded_data):
return False
# 5. 重新压缩
if not self._zip_to_cmp():
return False
# 6. 清理临时文件
self.clean_temp()
print("=" * 50)
print("转换成功!请用目标版本剪映打开输出文件")
print("注意:复杂特效/AI功能可能已降级为基础效果")
print("=" * 50)
return True
except Exception as e:
print(f"转换异常:{str(e)}")
self.clean_temp()
return False
# ------------------------------
# 用法示例
# ------------------------------
if __name__ == "__main__":
# 请修改以下参数
INPUT_CMP = "high_version_project.cmp" # 高版本草稿路径
OUTPUT_CMP = "low_version_project.cmp" # 输出低版本草稿路径
TARGET_VERSION = "4.0" # 目标低版本3.0 或 4.0
# 创建转换器并执行
converter = JianYingConverter(
input_cmp=INPUT_CMP,
output_cmp=OUTPUT_CMP,
target_version=TARGET_VERSION
)
converter.convert()