修复登录错误
This commit is contained in:
parent
ecca300271
commit
c7264ff597
@ -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
7
.idea/misc.xml
Normal 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>
|
||||
@ -124,3 +124,111 @@ def delete_device(device_id):
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"删除设备失败: {str(e)}")
|
||||
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
|
||||
@ -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
|
||||
|
||||
@ -224,3 +224,128 @@ def delete_product(product_id):
|
||||
'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': '服务器内部错误'
|
||||
}), 500
|
||||
@ -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
|
||||
|
||||
@ -90,3 +90,61 @@ def create_ticket():
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"创建工单失败: {str(e)}")
|
||||
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
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
"""是否为超级管理员"""
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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>
|
||||
搜索
|
||||
<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');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新批量操作按钮状态
|
||||
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>
|
||||
{% endblock %}
|
||||
@ -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>
|
||||
搜索
|
||||
<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 || '';
|
||||
}
|
||||
|
||||
// 更新批量操作按钮状态
|
||||
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>
|
||||
{% endblock %}
|
||||
@ -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>
|
||||
搜索
|
||||
<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-redo me-2"></i>
|
||||
重置
|
||||
<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 %}
|
||||
@ -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>
|
||||
搜索
|
||||
<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 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="mb-3">
|
||||
<label for="ticket_priority" class="form-label">优先级 *</label>
|
||||
<select class="form-select" id="ticket_priority" name="priority" required>
|
||||
<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');
|
||||
}
|
||||
|
||||
// 更新批量操作按钮状态
|
||||
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>
|
||||
{% endblock %}
|
||||
@ -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>
|
||||
搜索
|
||||
<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) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新批量操作按钮状态
|
||||
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>
|
||||
{% endblock %}
|
||||
@ -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
|
||||
|
||||
@ -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("=== 路由注册情况 ===")
|
||||
print("API routes:")
|
||||
routes = []
|
||||
for rule in app.url_map.iter_rules():
|
||||
print(f"{rule.rule} -> {rule.endpoint}")
|
||||
methods = rule.methods if rule.methods else set()
|
||||
routes.append((rule.rule, ','.join(sorted(methods)), rule.endpoint))
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_routes()
|
||||
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
220
test.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user