修复登录错误
This commit is contained in:
parent
ecca300271
commit
c7264ff597
@ -2,7 +2,7 @@
|
|||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<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>
|
||||||
@ -123,4 +123,112 @@ def delete_device(device_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"删除设备失败: {str(e)}")
|
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
|
||||||
@ -719,3 +719,130 @@ def delete_license(license_key):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'message': '服务器内部错误'
|
'message': '服务器内部错误'
|
||||||
}), 500
|
}), 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
|
||||||
|
|||||||
@ -220,6 +220,131 @@ def delete_product(product_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"删除产品失败: {str(e)}")
|
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({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': '服务器内部错误'
|
'message': '服务器内部错误'
|
||||||
|
|||||||
@ -14,7 +14,7 @@ def get_tickets():
|
|||||||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||||||
status = request.args.get('status', type=int)
|
status = request.args.get('status', type=int)
|
||||||
priority = request.args.get('priority', 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
|
query = Ticket.query
|
||||||
|
|
||||||
@ -89,4 +89,62 @@ def create_ticket():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"创建工单失败: {str(e)}")
|
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
|
||||||
@ -393,3 +393,152 @@ def delete_version(version_id):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"删除版本失败: {str(e)}")
|
current_app.logger.error(f"删除版本失败: {str(e)}")
|
||||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
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):
|
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):
|
def is_super_admin(self):
|
||||||
"""是否为超级管理员"""
|
"""是否为超级管理员"""
|
||||||
|
|||||||
@ -129,8 +129,8 @@ class Ticket(db.Model):
|
|||||||
'operator': self.operator,
|
'operator': self.operator,
|
||||||
'remark': self.remark,
|
'remark': self.remark,
|
||||||
'processing_days': self.get_processing_days(),
|
'processing_days': self.get_processing_days(),
|
||||||
'create_time': self.create_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'),
|
'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,
|
'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
|
'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 import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from app.models.admin import Admin
|
from app.models.admin import Admin
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
# 创建Web蓝图
|
|
||||||
web_bp = Blueprint('web', __name__)
|
web_bp = Blueprint('web', __name__)
|
||||||
|
|
||||||
@web_bp.route('/')
|
@web_bp.route('/')
|
||||||
@ -19,17 +19,25 @@ def login():
|
|||||||
username = request.form.get('username', '').strip()
|
username = request.form.get('username', '').strip()
|
||||||
password = request.form.get('password', '')
|
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:
|
if not username or not password:
|
||||||
flash('请输入用户名和密码', 'error')
|
flash('请输入用户名和密码', 'error')
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
# 查找用户
|
# 查找用户
|
||||||
admin = Admin.query.filter_by(username=username).first()
|
admin = Admin.query.filter_by(username=username).first()
|
||||||
|
print(f"DEBUG: Admin found: {admin.username if admin else 'None'} (ID: {admin.admin_id if admin else 'N/A'})")
|
||||||
# 查找用户
|
if admin:
|
||||||
admin = Admin.query.filter_by(username=username).first()
|
print(f"DEBUG: Admin details - Role: {admin.role}, Status: {admin.status}, Deleted: {admin.is_deleted}")
|
||||||
|
password_check_result = admin.check_password(password)
|
||||||
if admin and admin.verify_password(password) and admin.is_active():
|
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)
|
login_user(admin, remember=True)
|
||||||
|
|
||||||
@ -55,10 +63,12 @@ def login():
|
|||||||
|
|
||||||
# 获取next参数
|
# 获取next参数
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
|
print(f"DEBUG: Next page: {next_page}")
|
||||||
if next_page:
|
if next_page:
|
||||||
return redirect(next_page)
|
return redirect(next_page)
|
||||||
return redirect(url_for('web.dashboard'))
|
return redirect(url_for('web.dashboard'))
|
||||||
else:
|
else:
|
||||||
|
print("DEBUG: Authentication failed")
|
||||||
flash('用户名或密码错误', 'error')
|
flash('用户名或密码错误', 'error')
|
||||||
|
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|||||||
@ -5,14 +5,16 @@
|
|||||||
{% block page_title %}设备管理{% endblock %}
|
{% block page_title %}设备管理{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="search-form" class="row g-3">
|
<form id="search-form" class="row g-3">
|
||||||
<div class="col-md-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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<label for="search-status" class="form-label">状态</label>
|
||||||
<select class="form-select" id="search-status">
|
<select class="form-select" id="search-status">
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
<option value="0">离线</option>
|
<option value="0">离线</option>
|
||||||
@ -21,60 +23,107 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<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>
|
||||||
<div class="col-md-2">
|
<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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-search me-2"></i>
|
<button type="submit" class="btn btn-primary">
|
||||||
搜索
|
<i class="fas fa-search me-2"></i>搜索
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
||||||
|
<i class="fas fa-undo me-2"></i>重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th width="50">
|
||||||
|
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
|
||||||
|
</th>
|
||||||
<th>设备ID</th>
|
<th>设备ID</th>
|
||||||
<th>产品/卡密</th>
|
<th>产品/卡密</th>
|
||||||
<th>机器码</th>
|
<th>机器码</th>
|
||||||
<th>IP地址</th>
|
<th>IP地址</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>激活时间</th>
|
<th>激活时间</th>
|
||||||
<th>最后在线</th>
|
<th>最后验证</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="device-list">
|
<tbody id="device-list">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">
|
<td colspan="9" class="text-center text-muted">加载中...</td>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<nav aria-label="设备列表分页">
|
<nav aria-label="分页导航">
|
||||||
<ul class="pagination justify-content-center" id="pagination">
|
<ul class="pagination justify-content-center mb-0" id="pagination">
|
||||||
<!-- 分页将通过JavaScript动态生成 -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
<script>
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
|
|
||||||
@ -104,6 +153,42 @@ function initEventListeners() {
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadDevices();
|
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');
|
const tbody = document.getElementById('device-list');
|
||||||
|
|
||||||
if (devices.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = devices.map(device => `
|
tbody.innerHTML = devices.map(device => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="device-checkbox" data-device-id="${device.device_id}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>${String(device.device_id).substring(0, 8)}...</code>
|
<code>${String(device.device_id).substring(0, 8)}...</code>
|
||||||
<br><small class="text-muted">${device.device_id}</small>
|
<br><small class="text-muted">${device.device_id}</small>
|
||||||
@ -169,7 +257,7 @@ function renderDeviceList(devices) {
|
|||||||
<code>${device.machine_code || '-'}</code>
|
<code>${device.machine_code || '-'}</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
-
|
${device.ip_address || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge ${getDeviceStatusClass(device.status)}">
|
<span class="badge ${getDeviceStatusClass(device.status)}">
|
||||||
@ -240,6 +328,15 @@ function renderDeviceList(devices) {
|
|||||||
deleteDevice(deviceId);
|
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');
|
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>
|
||||||
@ -6,24 +6,30 @@
|
|||||||
|
|
||||||
{% block page_actions %}
|
{% block page_actions %}
|
||||||
<a href="{{ url_for('web.generate_license') }}" class="btn btn-primary">
|
<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>
|
||||||
<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>
|
<i class="fas fa-file-import me-2"></i>
|
||||||
导入卡密
|
导入卡密
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('web.export_license') }}" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-file-export me-2"></i>
|
||||||
|
导出卡密
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="search-form" class="row g-3">
|
<form id="search-form" class="row g-3">
|
||||||
<div class="col-md-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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<label for="search-status" class="form-label">状态</label>
|
||||||
<select class="form-select" id="search-status">
|
<select class="form-select" id="search-status">
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
<option value="0">未激活</option>
|
<option value="0">未激活</option>
|
||||||
@ -33,37 +39,73 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<label for="search-type" class="form-label">类型</label>
|
||||||
<select class="form-select" id="search-type">
|
<select class="form-select" id="search-type">
|
||||||
<option value="">全部类型</option>
|
<option value="">全部类型</option>
|
||||||
<option value="0">试用卡密</option>
|
|
||||||
<option value="1">正式卡密</option>
|
<option value="1">正式卡密</option>
|
||||||
|
<option value="2">试用卡密</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-search me-2"></i>
|
<button type="submit" class="btn btn-primary">
|
||||||
搜索
|
<i class="fas fa-search me-2"></i>搜索
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
||||||
|
<i class="fas fa-undo me-2"></i>重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<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>
|
<th>绑定信息</th>
|
||||||
<th>激活时间</th>
|
<th>激活时间</th>
|
||||||
<th>过期时间</th>
|
<th>过期时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
@ -71,26 +113,40 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="license-list">
|
<tbody id="license-list">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">
|
<td colspan="9" class="text-center text-muted">加载中...</td>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<nav aria-label="卡密列表分页">
|
<nav aria-label="分页导航">
|
||||||
<ul class="pagination justify-content-center" id="pagination">
|
<ul class="pagination justify-content-center mb-0" id="pagination">
|
||||||
<!-- 分页将通过JavaScript动态生成 -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
<script>
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
|
|
||||||
@ -108,6 +164,48 @@ function initEventListeners() {
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadLicenses();
|
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');
|
const tbody = document.getElementById('license-list');
|
||||||
|
|
||||||
if (licenses.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = licenses.map(license => `
|
tbody.innerHTML = licenses.map(license => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="license-checkbox" data-license-key="${license.license_key}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>${formatLicenseKey(license.license_key)}</code>
|
<code>${formatLicenseKey(license.license_key)}</code>
|
||||||
<br><small class="text-muted">${license.license_key}</small>
|
<br><small class="text-muted">${license.license_key}</small>
|
||||||
@ -244,6 +345,15 @@ function renderLicenseList(licenses) {
|
|||||||
deleteLicense(licenseKey);
|
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 || '';
|
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>
|
||||||
@ -12,34 +12,62 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="search-form" class="row g-3">
|
<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">
|
<div class="col-md-4">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<label for="search-keyword" class="form-label">关键词搜索</label>
|
||||||
<i class="fas fa-search me-2"></i>
|
<input type="text" class="form-control" id="search-keyword" placeholder="产品名称或ID">
|
||||||
搜索
|
</div>
|
||||||
</button>
|
<div class="col-md-8 d-flex align-items-end">
|
||||||
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-redo me-2"></i>
|
<button type="submit" class="btn btn-primary">
|
||||||
重置
|
<i class="fas fa-search me-2"></i>搜索
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
||||||
|
<i class="fas fa-undo me-2"></i>重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th width="50">
|
||||||
|
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
|
||||||
|
</th>
|
||||||
<th>产品ID</th>
|
<th>产品ID</th>
|
||||||
<th>产品名称</th>
|
<th>产品名称</th>
|
||||||
<th>描述</th>
|
<th>描述</th>
|
||||||
@ -52,19 +80,15 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="product-list">
|
<tbody id="product-list">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">
|
<td colspan="9" class="text-center text-muted">加载中...</td>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<nav aria-label="产品列表分页">
|
<nav aria-label="分页导航">
|
||||||
<ul class="pagination justify-content-center" id="pagination">
|
<ul class="pagination justify-content-center mb-0" id="pagination">
|
||||||
<!-- 分页将通过JavaScript动态生成 -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -79,19 +103,36 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>确定要删除产品 "<span id="delete-product-name"></span>" 吗?</p>
|
确定要删除产品 "<strong id="delete-product-name"></strong>" 吗?此操作不可恢复。
|
||||||
<p class="text-danger">注意:删除后无法恢复,且如果产品下有关联的卡密将无法删除。</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
<script>
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let currentKeyword = '';
|
let currentKeyword = '';
|
||||||
@ -125,6 +166,32 @@ function initEventListeners() {
|
|||||||
const productId = document.getElementById('confirm-delete').dataset.productId;
|
const productId = document.getElementById('confirm-delete').dataset.productId;
|
||||||
deleteProduct(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');
|
const tbody = document.getElementById('product-list');
|
||||||
|
|
||||||
if (products.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = products.map(product => `
|
tbody.innerHTML = products.map(product => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="product-checkbox" data-product-id="${product.product_id}">
|
||||||
|
</td>
|
||||||
<td><code>${product.product_id}</code></td>
|
<td><code>${product.product_id}</code></td>
|
||||||
<td>
|
<td>
|
||||||
<strong>${product.product_name}</strong>
|
<strong>${product.product_name}</strong>
|
||||||
@ -216,6 +286,15 @@ function renderProductList(products) {
|
|||||||
showDeleteModal(productId, productName);
|
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');
|
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>
|
</script>
|
||||||
{% endblock %}
|
|
||||||
@ -5,21 +5,23 @@
|
|||||||
{% block page_title %}工单管理{% endblock %}
|
{% block page_title %}工单管理{% endblock %}
|
||||||
|
|
||||||
{% block page_actions %}
|
{% 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>
|
<i class="fas fa-plus me-2"></i>
|
||||||
创建工单
|
创建工单
|
||||||
</button>
|
</button>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="search-form" class="row g-3">
|
<form id="search-form" class="row g-3">
|
||||||
<div class="col-md-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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<label for="search-status" class="form-label">状态</label>
|
||||||
<select class="form-select" id="search-status">
|
<select class="form-select" id="search-status">
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
<option value="0">待处理</option>
|
<option value="0">待处理</option>
|
||||||
@ -29,6 +31,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
<label for="search-priority" class="form-label">优先级</label>
|
||||||
<select class="form-select" id="search-priority">
|
<select class="form-select" id="search-priority">
|
||||||
<option value="">全部优先级</option>
|
<option value="">全部优先级</option>
|
||||||
<option value="0">低</option>
|
<option value="0">低</option>
|
||||||
@ -37,50 +40,81 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<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>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-search me-2"></i>
|
<button type="submit" class="btn btn-primary">
|
||||||
搜索
|
<i class="fas fa-search me-2"></i>搜索
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
||||||
|
<i class="fas fa-undo me-2"></i>重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th width="50">
|
||||||
|
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
|
||||||
|
</th>
|
||||||
<th>工单ID</th>
|
<th>工单ID</th>
|
||||||
<th>标题</th>
|
<th>标题</th>
|
||||||
<th>产品</th>
|
<th>产品</th>
|
||||||
<th>优先级</th>
|
<th>优先级</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
<th>最后更新</th>
|
<th>更新时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="ticket-list">
|
<tbody id="ticket-list">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">
|
<td colspan="9" class="text-center text-muted">加载中...</td>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<nav aria-label="工单列表分页">
|
<nav aria-label="分页导航">
|
||||||
<ul class="pagination justify-content-center" id="pagination">
|
<ul class="pagination justify-content-center mb-0" id="pagination">
|
||||||
<!-- 分页将通过JavaScript动态生成 -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -98,46 +132,66 @@
|
|||||||
<form id="create-ticket-form">
|
<form id="create-ticket-form">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ticket_title" class="form-label">标题 *</label>
|
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row mb-3">
|
||||||
<label for="ticket_product" class="form-label">关联产品</label>
|
<div class="col-md-6">
|
||||||
<select class="form-select" id="ticket_product" name="product_id">
|
<label for="ticket_product" class="form-label">产品</label>
|
||||||
<option value="">无关联产品</option>
|
<input type="text" class="form-control" id="ticket_product" placeholder="产品ID">
|
||||||
{% for product in products %}
|
</div>
|
||||||
<option value="{{ product.product_id }}">{{ product.product_name }}</option>
|
<div class="col-md-6">
|
||||||
{% endfor %}
|
<label for="ticket_priority" class="form-label">优先级</label>
|
||||||
</select>
|
<select class="form-select" id="ticket_priority">
|
||||||
</div>
|
<option value="0">低</option>
|
||||||
|
<option value="1" selected>中</option>
|
||||||
<div class="mb-3">
|
<option value="2">高</option>
|
||||||
<label for="ticket_priority" class="form-label">优先级 *</label>
|
</select>
|
||||||
<select class="form-select" id="ticket_priority" name="priority" required>
|
</div>
|
||||||
<option value="0">低</option>
|
|
||||||
<option value="1" selected>中</option>
|
|
||||||
<option value="2">高</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ticket_content" class="form-label">详细描述 *</label>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
<script>
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
|
let batchUpdateStatus = null; // 用于存储批量更新的状态
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
// 页面加载完成后初始化
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@ -154,10 +208,58 @@ function initEventListeners() {
|
|||||||
loadTickets();
|
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();
|
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 (keyword) params.append('keyword', keyword);
|
||||||
if (status) params.append('status', status);
|
if (status) params.append('status', status);
|
||||||
if (priority) params.append('priority', priority);
|
if (priority) params.append('priority', priority);
|
||||||
if (product) params.append('product', product);
|
if (product) params.append('product_id', product);
|
||||||
|
|
||||||
apiRequest(`/api/v1/tickets?${params}`)
|
apiRequest(`/api/v1/tickets?${params}`)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -198,12 +300,15 @@ function renderTicketList(tickets) {
|
|||||||
const tbody = document.getElementById('ticket-list');
|
const tbody = document.getElementById('ticket-list');
|
||||||
|
|
||||||
if (tickets.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = tickets.map(ticket => `
|
tbody.innerHTML = tickets.map(ticket => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${ticket.ticket_id}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>${ticket.ticket_id}</code>
|
<code>${ticket.ticket_id}</code>
|
||||||
</td>
|
</td>
|
||||||
@ -254,6 +359,15 @@ function renderTicketList(tickets) {
|
|||||||
updateTicketStatus(ticketId);
|
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');
|
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>
|
||||||
@ -12,14 +12,16 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="search-form" class="row g-3">
|
<form id="search-form" class="row g-3">
|
||||||
<div class="col-md-4">
|
<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>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<label for="search-product" class="form-label">产品</label>
|
||||||
<select class="form-select" id="search-product">
|
<select class="form-select" id="search-product">
|
||||||
<option value="">全部产品</option>
|
<option value="">全部产品</option>
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
@ -28,29 +30,61 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<label for="search-status" class="form-label">状态</label>
|
||||||
<select class="form-select" id="search-status">
|
<select class="form-select" id="search-status">
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
<option value="0">未发布</option>
|
<option value="0">未发布</option>
|
||||||
<option value="1">已发布</option>
|
<option value="1">已发布</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-search me-2"></i>
|
<button type="submit" class="btn btn-primary">
|
||||||
搜索
|
<i class="fas fa-search me-2"></i>搜索
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="reset-search">
|
||||||
|
<i class="fas fa-undo me-2"></i>重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<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>
|
||||||
@ -62,26 +96,40 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="version-list">
|
<tbody id="version-list">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="text-center text-muted">
|
<td colspan="8" class="text-center text-muted">加载中...</td>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<nav aria-label="版本列表分页">
|
<nav aria-label="分页导航">
|
||||||
<ul class="pagination justify-content-center" id="pagination">
|
<ul class="pagination justify-content-center mb-0" id="pagination">
|
||||||
<!-- 分页将通过JavaScript动态生成 -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
<script>
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
|
|
||||||
@ -99,6 +147,41 @@ function initEventListeners() {
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadVersions();
|
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');
|
const tbody = document.getElementById('version-list');
|
||||||
|
|
||||||
if (versions.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = versions.map(version => `
|
tbody.innerHTML = versions.map(version => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="version-checkbox" data-version-id="${version.version_id}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>${version.version_num}</strong>
|
<strong>${version.version_num}</strong>
|
||||||
<br><small class="text-muted">${version.platform || '-'}</small>
|
<br><small class="text-muted">${version.platform || '-'}</small>
|
||||||
@ -215,6 +301,15 @@ function renderVersionList(versions) {
|
|||||||
deleteVersion(versionId);
|
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>
|
||||||
@ -80,6 +80,13 @@ def import_license():
|
|||||||
"""导入卡密页面"""
|
"""导入卡密页面"""
|
||||||
return render_template('license/import.html')
|
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')
|
@web_bp.route('/versions')
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
def check_routes():
|
app = create_app()
|
||||||
"""检查路由注册情况"""
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
print("=== 路由注册情况 ===")
|
|
||||||
for rule in app.url_map.iter_rules():
|
|
||||||
print(f"{rule.rule} -> {rule.endpoint}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
print("API routes:")
|
||||||
check_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
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